From f99cb5c995884fc0e6783172380c292ffbf0b510 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 2 Apr 2018 13:05:08 -0700 Subject: [PATCH] fix(compiler-cli): flat module index metadata should be transformed (#23129) Currently, the flat module index metadata is produced directly from the source metadata. The compiler, however, applies transformations on the Typescript sources during transpilation, and also equivalent transformations on the metadata itself. This transformed metadata doesn't end up in the flat module index. This changes the compiler to generate the flat module index metadata from its transformed version instead of directly from source. PR Close #23129 --- .../src/metadata/bundle_index_host.ts | 48 ++++++++++++++----- packages/compiler-cli/src/metadata/bundler.ts | 13 ++++- .../compiler-cli/src/transformers/program.ts | 4 +- packages/compiler-cli/test/ngc_spec.ts | 20 +++++++- 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/packages/compiler-cli/src/metadata/bundle_index_host.ts b/packages/compiler-cli/src/metadata/bundle_index_host.ts index 198c48fc48..e98a821e6c 100644 --- a/packages/compiler-cli/src/metadata/bundle_index_host.ts +++ b/packages/compiler-cli/src/metadata/bundle_index_host.ts @@ -11,6 +11,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import {CompilerOptions} from '../transformers/api'; +import {MetadataCache} from '../transformers/metadata_cache'; import {CompilerHostAdapter, MetadataBundler} from './bundler'; import {privateEntriesToIndex} from './index_writer'; @@ -19,10 +20,8 @@ const DTS = /\.d\.ts$/; const JS_EXT = /(\.js|)$/; function createSyntheticIndexHost( - delegate: H, syntheticIndex: {name: string, content: string, metadata: string}): H { + delegate: H, syntheticIndex: {name: string, content: string, getMetadata: () => string}): H { const normalSyntheticIndexName = path.normalize(syntheticIndex.name); - const indexContent = syntheticIndex.content; - const indexMetadata = syntheticIndex.metadata; const newHost = Object.create(delegate); newHost.fileExists = (fileName: string): boolean => { @@ -30,14 +29,14 @@ function createSyntheticIndexHost( }; newHost.readFile = (fileName: string) => { - return path.normalize(fileName) == normalSyntheticIndexName ? indexContent : + return path.normalize(fileName) == normalSyntheticIndexName ? syntheticIndex.content : delegate.readFile(fileName); }; newHost.getSourceFile = (fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void) => { if (path.normalize(fileName) == normalSyntheticIndexName) { - const sf = ts.createSourceFile(fileName, indexContent, languageVersion, true); + const sf = ts.createSourceFile(fileName, syntheticIndex.content, languageVersion, true); if ((delegate as any).fileNameToModuleName) { sf.moduleName = (delegate as any).fileNameToModuleName(fileName); } @@ -55,6 +54,7 @@ function createSyntheticIndexHost( path.normalize(sourceFiles[0].fileName) === normalSyntheticIndexName) { // If we are writing the synthetic index, write the metadata along side. const metadataName = fileName.replace(DTS, '.metadata.json'); + const indexMetadata = syntheticIndex.getMetadata(); delegate.writeFile(metadataName, indexMetadata, writeByteOrderMark, onError, []); } }; @@ -62,8 +62,9 @@ function createSyntheticIndexHost( } export function createBundleIndexHost( - ngOptions: CompilerOptions, rootFiles: ReadonlyArray, - host: H): {host: H, indexName?: string, errors?: ts.Diagnostic[]} { + ngOptions: CompilerOptions, rootFiles: ReadonlyArray, host: H, + getMetadataCache: () => + MetadataCache): {host: H, indexName?: string, errors?: ts.Diagnostic[]} { const files = rootFiles.filter(f => !DTS.test(f)); let indexFile: string|undefined; if (files.length === 1) { @@ -94,15 +95,36 @@ export function createBundleIndexHost( } const indexModule = indexFile.replace(/\.ts$/, ''); - const bundler = new MetadataBundler( - indexModule, ngOptions.flatModuleId, new CompilerHostAdapter(host), - ngOptions.flatModulePrivateSymbolPrefix); - const metadataBundle = bundler.getMetadataBundle(); - const metadata = JSON.stringify(metadataBundle.metadata); + + // The operation of producing a metadata bundle happens twice - once during setup and once during + // the emit phase. The first time, the bundle is produced without a metadata cache, to compute the + // contents of the flat module index. The bundle produced during emit does use the metadata cache + // with associated transforms, so the metadata will have lowered expressions, resource inlining, + // etc. + const getMetadataBundle = (cache: MetadataCache | null) => { + const bundler = new MetadataBundler( + indexModule, ngOptions.flatModuleId, new CompilerHostAdapter(host, cache), + ngOptions.flatModulePrivateSymbolPrefix); + return bundler.getMetadataBundle(); + }; + + // First, produce the bundle with no MetadataCache. + const metadataBundle = getMetadataBundle(/* MetadataCache */ null); const name = path.join(path.dirname(indexModule), ngOptions.flatModuleOutFile !.replace(JS_EXT, '.ts')); const libraryIndex = `./${path.basename(indexModule)}`; const content = privateEntriesToIndex(libraryIndex, metadataBundle.privates); - host = createSyntheticIndexHost(host, {name, content, metadata}); + + host = createSyntheticIndexHost(host, { + name, + content, + getMetadata: () => { + // The second metadata bundle production happens on-demand, and uses the getMetadataCache + // closure to retrieve an up-to-date MetadataCache which is configured with whatever metadata + // transforms were used to produce the JS output. + const metadataBundle = getMetadataBundle(getMetadataCache()); + return JSON.stringify(metadataBundle.metadata); + } + }); return {host, indexName: name}; } diff --git a/packages/compiler-cli/src/metadata/bundler.ts b/packages/compiler-cli/src/metadata/bundler.ts index 6a0ef3082c..664f88d0d1 100644 --- a/packages/compiler-cli/src/metadata/bundler.ts +++ b/packages/compiler-cli/src/metadata/bundler.ts @@ -10,6 +10,7 @@ import * as ts from 'typescript'; import {MetadataCollector} from '../metadata/collector'; import {ClassMetadata, ConstructorMetadata, FunctionMetadata, METADATA_VERSION, MemberMetadata, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isInterfaceMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicExpression, isMethodMetadata} from '../metadata/schema'; +import {MetadataCache} from '../transformers/metadata_cache'; @@ -596,12 +597,20 @@ export class MetadataBundler { export class CompilerHostAdapter implements MetadataBundlerHost { private collector = new MetadataCollector(); - constructor(private host: ts.CompilerHost) {} + constructor(private host: ts.CompilerHost, private cache: MetadataCache|null) {} getMetadataFor(fileName: string): ModuleMetadata|undefined { if (!this.host.fileExists(fileName + '.ts')) return undefined; const sourceFile = this.host.getSourceFile(fileName + '.ts', ts.ScriptTarget.Latest); - return sourceFile && this.collector.getMetadata(sourceFile); + // If there is a metadata cache, use it to get the metadata for this source file. Otherwise, + // fall back on the locally created MetadataCollector. + if (!sourceFile) { + return undefined; + } else if (this.cache) { + return this.cache.getMetadata(sourceFile); + } else { + return this.collector.getMetadata(sourceFile); + } } } diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index b6f07b4a91..ad7a3c10c1 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -149,7 +149,7 @@ class AngularCompilerProgram implements Program { if (options.flatModuleOutFile) { const {host: bundleHost, indexName, errors} = - createBundleIndexHost(options, this.rootNames, host); + createBundleIndexHost(options, this.rootNames, host, () => this.metadataCache); if (errors) { this._optionsDiagnostics.push(...errors.map(e => ({ category: e.category, @@ -338,7 +338,6 @@ class AngularCompilerProgram implements Program { writeFile: writeTsFile, emitOnlyDtsFiles, customTransformers: tsCustomTransformers }); - return emitResult; } @@ -518,6 +517,7 @@ class AngularCompilerProgram implements Program { `- ${genJsonFiles.length + metadataJsonCount} generated json files`, ].join('\n'))]); } + return emitResult; } diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 259ea65783..40da113145 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -1009,7 +1009,8 @@ describe('ngc transformer command-line', () => { "angularCompilerOptions": { "flatModuleId": "flat_module", "flatModuleOutFile": "${outFile}", - "skipTemplateCodegen": true + "skipTemplateCodegen": true, + "enableResourceInlining": true }, "files": ["public-api.ts"] } @@ -1038,7 +1039,8 @@ describe('ngc transformer command-line', () => { ], exports: [ FlatComponent, - ] + ], + providers: [{provide: 'test', useFactory: () => true}], }) export class FlatModule { }`); @@ -1053,6 +1055,20 @@ describe('ngc transformer command-line', () => { shouldExist('index.metadata.json'); }); + it('should downlevel flat module metadata', () => { + writeFlatModule('index.js'); + + const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); + expect(exitCode).toEqual(0); + shouldExist('index.js'); + shouldExist('index.metadata.json'); + + const metadataPath = path.resolve(outDir, 'index.metadata.json'); + const metadataSource = fs.readFileSync(metadataPath, 'utf8'); + expect(metadataSource).not.toContain('templateUrl'); + expect(metadataSource).toContain('"useFactory":{"__symbolic":"reference","name":"ɵ0"}'); + }); + describe('with tree example', () => { beforeEach(() => { writeConfig();