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 #12239
  • Loading branch information
petebacondarwin authored and alxhub committed Oct 19, 2016
1 parent a2d3564 commit d6791ff
Show file tree
Hide file tree
Showing 30 changed files with 3,263 additions and 39 deletions.
2 changes: 1 addition & 1 deletion karma-js.conf.js
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
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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
}

0 comments on commit d6791ff

Please sign in to comment.