feat(compiler-cli): add watch mode to `ngc` (#18818)
With this change ngc now accepts a `-w` or a `--watch` command-line option that will automatically perform a recompile whenever any source files change on disk. PR Close #18818
This commit is contained in:
parent
c685cc2f0a
commit
06d01b2287
|
@ -275,6 +275,9 @@
|
||||||
"@types/base64-js": {
|
"@types/base64-js": {
|
||||||
"version": "1.2.5"
|
"version": "1.2.5"
|
||||||
},
|
},
|
||||||
|
"@types/chokidar": {
|
||||||
|
"version": "1.7.2"
|
||||||
|
},
|
||||||
"@types/fs-extra": {
|
"@types/fs-extra": {
|
||||||
"version": "0.0.22-alpha"
|
"version": "0.0.22-alpha"
|
||||||
},
|
},
|
||||||
|
|
|
@ -445,6 +445,11 @@
|
||||||
"from": "@types/base64-js@latest",
|
"from": "@types/base64-js@latest",
|
||||||
"resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.2.5.tgz"
|
"resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.2.5.tgz"
|
||||||
},
|
},
|
||||||
|
"@types/chokidar": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"from": "@types/chokidar@>=1.1.0 <2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chokidar/-/chokidar-1.7.2.tgz"
|
||||||
|
},
|
||||||
"@types/fs-extra": {
|
"@types/fs-extra": {
|
||||||
"version": "0.0.22-alpha",
|
"version": "0.0.22-alpha",
|
||||||
"from": "@types/fs-extra@latest",
|
"from": "@types/fs-extra@latest",
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"@bazel/typescript": "0.0.7",
|
"@bazel/typescript": "0.0.7",
|
||||||
"@types/angularjs": "^1.5.13-alpha",
|
"@types/angularjs": "^1.5.13-alpha",
|
||||||
"@types/base64-js": "^1.2.5",
|
"@types/base64-js": "^1.2.5",
|
||||||
|
"@types/chokidar": "^1.1.0",
|
||||||
"@types/fs-extra": "0.0.22-alpha",
|
"@types/fs-extra": "0.0.22-alpha",
|
||||||
"@types/hammerjs": "^2.0.33",
|
"@types/hammerjs": "^2.0.33",
|
||||||
"@types/jasmine": "^2.2.22-alpha",
|
"@types/jasmine": "^2.2.22-alpha",
|
||||||
|
|
|
@ -20,7 +20,7 @@ export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Sp
|
||||||
export * from './src/transformers/api';
|
export * from './src/transformers/api';
|
||||||
export * from './src/transformers/entry_points';
|
export * from './src/transformers/entry_points';
|
||||||
|
|
||||||
export {performCompilation, readConfiguration, formatDiagnostics, calcProjectFileAndBasePath, createNgCompilerOptions} from './src/perform-compile';
|
export {performCompilation, readConfiguration, formatDiagnostics, calcProjectFileAndBasePath, createNgCompilerOptions} from './src/perform_compile';
|
||||||
|
|
||||||
// TODO(hansl): moving to Angular 4 need to update this API.
|
// TODO(hansl): moving to Angular 4 need to update this API.
|
||||||
export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api';
|
export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api';
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
"@angular/tsc-wrapped": "5.0.0-beta.5",
|
"@angular/tsc-wrapped": "5.0.0-beta.5",
|
||||||
"reflect-metadata": "^0.1.2",
|
"reflect-metadata": "^0.1.2",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"tsickle": "^0.23.4"
|
"tsickle": "^0.23.4",
|
||||||
|
"chokidar": "^1.4.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^2.0.2",
|
"typescript": "^2.0.2",
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, NgAnalyzedModules, ParseSourceSpan, Statement, StaticReflector, TypeScriptEmitter, createAotCompiler} from '@angular/compiler';
|
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, NgAnalyzedModules, ParseSourceSpan, Statement, StaticReflector, TypeScriptEmitter, createAotCompiler} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {Diagnostic} from '../transformers/api';
|
import {DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from '../transformers/api';
|
||||||
|
|
||||||
interface FactoryInfo {
|
interface FactoryInfo {
|
||||||
source: ts.SourceFile;
|
source: ts.SourceFile;
|
||||||
|
@ -142,8 +142,10 @@ export class TypeChecker {
|
||||||
const fileName = span.start.file.url;
|
const fileName = span.start.file.url;
|
||||||
const diagnosticsList = diagnosticsFor(fileName);
|
const diagnosticsList = diagnosticsFor(fileName);
|
||||||
diagnosticsList.push({
|
diagnosticsList.push({
|
||||||
message: diagnosticMessageToString(diagnostic.messageText),
|
messageText: diagnosticMessageToString(diagnostic.messageText),
|
||||||
category: diagnostic.category, span
|
category: diagnostic.category, span,
|
||||||
|
source: SOURCE,
|
||||||
|
code: DEFAULT_ERROR_CODE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,19 @@ import * as path from 'path';
|
||||||
import * as tsickle from 'tsickle';
|
import * as tsickle from 'tsickle';
|
||||||
import * as api from './transformers/api';
|
import * as api from './transformers/api';
|
||||||
import * as ngc from './transformers/entry_points';
|
import * as ngc from './transformers/entry_points';
|
||||||
import {performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration} from './perform-compile';
|
|
||||||
|
|
||||||
|
import {calcProjectFileAndBasePath, exitCodeFromResult, performCompilation, readConfiguration, formatDiagnostics, Diagnostics, ParsedConfiguration, PerformCompilationResult} from './perform_compile';
|
||||||
|
import {performWatchCompilation, createPerformWatchHost} from './perform_watch';
|
||||||
import {isSyntaxError} from '@angular/compiler';
|
import {isSyntaxError} from '@angular/compiler';
|
||||||
import {CodeGenerator} from './codegen';
|
import {CodeGenerator} from './codegen';
|
||||||
|
|
||||||
export function main(
|
export function main(
|
||||||
args: string[], consoleError: (s: string) => void = console.error): Promise<number> {
|
args: string[], consoleError: (s: string) => void = console.error): Promise<number> {
|
||||||
const parsedArgs = require('minimist')(args);
|
const parsedArgs = require('minimist')(args);
|
||||||
|
if (parsedArgs.w || parsedArgs.watch) {
|
||||||
|
const result = watchMode(parsedArgs, consoleError);
|
||||||
|
return Promise.resolve(exitCodeFromResult(result.firstCompileResult));
|
||||||
|
}
|
||||||
const {rootNames, options, errors: configErrors} = readCommandLineAndConfiguration(parsedArgs);
|
const {rootNames, options, errors: configErrors} = readCommandLineAndConfiguration(parsedArgs);
|
||||||
if (configErrors.length) {
|
if (configErrors.length) {
|
||||||
return Promise.resolve(reportErrorsAndExit(options, configErrors, consoleError));
|
return Promise.resolve(reportErrorsAndExit(options, configErrors, consoleError));
|
||||||
|
@ -83,12 +88,16 @@ function createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function projectOf(args: any): string {
|
||||||
|
return (args && (args.p || args.project)) || '.';
|
||||||
|
}
|
||||||
|
|
||||||
function readCommandLineAndConfiguration(args: any): ParsedConfiguration {
|
function readCommandLineAndConfiguration(args: any): ParsedConfiguration {
|
||||||
const project = args.p || args.project || '.';
|
const project = projectOf(args);
|
||||||
const allDiagnostics: Diagnostics = [];
|
const allDiagnostics: Diagnostics = [];
|
||||||
const config = readConfiguration(project);
|
const config = readConfiguration(project);
|
||||||
const options = mergeCommandLineParams(args, config.options);
|
const options = mergeCommandLineParams(args, config.options);
|
||||||
return {rootNames: config.rootNames, options, errors: config.errors};
|
return {project, rootNames: config.rootNames, options, errors: config.errors};
|
||||||
}
|
}
|
||||||
|
|
||||||
function reportErrorsAndExit(
|
function reportErrorsAndExit(
|
||||||
|
@ -101,6 +110,15 @@ function reportErrorsAndExit(
|
||||||
return exitCode;
|
return exitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function watchMode(args: any, consoleError: (s: string) => void) {
|
||||||
|
const project = projectOf(args);
|
||||||
|
const {projectFile, basePath} = calcProjectFileAndBasePath(project);
|
||||||
|
const config = readConfiguration(project);
|
||||||
|
return performWatchCompilation(createPerformWatchHost(projectFile, diagnostics => {
|
||||||
|
consoleError(formatDiagnostics(config.options, diagnostics));
|
||||||
|
}, options => createEmitCallback(options)));
|
||||||
|
}
|
||||||
|
|
||||||
function mergeCommandLineParams(
|
function mergeCommandLineParams(
|
||||||
cliArgs: {[k: string]: string}, options: api.CompilerOptions): api.CompilerOptions {
|
cliArgs: {[k: string]: string}, options: api.CompilerOptions): api.CompilerOptions {
|
||||||
// TODO: also merge in tsc command line parameters by calling
|
// TODO: also merge in tsc command line parameters by calling
|
||||||
|
|
|
@ -20,7 +20,7 @@ const TS_EXT = /\.ts$/;
|
||||||
export type Diagnostics = Array<ts.Diagnostic|api.Diagnostic>;
|
export type Diagnostics = Array<ts.Diagnostic|api.Diagnostic>;
|
||||||
|
|
||||||
function isTsDiagnostic(diagnostic: any): diagnostic is ts.Diagnostic {
|
function isTsDiagnostic(diagnostic: any): diagnostic is ts.Diagnostic {
|
||||||
return diagnostic && (diagnostic.file || diagnostic.messageText);
|
return diagnostic && diagnostic.source != 'angular';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDiagnostics(options: api.CompilerOptions, diags: Diagnostics): string {
|
export function formatDiagnostics(options: api.CompilerOptions, diags: Diagnostics): string {
|
||||||
|
@ -41,9 +41,9 @@ export function formatDiagnostics(options: api.CompilerOptions, diags: Diagnosti
|
||||||
` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`;
|
` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`;
|
||||||
}
|
}
|
||||||
if (d.span && d.span.details) {
|
if (d.span && d.span.details) {
|
||||||
res += `: ${d.span.details}, ${d.message}\n`;
|
res += `: ${d.span.details}, ${d.messageText}\n`;
|
||||||
} else {
|
} else {
|
||||||
res += `: ${d.message}\n`;
|
res += `: ${d.messageText}\n`;
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@ export function formatDiagnostics(options: api.CompilerOptions, diags: Diagnosti
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParsedConfiguration {
|
export interface ParsedConfiguration {
|
||||||
|
project: string;
|
||||||
options: api.CompilerOptions;
|
options: api.CompilerOptions;
|
||||||
rootNames: string[];
|
rootNames: string[];
|
||||||
errors: Diagnostics;
|
errors: Diagnostics;
|
||||||
|
@ -81,7 +82,7 @@ export function readConfiguration(
|
||||||
let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile);
|
let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return {errors: [error], rootNames: [], options: {}};
|
return {project, errors: [error], rootNames: [], options: {}};
|
||||||
}
|
}
|
||||||
const parseConfigHost = {
|
const parseConfigHost = {
|
||||||
useCaseSensitiveFileNames: true,
|
useCaseSensitiveFileNames: true,
|
||||||
|
@ -94,16 +95,40 @@ export function readConfiguration(
|
||||||
const rootNames = parsed.fileNames.map(f => path.normalize(f));
|
const rootNames = parsed.fileNames.map(f => path.normalize(f));
|
||||||
|
|
||||||
const options = createNgCompilerOptions(basePath, config, parsed.options);
|
const options = createNgCompilerOptions(basePath, config, parsed.options);
|
||||||
return {rootNames, options, errors: parsed.errors};
|
return {project: projectFile, rootNames, options, errors: parsed.errors};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errors: Diagnostics = [{
|
const errors: Diagnostics = [{
|
||||||
category: ts.DiagnosticCategory.Error,
|
category: ts.DiagnosticCategory.Error,
|
||||||
message: e.stack,
|
messageText: e.stack,
|
||||||
|
source: api.SOURCE,
|
||||||
|
code: api.UNKNOWN_ERROR_CODE
|
||||||
}];
|
}];
|
||||||
return {errors, rootNames: [], options: {}};
|
return {project: '', errors, rootNames: [], options: {}};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PerformCompilationResult {
|
||||||
|
diagnostics: Diagnostics;
|
||||||
|
program?: api.Program;
|
||||||
|
emitResult?: ts.EmitResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitCodeFromResult(result: PerformCompilationResult | undefined): number {
|
||||||
|
if (!result) {
|
||||||
|
// If we didn't get a result we should return failure.
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!result.diagnostics || result.diagnostics.length === 0) {
|
||||||
|
// If we have a result and didn't get any errors, we succeeded.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return 2 if any of the errors were unknown.
|
||||||
|
return result.diagnostics.some(d => d.source === 'angular' && d.code === api.UNKNOWN_ERROR_CODE) ?
|
||||||
|
2 :
|
||||||
|
1;
|
||||||
|
}
|
||||||
|
|
||||||
export function performCompilation(
|
export function performCompilation(
|
||||||
{rootNames, options, host, oldProgram, emitCallback, customTransformers}: {
|
{rootNames, options, host, oldProgram, emitCallback, customTransformers}: {
|
||||||
rootNames: string[],
|
rootNames: string[],
|
||||||
|
@ -112,11 +137,7 @@ export function performCompilation(
|
||||||
oldProgram?: api.Program,
|
oldProgram?: api.Program,
|
||||||
emitCallback?: api.TsEmitCallback,
|
emitCallback?: api.TsEmitCallback,
|
||||||
customTransformers?: api.CustomTransformers
|
customTransformers?: api.CustomTransformers
|
||||||
}): {
|
}): PerformCompilationResult {
|
||||||
program?: api.Program,
|
|
||||||
emitResult?: ts.EmitResult,
|
|
||||||
diagnostics: Diagnostics,
|
|
||||||
} {
|
|
||||||
const [major, minor] = ts.version.split('.');
|
const [major, minor] = ts.version.split('.');
|
||||||
|
|
||||||
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 3)) {
|
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 3)) {
|
||||||
|
@ -168,19 +189,24 @@ export function performCompilation(
|
||||||
((options.skipMetadataEmit || options.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata)
|
((options.skipMetadataEmit || options.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata)
|
||||||
});
|
});
|
||||||
allDiagnostics.push(...emitResult.diagnostics);
|
allDiagnostics.push(...emitResult.diagnostics);
|
||||||
|
return {diagnostics: allDiagnostics, program, emitResult};
|
||||||
}
|
}
|
||||||
|
return {diagnostics: allDiagnostics, program};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let errMsg: string;
|
let errMsg: string;
|
||||||
|
let code: number;
|
||||||
if (isSyntaxError(e)) {
|
if (isSyntaxError(e)) {
|
||||||
// don't report the stack for syntax errors as they are well known errors.
|
// don't report the stack for syntax errors as they are well known errors.
|
||||||
errMsg = e.message;
|
errMsg = e.message;
|
||||||
|
code = api.DEFAULT_ERROR_CODE;
|
||||||
} else {
|
} else {
|
||||||
errMsg = e.stack;
|
errMsg = e.stack;
|
||||||
|
// It is not a syntax error we might have a program with unknown state, discard it.
|
||||||
|
program = undefined;
|
||||||
|
code = api.UNKNOWN_ERROR_CODE;
|
||||||
}
|
}
|
||||||
allDiagnostics.push({
|
allDiagnostics.push(
|
||||||
category: ts.DiagnosticCategory.Error,
|
{category: ts.DiagnosticCategory.Error, messageText: errMsg, code, source: api.SOURCE});
|
||||||
message: errMsg,
|
return {diagnostics: allDiagnostics, program};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return {program, emitResult, diagnostics: allDiagnostics};
|
|
||||||
}
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
/**
|
||||||
|
* @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 chokidar from 'chokidar';
|
||||||
|
import * as path from 'path';
|
||||||
|
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
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum FileChangeEvent {
|
||||||
|
Change,
|
||||||
|
CreateDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerformWatchHost {
|
||||||
|
reportDiagnostics(diagnostics: Diagnostics): void;
|
||||||
|
readConfiguration(): ParsedConfiguration;
|
||||||
|
createCompilerHost(options: api.CompilerOptions): api.CompilerHost;
|
||||||
|
createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined;
|
||||||
|
onFileChange(listener: (event: FileChangeEvent, fileName: string) => void):
|
||||||
|
{close: () => void, ready: (cb: () => void) => void};
|
||||||
|
setTimeout(callback: () => void, ms: number): any;
|
||||||
|
clearTimeout(timeoutId: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPerformWatchHost(
|
||||||
|
configFileName: string, reportDiagnostics: (diagnostics: Diagnostics) => void,
|
||||||
|
createEmitCallback?: (options: api.CompilerOptions) => api.TsEmitCallback): PerformWatchHost {
|
||||||
|
return {
|
||||||
|
reportDiagnostics: reportDiagnostics,
|
||||||
|
createCompilerHost: options => createCompilerHost({options}),
|
||||||
|
readConfiguration: () => readConfiguration(configFileName),
|
||||||
|
createEmitCallback: options => createEmitCallback ? createEmitCallback(options) : undefined,
|
||||||
|
onFileChange: (listeners) => {
|
||||||
|
const parsed = readConfiguration(configFileName);
|
||||||
|
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([{
|
||||||
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
messageText: 'Invalid configuration option. baseDir not specified',
|
||||||
|
source: api.SOURCE,
|
||||||
|
code: api.DEFAULT_ERROR_CODE
|
||||||
|
}]);
|
||||||
|
return {close: () => {}, ready: stubReady};
|
||||||
|
}
|
||||||
|
const watcher = chokidar.watch(parsed.options.basePath, {
|
||||||
|
// 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.
|
||||||
|
ignored: /((^[\/\\])\..)|(\.js$)|(\.map$)|(\.metadata\.json)/,
|
||||||
|
ignoreInitial: true,
|
||||||
|
persistent: true,
|
||||||
|
});
|
||||||
|
watcher.on('all', (event: string, path: string) => {
|
||||||
|
switch (event) {
|
||||||
|
case 'change':
|
||||||
|
listeners(FileChangeEvent.Change, path);
|
||||||
|
break;
|
||||||
|
case 'unlink':
|
||||||
|
case 'add':
|
||||||
|
listeners(FileChangeEvent.CreateDelete, path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function ready(cb: () => void) { watcher.on('ready', cb); }
|
||||||
|
return {close: () => watcher.close(), ready};
|
||||||
|
},
|
||||||
|
setTimeout: (ts.sys.clearTimeout && ts.sys.setTimeout) || setTimeout,
|
||||||
|
clearTimeout: (ts.sys.setTimeout && ts.sys.clearTimeout) || clearTimeout,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logic in this function is adapted from `tsc.ts` from TypeScript.
|
||||||
|
*/
|
||||||
|
export function performWatchCompilation(host: PerformWatchHost): {
|
||||||
|
close: () => void,
|
||||||
|
ready: (cb: () => void) => void,
|
||||||
|
firstCompileResult: PerformCompilationResult | undefined
|
||||||
|
} {
|
||||||
|
let cachedProgram: api.Program|undefined; // Program cached from last compilation
|
||||||
|
let cachedCompilerHost: api.CompilerHost|undefined; // CompilerHost 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
|
||||||
|
|
||||||
|
// Watch basePath, ignoring .dotfiles
|
||||||
|
const fileWatcher = host.onFileChange(watchedFileChanged);
|
||||||
|
const ingoreFilesForWatch = new Set<string>();
|
||||||
|
|
||||||
|
const firstCompileResult = doCompilation();
|
||||||
|
|
||||||
|
const readyPromise = new Promise(resolve => fileWatcher.ready(resolve));
|
||||||
|
|
||||||
|
return {close, ready: cb => readyPromise.then(cb), firstCompileResult};
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
fileWatcher.close();
|
||||||
|
if (timerHandleForRecompilation) {
|
||||||
|
host.clearTimeout(timerHandleForRecompilation);
|
||||||
|
timerHandleForRecompilation = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoked to perform initial compilation or re-compilation in watch mode
|
||||||
|
function doCompilation() {
|
||||||
|
if (!cachedOptions) {
|
||||||
|
cachedOptions = host.readConfiguration();
|
||||||
|
}
|
||||||
|
if (cachedOptions.errors && cachedOptions.errors.length) {
|
||||||
|
host.reportDiagnostics(cachedOptions.errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
const originalWriteFileCallback = cachedCompilerHost.writeFile;
|
||||||
|
cachedCompilerHost.writeFile = function(
|
||||||
|
fileName: string, data: string, writeByteOrderMark: boolean,
|
||||||
|
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) {
|
||||||
|
ingoreFilesForWatch.add(path.normalize(fileName));
|
||||||
|
return originalWriteFileCallback(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ingoreFilesForWatch.clear();
|
||||||
|
const compileResult = performCompilation({
|
||||||
|
rootNames: cachedOptions.rootNames,
|
||||||
|
options: cachedOptions.options,
|
||||||
|
host: cachedCompilerHost,
|
||||||
|
oldProgram: cachedProgram,
|
||||||
|
emitCallback: host.createEmitCallback(cachedOptions.options)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (compileResult.diagnostics.length) {
|
||||||
|
host.reportDiagnostics(compileResult.diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitCode = exitCodeFromResult(compileResult);
|
||||||
|
if (exitCode == 0) {
|
||||||
|
cachedProgram = compileResult.program;
|
||||||
|
host.reportDiagnostics([ChangeDiagnostics.Compilation_complete_Watching_for_file_changes]);
|
||||||
|
} else {
|
||||||
|
host.reportDiagnostics([ChangeDiagnostics.Compilation_failed_Watching_for_file_changes]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return compileResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetOptions() {
|
||||||
|
cachedProgram = undefined;
|
||||||
|
cachedCompilerHost = undefined;
|
||||||
|
cachedOptions = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchedFileChanged(event: FileChangeEvent, fileName: string) {
|
||||||
|
if (cachedOptions && event === FileChangeEvent.Change &&
|
||||||
|
// TODO(chuckj): validate that this is sufficient to skip files that were written.
|
||||||
|
// This assumes that the file path we write is the same file path we will receive in the
|
||||||
|
// change notification.
|
||||||
|
path.normalize(fileName) === path.normalize(cachedOptions.project)) {
|
||||||
|
// If the configuration file changes, forget everything and start the recompilation timer
|
||||||
|
resetOptions();
|
||||||
|
} else if (event === FileChangeEvent.CreateDelete) {
|
||||||
|
// If a file was added or removed, reread the configuration
|
||||||
|
// to determine the new list of root files.
|
||||||
|
cachedOptions = undefined;
|
||||||
|
}
|
||||||
|
if (!ingoreFilesForWatch.has(path.normalize(fileName))) {
|
||||||
|
// Ignore the file if the file is one that was written by the compiler.
|
||||||
|
startTimerForRecompilation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch
|
||||||
|
// operations (such as saving all modified files in an editor) a chance to complete before we kick
|
||||||
|
// off a new compilation.
|
||||||
|
function startTimerForRecompilation() {
|
||||||
|
if (timerHandleForRecompilation) {
|
||||||
|
host.clearTimeout(timerHandleForRecompilation);
|
||||||
|
}
|
||||||
|
timerHandleForRecompilation = host.setTimeout(recompile, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function recompile() {
|
||||||
|
timerHandleForRecompilation = undefined;
|
||||||
|
host.reportDiagnostics(
|
||||||
|
[ChangeDiagnostics.File_change_detected_Starting_incremental_compilation]);
|
||||||
|
doCompilation();
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,10 +9,16 @@
|
||||||
import {ParseSourceSpan} from '@angular/compiler';
|
import {ParseSourceSpan} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
export const DEFAULT_ERROR_CODE = 100;
|
||||||
|
export const UNKNOWN_ERROR_CODE = 500;
|
||||||
|
export const SOURCE = 'angular' as 'angular';
|
||||||
|
|
||||||
export interface Diagnostic {
|
export interface Diagnostic {
|
||||||
message: string;
|
messageText: string;
|
||||||
span?: ParseSourceSpan;
|
span?: ParseSourceSpan;
|
||||||
category: ts.DiagnosticCategory;
|
category: ts.DiagnosticCategory;
|
||||||
|
code: number;
|
||||||
|
source: 'angular';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompilerOptions extends ts.CompilerOptions {
|
export interface CompilerOptions extends ts.CompilerOptions {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import * as ts from 'typescript';
|
||||||
import {BaseAotCompilerHost} from '../compiler_host';
|
import {BaseAotCompilerHost} from '../compiler_host';
|
||||||
import {TypeChecker} from '../diagnostics/check_types';
|
import {TypeChecker} from '../diagnostics/check_types';
|
||||||
|
|
||||||
import {CompilerHost, CompilerOptions, CustomTransformers, Diagnostic, EmitFlags, Program, TsEmitArguments, TsEmitCallback} from './api';
|
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
|
||||||
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
|
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
|
||||||
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
|
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
|
||||||
|
|
||||||
|
@ -61,8 +61,12 @@ class AngularCompilerProgram implements Program {
|
||||||
if (errors) {
|
if (errors) {
|
||||||
// TODO(tbosch): once we move MetadataBundler from tsc_wrapped into compiler_cli,
|
// TODO(tbosch): once we move MetadataBundler from tsc_wrapped into compiler_cli,
|
||||||
// directly create ng.Diagnostic instead of using ts.Diagnostic here.
|
// directly create ng.Diagnostic instead of using ts.Diagnostic here.
|
||||||
this._optionsDiagnostics.push(
|
this._optionsDiagnostics.push(...errors.map(e => ({
|
||||||
...errors.map(e => ({category: e.category, message: e.messageText as string})));
|
category: e.category,
|
||||||
|
messageText: e.messageText as string,
|
||||||
|
source: SOURCE,
|
||||||
|
code: DEFAULT_ERROR_CODE
|
||||||
|
})));
|
||||||
} else {
|
} else {
|
||||||
rootNames.push(indexName !);
|
rootNames.push(indexName !);
|
||||||
this.host = host = bundleHost;
|
this.host = host = bundleHost;
|
||||||
|
@ -219,12 +223,19 @@ class AngularCompilerProgram implements Program {
|
||||||
if (parserErrors && parserErrors.length) {
|
if (parserErrors && parserErrors.length) {
|
||||||
this._structuralDiagnostics =
|
this._structuralDiagnostics =
|
||||||
parserErrors.map<Diagnostic>(e => ({
|
parserErrors.map<Diagnostic>(e => ({
|
||||||
message: e.contextualMessage(),
|
messageText: e.contextualMessage(),
|
||||||
category: ts.DiagnosticCategory.Error,
|
category: ts.DiagnosticCategory.Error,
|
||||||
span: e.span
|
span: e.span,
|
||||||
|
source: SOURCE,
|
||||||
|
code: DEFAULT_ERROR_CODE
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
this._structuralDiagnostics = [{message: e.message, category: ts.DiagnosticCategory.Error}];
|
this._structuralDiagnostics = [{
|
||||||
|
messageText: e.message,
|
||||||
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
source: SOURCE,
|
||||||
|
code: DEFAULT_ERROR_CODE
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
this._analyzedModules = emptyModules;
|
this._analyzedModules = emptyModules;
|
||||||
return emptyModules;
|
return emptyModules;
|
||||||
|
@ -252,8 +263,12 @@ class AngularCompilerProgram implements Program {
|
||||||
return this.options.skipTemplateCodegen ? [] : result;
|
return this.options.skipTemplateCodegen ? [] : result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isSyntaxError(e)) {
|
if (isSyntaxError(e)) {
|
||||||
this._generatedFileDiagnostics =
|
this._generatedFileDiagnostics = [{
|
||||||
[{message: e.message, category: ts.DiagnosticCategory.Error}];
|
messageText: e.message,
|
||||||
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
source: SOURCE,
|
||||||
|
code: DEFAULT_ERROR_CODE
|
||||||
|
}];
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -417,9 +432,11 @@ function getNgOptionDiagnostics(options: CompilerOptions): Diagnostic[] {
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return [{
|
return [{
|
||||||
message:
|
messageText:
|
||||||
'Angular compiler options "annotationsAs" only supports "static fields" and "decorators"',
|
'Angular compiler options "annotationsAs" only supports "static fields" and "decorators"',
|
||||||
category: ts.DiagnosticCategory.Error
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
source: SOURCE,
|
||||||
|
code: DEFAULT_ERROR_CODE
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,12 +39,13 @@ describe('ng type checker', () => {
|
||||||
if (!diagnostics || !diagnostics.length) {
|
if (!diagnostics || !diagnostics.length) {
|
||||||
throw new Error('Expected a diagnostic erorr message');
|
throw new Error('Expected a diagnostic erorr message');
|
||||||
} else {
|
} else {
|
||||||
const matches: (d: Diagnostic) => boolean =
|
const matches: (d: Diagnostic) => boolean = typeof message === 'string' ?
|
||||||
typeof message === 'string' ? d => d.message == message : d => message.test(d.message);
|
d => d.messageText == message :
|
||||||
|
d => message.test(d.messageText);
|
||||||
const matchingDiagnostics = diagnostics.filter(matches);
|
const matchingDiagnostics = diagnostics.filter(matches);
|
||||||
if (!matchingDiagnostics || !matchingDiagnostics.length) {
|
if (!matchingDiagnostics || !matchingDiagnostics.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Expected a diagnostics matching ${message}, received\n ${diagnostics.map(d => d.message).join('\n ')}`);
|
`Expected a diagnostics matching ${message}, received\n ${diagnostics.map(d => d.messageText).join('\n ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,6 +174,6 @@ const LOWERING_QUICKSTART: MockDirectory = {
|
||||||
|
|
||||||
function expectNoDiagnostics(diagnostics: Diagnostic[]) {
|
function expectNoDiagnostics(diagnostics: Diagnostic[]) {
|
||||||
if (diagnostics && diagnostics.length) {
|
if (diagnostics && diagnostics.length) {
|
||||||
throw new Error(diagnostics.map(d => `${d.span}: ${d.message}`).join('\n'));
|
throw new Error(diagnostics.map(d => `${d.span}: ${d.messageText}`).join('\n'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -191,7 +191,7 @@ export class DiagnosticContext {
|
||||||
analyzeHost);
|
analyzeHost);
|
||||||
|
|
||||||
analyzedModules = this._analyzedModules =
|
analyzedModules = this._analyzedModules =
|
||||||
analyzeNgModules(programSymbols, analyzeHost, this.resolver);
|
analyzeNgModules(programSymbols, analyzeHost, this.staticSymbolResolver, this.resolver);
|
||||||
}
|
}
|
||||||
return analyzedModules;
|
return analyzedModules;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,9 @@
|
||||||
import {makeTempDir} from '@angular/tsc-wrapped/test/test_support';
|
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 {main} from '../src/main';
|
import {main, watchMode} from '../src/main';
|
||||||
|
|
||||||
function getNgRootDir() {
|
function getNgRootDir() {
|
||||||
const moduleFilename = module.filename.replace(/\\/g, '/');
|
const moduleFilename = module.filename.replace(/\\/g, '/');
|
||||||
|
@ -309,4 +310,156 @@ describe('compiler-cli with disableTransformerPipeline', () => {
|
||||||
.catch(e => done.fail(e));
|
.catch(e => done.fail(e));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('watch mode', () => {
|
||||||
|
let timer: (() => void)|undefined = undefined;
|
||||||
|
let results: ((message: string) => void)|undefined = undefined;
|
||||||
|
let originalTimeout: number;
|
||||||
|
|
||||||
|
function trigger() {
|
||||||
|
const delay = 1000;
|
||||||
|
setTimeout(() => {
|
||||||
|
const t = timer;
|
||||||
|
timer = undefined;
|
||||||
|
if (!t) {
|
||||||
|
fail('Unexpected state. Timer was not set.');
|
||||||
|
} else {
|
||||||
|
t();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function whenResults(): Promise<string> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
results = message => {
|
||||||
|
resolve(message);
|
||||||
|
results = undefined;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorSpy(message: string): void {
|
||||||
|
if (results) results(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
|
||||||
|
const timerToken = 100;
|
||||||
|
spyOn(ts.sys, 'setTimeout').and.callFake((callback: () => void) => {
|
||||||
|
timer = callback;
|
||||||
|
return timerToken;
|
||||||
|
});
|
||||||
|
spyOn(ts.sys, 'clearTimeout').and.callFake((token: number) => {
|
||||||
|
if (token == timerToken) {
|
||||||
|
timer = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
write('greet.html', `<p class="greeting"> Hello {{name}}!</p>`);
|
||||||
|
write('greet.css', `p.greeting { color: #eee }`);
|
||||||
|
write('greet.ts', `
|
||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'greet',
|
||||||
|
templateUrl: 'greet.html',
|
||||||
|
styleUrls: ['greet.css']
|
||||||
|
})
|
||||||
|
export class Greet {
|
||||||
|
@Input()
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
write('app.ts', `
|
||||||
|
import {Component} from '@angular/core'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-app',
|
||||||
|
template: \`
|
||||||
|
<div>
|
||||||
|
<greet [name]='name'></greet>
|
||||||
|
</div>
|
||||||
|
\`,
|
||||||
|
})
|
||||||
|
export class App {
|
||||||
|
name:string;
|
||||||
|
constructor() {
|
||||||
|
this.name = \`Angular!\`
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
write('module.ts', `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {Greet} from './greet';
|
||||||
|
import {App} from './app';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [Greet, App]
|
||||||
|
})
|
||||||
|
export class MyModule {}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; });
|
||||||
|
|
||||||
|
function writeAppConfig(location: string) {
|
||||||
|
writeConfig(`{
|
||||||
|
"extends": "./tsconfig-base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "${location}"
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectRecompile(cb: () => void) {
|
||||||
|
return (done: DoneFn) => {
|
||||||
|
writeAppConfig('dist');
|
||||||
|
const compile = watchMode({p: basePath}, errorSpy);
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
compile.ready(() => {
|
||||||
|
cb();
|
||||||
|
|
||||||
|
// Allow the watch callbacks to occur and trigger the timer.
|
||||||
|
trigger();
|
||||||
|
|
||||||
|
// Expect the file to trigger a result.
|
||||||
|
whenResults().then(message => {
|
||||||
|
expect(message).toMatch(/File change detected/);
|
||||||
|
compile.close();
|
||||||
|
done();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should recompile when config file changes', expectRecompile(() => writeAppConfig('dist2')));
|
||||||
|
|
||||||
|
it('should recompile when a ts file changes', expectRecompile(() => {
|
||||||
|
write('greet.ts', `
|
||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'greet',
|
||||||
|
templateUrl: 'greet.html',
|
||||||
|
styleUrls: ['greet.css'],
|
||||||
|
})
|
||||||
|
export class Greet {
|
||||||
|
@Input()
|
||||||
|
name: string;
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should recomiple when the html file changes',
|
||||||
|
expectRecompile(() => { write('greet.html', '<p> Hello {{name}} again!</p>'); }));
|
||||||
|
|
||||||
|
it('should recompile when the css file changes',
|
||||||
|
expectRecompile(() => { write('greet.css', `p.greeting { color: blue }`); }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -42,8 +42,8 @@ export class AotCompiler {
|
||||||
|
|
||||||
analyzeModulesSync(rootFiles: string[]): NgAnalyzedModules {
|
analyzeModulesSync(rootFiles: string[]): NgAnalyzedModules {
|
||||||
const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host);
|
const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host);
|
||||||
const analyzeResult =
|
const analyzeResult = analyzeAndValidateNgModules(
|
||||||
analyzeAndValidateNgModules(programSymbols, this._host, this._metadataResolver);
|
programSymbols, this._host, this._symbolResolver, this._metadataResolver);
|
||||||
analyzeResult.ngModules.forEach(
|
analyzeResult.ngModules.forEach(
|
||||||
ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
|
ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
|
||||||
ngModule.type.reference, true));
|
ngModule.type.reference, true));
|
||||||
|
@ -52,8 +52,8 @@ export class AotCompiler {
|
||||||
|
|
||||||
analyzeModulesAsync(rootFiles: string[]): Promise<NgAnalyzedModules> {
|
analyzeModulesAsync(rootFiles: string[]): Promise<NgAnalyzedModules> {
|
||||||
const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host);
|
const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host);
|
||||||
const analyzeResult =
|
const analyzeResult = analyzeAndValidateNgModules(
|
||||||
analyzeAndValidateNgModules(programSymbols, this._host, this._metadataResolver);
|
programSymbols, this._host, this._symbolResolver, this._metadataResolver);
|
||||||
return Promise
|
return Promise
|
||||||
.all(analyzeResult.ngModules.map(
|
.all(analyzeResult.ngModules.map(
|
||||||
ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
|
ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
|
||||||
|
@ -400,16 +400,24 @@ export interface NgAnalyzeModulesHost { isSourceFile(filePath: string): boolean;
|
||||||
// Returns all the source files and a mapping from modules to directives
|
// Returns all the source files and a mapping from modules to directives
|
||||||
export function analyzeNgModules(
|
export function analyzeNgModules(
|
||||||
programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost,
|
programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost,
|
||||||
|
staticSymbolResolver: StaticSymbolResolver,
|
||||||
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
|
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
|
||||||
|
const programStaticSymbolsWithDecorators = programStaticSymbols.filter(
|
||||||
|
symbol => !symbol.filePath.endsWith('.d.ts') ||
|
||||||
|
staticSymbolResolver.hasDecorators(symbol.filePath));
|
||||||
const {ngModules, symbolsMissingModule} =
|
const {ngModules, symbolsMissingModule} =
|
||||||
_createNgModules(programStaticSymbols, host, metadataResolver);
|
_createNgModules(programStaticSymbolsWithDecorators, host, metadataResolver);
|
||||||
return _analyzeNgModules(programStaticSymbols, ngModules, symbolsMissingModule, metadataResolver);
|
return _analyzeNgModules(
|
||||||
|
programStaticSymbols, programStaticSymbolsWithDecorators, ngModules, symbolsMissingModule,
|
||||||
|
metadataResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function analyzeAndValidateNgModules(
|
export function analyzeAndValidateNgModules(
|
||||||
programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost,
|
programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost,
|
||||||
|
staticSymbolResolver: StaticSymbolResolver,
|
||||||
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
|
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
|
||||||
const result = analyzeNgModules(programStaticSymbols, host, metadataResolver);
|
const result =
|
||||||
|
analyzeNgModules(programStaticSymbols, host, staticSymbolResolver, metadataResolver);
|
||||||
if (result.symbolsMissingModule && result.symbolsMissingModule.length) {
|
if (result.symbolsMissingModule && result.symbolsMissingModule.length) {
|
||||||
const messages = result.symbolsMissingModule.map(
|
const messages = result.symbolsMissingModule.map(
|
||||||
s =>
|
s =>
|
||||||
|
@ -420,8 +428,8 @@ export function analyzeAndValidateNgModules(
|
||||||
}
|
}
|
||||||
|
|
||||||
function _analyzeNgModules(
|
function _analyzeNgModules(
|
||||||
programSymbols: StaticSymbol[], ngModuleMetas: CompileNgModuleMetadata[],
|
programSymbols: StaticSymbol[], programSymbolsWithDecorators: StaticSymbol[],
|
||||||
symbolsMissingModule: StaticSymbol[],
|
ngModuleMetas: CompileNgModuleMetadata[], symbolsMissingModule: StaticSymbol[],
|
||||||
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
|
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
|
||||||
const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>();
|
const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>();
|
||||||
ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule));
|
ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule));
|
||||||
|
@ -436,7 +444,10 @@ function _analyzeNgModules(
|
||||||
programSymbols.forEach((symbol) => {
|
programSymbols.forEach((symbol) => {
|
||||||
const filePath = symbol.filePath;
|
const filePath = symbol.filePath;
|
||||||
filePaths.add(filePath);
|
filePaths.add(filePath);
|
||||||
|
});
|
||||||
|
programSymbolsWithDecorators.forEach((symbol) => {
|
||||||
if (metadataResolver.isInjectable(symbol)) {
|
if (metadataResolver.isInjectable(symbol)) {
|
||||||
|
const filePath = symbol.filePath;
|
||||||
ngInjectablesByFile.set(filePath, (ngInjectablesByFile.get(filePath) || []).concat(symbol));
|
ngInjectablesByFile.set(filePath, (ngInjectablesByFile.get(filePath) || []).concat(symbol));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -250,6 +250,24 @@ export class StaticSymbolResolver {
|
||||||
return this.staticSymbolCache.get(declarationFile, name, members);
|
return this.staticSymbolCache.get(declarationFile, name, members);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hasDecorators checks a file's metadata for the presense of decorators without evalutating the
|
||||||
|
* metada.
|
||||||
|
*
|
||||||
|
* @param filePath the absolute path to examine for decorators.
|
||||||
|
* @returns true if any class in the file has a decorator.
|
||||||
|
*/
|
||||||
|
hasDecorators(filePath: string): boolean {
|
||||||
|
const metadata = this.getModuleMetadata(filePath);
|
||||||
|
if (metadata['metadata']) {
|
||||||
|
return Object.keys(metadata['metadata']).some((metadataKey) => {
|
||||||
|
const entry = metadata['metadata'][metadataKey];
|
||||||
|
return entry && entry.__symbolic === 'class' && entry.decorators;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
getSymbolsOf(filePath: string): StaticSymbol[] {
|
getSymbolsOf(filePath: string): StaticSymbol[] {
|
||||||
// Note: Some users use libraries that were not compiled with ngc, i.e. they don't
|
// Note: Some users use libraries that were not compiled with ngc, i.e. they don't
|
||||||
// have summaries, only .d.ts files. So we always need to check both, the summary
|
// have summaries, only .d.ts files. So we always need to check both, the summary
|
||||||
|
|
|
@ -57,8 +57,8 @@ export class Extractor {
|
||||||
|
|
||||||
extract(rootFiles: string[]): Promise<MessageBundle> {
|
extract(rootFiles: string[]): Promise<MessageBundle> {
|
||||||
const programSymbols = extractProgramSymbols(this.staticSymbolResolver, rootFiles, this.host);
|
const programSymbols = extractProgramSymbols(this.staticSymbolResolver, rootFiles, this.host);
|
||||||
const {files, ngModules} =
|
const {files, ngModules} = analyzeAndValidateNgModules(
|
||||||
analyzeAndValidateNgModules(programSymbols, this.host, this.metadataResolver);
|
programSymbols, this.host, this.staticSymbolResolver, this.metadataResolver);
|
||||||
return Promise
|
return Promise
|
||||||
.all(ngModules.map(
|
.all(ngModules.map(
|
||||||
ngModule => this.metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
|
ngModule => this.metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
|
||||||
|
|
|
@ -151,7 +151,7 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
|
||||||
analyzeHost);
|
analyzeHost);
|
||||||
|
|
||||||
analyzedModules = this.analyzedModules =
|
analyzedModules = this.analyzedModules =
|
||||||
analyzeNgModules(programSymbols, analyzeHost, this.resolver);
|
analyzeNgModules(programSymbols, analyzeHost, this.staticSymbolResolver, this.resolver);
|
||||||
}
|
}
|
||||||
return analyzedModules;
|
return analyzedModules;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue