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 * Find the statement that contains the given node
* @param node a node whose containing statement we wish to find * @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) { while (node.parent) {
if (ts.isBlock(node.parent) || ts.isSourceFile(node.parent)) { if (ts.isBlock(node.parent) || ts.isSourceFile(node.parent)) {
break; break;

View File

@ -8,10 +8,12 @@
import {Statement} from '@angular/compiler'; import {Statement} from '@angular/compiler';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports'; import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports';
import {ImportManager, translateStatement} from '../../../src/ngtsc/translator'; import {ImportManager, translateStatement} from '../../../src/ngtsc/translator';
import {CompiledClass} from '../analysis/types'; import {CompiledClass} from '../analysis/types';
import {getIifeBody} from '../host/esm5_host'; import {getContainingStatement} from '../host/esm2015_host';
import {EsmRenderingFormatter} from './esm_rendering_formatter'; import {EsmRenderingFormatter} from './esm_rendering_formatter';
/** /**
@ -20,11 +22,24 @@ import {EsmRenderingFormatter} from './esm_rendering_formatter';
*/ */
export class Esm5RenderingFormatter extends EsmRenderingFormatter { 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 { addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const iifeBody = getIifeBody(compiledClass.declaration); const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
if (!iifeBody) { 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 ${ throw new Error(`Compiled class declaration is not inside an IIFE: ${compiledClass.name} in ${
compiledClass.declaration.getSourceFile().fileName}`); 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 {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer';
import {ExportInfo} from '../analysis/private_declarations_analyzer'; import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {CompiledClass} from '../analysis/types'; 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 {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {RedundantDecoratorMap, RenderingFormatter} from './rendering_formatter'; import {RedundantDecoratorMap, RenderingFormatter} from './rendering_formatter';
@ -104,7 +104,8 @@ export class EsmRenderingFormatter implements RenderingFormatter {
if (!classSymbol) { if (!classSymbol) {
throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`); 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(); const insertionPoint = declarationStatement.getEnd();
output.appendLeft(insertionPoint, '\n' + definitions); 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 { function findStatement(node: ts.Node): ts.Statement|undefined {
while (node) { while (node) {
if (ts.isExpressionStatement(node) || ts.isReturnStatement(node)) { if (ts.isExpressionStatement(node) || ts.isReturnStatement(node)) {

View File

@ -315,8 +315,11 @@ SOME DEFINITION TEXT
program, absoluteFromSourceFile(sourceFile), 'NoIife', ts.isFunctionDeclaration); program, absoluteFromSourceFile(sourceFile), 'NoIife', ts.isFunctionDeclaration);
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'}; const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError(`Compiled class declaration is not inside an IIFE: NoIife in ${ .toThrowError(
_('/node_modules/test-package/some/file.js')}`); `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( const badIifeDeclaration = getDeclaration(
program, absoluteFromSourceFile(sourceFile), 'BadIife', ts.isVariableDeclaration); program, absoluteFromSourceFile(sourceFile), 'BadIife', ts.isVariableDeclaration);

View File

@ -321,8 +321,11 @@ SOME DEFINITION TEXT
program, absoluteFromSourceFile(sourceFile), 'NoIife', ts.isFunctionDeclaration); program, absoluteFromSourceFile(sourceFile), 'NoIife', ts.isFunctionDeclaration);
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'}; const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError(`Compiled class declaration is not inside an IIFE: NoIife in ${ .toThrowError(
_('/node_modules/test-package/some/file.js')}`); `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( const badIifeDeclaration = getDeclaration(
program, absoluteFromSourceFile(sourceFile), 'BadIife', ts.isVariableDeclaration); program, absoluteFromSourceFile(sourceFile), 'BadIife', ts.isVariableDeclaration);

View File

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