Bug 653140 - GCLI needs a commonjs require system; f=mihai.sucan r=dtownsend sr=dtownsend
authorJoe Walker <jwalker@mozilla.com>
Fri, 27 May 2011 11:48:50 +0100
changeset 73945 71881e5b06ca0968dd72bf56dd28bef3fad1d3df
parent 73836 6f1affa4bb5edf56d0093238dd0e802769bb04b1
child 73946 6daba4de8d1416e1546adfc4c6d643978ffcb69f
push id2
push userbsmedberg@mozilla.com
push dateFri, 19 Aug 2011 14:38:13 +0000
reviewersdtownsend, dtownsend
bugs653140
milestone8.0a1
Bug 653140 - GCLI needs a commonjs require system; f=mihai.sucan r=dtownsend sr=dtownsend
browser/devtools/webconsole/Makefile.in
browser/devtools/webconsole/gcli.jsm
browser/devtools/webconsole/test/browser/Makefile.in
browser/devtools/webconsole/test/browser/browser_webconsole_gcli_require.js
--- a/browser/devtools/webconsole/Makefile.in
+++ b/browser/devtools/webconsole/Makefile.in
@@ -44,16 +44,17 @@ VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 EXTRA_JS_MODULES = \
 		HUDService.jsm \
 		PropertyPanel.jsm \
 		NetworkHelper.jsm \
 		AutocompletePopup.jsm \
+		gcli.jsm \
 		$(NULL)
 
 ifdef ENABLE_TESTS
 ifneq (mobile,$(MOZ_BUILD_APP))
 	DIRS += test
 endif
 endif
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/gcli.jsm
@@ -0,0 +1,567 @@
+/* ***** 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 };
+
--- a/browser/devtools/webconsole/test/browser/Makefile.in
+++ b/browser/devtools/webconsole/test/browser/Makefile.in
@@ -103,16 +103,17 @@ include $(topsrcdir)/config/rules.mk
 	browser_webconsole_bug_592442_closing_brackets.js \
 	browser_webconsole_bug_593003_iframe_wrong_hud.js \
 	browser_webconsole_bug_601909_remember_height.js \
 	browser_webconsole_bug_613013_console_api_iframe.js \
 	browser_webconsole_bug_597756_reopen_closed_tab.js \
 	browser_webconsole_bug_600183_charset.js \
 	browser_webconsole_bug_601177_log_levels.js \
 	browser_webconsole_bug_597460_filter_scroll.js \
+	browser_webconsole_gcli_require.js \
 	browser_webconsole_console_extras.js \
 	browser_webconsole_bug_598357_jsterm_output.js \
 	browser_webconsole_bug_603750_websocket.js \
 	browser_webconsole_abbreviate_source_url.js \
 	browser_webconsole_view_source.js \
 	browser_webconsole_bug_602572_log_bodies_checkbox.js \
 	browser_webconsole_bug_614793_jsterm_scroll.js \
 	browser_webconsole_bug_599725_response_headers.js \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser/browser_webconsole_gcli_require.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that source URLs in the Web Console can be clicked to display the
+// standard View Source window.
+
+var modules = { gcli: null };
+
+Components.utils.import("resource:///modules/gcli.jsm", modules);
+
+var define, require, console;
+
+function test() {
+
+  define = modules.gcli._internal.define;
+  require = modules.gcli._internal.require;
+  console = modules.gcli._internal.console;
+
+  define('gclitest/requirable', [], function(require, exports, module) {
+    exports.thing1 = 'thing1';
+    exports.thing2 = 2;
+
+    let status = 'initial';
+    exports.setStatus = function(aStatus) { status = aStatus; };
+    exports.getStatus = function() { return status; };
+  });
+
+  define('gclitest/unrequirable', [], function(require, exports, module) {
+    null.throwNPE();
+  });
+
+  define('gclitest/recurse', [], function(require, exports, module) {
+    require('gclitest/recurse');
+  });
+
+  testWorking();
+  testLeakage();
+  testMultiImport();
+  testRecursive();
+  testUncompilable();
+
+  finishTest();
+
+  delete define.modules['gclitest/requirable'];
+  delete define.globalDomain.modules['gclitest/requirable'];
+  delete define.modules['gclitest/unrequirable'];
+  delete define.globalDomain.modules['gclitest/unrequirable'];
+  delete define.modules['gclitest/recurse'];
+  delete define.globalDomain.modules['gclitest/recurse'];
+
+  define = null;
+  require = null;
+  console = null;
+
+  modules = null;
+}
+
+function testWorking() {
+  // There are lots of requirement tests that we could be doing here
+  // The fact that we can get anything at all working is a testament to
+  // require doing what it should - we don't need to test the
+  let requireable = require('gclitest/requirable');
+  ok('thing1' == requireable.thing1, 'thing1 was required');
+  ok(2 == requireable.thing2, 'thing2 was required');
+  ok(requireable.thing3 === undefined, 'thing3 was not required');
+}
+
+function testDomains() {
+  let requireable = require('gclitest/requirable');
+  ok(requireable.status === undefined, 'requirable has no status');
+  requireable.setStatus(null);
+  ok(null === requireable.getStatus(), 'requirable.getStatus changed to null');
+  ok(requireable.status === undefined, 'requirable still has no status');
+  requireable.setStatus('42');
+  ok('42' == requireable.getStatus(), 'requirable.getStatus changed to 42');
+  ok(requireable.status === undefined, 'requirable *still* has no status');
+
+  let domain = new define.Domain();
+  let requireable2 = domain.require('gclitest/requirable');
+  ok(requireable2.status === undefined, 'requirable2 has no status');
+  ok('initial' === requireable2.getStatus(), 'requirable2.getStatus is initial');
+  requireable2.setStatus(999);
+  ok(999 === requireable2.getStatus(), 'requirable2.getStatus changed to 999');
+  ok(requireable2.status === undefined, 'requirable2 still has no status');
+
+  t.verifyEqual('42', requireable.getStatus());
+  ok(requireable.status === undefined, 'requirable has no status (as expected)');
+
+  delete domain.modules['gclitest/requirable'];
+}
+
+function testLeakage() {
+  let requireable = require('gclitest/requirable');
+  ok(requireable.setup == null, 'leakage of setup');
+  ok(requireable.shutdown == null, 'leakage of shutdown');
+  ok(requireable.testWorking == null, 'leakage of testWorking');
+}
+
+function testMultiImport() {
+  let r1 = require('gclitest/requirable');
+  let r2 = require('gclitest/requirable');
+  ok(r1 === r2, 'double require was strict equal');
+}
+
+function testUncompilable() {
+  // It's not totally clear how a module loader should perform with unusable
+  // modules, however at least it should go into a flat spin ...
+  // GCLI mini_require reports an error as it should
+  try {
+      let unrequireable = require('gclitest/unrequirable');
+      fail();
+  }
+  catch (ex) {
+      console.log(ex);
+  }
+}
+
+function testRecursive() {
+  // See Bug 658583
+  // require('gclitest/recurse');