
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
360 lines
16 KiB
TypeScript
360 lines
16 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
import {dirname} from 'canonical-path';
|
|
import MagicString from 'magic-string';
|
|
import * as ts from 'typescript';
|
|
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
|
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
|
|
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
|
|
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
|
|
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
|
import {EsmRenderer} from '../../src/rendering/esm_renderer';
|
|
import {makeTestEntryPointBundle} from '../helpers/utils';
|
|
import {MockLogger} from '../helpers/mock_logger';
|
|
|
|
function setup(file: {name: string, contents: string}) {
|
|
const logger = new MockLogger();
|
|
const dir = dirname(file.name);
|
|
const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, [file]) !;
|
|
const typeChecker = bundle.src.program.getTypeChecker();
|
|
const host = new Esm2015ReflectionHost(logger, false, typeChecker);
|
|
const referencesRegistry = new NgccReferencesRegistry(host);
|
|
const decorationAnalyses =
|
|
new DecorationAnalyzer(
|
|
bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host,
|
|
referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false)
|
|
.analyzeProgram();
|
|
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
|
|
const renderer = new EsmRenderer(logger, host, false, bundle, dir);
|
|
return {
|
|
host,
|
|
program: bundle.src.program,
|
|
sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses
|
|
};
|
|
}
|
|
|
|
const PROGRAM = {
|
|
name: '/some/file.js',
|
|
contents: `
|
|
/* A copyright notice */
|
|
import 'some-side-effect';
|
|
import {Directive} from '@angular/core';
|
|
export class A {}
|
|
A.decorators = [
|
|
{ type: Directive, args: [{ selector: '[a]' }] },
|
|
{ type: OtherA }
|
|
];
|
|
export class B {}
|
|
B.decorators = [
|
|
{ type: OtherB },
|
|
{ type: Directive, args: [{ selector: '[b]' }] }
|
|
];
|
|
export class C {}
|
|
C.decorators = [
|
|
{ type: Directive, args: [{ selector: '[c]' }] },
|
|
];
|
|
let compileNgModuleFactory = compileNgModuleFactory__PRE_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`
|
|
};
|
|
|
|
const PROGRAM_DECORATE_HELPER = {
|
|
name: '/some/file.js',
|
|
contents: `
|
|
import * as tslib_1 from "tslib";
|
|
var D_1;
|
|
/* A copyright notice */
|
|
import { Directive } from '@angular/core';
|
|
const OtherA = () => (node) => { };
|
|
const OtherB = () => (node) => { };
|
|
let A = class A {
|
|
};
|
|
A = tslib_1.__decorate([
|
|
Directive({ selector: '[a]' }),
|
|
OtherA()
|
|
], A);
|
|
export { A };
|
|
let B = class B {
|
|
};
|
|
B = tslib_1.__decorate([
|
|
OtherB(),
|
|
Directive({ selector: '[b]' })
|
|
], B);
|
|
export { B };
|
|
let C = class C {
|
|
};
|
|
C = tslib_1.__decorate([
|
|
Directive({ selector: '[c]' })
|
|
], C);
|
|
export { C };
|
|
let D = D_1 = class D {
|
|
};
|
|
D = D_1 = tslib_1.__decorate([
|
|
Directive({ selector: '[d]', providers: [D_1] })
|
|
], D);
|
|
export { D };
|
|
// Some other content`
|
|
};
|
|
|
|
describe('Esm2015Renderer', () => {
|
|
|
|
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';`);
|
|
});
|
|
});
|
|
|
|
describe('addExports', () => {
|
|
it('should insert the given exports at the end of the source file', () => {
|
|
const {renderer} = setup(PROGRAM);
|
|
const output = new MagicString(PROGRAM.contents);
|
|
renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [
|
|
{from: '/some/a.js', dtsFrom: '/some/a.d.ts', identifier: 'ComponentA1'},
|
|
{from: '/some/a.js', dtsFrom: '/some/a.d.ts', identifier: 'ComponentA2'},
|
|
{from: '/some/foo/b.js', dtsFrom: '/some/foo/b.d.ts', identifier: 'ComponentB'},
|
|
{from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'},
|
|
]);
|
|
expect(output.toString()).toContain(`
|
|
// Some other content
|
|
export {ComponentA1} from './a';
|
|
export {ComponentA2} from './a';
|
|
export {ComponentB} from './foo/b';
|
|
export {TopLevelComponent};`);
|
|
});
|
|
|
|
it('should not insert alias exports in js output', () => {
|
|
const {renderer} = setup(PROGRAM);
|
|
const output = new MagicString(PROGRAM.contents);
|
|
renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [
|
|
{from: '/some/a.js', alias: 'eComponentA1', identifier: 'ComponentA1'},
|
|
{from: '/some/a.js', alias: 'eComponentA2', identifier: 'ComponentA2'},
|
|
{from: '/some/foo/b.js', alias: 'eComponentB', identifier: 'ComponentB'},
|
|
{from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'},
|
|
]);
|
|
const outputString = output.toString();
|
|
expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`);
|
|
expect(outputString).not.toContain(`{eComponentB as ComponentB}`);
|
|
expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`);
|
|
});
|
|
});
|
|
|
|
describe('addConstants', () => {
|
|
it('should insert the given constants after imports in the source file', () => {
|
|
const {renderer, program} = setup(PROGRAM);
|
|
const file = program.getSourceFile('some/file.js');
|
|
if (file === undefined) {
|
|
throw new Error(`Could not find source file`);
|
|
}
|
|
const output = new MagicString(PROGRAM.contents);
|
|
renderer.addConstants(output, 'const x = 3;', file);
|
|
expect(output.toString()).toContain(`
|
|
import {Directive} from '@angular/core';
|
|
|
|
const x = 3;
|
|
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', () => {
|
|
it('should switch marked declaration initializers', () => {
|
|
const {renderer, program, switchMarkerAnalyses, sourceFile} = setup(PROGRAM);
|
|
const file = program.getSourceFile('some/file.js');
|
|
if (file === undefined) {
|
|
throw new Error(`Could not find source file`);
|
|
}
|
|
const output = new MagicString(PROGRAM.contents);
|
|
renderer.rewriteSwitchableDeclarations(
|
|
output, file, 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 {}
|
|
SOME DEFINITION TEXT
|
|
A.decorators = [
|
|
`);
|
|
});
|
|
|
|
});
|
|
|
|
|
|
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 (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]', () => {
|
|
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 };`);
|
|
});
|
|
});
|
|
});
|