refactor(ivy): move ngcc into a higher level folder (#29092)
PR Close #29092
This commit is contained in:

committed by
Matias Niemelä

parent
cf4718c366
commit
a770aa231d
@ -0,0 +1,335 @@
|
||||
/**
|
||||
* @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';
|
||||
|
||||
function setup(file: {name: string, contents: string}) {
|
||||
const dir = dirname(file.name);
|
||||
const bundle = makeTestEntryPointBundle('esm2015', [file]) !;
|
||||
const typeChecker = bundle.src.program.getTypeChecker();
|
||||
const host = new Esm2015ReflectionHost(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(host, false, bundle, dir, 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 {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 at the start of the source file', () => {
|
||||
const {renderer} = setup(PROGRAM);
|
||||
const output = new MagicString(PROGRAM.contents);
|
||||
renderer.addImports(output, [
|
||||
{specifier: '@angular/core', qualifier: 'i0'},
|
||||
{specifier: '@angular/common', qualifier: 'i1'}
|
||||
]);
|
||||
expect(output.toString()).toContain(`import * as i0 from '@angular/core';
|
||||
import * as i1 from '@angular/common';
|
||||
|
||||
/* A copyright notice */`);
|
||||
});
|
||||
});
|
||||
|
||||
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 {}`);
|
||||
});
|
||||
});
|
||||
|
||||
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 };`);
|
||||
});
|
||||
});
|
||||
});
|
405
packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts
Normal file
405
packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts
Normal file
@ -0,0 +1,405 @@
|
||||
/**
|
||||
* @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 {Esm5ReflectionHost} from '../../src/host/esm5_host';
|
||||
import {Esm5Renderer} from '../../src/rendering/esm5_renderer';
|
||||
import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils';
|
||||
|
||||
function setup(file: {name: string, contents: string}) {
|
||||
const dir = dirname(file.name);
|
||||
const bundle = makeTestEntryPointBundle('esm5', [file]);
|
||||
const typeChecker = bundle.src.program.getTypeChecker();
|
||||
const host = new Esm5ReflectionHost(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 Esm5Renderer(host, false, bundle, dir, 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 {Directive} from '@angular/core';
|
||||
var A = (function() {
|
||||
function A() {}
|
||||
A.decorators = [
|
||||
{ type: Directive, args: [{ selector: '[a]' }] },
|
||||
{ type: OtherA }
|
||||
];
|
||||
A.prototype.ngDoCheck = function() {
|
||||
//
|
||||
};
|
||||
return A;
|
||||
}());
|
||||
|
||||
var B = (function() {
|
||||
function B() {}
|
||||
B.decorators = [
|
||||
{ type: OtherB },
|
||||
{ type: Directive, args: [{ selector: '[b]' }] }
|
||||
];
|
||||
return B;
|
||||
}());
|
||||
|
||||
var C = (function() {
|
||||
function C() {}
|
||||
C.decorators = [
|
||||
{ type: Directive, args: [{ selector: '[c]' }] },
|
||||
];
|
||||
return C;
|
||||
}());
|
||||
|
||||
function NoIife() {}
|
||||
|
||||
var BadIife = (function() {
|
||||
function BadIife() {}
|
||||
BadIife.decorators = [
|
||||
{ type: Directive, args: [{ selector: '[c]' }] },
|
||||
];
|
||||
}());
|
||||
|
||||
var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;
|
||||
var 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
|
||||
export {A, B, C, NoIife, BadIife};`
|
||||
};
|
||||
|
||||
const PROGRAM_DECORATE_HELPER = {
|
||||
name: '/some/file.js',
|
||||
contents: `
|
||||
import * as tslib_1 from "tslib";
|
||||
/* A copyright notice */
|
||||
import { Directive } from '@angular/core';
|
||||
var OtherA = function () { return function (node) { }; };
|
||||
var OtherB = function () { return function (node) { }; };
|
||||
var A = /** @class */ (function () {
|
||||
function A() {
|
||||
}
|
||||
A = tslib_1.__decorate([
|
||||
Directive({ selector: '[a]' }),
|
||||
OtherA()
|
||||
], A);
|
||||
return A;
|
||||
}());
|
||||
export { A };
|
||||
var B = /** @class */ (function () {
|
||||
function B() {
|
||||
}
|
||||
B = tslib_1.__decorate([
|
||||
OtherB(),
|
||||
Directive({ selector: '[b]' })
|
||||
], B);
|
||||
return B;
|
||||
}());
|
||||
export { B };
|
||||
var C = /** @class */ (function () {
|
||||
function C() {
|
||||
}
|
||||
C = tslib_1.__decorate([
|
||||
Directive({ selector: '[c]' })
|
||||
], C);
|
||||
return C;
|
||||
}());
|
||||
export { C };
|
||||
var D = /** @class */ (function () {
|
||||
function D() {
|
||||
}
|
||||
D_1 = D;
|
||||
var D_1;
|
||||
D = D_1 = tslib_1.__decorate([
|
||||
Directive({ selector: '[d]', providers: [D_1] })
|
||||
], D);
|
||||
return D;
|
||||
}());
|
||||
export { D };
|
||||
// Some other content`
|
||||
};
|
||||
|
||||
describe('Esm5Renderer', () => {
|
||||
|
||||
describe('addImports', () => {
|
||||
it('should insert the given imports at the start of the source file', () => {
|
||||
const {renderer} = setup(PROGRAM);
|
||||
const output = new MagicString(PROGRAM.contents);
|
||||
renderer.addImports(output, [
|
||||
{specifier: '@angular/core', qualifier: 'i0'},
|
||||
{specifier: '@angular/common', qualifier: 'i1'}
|
||||
]);
|
||||
expect(output.toString()).toContain(`import * as i0 from '@angular/core';
|
||||
import * as i1 from '@angular/common';
|
||||
|
||||
/* A copyright notice */`);
|
||||
});
|
||||
});
|
||||
|
||||
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(`
|
||||
export {A, B, C, NoIife, BadIife};
|
||||
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;
|
||||
|
||||
var A = (function() {`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rewriteSwitchableDeclarations', () => {
|
||||
it('should switch marked declaration initializers', () => {
|
||||
const {renderer, program, sourceFile, switchMarkerAnalyses} = 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(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`);
|
||||
expect(output.toString())
|
||||
.toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`);
|
||||
expect(output.toString())
|
||||
.toContain(`var 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 before the return statement of the class IIFE',
|
||||
() => {
|
||||
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(`
|
||||
A.prototype.ngDoCheck = function() {
|
||||
//
|
||||
};
|
||||
SOME DEFINITION TEXT
|
||||
return A;
|
||||
`);
|
||||
});
|
||||
|
||||
it('should error if the compiledClass is not valid', () => {
|
||||
const {renderer, host, sourceFile, program} = setup(PROGRAM);
|
||||
const output = new MagicString(PROGRAM.contents);
|
||||
|
||||
const badSymbolDeclaration =
|
||||
getDeclaration(program, sourceFile.fileName, 'A', ts.isVariableDeclaration);
|
||||
const badSymbol: any = {name: 'BadSymbol', declaration: badSymbolDeclaration};
|
||||
const hostSpy = spyOn(host, 'getClassSymbol').and.returnValue(null);
|
||||
expect(() => renderer.addDefinitions(output, badSymbol, 'SOME DEFINITION TEXT'))
|
||||
.toThrowError('Compiled class does not have a valid symbol: BadSymbol in /some/file.js');
|
||||
|
||||
|
||||
const noIifeDeclaration =
|
||||
getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration);
|
||||
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
|
||||
hostSpy.and.returnValue({valueDeclaration: noIifeDeclaration});
|
||||
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
|
||||
.toThrowError(
|
||||
'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js');
|
||||
|
||||
const badIifeWrapper: any =
|
||||
getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration);
|
||||
const badIifeDeclaration =
|
||||
badIifeWrapper.initializer.expression.expression.body.statements[0];
|
||||
const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'};
|
||||
hostSpy.and.returnValue({valueDeclaration: badIifeDeclaration});
|
||||
expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT'))
|
||||
.toThrowError(
|
||||
'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('removeDecorators', () => {
|
||||
|
||||
it('should delete the decorator (and following comma) that was matched in the analysis', () => {
|
||||
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
|
||||
const output = new MagicString(PROGRAM.contents);
|
||||
const compiledClass =
|
||||
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
|
||||
const decorator = compiledClass.decorators[0];
|
||||
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
|
||||
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
|
||||
renderer.removeDecorators(output, decoratorsToRemove);
|
||||
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 {renderer, decorationAnalyses, sourceFile} = 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 not other decorators left) that was matched in the analysis',
|
||||
() => {
|
||||
const {renderer, decorationAnalyses, sourceFile} = 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);
|
||||
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
|
||||
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()).toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`);
|
||||
expect(output.toString()).not.toContain(`C.decorators = [
|
||||
{ type: Directive, args: [{ selector: '[c]' }] },
|
||||
];`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
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 no 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(`function C() {\n }\n return C;`);
|
||||
});
|
||||
});
|
||||
});
|
522
packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts
Normal file
522
packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts
Normal file
@ -0,0 +1,522 @@
|
||||
/**
|
||||
* @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 * as fs from 'fs';
|
||||
import MagicString from 'magic-string';
|
||||
import * as ts from 'typescript';
|
||||
import {fromObject, generateMapFileComment} from 'convert-source-map';
|
||||
import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
|
||||
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
|
||||
import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer';
|
||||
import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer';
|
||||
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
|
||||
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
||||
import {RedundantDecoratorMap, Renderer} from '../../src/rendering/renderer';
|
||||
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
|
||||
import {makeTestEntryPointBundle} from '../helpers/utils';
|
||||
|
||||
class TestRenderer extends Renderer {
|
||||
constructor(host: Esm2015ReflectionHost, isCore: boolean, bundle: EntryPointBundle) {
|
||||
super(host, isCore, bundle, '/src', '/dist');
|
||||
}
|
||||
addImports(output: MagicString, imports: {specifier: string, qualifier: string}[]) {
|
||||
output.prepend('\n// ADD IMPORTS\n');
|
||||
}
|
||||
addExports(output: MagicString, baseEntryPointPath: string, exports: {
|
||||
identifier: string,
|
||||
from: string
|
||||
}[]) {
|
||||
output.prepend('\n// ADD EXPORTS\n');
|
||||
}
|
||||
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
|
||||
output.prepend('\n// ADD CONSTANTS\n');
|
||||
}
|
||||
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string) {
|
||||
output.prepend('\n// ADD DEFINITIONS\n');
|
||||
}
|
||||
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap) {
|
||||
output.prepend('\n// REMOVE DECORATORS\n');
|
||||
}
|
||||
rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void {
|
||||
output.prepend('\n// REWRITTEN DECLARATIONS\n');
|
||||
}
|
||||
}
|
||||
|
||||
function createTestRenderer(
|
||||
packageName: string, files: {name: string, contents: string}[],
|
||||
dtsFiles?: {name: string, contents: string}[]) {
|
||||
const isCore = packageName === '@angular/core';
|
||||
const bundle = makeTestEntryPointBundle('esm2015', files, dtsFiles);
|
||||
const typeChecker = bundle.src.program.getTypeChecker();
|
||||
const host = new Esm2015ReflectionHost(isCore, typeChecker, bundle.dts);
|
||||
const referencesRegistry = new NgccReferencesRegistry(host);
|
||||
const decorationAnalyses = new DecorationAnalyzer(
|
||||
bundle.src.program, bundle.src.options, bundle.src.host,
|
||||
typeChecker, host, referencesRegistry, bundle.rootDirs, isCore)
|
||||
.analyzeProgram();
|
||||
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
|
||||
const moduleWithProvidersAnalyses =
|
||||
new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program);
|
||||
const privateDeclarationsAnalyses =
|
||||
new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program);
|
||||
const renderer = new TestRenderer(host, isCore, bundle);
|
||||
spyOn(renderer, 'addImports').and.callThrough();
|
||||
spyOn(renderer, 'addDefinitions').and.callThrough();
|
||||
spyOn(renderer, 'removeDecorators').and.callThrough();
|
||||
|
||||
return {renderer, decorationAnalyses, switchMarkerAnalyses, moduleWithProvidersAnalyses,
|
||||
privateDeclarationsAnalyses};
|
||||
}
|
||||
|
||||
|
||||
describe('Renderer', () => {
|
||||
const INPUT_PROGRAM = {
|
||||
name: '/src/file.js',
|
||||
contents:
|
||||
`import { Directive } from '@angular/core';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: '[a]' }] }\n];\n`
|
||||
};
|
||||
const INPUT_DTS_PROGRAM = {
|
||||
name: '/typings/file.d.ts',
|
||||
contents: `export declare class A {\nfoo(x: number): number;\n}\n`
|
||||
};
|
||||
|
||||
const COMPONENT_PROGRAM = {
|
||||
name: '/src/component.js',
|
||||
contents:
|
||||
`import { Component } from '@angular/core';\nexport class A {}\nA.decorators = [\n { type: Component, args: [{ selector: 'a', template: '{{ person!.name }}' }] }\n];\n`
|
||||
};
|
||||
|
||||
const INPUT_PROGRAM_MAP = fromObject({
|
||||
'version': 3,
|
||||
'file': '/src/file.js',
|
||||
'sourceRoot': '',
|
||||
'sources': ['/src/file.ts'],
|
||||
'names': [],
|
||||
'mappings':
|
||||
'AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,MAAM;IACF,GAAG,CAAC,CAAS;QACT,OAAO,CAAC,CAAC;IACb,CAAC;;AACM,YAAU,GAAG;IAChB,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE;CACnD,CAAC',
|
||||
'sourcesContent': [INPUT_PROGRAM.contents]
|
||||
});
|
||||
|
||||
const RENDERED_CONTENTS =
|
||||
`\n// ADD EXPORTS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n\n// REMOVE DECORATORS\n` +
|
||||
INPUT_PROGRAM.contents;
|
||||
|
||||
const OUTPUT_PROGRAM_MAP = fromObject({
|
||||
'version': 3,
|
||||
'file': '/dist/file.js',
|
||||
'sources': ['/src/file.js'],
|
||||
'sourcesContent': [INPUT_PROGRAM.contents],
|
||||
'names': [],
|
||||
'mappings': ';;;;;;;;;;AAAA;;;;;;;;;'
|
||||
});
|
||||
|
||||
const MERGED_OUTPUT_PROGRAM_MAP = fromObject({
|
||||
'version': 3,
|
||||
'sources': ['/src/file.ts'],
|
||||
'names': [],
|
||||
'mappings': ';;;;;;;;;;AAAA',
|
||||
'file': '/dist/file.js',
|
||||
'sourcesContent': [INPUT_PROGRAM.contents]
|
||||
});
|
||||
|
||||
describe('renderProgram()', () => {
|
||||
it('should render the modified contents; and a new map file, if the original provided no map file.',
|
||||
() => {
|
||||
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]);
|
||||
const result = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
expect(result[0].path).toEqual('/dist/file.js');
|
||||
expect(result[0].contents)
|
||||
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map'));
|
||||
expect(result[1].path).toEqual('/dist/file.js.map');
|
||||
expect(result[1].contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON());
|
||||
});
|
||||
|
||||
|
||||
it('should render as JavaScript', () => {
|
||||
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} = createTestRenderer('test-package', [COMPONENT_PROGRAM]);
|
||||
renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
|
||||
expect(addDefinitionsSpy.calls.first().args[2])
|
||||
.toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
|
||||
type: Component,
|
||||
args: [{ selector: 'a', template: '{{ person!.name }}' }]
|
||||
}], null, null);
|
||||
A.ngComponentDef = ɵngcc0.ɵdefineComponent({ type: A, selectors: [["a"]], factory: function A_Factory(t) { return new (t || A)(); }, consts: 1, vars: 1, template: function A_Template(rf, ctx) { if (rf & 1) {
|
||||
ɵngcc0.ɵtext(0);
|
||||
} if (rf & 2) {
|
||||
ɵngcc0.ɵtextBinding(0, ɵngcc0.ɵinterpolation1("", ctx.person.name, ""));
|
||||
} }, encapsulation: 2 });`);
|
||||
});
|
||||
|
||||
|
||||
describe('calling abstract methods', () => {
|
||||
it('should call addImports with the source code and info about the core Angular library.',
|
||||
() => {
|
||||
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} =
|
||||
createTestRenderer('test-package', [INPUT_PROGRAM]);
|
||||
const result = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
const addImportsSpy = renderer.addImports as jasmine.Spy;
|
||||
expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
|
||||
expect(addImportsSpy.calls.first().args[1]).toEqual([
|
||||
{specifier: '@angular/core', qualifier: 'ɵngcc0'}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.',
|
||||
() => {
|
||||
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} =
|
||||
createTestRenderer('test-package', [INPUT_PROGRAM]);
|
||||
const result = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
|
||||
expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
|
||||
expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({
|
||||
name: 'A',
|
||||
decorators: [jasmine.objectContaining({name: 'Directive'})],
|
||||
}));
|
||||
expect(addDefinitionsSpy.calls.first().args[2])
|
||||
.toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
|
||||
type: Directive,
|
||||
args: [{ selector: '[a]' }]
|
||||
}], null, { foo: [] });
|
||||
A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); } });`);
|
||||
});
|
||||
|
||||
it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',
|
||||
() => {
|
||||
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} =
|
||||
createTestRenderer('test-package', [INPUT_PROGRAM]);
|
||||
const result = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy;
|
||||
expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
|
||||
|
||||
// Each map key is the TS node of the decorator container
|
||||
// Each map value is an array of TS nodes that are the decorators to remove
|
||||
const map = removeDecoratorsSpy.calls.first().args[1] as Map<ts.Node, ts.Node[]>;
|
||||
const keys = Array.from(map.keys());
|
||||
expect(keys.length).toEqual(1);
|
||||
expect(keys[0].getText())
|
||||
.toEqual(`[\n { type: Directive, args: [{ selector: '[a]' }] }\n]`);
|
||||
const values = Array.from(map.values());
|
||||
expect(values.length).toEqual(1);
|
||||
expect(values[0].length).toEqual(1);
|
||||
expect(values[0][0].getText())
|
||||
.toEqual(`{ type: Directive, args: [{ selector: '[a]' }] }`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('source map merging', () => {
|
||||
it('should merge any inline source map from the original file and write the output as an inline source map',
|
||||
() => {
|
||||
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} =
|
||||
createTestRenderer(
|
||||
'test-package', [{
|
||||
...INPUT_PROGRAM,
|
||||
contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment()
|
||||
}]);
|
||||
const result = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
expect(result[0].path).toEqual('/dist/file.js');
|
||||
expect(result[0].contents)
|
||||
.toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment());
|
||||
expect(result[1]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should merge any external source map from the original file and write the output to an external source map',
|
||||
() => {
|
||||
// Mock out reading the map file from disk
|
||||
spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON());
|
||||
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} =
|
||||
createTestRenderer(
|
||||
'test-package', [{
|
||||
...INPUT_PROGRAM,
|
||||
contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map'
|
||||
}]);
|
||||
const result = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
expect(result[0].path).toEqual('/dist/file.js');
|
||||
expect(result[0].contents)
|
||||
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map'));
|
||||
expect(result[1].path).toEqual('/dist/file.js.map');
|
||||
expect(result[1].contents).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toJSON());
|
||||
});
|
||||
});
|
||||
|
||||
describe('@angular/core support', () => {
|
||||
it('should render relative imports in ESM bundles', () => {
|
||||
const CORE_FILE = {
|
||||
name: '/src/core.js',
|
||||
contents:
|
||||
`import { NgModule } from './ng_module';\nexport class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n`
|
||||
};
|
||||
const R3_SYMBOLS_FILE = {
|
||||
// r3_symbols in the file name indicates that this is the path to rewrite core imports to
|
||||
name: '/src/r3_symbols.js',
|
||||
contents: `export const NgModule = () => null;`
|
||||
};
|
||||
// The package name of `@angular/core` indicates that we are compiling the core library.
|
||||
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} =
|
||||
createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]);
|
||||
renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
|
||||
expect(addDefinitionsSpy.calls.first().args[2])
|
||||
.toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`);
|
||||
const addImportsSpy = renderer.addImports as jasmine.Spy;
|
||||
expect(addImportsSpy.calls.first().args[1]).toEqual([
|
||||
{specifier: './r3_symbols', qualifier: 'ɵngcc0'}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render no imports in FESM bundles', () => {
|
||||
const CORE_FILE = {
|
||||
name: '/src/core.js',
|
||||
contents: `export const NgModule = () => null;
|
||||
export class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n`
|
||||
};
|
||||
|
||||
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} = createTestRenderer('@angular/core', [CORE_FILE]);
|
||||
renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
|
||||
expect(addDefinitionsSpy.calls.first().args[2])
|
||||
.toContain(`/*@__PURE__*/ setClassMetadata(`);
|
||||
const addImportsSpy = renderer.addImports as jasmine.Spy;
|
||||
expect(addImportsSpy.calls.first().args[1]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering typings', () => {
|
||||
it('should render extract types into typings files', () => {
|
||||
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} =
|
||||
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
|
||||
const result = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
|
||||
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
|
||||
expect(typingsFile.contents)
|
||||
.toContain(
|
||||
'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ɵDirectiveDefWithMeta');
|
||||
});
|
||||
|
||||
it('should render imports into typings files', () => {
|
||||
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} =
|
||||
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
|
||||
const result = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
|
||||
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
|
||||
expect(typingsFile.contents).toContain(`// ADD IMPORTS\nexport declare class A`);
|
||||
});
|
||||
|
||||
it('should render exports into typings files', () => {
|
||||
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} =
|
||||
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
|
||||
|
||||
// Add a mock export to trigger export rendering
|
||||
privateDeclarationsAnalyses.push(
|
||||
{identifier: 'ComponentB', from: '/src/file.js', dtsFrom: '/typings/b.d.ts'});
|
||||
|
||||
const result = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
|
||||
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
|
||||
expect(typingsFile.contents)
|
||||
.toContain(`// ADD EXPORTS\n\n// ADD IMPORTS\nexport declare class A`);
|
||||
});
|
||||
|
||||
it('should fixup functions/methods that return ModuleWithProviders structures', () => {
|
||||
const MODULE_WITH_PROVIDERS_PROGRAM = [
|
||||
{
|
||||
name: '/src/index.js',
|
||||
contents: `
|
||||
import {ExternalModule} from './module';
|
||||
import {LibraryModule} from 'some-library';
|
||||
export class SomeClass {}
|
||||
export class SomeModule {
|
||||
static withProviders1() {
|
||||
return {ngModule: SomeModule};
|
||||
}
|
||||
static withProviders2() {
|
||||
return {ngModule: SomeModule};
|
||||
}
|
||||
static withProviders3() {
|
||||
return {ngModule: SomeClass};
|
||||
}
|
||||
static withProviders4() {
|
||||
return {ngModule: ExternalModule};
|
||||
}
|
||||
static withProviders5() {
|
||||
return {ngModule: ExternalModule};
|
||||
}
|
||||
static withProviders6() {
|
||||
return {ngModule: LibraryModule};
|
||||
}
|
||||
static withProviders7() {
|
||||
return {ngModule: SomeModule, providers: []};
|
||||
};
|
||||
static withProviders8() {
|
||||
return {ngModule: SomeModule};
|
||||
}
|
||||
}
|
||||
export function withProviders1() {
|
||||
return {ngModule: SomeModule};
|
||||
}
|
||||
export function withProviders2() {
|
||||
return {ngModule: SomeModule};
|
||||
}
|
||||
export function withProviders3() {
|
||||
return {ngModule: SomeClass};
|
||||
}
|
||||
export function withProviders4() {
|
||||
return {ngModule: ExternalModule};
|
||||
}
|
||||
export function withProviders5() {
|
||||
return {ngModule: ExternalModule};
|
||||
}
|
||||
export function withProviders6() {
|
||||
return {ngModule: LibraryModule};
|
||||
}
|
||||
export function withProviders7() {
|
||||
return {ngModule: SomeModule, providers: []};
|
||||
};
|
||||
export function withProviders8() {
|
||||
return {ngModule: SomeModule};
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: '/src/module.js',
|
||||
contents: `
|
||||
export class ExternalModule {
|
||||
static withProviders1() {
|
||||
return {ngModule: ExternalModule};
|
||||
}
|
||||
static withProviders2() {
|
||||
return {ngModule: ExternalModule};
|
||||
}
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: '/node_modules/some-library/index.d.ts',
|
||||
contents: 'export declare class LibraryModule {}'
|
||||
},
|
||||
];
|
||||
const MODULE_WITH_PROVIDERS_DTS_PROGRAM = [
|
||||
{
|
||||
name: '/typings/index.d.ts',
|
||||
contents: `
|
||||
import {ModuleWithProviders} from '@angular/core';
|
||||
export declare class SomeClass {}
|
||||
export interface MyModuleWithProviders extends ModuleWithProviders {}
|
||||
export declare class SomeModule {
|
||||
static withProviders1(): ModuleWithProviders;
|
||||
static withProviders2(): ModuleWithProviders<any>;
|
||||
static withProviders3(): ModuleWithProviders<SomeClass>;
|
||||
static withProviders4(): ModuleWithProviders;
|
||||
static withProviders5();
|
||||
static withProviders6(): ModuleWithProviders;
|
||||
static withProviders7(): {ngModule: SomeModule, providers: any[]};
|
||||
static withProviders8(): MyModuleWithProviders;
|
||||
}
|
||||
export declare function withProviders1(): ModuleWithProviders;
|
||||
export declare function withProviders2(): ModuleWithProviders<any>;
|
||||
export declare function withProviders3(): ModuleWithProviders<SomeClass>;
|
||||
export declare function withProviders4(): ModuleWithProviders;
|
||||
export declare function withProviders5();
|
||||
export declare function withProviders6(): ModuleWithProviders;
|
||||
export declare function withProviders7(): {ngModule: SomeModule, providers: any[]};
|
||||
export declare function withProviders8(): MyModuleWithProviders;`
|
||||
},
|
||||
{
|
||||
name: '/typings/module.d.ts',
|
||||
contents: `
|
||||
export interface ModuleWithProviders {}
|
||||
export declare class ExternalModule {
|
||||
static withProviders1(): ModuleWithProviders;
|
||||
static withProviders2(): ModuleWithProviders;
|
||||
}`
|
||||
},
|
||||
{
|
||||
name: '/node_modules/some-library/index.d.ts',
|
||||
contents: 'export declare class LibraryModule {}'
|
||||
},
|
||||
];
|
||||
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} =
|
||||
createTestRenderer(
|
||||
'test-package', MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM);
|
||||
|
||||
const result = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
|
||||
const typingsFile = result.find(f => f.path === '/typings/index.d.ts') !;
|
||||
|
||||
expect(typingsFile.contents).toContain(`
|
||||
static withProviders1(): ModuleWithProviders<SomeModule>;
|
||||
static withProviders2(): ModuleWithProviders<SomeModule>;
|
||||
static withProviders3(): ModuleWithProviders<SomeClass>;
|
||||
static withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>;
|
||||
static withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>;
|
||||
static withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>;
|
||||
static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule};
|
||||
static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`);
|
||||
expect(typingsFile.contents).toContain(`
|
||||
export declare function withProviders1(): ModuleWithProviders<SomeModule>;
|
||||
export declare function withProviders2(): ModuleWithProviders<SomeModule>;
|
||||
export declare function withProviders3(): ModuleWithProviders<SomeClass>;
|
||||
export declare function withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>;
|
||||
export declare function withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>;
|
||||
export declare function withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>;
|
||||
export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule};
|
||||
export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`);
|
||||
|
||||
expect(renderer.addImports).toHaveBeenCalledWith(jasmine.any(MagicString), [
|
||||
{specifier: './module', qualifier: 'ɵngcc0'},
|
||||
{specifier: '@angular/core', qualifier: 'ɵngcc1'},
|
||||
{specifier: 'some-library', qualifier: 'ɵngcc2'},
|
||||
]);
|
||||
|
||||
|
||||
// The following expectation checks that we do not mistake `ModuleWithProviders` types
|
||||
// that are not imported from `@angular/core`.
|
||||
const typingsFile2 = result.find(f => f.path === '/typings/module.d.ts') !;
|
||||
expect(typingsFile2.contents).toContain(`
|
||||
static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule};
|
||||
static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user