Skip to content

Commit

Permalink
feat(upgrade): add ngUpgrade integration support (#116)
Browse files Browse the repository at this point in the history
Closes #83
  • Loading branch information
JamesHenry authored and Hotell committed Jul 10, 2016
1 parent 5c57427 commit cdc86dc
Show file tree
Hide file tree
Showing 12 changed files with 573 additions and 52 deletions.
3 changes: 3 additions & 0 deletions src/core/reflection/platform_reflection_capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export interface PlatformReflectionCapabilities {
ownPropMetadata( typeOrFunc: Type ): {[key: string]: any[]},
registerPropMetadata( propMetadata, typeOrFunc: Type ): void,

registerDowngradedNg2ComponentName( componentName: string, typeOrFunc: Type ): void,
downgradedNg2ComponentName( typeOrFunc: Type ): string

getter( name: string ): GetterFn;
setter( name: string ): SetterFn;
method( name: string ): MethodFn;
Expand Down
13 changes: 13 additions & 0 deletions src/core/reflection/reflection_capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export const PARAM_REFLECT_META_KEY = 'design:paramtypes';
*/
export const PROP_META_KEY = 'propMetadata';

/**
* @internal
*/
export const DOWNGRADED_COMPONENT_NAME_KEY = 'downgradeComponentName';

function isReflectMetadata( reflect: any ): boolean {
return isPresent( reflect ) && isPresent( reflect.getMetadata );
}
Expand Down Expand Up @@ -253,6 +258,14 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities {
this._reflect.defineMetadata( PROP_META_KEY, propMetadata, typeOrFunc );
}

registerDowngradedNg2ComponentName( componentName: string, typeOrFunc: Type ): void {
this._reflect.defineMetadata( DOWNGRADED_COMPONENT_NAME_KEY, componentName, typeOrFunc );
}

downgradedNg2ComponentName( typeOrFunc: Type ): string {
return this._reflect.getOwnMetadata( DOWNGRADED_COMPONENT_NAME_KEY, typeOrFunc );
}

interfaces( type: Type ): any[] {
// throw new BaseException("JavaScript does not support interfaces");
throw new Error( 'JavaScript does not support interfaces' );
Expand Down
8 changes: 8 additions & 0 deletions src/core/reflection/reflector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ export class Reflector extends ReflectorReader {
this.reflectionCapabilities.registerPropMetadata( parameters, typeOrFunc );
}

registerDowngradedNg2ComponentName( componentName: string, typeOrFunc: Type ): void {
this.reflectionCapabilities.registerDowngradedNg2ComponentName( componentName, typeOrFunc );
}

downgradedNg2ComponentName( typeOrFunc: Type ): string {
return this.reflectionCapabilities.downgradedNg2ComponentName( typeOrFunc );
}

/** @internal */
_getReflectionInfo( typeOrFunc: any )/*: ReflectionInfo */ {
/*if (isPresent(this._usedKeys)) {
Expand Down
8 changes: 8 additions & 0 deletions src/core/util/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import { ListWrapper } from '../../facade/collections';

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

// Support registering downgraded ng2 components directly
const downgradedNgComponentName = reflector.downgradedNg2ComponentName( ComponentClass );
if (downgradedNgComponentName) {
const ngModule = NgModule || global.angular.module( downgradedNgComponentName, [] );
ngModule.directive( downgradedNgComponentName, ComponentClass );
return ngModule;
}

const ngModuleName = getInjectableName( ComponentClass );
const ngModule = NgModule || global.angular.module( ngModuleName, [] );
const annotations = reflector.annotations( ComponentClass );
Expand Down
2 changes: 1 addition & 1 deletion src/facade/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export function isJsObject( o: any ): boolean {

export function isArguments(value: any): boolean {
// Safari 8.1 incorrectly makes `arguments.callee` enumerable in strict mode.
return ('lenght' in value) && Object.prototype.hasOwnProperty.call(value, 'callee') &&
return ('length' in value) && Object.prototype.hasOwnProperty.call(value, 'callee') &&
(!Object.prototype.propertyIsEnumerable.call(value, 'callee') || Object.prototype.toString.call(value) == argsTag);
}

Expand Down
54 changes: 3 additions & 51 deletions src/platform/browser.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,5 @@
import { assertionsEnabled } from '../facade/lang';
import { bundle } from '../core/util/bundler';
import { createBootstrapFn } from './browser_utils'

export * from './title';
export type AppRoot = string | Element | Document;

/**
* bootstrap angular app
* @param {Type} rootComponent
* @param {Array<any>} providers
*/
export function bootstrap(
rootComponent: Type,
providers: any[]
) {

const ngModule = bundle( rootComponent, providers );
const ngModuleName = ngModule.name;
const strictDi = true;
const element = document;

if ( assertionsEnabled() ) {
console.info(
'Angular is running in the development mode. Call enableProdMode() to enable the production mode.'
);
} else {
angular.module( ngModuleName ).config( prodModeConfig );
}

const appRoot = _getAppRoot( element );

angular.element( document ).ready( ()=> {
angular.bootstrap( appRoot, [ ngModuleName ], {
strictDi
} )
} );
export const bootstrap = createBootstrapFn()

}

function _getAppRoot( element: AppRoot ): Element {

if ( typeof element === 'string' ) {
return document.querySelector( element );
}
return element as Element;

}

prodModeConfig.$inject = [ '$compileProvider', '$httpProvider' ];
function prodModeConfig( $compileProvider: ng.ICompileProvider, $httpProvider: ng.IHttpProvider ) {
$compileProvider.debugInfoEnabled( false );
$httpProvider.useApplyAsync( true );
}
export * from './title';
56 changes: 56 additions & 0 deletions src/platform/browser_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { assertionsEnabled } from '../facade/lang';
import { bundle } from '../core/util/bundler';

type AppRoot = string | Element | Document;

function _getAppRoot( element: AppRoot ): Element {

if ( typeof element === 'string' ) {
return document.querySelector( element );
}
return element as Element;

}

prodModeConfig.$inject = [ '$compileProvider', '$httpProvider' ];
function prodModeConfig( $compileProvider: ng.ICompileProvider, $httpProvider: ng.IHttpProvider ) {
$compileProvider.debugInfoEnabled( false );
$httpProvider.useApplyAsync( true );
}

export function createBootstrapFn(bootstrapFn: Function = angular.bootstrap.bind(angular)): Function {

/**
* bootstrap angular app
* @param {Type} rootComponent
* @param {Array<any>} providers
*/
return function bootstrap(
rootComponent: Type,
providers: any[]
) {

const ngModule = bundle( rootComponent, providers );
const ngModuleName = ngModule.name;
const strictDi = true;
const element = document;

if ( assertionsEnabled() ) {
console.info(
'Angular is running in the development mode. Call enableProdMode() to enable the production mode.'
);
} else {
angular.module( ngModuleName ).config( prodModeConfig );
}

const appRoot = _getAppRoot( element );

angular.element( document ).ready( ()=> {
bootstrapFn( appRoot, [ ngModuleName ], {
strictDi
} )
} );

}

}
32 changes: 32 additions & 0 deletions src/upgrade/upgrade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* `UgradeAdapterRef` controls a hybrid AngularJS v1 / Angular v2 application,
* but we don't have a use for it right now so no point in creating an interface for it...
*/
export type UgradeAdapterRef = void;

export interface UpgradeAdapter {
new (): UpgradeAdapterInstance;
}

export interface UpgradeAdapterInstance {
/**
* Allows Angular v2 Component to be used from AngularJS v1.
*/
downgradeNg2Component(type: Type): Function;
/**
* Bootstrap a hybrid AngularJS v1 / Angular v2 application.
*/
bootstrap(element: Element, modules?: any[], config?: angular.IAngularBootstrapConfig): UgradeAdapterRef;
/**
* Adds a provider to the top level environment of a hybrid AngularJS v1 / Angular v2 application.
*/
addProvider(provider: Type | any[] | any): void;
/**
* Allows Angular v2 service to be accessible from AngularJS v1.
*/
downgradeNg2Provider(token: any): Function;
/**
* Allows AngularJS v1 service to be accessible from Angular v2.
*/
upgradeNg1Provider(name: string, options?: { asToken: any; }): void;
}
137 changes: 137 additions & 0 deletions src/upgrade/upgrade_adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { UpgradeAdapter, UpgradeAdapterInstance } from './upgrade';
import { createBootstrapFn } from '../platform/browser_utils';
import { reflector } from '../core/reflection/reflection';
import { getInjectableName, OpaqueToken } from '../core/di';
import { ProviderLiteral } from '../core/di/provider_util';
import { resolveDirectiveNameFromSelector } from '../facade/lang';

export class NgMetadataUpgradeAdapter {

bootstrap: Function;

_upgradeAdapter: UpgradeAdapterInstance;

constructor( UpgradeAdapter: UpgradeAdapter ) {
/**
* Manage the @angular/upgrade singleton
*/
this._upgradeAdapter = new UpgradeAdapter();
/**
* Used to bootstrap a hybrid Angular 1 and Angular 2 application,
* using the same signature as `bootstrap` from ng-metadata/platform-browser-dynamic
*
* E.g. `upgradeAdapter.bootstrap(AppComponent, providers)`
*/
this.bootstrap = createBootstrapFn(this._upgradeAdapter.bootstrap.bind(this._upgradeAdapter));
}

/**
* Used to register an Angular 2 component as a directive on an Angular 1 module,
* where the directive name is automatically created from the selector.
*
* E.g. `.directive(...upgradeAdapter.downgradeNg2Component(Ng2Component))
*/
downgradeNg2Component( component: Type ): [ string, Function ] {
const annotations = reflector.annotations( component );
const cmpAnnotation = annotations[ 0 ];
const directiveName = resolveDirectiveNameFromSelector( cmpAnnotation.selector );
return [ directiveName, this._upgradeAdapter.downgradeNg2Component( component ) ];
}

/**
* Used to register an Angular 2 component by including it in the directives array
* of an ng-metadata annotated Angular 1 component.
*
* E.g.
* ```
* @Component({
* selector: 'foo',
* directives: [upgradeAdapter.provideNg2Component(Ng2Component)],
* })
* ```
*/
provideNg2Component( component: Type ): Function {
const [ directiveName, directiveFactory ] = this.downgradeNg2Component( component );
reflector.registerDowngradedNg2ComponentName( directiveName, directiveFactory );
return directiveFactory;
}

/**
* Adds an Angular 2 provider to the hybrid application.
*/
addProvider( provider: Type | any[] | any ): void {
return this._upgradeAdapter.addProvider( provider );
}

/**
* Downgrades an Angular 2 Provider so that it can be registered as an Angular 1
* factory. Either a string or an ng-metadata OpaqueToken can be used for the name.
*
* E.g.
* ```
* const otherServiceToken = new OpaqueToken('otherService')
*
* .factory(...upgradeAdapter.downgradeNg2Provider('ng2Service', { useClass: Ng2Service }))
* .factory(...upgradeAdapter.downgradeNg2Provider(otherServiceToken, { useClass: Ng2Service }))
* ```
*/
downgradeNg2Provider( name: string | OpaqueToken, options: { useClass: Type } ): [ string, Function ] {
const downgradedProvider = this._upgradeAdapter.downgradeNg2Provider( options.useClass );
return [ getInjectableName(name), downgradedProvider ];
}

/**
* Returns a ProviderLiteral which can be used to register an Angular 2 Provider
* by including it in the providers array of an ng-metadata annotated Angular 1
* component. Either a string or an ng-metadata OpaqueToken can be used for the name.
*
* E.g.
* ```
* const otherServiceToken = new OpaqueToken('otherService')
*
* @Component({
* selector: 'foo',
* providers: [
* upgradeAdapter.provideNg2Provider('ng2Service', { useClass: Ng2Service })
* upgradeAdapter.provideNg2Provider(otherServiceToken, { useClass: Ng2Service })
* ],
* })
* ```
*/
provideNg2Provider( name: string | OpaqueToken, options: { useClass: Type } ): ProviderLiteral {
const downgradedProvider = this._upgradeAdapter.downgradeNg2Provider( options.useClass );
return {
provide: getInjectableName(name),
useFactory: downgradedProvider,
deps: downgradedProvider.$inject,
};
}

/**
* Used to make an Angular 1 Provider available to Angular 2 Components and Providers.
* When using the upgraded Provider for DI, either the string name can be used with @Inject, or
* a given token can be injected by type.
*
* E.g.
* class $state {}
*
* upgradeAdapter.upgradeNg1Provider('$state', { asToken: $state })
* upgradeAdapter.upgradeNg1Provider('$rootScope')
*
* @Component({
* selector: 'ng2',
* template: `<h1>Ng2</h1>`,
* })
* class Ng2Component {
* constructor(
* @Inject('$rootScope') private $rootScope: any, // by name using @Inject
* private $state: $state // by type using the user defined token
* ) {}
* }
*
*/
upgradeNg1Provider( name: string, options?: { asToken: any; } ): void {
return this._upgradeAdapter.upgradeNg1Provider( name, options );
}

}
1 change: 1 addition & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import './facade/primitives.spec';
import './facade/collections.spec';
import './facade/async.spec';
import './common/pipes/async_pipe.spec';
import './upgrade/upgrade_adapter.spec';

describe( 'ng-metadata', ()=> {
} );
Loading

0 comments on commit cdc86dc

Please sign in to comment.