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, ''); }