fix(ivy): prevent ngcc from referencing missing ɵsetClassMetadata (#27055)

When ngtsc compiles @angular/core, it rewrites core imports to the
r3_symbols.ts file that exposes all internal symbols under their
external name. When creating the FESM bundle, the r3_symbols.ts file
causes the external symbol names to be rewritten to their internal name.

Under ngcc compilations of FESM bundles, the indirection of
r3_symbols.ts is no longer in place such that the external names are
retained in the bundle. Previously, the external name `ɵdefineNgModule`
was explicitly declared internally to resolve this issue, but the
recently added `setClassMetadata` was not declared as such, causing
runtime errors.

Instead of relying on the r3_symbols.ts file to perform the rewrite of
the external modules to their internal variants, the translation is
moved into the `ImportManager` during the compilation itself. This
avoids the need for providing the external name manually.

PR Close #27055
This commit is contained in:
JoostK 2018-11-11 19:16:04 +01:00 committed by Miško Hevery
parent 8ce59a583b
commit c8c8648abf
10 changed files with 121 additions and 61 deletions

View File

@ -24,13 +24,13 @@ ivy-ngcc
if [[ $? != 0 ]]; then exit 1; fi if [[ $? != 0 ]]; then exit 1; fi
# Did it compile @angular/core/ApplicationModule correctly? # Did it compile @angular/core/ApplicationModule correctly?
grep "ApplicationModule.ngModuleDef = ɵdefineNgModule" node_modules/@angular/core/fesm2015/core.js grep "ApplicationModule.ngModuleDef = defineNgModule" node_modules/@angular/core/fesm2015/core.js
if [[ $? != 0 ]]; then exit 1; fi if [[ $? != 0 ]]; then exit 1; fi
grep "ApplicationModule.ngModuleDef = ɵdefineNgModule" node_modules/@angular/core/fesm5/core.js grep "ApplicationModule.ngModuleDef = defineNgModule" node_modules/@angular/core/fesm5/core.js
if [[ $? != 0 ]]; then exit 1; fi if [[ $? != 0 ]]; then exit 1; fi
grep "ApplicationModule.ngModuleDef = ɵngcc0.ɵdefineNgModule" node_modules/@angular/core/esm2015/src/application_module.js grep "ApplicationModule.ngModuleDef = ɵngcc0.defineNgModule" node_modules/@angular/core/esm2015/src/application_module.js
if [[ $? != 0 ]]; then exit 1; fi if [[ $? != 0 ]]; then exit 1; fi
grep "ApplicationModule.ngModuleDef = ɵngcc0.ɵdefineNgModule" node_modules/@angular/core/esm5/src/application_module.js grep "ApplicationModule.ngModuleDef = ɵngcc0.defineNgModule" node_modules/@angular/core/esm5/src/application_module.js
if [[ $? != 0 ]]; then exit 1; fi if [[ $? != 0 ]]; then exit 1; fi
# Can it be safely run again (as a noop)? # Can it be safely run again (as a noop)?

View File

@ -12,10 +12,11 @@ import {ImportManager} from '../../../ngtsc/translator';
export class NgccImportManager extends ImportManager { export class NgccImportManager extends ImportManager {
constructor(private isFlat: boolean, isCore: boolean, prefix?: string) { super(isCore, prefix); } constructor(private isFlat: boolean, isCore: boolean, prefix?: string) { super(isCore, prefix); }
generateNamedImport(moduleName: string, symbol: string): string|null { generateNamedImport(moduleName: string, symbol: string):
{moduleImport: string | null, symbol: string} {
if (this.isFlat && this.isCore && moduleName === '@angular/core') { if (this.isFlat && this.isCore && moduleName === '@angular/core') {
return null; return {moduleImport: null, symbol: this.rewriteSymbol(moduleName, symbol)};
} }
return super.generateNamedImport(moduleName, symbol); return super.generateNamedImport(moduleName, symbol);
} }
} }

View File

@ -17,7 +17,10 @@ import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {Renderer} from '../../src/rendering/renderer'; import {Renderer} from '../../src/rendering/renderer';
class TestRenderer extends Renderer { class TestRenderer extends Renderer {
constructor(host: Esm2015ReflectionHost) { super(host, false, null, '/src', '/dist', false); } constructor(
host: Esm2015ReflectionHost, isCore: boolean, rewriteCoreImportsTo: ts.SourceFile|null) {
super(host, isCore, rewriteCoreImportsTo, '/src', '/dist', false);
}
addImports(output: MagicString, imports: {name: string, as: string}[]) { addImports(output: MagicString, imports: {name: string, as: string}[]) {
output.prepend('\n// ADD IMPORTS\n'); output.prepend('\n// ADD IMPORTS\n');
} }
@ -35,13 +38,18 @@ class TestRenderer extends Renderer {
} }
} }
function createTestRenderer(file: {name: string, contents: string}) { function createTestRenderer(
const program = makeProgram(file); files: {name: string, contents: string}[],
const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); options: {isCore?: boolean, rewriteCoreImportsTo?: string} = {}) {
const program = makeProgram(...files);
const host = new Esm2015ReflectionHost(options.isCore || false, program.getTypeChecker());
const decorationAnalyses = const decorationAnalyses =
new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program); new DecorationAnalyzer(program.getTypeChecker(), host, [''], options.isCore || false)
.analyzeProgram(program);
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program);
const renderer = new TestRenderer(host); const rewriteCoreImportsTo =
options.rewriteCoreImportsTo ? program.getSourceFile(options.rewriteCoreImportsTo) ! : null;
const renderer = new TestRenderer(host, options.isCore || false, rewriteCoreImportsTo);
spyOn(renderer, 'addImports').and.callThrough(); spyOn(renderer, 'addImports').and.callThrough();
spyOn(renderer, 'addDefinitions').and.callThrough(); spyOn(renderer, 'addDefinitions').and.callThrough();
spyOn(renderer, 'removeDecorators').and.callThrough(); spyOn(renderer, 'removeDecorators').and.callThrough();
@ -94,7 +102,7 @@ describe('Renderer', () => {
it('should render the modified contents; and a new map file, if the original provided no map file.', it('should render the modified contents; and a new map file, if the original provided no map file.',
() => { () => {
const {renderer, program, decorationAnalyses, switchMarkerAnalyses} = const {renderer, program, decorationAnalyses, switchMarkerAnalyses} =
createTestRenderer(INPUT_PROGRAM); createTestRenderer([INPUT_PROGRAM]);
const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
expect(result[0].path).toEqual('/dist/file.js'); expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents) expect(result[0].contents)
@ -106,7 +114,7 @@ describe('Renderer', () => {
it('should call addImports with the source code and info about the core Angular library.', it('should call addImports with the source code and info about the core Angular library.',
() => { () => {
const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = const {decorationAnalyses, program, renderer, switchMarkerAnalyses} =
createTestRenderer(INPUT_PROGRAM); createTestRenderer([INPUT_PROGRAM]);
renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
const addImportsSpy = renderer.addImports as jasmine.Spy; const addImportsSpy = renderer.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
@ -118,7 +126,7 @@ describe('Renderer', () => {
it('should call addDefinitions with the source code, the analyzed class and the renderered definitions.', it('should call addDefinitions with the source code, the analyzed class and the renderered definitions.',
() => { () => {
const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = const {decorationAnalyses, program, renderer, switchMarkerAnalyses} =
createTestRenderer(INPUT_PROGRAM); createTestRenderer([INPUT_PROGRAM]);
renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
@ -137,7 +145,7 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
it('should call removeDecorators with the source code, a map of class decorators that have been analyzed', it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',
() => { () => {
const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = const {decorationAnalyses, program, renderer, switchMarkerAnalyses} =
createTestRenderer(INPUT_PROGRAM); createTestRenderer([INPUT_PROGRAM]);
renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy; const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy;
expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
@ -157,10 +165,10 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
it('should merge any inline source map from the original file and write the output as an inline source map', it('should merge any inline source map from the original file and write the output as an inline source map',
() => { () => {
const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = createTestRenderer({ const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = createTestRenderer([{
...INPUT_PROGRAM, ...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment()
}); }]);
const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
expect(result[0].path).toEqual('/dist/file.js'); expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents) expect(result[0].contents)
@ -172,10 +180,10 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
() => { () => {
// Mock out reading the map file from disk // Mock out reading the map file from disk
spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON()); spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON());
const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = createTestRenderer({ const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = createTestRenderer([{
...INPUT_PROGRAM, ...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map' contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map'
}); }]);
const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
expect(result[0].path).toEqual('/dist/file.js'); expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents) expect(result[0].contents)
@ -183,5 +191,48 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
expect(result[1].path).toEqual('/dist/file.js.map'); expect(result[1].path).toEqual('/dist/file.js.map');
expect(result[1].contents).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toJSON()); expect(result[1].contents).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toJSON());
}); });
describe('@angular/core support', () => {
it('should render relative imports in ESM bundles', () => {
const R3_SYMBOLS_FILE = {
name: '/src/r3_symbols.js',
contents: `export const NgModule = () => null;`
};
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 {decorationAnalyses, program, renderer, switchMarkerAnalyses} = createTestRenderer(
[R3_SYMBOLS_FILE, CORE_FILE],
{isCore: true, rewriteCoreImportsTo: R3_SYMBOLS_FILE.name});
renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
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([{name: './r3_symbols', as: 'ɵ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, program, renderer, switchMarkerAnalyses} =
createTestRenderer([CORE_FILE], {isCore: true});
renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
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([]);
});
});
}); });
}); });

View File

@ -17,7 +17,7 @@ import * as ts from 'typescript';
import {ClassMemberKind, ReflectionHost} from '../../host'; import {ClassMemberKind, ReflectionHost} from '../../host';
const TS_DTS_JS_EXTENSION = /(\.d)?\.ts|\.js$/; const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
/** /**
* Represents a value which cannot be determined statically. * Represents a value which cannot be determined statically.

View File

@ -38,16 +38,16 @@ const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([
[BinaryOperator.Plus, ts.SyntaxKind.PlusToken], [BinaryOperator.Plus, ts.SyntaxKind.PlusToken],
]); ]);
const CORE_SUPPORTED_SYMBOLS = new Set<string>([ const CORE_SUPPORTED_SYMBOLS = new Map<string, string>([
'defineInjectable', ['defineInjectable', 'defineInjectable'],
'defineInjector', ['defineInjector', 'defineInjector'],
'ɵdefineNgModule', ['ɵdefineNgModule', 'defineNgModule'],
'inject', ['inject', 'inject'],
'ɵsetClassMetadata', ['ɵsetClassMetadata', 'setClassMetadata'],
'ɵInjectableDef', ['ɵInjectableDef', 'InjectableDef'],
'ɵInjectorDef', ['ɵInjectorDef', 'InjectorDef'],
'ɵNgModuleDefWithMeta', ['ɵNgModuleDefWithMeta', 'NgModuleDefWithMeta'],
'ɵNgModuleFactory', ['ɵNgModuleFactory', 'NgModuleFactory'],
]); ]);
export class ImportManager { export class ImportManager {
@ -56,14 +56,28 @@ export class ImportManager {
constructor(protected isCore: boolean, private prefix = 'i') {} constructor(protected isCore: boolean, private prefix = 'i') {}
generateNamedImport(moduleName: string, symbol: string): string|null { generateNamedImport(moduleName: string, symbol: string):
{moduleImport: string | null, symbol: string} {
if (!this.moduleToIndex.has(moduleName)) { if (!this.moduleToIndex.has(moduleName)) {
this.moduleToIndex.set(moduleName, `${this.prefix}${this.nextIndex++}`); this.moduleToIndex.set(moduleName, `${this.prefix}${this.nextIndex++}`);
} }
if (this.isCore && moduleName === '@angular/core' && !CORE_SUPPORTED_SYMBOLS.has(symbol)) {
throw new Error(`Importing unexpected symbol ${symbol} while compiling core`); return {
moduleImport: this.moduleToIndex.get(moduleName) !,
symbol: this.rewriteSymbol(moduleName, symbol)
};
}
protected rewriteSymbol(moduleName: string, symbol: string): string {
if (this.isCore && moduleName === '@angular/core') {
if (!CORE_SUPPORTED_SYMBOLS.has(symbol)) {
throw new Error(`Importing unexpected symbol ${symbol} while compiling core`);
}
symbol = CORE_SUPPORTED_SYMBOLS.get(symbol) !;
} }
return this.moduleToIndex.get(moduleName) !;
return symbol;
} }
getAllImports(contextPath: string, rewriteCoreImportsTo: ts.SourceFile|null): getAllImports(contextPath: string, rewriteCoreImportsTo: ts.SourceFile|null):
@ -216,12 +230,13 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
if (ast.value.moduleName === null || ast.value.name === null) { if (ast.value.moduleName === null || ast.value.name === null) {
throw new Error(`Import unknown module or symbol ${ast.value}`); throw new Error(`Import unknown module or symbol ${ast.value}`);
} }
const importIdentifier = this.imports.generateNamedImport(ast.value.moduleName, ast.value.name); const {moduleImport, symbol} =
if (importIdentifier === null) { this.imports.generateNamedImport(ast.value.moduleName, ast.value.name);
return ts.createIdentifier(ast.value.name); if (moduleImport === null) {
return ts.createIdentifier(symbol);
} else { } else {
return ts.createPropertyAccess( return ts.createPropertyAccess(
ts.createIdentifier(importIdentifier), ts.createIdentifier(ast.value.name)); ts.createIdentifier(moduleImport), ts.createIdentifier(symbol));
} }
} }
@ -382,8 +397,9 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
if (ast.value.moduleName === null || ast.value.name === null) { if (ast.value.moduleName === null || ast.value.name === null) {
throw new Error(`Import unknown module or symbol`); throw new Error(`Import unknown module or symbol`);
} }
const moduleSymbol = this.imports.generateNamedImport(ast.value.moduleName, ast.value.name); const {moduleImport, symbol} =
const base = `${moduleSymbol}.${ast.value.name}`; this.imports.generateNamedImport(ast.value.moduleName, ast.value.name);
const base = moduleImport ? `${moduleImport}.${symbol}` : symbol;
if (ast.typeParams !== null) { if (ast.typeParams !== null) {
const generics = ast.typeParams.map(type => type.visitType(this, context)).join(', '); const generics = ast.typeParams.map(type => type.visitType(this, context)).join(', ');
return `${base}<${generics}>`; return `${base}<${generics}>`;

View File

@ -10,10 +10,10 @@
import * as path from 'path'; import * as path from 'path';
const TS_DTS_EXTENSION = /(\.d)?\.ts$/; const TS_DTS_JS_EXTENSION = /(?:\.d)?\.ts$|\.js$/;
export function relativePathBetween(from: string, to: string): string|null { export function relativePathBetween(from: string, to: string): string|null {
let relative = path.posix.relative(path.dirname(from), to).replace(TS_DTS_EXTENSION, ''); let relative = path.posix.relative(path.dirname(from), to).replace(TS_DTS_JS_EXTENSION, '');
if (relative === '') { if (relative === '') {
return null; return null;
@ -25,4 +25,4 @@ export function relativePathBetween(from: string, to: string): string|null {
} }
return relative; return relative;
} }

View File

@ -7,15 +7,12 @@
*/ */
// clang-format off // clang-format off
// We need to have `ɵdefineNgModule` defined locally for flat-file ngcc compilation.
// More details in the commit where this is added.
import {defineNgModule} from './render3/index';
export const ɵdefineNgModule = defineNgModule;
export { export {
defineBase as ɵdefineBase, defineBase as ɵdefineBase,
defineComponent as ɵdefineComponent, defineComponent as ɵdefineComponent,
defineDirective as ɵdefineDirective, defineDirective as ɵdefineDirective,
definePipe as ɵdefinePipe, definePipe as ɵdefinePipe,
defineNgModule as ɵdefineNgModule,
detectChanges as ɵdetectChanges, detectChanges as ɵdetectChanges,
renderComponent as ɵrenderComponent, renderComponent as ɵrenderComponent,
ComponentType as ɵComponentType, ComponentType as ɵComponentType,

View File

@ -14,17 +14,18 @@
* compiler writes imports to this file. * compiler writes imports to this file.
* *
* Only a subset of such imports are supported - core is not allowed to declare components or pipes. * Only a subset of such imports are supported - core is not allowed to declare components or pipes.
* A check in ngtsc's translator.ts validates this condition. * A check in ngtsc's translator.ts validates this condition. The translator is responsible for
* translating an external name (prefixed with ɵ) to the internal symbol name as exported below.
* *
* The below symbols are used for @Injectable and @NgModule compilation. * The below symbols are used for @Injectable and @NgModule compilation.
*/ */
export {InjectableDef as ɵInjectableDef, InjectorDef as ɵInjectorDef, defineInjectable, defineInjector} from './di/defs'; export {InjectableDef, InjectorDef, defineInjectable, defineInjector} from './di/defs';
export {inject} from './di/injector_compatibility'; export {inject} from './di/injector_compatibility';
export {NgModuleDef as ɵNgModuleDef, NgModuleDefWithMeta as ɵNgModuleDefWithMeta} from './metadata/ng_module'; export {NgModuleDef, NgModuleDefWithMeta} from './metadata/ng_module';
export {defineNgModule as ɵdefineNgModule} from './render3/definition'; export {defineNgModule} from './render3/definition';
export {setClassMetadata as ɵsetClassMetadata} from './render3/metadata'; export {setClassMetadata} from './render3/metadata';
export {NgModuleFactory as ɵNgModuleFactory} from './render3/ng_module_ref'; export {NgModuleFactory} from './render3/ng_module_ref';
/** /**

View File

@ -1420,8 +1420,5 @@
}, },
{ {
"name": "wtfLeave" "name": "wtfLeave"
},
{
"name": "ɵdefineNgModule"
} }
] ]

View File

@ -2704,8 +2704,5 @@
}, },
{ {
"name": "wtfLeave" "name": "wtfLeave"
},
{
"name": "ɵdefineNgModule"
} }
] ]