Merge remote-tracking branch 'en/master' into aio
This commit is contained in:
commit
9a43523368
@ -71,6 +71,7 @@ def _ngc_tsconfig(ctx, files, srcs, **kwargs):
|
||||
"angularCompilerOptions": {
|
||||
"generateCodeForLibraries": False,
|
||||
"allowEmptyCodegenFiles": True,
|
||||
"enableSummariesforJit": True,
|
||||
# FIXME: wrong place to de-dupe
|
||||
"expectedOut": depset([o.path for o in expected_outs]).to_list()
|
||||
}
|
||||
|
@ -3,7 +3,8 @@
|
||||
// For TypeScript 1.8, we have to lay out generated files
|
||||
// in the same source directory with your code.
|
||||
"genDir": ".",
|
||||
"debug": true
|
||||
"debug": true,
|
||||
"enableSummariesForJit": true
|
||||
},
|
||||
|
||||
"compilerOptions": {
|
||||
|
@ -18,7 +18,7 @@ import * as api from './transformers/api';
|
||||
import * as ngc from './transformers/entry_points';
|
||||
import {GENERATED_FILES} from './transformers/util';
|
||||
|
||||
import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult} from './perform_compile';
|
||||
import {exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult, filterErrorsAndWarnings} from './perform_compile';
|
||||
import {performWatchCompilation, createPerformWatchHost} from './perform_watch';
|
||||
import {isSyntaxError} from '@angular/compiler';
|
||||
|
||||
@ -130,8 +130,9 @@ export function readCommandLineAndConfiguration(
|
||||
function reportErrorsAndExit(
|
||||
options: api.CompilerOptions, allDiagnostics: Diagnostics,
|
||||
consoleError: (s: string) => void = console.error): number {
|
||||
if (allDiagnostics.length) {
|
||||
consoleError(formatDiagnostics(options, allDiagnostics));
|
||||
const errorsAndWarnings = filterErrorsAndWarnings(allDiagnostics);
|
||||
if (errorsAndWarnings.length) {
|
||||
consoleError(formatDiagnostics(options, errorsAndWarnings));
|
||||
}
|
||||
return exitCodeFromResult(allDiagnostics);
|
||||
}
|
||||
|
@ -19,9 +19,11 @@ import {ParseSourceSpan} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {formatDiagnostics as formatDiagnosticsOrig} from './perform_compile';
|
||||
import {Program as ProgramOrig} from './transformers/api';
|
||||
import {createCompilerHost as createCompilerOrig} from './transformers/compiler_host';
|
||||
import {createProgram as createProgramOrig} from './transformers/program';
|
||||
|
||||
|
||||
// Interfaces from ./transformers/api;
|
||||
export interface Diagnostic {
|
||||
messageText: string;
|
||||
@ -92,12 +94,6 @@ 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[];
|
||||
@ -116,7 +112,6 @@ export interface Program {
|
||||
customTransformers?: CustomTransformers,
|
||||
emitCallback?: TsEmitCallback
|
||||
}): ts.EmitResult;
|
||||
getLibrarySummaries(): LibrarySummary[];
|
||||
}
|
||||
|
||||
// Wrapper for createProgram.
|
||||
@ -124,7 +119,7 @@ export function createProgram(
|
||||
{rootNames, options, host, oldProgram}:
|
||||
{rootNames: string[], options: CompilerOptions, host: CompilerHost, oldProgram?: Program}):
|
||||
Program {
|
||||
return createProgramOrig({rootNames, options, host, oldProgram});
|
||||
return createProgramOrig({rootNames, options, host, oldProgram: oldProgram as ProgramOrig});
|
||||
}
|
||||
|
||||
// Wrapper for createCompilerHost.
|
||||
|
@ -13,11 +13,16 @@ import * as ts from 'typescript';
|
||||
|
||||
import * as api from './transformers/api';
|
||||
import * as ng from './transformers/entry_points';
|
||||
import {createMessageDiagnostic} from './transformers/util';
|
||||
|
||||
const TS_EXT = /\.ts$/;
|
||||
|
||||
export type Diagnostics = Array<ts.Diagnostic|api.Diagnostic>;
|
||||
|
||||
export function filterErrorsAndWarnings(diagnostics: Diagnostics): Diagnostics {
|
||||
return diagnostics.filter(d => d.category !== ts.DiagnosticCategory.Message);
|
||||
}
|
||||
|
||||
export function formatDiagnostics(options: api.CompilerOptions, diags: Diagnostics): string {
|
||||
if (diags && diags.length) {
|
||||
const tsFormatHost: ts.FormatDiagnosticsHost = {
|
||||
@ -123,7 +128,7 @@ export interface PerformCompilationResult {
|
||||
}
|
||||
|
||||
export function exitCodeFromResult(diags: Diagnostics | undefined): number {
|
||||
if (!diags || diags.length === 0) {
|
||||
if (!diags || filterErrorsAndWarnings(diags).length === 0) {
|
||||
// If we have a result and didn't get any errors, we succeeded.
|
||||
return 0;
|
||||
}
|
||||
@ -154,7 +159,13 @@ export function performCompilation({rootNames, options, host, oldProgram, emitCa
|
||||
|
||||
program = ng.createProgram({rootNames, host, options, oldProgram});
|
||||
|
||||
const beforeDiags = Date.now();
|
||||
allDiagnostics.push(...gatherDiagnostics(program !));
|
||||
if (options.diagnostics) {
|
||||
const afterDiags = Date.now();
|
||||
allDiagnostics.push(
|
||||
createMessageDiagnostic(`Time for diagnostics: ${afterDiags - beforeDiags}ms.`));
|
||||
}
|
||||
|
||||
if (!hasErrors(allDiagnostics)) {
|
||||
emitResult = program !.emit({emitCallback, customTransformers, emitFlags});
|
||||
|
@ -13,27 +13,7 @@ import * as ts from 'typescript';
|
||||
import {Diagnostics, ParsedConfiguration, PerformCompilationResult, exitCodeFromResult, performCompilation, readConfiguration} from './perform_compile';
|
||||
import * as api from './transformers/api';
|
||||
import {createCompilerHost} from './transformers/entry_points';
|
||||
|
||||
const ChangeDiagnostics = {
|
||||
Compilation_complete_Watching_for_file_changes: {
|
||||
category: ts.DiagnosticCategory.Message,
|
||||
messageText: 'Compilation complete. Watching for file changes.',
|
||||
code: api.DEFAULT_ERROR_CODE,
|
||||
source: api.SOURCE
|
||||
},
|
||||
Compilation_failed_Watching_for_file_changes: {
|
||||
category: ts.DiagnosticCategory.Message,
|
||||
messageText: 'Compilation failed. Watching for file changes.',
|
||||
code: api.DEFAULT_ERROR_CODE,
|
||||
source: api.SOURCE
|
||||
},
|
||||
File_change_detected_Starting_incremental_compilation: {
|
||||
category: ts.DiagnosticCategory.Message,
|
||||
messageText: 'File change detected. Starting incremental compilation.',
|
||||
code: api.DEFAULT_ERROR_CODE,
|
||||
source: api.SOURCE
|
||||
},
|
||||
};
|
||||
import {createMessageDiagnostic} from './transformers/util';
|
||||
|
||||
function totalCompilationTimeDiagnostic(timeInMillis: number): api.Diagnostic {
|
||||
let duration: string;
|
||||
@ -231,9 +211,11 @@ export function performWatchCompilation(host: PerformWatchHost):
|
||||
const exitCode = exitCodeFromResult(compileResult.diagnostics);
|
||||
if (exitCode == 0) {
|
||||
cachedProgram = compileResult.program;
|
||||
host.reportDiagnostics([ChangeDiagnostics.Compilation_complete_Watching_for_file_changes]);
|
||||
host.reportDiagnostics(
|
||||
[createMessageDiagnostic('Compilation complete. Watching for file changes.')]);
|
||||
} else {
|
||||
host.reportDiagnostics([ChangeDiagnostics.Compilation_failed_Watching_for_file_changes]);
|
||||
host.reportDiagnostics(
|
||||
[createMessageDiagnostic('Compilation failed. Watching for file changes.')]);
|
||||
}
|
||||
|
||||
return compileResult.diagnostics;
|
||||
@ -285,7 +267,7 @@ export function performWatchCompilation(host: PerformWatchHost):
|
||||
function recompile() {
|
||||
timerHandleForRecompilation = undefined;
|
||||
host.reportDiagnostics(
|
||||
[ChangeDiagnostics.File_change_detected_Starting_incremental_compilation]);
|
||||
[createMessageDiagnostic('File change detected. Starting incremental compilation.')]);
|
||||
doCompilation();
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ParseSourceSpan} from '@angular/compiler';
|
||||
import {GeneratedFile, ParseSourceSpan} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export const DEFAULT_ERROR_CODE = 100;
|
||||
@ -144,6 +144,12 @@ export interface CompilerOptions extends ts.CompilerOptions {
|
||||
|
||||
/** generate all possible generated files */
|
||||
allowEmptyCodegenFiles?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to generate .ngsummary.ts files that allow to use AOTed artifacts
|
||||
* in JIT mode. This is off by default.
|
||||
*/
|
||||
enableSummariesForJit?: boolean;
|
||||
}
|
||||
|
||||
export interface CompilerHost extends ts.CompilerHost {
|
||||
@ -214,6 +220,9 @@ export interface TsEmitArguments {
|
||||
|
||||
export interface TsEmitCallback { (args: TsEmitArguments): ts.EmitResult; }
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface LibrarySummary {
|
||||
fileName: string;
|
||||
text: string;
|
||||
@ -300,6 +309,13 @@ export interface Program {
|
||||
* 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.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
getLibrarySummaries(): LibrarySummary[];
|
||||
getLibrarySummaries(): Map<string, LibrarySummary>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getEmittedGeneratedFiles(): Map<string, GeneratedFile>;
|
||||
}
|
||||
|
@ -58,7 +58,6 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
|
||||
private generatedSourceFiles = new Map<string, GenSourceFile>();
|
||||
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;
|
||||
@ -68,9 +67,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
|
||||
constructor(
|
||||
private rootFiles: string[], options: CompilerOptions, context: CompilerHost,
|
||||
private metadataProvider: MetadataProvider, private codeGenerator: CodeGenerator,
|
||||
librarySummaries: LibrarySummary[]) {
|
||||
private librarySummaries = new Map<string, 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 !;
|
||||
|
@ -10,6 +10,7 @@ import {GeneratedFile} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {TypeScriptNodeEmitter} from './node_emitter';
|
||||
import {GENERATED_FILES} from './util';
|
||||
|
||||
const PREAMBLE = `/**
|
||||
* @fileoverview This file is generated by the Angular template compiler.
|
||||
@ -18,17 +19,17 @@ const PREAMBLE = `/**
|
||||
* tslint:disable
|
||||
*/`;
|
||||
|
||||
export function getAngularEmitterTransformFactory(generatedFiles: GeneratedFile[]): () =>
|
||||
export function getAngularEmitterTransformFactory(generatedFiles: Map<string, GeneratedFile>): () =>
|
||||
(sourceFile: ts.SourceFile) => ts.SourceFile {
|
||||
return function() {
|
||||
const map = new Map(generatedFiles.filter(g => g.stmts && g.stmts.length)
|
||||
.map<[string, GeneratedFile]>(g => [g.genFileUrl, g]));
|
||||
const emitter = new TypeScriptNodeEmitter();
|
||||
return function(sourceFile: ts.SourceFile): ts.SourceFile {
|
||||
const g = map.get(sourceFile.fileName);
|
||||
const g = generatedFiles.get(sourceFile.fileName);
|
||||
if (g && g.stmts) {
|
||||
const [newSourceFile] = emitter.updateSourceFile(sourceFile, g.stmts, PREAMBLE);
|
||||
return newSourceFile;
|
||||
} else if (GENERATED_FILES.test(sourceFile.fileName)) {
|
||||
return ts.updateSourceFileNode(sourceFile, []);
|
||||
}
|
||||
return sourceFile;
|
||||
};
|
||||
|
@ -18,7 +18,13 @@ import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, D
|
||||
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';
|
||||
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, tsStructureIsReused} from './util';
|
||||
|
||||
/**
|
||||
* Maximum number of files that are emitable via calling ts.Program.emit
|
||||
* passing individual targetSourceFiles.
|
||||
*/
|
||||
const MAX_FILE_COUNT_FOR_SINGLE_FILE_EMIT = 20;
|
||||
|
||||
const emptyModules: NgAnalyzedModules = {
|
||||
ngModules: [],
|
||||
@ -32,18 +38,20 @@ const defaultEmitCallback: TsEmitCallback =
|
||||
program.emit(
|
||||
targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
|
||||
|
||||
|
||||
class AngularCompilerProgram implements Program {
|
||||
private metadataCache: LowerMetadataCache;
|
||||
private oldProgramLibrarySummaries: LibrarySummary[] = [];
|
||||
private oldProgramLibrarySummaries: Map<string, LibrarySummary>|undefined;
|
||||
private oldProgramEmittedGeneratedFiles: Map<string, GeneratedFile>|undefined;
|
||||
// Note: This will be cleared out as soon as we create the _tsProgram
|
||||
private oldTsProgram: ts.Program|undefined;
|
||||
private emittedLibrarySummaries: LibrarySummary[]|undefined;
|
||||
private emittedGeneratedFiles: GeneratedFile[]|undefined;
|
||||
|
||||
// Lazily initialized fields
|
||||
private _typeCheckHost: TypeCheckHost;
|
||||
private _compiler: AotCompiler;
|
||||
private _tsProgram: ts.Program;
|
||||
private _changedNonGenFileNames: string[]|undefined;
|
||||
private _analyzedModules: NgAnalyzedModules|undefined;
|
||||
private _structuralDiagnostics: Diagnostic[]|undefined;
|
||||
private _programWithStubs: ts.Program|undefined;
|
||||
@ -60,6 +68,7 @@ class AngularCompilerProgram implements Program {
|
||||
this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
|
||||
if (oldProgram) {
|
||||
this.oldProgramLibrarySummaries = oldProgram.getLibrarySummaries();
|
||||
this.oldProgramEmittedGeneratedFiles = oldProgram.getEmittedGeneratedFiles();
|
||||
}
|
||||
|
||||
if (options.flatModuleOutFile) {
|
||||
@ -81,10 +90,26 @@ class AngularCompilerProgram implements Program {
|
||||
this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit);
|
||||
}
|
||||
|
||||
getLibrarySummaries(): LibrarySummary[] {
|
||||
const result = [...this.oldProgramLibrarySummaries];
|
||||
getLibrarySummaries(): Map<string, LibrarySummary> {
|
||||
const result = new Map<string, LibrarySummary>();
|
||||
if (this.oldProgramLibrarySummaries) {
|
||||
this.oldProgramLibrarySummaries.forEach((summary, fileName) => result.set(fileName, summary));
|
||||
}
|
||||
if (this.emittedLibrarySummaries) {
|
||||
result.push(...this.emittedLibrarySummaries);
|
||||
this.emittedLibrarySummaries.forEach(
|
||||
(summary, fileName) => result.set(summary.fileName, summary));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getEmittedGeneratedFiles(): Map<string, GeneratedFile> {
|
||||
const result = new Map<string, GeneratedFile>();
|
||||
if (this.oldProgramEmittedGeneratedFiles) {
|
||||
this.oldProgramEmittedGeneratedFiles.forEach(
|
||||
(genFile, fileName) => result.set(fileName, genFile));
|
||||
}
|
||||
if (this.emittedGeneratedFiles) {
|
||||
this.emittedGeneratedFiles.forEach((genFile) => result.set(genFile.genFileUrl, genFile));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -142,6 +167,7 @@ class AngularCompilerProgram implements Program {
|
||||
customTransformers?: CustomTransformers,
|
||||
emitCallback?: TsEmitCallback
|
||||
} = {}): ts.EmitResult {
|
||||
const emitStart = Date.now();
|
||||
if (emitFlags & EmitFlags.I18nBundle) {
|
||||
const locale = this.options.i18nOutLocale || null;
|
||||
const file = this.options.i18nOutFile || null;
|
||||
@ -153,7 +179,7 @@ class AngularCompilerProgram implements Program {
|
||||
0) {
|
||||
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
|
||||
}
|
||||
const {genFiles, genDiags} = this.generateFilesForEmit(emitFlags);
|
||||
let {genFiles, genDiags} = this.generateFilesForEmit(emitFlags);
|
||||
if (genDiags.length) {
|
||||
return {
|
||||
diagnostics: genDiags,
|
||||
@ -161,11 +187,11 @@ class AngularCompilerProgram implements Program {
|
||||
emittedFiles: [],
|
||||
};
|
||||
}
|
||||
const emittedLibrarySummaries = this.emittedLibrarySummaries = [];
|
||||
|
||||
this.emittedGeneratedFiles = genFiles;
|
||||
const outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}> = [];
|
||||
const genFileByFileName = new Map<string, GeneratedFile>();
|
||||
genFiles.forEach(genFile => genFileByFileName.set(genFile.genFileUrl, genFile));
|
||||
this.emittedLibrarySummaries = [];
|
||||
const writeTsFile: ts.WriteFileCallback =
|
||||
(outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => {
|
||||
const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null;
|
||||
@ -176,7 +202,8 @@ class AngularCompilerProgram implements Program {
|
||||
}
|
||||
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles);
|
||||
};
|
||||
|
||||
const tsCustomTansformers = this.calculateTransforms(genFileByFileName, customTransformers);
|
||||
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
|
||||
// Restore the original references before we emit so TypeScript doesn't emit
|
||||
// a reference to the .d.ts file.
|
||||
const augmentedReferences = new Map<ts.SourceFile, ts.FileReference[]>();
|
||||
@ -187,16 +214,44 @@ class AngularCompilerProgram implements Program {
|
||||
sourceFile.referencedFiles = originalReferences;
|
||||
}
|
||||
}
|
||||
const genTsFiles: GeneratedFile[] = [];
|
||||
const genJsonFiles: GeneratedFile[] = [];
|
||||
genFiles.forEach(gf => {
|
||||
if (gf.stmts) {
|
||||
genTsFiles.push(gf);
|
||||
}
|
||||
if (gf.source) {
|
||||
genJsonFiles.push(gf);
|
||||
}
|
||||
});
|
||||
let emitResult: ts.EmitResult;
|
||||
let emittedUserTsCount: number;
|
||||
try {
|
||||
emitResult = emitCallback({
|
||||
program: this.tsProgram,
|
||||
host: this.host,
|
||||
options: this.options,
|
||||
writeFile: writeTsFile,
|
||||
emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS,
|
||||
customTransformers: this.calculateTransforms(genFiles, customTransformers)
|
||||
});
|
||||
const emitChangedFilesOnly = this._changedNonGenFileNames &&
|
||||
this._changedNonGenFileNames.length < MAX_FILE_COUNT_FOR_SINGLE_FILE_EMIT;
|
||||
if (emitChangedFilesOnly) {
|
||||
const fileNamesToEmit =
|
||||
[...this._changedNonGenFileNames !, ...genTsFiles.map(gf => gf.genFileUrl)];
|
||||
emitResult = mergeEmitResults(
|
||||
fileNamesToEmit.map((fileName) => emitResult = emitCallback({
|
||||
program: this.tsProgram,
|
||||
host: this.host,
|
||||
options: this.options,
|
||||
writeFile: writeTsFile, emitOnlyDtsFiles,
|
||||
customTransformers: tsCustomTansformers,
|
||||
targetSourceFile: this.tsProgram.getSourceFile(fileName),
|
||||
})));
|
||||
emittedUserTsCount = this._changedNonGenFileNames !.length;
|
||||
} else {
|
||||
emitResult = emitCallback({
|
||||
program: this.tsProgram,
|
||||
host: this.host,
|
||||
options: this.options,
|
||||
writeFile: writeTsFile, emitOnlyDtsFiles,
|
||||
customTransformers: tsCustomTansformers
|
||||
});
|
||||
emittedUserTsCount = this.tsProgram.getSourceFiles().length - genTsFiles.length;
|
||||
}
|
||||
} finally {
|
||||
// Restore the references back to the augmented value to ensure that the
|
||||
// checks that TypeScript makes for project structure reuse will succeed.
|
||||
@ -207,10 +262,10 @@ class AngularCompilerProgram implements Program {
|
||||
|
||||
if (!outSrcMapping.length) {
|
||||
// if no files were emitted by TypeScript, also don't emit .json files
|
||||
emitResult.diagnostics.push(createMessageDiagnostic(`Emitted no files.`));
|
||||
return emitResult;
|
||||
}
|
||||
|
||||
|
||||
let sampleSrcFileName: string|undefined;
|
||||
let sampleOutFileName: string|undefined;
|
||||
if (outSrcMapping.length) {
|
||||
@ -220,16 +275,16 @@ class AngularCompilerProgram implements Program {
|
||||
const srcToOutPath =
|
||||
createSrcToOutPathMapper(this.options.outDir, sampleSrcFileName, sampleOutFileName);
|
||||
if (emitFlags & EmitFlags.Codegen) {
|
||||
genFiles.forEach(gf => {
|
||||
if (gf.source) {
|
||||
const outFileName = srcToOutPath(gf.genFileUrl);
|
||||
this.writeFile(outFileName, gf.source, false, undefined, gf);
|
||||
}
|
||||
genJsonFiles.forEach(gf => {
|
||||
const outFileName = srcToOutPath(gf.genFileUrl);
|
||||
this.writeFile(outFileName, gf.source !, false, undefined, gf);
|
||||
});
|
||||
}
|
||||
let metadataJsonCount = 0;
|
||||
if (emitFlags & EmitFlags.Metadata) {
|
||||
this.tsProgram.getSourceFiles().forEach(sf => {
|
||||
if (!sf.isDeclarationFile && !GENERATED_FILES.test(sf.fileName)) {
|
||||
metadataJsonCount++;
|
||||
const metadata = this.metadataCache.getMetadata(sf);
|
||||
const metadataText = JSON.stringify([metadata]);
|
||||
const outFileName = srcToOutPath(sf.fileName.replace(/\.ts$/, '.metadata.json'));
|
||||
@ -237,6 +292,15 @@ class AngularCompilerProgram implements Program {
|
||||
}
|
||||
});
|
||||
}
|
||||
const emitEnd = Date.now();
|
||||
if (this.options.diagnostics) {
|
||||
emitResult.diagnostics.push(createMessageDiagnostic([
|
||||
`Emitted in ${emitEnd - emitStart}ms`,
|
||||
`- ${emittedUserTsCount} user ts files`,
|
||||
`- ${genTsFiles.length} generated ts files`,
|
||||
`- ${genJsonFiles.length + metadataJsonCount} generated json files`,
|
||||
].join('\n')));
|
||||
}
|
||||
return emitResult;
|
||||
}
|
||||
|
||||
@ -281,8 +345,9 @@ class AngularCompilerProgram implements Program {
|
||||
(this._semanticDiagnostics = this.generateSemanticDiagnostics());
|
||||
}
|
||||
|
||||
private calculateTransforms(genFiles: GeneratedFile[], customTransformers?: CustomTransformers):
|
||||
ts.CustomTransformers {
|
||||
private calculateTransforms(
|
||||
genFiles: Map<string, GeneratedFile>,
|
||||
customTransformers?: CustomTransformers): ts.CustomTransformers {
|
||||
const beforeTs: ts.TransformerFactory<ts.SourceFile>[] = [];
|
||||
if (!this.options.disableExpressionLowering) {
|
||||
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache));
|
||||
@ -353,6 +418,16 @@ class AngularCompilerProgram implements Program {
|
||||
sourceFiles.push(sf.fileName);
|
||||
}
|
||||
});
|
||||
if (oldTsProgram) {
|
||||
// TODO(tbosch): if one of the files contains a `const enum`
|
||||
// always emit all files!
|
||||
const changedNonGenFileNames = this._changedNonGenFileNames = [] as string[];
|
||||
tmpProgram.getSourceFiles().forEach(sf => {
|
||||
if (!GENERATED_FILES.test(sf.fileName) && oldTsProgram.getSourceFile(sf.fileName) !== sf) {
|
||||
changedNonGenFileNames.push(sf.fileName);
|
||||
}
|
||||
});
|
||||
}
|
||||
return {tmpProgram, sourceFiles, hostAdapter, rootNames};
|
||||
}
|
||||
|
||||
@ -418,7 +493,14 @@ class AngularCompilerProgram implements Program {
|
||||
if (!(emitFlags & EmitFlags.Codegen)) {
|
||||
return {genFiles: [], genDiags: []};
|
||||
}
|
||||
const genFiles = this.compiler.emitAllImpls(this.analyzedModules);
|
||||
let genFiles = this.compiler.emitAllImpls(this.analyzedModules);
|
||||
if (this.oldProgramEmittedGeneratedFiles) {
|
||||
const oldProgramEmittedGeneratedFiles = this.oldProgramEmittedGeneratedFiles;
|
||||
genFiles = genFiles.filter(genFile => {
|
||||
const oldGenFile = oldProgramEmittedGeneratedFiles.get(genFile.genFileUrl);
|
||||
return !oldGenFile || !genFile.isEquivalent(oldGenFile);
|
||||
});
|
||||
}
|
||||
return {genFiles, genDiags: []};
|
||||
} catch (e) {
|
||||
// TODO(tbosch): check whether we can actually have syntax errors here,
|
||||
@ -533,7 +615,7 @@ function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions {
|
||||
locale: options.i18nInLocale,
|
||||
i18nFormat: options.i18nInFormat || options.i18nOutFormat, translations, missingTranslation,
|
||||
enableLegacyTemplate: options.enableLegacyTemplate,
|
||||
enableSummariesForJit: true,
|
||||
enableSummariesForJit: options.enableSummariesForJit,
|
||||
preserveWhitespaces: options.preserveWhitespaces,
|
||||
fullTemplateTypeCheck: options.fullTemplateTypeCheck,
|
||||
allowEmptyCodegenFiles: options.allowEmptyCodegenFiles,
|
||||
@ -649,3 +731,15 @@ export function i18nGetExtension(formatName: string): string {
|
||||
|
||||
throw new Error(`Unsupported format "${formatName}"`);
|
||||
}
|
||||
|
||||
function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
|
||||
const diagnostics: ts.Diagnostic[] = [];
|
||||
let emitSkipped = true;
|
||||
const emittedFiles: string[] = [];
|
||||
for (const er of emitResults) {
|
||||
diagnostics.push(...er.diagnostics);
|
||||
emitSkipped = emitSkipped || er.emitSkipped;
|
||||
emittedFiles.push(...er.emittedFiles);
|
||||
}
|
||||
return {diagnostics, emitSkipped, emittedFiles};
|
||||
}
|
||||
|
@ -8,6 +8,8 @@
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from './api';
|
||||
|
||||
export const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/;
|
||||
|
||||
export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2}
|
||||
@ -15,4 +17,15 @@ export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2}
|
||||
// Note: This is an internal property in TypeScript. Use it only for assertions and tests.
|
||||
export function tsStructureIsReused(program: ts.Program): StructureIsReused {
|
||||
return (program as any).structureIsReused;
|
||||
}
|
||||
}
|
||||
|
||||
export function createMessageDiagnostic(messageText: string): ts.Diagnostic&Diagnostic {
|
||||
return {
|
||||
file: undefined,
|
||||
start: undefined,
|
||||
length: undefined,
|
||||
category: ts.DiagnosticCategory.Message, messageText,
|
||||
code: DEFAULT_ERROR_CODE,
|
||||
source: SOURCE,
|
||||
};
|
||||
}
|
||||
|
@ -336,18 +336,21 @@ describe('ngc transformer command-line', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function expectAllGeneratedFilesToExist() {
|
||||
function expectAllGeneratedFilesToExist(enableSummariesForJit = true) {
|
||||
modules.forEach(moduleName => {
|
||||
if (/module|comp/.test(moduleName)) {
|
||||
shouldExist(moduleName + '.ngfactory.js');
|
||||
shouldExist(moduleName + '.ngfactory.d.ts');
|
||||
shouldExist(moduleName + '.ngsummary.js');
|
||||
shouldExist(moduleName + '.ngsummary.d.ts');
|
||||
} else {
|
||||
shouldNotExist(moduleName + '.ngfactory.js');
|
||||
shouldNotExist(moduleName + '.ngfactory.d.ts');
|
||||
}
|
||||
if (enableSummariesForJit) {
|
||||
shouldExist(moduleName + '.ngsummary.js');
|
||||
shouldExist(moduleName + '.ngsummary.d.ts');
|
||||
} else {
|
||||
shouldNotExist(moduleName + '.ngsummary.js');
|
||||
shouldNotExist(moduleName + '.ngsummary.d.ts');
|
||||
}
|
||||
shouldExist(moduleName + '.ngsummary.json');
|
||||
shouldNotExist(moduleName + '.ngfactory.metadata.json');
|
||||
@ -359,10 +362,11 @@ describe('ngc transformer command-line', () => {
|
||||
shouldExist('emulated.css.shim.ngstyle.d.ts');
|
||||
}
|
||||
|
||||
it('should emit generated files from sources', () => {
|
||||
it('should emit generated files from sources with summariesForJit', () => {
|
||||
writeConfig(`{
|
||||
"extends": "./tsconfig-base.json",
|
||||
"angularCompilerOptions": {
|
||||
"enableSummariesForJit": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}`);
|
||||
@ -370,7 +374,22 @@ describe('ngc transformer command-line', () => {
|
||||
expect(exitCode).toEqual(0);
|
||||
outDir = path.resolve(basePath, 'built', 'src');
|
||||
expectJsDtsMetadataJsonToExist();
|
||||
expectAllGeneratedFilesToExist();
|
||||
expectAllGeneratedFilesToExist(true);
|
||||
});
|
||||
|
||||
it('should not emit generated files from sources without summariesForJit', () => {
|
||||
writeConfig(`{
|
||||
"extends": "./tsconfig-base.json",
|
||||
"angularCompilerOptions": {
|
||||
"enableSummariesForJit": false
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}`);
|
||||
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')], errorSpy);
|
||||
expect(exitCode).toEqual(0);
|
||||
outDir = path.resolve(basePath, 'built', 'src');
|
||||
expectJsDtsMetadataJsonToExist();
|
||||
expectAllGeneratedFilesToExist(false);
|
||||
});
|
||||
|
||||
it('should emit generated files from libraries', () => {
|
||||
@ -408,7 +427,8 @@ describe('ngc transformer command-line', () => {
|
||||
writeConfig(`{
|
||||
"extends": "./tsconfig-base.json",
|
||||
"angularCompilerOptions": {
|
||||
"skipTemplateCodegen": false
|
||||
"skipTemplateCodegen": false,
|
||||
"enableSummariesForJit": true
|
||||
},
|
||||
"compilerOptions": {
|
||||
"outDir": "built"
|
||||
@ -880,7 +900,8 @@ describe('ngc transformer command-line', () => {
|
||||
write('tsconfig-ng.json', `{
|
||||
"extends": "./tsconfig-base.json",
|
||||
"angularCompilerOptions": {
|
||||
"generateCodeForLibraries": true
|
||||
"generateCodeForLibraries": true,
|
||||
"enableSummariesForJit": true
|
||||
},
|
||||
"compilerOptions": {
|
||||
"outDir": "."
|
||||
@ -896,7 +917,8 @@ describe('ngc transformer command-line', () => {
|
||||
write('lib1/tsconfig-lib1.json', `{
|
||||
"extends": "../tsconfig-base.json",
|
||||
"angularCompilerOptions": {
|
||||
"generateCodeForLibraries": false
|
||||
"generateCodeForLibraries": false,
|
||||
"enableSummariesForJit": true
|
||||
},
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
@ -919,7 +941,8 @@ describe('ngc transformer command-line', () => {
|
||||
write('lib2/tsconfig-lib2.json', `{
|
||||
"extends": "../tsconfig-base.json",
|
||||
"angularCompilerOptions": {
|
||||
"generateCodeForLibraries": false
|
||||
"generateCodeForLibraries": false,
|
||||
"enableSummariesForJit": true
|
||||
},
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
@ -941,7 +964,8 @@ describe('ngc transformer command-line', () => {
|
||||
write('app/tsconfig-app.json', `{
|
||||
"extends": "../tsconfig-base.json",
|
||||
"angularCompilerOptions": {
|
||||
"generateCodeForLibraries": false
|
||||
"generateCodeForLibraries": false,
|
||||
"enableSummariesForJit": true
|
||||
},
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
|
@ -86,10 +86,9 @@ describe('perform watch', () => {
|
||||
|
||||
// trigger a single file change
|
||||
// -> all other files should be cached
|
||||
fs.unlinkSync(mainNgFactory);
|
||||
host.triggerFileChange(FileChangeEvent.Change, utilTsPath);
|
||||
expectNoDiagnostics(config.options, host.diagnostics);
|
||||
|
||||
expect(fs.existsSync(mainNgFactory)).toBe(true);
|
||||
expect(fileExistsSpy !).not.toHaveBeenCalledWith(mainTsPath);
|
||||
expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath);
|
||||
expect(getSourceFileSpy !).not.toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
||||
@ -97,11 +96,10 @@ describe('perform watch', () => {
|
||||
|
||||
// trigger a folder change
|
||||
// -> nothing should be cached
|
||||
fs.unlinkSync(mainNgFactory);
|
||||
host.triggerFileChange(
|
||||
FileChangeEvent.CreateDeleteDir, path.resolve(testSupport.basePath, 'src'));
|
||||
expectNoDiagnostics(config.options, host.diagnostics);
|
||||
|
||||
expect(fs.existsSync(mainNgFactory)).toBe(true);
|
||||
expect(fileExistsSpy !).toHaveBeenCalledWith(mainTsPath);
|
||||
expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath);
|
||||
expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
||||
|
@ -110,8 +110,9 @@ export function setup(): TestSupport {
|
||||
}
|
||||
|
||||
export function expectNoDiagnostics(options: ng.CompilerOptions, diags: ng.Diagnostics) {
|
||||
if (diags.length) {
|
||||
throw new Error(`Expected no diagnostics: ${ng.formatDiagnostics(options, diags)}`);
|
||||
const errorDiags = diags.filter(d => d.category !== ts.DiagnosticCategory.Message);
|
||||
if (errorDiags.length) {
|
||||
throw new Error(`Expected no diagnostics: ${ng.formatDiagnostics(options, errorDiags)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ describe('NgCompilerHost', () => {
|
||||
} = {}) {
|
||||
return new TsCompilerAotCompilerTypeCheckHostAdapter(
|
||||
['/tmp/index.ts'], options, ngHost, new MetadataCollector(), codeGenerator,
|
||||
librarySummaries);
|
||||
new Map(librarySummaries.map(entry => [entry.fileName, entry] as[string, LibrarySummary])));
|
||||
}
|
||||
|
||||
describe('fileNameToModuleName', () => {
|
||||
|
@ -57,17 +57,20 @@ describe('ng program', () => {
|
||||
}
|
||||
|
||||
function compile(
|
||||
oldProgram?: ng.Program, overrideOptions?: ng.CompilerOptions,
|
||||
rootNames?: string[]): ng.Program {
|
||||
oldProgram?: ng.Program, overrideOptions?: ng.CompilerOptions, rootNames?: string[],
|
||||
host?: CompilerHost): ng.Program {
|
||||
const options = testSupport.createCompilerOptions(overrideOptions);
|
||||
if (!rootNames) {
|
||||
rootNames = [path.resolve(testSupport.basePath, 'src/index.ts')];
|
||||
}
|
||||
|
||||
if (!host) {
|
||||
host = ng.createCompilerHost({options});
|
||||
}
|
||||
const program = ng.createProgram({
|
||||
rootNames: rootNames,
|
||||
options,
|
||||
host: ng.createCompilerHost({options}), oldProgram,
|
||||
host,
|
||||
oldProgram,
|
||||
});
|
||||
expectNoDiagnosticsInProgram(options, program);
|
||||
program.emit();
|
||||
@ -153,6 +156,59 @@ describe('ng program', () => {
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it('should only emit changed files', () => {
|
||||
testSupport.writeFiles({
|
||||
'src/index.ts': createModuleAndCompSource('comp', 'index.html'),
|
||||
'src/index.html': `Start`
|
||||
});
|
||||
const options: ng.CompilerOptions = {declaration: false};
|
||||
const host = ng.createCompilerHost({options});
|
||||
const originalGetSourceFile = host.getSourceFile;
|
||||
const fileCache = new Map<string, ts.SourceFile>();
|
||||
host.getSourceFile = (fileName: string) => {
|
||||
if (fileCache.has(fileName)) {
|
||||
return fileCache.get(fileName);
|
||||
}
|
||||
const sf = originalGetSourceFile.call(host, fileName);
|
||||
fileCache.set(fileName, sf);
|
||||
return sf;
|
||||
};
|
||||
|
||||
const written = new Map<string, string>();
|
||||
host.writeFile = (fileName: string, data: string) => written.set(fileName, data);
|
||||
|
||||
// compile libraries
|
||||
const p1 = compile(undefined, options, undefined, host);
|
||||
|
||||
// first compile without libraries
|
||||
const p2 = compile(p1, options, undefined, host);
|
||||
expect(written.has(path.resolve(testSupport.basePath, 'built/src/index.js'))).toBe(true);
|
||||
let ngFactoryContent =
|
||||
written.get(path.resolve(testSupport.basePath, 'built/src/index.ngfactory.js'));
|
||||
expect(ngFactoryContent).toMatch(/Start/);
|
||||
|
||||
// no change -> no emit
|
||||
written.clear();
|
||||
const p3 = compile(p2, options, undefined, host);
|
||||
expect(written.size).toBe(0);
|
||||
|
||||
// change a user file
|
||||
written.clear();
|
||||
fileCache.delete(path.resolve(testSupport.basePath, 'src/index.ts'));
|
||||
const p4 = compile(p3, options, undefined, host);
|
||||
expect(written.size).toBe(1);
|
||||
expect(written.has(path.resolve(testSupport.basePath, 'built/src/index.js'))).toBe(true);
|
||||
|
||||
// change a file that is input to generated files
|
||||
written.clear();
|
||||
testSupport.writeFiles({'src/index.html': 'Hello'});
|
||||
const p5 = compile(p4, options, undefined, host);
|
||||
expect(written.size).toBe(1);
|
||||
ngFactoryContent =
|
||||
written.get(path.resolve(testSupport.basePath, 'built/src/index.ngfactory.js'));
|
||||
expect(ngFactoryContent).toMatch(/Hello/);
|
||||
});
|
||||
|
||||
it('should store library summaries on emit', () => {
|
||||
compileLib('lib');
|
||||
testSupport.writeFiles({
|
||||
@ -163,17 +219,19 @@ describe('ng program', () => {
|
||||
`
|
||||
});
|
||||
const p1 = compile();
|
||||
expect(p1.getLibrarySummaries().some(
|
||||
sf => /node_modules\/lib\/index\.ngfactory\.d\.ts$/.test(sf.fileName)))
|
||||
expect(Array.from(p1.getLibrarySummaries().values())
|
||||
.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)))
|
||||
expect(Array.from(p1.getLibrarySummaries().values())
|
||||
.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)))
|
||||
expect(Array.from(p1.getLibrarySummaries().values())
|
||||
.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);
|
||||
expect(Array.from(p1.getLibrarySummaries().values())
|
||||
.some(sf => /src\/main.*$/.test(sf.fileName)))
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it('should reuse the old ts program completely if nothing changed', () => {
|
||||
@ -300,7 +358,10 @@ describe('ng program', () => {
|
||||
export * from './main';
|
||||
`,
|
||||
});
|
||||
const options = testSupport.createCompilerOptions({allowEmptyCodegenFiles: true});
|
||||
const options = testSupport.createCompilerOptions({
|
||||
allowEmptyCodegenFiles: true,
|
||||
enableSummariesForJit: true,
|
||||
});
|
||||
const host = ng.createCompilerHost({options});
|
||||
const written = new Map < string, {
|
||||
original: ts.SourceFile[]|undefined;
|
||||
@ -384,11 +445,11 @@ describe('ng program', () => {
|
||||
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');
|
||||
testSupport.shouldNotExist('built/node_modules/lib/index.js');
|
||||
testSupport.shouldNotExist('built/node_modules/lib/index.d.ts');
|
||||
testSupport.shouldNotExist('built/node_modules/lib/index.ngfactory.js');
|
||||
testSupport.shouldNotExist('built/node_modules/lib/index.ngfactory.d.ts');
|
||||
testSupport.shouldNotExist('built/node_modules/lib/index.ngsummary.json');
|
||||
});
|
||||
|
||||
describe('createSrcToOutPathMapper', () => {
|
||||
|
@ -98,7 +98,9 @@ export class AotCompiler {
|
||||
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));
|
||||
if (this._options.enableSummariesForJit) {
|
||||
genFileNames.push(summaryForJitFileName(file.fileName, true));
|
||||
}
|
||||
}
|
||||
const fileSuffix = splitTypescriptSuffix(file.fileName, true)[1];
|
||||
file.directives.forEach((dirSymbol) => {
|
||||
@ -376,7 +378,9 @@ export class AotCompiler {
|
||||
metadata: this._metadataResolver.getInjectableSummary(ref) !.type
|
||||
}))
|
||||
];
|
||||
const forJitOutputCtx = this._createOutputContext(summaryForJitFileName(srcFileName, true));
|
||||
const forJitOutputCtx = this._options.enableSummariesForJit ?
|
||||
this._createOutputContext(summaryForJitFileName(srcFileName, true)) :
|
||||
null;
|
||||
const {json, exportAs} = serializeSummaries(
|
||||
srcFileName, forJitOutputCtx, this._summaryResolver, this._symbolResolver, symbolSummaries,
|
||||
typeData);
|
||||
@ -387,11 +391,11 @@ export class AotCompiler {
|
||||
]));
|
||||
});
|
||||
const summaryJson = new GeneratedFile(srcFileName, summaryFileName(srcFileName), json);
|
||||
if (this._options.enableSummariesForJit) {
|
||||
return [summaryJson, this._codegenSourceModule(srcFileName, forJitOutputCtx)];
|
||||
const result = [summaryJson];
|
||||
if (forJitOutputCtx) {
|
||||
result.push(this._codegenSourceModule(srcFileName, forJitOutputCtx));
|
||||
}
|
||||
|
||||
return [summaryJson];
|
||||
return result;
|
||||
}
|
||||
|
||||
private _compileModule(outputCtx: OutputContext, ngModule: CompileNgModuleMetadata): void {
|
||||
|
@ -14,7 +14,6 @@ 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;
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {sourceUrl} from '../compile_metadata';
|
||||
import {Statement} from '../output/output_ast';
|
||||
import {Statement, areAllEquivalent} from '../output/output_ast';
|
||||
import {TypeScriptEmitter} from '../output/ts_emitter';
|
||||
|
||||
export class GeneratedFile {
|
||||
@ -24,6 +24,21 @@ export class GeneratedFile {
|
||||
this.stmts = sourceOrStmts;
|
||||
}
|
||||
}
|
||||
|
||||
isEquivalent(other: GeneratedFile): boolean {
|
||||
if (this.genFileUrl !== other.genFileUrl) {
|
||||
return false;
|
||||
}
|
||||
if (this.source) {
|
||||
return this.source === other.source;
|
||||
}
|
||||
if (other.stmts == null) {
|
||||
return false;
|
||||
}
|
||||
// Note: the constructor guarantees that if this.source is not filled,
|
||||
// then this.stmts is.
|
||||
return areAllEquivalent(this.stmts !, other.stmts !);
|
||||
}
|
||||
}
|
||||
|
||||
export function toTypeScript(file: GeneratedFile, preamble: string = ''): string {
|
||||
|
@ -15,14 +15,14 @@ import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolv
|
||||
import {summaryForJitFileName, summaryForJitName} from './util';
|
||||
|
||||
export function serializeSummaries(
|
||||
srcFileName: string, forJitCtx: OutputContext, summaryResolver: SummaryResolver<StaticSymbol>,
|
||||
symbolResolver: StaticSymbolResolver, symbols: ResolvedStaticSymbol[], types: {
|
||||
srcFileName: string, forJitCtx: OutputContext | null,
|
||||
summaryResolver: SummaryResolver<StaticSymbol>, symbolResolver: StaticSymbolResolver,
|
||||
symbols: ResolvedStaticSymbol[], types: {
|
||||
summary: CompileTypeSummary,
|
||||
metadata: CompileNgModuleMetadata | CompileDirectiveMetadata | CompilePipeMetadata |
|
||||
CompileTypeMetadata
|
||||
}[]): {json: string, exportAs: {symbol: StaticSymbol, exportAs: string}[]} {
|
||||
const toJsonSerializer = new ToJsonSerializer(symbolResolver, summaryResolver, srcFileName);
|
||||
const forJitSerializer = new ForJitSerializer(forJitCtx, symbolResolver);
|
||||
|
||||
// for symbols, we use everything except for the class metadata itself
|
||||
// (we keep the statics though), as the class metadata is contained in the
|
||||
@ -33,18 +33,20 @@ export function serializeSummaries(
|
||||
|
||||
// Add type summaries.
|
||||
types.forEach(({summary, metadata}) => {
|
||||
forJitSerializer.addSourceType(summary, metadata);
|
||||
toJsonSerializer.addSummary(
|
||||
{symbol: summary.type.reference, metadata: undefined, type: summary});
|
||||
});
|
||||
toJsonSerializer.unprocessedSymbolSummariesBySymbol.forEach((summary) => {
|
||||
if (summaryResolver.isLibraryFile(summary.symbol.filePath) && summary.type) {
|
||||
forJitSerializer.addLibType(summary.type);
|
||||
}
|
||||
});
|
||||
|
||||
const {json, exportAs} = toJsonSerializer.serialize();
|
||||
forJitSerializer.serialize(exportAs);
|
||||
if (forJitCtx) {
|
||||
const forJitSerializer = new ForJitSerializer(forJitCtx, symbolResolver);
|
||||
types.forEach(({summary, metadata}) => { forJitSerializer.addSourceType(summary, metadata); });
|
||||
toJsonSerializer.unprocessedSymbolSummariesBySymbol.forEach((summary) => {
|
||||
if (summaryResolver.isLibraryFile(summary.symbol.filePath) && summary.type) {
|
||||
forJitSerializer.addLibType(summary.type);
|
||||
}
|
||||
});
|
||||
forJitSerializer.serialize(exportAs);
|
||||
}
|
||||
return {json, exportAs};
|
||||
}
|
||||
|
||||
|
@ -104,6 +104,27 @@ export enum BinaryOperator {
|
||||
BiggerEquals
|
||||
}
|
||||
|
||||
export function nullSafeIsEquivalent<T extends{isEquivalent(other: T): boolean}>(
|
||||
base: T | null, other: T | null) {
|
||||
if (base == null || other == null) {
|
||||
return base == other;
|
||||
}
|
||||
return base.isEquivalent(other);
|
||||
}
|
||||
|
||||
export function areAllEquivalent<T extends{isEquivalent(other: T): boolean}>(
|
||||
base: T[], other: T[]) {
|
||||
const len = base.length;
|
||||
if (len !== other.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (!base[i].isEquivalent(other[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export abstract class Expression {
|
||||
public type: Type|null;
|
||||
@ -116,6 +137,12 @@ export abstract class Expression {
|
||||
|
||||
abstract visitExpression(visitor: ExpressionVisitor, context: any): any;
|
||||
|
||||
/**
|
||||
* Calculates whether this expression produces the same value as the given expression.
|
||||
* Note: We don't check Types nor ParseSourceSpans nor function arguments.
|
||||
*/
|
||||
abstract isEquivalent(e: Expression): boolean;
|
||||
|
||||
prop(name: string, sourceSpan?: ParseSourceSpan|null): ReadPropExpr {
|
||||
return new ReadPropExpr(this, name, null, sourceSpan);
|
||||
}
|
||||
@ -222,6 +249,10 @@ export class ReadVarExpr extends Expression {
|
||||
this.builtin = <BuiltinVar>name;
|
||||
}
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof ReadVarExpr && this.name === e.name && this.builtin === e.builtin;
|
||||
}
|
||||
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitReadVarExpr(this, context);
|
||||
}
|
||||
@ -242,6 +273,9 @@ export class WriteVarExpr extends Expression {
|
||||
super(type || value.type, sourceSpan);
|
||||
this.value = value;
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof WriteVarExpr && this.name === e.name && this.value.isEquivalent(e.value);
|
||||
}
|
||||
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitWriteVarExpr(this, context);
|
||||
@ -261,6 +295,10 @@ export class WriteKeyExpr extends Expression {
|
||||
super(type || value.type, sourceSpan);
|
||||
this.value = value;
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof WriteKeyExpr && this.receiver.isEquivalent(e.receiver) &&
|
||||
this.index.isEquivalent(e.index) && this.value.isEquivalent(e.value);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitWriteKeyExpr(this, context);
|
||||
}
|
||||
@ -275,6 +313,10 @@ export class WritePropExpr extends Expression {
|
||||
super(type || value.type, sourceSpan);
|
||||
this.value = value;
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof WritePropExpr && this.receiver.isEquivalent(e.receiver) &&
|
||||
this.name === e.name && this.value.isEquivalent(e.value);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitWritePropExpr(this, context);
|
||||
}
|
||||
@ -301,6 +343,10 @@ export class InvokeMethodExpr extends Expression {
|
||||
this.builtin = <BuiltinMethod>method;
|
||||
}
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof InvokeMethodExpr && this.receiver.isEquivalent(e.receiver) &&
|
||||
this.name === e.name && this.builtin === e.builtin && areAllEquivalent(this.args, e.args);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitInvokeMethodExpr(this, context);
|
||||
}
|
||||
@ -313,6 +359,10 @@ export class InvokeFunctionExpr extends Expression {
|
||||
sourceSpan?: ParseSourceSpan|null) {
|
||||
super(type, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof InvokeFunctionExpr && this.fn.isEquivalent(e.fn) &&
|
||||
areAllEquivalent(this.args, e.args);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitInvokeFunctionExpr(this, context);
|
||||
}
|
||||
@ -325,6 +375,10 @@ export class InstantiateExpr extends Expression {
|
||||
sourceSpan?: ParseSourceSpan|null) {
|
||||
super(type, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof InstantiateExpr && this.classExpr.isEquivalent(e.classExpr) &&
|
||||
areAllEquivalent(this.args, e.args);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitInstantiateExpr(this, context);
|
||||
}
|
||||
@ -332,9 +386,14 @@ export class InstantiateExpr extends Expression {
|
||||
|
||||
|
||||
export class LiteralExpr extends Expression {
|
||||
constructor(public value: any, type?: Type|null, sourceSpan?: ParseSourceSpan|null) {
|
||||
constructor(
|
||||
public value: number|string|boolean|null|undefined, type?: Type|null,
|
||||
sourceSpan?: ParseSourceSpan|null) {
|
||||
super(type, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof LiteralExpr && this.value === e.value;
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitLiteralExpr(this, context);
|
||||
}
|
||||
@ -347,6 +406,10 @@ export class ExternalExpr extends Expression {
|
||||
sourceSpan?: ParseSourceSpan|null) {
|
||||
super(type, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof ExternalExpr && this.value.name === e.value.name &&
|
||||
this.value.moduleName === e.value.moduleName && this.value.runtime === e.value.runtime;
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitExternalExpr(this, context);
|
||||
}
|
||||
@ -355,6 +418,7 @@ export class ExternalExpr extends Expression {
|
||||
export class ExternalReference {
|
||||
constructor(public moduleName: string|null, public name: string|null, public runtime?: any|null) {
|
||||
}
|
||||
// Note: no isEquivalent method here as we use this as an interface too.
|
||||
}
|
||||
|
||||
export class ConditionalExpr extends Expression {
|
||||
@ -365,6 +429,10 @@ export class ConditionalExpr extends Expression {
|
||||
super(type || trueCase.type, sourceSpan);
|
||||
this.trueCase = trueCase;
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof ConditionalExpr && this.condition.isEquivalent(e.condition) &&
|
||||
this.trueCase.isEquivalent(e.trueCase) && nullSafeIsEquivalent(this.falseCase, e.falseCase);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitConditionalExpr(this, context);
|
||||
}
|
||||
@ -375,6 +443,9 @@ export class NotExpr extends Expression {
|
||||
constructor(public condition: Expression, sourceSpan?: ParseSourceSpan|null) {
|
||||
super(BOOL_TYPE, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof NotExpr && this.condition.isEquivalent(e.condition);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitNotExpr(this, context);
|
||||
}
|
||||
@ -384,6 +455,9 @@ export class AssertNotNull extends Expression {
|
||||
constructor(public condition: Expression, sourceSpan?: ParseSourceSpan|null) {
|
||||
super(condition.type, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof AssertNotNull && this.condition.isEquivalent(e.condition);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitAssertNotNullExpr(this, context);
|
||||
}
|
||||
@ -393,6 +467,9 @@ export class CastExpr extends Expression {
|
||||
constructor(public value: Expression, type?: Type|null, sourceSpan?: ParseSourceSpan|null) {
|
||||
super(type, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof CastExpr && this.value.isEquivalent(e.value);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitCastExpr(this, context);
|
||||
}
|
||||
@ -401,6 +478,8 @@ export class CastExpr extends Expression {
|
||||
|
||||
export class FnParam {
|
||||
constructor(public name: string, public type: Type|null = null) {}
|
||||
|
||||
isEquivalent(param: FnParam): boolean { return this.name === param.name; }
|
||||
}
|
||||
|
||||
|
||||
@ -410,6 +489,10 @@ export class FunctionExpr extends Expression {
|
||||
sourceSpan?: ParseSourceSpan|null) {
|
||||
super(type, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof FunctionExpr && areAllEquivalent(this.params, e.params) &&
|
||||
areAllEquivalent(this.statements, e.statements);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitFunctionExpr(this, context);
|
||||
}
|
||||
@ -429,6 +512,10 @@ export class BinaryOperatorExpr extends Expression {
|
||||
super(type || lhs.type, sourceSpan);
|
||||
this.lhs = lhs;
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof BinaryOperatorExpr && this.operator === e.operator &&
|
||||
this.lhs.isEquivalent(e.lhs) && this.rhs.isEquivalent(e.rhs);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitBinaryOperatorExpr(this, context);
|
||||
}
|
||||
@ -441,6 +528,10 @@ export class ReadPropExpr extends Expression {
|
||||
sourceSpan?: ParseSourceSpan|null) {
|
||||
super(type, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof ReadPropExpr && this.receiver.isEquivalent(e.receiver) &&
|
||||
this.name === e.name;
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitReadPropExpr(this, context);
|
||||
}
|
||||
@ -456,6 +547,10 @@ export class ReadKeyExpr extends Expression {
|
||||
sourceSpan?: ParseSourceSpan|null) {
|
||||
super(type, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof ReadKeyExpr && this.receiver.isEquivalent(e.receiver) &&
|
||||
this.index.isEquivalent(e.index);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitReadKeyExpr(this, context);
|
||||
}
|
||||
@ -471,6 +566,9 @@ export class LiteralArrayExpr extends Expression {
|
||||
super(type, sourceSpan);
|
||||
this.entries = entries;
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof LiteralArrayExpr && areAllEquivalent(this.entries, e.entries);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitLiteralArrayExpr(this, context);
|
||||
}
|
||||
@ -478,6 +576,9 @@ export class LiteralArrayExpr extends Expression {
|
||||
|
||||
export class LiteralMapEntry {
|
||||
constructor(public key: string, public value: Expression, public quoted: boolean) {}
|
||||
isEquivalent(e: LiteralMapEntry): boolean {
|
||||
return this.key === e.key && this.value.isEquivalent(e.value);
|
||||
}
|
||||
}
|
||||
|
||||
export class LiteralMapExpr extends Expression {
|
||||
@ -489,6 +590,9 @@ export class LiteralMapExpr extends Expression {
|
||||
this.valueType = type.valueType;
|
||||
}
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof LiteralMapExpr && areAllEquivalent(this.entries, e.entries);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitLiteralMapExpr(this, context);
|
||||
}
|
||||
@ -498,6 +602,9 @@ export class CommaExpr extends Expression {
|
||||
constructor(public parts: Expression[], sourceSpan?: ParseSourceSpan|null) {
|
||||
super(parts[parts.length - 1].type, sourceSpan);
|
||||
}
|
||||
isEquivalent(e: Expression): boolean {
|
||||
return e instanceof CommaExpr && areAllEquivalent(this.parts, e.parts);
|
||||
}
|
||||
visitExpression(visitor: ExpressionVisitor, context: any): any {
|
||||
return visitor.visitCommaExpr(this, context);
|
||||
}
|
||||
@ -547,6 +654,11 @@ export abstract class Statement {
|
||||
this.modifiers = modifiers || [];
|
||||
this.sourceSpan = sourceSpan || null;
|
||||
}
|
||||
/**
|
||||
* Calculates whether this statement produces the same value as the given statement.
|
||||
* Note: We don't check Types nor ParseSourceSpans nor function arguments.
|
||||
*/
|
||||
abstract isEquivalent(stmt: Statement): boolean;
|
||||
|
||||
abstract visitStatement(visitor: StatementVisitor, context: any): any;
|
||||
|
||||
@ -562,7 +674,10 @@ export class DeclareVarStmt extends Statement {
|
||||
super(modifiers, sourceSpan);
|
||||
this.type = type || value.type;
|
||||
}
|
||||
|
||||
isEquivalent(stmt: Statement): boolean {
|
||||
return stmt instanceof DeclareVarStmt && this.name === stmt.name &&
|
||||
this.value.isEquivalent(stmt.value);
|
||||
}
|
||||
visitStatement(visitor: StatementVisitor, context: any): any {
|
||||
return visitor.visitDeclareVarStmt(this, context);
|
||||
}
|
||||
@ -576,6 +691,10 @@ export class DeclareFunctionStmt extends Statement {
|
||||
super(modifiers, sourceSpan);
|
||||
this.type = type || null;
|
||||
}
|
||||
isEquivalent(stmt: Statement): boolean {
|
||||
return stmt instanceof DeclareFunctionStmt && areAllEquivalent(this.params, stmt.params) &&
|
||||
areAllEquivalent(this.statements, stmt.statements);
|
||||
}
|
||||
|
||||
visitStatement(visitor: StatementVisitor, context: any): any {
|
||||
return visitor.visitDeclareFunctionStmt(this, context);
|
||||
@ -586,6 +705,9 @@ export class ExpressionStatement extends Statement {
|
||||
constructor(public expr: Expression, sourceSpan?: ParseSourceSpan|null) {
|
||||
super(null, sourceSpan);
|
||||
}
|
||||
isEquivalent(stmt: Statement): boolean {
|
||||
return stmt instanceof ExpressionStatement && this.expr.isEquivalent(stmt.expr);
|
||||
}
|
||||
|
||||
visitStatement(visitor: StatementVisitor, context: any): any {
|
||||
return visitor.visitExpressionStmt(this, context);
|
||||
@ -597,6 +719,9 @@ export class ReturnStatement extends Statement {
|
||||
constructor(public value: Expression, sourceSpan?: ParseSourceSpan|null) {
|
||||
super(null, sourceSpan);
|
||||
}
|
||||
isEquivalent(stmt: Statement): boolean {
|
||||
return stmt instanceof ReturnStatement && this.value.isEquivalent(stmt.value);
|
||||
}
|
||||
visitStatement(visitor: StatementVisitor, context: any): any {
|
||||
return visitor.visitReturnStmt(this, context);
|
||||
}
|
||||
@ -617,6 +742,7 @@ export class ClassField extends AbstractClassPart {
|
||||
constructor(public name: string, type?: Type|null, modifiers: StmtModifier[]|null = null) {
|
||||
super(type, modifiers);
|
||||
}
|
||||
isEquivalent(f: ClassField) { return this.name === f.name; }
|
||||
}
|
||||
|
||||
|
||||
@ -626,6 +752,9 @@ export class ClassMethod extends AbstractClassPart {
|
||||
type?: Type|null, modifiers: StmtModifier[]|null = null) {
|
||||
super(type, modifiers);
|
||||
}
|
||||
isEquivalent(m: ClassMethod) {
|
||||
return this.name === m.name && areAllEquivalent(this.body, m.body);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -635,6 +764,9 @@ export class ClassGetter extends AbstractClassPart {
|
||||
modifiers: StmtModifier[]|null = null) {
|
||||
super(type, modifiers);
|
||||
}
|
||||
isEquivalent(m: ClassGetter) {
|
||||
return this.name === m.name && areAllEquivalent(this.body, m.body);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -646,6 +778,14 @@ export class ClassStmt extends Statement {
|
||||
sourceSpan?: ParseSourceSpan|null) {
|
||||
super(modifiers, sourceSpan);
|
||||
}
|
||||
isEquivalent(stmt: Statement): boolean {
|
||||
return stmt instanceof ClassStmt && this.name === stmt.name &&
|
||||
nullSafeIsEquivalent(this.parent, stmt.parent) &&
|
||||
areAllEquivalent(this.fields, stmt.fields) &&
|
||||
areAllEquivalent(this.getters, stmt.getters) &&
|
||||
this.constructorMethod.isEquivalent(stmt.constructorMethod) &&
|
||||
areAllEquivalent(this.methods, stmt.methods);
|
||||
}
|
||||
visitStatement(visitor: StatementVisitor, context: any): any {
|
||||
return visitor.visitDeclareClassStmt(this, context);
|
||||
}
|
||||
@ -658,6 +798,11 @@ export class IfStmt extends Statement {
|
||||
public falseCase: Statement[] = [], sourceSpan?: ParseSourceSpan|null) {
|
||||
super(null, sourceSpan);
|
||||
}
|
||||
isEquivalent(stmt: Statement): boolean {
|
||||
return stmt instanceof IfStmt && this.condition.isEquivalent(stmt.condition) &&
|
||||
areAllEquivalent(this.trueCase, stmt.trueCase) &&
|
||||
areAllEquivalent(this.falseCase, stmt.falseCase);
|
||||
}
|
||||
visitStatement(visitor: StatementVisitor, context: any): any {
|
||||
return visitor.visitIfStmt(this, context);
|
||||
}
|
||||
@ -668,6 +813,7 @@ export class CommentStmt extends Statement {
|
||||
constructor(public comment: string, sourceSpan?: ParseSourceSpan|null) {
|
||||
super(null, sourceSpan);
|
||||
}
|
||||
isEquivalent(stmt: Statement): boolean { return stmt instanceof CommentStmt; }
|
||||
visitStatement(visitor: StatementVisitor, context: any): any {
|
||||
return visitor.visitCommentStmt(this, context);
|
||||
}
|
||||
@ -680,6 +826,10 @@ export class TryCatchStmt extends Statement {
|
||||
sourceSpan?: ParseSourceSpan|null) {
|
||||
super(null, sourceSpan);
|
||||
}
|
||||
isEquivalent(stmt: Statement): boolean {
|
||||
return stmt instanceof TryCatchStmt && areAllEquivalent(this.bodyStmts, stmt.bodyStmts) &&
|
||||
areAllEquivalent(this.catchStmts, stmt.catchStmts);
|
||||
}
|
||||
visitStatement(visitor: StatementVisitor, context: any): any {
|
||||
return visitor.visitTryCatchStmt(this, context);
|
||||
}
|
||||
@ -690,6 +840,9 @@ export class ThrowStmt extends Statement {
|
||||
constructor(public error: Expression, sourceSpan?: ParseSourceSpan|null) {
|
||||
super(null, sourceSpan);
|
||||
}
|
||||
isEquivalent(stmt: ThrowStmt): boolean {
|
||||
return stmt instanceof TryCatchStmt && this.error.isEquivalent(stmt.error);
|
||||
}
|
||||
visitStatement(visitor: StatementVisitor, context: any): any {
|
||||
return visitor.visitThrowStmt(this, context);
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ export function downgradeComponent(info: {
|
||||
facade.createComponent(projectableNodes);
|
||||
facade.setupInputs(needsNgZone, info.propagateDigest);
|
||||
facade.setupOutputs();
|
||||
facade.registerCleanup(needsNgZone);
|
||||
facade.registerCleanup();
|
||||
|
||||
injectorPromise.resolve(facade.getInjector());
|
||||
|
||||
|
@ -25,7 +25,6 @@ export class DowngradeComponentAdapter {
|
||||
private componentRef: ComponentRef<any>;
|
||||
private component: any;
|
||||
private changeDetector: ChangeDetectorRef;
|
||||
private appRef: ApplicationRef;
|
||||
|
||||
constructor(
|
||||
private element: angular.IAugmentedJQuery, private attrs: angular.IAttributes,
|
||||
@ -35,7 +34,6 @@ export class DowngradeComponentAdapter {
|
||||
private componentFactory: ComponentFactory<any>,
|
||||
private wrapCallback: <T>(cb: () => T) => () => T) {
|
||||
this.componentScope = scope.$new();
|
||||
this.appRef = parentInjector.get(ApplicationRef);
|
||||
}
|
||||
|
||||
compileContents(): Node[][] {
|
||||
@ -101,7 +99,7 @@ export class DowngradeComponentAdapter {
|
||||
})(input.prop);
|
||||
attrs.$observe(input.attr, observeFn);
|
||||
|
||||
// Use `$watch()` (in addition to `$observe()`) in order to initialize the input in time
|
||||
// Use `$watch()` (in addition to `$observe()`) in order to initialize the input in time
|
||||
// for `ngOnChanges()`. This is necessary if we are already in a `$digest`, which means that
|
||||
// `ngOnChanges()` (which is called by a watcher) will run before the `$observe()` callback.
|
||||
let unwatch: Function|null = this.componentScope.$watch(() => {
|
||||
@ -140,8 +138,7 @@ export class DowngradeComponentAdapter {
|
||||
(<OnChanges>this.component).ngOnChanges(inputChanges !);
|
||||
}
|
||||
|
||||
// If opted out of propagating digests, invoke change detection
|
||||
// when inputs change
|
||||
// If opted out of propagating digests, invoke change detection when inputs change.
|
||||
if (!propagateDigest) {
|
||||
detectChanges();
|
||||
}
|
||||
@ -152,9 +149,16 @@ export class DowngradeComponentAdapter {
|
||||
this.componentScope.$watch(this.wrapCallback(detectChanges));
|
||||
}
|
||||
|
||||
// Attach the view so that it will be dirty-checked.
|
||||
if (needsNgZone) {
|
||||
this.appRef.attachView(this.componentRef.hostView);
|
||||
// If necessary, attach the view so that it will be dirty-checked.
|
||||
// (Allow time for the initial input values to be set and `ngOnChanges()` to be called.)
|
||||
if (needsNgZone || !propagateDigest) {
|
||||
let unwatch: Function|null = this.componentScope.$watch(() => {
|
||||
unwatch !();
|
||||
unwatch = null;
|
||||
|
||||
const appRef = this.parentInjector.get<ApplicationRef>(ApplicationRef);
|
||||
appRef.attachView(this.componentRef.hostView);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,15 +206,14 @@ export class DowngradeComponentAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
registerCleanup(needsNgZone: boolean) {
|
||||
registerCleanup() {
|
||||
const destroyComponentRef = this.wrapCallback(() => this.componentRef.destroy());
|
||||
|
||||
this.element.on !('$destroy', () => {
|
||||
this.componentScope.$destroy();
|
||||
this.componentRef.injector.get(TestabilityRegistry)
|
||||
.unregisterApplication(this.componentRef.location.nativeElement);
|
||||
this.componentRef.destroy();
|
||||
if (needsNgZone) {
|
||||
this.appRef.detachView(this.componentRef.hostView);
|
||||
}
|
||||
destroyComponentRef();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ export function main() {
|
||||
let $compile = undefined as any;
|
||||
let $parse = undefined as any;
|
||||
let componentFactory: ComponentFactory<any>; // testbed
|
||||
let wrapCallback = undefined as any;
|
||||
let wrapCallback = (cb: any) => cb;
|
||||
|
||||
content = `
|
||||
<h1> new component </h1>
|
||||
@ -183,7 +183,7 @@ export function main() {
|
||||
expect(registry.getAllTestabilities().length).toEqual(0);
|
||||
adapter.createComponent([]);
|
||||
expect(registry.getAllTestabilities().length).toEqual(1);
|
||||
adapter.registerCleanup(true);
|
||||
adapter.registerCleanup();
|
||||
element.remove !();
|
||||
expect(registry.getAllTestabilities().length).toEqual(0);
|
||||
});
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, EventEmitter, Injector, Input, NgModule, NgModuleRef, OnChanges, OnDestroy, SimpleChanges, destroyPlatform} from '@angular/core';
|
||||
import {async} from '@angular/core/testing';
|
||||
import {async, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
import * as angular from '@angular/upgrade/src/common/angular1';
|
||||
@ -274,6 +274,42 @@ export function main() {
|
||||
});
|
||||
}));
|
||||
|
||||
it('should still run normal Angular change-detection regardless of `propagateDigest`',
|
||||
fakeAsync(() => {
|
||||
let ng2Component: Ng2Component;
|
||||
|
||||
@Component({selector: 'ng2', template: '{{ value }}'})
|
||||
class Ng2Component {
|
||||
value = 'foo';
|
||||
constructor() { setTimeout(() => this.value = 'bar', 1000); }
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [BrowserModule, UpgradeModule],
|
||||
declarations: [Ng2Component],
|
||||
entryComponents: [Ng2Component]
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
const ng1Module =
|
||||
angular.module('ng1', [])
|
||||
.directive(
|
||||
'ng2A', downgradeComponent({component: Ng2Component, propagateDigest: true}))
|
||||
.directive(
|
||||
'ng2B', downgradeComponent({component: Ng2Component, propagateDigest: false}));
|
||||
|
||||
const element = html('<ng2-a></ng2-a> | <ng2-b></ng2-b>');
|
||||
|
||||
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
|
||||
expect(element.textContent).toBe('foo | foo');
|
||||
|
||||
tick(1000);
|
||||
expect(element.textContent).toBe('bar | bar');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should initialize inputs in time for `ngOnChanges`', async(() => {
|
||||
@Component({
|
||||
selector: 'ng2',
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, Inject, Injector, Input, NgModule, NgZone, OnChanges, StaticProvider, destroyPlatform} from '@angular/core';
|
||||
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ApplicationRef, Component, DoCheck, Inject, Injector, Input, NgModule, NgZone, OnChanges, OnDestroy, OnInit, StaticProvider, ViewRef, destroyPlatform} from '@angular/core';
|
||||
import {async, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
@ -16,7 +16,7 @@ import {$ROOT_SCOPE, INJECTOR_KEY, LAZY_MODULE_REF} from '@angular/upgrade/src/c
|
||||
import {LazyModuleRef} from '@angular/upgrade/src/common/util';
|
||||
import {downgradeComponent, downgradeModule} from '@angular/upgrade/static';
|
||||
|
||||
import {html} from '../test_helpers';
|
||||
import {html, multiTrim} from '../test_helpers';
|
||||
|
||||
|
||||
export function main() {
|
||||
@ -170,6 +170,42 @@ export function main() {
|
||||
});
|
||||
}));
|
||||
|
||||
it('should destroy components inside the Angular zone', async(() => {
|
||||
let destroyedInTheZone = false;
|
||||
|
||||
@Component({selector: 'ng2', template: ''})
|
||||
class Ng2Component implements OnDestroy {
|
||||
ngOnDestroy() { destroyedInTheZone = NgZone.isInAngularZone(); }
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [Ng2Component],
|
||||
entryComponents: [Ng2Component],
|
||||
imports: [BrowserModule],
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
const bootstrapFn = (extraProviders: StaticProvider[]) =>
|
||||
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
||||
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
||||
const ng1Module =
|
||||
angular.module('ng1', [lazyModuleName])
|
||||
.directive(
|
||||
'ng2', downgradeComponent({component: Ng2Component, propagateDigest}));
|
||||
|
||||
const element = html('<ng2 ng-if="!hideNg2"></ng2>');
|
||||
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
||||
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||
|
||||
// Wait for the module to be bootstrapped.
|
||||
setTimeout(() => {
|
||||
$rootScope.$apply('hideNg2 = true');
|
||||
expect(destroyedInTheZone).toBe(true);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should propagate input changes inside the Angular zone', async(() => {
|
||||
let ng2Component: Ng2Component;
|
||||
|
||||
@ -270,6 +306,210 @@ export function main() {
|
||||
});
|
||||
}));
|
||||
|
||||
it('should run the lifecycle hooks in the correct order', async(() => {
|
||||
const logs: string[] = [];
|
||||
let rootScope: angular.IRootScopeService;
|
||||
|
||||
@Component({
|
||||
selector: 'ng2',
|
||||
template: `
|
||||
{{ value }}
|
||||
<button (click)="value = 'qux'"></button>
|
||||
<ng-content></ng-content>
|
||||
`
|
||||
})
|
||||
class Ng2Component implements AfterContentChecked,
|
||||
AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnDestroy,
|
||||
OnInit {
|
||||
@Input() value = 'foo';
|
||||
|
||||
ngAfterContentChecked() { this.log('AfterContentChecked'); }
|
||||
ngAfterContentInit() { this.log('AfterContentInit'); }
|
||||
ngAfterViewChecked() { this.log('AfterViewChecked'); }
|
||||
ngAfterViewInit() { this.log('AfterViewInit'); }
|
||||
ngDoCheck() { this.log('DoCheck'); }
|
||||
ngOnChanges() { this.log('OnChanges'); }
|
||||
ngOnDestroy() { this.log('OnDestroy'); }
|
||||
ngOnInit() { this.log('OnInit'); }
|
||||
|
||||
private log(hook: string) { logs.push(`${hook}(${this.value})`); }
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [Ng2Component],
|
||||
entryComponents: [Ng2Component],
|
||||
imports: [BrowserModule],
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
const bootstrapFn = (extraProviders: StaticProvider[]) =>
|
||||
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
||||
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
||||
const ng1Module =
|
||||
angular.module('ng1', [lazyModuleName])
|
||||
.directive('ng2', downgradeComponent({component: Ng2Component, propagateDigest}))
|
||||
.run(($rootScope: angular.IRootScopeService) => {
|
||||
rootScope = $rootScope;
|
||||
rootScope.value = 'bar';
|
||||
});
|
||||
|
||||
const element =
|
||||
html('<div><ng2 value="{{ value }}" ng-if="!hideNg2">Content</ng2></div>');
|
||||
angular.bootstrap(element, [ng1Module.name]);
|
||||
|
||||
setTimeout(() => { // Wait for the module to be bootstrapped.
|
||||
setTimeout(() => { // Wait for `$evalAsync()` to propagate inputs.
|
||||
const button = element.querySelector('button') !;
|
||||
|
||||
// Once initialized.
|
||||
expect(multiTrim(element.textContent)).toBe('bar Content');
|
||||
expect(logs).toEqual([
|
||||
// `ngOnChanges()` call triggered directly through the `inputChanges` $watcher.
|
||||
'OnChanges(bar)',
|
||||
// Initial CD triggered directly through the `detectChanges()` or `inputChanges`
|
||||
// $watcher (for `propagateDigest` true/false respectively).
|
||||
'OnInit(bar)',
|
||||
'DoCheck(bar)',
|
||||
'AfterContentInit(bar)',
|
||||
'AfterContentChecked(bar)',
|
||||
'AfterViewInit(bar)',
|
||||
'AfterViewChecked(bar)',
|
||||
...(propagateDigest ?
|
||||
[
|
||||
// CD triggered directly through the `detectChanges()` $watcher (2nd
|
||||
// $digest).
|
||||
'DoCheck(bar)',
|
||||
'AfterContentChecked(bar)',
|
||||
'AfterViewChecked(bar)',
|
||||
] :
|
||||
[]),
|
||||
// CD triggered due to entering/leaving the NgZone (in `downgradeFn()`).
|
||||
'DoCheck(bar)',
|
||||
'AfterContentChecked(bar)',
|
||||
'AfterViewChecked(bar)',
|
||||
]);
|
||||
logs.length = 0;
|
||||
|
||||
// Change inputs and run `$digest`.
|
||||
rootScope.$apply('value = "baz"');
|
||||
expect(multiTrim(element.textContent)).toBe('baz Content');
|
||||
expect(logs).toEqual([
|
||||
// `ngOnChanges()` call triggered directly through the `inputChanges` $watcher.
|
||||
'OnChanges(baz)',
|
||||
// `propagateDigest: true` (3 CD runs):
|
||||
// - CD triggered due to entering/leaving the NgZone (in `inputChanges`
|
||||
// $watcher).
|
||||
// - CD triggered directly through the `detectChanges()` $watcher.
|
||||
// - CD triggered due to entering/leaving the NgZone (in `detectChanges`
|
||||
// $watcher).
|
||||
// `propagateDigest: false` (2 CD runs):
|
||||
// - CD triggered directly through the `inputChanges` $watcher.
|
||||
// - CD triggered due to entering/leaving the NgZone (in `inputChanges`
|
||||
// $watcher).
|
||||
'DoCheck(baz)',
|
||||
'AfterContentChecked(baz)',
|
||||
'AfterViewChecked(baz)',
|
||||
'DoCheck(baz)',
|
||||
'AfterContentChecked(baz)',
|
||||
'AfterViewChecked(baz)',
|
||||
...(propagateDigest ?
|
||||
[
|
||||
'DoCheck(baz)',
|
||||
'AfterContentChecked(baz)',
|
||||
'AfterViewChecked(baz)',
|
||||
] :
|
||||
[]),
|
||||
]);
|
||||
logs.length = 0;
|
||||
|
||||
// Run `$digest` (without changing inputs).
|
||||
rootScope.$digest();
|
||||
expect(multiTrim(element.textContent)).toBe('baz Content');
|
||||
expect(logs).toEqual(
|
||||
propagateDigest ?
|
||||
[
|
||||
// CD triggered directly through the `detectChanges()` $watcher.
|
||||
'DoCheck(baz)',
|
||||
'AfterContentChecked(baz)',
|
||||
'AfterViewChecked(baz)',
|
||||
// CD triggered due to entering/leaving the NgZone (in the above $watcher).
|
||||
'DoCheck(baz)',
|
||||
'AfterContentChecked(baz)',
|
||||
'AfterViewChecked(baz)',
|
||||
] :
|
||||
[]);
|
||||
logs.length = 0;
|
||||
|
||||
// Trigger change detection (without changing inputs).
|
||||
button.click();
|
||||
expect(multiTrim(element.textContent)).toBe('qux Content');
|
||||
expect(logs).toEqual([
|
||||
'DoCheck(qux)',
|
||||
'AfterContentChecked(qux)',
|
||||
'AfterViewChecked(qux)',
|
||||
]);
|
||||
logs.length = 0;
|
||||
|
||||
// Destroy the component.
|
||||
rootScope.$apply('hideNg2 = true');
|
||||
expect(logs).toEqual([
|
||||
'OnDestroy(qux)',
|
||||
]);
|
||||
logs.length = 0;
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should detach hostViews from the ApplicationRef once destroyed', async(() => {
|
||||
let ng2Component: Ng2Component;
|
||||
|
||||
@Component({selector: 'ng2', template: ''})
|
||||
class Ng2Component {
|
||||
constructor(public appRef: ApplicationRef) {
|
||||
ng2Component = this;
|
||||
spyOn(appRef, 'attachView').and.callThrough();
|
||||
spyOn(appRef, 'detachView').and.callThrough();
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [Ng2Component],
|
||||
entryComponents: [Ng2Component],
|
||||
imports: [BrowserModule],
|
||||
})
|
||||
class Ng2Module {
|
||||
ngDoBootstrap() {}
|
||||
}
|
||||
|
||||
const bootstrapFn = (extraProviders: StaticProvider[]) =>
|
||||
platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module);
|
||||
const lazyModuleName = downgradeModule<Ng2Module>(bootstrapFn);
|
||||
const ng1Module =
|
||||
angular.module('ng1', [lazyModuleName])
|
||||
.directive(
|
||||
'ng2', downgradeComponent({component: Ng2Component, propagateDigest}));
|
||||
|
||||
const element = html('<ng2 ng-if="!hideNg2"></ng2>');
|
||||
const $injector = angular.bootstrap(element, [ng1Module.name]);
|
||||
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
|
||||
|
||||
setTimeout(() => { // Wait for the module to be bootstrapped.
|
||||
setTimeout(() => { // Wait for the hostView to be attached (during the `$digest`).
|
||||
const hostView: ViewRef =
|
||||
(ng2Component.appRef.attachView as jasmine.Spy).calls.mostRecent().args[0];
|
||||
|
||||
expect(hostView.destroyed).toBe(false);
|
||||
|
||||
$rootScope.$apply('hideNg2 = true');
|
||||
|
||||
expect(hostView.destroyed).toBe(true);
|
||||
expect(ng2Component.appRef.detachView).toHaveBeenCalledWith(hostView);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should only retrieve the Angular zone once (and cache it for later use)',
|
||||
fakeAsync(() => {
|
||||
let count = 0;
|
||||
|
@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {PlatformRef, Type} from '@angular/core';
|
||||
import {NgZone, PlatformRef, Type} from '@angular/core';
|
||||
import * as angular from '@angular/upgrade/src/common/angular1';
|
||||
import {$ROOT_SCOPE} from '@angular/upgrade/src/common/constants';
|
||||
import {UpgradeModule} from '@angular/upgrade/static';
|
||||
@ -18,11 +18,19 @@ export function bootstrap(
|
||||
// We bootstrap the Angular module first; then when it is ready (async) we bootstrap the AngularJS
|
||||
// module on the bootstrap element (also ensuring that AngularJS errors will fail the test).
|
||||
return platform.bootstrapModule(Ng2Module).then(ref => {
|
||||
const ngZone = ref.injector.get<NgZone>(NgZone);
|
||||
const upgrade = ref.injector.get(UpgradeModule);
|
||||
const failHardModule: any = ($provide: angular.IProvideService) => {
|
||||
$provide.value('$exceptionHandler', (err: any) => { throw err; });
|
||||
};
|
||||
upgrade.bootstrap(element, [failHardModule, ng1Module.name]);
|
||||
|
||||
// The `bootstrap()` helper is used for convenience in tests, so that we don't have to inject
|
||||
// and call `upgrade.bootstrap()` on every Angular module.
|
||||
// In order to closer emulate what happens in real application, ensure AngularJS is bootstrapped
|
||||
// inside the Angular zone.
|
||||
//
|
||||
ngZone.run(() => upgrade.bootstrap(element, [failHardModule, ng1Module.name]));
|
||||
|
||||
return upgrade;
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user