From a8a96601126407982f8cf7eb9cbe9f5c04379abe Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Thu, 21 Sep 2017 18:05:07 -0700 Subject: [PATCH] fix(compiler): various squashed fixes for the new ngc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit introduce the option `allowEmptyCodegenFiles` to generate all generated files, even if they are empty. - also provides the original source files from which the file was generated in the write file callback - needed e.g. for G3 when copying over pinto mod names from the original component to all generated files use `importAs` from flat modules when writing summaries - i.e. prevents incorrect entries like @angular/common/common in the .ngsummary.json files. change interaction between ng and ts to prevent race conditions - before Angular would rely on TS to first read the file for which we generate files, and then the generated files. However, this can break easily when we reuse an old program. don’t generate files for sources that are outside of `rootDir` (see #19337) --- packages/bazel/src/ng_module.bzl | 1 + packages/bazel/src/ngc-wrapped/index.ts | 54 ++-- packages/compiler-cli/src/compiler_host.ts | 49 +++- packages/compiler-cli/src/ngtools_api2.ts | 8 +- .../src/path_mapped_compiler_host.ts | 8 +- packages/compiler-cli/src/transformers/api.ts | 16 +- .../src/transformers/compiler_host.ts | 232 ++++++++++------- .../compiler-cli/src/transformers/program.ts | 201 ++++++++------ packages/compiler-cli/test/ngc_spec.ts | 78 +++++- .../test/transformers/compiler_host_spec.ts | 81 +++--- .../test/transformers/program_spec.ts | 207 ++++++++++++--- packages/compiler/src/aot/compiler.ts | 245 +++++++++--------- packages/compiler/src/aot/compiler_options.ts | 3 +- .../src/aot/static_symbol_resolver.ts | 9 +- packages/compiler/src/style_compiler.ts | 11 +- packages/compiler/test/aot/test_util.ts | 2 +- 16 files changed, 746 insertions(+), 459 deletions(-) diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index 9dee09338e..a5907ee33b 100644 --- a/packages/bazel/src/ng_module.bzl +++ b/packages/bazel/src/ng_module.bzl @@ -67,6 +67,7 @@ def _ngc_tsconfig(ctx, files, srcs, **kwargs): return dict(tsc_wrapped_tsconfig(ctx, files, srcs, **kwargs), **{ "angularCompilerOptions": { "generateCodeForLibraries": False, + "allowEmptyCodegenFiles": True, # FIXME: wrong place to de-dupe "expectedOut": depset([o.path for o in expected_outs]).to_list() } diff --git a/packages/bazel/src/ngc-wrapped/index.ts b/packages/bazel/src/ngc-wrapped/index.ts index f48e059d15..bf605271c1 100644 --- a/packages/bazel/src/ngc-wrapped/index.ts +++ b/packages/bazel/src/ngc-wrapped/index.ts @@ -40,24 +40,21 @@ function runOneBuild(args: string[], inputs?: {[path: string]: string}): boolean if (args[0] === '-p') args.shift(); // Strip leading at-signs, used to indicate a params file const project = args[0].replace(/^@+/, ''); - let fileLoader: FileLoader; - if (inputs) { - fileLoader = new CachedFileLoader(fileCache, ALLOW_NON_HERMETIC_READS); - // Resolve the inputs to absolute paths to match TypeScript internals - const resolvedInputs: {[path: string]: string} = {}; - for (const key of Object.keys(inputs)) { - resolvedInputs[path.resolve(key)] = inputs[key]; - } - fileCache.updateCache(resolvedInputs); - } else { - fileLoader = new UncachedFileLoader(); - } const [{options: tsOptions, bazelOpts, files, config}] = parseTsconfig(project); const expectedOuts = config['angularCompilerOptions']['expectedOut']; const {basePath} = ng.calcProjectFileAndBasePath(project); const compilerOpts = ng.createNgCompilerOptions(basePath, config, tsOptions); - const {diagnostics} = compile({fileLoader, compilerOpts, bazelOpts, files, expectedOuts}); + const tsHost = ts.createCompilerHost(compilerOpts, true); + const {diagnostics} = compile({ + allowNonHermeticReads: ALLOW_NON_HERMETIC_READS, + compilerOpts, + tsHost, + bazelOpts, + files, + inputs, + expectedOuts + }); return diagnostics.every(d => d.category !== ts.DiagnosticCategory.Error); } @@ -71,14 +68,28 @@ export function relativeToRootDirs(filePath: string, rootDirs: string[]): string return filePath; } -export function compile( - {fileLoader, compilerOpts, bazelOpts, files, expectedOuts, gatherDiagnostics}: { - fileLoader: FileLoader, - compilerOpts: ng.CompilerOptions, - bazelOpts: BazelOptions, - files: string[], - expectedOuts: string[], gatherDiagnostics?: (program: ng.Program) => ng.Diagnostics - }): {diagnostics: ng.Diagnostics, program: ng.Program} { +export function compile({allowNonHermeticReads, compilerOpts, tsHost, bazelOpts, files, inputs, + expectedOuts, gatherDiagnostics}: { + allowNonHermeticReads: boolean, + compilerOpts: ng.CompilerOptions, + tsHost: ts.CompilerHost, inputs?: {[path: string]: string}, + bazelOpts: BazelOptions, + files: string[], + expectedOuts: string[], gatherDiagnostics?: (program: ng.Program) => ng.Diagnostics +}): {diagnostics: ng.Diagnostics, program: ng.Program} { + let fileLoader: FileLoader; + if (inputs) { + fileLoader = new CachedFileLoader(fileCache, ALLOW_NON_HERMETIC_READS); + // Resolve the inputs to absolute paths to match TypeScript internals + const resolvedInputs: {[path: string]: string} = {}; + for (const key of Object.keys(inputs)) { + resolvedInputs[path.resolve(key)] = inputs[key]; + } + fileCache.updateCache(resolvedInputs); + } else { + fileLoader = new UncachedFileLoader(); + } + if (!bazelOpts.es5Mode) { compilerOpts.annotateForClosureCompiler = true; compilerOpts.annotationsAs = 'static fields'; @@ -89,7 +100,6 @@ export function compile( } const writtenExpectedOuts = [...expectedOuts]; - const tsHost = ts.createCompilerHost(compilerOpts, true); const originalWriteFile = tsHost.writeFile.bind(tsHost); tsHost.writeFile = diff --git a/packages/compiler-cli/src/compiler_host.ts b/packages/compiler-cli/src/compiler_host.ts index 23231237ea..75b9c6e25c 100644 --- a/packages/compiler-cli/src/compiler_host.ts +++ b/packages/compiler-cli/src/compiler_host.ts @@ -39,7 +39,7 @@ export abstract class BaseAotCompilerHost abstract resourceNameToFileName(m: string, containingFile: string): string|null; - abstract fileNameToModuleName(importedFile: string, containingFile: string): string|null; + abstract fileNameToModuleName(importedFile: string, containingFile: string): string; abstract toSummaryFileName(fileName: string, referringSrcFileName: string): string; @@ -47,6 +47,23 @@ export abstract class BaseAotCompilerHost abstract getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined; + protected getImportAs(fileName: string): string|undefined { + // Note: `importAs` can only be in .metadata.json files + // So it is enough to call this.readMetadata, and we get the + // benefit that this is cached. + if (DTS.test(fileName)) { + const metadatas = this.readMetadata(fileName); + if (metadatas) { + for (const metadata of metadatas) { + if (metadata.importAs) { + return metadata.importAs; + } + } + } + } + return undefined; + } + getMetadataFor(filePath: string): ModuleMetadata[]|undefined { if (!this.context.fileExists(filePath)) { // If the file doesn't exists then we cannot return metadata for the file. @@ -56,29 +73,34 @@ export abstract class BaseAotCompilerHost } if (DTS.test(filePath)) { - const metadataPath = filePath.replace(DTS, '.metadata.json'); - if (this.context.fileExists(metadataPath)) { - return this.readMetadata(metadataPath, filePath); - } else { + let metadatas = this.readMetadata(filePath); + if (!metadatas) { // If there is a .d.ts file but no metadata file we need to produce a // v3 metadata from the .d.ts file as v3 includes the exports we need // to resolve symbols. - return [this.upgradeVersion1Metadata( + metadatas = [this.upgradeVersion1Metadata( {'__symbolic': 'module', 'version': 1, 'metadata': {}}, filePath)]; } + return metadatas; } + // Attention: don't cache this, so that e.g. the LanguageService + // can read in changes from source files in the metadata! const metadata = this.getMetadataForSourceFile(filePath); return metadata ? [metadata] : []; } - readMetadata(filePath: string, dtsFilePath: string): ModuleMetadata[] { - let metadatas = this.resolverCache.get(filePath); + protected readMetadata(dtsFilePath: string): ModuleMetadata[]|undefined { + let metadatas = this.resolverCache.get(dtsFilePath); if (metadatas) { return metadatas; } + const metadataPath = dtsFilePath.replace(DTS, '.metadata.json'); + if (!this.context.fileExists(metadataPath)) { + return undefined; + } try { - const metadataOrMetadatas = JSON.parse(this.context.readFile(filePath)); + const metadataOrMetadatas = JSON.parse(this.context.readFile(metadataPath)); const metadatas: ModuleMetadata[] = metadataOrMetadatas ? (Array.isArray(metadataOrMetadatas) ? metadataOrMetadatas : [metadataOrMetadatas]) : []; @@ -87,10 +109,10 @@ export abstract class BaseAotCompilerHost if (!v3Metadata && v1Metadata) { metadatas.push(this.upgradeVersion1Metadata(v1Metadata, dtsFilePath)); } - this.resolverCache.set(filePath, metadatas); + this.resolverCache.set(dtsFilePath, metadatas); return metadatas; } catch (e) { - console.error(`Failed to read JSON file ${filePath}`); + console.error(`Failed to read JSON file ${metadataPath}`); throw e; } } @@ -347,6 +369,11 @@ export class CompilerHost extends BaseAotCompilerHost { * NOTE: (*) the relative path is computed depending on `isGenDirChildOfRootDir`. */ fileNameToModuleName(importedFile: string, containingFile: string): string { + const importAs = this.getImportAs(importedFile); + if (importAs) { + return importAs; + } + // If a file does not yet exist (because we compile it later), we still need to // assume it exists it so that the `resolve` method works! if (importedFile !== containingFile && !this.context.fileExists(importedFile)) { diff --git a/packages/compiler-cli/src/ngtools_api2.ts b/packages/compiler-cli/src/ngtools_api2.ts index fd95c8d9e0..9c0ab6584b 100644 --- a/packages/compiler-cli/src/ngtools_api2.ts +++ b/packages/compiler-cli/src/ngtools_api2.ts @@ -92,6 +92,12 @@ export interface TsEmitArguments { export interface TsEmitCallback { (args: TsEmitArguments): ts.EmitResult; } +export interface LibrarySummary { + fileName: string; + text: string; + sourceFile?: ts.SourceFile; +} + export interface Program { getTsProgram(): ts.Program; getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken): ts.Diagnostic[]; @@ -110,7 +116,7 @@ export interface Program { customTransformers?: CustomTransformers, emitCallback?: TsEmitCallback }): ts.EmitResult; - getLibrarySummaries(): {fileName: string, content: string}[]; + getLibrarySummaries(): LibrarySummary[]; } // Wrapper for createProgram. diff --git a/packages/compiler-cli/src/path_mapped_compiler_host.ts b/packages/compiler-cli/src/path_mapped_compiler_host.ts index 848bb51886..1b82c11b19 100644 --- a/packages/compiler-cli/src/path_mapped_compiler_host.ts +++ b/packages/compiler-cli/src/path_mapped_compiler_host.ts @@ -126,11 +126,13 @@ export class PathMappedCompilerHost extends CompilerHost { continue; } if (DTS.test(rootedPath)) { - const metadataPath = rootedPath.replace(DTS, '.metadata.json'); - if (this.context.fileExists(metadataPath)) { - return this.readMetadata(metadataPath, rootedPath); + const metadatas = this.readMetadata(rootedPath); + if (metadatas) { + return metadatas; } } else { + // Attention: don't cache this, so that e.g. the LanguageService + // can read in changes from source files in the metadata! const metadata = this.getMetadataForSourceFile(rootedPath); return metadata ? [metadata] : []; } diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 13f9081dcc..36fc85b528 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -133,6 +133,9 @@ export interface CompilerOptions extends ts.CompilerOptions { // Whether to remove blank text nodes from compiled templates. It is `true` by default // in Angular 5 and will be re-visited in Angular 6. preserveWhitespaces?: boolean; + + /** generate all possible generated files */ + allowEmptyCodegenFiles?: boolean; } export interface CompilerHost extends ts.CompilerHost { @@ -203,6 +206,12 @@ export interface TsEmitArguments { export interface TsEmitCallback { (args: TsEmitArguments): ts.EmitResult; } +export interface LibrarySummary { + fileName: string; + text: string; + sourceFile?: ts.SourceFile; +} + export interface Program { /** * Retrieve the TypeScript program used to produce semantic diagnostics and emit the sources. @@ -280,8 +289,9 @@ export interface Program { }): ts.EmitResult; /** - * Returns the .ngsummary.json files of libraries that have been compiled - * in this program or previous programs. + * Returns the .d.ts / .ngsummary.json / .ngfactory.d.ts files of libraries that have been emitted + * in this program or previous programs with paths that emulate the fact that these libraries + * have been compiled before with no outDir. */ - getLibrarySummaries(): {fileName: string, content: string}[]; + getLibrarySummaries(): LibrarySummary[]; } diff --git a/packages/compiler-cli/src/transformers/compiler_host.ts b/packages/compiler-cli/src/transformers/compiler_host.ts index 18e623e2f2..f460c82ca7 100644 --- a/packages/compiler-cli/src/transformers/compiler_host.ts +++ b/packages/compiler-cli/src/transformers/compiler_host.ts @@ -14,7 +14,7 @@ import {BaseAotCompilerHost} from '../compiler_host'; import {TypeCheckHost} from '../diagnostics/translate_diagnostics'; import {ModuleMetadata} from '../metadata/index'; -import {CompilerHost, CompilerOptions} from './api'; +import {CompilerHost, CompilerOptions, LibrarySummary} from './api'; import {GENERATED_FILES} from './util'; const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-)+|(@(\w|-)+\/(\w|-)+))/; @@ -37,6 +37,11 @@ interface GenSourceFile { emitCtx: EmitterVisitorContext; } +export interface CodeGenerator { + generateFile(genFileName: string, baseFileName?: string): GeneratedFile; + findGeneratedFileNames(fileName: string): string[]; +} + /** * Implements the following hosts based on an api.CompilerHost: * - ts.CompilerHost to be consumed by a ts.Program @@ -48,11 +53,12 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends TypeCheckHost { private rootDirs: string[]; private moduleResolutionCache: ts.ModuleResolutionCache; - private originalSourceFiles = new Map(); + private originalSourceFiles = new Map(); private originalFileExistsCache = new Map(); private generatedSourceFiles = new Map(); - private generatedCodeFor = new Set(); + private generatedCodeFor = new Map(); private emitter = new TypeScriptEmitter(); + private librarySummaries = new Map(); getCancellationToken: () => ts.CancellationToken; getDefaultLibLocation: () => string; trace: (s: string) => void; @@ -61,10 +67,10 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends constructor( private rootFiles: string[], options: CompilerOptions, context: CompilerHost, - private metadataProvider: MetadataProvider, - private codeGenerator: (fileName: string) => GeneratedFile[], - private summariesFromPreviousCompilations: Map) { + private metadataProvider: MetadataProvider, private codeGenerator: CodeGenerator, + librarySummaries: LibrarySummary[]) { super(options, context); + librarySummaries.forEach(summary => this.librarySummaries.set(summary.fileName, summary)); this.moduleResolutionCache = ts.createModuleResolutionCache( this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context)); const basePath = this.options.basePath !; @@ -168,6 +174,11 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends 'fileNameToModuleName from containingFile', containingFile, 'to importedFile', importedFile); } + const importAs = this.getImportAs(importedFile); + if (importAs) { + return importAs; + } + // drop extension importedFile = importedFile.replace(EXT, ''); const importedFilePackagName = getPackageName(importedFile); @@ -229,15 +240,18 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends private getOriginalSourceFile( filePath: string, languageVersion?: ts.ScriptTarget, - onError?: ((message: string) => void)|undefined): ts.SourceFile|undefined { - let sf = this.originalSourceFiles.get(filePath); - if (sf) { - return sf; + onError?: ((message: string) => void)|undefined): ts.SourceFile|null { + // Note: we need the explicit check via `has` as we also cache results + // that were null / undefined. + if (this.originalSourceFiles.has(filePath)) { + return this.originalSourceFiles.get(filePath) !; } if (!languageVersion) { languageVersion = this.options.target || ts.ScriptTarget.Latest; } - sf = this.context.getSourceFile(filePath, languageVersion, onError); + // Note: This can also return undefined, + // as the TS typings are not correct! + const sf = this.context.getSourceFile(filePath, languageVersion, onError) || null; this.originalSourceFiles.set(filePath, sf); return sf; } @@ -250,9 +264,10 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends return this.metadataProvider.getMetadata(sf); } - updateGeneratedFile(genFile: GeneratedFile): ts.SourceFile|null { + updateGeneratedFile(genFile: GeneratedFile): ts.SourceFile { if (!genFile.stmts) { - return null; + throw new Error( + `Invalid Argument: Expected a GenerateFile with statements. ${genFile.genFileUrl}`); } const oldGenFile = this.generatedSourceFiles.get(genFile.genFileUrl); if (!oldGenFile) { @@ -271,10 +286,10 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends return this.addGeneratedFile(genFile, newRefs); } - private addGeneratedFile(genFile: GeneratedFile, externalReferences: Set): ts.SourceFile - |null { + private addGeneratedFile(genFile: GeneratedFile, externalReferences: Set): ts.SourceFile { if (!genFile.stmts) { - return null; + throw new Error( + `Invalid Argument: Expected a GenerateFile with statements. ${genFile.genFileUrl}`); } const {sourceText, context} = this.emitter.emitStatementsAndContext( genFile.srcFileUrl, genFile.genFileUrl, genFile.stmts, /* preamble */ '', @@ -288,49 +303,95 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends return sf; } - private ensureCodeGeneratedFor(fileName: string): void { - if (this.generatedCodeFor.has(fileName)) { - return; + shouldGenerateFile(fileName: string): {generate: boolean, baseFileName?: string} { + // TODO(tbosch): allow generating files that are not in the rootDir + // See https://github.com/angular/angular/issues/19337 + if (this.options.rootDir && !fileName.startsWith(this.options.rootDir)) { + return {generate: false}; } - this.generatedCodeFor.add(fileName); + const genMatch = GENERATED_FILES.exec(fileName); + if (!genMatch) { + return {generate: false}; + } + const [, base, genSuffix, suffix] = genMatch; + if (suffix !== 'ts') { + return {generate: false}; + } + let baseFileName: string|undefined; + if (genSuffix.indexOf('ngstyle') >= 0) { + // Note: ngstyle files have names like `afile.css.ngstyle.ts` + if (!this.originalFileExists(base)) { + return {generate: false}; + } + } else { + // Note: on-the-fly generated files always have a `.ts` suffix, + // but the file from which we generated it can be a `.ts`/ `.d.ts` + // (see options.generateCodeForLibraries). + baseFileName = [`${base}.ts`, `${base}.d.ts`].find( + baseFileName => this.isSourceFile(baseFileName) && this.originalFileExists(baseFileName)); + if (!baseFileName) { + return {generate: false}; + } + } + return {generate: true, baseFileName}; + } - const baseNameFromGeneratedFile = this._getBaseNameForGeneratedSourceFile(fileName); - if (baseNameFromGeneratedFile) { - return this.ensureCodeGeneratedFor(baseNameFromGeneratedFile); - } - const sf = this.getOriginalSourceFile(fileName, this.options.target || ts.ScriptTarget.Latest); - if (!sf) { - return; - } - - const genFileNames: string[] = []; - if (this.isSourceFile(fileName)) { - // Note: we can't exit early here, - // as we might need to clear out old changes to `SourceFile.referencedFiles` - // that were created by a previous run, given an original CompilerHost - // that caches source files. - const genFiles = this.codeGenerator(fileName); - genFiles.forEach(genFile => { - const sf = this.addGeneratedFile(genFile, genFileExternalReferences(genFile)); - if (sf) { - genFileNames.push(sf.fileName); - } - }); - } - addReferencesToSourceFile(sf, genFileNames); + shouldGenerateFilesFor(fileName: string) { + // TODO(tbosch): allow generating files that are not in the rootDir + // See https://github.com/angular/angular/issues/19337 + return !GENERATED_FILES.test(fileName) && this.isSourceFile(fileName) && + (!this.options.rootDir || pathStartsWithPrefix(this.options.rootDir, fileName)); } getSourceFile( fileName: string, languageVersion: ts.ScriptTarget, onError?: ((message: string) => void)|undefined): ts.SourceFile { - this.ensureCodeGeneratedFor(fileName); - const genFile = this.generatedSourceFiles.get(fileName); - if (genFile) { - return genFile.sourceFile; + // Note: Don't exit early in this method to make sure + // we always have up to date references on the file! + let genFileNames: string[] = []; + let sf = this.getGeneratedFile(fileName); + if (!sf) { + const summary = this.librarySummaries.get(fileName); + if (summary) { + if (!summary.sourceFile) { + summary.sourceFile = ts.createSourceFile( + fileName, summary.text, this.options.target || ts.ScriptTarget.Latest); + } + sf = summary.sourceFile; + genFileNames = []; + } + } + if (!sf) { + sf = this.getOriginalSourceFile(fileName); + const cachedGenFiles = this.generatedCodeFor.get(fileName); + if (cachedGenFiles) { + genFileNames = cachedGenFiles; + } else { + if (!this.options.noResolve && this.shouldGenerateFilesFor(fileName)) { + genFileNames = this.codeGenerator.findGeneratedFileNames(fileName); + } + this.generatedCodeFor.set(fileName, genFileNames); + } + } + if (sf) { + addReferencesToSourceFile(sf, genFileNames); } // TODO(tbosch): TypeScript's typings for getSourceFile are incorrect, // as it can very well return undefined. - return this.getOriginalSourceFile(fileName, languageVersion, onError) !; + return sf !; + } + + private getGeneratedFile(fileName: string): ts.SourceFile|null { + const genSrcFile = this.generatedSourceFiles.get(fileName); + if (genSrcFile) { + return genSrcFile.sourceFile; + } + const {generate, baseFileName} = this.shouldGenerateFile(fileName); + if (generate) { + const genFile = this.codeGenerator.generateFile(fileName, baseFileName); + return this.addGeneratedFile(genFile, genFileExternalReferences(genFile)); + } + return null; } private originalFileExists(fileName: string): boolean { @@ -344,48 +405,19 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends fileExists(fileName: string): boolean { fileName = stripNgResourceSuffix(fileName); - if (fileName.endsWith('.ngfactory.d.ts')) { - // Note: the factories of a previous program - // are not reachable via the regular fileExists - // as they might be in the outDir. So we derive their - // fileExist information based on the .ngsummary.json file. - if (this.summariesFromPreviousCompilations.has(summaryFileName(fileName))) { - return true; - } - } - // Note: Don't rely on this.generatedSourceFiles here, - // as it might not have been filled yet. - if (this._getBaseNameForGeneratedSourceFile(fileName)) { + if (this.librarySummaries.has(fileName) || this.generatedSourceFiles.has(fileName)) { return true; } - return this.summariesFromPreviousCompilations.has(fileName) || - this.originalFileExists(fileName); - } - - private _getBaseNameForGeneratedSourceFile(genFileName: string): string|undefined { - const genMatch = GENERATED_FILES.exec(genFileName); - if (!genMatch) { - return undefined; - } - const [, base, genSuffix, suffix] = genMatch; - if (suffix !== 'ts') { - return undefined; - } - if (genSuffix.indexOf('ngstyle') >= 0) { - // Note: ngstyle files have names like `afile.css.ngstyle.ts` - return base; - } else { - // Note: on-the-fly generated files always have a `.ts` suffix, - // but the file from which we generated it can be a `.ts`/ `.d.ts` - // (see options.generateCodeForLibraries). - return [`${base}.ts`, `${base}.d.ts`].find( - baseFileName => this.isSourceFile(baseFileName) && this.originalFileExists(baseFileName)); + if (this.shouldGenerateFile(fileName).generate) { + return true; } + return this.originalFileExists(fileName); } loadSummary(filePath: string): string|null { - if (this.summariesFromPreviousCompilations.has(filePath)) { - return this.summariesFromPreviousCompilations.get(filePath) !; + const summary = this.librarySummaries.get(filePath); + if (summary) { + return summary.text; } return super.loadSummary(filePath); } @@ -393,13 +425,19 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends isSourceFile(filePath: string): boolean { // If we have a summary from a previous compilation, // treat the file never as a source file. - if (this.summariesFromPreviousCompilations.has(summaryFileName(filePath))) { + if (this.librarySummaries.has(filePath)) { return false; } return super.isSourceFile(filePath); } - readFile = (fileName: string) => this.context.readFile(fileName); + readFile(fileName: string) { + const summary = this.librarySummaries.get(fileName); + if (summary) { + return summary.text; + } + return this.context.readFile(fileName); + } getDefaultLibFileName = (options: ts.CompilerOptions) => this.context.getDefaultLibFileName(options) getCurrentDirectory = () => this.context.getCurrentDirectory(); @@ -451,12 +489,19 @@ function getPackageName(filePath: string): string|null { export function relativeToRootDirs(filePath: string, rootDirs: string[]): string { if (!filePath) return filePath; for (const dir of rootDirs || []) { - const rel = path.relative(dir, filePath); - if (rel.indexOf('.') != 0) return rel; + const rel = pathStartsWithPrefix(dir, filePath); + if (rel) { + return rel; + } } return filePath; } +function pathStartsWithPrefix(prefix: string, fullPath: string): string|null { + const rel = path.relative(prefix, fullPath); + return rel.startsWith('..') ? null : rel; +} + function stripNodeModulesPrefix(filePath: string): string { return filePath.replace(/.*node_modules\//, ''); } @@ -473,12 +518,3 @@ function stripNgResourceSuffix(fileName: string): string { function addNgResourceSuffix(fileName: string): string { return `${fileName}.$ngresource$`; } - -function summaryFileName(fileName: string): string { - const genFileMatch = GENERATED_FILES.exec(fileName); - if (genFileMatch) { - const base = genFileMatch[1]; - return base + '.ngsummary.json'; - } - return fileName.replace(EXT, '') + '.ngsummary.json'; -} \ No newline at end of file diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index 26b3edc417..2eb6366f62 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -14,8 +14,8 @@ import * as ts from 'typescript'; import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics'; import {ModuleMetadata, createBundleIndexHost} from '../metadata/index'; -import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api'; -import {TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host'; +import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api'; +import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host'; import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions'; import {getAngularEmitterTransformFactory} from './node_emitter_transform'; import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from './util'; @@ -35,10 +35,10 @@ const defaultEmitCallback: TsEmitCallback = class AngularCompilerProgram implements Program { private metadataCache: LowerMetadataCache; - private summariesFromPreviousCompilations = new Map(); + private oldProgramLibrarySummaries: LibrarySummary[] = []; // Note: This will be cleared out as soon as we create the _tsProgram private oldTsProgram: ts.Program|undefined; - private _emittedGenFiles: GeneratedFile[]|undefined; + private emittedLibrarySummaries: LibrarySummary[]|undefined; // Lazily initialized fields private _typeCheckHost: TypeCheckHost; @@ -59,8 +59,7 @@ class AngularCompilerProgram implements Program { } this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined; if (oldProgram) { - oldProgram.getLibrarySummaries().forEach( - ({content, fileName}) => this.summariesFromPreviousCompilations.set(fileName, content)); + this.oldProgramLibrarySummaries = oldProgram.getLibrarySummaries(); } if (options.flatModuleOutFile) { @@ -82,21 +81,12 @@ class AngularCompilerProgram implements Program { this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit); } - getLibrarySummaries(): {fileName: string, content: string}[] { - const emittedLibSummaries: {fileName: string, content: string}[] = []; - this.summariesFromPreviousCompilations.forEach( - (content, fileName) => emittedLibSummaries.push({fileName, content})); - if (this._emittedGenFiles) { - this._emittedGenFiles.forEach(genFile => { - if (genFile.srcFileUrl.endsWith('.d.ts') && - genFile.genFileUrl.endsWith('.ngsummary.json')) { - // Note: ! is ok here as ngsummary.json files are always plain text, so genFile.source - // is filled. - emittedLibSummaries.push({fileName: genFile.genFileUrl, content: genFile.source !}); - } - }); + getLibrarySummaries(): LibrarySummary[] { + const result = [...this.oldProgramLibrarySummaries]; + if (this.emittedLibrarySummaries) { + result.push(...this.emittedLibrarySummaries); } - return emittedLibSummaries; + return result; } getTsProgram(): ts.Program { return this.tsProgram; } @@ -132,8 +122,8 @@ class AngularCompilerProgram implements Program { if (this._analyzedModules) { throw new Error('Angular structure already loaded'); } - const {tmpProgram, analyzedFiles, hostAdapter, rootNames} = this._createProgramWithBasicStubs(); - return this._compiler.loadFilesAsync(analyzedFiles) + const {tmpProgram, sourceFiles, hostAdapter, rootNames} = this._createProgramWithBasicStubs(); + return this._compiler.loadFilesAsync(sourceFiles) .catch(this.catchAnalysisError.bind(this)) .then(analyzedModules => { if (this._analyzedModules) { @@ -159,7 +149,6 @@ class AngularCompilerProgram implements Program { const bundle = this.compiler.emitMessageBundle(this.analyzedModules, locale); i18nExtract(format, file, this.host, this.options, bundle); } - const outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}> = []; if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Codegen)) === 0) { return {emitSkipped: true, diagnostics: [], emittedFiles: []}; @@ -172,6 +161,21 @@ class AngularCompilerProgram implements Program { emittedFiles: [], }; } + const emittedLibrarySummaries = this.emittedLibrarySummaries = []; + + const outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}> = []; + const genFileByFileName = new Map(); + genFiles.forEach(genFile => genFileByFileName.set(genFile.genFileUrl, genFile)); + const writeTsFile: ts.WriteFileCallback = + (outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => { + const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null; + let genFile: GeneratedFile|undefined; + if (sourceFile) { + outSrcMapping.push({outFileName: outFileName, sourceFile}); + genFile = genFileByFileName.get(sourceFile.fileName); + } + this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles); + }; // Restore the original references before we emit so TypeScript doesn't emit // a reference to the .d.ts file. @@ -183,14 +187,13 @@ class AngularCompilerProgram implements Program { sourceFile.referencedFiles = originalReferences; } } - let emitResult: ts.EmitResult; try { emitResult = emitCallback({ program: this.tsProgram, host: this.host, options: this.options, - writeFile: createWriteFileCallback(genFiles, this.host, outSrcMapping), + writeFile: writeTsFile, emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, customTransformers: this.calculateTransforms(genFiles, customTransformers) }); @@ -211,7 +214,8 @@ class AngularCompilerProgram implements Program { if (emitFlags & EmitFlags.Codegen) { genFiles.forEach(gf => { if (gf.source) { - this.host.writeFile(srcToOutPath(gf.genFileUrl), gf.source, false); + const outFileName = srcToOutPath(gf.genFileUrl); + this.writeFile(outFileName, gf.source, false, undefined, gf); } }); } @@ -220,8 +224,8 @@ class AngularCompilerProgram implements Program { if (!sf.isDeclarationFile && !GENERATED_FILES.test(sf.fileName)) { const metadata = this.metadataCache.getMetadata(sf); const metadataText = JSON.stringify([metadata]); - this.host.writeFile( - srcToOutPath(sf.fileName.replace(/\.ts$/, '.metadata.json')), metadataText, false); + const outFileName = srcToOutPath(sf.fileName.replace(/\.ts$/, '.metadata.json')); + this.writeFile(outFileName, metadataText, false, undefined, undefined, [sf]); } }); } @@ -310,10 +314,10 @@ class AngularCompilerProgram implements Program { if (this._analyzedModules) { return; } - const {tmpProgram, analyzedFiles, hostAdapter, rootNames} = this._createProgramWithBasicStubs(); - let analyzedModules: NgAnalyzedModules; + const {tmpProgram, sourceFiles, hostAdapter, rootNames} = this._createProgramWithBasicStubs(); + let analyzedModules: NgAnalyzedModules|null; try { - analyzedModules = this._compiler.loadFilesSync(analyzedFiles); + analyzedModules = this._compiler.loadFilesSync(sourceFiles); } catch (e) { analyzedModules = this.catchAnalysisError(e); } @@ -322,9 +326,9 @@ class AngularCompilerProgram implements Program { private _createProgramWithBasicStubs(): { tmpProgram: ts.Program, - analyzedFiles: NgAnalyzedFile[], hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter, rootNames: string[], + sourceFiles: string[], } { if (this._analyzedModules) { throw new Error(`Internal Error: already initalized!`); @@ -332,19 +336,16 @@ class AngularCompilerProgram implements Program { // Note: This is important to not produce a memory leak! const oldTsProgram = this.oldTsProgram; this.oldTsProgram = undefined; - const analyzedFiles: NgAnalyzedFile[] = []; - const codegen = (fileName: string) => { - if (this._analyzedModules) { - throw new Error(`Internal Error: already initalized!`); - } - const analyzedFile = this._compiler.analyzeFile(fileName); - analyzedFiles.push(analyzedFile); - const debug = fileName.endsWith('application_ref.ts'); - return this._compiler.emitBasicStubs(analyzedFile); + + const codegen: CodeGenerator = { + generateFile: (genFileName, baseFileName) => + this._compiler.emitBasicStub(genFileName, baseFileName), + findGeneratedFileNames: (fileName) => this._compiler.findGeneratedFileNames(fileName), }; + const hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter( this.rootNames, this.options, this.host, this.metadataCache, codegen, - this.summariesFromPreviousCompilations); + this.oldProgramLibrarySummaries); const aotOptions = getAotCompilerOptions(this.options); this._compiler = createAotCompiler(hostAdapter, aotOptions).compiler; this._typeCheckHost = hostAdapter; @@ -354,26 +355,41 @@ class AngularCompilerProgram implements Program { this.rootNames.filter(fn => !GENERATED_FILES.test(fn) || !hostAdapter.isSourceFile(fn)); if (this.options.noResolve) { this.rootNames.forEach(rootName => { - const sf = - hostAdapter.getSourceFile(rootName, this.options.target || ts.ScriptTarget.Latest); - sf.referencedFiles.forEach((fileRef) => { - if (GENERATED_FILES.test(fileRef.fileName)) { - rootNames.push(fileRef.fileName); - } - }); + if (hostAdapter.shouldGenerateFilesFor(rootName)) { + rootNames.push(...this._compiler.findGeneratedFileNames(rootName)); + } }); } const tmpProgram = ts.createProgram(rootNames, this.options, hostAdapter, oldTsProgram); - return {tmpProgram, analyzedFiles, hostAdapter, rootNames}; + const sourceFiles: string[] = []; + tmpProgram.getSourceFiles().forEach(sf => { + if (hostAdapter.isSourceFile(sf.fileName)) { + sourceFiles.push(sf.fileName); + } + }); + return {tmpProgram, sourceFiles, hostAdapter, rootNames}; } private _updateProgramWithTypeCheckStubs( - tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules, + tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules|null, hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter, rootNames: string[]) { - this._analyzedModules = analyzedModules; - const genFiles = this._compiler.emitTypeCheckStubs(analyzedModules); - genFiles.forEach(gf => hostAdapter.updateGeneratedFile(gf)); + this._analyzedModules = analyzedModules || emptyModules; + if (analyzedModules) { + tmpProgram.getSourceFiles().forEach(sf => { + if (sf.fileName.endsWith('.ngfactory.ts')) { + const {generate, baseFileName} = hostAdapter.shouldGenerateFile(sf.fileName); + if (generate) { + // Note: ! is ok as hostAdapter.shouldGenerateFile will always return a basefileName + // for .ngfactory.ts files. + const genFile = this._compiler.emitTypeCheckStub(sf.fileName, baseFileName !); + if (genFile) { + hostAdapter.updateGeneratedFile(genFile); + } + } + } + }); + } this._tsProgram = ts.createProgram(rootNames, this.options, hostAdapter, tmpProgram); // Note: the new ts program should be completely reusable by TypeScript as: // - we cache all the files in the hostAdapter @@ -384,7 +400,7 @@ class AngularCompilerProgram implements Program { } } - private catchAnalysisError(e: any): NgAnalyzedModules { + private catchAnalysisError(e: any): NgAnalyzedModules|null { if (isSyntaxError(e)) { const parserErrors = getParseErrors(e); if (parserErrors && parserErrors.length) { @@ -404,7 +420,7 @@ class AngularCompilerProgram implements Program { code: DEFAULT_ERROR_CODE }]; } - return emptyModules; + return null; } throw e; } @@ -417,7 +433,7 @@ class AngularCompilerProgram implements Program { if (!(emitFlags & EmitFlags.Codegen)) { return {genFiles: [], genDiags: []}; } - const genFiles = this._emittedGenFiles = this.compiler.emitAllImpls(this.analyzedModules); + const genFiles = this.compiler.emitAllImpls(this.analyzedModules); return {genFiles, genDiags: []}; } catch (e) { // TODO(tbosch): check whether we can actually have syntax errors here, @@ -441,6 +457,51 @@ class AngularCompilerProgram implements Program { private generateSemanticDiagnostics(): {ts: ts.Diagnostic[], ng: Diagnostic[]} { return translateDiagnostics(this.typeCheckHost, this.tsProgram.getSemanticDiagnostics()); } + + private writeFile( + outFileName: string, outData: string, writeByteOrderMark: boolean, + onError?: (message: string) => void, genFile?: GeneratedFile, sourceFiles?: ts.SourceFile[]) { + // collect emittedLibrarySummaries + let baseFile: ts.SourceFile|undefined; + if (genFile) { + baseFile = this.tsProgram.getSourceFile(genFile.srcFileUrl); + if (baseFile) { + if (!this.emittedLibrarySummaries) { + this.emittedLibrarySummaries = []; + } + if (genFile.genFileUrl.endsWith('.ngsummary.json') && baseFile.fileName.endsWith('.d.ts')) { + this.emittedLibrarySummaries.push({ + fileName: baseFile.fileName, + text: baseFile.text, + sourceFile: baseFile, + }); + this.emittedLibrarySummaries.push({fileName: genFile.genFileUrl, text: outData}); + } else if (outFileName.endsWith('.d.ts') && baseFile.fileName.endsWith('.d.ts')) { + const dtsSourceFilePath = genFile.genFileUrl.replace(/\.ts$/, '.d.ts'); + // Note: Don't use sourceFiles here as the created .d.ts has a path in the outDir, + // but we need one that is next to the .ts file + this.emittedLibrarySummaries.push({fileName: dtsSourceFilePath, text: outData}); + } + } + } + // Filter out generated files for which we didn't generate code. + // This can happen as the stub caclulation is not completely exact. + // Note: sourceFile refers to the .ngfactory.ts / .ngsummary.ts file + const isGenerated = GENERATED_FILES.test(outFileName); + if (isGenerated) { + if (!genFile || !genFile.stmts || genFile.stmts.length === 0) { + if (this.options.allowEmptyCodegenFiles) { + outData = ''; + } else { + return; + } + } + } + if (baseFile) { + sourceFiles = sourceFiles ? [...sourceFiles, baseFile] : [baseFile]; + } + this.host.writeFile(outFileName, outData, writeByteOrderMark, onError, sourceFiles); + } } export function createProgram( @@ -483,31 +544,7 @@ function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions { enableSummariesForJit: true, preserveWhitespaces: options.preserveWhitespaces, fullTemplateTypeCheck: options.fullTemplateTypeCheck, - rootDir: options.rootDir, - }; -} - -function createWriteFileCallback( - generatedFiles: GeneratedFile[], host: ts.CompilerHost, - outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}>) { - const genFileByFileName = new Map(); - generatedFiles.forEach(genFile => genFileByFileName.set(genFile.genFileUrl, genFile)); - return (fileName: string, data: string, writeByteOrderMark: boolean, - onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { - const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null; - if (sourceFile) { - outSrcMapping.push({outFileName: fileName, sourceFile}); - } - const isGenerated = GENERATED_FILES.test(fileName); - if (isGenerated && sourceFile) { - // Filter out generated files for which we didn't generate code. - // This can happen as the stub caclulation is not completely exact. - const genFile = genFileByFileName.get(sourceFile.fileName); - if (!genFile || !genFile.stmts || genFile.stmts.length === 0) { - return; - } - } - host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); + allowEmptyCodegenFiles: options.allowEmptyCodegenFiles, }; } diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 8d3a52040b..9071939714 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -713,17 +713,17 @@ describe('ngc transformer command-line', () => { }); }); - it('should be able to generate a flat module library', () => { + function writeFlatModule(outFile: string) { writeConfig(` - { - "extends": "./tsconfig-base.json", - "angularCompilerOptions": { - "flatModuleId": "flat_module", - "flatModuleOutFile": "index.js", - "skipTemplateCodegen": true - }, - "files": ["public-api.ts"] - } + { + "extends": "./tsconfig-base.json", + "angularCompilerOptions": { + "flatModuleId": "flat_module", + "flatModuleOutFile": "${outFile}", + "skipTemplateCodegen": true + }, + "files": ["public-api.ts"] + } `); write('public-api.ts', ` export * from './src/flat.component'; @@ -753,6 +753,10 @@ describe('ngc transformer command-line', () => { }) export class FlatModule { }`); + } + + it('should be able to generate a flat module library', () => { + writeFlatModule('index.js'); const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy); expect(exitCode).toEqual(0); @@ -760,6 +764,60 @@ describe('ngc transformer command-line', () => { shouldExist('index.metadata.json'); }); + it('should use the importAs for flat libraries instead of deep imports', () => { + // compile the flat module + writeFlatModule('index.js'); + expect(main(['-p', basePath], errorSpy)).toBe(0); + + // move the flat module output into node_modules + const flatModuleNodeModulesPath = path.resolve(basePath, 'node_modules', 'flat_module'); + fs.renameSync(outDir, flatModuleNodeModulesPath); + fs.renameSync( + path.resolve(basePath, 'src/flat.component.html'), + path.resolve(flatModuleNodeModulesPath, 'src/flat.component.html')); + // add a package.json + fs.writeFileSync( + path.resolve(flatModuleNodeModulesPath, 'package.json'), `{"typings": "./index.d.ts"}`); + + // and remove the sources. + fs.renameSync(path.resolve(basePath, 'src'), path.resolve(basePath, 'flat_module_src')); + fs.unlinkSync(path.resolve(basePath, 'public-api.ts')); + + writeConfig(` + { + "extends": "./tsconfig-base.json", + "files": ["index.ts"] + } + `); + write('index.ts', ` + import {NgModule} from '@angular/core'; + import {FlatModule} from 'flat_module'; + + @NgModule({ + imports: [FlatModule] + }) + export class MyModule {} + `); + + expect(main(['-p', basePath], errorSpy)).toBe(0); + + shouldExist('index.js'); + + const summary = + fs.readFileSync(path.resolve(basePath, 'built', 'index.ngsummary.json')).toString(); + // reference to the module itself + expect(summary).toMatch(/"filePath":"flat_module"/); + // no reference to a deep file + expect(summary).not.toMatch(/"filePath":"flat_module\//); + + const factory = + fs.readFileSync(path.resolve(basePath, 'built', 'index.ngfactory.js')).toString(); + // reference to the module itself + expect(factory).toMatch(/from "flat_module"/); + // no reference to a deep file + expect(factory).not.toMatch(/from "flat_module\//); + }); + describe('with tree example', () => { beforeEach(() => { writeConfig(); diff --git a/packages/compiler-cli/test/transformers/compiler_host_spec.ts b/packages/compiler-cli/test/transformers/compiler_host_spec.ts index 8784ff0c6a..e2579a7e53 100644 --- a/packages/compiler-cli/test/transformers/compiler_host_spec.ts +++ b/packages/compiler-cli/test/transformers/compiler_host_spec.ts @@ -10,7 +10,7 @@ import * as compiler from '@angular/compiler'; import * as ts from 'typescript'; import {MetadataCollector} from '../../src/metadata/collector'; -import {CompilerHost, CompilerOptions} from '../../src/transformers/api'; +import {CompilerHost, CompilerOptions, LibrarySummary} from '../../src/transformers/api'; import {TsCompilerAotCompilerTypeCheckHostAdapter, createCompilerHost} from '../../src/transformers/compiler_host'; import {Directory, Entry, MockAotContext, MockCompilerHost} from '../mocks'; @@ -21,9 +21,14 @@ const aGeneratedFile = new compiler.GeneratedFile( const aGeneratedFileText = `var x:any = 1;\n`; describe('NgCompilerHost', () => { - let codeGenerator: jasmine.Spy; + let codeGenerator: {generateFile: jasmine.Spy; findGeneratedFileNames: jasmine.Spy;}; - beforeEach(() => { codeGenerator = jasmine.createSpy('codeGenerator').and.returnValue([]); }); + beforeEach(() => { + codeGenerator = { + generateFile: jasmine.createSpy('generateFile').and.returnValue(null), + findGeneratedFileNames: jasmine.createSpy('findGeneratedFileNames').and.returnValue([]), + }; + }); function createNgHost({files = {}}: {files?: Directory} = {}): CompilerHost { const context = new MockAotContext('/tmp/', files); @@ -37,16 +42,16 @@ describe('NgCompilerHost', () => { moduleResolution: ts.ModuleResolutionKind.NodeJs, }, ngHost = createNgHost({files}), - summariesFromPreviousCompilations = new Map(), + librarySummaries = [], }: { files?: Directory, options?: CompilerOptions, ngHost?: CompilerHost, - summariesFromPreviousCompilations?: Map + librarySummaries?: LibrarySummary[] } = {}) { return new TsCompilerAotCompilerTypeCheckHostAdapter( ['/tmp/index.ts'], options, ngHost, new MetadataCollector(), codeGenerator, - summariesFromPreviousCompilations); + librarySummaries); } describe('fileNameToModuleName', () => { @@ -180,7 +185,8 @@ describe('NgCompilerHost', () => { }); it('should generate code when asking for the base name and add it as referencedFiles', () => { - codeGenerator.and.returnValue([aGeneratedFile]); + codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']); + codeGenerator.generateFile.and.returnValue(aGeneratedFile); const host = createHost({ files: { 'tmp': { @@ -201,11 +207,13 @@ describe('NgCompilerHost', () => { expect(genSf.text).toBe(aGeneratedFileText); // the codegen should have been cached - expect(codeGenerator).toHaveBeenCalledTimes(1); + expect(codeGenerator.generateFile).toHaveBeenCalledTimes(1); + expect(codeGenerator.findGeneratedFileNames).toHaveBeenCalledTimes(1); }); it('should generate code when asking for the generated name first', () => { - codeGenerator.and.returnValue([aGeneratedFile]); + codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']); + codeGenerator.generateFile.and.returnValue(aGeneratedFile); const host = createHost({ files: { 'tmp': { @@ -226,10 +234,13 @@ describe('NgCompilerHost', () => { expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts'); // the codegen should have been cached - expect(codeGenerator).toHaveBeenCalledTimes(1); + expect(codeGenerator.generateFile).toHaveBeenCalledTimes(1); + expect(codeGenerator.findGeneratedFileNames).toHaveBeenCalledTimes(1); }); it('should clear old generated references if the original host cached them', () => { + codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']); + const ngHost = createNgHost(); const sfText = ` /// @@ -237,8 +248,9 @@ describe('NgCompilerHost', () => { const sf = ts.createSourceFile('/tmp/src/index.ts', sfText, ts.ScriptTarget.Latest); ngHost.getSourceFile = () => sf; - codeGenerator.and.returnValue( - [new compiler.GeneratedFile('/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', [])]); + codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']); + codeGenerator.generateFile.and.returnValue( + new compiler.GeneratedFile('/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', [])); const host1 = createHost({ngHost}); host1.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest); @@ -246,7 +258,8 @@ describe('NgCompilerHost', () => { expect(sf.referencedFiles[0].fileName).toBe('main.ts'); expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts'); - codeGenerator.and.returnValue([]); + codeGenerator.findGeneratedFileNames.and.returnValue([]); + codeGenerator.generateFile.and.returnValue(null); const host2 = createHost({ngHost}); host2.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest); @@ -257,7 +270,8 @@ describe('NgCompilerHost', () => { describe('updateSourceFile', () => { it('should update source files', () => { - codeGenerator.and.returnValue([aGeneratedFile]); + codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']); + codeGenerator.generateFile.and.returnValue(aGeneratedFile); const host = createHost({files: {'tmp': {'src': {'index.ts': ''}}}}); let genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest); @@ -271,11 +285,12 @@ describe('NgCompilerHost', () => { }); it('should error if the imports changed', () => { - codeGenerator.and.returnValue( - [new compiler.GeneratedFile('/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', [ - new compiler.DeclareVarStmt( - 'x', new compiler.ExternalExpr(new compiler.ExternalReference('aModule', 'aName'))) - ])]); + codeGenerator.findGeneratedFileNames.and.returnValue(['/tmp/src/index.ngfactory.ts']); + codeGenerator.generateFile.and.returnValue(new compiler.GeneratedFile( + '/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', + [new compiler.DeclareVarStmt( + 'x', + new compiler.ExternalExpr(new compiler.ExternalReference('aModule', 'aName')))])); const host = createHost({files: {'tmp': {'src': {'index.ts': ''}}}}); host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest); @@ -292,32 +307,4 @@ describe('NgCompilerHost', () => { ].join('\n')); }); }); - - describe('fileExists', () => { - it('should cache calls', () => { - const ngHost = createNgHost({files: {'tmp': {'src': {'index.ts': ``}}}}); - spyOn(ngHost, 'fileExists').and.callThrough(); - const host = createHost({ngHost}); - - expect(host.fileExists('/tmp/src/index.ts')).toBe(true); - expect(host.fileExists('/tmp/src/index.ts')).toBe(true); - - expect(ngHost.fileExists).toHaveBeenCalledTimes(1); - }); - - it(`should not derive the existence of generated files baesd on summaries on disc`, () => { - const host = createHost({files: {'tmp': {'lib': {'module.ngsummary.json': ``}}}}); - expect(host.fileExists('/tmp/lib/module.ngfactory.ts')).toBe(false); - expect(host.fileExists('/tmp/lib/module.ngfactory.d.ts')).toBe(false); - }); - - it(`should derive the existence of generated .d.ts files based on the summaries from an old program`, - () => { - const summariesFromPreviousCompilations = new Map(); - summariesFromPreviousCompilations.set('/tmp/lib/module.ngsummary.json', `{}`); - const host = createHost({summariesFromPreviousCompilations}); - expect(host.fileExists('/tmp/lib/module.ngfactory.ts')).toBe(false); - expect(host.fileExists('/tmp/lib/module.ngfactory.d.ts')).toBe(true); - }); - }); }); diff --git a/packages/compiler-cli/test/transformers/program_spec.ts b/packages/compiler-cli/test/transformers/program_spec.ts index ee8747a903..679bd6544d 100644 --- a/packages/compiler-cli/test/transformers/program_spec.ts +++ b/packages/compiler-cli/test/transformers/program_spec.ts @@ -38,41 +38,42 @@ describe('ng program', () => { `; } + function compileLib(libName: string) { + testSupport.writeFiles({ + [`${libName}_src/index.ts`]: createModuleAndCompSource(libName), + }); + const options = testSupport.createCompilerOptions(); + const program = ng.createProgram({ + rootNames: [path.resolve(testSupport.basePath, `${libName}_src/index.ts`)], + options, + host: ng.createCompilerHost({options}), + }); + expectNoDiagnosticsInProgram(options, program); + fs.symlinkSync( + path.resolve(testSupport.basePath, 'built', `${libName}_src`), + path.resolve(testSupport.basePath, 'node_modules', libName)); + program.emit({emitFlags: ng.EmitFlags.DTS | ng.EmitFlags.JS | ng.EmitFlags.Metadata}); + } + + function compile( + oldProgram?: ng.Program, overrideOptions?: ng.CompilerOptions, + rootNames?: string[]): ng.Program { + const options = testSupport.createCompilerOptions(overrideOptions); + if (!rootNames) { + rootNames = [path.resolve(testSupport.basePath, 'src/index.ts')]; + } + + const program = ng.createProgram({ + rootNames: rootNames, + options, + host: ng.createCompilerHost({options}), oldProgram, + }); + expectNoDiagnosticsInProgram(options, program); + program.emit(); + return program; + } + describe('reuse of old program', () => { - - function compileLib(libName: string) { - testSupport.writeFiles({ - [`${libName}_src/index.ts`]: createModuleAndCompSource(libName), - }); - const options = testSupport.createCompilerOptions({ - skipTemplateCodegen: true, - }); - const program = ng.createProgram({ - rootNames: [path.resolve(testSupport.basePath, `${libName}_src/index.ts`)], - options, - host: ng.createCompilerHost({options}), - }); - expectNoDiagnosticsInProgram(options, program); - fs.symlinkSync( - path.resolve(testSupport.basePath, 'built', `${libName}_src`), - path.resolve(testSupport.basePath, 'node_modules', libName)); - program.emit({emitFlags: ng.EmitFlags.DTS | ng.EmitFlags.JS | ng.EmitFlags.Metadata}); - } - - function compile(oldProgram?: ng.Program): ng.Program { - const options = testSupport.createCompilerOptions(); - const rootNames = [path.resolve(testSupport.basePath, 'src/index.ts')]; - - const program = ng.createProgram({ - rootNames: rootNames, - options: testSupport.createCompilerOptions(), - host: ng.createCompilerHost({options}), oldProgram, - }); - expectNoDiagnosticsInProgram(options, program); - program.emit(); - return program; - } - it('should reuse generated code for libraries from old programs', () => { compileLib('lib'); testSupport.writeFiles({ @@ -123,6 +124,29 @@ describe('ng program', () => { .toBe(false); }); + it('should store library summaries on emit', () => { + compileLib('lib'); + testSupport.writeFiles({ + 'src/main.ts': createModuleAndCompSource('main'), + 'src/index.ts': ` + export * from './main'; + export * from 'lib/index'; + ` + }); + const p1 = compile(); + expect(p1.getLibrarySummaries().some( + sf => /node_modules\/lib\/index\.ngfactory\.d\.ts$/.test(sf.fileName))) + .toBe(true); + expect(p1.getLibrarySummaries().some( + sf => /node_modules\/lib\/index\.ngsummary\.json$/.test(sf.fileName))) + .toBe(true); + expect( + p1.getLibrarySummaries().some(sf => /node_modules\/lib\/index\.d\.ts$/.test(sf.fileName))) + .toBe(true); + + expect(p1.getLibrarySummaries().some(sf => /src\/main.*$/.test(sf.fileName))).toBe(false); + }); + it('should reuse the old ts program completely if nothing changed', () => { testSupport.writeFiles({'src/index.ts': createModuleAndCompSource('main')}); // Note: the second compile drops factories for library files, @@ -223,13 +247,118 @@ describe('ng program', () => { const allRootNames = preProgram.getSourceFiles().map(sf => sf.fileName); // now do the actual test with noResolve - const options = testSupport.createCompilerOptions({noResolve: true}); - const host = ng.createCompilerHost({options}); - const program = ng.createProgram({rootNames: allRootNames, options, host}); - expectNoDiagnosticsInProgram(options, program); - program.emit(); + const program = compile(undefined, {noResolve: true}, allRootNames); testSupport.shouldExist('built/src/main.ngfactory.js'); testSupport.shouldExist('built/src/main.ngfactory.d.ts'); }); + + it('should emit also empty generated files depending on the options', () => { + testSupport.writeFiles({ + 'src/main.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({selector: 'main', template: '', styleUrls: ['main.css']}) + export class MainComp {} + + @NgModule({declarations: [MainComp]}) + export class MainModule {} + `, + 'src/main.css': ``, + 'src/util.ts': 'export const x = 1;', + 'src/index.ts': ` + export * from './util'; + export * from './main'; + `, + }); + const options = testSupport.createCompilerOptions({allowEmptyCodegenFiles: true}); + const host = ng.createCompilerHost({options}); + const written = new Map < string, { + original: ts.SourceFile[]|undefined; + data: string; + } + > (); + + host.writeFile = + (fileName: string, data: string, writeByteOrderMark: boolean, + onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { + written.set(fileName, {original: sourceFiles, data}); + }; + const program = ng.createProgram( + {rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host}); + program.emit(); + + function assertGenFile( + fileName: string, checks: {originalFileName: string, shouldBeEmpty: boolean}) { + const writeData = written.get(path.join(testSupport.basePath, fileName)); + expect(writeData).toBeTruthy(); + expect(writeData !.original !.some( + sf => sf.fileName === path.join(testSupport.basePath, checks.originalFileName))) + .toBe(true); + if (checks.shouldBeEmpty) { + expect(writeData !.data).toBe(''); + } else { + expect(writeData !.data).not.toBe(''); + } + } + + assertGenFile( + 'built/src/util.ngfactory.js', {originalFileName: 'src/util.ts', shouldBeEmpty: true}); + assertGenFile( + 'built/src/util.ngfactory.d.ts', {originalFileName: 'src/util.ts', shouldBeEmpty: true}); + assertGenFile( + 'built/src/util.ngsummary.js', {originalFileName: 'src/util.ts', shouldBeEmpty: true}); + assertGenFile( + 'built/src/util.ngsummary.d.ts', {originalFileName: 'src/util.ts', shouldBeEmpty: true}); + assertGenFile( + 'built/src/util.ngsummary.json', {originalFileName: 'src/util.ts', shouldBeEmpty: false}); + + // Note: we always fill non shim and shim style files as they might + // be shared by component with and without ViewEncapsulation. + assertGenFile( + 'built/src/main.css.ngstyle.js', {originalFileName: 'src/main.ts', shouldBeEmpty: false}); + assertGenFile( + 'built/src/main.css.ngstyle.d.ts', {originalFileName: 'src/main.ts', shouldBeEmpty: true}); + // Note: this file is not empty as we actually generated code for it + assertGenFile( + 'built/src/main.css.shim.ngstyle.js', + {originalFileName: 'src/main.ts', shouldBeEmpty: false}); + assertGenFile( + 'built/src/main.css.shim.ngstyle.d.ts', + {originalFileName: 'src/main.ts', shouldBeEmpty: true}); + }); + + it('should not emit /// references in .d.ts files', () => { + testSupport.writeFiles({ + 'src/main.ts': createModuleAndCompSource('main'), + }); + compile(undefined, {declaration: true}, [path.resolve(testSupport.basePath, 'src/main.ts')]); + + const dts = + fs.readFileSync(path.resolve(testSupport.basePath, 'built', 'src', 'main.d.ts')).toString(); + expect(dts).toMatch('export declare class'); + expect(dts).not.toMatch('///'); + }); + + it('should not emit generated files whose sources are outside of the rootDir', () => { + compileLib('lib'); + testSupport.writeFiles({ + 'src/main.ts': createModuleAndCompSource('main'), + 'src/index.ts': ` + export * from './main'; + export * from 'lib/index'; + ` + }); + compile(undefined, {rootDir: path.resolve(testSupport.basePath, 'src')}); + testSupport.shouldExist('built/main.js'); + testSupport.shouldExist('built/main.d.ts'); + testSupport.shouldExist('built/main.ngfactory.js'); + testSupport.shouldExist('built/main.ngfactory.d.ts'); + testSupport.shouldExist('built/main.ngsummary.json'); + testSupport.shouldNotExist('build/node_modules/lib/index.js'); + testSupport.shouldNotExist('build/node_modules/lib/index.d.ts'); + testSupport.shouldNotExist('build/node_modules/lib/index.ngfactory.js'); + testSupport.shouldNotExist('build/node_modules/lib/index.ngfactory.d.ts'); + testSupport.shouldNotExist('build/node_modules/lib/index.ngsummary.json'); + }); }); diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index 852e045d36..2f1f7017cd 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -44,9 +44,10 @@ enum StubEmitFlags { export class AotCompiler { private _templateAstCache = new Map(); + private _analyzedFiles = new Map(); constructor( - private _config: CompilerConfig, private options: AotCompilerOptions, + private _config: CompilerConfig, private _options: AotCompilerOptions, private _host: AotCompilerHost, private _reflector: StaticReflector, private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, @@ -76,23 +77,99 @@ export class AotCompiler { .then(() => analyzeResult); } - analyzeFile(fileName: string): NgAnalyzedFile { - return analyzeFile(this._host, this._symbolResolver, this._metadataResolver, fileName); + private _analyzeFile(fileName: string): NgAnalyzedFile { + let analyzedFile = this._analyzedFiles.get(fileName); + if (!analyzedFile) { + analyzedFile = + analyzeFile(this._host, this._symbolResolver, this._metadataResolver, fileName); + this._analyzedFiles.set(fileName, analyzedFile); + } + return analyzedFile; } - emitBasicStubs(file: NgAnalyzedFile): GeneratedFile[] { - return this._emitStubs(file, StubEmitFlags.Basic); + findGeneratedFileNames(fileName: string): string[] { + const genFileNames: string[] = []; + const file = this._analyzeFile(fileName); + // Make sure we create a .ngfactory if we have a injectable/directive/pipe/NgModule + // or a reference to a non source file. + // Note: This is overestimating the required .ngfactory files as the real calculation is harder. + // Only do this for StubEmitFlags.Basic, as adding a type check block + // does not change this file (as we generate type check blocks based on NgModules). + if (this._options.allowEmptyCodegenFiles || file.directives.length || file.pipes.length || + file.injectables.length || file.ngModules.length || file.exportsNonSourceFiles) { + genFileNames.push(ngfactoryFilePath(file.fileName, true)); + genFileNames.push(summaryForJitFileName(file.fileName, true)); + } + const fileSuffix = splitTypescriptSuffix(file.fileName, true)[1]; + file.directives.forEach((dirSymbol) => { + const compMeta = + this._metadataResolver.getNonNormalizedDirectiveMetadata(dirSymbol) !.metadata; + if (!compMeta.isComponent) { + return; + } + // Note: compMeta is a component and therefore template is non null. + compMeta.template !.styleUrls.forEach((styleUrl) => { + const normalizedUrl = this._host.resourceNameToFileName(styleUrl, file.fileName); + if (!normalizedUrl) { + throw new Error(`Couldn't resolve resource ${styleUrl} relative to ${file.fileName}`); + } + const needsShim = (compMeta.template !.encapsulation || + this._config.defaultEncapsulation) === ViewEncapsulation.Emulated; + genFileNames.push(_stylesModuleUrl(normalizedUrl, needsShim, fileSuffix)); + if (this._options.allowEmptyCodegenFiles) { + genFileNames.push(_stylesModuleUrl(normalizedUrl, !needsShim, fileSuffix)); + } + }); + }); + return genFileNames; } - emitTypeCheckStubs(files: NgAnalyzedModules): GeneratedFile[] { - const generatedFiles: GeneratedFile[] = []; - files.files.forEach( - file => this._emitStubs(file, StubEmitFlags.TypeCheck) - .forEach(genFile => generatedFiles.push(genFile))); - return generatedFiles; + emitBasicStub(genFileName: string, originalFileName?: string): GeneratedFile { + const outputCtx = this._createOutputContext(genFileName); + if (genFileName.endsWith('.ngfactory.ts')) { + if (!originalFileName) { + throw new Error( + `Assertion error: require the original file for .ngfactory.ts stubs. File: ${genFileName}`); + } + const originalFile = this._analyzeFile(originalFileName); + this._createNgFactoryStub(outputCtx, originalFile, StubEmitFlags.Basic); + } else if (genFileName.endsWith('.ngsummary.ts')) { + if (this._options.enableSummariesForJit) { + if (!originalFileName) { + throw new Error( + `Assertion error: require the original file for .ngsummary.ts stubs. File: ${genFileName}`); + } + const originalFile = this._analyzeFile(originalFileName); + _createEmptyStub(outputCtx); + originalFile.ngModules.forEach(ngModule => { + // create exports that user code can reference + createForJitStub(outputCtx, ngModule.type.reference); + }); + } + } else if (genFileName.endsWith('.ngstyle.ts')) { + _createEmptyStub(outputCtx); + } + // Note: for the stubs, we don't need a property srcFileUrl, + // as lateron in emitAllImpls we will create the proper GeneratedFiles with the + // correct srcFileUrl. + // This is good as e.g. for .ngstyle.ts files we can't derive + // the url of components based on the genFileUrl. + return this._codegenSourceModule('unknown', outputCtx); } - loadFilesAsync(files: NgAnalyzedFile[]): Promise { + emitTypeCheckStub(genFileName: string, originalFileName: string): GeneratedFile|null { + const originalFile = this._analyzeFile(originalFileName); + const outputCtx = this._createOutputContext(genFileName); + if (genFileName.endsWith('.ngfactory.ts')) { + this._createNgFactoryStub(outputCtx, originalFile, StubEmitFlags.TypeCheck); + } + return outputCtx.statements.length > 0 ? + this._codegenSourceModule(originalFile.fileName, outputCtx) : + null; + } + + loadFilesAsync(fileNames: string[]): Promise { + const files = fileNames.map(fileName => this._analyzeFile(fileName)); const loadingPromises: Promise[] = []; files.forEach( file => file.ngModules.forEach( @@ -102,7 +179,8 @@ export class AotCompiler { return Promise.all(loadingPromises).then(_ => mergeAndValidateNgFiles(files)); } - loadFilesSync(files: NgAnalyzedFile[]): NgAnalyzedModules { + loadFilesSync(fileNames: string[]): NgAnalyzedModules { + const files = fileNames.map(fileName => this._analyzeFile(fileName)); files.forEach( file => file.ngModules.forEach( ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata( @@ -110,19 +188,8 @@ export class AotCompiler { return mergeAndValidateNgFiles(files); } - private _emitStubs(file: NgAnalyzedFile, emitFlags: StubEmitFlags): GeneratedFile[] { - return [ - ...this._createNgFactoryStub(file, emitFlags), - ...this._createExternalStyleSheetNgFactoryStubs(file, emitFlags), - ...this._createNgSummaryStub(file, emitFlags) - ]; - } - - private _createNgFactoryStub(file: NgAnalyzedFile, emitFlags: StubEmitFlags): GeneratedFile[] { - const generatedFiles: GeneratedFile[] = []; - const outputCtx = this._createOutputContext( - calculateGenFileName(ngfactoryFilePath(file.fileName, true), this.options.rootDir)); - + private _createNgFactoryStub( + outputCtx: OutputContext, file: NgAnalyzedFile, emitFlags: StubEmitFlags) { file.ngModules.forEach((ngModuleMeta, ngModuleIndex) => { // Note: the code below needs to executed for StubEmitFlags.Basic and StubEmitFlags.TypeCheck, // so we don't change the .ngfactory file too much when adding the typecheck block. @@ -170,76 +237,9 @@ export class AotCompiler { } }); - // Make sure we create a .ngfactory if we have a injectable/directive/pipe/NgModule - // or a reference to a non source file. - // Note: This is overestimating the required .ngfactory files as the real calculation is harder. - // Only do this for StubEmitFlags.Basic, as adding a type check block - // does not change this file (as we generate type check blocks based on NgModules). - if (outputCtx.statements.length === 0 && (emitFlags & StubEmitFlags.Basic) && - (file.directives.length || file.pipes.length || file.injectables.length || - file.ngModules.length || file.exportsNonSourceFiles)) { + if (outputCtx.statements.length === 0) { _createEmptyStub(outputCtx); } - - if (outputCtx.statements.length > 0) { - generatedFiles.push(this._codegenSourceModule(file.fileName, outputCtx)); - } - return generatedFiles; - } - - private _createExternalStyleSheetNgFactoryStubs(file: NgAnalyzedFile, emitFlags: StubEmitFlags): - GeneratedFile[] { - const generatedFiles: GeneratedFile[] = []; - if (!(emitFlags & StubEmitFlags.Basic)) { - // note: stylesheet stubs don't change when we produce type check stubs - return generatedFiles; - } - const fileSuffix = splitTypescriptSuffix(file.fileName, true)[1]; - file.directives.forEach((dirSymbol) => { - const compMeta = - this._metadataResolver.getNonNormalizedDirectiveMetadata(dirSymbol) !.metadata; - if (!compMeta.isComponent) { - return; - } - // Note: compMeta is a component and therefore template is non null. - compMeta.template !.styleUrls.forEach((styleUrl) => { - const normalizedUrl = this._host.resourceNameToFileName(styleUrl, file.fileName); - if (!normalizedUrl) { - throw new Error(`Couldn't resolve resource ${styleUrl} relative to ${file.fileName}`); - } - const encapsulation = - compMeta.template !.encapsulation || this._config.defaultEncapsulation; - const outputCtx = this._createOutputContext(calculateGenFileName( - _stylesModuleUrl( - normalizedUrl, encapsulation === ViewEncapsulation.Emulated, fileSuffix), - this.options.rootDir)); - _createEmptyStub(outputCtx); - generatedFiles.push(this._codegenSourceModule(normalizedUrl, outputCtx)); - }); - }); - return generatedFiles; - } - - private _createNgSummaryStub(file: NgAnalyzedFile, emitFlags: StubEmitFlags): GeneratedFile[] { - const generatedFiles: GeneratedFile[] = []; - // note: .ngsummary.js stubs don't change when we produce type check stubs - if (!this.options.enableSummariesForJit || !(emitFlags & StubEmitFlags.Basic)) { - return generatedFiles; - } - if (file.directives.length || file.injectables.length || file.ngModules.length || - file.pipes.length || file.exportsNonSourceFiles) { - const outputCtx = this._createOutputContext( - calculateGenFileName(summaryForJitFileName(file.fileName, true), this.options.rootDir)); - file.ngModules.forEach(ngModule => { - // create exports that user code can reference - createForJitStub(outputCtx, ngModule.type.reference); - }); - if (outputCtx.statements.length === 0) { - _createEmptyStub(outputCtx); - } - generatedFiles.push(this._codegenSourceModule(file.fileName, outputCtx)); - } - return generatedFiles; } private _createTypeCheckBlock( @@ -298,8 +298,7 @@ export class AotCompiler { const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1]; const generatedFiles: GeneratedFile[] = []; - const outputCtx = this._createOutputContext( - calculateGenFileName(ngfactoryFilePath(srcFileUrl, true), this.options.rootDir)); + const outputCtx = this._createOutputContext(ngfactoryFilePath(srcFileUrl, true)); generatedFiles.push( ...this._createSummary(srcFileUrl, directives, pipes, ngModules, injectables, outputCtx)); @@ -323,8 +322,15 @@ export class AotCompiler { const componentStylesheet = this._styleCompiler.compileComponent(outputCtx, compMeta); // Note: compMeta is a component and therefore template is non null. compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => { + // Note: fill non shim and shim style files as they might + // be shared by component with and without ViewEncapsulation. + const shim = this._styleCompiler.needsStyleShim(compMeta); generatedFiles.push( - this._codegenStyles(stylesheetMeta.moduleUrl !, compMeta, stylesheetMeta, fileSuffix)); + this._codegenStyles(srcFileUrl, compMeta, stylesheetMeta, shim, fileSuffix)); + if (this._options.allowEmptyCodegenFiles) { + generatedFiles.push( + this._codegenStyles(srcFileUrl, compMeta, stylesheetMeta, !shim, fileSuffix)); + } }); // compile components @@ -333,7 +339,7 @@ export class AotCompiler { fileSuffix); this._compileComponentFactory(outputCtx, compMeta, ngModule, fileSuffix); }); - if (outputCtx.statements.length > 0) { + if (outputCtx.statements.length > 0 || this._options.allowEmptyCodegenFiles) { const srcModule = this._codegenSourceModule(srcFileUrl, outputCtx); generatedFiles.unshift(srcModule); } @@ -370,8 +376,7 @@ export class AotCompiler { metadata: this._metadataResolver.getInjectableSummary(ref) !.type })) ]; - const forJitOutputCtx = this._createOutputContext( - calculateGenFileName(summaryForJitFileName(srcFileName, true), this.options.rootDir)); + const forJitOutputCtx = this._createOutputContext(summaryForJitFileName(srcFileName, true)); const {json, exportAs} = serializeSummaries( srcFileName, forJitOutputCtx, this._summaryResolver, this._symbolResolver, symbolSummaries, typeData); @@ -382,7 +387,7 @@ export class AotCompiler { ])); }); const summaryJson = new GeneratedFile(srcFileName, summaryFileName(srcFileName), json); - if (this.options.enableSummariesForJit) { + if (this._options.enableSummariesForJit) { return [summaryJson, this._codegenSourceModule(srcFileName, forJitOutputCtx)]; } @@ -392,18 +397,18 @@ export class AotCompiler { private _compileModule(outputCtx: OutputContext, ngModule: CompileNgModuleMetadata): void { const providers: CompileProviderMetadata[] = []; - if (this.options.locale) { - const normalizedLocale = this.options.locale.replace(/_/g, '-'); + if (this._options.locale) { + const normalizedLocale = this._options.locale.replace(/_/g, '-'); providers.push({ token: createTokenForExternalReference(this._reflector, Identifiers.LOCALE_ID), useValue: normalizedLocale, }); } - if (this.options.i18nFormat) { + if (this._options.i18nFormat) { providers.push({ token: createTokenForExternalReference(this._reflector, Identifiers.TRANSLATIONS_FORMAT), - useValue: this.options.i18nFormat + useValue: this._options.i18nFormat }); } @@ -520,17 +525,13 @@ export class AotCompiler { private _codegenStyles( srcFileUrl: string, compMeta: CompileDirectiveMetadata, - stylesheetMetadata: CompileStylesheetMetadata, fileSuffix: string): GeneratedFile { - const outputCtx = this._createOutputContext(calculateGenFileName( - _stylesModuleUrl( - stylesheetMetadata.moduleUrl !, this._styleCompiler.needsStyleShim(compMeta), - fileSuffix), - this.options.rootDir)); + stylesheetMetadata: CompileStylesheetMetadata, isShimmed: boolean, + fileSuffix: string): GeneratedFile { + const outputCtx = this._createOutputContext( + _stylesModuleUrl(stylesheetMetadata.moduleUrl !, isShimmed, fileSuffix)); const compiledStylesheet = - this._styleCompiler.compileStyles(outputCtx, compMeta, stylesheetMetadata); - _resolveStyleStatements( - this._symbolResolver, compiledStylesheet, this._styleCompiler.needsStyleShim(compMeta), - fileSuffix); + this._styleCompiler.compileStyles(outputCtx, compMeta, stylesheetMetadata, isShimmed); + _resolveStyleStatements(this._symbolResolver, compiledStylesheet, isShimmed, fileSuffix); return this._codegenSourceModule(srcFileUrl, outputCtx); } @@ -731,17 +732,3 @@ export function mergeAnalyzedFiles(analyzedFiles: NgAnalyzedFile[]): NgAnalyzedM function mergeAndValidateNgFiles(files: NgAnalyzedFile[]): NgAnalyzedModules { return validateAnalyzedModules(mergeAnalyzedFiles(files)); } - -function calculateGenFileName(fileName: string, rootDir: string | undefined): string { - if (!rootDir) return fileName; - - const fileNameParts = fileName.split(/\\|\//); - const rootDirParts = rootDir.split(/\\|\//); - if (!rootDirParts[rootDirParts.length - 1]) rootDirParts.pop(); - let i = 0; - while (i < Math.min(fileNameParts.length, rootDirParts.length) && - fileNameParts[i] === rootDirParts[i]) - i++; - const result = [...rootDirParts, ...fileNameParts.slice(i)].join('/'); - return result; -} \ No newline at end of file diff --git a/packages/compiler/src/aot/compiler_options.ts b/packages/compiler/src/aot/compiler_options.ts index 784d765342..cf12e2b6a3 100644 --- a/packages/compiler/src/aot/compiler_options.ts +++ b/packages/compiler/src/aot/compiler_options.ts @@ -14,8 +14,9 @@ export interface AotCompilerOptions { translations?: string; missingTranslation?: MissingTranslationStrategy; enableLegacyTemplate?: boolean; + /** TODO(tbosch): remove this flag as it is always on in the new ngc */ enableSummariesForJit?: boolean; preserveWhitespaces?: boolean; fullTemplateTypeCheck?: boolean; - rootDir?: string; + allowEmptyCodegenFiles?: boolean; } diff --git a/packages/compiler/src/aot/static_symbol_resolver.ts b/packages/compiler/src/aot/static_symbol_resolver.ts index ee04005d73..884213b97d 100644 --- a/packages/compiler/src/aot/static_symbol_resolver.ts +++ b/packages/compiler/src/aot/static_symbol_resolver.ts @@ -45,7 +45,7 @@ export interface StaticSymbolResolverHost { * * See ImportResolver. */ - fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null; + fileNameToModuleName(importedFilePath: string, containingFilePath: string): string; } const SUPPORTED_SCHEMA_VERSION = 3; @@ -163,7 +163,7 @@ export class StaticSymbolResolver { /** * Converts a file path to a module name that can be used as an `import`. */ - fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null { + fileNameToModuleName(importedFilePath: string, containingFilePath: string): string { return this.knownFileNameToModuleNames.get(importedFilePath) || this.host.fileNameToModuleName(importedFilePath, containingFilePath); } @@ -292,11 +292,6 @@ export class StaticSymbolResolver { this.resolvedFilePaths.add(filePath); const resolvedSymbols: ResolvedStaticSymbol[] = []; const metadata = this.getModuleMetadata(filePath); - if (metadata['importAs']) { - // Index bundle indices should use the importAs module name defined - // in the bundle. - this.knownFileNameToModuleNames.set(filePath, metadata['importAs']); - } if (metadata['metadata']) { // handle direct declarations of the symbol const topLevelSymbolNames = diff --git a/packages/compiler/src/style_compiler.ts b/packages/compiler/src/style_compiler.ts index 59010f393e..666c501a3e 100644 --- a/packages/compiler/src/style_compiler.ts +++ b/packages/compiler/src/style_compiler.ts @@ -42,13 +42,14 @@ export class StyleCompiler { styleUrls: template.styleUrls, moduleUrl: identifierModuleUrl(comp.type) }), - true); + this.needsStyleShim(comp), true); } compileStyles( outputCtx: OutputContext, comp: CompileDirectiveMetadata, - stylesheet: CompileStylesheetMetadata): CompiledStylesheet { - return this._compileStyles(outputCtx, comp, stylesheet, false); + stylesheet: CompileStylesheetMetadata, + shim: boolean = this.needsStyleShim(comp)): CompiledStylesheet { + return this._compileStyles(outputCtx, comp, stylesheet, shim, false); } needsStyleShim(comp: CompileDirectiveMetadata): boolean { @@ -57,8 +58,8 @@ export class StyleCompiler { private _compileStyles( outputCtx: OutputContext, comp: CompileDirectiveMetadata, - stylesheet: CompileStylesheetMetadata, isComponentStylesheet: boolean): CompiledStylesheet { - const shim = this.needsStyleShim(comp); + stylesheet: CompileStylesheetMetadata, shim: boolean, + isComponentStylesheet: boolean): CompiledStylesheet { const styleExpressions: o.Expression[] = stylesheet.styles.map(plainStyle => o.literal(this._shimIfNeeded(plainStyle, shim))); const dependencies: StylesCompileDependency[] = []; diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index 82368119fc..9910e82a17 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -410,7 +410,7 @@ export class MockAotCompilerHost implements AotCompilerHost { fromSummaryFileName(filePath: string): string { return filePath; } // AotCompilerHost - fileNameToModuleName(importedFile: string, containingFile: string): string|null { + fileNameToModuleName(importedFile: string, containingFile: string): string { return importedFile.replace(EXT, ''); }