fix(ivy): ngcc - insert new imports after existing ones (#30029)

Previously, ngcc would insert new imports at the beginning of the file, for
convenience. This is problematic for imports that have side-effects, as the
side-effects imposed by such imports may affect the behavior of subsequent
imports.

This commit teaches ngcc to insert imports after any existing imports. Special
care has been taken to ensure inserted constants will still follow after the
inserted imports.

Resolves FW-1271

PR Close #30029
This commit is contained in:
JoostK 2019-04-22 14:57:31 +02:00 committed by Ben Lesh
parent ceea0b418c
commit 8c80b851c8
5 changed files with 107 additions and 44 deletions

View File

@ -26,10 +26,13 @@ export class EsmRenderer extends Renderer {
/** /**
* Add the imports at the top of the file * Add the imports at the top of the file
*/ */
addImports(output: MagicString, imports: {specifier: string; qualifier: string;}[]): void { addImports(
// The imports get inserted at the very top of the file. output: MagicString, imports: {specifier: string; qualifier: string;}[],
imports.forEach( sf: ts.SourceFile): void {
i => { output.appendLeft(0, `import * as ${i.qualifier} from '${i.specifier}';\n`); }); const insertionPoint = findEndOfImports(sf);
const renderedImports =
imports.map(i => `import * as ${i.qualifier} from '${i.specifier}';\n`).join('');
output.appendLeft(insertionPoint, renderedImports);
} }
addExports(output: MagicString, entryPointBasePath: string, exports: ExportInfo[]): void { addExports(output: MagicString, entryPointBasePath: string, exports: ExportInfo[]): void {
@ -55,14 +58,11 @@ export class EsmRenderer extends Renderer {
if (constants === '') { if (constants === '') {
return; return;
} }
const insertionPoint = file.statements.reduce((prev, stmt) => { const insertionPoint = findEndOfImports(file);
if (ts.isImportDeclaration(stmt) || ts.isImportEqualsDeclaration(stmt) ||
ts.isNamespaceImport(stmt)) { // Append the constants to the right of the insertion point, to ensure they get ordered after
return stmt.getEnd(); // added imports (those are appended left to the insertion point).
} output.appendRight(insertionPoint, '\n' + constants + '\n');
return prev;
}, 0);
output.appendLeft(insertionPoint, '\n' + constants + '\n');
} }
/** /**
@ -115,6 +115,17 @@ export class EsmRenderer extends Renderer {
} }
} }
function findEndOfImports(sf: ts.SourceFile): number {
for (const stmt of sf.statements) {
if (!ts.isImportDeclaration(stmt) && !ts.isImportEqualsDeclaration(stmt) &&
!ts.isNamespaceImport(stmt)) {
return stmt.getStart();
}
}
return 0;
}
function findStatement(node: ts.Node) { function findStatement(node: ts.Node) {
while (node) { while (node) {
if (ts.isExpressionStatement(node)) { if (ts.isExpressionStatement(node)) {

View File

@ -155,7 +155,9 @@ export abstract class Renderer {
renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager), renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager),
compiledFile.sourceFile); compiledFile.sourceFile);
this.addImports(outputText, importManager.getAllImports(compiledFile.sourceFile.fileName)); this.addImports(
outputText, importManager.getAllImports(compiledFile.sourceFile.fileName),
compiledFile.sourceFile);
} }
// Add exports to the entry-point file // Add exports to the entry-point file
@ -185,7 +187,7 @@ export abstract class Renderer {
}); });
this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager); this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager);
this.addImports(outputText, importManager.getAllImports(dtsFile.fileName)); this.addImports(outputText, importManager.getAllImports(dtsFile.fileName), dtsFile);
this.addExports(outputText, dtsFile.fileName, renderInfo.privateExports); this.addExports(outputText, dtsFile.fileName, renderInfo.privateExports);
@ -246,8 +248,9 @@ export abstract class Renderer {
protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile): protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile):
void; void;
protected abstract addImports(output: MagicString, imports: {specifier: string, protected abstract addImports(
qualifier: string}[]): void; output: MagicString, imports: {specifier: string, qualifier: string}[],
sf: ts.SourceFile): void;
protected abstract addExports( protected abstract addExports(
output: MagicString, entryPointBasePath: string, exports: ExportInfo[]): void; output: MagicString, entryPointBasePath: string, exports: ExportInfo[]): void;
protected abstract addDefinitions( protected abstract addDefinitions(

View File

@ -42,6 +42,7 @@ const PROGRAM = {
name: '/some/file.js', name: '/some/file.js',
contents: ` contents: `
/* A copyright notice */ /* A copyright notice */
import 'some-side-effect';
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
export class A {} export class A {}
A.decorators = [ A.decorators = [
@ -114,17 +115,21 @@ export { D };
describe('Esm2015Renderer', () => { describe('Esm2015Renderer', () => {
describe('addImports', () => { describe('addImports', () => {
it('should insert the given imports at the start of the source file', () => { it('should insert the given imports after existing imports of the source file', () => {
const {renderer} = setup(PROGRAM); const {renderer, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports(output, [ renderer.addImports(
{specifier: '@angular/core', qualifier: 'i0'}, output,
{specifier: '@angular/common', qualifier: 'i1'} [
]); {specifier: '@angular/core', qualifier: 'i0'},
expect(output.toString()).toContain(`import * as i0 from '@angular/core'; {specifier: '@angular/common', qualifier: 'i1'}
import * as i1 from '@angular/common'; ],
sourceFile);
/* A copyright notice */`); 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';`);
}); });
}); });
@ -173,10 +178,27 @@ export {TopLevelComponent};`);
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 {}`); export class A {}`);
}); });
it('should insert constants after inserted imports', () => {
const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js');
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'const x = 3;', file);
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', () => { describe('rewriteSwitchableDeclarations', () => {

View File

@ -42,6 +42,7 @@ const PROGRAM = {
name: '/some/file.js', name: '/some/file.js',
contents: ` contents: `
/* A copyright notice */ /* A copyright notice */
import 'some-side-effect';
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
var A = (function() { var A = (function() {
function A() {} function A() {}
@ -151,17 +152,21 @@ export { D };
describe('Esm5Renderer', () => { describe('Esm5Renderer', () => {
describe('addImports', () => { describe('addImports', () => {
it('should insert the given imports at the start of the source file', () => { it('should insert the given imports after existing imports of the source file', () => {
const {renderer} = setup(PROGRAM); const {renderer, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports(output, [ renderer.addImports(
{specifier: '@angular/core', qualifier: 'i0'}, output,
{specifier: '@angular/common', qualifier: 'i1'} [
]); {specifier: '@angular/core', qualifier: 'i0'},
expect(output.toString()).toContain(`import * as i0 from '@angular/core'; {specifier: '@angular/common', qualifier: 'i1'}
import * as i1 from '@angular/common'; ],
sourceFile);
/* A copyright notice */`); 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';`);
}); });
}); });
@ -210,8 +215,25 @@ export {TopLevelComponent};`);
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;
var A = (function() {`);
});
it('should insert constants after inserted imports', () => {
const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js');
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'const x = 3;', file);
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;
var A = (function() {`); var A = (function() {`);
}); });
}); });

View File

@ -26,7 +26,8 @@ class TestRenderer extends Renderer {
logger: Logger, host: Esm2015ReflectionHost, isCore: boolean, bundle: EntryPointBundle) { logger: Logger, host: Esm2015ReflectionHost, isCore: boolean, bundle: EntryPointBundle) {
super(logger, host, isCore, bundle, '/src'); super(logger, host, isCore, bundle, '/src');
} }
addImports(output: MagicString, imports: {specifier: string, qualifier: string}[]) { addImports(
output: MagicString, imports: {specifier: string, qualifier: string}[], sf: ts.SourceFile) {
output.prepend('\n// ADD IMPORTS\n'); output.prepend('\n// ADD IMPORTS\n');
} }
addExports(output: MagicString, baseEntryPointPath: string, exports: { addExports(output: MagicString, baseEntryPointPath: string, exports: {
@ -510,11 +511,15 @@ describe('Renderer', () => {
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};`);
expect(renderer.addImports).toHaveBeenCalledWith(jasmine.any(MagicString), [ expect(renderer.addImports)
{specifier: './module', qualifier: 'ɵngcc0'}, .toHaveBeenCalledWith(
{specifier: '@angular/core', qualifier: 'ɵngcc1'}, jasmine.any(MagicString),
{specifier: 'some-library', qualifier: 'ɵngcc2'}, [
]); {specifier: './module', qualifier: 'ɵngcc0'},
{specifier: '@angular/core', qualifier: 'ɵngcc1'},
{specifier: 'some-library', qualifier: 'ɵngcc2'},
],
jasmine.anything());
// The following expectation checks that we do not mistake `ModuleWithProviders` types // The following expectation checks that we do not mistake `ModuleWithProviders` types