fix(compiler): various squashed fixes for the new ngc

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)
This commit is contained in:
Tobias Bosch
2017-09-21 18:05:07 -07:00
committed by Victor Berchet
parent 13613d4acb
commit a8a9660112
16 changed files with 746 additions and 459 deletions

View File

@ -39,7 +39,7 @@ export abstract class BaseAotCompilerHost<C extends BaseAotCompilerHostContext>
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<C extends BaseAotCompilerHostContext>
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<C extends BaseAotCompilerHostContext>
}
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<C extends BaseAotCompilerHostContext>
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<CompilerHostContext> {
* 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)) {

View File

@ -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.

View File

@ -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] : [];
}

View File

@ -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[];
}

View File

@ -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<string, ts.SourceFile>();
private originalSourceFiles = new Map<string, ts.SourceFile|undefined>();
private originalFileExistsCache = new Map<string, boolean>();
private generatedSourceFiles = new Map<string, GenSourceFile>();
private generatedCodeFor = new Set<string>();
private generatedCodeFor = new Map<string, string[]>();
private emitter = new TypeScriptEmitter();
private librarySummaries = new Map<string, LibrarySummary>();
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<string, string>) {
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<string>): ts.SourceFile
|null {
private addGeneratedFile(genFile: GeneratedFile, externalReferences: Set<string>): 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';
}

View File

@ -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<string, string>();
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<string, GeneratedFile>();
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<string, GeneratedFile>();
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,
};
}

View File

@ -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();

View File

@ -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<string, string>(),
librarySummaries = [],
}: {
files?: Directory,
options?: CompilerOptions,
ngHost?: CompilerHost,
summariesFromPreviousCompilations?: Map<string, string>
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 = `
/// <reference path="main.ts"/>
@ -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<string, string>();
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);
});
});
});

View File

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