diff --git a/packages/compiler-cli/src/metadata/bundle_index_host.ts b/packages/compiler-cli/src/metadata/bundle_index_host.ts index 198c48fc487f8..e98a821e6c11b 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 6a0ef3082c35c..664f88d0d111a 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 b10c8b8027609..c187b31a26c4b 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 779ca766c76ed..c4d700272d561 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -975,7 +975,8 @@ describe('ngc transformer command-line', () => { "angularCompilerOptions": { "flatModuleId": "flat_module", "flatModuleOutFile": "${outFile}", - "skipTemplateCodegen": true + "skipTemplateCodegen": true, + "enableResourceInlining": true }, "files": ["public-api.ts"] } @@ -1004,7 +1005,8 @@ describe('ngc transformer command-line', () => { ], exports: [ FlatComponent, - ] + ], + providers: [{provide: 'test', useFactory: () => true}], }) export class FlatModule { }`); @@ -1019,6 +1021,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();