fix(ngcc): ensure rendering formatters work with IIFE wrapped classes (#36989)

After the refactoring of the reflection hosts to accommodate
ES2015 classes wrapped in IIFEs. The same treatment needs to
be applied to the rendering formatters.

PR Close #36989
This commit is contained in:
Pete Bacon Darwin 2020-05-12 08:20:00 +01:00 committed by Kara Erickson
parent d7440c452a
commit c8ee390d23
7 changed files with 469 additions and 409 deletions

View File

@ -2489,7 +2489,7 @@ function isSynthesizedSuperCall(expression: ts.Expression): boolean {
* Find the statement that contains the given node
* @param node a node whose containing statement we wish to find
*/
function getContainingStatement(node: ts.Node): ts.Statement {
export function getContainingStatement(node: ts.Node): ts.Statement {
while (node.parent) {
if (ts.isBlock(node.parent) || ts.isSourceFile(node.parent)) {
break;

View File

@ -8,10 +8,12 @@
import {Statement} from '@angular/compiler';
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports';
import {ImportManager, translateStatement} from '../../../src/ngtsc/translator';
import {CompiledClass} from '../analysis/types';
import {getIifeBody} from '../host/esm5_host';
import {getContainingStatement} from '../host/esm2015_host';
import {EsmRenderingFormatter} from './esm_rendering_formatter';
/**
@ -20,11 +22,24 @@ import {EsmRenderingFormatter} from './esm_rendering_formatter';
*/
export class Esm5RenderingFormatter extends EsmRenderingFormatter {
/**
* Add the definitions inside the IIFE of each decorated class
* Add the definitions, directly before the return statement, inside the IIFE of each decorated
* class.
*/
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const iifeBody = getIifeBody(compiledClass.declaration);
if (!iifeBody) {
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
if (!classSymbol) {
throw new Error(
`Compiled class "${compiledClass.name}" in "${
compiledClass.declaration.getSourceFile()
.fileName}" does not have a valid syntax.\n` +
`Expected an ES5 IIFE wrapped function. But got:\n` +
compiledClass.declaration.getText());
}
const declarationStatement =
getContainingStatement(classSymbol.implementation.valueDeclaration);
const iifeBody = declarationStatement.parent;
if (!iifeBody || !ts.isBlock(iifeBody)) {
throw new Error(`Compiled class declaration is not inside an IIFE: ${compiledClass.name} in ${
compiledClass.declaration.getSourceFile().fileName}`);
}

View File

@ -16,7 +16,7 @@ import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer';
import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {CompiledClass} from '../analysis/types';
import {isAssignment} from '../host/esm2015_host';
import {getContainingStatement, isAssignment} from '../host/esm2015_host';
import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {RedundantDecoratorMap, RenderingFormatter} from './rendering_formatter';
@ -104,7 +104,8 @@ export class EsmRenderingFormatter implements RenderingFormatter {
if (!classSymbol) {
throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`);
}
const declarationStatement = getDeclarationStatement(classSymbol.declaration.valueDeclaration);
const declarationStatement =
getContainingStatement(classSymbol.implementation.valueDeclaration);
const insertionPoint = declarationStatement.getEnd();
output.appendLeft(insertionPoint, '\n' + definitions);
}
@ -277,17 +278,6 @@ export class EsmRenderingFormatter implements RenderingFormatter {
}
}
function getDeclarationStatement(node: ts.Node): ts.Statement {
let statement = node;
while (statement) {
if (ts.isVariableStatement(statement) || ts.isClassDeclaration(statement)) {
return statement;
}
statement = statement.parent;
}
throw new Error(`Class is not defined in a declaration statement: ${node.getText()}`);
}
function findStatement(node: ts.Node): ts.Statement|undefined {
while (node) {
if (ts.isExpressionStatement(node) || ts.isReturnStatement(node)) {

View File

@ -315,8 +315,11 @@ SOME DEFINITION TEXT
program, absoluteFromSourceFile(sourceFile), 'NoIife', ts.isFunctionDeclaration);
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError(`Compiled class declaration is not inside an IIFE: NoIife in ${
_('/node_modules/test-package/some/file.js')}`);
.toThrowError(
`Compiled class "NoIife" in "${
_('/node_modules/test-package/some/file.js')}" does not have a valid syntax.\n` +
`Expected an ES5 IIFE wrapped function. But got:\n` +
`function NoIife() {}`);
const badIifeDeclaration = getDeclaration(
program, absoluteFromSourceFile(sourceFile), 'BadIife', ts.isVariableDeclaration);

View File

@ -321,8 +321,11 @@ SOME DEFINITION TEXT
program, absoluteFromSourceFile(sourceFile), 'NoIife', ts.isFunctionDeclaration);
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError(`Compiled class declaration is not inside an IIFE: NoIife in ${
_('/node_modules/test-package/some/file.js')}`);
.toThrowError(
`Compiled class "NoIife" in "${
_('/node_modules/test-package/some/file.js')}" does not have a valid syntax.\n` +
`Expected an ES5 IIFE wrapped function. But got:\n` +
`function NoIife() {}`);
const badIifeDeclaration = getDeclaration(
program, absoluteFromSourceFile(sourceFile), 'BadIife', ts.isVariableDeclaration);

View File

@ -54,17 +54,8 @@ function setup(files: TestFile[], dtsFiles?: TestFile[]) {
};
}
runInEachFileSystem(() => {
describe('EsmRenderingFormatter', () => {
let _: typeof absoluteFrom;
let PROGRAM: TestFile;
beforeEach(() => {
_ = absoluteFrom;
PROGRAM = {
name: _('/node_modules/test-package/some/file.js'),
contents: `
const PROGRAM_CONTENT: Record<string, string> = {
'top-level': `
/* A copyright notice */
import 'some-side-effect';
import {Directive} from '@angular/core';
@ -97,245 +88,298 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {
ngDevMode && assertNgModuleType(moduleType);
return Promise.resolve(new R3NgModuleFactory(moduleType));
}
// Some other content`
};
});
// Some other content`,
describe('addImports', () => {
it('should insert the given imports after existing imports of the source file', () => {
const {renderer, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
renderer.addImports(
output,
[
{specifier: '@angular/core', qualifier: 'i0'},
{specifier: '@angular/common', qualifier: 'i1'}
],
sourceFile);
expect(output.toString()).toContain(`/* A copyright notice */
'iife-wrapped': `
/* A copyright notice */
import 'some-side-effect';
import {Directive} from '@angular/core';
let A = /** @class */ (() => {
class A {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] },
{ type: OtherA }
];
return A;
})();
let B = /** @class */ (() => {
class B {}
B.decorators = [
{ type: OtherB },
{ type: Directive, args: [{ selector: '[b]' }] }
];
return B;
})();
var C_1;
let C = C_1 = /** @class */ (() => {
class C {}
C.decorators = [
{ type: Directive, args: [{ selector: '[c]' }] },
];
return C;
})();
export A, B, C;
let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;
let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;
function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {
const compilerFactory = injector.get(CompilerFactory);
const compiler = compilerFactory.createCompiler([options]);
return compiler.compileModuleAsync(moduleType);
}
function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {
ngDevMode && assertNgModuleType(moduleType);
return Promise.resolve(new R3NgModuleFactory(moduleType));
}
// Some other content`
};
runInEachFileSystem(() => {
['top-level', 'iife-wrapped'].forEach(classLayout => {
describe(`EsmRenderingFormatter {${classLayout} classes}`, () => {
let _: typeof absoluteFrom;
let PROGRAM: TestFile;
beforeEach(() => {
_ = absoluteFrom;
PROGRAM = {
name: _('/node_modules/test-package/some/file.js'),
contents: PROGRAM_CONTENT[classLayout],
};
});
describe('addImports', () => {
it('should insert the given imports after existing imports of the source file', () => {
const {renderer, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
renderer.addImports(
output,
[
{specifier: '@angular/core', qualifier: 'i0'},
{specifier: '@angular/common', qualifier: 'i1'}
],
sourceFile);
expect(output.toString()).toContain(`/* A copyright notice */
import 'some-side-effect';
import {Directive} from '@angular/core';
import * as i0 from '@angular/core';
import * as i1 from '@angular/common';`);
});
it('should leave the file unchanged if there are no imports to add', () => {
const {renderer, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const contentsBefore = output.toString();
renderer.addImports(output, [], sourceFile);
const contentsAfter = output.toString();
expect(contentsAfter).toBe(contentsBefore);
});
});
it('should leave the file unchanged if there are no imports to add', () => {
const {renderer, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const contentsBefore = output.toString();
renderer.addImports(output, [], sourceFile);
const contentsAfter = output.toString();
expect(contentsAfter).toBe(contentsBefore);
});
});
describe('addExports', () => {
it('should insert the given exports at the end of the source file', () => {
const {importManager, renderer, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
renderer.addExports(
output, _(PROGRAM.name.replace(/\.js$/, '')),
[
{
from: _('/node_modules/test-package/some/a.js'),
dtsFrom: _('/node_modules/test-package/some/a.d.ts'),
identifier: 'ComponentA1'
},
{
from: _('/node_modules/test-package/some/a.js'),
dtsFrom: _('/node_modules/test-package/some/a.d.ts'),
identifier: 'ComponentA2'
},
{
from: _('/node_modules/test-package/some/foo/b.js'),
dtsFrom: _('/node_modules/test-package/some/foo/b.d.ts'),
identifier: 'ComponentB'
},
{from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'},
],
importManager, sourceFile);
expect(output.toString()).toContain(`
describe('addExports', () => {
it('should insert the given exports at the end of the source file', () => {
const {importManager, renderer, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
renderer.addExports(
output, _(PROGRAM.name.replace(/\.js$/, '')),
[
{
from: _('/node_modules/test-package/some/a.js'),
dtsFrom: _('/node_modules/test-package/some/a.d.ts'),
identifier: 'ComponentA1'
},
{
from: _('/node_modules/test-package/some/a.js'),
dtsFrom: _('/node_modules/test-package/some/a.d.ts'),
identifier: 'ComponentA2'
},
{
from: _('/node_modules/test-package/some/foo/b.js'),
dtsFrom: _('/node_modules/test-package/some/foo/b.d.ts'),
identifier: 'ComponentB'
},
{from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'},
],
importManager, sourceFile);
expect(output.toString()).toContain(`
// Some other content
export {ComponentA1} from './a';
export {ComponentA2} from './a';
export {ComponentB} from './foo/b';
export {TopLevelComponent};`);
});
});
});
describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => {
const {renderer, program} = setup([PROGRAM]);
const file = getSourceFileOrError(program, _('/node_modules/test-package/some/file.js'));
const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'const x = 3;', file);
expect(output.toString()).toContain(`
describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => {
const {renderer, program} = setup([PROGRAM]);
const file = getSourceFileOrError(program, _('/node_modules/test-package/some/file.js'));
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;
export class A {}`);
});
`);
});
it('should insert constants after inserted imports', () => {
const {renderer, program} = setup([PROGRAM]);
const file = getSourceFileOrError(program, _('/node_modules/test-package/some/file.js'));
const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'const x = 3;', file);
renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file);
expect(output.toString()).toContain(`
it('should insert constants after inserted imports', () => {
const {renderer, program} = setup([PROGRAM]);
const file = getSourceFileOrError(program, _('/node_modules/test-package/some/file.js'));
const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'const x = 3;', file);
renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file);
expect(output.toString()).toContain(`
import {Directive} from '@angular/core';
import * as i0 from '@angular/core';
const x = 3;
export class A {`);
`);
});
});
});
describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => {
const {renderer, program, switchMarkerAnalyses, sourceFile} = setup([PROGRAM]);
const file = getSourceFileOrError(program, _('/node_modules/test-package/some/file.js'));
const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile)!.declarations);
expect(output.toString())
.not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`);
expect(output.toString())
.toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`);
expect(output.toString())
.toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
expect(output.toString())
.toContain(
`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString())
.toContain(
`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => {
const {renderer, program, switchMarkerAnalyses, sourceFile} = setup([PROGRAM]);
const file = getSourceFileOrError(program, _('/node_modules/test-package/some/file.js'));
const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile)!.declarations);
expect(output.toString())
.not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`);
expect(output.toString())
.toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`);
expect(output.toString())
.toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
expect(output.toString())
.toContain(
`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString())
.toContain(
`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
});
});
});
describe('addDefinitions', () => {
it('should insert the definitions directly after the class declaration', () => {
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'A')!;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(`
export class A {}
describe('addDefinitions', () => {
it('should insert the definitions directly after the class declaration', () => {
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'A')!;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(`class A {}
SOME DEFINITION TEXT
A.decorators = [
`);
});
});
it('should insert the definitions after the variable declaration of class expressions',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'C')!;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(`
let C = C_1 = class C {};
SOME DEFINITION TEXT
C.decorators = [
`);
});
});
describe('addAdjacentStatements', () => {
const contents = `import {Directive, NgZone, Console} from '@angular/core';\n` +
`export class SomeDirective {\n` +
` constructor(zone, cons) {}\n` +
` method() {}\n` +
`}\n` +
`SomeDirective.decorators = [\n` +
` { type: Directive, args: [{ selector: '[a]' }] },\n` +
` { type: OtherA }\n` +
`];\n` +
`SomeDirective.ctorParameters = () => [\n` +
` { type: NgZone },\n` +
` { type: Console }\n` +
`];`;
it('should insert the statements after all the static methods of the class', () => {
const program = {name: _('/node_modules/test-package/some/file.js'), contents};
const {renderer, decorationAnalyses, sourceFile} = setup([program]);
const output = new MagicString(contents);
const compiledClass = decorationAnalyses.get(sourceFile)!.compiledClasses.find(
c => c.name === 'SomeDirective')!;
renderer.addAdjacentStatements(output, compiledClass, 'SOME STATEMENTS');
expect(output.toString())
.toContain(
`SomeDirective.ctorParameters = () => [\n` +
` { type: NgZone },\n` +
` { type: Console }\n` +
`];\n` +
`SOME STATEMENTS`);
});
it('should insert the statements after any definitions', () => {
const program = {name: _('/node_modules/test-package/some/file.js'), contents};
const {renderer, decorationAnalyses, sourceFile} = setup([program]);
const output = new MagicString(contents);
const compiledClass = decorationAnalyses.get(sourceFile)!.compiledClasses.find(
c => c.name === 'SomeDirective')!;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITIONS');
renderer.addAdjacentStatements(output, compiledClass, 'SOME STATEMENTS');
const definitionsPosition = output.toString().indexOf('SOME DEFINITIONS');
const statementsPosition = output.toString().indexOf('SOME STATEMENTS');
expect(definitionsPosition).not.toEqual(-1, 'definitions should exist');
expect(statementsPosition).not.toEqual(-1, 'statements should exist');
expect(statementsPosition).toBeGreaterThan(definitionsPosition);
});
});
describe('removeDecorators', () => {
describe('[static property declaration]', () => {
it('should delete the decorator (and following comma) that was matched in the analysis',
it('should insert the definitions after the variable declaration of class expressions',
() => {
const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]);
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'A')!;
const decorator = compiledClass.decorators![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'C')!;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
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]' }] }`);
.toMatch(/class C \{\};?\nSOME DEFINITION TEXT\nC.decorators = \[/);
});
});
describe('addAdjacentStatements', () => {
const contents = `import {Directive, NgZone, Console} from '@angular/core';\n` +
`export class SomeDirective {\n` +
` constructor(zone, cons) {}\n` +
` method() {}\n` +
`}\n` +
`SomeDirective.decorators = [\n` +
` { type: Directive, args: [{ selector: '[a]' }] },\n` +
` { type: OtherA }\n` +
`];\n` +
`SomeDirective.ctorParameters = () => [\n` +
` { type: NgZone },\n` +
` { type: Console }\n` +
`];`;
it('should insert the statements after all the static methods of the class', () => {
const program = {name: _('/node_modules/test-package/some/file.js'), contents};
const {renderer, decorationAnalyses, sourceFile} = setup([program]);
const output = new MagicString(contents);
const compiledClass = decorationAnalyses.get(sourceFile)!.compiledClasses.find(
c => c.name === 'SomeDirective')!;
renderer.addAdjacentStatements(output, compiledClass, 'SOME STATEMENTS');
expect(output.toString())
.toContain(
`SomeDirective.ctorParameters = () => [\n` +
` { type: NgZone },\n` +
` { type: Console }\n` +
`];\n` +
`SOME STATEMENTS`);
});
it('should insert the statements after any definitions', () => {
const program = {name: _('/node_modules/test-package/some/file.js'), contents};
const {renderer, decorationAnalyses, sourceFile} = setup([program]);
const output = new MagicString(contents);
const compiledClass = decorationAnalyses.get(sourceFile)!.compiledClasses.find(
c => c.name === 'SomeDirective')!;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITIONS');
renderer.addAdjacentStatements(output, compiledClass, 'SOME STATEMENTS');
const definitionsPosition = output.toString().indexOf('SOME DEFINITIONS');
const statementsPosition = output.toString().indexOf('SOME STATEMENTS');
expect(definitionsPosition).not.toEqual(-1, 'definitions should exist');
expect(statementsPosition).not.toEqual(-1, 'statements should exist');
expect(statementsPosition).toBeGreaterThan(definitionsPosition);
});
});
describe('removeDecorators', () => {
describe('[static property declaration]', () => {
it('should delete the decorator (and following comma) that was matched in the analysis',
() => {
const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'A')!;
const decorator = compiledClass.decorators![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
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 {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'B')!;
const decorator = compiledClass.decorators![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
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 (but cope with no trailing comma) that was matched in the analysis',
() => {
const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'B')!;
const decorator = compiledClass.decorators![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
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 handle a decorator with a trailing comment', () => {
const text = `
it('should handle a decorator with a trailing comment', () => {
const text = `
import {Directive} from '@angular/core';
export class A {}
A.decorators = [
@ -343,50 +387,50 @@ A.decorators = [
{ type: OtherA }
];
`;
const file = {name: _('/node_modules/test-package/index.js'), contents: text};
const {decorationAnalyses, sourceFile, renderer} = setup([file]);
const output = new MagicString(text);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'A')!;
const decorator = compiledClass.decorators![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
// The decorator should have been removed correctly.
expect(output.toString()).toContain('A.decorators = [ { type: OtherA }');
const file = {name: _('/node_modules/test-package/index.js'), contents: text};
const {decorationAnalyses, sourceFile, renderer} = setup([file]);
const output = new MagicString(text);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'A')!;
const decorator = compiledClass.decorators![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
// The decorator should have been removed correctly.
expect(output.toString()).toContain('A.decorators = [ { type: OtherA }');
});
it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis',
() => {
const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'C')!;
const decorator = compiledClass.decorators![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
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(`{ type: Directive, args: [{ selector: '[c]' }] }`);
expect(output.toString()).not.toContain(`C.decorators = [`);
});
});
it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis',
() => {
const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'C')!;
const decorator = compiledClass.decorators![0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
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(`{ type: Directive, args: [{ selector: '[c]' }] }`);
expect(output.toString()).not.toContain(`C.decorators = [`);
});
});
});
describe('[__decorate declarations]', () => {
let PROGRAM_DECORATE_HELPER: TestFile;
describe('[__decorate declarations]', () => {
let PROGRAM_DECORATE_HELPER: TestFile;
beforeEach(() => {
PROGRAM_DECORATE_HELPER = {
name: _('/node_modules/test-package/some/file.js'),
contents: `
beforeEach(() => {
PROGRAM_DECORATE_HELPER = {
name: _('/node_modules/test-package/some/file.js'),
contents: `
import * as tslib_1 from "tslib";
var D_1;
/* A copyright notice */
@ -420,72 +464,72 @@ D = D_1 = tslib_1.__decorate([
], D);
export { D };
// Some other content`
};
};
});
it('should delete the decorator (and following comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'A')!;
const decorator = compiledClass.decorators!.find(d => d.name === 'Directive')!;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'B')!;
const decorator = compiledClass.decorators!.find(d => d.name === 'Directive')!;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'C')!;
const decorator = compiledClass.decorators!.find(d => d.name === 'Directive')!;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`);
expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`);
expect(output.toString()).toContain(`let C = class C {\n};\nexport { C };`);
});
});
it('should delete the decorator (and following comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'A')!;
const decorator = compiledClass.decorators!.find(d => d.name === 'Directive')!;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'B')!;
const decorator = compiledClass.decorators!.find(d => d.name === 'Directive')!;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile)!.compiledClasses.find(c => c.name === 'C')!;
const decorator = compiledClass.decorators!.find(d => d.name === 'Directive')!;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node!.parent!, [decorator.node!]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`);
expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`);
expect(output.toString()).toContain(`let C = class C {\n};\nexport { C };`);
});
});
describe('addModuleWithProvidersParams', () => {
let MODULE_WITH_PROVIDERS_PROGRAM: TestFile[];
let MODULE_WITH_PROVIDERS_DTS_PROGRAM: TestFile[];
beforeEach(() => {
MODULE_WITH_PROVIDERS_PROGRAM = [
{
name: _('/node_modules/test-package/src/index.js'),
contents: `
describe('addModuleWithProvidersParams', () => {
let MODULE_WITH_PROVIDERS_PROGRAM: TestFile[];
let MODULE_WITH_PROVIDERS_DTS_PROGRAM: TestFile[];
beforeEach(() => {
MODULE_WITH_PROVIDERS_PROGRAM = [
{
name: _('/node_modules/test-package/src/index.js'),
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class SomeClass {}
@ -509,25 +553,25 @@ export { D };
export function withProviders8() { return {ngModule: SomeModule}; }
export {ExternalModule} from './module';
`
},
{
name: _('/node_modules/test-package/src/module.js'),
contents: `
},
{
name: _('/node_modules/test-package/src/module.js'),
contents: `
export class ExternalModule {
static withProviders1() { return {ngModule: ExternalModule}; }
static withProviders2() { return {ngModule: ExternalModule}; }
}`
},
{
name: _('/node_modules/some-library/index.d.ts'),
contents: 'export declare class LibraryModule {}'
},
];
},
{
name: _('/node_modules/some-library/index.d.ts'),
contents: 'export declare class LibraryModule {}'
},
];
MODULE_WITH_PROVIDERS_DTS_PROGRAM = [
{
name: _('/node_modules/test-package/typings/index.d.ts'),
contents: `
MODULE_WITH_PROVIDERS_DTS_PROGRAM = [
{
name: _('/node_modules/test-package/typings/index.d.ts'),
contents: `
import {ModuleWithProviders} from '@angular/core';
export declare class SomeClass {}
export interface MyModuleWithProviders extends ModuleWithProviders {}
@ -551,41 +595,41 @@ export { D };
export declare function withProviders8(): MyModuleWithProviders;
export {ExternalModule} from './module';
`
},
{
name: _('/node_modules/test-package/typings/module.d.ts'),
contents: `
},
{
name: _('/node_modules/test-package/typings/module.d.ts'),
contents: `
export interface ModuleWithProviders {}
export declare class ExternalModule {
static withProviders1(): ModuleWithProviders;
static withProviders2(): ModuleWithProviders;
}`
},
{
name: _('/node_modules/some-library/index.d.ts'),
contents: 'export declare class LibraryModule {}'
},
];
});
},
{
name: _('/node_modules/some-library/index.d.ts'),
contents: 'export declare class LibraryModule {}'
},
];
});
it('should fixup functions/methods that return ModuleWithProviders structures', () => {
const {bundle, renderer, host} =
setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM);
it('should fixup functions/methods that return ModuleWithProviders structures', () => {
const {bundle, renderer, host} =
setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM);
const referencesRegistry = new NgccReferencesRegistry(host);
const moduleWithProvidersAnalyses =
new ModuleWithProvidersAnalyzer(
host, bundle.src.program.getTypeChecker(), referencesRegistry, true)
.analyzeProgram(bundle.src.program);
const typingsFile = getSourceFileOrError(
bundle.dts!.program, _('/node_modules/test-package/typings/index.d.ts'));
const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile)!;
const referencesRegistry = new NgccReferencesRegistry(host);
const moduleWithProvidersAnalyses =
new ModuleWithProvidersAnalyzer(
host, bundle.src.program.getTypeChecker(), referencesRegistry, true)
.analyzeProgram(bundle.src.program);
const typingsFile = getSourceFileOrError(
bundle.dts!.program, _('/node_modules/test-package/typings/index.d.ts'));
const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile)!;
const output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[0].contents);
const importManager = new ImportManager(new NoopImportRewriter(), 'i');
renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager);
const output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[0].contents);
const importManager = new ImportManager(new NoopImportRewriter(), 'i');
renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager);
expect(output.toString()).toContain(`
expect(output.toString()).toContain(`
static withProviders1(): ModuleWithProviders<SomeModule>;
static withProviders2(): ModuleWithProviders<SomeModule>;
static withProviders3(): ModuleWithProviders<SomeClass>;
@ -594,7 +638,7 @@ export { D };
static withProviders6(): ModuleWithProviders<i2.LibraryModule>;
static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule};
static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`);
expect(output.toString()).toContain(`
expect(output.toString()).toContain(`
export declare function withProviders1(): ModuleWithProviders<SomeModule>;
export declare function withProviders2(): ModuleWithProviders<SomeModule>;
export declare function withProviders3(): ModuleWithProviders<SomeClass>;
@ -603,42 +647,44 @@ export { D };
export declare function withProviders6(): ModuleWithProviders<i2.LibraryModule>;
export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule};
export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`);
});
});
it('should not mistake `ModuleWithProviders` types that are not imported from `@angular/core',
() => {
const {bundle, renderer, host} =
setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM);
it('should not mistake `ModuleWithProviders` types that are not imported from `@angular/core',
() => {
const {bundle, renderer, host} =
setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM);
const referencesRegistry = new NgccReferencesRegistry(host);
const moduleWithProvidersAnalyses =
new ModuleWithProvidersAnalyzer(
host, bundle.src.program.getTypeChecker(), referencesRegistry, true)
.analyzeProgram(bundle.src.program);
const typingsFile = getSourceFileOrError(
bundle.dts!.program, _('/node_modules/test-package/typings/module.d.ts'));
const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile)!;
const referencesRegistry = new NgccReferencesRegistry(host);
const moduleWithProvidersAnalyses =
new ModuleWithProvidersAnalyzer(
host, bundle.src.program.getTypeChecker(), referencesRegistry, true)
.analyzeProgram(bundle.src.program);
const typingsFile = getSourceFileOrError(
bundle.dts!.program, _('/node_modules/test-package/typings/module.d.ts'));
const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile)!;
const output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[1].contents);
const importManager = new ImportManager(new NoopImportRewriter(), 'i');
renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager);
expect(output.toString()).toContain(`
const output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[1].contents);
const importManager = new ImportManager(new NoopImportRewriter(), 'i');
renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager);
expect(output.toString()).toContain(`
static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule};
static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`);
});
});
});
});
describe('printStatement', () => {
it('should transpile code to ES2015', () => {
const {renderer, sourceFile, importManager} = setup([PROGRAM]);
describe('printStatement', () => {
it('should transpile code to ES2015', () => {
const {renderer, sourceFile, importManager} = setup([PROGRAM]);
const stmt1 = new DeclareVarStmt('foo', new LiteralExpr(42), null, [StmtModifier.Final]);
const stmt2 = new DeclareVarStmt('bar', new LiteralExpr(true));
const stmt3 = new DeclareVarStmt('baz', new LiteralExpr('qux'), undefined, []);
const stmt1 = new DeclareVarStmt('foo', new LiteralExpr(42), null, [StmtModifier.Final]);
const stmt2 = new DeclareVarStmt('bar', new LiteralExpr(true));
const stmt3 = new DeclareVarStmt('baz', new LiteralExpr('qux'), undefined, []);
expect(renderer.printStatement(stmt1, sourceFile, importManager)).toBe('const foo = 42;');
expect(renderer.printStatement(stmt2, sourceFile, importManager)).toBe('var bar = true;');
expect(renderer.printStatement(stmt3, sourceFile, importManager)).toBe('var baz = "qux";');
expect(renderer.printStatement(stmt1, sourceFile, importManager)).toBe('const foo = 42;');
expect(renderer.printStatement(stmt2, sourceFile, importManager)).toBe('var bar = true;');
expect(renderer.printStatement(stmt3, sourceFile, importManager))
.toBe('var baz = "qux";');
});
});
});
});

View File

@ -481,8 +481,11 @@ SOME DEFINITION TEXT
program, absoluteFromSourceFile(sourceFile), 'NoIife', ts.isFunctionDeclaration);
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError(`Compiled class declaration is not inside an IIFE: NoIife in ${
_('/node_modules/test-package/some/file.js')}`);
.toThrowError(
`Compiled class "NoIife" in "${
_('/node_modules/test-package/some/file.js')}" does not have a valid syntax.\n` +
`Expected an ES5 IIFE wrapped function. But got:\n` +
`function NoIife() {}`);
const badIifeDeclaration = getDeclaration(
program, absoluteFromSourceFile(sourceFile), 'BadIife', ts.isVariableDeclaration);