gecko-dev/browser/devtools/webconsole/gcli.jsm

568 lines
16 KiB
JavaScript

/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is GCLI.
*
* The Initial Developer of the Original Code is
* The Mozilla Foundation
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Joe Walker <jwalker@mozilla.com> (Original Author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/*
*
*
*
*
*
*
*
*********************************** WARNING ***********************************
*
* Do not edit this file without understanding where it comes from,
* Your changes are likely to be overwritten without warning.
*
* The original source for this file is:
* https://github.com/mozilla/gcli/
*
*******************************************************************************
*
*
*
*
*
*
*
*
*
*/
///////////////////////////////////////////////////////////////////////////////
/*
* This build of GCLI for Firefox is really 4 bits of code:
* - Browser support code - Currently just an implementation of the console
* object that uses dump. We may need to add other browser shims to this.
* - A very basic commonjs AMD (Asynchronous Modules Definition) 'require'
* implementation (which is just good enough to load GCLI). For more, see
* http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition.
* This alleviates the need for requirejs (http://requirejs.org/) which is
* used when running in the browser.
* This section of code is a copy of mini_require.js without the header and
* footers. Changes to one should be reflected in the other.
* - A build of GCLI itself, packaged using dryice (for more details see the
* project https://github.com/mozilla/dryice and the build file in this
* project at Makefile.dryice.js)
* - Lastly, code to require the gcli object as needed by EXPORTED_SYMBOLS.
*/
var EXPORTED_SYMBOLS = [ "gcli" ];
///////////////////////////////////////////////////////////////////////////////
/*
* This creates a console object that somewhat replicates Firebug's console
* object. It currently writes to dump(), but should write to the web
* console's chrome error section (when it has one)
*/
/**
* String utility to ensure that strings are a specified length. Strings
* that are too long are truncated to the max length and the last char is
* set to "_". Strings that are too short are left padded with spaces.
*
* @param {string} aStr
* The string to format to the correct length
* @param {number} aMaxLen
* The maximum allowed length of the returned string
* @param {number} aMinLen (optional)
* The minimum allowed length of the returned string. If undefined,
* then aMaxLen will be used
* @param {object} aOptions (optional)
* An object allowing format customization. The only customization
* allowed currently is 'truncate' which can take the value "start" to
* truncate strings from the start as opposed to the end.
* @return {string}
* The original string formatted to fit the specified lengths
*/
function fmt(aStr, aMaxLen, aMinLen, aOptions) {
if (aMinLen == undefined) {
aMinLen = aMaxLen;
}
if (aStr == null) {
aStr = "";
}
if (aStr.length > aMaxLen) {
if (aOptions && aOptions.truncate == "start") {
return "_" + aStr.substring(aStr.length - aMaxLen + 1);
}
else {
return aStr.substring(0, aMaxLen - 1) + "_";
}
}
if (aStr.length < aMinLen) {
return Array(aMinLen - aStr.length + 1).join(" ") + aStr;
}
return aStr;
}
/**
* Utility to extract the constructor name of an object.
* Object.toString gives: "[object ?????]"; we want the "?????".
*
* @param {object} aObj
* The object from which to extract the constructor name
* @return {string}
* The constructor name
*/
function getCtorName(aObj) {
return Object.prototype.toString.call(aObj).slice(8, -1);
}
/**
* A single line stringification of an object designed for use by humans
*
* @param {any} aThing
* The object to be stringified
* @return {string}
* A single line representation of aThing, which will generally be at
* most 60 chars long
*/
function stringify(aThing) {
if (aThing === undefined) {
return "undefined";
}
if (aThing === null) {
return "null";
}
if (typeof aThing == "object") {
try {
return getCtorName(aThing) + " " + fmt(JSON.stringify(aThing), 50, 0);
}
catch (ex) {
return "[stringify error]";
}
}
var str = aThing.toString().replace(/\s+/g, " ");
return fmt(str, 60, 0);
}
/**
* A multi line stringification of an object, designed for use by humans
*
* @param {any} aThing
* The object to be stringified
* @return {string}
* A multi line representation of aThing
*/
function log(aThing) {
if (aThing == null) {
return "null";
}
if (aThing == undefined) {
return "undefined";
}
if (typeof aThing == "object") {
var reply = "";
var type = getCtorName(aThing);
if (type == "Error") {
reply += " " + aThing.message + "\n";
reply += logProperty("stack", aThing.stack);
}
else {
var keys = Object.getOwnPropertyNames(aThing);
if (keys.length > 0) {
reply += type + "\n";
keys.forEach(function(aProp) {
reply += logProperty(aProp, aThing[aProp]);
}, this);
}
else {
reply += type + " (enumerated with for-in)\n";
var prop;
for (prop in aThing) {
reply += logProperty(prop, aThing[prop]);
}
}
}
return reply;
}
return " " + aThing.toString() + "\n";
}
/**
* Helper for log() which converts a property/value pair into an output
* string
*
* @param {string} aProp
* The name of the property to include in the output string
* @param {object} aValue
* Value assigned to aProp to be converted to a single line string
* @return {string}
* Multi line output string describing the property/value pair
*/
function logProperty(aProp, aValue) {
var reply = "";
if (aProp == "stack" && typeof value == "string") {
var trace = parseStack(aValue);
reply += formatTrace(trace);
}
else {
reply += " - " + aProp + " = " + stringify(aValue) + "\n";
}
return reply;
}
/**
* Parse a stack trace, returning an array of stack frame objects, where
* each has file/line/call members
*
* @param {string} aStack
* The serialized stack trace
* @return {object[]}
* Array of { file: "...", line: NNN, call: "..." } objects
*/
function parseStack(aStack) {
var trace = [];
aStack.split("\n").forEach(function(line) {
if (!line) {
return;
}
var at = line.lastIndexOf("@");
var posn = line.substring(at + 1);
trace.push({
file: posn.split(":")[0],
line: posn.split(":")[1],
call: line.substring(0, at)
});
}, this);
return trace;
}
/**
* parseStack() takes output from an exception from which it creates the an
* array of stack frame objects, this has the same output but using data from
* Components.stack
*
* @param {string} aFrame
* The stack frame from which to begin the walk
* @return {object[]}
* Array of { file: "...", line: NNN, call: "..." } objects
*/
function getStack(aFrame) {
if (!aFrame) {
aFrame = Components.stack.caller;
}
var trace = [];
while (aFrame) {
trace.push({
file: aFrame.filename,
line: aFrame.lineNumber,
call: aFrame.name
});
aFrame = aFrame.caller;
}
return trace;
};
/**
* Take the output from parseStack() and convert it to nice readable
* output
*
* @param {object[]} aTrace
* Array of trace objects as created by parseStack()
* @return {string} Multi line report of the stack trace
*/
function formatTrace(aTrace) {
var reply = "";
aTrace.forEach(function(frame) {
reply += fmt(frame.file, 20, 20, { truncate: "start" }) + " " +
fmt(frame.line, 5, 5) + " " +
fmt(frame.call, 75, 75) + "\n";
});
return reply;
}
/**
* Create a function which will output a concise level of output when used
* as a logging function
*
* @param {string} aLevel
* A prefix to all output generated from this function detailing the
* level at which output occurred
* @return {function}
* A logging function
* @see createMultiLineDumper()
*/
function createDumper(aLevel) {
return function() {
var args = Array.prototype.slice.call(arguments, 0);
var data = args.map(function(arg) {
return stringify(arg);
});
dump(aLevel + ": " + data.join(", ") + "\n");
};
}
/**
* Create a function which will output more detailed level of output when
* used as a logging function
*
* @param {string} aLevel
* A prefix to all output generated from this function detailing the
* level at which output occurred
* @return {function}
* A logging function
* @see createDumper()
*/
function createMultiLineDumper(aLevel) {
return function() {
dump(aLevel + "\n");
var args = Array.prototype.slice.call(arguments, 0);
args.forEach(function(arg) {
dump(log(arg));
});
};
}
/**
* The console object to expose
*/
var console = {
debug: createMultiLineDumper("debug"),
log: createDumper("log"),
info: createDumper("info"),
warn: createDumper("warn"),
error: createMultiLineDumper("error"),
trace: function Console_trace() {
var trace = getStack(Components.stack.caller);
dump(formatTrace(trace) + "\n");
},
clear: function Console_clear() {},
dir: createMultiLineDumper("dir"),
dirxml: createMultiLineDumper("dirxml"),
group: createDumper("group"),
groupEnd: createDumper("groupEnd")
};
///////////////////////////////////////////////////////////////////////////////
// There are 2 virtually identical copies of this code:
// - $GCLI_HOME/build/prefix-gcli.jsm
// - $GCLI_HOME/build/mini_require.js
// They should both be kept in sync
var debugDependencies = false;
/**
* Define a module along with a payload.
* @param {string} moduleName Name for the payload
* @param {ignored} deps Ignored. For compatibility with CommonJS AMD Spec
* @param {function} payload Function with (require, exports, module) params
*/
function define(moduleName, deps, payload) {
if (typeof moduleName != "string") {
console.error(this.depth + " Error: Module name is not a string.");
console.trace();
return;
}
if (arguments.length == 2) {
payload = deps;
}
if (debugDependencies) {
console.log("define: " + moduleName + " -> " + payload.toString()
.slice(0, 40).replace(/\n/, '\\n').replace(/\r/, '\\r') + "...");
}
if (moduleName in define.modules) {
console.error(this.depth + " Error: Redefining module: " + moduleName);
}
define.modules[moduleName] = payload;
};
/**
* The global store of un-instantiated modules
*/
define.modules = {};
/**
* We invoke require() in the context of a Domain so we can have multiple
* sets of modules running separate from each other.
* This contrasts with JSMs which are singletons, Domains allows us to
* optionally load a CommonJS module twice with separate data each time.
* Perhaps you want 2 command lines with a different set of commands in each,
* for example.
*/
function Domain() {
this.modules = {};
if (debugDependencies) {
this.depth = "";
}
}
/**
* Lookup module names and resolve them by calling the definition function if
* needed.
* There are 2 ways to call this, either with an array of dependencies and a
* callback to call when the dependencies are found (which can happen
* asynchronously in an in-page context) or with a single string an no callback
* where the dependency is resolved synchronously and returned.
* The API is designed to be compatible with the CommonJS AMD spec and
* RequireJS.
* @param {string[]|string} deps A name, or names for the payload
* @param {function|undefined} callback Function to call when the dependencies
* are resolved
* @return {undefined|object} The module required or undefined for
* array/callback method
*/
Domain.prototype.require = function(deps, callback) {
if (Array.isArray(deps)) {
var params = deps.map(function(dep) {
return this.lookup(dep);
}, this);
if (callback) {
callback.apply(null, params);
}
return undefined;
}
else {
return this.lookup(deps);
}
};
/**
* Lookup module names and resolve them by calling the definition function if
* needed.
* @param {string} moduleName A name for the payload to lookup
* @return {object} The module specified by aModuleName or null if not found.
*/
Domain.prototype.lookup = function(moduleName) {
if (moduleName in this.modules) {
var module = this.modules[moduleName];
if (debugDependencies) {
console.log(this.depth + " Using module: " + moduleName);
}
return module;
}
if (!(moduleName in define.modules)) {
console.error(this.depth + " Missing module: " + moduleName);
return null;
}
var module = define.modules[moduleName];
if (debugDependencies) {
console.log(this.depth + " Compiling module: " + moduleName);
}
if (typeof module == "function") {
if (debugDependencies) {
this.depth += ".";
}
var exports = {};
try {
module(this.require.bind(this), exports, { id: moduleName, uri: "" });
}
catch (ex) {
console.error("Error using module: " + moduleName, ex);
throw ex;
}
module = exports;
if (debugDependencies) {
this.depth = this.depth.slice(0, -1);
}
}
// cache the resulting module object for next time
this.modules[moduleName] = module;
return module;
};
/**
* Expose the Domain constructor and a global domain (on the define function
* to avoid exporting more than we need. This is a common pattern with require
* systems)
*/
define.Domain = Domain;
define.globalDomain = new Domain();
/**
* Expose a default require function which is the require of the global
* sandbox to make it easy to use.
*/
var require = define.globalDomain.require.bind(define.globalDomain);
///////////////////////////////////////////////////////////////////////////////
/*
* The API of interest to people wanting to create GCLI commands is as
* follows. The implementation of this API is left to bug 659061 and other
* bugs.
*/
define('gcli/index', [ ], function(require, exports, module) {
exports.addCommand = function() { /* implementation goes here */ };
exports.removeCommand = function() { /* implementation goes here */ };
exports.startup = function() { /* implementation goes here */ };
exports.shutdown = function() { /* implementation goes here */ };
});
///////////////////////////////////////////////////////////////////////////////
/*
* require GCLI so it can be exported as declared in EXPORTED_SYMBOLS
* The dependencies specified here should be the same as in Makefile.dryice.js
*/
var gcli = require("gcli/index");
gcli.createView = require("gcli/ui/start/firefox");
gcli._internal = { require: require, define: define, console: console };