Merge remote-tracking branch 'en/master' into aio

This commit is contained in:
Zhicheng Wang 2017-10-03 11:37:04 +08:00
commit 9a43523368
27 changed files with 808 additions and 151 deletions

View File

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

View File

@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 !;

View File

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

View File

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

View File

@ -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,
};
}

View File

@ -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": ".",

View File

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

View File

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

View File

@ -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', () => {

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',

View File

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

View File

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