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:
parent
ceea0b418c
commit
8c80b851c8
|
@ -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)) {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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() {`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue