Permalink
Browse files

feat(ngUpgrade): add support for AoT compiled upgrade applications

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 d6791ff0e0007801d987af325dbb34af3c34459f
Showing with 3,263 additions and 39 deletions.
  1. +1 −1 karma-js.conf.js
  2. +1 −1 modules/@angular/upgrade/index.ts
  3. +59 −31 modules/@angular/upgrade/src/angular_js.ts
  4. +12 −0 modules/@angular/upgrade/src/aot.ts
  5. +46 −0 modules/@angular/upgrade/src/aot/angular1_providers.ts
  6. +41 −0 modules/@angular/upgrade/src/aot/component_info.ts
  7. +19 −0 modules/@angular/upgrade/src/aot/constants.ts
  8. +64 −0 modules/@angular/upgrade/src/aot/downgrade_component.ts
  9. +180 −0 modules/@angular/upgrade/src/aot/downgrade_component_adapter.ts
  10. +26 −0 modules/@angular/upgrade/src/aot/downgrade_injectable.ts
  11. +301 −0 modules/@angular/upgrade/src/aot/upgrade_component.ts
  12. +65 −0 modules/@angular/upgrade/src/aot/upgrade_module.ts
  13. +1 −0 modules/@angular/upgrade/src/constants.ts
  14. +2 −2 modules/@angular/upgrade/src/downgrade_ng2_adapter.ts
  15. +1 −0 modules/@angular/upgrade/src/facade
  16. +2 −2 modules/@angular/upgrade/src/metadata.ts
  17. +3 −2 modules/@angular/upgrade/src/upgrade_ng1_adapter.ts
  18. +58 −0 modules/@angular/upgrade/test/aot/angular1_providers_spec.ts
  19. +52 −0 modules/@angular/upgrade/test/aot/component_info_spec.ts
  20. +24 −0 modules/@angular/upgrade/test/aot/downgrade_injectable_spec.ts
  21. +80 −0 modules/@angular/upgrade/test/aot/integration/change_detection_spec.ts
  22. +99 −0 modules/@angular/upgrade/test/aot/integration/content_projection_spec.ts
  23. +259 −0 modules/@angular/upgrade/test/aot/integration/downgrade_component_spec.ts
  24. +89 −0 modules/@angular/upgrade/test/aot/integration/examples_spec.ts
  25. +80 −0 modules/@angular/upgrade/test/aot/integration/injection_spec.ts
  26. +71 −0 modules/@angular/upgrade/test/aot/integration/testability_spec.ts
  27. +1,564 −0 modules/@angular/upgrade/test/aot/integration/upgrade_component_spec.ts
  28. +39 −0 modules/@angular/upgrade/test/aot/test_helpers.ts
  29. +1 −0 scripts/windows/packages.txt
  30. +23 −0 tools/public_api_guard/upgrade/index.d.ts
View
@@ -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',
@@ -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.
@@ -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;
@@ -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;
@@ -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 {
@@ -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; }
@@ -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;
}
@@ -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,
@@ -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';
@@ -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']}
+];
@@ -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}`;
+ }
+}
@@ -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';
@@ -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;
+}
Oops, something went wrong.

0 comments on commit d6791ff

Please sign in to comment.