Skip to content

Commit

Permalink
feat(ngUpgrade): add support for AoT compiled upgrade applications
Browse files Browse the repository at this point in the history
This commit introduces a new API to the ngUpgrade module, which is compatible
with AoT compilation. Primarily, it removes the dependency on reflection
over the Angular 2 metadata by introducing an API where this information
is explicitly defined, in the source code, in a way that is not lost through
AoT compilation.

This commit is a collaboration between @mhevery (who provided the original
design of the API); @gkalpak & @petebacondarwin (who implemented the
API and migrated the specs from the original ngUpgrade tests) and alexeagle
(who provided input and review).

This commit is an starting point, there is still work to be done:

* add more documentation
* validate the API via internal projects
* align the ngUpgrade compilation of A1 directives closer to the real A1
  compiler
* add more unit tests
* consider support for async `templateUrl` A1 upgraded components

Closes angular#12239
  • Loading branch information
petebacondarwin committed Oct 19, 2016
1 parent a2d3564 commit e7d76b5
Show file tree
Hide file tree
Showing 31 changed files with 3,270 additions and 46 deletions.
2 changes: 1 addition & 1 deletion karma-js.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ module.exports = function(config) {

'node_modules/core-js/client/core.js',
// include Angular v1 for upgrade module testing
'node_modules/angular/angular.min.js',
'node_modules/angular/angular.js',

'node_modules/zone.js/dist/zone.js', 'node_modules/zone.js/dist/long-stack-trace-zone.js',
'node_modules/zone.js/dist/proxy.js', 'node_modules/zone.js/dist/sync-test.js',
Expand Down
2 changes: 1 addition & 1 deletion modules/@angular/upgrade/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
* Entry point for all public APIs of the upgrade package.
*/
export * from './src/upgrade';

export * from './src/aot';
// This file only reexports content of the `src` folder. Keep it that way.
90 changes: 59 additions & 31 deletions modules/@angular/upgrade/src/angular_js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,28 @@
* found in the LICENSE file at https://angular.io/license
*/

export type Ng1Token = string;

export interface IAnnotatedFunction extends Function { $inject?: Ng1Token[]; }

export type IInjectable = (Ng1Token | Function)[] | IAnnotatedFunction;

export interface IModule {
config(fn: any): IModule;
directive(selector: string, factory: any): IModule;
name: string;
requires: (string|IInjectable)[];
config(fn: IInjectable): IModule;
directive(selector: string, factory: IInjectable): IModule;
component(selector: string, component: IComponent): IModule;
controller(name: string, type: any): IModule;
factory(key: string, factoryFn: any): IModule;
value(key: string, value: any): IModule;
run(a: any): void;
controller(name: string, type: IInjectable): IModule;
factory(key: Ng1Token, factoryFn: IInjectable): IModule;
value(key: Ng1Token, value: any): IModule;
run(a: IInjectable): IModule;
}
export interface ICompileService {
(element: Element|NodeList|string, transclude?: Function): ILinkFn;
}
export interface ILinkFn {
(scope: IScope, cloneAttachFn?: Function, options?: ILinkFnOptions): void;
(scope: IScope, cloneAttachFn?: ICloneAttachFunction, options?: ILinkFnOptions): IAugmentedJQuery;
}
export interface ILinkFnOptions {
parentBoundTranscludeFn?: Function;
Expand All @@ -29,35 +37,42 @@ export interface ILinkFnOptions {
export interface IRootScopeService {
$new(isolate?: boolean): IScope;
$id: string;
$parent: IScope;
$root: IScope;
$watch(expr: any, fn?: (a1?: any, a2?: any) => void): Function;
$destroy(): any;
$apply(): any;
$apply(exp: string): any;
$apply(exp: Function): any;
$evalAsync(): any;
$on(event: string, fn?: (event?: any, ...args: any[]) => void): Function;
$$childTail: IScope;
$$childHead: IScope;
$$nextSibling: IScope;
[key: string]: any;
}
export interface IScope extends IRootScopeService {}
export interface IAngularBootstrapConfig {}
;
export interface IAngularBootstrapConfig { strictDi?: boolean; }
export interface IDirective {
compile?: IDirectiveCompileFn;
controller?: any;
controller?: IController;
controllerAs?: string;
bindToController?: boolean|Object;
bindToController?: boolean|{[key: string]: string};
link?: IDirectiveLinkFn|IDirectivePrePost;
name?: string;
priority?: number;
replace?: boolean;
require?: any;
require?: DirectiveRequireProperty;
restrict?: string;
scope?: any;
template?: any;
templateUrl?: any;
scope?: boolean|{[key: string]: string};
template?: string|Function;
templateUrl?: string|Function;
templateNamespace?: string;
terminal?: boolean;
transclude?: any;
transclude?: boolean|'element'|{[key: string]: string};
}
export type DirectiveRequireProperty = Ng1Token[] | Ng1Token | {[key: string]: Ng1Token};
export interface IDirectiveCompileFn {
(templateElement: IAugmentedJQuery, templateAttributes: IAttributes,
transclude: ITranscludeFunction): IDirectivePrePost;
Expand All @@ -71,13 +86,13 @@ export interface IDirectiveLinkFn {
controller: any, transclude: ITranscludeFunction): void;
}
export interface IComponent {
bindings?: Object;
controller?: any;
bindings?: {[key: string]: string};
controller?: string|IInjectable;
controllerAs?: string;
require?: any;
template?: any;
templateUrl?: any;
transclude?: any;
require?: DirectiveRequireProperty;
template?: string|Function;
templateUrl?: string|Function;
transclude?: boolean;
}
export interface IAttributes { $observe(attr: string, fn: (v: string) => void): void; }
export interface ITranscludeFunction {
Expand All @@ -90,14 +105,25 @@ export interface ICloneAttachFunction {
// Let's hint but not force cloneAttachFn's signature
(clonedElement?: IAugmentedJQuery, scope?: IScope): any;
}
export interface IAugmentedJQuery {
bind(name: string, fn: () => void): void;
data(name: string, value?: any): any;
inheritedData(name: string, value?: any): any;
contents(): IAugmentedJQuery;
parent(): IAugmentedJQuery;
length: number;
[index: number]: Node;
export type IAugmentedJQuery = Node[] & {
bind?: (name: string, fn: () => void) => void;
data?: (name: string, value?: any) => any;
inheritedData?: (name: string, value?: any) => any;
contents?: () => IAugmentedJQuery;
parent?: () => IAugmentedJQuery;
empty?: () => void;
append?: (content: IAugmentedJQuery | string) => IAugmentedJQuery;
controller?: (name: string) => any;
isolateScope?: () => IScope;
};
export interface IProvider { $get: IInjectable; }
export interface IProvideService {
provider(token: Ng1Token, provider: IProvider): IProvider;
factory(token: Ng1Token, factory: IInjectable): IProvider;
service(token: Ng1Token, type: IInjectable): IProvider;
value(token: Ng1Token, value: any): IProvider;
constant(token: Ng1Token, value: any): void;
decorator(token: Ng1Token, factory: IInjectable): void;
}
export interface IParseService { (expression: string): ICompiledExpression; }
export interface ICompiledExpression { assign(context: any, value: any): any; }
Expand All @@ -110,8 +136,9 @@ export interface ICacheObject {
get(key: string): any;
}
export interface ITemplateCacheService extends ICacheObject {}
export type IController = string | IInjectable;
export interface IControllerService {
(controllerConstructor: Function, locals?: any, later?: any, ident?: any): any;
(controllerConstructor: IController, locals?: any, later?: any, ident?: any): any;
(controllerName: string, locals?: any): any;
}

Expand All @@ -133,7 +160,8 @@ function noNg() {
}

var angular: {
bootstrap: (e: Element, modules: string[], config: IAngularBootstrapConfig) => void,
bootstrap: (e: Element, modules: (string | IInjectable)[], config: IAngularBootstrapConfig) =>
void,
module: (prefix: string, dependencies?: string[]) => IModule,
element: (e: Element) => IAugmentedJQuery,
version: {major: number}, resumeBootstrap?: () => void,
Expand Down
12 changes: 12 additions & 0 deletions modules/@angular/upgrade/src/aot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export {downgradeComponent} from './aot/downgrade_component';
export {downgradeInjectable} from './aot/downgrade_injectable';
export {UpgradeComponent} from './aot/upgrade_component';
export {UpgradeModule} from './aot/upgrade_module';
46 changes: 46 additions & 0 deletions modules/@angular/upgrade/src/aot/angular1_providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as angular from '../angular_js';

// We have to do a little dance to get the ng1 injector into the module injector.
// We store the ng1 injector so that the provider in the module injector can access it
// Then we "get" the ng1 injector from the module injector, which triggers the provider to read
// the stored injector and release the reference to it.
let tempInjectorRef: angular.IInjectorService;
export function setTempInjectorRef(injector: angular.IInjectorService) {
tempInjectorRef = injector;
}
export function injectorFactory() {
const injector: angular.IInjectorService = tempInjectorRef;
tempInjectorRef = null; // clear the value to prevent memory leaks
return injector;
}

export function rootScopeFactory(i: angular.IInjectorService) {
return i.get('$rootScope');
}

export function compileFactory(i: angular.IInjectorService) {
return i.get('$compile');
}

export function parseFactory(i: angular.IInjectorService) {
return i.get('$parse');
}

export const angular1Providers = [
// We must use exported named functions for the ng2 factories to keep the compiler happy:
// > Metadata collected contains an error that will be reported at runtime:
// > Function calls are not supported.
// > Consider replacing the function or lambda with a reference to an exported function
{provide: '$injector', useFactory: injectorFactory},
{provide: '$rootScope', useFactory: rootScopeFactory, deps: ['$injector']},
{provide: '$compile', useFactory: compileFactory, deps: ['$injector']},
{provide: '$parse', useFactory: parseFactory, deps: ['$injector']}
];
41 changes: 41 additions & 0 deletions modules/@angular/upgrade/src/aot/component_info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Type} from '@angular/core';

export interface ComponentInfo {
component: Type<any>;
inputs?: string[];
outputs?: string[];
}

export class PropertyBinding {
prop: string;
attr: string;
bracketAttr: string;
bracketParenAttr: string;
parenAttr: string;
onAttr: string;
bindAttr: string;
bindonAttr: string;

constructor(public binding: string) { this.parseBinding(); }

private parseBinding() {
const parts = this.binding.split(':');
this.prop = parts[0].trim();
this.attr = (parts[1] || this.prop).trim();
this.bracketAttr = `[${this.attr}]`;
this.parenAttr = `(${this.attr})`;
this.bracketParenAttr = `[(${this.attr})]`;
const capitalAttr = this.attr.charAt(0).toUpperCase() + this.attr.substr(1);
this.onAttr = `on${capitalAttr}`;
this.bindAttr = `bind${capitalAttr}`;
this.bindonAttr = `bindon${capitalAttr}`;
}
}
19 changes: 19 additions & 0 deletions modules/@angular/upgrade/src/aot/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export const UPGRADE_MODULE_NAME = '$$UpgradeModule';
export const INJECTOR_KEY = '$$angularInjector';

export const $INJECTOR = '$injector';
export const $PARSE = '$parse';
export const $SCOPE = '$scope';

export const $COMPILE = '$compile';
export const $TEMPLATE_CACHE = '$templateCache';
export const $HTTP_BACKEND = '$httpBackend';
export const $CONTROLLER = '$controller';
64 changes: 64 additions & 0 deletions modules/@angular/upgrade/src/aot/downgrade_component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {ComponentFactory, ComponentFactoryResolver, Injector} from '@angular/core';

import * as angular from '../angular_js';

import {ComponentInfo} from './component_info';
import {$INJECTOR, $PARSE, INJECTOR_KEY} from './constants';
import {DowngradeComponentAdapter} from './downgrade_component_adapter';

let downgradeCount = 0;

/**
* @experimental
*/
export function downgradeComponent(info: ComponentInfo): angular.IInjectable {
const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`;
let idCount = 0;

const directiveFactory:
angular.IAnnotatedFunction = function(
$injector: angular.IInjectorService,
$parse: angular.IParseService): angular.IDirective {

return {
restrict: 'E',
require: '?^' + INJECTOR_KEY,
link: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes,
parentInjector: Injector, transclude: angular.ITranscludeFunction) => {

if (parentInjector === null) {
parentInjector = $injector.get(INJECTOR_KEY);
}

const componentFactoryResolver: ComponentFactoryResolver =
parentInjector.get(ComponentFactoryResolver);
const componentFactory: ComponentFactory<any> =
componentFactoryResolver.resolveComponentFactory(info.component);

if (!componentFactory) {
throw new Error('Expecting ComponentFactory for: ' + info.component);
}

const facade = new DowngradeComponentAdapter(
idPrefix + (idCount++), info, element, attrs, scope, parentInjector, $parse,
componentFactory);
facade.setupInputs();
facade.createComponent();
facade.projectContent();
facade.setupOutputs();
facade.registerCleanup();
}
};
};

directiveFactory.$inject = [$INJECTOR, $PARSE];
return directiveFactory;
}
Loading

0 comments on commit e7d76b5

Please sign in to comment.