2018-07-16 05:23:37 -04:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
2019-11-04 12:29:01 -05:00
|
|
|
import {Statement} from '@angular/compiler';
|
2018-07-16 05:23:37 -04:00
|
|
|
import MagicString from 'magic-string';
|
2018-11-25 16:40:25 -05:00
|
|
|
import * as ts from 'typescript';
|
2019-06-06 15:22:32 -04:00
|
|
|
import {fromObject, generateMapFileComment, SourceMapConverter} from 'convert-source-map';
|
|
|
|
import {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
|
|
|
|
import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
2019-11-04 12:29:01 -05:00
|
|
|
import {NOOP_DEFAULT_IMPORT_RECORDER, Reexport} from '../../../src/ngtsc/imports';
|
2019-06-06 15:22:32 -04:00
|
|
|
import {loadTestFiles} from '../../../test/helpers';
|
2019-11-04 12:29:01 -05:00
|
|
|
import {Import, ImportManager, translateStatement} from '../../../src/ngtsc/translator';
|
2019-07-18 16:05:32 -04:00
|
|
|
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
|
|
|
|
import {CompiledClass} from '../../src/analysis/types';
|
2018-11-13 09:40:54 -05:00
|
|
|
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
|
2019-04-28 15:48:35 -04:00
|
|
|
import {ModuleWithProvidersInfo} from '../../src/analysis/module_with_providers_analyzer';
|
2019-04-28 15:48:35 -04:00
|
|
|
import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer';
|
2018-10-04 07:19:11 -04:00
|
|
|
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
|
2018-10-10 09:17:32 -04:00
|
|
|
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
2019-11-01 12:55:10 -04:00
|
|
|
import {Esm5ReflectionHost} from '../../src/host/esm5_host';
|
2019-04-28 15:48:35 -04:00
|
|
|
import {Renderer} from '../../src/rendering/renderer';
|
|
|
|
import {MockLogger} from '../helpers/mock_logger';
|
|
|
|
import {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter';
|
2019-06-06 15:22:32 -04:00
|
|
|
import {makeTestEntryPointBundle, getRootFiles} from '../helpers/utils';
|
2019-04-28 15:47:57 -04:00
|
|
|
|
2019-04-28 15:48:35 -04:00
|
|
|
class TestRenderingFormatter implements RenderingFormatter {
|
2019-11-04 12:29:01 -05:00
|
|
|
private printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
|
|
|
|
|
2019-04-28 15:48:33 -04:00
|
|
|
addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) {
|
2018-07-16 05:23:37 -04:00
|
|
|
output.prepend('\n// ADD IMPORTS\n');
|
|
|
|
}
|
2019-04-28 15:48:35 -04:00
|
|
|
addExports(output: MagicString, baseEntryPointPath: string, exports: ExportInfo[]) {
|
2018-11-25 17:07:51 -05:00
|
|
|
output.prepend('\n// ADD EXPORTS\n');
|
|
|
|
}
|
2019-10-14 16:04:42 -04:00
|
|
|
addDirectExports(output: MagicString, exports: Reexport[]): void {
|
|
|
|
output.prepend('\n// ADD DIRECT EXPORTS\n');
|
|
|
|
}
|
2018-06-27 11:41:11 -04:00
|
|
|
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
|
|
|
|
output.prepend('\n// ADD CONSTANTS\n');
|
|
|
|
}
|
2018-10-16 03:56:54 -04:00
|
|
|
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string) {
|
2018-07-16 05:23:37 -04:00
|
|
|
output.prepend('\n// ADD DEFINITIONS\n');
|
|
|
|
}
|
2019-11-06 12:03:56 -05:00
|
|
|
addAdjacentStatements(output: MagicString, compiledClass: CompiledClass, statements: string) {
|
|
|
|
output.prepend('\n// ADD ADJACENT STATEMENTS\n');
|
|
|
|
}
|
2018-11-18 15:37:30 -05:00
|
|
|
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap) {
|
2018-07-16 05:23:37 -04:00
|
|
|
output.prepend('\n// REMOVE DECORATORS\n');
|
|
|
|
}
|
2018-08-17 02:50:45 -04:00
|
|
|
rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void {
|
|
|
|
output.prepend('\n// REWRITTEN DECLARATIONS\n');
|
|
|
|
}
|
2019-04-28 15:48:35 -04:00
|
|
|
addModuleWithProvidersParams(
|
|
|
|
output: MagicString, moduleWithProviders: ModuleWithProvidersInfo[],
|
|
|
|
importManager: ImportManager): void {
|
|
|
|
output.prepend('\n// ADD MODUlE WITH PROVIDERS PARAMS\n');
|
|
|
|
}
|
2019-11-04 12:29:01 -05:00
|
|
|
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
|
|
|
|
const node = translateStatement(
|
|
|
|
stmt, importManager, NOOP_DEFAULT_IMPORT_RECORDER, ts.ScriptTarget.ES2015);
|
|
|
|
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
|
|
|
|
|
|
|
|
return `// TRANSPILED\n${code}`;
|
|
|
|
}
|
2018-07-16 05:23:37 -04:00
|
|
|
}
|
|
|
|
|
2018-11-11 13:16:04 -05:00
|
|
|
function createTestRenderer(
|
2019-11-01 12:55:10 -04:00
|
|
|
packageName: string, files: TestFile[], dtsFiles?: TestFile[], mappingFiles?: TestFile[],
|
|
|
|
isEs5 = false) {
|
2019-03-29 06:13:14 -04:00
|
|
|
const logger = new MockLogger();
|
2019-06-06 15:22:32 -04:00
|
|
|
loadTestFiles(files);
|
|
|
|
if (dtsFiles) {
|
|
|
|
loadTestFiles(dtsFiles);
|
|
|
|
}
|
|
|
|
if (mappingFiles) {
|
|
|
|
loadTestFiles(mappingFiles);
|
|
|
|
}
|
|
|
|
const fs = getFileSystem();
|
2018-11-25 16:40:25 -05:00
|
|
|
const isCore = packageName === '@angular/core';
|
2019-06-06 15:22:32 -04:00
|
|
|
const bundle = makeTestEntryPointBundle(
|
2019-11-01 12:55:10 -04:00
|
|
|
'test-package', 'esm5', isCore, getRootFiles(files), dtsFiles && getRootFiles(dtsFiles));
|
2018-11-25 16:40:25 -05:00
|
|
|
const typeChecker = bundle.src.program.getTypeChecker();
|
2019-11-01 12:55:10 -04:00
|
|
|
const host = isEs5 ? new Esm5ReflectionHost(logger, isCore, typeChecker, bundle.dts) :
|
|
|
|
new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts);
|
2018-11-13 09:40:54 -05:00
|
|
|
const referencesRegistry = new NgccReferencesRegistry(host);
|
2019-05-25 15:53:52 -04:00
|
|
|
const decorationAnalyses =
|
|
|
|
new DecorationAnalyzer(fs, bundle, host, referencesRegistry).analyzeProgram();
|
2019-05-25 16:34:40 -04:00
|
|
|
const switchMarkerAnalyses =
|
|
|
|
new SwitchMarkerAnalyzer(host, bundle.entryPoint.package).analyzeProgram(bundle.src.program);
|
2018-11-25 17:07:51 -05:00
|
|
|
const privateDeclarationsAnalyses =
|
|
|
|
new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program);
|
2019-04-28 15:48:35 -04:00
|
|
|
const testFormatter = new TestRenderingFormatter();
|
|
|
|
spyOn(testFormatter, 'addExports').and.callThrough();
|
|
|
|
spyOn(testFormatter, 'addImports').and.callThrough();
|
|
|
|
spyOn(testFormatter, 'addDefinitions').and.callThrough();
|
2019-11-06 12:03:56 -05:00
|
|
|
spyOn(testFormatter, 'addAdjacentStatements').and.callThrough();
|
2019-04-28 15:48:35 -04:00
|
|
|
spyOn(testFormatter, 'addConstants').and.callThrough();
|
|
|
|
spyOn(testFormatter, 'removeDecorators').and.callThrough();
|
|
|
|
spyOn(testFormatter, 'rewriteSwitchableDeclarations').and.callThrough();
|
|
|
|
spyOn(testFormatter, 'addModuleWithProvidersParams').and.callThrough();
|
2019-11-04 12:29:01 -05:00
|
|
|
spyOn(testFormatter, 'printStatement').and.callThrough();
|
2019-04-28 15:48:35 -04:00
|
|
|
|
2019-11-01 12:55:10 -04:00
|
|
|
const renderer = new Renderer(host, testFormatter, fs, logger, bundle);
|
2018-09-28 09:57:50 -04:00
|
|
|
|
2019-04-28 15:48:33 -04:00
|
|
|
return {renderer,
|
2019-04-28 15:48:35 -04:00
|
|
|
testFormatter,
|
2019-04-28 15:48:33 -04:00
|
|
|
decorationAnalyses,
|
|
|
|
switchMarkerAnalyses,
|
|
|
|
privateDeclarationsAnalyses,
|
|
|
|
bundle};
|
2018-07-16 05:23:37 -04:00
|
|
|
}
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
runInEachFileSystem(() => {
|
|
|
|
describe('Renderer', () => {
|
|
|
|
let _: typeof absoluteFrom;
|
|
|
|
let INPUT_PROGRAM: TestFile;
|
|
|
|
let COMPONENT_PROGRAM: TestFile;
|
2019-07-30 18:57:46 -04:00
|
|
|
let NGMODULE_PROGRAM: TestFile;
|
2019-06-06 15:22:32 -04:00
|
|
|
let INPUT_PROGRAM_MAP: SourceMapConverter;
|
|
|
|
let RENDERED_CONTENTS: string;
|
|
|
|
let OUTPUT_PROGRAM_MAP: SourceMapConverter;
|
|
|
|
let MERGED_OUTPUT_PROGRAM_MAP: SourceMapConverter;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
_ = absoluteFrom;
|
|
|
|
|
|
|
|
INPUT_PROGRAM = {
|
2019-05-25 16:34:40 -04:00
|
|
|
name: _('/node_modules/test-package/src/file.js'),
|
2019-06-06 15:22:32 -04:00
|
|
|
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`
|
|
|
|
};
|
|
|
|
|
|
|
|
COMPONENT_PROGRAM = {
|
2019-05-25 16:34:40 -04:00
|
|
|
name: _('/node_modules/test-package/src/component.js'),
|
2019-06-06 15:22:32 -04:00
|
|
|
contents:
|
|
|
|
`import { Component } from '@angular/core';\nexport class A {}\nA.decorators = [\n { type: Component, args: [{ selector: 'a', template: '{{ person!.name }}' }] }\n];\n`
|
|
|
|
};
|
|
|
|
|
2019-07-30 18:57:46 -04:00
|
|
|
NGMODULE_PROGRAM = {
|
|
|
|
name: _('/node_modules/test-package/src/ngmodule.js'),
|
|
|
|
contents:
|
|
|
|
`import { NgModule } from '@angular/core';\nexport class A {}\nA.decorators = [\n { type: NgModule, args: [{}] }\n];\n`
|
|
|
|
};
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
INPUT_PROGRAM_MAP = fromObject({
|
|
|
|
'version': 3,
|
2019-05-25 16:34:40 -04:00
|
|
|
'file': _('/node_modules/test-package/src/file.js'),
|
2019-06-06 15:22:32 -04:00
|
|
|
'sourceRoot': '',
|
2019-05-25 16:34:40 -04:00
|
|
|
'sources': [_('/node_modules/test-package/src/file.ts')],
|
2019-06-06 15:22:32 -04:00
|
|
|
'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]
|
|
|
|
});
|
2018-09-28 09:57:50 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
RENDERED_CONTENTS = `
|
2019-04-28 15:48:35 -04:00
|
|
|
// ADD IMPORTS
|
|
|
|
|
|
|
|
// ADD EXPORTS
|
|
|
|
|
|
|
|
// ADD CONSTANTS
|
|
|
|
|
2019-11-06 12:03:56 -05:00
|
|
|
// ADD ADJACENT STATEMENTS
|
|
|
|
|
2019-04-28 15:48:35 -04:00
|
|
|
// ADD DEFINITIONS
|
|
|
|
|
|
|
|
// REMOVE DECORATORS
|
|
|
|
` + INPUT_PROGRAM.contents;
|
2018-10-04 07:19:11 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
OUTPUT_PROGRAM_MAP = fromObject({
|
|
|
|
'version': 3,
|
|
|
|
'file': 'file.js',
|
2019-05-25 16:34:40 -04:00
|
|
|
'sources': [_('/node_modules/test-package/src/file.js')],
|
2019-06-06 15:22:32 -04:00
|
|
|
'sourcesContent': [INPUT_PROGRAM.contents],
|
|
|
|
'names': [],
|
2019-11-06 12:03:56 -05:00
|
|
|
'mappings': ';;;;;;;;;;;;AAAA;;;;;;;;;'
|
2019-06-06 15:22:32 -04:00
|
|
|
});
|
2018-07-16 05:23:37 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
MERGED_OUTPUT_PROGRAM_MAP = fromObject({
|
|
|
|
'version': 3,
|
2019-05-25 16:34:40 -04:00
|
|
|
'sources': [_('/node_modules/test-package/src/file.ts')],
|
2019-06-06 15:22:32 -04:00
|
|
|
'names': [],
|
2019-11-06 12:03:56 -05:00
|
|
|
'mappings': ';;;;;;;;;;;;AAAA',
|
2019-06-06 15:22:32 -04:00
|
|
|
'file': '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} =
|
|
|
|
createTestRenderer('test-package', [INPUT_PROGRAM]);
|
|
|
|
const result = renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
2019-05-25 16:34:40 -04:00
|
|
|
expect(result[0].path).toEqual(_('/node_modules/test-package/src/file.js'));
|
2019-06-06 15:22:32 -04:00
|
|
|
expect(result[0].contents)
|
|
|
|
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map'));
|
2019-05-25 16:34:40 -04:00
|
|
|
expect(result[1].path).toEqual(_('/node_modules/test-package/src/file.js.map'));
|
2019-06-06 15:22:32 -04:00
|
|
|
expect(result[1].contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON());
|
|
|
|
});
|
2018-07-16 05:23:37 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
|
|
|
|
it('should render as JavaScript', () => {
|
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} = createTestRenderer('test-package', [COMPONENT_PROGRAM]);
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
|
2019-11-04 12:29:01 -05:00
|
|
|
expect(addDefinitionsSpy.calls.first().args[2]).toEqual(`// TRANSPILED
|
|
|
|
A.ɵfac = function A_Factory(t) { return new (t || A)(); };
|
|
|
|
// TRANSPILED
|
2019-10-10 17:57:15 -04:00
|
|
|
A.ɵcmp = ɵngcc0.ɵɵdefineComponent({ type: A, selectors: [["a"]], decls: 1, vars: 1, template: function A_Template(rf, ctx) { if (rf & 1) {
|
2019-05-17 21:49:21 -04:00
|
|
|
ɵngcc0.ɵɵtext(0);
|
2018-12-10 11:34:56 -05:00
|
|
|
} if (rf & 2) {
|
2019-04-23 23:40:05 -04:00
|
|
|
ɵngcc0.ɵɵtextInterpolate(ctx.person.name);
|
2019-11-06 12:03:56 -05:00
|
|
|
} }, encapsulation: 2 });`);
|
|
|
|
|
|
|
|
const addAdjacentStatementsSpy = testFormatter.addAdjacentStatements as jasmine.Spy;
|
2019-11-04 12:29:01 -05:00
|
|
|
expect(addAdjacentStatementsSpy.calls.first().args[2]).toEqual(`// TRANSPILED
|
|
|
|
/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
|
2019-03-29 16:31:22 -04:00
|
|
|
type: Component,
|
|
|
|
args: [{ selector: 'a', template: '{{ person!.name }}' }]
|
|
|
|
}], null, null);`);
|
2019-06-06 15:22:32 -04:00
|
|
|
});
|
2018-12-10 11:34:56 -05:00
|
|
|
|
2018-07-16 05:23:37 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
describe('calling RenderingFormatter methods', () => {
|
|
|
|
it('should call addImports with the source code and info about the core Angular library.',
|
|
|
|
() => {
|
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
|
|
|
|
const result = renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
const addImportsSpy = testFormatter.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,
|
|
|
|
testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
|
2019-11-06 12:03:56 -05:00
|
|
|
renderer.renderProgram(
|
2019-06-06 15:22:32 -04:00
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
const addDefinitionsSpy = testFormatter.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'})]
|
|
|
|
}));
|
2019-11-04 12:29:01 -05:00
|
|
|
expect(addDefinitionsSpy.calls.first().args[2]).toEqual(`// TRANSPILED
|
|
|
|
A.ɵfac = function A_Factory(t) { return new (t || A)(); };
|
|
|
|
// TRANSPILED
|
2019-11-06 12:03:56 -05:00
|
|
|
A.ɵdir = ɵngcc0.ɵɵdefineDirective({ type: A, selectors: [["", "a", ""]] });`);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should call addAdjacentStatements with the source code, the analyzed class and the rendered statements',
|
|
|
|
() => {
|
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
const addAdjacentStatementsSpy = testFormatter.addAdjacentStatements as jasmine.Spy;
|
|
|
|
expect(addAdjacentStatementsSpy.calls.first().args[0].toString())
|
|
|
|
.toEqual(RENDERED_CONTENTS);
|
|
|
|
expect(addAdjacentStatementsSpy.calls.first().args[1])
|
|
|
|
.toEqual(jasmine.objectContaining(
|
|
|
|
{name: 'A', decorators: [jasmine.objectContaining({name: 'Directive'})]}));
|
2019-11-04 12:29:01 -05:00
|
|
|
expect(addAdjacentStatementsSpy.calls.first().args[2]).toEqual(`// TRANSPILED
|
|
|
|
/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
|
2018-10-30 14:19:10 -04:00
|
|
|
type: Directive,
|
|
|
|
args: [{ selector: '[a]' }]
|
fix(ngcc): generate correct metadata for classes with getter/setter properties (#33514)
While processing class metadata, ngtsc generates a `setClassMetadata()`
call which (among other things) contains info about property decorators.
Previously, processing getter/setter pairs with some of ngcc's
`ReflectionHost`s resulted in multiple metadata entries for the same
property, which resulted in duplicate object keys, which in turn causes
an error in ES5 strict mode.
This commit fixes it by ensuring that there are no duplicate property
names in the `setClassMetadata()` calls.
In addition, `generateSetClassMetadataCall()` is updated to treat
`ClassMember#decorators: []` the same as `ClassMember.decorators: null`
(i.e. omitting the `ClassMember` from the generated `setClassMetadata()`
call). Alternatively, ngcc's `ReflectionHost`s could be updated to do
this transformation (`decorators: []` --> `decorators: null`) when
reflecting on class members, but this would require changes in many
places and be less future-proof.
For example, given a class such as:
```ts
class Foo {
@Input() get bar() { return 'bar'; }
set bar(value: any) {}
}
```
...previously the generated `setClassMetadata()` call would look like:
```ts
ɵsetClassMetadata(..., {
bar: [{type: Input}],
bar: [],
});
```
The same class will now result in a call like:
```ts
ɵsetClassMetadata(..., {
bar: [{type: Input}],
});
```
Fixes #30569
PR Close #33514
2019-10-30 09:50:19 -04:00
|
|
|
}], null, null);`);
|
2019-06-06 15:22:32 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',
|
|
|
|
() => {
|
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
|
2019-11-04 12:29:01 -05:00
|
|
|
renderer.renderProgram(
|
2019-06-06 15:22:32 -04:00
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
const removeDecoratorsSpy = testFormatter.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]' }] }`);
|
|
|
|
});
|
|
|
|
|
2019-11-06 12:03:56 -05:00
|
|
|
it('should render definitions as static fields', () => {
|
2019-07-30 18:57:46 -04:00
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} = createTestRenderer('test-package', [NGMODULE_PROGRAM]);
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
|
|
|
|
const definitions: string = addDefinitionsSpy.calls.first().args[2];
|
2019-11-06 12:03:56 -05:00
|
|
|
expect(definitions).toContain('A.ɵmod = ɵngcc0.ɵɵdefineNgModule(');
|
|
|
|
expect(definitions).toContain('A.ɵinj = ɵngcc0.ɵɵdefineInjector(');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should render adjacent statements', () => {
|
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} = createTestRenderer('test-package', [NGMODULE_PROGRAM]);
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
const addAdjacentStatementsSpy = testFormatter.addAdjacentStatements as jasmine.Spy;
|
|
|
|
const statements: string = addAdjacentStatementsSpy.calls.first().args[2];
|
|
|
|
expect(statements).toContain('ɵsetClassMetadata(A');
|
2019-07-30 18:57:46 -04:00
|
|
|
});
|
|
|
|
|
2019-11-01 12:55:10 -04:00
|
|
|
it('should render directives using the inner class name if different from outer', () => {
|
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} =
|
|
|
|
createTestRenderer(
|
|
|
|
'test-package', [{
|
|
|
|
name: _('/node_modules/test-package/src/file.js'),
|
|
|
|
contents: `
|
|
|
|
import { Directive } from '@angular/core';
|
|
|
|
var OuterClass = /** @class */ (function () {
|
|
|
|
function InnerClass() {}
|
|
|
|
return InnerClass;
|
|
|
|
}());
|
|
|
|
OuterClass.decorators = [{ type: Directive, args: [{ selector: '[test]' }]
|
|
|
|
export OuterClass;`
|
|
|
|
}],
|
|
|
|
undefined, undefined, /* isEs5 */ true);
|
|
|
|
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
|
|
|
|
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
|
2019-11-06 12:03:56 -05:00
|
|
|
const definitions = addDefinitionsSpy.calls.first().args[2];
|
|
|
|
expect(definitions).toContain('InnerClass.ɵfac');
|
|
|
|
expect(definitions).toContain('new (t || InnerClass)');
|
|
|
|
expect(definitions).toContain('InnerClass.ɵdir');
|
|
|
|
expect(definitions).toContain('type: InnerClass');
|
|
|
|
|
|
|
|
const addAdjacentStatementsSpy = testFormatter.addAdjacentStatements as jasmine.Spy;
|
|
|
|
const statements = addAdjacentStatementsSpy.calls.first().args[2];
|
|
|
|
expect(statements).toContain('ɵsetClassMetadata(InnerClass');
|
2019-11-01 12:55:10 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should render injectables using the inner class name if different from outer', () => {
|
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} =
|
|
|
|
createTestRenderer(
|
|
|
|
'test-package', [{
|
|
|
|
name: _('/node_modules/test-package/src/file.js'),
|
|
|
|
contents: `
|
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
var OuterClass = /** @class */ (function () {
|
|
|
|
function InnerClass() {}
|
|
|
|
return InnerClass;
|
|
|
|
}());
|
|
|
|
OuterClass.decorators = [{ type: Injectable }]
|
|
|
|
export OuterClass;`
|
|
|
|
}],
|
|
|
|
undefined, undefined, /* isEs5 */ true);
|
|
|
|
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
|
|
|
|
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
|
2019-11-06 12:03:56 -05:00
|
|
|
const definitions = addDefinitionsSpy.calls.first().args[2];
|
|
|
|
expect(definitions).toContain('InnerClass.ɵfac');
|
|
|
|
expect(definitions).toContain('new (t || InnerClass)()');
|
|
|
|
expect(definitions).toContain('InnerClass.ɵprov');
|
|
|
|
expect(definitions).toContain('token: InnerClass');
|
|
|
|
|
|
|
|
const addAdjacentStatementsSpy = testFormatter.addAdjacentStatements as jasmine.Spy;
|
|
|
|
const statements = addAdjacentStatementsSpy.calls.first().args[2];
|
|
|
|
expect(statements).toContain('ɵsetClassMetadata(InnerClass');
|
2019-11-01 12:55:10 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should render ng-modules using the inner class name if different from outer', () => {
|
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} =
|
|
|
|
createTestRenderer(
|
|
|
|
'test-package', [{
|
|
|
|
name: _('/node_modules/test-package/src/file.js'),
|
|
|
|
contents: `
|
|
|
|
import { NgModule, Directive } from '@angular/core';
|
|
|
|
var DirectiveClass = /** @class */ (function () {
|
|
|
|
function DirectiveClass() {}
|
|
|
|
return DirectiveClass;
|
|
|
|
}());
|
|
|
|
DirectiveClass.decorators = [{ type: Directive, args: [{selector: 'x'}] }];
|
|
|
|
var OuterClass = /** @class */ (function () {
|
|
|
|
function InnerClass() {}
|
|
|
|
return InnerClass;
|
|
|
|
}());
|
|
|
|
OuterClass.decorators = [{ type: NgModule, args: [{declarations: [DirectiveClass], exports: [DirectiveClass]}]
|
|
|
|
export OuterClass;`
|
|
|
|
}],
|
|
|
|
undefined, undefined, /* isEs5 */ true);
|
|
|
|
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
|
|
|
|
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
|
2019-11-06 12:03:56 -05:00
|
|
|
const definitions = addDefinitionsSpy.calls.all()[1].args[2];
|
|
|
|
expect(definitions).toContain('InnerClass.ɵmod');
|
|
|
|
expect(definitions).toContain('type: InnerClass');
|
|
|
|
|
|
|
|
|
|
|
|
const addAdjacentStatementsSpy = testFormatter.addAdjacentStatements as jasmine.Spy;
|
|
|
|
const statements = addAdjacentStatementsSpy.calls.all()[1].args[2];
|
|
|
|
expect(statements).toContain('ɵɵsetNgModuleScope(InnerClass');
|
|
|
|
expect(statements).toContain('ɵsetClassMetadata(InnerClass');
|
2019-11-01 12:55:10 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should render pipes using the inner class name if different from outer', () => {
|
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} =
|
|
|
|
createTestRenderer(
|
|
|
|
'test-package', [{
|
|
|
|
name: _('/node_modules/test-package/src/file.js'),
|
|
|
|
contents: `
|
|
|
|
import { Pipe } from '@angular/core';
|
|
|
|
var OuterClass = /** @class */ (function () {
|
|
|
|
function InnerClass() {}
|
|
|
|
return InnerClass;
|
|
|
|
}());
|
|
|
|
OuterClass.decorators = [{ type: Pipe, args: [{name: 'pipe'}]
|
|
|
|
export OuterClass;`
|
|
|
|
}],
|
|
|
|
undefined, undefined, /* isEs5 */ true);
|
|
|
|
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
|
|
|
|
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
|
2019-11-06 12:03:56 -05:00
|
|
|
const definitions = addDefinitionsSpy.calls.first().args[2];
|
|
|
|
expect(definitions).toContain('InnerClass.ɵfac');
|
|
|
|
expect(definitions).toContain('new (t || InnerClass)()');
|
|
|
|
expect(definitions).toContain('InnerClass.ɵpipe');
|
|
|
|
|
|
|
|
const addAdjacentStatementsSpy = testFormatter.addAdjacentStatements as jasmine.Spy;
|
|
|
|
const statements = addAdjacentStatementsSpy.calls.first().args[2];
|
|
|
|
expect(statements).toContain('ɵsetClassMetadata(InnerClass');
|
2019-11-01 12:55:10 -04:00
|
|
|
});
|
|
|
|
|
2019-10-25 13:45:08 -04:00
|
|
|
it('should render classes without decorators if class fields are decorated', () => {
|
2019-06-06 15:22:32 -04:00
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
2019-05-25 16:34:40 -04:00
|
|
|
testFormatter} =
|
|
|
|
createTestRenderer('test-package', [{
|
|
|
|
name: _('/node_modules/test-package/src/file.js'),
|
|
|
|
contents: `
|
2019-06-06 15:22:32 -04:00
|
|
|
import { Directive, ViewChild } from '@angular/core';
|
|
|
|
|
|
|
|
export class UndecoratedBase { test = null; }
|
|
|
|
|
|
|
|
UndecoratedBase.propDecorators = {
|
|
|
|
test: [{
|
|
|
|
type: ViewChild,
|
|
|
|
args: ["test", {static: true}]
|
|
|
|
}],
|
|
|
|
};
|
|
|
|
`
|
2019-05-25 16:34:40 -04:00
|
|
|
}]);
|
2019-06-06 15:22:32 -04:00
|
|
|
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
|
|
|
|
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
|
2019-11-04 12:29:01 -05:00
|
|
|
expect(addDefinitionsSpy.calls.first().args[2]).toEqual(`// TRANSPILED
|
|
|
|
UndecoratedBase.ɵfac = function UndecoratedBase_Factory(t) { return new (t || UndecoratedBase)(); };
|
|
|
|
// TRANSPILED
|
2019-10-27 05:59:23 -04:00
|
|
|
UndecoratedBase.ɵdir = ɵngcc0.ɵɵdefineDirective({ type: UndecoratedBase, viewQuery: function UndecoratedBase_Query(rf, ctx) { if (rf & 1) {
|
2019-07-20 06:32:29 -04:00
|
|
|
ɵngcc0.ɵɵstaticViewQuery(_c0, true);
|
2019-06-03 12:41:47 -04:00
|
|
|
} if (rf & 2) {
|
|
|
|
var _t;
|
2019-08-12 04:27:18 -04:00
|
|
|
ɵngcc0.ɵɵqueryRefresh(_t = ɵngcc0.ɵɵloadQuery()) && (ctx.test = _t.first);
|
2019-06-03 12:41:47 -04:00
|
|
|
} } });`);
|
2019-06-06 15:22:32 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should call renderImports after other abstract methods', () => {
|
|
|
|
// This allows the other methods to add additional imports if necessary
|
|
|
|
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
|
|
|
|
const addExportsSpy = testFormatter.addExports as jasmine.Spy;
|
|
|
|
const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
|
2019-11-06 12:03:56 -05:00
|
|
|
const addAdjacentStatementsSpy = testFormatter.addAdjacentStatements as jasmine.Spy;
|
2019-06-06 15:22:32 -04:00
|
|
|
const addConstantsSpy = testFormatter.addConstants as jasmine.Spy;
|
|
|
|
const addImportsSpy = testFormatter.addImports as jasmine.Spy;
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
|
|
|
expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy);
|
|
|
|
expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy);
|
2019-11-06 12:03:56 -05:00
|
|
|
expect(addAdjacentStatementsSpy).toHaveBeenCalledBefore(addImportsSpy);
|
2019-06-06 15:22:32 -04:00
|
|
|
expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy);
|
|
|
|
});
|
2019-06-03 12:41:47 -04:00
|
|
|
});
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
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} =
|
|
|
|
createTestRenderer(
|
|
|
|
'test-package', [{
|
|
|
|
...INPUT_PROGRAM,
|
|
|
|
contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment()
|
|
|
|
}]);
|
|
|
|
const result = renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
2019-05-25 16:34:40 -04:00
|
|
|
expect(result[0].path).toEqual(_('/node_modules/test-package/src/file.js'));
|
2019-06-06 15:22:32 -04:00
|
|
|
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',
|
|
|
|
() => {
|
|
|
|
const sourceFiles: TestFile[] = [{
|
|
|
|
...INPUT_PROGRAM,
|
|
|
|
contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map'
|
|
|
|
}];
|
|
|
|
const mappingFiles: TestFile[] =
|
|
|
|
[{name: _(INPUT_PROGRAM.name + '.map'), contents: INPUT_PROGRAM_MAP.toJSON()}];
|
|
|
|
const {decorationAnalyses, renderer, switchMarkerAnalyses,
|
|
|
|
privateDeclarationsAnalyses} =
|
|
|
|
createTestRenderer('test-package', sourceFiles, undefined, mappingFiles);
|
|
|
|
const result = renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
2019-05-25 16:34:40 -04:00
|
|
|
expect(result[0].path).toEqual(_('/node_modules/test-package/src/file.js'));
|
2019-06-06 15:22:32 -04:00
|
|
|
expect(result[0].contents)
|
|
|
|
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map'));
|
2019-05-25 16:34:40 -04:00
|
|
|
expect(result[1].path).toEqual(_('/node_modules/test-package/src/file.js.map'));
|
2019-06-06 15:22:32 -04:00
|
|
|
expect(JSON.parse(result[1].contents)).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toObject());
|
|
|
|
});
|
2018-11-11 13:16:04 -05:00
|
|
|
});
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
describe('@angular/core support', () => {
|
|
|
|
it('should render relative imports in ESM bundles', () => {
|
|
|
|
const CORE_FILE: TestFile = {
|
2019-05-25 16:34:40 -04:00
|
|
|
name: _('/node_modules/test-package/src/core.js'),
|
2019-06-06 15:22:32 -04:00
|
|
|
contents:
|
|
|
|
`import { NgModule } from './ng_module';\nexport class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n`
|
|
|
|
};
|
|
|
|
const R3_SYMBOLS_FILE: TestFile = {
|
|
|
|
// r3_symbols in the file name indicates that this is the path to rewrite core imports
|
|
|
|
// to
|
2019-05-25 16:34:40 -04:00
|
|
|
name: _('/node_modules/test-package/src/r3_symbols.js'),
|
2019-06-06 15:22:32 -04:00
|
|
|
contents: `export const NgModule = () => null;`
|
|
|
|
};
|
|
|
|
// The package name of `@angular/core` indicates that we are compiling the core library.
|
|
|
|
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} = createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]);
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
2019-11-06 12:03:56 -05:00
|
|
|
const addAdjacentStatementsSpy = testFormatter.addAdjacentStatements as jasmine.Spy;
|
|
|
|
expect(addAdjacentStatementsSpy.calls.first().args[2])
|
2019-06-06 15:22:32 -04:00
|
|
|
.toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`);
|
|
|
|
const addImportsSpy = testFormatter.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: TestFile = {
|
2019-05-25 16:34:40 -04:00
|
|
|
name: _('/node_modules/test-package/src/core.js'),
|
2019-06-06 15:22:32 -04:00
|
|
|
contents: `export const NgModule = () => null;
|
2018-11-11 13:16:04 -05:00
|
|
|
export class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n`
|
2019-06-06 15:22:32 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
|
|
|
testFormatter} = createTestRenderer('@angular/core', [CORE_FILE]);
|
|
|
|
renderer.renderProgram(
|
|
|
|
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
|
2019-11-06 12:03:56 -05:00
|
|
|
const addAdjacentStatementsSpy = testFormatter.addAdjacentStatements as jasmine.Spy;
|
|
|
|
expect(addAdjacentStatementsSpy.calls.first().args[2])
|
2019-06-06 15:22:32 -04:00
|
|
|
.toContain(`/*@__PURE__*/ setClassMetadata(`);
|
|
|
|
const addImportsSpy = testFormatter.addImports as jasmine.Spy;
|
|
|
|
expect(addImportsSpy.calls.first().args[1]).toEqual([]);
|
|
|
|
});
|
2018-11-11 13:16:04 -05:00
|
|
|
});
|
2018-11-25 16:40:25 -05:00
|
|
|
});
|
2018-07-16 05:23:37 -04:00
|
|
|
});
|
2018-07-27 15:36:54 -04:00
|
|
|
});
|