From d648d709f3edcac56132e9e2a84a0fc65f5b48ac Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 12 Jan 2012 11:06:10 -0800 Subject: refactor(module): strict separation between module-config / app-runtime --- src/Angular.js | 7 +- src/AngularPublic.js | 6 +- src/Injector.js | 270 +++++++++++++++++++++++++------------------- src/angular-mocks.js | 52 ++++++++- src/loader.js | 8 +- src/scenario/Application.js | 2 +- src/scenario/Runner.js | 2 +- src/scenario/dsl.js | 8 +- src/service/compiler.js | 12 +- src/service/scope.js | 4 +- 10 files changed, 226 insertions(+), 145 deletions(-) (limited to 'src') diff --git a/src/Angular.js b/src/Angular.js index 74443bd1..0f79d363 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -128,7 +128,7 @@ var $$scope = '$scope', * * @param {Object|Array} obj Object to iterate over. - * @param {function()} iterator Iterator function. + * @param {Function} iterator Iterator function. * @param {Object=} context Object to become context (`this`) for the iterator function. * @returns {Object|Array} Reference to `obj`. */ @@ -897,12 +897,14 @@ function angularInit(element, bootstrap) { * * @param {Element} element DOM element which is the root of angular application. * @param {Array=} modules an array of module declarations. See: {@link angular.module modules} + * @param {angular.module.auta.$injector} the injector; */ function bootstrap(element, modules) { element = jqLite(element); modules = modules || []; modules.unshift('ng'); - createInjector(modules).invoke(null, + var injector = createInjector(modules); + injector.invoke( ['$rootScope', '$compile', '$injector', function(scope, compile, injector){ scope.$apply(function() { element.data('$injector', injector); @@ -910,6 +912,7 @@ function bootstrap(element, modules) { }); }] ); + return injector; } function bindJQuery() { diff --git a/src/AngularPublic.js b/src/AngularPublic.js index ef3d3ccb..4973f574 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -30,7 +30,7 @@ function publishExternalAPI(angular){ 'equals': equals, 'element': jqLite, 'forEach': forEach, - 'injector': function(){ return createInjector(arguments); }, + 'injector': createInjector, 'noop':noop, 'bind':bind, 'toJson': toJson, @@ -58,8 +58,8 @@ function publishExternalAPI(angular){ angularModule('ngLocale', []).service('$locale', $LocaleProvider); } - angularModule('ng', ['ngLocale'], ['$provide', '$injector', - function ngModule($provide, $injector) { + angularModule('ng', ['ngLocale'], ['$provide', + function ngModule($provide) { // TODO(misko): temporary services to get the compiler working; $provide.value('$textMarkup', angularTextMarkup); $provide.value('$attrMarkup', angularAttrMarkup); diff --git a/src/Injector.js b/src/Injector.js index 8326d645..c9901513 100644 --- a/src/Injector.js +++ b/src/Injector.js @@ -10,7 +10,7 @@ * dependency injection (see {@link guide/dev_guide.di dependency injection}). * - * @param {} modules... A list of module functions or their aliases. See + * @param {Array.} modules A list of module functions or their aliases. See * {@link angular.module}. The `ng` module must be explicitly added. * @returns {function()} Injector function. See {@link angular.module.AUTO.$injector $injector}. * @@ -18,11 +18,11 @@ * Typical usage *
  *   // create an injector
- *   var $injector = angular.injector('ng');
+ *   var $injector = angular.injector(['ng']);
  *
  *   // use the injector to kick of your application
  *   // use the type inference to auto inject arguments, or use implicit injection
- *   $injector.invoke(null, function($rootScope, $compile, $document){
+ *   $injector.invoke(function($rootScope, $compile, $document){
  *     $compile($document)($rootScope);
  *     $rootScope.$digest();
  *   });
@@ -111,6 +111,18 @@ function inferInjectionArgs(fn) {
  * As an array of injection names, where the last item in the array is the function to call.
  */
 
+/**
+ * @ngdoc method
+ * @name angular.module.AUTO.$injector#get
+ * @methodOf angular.module.AUTO.$injector
+ *
+ * @description
+ * Return an instance of the service.
+ *
+ * @param {string} name The name of the instance to retrieve.
+ * @return {*} The instance.
+ */
+
 /**
  * @ngdoc method
  * @name angular.module.AUTO.$injector#invoke
@@ -119,8 +131,8 @@ function inferInjectionArgs(fn) {
  * @description
  * Invoke the method and supply the method arguments from the `$injector`.
  *
- * @param {Object} self The `this` for the invoked method.
- * @param {function} fn The function to invoke. The function arguments come form the function annotation.
+ * @param {!function} fn The function to invoke. The function arguments come form the function annotation.
+ * @param {Object=} self The `this` for the invoked method.
  * @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before
  *   the `$injector` is consulted.
  * @return the value returned by the invoked `fn` function.
@@ -238,28 +250,34 @@ function inferInjectionArgs(fn) {
 
 
 function createInjector(modulesToLoad) {
-  var cache = {},
-      providerSuffix = 'Provider',
+  var providerSuffix = 'Provider',
       path = [],
-      $injector,
-      loadedModules = new HashMap();
-
-  value('$injector', $injector = {
-    get: getService,
-    invoke: invoke,
-    instantiate: instantiate
-  });
-  value('$provide', {
-    service: supportObject(service),
-    factory: supportObject(factory),
-    value: supportObject(value),
-    decorator: decorator
-  });
+      loadedModules = new HashMap(),
+      providerCache = {
+        $provide: {
+            service: supportObject(service),
+            factory: supportObject(factory),
+            value: supportObject(value),
+            decorator: decorator
+          }
+      },
+      providerInjector = createInternalInjector(providerCache, function() {
+        throw Error("Unknown provider: " + path.join(' <- '));
+      }),
+      instanceCache = {},
+      instanceInjector = (instanceCache.$injector =
+          createInternalInjector(instanceCache, function(servicename) {
+            var provider = providerInjector.get(servicename + providerSuffix);
+            return instanceInjector.invoke(provider.$get, provider);
+          }));
+
 
   loadModules(modulesToLoad);
 
-  return $injector;
+  return instanceInjector;
 
+  ////////////////////////////////////
+  // $provider
   ////////////////////////////////////
 
   function supportObject(delegate) {
@@ -274,124 +292,146 @@ function createInjector(modulesToLoad) {
 
   function service(name, provider) {
     if (isFunction(provider)){
-      provider = instantiate(provider);
+      provider = providerInjector.instantiate(provider);
     }
     if (!provider.$get) {
       throw Error('Provider ' + name + ' must define $get factory method.');
     }
-    cache['#' + name + providerSuffix] = provider;
+    providerCache[name + providerSuffix] = provider;
   }
 
   function factory(name, factoryFn) { service(name, { $get:factoryFn }); }
 
   function value(name, value) { factory(name, valueFn(value)); }
 
-  function decorator(name, decorFn) {
-    var origProvider = cache['#' + name + providerSuffix];
-    if (!origProvider) throw Error("Can't find provider for: " + name);
-    if (cache['#' + name]) throw Error("Service " + name + " already instantiated, can't decorate!");
-    var orig$get = origProvider.$get;
+  function decorator(serviceName, decorFn) {
+    var origProvider = providerInjector.get(serviceName + providerSuffix),
+        orig$get = origProvider.$get;
+
     origProvider.$get = function() {
-      var origInstance = $injector.invoke(origProvider, orig$get);
-      return $injector.invoke(null, decorFn, {$delegate: origInstance});
+      var origInstance = instanceInjector.invoke(orig$get, origProvider);
+      return instanceInjector.invoke(decorFn, null, {$delegate: origInstance});
     };
   }
 
-
-  function getService(value) {
-    if (typeof value !== 'string') {
-      throw Error('Service name expected');
-    }
-    var instanceKey = '#' + value,
-        instance = cache[instanceKey];
-    if (instance !== undefined || cache.hasOwnProperty(instanceKey)) {
-      return instance;
-    }
-    try {
-      path.unshift(value);
-      var providerKey = instanceKey + providerSuffix,
-          provider = cache[providerKey];
-      if (provider) {
-        return cache[instanceKey] = invoke(provider, provider.$get);
+  ////////////////////////////////////
+  // Module Loading
+  ////////////////////////////////////
+  function loadModules(modulesToLoad){
+    forEach(modulesToLoad, function(module) {
+      if (loadedModules.get(module)) return;
+      loadedModules.put(module, true);
+      if (isString(module)) {
+        var moduleFn = angularModule(module);
+        loadModules(moduleFn.requires);
+
+        try {
+          for(var invokeQueue = moduleFn.invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) {
+            var invokeArgs = invokeQueue[i],
+                provider = invokeArgs[0] == '$injector'
+                    ? providerInjector
+                    : providerInjector.get(invokeArgs[0]);
+
+            provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
+          }
+        } catch (e) {
+          if (e.message) e.message += ' from ' + module;
+          throw e;
+        }
+      } else if (isFunction(module)) {
+        try {
+          providerInjector.invoke(module);
+        } catch (e) {
+          if (e.message) e.message += ' from ' + module;
+          throw e;
+        }
+      } else if (isArray(module)) {
+        try {
+          providerInjector.invoke(module);
+        } catch (e) {
+          if (e.message) e.message += ' from ' + String(module[module.length - 1]);
+          throw e;
+        }
       } else {
-        throw Error("Unknown provider for '" + path.join("' <- '") + "'.");
+        assertArgFn(module, 'module');
       }
-    } finally {
-      path.shift();
-    }
+    });
   }
 
-  function invoke(self, fn, locals){
-    var args = [],
-        $inject,
-        length,
-        key;
-
-    if (typeof fn == 'function') {
-      $inject = inferInjectionArgs(fn);
-      length = $inject.length;
-    } else {
-      if (isArray(fn)) {
-        $inject = fn;
-        length = $inject.length;
-        fn = $inject[--length];
-      }
-      assertArgFn(fn, 'fn');
-    }
+  ////////////////////////////////////
+  // internal Injector
+  ////////////////////////////////////
 
-    while(length--) {
-      key = $inject[length];
-      args.unshift(
-        locals && locals.hasOwnProperty(key)
-        ? locals[key]
-        : getService($inject[length], path)
-      );
-    }
+  function createInternalInjector(cache, factory) {
 
-    // Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke
-    switch (self ? -1 : args.length) {
-      case  0: return fn();
-      case  1: return fn(args[0]);
-      case  2: return fn(args[0], args[1]);
-      case  3: return fn(args[0], args[1], args[2]);
-      case  4: return fn(args[0], args[1], args[2], args[3]);
-      case  5: return fn(args[0], args[1], args[2], args[3], args[4]);
-      case  6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]);
-      case  7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
-      case  8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
-      case  9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
-      case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);
-      default: return fn.apply(self, args);
+    function getService(serviceName) {
+      if (typeof serviceName !== 'string') {
+        throw Error('Service name expected');
+      }
+      if (cache.hasOwnProperty(serviceName)) {
+        return cache[serviceName];
+      } else {
+        try {
+          path.unshift(serviceName);
+          return cache[serviceName] = factory(serviceName);
+        } finally {
+          path.shift();
+        }
+      }
     }
-  }
 
-  function instantiate(Type, locals){
-    var Constructor = function(){},
-        instance;
-    Constructor.prototype = Type.prototype;
-    instance = new Constructor();
-    return invoke(instance, Type, locals) || instance;
-  }
+    function invoke(fn, self, locals){
+      var args = [],
+          $injectAnnotation,
+          $injectAnnotationIndex,
+          key;
 
-  function loadModules(modulesToLoad){
-    forEach(modulesToLoad, function(module) {
-      if (loadedModules.get(module)) return;
-      loadedModules.put(module, true);
-      if (isString(module)) {
-        module = angularModule(module);
-        loadModules(module.requires);
+      if (typeof fn == 'function') {
+        $injectAnnotation = inferInjectionArgs(fn);
+        $injectAnnotationIndex = $injectAnnotation.length;
+      } else {
+        if (isArray(fn)) {
+          $injectAnnotation = fn;
+          $injectAnnotationIndex = $injectAnnotation.length;
+          fn = $injectAnnotation[--$injectAnnotationIndex];
+        }
+        assertArgFn(fn, 'fn');
+      }
 
-        for(var invokeQueue = module.invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) {
-          var invokeArgs = invokeQueue[i],
-              service = getService(invokeArgs[0]);
+      while($injectAnnotationIndex--) {
+        key = $injectAnnotation[$injectAnnotationIndex];
+        args.unshift(locals && locals.hasOwnProperty(key) ? locals[key] : getService(key));
+      }
 
-          service[invokeArgs[1]].apply(service, invokeArgs[2]);
-        }
-      } else if (isFunction(module) || isArray(module)) {
-        invoke(null, module);
-      } else {
-        assertArgFn(module, 'module');
+      // Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke
+      switch (self ? -1 : args.length) {
+        case  0: return fn();
+        case  1: return fn(args[0]);
+        case  2: return fn(args[0], args[1]);
+        case  3: return fn(args[0], args[1], args[2]);
+        case  4: return fn(args[0], args[1], args[2], args[3]);
+        case  5: return fn(args[0], args[1], args[2], args[3], args[4]);
+        case  6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]);
+        case  7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
+        case  8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
+        case  9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
+        case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);
+        default: return fn.apply(self, args);
       }
-    });
+    }
+
+    function instantiate(Type, locals){
+      var Constructor = function(){},
+          instance;
+      Constructor.prototype = Type.prototype;
+      instance = new Constructor();
+      return invoke(Type, instance, locals) || instance;
+    }
+
+    return {
+      invoke: invoke,
+      instantiate: instantiate,
+      get: getService
+    };
   }
 }
diff --git a/src/angular-mocks.js b/src/angular-mocks.js
index f70731d3..d2dbb114 100644
--- a/src/angular-mocks.js
+++ b/src/angular-mocks.js
@@ -935,16 +935,56 @@ window.jstestdriver && (function(window) {
  * @return a method
  */
 window.jasmine && (function(window) {
-  window.inject = function () {
+
+  function getCurrentSpec() {
+    return jasmine.getEnv().currentSpec;
+  }
+
+  function isSpecRunning() {
+    var spec = getCurrentSpec();
+    return spec && spec.queue.running;
+  }
+
+  window.module = function() {
+    var moduleFns = Array.prototype.slice.call(arguments, 0);
+    var stack = Error('Declaration Location').stack;
+    return isSpecRunning() ? workFn() : workFn;
+    /////////////////////
+    function workFn() {
+      var spec = getCurrentSpec();
+      if (spec.$injector) {
+        throw Error('Injector already created, can not register a module!');
+      } else {
+        var modules = spec.$modules || (spec.$modules = []);
+        angular.forEach(moduleFns, function(module) {
+          modules.push(module);
+        });
+      }
+    }
+  };
+  window.inject = function() {
     var blockFns = Array.prototype.slice.call(arguments, 0);
-    return function() {
-      var injector = this.$injector;
+    var stack = Error('Declaration Location').stack;
+    return isSpecRunning() ? workFn() : workFn;
+    /////////////////////
+    function workFn() {
+      var spec = getCurrentSpec();
+      var modules = spec.$modules || [];
+      modules.unshift('ngMock');
+      modules.unshift('ng');
+      var injector = spec.$injector;
       if (!injector) {
-        injector = this.$injector = angular.injector('ng', 'ngMock');
+        injector = spec.$injector = angular.injector(modules);
       }
+      console.log('inject', modules)
       for(var i = 0, ii = blockFns.length; i < ii; i++) {
-        injector.invoke(this, blockFns[i]);
+        try {
+          injector.invoke(blockFns[i] || angular.noop, this);
+        } catch (e) {
+          if(e.stack) e.stack +=  '\n' + stack;
+          throw e;
+        }
       }
-    };
+    }
   }
 })(window);
diff --git a/src/loader.js b/src/loader.js
index ec30ad9a..3d5edd31 100644
--- a/src/loader.js
+++ b/src/loader.js
@@ -48,7 +48,7 @@ function setupModuleLoader(window) {
      * Then you can load your module like this:
      *
      * 
-     * var injector = angular.injector('ng', 'MyModule')
+     * var injector = angular.injector(['ng', 'MyModule'])
      * 
* * @param {!string} name The name of the module to create or retrieve. @@ -67,13 +67,11 @@ function setupModuleLoader(window) { throw Error('No module: ' + name); } - function init(fn) { - invokeQueue.push(['$injector', 'invoke', [null, fn]]); - } - /** @type {!Array.>} */ var invokeQueue = []; + var init = invokeLater('$injector', 'invoke'); + /** @type {angular.Module} */ var moduleInstance = { /** diff --git a/src/scenario/Application.js b/src/scenario/Application.js index ba6bbea7..d3a70569 100644 --- a/src/scenario/Application.js +++ b/src/scenario/Application.js @@ -93,7 +93,7 @@ angular.scenario.Application.prototype.executeAction = function(action) { angularInit($window.document, function(element) { element = $window.angular.element(element); var $injector = element.inheritedData('$injector'); - $injector.invoke(null, function($browser){ + $injector.invoke(function($browser){ $browser.notifyWhenNoOutstandingRequests(function() { action.call(self, $window, _jQuery($window.document)); }); diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js index aeb5196a..cfde1f64 100644 --- a/src/scenario/Runner.js +++ b/src/scenario/Runner.js @@ -163,7 +163,7 @@ angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) { */ angular.scenario.Runner.prototype.run = function(application) { var self = this; - var $root = angular.injector('ng').get('$rootScope'); + var $root = angular.injector(['ng']).get('$rootScope'); angular.extend($root, this); angular.forEach(angular.scenario.Runner.prototype, function(fn, name) { $root[name] = angular.bind(self, fn); diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js index 12cbdb21..bbe29948 100644 --- a/src/scenario/dsl.js +++ b/src/scenario/dsl.js @@ -103,25 +103,25 @@ angular.scenario.dsl('browser', function() { api.url = function() { return this.addFutureAction('$location.url()', function($window, $document, done) { - done(null, $window.angular.injector('ng').get('$location').url()); + done(null, $window.angular.injector(['ng']).get('$location').url()); }); }; api.path = function() { return this.addFutureAction('$location.path()', function($window, $document, done) { - done(null, $window.angular.injector('ng').get('$location').path()); + done(null, $window.angular.injector(['ng']).get('$location').path()); }); }; api.search = function() { return this.addFutureAction('$location.search()', function($window, $document, done) { - done(null, $window.angular.injector('ng').get('$location').search()); + done(null, $window.angular.injector(['ng']).get('$location').search()); }); }; api.hash = function() { return this.addFutureAction('$location.hash()', function($window, $document, done) { - done(null, $window.angular.injector('ng').get('$location').hash()); + done(null, $window.angular.injector(['ng']).get('$location').hash()); }); }; diff --git a/src/service/compiler.js b/src/service/compiler.js index 8a0dca7d..727f7983 100644 --- a/src/service/compiler.js +++ b/src/service/compiler.js @@ -28,7 +28,7 @@ function $CompileProvider(){ forEach(this.linkFns, function(fn) { try { if (isArray(fn) || fn.$inject) { - $injector.invoke(childScope, fn, locals); + $injector.invoke(fn, childScope, locals); } else { fn.call(childScope, element); } @@ -97,7 +97,7 @@ function $CompileProvider(){ * that is a DOM clone of the original template. *
-          angular.injector('ng').invoke(null, function($rootScope, $compile) {
+          angular.injector(['ng']).invoke(function($rootScope, $compile) {
             // Chose one:
 
             // A: compile the entire window.document.
@@ -143,8 +143,8 @@ function $CompileProvider(){
        * - If you are not asking the linking function to clone the template, create the DOM element(s)
        *   before you send them to the compiler and keep this reference around.
        *   
-       *     var $injector = angular.injector('ng');
-       *     var scope = $injector.invoke(null, function($rootScope, $compile){
+       *     var $injector = angular.injector(['ng']);
+       *     var scope = $injector.invoke(function($rootScope, $compile){
        *       var element = $compile('

{{total}}

')($rootScope); * }); *
@@ -277,7 +277,7 @@ function $CompileProvider(){ descend = false; directives = false; var parent = element.parent(); - template.addLinkFn($injector.invoke(selfApi, widget, locals)); + template.addLinkFn($injector.invoke(widget, selfApi, locals)); if (parent && parent[0]) { element = jqLite(parent[0].childNodes[elementIndex]); } @@ -310,7 +310,7 @@ function $CompileProvider(){ if (fn) { element.addClass('ng-directive'); template.addLinkFn((isArray(fn) || fn.$inject) - ? $injector.invoke(selfApi, fn, {$value:value, $element: element}) + ? $injector.invoke(fn, selfApi, {$value:value, $element: element}) : fn.call(selfApi, value, element)); } }); diff --git a/src/service/scope.js b/src/service/scope.js index 2836b42e..a19bf83e 100644 --- a/src/service/scope.js +++ b/src/service/scope.js @@ -50,7 +50,7 @@ function $RootScopeProvider(){ * * Here is a simple scope snippet to show how you can interact with the scope. *
-        angular.injector(function($rootScope) {
+        angular.injector(['ng']).invoke(function($rootScope) {
            var scope = $rootScope.$new();
            scope.salutation = 'Hello';
            scope.name = 'World';
@@ -168,7 +168,7 @@ function $RootScopeProvider(){
           for(var key in ClassPrototype) {
             child[key] = bind(child, ClassPrototype[key]);
           }
-          $injector.invoke(child, Class, curryArguments);
+          $injector.invoke(Class, child, curryArguments);
         }
         return child;
       },
-- 
cgit v1.2.3