Skip to content

Commit

Permalink
feat(core/util): add bundler helper for collecting DI Tree via compon…
Browse files Browse the repository at this point in the history
…ents
  • Loading branch information
Hotell committed Jun 11, 2016
1 parent 89ef73c commit ea6ce56
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 0 deletions.
158 changes: 158 additions & 0 deletions src/core/util/bundler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { global, isArray, isString, isType } from '../../facade/lang';
import { reflector } from '../reflection/reflection';
import { ComponentMetadata } from '../directives/metadata_directives';
import { getInjectableName, provide } from '../di/provider';
import { isProviderLiteral, createProvider, ProviderLiteral } from '../di/provider_util';
import { resolveReflectiveProvider } from '../di/reflective_provider';
import { getNgModuleMethodByType } from '../di/provider';

export function bundle( ComponentClass: Type, otherProviders: any[] = [], Module?: ng.IModule ): ng.IModule {

const ngModuleName = getInjectableName( ComponentClass );
const ngModule = Module || global.angular.module( ngModuleName, [] );
const annotations = reflector.annotations( ComponentClass );
const cmpAnnotation: ComponentMetadata = annotations[ 0 ];
const { directives = [], pipes = [], providers = [], viewProviders = [] }={} = cmpAnnotation;

// console.log( 'directives:', directives );
// console.log( 'pipes:', pipes );
// console.log( 'providers:', providers );
// console.log( 'viewProviders:', viewProviders );


// process component
const cmpProvider = provide( ComponentClass );

if ( isTypeRegistered( cmpProvider[ 0 ], ngModule, '$compileProvider', 'directive' ) ) {
return ngModule;
}

ngModule.directive( cmpProvider[ 0 ], cmpProvider[ 1 ] );


// 1. process component tree

// step through all providers
providers.forEach( ( ProviderType ) => {

// @TODO
// recursive
if ( isArray( ProviderType ) ) {
return;
}

if ( isString( ProviderType ) ) {
ngModule.requires.push( ProviderType );
return;
}

if ( isProviderLiteral( ProviderType ) ) {
const provider = createProvider( ProviderType );
const { method, name, value } = resolveReflectiveProvider( provider );
if ( !isTypeRegistered( name, ngModule, '$provide', method ) ) {
ngModule[ method ]( name, value );
}
return;
}

const serviceProvider = provide( ProviderType );
if ( !isTypeRegistered( serviceProvider[ 0 ], ngModule, '$provide', 'service' ) ) {
ngModule.service( ...provide( ProviderType ) );
}

} );
// step through all viewProviders
viewProviders.forEach( ( ViewProviderType ) => {

// @TODO
// recursive
if ( isArray( ViewProviderType ) ) {
return;
}

if ( isString( ViewProviderType ) ) {
ngModule.requires.push( ViewProviderType );
return;
}

if ( isProviderLiteral( ViewProviderType ) ) {
const provider = createProvider( ViewProviderType );
const { method, name, value } = resolveReflectiveProvider( provider );
if ( !isTypeRegistered( name, ngModule, '$provide', method ) ) {
ngModule[ method ]( name, value );
}
return;
}

const serviceProvider = provide( ViewProviderType );
if ( !isTypeRegistered( serviceProvider[ 0 ], ngModule, '$provide', 'service' ) ) {
ngModule.service( ...provide( ViewProviderType ) );
}

} );
// step through all pipes
pipes.forEach( ( PipeType: Type ) => {
// @TODO
// recursive
if ( isArray( PipeType ) ) {
return;
}

const pipeProvider = provide( PipeType );
if ( !isTypeRegistered( pipeProvider[ 0 ], ngModule, '$filterProvider', 'register' ) ) {
ngModule.filter( ...provide( PipeType ) );
}
} );
// step through all directives
directives.forEach( ( directiveType: Type ) => {
return bundle( directiveType, [], ngModule );
} );

// 2. process otherProviders argument
// - providers can be string(ngModule reference), Type, StringMap(providerLiteral)
otherProviders.forEach( ( providerType: string|Type|ProviderLiteral|any[] ) => {
if ( isString( providerType ) ) {
ngModule.requires.push( providerType );
}
if ( isType( providerType ) ) {
ngModule[ getNgModuleMethodByType(providerType) ]( ...provide(providerType) );
}
if ( isProviderLiteral( providerType ) ) {
const provider = createProvider( providerType );
const { method, name, value } = resolveReflectiveProvider( provider );
ngModule[ method ]( name, value );
}
// @TODO
// recursive
if ( isArray( providerType ) ) {

}
} );

return ngModule;

}

function isTypeRegistered(
findRegisteredType: string,
ngModule: ng.IModule,
instanceType: string,
methodName: string
): boolean {
const invokeQueue: any[] = (ngModule as any)._invokeQueue;
const types = invokeQueue
.filter( ( [type,fnName]:[string,string] ) => {
return type === instanceType && fnName === methodName;
} )
.map( ( [type,fnName, registeredProvider]:[string,string,[string,any]] ) => {
return registeredProvider
} );

return types.some( ( [typeName,typeFn] )=> {
return findRegisteredType === typeName;
} )
}

function registerProvider( ngModule: ng.IModule, provider: any ): void {

}
155 changes: 155 additions & 0 deletions test/core/util/bundler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import { global } from '../../../src/facade/lang';
import { Component } from '../../../src/core/directives/decorators';
import { createNgModule } from '../../utils';
import { provide } from '../../../src/core/di';
import { bundle } from '../../../src/core/util/bundler';
import { Pipe } from '../../../src/core/pipes/decorators';
import { Injectable } from '../../../src/core/di/decorators';
import { OpaqueToken } from '../../../src/core/di/opaque_token';

describe( `util/bundler`, () => {

let sandbox: Sinon.SinonSandbox;
global.angular = createNgModule() as any;
beforeEach( () => {
sandbox = sinon.sandbox.create();
} );
beforeEach( () => {


} );
afterEach( () => {
sandbox.restore();
} );

describe( `#bundle`, () => {


@Pipe( { name: 'ups' } )
class UpsPipe {
transform( input: string ) {}
}

@Injectable()
class SomeGlobalService {
}

@Injectable()
class MySingleton {
}

@Injectable()
class MyService {
}

@Injectable()
class OtherService {
}

@Injectable()
class MyPrivateService {
}

@Injectable()
class ViaProviderLiteralService {
}

@Component( {
selector: 'nested',
template: `Im nested`,
viewProviders: [ MyPrivateService ],
pipes: [ UpsPipe ]
} )
class NestedComponent {
}

@Component( {
selector: 'child-one',
directives: [ NestedComponent ],
providers: [ MyService, 'ui.bootstrap.modal' ],
pipes: [ UpsPipe ],
template: `hello Im childOne <nested></nested>`
} )
class ChildOneComponent {
}

const MyFactoryToken = new OpaqueToken( 'myFactory' );
@Component( {
selector: 'child-two',
directives: [ NestedComponent ],
providers: [ OtherService, MyService, { provide: MyFactoryToken, deps: [ '$q' ], useFactory: ( $q )=>({}) } ],
pipes: [ UpsPipe ],
template: `hello Im childTwo <nested></nested>`
} )
class ChildTwoComponent {
}

@Component( {
selector: 'app',
directives: [ ChildOneComponent, ChildTwoComponent ],
providers: [ MySingleton, { provide: 'tokenAsClass', useClass: ViaProviderLiteralService } ],
template: `Hello App
<child-one></child-one>
<child-two></child-two>
`
} )
class AppComponent {
}

it( `should create module which has name as root component selector`, () => {

const ngModule = bundle( AppComponent );

expect( ngModule.name ).to.equal( 'app' );
expect( ngModule.requires ).to.deep.equal( ['ui.bootstrap.modal'] );

} );

it( `should parse whole component tree and register all providers,viewProviders,pipes,directives`, () => {

const thirdPartyModule = createNgModule().module( '3rdParty', [] ).name;
const ngModule = bundle( AppComponent, [ SomeGlobalService, thirdPartyModule ] );

const expectedInvokeQueue = [
[ '$compileProvider', 'directive', provide( AppComponent ) ],
[ '$provide', 'service', provide( MySingleton ) ],
[ '$provide', 'service', provide( 'tokenAsClass', { useClass: ViaProviderLiteralService } ) ],
[ '$compileProvider', 'directive', provide( ChildOneComponent ) ],
[ '$provide', 'service', provide( MyService ) ],
[ '$filterProvider', 'register', provide( UpsPipe ) ],
[ '$compileProvider', 'directive', provide( NestedComponent ) ],
[ '$provide', 'service', provide( MyPrivateService ) ],
[ '$compileProvider', 'directive', provide( ChildTwoComponent ) ],
[ '$provide', 'service', provide( OtherService ) ],
[ '$provide', 'factory', provide( MyFactoryToken, { useFactory: ()=>({}) } ) ],
[ '$provide', 'service', provide( SomeGlobalService ) ]
];
const actualInvokeQueue = (ngModule as any)._invokeQueue;
const actual = _invokeQueueToCompare( actualInvokeQueue, false );
const expected = _invokeQueueToCompare( expectedInvokeQueue, false );

// console.log( actual );
// console.log( '========' );
// console.log( expected );

expect( actual ).to.deep.equal( expected );
expect( ngModule.requires ).to.deep.equal( [ 'ui.bootstrap.modal','3rdParty' ] );

} );

} );

function _invokeQueueToCompare( invokeQueue: any, shouldSort = true ): [string,string,string] {

const processedQueue = invokeQueue
.map( ( queueItem: any ) => {
return [ queueItem[ 0 ], queueItem[ 1 ], queueItem[ 2 ][ 0 ] ]
} );
return shouldSort
? processedQueue.sort( ( a, b )=>+(a[ 0 ] > b[ 0 ]) || +(a[ 0 ] === b[ 0 ]) - 1 )
: processedQueue
}

} );
1 change: 1 addition & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import './core/di/provider.spec';
import './core/di/key.spec';
import './core/di/forward_ref.spec';
import './core/util/decorators.spec';
import './core/util/bundler.spec';
import './core/reflection/reflection.spec';
import './core/linker/pipe_resolver.spec';
import './core/pipes/pipe_provider.spec';
Expand Down

0 comments on commit ea6ce56

Please sign in to comment.