├── chrome-extension ├── visualize ├── icon.png ├── background.html ├── devtools_background.html ├── devtools_background.js ├── manifest.json ├── background.js ├── panel.js └── panel.html ├── .gitignore ├── example ├── kitchen │ ├── electricity.js │ ├── fridge.js │ ├── skillet.js │ ├── grinder.js │ ├── pump.js │ ├── heater.js │ ├── dishwasher.js │ ├── stove.js │ ├── coffee_maker.js │ ├── index.html │ ├── kitchen.js │ └── main.js ├── kitchen-di │ ├── electricity.js │ ├── skillet.js │ ├── mock_heater.js │ ├── fridge.js │ ├── main.js │ ├── coffee_maker │ │ ├── grinder.js │ │ ├── pump.js │ │ ├── heater.js │ │ └── coffee_maker.js │ ├── dishwasher.js │ ├── stove.js │ ├── index.html │ └── kitchen.js ├── coffee │ ├── heater.js │ ├── mock_heater.js │ ├── coffee_module.js │ ├── pump.js │ ├── electric_heater.js │ └── coffee_maker.js ├── node │ ├── engine.js │ ├── test │ │ ├── mock_engine.js │ │ └── main.spec.js │ ├── main.js │ └── car.js └── testing │ ├── mocks.js │ └── coffee.spec.js ├── visualize ├── injectors │ ├── kitchen.png │ ├── Untitled.png │ ├── coffee_maker.json │ ├── data.json │ ├── index.html │ ├── kitchen.json │ ├── playground.html │ ├── style.css │ ├── kitchen.svg │ └── app.js └── dag │ ├── data.json │ ├── index.html │ ├── style.css │ └── dag.js ├── .npmignore ├── test ├── fixtures │ ├── shiny_house.js │ ├── house.js │ └── car.js ├── coffee.spec.js ├── matchers.js ├── async.spec.js ├── annotations.spec.js └── injector.spec.js ├── src ├── index.js ├── util.js ├── profiler.js ├── testing.js ├── annotations.js ├── providers.js └── injector.js ├── index.html ├── node └── index.js ├── .travis.yml ├── LICENSE ├── docs ├── terminology.md ├── how-can-i-use-it.md ├── interfaces.md └── di_sl.md ├── karma.conf.js ├── test-main.js ├── dist ├── amd │ ├── index.js │ ├── util.js │ ├── profiler.js │ ├── testing.js │ ├── providers.js │ ├── annotations.js │ └── injector.js └── cjs │ ├── util.js │ ├── index.js │ ├── profiler.js │ ├── testing.js │ ├── providers.js │ ├── annotations.js │ └── injector.js ├── package.json ├── README.md └── gulpfile.js /chrome-extension/visualize: -------------------------------------------------------------------------------- 1 | ../visualize -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | compiled 3 | sauce_connect.log 4 | -------------------------------------------------------------------------------- /example/kitchen/electricity.js: -------------------------------------------------------------------------------- 1 | 2 | export class Electricity {} 3 | -------------------------------------------------------------------------------- /example/kitchen-di/electricity.js: -------------------------------------------------------------------------------- 1 | 2 | export class Electricity {} 3 | -------------------------------------------------------------------------------- /example/coffee/heater.js: -------------------------------------------------------------------------------- 1 | // This is an interface. 2 | export class Heater {} 3 | -------------------------------------------------------------------------------- /chrome-extension/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RackHD/di.js/master/chrome-extension/icon.png -------------------------------------------------------------------------------- /visualize/injectors/kitchen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RackHD/di.js/master/visualize/injectors/kitchen.png -------------------------------------------------------------------------------- /visualize/injectors/Untitled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RackHD/di.js/master/visualize/injectors/Untitled.png -------------------------------------------------------------------------------- /chrome-extension/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /chrome-extension/devtools_background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /chrome-extension/devtools_background.js: -------------------------------------------------------------------------------- 1 | // Create DI panel. 2 | chrome.devtools.panels.create( 3 | 'DI', 4 | 'icon.png', 5 | 'panel.html' 6 | ); 7 | -------------------------------------------------------------------------------- /example/coffee/mock_heater.js: -------------------------------------------------------------------------------- 1 | import {Provide} from 'di'; 2 | 3 | import {Heater} from './heater'; 4 | 5 | @Provide(Heater) 6 | export class MockHeater { 7 | on() {} 8 | off() {} 9 | } 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | visualize/ 3 | *.sublime-project 4 | *.sublime-workspace 5 | gulpfile.js 6 | karma.conf.js 7 | /index.html 8 | test-main.js 9 | .* 10 | *.orig 11 | Gruntfile.* 12 | compiled/ 13 | -------------------------------------------------------------------------------- /example/kitchen/fridge.js: -------------------------------------------------------------------------------- 1 | 2 | export class Fridge { 3 | constructor(electricity) { 4 | this.electricity = electricity; 5 | } 6 | 7 | getEggs() { 8 | return '3 eggs'; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/kitchen/skillet.js: -------------------------------------------------------------------------------- 1 | 2 | export class Skillet { 3 | add(item) { 4 | console.log('Adding ' + item + ' to the skillet.'); 5 | } 6 | 7 | toString() { 8 | return 'skillet'; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/coffee/coffee_module.js: -------------------------------------------------------------------------------- 1 | import {CoffeeMaker} from './coffee_maker'; 2 | import {ElectricHeater} from './electric_heater'; 3 | import {Pump} from './pump'; 4 | 5 | export var module = [CoffeeMaker, ElectricHeater, Pump]; 6 | -------------------------------------------------------------------------------- /example/kitchen/grinder.js: -------------------------------------------------------------------------------- 1 | 2 | export class Grinder { 3 | constructor(electricity) { 4 | this.electricity = electricity; 5 | } 6 | 7 | grind() { 8 | console.log('Grinding coffee beans...'); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/kitchen-di/skillet.js: -------------------------------------------------------------------------------- 1 | 2 | export class Skillet { 3 | constructor() {} 4 | 5 | add(item) { 6 | console.log('Adding ' + item + ' to the skillet.'); 7 | } 8 | 9 | toString() { 10 | return 'skillet'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/kitchen/pump.js: -------------------------------------------------------------------------------- 1 | 2 | export class Pump { 3 | constructor(heater, electricity) { 4 | this.heater = heater; 5 | this.electricity = electricity; 6 | } 7 | 8 | pump() { 9 | console.log('Pumping the water...'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /visualize/dag/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "source": "CoffeeMaker", 4 | "target": "Heater" 5 | }, 6 | { 7 | "source": "CoffeeMaker", 8 | "target": "Pump" 9 | }, 10 | { 11 | "source": "Pump", 12 | "target": "Heater" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /example/node/engine.js: -------------------------------------------------------------------------------- 1 | var Engine = function() { 2 | this.state = 'stopped'; 3 | }; 4 | 5 | Engine.prototype = { 6 | start: function() { 7 | console.log('Starting engine...'); 8 | this.state = 'running'; 9 | } 10 | }; 11 | 12 | module.exports = Engine; 13 | -------------------------------------------------------------------------------- /test/fixtures/shiny_house.js: -------------------------------------------------------------------------------- 1 | import {Inject, Provide} from '../../src/annotations'; 2 | 3 | @Provide('House') 4 | @Inject('Kitchen') 5 | export class ShinyHouse { 6 | constructor(kitchen) {} 7 | 8 | nothing() {} 9 | } 10 | 11 | export var module = [ShinyHouse]; 12 | -------------------------------------------------------------------------------- /visualize/dag/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /example/node/test/mock_engine.js: -------------------------------------------------------------------------------- 1 | var di = require('di'); 2 | var Engine = require('../engine'); 3 | 4 | var MockEngine = function() {}; 5 | MockEngine.prototype = { 6 | state: 'running' 7 | }; 8 | 9 | di.annotate(MockEngine, new di.Provide(Engine)); 10 | 11 | module.exports = MockEngine; 12 | -------------------------------------------------------------------------------- /example/kitchen-di/mock_heater.js: -------------------------------------------------------------------------------- 1 | import {Provide} from 'di'; 2 | import {Heater} from './coffee_maker/heater'; 3 | 4 | @Provide(Heater) 5 | export class MockHeater { 6 | constructor() {} 7 | 8 | on() { 9 | console.log('Turning on the MOCK heater...'); 10 | } 11 | 12 | off() {} 13 | } 14 | -------------------------------------------------------------------------------- /example/kitchen-di/fridge.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'di'; 2 | import {Electricity} from './electricity'; 3 | 4 | @Inject(Electricity) 5 | export class Fridge { 6 | constructor(electricity) { 7 | this.electricity = electricity; 8 | } 9 | 10 | getEggs() { 11 | return '3 eggs'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/coffee/pump.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'di'; 2 | 3 | import {Heater} from './heater'; 4 | 5 | @Inject(Heater) 6 | export class Pump { 7 | constructor(heater) { 8 | this.heater = heater; 9 | } 10 | 11 | pump() { 12 | this.heater.on(); 13 | // console.log('Pumping...'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/kitchen/heater.js: -------------------------------------------------------------------------------- 1 | 2 | export class Heater { 3 | constructor(electricity) { 4 | this.electricity = electricity; 5 | } 6 | 7 | on() { 8 | console.log('Turning on the coffee heater...'); 9 | } 10 | 11 | off() { 12 | console.log('Turning off the coffee heater...'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // PUBLIC API 2 | 3 | export {Injector} from './injector'; 4 | export { 5 | annotate, 6 | Inject, 7 | InjectLazy, 8 | InjectPromise, 9 | Provide, 10 | ProvidePromise, 11 | SuperConstructor, 12 | TransientScope, 13 | ClassProvider, 14 | FactoryProvider 15 | } from './annotations'; 16 | -------------------------------------------------------------------------------- /example/kitchen/dishwasher.js: -------------------------------------------------------------------------------- 1 | 2 | export class Dishwasher { 3 | constructor(electricity) { 4 | this.electricity = electricity; 5 | } 6 | 7 | add(item) { 8 | console.log('Putting ' + item + ' into the dishwasher...'); 9 | } 10 | wash() { 11 | console.log('Running the dishwasher...'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /chrome-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DI visualizer", 3 | "version": "0.0.1", 4 | "description": "", 5 | "background": { 6 | "page": "background.html" 7 | }, 8 | "devtools_page": "devtools_background.html", 9 | "manifest_version": 2, 10 | "permissions": [ 11 | "tabs", 12 | "" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /example/kitchen-di/main.js: -------------------------------------------------------------------------------- 1 | import {Injector} from 'di'; 2 | import {Kitchen} from './kitchen'; 3 | import {MockHeater} from './mock_heater'; 4 | 5 | 6 | function main() { 7 | var injector = new Injector([MockHeater]); 8 | var kitchen = injector.get(Kitchen); 9 | 10 | kitchen.makeBreakfast(); 11 | } 12 | 13 | main(); 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/kitchen-di/coffee_maker/grinder.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'di'; 2 | import {Electricity} from '../electricity'; 3 | 4 | @Inject(Electricity) 5 | export class Grinder { 6 | constructor(electricity) { 7 | this.electricity = electricity; 8 | } 9 | 10 | grind() { 11 | console.log('Grinding coffee beans...'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/coffee/electric_heater.js: -------------------------------------------------------------------------------- 1 | import {Provide} from 'di'; 2 | 3 | import {Heater} from './heater'; 4 | 5 | @Provide(Heater) 6 | export class ElectricHeater { 7 | constructor() {} 8 | 9 | on() { 10 | // console.log('Turning on electric heater...'); 11 | } 12 | 13 | off() { 14 | // console.log('Turning off electric heater...'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/node/main.js: -------------------------------------------------------------------------------- 1 | var di = require('di'); 2 | var Car = require('./car'); 3 | 4 | 5 | var injector = new di.Injector([]); 6 | 7 | console.log('Getting in the car...'); 8 | var car = injector.get(Car); 9 | 10 | car.run(); 11 | 12 | if (car.isRunning()) { 13 | console.log('The car is running'); 14 | } else { 15 | console.log('The car is stopped'); 16 | } 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | DI v2 4 | 5 | 6 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/kitchen/stove.js: -------------------------------------------------------------------------------- 1 | 2 | export class Stove { 3 | constructor(electricity) { 4 | this.electricity = electricity; 5 | } 6 | 7 | add(item) { 8 | console.log('Adding ' + item + ' onto the stove.'); 9 | } 10 | 11 | on() { 12 | console.log('Turning on the stove...'); 13 | } 14 | 15 | off() { 16 | console.log('Turning off the stove...'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/kitchen-di/coffee_maker/pump.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'di'; 2 | import {Heater} from './heater'; 3 | import {Electricity} from '../electricity'; 4 | 5 | @Inject(Heater, Electricity) 6 | export class Pump { 7 | constructor(heater, electricity) { 8 | this.heater = heater; 9 | this.electricity = electricity; 10 | } 11 | 12 | pump() { 13 | console.log('Pumping the water...'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/node/test/main.spec.js: -------------------------------------------------------------------------------- 1 | var di = require('di'); 2 | var Car = require('../car'); 3 | var MockEngine = require('./mock_engine'); 4 | 5 | describe('Car', function() { 6 | beforeEach(function() { 7 | var injector = new di.Injector([MockEngine]); 8 | this.car = injector.get(Car); 9 | }); 10 | 11 | it('is running', function() { 12 | expect(this.car.isRunning()).toBe(true); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /example/coffee/coffee_maker.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'di'; 2 | 3 | import {Heater} from './heater'; 4 | import {Pump} from './pump'; 5 | 6 | @Inject(Heater, Pump) 7 | export class CoffeeMaker { 8 | constructor(heater, pump) { 9 | this.heater = heater; 10 | this.pump = pump; 11 | } 12 | 13 | brew() { 14 | this.pump.pump(); 15 | this.heater.on(); 16 | // console.log('Brewing...') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/node/car.js: -------------------------------------------------------------------------------- 1 | var di = require('di'); 2 | var Engine = require('./engine'); 3 | 4 | var Car = function(engine) { 5 | this.engine = engine; 6 | }; 7 | 8 | Car.prototype = { 9 | run: function() { 10 | this.engine.start(); 11 | }, 12 | isRunning: function() { 13 | return this.engine.state === 'running'; 14 | } 15 | }; 16 | 17 | di.annotate(Car, new di.Inject(Engine)); 18 | 19 | module.exports = Car; 20 | -------------------------------------------------------------------------------- /example/testing/mocks.js: -------------------------------------------------------------------------------- 1 | import {Provide} from '../../src/annotations'; 2 | import {Heater} from '../coffee/heater'; 3 | 4 | class MockHeater { 5 | constructor() { 6 | this.on = jasmine.createSpy('on'); 7 | this.off = jasmine.createSpy('off'); 8 | } 9 | } 10 | 11 | @Provide(Heater) 12 | class DummyHeater { 13 | constructor() {} 14 | on() {} 15 | off() {} 16 | } 17 | 18 | export {MockHeater, DummyHeater} 19 | -------------------------------------------------------------------------------- /example/kitchen-di/coffee_maker/heater.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'di'; 2 | import {Electricity} from '../electricity'; 3 | 4 | @Inject(Electricity) 5 | export class Heater { 6 | constructor(electricity) { 7 | this.electricity = electricity; 8 | } 9 | 10 | on() { 11 | console.log('Turning on the coffee heater...'); 12 | } 13 | 14 | off() { 15 | console.log('Turning off the coffee heater...'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/kitchen-di/dishwasher.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'di'; 2 | import {Electricity} from './electricity'; 3 | 4 | @Inject(Electricity) 5 | export class Dishwasher { 6 | constructor(electricity) { 7 | this.electricity = electricity; 8 | } 9 | 10 | add(item) { 11 | console.log('Putting ' + item + ' into the dishwasher...'); 12 | } 13 | wash() { 14 | console.log('Running the dishwasher...'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/kitchen/coffee_maker.js: -------------------------------------------------------------------------------- 1 | 2 | export class CoffeeMaker { 3 | constructor(grinder, pump, heater) { 4 | this.grinder = grinder; 5 | this.pump = pump; 6 | this.heater = heater; 7 | } 8 | 9 | brew() { 10 | console.log('Brewing a coffee...'); 11 | this.grinder.grind(); 12 | this.heater.on(); 13 | this.pump.pump(); 14 | this.heater.off(); 15 | console.log('A coffee is ready.'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/kitchen-di/stove.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'di'; 2 | import {Electricity} from './electricity'; 3 | 4 | @Inject(Electricity) 5 | export class Stove { 6 | constructor(electricity) { 7 | this.electricity = electricity; 8 | } 9 | 10 | add(item) { 11 | console.log('Adding ' + item + ' onto the stove.'); 12 | } 13 | 14 | on() { 15 | console.log('Turning on the stove...'); 16 | } 17 | 18 | off() { 19 | console.log('Turning off the stove...'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /node/index.js: -------------------------------------------------------------------------------- 1 | // This is the file that gets included when you use "di" module in Node.js. 2 | 3 | // Include Traceur runtime. 4 | require('traceur/bin/traceur-runtime'); 5 | 6 | // Node.js has to be run with --harmony_collections to support ES6 Map. 7 | // If not defined, include a polyfill. 8 | if (typeof Map === 'undefined') { 9 | require('es6-shim'); 10 | } 11 | 12 | if (!global.__di) { 13 | global.__di = require('../dist/cjs/index'); 14 | } 15 | 16 | module.exports = global.__di; 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | 6 | env: 7 | global: 8 | - SAUCE_USERNAME=angular-ci 9 | - SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987 10 | 11 | install: 12 | - npm install 13 | - npm install -g gulp 14 | - npm install -g karma-cli 15 | - npm install -g jasmine-node 16 | 17 | before_script: 18 | 19 | script: 20 | - karma start --single-run --browsers SL_Chrome --reporters dots 21 | - jasmine-node example/node/test 22 | 23 | after_script: 24 | -------------------------------------------------------------------------------- /visualize/dag/style.css: -------------------------------------------------------------------------------- 1 | .link { 2 | fill: none; 3 | stroke: #666; 4 | stroke-width: 1.5px; 5 | } 6 | 7 | #licensing { 8 | fill: green; 9 | } 10 | 11 | .link.licensing { 12 | stroke: green; 13 | } 14 | 15 | .link.resolved { 16 | stroke-dasharray: 0,2 1; 17 | } 18 | 19 | circle { 20 | fill: #ccc; 21 | stroke: #333; 22 | stroke-width: 1.5px; 23 | } 24 | 25 | text { 26 | font: 10px sans-serif; 27 | pointer-events: none; 28 | text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; 29 | } 30 | -------------------------------------------------------------------------------- /example/kitchen/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Kitchen example 4 | 5 | 6 | Kitchen example, transpiled on the server into RequireJS, without DI. 7 | 8 | 9 | 10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/fixtures/house.js: -------------------------------------------------------------------------------- 1 | import {Inject, Provide} from '../../src/annotations'; 2 | 3 | // This is an example of using string as tokens. 4 | 5 | @Provide('House') 6 | @Inject('Kitchen') 7 | export class House { 8 | constructor(kitchen) { 9 | 10 | } 11 | 12 | nothing() {} 13 | } 14 | 15 | @Provide('Kitchen') 16 | @Inject('Sink') 17 | export class Kitchen { 18 | constructor(sink) { 19 | 20 | } 21 | 22 | nothing() {} 23 | } 24 | 25 | // Sink is missing. 26 | // @Provide('Sink') 27 | // export class Sink { 28 | // nothing() {} 29 | // } 30 | 31 | export var module = [House, Kitchen]; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Google, Inc. http://angularjs.org 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /test/coffee.spec.js: -------------------------------------------------------------------------------- 1 | import {Injector} from '../src/injector'; 2 | 3 | import {module as coffeeModule} from '../example/coffee/coffee_module'; 4 | import {CoffeeMaker} from '../example/coffee/coffee_maker'; 5 | import {MockHeater} from '../example/coffee/mock_heater'; 6 | 7 | 8 | describe('coffee example', function() { 9 | it('should work', function() { 10 | var i = new Injector(coffeeModule); 11 | 12 | i.get(CoffeeMaker).brew(); 13 | }); 14 | 15 | 16 | it('should work with mocked heater', function() { 17 | var i = new Injector([MockHeater]); 18 | 19 | i.get(CoffeeMaker).brew(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /example/kitchen-di/coffee_maker/coffee_maker.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'di'; 2 | 3 | import {Grinder} from './grinder'; 4 | import {Pump} from './pump'; 5 | import {Heater} from './heater'; 6 | 7 | @Inject(Grinder, Pump, Heater) 8 | export class CoffeeMaker { 9 | constructor(grinder, pump, heater) { 10 | this.grinder = grinder; 11 | this.pump = pump; 12 | this.heater = heater; 13 | } 14 | 15 | brew() { 16 | console.log('Brewing a coffee...'); 17 | this.grinder.grind(); 18 | this.heater.on(); 19 | this.pump.pump(); 20 | this.heater.off(); 21 | console.log('A coffee is ready.'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /chrome-extension/background.js: -------------------------------------------------------------------------------- 1 | // TODO(vojta): Make refresh work 2 | // notify of page refreshes 3 | chrome.extension.onConnect.addListener(function(port) { 4 | port.onMessage.addListener(function (msg) { 5 | if (msg.action === 'register') { 6 | var respond = function (tabId, changeInfo, tab) { 7 | if (tabId !== msg.inspectedTabId) { 8 | return; 9 | } 10 | port.postMessage('refresh'); 11 | }; 12 | 13 | chrome.tabs.onUpdated.addListener(respond); 14 | port.onDisconnect.addListener(function () { 15 | chrome.tabs.onUpdated.removeListener(respond); 16 | }); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /docs/terminology.md: -------------------------------------------------------------------------------- 1 | # Terminology 2 | 3 | **Injector** - a container, capable of instantiating objects. 4 | 5 | **Provider** - a recipe for constructing an object, typically a class or a factory function. A provider is typically annotated with Tokens, which tells Injector what dependencies particular provider needs. 6 | 7 | **Token** - a contract, an identifier of a dependency. Typically a class or a string. 8 | 9 | 10 | ## An example 11 | 12 | ```js 13 | @Provide(Heater) 14 | class MockHeater { 15 | // ... 16 | } 17 | 18 | @Provide(Heater) 19 | function createElectricHeater() { 20 | // ... 21 | } 22 | ``` 23 | 24 | - Heater class is a token. 25 | - MockHeater is a provider. 26 | - createElectricHeater function is a provider. 27 | -------------------------------------------------------------------------------- /test/fixtures/car.js: -------------------------------------------------------------------------------- 1 | import {annotate, Inject, Provide} from '../../src/annotations'; 2 | 3 | export class Engine {} 4 | 5 | export class Car { 6 | constructor(engine) { 7 | this.engine = engine; 8 | } 9 | 10 | start() {} 11 | } 12 | 13 | export function createEngine() { 14 | return 'strong engine'; 15 | } 16 | 17 | export class CyclicEngine { 18 | constructor(car) {} 19 | } 20 | 21 | // This is an example of using annotate helper, instead of annotations. 22 | 23 | // @Inject(Engine) 24 | annotate(Car, new Inject(Engine)); 25 | 26 | // @Provide(Engine) 27 | annotate(createEngine, new Provide(Engine)); 28 | 29 | // @Inject(Car) 30 | annotate(CyclicEngine, new Inject(Car)); 31 | // @Provide(Engine) 32 | annotate(CyclicEngine, new Provide(Engine)); 33 | -------------------------------------------------------------------------------- /example/kitchen-di/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Kitchen example 4 | 5 | 6 | Kitchen example, transpiled on the server into RequireJS. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /example/kitchen/kitchen.js: -------------------------------------------------------------------------------- 1 | 2 | export class Kitchen { 3 | constructor(coffeeMaker, skillet, stove, fridge, dishwasher) { 4 | this.coffeeMaker = coffeeMaker; 5 | this.skillet = skillet; 6 | this.stove = stove; 7 | this.fridge = fridge; 8 | this.dishwasher = dishwasher; 9 | } 10 | 11 | makeScrambledEggs() { 12 | console.log('Making some eggs...'); 13 | this.skillet.add(this.fridge.getEggs()); 14 | this.stove.add(this.skillet); 15 | this.stove.on(); 16 | this.stove.off(); 17 | console.log('Scrambled eggs are ready.'); 18 | } 19 | 20 | makeBreakfast() { 21 | // make a cofee 22 | this.coffeeMaker.brew(); 23 | 24 | // make some eggs 25 | this.makeScrambledEggs(); 26 | 27 | // clean the dishes 28 | this.dishwasher.add(this.skillet); 29 | this.dishwasher.wash(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /chrome-extension/panel.js: -------------------------------------------------------------------------------- 1 | function evalInInspectedWindow(fn, args, cb) { 2 | var code = '(' + fn.toString() + ')(window, ' + JSON.stringify(args) + ')'; 3 | chrome.devtools.inspectedWindow.eval(code, cb); 4 | } 5 | 6 | app.factory('data', function($q) { 7 | var d = $q.defer(); 8 | 9 | evalInInspectedWindow(function(window) { 10 | var injectors = window.__di_dump__.injectors; 11 | var tokens = Object.create(null); 12 | 13 | // Serialize the map so that we can pass it back to the console panel. 14 | window.__di_dump__.tokens.forEach(function(id, token) { 15 | tokens[id] = token.name; 16 | }); 17 | 18 | return { 19 | injectors: injectors, 20 | tokens: tokens 21 | }; 22 | }, [], function(dump) { 23 | d.resolve(dump); 24 | }); 25 | 26 | d.promise.success = d.promise.then; 27 | return d.promise; 28 | }); 29 | -------------------------------------------------------------------------------- /visualize/injectors/coffee_maker.json: -------------------------------------------------------------------------------- 1 | { 2 | "injectors": [{ 3 | "id": 1, 4 | "parent_id": null, 5 | "providers": { 6 | "CoffeeMaker": { 7 | "name": "CoffeeMaker", 8 | "dependencies": [ 9 | "Grinder", 10 | "Pump", 11 | "Heater" 12 | ] 13 | }, 14 | "Grinder": { 15 | "name": "Grinder", 16 | "dependencies": [ 17 | "Electricity" 18 | ] 19 | }, 20 | "Electricity": { 21 | "name": "Electricity", 22 | "dependencies": [] 23 | }, 24 | "Pump": { 25 | "name": "Pump", 26 | "dependencies": [ 27 | "Heater", 28 | "Electricity" 29 | ] 30 | }, 31 | "Heater": { 32 | "name": "Heater", 33 | "dependencies": [ 34 | "Electricity" 35 | ] 36 | } 37 | } 38 | }] 39 | } 40 | -------------------------------------------------------------------------------- /visualize/injectors/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "injectors": [ 3 | { 4 | "id": "1", 5 | "parent_id": null, 6 | "providers": { 7 | "CoffeMaker": {"name": "CoffeMaker", "dependencies": ["Heater", "Pump"]}, 8 | "Heater": {"name": "Heater", "dependencies": []}, 9 | "Pump": {"name": "Pump", "dependencies": ["Heater"]}, 10 | "Extra": {"name": "Extra", "dependencies": ["Pump"]} 11 | } 12 | }, 13 | { 14 | "id": "2", 15 | "parent_id": "1", 16 | "providers": { 17 | "Heater": {"name": "Heater", "dependencies": []}, 18 | "Extra": {"name": "Extra", "dependencies": []} 19 | } 20 | }, 21 | { 22 | "id": "3", 23 | "parent_id": "1", 24 | "providers": { 25 | "Heater": {"name": "Heater", "dependencies": ["Pump", "Extra"]}, 26 | "Pump": {"name": "Pump", "dependencies": []} 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var sharedConfig = require('pipe/karma'); 2 | 3 | module.exports = function(config) { 4 | sharedConfig(config); 5 | 6 | config.set({ 7 | // list of files / patterns to load in the browser 8 | files: [ 9 | 'test-main.js', 10 | 11 | {pattern: 'src/**/*.js', included: false}, 12 | {pattern: 'test/**/*.js', included: false}, 13 | {pattern: 'example/coffee/*.js', included: false}, 14 | {pattern: 'example/testing/*.js', included: false}, 15 | {pattern: 'node_modules/es6-shim/es6-shim.js', included: false}, 16 | {pattern: 'node_modules/rtts-assert/src/**/*.js', included: false} 17 | ], 18 | 19 | preprocessors: { 20 | 'src/**/*.js': ['traceur'], 21 | 'test/**/*.js': ['traceur'], 22 | 'example/**/*.js': ['traceur'], 23 | 'node_modules/rtts-assert/src/**/*.js': ['traceur'] 24 | } 25 | }); 26 | 27 | config.sauceLabs.testName = 'di.js'; 28 | }; 29 | -------------------------------------------------------------------------------- /docs/how-can-i-use-it.md: -------------------------------------------------------------------------------- 1 | # How can I use it? 2 | 3 | ## In the browser 4 | See the [kitchen-di example](../example/kitchen-di/index.html) on how to use the DI with [RequireJS]. 5 | 6 | You will need: 7 | - a browser that supports `Map`, or include a polyfill, 8 | - include the [Traceur runtime]. 9 | 10 | ## In Node.js 11 | ```bash 12 | npm install di --tag v2 13 | ``` 14 | 15 | See the [node example] on how to use the DI in regular ES5. 16 | 17 | ## Using ES6 18 | If you decide to write your project in ES6, you can consume the ES6 source code in [`src/`] and compile it with [Traceur] on your own, similar to the [kitchen-di example]. 19 | 20 | 21 | [kitchen-di example]: ../example/kitchen-di 22 | [RequireJS]: http://requirejs.org/ 23 | [Traceur runtime]: https://github.com/google/traceur-compiler/blob/master/src/runtime/runtime.js 24 | [node example]: ../example/node 25 | [`src/`]: ../src 26 | [Traceur]: https://github.com/google/traceur-compiler 27 | -------------------------------------------------------------------------------- /test-main.js: -------------------------------------------------------------------------------- 1 | var allTestFiles = []; 2 | var TEST_REGEXP = /\.spec\.js$/; 3 | 4 | var pathToModule = function(path) { 5 | return path.replace(/^\/base\//, '').replace(/\.js$/, ''); 6 | }; 7 | 8 | Object.keys(window.__karma__.files).forEach(function(file) { 9 | if (TEST_REGEXP.test(file)) { 10 | // Normalize paths to RequireJS module names. 11 | allTestFiles.push(pathToModule(file)); 12 | } 13 | }); 14 | 15 | require.config({ 16 | // Karma serves files under /base, which is the basePath from your config file 17 | baseUrl: '/base', 18 | 19 | paths: { 20 | 'rtts-assert': './node_modules/rtts-assert/src/assert' 21 | }, 22 | 23 | map: { 24 | '*': { 25 | 'di': 'src/index' 26 | } 27 | }, 28 | 29 | // Dynamically load all test files and ES6 polyfill. 30 | deps: allTestFiles.concat(['node_modules/es6-shim/es6-shim', 'test/matchers']), 31 | 32 | // we have to kickoff jasmine, as it is asynchronous 33 | callback: window.__karma__.start 34 | }); 35 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | // A bunch of helper functions. 2 | 3 | function isUpperCase(char) { 4 | return char.toUpperCase() === char; 5 | } 6 | 7 | function isFunction(value) { 8 | return typeof value === 'function'; 9 | } 10 | 11 | 12 | function isObject(value) { 13 | return typeof value === 'object'; 14 | } 15 | 16 | 17 | function toString(token) { 18 | if (typeof token === 'string') { 19 | return token; 20 | } 21 | 22 | if (token === undefined || token === null) { 23 | return '' + token; 24 | } 25 | 26 | if (token.name) { 27 | return token.name; 28 | } 29 | 30 | return token.toString(); 31 | } 32 | 33 | var ownKeys = (this.Reflect && Reflect.ownKeys ? Reflect.ownKeys : function ownKeys(O) { 34 | var keys = Object.getOwnPropertyNames(O); 35 | if (Object.getOwnPropertySymbols) return keys.concat(Object.getOwnPropertySymbols(O)); 36 | return keys; 37 | }); 38 | 39 | 40 | export { 41 | isUpperCase, 42 | isFunction, 43 | isObject, 44 | toString, 45 | ownKeys 46 | }; 47 | -------------------------------------------------------------------------------- /visualize/injectors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Visualization of injectors 5 | 6 | 7 | 8 | {{message}} 9 | 10 |
11 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/kitchen/main.js: -------------------------------------------------------------------------------- 1 | import {Kitchen} from './kitchen'; 2 | import {CoffeeMaker} from './coffee_maker'; 3 | import {Grinder} from './grinder'; 4 | import {Electricity} from './electricity'; 5 | import {Pump} from './pump'; 6 | import {Heater} from './heater'; 7 | import {Skillet} from './skillet'; 8 | import {Stove} from './stove'; 9 | import {Fridge} from './fridge'; 10 | import {Dishwasher} from './dishwasher'; 11 | 12 | 13 | function main() { 14 | var electricity = new Electricity(); 15 | var skillet = new Skillet(); 16 | var stove = new Stove(electricity); 17 | var fridge = new Fridge(electricity); 18 | var dishwasher = new Dishwasher(electricity); 19 | 20 | // assemble the CoffeeMaker 21 | var grinder = new Grinder(electricity); 22 | var heater = new Heater(electricity); 23 | var pump = new Pump(heater, electricity); 24 | var coffeeMaker = new CoffeeMaker(grinder, pump, heater); 25 | 26 | // assemble the Kitchen 27 | var kitchen = new Kitchen(coffeeMaker, skillet, stove, fridge, dishwasher); 28 | 29 | // finally, make the breakfast 30 | kitchen.makeBreakfast(); 31 | } 32 | 33 | main(); 34 | -------------------------------------------------------------------------------- /dist/amd/index.js: -------------------------------------------------------------------------------- 1 | define(['./injector', './annotations'], function($__0,$__1) { 2 | "use strict"; 3 | if (!$__0 || !$__0.__esModule) 4 | $__0 = {default: $__0}; 5 | if (!$__1 || !$__1.__esModule) 6 | $__1 = {default: $__1}; 7 | var $__injector__ = $__0; 8 | var $__annotations__ = $__1; 9 | return { 10 | get Injector() { 11 | return $__injector__.Injector; 12 | }, 13 | get annotate() { 14 | return $__annotations__.annotate; 15 | }, 16 | get Inject() { 17 | return $__annotations__.Inject; 18 | }, 19 | get InjectLazy() { 20 | return $__annotations__.InjectLazy; 21 | }, 22 | get InjectPromise() { 23 | return $__annotations__.InjectPromise; 24 | }, 25 | get Provide() { 26 | return $__annotations__.Provide; 27 | }, 28 | get ProvidePromise() { 29 | return $__annotations__.ProvidePromise; 30 | }, 31 | get SuperConstructor() { 32 | return $__annotations__.SuperConstructor; 33 | }, 34 | get TransientScope() { 35 | return $__annotations__.TransientScope; 36 | }, 37 | __esModule: true 38 | }; 39 | }); 40 | -------------------------------------------------------------------------------- /example/kitchen-di/kitchen.js: -------------------------------------------------------------------------------- 1 | import {Inject} from 'di'; 2 | import {CoffeeMaker} from './coffee_maker/coffee_maker'; 3 | import {Skillet} from './skillet'; 4 | import {Stove} from './stove'; 5 | import {Fridge} from './fridge'; 6 | import {Dishwasher} from './dishwasher'; 7 | 8 | @Inject(CoffeeMaker, Skillet, Stove, Fridge, Dishwasher) 9 | export class Kitchen { 10 | constructor(coffeeMaker, skillet, stove, fridge, dishwasher) { 11 | this.coffeeMaker = coffeeMaker; 12 | this.skillet = skillet; 13 | this.stove = stove; 14 | this.fridge = fridge; 15 | this.dishwasher = dishwasher; 16 | } 17 | 18 | makeScrambledEggs() { 19 | console.log('Making some eggs...'); 20 | this.skillet.add(this.fridge.getEggs()); 21 | this.stove.add(this.skillet); 22 | this.stove.on(); 23 | this.stove.off(); 24 | console.log('Scrambled eggs are ready.'); 25 | } 26 | 27 | makeBreakfast() { 28 | // make a cofee 29 | this.coffeeMaker.brew(); 30 | 31 | // make some eggs 32 | this.makeScrambledEggs(); 33 | 34 | // clean the dishes 35 | this.dishwasher.add(this.skillet); 36 | this.dishwasher.wash(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /chrome-extension/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Visualization of injectors 5 | 6 | 7 | 8 | {{message}} 9 | 10 |
11 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /dist/cjs/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperties(exports, { 3 | isUpperCase: {get: function() { 4 | return isUpperCase; 5 | }}, 6 | isClass: {get: function() { 7 | return isClass; 8 | }}, 9 | isFunction: {get: function() { 10 | return isFunction; 11 | }}, 12 | isObject: {get: function() { 13 | return isObject; 14 | }}, 15 | toString: {get: function() { 16 | return toString; 17 | }}, 18 | __esModule: {value: true} 19 | }); 20 | function isUpperCase(char) { 21 | return char.toUpperCase() === char; 22 | } 23 | function isClass(clsOrFunction) { 24 | if (clsOrFunction.name) { 25 | return isUpperCase(clsOrFunction.name.charAt(0)); 26 | } 27 | return Object.keys(clsOrFunction.prototype).length > 0; 28 | } 29 | function isFunction(value) { 30 | return typeof value === 'function'; 31 | } 32 | function isObject(value) { 33 | return typeof value === 'object'; 34 | } 35 | function toString(token) { 36 | if (typeof token === 'string') { 37 | return token; 38 | } 39 | if (token === undefined || token === null) { 40 | return '' + token; 41 | } 42 | if (token.name) { 43 | return token.name; 44 | } 45 | return token.toString(); 46 | } 47 | ; 48 | -------------------------------------------------------------------------------- /dist/amd/util.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | "use strict"; 3 | function isUpperCase(char) { 4 | return char.toUpperCase() === char; 5 | } 6 | function isClass(clsOrFunction) { 7 | if (clsOrFunction.name) { 8 | return isUpperCase(clsOrFunction.name.charAt(0)); 9 | } 10 | return Object.keys(clsOrFunction.prototype).length > 0; 11 | } 12 | function isFunction(value) { 13 | return typeof value === 'function'; 14 | } 15 | function isObject(value) { 16 | return typeof value === 'object'; 17 | } 18 | function toString(token) { 19 | if (typeof token === 'string') { 20 | return token; 21 | } 22 | if (token === undefined || token === null) { 23 | return '' + token; 24 | } 25 | if (token.name) { 26 | return token.name; 27 | } 28 | return token.toString(); 29 | } 30 | ; 31 | return { 32 | get isUpperCase() { 33 | return isUpperCase; 34 | }, 35 | get isClass() { 36 | return isClass; 37 | }, 38 | get isFunction() { 39 | return isFunction; 40 | }, 41 | get isObject() { 42 | return isObject; 43 | }, 44 | get toString() { 45 | return toString; 46 | }, 47 | __esModule: true 48 | }; 49 | }); 50 | -------------------------------------------------------------------------------- /dist/cjs/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperties(exports, { 3 | Injector: {get: function() { 4 | return $__injector__.Injector; 5 | }}, 6 | annotate: {get: function() { 7 | return $__annotations__.annotate; 8 | }}, 9 | Inject: {get: function() { 10 | return $__annotations__.Inject; 11 | }}, 12 | InjectLazy: {get: function() { 13 | return $__annotations__.InjectLazy; 14 | }}, 15 | InjectPromise: {get: function() { 16 | return $__annotations__.InjectPromise; 17 | }}, 18 | Provide: {get: function() { 19 | return $__annotations__.Provide; 20 | }}, 21 | ProvidePromise: {get: function() { 22 | return $__annotations__.ProvidePromise; 23 | }}, 24 | SuperConstructor: {get: function() { 25 | return $__annotations__.SuperConstructor; 26 | }}, 27 | TransientScope: {get: function() { 28 | return $__annotations__.TransientScope; 29 | }}, 30 | __esModule: {value: true} 31 | }); 32 | var $__injector__, 33 | $__annotations__; 34 | var $__injector__ = ($__injector__ = require("./injector"), $__injector__ && $__injector__.__esModule && $__injector__ || {default: $__injector__}); 35 | var $__annotations__ = ($__annotations__ = require("./annotations"), $__annotations__ && $__annotations__.__esModule && $__annotations__ || {default: $__annotations__}); 36 | -------------------------------------------------------------------------------- /test/matchers.js: -------------------------------------------------------------------------------- 1 | beforeEach(function() { 2 | jasmine.addMatchers({ 3 | toBeInstanceOf: function() { 4 | return { 5 | compare: function(actual, expectedClass) { 6 | var pass = typeof actual === 'object' && actual instanceof expectedClass; 7 | 8 | return { 9 | pass: pass, 10 | get message() { 11 | if (pass) { 12 | // TODO(vojta): support not.toBeInstanceOf 13 | throw new Error('not.toBeInstanceOf is not supported!'); 14 | } 15 | 16 | return 'Expected ' + actual + ' to be an instance of ' + expectedClass; 17 | } 18 | }; 19 | } 20 | }; 21 | }, 22 | 23 | toBePromiseLike: function() { 24 | return { 25 | compare: function(actual, expectedClass) { 26 | var pass = typeof actual === 'object' && typeof actual.then === 'function'; 27 | 28 | return { 29 | pass: pass, 30 | get message() { 31 | if (pass) { 32 | // TODO(vojta): support not.toBePromiseLike 33 | throw new Error('not.toBePromiseLike is not supported!'); 34 | } 35 | 36 | return 'Expected ' + actual + ' to be a promise'; 37 | } 38 | }; 39 | } 40 | }; 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "Copyright 2017, Dell EMC, Inc.", 3 | "name": "di", 4 | "version": "2.33.0", 5 | "description": "A DI framework.", 6 | "main": "node/index.js", 7 | "homepage": "https://github.com/angular/di.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/angular/di.js.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/angular/di.js/issues" 14 | }, 15 | "dependencies": { 16 | "traceur": "0.0.111", 17 | "es6-shim": "~0.11.0" 18 | }, 19 | "devDependencies": { 20 | "rtts-assert": "angular/assert", 21 | "gulp": "^3.5.6", 22 | "gulp-connect": "~1.0.5", 23 | "gulp-traceur": "vojtajina/gulp-traceur#traceur-as-peer", 24 | "gulp-git": "vojtajina/gulp-git#hacked", 25 | "karma": "^0.12.1", 26 | "karma-script-launcher": "~0.1.0", 27 | "karma-chrome-launcher": "~0.1.2", 28 | "karma-firefox-launcher": "~0.1.3", 29 | "karma-phantomjs-launcher": "~0.1.2", 30 | "karma-sauce-launcher": "~0.2.4", 31 | "karma-jasmine": "^0.2.2", 32 | "karma-requirejs": "^0.2.1", 33 | "karma-traceur-preprocessor": "vojtajina/karma-traceur-preprocessor#traceur-as-peer", 34 | "pipe": "angular/pipe#remove-transitive-deps", 35 | "requirejs": "2.1.10", 36 | "through2": "~0.4.1" 37 | }, 38 | "scripts": { 39 | "test": "karma start --single-run" 40 | }, 41 | "author": "Vojta Jína ", 42 | "license": "Apache-2.0" 43 | } 44 | -------------------------------------------------------------------------------- /visualize/injectors/kitchen.json: -------------------------------------------------------------------------------- 1 | { 2 | "injectors": [{ 3 | "id": 1, 4 | "parent_id": null, 5 | "providers": { 6 | "Kitchen": { 7 | "name": "Kitchen", 8 | "dependencies": [ 9 | "CoffeeMaker", 10 | "Skillet", 11 | "Stove", 12 | "Fridge", 13 | "Dishwasher" 14 | ] 15 | }, 16 | "CoffeeMaker": { 17 | "name": "CoffeeMaker", 18 | "dependencies": [ 19 | "Grinder", 20 | "Pump", 21 | "Heater" 22 | ] 23 | }, 24 | "Grinder": { 25 | "name": "Grinder", 26 | "dependencies": [ 27 | "Electricity" 28 | ] 29 | }, 30 | "Electricity": { 31 | "name": "Electricity", 32 | "dependencies": [] 33 | }, 34 | "Pump": { 35 | "name": "Pump", 36 | "dependencies": [ 37 | "Heater", 38 | "Electricity" 39 | ] 40 | }, 41 | "Heater": { 42 | "name": "Heater", 43 | "dependencies": [ 44 | "Electricity" 45 | ] 46 | }, 47 | "Skillet": { 48 | "name": "Skillet", 49 | "dependencies": [] 50 | }, 51 | "Stove": { 52 | "name": "Stove", 53 | "dependencies": [ 54 | "Electricity" 55 | ] 56 | }, 57 | "Fridge": { 58 | "name": "Fridge", 59 | "dependencies": [ 60 | "Electricity" 61 | ] 62 | }, 63 | "Dishwasher": { 64 | "name": "Dishwasher", 65 | "dependencies": [ 66 | "Electricity" 67 | ] 68 | } 69 | } 70 | }] 71 | } 72 | -------------------------------------------------------------------------------- /example/testing/coffee.spec.js: -------------------------------------------------------------------------------- 1 | // TODO(vojta): add an API for exporting these APIs to global namespace. 2 | import {use, inject} from '../../src/testing'; 3 | 4 | import {CoffeeMaker} from '../coffee/coffee_maker'; 5 | import {Heater} from '../coffee/heater'; 6 | import {Pump} from '../coffee/pump'; 7 | import {MockHeater, DummyHeater} from './mocks'; 8 | 9 | 10 | describe('simple mocking in beforeEach', function() { 11 | // Use DummyHeater, which has @Provide(Heater) annotation. 12 | beforeEach(use(DummyHeater)); 13 | 14 | it('should brew', inject(CoffeeMaker, function(cm) { 15 | cm.brew(); 16 | })); 17 | }); 18 | 19 | describe('more mocking in beforeEach', function() { 20 | 21 | beforeEach(function() { 22 | // Use MockHeater, instead of Heater. 23 | use(MockHeater).as(Heater); 24 | 25 | // Inlined mock - if it's not a function/class, the actual value is used. 26 | use({ 27 | pump: jasmine.createSpy('pump') 28 | }).as(Pump); 29 | }); 30 | 31 | it('should brew', inject(CoffeeMaker, Heater, Pump, function(cm, heater, pump) { 32 | cm.brew(); 33 | 34 | expect(heater.on).toHaveBeenCalled(); 35 | expect(pump.pump).toHaveBeenCalled(); 36 | })); 37 | }); 38 | 39 | describe('mocking inside it', function() { 40 | it('should brew', function() { 41 | // Both use().as() and inject() can be also used inside it(). 42 | use(MockHeater).as(Heater); 43 | inject(CoffeeMaker, function(cm, heater) { 44 | cm.brew(); 45 | }); 46 | 47 | // There can be multiple use() or inject() calls. 48 | // However, you can't use use() after you already called inject(). 49 | inject(Heater, function(heater) { 50 | expect(heater.on).toHaveBeenCalled(); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /visualize/injectors/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Playground 4 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /docs/interfaces.md: -------------------------------------------------------------------------------- 1 | # Using "interfaces" 2 | 3 | This DI framework uses annotations to figure out what dependencies particular provider needs. You can use *anything* as a token (Angular v1.x uses strings). The recomended way is to use references to the actual dependencies, as shown in the [example]. That has a couple benefits: 4 | - the framework can use these classes as default providers, 5 | - no conflicts, 6 | - easy minification. 7 | 8 | This works very well until you override providers. If you override a provider, the original class is still imported (because it is used as the token). All the transitive dependencies are imported too. In the [example], `MockHeater` is used but the real `Heater` is still imported, even though it is not used. 9 | 10 | For small objects, this is not a problem. **For a bigger object you wanna use an interface**, which is an empty class without any dependencies. 11 | 12 | You can easily refactor an existing provider into an "interface" once it gets too big. Here is an example - let's say `Heater` is too big (or has many dependencies), we don't wanna import it when using `MockHeater`. 13 | 14 | ```js 15 | // heater.js 16 | // This is the interface which is used as the token and therefore always imported. 17 | class Heater {} 18 | 19 | // electric_heater.js 20 | // This is the real implementation (with many dependencies and tons of code), only imported when used. 21 | import {Heater} from './heater'; 22 | 23 | @Provide(Heater) 24 | class ElectricHeater { 25 | // ... 26 | } 27 | 28 | // mock_heater.js 29 | // This is the mock version. 30 | import {Heater} from './heater'; 31 | 32 | @Provide(Heater) 33 | class MockHeater { 34 | // ... 35 | } 36 | ``` 37 | 38 | Now, only the interface (`Heater` - an empty class) is always imported. 39 | 40 | [example]: ./example/kitchen-di 41 | -------------------------------------------------------------------------------- /visualize/injectors/style.css: -------------------------------------------------------------------------------- 1 | [ng-cloak] { 2 | display: none; 3 | } 4 | 5 | body { 6 | font-family: monospace; 7 | } 8 | 9 | label { 10 | cursor: pointer; 11 | } 12 | 13 | 14 | /* circles */ 15 | 16 | .node { 17 | cursor: pointer; 18 | } 19 | 20 | .fake-root { 21 | stroke: 0; 22 | fill: none; 23 | } 24 | 25 | .injector circle { 26 | fill: #0a629f; 27 | fill-opacity: .2; 28 | stroke: #0a629f; 29 | } 30 | 31 | .provider circle { 32 | fill: #fff; 33 | fill-opacity: .8; 34 | stroke-width: 1px; 35 | stroke: #000; 36 | } 37 | 38 | .injector.highlighted circle { 39 | stroke-width: 4px; 40 | } 41 | 42 | .provider.highlighted circle { 43 | stroke-width: 2px; 44 | stroke: #000; 45 | fill: #ff6; 46 | } 47 | 48 | 49 | /* links */ 50 | 51 | .link { 52 | fill: none; 53 | stroke-linecap: round; 54 | 55 | } 56 | 57 | .link.dependency { 58 | display: none; 59 | stroke-width: 2px; 60 | } 61 | 62 | .link.dependency.highlighted { 63 | display: inline; 64 | } 65 | 66 | .link.dependency.in { 67 | stroke: green; 68 | } 69 | 70 | .link.dependency.out { 71 | stroke: red; 72 | } 73 | 74 | .link.override { 75 | stroke: #000; 76 | stroke-width: 1px; 77 | stroke-dasharray: 10px; 78 | } 79 | 80 | .link.override.highlighted { 81 | } 82 | 83 | marker#dependency-in { 84 | fill: green; 85 | } 86 | 87 | marker#dependency-out { 88 | fill: red; 89 | } 90 | 91 | /* list of providers */ 92 | .control-panel { 93 | float: right; 94 | } 95 | 96 | .control-panel button { 97 | width: 144px; 98 | } 99 | 100 | .providers { 101 | width: 140px; 102 | margin: 0; 103 | padding: 0; 104 | list-style: none; 105 | border: 2px solid #0a629f; 106 | } 107 | 108 | .providers li { 109 | cursor: pointer; 110 | padding: 1px 6px 1px 6px; 111 | } 112 | 113 | .providers li.highlighted { 114 | background: #ff6; 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/angular/di.js.png?branch=master)](https://travis-ci.org/angular/di.js) 2 | 3 | ## Dependency Injection v2 4 | 5 | This readme describes how to set up your working space in order to run the tests and hack on it. See [How can I use it](./docs/how-can-i-use-it.md) on how to use this DI framework in your project. 6 | 7 | ### Installation 8 | ```bash 9 | # Clone this repo (or your fork). 10 | git clone https://github.com/angular/di.js.git 11 | 12 | # Install all the dev dependencies, such as Karma, Gulp, etc. 13 | npm install 14 | 15 | # If you wanna use "karma" or "gulp" commands, install also: 16 | npm install -g karma-cli 17 | npm install -g gulp 18 | ``` 19 | 20 | ### Running the [tests](./test/) 21 | This will start Karma and Chrome (with `--harmony` enabled). Karma will watch the source code and run the tests anytime you save a change. 22 | 23 | ```bash 24 | karma start 25 | ``` 26 | 27 | ### Transpiling ES6 28 | All the source code is written in the upcoming version of JavaScript - ES6. In order to use it in the current browsers you need to transpile the code into ES5 using [Traceur]. 29 | 30 | 31 | ```bash 32 | # Transpile ES6 into ./compiled/* 33 | gulp build 34 | 35 | # Watch all the sources and transpile on any change 36 | gulp watch 37 | ``` 38 | 39 | 40 | ### Examples 41 | ```bash 42 | gulp build_examples 43 | gulp serve 44 | ``` 45 | 46 | 47 | ### More stuff 48 | 49 | I talked about this DI framework at the [ng-conf], here are some more links... 50 | 51 | - [video](http://www.youtube.com/watch?v=_OGGsf1ZXMs) 52 | - [slides](https://dl.dropboxusercontent.com/u/36607830/talks/ng-conf-di-v2.pdf) ([annotated version](https://dl.dropboxusercontent.com/u/36607830/talks/ng-conf-di-v2-annotated.pdf)) 53 | 54 | Also, [here](https://docs.google.com/document/d/1fTR4TcTGbmExa5w2SRNAkM1fsB9kYeOvfuiI99FgR24/edit?usp=sharing) is the original design doc, which is quickly becoming out-dated ;-) 55 | 56 | [Traceur]: https://github.com/google/traceur-compiler 57 | [ng-conf]: http://ng-conf.org/ 58 | -------------------------------------------------------------------------------- /src/profiler.js: -------------------------------------------------------------------------------- 1 | import {toString} from './util'; 2 | 3 | 4 | var IS_DEBUG = false; 5 | var _global = null; 6 | 7 | if (typeof process === 'object' && process.env) { 8 | // Node.js 9 | IS_DEBUG = !!process.env['DEBUG']; 10 | _global = global; 11 | } else if (typeof location === 'object' && location.search) { 12 | // Browser 13 | IS_DEBUG = /di_debug/.test(location.search); 14 | _global = window; 15 | } 16 | 17 | 18 | var globalCounter = 0; 19 | function getUniqueId() { 20 | return ++globalCounter; 21 | } 22 | 23 | 24 | function serializeToken(token, tokens) { 25 | if (!tokens.has(token)) { 26 | tokens.set(token, getUniqueId().toString()); 27 | } 28 | 29 | return tokens.get(token); 30 | } 31 | 32 | function serializeProvider(provider, key, tokens) { 33 | return { 34 | id: serializeToken(key, tokens), 35 | name: toString(key), 36 | isPromise: provider.isPromise, 37 | dependencies: provider.params.map(function(param) { 38 | return { 39 | token: serializeToken(param.token, tokens), 40 | isPromise: param.isPromise, 41 | isLazy: param.isLazy 42 | }; 43 | }) 44 | }; 45 | } 46 | 47 | 48 | function serializeInjector(injector, tokens, Injector) { 49 | var serializedInjector = { 50 | id: serializeToken(injector, tokens), 51 | parent_id: injector._parent ? serializeToken(injector._parent, tokens) : null, 52 | providers: {} 53 | }; 54 | 55 | var injectorClassId = serializeToken(Injector, tokens); 56 | serializedInjector.providers[injectorClassId] = { 57 | id: injectorClassId, 58 | name: toString(Injector), 59 | isPromise: false, 60 | dependencies: [] 61 | }; 62 | 63 | injector._providers.forEach(function(provider, key) { 64 | var serializedProvider = serializeProvider(provider, key, tokens); 65 | serializedInjector.providers[serializedProvider.id] = serializedProvider; 66 | }); 67 | 68 | return serializedInjector; 69 | } 70 | 71 | 72 | export function profileInjector(injector, Injector) { 73 | if (!IS_DEBUG) { 74 | return; 75 | } 76 | 77 | if (!_global.__di_dump__) { 78 | _global.__di_dump__ = { 79 | injectors: [], 80 | tokens: new Map() 81 | }; 82 | } 83 | 84 | _global.__di_dump__.injectors.push(serializeInjector(injector, _global.__di_dump__.tokens, Injector)); 85 | } 86 | -------------------------------------------------------------------------------- /dist/cjs/profiler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperties(exports, { 3 | profileInjector: {get: function() { 4 | return profileInjector; 5 | }}, 6 | __esModule: {value: true} 7 | }); 8 | var $__util__; 9 | var toString = ($__util__ = require("./util"), $__util__ && $__util__.__esModule && $__util__ || {default: $__util__}).toString; 10 | var IS_DEBUG = false; 11 | var _global = null; 12 | if (typeof process === 'object' && process.env) { 13 | IS_DEBUG = !!process.env['DEBUG']; 14 | _global = global; 15 | } else if (typeof location === 'object' && location.search) { 16 | IS_DEBUG = /di_debug/.test(location.search); 17 | _global = window; 18 | } 19 | var globalCounter = 0; 20 | function getUniqueId() { 21 | return ++globalCounter; 22 | } 23 | function serializeToken(token, tokens) { 24 | if (!tokens.has(token)) { 25 | tokens.set(token, getUniqueId().toString()); 26 | } 27 | return tokens.get(token); 28 | } 29 | function serializeProvider(provider, key, tokens) { 30 | return { 31 | id: serializeToken(key, tokens), 32 | name: toString(key), 33 | isPromise: provider.isPromise, 34 | dependencies: provider.params.map(function(param) { 35 | return { 36 | token: serializeToken(param.token, tokens), 37 | isPromise: param.isPromise, 38 | isLazy: param.isLazy 39 | }; 40 | }) 41 | }; 42 | } 43 | function serializeInjector(injector, tokens, Injector) { 44 | var serializedInjector = { 45 | id: serializeToken(injector, tokens), 46 | parent_id: injector._parent ? serializeToken(injector._parent, tokens) : null, 47 | providers: {} 48 | }; 49 | var injectorClassId = serializeToken(Injector, tokens); 50 | serializedInjector.providers[injectorClassId] = { 51 | id: injectorClassId, 52 | name: toString(Injector), 53 | isPromise: false, 54 | dependencies: [] 55 | }; 56 | injector._providers.forEach(function(provider, key) { 57 | var serializedProvider = serializeProvider(provider, key, tokens); 58 | serializedInjector.providers[serializedProvider.id] = serializedProvider; 59 | }); 60 | return serializedInjector; 61 | } 62 | function profileInjector(injector, Injector) { 63 | if (!IS_DEBUG) { 64 | return; 65 | } 66 | if (!_global.__di_dump__) { 67 | _global.__di_dump__ = { 68 | injectors: [], 69 | tokens: new Map() 70 | }; 71 | } 72 | _global.__di_dump__.injectors.push(serializeInjector(injector, _global.__di_dump__.tokens, Injector)); 73 | } 74 | -------------------------------------------------------------------------------- /dist/amd/profiler.js: -------------------------------------------------------------------------------- 1 | define(['./util'], function($__0) { 2 | "use strict"; 3 | if (!$__0 || !$__0.__esModule) 4 | $__0 = {default: $__0}; 5 | var toString = $__0.toString; 6 | var IS_DEBUG = false; 7 | var _global = null; 8 | if (typeof process === 'object' && process.env) { 9 | IS_DEBUG = !!process.env['DEBUG']; 10 | _global = global; 11 | } else if (typeof location === 'object' && location.search) { 12 | IS_DEBUG = /di_debug/.test(location.search); 13 | _global = window; 14 | } 15 | var globalCounter = 0; 16 | function getUniqueId() { 17 | return ++globalCounter; 18 | } 19 | function serializeToken(token, tokens) { 20 | if (!tokens.has(token)) { 21 | tokens.set(token, getUniqueId().toString()); 22 | } 23 | return tokens.get(token); 24 | } 25 | function serializeProvider(provider, key, tokens) { 26 | return { 27 | id: serializeToken(key, tokens), 28 | name: toString(key), 29 | isPromise: provider.isPromise, 30 | dependencies: provider.params.map(function(param) { 31 | return { 32 | token: serializeToken(param.token, tokens), 33 | isPromise: param.isPromise, 34 | isLazy: param.isLazy 35 | }; 36 | }) 37 | }; 38 | } 39 | function serializeInjector(injector, tokens, Injector) { 40 | var serializedInjector = { 41 | id: serializeToken(injector, tokens), 42 | parent_id: injector._parent ? serializeToken(injector._parent, tokens) : null, 43 | providers: {} 44 | }; 45 | var injectorClassId = serializeToken(Injector, tokens); 46 | serializedInjector.providers[injectorClassId] = { 47 | id: injectorClassId, 48 | name: toString(Injector), 49 | isPromise: false, 50 | dependencies: [] 51 | }; 52 | injector._providers.forEach(function(provider, key) { 53 | var serializedProvider = serializeProvider(provider, key, tokens); 54 | serializedInjector.providers[serializedProvider.id] = serializedProvider; 55 | }); 56 | return serializedInjector; 57 | } 58 | function profileInjector(injector, Injector) { 59 | if (!IS_DEBUG) { 60 | return; 61 | } 62 | if (!_global.__di_dump__) { 63 | _global.__di_dump__ = { 64 | injectors: [], 65 | tokens: new Map() 66 | }; 67 | } 68 | _global.__di_dump__.injectors.push(serializeInjector(injector, _global.__di_dump__.tokens, Injector)); 69 | } 70 | return { 71 | get profileInjector() { 72 | return profileInjector; 73 | }, 74 | __esModule: true 75 | }; 76 | }); 77 | -------------------------------------------------------------------------------- /visualize/dag/dag.js: -------------------------------------------------------------------------------- 1 | function initDAG(links) { 2 | var nodes = {}; 3 | 4 | // Compute the distinct nodes from the links. 5 | links.forEach(function(link) { 6 | link.source = nodes[link.source] || (nodes[link.source] = {name: link.source}); 7 | link.target = nodes[link.target] || (nodes[link.target] = {name: link.target}); 8 | }); 9 | 10 | var width = 960, 11 | height = 500; 12 | 13 | var force = d3.layout.force() 14 | .nodes(d3.values(nodes)) 15 | .links(links) 16 | .size([width, height]) 17 | .linkDistance(60) 18 | .charge(-300) 19 | .on("tick", tick) 20 | .start(); 21 | 22 | var svg = d3.select("body").append("svg") 23 | .attr("width", width) 24 | .attr("height", height); 25 | 26 | // Per-type markers, as they don't inherit styles. 27 | svg.append("defs").selectAll("marker") 28 | .data(["suit", "licensing", "resolved"]) 29 | .enter().append("marker") 30 | .attr("id", function(d) { return d; }) 31 | .attr("viewBox", "0 -5 10 10") 32 | .attr("refX", 15) 33 | .attr("refY", -1.5) 34 | .attr("markerWidth", 6) 35 | .attr("markerHeight", 6) 36 | .attr("orient", "auto") 37 | .append("path") 38 | .attr("d", "M0,-5L10,0L0,5"); 39 | 40 | var path = svg.append("g").selectAll("path") 41 | .data(force.links()) 42 | .enter().append("path") 43 | .attr("class", function(d) { return "link " + (d.type || "resolved"); }) 44 | .attr("marker-end", function(d) { return "url(#" + (d.type || "resolved") + ")"; }); 45 | 46 | var circle = svg.append("g").selectAll("circle") 47 | .data(force.nodes()) 48 | .enter().append("circle") 49 | .attr("r", 6) 50 | .call(force.drag); 51 | 52 | var text = svg.append("g").selectAll("text") 53 | .data(force.nodes()) 54 | .enter().append("text") 55 | .attr("x", 8) 56 | .attr("y", "0.31em") 57 | .text(function(d) { return d.name; }); 58 | 59 | // Use elliptical arc path segments to doubly-encode directionality. 60 | function tick() { 61 | path.attr("d", linkArc); 62 | circle.attr("transform", transform); 63 | text.attr("transform", transform); 64 | } 65 | 66 | function linkArc(d) { 67 | var dx = d.target.x - d.source.x, 68 | dy = d.target.y - d.source.y, 69 | dr = Math.sqrt(dx * dx + dy * dy); 70 | return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; 71 | } 72 | 73 | function transform(d) { 74 | return "translate(" + d.x + "," + d.y + ")"; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/testing.js: -------------------------------------------------------------------------------- 1 | import {Injector} from './injector'; 2 | import {Inject, annotate, readAnnotations} from './annotations'; 3 | import {isFunction} from './util'; 4 | import {createProviderFromFnOrClass} from './providers'; 5 | 6 | 7 | var currentSpec = null; 8 | beforeEach(function() { 9 | currentSpec = this; 10 | currentSpec.$$providers = []; 11 | }); 12 | 13 | afterEach(function() { 14 | currentSpec.$$providers = null; 15 | currentSpec.$$injector = null; 16 | currentSpec = null; 17 | }); 18 | 19 | function isRunning() { 20 | return !!currentSpec; 21 | } 22 | 23 | function use(mock) { 24 | if (currentSpec && currentSpec.$$injector) { 25 | throw new Error('Cannot call use() after inject() has already been called.'); 26 | } 27 | 28 | var providerWrapper = { 29 | provider: mock 30 | }; 31 | 32 | var fn = function() { 33 | currentSpec.$$providers.push(providerWrapper); 34 | }; 35 | 36 | fn.as = function(token) { 37 | if (currentSpec && currentSpec.$$injector) { 38 | throw new Error('Cannot call as() after inject() has already been called.'); 39 | } 40 | 41 | providerWrapper.as = token; 42 | if (isRunning()) { 43 | return undefined; 44 | } 45 | 46 | return fn; 47 | }; 48 | 49 | if (isRunning()) { 50 | fn(); 51 | } 52 | 53 | return fn; 54 | } 55 | 56 | function inject(...params) { 57 | var behavior = params.pop(); 58 | 59 | annotate(behavior, new Inject(...params)); 60 | 61 | var run = function() { 62 | if (!currentSpec.$$injector) { 63 | var providers = new Map(); 64 | var modules = []; 65 | var annotations; 66 | 67 | currentSpec.$$providers.forEach(function(providerWrapper) { 68 | if (!providerWrapper.as) { 69 | // load as a regular module 70 | modules.push(providerWrapper.provider); 71 | } else { 72 | if (!isFunction(providerWrapper.provider)) { 73 | // inlined mock 74 | providers.set(providerWrapper.as, createProviderFromFnOrClass(function() { 75 | return providerWrapper.provider; 76 | }, {provide: {token: null, isPromise: false}, params: []})); 77 | } else { 78 | // a fn/class provider with overridden token 79 | annotations = readAnnotations(providerWrapper.provider); 80 | providers.set(providerWrapper.as, createProviderFromFnOrClass(providerWrapper.provider, annotations)); 81 | } 82 | } 83 | }); 84 | 85 | currentSpec.$$injector = new Injector(modules, null, providers); 86 | } 87 | 88 | currentSpec.$$injector.get(behavior); 89 | }; 90 | 91 | return isRunning() ? run() : run; 92 | } 93 | 94 | export { 95 | use, inject 96 | }; 97 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var pipe = require('pipe/gulp'); 3 | var traceur = require('gulp-traceur'); 4 | var connect = require('gulp-connect'); 5 | 6 | 7 | var path = { 8 | src: './src/**/*.js', 9 | // we have to skip example/node (because of the cyclic symlink) 10 | examples: './example/!(node)/**/*.js', 11 | pkg: './package.json' 12 | }; 13 | 14 | 15 | // TRANSPILE ES6 16 | gulp.task('build_source_amd', function() { 17 | gulp.src(path.src) 18 | .pipe(traceur(pipe.traceur())) 19 | .pipe(gulp.dest('dist/amd')); 20 | }); 21 | 22 | gulp.task('build_source_cjs', function() { 23 | gulp.src(path.src) 24 | .pipe(traceur(pipe.traceur({modules: 'commonjs'}))) 25 | .pipe(gulp.dest('dist/cjs')); 26 | }); 27 | 28 | gulp.task('build_examples', function() { 29 | gulp.src(path.examples) 30 | .pipe(traceur(pipe.traceur())) 31 | .pipe(gulp.dest('compiled/example')); 32 | }); 33 | 34 | gulp.task('build_dist', ['build_source_cjs', 'build_source_amd']); 35 | gulp.task('build', ['build_dist', 'build_examples']); 36 | 37 | 38 | // WATCH FILES FOR CHANGES 39 | gulp.task('watch', function() { 40 | gulp.watch(path.src, ['build']); 41 | }); 42 | 43 | 44 | // WEB SERVER 45 | gulp.task('serve', connect.server({ 46 | root: [__dirname], 47 | port: 8000, 48 | open: { 49 | browser: 'Google Chrome' 50 | } 51 | })); 52 | 53 | 54 | 55 | 56 | // This is a super nasty, hacked release task. 57 | // TODO(vojta): fix gulp-git and clean this up 58 | var git = require('gulp-git'); 59 | var through = require('through2'); 60 | var exec = require('child_process').exec; 61 | 62 | var VERSION_REGEXP = /([\'|\"]?version[\'|\"]?[ ]*:[ ]*[\'|\"]?)([\d||A-a|.|-]*)([\'|\"]?)/i; 63 | gulp.task('release', ['build_dist'], function() { 64 | var incrementedVersion; 65 | 66 | // increment version 67 | gulp.src(path.pkg) 68 | .pipe(through.obj(function(file, _, done) { 69 | var incrementedVersion; 70 | var content = file.contents.toString().replace(VERSION_REGEXP, function(_, prefix, parsedVersion, suffix) { 71 | incrementedVersion = parsedVersion.replace(/pre-(\d+)/, function(_, number) { 72 | return 'pre-' + (parseInt(number, 10) + 1); 73 | }); 74 | 75 | return prefix + incrementedVersion + suffix; 76 | }); 77 | 78 | // TODO(vojta): we should rather create a new file object 79 | file.contents = new Buffer(content); 80 | file.meta = { 81 | incrementedVersion: incrementedVersion 82 | }; 83 | 84 | this.push(file); 85 | done(); 86 | })) 87 | .pipe(gulp.dest('.')) 88 | .pipe(git.commit('chore(release): <%= incrementedVersion %>')) 89 | .on('commit_done', function() { 90 | git.push('upstream', 'master'); 91 | exec('npm publish --tag v2', function() { 92 | exec('npm tag di@0.0.1 latest'); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /dist/cjs/testing.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperties(exports, { 3 | use: {get: function() { 4 | return use; 5 | }}, 6 | inject: {get: function() { 7 | return inject; 8 | }}, 9 | __esModule: {value: true} 10 | }); 11 | var $__injector__, 12 | $__annotations__, 13 | $__util__, 14 | $__providers__; 15 | var Injector = ($__injector__ = require("./injector"), $__injector__ && $__injector__.__esModule && $__injector__ || {default: $__injector__}).Injector; 16 | var $__1 = ($__annotations__ = require("./annotations"), $__annotations__ && $__annotations__.__esModule && $__annotations__ || {default: $__annotations__}), 17 | Inject = $__1.Inject, 18 | annotate = $__1.annotate, 19 | readAnnotations = $__1.readAnnotations; 20 | var isFunction = ($__util__ = require("./util"), $__util__ && $__util__.__esModule && $__util__ || {default: $__util__}).isFunction; 21 | var createProviderFromFnOrClass = ($__providers__ = require("./providers"), $__providers__ && $__providers__.__esModule && $__providers__ || {default: $__providers__}).createProviderFromFnOrClass; 22 | var currentSpec = null; 23 | beforeEach(function() { 24 | currentSpec = this; 25 | currentSpec.$$providers = []; 26 | }); 27 | afterEach(function() { 28 | currentSpec.$$providers = null; 29 | currentSpec.$$injector = null; 30 | currentSpec = null; 31 | }); 32 | function isRunning() { 33 | return !!currentSpec; 34 | } 35 | function use(mock) { 36 | if (currentSpec && currentSpec.$$injector) { 37 | throw new Error('Cannot call use() after inject() has already been called.'); 38 | } 39 | var providerWrapper = {provider: mock}; 40 | var fn = function() { 41 | currentSpec.$$providers.push(providerWrapper); 42 | }; 43 | fn.as = function(token) { 44 | if (currentSpec && currentSpec.$$injector) { 45 | throw new Error('Cannot call as() after inject() has already been called.'); 46 | } 47 | providerWrapper.as = token; 48 | if (isRunning()) { 49 | return undefined; 50 | } 51 | return fn; 52 | }; 53 | if (isRunning()) { 54 | fn(); 55 | } 56 | return fn; 57 | } 58 | function inject() { 59 | for (var params = [], 60 | $__4 = 0; $__4 < arguments.length; $__4++) 61 | params[$__4] = arguments[$__4]; 62 | var behavior = params.pop(); 63 | annotate(behavior, new (Function.prototype.bind.apply(Inject, $traceurRuntime.spread([null], params)))()); 64 | var run = function() { 65 | if (!currentSpec.$$injector) { 66 | var providers = new Map(); 67 | var modules = []; 68 | var annotations; 69 | currentSpec.$$providers.forEach(function(providerWrapper) { 70 | if (!providerWrapper.as) { 71 | modules.push(providerWrapper.provider); 72 | } else { 73 | if (!isFunction(providerWrapper.provider)) { 74 | providers.set(providerWrapper.as, createProviderFromFnOrClass(function() { 75 | return providerWrapper.provider; 76 | }, { 77 | provide: { 78 | token: null, 79 | isPromise: false 80 | }, 81 | params: [] 82 | })); 83 | } else { 84 | annotations = readAnnotations(providerWrapper.provider); 85 | providers.set(providerWrapper.as, createProviderFromFnOrClass(providerWrapper.provider, annotations)); 86 | } 87 | } 88 | }); 89 | currentSpec.$$injector = new Injector(modules, null, providers); 90 | } 91 | currentSpec.$$injector.get(behavior); 92 | }; 93 | return isRunning() ? run() : run; 94 | } 95 | ; 96 | -------------------------------------------------------------------------------- /dist/amd/testing.js: -------------------------------------------------------------------------------- 1 | define(['./injector', './annotations', './util', './providers'], function($__0,$__2,$__4,$__6) { 2 | "use strict"; 3 | if (!$__0 || !$__0.__esModule) 4 | $__0 = {default: $__0}; 5 | if (!$__2 || !$__2.__esModule) 6 | $__2 = {default: $__2}; 7 | if (!$__4 || !$__4.__esModule) 8 | $__4 = {default: $__4}; 9 | if (!$__6 || !$__6.__esModule) 10 | $__6 = {default: $__6}; 11 | var Injector = $__0.Injector; 12 | var $__3 = $__2, 13 | Inject = $__3.Inject, 14 | annotate = $__3.annotate, 15 | readAnnotations = $__3.readAnnotations; 16 | var isFunction = $__4.isFunction; 17 | var createProviderFromFnOrClass = $__6.createProviderFromFnOrClass; 18 | var currentSpec = null; 19 | beforeEach(function() { 20 | currentSpec = this; 21 | currentSpec.$$providers = []; 22 | }); 23 | afterEach(function() { 24 | currentSpec.$$providers = null; 25 | currentSpec.$$injector = null; 26 | currentSpec = null; 27 | }); 28 | function isRunning() { 29 | return !!currentSpec; 30 | } 31 | function use(mock) { 32 | if (currentSpec && currentSpec.$$injector) { 33 | throw new Error('Cannot call use() after inject() has already been called.'); 34 | } 35 | var providerWrapper = {provider: mock}; 36 | var fn = function() { 37 | currentSpec.$$providers.push(providerWrapper); 38 | }; 39 | fn.as = function(token) { 40 | if (currentSpec && currentSpec.$$injector) { 41 | throw new Error('Cannot call as() after inject() has already been called.'); 42 | } 43 | providerWrapper.as = token; 44 | if (isRunning()) { 45 | return undefined; 46 | } 47 | return fn; 48 | }; 49 | if (isRunning()) { 50 | fn(); 51 | } 52 | return fn; 53 | } 54 | function inject() { 55 | for (var params = [], 56 | $__8 = 0; $__8 < arguments.length; $__8++) 57 | params[$__8] = arguments[$__8]; 58 | var behavior = params.pop(); 59 | annotate(behavior, new (Function.prototype.bind.apply(Inject, $traceurRuntime.spread([null], params)))()); 60 | var run = function() { 61 | if (!currentSpec.$$injector) { 62 | var providers = new Map(); 63 | var modules = []; 64 | var annotations; 65 | currentSpec.$$providers.forEach(function(providerWrapper) { 66 | if (!providerWrapper.as) { 67 | modules.push(providerWrapper.provider); 68 | } else { 69 | if (!isFunction(providerWrapper.provider)) { 70 | providers.set(providerWrapper.as, createProviderFromFnOrClass(function() { 71 | return providerWrapper.provider; 72 | }, { 73 | provide: { 74 | token: null, 75 | isPromise: false 76 | }, 77 | params: [] 78 | })); 79 | } else { 80 | annotations = readAnnotations(providerWrapper.provider); 81 | providers.set(providerWrapper.as, createProviderFromFnOrClass(providerWrapper.provider, annotations)); 82 | } 83 | } 84 | }); 85 | currentSpec.$$injector = new Injector(modules, null, providers); 86 | } 87 | currentSpec.$$injector.get(behavior); 88 | }; 89 | return isRunning() ? run() : run; 90 | } 91 | ; 92 | return { 93 | get use() { 94 | return use; 95 | }, 96 | get inject() { 97 | return inject; 98 | }, 99 | __esModule: true 100 | }; 101 | }); 102 | -------------------------------------------------------------------------------- /test/async.spec.js: -------------------------------------------------------------------------------- 1 | import {ProvidePromise, InjectPromise, Inject, TransientScope} from '../src/annotations'; 2 | import {Injector} from '../src/injector'; 3 | 4 | 5 | class UserList {} 6 | 7 | // An async provider. 8 | @ProvidePromise(UserList) 9 | function fetchUsers() { 10 | return Promise.resolve(new UserList); 11 | } 12 | 13 | class SynchronousUserList {} 14 | 15 | class UserController { 16 | constructor(@Inject(UserList) list) { 17 | this.list = list; 18 | } 19 | } 20 | 21 | class SmartUserController { 22 | constructor(@InjectPromise(UserList) promise) { 23 | this.promise = promise; 24 | } 25 | } 26 | 27 | 28 | describe('async', function() { 29 | 30 | it('should return a promise', function() { 31 | var injector = new Injector([fetchUsers]); 32 | var p = injector.getPromise(UserList) 33 | 34 | expect(p).toBePromiseLike(); 35 | }); 36 | 37 | 38 | it('should throw when instantiating promise provider synchronously', function() { 39 | var injector = new Injector([fetchUsers]); 40 | 41 | expect(() => injector.get(UserList)) 42 | .toThrowError('Cannot instantiate UserList synchronously. It is provided as a promise!'); 43 | }); 44 | 45 | 46 | it('should return promise even if the provider is sync', function() { 47 | var injector = new Injector(); 48 | var p = injector.getPromise(SynchronousUserList); 49 | 50 | expect(p).toBePromiseLike(); 51 | }); 52 | 53 | 54 | // regression 55 | it('should return promise even if the provider is sync, from cache', function() { 56 | var injector = new Injector(); 57 | var p1 = injector.getPromise(SynchronousUserList); 58 | var p2 = injector.getPromise(SynchronousUserList); 59 | 60 | expect(p2).toBePromiseLike(); 61 | }); 62 | 63 | 64 | it('should return promise when a dependency is async', function(done) { 65 | var injector = new Injector([fetchUsers]); 66 | 67 | injector.getPromise(UserController).then(function(userController) { 68 | expect(userController).toBeInstanceOf(UserController); 69 | expect(userController.list).toBeInstanceOf(UserList); 70 | done(); 71 | }); 72 | }); 73 | 74 | 75 | // regression 76 | it('should return a promise even from parent injector', function() { 77 | var injector = new Injector([SynchronousUserList]); 78 | var childInjector = injector.createChild([]) 79 | 80 | expect(childInjector.getPromise(SynchronousUserList)).toBePromiseLike(); 81 | }); 82 | 83 | 84 | it('should throw when a dependency is async', function() { 85 | var injector = new Injector([fetchUsers]); 86 | 87 | expect(() => injector.get(UserController)) 88 | .toThrowError('Cannot instantiate UserList synchronously. It is provided as a promise! (UserController -> UserList)'); 89 | }); 90 | 91 | 92 | it('should resolve synchronously when async dependency requested as a promise', function() { 93 | var injector = new Injector([fetchUsers]); 94 | var controller = injector.get(SmartUserController); 95 | 96 | expect(controller).toBeInstanceOf(SmartUserController); 97 | expect(controller.promise).toBePromiseLike(); 98 | }); 99 | 100 | 101 | // regression 102 | it('should not cache TransientScope', function(done) { 103 | @TransientScope 104 | @Inject(UserList) 105 | class NeverCachedUserController { 106 | constructor(list) { 107 | this.list = list; 108 | } 109 | } 110 | 111 | var injector = new Injector([fetchUsers]); 112 | 113 | injector.getPromise(NeverCachedUserController).then(function(controller1) { 114 | injector.getPromise(NeverCachedUserController).then(function(controller2) { 115 | expect(controller1).not.toBe(controller2); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | 121 | 122 | it('should allow async dependency in a parent constructor', function(done) { 123 | class ChildUserController extends UserController {} 124 | 125 | var injector = new Injector([fetchUsers]); 126 | 127 | injector.getPromise(ChildUserController).then(function(childUserController) { 128 | expect(childUserController).toBeInstanceOf(ChildUserController); 129 | expect(childUserController.list).toBeInstanceOf(UserList); 130 | done(); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /dist/cjs/providers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperties(exports, { 3 | createProviderFromFnOrClass: {get: function() { 4 | return createProviderFromFnOrClass; 5 | }}, 6 | __esModule: {value: true} 7 | }); 8 | var $__annotations__, 9 | $__util__; 10 | var $__0 = ($__annotations__ = require("./annotations"), $__annotations__ && $__annotations__.__esModule && $__annotations__ || {default: $__annotations__}), 11 | SuperConstructorAnnotation = $__0.SuperConstructor, 12 | readAnnotations = $__0.readAnnotations; 13 | var $__1 = ($__util__ = require("./util"), $__util__ && $__util__.__esModule && $__util__ || {default: $__util__}), 14 | isClass = $__1.isClass, 15 | isFunction = $__1.isFunction, 16 | isObject = $__1.isObject, 17 | toString = $__1.toString; 18 | var EmptyFunction = Object.getPrototypeOf(Function); 19 | var ClassProvider = function ClassProvider(clazz, params, isPromise) { 20 | this.provider = clazz; 21 | this.isPromise = isPromise; 22 | this.params = []; 23 | this._constructors = []; 24 | this._flattenParams(clazz, params); 25 | this._constructors.unshift([clazz, 0, this.params.length - 1]); 26 | }; 27 | ($traceurRuntime.createClass)(ClassProvider, { 28 | _flattenParams: function(constructor, params) { 29 | var SuperConstructor; 30 | var constructorInfo; 31 | for (var $__3 = params[Symbol.iterator](), 32 | $__4; !($__4 = $__3.next()).done; ) { 33 | var param = $__4.value; 34 | { 35 | if (param.token === SuperConstructorAnnotation) { 36 | SuperConstructor = Object.getPrototypeOf(constructor); 37 | if (SuperConstructor === EmptyFunction) { 38 | throw new Error((toString(constructor) + " does not have a parent constructor. Only classes with a parent can ask for SuperConstructor!")); 39 | } 40 | constructorInfo = [SuperConstructor, this.params.length]; 41 | this._constructors.push(constructorInfo); 42 | this._flattenParams(SuperConstructor, readAnnotations(SuperConstructor).params); 43 | constructorInfo.push(this.params.length - 1); 44 | } else { 45 | this.params.push(param); 46 | } 47 | } 48 | } 49 | }, 50 | _createConstructor: function(currentConstructorIdx, context, allArguments) { 51 | var constructorInfo = this._constructors[currentConstructorIdx]; 52 | var nextConstructorInfo = this._constructors[currentConstructorIdx + 1]; 53 | var argsForCurrentConstructor; 54 | if (nextConstructorInfo) { 55 | argsForCurrentConstructor = allArguments.slice(constructorInfo[1], nextConstructorInfo[1]).concat([this._createConstructor(currentConstructorIdx + 1, context, allArguments)]).concat(allArguments.slice(nextConstructorInfo[2] + 1, constructorInfo[2] + 1)); 56 | } else { 57 | argsForCurrentConstructor = allArguments.slice(constructorInfo[1], constructorInfo[2] + 1); 58 | } 59 | return function InjectedAndBoundSuperConstructor() { 60 | return constructorInfo[0].apply(context, argsForCurrentConstructor); 61 | }; 62 | }, 63 | create: function(args) { 64 | var context = Object.create(this.provider.prototype); 65 | var constructor = this._createConstructor(0, context, args); 66 | var returnedValue = constructor(); 67 | if (isFunction(returnedValue) || isObject(returnedValue)) { 68 | return returnedValue; 69 | } 70 | return context; 71 | } 72 | }, {}); 73 | var FactoryProvider = function FactoryProvider(factoryFunction, params, isPromise) { 74 | this.provider = factoryFunction; 75 | this.params = params; 76 | this.isPromise = isPromise; 77 | for (var $__3 = params[Symbol.iterator](), 78 | $__4; !($__4 = $__3.next()).done; ) { 79 | var param = $__4.value; 80 | { 81 | if (param.token === SuperConstructorAnnotation) { 82 | throw new Error((toString(factoryFunction) + " is not a class. Only classes with a parent can ask for SuperConstructor!")); 83 | } 84 | } 85 | } 86 | }; 87 | ($traceurRuntime.createClass)(FactoryProvider, {create: function(args) { 88 | return this.provider.apply(undefined, args); 89 | }}, {}); 90 | function createProviderFromFnOrClass(fnOrClass, annotations) { 91 | if (isClass(fnOrClass)) { 92 | return new ClassProvider(fnOrClass, annotations.params, annotations.provide.isPromise); 93 | } 94 | return new FactoryProvider(fnOrClass, annotations.params, annotations.provide.isPromise); 95 | } 96 | -------------------------------------------------------------------------------- /src/annotations.js: -------------------------------------------------------------------------------- 1 | import {isFunction} from './util'; 2 | 3 | // This module contains: 4 | // - built-in annotation classes 5 | // - helpers to read/write annotations 6 | 7 | 8 | // ANNOTATIONS 9 | 10 | // A built-in token. 11 | // Used to ask for pre-injected parent constructor. 12 | // A class constructor can ask for this. 13 | class SuperConstructor {} 14 | 15 | // A built-in scope. 16 | // Never cache. 17 | class TransientScope {} 18 | 19 | class Inject { 20 | constructor(...tokens) { 21 | this.tokens = tokens; 22 | this.isPromise = false; 23 | this.isLazy = false; 24 | } 25 | } 26 | 27 | class InjectPromise extends Inject { 28 | constructor(...tokens) { 29 | this.tokens = tokens; 30 | this.isPromise = true; 31 | this.isLazy = false; 32 | } 33 | } 34 | 35 | class InjectLazy extends Inject { 36 | constructor(...tokens) { 37 | this.tokens = tokens; 38 | this.isPromise = false; 39 | this.isLazy = true; 40 | } 41 | } 42 | 43 | class Provide { 44 | constructor(token) { 45 | this.token = token; 46 | this.isPromise = false; 47 | } 48 | } 49 | 50 | class ProvidePromise extends Provide { 51 | constructor(token) { 52 | this.token = token; 53 | this.isPromise = true; 54 | } 55 | } 56 | 57 | class ClassProvider {} 58 | class FactoryProvider {} 59 | 60 | 61 | // HELPERS 62 | 63 | // Append annotation on a function or class. 64 | // This can be helpful when not using ES6+. 65 | function annotate(fn, annotation) { 66 | fn.annotations = fn.annotations || []; 67 | fn.annotations.push(annotation); 68 | } 69 | 70 | 71 | // Read annotations on a function or class and return whether given annotation is present. 72 | function hasAnnotation(fn, annotationClass) { 73 | if (!fn.annotations || fn.annotations.length === 0) { 74 | return false; 75 | } 76 | 77 | for (var annotation of fn.annotations) { 78 | if (annotation instanceof annotationClass) { 79 | return true; 80 | } 81 | } 82 | 83 | return false; 84 | } 85 | 86 | 87 | // Read annotations on a function or class and collect "interesting" metadata: 88 | function readAnnotations(fn) { 89 | var collectedAnnotations = { 90 | // Description of the provided value. 91 | provide: { 92 | token: null, 93 | isPromise: false 94 | }, 95 | 96 | // List of parameter descriptions. 97 | // A parameter description is an object with properties: 98 | // - token (anything) 99 | // - isPromise (boolean) 100 | // - isLazy (boolean) 101 | params: [] 102 | }; 103 | 104 | if (fn.annotations && fn.annotations.length) { 105 | for (var annotation of fn.annotations) { 106 | if (annotation instanceof Inject) { 107 | annotation.tokens.forEach((token) => { 108 | collectedAnnotations.params.push({ 109 | token: token, 110 | isPromise: annotation.isPromise, 111 | isLazy: annotation.isLazy 112 | }); 113 | }); 114 | } 115 | 116 | if (annotation instanceof Provide) { 117 | collectedAnnotations.provide.token = annotation.token; 118 | collectedAnnotations.provide.isPromise = annotation.isPromise; 119 | } 120 | } 121 | } 122 | 123 | // Read annotations for individual parameters. 124 | if (fn.parameters) { 125 | fn.parameters.forEach((param, idx) => { 126 | for (var paramAnnotation of param) { 127 | // Type annotation. 128 | if (isFunction(paramAnnotation) && !collectedAnnotations.params[idx]) { 129 | collectedAnnotations.params[idx] = { 130 | token: paramAnnotation, 131 | isPromise: false, 132 | isLazy: false 133 | }; 134 | } else if (paramAnnotation instanceof Inject) { 135 | collectedAnnotations.params[idx] = { 136 | token: paramAnnotation.tokens[0], 137 | isPromise: paramAnnotation.isPromise, 138 | isLazy: paramAnnotation.isLazy 139 | }; 140 | } 141 | } 142 | }); 143 | } 144 | 145 | return collectedAnnotations; 146 | } 147 | 148 | 149 | export { 150 | annotate, 151 | hasAnnotation, 152 | readAnnotations, 153 | 154 | SuperConstructor, 155 | TransientScope, 156 | Inject, 157 | InjectPromise, 158 | InjectLazy, 159 | Provide, 160 | ProvidePromise, 161 | ClassProvider, 162 | FactoryProvider 163 | }; 164 | -------------------------------------------------------------------------------- /dist/amd/providers.js: -------------------------------------------------------------------------------- 1 | define(['./annotations', './util'], function($__0,$__2) { 2 | "use strict"; 3 | if (!$__0 || !$__0.__esModule) 4 | $__0 = {default: $__0}; 5 | if (!$__2 || !$__2.__esModule) 6 | $__2 = {default: $__2}; 7 | var $__1 = $__0, 8 | SuperConstructorAnnotation = $__1.SuperConstructor, 9 | readAnnotations = $__1.readAnnotations; 10 | var $__3 = $__2, 11 | isClass = $__3.isClass, 12 | isFunction = $__3.isFunction, 13 | isObject = $__3.isObject, 14 | toString = $__3.toString; 15 | var EmptyFunction = Object.getPrototypeOf(Function); 16 | var ClassProvider = function ClassProvider(clazz, params, isPromise) { 17 | this.provider = clazz; 18 | this.isPromise = isPromise; 19 | this.params = []; 20 | this._constructors = []; 21 | this._flattenParams(clazz, params); 22 | this._constructors.unshift([clazz, 0, this.params.length - 1]); 23 | }; 24 | ($traceurRuntime.createClass)(ClassProvider, { 25 | _flattenParams: function(constructor, params) { 26 | var SuperConstructor; 27 | var constructorInfo; 28 | for (var $__5 = params[Symbol.iterator](), 29 | $__6; !($__6 = $__5.next()).done; ) { 30 | var param = $__6.value; 31 | { 32 | if (param.token === SuperConstructorAnnotation) { 33 | SuperConstructor = Object.getPrototypeOf(constructor); 34 | if (SuperConstructor === EmptyFunction) { 35 | throw new Error((toString(constructor) + " does not have a parent constructor. Only classes with a parent can ask for SuperConstructor!")); 36 | } 37 | constructorInfo = [SuperConstructor, this.params.length]; 38 | this._constructors.push(constructorInfo); 39 | this._flattenParams(SuperConstructor, readAnnotations(SuperConstructor).params); 40 | constructorInfo.push(this.params.length - 1); 41 | } else { 42 | this.params.push(param); 43 | } 44 | } 45 | } 46 | }, 47 | _createConstructor: function(currentConstructorIdx, context, allArguments) { 48 | var constructorInfo = this._constructors[currentConstructorIdx]; 49 | var nextConstructorInfo = this._constructors[currentConstructorIdx + 1]; 50 | var argsForCurrentConstructor; 51 | if (nextConstructorInfo) { 52 | argsForCurrentConstructor = allArguments.slice(constructorInfo[1], nextConstructorInfo[1]).concat([this._createConstructor(currentConstructorIdx + 1, context, allArguments)]).concat(allArguments.slice(nextConstructorInfo[2] + 1, constructorInfo[2] + 1)); 53 | } else { 54 | argsForCurrentConstructor = allArguments.slice(constructorInfo[1], constructorInfo[2] + 1); 55 | } 56 | return function InjectedAndBoundSuperConstructor() { 57 | return constructorInfo[0].apply(context, argsForCurrentConstructor); 58 | }; 59 | }, 60 | create: function(args) { 61 | var context = Object.create(this.provider.prototype); 62 | var constructor = this._createConstructor(0, context, args); 63 | var returnedValue = constructor(); 64 | if (isFunction(returnedValue) || isObject(returnedValue)) { 65 | return returnedValue; 66 | } 67 | return context; 68 | } 69 | }, {}); 70 | var FactoryProvider = function FactoryProvider(factoryFunction, params, isPromise) { 71 | this.provider = factoryFunction; 72 | this.params = params; 73 | this.isPromise = isPromise; 74 | for (var $__5 = params[Symbol.iterator](), 75 | $__6; !($__6 = $__5.next()).done; ) { 76 | var param = $__6.value; 77 | { 78 | if (param.token === SuperConstructorAnnotation) { 79 | throw new Error((toString(factoryFunction) + " is not a class. Only classes with a parent can ask for SuperConstructor!")); 80 | } 81 | } 82 | } 83 | }; 84 | ($traceurRuntime.createClass)(FactoryProvider, {create: function(args) { 85 | return this.provider.apply(undefined, args); 86 | }}, {}); 87 | function createProviderFromFnOrClass(fnOrClass, annotations) { 88 | if (isClass(fnOrClass)) { 89 | return new ClassProvider(fnOrClass, annotations.params, annotations.provide.isPromise); 90 | } 91 | return new FactoryProvider(fnOrClass, annotations.params, annotations.provide.isPromise); 92 | } 93 | return { 94 | get createProviderFromFnOrClass() { 95 | return createProviderFromFnOrClass; 96 | }, 97 | __esModule: true 98 | }; 99 | }); 100 | -------------------------------------------------------------------------------- /dist/cjs/annotations.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperties(exports, { 3 | annotate: {get: function() { 4 | return annotate; 5 | }}, 6 | hasAnnotation: {get: function() { 7 | return hasAnnotation; 8 | }}, 9 | readAnnotations: {get: function() { 10 | return readAnnotations; 11 | }}, 12 | SuperConstructor: {get: function() { 13 | return SuperConstructor; 14 | }}, 15 | TransientScope: {get: function() { 16 | return TransientScope; 17 | }}, 18 | Inject: {get: function() { 19 | return Inject; 20 | }}, 21 | InjectPromise: {get: function() { 22 | return InjectPromise; 23 | }}, 24 | InjectLazy: {get: function() { 25 | return InjectLazy; 26 | }}, 27 | Provide: {get: function() { 28 | return Provide; 29 | }}, 30 | ProvidePromise: {get: function() { 31 | return ProvidePromise; 32 | }}, 33 | __esModule: {value: true} 34 | }); 35 | var $__util__; 36 | var isFunction = ($__util__ = require("./util"), $__util__ && $__util__.__esModule && $__util__ || {default: $__util__}).isFunction; 37 | var SuperConstructor = function SuperConstructor() {}; 38 | ($traceurRuntime.createClass)(SuperConstructor, {}, {}); 39 | var TransientScope = function TransientScope() {}; 40 | ($traceurRuntime.createClass)(TransientScope, {}, {}); 41 | var Inject = function Inject() { 42 | for (var tokens = [], 43 | $__6 = 0; $__6 < arguments.length; $__6++) 44 | tokens[$__6] = arguments[$__6]; 45 | this.tokens = tokens; 46 | this.isPromise = false; 47 | this.isLazy = false; 48 | }; 49 | ($traceurRuntime.createClass)(Inject, {}, {}); 50 | var InjectPromise = function InjectPromise() { 51 | for (var tokens = [], 52 | $__7 = 0; $__7 < arguments.length; $__7++) 53 | tokens[$__7] = arguments[$__7]; 54 | this.tokens = tokens; 55 | this.isPromise = true; 56 | this.isLazy = false; 57 | }; 58 | ($traceurRuntime.createClass)(InjectPromise, {}, {}, Inject); 59 | var InjectLazy = function InjectLazy() { 60 | for (var tokens = [], 61 | $__8 = 0; $__8 < arguments.length; $__8++) 62 | tokens[$__8] = arguments[$__8]; 63 | this.tokens = tokens; 64 | this.isPromise = false; 65 | this.isLazy = true; 66 | }; 67 | ($traceurRuntime.createClass)(InjectLazy, {}, {}, Inject); 68 | var Provide = function Provide(token) { 69 | this.token = token; 70 | this.isPromise = false; 71 | }; 72 | ($traceurRuntime.createClass)(Provide, {}, {}); 73 | var ProvidePromise = function ProvidePromise(token) { 74 | this.token = token; 75 | this.isPromise = true; 76 | }; 77 | ($traceurRuntime.createClass)(ProvidePromise, {}, {}, Provide); 78 | function annotate(fn, annotation) { 79 | fn.annotations = fn.annotations || []; 80 | fn.annotations.push(annotation); 81 | } 82 | function hasAnnotation(fn, annotationClass) { 83 | if (!fn.annotations || fn.annotations.length === 0) { 84 | return false; 85 | } 86 | for (var $__2 = fn.annotations[Symbol.iterator](), 87 | $__3; !($__3 = $__2.next()).done; ) { 88 | var annotation = $__3.value; 89 | { 90 | if (annotation instanceof annotationClass) { 91 | return true; 92 | } 93 | } 94 | } 95 | return false; 96 | } 97 | function readAnnotations(fn) { 98 | var collectedAnnotations = { 99 | provide: { 100 | token: null, 101 | isPromise: false 102 | }, 103 | params: [] 104 | }; 105 | if (fn.annotations && fn.annotations.length) { 106 | for (var $__2 = fn.annotations[Symbol.iterator](), 107 | $__3; !($__3 = $__2.next()).done; ) { 108 | var annotation = $__3.value; 109 | { 110 | if (annotation instanceof Inject) { 111 | annotation.tokens.forEach((function(token) { 112 | collectedAnnotations.params.push({ 113 | token: token, 114 | isPromise: annotation.isPromise, 115 | isLazy: annotation.isLazy 116 | }); 117 | })); 118 | } 119 | if (annotation instanceof Provide) { 120 | collectedAnnotations.provide.token = annotation.token; 121 | collectedAnnotations.provide.isPromise = annotation.isPromise; 122 | } 123 | } 124 | } 125 | } 126 | if (fn.parameters) { 127 | fn.parameters.forEach((function(param, idx) { 128 | for (var $__4 = param[Symbol.iterator](), 129 | $__5; !($__5 = $__4.next()).done; ) { 130 | var paramAnnotation = $__5.value; 131 | { 132 | if (isFunction(paramAnnotation) && !collectedAnnotations.params[idx]) { 133 | collectedAnnotations.params[idx] = { 134 | token: paramAnnotation, 135 | isPromise: false, 136 | isLazy: false 137 | }; 138 | } else if (paramAnnotation instanceof Inject) { 139 | collectedAnnotations.params[idx] = { 140 | token: paramAnnotation.tokens[0], 141 | isPromise: paramAnnotation.isPromise, 142 | isLazy: paramAnnotation.isLazy 143 | }; 144 | } 145 | } 146 | } 147 | })); 148 | } 149 | return collectedAnnotations; 150 | } 151 | ; 152 | -------------------------------------------------------------------------------- /dist/amd/annotations.js: -------------------------------------------------------------------------------- 1 | define(['./util'], function($__0) { 2 | "use strict"; 3 | if (!$__0 || !$__0.__esModule) 4 | $__0 = {default: $__0}; 5 | var isFunction = $__0.isFunction; 6 | var SuperConstructor = function SuperConstructor() {}; 7 | ($traceurRuntime.createClass)(SuperConstructor, {}, {}); 8 | var TransientScope = function TransientScope() {}; 9 | ($traceurRuntime.createClass)(TransientScope, {}, {}); 10 | var Inject = function Inject() { 11 | for (var tokens = [], 12 | $__7 = 0; $__7 < arguments.length; $__7++) 13 | tokens[$__7] = arguments[$__7]; 14 | this.tokens = tokens; 15 | this.isPromise = false; 16 | this.isLazy = false; 17 | }; 18 | ($traceurRuntime.createClass)(Inject, {}, {}); 19 | var InjectPromise = function InjectPromise() { 20 | for (var tokens = [], 21 | $__8 = 0; $__8 < arguments.length; $__8++) 22 | tokens[$__8] = arguments[$__8]; 23 | this.tokens = tokens; 24 | this.isPromise = true; 25 | this.isLazy = false; 26 | }; 27 | ($traceurRuntime.createClass)(InjectPromise, {}, {}, Inject); 28 | var InjectLazy = function InjectLazy() { 29 | for (var tokens = [], 30 | $__9 = 0; $__9 < arguments.length; $__9++) 31 | tokens[$__9] = arguments[$__9]; 32 | this.tokens = tokens; 33 | this.isPromise = false; 34 | this.isLazy = true; 35 | }; 36 | ($traceurRuntime.createClass)(InjectLazy, {}, {}, Inject); 37 | var Provide = function Provide(token) { 38 | this.token = token; 39 | this.isPromise = false; 40 | }; 41 | ($traceurRuntime.createClass)(Provide, {}, {}); 42 | var ProvidePromise = function ProvidePromise(token) { 43 | this.token = token; 44 | this.isPromise = true; 45 | }; 46 | ($traceurRuntime.createClass)(ProvidePromise, {}, {}, Provide); 47 | function annotate(fn, annotation) { 48 | fn.annotations = fn.annotations || []; 49 | fn.annotations.push(annotation); 50 | } 51 | function hasAnnotation(fn, annotationClass) { 52 | if (!fn.annotations || fn.annotations.length === 0) { 53 | return false; 54 | } 55 | for (var $__3 = fn.annotations[Symbol.iterator](), 56 | $__4; !($__4 = $__3.next()).done; ) { 57 | var annotation = $__4.value; 58 | { 59 | if (annotation instanceof annotationClass) { 60 | return true; 61 | } 62 | } 63 | } 64 | return false; 65 | } 66 | function readAnnotations(fn) { 67 | var collectedAnnotations = { 68 | provide: { 69 | token: null, 70 | isPromise: false 71 | }, 72 | params: [] 73 | }; 74 | if (fn.annotations && fn.annotations.length) { 75 | for (var $__3 = fn.annotations[Symbol.iterator](), 76 | $__4; !($__4 = $__3.next()).done; ) { 77 | var annotation = $__4.value; 78 | { 79 | if (annotation instanceof Inject) { 80 | annotation.tokens.forEach((function(token) { 81 | collectedAnnotations.params.push({ 82 | token: token, 83 | isPromise: annotation.isPromise, 84 | isLazy: annotation.isLazy 85 | }); 86 | })); 87 | } 88 | if (annotation instanceof Provide) { 89 | collectedAnnotations.provide.token = annotation.token; 90 | collectedAnnotations.provide.isPromise = annotation.isPromise; 91 | } 92 | } 93 | } 94 | } 95 | if (fn.parameters) { 96 | fn.parameters.forEach((function(param, idx) { 97 | for (var $__5 = param[Symbol.iterator](), 98 | $__6; !($__6 = $__5.next()).done; ) { 99 | var paramAnnotation = $__6.value; 100 | { 101 | if (isFunction(paramAnnotation) && !collectedAnnotations.params[idx]) { 102 | collectedAnnotations.params[idx] = { 103 | token: paramAnnotation, 104 | isPromise: false, 105 | isLazy: false 106 | }; 107 | } else if (paramAnnotation instanceof Inject) { 108 | collectedAnnotations.params[idx] = { 109 | token: paramAnnotation.tokens[0], 110 | isPromise: paramAnnotation.isPromise, 111 | isLazy: paramAnnotation.isLazy 112 | }; 113 | } 114 | } 115 | } 116 | })); 117 | } 118 | return collectedAnnotations; 119 | } 120 | ; 121 | return { 122 | get annotate() { 123 | return annotate; 124 | }, 125 | get hasAnnotation() { 126 | return hasAnnotation; 127 | }, 128 | get readAnnotations() { 129 | return readAnnotations; 130 | }, 131 | get SuperConstructor() { 132 | return SuperConstructor; 133 | }, 134 | get TransientScope() { 135 | return TransientScope; 136 | }, 137 | get Inject() { 138 | return Inject; 139 | }, 140 | get InjectPromise() { 141 | return InjectPromise; 142 | }, 143 | get InjectLazy() { 144 | return InjectLazy; 145 | }, 146 | get Provide() { 147 | return Provide; 148 | }, 149 | get ProvidePromise() { 150 | return ProvidePromise; 151 | }, 152 | __esModule: true 153 | }; 154 | }); 155 | -------------------------------------------------------------------------------- /test/annotations.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | hasAnnotation, 3 | readAnnotations, 4 | Inject, 5 | InjectLazy, 6 | InjectPromise, 7 | Provide, 8 | ProvidePromise 9 | } from '../src/annotations'; 10 | 11 | 12 | describe('hasAnnotation', function() { 13 | 14 | it('should return false if fn not annotated', function() { 15 | function foo() {} 16 | class Bar {} 17 | class SomeAnnotation {} 18 | 19 | expect(hasAnnotation(foo, SomeAnnotation)).toBe(false); 20 | expect(hasAnnotation(Bar, SomeAnnotation)).toBe(false); 21 | }); 22 | 23 | 24 | it('should return true if the fn has an instance of given annotation', function() { 25 | class SomeAnnotation {} 26 | 27 | @SomeAnnotation 28 | function foo() {} 29 | 30 | expect(hasAnnotation(foo, SomeAnnotation)).toBe(true); 31 | }); 32 | 33 | 34 | it('should return false if fn does not have given annotation', function() { 35 | class YepAnnotation {} 36 | class NopeAnnotation {} 37 | 38 | @YepAnnotation 39 | function foo() {} 40 | 41 | expect(hasAnnotation(foo, NopeAnnotation)).toBe(false); 42 | }); 43 | }); 44 | 45 | 46 | describe('readAnnotations', function() { 47 | 48 | it('should read @Provide', function() { 49 | class Bar {} 50 | 51 | @Provide(Bar) 52 | class Foo {} 53 | 54 | var annotations = readAnnotations(Foo); 55 | 56 | expect(annotations.provide.token).toBe(Bar); 57 | expect(annotations.provide.isPromise).toBe(false); 58 | }); 59 | 60 | 61 | it('should read @ProvidePromise', function() { 62 | class Bar {} 63 | 64 | @ProvidePromise(Bar) 65 | class Foo {} 66 | 67 | var annotations = readAnnotations(Foo); 68 | 69 | expect(annotations.provide.token).toBe(Bar); 70 | expect(annotations.provide.isPromise).toBe(true); 71 | }); 72 | 73 | 74 | it('should read @Inject', function() { 75 | class One {} 76 | class Two {} 77 | 78 | @Inject(One, Two) 79 | class Foo {} 80 | 81 | var annotations = readAnnotations(Foo); 82 | 83 | expect(annotations.params[0].token).toBe(One); 84 | expect(annotations.params[0].isPromise).toBe(false); 85 | expect(annotations.params[0].isLazy).toBe(false); 86 | 87 | expect(annotations.params[1].token).toBe(Two); 88 | expect(annotations.params[1].isPromise).toBe(false); 89 | expect(annotations.params[1].isLazy).toBe(false); 90 | }); 91 | 92 | 93 | it('should read @Inject on a parameter', function() { 94 | class One {} 95 | class Two {} 96 | 97 | class Foo { 98 | constructor(@Inject(One) one, @Inject(Two) two) {} 99 | } 100 | 101 | var annotations = readAnnotations(Foo); 102 | 103 | expect(annotations.params[0].token).toBe(One); 104 | expect(annotations.params[0].isPromise).toBe(false); 105 | expect(annotations.params[0].isLazy).toBe(false); 106 | 107 | expect(annotations.params[1].token).toBe(Two); 108 | expect(annotations.params[1].isPromise).toBe(false); 109 | expect(annotations.params[1].isLazy).toBe(false); 110 | }); 111 | 112 | 113 | it('should read @InjectLazy on a parameter', function() { 114 | class One {} 115 | 116 | class Foo { 117 | constructor(@InjectLazy(One) one) {} 118 | } 119 | 120 | var annotations = readAnnotations(Foo); 121 | 122 | expect(annotations.params[0].token).toBe(One); 123 | expect(annotations.params[0].isPromise).toBe(false); 124 | expect(annotations.params[0].isLazy).toBe(true); 125 | }); 126 | 127 | 128 | it('should read @InjectPromise on a parameter', function() { 129 | class One {} 130 | 131 | class Foo { 132 | constructor(@InjectPromise(One) one) {} 133 | } 134 | 135 | var annotations = readAnnotations(Foo); 136 | 137 | expect(annotations.params[0].token).toBe(One); 138 | expect(annotations.params[0].isPromise).toBe(true); 139 | expect(annotations.params[0].isLazy).toBe(false); 140 | }); 141 | 142 | 143 | it('should read type annotations', function() { 144 | class One {} 145 | class Two {} 146 | 147 | class Foo { 148 | constructor(one: One, two: Two) {} 149 | } 150 | 151 | var annotations = readAnnotations(Foo); 152 | 153 | expect(annotations.params[0].token).toBe(One); 154 | expect(annotations.params[0].isPromise).toBe(false); 155 | expect(annotations.params[0].isLazy).toBe(false); 156 | 157 | expect(annotations.params[1].token).toBe(Two); 158 | expect(annotations.params[1].isPromise).toBe(false); 159 | expect(annotations.params[1].isLazy).toBe(false); 160 | }); 161 | 162 | it('should read stacked @Inject{Lazy, Promise} annotations', function() { 163 | class One {} 164 | class Two {} 165 | class Three {} 166 | 167 | @Inject(One) 168 | @InjectLazy(Two) 169 | @InjectPromise(Three) 170 | class Foo {} 171 | 172 | var annotations = readAnnotations(Foo); 173 | 174 | expect(annotations.params[0].token).toBe(One); 175 | expect(annotations.params[0].isPromise).toBe(false); 176 | expect(annotations.params[0].isLazy).toBe(false); 177 | 178 | expect(annotations.params[1].token).toBe(Two); 179 | expect(annotations.params[1].isPromise).toBe(false); 180 | expect(annotations.params[1].isLazy).toBe(true); 181 | 182 | expect(annotations.params[2].token).toBe(Three); 183 | expect(annotations.params[2].isPromise).toBe(true); 184 | expect(annotations.params[2].isLazy).toBe(false); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /src/providers.js: -------------------------------------------------------------------------------- 1 | import { 2 | ClassProvider as ClassProviderAnnotation, 3 | FactoryProvider as FactoryProviderAnnotation, 4 | SuperConstructor as SuperConstructorAnnotation, 5 | readAnnotations, 6 | hasAnnotation 7 | } from './annotations'; 8 | import {isFunction, isObject, toString, isUpperCase, ownKeys} from './util'; 9 | 10 | function isClass(clsOrFunction) { 11 | 12 | if (hasAnnotation(clsOrFunction, ClassProviderAnnotation)) { 13 | return true 14 | } 15 | else if(hasAnnotation(clsOrFunction, FactoryProviderAnnotation)) { 16 | return false 17 | } 18 | else if (clsOrFunction.name) { 19 | return isUpperCase(clsOrFunction.name.charAt(0)); 20 | } else { 21 | return ownKeys(clsOrFunction.prototype).length > 0; 22 | } 23 | } 24 | 25 | // Provider is responsible for creating instances. 26 | // 27 | // responsibilities: 28 | // - create instances 29 | // 30 | // communication: 31 | // - exposes `create()` which creates an instance of something 32 | // - exposes `params` (information about which arguments it requires to be passed into `create()`) 33 | // 34 | // Injector reads `provider.params` first, create these dependencies (however it wants), 35 | // then calls `provider.create(args)`, passing in these arguments. 36 | 37 | 38 | var EmptyFunction = Object.getPrototypeOf(Function); 39 | 40 | 41 | // ClassProvider knows how to instantiate classes. 42 | // 43 | // If a class inherits (has parent constructors), this provider normalizes all the dependencies 44 | // into a single flat array first, so that the injector does not need to worry about inheritance. 45 | // 46 | // - all the state is immutable (constructed) 47 | // 48 | // TODO(vojta): super constructor - should be only allowed during the constructor call? 49 | class ClassProvider { 50 | constructor(clazz, params, isPromise) { 51 | // TODO(vojta): can we hide this.provider? (only used for hasAnnotation(provider.provider)) 52 | this.provider = clazz; 53 | this.isPromise = isPromise; 54 | 55 | this.params = []; 56 | this._constructors = []; 57 | 58 | this._flattenParams(clazz, params); 59 | this._constructors.unshift([clazz, 0, this.params.length - 1]); 60 | } 61 | 62 | // Normalize params for all the constructors (in the case of inheritance), 63 | // into a single flat array of DependencyDescriptors. 64 | // So that the injector does not have to worry about inheritance. 65 | // 66 | // This function mutates `this.params` and `this._constructors`, 67 | // but it is only called during the constructor. 68 | // TODO(vojta): remove the annotations argument? 69 | _flattenParams(constructor, params) { 70 | var SuperConstructor; 71 | var constructorInfo; 72 | 73 | for (var param of params) { 74 | if (param.token === SuperConstructorAnnotation) { 75 | SuperConstructor = Object.getPrototypeOf(constructor); 76 | 77 | if (SuperConstructor === EmptyFunction) { 78 | throw new Error(`${toString(constructor)} does not have a parent constructor. Only classes with a parent can ask for SuperConstructor!`); 79 | } 80 | 81 | constructorInfo = [SuperConstructor, this.params.length]; 82 | this._constructors.push(constructorInfo); 83 | this._flattenParams(SuperConstructor, readAnnotations(SuperConstructor).params); 84 | constructorInfo.push(this.params.length - 1); 85 | } else { 86 | this.params.push(param); 87 | } 88 | } 89 | } 90 | 91 | // Basically the reverse process to `this._flattenParams`: 92 | // We get arguments for all the constructors as a single flat array. 93 | // This method generates pre-bound "superConstructor" wrapper with correctly passing arguments. 94 | _createConstructor(currentConstructorIdx, context, allArguments) { 95 | var constructorInfo = this._constructors[currentConstructorIdx]; 96 | var nextConstructorInfo = this._constructors[currentConstructorIdx + 1]; 97 | var argsForCurrentConstructor; 98 | 99 | if (nextConstructorInfo) { 100 | argsForCurrentConstructor = allArguments 101 | .slice(constructorInfo[1], nextConstructorInfo[1]) 102 | .concat([this._createConstructor(currentConstructorIdx + 1, context, allArguments)]) 103 | .concat(allArguments.slice(nextConstructorInfo[2] + 1, constructorInfo[2] + 1)); 104 | } else { 105 | argsForCurrentConstructor = allArguments.slice(constructorInfo[1], constructorInfo[2] + 1); 106 | } 107 | 108 | return function InjectedAndBoundSuperConstructor() { 109 | // TODO(vojta): throw if arguments given 110 | return constructorInfo[0].apply(context, argsForCurrentConstructor); 111 | }; 112 | } 113 | 114 | // It is called by injector to create an instance. 115 | create(args) { 116 | var context = Object.create(this.provider.prototype); 117 | var constructor = this._createConstructor(0, context, args); 118 | var returnedValue = constructor(); 119 | 120 | if (isFunction(returnedValue) || isObject(returnedValue)) { 121 | return returnedValue; 122 | } 123 | 124 | return context; 125 | } 126 | } 127 | 128 | 129 | // FactoryProvider knows how to create instance from a factory function. 130 | // - all the state is immutable 131 | class FactoryProvider { 132 | constructor(factoryFunction, params, isPromise) { 133 | this.provider = factoryFunction; 134 | this.params = params; 135 | this.isPromise = isPromise; 136 | 137 | for (var param of params) { 138 | if (param.token === SuperConstructorAnnotation) { 139 | throw new Error(`${toString(factoryFunction)} is not a class. Only classes with a parent can ask for SuperConstructor!`); 140 | } 141 | } 142 | } 143 | 144 | create(args) { 145 | return this.provider.apply(undefined, args); 146 | } 147 | } 148 | 149 | 150 | export function createProviderFromFnOrClass(fnOrClass, annotations) { 151 | if (isClass(fnOrClass)) { 152 | return new ClassProvider(fnOrClass, annotations.params, annotations.provide.isPromise); 153 | } 154 | 155 | return new FactoryProvider(fnOrClass, annotations.params, annotations.provide.isPromise); 156 | } 157 | -------------------------------------------------------------------------------- /docs/di_sl.md: -------------------------------------------------------------------------------- 1 | This is my attempt to explain "Service Locator", "Dependency Injection" and "Module Loading", based on how I understand it. I'm not saying this is some ultimate truth. 2 | 3 | ### Service Locator 4 | 5 | ```js 6 | class Car { 7 | constructor(context) { 8 | this.engine = context.get('engine'); 9 | this.transmission = context.get('transmission'); 10 | } 11 | } 12 | ``` 13 | 14 | 15 | 16 | ### Dependency Injection 17 | 18 | ```js 19 | class Car { 20 | @Inject(Engine, Transmission) 21 | constructor(engine, transmission) { 22 | this.engine = engine; 23 | this.transmission = transmission; 24 | } 25 | } 26 | ``` 27 | 28 | 29 | ### Why is DI better? 30 | DI style is less coupled. If you wanna reuse `Car`, the only requirement is that there is something that acts as an engine and something that acts as a transmission. With the service locator, you need to make sure there is some context object with a `get` method that gives you these instances. This is not a terrible requirement, I know, but it is an additional requirement compare to the DI style. **DI style is cleaner, you can take the same code and use it without DI, assembling it manually.** 31 | 32 | Service locator breaks the [Law of Demeter]. `Car` needs an engine and a transmission. So why bother with a context object? `Car` does not need it. This makes testing slightly more painful. Instead of creating just mocks, you need to create also this "context object". 33 | 34 | With DI, the constructor serves as a documentation, you look at it and you know what you need in order to instantiate particular class. With service locator, the constructor basically lies to you. This can actually become a big deal. There are some big projects at Google using service locator pattern and I hate it! You spend so much time trying to figure out how to instantiate something you wanna use, because you have to scan the entire code to see what it really needs. Remember that the context can be passed around and so even a dependency can call `get()` on it. I wasted so much time with this. 35 | 36 | Also, DI is declarative and that makes it easier for tooling. For instance checking the dependency graph statically will be much simpler than with the service locator. 37 | 38 | I think it's easy to see that for describing depenendecies, declarative is better than imperative. Imperative code is good for describing behaviors not structures. 39 | 40 | That said, service locator is much better than looking up dependencies in the global state, as it is more clear what the context is. Looking up dependencies in the global state is essentially a service locator pattern too - where the context is global state and its interface is not that clear. 41 | 42 | I tried to explain why Dependency Injection is good in my [ng-conf talk] (in the first part). There is also the [testability interview repo] where I tried to show some of these problems. 43 | 44 | 45 | 46 | ### Module Loader (eg. RequireJS or ES6 module loader) 47 | The purpose of a module loader is to load source files (locate them on the disk or network, fetch, evaluate, etc). I would call this "static dependencies" - one should be able to resolve them statically, without evaluating the code. DI resolves runtime dependencies between concrete instances. Thus DI and module loader should both work together and it is not a question of which one is better. 48 | 49 | The problem is that many people are over-using module loaders (mostly RequireJS in the client and CommonJS in Node.js) for both "static" and "runtime" dependencies. In a more static environment (eg. C, Java, Dart), this is not even possible. It won't be possible with [ES6 modules] either. 50 | 51 | If you wanna use just moduler loader (both for "static" and "runtime" dependencies), you need to keep state in these modules. Otherwise you won't be able to share instances (dependencies) between multiple objects (dependents). 52 | 53 | This is very tricky to test. You need to clean up the state after each test, otherwise you get into troubles such as "this test is passing on its own, but failing when run in the whole suite". It is also very hard to mock things out. 54 | 55 | These issues are caused by 1/ not having acces inside the module and 2/ modules being cached. Of course, JS is a very dynamic environment and so there are workarounds/hacks. There are different workarounds (see [proxyquire](https://github.com/thlorenz/proxyquire), [mockery](https://github.com/mfncooper/mockery), [rewire](https://github.com/jhnns/rewire), [injectr](https://github.com/nathanmacinnes/injectr), [squire](https://github.com/iammerrick/Squire.js/) and many more), but the point is more less always the same - make it possible to override stuff inside a module. I've done this too (see [Testing Private State and Mocking Dependencies]). I still use this technique in Karma. I hate it! I wish I never did it and can't wait to refactor it out. It causes so many subtle issues that are so hard to debug. I wasted so much time with this! 56 | 57 | So Karma uses one of these hacks, `loadFile()` from [node-mocks], which evaluates the module again and again, so that you can pass in different mocks. It also exposes module internals. Here are some concrete issues with this approach. Well, just those I can remember right now: 58 | 59 | 1. You can't override the top level variables once module is instantiated (well, you can but it won't work the way you would expect), and so you end up putting stuff into object containers so that you can do it (after wasting some time to figure out why it silently did not work). 60 | 2. Stuff like `instanceof` don't work, as there are many "classes" of the same thing (because you evaluated the code many times) and in order to make instanceof to work, you have to compare the same references. Again, you will waste some time first to figure out why this is the case. 61 | 3. Tests are slow, because each module is executed many times (of course it also takes much more memory). 62 | 4. It's ugly. 63 | 5. Feels wrong. 64 | 65 | I'm pretty sure there are many other issues that I have succesfully forgotten. Anyway, you can probably achieve all the stuff you need with it. You will just have to use some of these workarounds and will probably waste some time debugging issues caused by these worarounds. **The DI approach is cleaner and you don't need any workarounds. It's true that bringing a DI framework into your app also brings some additional complexity. I use DI framework because I think this additional complexity is worthy making my code cleaner.** I also want to make [di.js] as simple and transparent as possible, in order to minimize this additional cost. 66 | 67 | So the best way that works for me right now is something like this: Modules are stateless. They only contain definitions of classes/functions/constants. I use module loader to load all the modules my app needs and Dependency Injection framework for assembling the app (resolving the runtime dependencies) based on declarative annotations. 68 | 69 | That said, everybody can use whatever works for them ;-) 70 | 71 | 72 | [Law of Demeter]: http://en.wikipedia.org/wiki/Law_of_Demeter 73 | [ng-conf talk]: http://www.youtube.com/watch?v=_OGGsf1ZXMs 74 | [testability interview repo]: https://github.com/vojtajina/testability-interview 75 | [ES6 modules]: http://wiki.ecmascript.org/doku.php?id=harmony:modules 76 | [Testing Private State and Mocking Dependencies]: http://howtonode.org/testing-private-state-and-mocking-deps 77 | [node-mocks]: https://github.com/vojtajina/node-mocks 78 | [di.js]: https://github.com/angular/di.js 79 | -------------------------------------------------------------------------------- /visualize/injectors/kitchen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Injector 1Kitchen (Injector 1)KitchenCoffeeMaker (Injector 1)CoffeeMakerGrinder (Injector 1)GrinderElectricity (Injector 1)ElectricityPump (Injector 1)PumpHeater (Injector 1)HeaterSkillet (Injector 1)SkilletStove (Injector 1)StoveFridge (Injector 1)FridgeDishwasher (Injector 1)Dishwasher 4 | -------------------------------------------------------------------------------- /dist/cjs/injector.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperties(exports, { 3 | Injector: {get: function() { 4 | return Injector; 5 | }}, 6 | __esModule: {value: true} 7 | }); 8 | var $__annotations__, 9 | $__util__, 10 | $__profiler__, 11 | $__providers__; 12 | var $__0 = ($__annotations__ = require("./annotations"), $__annotations__ && $__annotations__.__esModule && $__annotations__ || {default: $__annotations__}), 13 | annotate = $__0.annotate, 14 | readAnnotations = $__0.readAnnotations, 15 | hasAnnotation = $__0.hasAnnotation, 16 | ProvideAnnotation = $__0.Provide, 17 | TransientScopeAnnotation = $__0.TransientScope; 18 | var $__1 = ($__util__ = require("./util"), $__util__ && $__util__.__esModule && $__util__ || {default: $__util__}), 19 | isFunction = $__1.isFunction, 20 | toString = $__1.toString; 21 | var profileInjector = ($__profiler__ = require("./profiler"), $__profiler__ && $__profiler__.__esModule && $__profiler__ || {default: $__profiler__}).profileInjector; 22 | var createProviderFromFnOrClass = ($__providers__ = require("./providers"), $__providers__ && $__providers__.__esModule && $__providers__ || {default: $__providers__}).createProviderFromFnOrClass; 23 | function constructResolvingMessage(resolving, token) { 24 | if (arguments.length > 1) { 25 | resolving.push(token); 26 | } 27 | if (resolving.length > 1) { 28 | return (" (" + resolving.map(toString).join(' -> ') + ")"); 29 | } 30 | return ''; 31 | } 32 | var Injector = function Injector() { 33 | var modules = arguments[0] !== (void 0) ? arguments[0] : []; 34 | var parentInjector = arguments[1] !== (void 0) ? arguments[1] : null; 35 | var providers = arguments[2] !== (void 0) ? arguments[2] : new Map(); 36 | var scopes = arguments[3] !== (void 0) ? arguments[3] : []; 37 | this._cache = new Map(); 38 | this._providers = providers; 39 | this._parent = parentInjector; 40 | this._scopes = scopes; 41 | this._loadModules(modules); 42 | profileInjector(this, $Injector); 43 | }; 44 | var $Injector = Injector; 45 | ($traceurRuntime.createClass)(Injector, { 46 | _collectProvidersWithAnnotation: function(annotationClass, collectedProviders) { 47 | this._providers.forEach((function(provider, token) { 48 | if (!collectedProviders.has(token) && hasAnnotation(provider.provider, annotationClass)) { 49 | collectedProviders.set(token, provider); 50 | } 51 | })); 52 | if (this._parent) { 53 | this._parent._collectProvidersWithAnnotation(annotationClass, collectedProviders); 54 | } 55 | }, 56 | _loadModules: function(modules) { 57 | for (var $__6 = modules[Symbol.iterator](), 58 | $__7; !($__7 = $__6.next()).done; ) { 59 | var module = $__7.value; 60 | { 61 | if (isFunction(module)) { 62 | this._loadFnOrClass(module); 63 | continue; 64 | } 65 | throw new Error('Invalid module!'); 66 | } 67 | } 68 | }, 69 | _loadFnOrClass: function(fnOrClass) { 70 | var annotations = readAnnotations(fnOrClass); 71 | var token = annotations.provide.token || fnOrClass; 72 | var provider = createProviderFromFnOrClass(fnOrClass, annotations); 73 | this._providers.set(token, provider); 74 | }, 75 | _hasProviderFor: function(token) { 76 | if (this._providers.has(token)) { 77 | return true; 78 | } 79 | if (this._parent) { 80 | return this._parent._hasProviderFor(token); 81 | } 82 | return false; 83 | }, 84 | _instantiateDefaultProvider: function(provider, token, resolving, wantPromise, wantLazy) { 85 | if (!this._parent) { 86 | this._providers.set(token, provider); 87 | return this.get(token, resolving, wantPromise, wantLazy); 88 | } 89 | for (var $__6 = this._scopes[Symbol.iterator](), 90 | $__7; !($__7 = $__6.next()).done; ) { 91 | var ScopeClass = $__7.value; 92 | { 93 | if (hasAnnotation(provider.provider, ScopeClass)) { 94 | this._providers.set(token, provider); 95 | return this.get(token, resolving, wantPromise, wantLazy); 96 | } 97 | } 98 | } 99 | return this._parent._instantiateDefaultProvider(provider, token, resolving, wantPromise, wantLazy); 100 | }, 101 | get: function(token) { 102 | var resolving = arguments[1] !== (void 0) ? arguments[1] : []; 103 | var wantPromise = arguments[2] !== (void 0) ? arguments[2] : false; 104 | var wantLazy = arguments[3] !== (void 0) ? arguments[3] : false; 105 | var $__4 = this; 106 | var resolvingMsg = ''; 107 | var provider; 108 | var instance; 109 | var injector = this; 110 | if (token === null || token === undefined) { 111 | resolvingMsg = constructResolvingMessage(resolving, token); 112 | throw new Error(("Invalid token \"" + token + "\" requested!" + resolvingMsg)); 113 | } 114 | if (token === $Injector) { 115 | if (wantPromise) { 116 | return Promise.resolve(this); 117 | } 118 | return this; 119 | } 120 | if (wantLazy) { 121 | return function createLazyInstance() { 122 | var lazyInjector = injector; 123 | if (arguments.length) { 124 | var locals = []; 125 | var args = arguments; 126 | for (var i = 0; i < args.length; i += 2) { 127 | locals.push((function(ii) { 128 | var fn = function createLocalInstance() { 129 | return args[ii + 1]; 130 | }; 131 | annotate(fn, new ProvideAnnotation(args[ii])); 132 | return fn; 133 | })(i)); 134 | } 135 | lazyInjector = injector.createChild(locals); 136 | } 137 | return lazyInjector.get(token, resolving, wantPromise, false); 138 | }; 139 | } 140 | if (this._cache.has(token)) { 141 | instance = this._cache.get(token); 142 | provider = this._providers.get(token); 143 | if (provider.isPromise && !wantPromise) { 144 | resolvingMsg = constructResolvingMessage(resolving, token); 145 | throw new Error(("Cannot instantiate " + toString(token) + " synchronously. It is provided as a promise!" + resolvingMsg)); 146 | } 147 | if (!provider.isPromise && wantPromise) { 148 | return Promise.resolve(instance); 149 | } 150 | return instance; 151 | } 152 | provider = this._providers.get(token); 153 | if (!provider && isFunction(token) && !this._hasProviderFor(token)) { 154 | provider = createProviderFromFnOrClass(token, readAnnotations(token)); 155 | return this._instantiateDefaultProvider(provider, token, resolving, wantPromise, wantLazy); 156 | } 157 | if (!provider) { 158 | if (!this._parent) { 159 | resolvingMsg = constructResolvingMessage(resolving, token); 160 | throw new Error(("No provider for " + toString(token) + "!" + resolvingMsg)); 161 | } 162 | return this._parent.get(token, resolving, wantPromise, wantLazy); 163 | } 164 | if (resolving.indexOf(token) !== -1) { 165 | resolvingMsg = constructResolvingMessage(resolving, token); 166 | throw new Error(("Cannot instantiate cyclic dependency!" + resolvingMsg)); 167 | } 168 | resolving.push(token); 169 | var delayingInstantiation = wantPromise && provider.params.some((function(param) { 170 | return !param.isPromise; 171 | })); 172 | var args = provider.params.map((function(param) { 173 | if (delayingInstantiation) { 174 | return $__4.get(param.token, resolving, true, param.isLazy); 175 | } 176 | return $__4.get(param.token, resolving, param.isPromise, param.isLazy); 177 | })); 178 | if (delayingInstantiation) { 179 | var delayedResolving = resolving.slice(); 180 | resolving.pop(); 181 | return Promise.all(args).then(function(args) { 182 | try { 183 | instance = provider.create(args); 184 | } catch (e) { 185 | resolvingMsg = constructResolvingMessage(delayedResolving); 186 | var originalMsg = 'ORIGINAL ERROR: ' + e.message; 187 | e.message = ("Error during instantiation of " + toString(token) + "!" + resolvingMsg + "\n" + originalMsg); 188 | throw e; 189 | } 190 | if (!hasAnnotation(provider.provider, TransientScopeAnnotation)) { 191 | injector._cache.set(token, instance); 192 | } 193 | return instance; 194 | }); 195 | } 196 | try { 197 | instance = provider.create(args); 198 | } catch (e) { 199 | resolvingMsg = constructResolvingMessage(resolving); 200 | var originalMsg = 'ORIGINAL ERROR: ' + e.message; 201 | e.message = ("Error during instantiation of " + toString(token) + "!" + resolvingMsg + "\n" + originalMsg); 202 | throw e; 203 | } 204 | if (!hasAnnotation(provider.provider, TransientScopeAnnotation)) { 205 | this._cache.set(token, instance); 206 | } 207 | if (!wantPromise && provider.isPromise) { 208 | resolvingMsg = constructResolvingMessage(resolving); 209 | throw new Error(("Cannot instantiate " + toString(token) + " synchronously. It is provided as a promise!" + resolvingMsg)); 210 | } 211 | if (wantPromise && !provider.isPromise) { 212 | instance = Promise.resolve(instance); 213 | } 214 | resolving.pop(); 215 | return instance; 216 | }, 217 | getPromise: function(token) { 218 | return this.get(token, [], true); 219 | }, 220 | createChild: function() { 221 | var modules = arguments[0] !== (void 0) ? arguments[0] : []; 222 | var forceNewInstancesOf = arguments[1] !== (void 0) ? arguments[1] : []; 223 | var forcedProviders = new Map(); 224 | forceNewInstancesOf.push(TransientScopeAnnotation); 225 | for (var $__6 = forceNewInstancesOf[Symbol.iterator](), 226 | $__7; !($__7 = $__6.next()).done; ) { 227 | var annotation = $__7.value; 228 | { 229 | this._collectProvidersWithAnnotation(annotation, forcedProviders); 230 | } 231 | } 232 | return new $Injector(modules, this, forcedProviders, forceNewInstancesOf); 233 | } 234 | }, {}); 235 | ; 236 | -------------------------------------------------------------------------------- /dist/amd/injector.js: -------------------------------------------------------------------------------- 1 | define(['./annotations', './util', './profiler', './providers'], function($__0,$__2,$__4,$__6) { 2 | "use strict"; 3 | if (!$__0 || !$__0.__esModule) 4 | $__0 = {default: $__0}; 5 | if (!$__2 || !$__2.__esModule) 6 | $__2 = {default: $__2}; 7 | if (!$__4 || !$__4.__esModule) 8 | $__4 = {default: $__4}; 9 | if (!$__6 || !$__6.__esModule) 10 | $__6 = {default: $__6}; 11 | var $__1 = $__0, 12 | annotate = $__1.annotate, 13 | readAnnotations = $__1.readAnnotations, 14 | hasAnnotation = $__1.hasAnnotation, 15 | ProvideAnnotation = $__1.Provide, 16 | TransientScopeAnnotation = $__1.TransientScope; 17 | var $__3 = $__2, 18 | isFunction = $__3.isFunction, 19 | toString = $__3.toString; 20 | var profileInjector = $__4.profileInjector; 21 | var createProviderFromFnOrClass = $__6.createProviderFromFnOrClass; 22 | function constructResolvingMessage(resolving, token) { 23 | if (arguments.length > 1) { 24 | resolving.push(token); 25 | } 26 | if (resolving.length > 1) { 27 | return (" (" + resolving.map(toString).join(' -> ') + ")"); 28 | } 29 | return ''; 30 | } 31 | var Injector = function Injector() { 32 | var modules = arguments[0] !== (void 0) ? arguments[0] : []; 33 | var parentInjector = arguments[1] !== (void 0) ? arguments[1] : null; 34 | var providers = arguments[2] !== (void 0) ? arguments[2] : new Map(); 35 | var scopes = arguments[3] !== (void 0) ? arguments[3] : []; 36 | this._cache = new Map(); 37 | this._providers = providers; 38 | this._parent = parentInjector; 39 | this._scopes = scopes; 40 | this._loadModules(modules); 41 | profileInjector(this, $Injector); 42 | }; 43 | var $Injector = Injector; 44 | ($traceurRuntime.createClass)(Injector, { 45 | _collectProvidersWithAnnotation: function(annotationClass, collectedProviders) { 46 | this._providers.forEach((function(provider, token) { 47 | if (!collectedProviders.has(token) && hasAnnotation(provider.provider, annotationClass)) { 48 | collectedProviders.set(token, provider); 49 | } 50 | })); 51 | if (this._parent) { 52 | this._parent._collectProvidersWithAnnotation(annotationClass, collectedProviders); 53 | } 54 | }, 55 | _loadModules: function(modules) { 56 | for (var $__10 = modules[Symbol.iterator](), 57 | $__11; !($__11 = $__10.next()).done; ) { 58 | var module = $__11.value; 59 | { 60 | if (isFunction(module)) { 61 | this._loadFnOrClass(module); 62 | continue; 63 | } 64 | throw new Error('Invalid module!'); 65 | } 66 | } 67 | }, 68 | _loadFnOrClass: function(fnOrClass) { 69 | var annotations = readAnnotations(fnOrClass); 70 | var token = annotations.provide.token || fnOrClass; 71 | var provider = createProviderFromFnOrClass(fnOrClass, annotations); 72 | this._providers.set(token, provider); 73 | }, 74 | _hasProviderFor: function(token) { 75 | if (this._providers.has(token)) { 76 | return true; 77 | } 78 | if (this._parent) { 79 | return this._parent._hasProviderFor(token); 80 | } 81 | return false; 82 | }, 83 | _instantiateDefaultProvider: function(provider, token, resolving, wantPromise, wantLazy) { 84 | if (!this._parent) { 85 | this._providers.set(token, provider); 86 | return this.get(token, resolving, wantPromise, wantLazy); 87 | } 88 | for (var $__10 = this._scopes[Symbol.iterator](), 89 | $__11; !($__11 = $__10.next()).done; ) { 90 | var ScopeClass = $__11.value; 91 | { 92 | if (hasAnnotation(provider.provider, ScopeClass)) { 93 | this._providers.set(token, provider); 94 | return this.get(token, resolving, wantPromise, wantLazy); 95 | } 96 | } 97 | } 98 | return this._parent._instantiateDefaultProvider(provider, token, resolving, wantPromise, wantLazy); 99 | }, 100 | get: function(token) { 101 | var resolving = arguments[1] !== (void 0) ? arguments[1] : []; 102 | var wantPromise = arguments[2] !== (void 0) ? arguments[2] : false; 103 | var wantLazy = arguments[3] !== (void 0) ? arguments[3] : false; 104 | var $__8 = this; 105 | var resolvingMsg = ''; 106 | var provider; 107 | var instance; 108 | var injector = this; 109 | if (token === null || token === undefined) { 110 | resolvingMsg = constructResolvingMessage(resolving, token); 111 | throw new Error(("Invalid token \"" + token + "\" requested!" + resolvingMsg)); 112 | } 113 | if (token === $Injector) { 114 | if (wantPromise) { 115 | return Promise.resolve(this); 116 | } 117 | return this; 118 | } 119 | if (wantLazy) { 120 | return function createLazyInstance() { 121 | var lazyInjector = injector; 122 | if (arguments.length) { 123 | var locals = []; 124 | var args = arguments; 125 | for (var i = 0; i < args.length; i += 2) { 126 | locals.push((function(ii) { 127 | var fn = function createLocalInstance() { 128 | return args[ii + 1]; 129 | }; 130 | annotate(fn, new ProvideAnnotation(args[ii])); 131 | return fn; 132 | })(i)); 133 | } 134 | lazyInjector = injector.createChild(locals); 135 | } 136 | return lazyInjector.get(token, resolving, wantPromise, false); 137 | }; 138 | } 139 | if (this._cache.has(token)) { 140 | instance = this._cache.get(token); 141 | provider = this._providers.get(token); 142 | if (provider.isPromise && !wantPromise) { 143 | resolvingMsg = constructResolvingMessage(resolving, token); 144 | throw new Error(("Cannot instantiate " + toString(token) + " synchronously. It is provided as a promise!" + resolvingMsg)); 145 | } 146 | if (!provider.isPromise && wantPromise) { 147 | return Promise.resolve(instance); 148 | } 149 | return instance; 150 | } 151 | provider = this._providers.get(token); 152 | if (!provider && isFunction(token) && !this._hasProviderFor(token)) { 153 | provider = createProviderFromFnOrClass(token, readAnnotations(token)); 154 | return this._instantiateDefaultProvider(provider, token, resolving, wantPromise, wantLazy); 155 | } 156 | if (!provider) { 157 | if (!this._parent) { 158 | resolvingMsg = constructResolvingMessage(resolving, token); 159 | throw new Error(("No provider for " + toString(token) + "!" + resolvingMsg)); 160 | } 161 | return this._parent.get(token, resolving, wantPromise, wantLazy); 162 | } 163 | if (resolving.indexOf(token) !== -1) { 164 | resolvingMsg = constructResolvingMessage(resolving, token); 165 | throw new Error(("Cannot instantiate cyclic dependency!" + resolvingMsg)); 166 | } 167 | resolving.push(token); 168 | var delayingInstantiation = wantPromise && provider.params.some((function(param) { 169 | return !param.isPromise; 170 | })); 171 | var args = provider.params.map((function(param) { 172 | if (delayingInstantiation) { 173 | return $__8.get(param.token, resolving, true, param.isLazy); 174 | } 175 | return $__8.get(param.token, resolving, param.isPromise, param.isLazy); 176 | })); 177 | if (delayingInstantiation) { 178 | var delayedResolving = resolving.slice(); 179 | resolving.pop(); 180 | return Promise.all(args).then(function(args) { 181 | try { 182 | instance = provider.create(args); 183 | } catch (e) { 184 | resolvingMsg = constructResolvingMessage(delayedResolving); 185 | var originalMsg = 'ORIGINAL ERROR: ' + e.message; 186 | e.message = ("Error during instantiation of " + toString(token) + "!" + resolvingMsg + "\n" + originalMsg); 187 | throw e; 188 | } 189 | if (!hasAnnotation(provider.provider, TransientScopeAnnotation)) { 190 | injector._cache.set(token, instance); 191 | } 192 | return instance; 193 | }); 194 | } 195 | try { 196 | instance = provider.create(args); 197 | } catch (e) { 198 | resolvingMsg = constructResolvingMessage(resolving); 199 | var originalMsg = 'ORIGINAL ERROR: ' + e.message; 200 | e.message = ("Error during instantiation of " + toString(token) + "!" + resolvingMsg + "\n" + originalMsg); 201 | throw e; 202 | } 203 | if (!hasAnnotation(provider.provider, TransientScopeAnnotation)) { 204 | this._cache.set(token, instance); 205 | } 206 | if (!wantPromise && provider.isPromise) { 207 | resolvingMsg = constructResolvingMessage(resolving); 208 | throw new Error(("Cannot instantiate " + toString(token) + " synchronously. It is provided as a promise!" + resolvingMsg)); 209 | } 210 | if (wantPromise && !provider.isPromise) { 211 | instance = Promise.resolve(instance); 212 | } 213 | resolving.pop(); 214 | return instance; 215 | }, 216 | getPromise: function(token) { 217 | return this.get(token, [], true); 218 | }, 219 | createChild: function() { 220 | var modules = arguments[0] !== (void 0) ? arguments[0] : []; 221 | var forceNewInstancesOf = arguments[1] !== (void 0) ? arguments[1] : []; 222 | var forcedProviders = new Map(); 223 | forceNewInstancesOf.push(TransientScopeAnnotation); 224 | for (var $__10 = forceNewInstancesOf[Symbol.iterator](), 225 | $__11; !($__11 = $__10.next()).done; ) { 226 | var annotation = $__11.value; 227 | { 228 | this._collectProvidersWithAnnotation(annotation, forcedProviders); 229 | } 230 | } 231 | return new $Injector(modules, this, forcedProviders, forceNewInstancesOf); 232 | } 233 | }, {}); 234 | ; 235 | return { 236 | get Injector() { 237 | return Injector; 238 | }, 239 | __esModule: true 240 | }; 241 | }); 242 | -------------------------------------------------------------------------------- /src/injector.js: -------------------------------------------------------------------------------- 1 | import { 2 | annotate, 3 | readAnnotations, 4 | hasAnnotation, 5 | Provide as ProvideAnnotation, 6 | TransientScope as TransientScopeAnnotation 7 | } from './annotations'; 8 | import {isFunction, toString} from './util'; 9 | import {profileInjector} from './profiler'; 10 | import {createProviderFromFnOrClass} from './providers'; 11 | 12 | 13 | function constructResolvingMessage(resolving, token) { 14 | // If a token is passed in, add it into the resolving array. 15 | // We need to check arguments.length because it can be null/undefined. 16 | if (arguments.length > 1) { 17 | resolving.push(token); 18 | } 19 | 20 | if (resolving.length > 1) { 21 | return ` (${resolving.map(toString).join(' -> ')})`; 22 | } 23 | 24 | return ''; 25 | } 26 | 27 | 28 | // Injector encapsulate a life scope. 29 | // There is exactly one instance for given token in given injector. 30 | // 31 | // All the state is immutable, the only state changes is the cache. There is however no way to produce different instance under given token. In that sense it is immutable. 32 | // 33 | // Injector is responsible for: 34 | // - resolving tokens into 35 | // - provider 36 | // - value (cache/calling provider) 37 | // - dealing with isPromise 38 | // - dealing with isLazy 39 | // - loading different "providers" and modules 40 | class Injector { 41 | 42 | constructor(modules = [], parentInjector = null, providers = new Map(), scopes = []) { 43 | this._cache = new Map(); 44 | this._providers = providers; 45 | this._parent = parentInjector; 46 | this._scopes = scopes; 47 | 48 | this._loadModules(modules); 49 | 50 | profileInjector(this, Injector); 51 | } 52 | 53 | 54 | // Collect all registered providers that has given annotation. 55 | // Including providers defined in parent injectors. 56 | _collectProvidersWithAnnotation(annotationClass, collectedProviders) { 57 | this._providers.forEach((provider, token) => { 58 | if (!collectedProviders.has(token) && hasAnnotation(provider.provider, annotationClass)) { 59 | collectedProviders.set(token, provider); 60 | } 61 | }); 62 | 63 | if (this._parent) { 64 | this._parent._collectProvidersWithAnnotation(annotationClass, collectedProviders); 65 | } 66 | } 67 | 68 | 69 | // Load modules/function/classes. 70 | // This mutates `this._providers`, but it is only called during the constructor. 71 | _loadModules(modules) { 72 | for (var module of modules) { 73 | // A single provider (class or function). 74 | if (isFunction(module)) { 75 | this._loadFnOrClass(module); 76 | continue; 77 | } 78 | 79 | throw new Error('Invalid module!'); 80 | } 81 | } 82 | 83 | 84 | // Load a function or class. 85 | // This mutates `this._providers`, but it is only called during the constructor. 86 | _loadFnOrClass(fnOrClass) { 87 | // TODO(vojta): should we expose provider.token? 88 | var annotations = readAnnotations(fnOrClass); 89 | var token = annotations.provide.token || fnOrClass; 90 | var provider = createProviderFromFnOrClass(fnOrClass, annotations); 91 | 92 | this._providers.set(token, provider); 93 | } 94 | 95 | 96 | // Returns true if there is any provider registered for given token. 97 | // Including parent injectors. 98 | _hasProviderFor(token) { 99 | if (this._providers.has(token)) { 100 | return true; 101 | } 102 | 103 | if (this._parent) { 104 | return this._parent._hasProviderFor(token); 105 | } 106 | 107 | return false; 108 | } 109 | 110 | // Find the correct injector where the default provider should be instantiated and cached. 111 | _instantiateDefaultProvider(provider, token, resolving, wantPromise, wantLazy) { 112 | // In root injector, instantiate here. 113 | if (!this._parent) { 114 | this._providers.set(token, provider); 115 | return this.get(token, resolving, wantPromise, wantLazy); 116 | } 117 | 118 | // Check if this injector forces new instance of this provider. 119 | for (var ScopeClass of this._scopes) { 120 | if (hasAnnotation(provider.provider, ScopeClass)) { 121 | this._providers.set(token, provider); 122 | return this.get(token, resolving, wantPromise, wantLazy); 123 | } 124 | } 125 | 126 | // Otherwise ask parent injector. 127 | return this._parent._instantiateDefaultProvider(provider, token, resolving, wantPromise, wantLazy); 128 | } 129 | 130 | 131 | // Return an instance for given token. 132 | get(token, resolving = [], wantPromise = false, wantLazy = false) { 133 | var resolvingMsg = ''; 134 | var provider; 135 | var instance; 136 | var injector = this; 137 | 138 | if (token === null || token === undefined) { 139 | resolvingMsg = constructResolvingMessage(resolving, token); 140 | throw new Error(`Invalid token "${token}" requested!${resolvingMsg}`); 141 | } 142 | 143 | // Special case, return itself. 144 | if (token === Injector) { 145 | if (wantPromise) { 146 | return Promise.resolve(this); 147 | } 148 | 149 | return this; 150 | } 151 | 152 | // TODO(vojta): optimize - no child injector for locals? 153 | if (wantLazy) { 154 | return function createLazyInstance() { 155 | var lazyInjector = injector; 156 | 157 | if (arguments.length) { 158 | var locals = []; 159 | var args = arguments; 160 | 161 | for (var i = 0; i < args.length; i += 2) { 162 | locals.push((function(ii) { 163 | var fn = function createLocalInstance() { 164 | return args[ii + 1]; 165 | }; 166 | 167 | annotate(fn, new ProvideAnnotation(args[ii])); 168 | 169 | return fn; 170 | })(i)); 171 | } 172 | 173 | lazyInjector = injector.createChild(locals); 174 | } 175 | 176 | return lazyInjector.get(token, resolving, wantPromise, false); 177 | }; 178 | } 179 | 180 | // Check if there is a cached instance already. 181 | if (this._cache.has(token)) { 182 | instance = this._cache.get(token); 183 | provider = this._providers.get(token); 184 | 185 | if (provider.isPromise && !wantPromise) { 186 | resolvingMsg = constructResolvingMessage(resolving, token); 187 | throw new Error(`Cannot instantiate ${toString(token)} synchronously. It is provided as a promise!${resolvingMsg}`); 188 | } 189 | 190 | if (!provider.isPromise && wantPromise) { 191 | return Promise.resolve(instance); 192 | } 193 | 194 | return instance; 195 | } 196 | 197 | provider = this._providers.get(token); 198 | 199 | // No provider defined (overridden), use the default provider (token). 200 | if (!provider && isFunction(token) && !this._hasProviderFor(token)) { 201 | provider = createProviderFromFnOrClass(token, readAnnotations(token)); 202 | return this._instantiateDefaultProvider(provider, token, resolving, wantPromise, wantLazy); 203 | } 204 | 205 | if (!provider) { 206 | if (!this._parent) { 207 | resolvingMsg = constructResolvingMessage(resolving, token); 208 | throw new Error(`No provider for ${toString(token)}!${resolvingMsg}`); 209 | } 210 | 211 | return this._parent.get(token, resolving, wantPromise, wantLazy); 212 | } 213 | 214 | if (resolving.indexOf(token) !== -1) { 215 | resolvingMsg = constructResolvingMessage(resolving, token); 216 | throw new Error(`Cannot instantiate cyclic dependency!${resolvingMsg}`); 217 | } 218 | 219 | resolving.push(token); 220 | 221 | // TODO(vojta): handle these cases: 222 | // 1/ 223 | // - requested as promise (delayed) 224 | // - requested again as promise (before the previous gets resolved) -> cache the promise 225 | // 2/ 226 | // - requested as promise (delayed) 227 | // - requested again sync (before the previous gets resolved) 228 | // -> error, but let it go inside to throw where exactly is the async provider 229 | var delayingInstantiation = wantPromise && provider.params.some((param) => !param.isPromise); 230 | var args = provider.params.map((param) => { 231 | 232 | if (delayingInstantiation) { 233 | return this.get(param.token, resolving, true, param.isLazy); 234 | } 235 | 236 | return this.get(param.token, resolving, param.isPromise, param.isLazy); 237 | }); 238 | 239 | // Delaying the instantiation - return a promise. 240 | if (delayingInstantiation) { 241 | var delayedResolving = resolving.slice(); // clone 242 | 243 | resolving.pop(); 244 | 245 | // Once all dependencies (promises) are resolved, instantiate. 246 | return Promise.all(args).then(function(args) { 247 | try { 248 | instance = provider.create(args); 249 | } catch (e) { 250 | resolvingMsg = constructResolvingMessage(delayedResolving); 251 | var originalMsg = 'ORIGINAL ERROR: ' + e.message; 252 | e.message = `Error during instantiation of ${toString(token)}!${resolvingMsg}\n${originalMsg}`; 253 | throw e; 254 | } 255 | 256 | if (!hasAnnotation(provider.provider, TransientScopeAnnotation)) { 257 | injector._cache.set(token, instance); 258 | } 259 | 260 | // TODO(vojta): if a provider returns a promise (but is not declared as @ProvidePromise), 261 | // here the value will get unwrapped (because it is returned from a promise callback) and 262 | // the actual value will be injected. This is probably not desired behavior. Maybe we could 263 | // get rid off the @ProvidePromise and just check the returned value, whether it is 264 | // a promise or not. 265 | return instance; 266 | }); 267 | } 268 | 269 | try { 270 | instance = provider.create(args); 271 | } catch (e) { 272 | resolvingMsg = constructResolvingMessage(resolving); 273 | var originalMsg = 'ORIGINAL ERROR: ' + e.message; 274 | e.message = `Error during instantiation of ${toString(token)}!${resolvingMsg}\n${originalMsg}`; 275 | throw e; 276 | } 277 | 278 | if (!hasAnnotation(provider.provider, TransientScopeAnnotation)) { 279 | this._cache.set(token, instance); 280 | } 281 | 282 | if (!wantPromise && provider.isPromise) { 283 | resolvingMsg = constructResolvingMessage(resolving); 284 | 285 | throw new Error(`Cannot instantiate ${toString(token)} synchronously. It is provided as a promise!${resolvingMsg}`); 286 | } 287 | 288 | if (wantPromise && !provider.isPromise) { 289 | instance = Promise.resolve(instance); 290 | } 291 | 292 | resolving.pop(); 293 | 294 | return instance; 295 | } 296 | 297 | 298 | getPromise(token) { 299 | return this.get(token, [], true); 300 | } 301 | 302 | 303 | // Create a child injector, which encapsulate shorter life scope. 304 | // It is possible to add additional providers and also force new instances of existing providers. 305 | createChild(modules = [], forceNewInstancesOf = []) { 306 | var forcedProviders = new Map(); 307 | 308 | // Always force new instance of TransientScope. 309 | forceNewInstancesOf.push(TransientScopeAnnotation); 310 | 311 | for (var annotation of forceNewInstancesOf) { 312 | this._collectProvidersWithAnnotation(annotation, forcedProviders); 313 | } 314 | 315 | return new Injector(modules, this, forcedProviders, forceNewInstancesOf); 316 | } 317 | } 318 | 319 | 320 | export {Injector}; 321 | -------------------------------------------------------------------------------- /visualize/injectors/app.js: -------------------------------------------------------------------------------- 1 | // TODO: 2 | // - do not cancel highlight when hovering a selected link 3 | // - show overrided provider vs. forced instance 4 | // - click open code in the console (instance / injector) 5 | // - delay hover change (eg when quickly moving from a provider to another provider, acros a parent injector - do not highlight the injector) 6 | // - hovering provider name highlights all providers (with override links) 7 | // - hovering circle highlights only that circle, shows all dependencies (incoming outcoming; different color) 8 | 9 | 10 | var app = angular.module('di-visualiser', []); 11 | 12 | var GraphController = function($scope, $element, $attrs) { 13 | var width = parseInt($attrs.width, 10); 14 | var height = parseInt($attrs.height, 10); 15 | var r = $attrs.radius ? parseInt($attrs.radius, 10) : Math.min(width, height) - 10; 16 | 17 | var emitOnScope = function(name) { 18 | return function(item) { 19 | $scope.$apply(function() { 20 | $scope.$emit(name, item); 21 | }); 22 | }; 23 | }; 24 | 25 | var pack = d3.layout.pack() 26 | .size([r, r]) 27 | .padding(10) 28 | .value(function(d) { 29 | return d.dependencies ? (d.dependencies.length || 0.5) : 1; 30 | return 1; 31 | }); 32 | 33 | // Create SVG canvas. 34 | var svg = d3.select($element[0]) 35 | .insert('svg:svg') 36 | .attr('width', width) 37 | .attr('height', height) 38 | .append('svg:g') 39 | .attr('transform', 'translate(' + (width - r) / 2 + ',' + (height - r) / 2 + ')'); 40 | 41 | // Define markers. 42 | var markerWidth = 6, 43 | markerHeight = 6, 44 | refX = 3, 45 | refY = 0; 46 | 47 | svg.append('svg:defs').selectAll('marker') 48 | .data(['dependency-in', 'dependency-out']) 49 | .enter().append('svg:marker') 50 | .attr('id', String) 51 | .attr('viewBox', '0 -5 10 10') 52 | .attr('refX', refX) 53 | .attr('refY', refY) 54 | .attr('markerWidth', markerWidth) 55 | .attr('markerHeight', markerHeight) 56 | .attr('orient', 'auto') 57 | .append('svg:path') 58 | .attr('d', 'M0,-5L10,0L0,5'); 59 | 60 | var classWithTypeAndHighlighted = function(name) { 61 | return function(d) { 62 | var cls = name + ' ' + d.type; 63 | 64 | if (d.highlighted) { 65 | cls += ' highlighted'; 66 | if (typeof d.highlighted === 'string') { 67 | cls += ' ' + d.highlighted; 68 | } 69 | } 70 | 71 | return cls; 72 | }; 73 | }; 74 | 75 | var updateNodes = function(nodes) { 76 | var node = svg.selectAll('g.node') 77 | .data(nodes) 78 | 79 | // Update. 80 | .attr('class', classWithTypeAndHighlighted('node')) 81 | 82 | // New items. 83 | .enter().append('g') 84 | .attr('class', classWithTypeAndHighlighted('node')) 85 | .attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; }) 86 | .on('mouseover', emitOnScope('graph_mouseover')) 87 | .on('mouseout', emitOnScope('graph_mouseout')) 88 | .on('click', emitOnScope('graph_click')) 89 | 90 | node.append('svg:circle') 91 | .attr('r', function(d) { return d.r; }) 92 | 93 | node.append('svg:title') 94 | .text(function(d) { return d.title; }); 95 | node.append('text') 96 | .attr('dy', '.3em') 97 | .style('text-anchor', 'middle') 98 | .text(function(d) { return d.name ? d.name.substring(0, d.r / 3) : null; }); 99 | }; 100 | 101 | var linkStraightFromEdge = function(d) { 102 | var dx = d.target.x - d.source.x, 103 | dy = d.target.y - d.source.y, 104 | dr = Math.sqrt(dx * dx + dy * dy); 105 | 106 | var sx = d.source.x + (d.source.r * dx / dr), 107 | sy = d.source.y + (d.source.r * dy / dr), 108 | tx = d.target.x - (d.target.r * dx / dr), 109 | ty = d.target.y - (d.target.r * dy / dr); 110 | 111 | return 'M' + sx + ',' + sy + 'L' + tx + ',' + ty; 112 | }; 113 | 114 | var LABEL_HEIGHT = 15; 115 | var MARKER_SIZE = 10; 116 | var linkArcFromLabels = function(d) { 117 | var dx = d.target.x - d.source.x, 118 | dy = d.target.y - d.source.y, 119 | dr = Math.sqrt(dx * dx + dy * dy); 120 | 121 | var sx = d.source.x, 122 | sy = d.source.y, 123 | tx = d.target.x, 124 | ty = d.target.y; 125 | 126 | var idx; 127 | 128 | // source 129 | if ((idx = d.source.links.bottom.indexOf(d)) > -1) { 130 | // bottom 131 | sy = sy + LABEL_HEIGHT; 132 | sx = sx - d.source.links.bottom.length * MARKER_SIZE / 2 + MARKER_SIZE * idx; 133 | } else if ((idx = d.source.links.top.indexOf(d)) > -1) { 134 | // top 135 | sy = sy - LABEL_HEIGHT; 136 | sx = sx - d.source.links.top.length * MARKER_SIZE / 2 + MARKER_SIZE * idx; 137 | } 138 | 139 | // target 140 | if ((idx = d.target.links.bottom.indexOf(d)) > -1) { 141 | // bottom 142 | ty = ty + LABEL_HEIGHT; 143 | tx = tx - d.target.links.bottom.length * MARKER_SIZE / 2 + MARKER_SIZE * idx; 144 | } else if ((idx = d.target.links.top.indexOf(d)) > -1) { 145 | // top 146 | ty = ty - LABEL_HEIGHT; 147 | tx = tx - d.target.links.top.length * MARKER_SIZE / 2 + MARKER_SIZE * idx; 148 | } 149 | 150 | var curving = dx < 0 ? 0 : 1; 151 | 152 | 153 | return 'M' + sx + ',' + sy + 'A' + dr + ',' + dr + ' 0 0,' + curving + ' ' + tx + ',' + ty; 154 | }; 155 | 156 | var linkMarkerEnd = function (d) { 157 | if (d.type === 'dependency' && typeof d.highlighted === 'string') { 158 | return 'url(#dependency-' + d.highlighted + ')'; 159 | } 160 | 161 | return ''; 162 | }; 163 | 164 | 165 | var updateLinks = function(links) { 166 | svg.selectAll('.link') 167 | .data(links) 168 | // Update. 169 | .attr('class', classWithTypeAndHighlighted('link')) 170 | .attr('marker-end', linkMarkerEnd) 171 | // New items. 172 | .enter().append('path') 173 | .attr('class', classWithTypeAndHighlighted('link')) 174 | .attr('marker-end', linkMarkerEnd) 175 | .attr('d', function(d) { 176 | if (d.type === 'override') { 177 | return linkStraightFromEdge(d); 178 | } else { 179 | return linkArcFromLabels(d); 180 | } 181 | }) 182 | .attr('pointer-events', 'none') 183 | 184 | }; 185 | 186 | var SAME_LEVEL_RANGE = 30; 187 | var lastNodes, lastLinks; 188 | $scope.$on('graph_data_changed', function(_, root, links) { 189 | if (root) { 190 | // console.log('Layout...'); 191 | lastNodes = pack(root); 192 | } 193 | 194 | if (links) { 195 | // process links 196 | lastNodes.forEach(function(node) { 197 | node.links = { 198 | top: [], 199 | bottom: [], 200 | same: [] 201 | }; 202 | }); 203 | 204 | links.forEach(function(link) { 205 | if (link.type !== 'dependency') { 206 | return 207 | } 208 | 209 | var dy = link.target.y - link.source.y; 210 | 211 | if (Math.abs(dy) <= SAME_LEVEL_RANGE) { 212 | link.source.links.top.push(link); 213 | link.target.links.top.push(link); 214 | // link.source.links.same.push(link); 215 | // link.target.links.same.push(link); 216 | } else if (dy > 0) { 217 | link.source.links.bottom.push(link); 218 | link.target.links.top.push(link); 219 | } else if (dy < 0) { 220 | link.source.links.top.push(link); 221 | link.target.links.bottom.push(link); 222 | } 223 | }); 224 | 225 | lastNodes.forEach(function(node) { 226 | // Equally distribute links on the same level (into top/bottom). 227 | while (node.links.same.length) { 228 | if (node.links.top.length > node.links.bottom.length) { 229 | node.links.bottom.push(node.links.same.pop()); 230 | } else { 231 | node.links.top.push(node.links.same.pop()); 232 | } 233 | } 234 | 235 | // Sort left -> right. 236 | node.links.top.sort(function(a, b) { 237 | var otherNodeA = a.source === node ? a.target : a.source; 238 | var otherNodeB = b.source === node ? b.target : b.source; 239 | 240 | return otherNodeA.x > otherNodeB.x; 241 | }); 242 | 243 | node.links.bottom.sort(function(a, b) { 244 | var otherNodeA = a.source === node ? a.target : a.source; 245 | var otherNodeB = b.source === node ? b.target : b.source; 246 | 247 | return otherNodeA.x > otherNodeB.x; 248 | }); 249 | }); 250 | 251 | lastLinks = links; 252 | 253 | 254 | } 255 | 256 | if (lastNodes && lastLinks) { 257 | updateNodes(lastNodes); 258 | updateLinks(lastLinks); 259 | } 260 | }); 261 | }; 262 | 263 | 264 | // EVENTS: 265 | // - graph_data_changed 266 | // - graph_mouseover 267 | // - graph_mouseout 268 | // - graph_click 269 | 270 | app.directive('graph', function() { 271 | return { 272 | restrict: 'E', 273 | controller: GraphController, 274 | scope: {} 275 | } 276 | }); 277 | 278 | 279 | app.directive('onToggle', function() { 280 | return { 281 | scope: { 282 | onToggle: '&' 283 | }, 284 | link: function(scope, elm, attr) { 285 | var clicked = false; 286 | var toggled = false; 287 | var set = function(value) { 288 | if (toggled !== value) { 289 | toggled = value; 290 | scope.$apply(function() { 291 | scope.onToggle({toggled: toggled}); 292 | }); 293 | } 294 | }; 295 | 296 | elm.on('mouseenter', function() { 297 | set(true); 298 | }); 299 | 300 | elm.on('mouseleave', function() { 301 | if (!clicked) { 302 | set(false); 303 | } 304 | }); 305 | 306 | elm.on('click', function() { 307 | clicked = !clicked; 308 | set(clicked); 309 | }); 310 | } 311 | }; 312 | }); 313 | 314 | 315 | var forEachInjector = function(items, callback, parent) { 316 | items.forEach(function(item) { 317 | if (!item.id) { 318 | return; 319 | } 320 | 321 | callback(item, parent || null); 322 | forEachInjector(item.children || [], callback, item); 323 | }); 324 | }; 325 | 326 | 327 | app.controller('Main', function($scope, data) { 328 | 329 | var providersMap = {}; 330 | var links = []; 331 | 332 | data.success(function(data) { 333 | if (!data) { 334 | $scope.message = 'Enable DI profiling by appending ?di_debug into the url.'; 335 | return; 336 | } 337 | 338 | var injectors = data.injectors; 339 | var injectorsMap = {}; 340 | var rootInjectors = []; 341 | 342 | // Prepare a map of injectors. 343 | injectors.forEach(function(injector) { 344 | injectorsMap[injector.id] = injector; 345 | injector.children = []; 346 | injector.type = 'injector'; 347 | injector.title = 'Injector ' + injector.id; 348 | }); 349 | 350 | injectors.forEach(function(injector) { 351 | var parent = null; 352 | 353 | // Add into parent's children array. 354 | if (!injector.parent_id) { 355 | rootInjectors.push(injector); 356 | } else { 357 | parent = injectorsMap[injector.parent_id]; 358 | parent.children.push(injector); 359 | } 360 | 361 | // Process all providers defined in this injector. 362 | Object.keys(injector.providers).forEach(function(id) { 363 | var provider = injector.providers[id]; 364 | 365 | providersMap[id] = providersMap[id] || []; 366 | providersMap[id].push(provider); 367 | 368 | // Compute override links. 369 | if (parent && parent.providers[id]) { 370 | links.push({source: parent.providers[id], target: provider, type: 'override'}); 371 | } 372 | 373 | // Compute dependency links. 374 | provider.dependencies.forEach(function(dep) { 375 | if (injector.providers[dep.token]) { 376 | links.push({source: provider, target: injector.providers[dep.token], type: 'dependency', isPromise: dep.isPromise, isLazy: dep.isLazy}); 377 | } else { 378 | var pivot = parent; 379 | while (pivot) { 380 | if (pivot.providers[dep.token]) { 381 | links.push({source: provider, target: pivot.providers[dep.token], type: 'dependency', isPromise: dep.isPromise, isLazy: dep.isLazy}); 382 | break; 383 | } else { 384 | pivot = pivot.parent_id && injectorsMap[pivot.parent_id] || null; 385 | } 386 | } 387 | } 388 | }); 389 | 390 | injector.children.push(provider); 391 | provider.type = 'provider'; 392 | provider.title = provider.name + ' (' + injector.title + ')'; 393 | }); 394 | }); 395 | 396 | $scope.providers = Object.keys(providersMap).map(function(id) { 397 | return { 398 | id: id, 399 | name: providersMap[id][0].name, 400 | highlighted: false 401 | }; 402 | }); 403 | 404 | var fakeRoot = rootInjectors.length > 1 ? { 405 | type: 'fake-root', 406 | children: rootInjectors 407 | } : rootInjectors; 408 | 409 | $scope.$broadcast('graph_data_changed', fakeRoot, links); 410 | }); 411 | 412 | var highlightProviders = function(id, highlighted) { 413 | // Highlight the provider name in the list. 414 | $scope.providers.forEach(function(provider) { 415 | if (provider.id === id) { 416 | provider.highlighted = highlighted; 417 | } 418 | }); 419 | 420 | // Highlight all the circles in the graph. 421 | providersMap[id].forEach(function(p) { 422 | p.highlighted = highlighted; 423 | }); 424 | 425 | // Highlight all the "override" links in the graph. 426 | links.forEach(function(link) { 427 | if (link.type === 'override' && link.source.id === id) { 428 | link.highlighted = highlighted; 429 | } 430 | }); 431 | 432 | $scope.$broadcast('graph_data_changed'); 433 | }; 434 | 435 | var highlightAllProvidersById = function(id, highlighted) { 436 | providersMap[id].forEach(function(p) { 437 | p.highlighted = highlighted; 438 | }); 439 | }; 440 | 441 | var highlightOverrideLinksForProviderById = function(id, highlighted) { 442 | links.forEach(function(link) { 443 | if (link.type === 'override' && link.source.id === id) { 444 | link.highlighted = highlighted; 445 | } 446 | }); 447 | }; 448 | 449 | var highlightDependenciesForProvider = function(provider, highlighted) { 450 | if ($scope.showAllDependencies) { 451 | return; 452 | } 453 | 454 | links.forEach(function(link) { 455 | if (link.type === 'dependency') { 456 | if (link.source === provider) { 457 | link.highlighted = highlighted ? 'out' : false; 458 | } else if (link.target === provider) { 459 | link.highlighted = highlighted ? 'in' : false; 460 | } 461 | } 462 | }); 463 | }; 464 | 465 | $scope.highlight = function(provider, highlighted) { 466 | provider.highlighted = highlighted; 467 | 468 | highlightAllProvidersById(provider.id, highlighted); 469 | highlightOverrideLinksForProviderById(provider.id, highlighted); 470 | 471 | $scope.$broadcast('graph_data_changed'); 472 | }; 473 | 474 | $scope.$watch('showAllDependencies', function(highlighted) { 475 | links.forEach(function(link) { 476 | if (link.type === 'dependency') { 477 | link.highlighted = highlighted ? 'in' : false; 478 | } 479 | }); 480 | 481 | $scope.$broadcast('graph_data_changed'); 482 | }); 483 | 484 | 485 | $scope.$on('graph_mouseover', function(_, item) { 486 | if (item.name) { // is provider 487 | highlightDependenciesForProvider(item, true); 488 | } 489 | 490 | item.highlighted = true; 491 | $scope.$broadcast('graph_data_changed'); 492 | }); 493 | 494 | $scope.$on('graph_mouseout', function(_, item) { 495 | if (item.name) { // is provider 496 | highlightDependenciesForProvider(item, false); 497 | } 498 | 499 | item.highlighted = false; 500 | $scope.$broadcast('graph_data_changed'); 501 | }); 502 | 503 | $scope.$on('graph_click', function(_, d) { 504 | console.log(d) 505 | if (d.type === 'provider') { 506 | evalInInspectedWindow(function(window, args) { 507 | var tokenId = args[0]; 508 | var injectorId = args[1]; 509 | var injector; 510 | var token; 511 | 512 | window.__di_dump__.tokens.forEach(function(id, instance) { 513 | if (id === tokenId) token = instance; 514 | if (id === injectorId) injector = instance; 515 | }) 516 | window.console.log(injector.get(token)); 517 | }, [d.id, d.parent.id], function() {}); 518 | } else if (d.type === 'injector') { 519 | evalInInspectedWindow(function(window, args) { 520 | var injectorId = args[0]; 521 | var injector; 522 | window.__di_dump__.tokens.forEach(function(id, instance) { 523 | if (id === injectorId) injector = instance; 524 | }) 525 | window.console.log(injector); 526 | }, [d.id], function() {}); 527 | } 528 | }); 529 | }); 530 | 531 | app.factory('data', function($http, $location) { 532 | var url = $location.search().data || './data.json'; 533 | 534 | return $http.get(url); 535 | }); 536 | -------------------------------------------------------------------------------- /test/injector.spec.js: -------------------------------------------------------------------------------- 1 | import {Injector} from '../src/injector'; 2 | import {Inject, Provide, SuperConstructor, InjectLazy, TransientScope, ClassProvider, FactoryProvider} from '../src/annotations'; 3 | 4 | import {Car, CyclicEngine} from './fixtures/car'; 5 | import {module as houseModule} from './fixtures/house'; 6 | import {module as shinyHouseModule} from './fixtures/shiny_house'; 7 | 8 | 9 | describe('injector', function() { 10 | 11 | it('should instantiate a class without dependencies', function() { 12 | class Car { 13 | constructor() {} 14 | start() {} 15 | } 16 | 17 | var injector = new Injector(); 18 | var car = injector.get(Car); 19 | 20 | expect(car).toBeInstanceOf(Car); 21 | }); 22 | 23 | 24 | it('should resolve dependencies based on @Inject annotation', function() { 25 | class Engine { 26 | start() {} 27 | } 28 | 29 | @Inject(Engine) 30 | class Car { 31 | constructor(engine) { 32 | this.engine = engine; 33 | } 34 | 35 | start() {} 36 | } 37 | 38 | var injector = new Injector(); 39 | var car = injector.get(Car); 40 | 41 | expect(car).toBeInstanceOf(Car); 42 | expect(car.engine).toBeInstanceOf(Engine); 43 | }); 44 | 45 | 46 | it('should override providers', function() { 47 | class Engine {} 48 | 49 | @Inject(Engine) 50 | class Car { 51 | constructor(engine) { 52 | this.engine = engine; 53 | } 54 | 55 | start() {} 56 | } 57 | 58 | @Provide(Engine) 59 | class MockEngine { 60 | start() {} 61 | } 62 | 63 | var injector = new Injector([MockEngine]); 64 | var car = injector.get(Car); 65 | 66 | expect(car).toBeInstanceOf(Car); 67 | expect(car.engine).toBeInstanceOf(MockEngine); 68 | }); 69 | 70 | 71 | it('should allow factory function', function() { 72 | class Size {} 73 | 74 | @Provide(Size) 75 | function computeSize() { 76 | return 0; 77 | } 78 | 79 | var injector = new Injector([computeSize]); 80 | var size = injector.get(Size); 81 | 82 | expect(size).toBe(0); 83 | }); 84 | 85 | 86 | it('should use type annotations when available', function() { 87 | class Engine { 88 | start() {} 89 | } 90 | 91 | class Car { 92 | constructor(e: Engine) { 93 | this.engine = e; 94 | } 95 | start() {} 96 | } 97 | 98 | var injector = new Injector(); 99 | var car = injector.get(Car); 100 | 101 | expect(car).toBeInstanceOf(Car); 102 | expect(car.engine).toBeInstanceOf(Engine); 103 | }); 104 | 105 | 106 | it('should use @Inject over type annotations', function() { 107 | class Engine {} 108 | 109 | class MockEngine extends Engine { 110 | start() {} 111 | } 112 | 113 | @Inject(MockEngine) 114 | class Car { 115 | constructor(e: Engine) { 116 | this.engine = e; 117 | } 118 | start() {} 119 | } 120 | 121 | var injector = new Injector([]); 122 | var car = injector.get(Car); 123 | 124 | expect(car).toBeInstanceOf(Car); 125 | expect(car.engine).toBeInstanceOf(MockEngine); 126 | }); 127 | 128 | 129 | it('should use mixed @Inject with type annotations', function() { 130 | class Engine { 131 | start() {} 132 | } 133 | 134 | class Bumper { 135 | start() {} 136 | } 137 | 138 | class Car { 139 | constructor(e: Engine, @Inject(Bumper) b) { 140 | this.engine = e; 141 | this.bumper = b; 142 | } 143 | start() {} 144 | } 145 | 146 | var injector = new Injector([]); 147 | var car = injector.get(Car); 148 | 149 | expect(car).toBeInstanceOf(Car); 150 | expect(car.engine).toBeInstanceOf(Engine); 151 | expect(car.bumper).toBeInstanceOf(Bumper); 152 | }); 153 | 154 | 155 | it('should cache instances', function() { 156 | class Car {} 157 | 158 | var injector = new Injector(); 159 | var car = injector.get(Car); 160 | 161 | expect(injector.get(Car)).toBe(car); 162 | }); 163 | 164 | 165 | it('should throw when no provider defined', function() { 166 | var injector = new Injector(); 167 | 168 | expect(() => injector.get('NonExisting')) 169 | .toThrowError('No provider for NonExisting!'); 170 | }); 171 | 172 | 173 | it('should show the full path when no provider defined', function() { 174 | var injector = new Injector(houseModule); 175 | 176 | expect(() => injector.get('House')) 177 | .toThrowError('No provider for Sink! (House -> Kitchen -> Sink)'); 178 | }); 179 | 180 | 181 | it('should throw when trying to instantiate a cyclic dependency', function() { 182 | var injector = new Injector([CyclicEngine]); 183 | 184 | expect(() => injector.get(Car)) 185 | .toThrowError('Cannot instantiate cyclic dependency! (Car -> Engine -> Car)'); 186 | }); 187 | 188 | 189 | it('should show the full path when error happens in a constructor', function() { 190 | class Engine { 191 | constructor() { 192 | throw new Error('This engine is broken!'); 193 | } 194 | } 195 | 196 | @Inject(Engine) 197 | class Car { 198 | constructor(e) {} 199 | } 200 | 201 | var injector = new Injector(); 202 | 203 | expect(() => injector.get(Car)) 204 | .toThrowError(/Error during instantiation of Engine! \(Car -> Engine\)/); 205 | }); 206 | 207 | 208 | describe('SuperConstructor', function () { 209 | 210 | 211 | it('should support "super" to call a parent constructor', function() { 212 | class Something {} 213 | 214 | @Inject(Something) 215 | class Parent { 216 | constructor(something) { 217 | this.parentSomething = something; 218 | } 219 | } 220 | 221 | @Inject(SuperConstructor, Something) 222 | class Child extends Parent { 223 | constructor(superConstructor, something) { 224 | superConstructor(); 225 | this.childSomething = something; 226 | } 227 | } 228 | 229 | var injector = new Injector(); 230 | var instance = injector.get(Child); 231 | 232 | expect(instance.parentSomething).toBeInstanceOf(Something); 233 | expect(instance.childSomething).toBeInstanceOf(Something); 234 | expect(instance.childSomething).toBe(instance.parentSomething); 235 | }); 236 | 237 | 238 | it('should support "super" to call multiple parent constructors', function() { 239 | class Foo {} 240 | class Bar {} 241 | 242 | @Inject(Foo) 243 | class Parent { 244 | constructor(foo) { 245 | this.parentFoo = foo; 246 | } 247 | } 248 | 249 | @Inject(SuperConstructor, Foo) 250 | class Child extends Parent { 251 | constructor(superConstructor, foo) { 252 | superConstructor(); 253 | this.childFoo = foo; 254 | } 255 | } 256 | 257 | @Inject(Bar, SuperConstructor, Foo) 258 | class GrandChild extends Child { 259 | constructor(bar, superConstructor, foo) { 260 | superConstructor(); 261 | this.grandChildBar = bar; 262 | this.grandChildFoo = foo; 263 | } 264 | } 265 | 266 | var injector = new Injector(); 267 | var instance = injector.get(GrandChild); 268 | 269 | expect(instance.parentFoo).toBeInstanceOf(Foo); 270 | expect(instance.childFoo).toBeInstanceOf(Foo); 271 | expect(instance.grandChildFoo).toBeInstanceOf(Foo); 272 | expect(instance.grandChildBar).toBeInstanceOf(Bar); 273 | }); 274 | 275 | 276 | it('should throw an error when used in a factory function', function() { 277 | class Something {} 278 | 279 | @Provide(Something) 280 | @Inject(SuperConstructor) 281 | function createSomething(parent) { 282 | console.log('init', parent) 283 | } 284 | 285 | expect(function() { 286 | var injector = new Injector([createSomething]); 287 | injector.get(Something); 288 | }).toThrowError(/Only classes with a parent can ask for SuperConstructor!/); 289 | }); 290 | 291 | 292 | }); 293 | 294 | 295 | it('should throw an error when used in a class without any parent', function() { 296 | @Inject(SuperConstructor) 297 | class WithoutParent {} 298 | 299 | var injector = new Injector(); 300 | 301 | expect(function() { 302 | injector.get(WithoutParent); 303 | }).toThrowError(/Only classes with a parent can ask for SuperConstructor!/); 304 | }); 305 | 306 | 307 | it('should throw an error when null/undefined token requested', function() { 308 | var injector = new Injector(); 309 | 310 | expect(function() { 311 | injector.get(null); 312 | }).toThrowError(/Invalid token "null" requested!/); 313 | 314 | expect(function() { 315 | injector.get(undefined); 316 | }).toThrowError(/Invalid token "undefined" requested!/); 317 | }); 318 | 319 | 320 | // regression 321 | it('should show the full path when null/undefined token requested', function() { 322 | @Inject(undefined) 323 | class Foo {} 324 | 325 | @Inject(null) 326 | class Bar {} 327 | 328 | var injector = new Injector(); 329 | 330 | expect(function() { 331 | injector.get(Foo); 332 | }).toThrowError(/Invalid token "undefined" requested! \(Foo -> undefined\)/); 333 | 334 | expect(function() { 335 | injector.get(Bar); 336 | }).toThrowError(/Invalid token "null" requested! \(Bar -> null\)/); 337 | }); 338 | 339 | 340 | it('should provide itself', function() { 341 | var injector = new Injector(); 342 | 343 | expect(injector.get(Injector)).toBe(injector); 344 | }); 345 | 346 | describe('provider', function() { 347 | it('should read class annotation as a class', function(){ 348 | @ClassProvider 349 | class lowercase{} 350 | 351 | var injector = new Injector(); 352 | expect(injector.get(lowercase)).toBeInstanceOf(lowercase); 353 | }); 354 | 355 | it('should read as class even if properties are non-enumerable', function() { 356 | // Class without a name will fall through to keys check 357 | var SomeClass = function (){} 358 | Object.defineProperty(SomeClass.prototype, 'method', { enumerable: false, value: function() {} }); 359 | 360 | var injector = new Injector(); 361 | expect(injector.get(SomeClass)).toBeInstanceOf(SomeClass); 362 | }); 363 | 364 | it('should read factory annotation as a factory', function(){ 365 | @FactoryProvider 366 | function Uppercase() {} 367 | 368 | var injector = new Injector(); 369 | expect(injector.get(Uppercase)).not.toBeInstanceOf(Uppercase); 370 | }); 371 | }); 372 | 373 | 374 | describe('transient', function() { 375 | 376 | 377 | it('should never cache', function() { 378 | @TransientScope 379 | class Foo {} 380 | 381 | var injector = new Injector(); 382 | expect(injector.get(Foo)).not.toBe(injector.get(Foo)); 383 | }); 384 | 385 | 386 | it('should always use dependencies (default providers) from the youngest injector', function() { 387 | @Inject 388 | class Foo {} 389 | 390 | @TransientScope 391 | @Inject(Foo) 392 | class AlwaysNewInstance { 393 | constructor(foo) { 394 | this.foo = foo; 395 | } 396 | } 397 | 398 | var injector = new Injector(); 399 | var child = injector.createChild([Foo]); // force new instance of Foo 400 | 401 | var fooFromChild = child.get(Foo); 402 | var fooFromParent = injector.get(Foo); 403 | 404 | var alwaysNew1 = child.get(AlwaysNewInstance); 405 | var alwaysNew2 = child.get(AlwaysNewInstance); 406 | var alwaysNewFromParent = injector.get(AlwaysNewInstance); 407 | 408 | expect(alwaysNew1.foo).toBe(fooFromChild); 409 | expect(alwaysNew2.foo).toBe(fooFromChild); 410 | expect(alwaysNewFromParent.foo).toBe(fooFromParent); 411 | }); 412 | 413 | 414 | it('should always use dependencies from the youngest injector', function() { 415 | @Inject 416 | class Foo {} 417 | 418 | @TransientScope 419 | @Inject(Foo) 420 | class AlwaysNewInstance { 421 | constructor(foo) { 422 | this.foo = foo; 423 | } 424 | } 425 | 426 | var injector = new Injector([AlwaysNewInstance]); 427 | var child = injector.createChild([Foo]); // force new instance of Foo 428 | 429 | var fooFromChild = child.get(Foo); 430 | var fooFromParent = injector.get(Foo); 431 | 432 | var alwaysNew1 = child.get(AlwaysNewInstance); 433 | var alwaysNew2 = child.get(AlwaysNewInstance); 434 | var alwaysNewFromParent = injector.get(AlwaysNewInstance); 435 | 436 | expect(alwaysNew1.foo).toBe(fooFromChild); 437 | expect(alwaysNew2.foo).toBe(fooFromChild); 438 | expect(alwaysNewFromParent.foo).toBe(fooFromParent); 439 | }); 440 | }); 441 | 442 | 443 | describe('child', function() { 444 | 445 | it('should load instances from parent injector', function() { 446 | class Car { 447 | start() {} 448 | } 449 | 450 | var parent = new Injector([Car]); 451 | var child = parent.createChild([]); 452 | 453 | var carFromParent = parent.get(Car); 454 | var carFromChild = child.get(Car); 455 | 456 | expect(carFromChild).toBe(carFromParent); 457 | }); 458 | 459 | 460 | it('should create new instance in a child injector', function() { 461 | class Car { 462 | start() {} 463 | } 464 | 465 | @Provide(Car) 466 | class MockCar { 467 | start() {} 468 | } 469 | 470 | var parent = new Injector([Car]); 471 | var child = parent.createChild([MockCar]); 472 | 473 | var carFromParent = parent.get(Car); 474 | var carFromChild = child.get(Car); 475 | 476 | expect(carFromParent).not.toBe(carFromChild); 477 | expect(carFromChild).toBeInstanceOf(MockCar); 478 | }); 479 | 480 | 481 | it('should force new instances by annotation', function() { 482 | class RouteScope {} 483 | 484 | class Engine { 485 | start() {} 486 | } 487 | 488 | @RouteScope 489 | @Inject(Engine) 490 | class Car { 491 | constructor(engine) { 492 | this.engine = engine; 493 | } 494 | 495 | start() {} 496 | } 497 | 498 | var parent = new Injector([Car, Engine]); 499 | var child = parent.createChild([], [RouteScope]); 500 | 501 | var carFromParent = parent.get(Car); 502 | var carFromChild = child.get(Car); 503 | 504 | expect(carFromChild).not.toBe(carFromParent); 505 | expect(carFromChild.engine).toBe(carFromParent.engine); 506 | }); 507 | 508 | 509 | it('should force new instances by annotation using overridden provider', function() { 510 | class RouteScope {} 511 | 512 | class Engine { 513 | start() {} 514 | } 515 | 516 | @RouteScope 517 | @Provide(Engine) 518 | class MockEngine { 519 | start() {} 520 | } 521 | 522 | var parent = new Injector([MockEngine]); 523 | var childA = parent.createChild([], [RouteScope]); 524 | var childB = parent.createChild([], [RouteScope]); 525 | 526 | var engineFromA = childA.get(Engine); 527 | var engineFromB = childB.get(Engine); 528 | 529 | expect(engineFromA).not.toBe(engineFromB); 530 | expect(engineFromA).toBeInstanceOf(MockEngine); 531 | expect(engineFromB).toBeInstanceOf(MockEngine); 532 | }); 533 | 534 | 535 | it('should force new instance by annotation using the lowest overridden provider', function() { 536 | class RouteScope {} 537 | 538 | @RouteScope 539 | class Engine { 540 | constructor() {} 541 | start() {} 542 | } 543 | 544 | @RouteScope 545 | @Provide(Engine) 546 | class MockEngine { 547 | constructor() {} 548 | start() {} 549 | } 550 | 551 | @RouteScope 552 | @Provide(Engine) 553 | class DoubleMockEngine { 554 | start() {} 555 | } 556 | 557 | var parent = new Injector([Engine]); 558 | var child = parent.createChild([MockEngine]); 559 | var grantChild = child.createChild([], [RouteScope]); 560 | 561 | var engineFromParent = parent.get(Engine); 562 | var engineFromChild = child.get(Engine); 563 | var engineFromGrantChild = grantChild.get(Engine); 564 | 565 | expect(engineFromParent).toBeInstanceOf(Engine); 566 | expect(engineFromChild).toBeInstanceOf(MockEngine); 567 | expect(engineFromGrantChild).toBeInstanceOf(MockEngine); 568 | expect(engineFromGrantChild).not.toBe(engineFromChild); 569 | }); 570 | 571 | 572 | it('should show the full path when no provider', function() { 573 | var parent = new Injector(houseModule); 574 | var child = parent.createChild(shinyHouseModule); 575 | 576 | expect(() => child.get('House')) 577 | .toThrowError('No provider for Sink! (House -> Kitchen -> Sink)'); 578 | }); 579 | 580 | 581 | it('should provide itself', function() { 582 | var parent = new Injector(); 583 | var child = parent.createChild([]); 584 | 585 | expect(child.get(Injector)).toBe(child); 586 | }); 587 | 588 | 589 | it('should cache default provider in parent injector', function() { 590 | @Inject 591 | class Foo {} 592 | 593 | var parent = new Injector(); 594 | var child = parent.createChild([]); 595 | 596 | var fooFromChild = child.get(Foo); 597 | var fooFromParent = parent.get(Foo); 598 | 599 | expect(fooFromParent).toBe(fooFromChild); 600 | }); 601 | 602 | 603 | it('should force new instance by annotation for default provider', function() { 604 | class RequestScope {} 605 | 606 | @Inject 607 | @RequestScope 608 | class Foo {} 609 | 610 | var parent = new Injector(); 611 | var child = parent.createChild([], [RequestScope]); 612 | 613 | var fooFromChild = child.get(Foo); 614 | var fooFromParent = parent.get(Foo); 615 | 616 | expect(fooFromParent).not.toBe(fooFromChild); 617 | }); 618 | }); 619 | 620 | 621 | describe('lazy', function() { 622 | 623 | it('should instantiate lazily', function() { 624 | var constructorSpy = jasmine.createSpy('constructor'); 625 | 626 | class ExpensiveEngine { 627 | constructor() { 628 | constructorSpy(); 629 | } 630 | } 631 | 632 | class Car { 633 | constructor(@InjectLazy(ExpensiveEngine) createEngine) { 634 | this.engine = null; 635 | this.createEngine = createEngine; 636 | } 637 | 638 | start() { 639 | this.engine = this.createEngine(); 640 | } 641 | } 642 | 643 | var injector = new Injector(); 644 | var car = injector.get(Car); 645 | 646 | expect(constructorSpy).not.toHaveBeenCalled(); 647 | 648 | car.start(); 649 | expect(constructorSpy).toHaveBeenCalled(); 650 | expect(car.engine).toBeInstanceOf(ExpensiveEngine); 651 | }); 652 | 653 | 654 | // regression 655 | it('should instantiate lazily from a parent injector', function() { 656 | var constructorSpy = jasmine.createSpy('constructor'); 657 | 658 | class ExpensiveEngine { 659 | constructor() { 660 | constructorSpy(); 661 | } 662 | } 663 | 664 | class Car { 665 | constructor(@InjectLazy(ExpensiveEngine) createEngine) { 666 | this.engine = null; 667 | this.createEngine = createEngine; 668 | } 669 | 670 | start() { 671 | this.engine = this.createEngine(); 672 | } 673 | } 674 | 675 | var injector = new Injector([ExpensiveEngine]); 676 | var childInjector = injector.createChild([Car]); 677 | var car = childInjector.get(Car); 678 | 679 | expect(constructorSpy).not.toHaveBeenCalled(); 680 | 681 | car.start(); 682 | expect(constructorSpy).toHaveBeenCalled(); 683 | expect(car.engine).toBeInstanceOf(ExpensiveEngine); 684 | }); 685 | 686 | 687 | describe('with locals', function() { 688 | it('should always create a new instance', function() { 689 | var constructorSpy = jasmine.createSpy('constructor'); 690 | 691 | @TransientScope 692 | class ExpensiveEngine { 693 | constructor(@Inject('power') power) { 694 | constructorSpy(); 695 | this.power = power; 696 | } 697 | } 698 | 699 | class Car { 700 | constructor(@InjectLazy(ExpensiveEngine) createEngine) { 701 | this.createEngine = createEngine; 702 | } 703 | } 704 | 705 | var injector = new Injector(); 706 | var car = injector.get(Car); 707 | 708 | var veyronEngine = car.createEngine('power', 1184); 709 | var mustangEngine = car.createEngine('power', 420); 710 | 711 | expect(veyronEngine).not.toBe(mustangEngine); 712 | expect(veyronEngine.power).toBe(1184); 713 | expect(mustangEngine.power).toBe(420); 714 | 715 | var mustangEngine2 = car.createEngine('power', 420); 716 | expect(mustangEngine).not.toBe(mustangEngine2); 717 | }); 718 | }); 719 | }); 720 | }); 721 | --------------------------------------------------------------------------------