diff --git a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts index 5a03b789f1c5..f2b2ebccf7d4 100644 --- a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts @@ -10,9 +10,9 @@ import * as ts from 'typescript'; import {ClassMember, ClassMemberKind, CtorParameter, Decorator} from '../../../ngtsc/host'; import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata'; -import {getNameText} from '../utils'; +import {findAll, getNameText} from '../utils'; -import {NgccReflectionHost} from './ngcc_host'; +import {NgccReflectionHost, PRE_NGCC_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; export const DECORATORS = 'decorators' as ts.__String; export const PROP_DECORATORS = 'propDecorators' as ts.__String; @@ -198,6 +198,19 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements undefined; } + /** + * Search the given module for variable declarations in which the initializer + * is an identifier marked with the `PRE_NGCC_MARKER`. + * @param module The module in which to search for switchable declarations. + * @returns An array of variable declarations that match. + */ + getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[] { + // Don't bother to walk the AST if the marker is not found in the text + return module.getText().indexOf(PRE_NGCC_MARKER) >= 0 ? + findAll(module, isSwitchableVariableDeclaration) : + []; + } + /** * Member decorators are declared as static properties of the class in ES2015: * diff --git a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts index c0746643fccb..efe7b09383c2 100644 --- a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts @@ -8,9 +8,33 @@ import * as ts from 'typescript'; import {ReflectionHost} from '../../../ngtsc/host'; +export const PRE_NGCC_MARKER = '__PRE_NGCC__'; +export const POST_NGCC_MARKER = '__POST_NGCC__'; + +export type SwitchableVariableDeclaration = ts.VariableDeclaration & {initializer: ts.Identifier}; +export function isSwitchableVariableDeclaration(node: ts.Node): + node is SwitchableVariableDeclaration { + return ts.isVariableDeclaration(node) && !!node.initializer && + ts.isIdentifier(node.initializer) && node.initializer.text.endsWith(PRE_NGCC_MARKER); +} + /** * A reflection host that has extra methods for looking at non-Typescript package formats */ export interface NgccReflectionHost extends ReflectionHost { + /** + * Find a symbol for a declaration that we think is a class. + * @param declaration The declaration whose symbol we are finding + * @returns the symbol for the declaration or `undefined` if it is not + * a "class" or has no symbol. + */ getClassSymbol(node: ts.Node): ts.Symbol|undefined; + + /** + * Search the given module for variable declarations in which the initializer + * is an identifier marked with the `PRE_NGCC_MARKER`. + * @param module The module in which to search for switchable declarations. + * @returns An array of variable declarations that match. + */ + getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[]; } diff --git a/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts index d3a473a4f5c2..9714a3714bd2 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts @@ -7,6 +7,7 @@ */ import * as ts from 'typescript'; import MagicString from 'magic-string'; +import {POST_NGCC_MARKER, PRE_NGCC_MARKER} from '../host/ngcc_host'; import {AnalyzedClass} from '../analyzer'; import {Renderer} from './renderer'; @@ -70,4 +71,14 @@ export class Esm2015Renderer extends Renderer { } }); } + + rewriteSwitchableDeclarations(outputText: MagicString, sourceFile: ts.SourceFile): void { + const declarations = this.host.getSwitchableDeclarations(sourceFile); + declarations.forEach(declaration => { + const start = declaration.initializer.getStart(); + const end = declaration.initializer.getEnd(); + const replacement = declaration.initializer.text.replace(PRE_NGCC_MARKER, POST_NGCC_MARKER); + outputText.overwrite(start, end, replacement); + }); + } } diff --git a/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts index a5f421352c7f..2556c5e06642 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts @@ -89,9 +89,13 @@ export abstract class Renderer { file.sourceFile); this.addImports(outputText, importManager.getAllImports(file.sourceFile.fileName, null)); - // QUESTION: do we need to remove contructor param metadata and property decorators? + + // TODO: remove contructor param metadata and property decorators (we need info from the + // handlers to do this) this.removeDecorators(outputText, decoratorsToRemove); + this.rewriteSwitchableDeclarations(outputText, file.sourceFile); + return this.renderSourceAndMap(file, input, outputText, targetPath); } @@ -102,6 +106,8 @@ export abstract class Renderer { output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void; protected abstract removeDecorators( output: MagicString, decoratorsToRemove: Map): void; + protected abstract rewriteSwitchableDeclarations( + outputText: MagicString, sourceFile: ts.SourceFile): void; /** * Add the decorator nodes that are to be removed to a map diff --git a/packages/compiler-cli/src/ngcc/src/utils.ts b/packages/compiler-cli/src/ngcc/src/utils.ts index 7fe3aee36f5e..94682bd8b6c4 100644 --- a/packages/compiler-cli/src/ngcc/src/utils.ts +++ b/packages/compiler-cli/src/ngcc/src/utils.ts @@ -20,3 +20,23 @@ export function isDefined(value: T | undefined | null): value is T { export function getNameText(name: ts.PropertyName | ts.BindingName): string { return ts.isIdentifier(name) || ts.isLiteralExpression(name) ? name.text : name.getText(); } + +/** + * Parse down the AST and capture all the nodes that satisfy the test. + * @param node The start node. + * @param test The function that tests whether a node should be included. + * @returns a collection of nodes that satisfy the test. + */ +export function findAll(node: ts.Node, test: (node: ts.Node) => node is ts.Node & T): T[] { + const nodes: T[] = []; + findAllVisitor(node); + return nodes; + + function findAllVisitor(n: ts.Node) { + if (test(n)) { + nodes.push(n); + } else { + n.forEachChild(child => findAllVisitor(child)); + } + } +} diff --git a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts index 5296927c8962..b2f705ebe976 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts @@ -32,6 +32,24 @@ const CLASSES = [ }, ]; +const MARKER_FILE = { + name: '/marker.js', + contents: ` + let compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__; + + function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) { + const compilerFactory = injector.get(CompilerFactory); + const compiler = compilerFactory.createCompiler([options]); + return compiler.compileModuleAsync(moduleType); + } + + function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) { + ngDevMode && assertNgModuleType(moduleType); + return Promise.resolve(new R3NgModuleFactory(moduleType)); + } + ` +}; + describe('Esm2015ReflectionHost', () => { describe('getGenericArityOfClass()', () => { it('should properly count type parameters', () => { @@ -52,4 +70,18 @@ describe('Esm2015ReflectionHost', () => { expect(host.getGenericArityOfClass(twoTypeParamsClass)).toBe(2); }); }); + + describe('getSwitchableDeclarations()', () => { + it('should return a collection of all the switchable variable declarations in the given module', + () => { + const program = makeProgram(MARKER_FILE); + const dtsMapper = new DtsMapper('/src', '/typings'); + const host = new Esm2015ReflectionHost(program.getTypeChecker(), dtsMapper); + const file = program.getSourceFile(MARKER_FILE.name) !; + const declarations = host.getSwitchableDeclarations(file); + expect(declarations.map(d => [d.name.getText(), d.initializer !.getText()])).toEqual([ + ['compileNgModuleFactory', 'compileNgModuleFactory__PRE_NGCC__'] + ]); + }); + }); }); diff --git a/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts index b2e0b9d6a426..0d8d6373ad97 100644 --- a/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts @@ -385,6 +385,24 @@ const FUNCTION_BODY_FILE = { ` }; +const MARKER_FILE = { + name: '/marker.js', + contents: ` + var compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__; + + function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) { + var compilerFactory = injector.get(CompilerFactory); + var compiler = compilerFactory.createCompiler([options]); + return compiler.compileModuleAsync(moduleType); + } + + function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) { + ngDevMode && assertNgModuleType(moduleType); + return Promise.resolve(new R3NgModuleFactory(moduleType)); + } + ` +}; + describe('Fesm2015ReflectionHost', () => { describe('getDecoratorsOfDeclaration()', () => { @@ -1120,4 +1138,17 @@ describe('Fesm2015ReflectionHost', () => { expect(host.getGenericArityOfClass(node)).toBe(0); }); }); + + describe('getSwitchableDeclarations()', () => { + it('should return a collection of all the switchable variable declarations in the given module', + () => { + const program = makeProgram(MARKER_FILE); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const file = program.getSourceFile(MARKER_FILE.name) !; + const declarations = host.getSwitchableDeclarations(file); + expect(declarations.map(d => [d.name.getText(), d.initializer !.getText()])).toEqual([ + ['compileNgModuleFactory', 'compileNgModuleFactory__PRE_NGCC__'] + ]); + }); + }); }); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts index 2b540491b2c9..a8ff47e7f064 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -27,49 +27,59 @@ function analyze(parser: Esm2015FileParser, analyzer: Analyzer, file: ts.SourceF return parsedFiles.map(file => analyzer.analyzeFile(file))[0]; } - -describe('Esm2015Renderer', () => { - - describe('addImports', () => { - it('should insert the given imports at the start of the source file', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` +const PROGRAM = { + name: 'some/file.js', + contents: ` /* A copyright notice */ import {Directive} from '@angular/core'; export class A {} A.decorators = [ { type: Directive, args: [{ selector: '[a]' }] }, - { type: Other } + { type: OtherA } +]; +export class B {} +B.decorators = [ + { type: OtherB }, + { type: Directive, args: [{ selector: '[b]' }] } ]; +export class C {} +C.decorators = [ + { type: Directive, args: [{ selector: '[c]' }] }, +]; +let compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__; +let badlyFormattedVariable = __PRE_NGCC__badlyFormattedVariable; + +function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) { + const compilerFactory = injector.get(CompilerFactory); + const compiler = compilerFactory.createCompiler([options]); + return compiler.compileModuleAsync(moduleType); +} + +function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) { + ngDevMode && assertNgModuleType(moduleType); + return Promise.resolve(new R3NgModuleFactory(moduleType)); +} // Some other content` - }; +}; + +describe('Esm2015Renderer', () => { + + describe('addImports', () => { + it('should insert the given imports at the start of the source file', () => { const {renderer} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); renderer.addImports( output, [{name: '@angular/core', as: 'i0'}, {name: '@angular/common', as: 'i1'}]); - expect(output.toString()) - .toEqual( - `import * as i0 from '@angular/core';\n` + - `import * as i1 from '@angular/common';\n` + PROGRAM.contents); + expect(output.toString()).toContain(`import * as i0 from '@angular/core'; +import * as i1 from '@angular/common'; + +/* A copyright notice */`); }); }); describe('addConstants', () => { it('should insert the given constants after imports in the source file', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` -/* A copyright notice */ -import {Directive} from '@angular/core'; -export class A {} -A.decorators = [ - { type: Directive, args: [{ selector: '[a]' }] }, - { type: Other } -]; -// Some other content` - }; const {renderer, program} = setup(PROGRAM); const file = program.getSourceFile('some/file.js'); if (file === undefined) { @@ -85,35 +95,41 @@ export class A {}`); }); }); + describe('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.rewriteSwitchableDeclarations(output, file); + expect(output.toString()) + .not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;`); + expect(output.toString()) + .toContain(`let badlyFormattedVariable = __PRE_NGCC__badlyFormattedVariable;`); + expect(output.toString()) + .toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_NGCC__;`); + expect(output.toString()) + .toContain( + `function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) {`); + expect(output.toString()) + .toContain( + `function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) {`); + }); + }); describe('addDefinitions', () => { it('should insert the definitions directly after the class declaration', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` -/* A copyright notice */ -import {Directive} from '@angular/core'; -export class A {} -A.decorators = [ - { type: Directive, args: [{ selector: '[a]' }] }, - { type: Other } -]; -// Some other content` - }; const {analyzer, parser, program, renderer} = setup(PROGRAM); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const output = new MagicString(PROGRAM.contents); renderer.addDefinitions(output, analyzedFile.analyzedClasses[0], 'SOME DEFINITION TEXT'); - expect(output.toString()).toEqual(` -/* A copyright notice */ -import {Directive} from '@angular/core'; + expect(output.toString()).toContain(` export class A {} SOME DEFINITION TEXT A.decorators = [ - { type: Directive, args: [{ selector: '[a]' }] }, - { type: Other } -]; -// Some other content`); +`); }); }); @@ -122,96 +138,58 @@ A.decorators = [ describe('removeDecorators', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` -/* A copyright notice */ -import {Directive} from '@angular/core'; -export class A {} -A.decorators = [ - { type: Directive, args: [{ selector: '[a]' }] }, - { type: Other } -]; -// Some other content` - }; const {analyzer, parser, program, renderer} = setup(PROGRAM); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const output = new MagicString(PROGRAM.contents); const analyzedClass = analyzedFile.analyzedClasses[0]; + const decorator = analyzedClass.decorators[0]; const decoratorsToRemove = new Map(); - decoratorsToRemove.set( - analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[0].node]); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toEqual(` -/* A copyright notice */ -import {Directive} from '@angular/core'; -export class A {} -A.decorators = [ - { type: Other } -]; -// Some other content`); + expect(output.toString()).not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); }); it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` -/* A copyright notice */ -import {Directive} from '@angular/core'; -export class A {} -A.decorators = [ - { type: Other }, - { type: Directive, args: [{ selector: '[a]' }] } -]; -// Some other content` - }; const {analyzer, parser, program, renderer} = setup(PROGRAM); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[0]; + const analyzedClass = analyzedFile.analyzedClasses[1]; + const decorator = analyzedClass.decorators[1]; const decoratorsToRemove = new Map(); - decoratorsToRemove.set( - analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[1].node]); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toEqual(` -/* A copyright notice */ -import {Directive} from '@angular/core'; -export class A {} -A.decorators = [ - { type: Other }, -]; -// Some other content`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); }); it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` -/* A copyright notice */ -import {Directive} from '@angular/core'; -export class A {} -A.decorators = [ - { type: Directive, args: [{ selector: '[a]' }] } -]; -// Some other content` - }; const {analyzer, parser, program, renderer} = setup(PROGRAM); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[0]; + const analyzedClass = analyzedFile.analyzedClasses[2]; + const decorator = analyzedClass.decorators[0]; const decoratorsToRemove = new Map(); - decoratorsToRemove.set( - analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[0].node]); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toEqual(` -/* A copyright notice */ -import {Directive} from '@angular/core'; -export class A {} -// Some other content`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).not.toContain(`C.decorators = [ + { type: Directive, args: [{ selector: '[c]' }] }, +];`); }); }); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts index b3ab70ac32f5..a054be832a03 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts @@ -27,74 +27,121 @@ function analyze(parser: Esm5FileParser, analyzer: Analyzer, file: ts.SourceFile return parsedFiles.map(file => analyzer.analyzeFile(file))[0]; } -describe('Esm5Renderer', () => { - - describe('addImports', () => { - it('should insert the given imports at the start of the source file', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` +const PROGRAM = { + name: 'some/file.js', + contents: ` /* A copyright notice */ import {Directive} from '@angular/core'; var A = (function() { function A() {} A.decorators = [ { type: Directive, args: [{ selector: '[a]' }] }, - { type: Other } + { type: OtherA } ]; return A; }()); + +var B = (function() { + function B() {} + B.decorators = [ + { type: OtherB }, + { type: Directive, args: [{ selector: '[b]' }] } + ]; + return B; +}()); + +var C = (function() { + function C() {} + C.decorators = [ + { type: Directive, args: [{ selector: '[c]' }] }, + ]; + return C; +}()); + +var compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__; +var badlyFormattedVariable = __PRE_NGCC__badlyFormattedVariable; +function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) { + const compilerFactory = injector.get(CompilerFactory); + const compiler = compilerFactory.createCompiler([options]); + return compiler.compileModuleAsync(moduleType); +} + +function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) { + ngDevMode && assertNgModuleType(moduleType); + return Promise.resolve(new R3NgModuleFactory(moduleType)); +} // Some other content -export {A};` - }; +export {A, B, C};` +}; + +describe('Esm5Renderer', () => { + + describe('addImports', () => { + it('should insert the given imports at the start of the source file', () => { const {renderer} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); renderer.addImports( output, [{name: '@angular/core', as: 'i0'}, {name: '@angular/common', as: 'i1'}]); - expect(output.toString()) - .toEqual( - `import * as i0 from '@angular/core';\n` + - `import * as i1 from '@angular/common';\n` + PROGRAM.contents); + expect(output.toString()).toContain(`import * as i0 from '@angular/core'; +import * as i1 from '@angular/common'; + +/* A copyright notice */`); + }); + }); + + + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'const x = 3;', file); + expect(output.toString()).toContain(` +import {Directive} from '@angular/core'; +const x = 3; + +var A = (function() {`); }); }); + describe('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.rewriteSwitchableDeclarations(output, file); + expect(output.toString()) + .not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;`); + expect(output.toString()) + .toContain(`var badlyFormattedVariable = __PRE_NGCC__badlyFormattedVariable;`); + expect(output.toString()) + .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_NGCC__;`); + expect(output.toString()) + .toContain( + `function compileNgModuleFactory__PRE_NGCC__(injector, options, moduleType) {`); + expect(output.toString()) + .toContain( + `function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) {`); + }); + }); describe('addDefinitions', () => { it('should insert the definitions directly after the class declaration', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` -/* A copyright notice */ -import {Directive} from '@angular/core'; -var A = (function() { - function A() {} - A.decorators = [ - { type: Directive, args: [{ selector: '[a]' }] }, - { type: Other } - ]; - return A; -}()); -// Some other content -export {A};` - }; const {analyzer, parser, program, renderer} = setup(PROGRAM); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const output = new MagicString(PROGRAM.contents); renderer.addDefinitions(output, analyzedFile.analyzedClasses[0], 'SOME DEFINITION TEXT'); - expect(output.toString()).toEqual(` -/* A copyright notice */ -import {Directive} from '@angular/core'; -var A = (function() { + expect(output.toString()).toContain(` function A() {} SOME DEFINITION TEXT A.decorators = [ - { type: Directive, args: [{ selector: '[a]' }] }, - { type: Other } - ]; - return A; -}()); -// Some other content -export {A};`); +`); }); }); @@ -103,120 +150,58 @@ export {A};`); describe('removeDecorators', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` -/* A copyright notice */ -import {Directive} from '@angular/core'; -var A = (function() { - function A() {} - A.decorators = [ - { type: Directive, args: [{ selector: '[a]' }] }, - { type: Other } - ]; - return A; -}()); -// Some other content -export {A};` - }; const {analyzer, parser, program, renderer} = setup(PROGRAM); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const output = new MagicString(PROGRAM.contents); const analyzedClass = analyzedFile.analyzedClasses[0]; + const decorator = analyzedClass.decorators[0]; const decoratorsToRemove = new Map(); - decoratorsToRemove.set( - analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[0].node]); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toEqual(` -/* A copyright notice */ -import {Directive} from '@angular/core'; -var A = (function() { - function A() {} - A.decorators = [ - { type: Other } - ]; - return A; -}()); -// Some other content -export {A};`); + expect(output.toString()).not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); }); it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` -/* A copyright notice */ -import {Directive} from '@angular/core'; -var A = (function() { - function A() {} - A.decorators = [ - { type: Other }, - { type: Directive, args: [{ selector: '[a]' }] } - ]; - return A; -}()); -// Some other content -export {A};` - }; const {analyzer, parser, program, renderer} = setup(PROGRAM); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[0]; + const analyzedClass = analyzedFile.analyzedClasses[1]; + const decorator = analyzedClass.decorators[1]; const decoratorsToRemove = new Map(); - decoratorsToRemove.set( - analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[1].node]); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toEqual(` -/* A copyright notice */ -import {Directive} from '@angular/core'; -var A = (function() { - function A() {} - A.decorators = [ - { type: Other }, - ]; - return A; -}()); -// Some other content -export {A};`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); }); it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', () => { - const PROGRAM = { - name: 'some/file.js', - contents: ` -/* A copyright notice */ -import {Directive} from '@angular/core'; -var A = (function() { - function A() {} - A.decorators = [ - { type: Directive, args: [{ selector: '[a]' }] } - ]; - return A; -}()); -// Some other content -export {A};` - }; const {analyzer, parser, program, renderer} = setup(PROGRAM); const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[0]; + const analyzedClass = analyzedFile.analyzedClasses[2]; + const decorator = analyzedClass.decorators[0]; const decoratorsToRemove = new Map(); - decoratorsToRemove.set( - analyzedClass.decorators[0].node.parent !, [analyzedClass.decorators[0].node]); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toEqual(` -/* A copyright notice */ -import {Directive} from '@angular/core'; -var A = (function() { - function A() {} - return A; -}()); -// Some other content -export {A};`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).not.toContain(`C.decorators = [ + { type: Directive, args: [{ selector: '[c]' }] }, +];`); }); }); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts index a298e7a25cc7..c58559ea64c3 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts @@ -29,6 +29,9 @@ class TestRenderer extends Renderer { removeDecorators(output: MagicString, decoratorsToRemove: Map) { output.prepend('\n// REMOVE DECORATORS\n'); } + rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void { + output.prepend('\n// REWRITTEN DECLARATIONS\n'); + } } function createTestRenderer() { @@ -68,7 +71,7 @@ describe('Renderer', () => { ] }); const RENDERED_CONTENTS = - `\n// REMOVE DECORATORS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n` + + `\n// REWRITTEN DECLARATIONS\n\n// REMOVE DECORATORS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n` + INPUT_PROGRAM.contents; const OUTPUT_PROGRAM_MAP = fromObject({ 'version': 3, @@ -78,14 +81,14 @@ describe('Renderer', () => { 'import { Directive } from \'@angular/core\';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: \'[a]\' }] }\n];\n' ], 'names': [], - 'mappings': ';;;;;;;;AAAA;;;;;;;;;' + 'mappings': ';;;;;;;;;;AAAA;;;;;;;;;' }); const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ 'version': 3, 'sources': ['/file.ts'], 'names': [], - 'mappings': ';;;;;;;;AAAA', + 'mappings': ';;;;;;;;;;AAAA', 'file': '/output_file.js', 'sourcesContent': [ 'import { Directive } from \'@angular/core\';\nexport class A {\n foo(x: string): string {\n return x;\n }\n static decorators = [\n { type: Directive, args: [{ selector: \'[a]\' }] }\n ];\n}'