perf(compiler): speed up watch mode (#19275)
- don’t regenerate code for .d.ts files when an oldProgram is passed to `createProgram` - cache `fileExists` / `getSourceFile` / `readFile` in watch mode - refactor tests to share common code in `test_support` - support `—diagnostic` command line to print total time used per watch mode compilation. PR Close #19275
This commit is contained in:
parent
ad7251c8bb
commit
6665d76fbb
@ -260,7 +260,7 @@ export class CompilerHost extends BaseAotCompilerHost<CompilerHostContext> {
|
|||||||
this.urlResolver = createOfflineCompileUrlResolver();
|
this.urlResolver = createOfflineCompileUrlResolver();
|
||||||
}
|
}
|
||||||
|
|
||||||
getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined {
|
protected getSourceFile(filePath: string): ts.SourceFile {
|
||||||
let sf = this.program.getSourceFile(filePath);
|
let sf = this.program.getSourceFile(filePath);
|
||||||
if (!sf) {
|
if (!sf) {
|
||||||
if (this.context.fileExists(filePath)) {
|
if (this.context.fileExists(filePath)) {
|
||||||
@ -270,7 +270,11 @@ export class CompilerHost extends BaseAotCompilerHost<CompilerHostContext> {
|
|||||||
throw new Error(`Source file ${filePath} not present in program.`);
|
throw new Error(`Source file ${filePath} not present in program.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.metadataProvider.getMetadata(sf);
|
return sf;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined {
|
||||||
|
return this.metadataProvider.getMetadata(this.getSourceFile(filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
toSummaryFileName(fileName: string, referringSrcFileName: string): string {
|
toSummaryFileName(fileName: string, referringSrcFileName: string): string {
|
||||||
|
@ -66,8 +66,9 @@ export interface CompilerHost extends ts.CompilerHost {
|
|||||||
export enum EmitFlags {
|
export enum EmitFlags {
|
||||||
DTS = 1 << 0,
|
DTS = 1 << 0,
|
||||||
JS = 1 << 1,
|
JS = 1 << 1,
|
||||||
|
Codegen = 1 << 4,
|
||||||
|
|
||||||
Default = DTS | JS
|
Default = DTS | JS | Codegen
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomTransformers {
|
export interface CustomTransformers {
|
||||||
@ -106,6 +107,7 @@ export interface Program {
|
|||||||
customTransformers?: CustomTransformers,
|
customTransformers?: CustomTransformers,
|
||||||
emitCallback?: TsEmitCallback
|
emitCallback?: TsEmitCallback
|
||||||
}): ts.EmitResult;
|
}): ts.EmitResult;
|
||||||
|
getLibrarySummaries(): {fileName: string, content: string}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper for createProgram.
|
// Wrapper for createProgram.
|
||||||
|
@ -35,9 +35,25 @@ const ChangeDiagnostics = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function totalCompilationTimeDiagnostic(timeInMillis: number): api.Diagnostic {
|
||||||
|
let duration: string;
|
||||||
|
if (timeInMillis > 1000) {
|
||||||
|
duration = `${(timeInMillis / 1000).toPrecision(2)}s`;
|
||||||
|
} else {
|
||||||
|
duration = `${timeInMillis}ms`;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
category: ts.DiagnosticCategory.Message,
|
||||||
|
messageText: `Total time: ${duration}`,
|
||||||
|
code: api.DEFAULT_ERROR_CODE,
|
||||||
|
source: api.SOURCE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export enum FileChangeEvent {
|
export enum FileChangeEvent {
|
||||||
Change,
|
Change,
|
||||||
CreateDelete
|
CreateDelete,
|
||||||
|
CreateDeleteDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PerformWatchHost {
|
export interface PerformWatchHost {
|
||||||
@ -45,8 +61,9 @@ export interface PerformWatchHost {
|
|||||||
readConfiguration(): ParsedConfiguration;
|
readConfiguration(): ParsedConfiguration;
|
||||||
createCompilerHost(options: api.CompilerOptions): api.CompilerHost;
|
createCompilerHost(options: api.CompilerOptions): api.CompilerHost;
|
||||||
createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined;
|
createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined;
|
||||||
onFileChange(listener: (event: FileChangeEvent, fileName: string) => void):
|
onFileChange(
|
||||||
{close: () => void, ready: (cb: () => void) => void};
|
options: api.CompilerOptions, listener: (event: FileChangeEvent, fileName: string) => void,
|
||||||
|
ready: () => void): {close: () => void};
|
||||||
setTimeout(callback: () => void, ms: number): any;
|
setTimeout(callback: () => void, ms: number): any;
|
||||||
clearTimeout(timeoutId: any): void;
|
clearTimeout(timeoutId: any): void;
|
||||||
}
|
}
|
||||||
@ -60,23 +77,17 @@ export function createPerformWatchHost(
|
|||||||
createCompilerHost: options => createCompilerHost({options}),
|
createCompilerHost: options => createCompilerHost({options}),
|
||||||
readConfiguration: () => readConfiguration(configFileName, existingOptions),
|
readConfiguration: () => readConfiguration(configFileName, existingOptions),
|
||||||
createEmitCallback: options => createEmitCallback ? createEmitCallback(options) : undefined,
|
createEmitCallback: options => createEmitCallback ? createEmitCallback(options) : undefined,
|
||||||
onFileChange: (listeners) => {
|
onFileChange: (options, listener, ready: () => void) => {
|
||||||
const parsed = readConfiguration(configFileName, existingOptions);
|
if (!options.basePath) {
|
||||||
function stubReady(cb: () => void) { process.nextTick(cb); }
|
|
||||||
if (parsed.errors && parsed.errors.length) {
|
|
||||||
reportDiagnostics(parsed.errors);
|
|
||||||
return {close: () => {}, ready: stubReady};
|
|
||||||
}
|
|
||||||
if (!parsed.options.basePath) {
|
|
||||||
reportDiagnostics([{
|
reportDiagnostics([{
|
||||||
category: ts.DiagnosticCategory.Error,
|
category: ts.DiagnosticCategory.Error,
|
||||||
messageText: 'Invalid configuration option. baseDir not specified',
|
messageText: 'Invalid configuration option. baseDir not specified',
|
||||||
source: api.SOURCE,
|
source: api.SOURCE,
|
||||||
code: api.DEFAULT_ERROR_CODE
|
code: api.DEFAULT_ERROR_CODE
|
||||||
}]);
|
}]);
|
||||||
return {close: () => {}, ready: stubReady};
|
return {close: () => {}};
|
||||||
}
|
}
|
||||||
const watcher = chokidar.watch(parsed.options.basePath, {
|
const watcher = chokidar.watch(options.basePath, {
|
||||||
// ignore .dotfiles, .js and .map files.
|
// ignore .dotfiles, .js and .map files.
|
||||||
// can't ignore other files as we e.g. want to recompile if an `.html` file changes as well.
|
// can't ignore other files as we e.g. want to recompile if an `.html` file changes as well.
|
||||||
ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json)/,
|
ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json)/,
|
||||||
@ -86,15 +97,19 @@ export function createPerformWatchHost(
|
|||||||
watcher.on('all', (event: string, path: string) => {
|
watcher.on('all', (event: string, path: string) => {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case 'change':
|
case 'change':
|
||||||
listeners(FileChangeEvent.Change, path);
|
listener(FileChangeEvent.Change, path);
|
||||||
break;
|
break;
|
||||||
case 'unlink':
|
case 'unlink':
|
||||||
case 'add':
|
case 'add':
|
||||||
listeners(FileChangeEvent.CreateDelete, path);
|
listener(FileChangeEvent.CreateDelete, path);
|
||||||
|
break;
|
||||||
|
case 'unlinkDir':
|
||||||
|
case 'addDir':
|
||||||
|
listener(FileChangeEvent.CreateDeleteDir, path);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
function ready(cb: () => void) { watcher.on('ready', cb); }
|
watcher.on('ready', ready);
|
||||||
return {close: () => watcher.close(), ready};
|
return {close: () => watcher.close(), ready};
|
||||||
},
|
},
|
||||||
setTimeout: (ts.sys.clearTimeout && ts.sys.setTimeout) || setTimeout,
|
setTimeout: (ts.sys.clearTimeout && ts.sys.setTimeout) || setTimeout,
|
||||||
@ -102,6 +117,12 @@ export function createPerformWatchHost(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CacheEntry {
|
||||||
|
exists?: boolean;
|
||||||
|
sf?: ts.SourceFile;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The logic in this function is adapted from `tsc.ts` from TypeScript.
|
* The logic in this function is adapted from `tsc.ts` from TypeScript.
|
||||||
*/
|
*/
|
||||||
@ -112,16 +133,30 @@ export function performWatchCompilation(host: PerformWatchHost):
|
|||||||
let cachedOptions: ParsedConfiguration|undefined; // CompilerOptions cached from last compilation
|
let cachedOptions: ParsedConfiguration|undefined; // CompilerOptions cached from last compilation
|
||||||
let timerHandleForRecompilation: any; // Handle for 0.25s wait timer to trigger recompilation
|
let timerHandleForRecompilation: any; // Handle for 0.25s wait timer to trigger recompilation
|
||||||
|
|
||||||
// Watch basePath, ignoring .dotfiles
|
|
||||||
const fileWatcher = host.onFileChange(watchedFileChanged);
|
|
||||||
const ingoreFilesForWatch = new Set<string>();
|
const ingoreFilesForWatch = new Set<string>();
|
||||||
|
const fileCache = new Map<string, CacheEntry>();
|
||||||
|
|
||||||
const firstCompileResult = doCompilation();
|
const firstCompileResult = doCompilation();
|
||||||
|
|
||||||
const readyPromise = new Promise(resolve => fileWatcher.ready(resolve));
|
// Watch basePath, ignoring .dotfiles
|
||||||
|
let resolveReadyPromise: () => void;
|
||||||
|
const readyPromise = new Promise(resolve => resolveReadyPromise = resolve);
|
||||||
|
// Note: ! is ok as options are filled after the first compilation
|
||||||
|
// Note: ! is ok as resolvedReadyPromise is filled by the previous call
|
||||||
|
const fileWatcher =
|
||||||
|
host.onFileChange(cachedOptions !.options, watchedFileChanged, resolveReadyPromise !);
|
||||||
|
|
||||||
return {close, ready: cb => readyPromise.then(cb), firstCompileResult};
|
return {close, ready: cb => readyPromise.then(cb), firstCompileResult};
|
||||||
|
|
||||||
|
function cacheEntry(fileName: string): CacheEntry {
|
||||||
|
let entry = fileCache.get(fileName);
|
||||||
|
if (!entry) {
|
||||||
|
entry = {};
|
||||||
|
fileCache.set(fileName, entry);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
fileWatcher.close();
|
fileWatcher.close();
|
||||||
if (timerHandleForRecompilation) {
|
if (timerHandleForRecompilation) {
|
||||||
@ -139,11 +174,8 @@ export function performWatchCompilation(host: PerformWatchHost):
|
|||||||
host.reportDiagnostics(cachedOptions.errors);
|
host.reportDiagnostics(cachedOptions.errors);
|
||||||
return cachedOptions.errors;
|
return cachedOptions.errors;
|
||||||
}
|
}
|
||||||
|
const startTime = Date.now();
|
||||||
if (!cachedCompilerHost) {
|
if (!cachedCompilerHost) {
|
||||||
// TODO(chuckj): consider avoiding re-generating factories for libraries.
|
|
||||||
// Consider modifying the AotCompilerHost to be able to remember the summary files
|
|
||||||
// generated from previous compiliations and return false from isSourceFile for
|
|
||||||
// .d.ts files for which a summary file was already generated.å
|
|
||||||
cachedCompilerHost = host.createCompilerHost(cachedOptions.options);
|
cachedCompilerHost = host.createCompilerHost(cachedOptions.options);
|
||||||
const originalWriteFileCallback = cachedCompilerHost.writeFile;
|
const originalWriteFileCallback = cachedCompilerHost.writeFile;
|
||||||
cachedCompilerHost.writeFile = function(
|
cachedCompilerHost.writeFile = function(
|
||||||
@ -152,6 +184,31 @@ export function performWatchCompilation(host: PerformWatchHost):
|
|||||||
ingoreFilesForWatch.add(path.normalize(fileName));
|
ingoreFilesForWatch.add(path.normalize(fileName));
|
||||||
return originalWriteFileCallback(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
return originalWriteFileCallback(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
||||||
};
|
};
|
||||||
|
const originalFileExists = cachedCompilerHost.fileExists;
|
||||||
|
cachedCompilerHost.fileExists = function(fileName: string) {
|
||||||
|
const ce = cacheEntry(fileName);
|
||||||
|
if (ce.exists == null) {
|
||||||
|
ce.exists = originalFileExists.call(this, fileName);
|
||||||
|
}
|
||||||
|
return ce.exists !;
|
||||||
|
};
|
||||||
|
const originalGetSourceFile = cachedCompilerHost.getSourceFile;
|
||||||
|
cachedCompilerHost.getSourceFile = function(
|
||||||
|
fileName: string, languageVersion: ts.ScriptTarget) {
|
||||||
|
const ce = cacheEntry(fileName);
|
||||||
|
if (!ce.sf) {
|
||||||
|
ce.sf = originalGetSourceFile.call(this, fileName, languageVersion);
|
||||||
|
}
|
||||||
|
return ce.sf !;
|
||||||
|
};
|
||||||
|
const originalReadFile = cachedCompilerHost.readFile;
|
||||||
|
cachedCompilerHost.readFile = function(fileName: string) {
|
||||||
|
const ce = cacheEntry(fileName);
|
||||||
|
if (ce.content == null) {
|
||||||
|
ce.content = originalReadFile.call(this, fileName);
|
||||||
|
}
|
||||||
|
return ce.content !;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
ingoreFilesForWatch.clear();
|
ingoreFilesForWatch.clear();
|
||||||
const compileResult = performCompilation({
|
const compileResult = performCompilation({
|
||||||
@ -166,6 +223,11 @@ export function performWatchCompilation(host: PerformWatchHost):
|
|||||||
host.reportDiagnostics(compileResult.diagnostics);
|
host.reportDiagnostics(compileResult.diagnostics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
if (cachedOptions.options.diagnostics) {
|
||||||
|
const totalTime = (endTime - startTime) / 1000;
|
||||||
|
host.reportDiagnostics([totalCompilationTimeDiagnostic(endTime - startTime)]);
|
||||||
|
}
|
||||||
const exitCode = exitCodeFromResult(compileResult.diagnostics);
|
const exitCode = exitCodeFromResult(compileResult.diagnostics);
|
||||||
if (exitCode == 0) {
|
if (exitCode == 0) {
|
||||||
cachedProgram = compileResult.program;
|
cachedProgram = compileResult.program;
|
||||||
@ -191,11 +253,19 @@ export function performWatchCompilation(host: PerformWatchHost):
|
|||||||
path.normalize(fileName) === path.normalize(cachedOptions.project)) {
|
path.normalize(fileName) === path.normalize(cachedOptions.project)) {
|
||||||
// If the configuration file changes, forget everything and start the recompilation timer
|
// If the configuration file changes, forget everything and start the recompilation timer
|
||||||
resetOptions();
|
resetOptions();
|
||||||
} else if (event === FileChangeEvent.CreateDelete) {
|
} else if (
|
||||||
|
event === FileChangeEvent.CreateDelete || event === FileChangeEvent.CreateDeleteDir) {
|
||||||
// If a file was added or removed, reread the configuration
|
// If a file was added or removed, reread the configuration
|
||||||
// to determine the new list of root files.
|
// to determine the new list of root files.
|
||||||
cachedOptions = undefined;
|
cachedOptions = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event === FileChangeEvent.CreateDeleteDir) {
|
||||||
|
fileCache.clear();
|
||||||
|
} else {
|
||||||
|
fileCache.delete(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
if (!ingoreFilesForWatch.has(path.normalize(fileName))) {
|
if (!ingoreFilesForWatch.has(path.normalize(fileName))) {
|
||||||
// Ignore the file if the file is one that was written by the compiler.
|
// Ignore the file if the file is one that was written by the compiler.
|
||||||
startTimerForRecompilation();
|
startTimerForRecompilation();
|
||||||
|
@ -30,6 +30,11 @@ export function isNgDiagnostic(diagnostic: any): diagnostic is Diagnostic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CompilerOptions extends ts.CompilerOptions {
|
export interface CompilerOptions extends ts.CompilerOptions {
|
||||||
|
// Write statistics about compilation (e.g. total time, ...)
|
||||||
|
// Note: this is the --diagnostics command line option from TS (which is @internal
|
||||||
|
// on ts.CompilerOptions interface).
|
||||||
|
diagnostics?: boolean;
|
||||||
|
|
||||||
// Absolute path to a directory where generated file structure is written.
|
// Absolute path to a directory where generated file structure is written.
|
||||||
// If unspecified, generated files will be written alongside sources.
|
// If unspecified, generated files will be written alongside sources.
|
||||||
// @deprecated - no effect
|
// @deprecated - no effect
|
||||||
@ -273,4 +278,10 @@ export interface Program {
|
|||||||
customTransformers?: CustomTransformers,
|
customTransformers?: CustomTransformers,
|
||||||
emitCallback?: TsEmitCallback
|
emitCallback?: TsEmitCallback
|
||||||
}): ts.EmitResult;
|
}): ts.EmitResult;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the .ngsummary.json files of libraries that have been compiled
|
||||||
|
* in this program or previous programs.
|
||||||
|
*/
|
||||||
|
getLibrarySummaries(): {fileName: string, content: string}[];
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
|
|||||||
constructor(
|
constructor(
|
||||||
private rootFiles: string[], options: CompilerOptions, context: CompilerHost,
|
private rootFiles: string[], options: CompilerOptions, context: CompilerHost,
|
||||||
private metadataProvider: MetadataProvider,
|
private metadataProvider: MetadataProvider,
|
||||||
private codeGenerator: (fileName: string) => GeneratedFile[]) {
|
private codeGenerator: (fileName: string) => GeneratedFile[],
|
||||||
|
private summariesFromPreviousCompilations: Map<string, string>) {
|
||||||
super(options, context);
|
super(options, context);
|
||||||
this.moduleResolutionCache = ts.createModuleResolutionCache(
|
this.moduleResolutionCache = ts.createModuleResolutionCache(
|
||||||
this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context));
|
this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context));
|
||||||
@ -292,7 +293,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
|
|||||||
}
|
}
|
||||||
this.generatedCodeFor.add(fileName);
|
this.generatedCodeFor.add(fileName);
|
||||||
|
|
||||||
const baseNameFromGeneratedFile = this._getBaseNameForGeneratedFile(fileName);
|
const baseNameFromGeneratedFile = this._getBaseNamesForGeneratedFile(fileName).find(
|
||||||
|
fileName => this.isSourceFile(fileName) && this.fileExists(fileName));
|
||||||
if (baseNameFromGeneratedFile) {
|
if (baseNameFromGeneratedFile) {
|
||||||
return this.ensureCodeGeneratedFor(baseNameFromGeneratedFile);
|
return this.ensureCodeGeneratedFor(baseNameFromGeneratedFile);
|
||||||
}
|
}
|
||||||
@ -336,29 +338,58 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
|
|||||||
fileName = stripNgResourceSuffix(fileName);
|
fileName = stripNgResourceSuffix(fileName);
|
||||||
// Note: Don't rely on this.generatedSourceFiles here,
|
// Note: Don't rely on this.generatedSourceFiles here,
|
||||||
// as it might not have been filled yet.
|
// as it might not have been filled yet.
|
||||||
if (this._getBaseNameForGeneratedFile(fileName)) {
|
if (this._getBaseNamesForGeneratedFile(fileName).find(baseFileName => {
|
||||||
|
if (this.isSourceFile(baseFileName)) {
|
||||||
|
return this.fileExists(baseFileName);
|
||||||
|
} else {
|
||||||
|
// Note: the factories of a previous program
|
||||||
|
// are not reachable via the regular fileExists
|
||||||
|
// as they might be in the outDir. So we derive their
|
||||||
|
// fileExist information based on the .ngsummary.json file.
|
||||||
|
return this.fileExists(summaryFileName(baseFileName));
|
||||||
|
}
|
||||||
|
})) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this.originalSourceFiles.has(fileName) || this.context.fileExists(fileName);
|
return this.summariesFromPreviousCompilations.has(fileName) ||
|
||||||
|
this.originalSourceFiles.has(fileName) || this.context.fileExists(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getBaseNameForGeneratedFile(genFileName: string): string|null {
|
private _getBaseNamesForGeneratedFile(genFileName: string): string[] {
|
||||||
const genMatch = GENERATED_FILES.exec(genFileName);
|
const genMatch = GENERATED_FILES.exec(genFileName);
|
||||||
if (genMatch) {
|
if (genMatch) {
|
||||||
const [, base, genSuffix, suffix] = genMatch;
|
const [, base, genSuffix, suffix] = genMatch;
|
||||||
// Note: on-the-fly generated files always have a `.ts` suffix,
|
let baseNames: string[] = [];
|
||||||
// but the file from which we generated it can be a `.ts`/ `.d.ts`
|
if (genSuffix.indexOf('ngstyle') >= 0) {
|
||||||
// (see options.generateCodeForLibraries).
|
// Note: ngstlye files have names like `afile.css.ngstyle.ts`
|
||||||
// It can also be a `.css` file in case of a `.css.ngstyle.ts` file
|
baseNames = [base];
|
||||||
if (suffix === 'ts') {
|
} else if (suffix === 'd.ts') {
|
||||||
const baseNames =
|
baseNames = [base + '.d.ts'];
|
||||||
genSuffix.indexOf('ngstyle') >= 0 ? [base] : [`${base}.ts`, `${base}.d.ts`];
|
} else if (suffix === 'ts') {
|
||||||
return baseNames.find(
|
// Note: on-the-fly generated files always have a `.ts` suffix,
|
||||||
baseName => this.isSourceFile(baseName) && this.fileExists(baseName)) ||
|
// but the file from which we generated it can be a `.ts`/ `.d.ts`
|
||||||
null;
|
// (see options.generateCodeForLibraries).
|
||||||
|
baseNames = [`${base}.ts`, `${base}.d.ts`];
|
||||||
}
|
}
|
||||||
|
return baseNames;
|
||||||
}
|
}
|
||||||
return null;
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSummary(filePath: string): string|null {
|
||||||
|
if (this.summariesFromPreviousCompilations.has(filePath)) {
|
||||||
|
return this.summariesFromPreviousCompilations.get(filePath) !;
|
||||||
|
}
|
||||||
|
return super.loadSummary(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSourceFile(filePath: string): boolean {
|
||||||
|
// If we have a summary from a previous compilation,
|
||||||
|
// treat the file never as a source file.
|
||||||
|
if (this.summariesFromPreviousCompilations.has(summaryFileName(filePath))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.isSourceFile(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
readFile = (fileName: string) => this.context.readFile(fileName);
|
readFile = (fileName: string) => this.context.readFile(fileName);
|
||||||
@ -431,3 +462,7 @@ function stripNgResourceSuffix(fileName: string): string {
|
|||||||
function addNgResourceSuffix(fileName: string): string {
|
function addNgResourceSuffix(fileName: string): string {
|
||||||
return `${fileName}.$ngresource$`;
|
return `${fileName}.$ngresource$`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function summaryFileName(fileName: string): string {
|
||||||
|
return fileName.replace(EXT, '') + '.ngsummary.json';
|
||||||
|
}
|
@ -35,6 +35,9 @@ const defaultEmitCallback: TsEmitCallback =
|
|||||||
|
|
||||||
class AngularCompilerProgram implements Program {
|
class AngularCompilerProgram implements Program {
|
||||||
private metadataCache: LowerMetadataCache;
|
private metadataCache: LowerMetadataCache;
|
||||||
|
private summariesFromPreviousCompilations = new Map<string, string>();
|
||||||
|
// Note: This will be cleared out as soon as we create the _tsProgram
|
||||||
|
private oldTsProgram: ts.Program|undefined;
|
||||||
private _emittedGenFiles: GeneratedFile[]|undefined;
|
private _emittedGenFiles: GeneratedFile[]|undefined;
|
||||||
|
|
||||||
// Lazily initialized fields
|
// Lazily initialized fields
|
||||||
@ -54,6 +57,11 @@ class AngularCompilerProgram implements Program {
|
|||||||
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
|
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
|
||||||
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
|
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
|
||||||
}
|
}
|
||||||
|
this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
|
||||||
|
if (oldProgram) {
|
||||||
|
oldProgram.getLibrarySummaries().forEach(
|
||||||
|
({content, fileName}) => this.summariesFromPreviousCompilations.set(fileName, content));
|
||||||
|
}
|
||||||
|
|
||||||
this.rootNames = rootNames = rootNames.filter(r => !GENERATED_FILES.test(r));
|
this.rootNames = rootNames = rootNames.filter(r => !GENERATED_FILES.test(r));
|
||||||
if (options.flatModuleOutFile) {
|
if (options.flatModuleOutFile) {
|
||||||
@ -75,6 +83,23 @@ class AngularCompilerProgram implements Program {
|
|||||||
this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit);
|
this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLibrarySummaries(): {fileName: string, content: string}[] {
|
||||||
|
const emittedLibSummaries: {fileName: string, content: string}[] = [];
|
||||||
|
this.summariesFromPreviousCompilations.forEach(
|
||||||
|
(content, fileName) => emittedLibSummaries.push({fileName, content}));
|
||||||
|
if (this._emittedGenFiles) {
|
||||||
|
this._emittedGenFiles.forEach(genFile => {
|
||||||
|
if (genFile.srcFileUrl.endsWith('.d.ts') &&
|
||||||
|
genFile.genFileUrl.endsWith('.ngsummary.json')) {
|
||||||
|
// Note: ! is ok here as ngsummary.json files are always plain text, so genFile.source
|
||||||
|
// is filled.
|
||||||
|
emittedLibSummaries.push({fileName: genFile.genFileUrl, content: genFile.source !});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return emittedLibSummaries;
|
||||||
|
}
|
||||||
|
|
||||||
getTsProgram(): ts.Program { return this.tsProgram; }
|
getTsProgram(): ts.Program { return this.tsProgram; }
|
||||||
|
|
||||||
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken) {
|
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken) {
|
||||||
@ -284,6 +309,9 @@ class AngularCompilerProgram implements Program {
|
|||||||
if (this._analyzedModules) {
|
if (this._analyzedModules) {
|
||||||
throw new Error(`Internal Error: already initalized!`);
|
throw new Error(`Internal Error: already initalized!`);
|
||||||
}
|
}
|
||||||
|
// Note: This is important to not produce a memory leak!
|
||||||
|
const oldTsProgram = this.oldTsProgram;
|
||||||
|
this.oldTsProgram = undefined;
|
||||||
const analyzedFiles: NgAnalyzedFile[] = [];
|
const analyzedFiles: NgAnalyzedFile[] = [];
|
||||||
const codegen = (fileName: string) => {
|
const codegen = (fileName: string) => {
|
||||||
if (this._analyzedModules) {
|
if (this._analyzedModules) {
|
||||||
@ -295,15 +323,13 @@ class AngularCompilerProgram implements Program {
|
|||||||
return this._compiler.emitBasicStubs(analyzedFile);
|
return this._compiler.emitBasicStubs(analyzedFile);
|
||||||
};
|
};
|
||||||
const hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter(
|
const hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter(
|
||||||
this.rootNames, this.options, this.host, this.metadataCache, codegen);
|
this.rootNames, this.options, this.host, this.metadataCache, codegen,
|
||||||
|
this.summariesFromPreviousCompilations);
|
||||||
const aotOptions = getAotCompilerOptions(this.options);
|
const aotOptions = getAotCompilerOptions(this.options);
|
||||||
this._compiler = createAotCompiler(hostAdapter, aotOptions).compiler;
|
this._compiler = createAotCompiler(hostAdapter, aotOptions).compiler;
|
||||||
this._typeCheckHost = hostAdapter;
|
this._typeCheckHost = hostAdapter;
|
||||||
this._structuralDiagnostics = [];
|
this._structuralDiagnostics = [];
|
||||||
|
|
||||||
const oldTsProgram = this.oldProgram ? this.oldProgram.getTsProgram() : undefined;
|
|
||||||
// Note: This is important to not produce a memory leak!
|
|
||||||
this.oldProgram = undefined;
|
|
||||||
const tmpProgram = ts.createProgram(this.rootNames, this.options, hostAdapter, oldTsProgram);
|
const tmpProgram = ts.createProgram(this.rootNames, this.options, hostAdapter, oldTsProgram);
|
||||||
return {tmpProgram, analyzedFiles, hostAdapter};
|
return {tmpProgram, analyzedFiles, hostAdapter};
|
||||||
}
|
}
|
||||||
|
@ -7,84 +7,41 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as ng from '@angular/compiler-cli';
|
import * as ng from '@angular/compiler-cli';
|
||||||
import {makeTempDir} from '@angular/tsc-wrapped/test/test_support';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
function getNgRootDir() {
|
import {TestSupport, expectNoDiagnostics, setup} from '../test_support';
|
||||||
const moduleFilename = module.filename.replace(/\\/g, '/');
|
|
||||||
const distIndex = moduleFilename.indexOf('/dist/all');
|
|
||||||
return moduleFilename.substr(0, distIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ng type checker', () => {
|
describe('ng type checker', () => {
|
||||||
let basePath: string;
|
|
||||||
let write: (fileName: string, content: string) => void;
|
|
||||||
let errorSpy: jasmine.Spy&((s: string) => void);
|
let errorSpy: jasmine.Spy&((s: string) => void);
|
||||||
|
let testSupport: TestSupport;
|
||||||
|
|
||||||
function compileAndCheck(
|
function compileAndCheck(
|
||||||
mockDirs: {[fileName: string]: string}[],
|
mockDirs: {[fileName: string]: string}[],
|
||||||
overrideOptions: ng.CompilerOptions = {}): ng.Diagnostics {
|
overrideOptions: ng.CompilerOptions = {}): ng.Diagnostics {
|
||||||
|
testSupport.writeFiles(...mockDirs);
|
||||||
const fileNames: string[] = [];
|
const fileNames: string[] = [];
|
||||||
mockDirs.forEach((dir) => {
|
mockDirs.forEach((dir) => {
|
||||||
Object.keys(dir).forEach((fileName) => {
|
Object.keys(dir).forEach((fileName) => {
|
||||||
if (fileName.endsWith('.ts')) {
|
if (fileName.endsWith('.ts')) {
|
||||||
fileNames.push(path.resolve(basePath, fileName));
|
fileNames.push(path.resolve(testSupport.basePath, fileName));
|
||||||
}
|
}
|
||||||
write(fileName, dir[fileName]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const options: ng.CompilerOptions = {
|
const options = testSupport.createCompilerOptions(overrideOptions);
|
||||||
basePath,
|
|
||||||
'experimentalDecorators': true,
|
|
||||||
'skipLibCheck': true,
|
|
||||||
'strict': true,
|
|
||||||
'types': [],
|
|
||||||
'outDir': path.resolve(basePath, 'built'),
|
|
||||||
'rootDir': basePath,
|
|
||||||
'baseUrl': basePath,
|
|
||||||
'declaration': true,
|
|
||||||
'target': ts.ScriptTarget.ES5,
|
|
||||||
'module': ts.ModuleKind.ES2015,
|
|
||||||
'moduleResolution': ts.ModuleResolutionKind.NodeJs,
|
|
||||||
'lib': [
|
|
||||||
path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'),
|
|
||||||
path.resolve(basePath, 'node_modules/typescript/lib/lib.dom.d.ts')
|
|
||||||
],
|
|
||||||
'typeRoots': [path.resolve(basePath, 'node_modules/@types')], ...overrideOptions
|
|
||||||
};
|
|
||||||
const {diagnostics} = ng.performCompilation({rootNames: fileNames, options});
|
const {diagnostics} = ng.performCompilation({rootNames: fileNames, options});
|
||||||
return diagnostics;
|
return diagnostics;
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
||||||
basePath = makeTempDir();
|
testSupport = setup();
|
||||||
write = (fileName: string, content: string) => {
|
|
||||||
const dir = path.dirname(fileName);
|
|
||||||
if (dir != '.') {
|
|
||||||
const newDir = path.join(basePath, dir);
|
|
||||||
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir);
|
|
||||||
}
|
|
||||||
fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'});
|
|
||||||
};
|
|
||||||
const ngRootDir = getNgRootDir();
|
|
||||||
const nodeModulesPath = path.resolve(basePath, 'node_modules');
|
|
||||||
fs.mkdirSync(nodeModulesPath);
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.resolve(ngRootDir, 'dist', 'all', '@angular'),
|
|
||||||
path.resolve(nodeModulesPath, '@angular'));
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs'));
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.resolve(ngRootDir, 'node_modules', 'typescript'),
|
|
||||||
path.resolve(nodeModulesPath, 'typescript'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function accept(
|
function accept(
|
||||||
files: {[fileName: string]: string} = {}, overrideOptions: ng.CompilerOptions = {}) {
|
files: {[fileName: string]: string} = {}, overrideOptions: ng.CompilerOptions = {}) {
|
||||||
expectNoDiagnostics(compileAndCheck([QUICKSTART, files], overrideOptions));
|
expectNoDiagnostics({}, compileAndCheck([QUICKSTART, files], overrideOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
function reject(
|
function reject(
|
||||||
@ -193,7 +150,7 @@ describe('ng type checker', () => {
|
|||||||
|
|
||||||
describe('with lowered expressions', () => {
|
describe('with lowered expressions', () => {
|
||||||
it('should not report lowered expressions as errors',
|
it('should not report lowered expressions as errors',
|
||||||
() => { expectNoDiagnostics(compileAndCheck([LOWERING_QUICKSTART])); });
|
() => { expectNoDiagnostics({}, compileAndCheck([LOWERING_QUICKSTART])); });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -284,9 +241,3 @@ const LOWERING_QUICKSTART = {
|
|||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|
||||||
function expectNoDiagnostics(diagnostics: ng.Diagnostics) {
|
|
||||||
if (diagnostics && diagnostics.length) {
|
|
||||||
throw new Error(ng.formatDiagnostics({}, diagnostics));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
165
packages/compiler-cli/test/perform_watch_spec.ts
Normal file
165
packages/compiler-cli/test/perform_watch_spec.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import * as ng from '../index';
|
||||||
|
import {FileChangeEvent, performWatchCompilation} from '../src/perform_watch';
|
||||||
|
|
||||||
|
import {TestSupport, expectNoDiagnostics, setup} from './test_support';
|
||||||
|
|
||||||
|
describe('perform watch', () => {
|
||||||
|
let testSupport: TestSupport;
|
||||||
|
let outDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testSupport = setup();
|
||||||
|
outDir = path.resolve(testSupport.basePath, 'outDir');
|
||||||
|
});
|
||||||
|
|
||||||
|
function createConfig(): ng.ParsedConfiguration {
|
||||||
|
const options = testSupport.createCompilerOptions({outDir});
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')],
|
||||||
|
project: path.resolve(testSupport.basePath, 'src/tsconfig.json'),
|
||||||
|
emitFlags: ng.EmitFlags.Default,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should compile files during the initial run', () => {
|
||||||
|
const config = createConfig();
|
||||||
|
const host = new MockWatchHost(config);
|
||||||
|
|
||||||
|
testSupport.writeFiles({
|
||||||
|
'src/main.ts': createModuleAndCompSource('main'),
|
||||||
|
'src/index.ts': `export * from './main'; `,
|
||||||
|
});
|
||||||
|
|
||||||
|
const watchResult = performWatchCompilation(host);
|
||||||
|
expectNoDiagnostics(config.options, watchResult.firstCompileResult);
|
||||||
|
|
||||||
|
expect(fs.existsSync(path.resolve(outDir, 'src', 'main.ngfactory.js'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache files on subsequent runs', () => {
|
||||||
|
const config = createConfig();
|
||||||
|
const host = new MockWatchHost(config);
|
||||||
|
let fileExistsSpy: jasmine.Spy;
|
||||||
|
let getSourceFileSpy: jasmine.Spy;
|
||||||
|
host.createCompilerHost = (options: ng.CompilerOptions) => {
|
||||||
|
const ngHost = ng.createCompilerHost({options});
|
||||||
|
fileExistsSpy = spyOn(ngHost, 'fileExists').and.callThrough();
|
||||||
|
getSourceFileSpy = spyOn(ngHost, 'getSourceFile').and.callThrough();
|
||||||
|
return ngHost;
|
||||||
|
};
|
||||||
|
|
||||||
|
testSupport.writeFiles({
|
||||||
|
'src/main.ts': createModuleAndCompSource('main'),
|
||||||
|
'src/util.ts': `export const x = 1;`,
|
||||||
|
'src/index.ts': `
|
||||||
|
export * from './main';
|
||||||
|
export * from './util';
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainTsPath = path.resolve(testSupport.basePath, 'src', 'main.ts');
|
||||||
|
const utilTsPath = path.resolve(testSupport.basePath, 'src', 'util.ts');
|
||||||
|
const mainNgFactory = path.resolve(outDir, 'src', 'main.ngfactory.js');
|
||||||
|
performWatchCompilation(host);
|
||||||
|
expect(fs.existsSync(mainNgFactory)).toBe(true);
|
||||||
|
expect(fileExistsSpy !).toHaveBeenCalledWith(mainTsPath);
|
||||||
|
expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath);
|
||||||
|
expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
||||||
|
expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5);
|
||||||
|
|
||||||
|
fileExistsSpy !.calls.reset();
|
||||||
|
getSourceFileSpy !.calls.reset();
|
||||||
|
|
||||||
|
// trigger a single file change
|
||||||
|
// -> all other files should be cached
|
||||||
|
fs.unlinkSync(mainNgFactory);
|
||||||
|
host.triggerFileChange(FileChangeEvent.Change, utilTsPath);
|
||||||
|
|
||||||
|
expect(fs.existsSync(mainNgFactory)).toBe(true);
|
||||||
|
expect(fileExistsSpy !).not.toHaveBeenCalledWith(mainTsPath);
|
||||||
|
expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath);
|
||||||
|
expect(getSourceFileSpy !).not.toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
||||||
|
expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5);
|
||||||
|
|
||||||
|
// trigger a folder change
|
||||||
|
// -> nothing should be cached
|
||||||
|
fs.unlinkSync(mainNgFactory);
|
||||||
|
host.triggerFileChange(
|
||||||
|
FileChangeEvent.CreateDeleteDir, path.resolve(testSupport.basePath, 'src'));
|
||||||
|
|
||||||
|
expect(fs.existsSync(mainNgFactory)).toBe(true);
|
||||||
|
expect(fileExistsSpy !).toHaveBeenCalledWith(mainTsPath);
|
||||||
|
expect(fileExistsSpy !).toHaveBeenCalledWith(utilTsPath);
|
||||||
|
expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
||||||
|
expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createModuleAndCompSource(prefix: string, template: string = prefix + 'template') {
|
||||||
|
const templateEntry =
|
||||||
|
template.endsWith('.html') ? `templateUrl: '${template}'` : `template: \`${template}\``;
|
||||||
|
return `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({selector: '${prefix}', ${templateEntry}})
|
||||||
|
export class ${prefix}Comp {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [${prefix}Comp]})
|
||||||
|
export class ${prefix}Module {}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockWatchHost {
|
||||||
|
timeoutListeners: Array<(() => void)|null> = [];
|
||||||
|
fileChangeListeners: Array<((event: FileChangeEvent, fileName: string) => void)|null> = [];
|
||||||
|
diagnostics: ng.Diagnostics = [];
|
||||||
|
constructor(public config: ng.ParsedConfiguration) {}
|
||||||
|
|
||||||
|
reportDiagnostics(diags: ng.Diagnostics) { this.diagnostics.push(...diags); }
|
||||||
|
readConfiguration() { return this.config; }
|
||||||
|
createCompilerHost(options: ng.CompilerOptions) { return ng.createCompilerHost({options}); };
|
||||||
|
createEmitCallback() { return undefined; }
|
||||||
|
onFileChange(
|
||||||
|
options: ng.CompilerOptions, listener: (event: FileChangeEvent, fileName: string) => void,
|
||||||
|
ready: () => void) {
|
||||||
|
const id = this.fileChangeListeners.length;
|
||||||
|
this.fileChangeListeners.push(listener);
|
||||||
|
ready();
|
||||||
|
return {
|
||||||
|
close: () => this.fileChangeListeners[id] = null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setTimeout(callback: () => void, ms: number): any {
|
||||||
|
const id = this.timeoutListeners.length;
|
||||||
|
this.timeoutListeners.push(callback);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
clearTimeout(timeoutId: any): void { this.timeoutListeners[timeoutId] = null; }
|
||||||
|
flushTimeouts() {
|
||||||
|
this.timeoutListeners.forEach(cb => {
|
||||||
|
if (cb) cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
triggerFileChange(event: FileChangeEvent, fileName: string) {
|
||||||
|
this.fileChangeListeners.forEach(listener => {
|
||||||
|
if (listener) {
|
||||||
|
listener(event, fileName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.flushTimeouts();
|
||||||
|
}
|
||||||
|
}
|
@ -9,9 +9,17 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import * as ng from '../index';
|
||||||
|
|
||||||
const tmpdir = process.env.TEST_TMPDIR || os.tmpdir();
|
const tmpdir = process.env.TEST_TMPDIR || os.tmpdir();
|
||||||
|
|
||||||
|
function getNgRootDir() {
|
||||||
|
const moduleFilename = module.filename.replace(/\\/g, '/');
|
||||||
|
const distIndex = moduleFilename.indexOf('/dist/all');
|
||||||
|
return moduleFilename.substr(0, distIndex);
|
||||||
|
}
|
||||||
|
|
||||||
export function writeTempFile(name: string, contents: string): string {
|
export function writeTempFile(name: string, contents: string): string {
|
||||||
// TEST_TMPDIR is set by bazel.
|
// TEST_TMPDIR is set by bazel.
|
||||||
const id = (Math.random() * 1000000).toFixed(0);
|
const id = (Math.random() * 1000000).toFixed(0);
|
||||||
@ -26,3 +34,90 @@ export function makeTempDir(): string {
|
|||||||
fs.mkdirSync(dir);
|
fs.mkdirSync(dir);
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TestSupport {
|
||||||
|
basePath: string;
|
||||||
|
write(fileName: string, content: string): void;
|
||||||
|
writeFiles(...mockDirs: {[fileName: string]: string}[]): void;
|
||||||
|
createCompilerOptions(overrideOptions?: ng.CompilerOptions): ng.CompilerOptions;
|
||||||
|
shouldExist(fileName: string): void;
|
||||||
|
shouldNotExist(fileName: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setup(): TestSupport {
|
||||||
|
const basePath = makeTempDir();
|
||||||
|
|
||||||
|
const ngRootDir = getNgRootDir();
|
||||||
|
const nodeModulesPath = path.resolve(basePath, 'node_modules');
|
||||||
|
fs.mkdirSync(nodeModulesPath);
|
||||||
|
fs.symlinkSync(
|
||||||
|
path.resolve(ngRootDir, 'dist', 'all', '@angular'),
|
||||||
|
path.resolve(nodeModulesPath, '@angular'));
|
||||||
|
fs.symlinkSync(
|
||||||
|
path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs'));
|
||||||
|
fs.symlinkSync(
|
||||||
|
path.resolve(ngRootDir, 'node_modules', 'typescript'),
|
||||||
|
path.resolve(nodeModulesPath, 'typescript'));
|
||||||
|
|
||||||
|
return {basePath, write, writeFiles, createCompilerOptions, shouldExist, shouldNotExist};
|
||||||
|
|
||||||
|
function write(fileName: string, content: string) {
|
||||||
|
const dir = path.dirname(fileName);
|
||||||
|
if (dir != '.') {
|
||||||
|
const newDir = path.join(basePath, dir);
|
||||||
|
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeFiles(...mockDirs: {[fileName: string]: string}[]) {
|
||||||
|
mockDirs.forEach(
|
||||||
|
(dir) => { Object.keys(dir).forEach((fileName) => { write(fileName, dir[fileName]); }); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCompilerOptions(overrideOptions: ng.CompilerOptions = {}): ng.CompilerOptions {
|
||||||
|
return {
|
||||||
|
basePath,
|
||||||
|
'experimentalDecorators': true,
|
||||||
|
'skipLibCheck': true,
|
||||||
|
'strict': true,
|
||||||
|
'types': [],
|
||||||
|
'outDir': path.resolve(basePath, 'built'),
|
||||||
|
'rootDir': basePath,
|
||||||
|
'baseUrl': basePath,
|
||||||
|
'declaration': true,
|
||||||
|
'target': ts.ScriptTarget.ES5,
|
||||||
|
'module': ts.ModuleKind.ES2015,
|
||||||
|
'moduleResolution': ts.ModuleResolutionKind.NodeJs,
|
||||||
|
'lib': [
|
||||||
|
path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'),
|
||||||
|
],
|
||||||
|
...overrideOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldExist(fileName: string) {
|
||||||
|
if (!fs.existsSync(path.resolve(basePath, fileName))) {
|
||||||
|
throw new Error(`Expected ${fileName} to be emitted (basePath: ${basePath})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldNotExist(fileName: string) {
|
||||||
|
if (fs.existsSync(path.resolve(basePath, fileName))) {
|
||||||
|
throw new Error(`Did not expect ${fileName} to be emitted (basePath: ${basePath})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expectNoDiagnostics(options: ng.CompilerOptions, diags: ng.Diagnostics) {
|
||||||
|
if (diags.length) {
|
||||||
|
throw new Error(`Expected no diagnostics: ${ng.formatDiagnostics(options, diags)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expectNoDiagnosticsInProgram(options: ng.CompilerOptions, p: ng.Program) {
|
||||||
|
expectNoDiagnostics(options, [
|
||||||
|
...p.getNgStructuralDiagnostics(), ...p.getTsSemanticDiagnostics(),
|
||||||
|
...p.getNgSemanticDiagnostics()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
@ -39,7 +39,7 @@ describe('NgCompilerHost', () => {
|
|||||||
ngHost = createNgHost({files}),
|
ngHost = createNgHost({files}),
|
||||||
}: {files?: Directory, options?: CompilerOptions, ngHost?: CompilerHost} = {}) {
|
}: {files?: Directory, options?: CompilerOptions, ngHost?: CompilerHost} = {}) {
|
||||||
return new TsCompilerAotCompilerTypeCheckHostAdapter(
|
return new TsCompilerAotCompilerTypeCheckHostAdapter(
|
||||||
['/tmp/index.ts'], options, ngHost, new MetadataCollector(), codeGenerator);
|
['/tmp/index.ts'], options, ngHost, new MetadataCollector(), codeGenerator, new Map());
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('fileNameToModuleName', () => {
|
describe('fileNameToModuleName', () => {
|
||||||
|
@ -7,288 +7,205 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as ng from '@angular/compiler-cli';
|
import * as ng from '@angular/compiler-cli';
|
||||||
import {makeTempDir} from '@angular/tsc-wrapped/test/test_support';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {StructureIsReused, tsStructureIsReused} from '../../src/transformers/util';
|
import {CompilerHost} from '../../src/transformers/api';
|
||||||
|
import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from '../../src/transformers/util';
|
||||||
function getNgRootDir() {
|
import {TestSupport, expectNoDiagnosticsInProgram, setup} from '../test_support';
|
||||||
const moduleFilename = module.filename.replace(/\\/g, '/');
|
|
||||||
const distIndex = moduleFilename.indexOf('/dist/all');
|
|
||||||
return moduleFilename.substr(0, distIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('ng program', () => {
|
describe('ng program', () => {
|
||||||
let basePath: string;
|
let testSupport: TestSupport;
|
||||||
let write: (fileName: string, content: string) => void;
|
|
||||||
let errorSpy: jasmine.Spy&((s: string) => void);
|
let errorSpy: jasmine.Spy&((s: string) => void);
|
||||||
|
|
||||||
function writeFiles(...mockDirs: {[fileName: string]: string}[]) {
|
|
||||||
mockDirs.forEach(
|
|
||||||
(dir) => { Object.keys(dir).forEach((fileName) => { write(fileName, dir[fileName]); }); });
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCompilerOptions(overrideOptions: ng.CompilerOptions = {}): ng.CompilerOptions {
|
|
||||||
return {
|
|
||||||
basePath,
|
|
||||||
'experimentalDecorators': true,
|
|
||||||
'skipLibCheck': true,
|
|
||||||
'strict': true,
|
|
||||||
'types': [],
|
|
||||||
'outDir': path.resolve(basePath, 'built'),
|
|
||||||
'rootDir': basePath,
|
|
||||||
'baseUrl': basePath,
|
|
||||||
'declaration': true,
|
|
||||||
'target': ts.ScriptTarget.ES5,
|
|
||||||
'module': ts.ModuleKind.ES2015,
|
|
||||||
'moduleResolution': ts.ModuleResolutionKind.NodeJs,
|
|
||||||
'lib': [
|
|
||||||
path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'),
|
|
||||||
path.resolve(basePath, 'node_modules/typescript/lib/lib.dom.d.ts')
|
|
||||||
],
|
|
||||||
'typeRoots': [path.resolve(basePath, 'node_modules/@types')], ...overrideOptions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectNoDiagnostics(options: ng.CompilerOptions, p: ng.Program) {
|
|
||||||
const diags: ng.Diagnostics =
|
|
||||||
[...p.getTsSemanticDiagnostics(), ...p.getNgSemanticDiagnostics()];
|
|
||||||
if (diags.length > 0) {
|
|
||||||
console.error('Diagnostics: ' + ng.formatDiagnostics(options, diags));
|
|
||||||
throw new Error('Expected no diagnostics.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
|
||||||
basePath = makeTempDir();
|
testSupport = setup();
|
||||||
write = (fileName: string, content: string) => {
|
|
||||||
const dir = path.dirname(fileName);
|
|
||||||
if (dir != '.') {
|
|
||||||
const newDir = path.join(basePath, dir);
|
|
||||||
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir);
|
|
||||||
}
|
|
||||||
fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'});
|
|
||||||
};
|
|
||||||
const ngRootDir = getNgRootDir();
|
|
||||||
const nodeModulesPath = path.resolve(basePath, 'node_modules');
|
|
||||||
fs.mkdirSync(nodeModulesPath);
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.resolve(ngRootDir, 'dist', 'all', '@angular'),
|
|
||||||
path.resolve(nodeModulesPath, '@angular'));
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs'));
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.resolve(ngRootDir, 'node_modules', 'typescript'),
|
|
||||||
path.resolve(nodeModulesPath, 'typescript'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('reuse of old ts program', () => {
|
function createModuleAndCompSource(prefix: string, template: string = prefix + 'template') {
|
||||||
const files = {
|
const templateEntry =
|
||||||
'src/util.ts': `export const x = 1;`,
|
template.endsWith('.html') ? `templateUrl: '${template}'` : `template: \`${template}\``;
|
||||||
'src/main.ts': `
|
return `
|
||||||
import {NgModule, Component} from '@angular/core';
|
import {Component, NgModule} from '@angular/core';
|
||||||
import {x} from './util';
|
|
||||||
|
|
||||||
@Component({selector: 'comp', templateUrl: './main.html'})
|
@Component({selector: '${prefix}', ${templateEntry}})
|
||||||
export class MyComp {}
|
export class ${prefix}Comp {}
|
||||||
|
|
||||||
@NgModule()
|
@NgModule({declarations: [${prefix}Comp]})
|
||||||
export class MyModule {}
|
export class ${prefix}Module {}
|
||||||
`,
|
`;
|
||||||
'src/main.html': `Hello world`,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
function expectResuse(newFiles: {[fileName: string]: string}, reuseLevel: StructureIsReused) {
|
describe('reuse of old program', () => {
|
||||||
writeFiles(files);
|
|
||||||
|
|
||||||
const options1 = createCompilerOptions();
|
function compileLib(libName: string) {
|
||||||
const host1 = ng.createCompilerHost({options: options1});
|
testSupport.writeFiles({
|
||||||
const rootNames1 = [path.resolve(basePath, 'src/main.ts')];
|
[`${libName}_src/index.ts`]: createModuleAndCompSource(libName),
|
||||||
|
});
|
||||||
const p1 = ng.createProgram({rootNames: rootNames1, options: options1, host: host1});
|
const options = testSupport.createCompilerOptions({
|
||||||
expectNoDiagnostics(options1, p1);
|
skipTemplateCodegen: true,
|
||||||
|
});
|
||||||
// Note: we recreate the options, rootNames and the host
|
const program = ng.createProgram({
|
||||||
// to check that TS checks against values, and not references!
|
rootNames: [path.resolve(testSupport.basePath, `${libName}_src/index.ts`)],
|
||||||
writeFiles(newFiles);
|
options,
|
||||||
const options2 = {...options1};
|
host: ng.createCompilerHost({options}),
|
||||||
const host2 = ng.createCompilerHost({options: options2});
|
});
|
||||||
const rootNames2 = [...rootNames1];
|
expectNoDiagnosticsInProgram(options, program);
|
||||||
|
fs.symlinkSync(
|
||||||
const p2 =
|
path.resolve(testSupport.basePath, 'built', `${libName}_src`),
|
||||||
ng.createProgram({rootNames: rootNames2, options: options2, host: host2, oldProgram: p1});
|
path.resolve(testSupport.basePath, 'node_modules', libName));
|
||||||
expectNoDiagnostics(options1, p2);
|
program.emit({emitFlags: ng.EmitFlags.DTS | ng.EmitFlags.JS | ng.EmitFlags.Metadata});
|
||||||
|
|
||||||
expect(tsStructureIsReused(p1.getTsProgram())).toBe(reuseLevel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should reuse completely if nothing changed',
|
function compile(oldProgram?: ng.Program): ng.Program {
|
||||||
() => { expectResuse({}, StructureIsReused.Completely); });
|
const options = testSupport.createCompilerOptions();
|
||||||
|
const rootNames = [path.resolve(testSupport.basePath, 'src/index.ts')];
|
||||||
|
|
||||||
it('should resuse if a template or a ts file changed', () => {
|
const program = ng.createProgram({
|
||||||
expectResuse(
|
rootNames: rootNames,
|
||||||
{
|
options: testSupport.createCompilerOptions(),
|
||||||
'src/main.html': `Some other text`,
|
host: ng.createCompilerHost({options}), oldProgram,
|
||||||
'src/util.ts': `export const x = 2;`,
|
});
|
||||||
},
|
expectNoDiagnosticsInProgram(options, program);
|
||||||
StructureIsReused.Completely);
|
program.emit();
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should reuse generated code for libraries from old programs', () => {
|
||||||
|
compileLib('lib');
|
||||||
|
testSupport.writeFiles({
|
||||||
|
'src/main.ts': createModuleAndCompSource('main'),
|
||||||
|
'src/index.ts': `
|
||||||
|
export * from './main';
|
||||||
|
export * from 'lib/index';
|
||||||
|
`
|
||||||
|
});
|
||||||
|
const p1 = compile();
|
||||||
|
expect(p1.getTsProgram().getSourceFiles().some(
|
||||||
|
sf => /node_modules\/lib\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
||||||
|
.toBe(true);
|
||||||
|
expect(p1.getTsProgram().getSourceFiles().some(
|
||||||
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||||
|
.toBe(false);
|
||||||
|
const p2 = compile(p1);
|
||||||
|
expect(p2.getTsProgram().getSourceFiles().some(
|
||||||
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||||
|
.toBe(false);
|
||||||
|
expect(p2.getTsProgram().getSourceFiles().some(
|
||||||
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||||
|
.toBe(false);
|
||||||
|
|
||||||
|
// import a library for which we didn't generate code before
|
||||||
|
compileLib('lib2');
|
||||||
|
testSupport.writeFiles({
|
||||||
|
'src/index.ts': `
|
||||||
|
export * from './main';
|
||||||
|
export * from 'lib/index';
|
||||||
|
export * from 'lib2/index';
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
const p3 = compile(p2);
|
||||||
|
expect(p3.getTsProgram().getSourceFiles().some(
|
||||||
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||||
|
.toBe(false);
|
||||||
|
expect(p3.getTsProgram().getSourceFiles().some(
|
||||||
|
sf => /node_modules\/lib2\/.*\.ngfactory\.ts$/.test(sf.fileName)))
|
||||||
|
.toBe(true);
|
||||||
|
|
||||||
|
const p4 = compile(p3);
|
||||||
|
expect(p4.getTsProgram().getSourceFiles().some(
|
||||||
|
sf => /node_modules\/lib\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||||
|
.toBe(false);
|
||||||
|
expect(p4.getTsProgram().getSourceFiles().some(
|
||||||
|
sf => /node_modules\/lib2\/.*\.ngfactory.*$/.test(sf.fileName)))
|
||||||
|
.toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not reuse if an import changed', () => {
|
it('should reuse the old ts program completely if nothing changed', () => {
|
||||||
expectResuse(
|
testSupport.writeFiles({'src/index.ts': createModuleAndCompSource('main')});
|
||||||
{
|
// Note: the second compile drops factories for library files,
|
||||||
'src/util.ts': `
|
// and therefore changes the structure again
|
||||||
import {Injectable} from '@angular/core';
|
const p1 = compile();
|
||||||
export const x = 2;
|
const p2 = compile(p1);
|
||||||
`,
|
const p3 = compile(p2);
|
||||||
},
|
expect(tsStructureIsReused(p2.getTsProgram())).toBe(StructureIsReused.Completely);
|
||||||
StructureIsReused.SafeModules);
|
});
|
||||||
|
|
||||||
|
it('should reuse the old ts program completely if a template or a ts file changed', () => {
|
||||||
|
testSupport.writeFiles({
|
||||||
|
'src/main.ts': createModuleAndCompSource('main', 'main.html'),
|
||||||
|
'src/main.html': `Some template`,
|
||||||
|
'src/util.ts': `export const x = 1`,
|
||||||
|
'src/index.ts': `
|
||||||
|
export * from './main';
|
||||||
|
export * from './util';
|
||||||
|
`
|
||||||
|
});
|
||||||
|
// Note: the second compile drops factories for library files,
|
||||||
|
// and therefore changes the structure again
|
||||||
|
const p1 = compile();
|
||||||
|
const p2 = compile(p1);
|
||||||
|
testSupport.writeFiles({
|
||||||
|
'src/main.html': `Another template`,
|
||||||
|
'src/util.ts': `export const x = 2`,
|
||||||
|
});
|
||||||
|
const p3 = compile(p2);
|
||||||
|
expect(tsStructureIsReused(p2.getTsProgram())).toBe(StructureIsReused.Completely);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not reuse the old ts program if an import changed', () => {
|
||||||
|
testSupport.writeFiles({
|
||||||
|
'src/main.ts': createModuleAndCompSource('main'),
|
||||||
|
'src/util.ts': `export const x = 1`,
|
||||||
|
'src/index.ts': `
|
||||||
|
export * from './main';
|
||||||
|
export * from './util';
|
||||||
|
`
|
||||||
|
});
|
||||||
|
// Note: the second compile drops factories for library files,
|
||||||
|
// and therefore changes the structure again
|
||||||
|
const p1 = compile();
|
||||||
|
const p2 = compile(p1);
|
||||||
|
testSupport.writeFiles(
|
||||||
|
{'src/util.ts': `import {Injectable} from '@angular/core'; export const x = 1;`});
|
||||||
|
const p3 = compile(p2);
|
||||||
|
expect(tsStructureIsReused(p2.getTsProgram())).toBe(StructureIsReused.SafeModules);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should typecheck templates even if skipTemplateCodegen is set', () => {
|
it('should typecheck templates even if skipTemplateCodegen is set', () => {
|
||||||
writeFiles({
|
testSupport.writeFiles({
|
||||||
'src/main.ts': `
|
'src/main.ts': createModuleAndCompSource('main', `{{nonExistent}}`),
|
||||||
import {NgModule, Component} from '@angular/core';
|
|
||||||
|
|
||||||
@Component({selector: 'mycomp', template: '{{nonExistent}}'})
|
|
||||||
export class MyComp {}
|
|
||||||
|
|
||||||
@NgModule({declarations: [MyComp]})
|
|
||||||
export class MyModule {}
|
|
||||||
`
|
|
||||||
});
|
});
|
||||||
const options = createCompilerOptions({skipTemplateCodegen: true});
|
const options = testSupport.createCompilerOptions({skipTemplateCodegen: true});
|
||||||
const host = ng.createCompilerHost({options});
|
const host = ng.createCompilerHost({options});
|
||||||
const program =
|
const program = ng.createProgram(
|
||||||
ng.createProgram({rootNames: [path.resolve(basePath, 'src/main.ts')], options, host});
|
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
||||||
const diags = program.getNgSemanticDiagnostics();
|
const diags = program.getNgSemanticDiagnostics();
|
||||||
expect(diags.length).toBe(1);
|
expect(diags.length).toBe(1);
|
||||||
expect(diags[0].messageText).toBe(`Property 'nonExistent' does not exist on type 'MyComp'.`);
|
expect(diags[0].messageText).toBe(`Property 'nonExistent' does not exist on type 'mainComp'.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to use asynchronously loaded resources', (done) => {
|
it('should be able to use asynchronously loaded resources', (done) => {
|
||||||
writeFiles({
|
testSupport.writeFiles({
|
||||||
'src/main.ts': `
|
'src/main.ts': createModuleAndCompSource('main', 'main.html'),
|
||||||
import {NgModule, Component} from '@angular/core';
|
|
||||||
|
|
||||||
@Component({selector: 'mycomp', templateUrl: './main.html'})
|
|
||||||
export class MyComp {}
|
|
||||||
|
|
||||||
@NgModule({declarations: [MyComp]})
|
|
||||||
export class MyModule {}
|
|
||||||
`,
|
|
||||||
// Note: we need to be able to resolve the template synchronously,
|
// Note: we need to be able to resolve the template synchronously,
|
||||||
// only the content is delivered asynchronously.
|
// only the content is delivered asynchronously.
|
||||||
'src/main.html': '',
|
'src/main.html': '',
|
||||||
});
|
});
|
||||||
const options = createCompilerOptions();
|
const options = testSupport.createCompilerOptions();
|
||||||
const host = ng.createCompilerHost({options});
|
const host = ng.createCompilerHost({options});
|
||||||
host.readResource = () => Promise.resolve('Hello world!');
|
host.readResource = () => Promise.resolve('Hello world!');
|
||||||
const program =
|
const program = ng.createProgram(
|
||||||
ng.createProgram({rootNames: [path.resolve(basePath, 'src/main.ts')], options, host});
|
{rootNames: [path.resolve(testSupport.basePath, 'src/main.ts')], options, host});
|
||||||
program.loadNgStructureAsync().then(() => {
|
program.loadNgStructureAsync().then(() => {
|
||||||
program.emit();
|
program.emit();
|
||||||
const factory = fs.readFileSync(path.resolve(basePath, 'built/src/main.ngfactory.js'));
|
const factory =
|
||||||
|
fs.readFileSync(path.resolve(testSupport.basePath, 'built/src/main.ngfactory.js'));
|
||||||
expect(factory).toContain('Hello world!');
|
expect(factory).toContain('Hello world!');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function appComponentSource(): string {
|
|
||||||
return `
|
|
||||||
import {Component, Pipe, Directive} from '@angular/core';
|
|
||||||
|
|
||||||
export interface Person {
|
|
||||||
name: string;
|
|
||||||
address: Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Address {
|
|
||||||
street: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
zip: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
templateUrl: './app.component.html'
|
|
||||||
})
|
|
||||||
export class AppComponent {
|
|
||||||
name = 'Angular';
|
|
||||||
person: Person;
|
|
||||||
people: Person[];
|
|
||||||
maybePerson?: Person;
|
|
||||||
|
|
||||||
getName(): string { return this.name; }
|
|
||||||
getPerson(): Person { return this.person; }
|
|
||||||
getMaybePerson(): Person | undefined { return this.maybePerson; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Pipe({
|
|
||||||
name: 'aPipe',
|
|
||||||
})
|
|
||||||
export class APipe {
|
|
||||||
transform(n: number): number { return n + 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Directive({
|
|
||||||
selector: '[aDir]',
|
|
||||||
exportAs: 'aDir'
|
|
||||||
})
|
|
||||||
export class ADirective {
|
|
||||||
name = 'ADirective';
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QUICKSTART = {
|
|
||||||
'src/app.component.ts': appComponentSource(),
|
|
||||||
'src/app.component.html': '<h1>Hello {{name}}</h1>',
|
|
||||||
'src/app.module.ts': `
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { AppComponent, APipe, ADirective } from './app.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [ AppComponent, APipe, ADirective ],
|
|
||||||
bootstrap: [ AppComponent ]
|
|
||||||
})
|
|
||||||
export class AppModule { }
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
const LOWERING_QUICKSTART = {
|
|
||||||
'src/app.component.ts': appComponentSource(),
|
|
||||||
'src/app.component.html': '<h1>Hello {{name}}</h1>',
|
|
||||||
'src/app.module.ts': `
|
|
||||||
import { NgModule, Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { AppComponent, APipe, ADirective } from './app.component';
|
|
||||||
|
|
||||||
class Foo {}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
template: '',
|
|
||||||
providers: [
|
|
||||||
{provide: 'someToken', useFactory: () => new Foo()}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class Bar {}
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [ AppComponent, APipe, ADirective, Bar ],
|
|
||||||
bootstrap: [ AppComponent ]
|
|
||||||
})
|
|
||||||
export class AppModule { }
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
function expectNoDiagnostics(diagnostics: ng.Diagnostics) {
|
|
||||||
if (diagnostics && diagnostics.length) {
|
|
||||||
throw new Error(ng.formatDiagnostics({}, diagnostics));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user