feat(compiler-cli): new compiler api and command-line using TypeScript transformers
This commit is contained in:

committed by
Matias Niemelä

parent
43c187b624
commit
3097083277
@ -17,5 +17,10 @@ export {AstType, ExpressionDiagnosticsContext} from './src/diagnostics/expressio
|
|||||||
export {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './src/diagnostics/typescript_symbols';
|
export {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './src/diagnostics/typescript_symbols';
|
||||||
export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './src/diagnostics/symbols';
|
export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './src/diagnostics/symbols';
|
||||||
|
|
||||||
|
export * from './src/transformers/api';
|
||||||
|
export * from './src/transformers/entry_points';
|
||||||
|
|
||||||
|
export {main as ngc} from './src/ngc';
|
||||||
|
|
||||||
// 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';
|
||||||
|
@ -15,7 +15,7 @@ import * as path from 'path';
|
|||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import {tsc} from '@angular/tsc-wrapped/src/tsc';
|
import {tsc} from '@angular/tsc-wrapped/src/tsc';
|
||||||
import {NodeCompilerHostContext, __NGTOOLS_PRIVATE_API_2} from '@angular/compiler-cli';
|
import {__NGTOOLS_PRIVATE_API_2} from '@angular/compiler-cli';
|
||||||
|
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
|
|
||||||
@ -51,7 +51,6 @@ function codeGenTest(forceError = false) {
|
|||||||
const wroteFiles: string[] = [];
|
const wroteFiles: string[] = [];
|
||||||
|
|
||||||
const config = tsc.readConfiguration(project, basePath);
|
const config = tsc.readConfiguration(project, basePath);
|
||||||
const hostContext = new NodeCompilerHostContext();
|
|
||||||
const delegateHost = ts.createCompilerHost(config.parsed.options, true);
|
const delegateHost = ts.createCompilerHost(config.parsed.options, true);
|
||||||
const host: ts.CompilerHost = Object.assign(
|
const host: ts.CompilerHost = Object.assign(
|
||||||
{}, delegateHost,
|
{}, delegateHost,
|
||||||
@ -79,7 +78,10 @@ function codeGenTest(forceError = false) {
|
|||||||
|
|
||||||
readResource: (fileName: string) => {
|
readResource: (fileName: string) => {
|
||||||
readResources.push(fileName);
|
readResources.push(fileName);
|
||||||
return hostContext.readResource(fileName);
|
if (!host.fileExists(fileName)) {
|
||||||
|
throw new Error(`Compilation failed. Resource file not found: ${fileName}`);
|
||||||
|
}
|
||||||
|
return Promise.resolve(host.readFile(fileName));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@ -128,7 +130,6 @@ function i18nTest() {
|
|||||||
const wroteFiles: string[] = [];
|
const wroteFiles: string[] = [];
|
||||||
|
|
||||||
const config = tsc.readConfiguration(project, basePath);
|
const config = tsc.readConfiguration(project, basePath);
|
||||||
const hostContext = new NodeCompilerHostContext();
|
|
||||||
const delegateHost = ts.createCompilerHost(config.parsed.options, true);
|
const delegateHost = ts.createCompilerHost(config.parsed.options, true);
|
||||||
const host: ts.CompilerHost = Object.assign(
|
const host: ts.CompilerHost = Object.assign(
|
||||||
{}, delegateHost,
|
{}, delegateHost,
|
||||||
@ -148,7 +149,10 @@ function i18nTest() {
|
|||||||
outFile: undefined,
|
outFile: undefined,
|
||||||
readResource: (fileName: string) => {
|
readResource: (fileName: string) => {
|
||||||
readResources.push(fileName);
|
readResources.push(fileName);
|
||||||
return hostContext.readResource(fileName);
|
if (!host.fileExists(fileName)) {
|
||||||
|
throw new Error(`Compilation failed. Resource file not found: ${fileName}`);
|
||||||
|
}
|
||||||
|
return Promise.resolve(host.readFile(fileName));
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
// For TypeScript 1.8, we have to lay out generated files
|
// For TypeScript 1.8, we have to lay out generated files
|
||||||
// in the same source directory with your code.
|
// in the same source directory with your code.
|
||||||
"genDir": ".",
|
"skipTemplateCodegen": true
|
||||||
"debug": true
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
// For TypeScript 1.8, we have to lay out generated files
|
||||||
|
// in the same source directory with your code.
|
||||||
|
"genDir": ".",
|
||||||
|
"debug": true,
|
||||||
|
"enableSummariesForJit": true,
|
||||||
|
"alwaysCompileGeneratedCode": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"rootDir": "",
|
||||||
|
"declaration": true,
|
||||||
|
"lib": ["es6", "dom"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
// Prevent scanning up the directory tree for types
|
||||||
|
"typeRoots": ["node_modules/@types"],
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"files": [
|
||||||
|
"src/module",
|
||||||
|
"src/bootstrap",
|
||||||
|
"test/all_spec",
|
||||||
|
"test/test_ngtools_api",
|
||||||
|
"benchmarks/src/tree/ng2/index_aot.ts",
|
||||||
|
"benchmarks/src/tree/ng2_switch/index_aot.ts",
|
||||||
|
"benchmarks/src/largetable/ng2/index_aot.ts",
|
||||||
|
"benchmarks/src/largetable/ng2_switch/index_aot.ts"
|
||||||
|
]
|
||||||
|
}
|
@ -21,7 +21,7 @@ const GENERATED_OR_DTS_FILES = /\.d\.ts$|\.ngfactory\.ts$|\.ngstyle\.ts$|\.ngsum
|
|||||||
const SHALLOW_IMPORT = /^((\w|-)+|(@(\w|-)+(\/(\w|-)+)+))$/;
|
const SHALLOW_IMPORT = /^((\w|-)+|(@(\w|-)+(\/(\w|-)+)+))$/;
|
||||||
|
|
||||||
export interface CompilerHostContext extends ts.ModuleResolutionHost {
|
export interface CompilerHostContext extends ts.ModuleResolutionHost {
|
||||||
readResource(fileName: string): Promise<string>;
|
readResource?(fileName: string): Promise<string>|string;
|
||||||
assumeFileExists(fileName: string): void;
|
assumeFileExists(fileName: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,10 +187,11 @@ export class CompilerHost implements AotCompilerHost {
|
|||||||
getMetadataFor(filePath: string): ModuleMetadata[]|undefined {
|
getMetadataFor(filePath: string): ModuleMetadata[]|undefined {
|
||||||
if (!this.context.fileExists(filePath)) {
|
if (!this.context.fileExists(filePath)) {
|
||||||
// If the file doesn't exists then we cannot return metadata for the file.
|
// If the file doesn't exists then we cannot return metadata for the file.
|
||||||
// This will occur if the user refernced a declared module for which no file
|
// This will occur if the user referenced a declared module for which no file
|
||||||
// exists for the module (i.e. jQuery or angularjs).
|
// exists for the module (i.e. jQuery or angularjs).
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DTS.test(filePath)) {
|
if (DTS.test(filePath)) {
|
||||||
const metadataPath = filePath.replace(DTS, '.metadata.json');
|
const metadataPath = filePath.replace(DTS, '.metadata.json');
|
||||||
if (this.context.fileExists(metadataPath)) {
|
if (this.context.fileExists(metadataPath)) {
|
||||||
@ -202,11 +203,11 @@ export class CompilerHost implements AotCompilerHost {
|
|||||||
return [this.upgradeVersion1Metadata(
|
return [this.upgradeVersion1Metadata(
|
||||||
{'__symbolic': 'module', 'version': 1, 'metadata': {}}, filePath)];
|
{'__symbolic': 'module', 'version': 1, 'metadata': {}}, filePath)];
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const sf = this.getSourceFile(filePath);
|
|
||||||
const metadata = this.metadataCollector.getMetadata(sf);
|
|
||||||
return metadata ? [metadata] : [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sf = this.getSourceFile(filePath);
|
||||||
|
const metadata = this.metadataCollector.getMetadata(sf);
|
||||||
|
return metadata ? [metadata] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
readMetadata(filePath: string, dtsFilePath: string): ModuleMetadata[] {
|
readMetadata(filePath: string, dtsFilePath: string): ModuleMetadata[] {
|
||||||
@ -259,7 +260,8 @@ export class CompilerHost implements AotCompilerHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadResource(filePath: string): Promise<string>|string {
|
loadResource(filePath: string): Promise<string>|string {
|
||||||
return this.context.readResource(filePath);
|
if (this.context.readResource) return this.context.readResource(filePath);
|
||||||
|
return this.context.readFile(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSummary(filePath: string): string|null {
|
loadSummary(filePath: string): string|null {
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
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, DiagnosticCategory} from '../transformers/api';
|
||||||
|
|
||||||
interface FactoryInfo {
|
interface FactoryInfo {
|
||||||
source: ts.SourceFile;
|
source: ts.SourceFile;
|
||||||
context: EmitterVisitorContext;
|
context: EmitterVisitorContext;
|
||||||
@ -16,17 +18,10 @@ interface FactoryInfo {
|
|||||||
|
|
||||||
type FactoryInfoMap = Map<string, FactoryInfo>;
|
type FactoryInfoMap = Map<string, FactoryInfo>;
|
||||||
|
|
||||||
export enum DiagnosticKind {
|
const stubCancellationToken: ts.CancellationToken = {
|
||||||
Message,
|
isCancellationRequested(): boolean{return false;},
|
||||||
Warning,
|
throwIfCancellationRequested(): void{}
|
||||||
Error,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export interface Diagnostic {
|
|
||||||
message: string;
|
|
||||||
kind: DiagnosticKind;
|
|
||||||
span: ParseSourceSpan;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TypeChecker {
|
export class TypeChecker {
|
||||||
private _aotCompiler: AotCompiler|undefined;
|
private _aotCompiler: AotCompiler|undefined;
|
||||||
@ -35,6 +30,8 @@ export class TypeChecker {
|
|||||||
private _factoryNames: string[]|undefined;
|
private _factoryNames: string[]|undefined;
|
||||||
private _diagnosticProgram: ts.Program|undefined;
|
private _diagnosticProgram: ts.Program|undefined;
|
||||||
private _diagnosticsByFile: Map<string, Diagnostic[]>|undefined;
|
private _diagnosticsByFile: Map<string, Diagnostic[]>|undefined;
|
||||||
|
private _currentCancellationToken: ts.CancellationToken = stubCancellationToken;
|
||||||
|
private _partial: boolean = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private program: ts.Program, private tsOptions: ts.CompilerOptions,
|
private program: ts.Program, private tsOptions: ts.CompilerOptions,
|
||||||
@ -42,12 +39,19 @@ export class TypeChecker {
|
|||||||
private aotOptions: AotCompilerOptions, private _analyzedModules?: NgAnalyzedModules,
|
private aotOptions: AotCompilerOptions, private _analyzedModules?: NgAnalyzedModules,
|
||||||
private _generatedFiles?: GeneratedFile[]) {}
|
private _generatedFiles?: GeneratedFile[]) {}
|
||||||
|
|
||||||
getDiagnostics(fileName?: string): Diagnostic[] {
|
getDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken): Diagnostic[] {
|
||||||
return fileName ?
|
this._currentCancellationToken = cancellationToken || stubCancellationToken;
|
||||||
this.diagnosticsByFileName.get(fileName) || [] :
|
try {
|
||||||
([] as Diagnostic[]).concat(...Array.from(this.diagnosticsByFileName.values()));
|
return fileName ?
|
||||||
|
this.diagnosticsByFileName.get(fileName) || [] :
|
||||||
|
([] as Diagnostic[]).concat(...Array.from(this.diagnosticsByFileName.values()));
|
||||||
|
} finally {
|
||||||
|
this._currentCancellationToken = stubCancellationToken;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get partialResults(): boolean { return this._partial; }
|
||||||
|
|
||||||
private get analyzedModules(): NgAnalyzedModules {
|
private get analyzedModules(): NgAnalyzedModules {
|
||||||
return this._analyzedModules || (this._analyzedModules = this.aotCompiler.analyzeModulesSync(
|
return this._analyzedModules || (this._analyzedModules = this.aotCompiler.analyzeModulesSync(
|
||||||
this.program.getSourceFiles().map(sf => sf.fileName)));
|
this.program.getSourceFiles().map(sf => sf.fileName)));
|
||||||
@ -130,6 +134,7 @@ export class TypeChecker {
|
|||||||
};
|
};
|
||||||
const program = this.diagnosticProgram;
|
const program = this.diagnosticProgram;
|
||||||
for (const factoryName of this.factoryNames) {
|
for (const factoryName of this.factoryNames) {
|
||||||
|
if (this._currentCancellationToken.isCancellationRequested()) return result;
|
||||||
const sourceFile = program.getSourceFile(factoryName);
|
const sourceFile = program.getSourceFile(factoryName);
|
||||||
for (const diagnostic of this.diagnosticProgram.getSemanticDiagnostics(sourceFile)) {
|
for (const diagnostic of this.diagnosticProgram.getSemanticDiagnostics(sourceFile)) {
|
||||||
const span = this.sourceSpanOf(diagnostic.file, diagnostic.start, diagnostic.length);
|
const span = this.sourceSpanOf(diagnostic.file, diagnostic.start, diagnostic.length);
|
||||||
@ -138,7 +143,7 @@ export class TypeChecker {
|
|||||||
const diagnosticsList = diagnosticsFor(fileName);
|
const diagnosticsList = diagnosticsFor(fileName);
|
||||||
diagnosticsList.push({
|
diagnosticsList.push({
|
||||||
message: diagnosticMessageToString(diagnostic.messageText),
|
message: diagnosticMessageToString(diagnostic.messageText),
|
||||||
kind: diagnosticKindConverter(diagnostic.category), span
|
category: diagnosticCategoryConverter(diagnostic.category), span
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,12 +163,12 @@ export class TypeChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function diagnosticMessageToString(message: ts.DiagnosticMessageChain | string): string {
|
function diagnosticMessageToString(message: ts.DiagnosticMessageChain | string): string {
|
||||||
return typeof message === 'string' ? message : diagnosticMessageToString(message.messageText);
|
return ts.flattenDiagnosticMessageText(message, '\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function diagnosticKindConverter(kind: ts.DiagnosticCategory) {
|
function diagnosticCategoryConverter(kind: ts.DiagnosticCategory) {
|
||||||
// The diagnostics kind matches ts.DiagnosticCategory. Review this code if this changes.
|
// The diagnostics kind matches ts.DiagnosticCategory. Review this code if this changes.
|
||||||
return kind as any as DiagnosticKind;
|
return kind as any as DiagnosticCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFactoryInfo(emitter: TypeScriptEmitter, file: GeneratedFile): FactoryInfo {
|
function createFactoryInfo(emitter: TypeScriptEmitter, file: GeneratedFile): FactoryInfo {
|
||||||
|
182
packages/compiler-cli/src/ngc.ts
Normal file
182
packages/compiler-cli/src/ngc.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Must be imported first, because Angular decorators throw on load.
|
||||||
|
import 'reflect-metadata';
|
||||||
|
|
||||||
|
import {isSyntaxError, syntaxError} from '@angular/compiler';
|
||||||
|
import {MetadataBundler, createBundleIndexHost} from '@angular/tsc-wrapped';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
import * as api from './transformers/api';
|
||||||
|
import * as ng from './transformers/entry_points';
|
||||||
|
|
||||||
|
const TS_EXT = /\.ts$/;
|
||||||
|
|
||||||
|
type Diagnostics = ts.Diagnostic[] | api.Diagnostic[];
|
||||||
|
|
||||||
|
function isTsDiagnostics(diagnostics: any): diagnostics is ts.Diagnostic[] {
|
||||||
|
return diagnostics && diagnostics[0] && (diagnostics[0].file || diagnostics[0].messageText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDiagnostics(cwd: string, diags: Diagnostics): string {
|
||||||
|
if (diags && diags.length) {
|
||||||
|
if (isTsDiagnostics(diags)) {
|
||||||
|
return ts.formatDiagnostics(diags, {
|
||||||
|
getCurrentDirectory(): string{return cwd;},
|
||||||
|
getCanonicalFileName(fileName: string): string{return fileName;},
|
||||||
|
getNewLine(): string{return '\n';}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return diags
|
||||||
|
.map(d => {
|
||||||
|
let res = api.DiagnosticCategory[d.category];
|
||||||
|
if (d.span) {
|
||||||
|
res +=
|
||||||
|
` at ${d.span.start.file.url}(${d.span.start.line + 1},${d.span.start.col + 1})`;
|
||||||
|
}
|
||||||
|
if (d.span && d.span.details) {
|
||||||
|
res += `: ${d.span.details}, ${d.message}\n`;
|
||||||
|
} else {
|
||||||
|
res += `: ${d.message}\n`;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.join();
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function check(cwd: string, ...args: Diagnostics[]) {
|
||||||
|
if (args.some(diags => !!(diags && diags[0]))) {
|
||||||
|
throw syntaxError(args.map(diags => {
|
||||||
|
if (diags && diags[0]) {
|
||||||
|
return formatDiagnostics(cwd, diags);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(message => !!message)
|
||||||
|
.join(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syntheticError(message: string): ts.Diagnostic {
|
||||||
|
return {
|
||||||
|
file: null as any as ts.SourceFile,
|
||||||
|
start: 0,
|
||||||
|
length: 0,
|
||||||
|
messageText: message,
|
||||||
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
code: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readConfiguration(
|
||||||
|
project: string, basePath: string, existingOptions?: ts.CompilerOptions) {
|
||||||
|
// Allow a directory containing tsconfig.json as the project value
|
||||||
|
// Note, TS@next returns an empty array, while earlier versions throw
|
||||||
|
const projectFile =
|
||||||
|
fs.lstatSync(project).isDirectory() ? path.join(project, 'tsconfig.json') : project;
|
||||||
|
let {config, error} = ts.readConfigFile(projectFile, ts.sys.readFile);
|
||||||
|
|
||||||
|
if (error) check(basePath, [error]);
|
||||||
|
const parseConfigHost = {
|
||||||
|
useCaseSensitiveFileNames: true,
|
||||||
|
fileExists: fs.existsSync,
|
||||||
|
readDirectory: ts.sys.readDirectory,
|
||||||
|
readFile: ts.sys.readFile
|
||||||
|
};
|
||||||
|
const parsed = ts.parseJsonConfigFileContent(config, parseConfigHost, basePath, existingOptions);
|
||||||
|
|
||||||
|
check(basePath, parsed.errors);
|
||||||
|
|
||||||
|
// Default codegen goes to the current directory
|
||||||
|
// Parsed options are already converted to absolute paths
|
||||||
|
const ngOptions = config.angularCompilerOptions || {};
|
||||||
|
// Ignore the genDir option
|
||||||
|
ngOptions.genDir = basePath;
|
||||||
|
for (const key of Object.keys(parsed.options)) {
|
||||||
|
ngOptions[key] = parsed.options[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {parsed, ngOptions};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function main(args: string[], consoleError: (s: string) => void = console.error): number {
|
||||||
|
try {
|
||||||
|
const parsedArgs = require('minimist')(args);
|
||||||
|
const project = parsedArgs.p || parsedArgs.project || '.';
|
||||||
|
|
||||||
|
const projectDir = fs.lstatSync(project).isFile() ? path.dirname(project) : project;
|
||||||
|
|
||||||
|
|
||||||
|
// file names in tsconfig are resolved relative to this absolute path
|
||||||
|
const basePath = path.resolve(process.cwd(), projectDir);
|
||||||
|
|
||||||
|
const {parsed, ngOptions} = readConfiguration(project, basePath);
|
||||||
|
ngOptions.basePath = basePath;
|
||||||
|
|
||||||
|
let host = ts.createCompilerHost(parsed.options, true);
|
||||||
|
|
||||||
|
const rootFileNames = parsed.fileNames.slice(0);
|
||||||
|
|
||||||
|
const addGeneratedFileName =
|
||||||
|
(fileName: string) => {
|
||||||
|
if (fileName.startsWith(basePath) && TS_EXT.exec(fileName)) {
|
||||||
|
rootFileNames.push(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) {
|
||||||
|
const {host: bundleHost, indexName, errors} =
|
||||||
|
createBundleIndexHost(ngOptions, rootFileNames, host);
|
||||||
|
if (errors) check(basePath, errors);
|
||||||
|
if (indexName) addGeneratedFileName(indexName);
|
||||||
|
host = bundleHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ngHost = ng.createHost({tsHost: host, options: ngOptions});
|
||||||
|
|
||||||
|
const ngProgram =
|
||||||
|
ng.createProgram({rootNames: rootFileNames, host: ngHost, options: ngOptions});
|
||||||
|
|
||||||
|
// Check parameter diagnostics
|
||||||
|
check(basePath, ngProgram.getTsOptionDiagnostics(), ngProgram.getNgOptionDiagnostics());
|
||||||
|
|
||||||
|
// Check syntactic diagnostics
|
||||||
|
check(basePath, ngProgram.getTsSyntacticDiagnostics());
|
||||||
|
|
||||||
|
// Check TypeScript semantic and Angular structure diagnostics
|
||||||
|
check(basePath, ngProgram.getTsSemanticDiagnostics(), ngProgram.getNgStructuralDiagnostics());
|
||||||
|
|
||||||
|
// Check Angular semantic diagnostics
|
||||||
|
check(basePath, ngProgram.getNgSemanticDiagnostics());
|
||||||
|
|
||||||
|
ngProgram.emit({
|
||||||
|
emitFlags: api.EmitFlags.Default |
|
||||||
|
((ngOptions.skipMetadataEmit || ngOptions.flatModuleOutFile) ? 0 : api.EmitFlags.Metadata)
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (isSyntaxError(e)) {
|
||||||
|
consoleError(e.message);
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
consoleError(e.stack);
|
||||||
|
consoleError('Compilation failed');
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI entry point
|
||||||
|
if (require.main === module) {
|
||||||
|
process.exit(main(process.argv.slice(2), s => console.error(s)));
|
||||||
|
}
|
@ -79,7 +79,6 @@ class CustomLoaderModuleResolutionHostAdapter extends ModuleResolutionHostAdapte
|
|||||||
readResource(path: string) { return this._readResource(path); }
|
readResource(path: string) { return this._readResource(path); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
* @private
|
* @private
|
||||||
@ -92,6 +91,7 @@ export class NgTools_InternalApi_NG_2 {
|
|||||||
static codeGen(options: NgTools_InternalApi_NG2_CodeGen_Options): Promise<any> {
|
static codeGen(options: NgTools_InternalApi_NG2_CodeGen_Options): Promise<any> {
|
||||||
const hostContext: CompilerHostContext =
|
const hostContext: CompilerHostContext =
|
||||||
new CustomLoaderModuleResolutionHostAdapter(options.readResource, options.host);
|
new CustomLoaderModuleResolutionHostAdapter(options.readResource, options.host);
|
||||||
|
|
||||||
const cliOptions: NgcCliOptions = {
|
const cliOptions: NgcCliOptions = {
|
||||||
i18nFormat: options.i18nFormat !,
|
i18nFormat: options.i18nFormat !,
|
||||||
i18nFile: options.i18nFile !,
|
i18nFile: options.i18nFile !,
|
||||||
|
227
packages/compiler-cli/src/transformers/api.ts
Normal file
227
packages/compiler-cli/src/transformers/api.ts
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
/**
|
||||||
|
* @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 {ParseSourceSpan} from '@angular/compiler';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
export enum DiagnosticCategory {
|
||||||
|
Warning = 0,
|
||||||
|
Error = 1,
|
||||||
|
Message = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Diagnostic {
|
||||||
|
message: string;
|
||||||
|
span?: ParseSourceSpan;
|
||||||
|
category: DiagnosticCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompilerOptions extends ts.CompilerOptions {
|
||||||
|
// Absolute path to a directory where generated file structure is written.
|
||||||
|
// If unspecified, generated files will be written alongside sources.
|
||||||
|
genDir?: string;
|
||||||
|
|
||||||
|
// Path to the directory containing the tsconfig.json file.
|
||||||
|
basePath?: string;
|
||||||
|
|
||||||
|
// Don't produce .metadata.json files (they don't work for bundled emit with --out)
|
||||||
|
skipMetadataEmit?: boolean;
|
||||||
|
|
||||||
|
// Produce an error if the metadata written for a class would produce an error if used.
|
||||||
|
strictMetadataEmit?: boolean;
|
||||||
|
|
||||||
|
// Don't produce .ngfactory.ts or .ngstyle.ts files
|
||||||
|
skipTemplateCodegen?: boolean;
|
||||||
|
|
||||||
|
// Whether to generate a flat module index of the given name and the corresponding
|
||||||
|
// flat module metadata. This option is intended to be used when creating flat
|
||||||
|
// modules similar to how `@angular/core` and `@angular/common` are packaged.
|
||||||
|
// When this option is used the `package.json` for the library should refered to the
|
||||||
|
// generated flat module index instead of the library index file. When using this
|
||||||
|
// option only one .metadata.json file is produced that contains all the metadata
|
||||||
|
// necessary for symbols exported from the library index.
|
||||||
|
// In the generated .ngfactory.ts files flat module index is used to import symbols
|
||||||
|
// includes both the public API from the library index as well as shrowded internal
|
||||||
|
// symbols.
|
||||||
|
// By default the .ts file supplied in the `files` files field is assumed to be
|
||||||
|
// library index. If more than one is specified, uses `libraryIndex` to select the
|
||||||
|
// file to use. If more than on .ts file is supplied and no `libraryIndex` is supplied
|
||||||
|
// an error is produced.
|
||||||
|
// A flat module index .d.ts and .js will be created with the given `flatModuleOutFile`
|
||||||
|
// name in the same location as the library index .d.ts file is emitted.
|
||||||
|
// For example, if a library uses `public_api.ts` file as the library index of the
|
||||||
|
// module the `tsconfig.json` `files` field would be `["public_api.ts"]`. The
|
||||||
|
// `flatModuleOutFile` options could then be set to, for example `"index.js"`, which
|
||||||
|
// produces `index.d.ts` and `index.metadata.json` files. The library's
|
||||||
|
// `package.json`'s `module` field would be `"index.js"` and the `typings` field would
|
||||||
|
// be `"index.d.ts"`.
|
||||||
|
flatModuleOutFile?: string;
|
||||||
|
|
||||||
|
// Preferred module id to use for importing flat module. References generated by `ngc`
|
||||||
|
// will use this module name when importing symbols from the flat module. This is only
|
||||||
|
// meaningful when `flatModuleOutFile` is also supplied. It is otherwise ignored.
|
||||||
|
flatModuleId?: string;
|
||||||
|
|
||||||
|
// Whether to generate code for library code.
|
||||||
|
// If true, produce .ngfactory.ts and .ngstyle.ts files for .d.ts inputs.
|
||||||
|
// Default is true.
|
||||||
|
generateCodeForLibraries?: boolean;
|
||||||
|
|
||||||
|
// Insert JSDoc type annotations needed by Closure Compiler
|
||||||
|
annotateForClosureCompiler?: boolean;
|
||||||
|
|
||||||
|
// Modify how angular annotations are emitted to improve tree-shaking.
|
||||||
|
// Default is static fields.
|
||||||
|
// decorators: Leave the Decorators in-place. This makes compilation faster.
|
||||||
|
// TypeScript will emit calls to the __decorate helper.
|
||||||
|
// `--emitDecoratorMetadata` can be used for runtime reflection.
|
||||||
|
// However, the resulting code will not properly tree-shake.
|
||||||
|
// static fields: Replace decorators with a static field in the class.
|
||||||
|
// Allows advanced tree-shakers like Closure Compiler to remove
|
||||||
|
// unused classes.
|
||||||
|
annotationsAs?: 'decorators'|'static fields';
|
||||||
|
|
||||||
|
// Print extra information while running the compiler
|
||||||
|
trace?: boolean;
|
||||||
|
|
||||||
|
// Whether to enable support for <template> and the template attribute (true by default)
|
||||||
|
enableLegacyTemplate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModuleFilenameResolver {
|
||||||
|
/**
|
||||||
|
* Converts a module name that is used in an `import` to a file path.
|
||||||
|
* I.e. `path/to/containingFile.ts` containing `import {...} from 'module-name'`.
|
||||||
|
*/
|
||||||
|
moduleNameToFileName(moduleName: string, containingFile?: string): string|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a file path to a module name that can be used as an `import.
|
||||||
|
* I.e. `path/to/importedFile.ts` should be imported by `path/to/containingFile.ts`.
|
||||||
|
*
|
||||||
|
* See ImportResolver.
|
||||||
|
*/
|
||||||
|
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null;
|
||||||
|
|
||||||
|
getNgCanonicalFileName(fileName: string): string;
|
||||||
|
|
||||||
|
assumeFileExists(fileName: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompilerHost extends ts.CompilerHost, ModuleFilenameResolver {
|
||||||
|
/**
|
||||||
|
* Load a referenced resource either statically or asynchronously. If the host returns a
|
||||||
|
* `Promise<string>` it is assumed the user of the corresponding `Program` will call
|
||||||
|
* `loadNgStructureAsync()`. Returing `Promise<string>` outside `loadNgStructureAsync()` will
|
||||||
|
* cause a diagnostics diagnostic error or an exception to be thrown.
|
||||||
|
*
|
||||||
|
* If `loadResource()` is not provided, `readFile()` will be called to load the resource.
|
||||||
|
*/
|
||||||
|
readResource?(fileName: string): Promise<string>|string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EmitFlags {
|
||||||
|
DTS = 1 << 0,
|
||||||
|
JS = 1 << 1,
|
||||||
|
Metadata = 1 << 2,
|
||||||
|
I18nBundle = 1 << 3,
|
||||||
|
Summary = 1 << 4,
|
||||||
|
|
||||||
|
Default = DTS | JS,
|
||||||
|
All = DTS | JS | Metadata | I18nBundle | Summary
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(chuckj): Support CustomTransformers once we require TypeScript 2.3+
|
||||||
|
// export interface CustomTransformers {
|
||||||
|
// beforeTs?: ts.TransformerFactory<ts.SourceFile>[];
|
||||||
|
// afterTs?: ts.TransformerFactory<ts.SourceFile>[];
|
||||||
|
// }
|
||||||
|
|
||||||
|
export interface Program {
|
||||||
|
/**
|
||||||
|
* Retrieve the TypeScript program used to produce semantic diagnostics and emit the sources.
|
||||||
|
*
|
||||||
|
* Angular structural information is required to produce the program.
|
||||||
|
*/
|
||||||
|
getTsProgram(): ts.Program;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retreive options diagnostics for the TypeScript options used to create the program. This is
|
||||||
|
* faster than calling `getTsProgram().getOptionsDiagnostics()` since it does not need to
|
||||||
|
* collect Angular structural information to produce the errors.
|
||||||
|
*/
|
||||||
|
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken): ts.Diagnostic[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve options diagnostics for the Angular options used to create the program.
|
||||||
|
*/
|
||||||
|
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrive the syntax diagnostics from TypeScript. This is faster than calling
|
||||||
|
* `getTsProgram().getSyntacticDiagnostics()` since it does not need to collect Angular structural
|
||||||
|
* information to produce the errors.
|
||||||
|
*/
|
||||||
|
getTsSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
|
||||||
|
ts.Diagnostic[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the diagnostics for the structure of an Angular application is correctly formed.
|
||||||
|
* This includes validating Angular annotations and the syntax of referenced and imbedded HTML
|
||||||
|
* and CSS.
|
||||||
|
*
|
||||||
|
* Note it is important to displaying TypeScript semantic diagnostics along with Angular
|
||||||
|
* structural diagnostics as an error in the program strucutre might cause errors detected in
|
||||||
|
* semantic analysis and a semantic error might cause errors in specifying the program structure.
|
||||||
|
*
|
||||||
|
* Angular structural information is required to produce these diagnostics.
|
||||||
|
*/
|
||||||
|
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retreive the semantic diagnostics from TypeScript. This is equivilent to calling
|
||||||
|
* `getTsProgram().getSemanticDiagnostics()` directly and is included for completeness.
|
||||||
|
*/
|
||||||
|
getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
|
||||||
|
ts.Diagnostic[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the Angular semantic diagnostics.
|
||||||
|
*
|
||||||
|
* Angular structural information is required to produce these diagnostics.
|
||||||
|
*/
|
||||||
|
getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken):
|
||||||
|
Diagnostic[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Angular structural information asynchronously. If this method is not called then the
|
||||||
|
* Angular structural information, including referenced HTML and CSS files, are loaded
|
||||||
|
* synchronously. If the supplied Angular compiler host returns a promise from `loadResource()`
|
||||||
|
* will produce a diagnostic error message or, `getTsProgram()` or `emit` to throw.
|
||||||
|
*/
|
||||||
|
loadNgStructureAsync(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the lazy route references in the program.
|
||||||
|
*
|
||||||
|
* Angular structural information is required to produce these routes.
|
||||||
|
*/
|
||||||
|
getLazyRoutes(cancellationToken?: ts.CancellationToken): {[route: string]: string};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit the files requested by emitFlags implied by the program.
|
||||||
|
*
|
||||||
|
* Angular structural information is required to emit files.
|
||||||
|
*/
|
||||||
|
emit({// transformers,
|
||||||
|
emitFlags, cancellationToken}: {
|
||||||
|
emitFlags: EmitFlags,
|
||||||
|
// transformers?: CustomTransformers, // See TODO above
|
||||||
|
cancellationToken?: ts.CancellationToken,
|
||||||
|
}): void;
|
||||||
|
}
|
24
packages/compiler-cli/src/transformers/entry_points.ts
Normal file
24
packages/compiler-cli/src/transformers/entry_points.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {CompilerHost, CompilerOptions, Program} from './api';
|
||||||
|
import {createModuleFilenameResolver} from './module_filename_resolver';
|
||||||
|
export {createProgram} from './program';
|
||||||
|
export {createModuleFilenameResolver};
|
||||||
|
|
||||||
|
export function createHost({tsHost, options}: {tsHost: ts.CompilerHost, options: CompilerOptions}):
|
||||||
|
CompilerHost {
|
||||||
|
const resolver = createModuleFilenameResolver(tsHost, options);
|
||||||
|
|
||||||
|
const host = Object.create(tsHost);
|
||||||
|
|
||||||
|
host.moduleNameToFileName = resolver.moduleNameToFileName.bind(resolver);
|
||||||
|
host.fileNameToModuleName = resolver.fileNameToModuleName.bind(resolver);
|
||||||
|
host.getNgCanonicalFileName = resolver.getNgCanonicalFileName.bind(resolver);
|
||||||
|
host.assumeFileExists = resolver.assumeFileExists.bind(resolver);
|
||||||
|
|
||||||
|
// Make sure we do not `host.realpath()` from TS as we do not want to resolve symlinks.
|
||||||
|
// https://github.com/Microsoft/TypeScript/issues/9552
|
||||||
|
host.realpath = (fileName: string) => fileName;
|
||||||
|
|
||||||
|
return host;
|
||||||
|
}
|
@ -0,0 +1,285 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {ModuleFilenameResolver} from './api';
|
||||||
|
import {CompilerOptions} from './api';
|
||||||
|
|
||||||
|
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
||||||
|
const DTS = /\.d\.ts$/;
|
||||||
|
const NODE_MODULES = '/node_modules/';
|
||||||
|
const IS_GENERATED = /\.(ngfactory|ngstyle|ngsummary)$/;
|
||||||
|
const SHALLOW_IMPORT = /^((\w|-)+|(@(\w|-)+(\/(\w|-)+)+))$/;
|
||||||
|
|
||||||
|
export function createModuleFilenameResolver(
|
||||||
|
tsHost: ts.ModuleResolutionHost, options: CompilerOptions): ModuleFilenameResolver {
|
||||||
|
const host = createModuleFilenameResolverHost(tsHost);
|
||||||
|
|
||||||
|
return options.rootDirs && options.rootDirs.length > 0 ?
|
||||||
|
new MultipleRootDirModuleFilenameResolver(host, options) :
|
||||||
|
new SingleRootDirModuleFilenameResolver(host, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SingleRootDirModuleFilenameResolver implements ModuleFilenameResolver {
|
||||||
|
private isGenDirChildOfRootDir: boolean;
|
||||||
|
private basePath: string;
|
||||||
|
private genDir: string;
|
||||||
|
private moduleFileNames = new Map<string, string|null>();
|
||||||
|
|
||||||
|
constructor(private host: ModuleFilenameResolutionHost, private options: CompilerOptions) {
|
||||||
|
// normalize the path so that it never ends with '/'.
|
||||||
|
this.basePath = path.normalize(path.join(options.basePath, '.')).replace(/\\/g, '/');
|
||||||
|
this.genDir = path.normalize(path.join(options.genDir, '.')).replace(/\\/g, '/');
|
||||||
|
|
||||||
|
const genPath: string = path.relative(this.basePath, this.genDir);
|
||||||
|
this.isGenDirChildOfRootDir = genPath === '' || !genPath.startsWith('..');
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleNameToFileName(m: string, containingFile: string): string|null {
|
||||||
|
const key = m + ':' + (containingFile || '');
|
||||||
|
let result: string|null = this.moduleFileNames.get(key) || null;
|
||||||
|
if (!result) {
|
||||||
|
if (!containingFile) {
|
||||||
|
if (m.indexOf('.') === 0) {
|
||||||
|
throw new Error('Resolution of relative paths requires a containing file.');
|
||||||
|
}
|
||||||
|
// Any containing file gives the same result for absolute imports
|
||||||
|
containingFile = this.getNgCanonicalFileName(path.join(this.basePath, 'index.ts'));
|
||||||
|
}
|
||||||
|
m = m.replace(EXT, '');
|
||||||
|
const resolved =
|
||||||
|
ts.resolveModuleName(m, containingFile.replace(/\\/g, '/'), this.options, this.host)
|
||||||
|
.resolvedModule;
|
||||||
|
result = resolved ? this.getNgCanonicalFileName(resolved.resolvedFileName) : null;
|
||||||
|
this.moduleFileNames.set(key, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We want a moduleId that will appear in import statements in the generated code.
|
||||||
|
* These need to be in a form that system.js can load, so absolute file paths don't work.
|
||||||
|
*
|
||||||
|
* The `containingFile` is always in the `genDir`, where as the `importedFile` can be in
|
||||||
|
* `genDir`, `node_module` or `basePath`. The `importedFile` is either a generated file or
|
||||||
|
* existing file.
|
||||||
|
*
|
||||||
|
* | genDir | node_module | rootDir
|
||||||
|
* --------------+----------+-------------+----------
|
||||||
|
* generated | relative | relative | n/a
|
||||||
|
* existing file | n/a | absolute | relative(*)
|
||||||
|
*
|
||||||
|
* NOTE: (*) the relative path is computed depending on `isGenDirChildOfRootDir`.
|
||||||
|
*/
|
||||||
|
fileNameToModuleName(importedFile: string, containingFile: string): string {
|
||||||
|
// If a file does not yet exist (because we compile it later), we still need to
|
||||||
|
// assume it exists it so that the `resolve` method works!
|
||||||
|
if (!this.host.fileExists(importedFile)) {
|
||||||
|
this.host.assumeFileExists(importedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
containingFile = this.rewriteGenDirPath(containingFile);
|
||||||
|
const containingDir = path.dirname(containingFile);
|
||||||
|
// drop extension
|
||||||
|
importedFile = importedFile.replace(EXT, '');
|
||||||
|
|
||||||
|
const nodeModulesIndex = importedFile.indexOf(NODE_MODULES);
|
||||||
|
const importModule = nodeModulesIndex === -1 ?
|
||||||
|
null :
|
||||||
|
importedFile.substring(nodeModulesIndex + NODE_MODULES.length);
|
||||||
|
const isGeneratedFile = IS_GENERATED.test(importedFile);
|
||||||
|
|
||||||
|
if (isGeneratedFile) {
|
||||||
|
// rewrite to genDir path
|
||||||
|
if (importModule) {
|
||||||
|
// it is generated, therefore we do a relative path to the factory
|
||||||
|
return this.dotRelative(containingDir, this.genDir + NODE_MODULES + importModule);
|
||||||
|
} else {
|
||||||
|
// assume that import is also in `genDir`
|
||||||
|
importedFile = this.rewriteGenDirPath(importedFile);
|
||||||
|
return this.dotRelative(containingDir, importedFile);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// user code import
|
||||||
|
if (importModule) {
|
||||||
|
return importModule;
|
||||||
|
} else {
|
||||||
|
if (!this.isGenDirChildOfRootDir) {
|
||||||
|
// assume that they are on top of each other.
|
||||||
|
importedFile = importedFile.replace(this.basePath, this.genDir);
|
||||||
|
}
|
||||||
|
if (SHALLOW_IMPORT.test(importedFile)) {
|
||||||
|
return importedFile;
|
||||||
|
}
|
||||||
|
return this.dotRelative(containingDir, importedFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use absolute paths on disk as canonical.
|
||||||
|
getNgCanonicalFileName(fileName: string): string { return fileName; }
|
||||||
|
|
||||||
|
assumeFileExists(fileName: string) { this.host.assumeFileExists(fileName); }
|
||||||
|
|
||||||
|
private dotRelative(from: string, to: string): string {
|
||||||
|
const rPath: string = path.relative(from, to).replace(/\\/g, '/');
|
||||||
|
return rPath.startsWith('.') ? rPath : './' + rPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the path into `genDir` folder while preserving the `node_modules` directory.
|
||||||
|
*/
|
||||||
|
private rewriteGenDirPath(filepath: string) {
|
||||||
|
const nodeModulesIndex = filepath.indexOf(NODE_MODULES);
|
||||||
|
if (nodeModulesIndex !== -1) {
|
||||||
|
// If we are in node_module, transplant them into `genDir`.
|
||||||
|
return path.join(this.genDir, filepath.substring(nodeModulesIndex));
|
||||||
|
} else {
|
||||||
|
// pretend that containing file is on top of the `genDir` to normalize the paths.
|
||||||
|
// we apply the `genDir` => `rootDir` delta through `rootDirPrefix` later.
|
||||||
|
return filepath.replace(this.basePath, this.genDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This version of the AotCompilerHost expects that the program will be compiled
|
||||||
|
* and executed with a "path mapped" directory structure, where generated files
|
||||||
|
* are in a parallel tree with the sources, and imported using a `./` relative
|
||||||
|
* import. This requires using TS `rootDirs` option and also teaching the module
|
||||||
|
* loader what to do.
|
||||||
|
*/
|
||||||
|
class MultipleRootDirModuleFilenameResolver implements ModuleFilenameResolver {
|
||||||
|
private basePath: string;
|
||||||
|
|
||||||
|
constructor(private host: ModuleFilenameResolutionHost, private options: CompilerOptions) {
|
||||||
|
// normalize the path so that it never ends with '/'.
|
||||||
|
this.basePath = path.normalize(path.join(options.basePath, '.')).replace(/\\/g, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
getNgCanonicalFileName(fileName: string): string {
|
||||||
|
if (!fileName) return fileName;
|
||||||
|
// NB: the rootDirs should have been sorted longest-first
|
||||||
|
for (const dir of this.options.rootDirs || []) {
|
||||||
|
if (fileName.indexOf(dir) === 0) {
|
||||||
|
fileName = fileName.substring(dir.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
assumeFileExists(fileName: string) { this.host.assumeFileExists(fileName); }
|
||||||
|
|
||||||
|
moduleNameToFileName(m: string, containingFile: string): string|null {
|
||||||
|
if (!containingFile) {
|
||||||
|
if (m.indexOf('.') === 0) {
|
||||||
|
throw new Error('Resolution of relative paths requires a containing file.');
|
||||||
|
}
|
||||||
|
// Any containing file gives the same result for absolute imports
|
||||||
|
containingFile = this.getNgCanonicalFileName(path.join(this.basePath, 'index.ts'));
|
||||||
|
}
|
||||||
|
for (const root of this.options.rootDirs || ['']) {
|
||||||
|
const rootedContainingFile = path.join(root, containingFile);
|
||||||
|
const resolved =
|
||||||
|
ts.resolveModuleName(m, rootedContainingFile, this.options, this.host).resolvedModule;
|
||||||
|
if (resolved) {
|
||||||
|
if (this.options.traceResolution) {
|
||||||
|
console.error('resolve', m, containingFile, '=>', resolved.resolvedFileName);
|
||||||
|
}
|
||||||
|
return this.getNgCanonicalFileName(resolved.resolvedFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We want a moduleId that will appear in import statements in the generated code.
|
||||||
|
* These need to be in a form that system.js can load, so absolute file paths don't work.
|
||||||
|
* Relativize the paths by checking candidate prefixes of the absolute path, to see if
|
||||||
|
* they are resolvable by the moduleResolution strategy from the CompilerHost.
|
||||||
|
*/
|
||||||
|
fileNameToModuleName(importedFile: string, containingFile: string): string {
|
||||||
|
if (this.options.traceResolution) {
|
||||||
|
console.error(
|
||||||
|
'getImportPath from containingFile', containingFile, 'to importedFile', importedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a file does not yet exist (because we compile it later), we still need to
|
||||||
|
// assume it exists so that the `resolve` method works!
|
||||||
|
if (!this.host.fileExists(importedFile)) {
|
||||||
|
if (this.options.rootDirs && this.options.rootDirs.length > 0) {
|
||||||
|
this.host.assumeFileExists(path.join(this.options.rootDirs[0], importedFile));
|
||||||
|
} else {
|
||||||
|
this.host.assumeFileExists(importedFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvable = (candidate: string) => {
|
||||||
|
const resolved = this.moduleNameToFileName(candidate, importedFile);
|
||||||
|
return resolved && resolved.replace(EXT, '') === importedFile.replace(EXT, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const importModuleName = importedFile.replace(EXT, '');
|
||||||
|
const parts = importModuleName.split(path.sep).filter(p => !!p);
|
||||||
|
let foundRelativeImport: string|undefined;
|
||||||
|
|
||||||
|
for (let index = parts.length - 1; index >= 0; index--) {
|
||||||
|
let candidate = parts.slice(index, parts.length).join(path.sep);
|
||||||
|
if (resolvable(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
candidate = '.' + path.sep + candidate;
|
||||||
|
if (resolvable(candidate)) {
|
||||||
|
foundRelativeImport = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundRelativeImport) return foundRelativeImport;
|
||||||
|
|
||||||
|
// Try a relative import
|
||||||
|
const candidate = path.relative(path.dirname(containingFile), importModuleName);
|
||||||
|
if (resolvable(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Unable to find any resolvable import for ${importedFile} relative to ${containingFile}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModuleFilenameResolutionHost extends ts.ModuleResolutionHost {
|
||||||
|
assumeFileExists(fileName: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createModuleFilenameResolverHost(host: ts.ModuleResolutionHost):
|
||||||
|
ModuleFilenameResolutionHost {
|
||||||
|
const assumedExists = new Set<string>();
|
||||||
|
const resolveModuleNameHost = Object.create(host);
|
||||||
|
// When calling ts.resolveModuleName, additional allow checks for .d.ts files to be done based on
|
||||||
|
// checks for .ngsummary.json files, so that our codegen depends on fewer inputs and requires
|
||||||
|
// to be called less often.
|
||||||
|
// This is needed as we use ts.resolveModuleName in reflector_host and it should be able to
|
||||||
|
// resolve summary file names.
|
||||||
|
resolveModuleNameHost.fileExists = (fileName: string): boolean => {
|
||||||
|
if (assumedExists.has(fileName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.fileExists(fileName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DTS.test(fileName)) {
|
||||||
|
const base = fileName.substring(0, fileName.length - 5);
|
||||||
|
return host.fileExists(base + '.ngsummary.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
resolveModuleNameHost.assumeFileExists = (fileName: string) => assumedExists.add(fileName);
|
||||||
|
// Make sure we do not `host.realpath()` from TS as we do not want to resolve symlinks.
|
||||||
|
// https://github.com/Microsoft/TypeScript/issues/9552
|
||||||
|
resolveModuleNameHost.realpath = (fileName: string) => fileName;
|
||||||
|
|
||||||
|
return resolveModuleNameHost;
|
||||||
|
}
|
@ -16,9 +16,8 @@ const CATCH_ERROR_NAME = 'error';
|
|||||||
const CATCH_STACK_NAME = 'stack';
|
const CATCH_STACK_NAME = 'stack';
|
||||||
|
|
||||||
export class TypeScriptNodeEmitter {
|
export class TypeScriptNodeEmitter {
|
||||||
updateSourceFile(
|
updateSourceFile(sourceFile: ts.SourceFile, stmts: Statement[], preamble?: string):
|
||||||
sourceFile: ts.SourceFile, srcFilePath: string, genFilePath: string, stmts: Statement[],
|
[ts.SourceFile, Map<ts.Node, Node>] {
|
||||||
exportedVars: string[], preamble?: string): [ts.SourceFile, Map<ts.Node, Node>] {
|
|
||||||
const converter = new _NodeEmitterVisitor();
|
const converter = new _NodeEmitterVisitor();
|
||||||
const statements =
|
const statements =
|
||||||
stmts.map(stmt => stmt.visitStatement(converter, null)).filter(stmt => stmt != null);
|
stmts.map(stmt => stmt.visitStatement(converter, null)).filter(stmt => stmt != null);
|
||||||
@ -99,9 +98,6 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
|||||||
if (stmt.hasModifier(StmtModifier.Exported)) {
|
if (stmt.hasModifier(StmtModifier.Exported)) {
|
||||||
modifiers.push(ts.createToken(ts.SyntaxKind.ExportKeyword));
|
modifiers.push(ts.createToken(ts.SyntaxKind.ExportKeyword));
|
||||||
}
|
}
|
||||||
if (stmt.hasModifier(StmtModifier.Final)) {
|
|
||||||
modifiers.push(ts.createToken(ts.SyntaxKind.ConstKeyword));
|
|
||||||
}
|
|
||||||
return modifiers;
|
return modifiers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +136,7 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
|||||||
p => ts.createParameter(
|
p => ts.createParameter(
|
||||||
/* decorators */ undefined, /* modifiers */ undefined,
|
/* decorators */ undefined, /* modifiers */ undefined,
|
||||||
/* dotDotDotToken */ undefined, p.name)),
|
/* dotDotDotToken */ undefined, p.name)),
|
||||||
undefined, this._visitStatements(stmt.statements)));
|
/* type */ undefined, this._visitStatements(stmt.statements)));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitExpressionStmt(stmt: ExpressionStatement) {
|
visitExpressionStmt(stmt: ExpressionStatement) {
|
||||||
@ -187,7 +183,7 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
|||||||
p => ts.createParameter(
|
p => ts.createParameter(
|
||||||
/* decorators */ undefined, /* modifiers */ undefined,
|
/* decorators */ undefined, /* modifiers */ undefined,
|
||||||
/* dotDotDotToken */ undefined, p.name)),
|
/* dotDotDotToken */ undefined, p.name)),
|
||||||
undefined, this._visitStatements(method.body)));
|
/* type */ undefined, this._visitStatements(method.body)));
|
||||||
return this.record(
|
return this.record(
|
||||||
stmt, ts.createClassDeclaration(
|
stmt, ts.createClassDeclaration(
|
||||||
/* decorators */ undefined, modifiers, stmt.name, /* typeParameters*/ undefined,
|
/* decorators */ undefined, modifiers, stmt.name, /* typeParameters*/ undefined,
|
||||||
@ -221,7 +217,7 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
|||||||
ts.createIdentifier(CATCH_ERROR_NAME),
|
ts.createIdentifier(CATCH_ERROR_NAME),
|
||||||
ts.createIdentifier(CATCH_STACK_NAME)))])],
|
ts.createIdentifier(CATCH_STACK_NAME)))])],
|
||||||
stmt.catchStmts)),
|
stmt.catchStmts)),
|
||||||
undefined));
|
/* finallyBlock */ undefined));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitThrowStmt(stmt: ThrowStmt) {
|
visitThrowStmt(stmt: ThrowStmt) {
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* @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 {GeneratedFile} from '@angular/compiler';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {TypeScriptNodeEmitter} from './node_emitter';
|
||||||
|
|
||||||
|
export function getAngularEmitterTransformFactory(generatedFiles: GeneratedFile[]): () =>
|
||||||
|
(sourceFile: ts.SourceFile) => ts.SourceFile {
|
||||||
|
return function() {
|
||||||
|
const map = new Map(generatedFiles.filter(g => g.stmts && g.stmts.length)
|
||||||
|
.map<[string, GeneratedFile]>(g => [g.genFileUrl, g]));
|
||||||
|
const emitter = new TypeScriptNodeEmitter();
|
||||||
|
return function(sourceFile: ts.SourceFile): ts.SourceFile {
|
||||||
|
const g = map.get(sourceFile.fileName);
|
||||||
|
if (g && g.stmts) {
|
||||||
|
const [newSourceFile] = emitter.updateSourceFile(sourceFile, g.stmts);
|
||||||
|
return newSourceFile;
|
||||||
|
}
|
||||||
|
return sourceFile;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
391
packages/compiler-cli/src/transformers/program.ts
Normal file
391
packages/compiler-cli/src/transformers/program.ts
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
/**
|
||||||
|
* @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 {AotCompiler, GeneratedFile, NgAnalyzedModules, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler';
|
||||||
|
import {MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
|
||||||
|
import {writeFileSync} from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {CompilerHost as AotCompilerHost, CompilerHostContext} from '../compiler_host';
|
||||||
|
import {TypeChecker} from '../diagnostics/check_types';
|
||||||
|
|
||||||
|
import {CompilerHost, CompilerOptions, DiagnosticCategory} from './api';
|
||||||
|
import {Diagnostic, EmitFlags, Program} from './api';
|
||||||
|
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
|
||||||
|
|
||||||
|
const GENERATED_FILES = /\.ngfactory\.js$|\.ngstyle\.js$|\.ngsummary\.js$/;
|
||||||
|
const SUMMARY_JSON_FILES = /\.ngsummary.json$/;
|
||||||
|
|
||||||
|
const emptyModules: NgAnalyzedModules = {
|
||||||
|
ngModules: [],
|
||||||
|
ngModuleByPipeOrDirective: new Map(),
|
||||||
|
files: []
|
||||||
|
};
|
||||||
|
|
||||||
|
class AngularCompilerProgram implements Program {
|
||||||
|
// Initialized in the constructor
|
||||||
|
private oldTsProgram: ts.Program|undefined;
|
||||||
|
private tsProgram: ts.Program;
|
||||||
|
private aotCompilerHost: AotCompilerHost;
|
||||||
|
private compiler: AotCompiler;
|
||||||
|
private srcNames: string[];
|
||||||
|
private collector: MetadataCollector;
|
||||||
|
// Lazily initialized fields
|
||||||
|
private _analyzedModules: NgAnalyzedModules|undefined;
|
||||||
|
private _structuralDiagnostics: Diagnostic[] = [];
|
||||||
|
private _stubs: GeneratedFile[]|undefined;
|
||||||
|
private _stubFiles: string[]|undefined;
|
||||||
|
private _programWithStubsHost: ts.CompilerHost|undefined;
|
||||||
|
private _programWithStubs: ts.Program|undefined;
|
||||||
|
private _generatedFiles: GeneratedFile[]|undefined;
|
||||||
|
private _generatedFileDiagnostics: Diagnostic[]|undefined;
|
||||||
|
private _typeChecker: TypeChecker|undefined;
|
||||||
|
private _semanticDiagnostics: Diagnostic[]|undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private rootNames: string[], private options: CompilerOptions, private host: CompilerHost,
|
||||||
|
private oldProgram?: Program) {
|
||||||
|
this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
|
||||||
|
|
||||||
|
this.tsProgram = ts.createProgram(rootNames, options, host, this.oldTsProgram);
|
||||||
|
this.srcNames = this.tsProgram.getSourceFiles().map(sf => sf.fileName);
|
||||||
|
this.aotCompilerHost = new AotCompilerHost(this.tsProgram, options, host);
|
||||||
|
if (host.readResource) {
|
||||||
|
this.aotCompilerHost.loadResource = host.readResource.bind(host);
|
||||||
|
}
|
||||||
|
const {compiler} = createAotCompiler(this.aotCompilerHost, options);
|
||||||
|
this.compiler = compiler;
|
||||||
|
this.collector = new MetadataCollector({quotedNames: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Program implementation
|
||||||
|
getTsProgram(): ts.Program { return this.programWithStubs; }
|
||||||
|
|
||||||
|
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken) {
|
||||||
|
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[] {
|
||||||
|
return getNgOptionDiagnostics(this.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTsSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
|
||||||
|
ts.Diagnostic[] {
|
||||||
|
return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken): Diagnostic[] {
|
||||||
|
return this.structuralDiagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
|
||||||
|
ts.Diagnostic[] {
|
||||||
|
return this.programWithStubs.getSemanticDiagnostics(sourceFile, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken):
|
||||||
|
Diagnostic[] {
|
||||||
|
const compilerDiagnostics = this.generatedFileDiagnostics;
|
||||||
|
|
||||||
|
// If we have diagnostics during the parser phase the type check phase is not meaningful so skip
|
||||||
|
// it.
|
||||||
|
if (compilerDiagnostics && compilerDiagnostics.length) return compilerDiagnostics;
|
||||||
|
|
||||||
|
return this.typeChecker.getDiagnostics(fileName, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNgStructureAsync(): Promise<void> {
|
||||||
|
return this.compiler.analyzeModulesAsync(this.rootNames)
|
||||||
|
.catch(this.catchAnalysisError.bind(this))
|
||||||
|
.then(analyzedModules => {
|
||||||
|
if (this._analyzedModules) {
|
||||||
|
throw new Error('Angular structure loaded both synchronously and asynchronsly');
|
||||||
|
}
|
||||||
|
this._analyzedModules = analyzedModules;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getLazyRoutes(cancellationToken?: ts.CancellationToken): {[route: string]: string} { return {}; }
|
||||||
|
|
||||||
|
emit({emitFlags = EmitFlags.Default, cancellationToken}:
|
||||||
|
{emitFlags?: EmitFlags, cancellationToken?: ts.CancellationToken}): ts.EmitResult {
|
||||||
|
const emitMap = new Map<string, string>();
|
||||||
|
const result = this.programWithStubs.emit(
|
||||||
|
/* targetSourceFile */ undefined,
|
||||||
|
createWriteFileCallback(emitFlags, this.host, this.collector, this.options, emitMap),
|
||||||
|
cancellationToken, (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, {
|
||||||
|
after: this.options.skipTemplateCodegen ? [] : [getAngularEmitterTransformFactory(
|
||||||
|
this.generatedFiles)]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.generatedFiles.forEach(file => {
|
||||||
|
if (file.source && file.source.length && SUMMARY_JSON_FILES.test(file.genFileUrl)) {
|
||||||
|
// If we have emitted the ngsummary.ts file, ensure the ngsummary.json file is emitted to
|
||||||
|
// the same location.
|
||||||
|
const emittedFile = emitMap.get(file.srcFileUrl);
|
||||||
|
const fileName = emittedFile ?
|
||||||
|
path.join(path.dirname(emittedFile), path.basename(file.genFileUrl)) :
|
||||||
|
file.genFileUrl;
|
||||||
|
this.host.writeFile(fileName, file.source, false, error => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private members
|
||||||
|
private get analyzedModules(): NgAnalyzedModules {
|
||||||
|
return this._analyzedModules || (this._analyzedModules = this.analyzeModules());
|
||||||
|
}
|
||||||
|
|
||||||
|
private get structuralDiagnostics(): Diagnostic[] {
|
||||||
|
return this.analyzedModules && this._structuralDiagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
private get stubs(): GeneratedFile[] {
|
||||||
|
return this._stubs || (this._stubs = this.generateStubs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private get stubFiles(): string[] {
|
||||||
|
return this._stubFiles ||
|
||||||
|
(this._stubFiles = this.stubs.reduce((files: string[], generatedFile) => {
|
||||||
|
if (generatedFile.source || (generatedFile.stmts && generatedFile.stmts.length)) {
|
||||||
|
return [...files, generatedFile.genFileUrl];
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}, []));
|
||||||
|
}
|
||||||
|
|
||||||
|
private get programWithStubsHost(): ts.CompilerHost {
|
||||||
|
return this._programWithStubsHost || (this._programWithStubsHost = createProgramWithStubsHost(
|
||||||
|
this.stubs, this.tsProgram, this.host));
|
||||||
|
}
|
||||||
|
|
||||||
|
private get programWithStubs(): ts.Program {
|
||||||
|
return this._programWithStubs || (this._programWithStubs = this.createProgramWithStubs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private get generatedFiles(): GeneratedFile[] {
|
||||||
|
return this._generatedFiles || (this._generatedFiles = this.generateFiles())
|
||||||
|
}
|
||||||
|
|
||||||
|
private get typeChecker(): TypeChecker {
|
||||||
|
return (this._typeChecker && !this._typeChecker.partialResults) ?
|
||||||
|
this._typeChecker :
|
||||||
|
(this._typeChecker = this.createTypeChecker());
|
||||||
|
}
|
||||||
|
|
||||||
|
private get generatedFileDiagnostics(): Diagnostic[]|undefined {
|
||||||
|
return this.generatedFiles && this._generatedFileDiagnostics !;
|
||||||
|
}
|
||||||
|
|
||||||
|
private catchAnalysisError(e: any): NgAnalyzedModules {
|
||||||
|
if (isSyntaxError(e)) {
|
||||||
|
const parserErrors = getParseErrors(e);
|
||||||
|
if (parserErrors && parserErrors.length) {
|
||||||
|
this._structuralDiagnostics =
|
||||||
|
parserErrors.map<Diagnostic>(e => ({
|
||||||
|
message: e.contextualMessage(),
|
||||||
|
category: DiagnosticCategory.Error,
|
||||||
|
span: e.span
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this._structuralDiagnostics = [{message: e.message, category: DiagnosticCategory.Error}];
|
||||||
|
}
|
||||||
|
this._analyzedModules = emptyModules;
|
||||||
|
return emptyModules;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private analyzeModules() {
|
||||||
|
try {
|
||||||
|
return this.compiler.analyzeModulesSync(this.srcNames);
|
||||||
|
} catch (e) {
|
||||||
|
return this.catchAnalysisError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateStubs() {
|
||||||
|
return this.options.skipTemplateCodegen ? [] :
|
||||||
|
this.options.generateCodeForLibraries === false ?
|
||||||
|
this.compiler.emitAllStubs(this.analyzedModules) :
|
||||||
|
this.compiler.emitPartialStubs(this.analyzedModules);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateFiles() {
|
||||||
|
try {
|
||||||
|
// Always generate the files if requested to ensure we capture any diagnostic errors but only
|
||||||
|
// keep the results if we are not skipping template code generation.
|
||||||
|
const result = this.compiler.emitAllImpls(this.analyzedModules);
|
||||||
|
return this.options.skipTemplateCodegen ? [] : result;
|
||||||
|
} catch (e) {
|
||||||
|
if (isSyntaxError(e)) {
|
||||||
|
this._generatedFileDiagnostics = [{message: e.message, category: DiagnosticCategory.Error}];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createTypeChecker(): TypeChecker {
|
||||||
|
return new TypeChecker(
|
||||||
|
this.tsProgram, this.options, this.host, this.aotCompilerHost, this.options,
|
||||||
|
this.analyzedModules, this.generatedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createProgramWithStubs(): ts.Program {
|
||||||
|
// If we are skipping code generation just use the original program.
|
||||||
|
// Otherwise, create a new program that includes the stub files.
|
||||||
|
return this.options.skipTemplateCodegen ?
|
||||||
|
this.tsProgram :
|
||||||
|
ts.createProgram(
|
||||||
|
[...this.rootNames, ...this.stubFiles], this.options, this.programWithStubsHost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProgram(
|
||||||
|
{rootNames, options, host, oldProgram}:
|
||||||
|
{rootNames: string[], options: CompilerOptions, host: CompilerHost, oldProgram?: Program}):
|
||||||
|
Program {
|
||||||
|
return new AngularCompilerProgram(rootNames, options, host, oldProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeMetadata(
|
||||||
|
emitFilePath: string, sourceFile: ts.SourceFile, collector: MetadataCollector,
|
||||||
|
ngOptions: CompilerOptions) {
|
||||||
|
if (/\.js$/.test(emitFilePath)) {
|
||||||
|
const path = emitFilePath.replace(/\.js$/, '.metadata.json');
|
||||||
|
|
||||||
|
// Beginning with 2.1, TypeScript transforms the source tree before emitting it.
|
||||||
|
// We need the original, unmodified, tree which might be several levels back
|
||||||
|
// depending on the number of transforms performed. All SourceFile's prior to 2.1
|
||||||
|
// will appear to be the original source since they didn't include an original field.
|
||||||
|
let collectableFile = sourceFile;
|
||||||
|
while ((collectableFile as any).original) {
|
||||||
|
collectableFile = (collectableFile as any).original;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = collector.getMetadata(collectableFile, !!ngOptions.strictMetadataEmit);
|
||||||
|
if (metadata) {
|
||||||
|
const metadataText = JSON.stringify([metadata]);
|
||||||
|
writeFileSync(path, metadataText, {encoding: 'utf-8'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWriteFileCallback(
|
||||||
|
emitFlags: EmitFlags, host: ts.CompilerHost, collector: MetadataCollector,
|
||||||
|
ngOptions: CompilerOptions, emitMap: Map<string, string>) {
|
||||||
|
const withMetadata =
|
||||||
|
(fileName: string, data: string, writeByteOrderMark: boolean,
|
||||||
|
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
|
||||||
|
const generatedFile = GENERATED_FILES.test(fileName);
|
||||||
|
if (!generatedFile || data != '') {
|
||||||
|
host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
||||||
|
}
|
||||||
|
if (!generatedFile && sourceFiles && sourceFiles.length == 1) {
|
||||||
|
emitMap.set(sourceFiles[0].fileName, fileName);
|
||||||
|
writeMetadata(fileName, sourceFiles[0], collector, ngOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const withoutMetadata =
|
||||||
|
(fileName: string, data: string, writeByteOrderMark: boolean,
|
||||||
|
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
|
||||||
|
const generatedFile = GENERATED_FILES.test(fileName);
|
||||||
|
if (!generatedFile || data != '') {
|
||||||
|
host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
|
||||||
|
}
|
||||||
|
if (!generatedFile && sourceFiles && sourceFiles.length == 1) {
|
||||||
|
emitMap.set(sourceFiles[0].fileName, fileName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (emitFlags & EmitFlags.Metadata) != 0 ? withMetadata : withoutMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNgOptionDiagnostics(options: CompilerOptions): Diagnostic[] {
|
||||||
|
if (options.annotationsAs) {
|
||||||
|
switch (options.annotationsAs) {
|
||||||
|
case 'decorators':
|
||||||
|
case 'static fields':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return [{
|
||||||
|
message:
|
||||||
|
'Angular compiler options "annotationsAs" only supports "static fields" and "decorators"',
|
||||||
|
category: DiagnosticCategory.Error
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProgramWithStubsHost(
|
||||||
|
generatedFiles: GeneratedFile[], originalProgram: ts.Program,
|
||||||
|
originalHost: ts.CompilerHost): ts.CompilerHost {
|
||||||
|
interface FileData {
|
||||||
|
g: GeneratedFile
|
||||||
|
s?: ts.SourceFile;
|
||||||
|
}
|
||||||
|
return new class implements ts.CompilerHost {
|
||||||
|
private generatedFiles: Map<string, FileData>;
|
||||||
|
writeFile: ts.WriteFileCallback;
|
||||||
|
getCancellationToken: () => ts.CancellationToken;
|
||||||
|
getDefaultLibLocation: () => string;
|
||||||
|
trace: (s: string) => void;
|
||||||
|
getDirectories: (path: string) => string[];
|
||||||
|
directoryExists: (directoryName: string) => boolean;
|
||||||
|
constructor() {
|
||||||
|
this.generatedFiles =
|
||||||
|
new Map(generatedFiles.filter(g => g.source || (g.stmts && g.stmts.length))
|
||||||
|
.map<[string, FileData]>(g => [g.genFileUrl, {g}]));
|
||||||
|
this.writeFile = originalHost.writeFile;
|
||||||
|
if (originalHost.getDirectories) {
|
||||||
|
this.getDirectories = path => originalHost.getDirectories !(path);
|
||||||
|
}
|
||||||
|
if (originalHost.directoryExists) {
|
||||||
|
this.directoryExists = directoryName => originalHost.directoryExists !(directoryName);
|
||||||
|
}
|
||||||
|
if (originalHost.getCancellationToken) {
|
||||||
|
this.getCancellationToken = () => originalHost.getCancellationToken !();
|
||||||
|
}
|
||||||
|
if (originalHost.getDefaultLibLocation) {
|
||||||
|
this.getDefaultLibLocation = () => originalHost.getDefaultLibLocation !();
|
||||||
|
}
|
||||||
|
if (originalHost.trace) {
|
||||||
|
this.trace = s => originalHost.trace !(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getSourceFile(
|
||||||
|
fileName: string, languageVersion: ts.ScriptTarget,
|
||||||
|
onError?: ((message: string) => void)|undefined): ts.SourceFile {
|
||||||
|
const data = this.generatedFiles.get(fileName);
|
||||||
|
if (data) {
|
||||||
|
return data.s || (data.s = ts.createSourceFile(
|
||||||
|
fileName, data.g.source || toTypeScript(data.g), languageVersion));
|
||||||
|
}
|
||||||
|
return originalProgram.getSourceFile(fileName) ||
|
||||||
|
originalHost.getSourceFile(fileName, languageVersion, onError);
|
||||||
|
}
|
||||||
|
readFile(fileName: string): string {
|
||||||
|
const data = this.generatedFiles.get(fileName);
|
||||||
|
if (data) {
|
||||||
|
return data.g.source || toTypeScript(data.g);
|
||||||
|
}
|
||||||
|
return originalHost.readFile(fileName);
|
||||||
|
}
|
||||||
|
getDefaultLibFileName = (options: ts.CompilerOptions) =>
|
||||||
|
originalHost.getDefaultLibFileName(options);
|
||||||
|
getCurrentDirectory = () => originalHost.getCurrentDirectory();
|
||||||
|
getCanonicalFileName = (fileName: string) => originalHost.getCanonicalFileName(fileName);
|
||||||
|
useCaseSensitiveFileNames = () => originalHost.useCaseSensitiveFileNames();
|
||||||
|
getNewLine = () => originalHost.getNewLine();
|
||||||
|
fileExists = (fileName: string) =>
|
||||||
|
this.generatedFiles.has(fileName) || originalHost.fileExists(fileName);
|
||||||
|
};
|
||||||
|
}
|
@ -10,7 +10,8 @@ import {AotCompilerOptions, createAotCompiler} from '@angular/compiler';
|
|||||||
import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, arrayToMockDir, arrayToMockMap, isSource, settings, setup, toMockFileArray} from '@angular/compiler/test/aot/test_util';
|
import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, arrayToMockDir, arrayToMockMap, isSource, settings, setup, toMockFileArray} from '@angular/compiler/test/aot/test_util';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {Diagnostic, TypeChecker} from '../../src/diagnostics/check_types';
|
import {TypeChecker} from '../../src/diagnostics/check_types';
|
||||||
|
import {Diagnostic} from '../../src/transformers/api';
|
||||||
|
|
||||||
function compile(
|
function compile(
|
||||||
rootDirs: MockData, options: AotCompilerOptions = {},
|
rootDirs: MockData, options: AotCompilerOptions = {},
|
||||||
|
@ -18,7 +18,6 @@ import {DiagnosticContext, MockLanguageServiceHost, getDiagnosticTemplateInfo} f
|
|||||||
describe('expression diagnostics', () => {
|
describe('expression diagnostics', () => {
|
||||||
let registry: ts.DocumentRegistry;
|
let registry: ts.DocumentRegistry;
|
||||||
let host: MockLanguageServiceHost;
|
let host: MockLanguageServiceHost;
|
||||||
let compilerHost: CompilerHost;
|
|
||||||
let service: ts.LanguageService;
|
let service: ts.LanguageService;
|
||||||
let context: DiagnosticContext;
|
let context: DiagnosticContext;
|
||||||
let aotHost: CompilerHost;
|
let aotHost: CompilerHost;
|
||||||
|
@ -46,7 +46,7 @@ export class MockLanguageServiceHost implements ts.LanguageServiceHost, Compiler
|
|||||||
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
|
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
|
||||||
paths: {'@angular/*': [calcRootPath() + '/packages/*']}
|
paths: {'@angular/*': [calcRootPath() + '/packages/*']}
|
||||||
};
|
};
|
||||||
this.context = new MockAotContext(currentDirectory, files)
|
this.context = new MockAotContext(currentDirectory, files);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompilationSettings(): ts.CompilerOptions { return this.options; }
|
getCompilationSettings(): ts.CompilerOptions { return this.options; }
|
||||||
|
629
packages/compiler-cli/test/ngc_spec.ts
Normal file
629
packages/compiler-cli/test/ngc_spec.ts
Normal file
@ -0,0 +1,629 @@
|
|||||||
|
/**
|
||||||
|
* @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 {makeTempDir} from '@angular/tsc-wrapped/test/test_support';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import {main} from '../src/ngc';
|
||||||
|
|
||||||
|
function getNgRootDir() {
|
||||||
|
const moduleFilename = module.filename.replace(/\\/g, '/');
|
||||||
|
const distIndex = moduleFilename.indexOf('/dist/all');
|
||||||
|
return moduleFilename.substr(0, distIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ngc command-line', () => {
|
||||||
|
let basePath: string;
|
||||||
|
let outDir: string;
|
||||||
|
let write: (fileName: string, content: string) => void;
|
||||||
|
|
||||||
|
function writeConfig(tsconfig: string = '{"extends": "./tsconfig-base.json"}') {
|
||||||
|
write('tsconfig.json', tsconfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
basePath = makeTempDir();
|
||||||
|
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'});
|
||||||
|
};
|
||||||
|
write('tsconfig-base.json', `{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"types": [],
|
||||||
|
"outDir": "built",
|
||||||
|
"declaration": true,
|
||||||
|
"module": "es2015",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": ["es6", "dom"]
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
outDir = path.resolve(basePath, 'built');
|
||||||
|
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'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compile without errors', () => {
|
||||||
|
writeConfig();
|
||||||
|
write('test.ts', 'export const A = 1;');
|
||||||
|
|
||||||
|
const mockConsole = {error: (s: string) => {}};
|
||||||
|
|
||||||
|
spyOn(mockConsole, 'error');
|
||||||
|
|
||||||
|
const result = main(['-p', basePath], mockConsole.error);
|
||||||
|
expect(mockConsole.error).not.toHaveBeenCalled();
|
||||||
|
expect(result).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not print the stack trace if user input file does not exist', () => {
|
||||||
|
writeConfig(`{
|
||||||
|
"extends": "./tsconfig-base.json",
|
||||||
|
"files": ["test.ts"]
|
||||||
|
}`);
|
||||||
|
const mockConsole = {error: (s: string) => {}};
|
||||||
|
|
||||||
|
spyOn(mockConsole, 'error');
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath], mockConsole.error);
|
||||||
|
expect(mockConsole.error)
|
||||||
|
.toHaveBeenCalledWith(
|
||||||
|
`error TS6053: File '` + path.join(basePath, 'test.ts') + `' not found.` +
|
||||||
|
'\n');
|
||||||
|
expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed');
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not print the stack trace if user input file is malformed', () => {
|
||||||
|
writeConfig();
|
||||||
|
write('test.ts', 'foo;');
|
||||||
|
|
||||||
|
const mockConsole = {error: (s: string) => {}};
|
||||||
|
|
||||||
|
spyOn(mockConsole, 'error');
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath], mockConsole.error);
|
||||||
|
expect(mockConsole.error)
|
||||||
|
.toHaveBeenCalledWith(
|
||||||
|
`test.ts(1,1): error TS2304: Cannot find name 'foo'.` +
|
||||||
|
'\n');
|
||||||
|
expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed');
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not print the stack trace if cannot find the imported module', () => {
|
||||||
|
writeConfig();
|
||||||
|
write('test.ts', `import {MyClass} from './not-exist-deps';`);
|
||||||
|
|
||||||
|
const mockConsole = {error: (s: string) => {}};
|
||||||
|
|
||||||
|
spyOn(mockConsole, 'error');
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath], mockConsole.error);
|
||||||
|
expect(mockConsole.error)
|
||||||
|
.toHaveBeenCalledWith(
|
||||||
|
`test.ts(1,23): error TS2307: Cannot find module './not-exist-deps'.` +
|
||||||
|
'\n');
|
||||||
|
expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed');
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not print the stack trace if cannot import', () => {
|
||||||
|
writeConfig();
|
||||||
|
write('empty-deps.ts', 'export const A = 1;');
|
||||||
|
write('test.ts', `import {MyClass} from './empty-deps';`);
|
||||||
|
|
||||||
|
const mockConsole = {error: (s: string) => {}};
|
||||||
|
|
||||||
|
spyOn(mockConsole, 'error');
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath], mockConsole.error);
|
||||||
|
expect(mockConsole.error)
|
||||||
|
.toHaveBeenCalledWith(
|
||||||
|
`test.ts(1,9): error TS2305: Module '"` + path.join(basePath, 'empty-deps') +
|
||||||
|
`"' has no exported member 'MyClass'.` +
|
||||||
|
'\n');
|
||||||
|
expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed');
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not print the stack trace if type mismatches', () => {
|
||||||
|
writeConfig();
|
||||||
|
write('empty-deps.ts', 'export const A = "abc";');
|
||||||
|
write('test.ts', `
|
||||||
|
import {A} from './empty-deps';
|
||||||
|
A();
|
||||||
|
`);
|
||||||
|
|
||||||
|
const mockConsole = {error: (s: string) => {}};
|
||||||
|
|
||||||
|
spyOn(mockConsole, 'error');
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath], mockConsole.error);
|
||||||
|
expect(mockConsole.error)
|
||||||
|
.toHaveBeenCalledWith(
|
||||||
|
'test.ts(3,7): error TS2349: Cannot invoke an expression whose type lacks a call signature. ' +
|
||||||
|
'Type \'String\' has no compatible call signatures.\n');
|
||||||
|
expect(mockConsole.error).not.toHaveBeenCalledWith('Compilation failed');
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should print the stack trace on compiler internal errors', () => {
|
||||||
|
write('test.ts', 'export const A = 1;');
|
||||||
|
|
||||||
|
const mockConsole = {error: (s: string) => {}};
|
||||||
|
|
||||||
|
spyOn(mockConsole, 'error');
|
||||||
|
|
||||||
|
const exitCode = main(['-p', 'not-exist'], mockConsole.error);
|
||||||
|
expect(mockConsole.error).toHaveBeenCalled();
|
||||||
|
expect(mockConsole.error).toHaveBeenCalledWith('Compilation failed');
|
||||||
|
expect(exitCode).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('compile ngfactory files', () => {
|
||||||
|
it('should report errors for ngfactory files that are not referenced by root files', () => {
|
||||||
|
writeConfig(`{
|
||||||
|
"extends": "./tsconfig-base.json",
|
||||||
|
"files": ["mymodule.ts"]
|
||||||
|
}`);
|
||||||
|
write('mymodule.ts', `
|
||||||
|
import {NgModule, Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({template: '{{unknownProp}}'})
|
||||||
|
export class MyComp {}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComp]})
|
||||||
|
export class MyModule {}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const mockConsole = {error: (s: string) => {}};
|
||||||
|
|
||||||
|
const errorSpy = spyOn(mockConsole, 'error');
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath], mockConsole.error);
|
||||||
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(errorSpy.calls.mostRecent().args[0])
|
||||||
|
.toContain('Error at ng://' + path.join(basePath, 'mymodule.ts.MyComp.html'));
|
||||||
|
expect(errorSpy.calls.mostRecent().args[0])
|
||||||
|
.toContain(`Property 'unknownProp' does not exist on type 'MyComp'`);
|
||||||
|
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report errors as coming from the html file, not the factory', () => {
|
||||||
|
writeConfig(`{
|
||||||
|
"extends": "./tsconfig-base.json",
|
||||||
|
"files": ["mymodule.ts"]
|
||||||
|
}`);
|
||||||
|
write('my.component.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
@Component({templateUrl: './my.component.html'})
|
||||||
|
export class MyComp {}
|
||||||
|
`);
|
||||||
|
write('my.component.html', `<h1>
|
||||||
|
{{unknownProp}}
|
||||||
|
</h1>`);
|
||||||
|
write('mymodule.ts', `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {MyComp} from './my.component';
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComp]})
|
||||||
|
export class MyModule {}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const mockConsole = {error: (s: string) => {}};
|
||||||
|
|
||||||
|
const errorSpy = spyOn(mockConsole, 'error');
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath], mockConsole.error);
|
||||||
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(errorSpy.calls.mostRecent().args[0])
|
||||||
|
.toContain('Error at ng://' + path.join(basePath, 'my.component.html(1,5):'));
|
||||||
|
expect(errorSpy.calls.mostRecent().args[0])
|
||||||
|
.toContain(`Property 'unknownProp' does not exist on type 'MyComp'`);
|
||||||
|
|
||||||
|
expect(exitCode).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compile ngfactory files that are not referenced by root files', () => {
|
||||||
|
writeConfig(`{
|
||||||
|
"extends": "./tsconfig-base.json",
|
||||||
|
"files": ["mymodule.ts"]
|
||||||
|
}`);
|
||||||
|
write('mymodule.ts', `
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [CommonModule]
|
||||||
|
})
|
||||||
|
export class MyModule {}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const exitCode = main(['-p', basePath]);
|
||||||
|
expect(exitCode).toEqual(0);
|
||||||
|
|
||||||
|
expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.resolve(
|
||||||
|
outDir, 'node_modules', '@angular', 'core', 'src',
|
||||||
|
'application_module.ngfactory.js')))
|
||||||
|
.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compile with a explicit tsconfig reference', () => {
|
||||||
|
writeConfig(`{
|
||||||
|
"extends": "./tsconfig-base.json",
|
||||||
|
"files": ["mymodule.ts"]
|
||||||
|
}`);
|
||||||
|
write('mymodule.ts', `
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [CommonModule]
|
||||||
|
})
|
||||||
|
export class MyModule {}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]);
|
||||||
|
expect(exitCode).toEqual(0);
|
||||||
|
|
||||||
|
expect(fs.existsSync(path.resolve(outDir, 'mymodule.ngfactory.js'))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.resolve(
|
||||||
|
outDir, 'node_modules', '@angular', 'core', 'src',
|
||||||
|
'application_module.ngfactory.js')))
|
||||||
|
.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldExist = (fileName: string) => {
|
||||||
|
if (!fs.existsSync(path.resolve(outDir, fileName))) {
|
||||||
|
throw new Error(`Expected ${fileName} to be emitted (outDir: ${outDir})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const shouldNotExist =
|
||||||
|
(fileName: string) => {
|
||||||
|
if (fs.existsSync(path.resolve(outDir, fileName))) {
|
||||||
|
throw new Error(`Did not expect ${fileName} to be emitted (outDir: ${outDir})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should be able to generate a flat module library', () => {
|
||||||
|
writeConfig(`
|
||||||
|
{
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"genDir": "ng",
|
||||||
|
"flatModuleId": "flat_module",
|
||||||
|
"flatModuleOutFile": "index.js",
|
||||||
|
"skipTemplateCodegen": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"rootDir": "",
|
||||||
|
"declaration": true,
|
||||||
|
"lib": ["es6", "dom"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "built",
|
||||||
|
"typeRoots": ["node_modules/@types"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"files": ["public-api.ts"]
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
write('public-api.ts', `
|
||||||
|
export * from './src/flat.component';
|
||||||
|
export * from './src/flat.module';`);
|
||||||
|
write('src/flat.component.html', '<div>flat module component</div>');
|
||||||
|
write('src/flat.component.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'flat-comp',
|
||||||
|
templateUrl: 'flat.component.html',
|
||||||
|
})
|
||||||
|
export class FlatComponent {
|
||||||
|
}`);
|
||||||
|
write('src/flat.module.ts', `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
import {FlatComponent} from './flat.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
FlatComponent,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
FlatComponent,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class FlatModule {
|
||||||
|
}`);
|
||||||
|
|
||||||
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]);
|
||||||
|
expect(exitCode).toEqual(0);
|
||||||
|
shouldExist('index.js');
|
||||||
|
shouldExist('index.metadata.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a third-party library', () => {
|
||||||
|
const writeGenConfig = (skipCodegen: boolean) => {
|
||||||
|
writeConfig(`{
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"skipTemplateCodegen": ${skipCodegen},
|
||||||
|
"enableSummariesForJit": true
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"rootDir": "",
|
||||||
|
"declaration": true,
|
||||||
|
"lib": ["es6", "dom"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "built",
|
||||||
|
"typeRoots": ["node_modules/@types"]
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
};
|
||||||
|
beforeEach(() => {
|
||||||
|
write('comp.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'third-party-comp',
|
||||||
|
template: '<div>3rdP-component</div>',
|
||||||
|
})
|
||||||
|
export class ThirdPartyComponent {
|
||||||
|
}`);
|
||||||
|
write('directive.ts', `
|
||||||
|
import {Directive, Input} from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[thirdParty]',
|
||||||
|
host: {'[title]': 'thirdParty'},
|
||||||
|
})
|
||||||
|
export class ThirdPartyDirective {
|
||||||
|
@Input() thirdParty: string;
|
||||||
|
}`);
|
||||||
|
write('module.ts', `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
import {ThirdPartyComponent} from './comp';
|
||||||
|
import {ThirdPartyDirective} from './directive';
|
||||||
|
import {AnotherThirdPartyModule} from './other_module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
ThirdPartyComponent,
|
||||||
|
ThirdPartyDirective,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AnotherThirdPartyModule,
|
||||||
|
ThirdPartyComponent,
|
||||||
|
ThirdPartyDirective,
|
||||||
|
],
|
||||||
|
imports: [AnotherThirdPartyModule]
|
||||||
|
})
|
||||||
|
export class ThirdpartyModule {
|
||||||
|
}`);
|
||||||
|
write('other_comp.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'another-third-party-comp',
|
||||||
|
template: \`<div i18n>other-3rdP-component
|
||||||
|
multi-lines</div>\`,
|
||||||
|
})
|
||||||
|
export class AnotherThirdpartyComponent {
|
||||||
|
}`);
|
||||||
|
write('other_module.ts', `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
import {AnotherThirdpartyComponent} from './other_comp';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AnotherThirdpartyComponent],
|
||||||
|
exports: [AnotherThirdpartyComponent],
|
||||||
|
})
|
||||||
|
export class AnotherThirdPartyModule {
|
||||||
|
}`);
|
||||||
|
});
|
||||||
|
const modules = ['comp', 'directive', 'module', 'other_comp', 'other_module'];
|
||||||
|
it('should honor skip code generation', () => {
|
||||||
|
// First ensure that we skip code generation when requested;.
|
||||||
|
writeGenConfig(/* skipCodegen */ true);
|
||||||
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]);
|
||||||
|
expect(exitCode).toEqual(0);
|
||||||
|
modules.forEach(moduleName => {
|
||||||
|
shouldExist(moduleName + '.js');
|
||||||
|
shouldExist(moduleName + '.d.ts');
|
||||||
|
shouldExist(moduleName + '.metadata.json');
|
||||||
|
shouldNotExist(moduleName + '.ngfactory.js');
|
||||||
|
shouldNotExist(moduleName + '.ngfactory.d.ts');
|
||||||
|
shouldNotExist(moduleName + '.ngsummary.js');
|
||||||
|
shouldNotExist(moduleName + '.ngsummary.d.ts');
|
||||||
|
shouldNotExist(moduleName + '.ngsummary.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should produce factories', () => {
|
||||||
|
// First ensure that we skip code generation when requested;.
|
||||||
|
writeGenConfig(/* skipCodegen */ false);
|
||||||
|
const exitCode = main(['-p', path.join(basePath, 'tsconfig.json')]);
|
||||||
|
expect(exitCode).toEqual(0);
|
||||||
|
modules.forEach(moduleName => {
|
||||||
|
shouldExist(moduleName + '.js');
|
||||||
|
shouldExist(moduleName + '.d.ts');
|
||||||
|
shouldExist(moduleName + '.metadata.json');
|
||||||
|
if (!/(directive)|(pipe)/.test(moduleName)) {
|
||||||
|
shouldExist(moduleName + '.ngfactory.js');
|
||||||
|
shouldExist(moduleName + '.ngfactory.d.ts');
|
||||||
|
}
|
||||||
|
shouldExist(moduleName + '.ngsummary.js');
|
||||||
|
shouldExist(moduleName + '.ngsummary.d.ts');
|
||||||
|
shouldExist(moduleName + '.ngsummary.json');
|
||||||
|
shouldNotExist(moduleName + '.ngfactory.metadata.json');
|
||||||
|
shouldNotExist(moduleName + '.ngsummary.metadata.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with tree example', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
writeConfig();
|
||||||
|
write('index_aot.ts', `
|
||||||
|
import {enableProdMode} from '@angular/core';
|
||||||
|
import {platformBrowser} from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import {AppModuleNgFactory} from './tree.ngfactory';
|
||||||
|
|
||||||
|
enableProdMode();
|
||||||
|
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);`);
|
||||||
|
write('tree.ts', `
|
||||||
|
import {Component, NgModule} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tree',
|
||||||
|
inputs: ['data'],
|
||||||
|
template:
|
||||||
|
\`<span [style.backgroundColor]="bgColor"> {{data.value}} </span><tree *ngIf='data.right != null' [data]='data.right'></tree><tree *ngIf='data.left != null' [data]='data.left'></tree>\`
|
||||||
|
})
|
||||||
|
export class TreeComponent {
|
||||||
|
data: any;
|
||||||
|
bgColor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({imports: [CommonModule], bootstrap: [TreeComponent], declarations: [TreeComponent]})
|
||||||
|
export class AppModule {}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compile without error', () => {
|
||||||
|
expect(main(['-p', path.join(basePath, 'tsconfig.json')])).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with summary libraries', () => {
|
||||||
|
// TODO{chuckj}: Emitting using summaries only works if outDir is set to '.'
|
||||||
|
const shouldExist = (fileName: string) => {
|
||||||
|
if (!fs.existsSync(path.resolve(basePath, fileName))) {
|
||||||
|
throw new Error(`Expected ${fileName} to be emitted (basePath: ${basePath})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const shouldNotExist = (fileName: string) => {
|
||||||
|
if (fs.existsSync(path.resolve(basePath, fileName))) {
|
||||||
|
throw new Error(`Did not expect ${fileName} to be emitted (basePath: ${basePath})`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
beforeEach(() => {
|
||||||
|
const writeConfig = (dir: string) => {
|
||||||
|
write(path.join(dir, 'tsconfig.json'), `
|
||||||
|
{
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"generateCodeForLibraries": false,
|
||||||
|
"enableSummariesForJit": true
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"rootDir": "",
|
||||||
|
"declaration": true,
|
||||||
|
"lib": ["es6", "dom"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": { "lib1/*": ["../lib1/*"], "lib2/*": ["../lib2/*"] },
|
||||||
|
"typeRoots": []
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lib 1
|
||||||
|
writeConfig('lib1');
|
||||||
|
write('lib1/module.ts', `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
export function someFactory(): any { return null; }
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
providers: [{provide: 'foo', useFactory: someFactory}]
|
||||||
|
})
|
||||||
|
export class Module {}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Lib 2
|
||||||
|
writeConfig('lib2');
|
||||||
|
write('lib2/module.ts', `
|
||||||
|
export {Module} from 'lib1/module';
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Application
|
||||||
|
writeConfig('app');
|
||||||
|
write('app/main.ts', `
|
||||||
|
import {NgModule, Inject} from '@angular/core';
|
||||||
|
import {Module} from 'lib2/module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [Module]
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
constructor(@Inject('foo') public foo: any) {}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to compile library 1', () => {
|
||||||
|
expect(main(['-p', path.join(basePath, 'lib1')])).toBe(0);
|
||||||
|
shouldExist('lib1/module.js');
|
||||||
|
shouldExist('lib1/module.ngsummary.json');
|
||||||
|
shouldExist('lib1/module.ngsummary.js');
|
||||||
|
shouldExist('lib1/module.ngsummary.d.ts');
|
||||||
|
shouldExist('lib1/module.ngfactory.js');
|
||||||
|
shouldExist('lib1/module.ngfactory.d.ts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to compiler library 2', () => {
|
||||||
|
expect(main(['-p', path.join(basePath, 'lib1')])).toBe(0);
|
||||||
|
expect(main(['-p', path.join(basePath, 'lib2')])).toBe(0);
|
||||||
|
shouldExist('lib2/module.js');
|
||||||
|
shouldExist('lib2/module.ngsummary.json');
|
||||||
|
shouldExist('lib2/module.ngsummary.js');
|
||||||
|
shouldExist('lib2/module.ngsummary.d.ts');
|
||||||
|
shouldExist('lib2/module.ngfactory.js');
|
||||||
|
shouldExist('lib2/module.ngfactory.d.ts');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('building an application', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
expect(main(['-p', path.join(basePath, 'lib1')])).toBe(0);
|
||||||
|
expect(main(['-p', path.join(basePath, 'lib2')])).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build without error', () => {
|
||||||
|
expect(main(['-p', path.join(basePath, 'app')])).toBe(0);
|
||||||
|
shouldExist('app/main.js');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -43,8 +43,7 @@ describe('TypeScriptEmitter', () => {
|
|||||||
const transformers: ts.CustomTransformers = {
|
const transformers: ts.CustomTransformers = {
|
||||||
before: [context => {
|
before: [context => {
|
||||||
return sourceFile => {
|
return sourceFile => {
|
||||||
const [newSourceFile] = emitter.updateSourceFile(
|
const [newSourceFile] = emitter.updateSourceFile(sourceFile, stmts, preamble);
|
||||||
sourceFile, someGenFileName, someGenFilePath, stmts, [], preamble);
|
|
||||||
return newSourceFile;
|
return newSourceFile;
|
||||||
};
|
};
|
||||||
}]
|
}]
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"index.ts",
|
"index.ts",
|
||||||
"src/main.ts",
|
"src/main.ts",
|
||||||
|
"src/ngc.ts",
|
||||||
"src/extract_i18n.ts",
|
"src/extract_i18n.ts",
|
||||||
"../../node_modules/@types/node/index.d.ts",
|
"../../node_modules/@types/node/index.d.ts",
|
||||||
"../../node_modules/@types/jasmine/index.d.ts",
|
"../../node_modules/@types/jasmine/index.d.ts",
|
||||||
|
@ -63,8 +63,17 @@ export class AotCompiler {
|
|||||||
|
|
||||||
emitAllStubs(analyzeResult: NgAnalyzedModules): GeneratedFile[] {
|
emitAllStubs(analyzeResult: NgAnalyzedModules): GeneratedFile[] {
|
||||||
const {files} = analyzeResult;
|
const {files} = analyzeResult;
|
||||||
const sourceModules =
|
const sourceModules = files.map(
|
||||||
files.map(file => this._compileStubFile(file.srcUrl, file.directives, file.ngModules));
|
file =>
|
||||||
|
this._compileStubFile(file.srcUrl, file.directives, file.pipes, file.ngModules, false));
|
||||||
|
return flatten(sourceModules);
|
||||||
|
}
|
||||||
|
|
||||||
|
emitPartialStubs(analyzeResult: NgAnalyzedModules): GeneratedFile[] {
|
||||||
|
const {files} = analyzeResult;
|
||||||
|
const sourceModules = files.map(
|
||||||
|
file =>
|
||||||
|
this._compileStubFile(file.srcUrl, file.directives, file.pipes, file.ngModules, true));
|
||||||
return flatten(sourceModules);
|
return flatten(sourceModules);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,13 +87,22 @@ export class AotCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _compileStubFile(
|
private _compileStubFile(
|
||||||
srcFileUrl: string, directives: StaticSymbol[], ngModules: StaticSymbol[]): GeneratedFile[] {
|
srcFileUrl: string, directives: StaticSymbol[], pipes: StaticSymbol[],
|
||||||
|
ngModules: StaticSymbol[], partial: boolean): GeneratedFile[] {
|
||||||
|
// partial is true when we only need the files we are certain will produce a factory and/or
|
||||||
|
// summary.
|
||||||
|
// This is the normal case for `ngc` but if we assume libraryies are generating their own
|
||||||
|
// factories
|
||||||
|
// then we might need a factory for a file that re-exports a module or factory which we cannot
|
||||||
|
// know
|
||||||
|
// ahead of time so we need a stub generate for all non-.d.ts files. The .d.ts files do not need
|
||||||
|
// to
|
||||||
|
// be excluded here because they are excluded when the modules are analyzed. If a factory ends
|
||||||
|
// up
|
||||||
|
// not being needed, the factory file is not written in writeFile callback.
|
||||||
const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1];
|
const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1];
|
||||||
const generatedFiles: GeneratedFile[] = [];
|
const generatedFiles: GeneratedFile[] = [];
|
||||||
|
|
||||||
const jitSummaryStmts: o.Statement[] = [];
|
|
||||||
const ngFactoryStms: o.Statement[] = [];
|
|
||||||
|
|
||||||
const ngFactoryOutputCtx = this._createOutputContext(ngfactoryFilePath(srcFileUrl, true));
|
const ngFactoryOutputCtx = this._createOutputContext(ngfactoryFilePath(srcFileUrl, true));
|
||||||
const jitSummaryOutputCtx = this._createOutputContext(summaryForJitFileName(srcFileUrl, true));
|
const jitSummaryOutputCtx = this._createOutputContext(summaryForJitFileName(srcFileUrl, true));
|
||||||
|
|
||||||
@ -93,29 +111,50 @@ export class AotCompiler {
|
|||||||
this._ngModuleCompiler.createStub(ngFactoryOutputCtx, ngModuleReference);
|
this._ngModuleCompiler.createStub(ngFactoryOutputCtx, ngModuleReference);
|
||||||
createForJitStub(jitSummaryOutputCtx, ngModuleReference);
|
createForJitStub(jitSummaryOutputCtx, ngModuleReference);
|
||||||
});
|
});
|
||||||
// Note: we are creating stub ngfactory/ngsummary for all source files,
|
|
||||||
// as the real calculation requires almost the same logic as producing the real content for
|
let partialJitStubRequired = false;
|
||||||
// them.
|
let partialFactoryStubRequired = false;
|
||||||
// Our pipeline will filter out empty ones at the end.
|
|
||||||
generatedFiles.push(this._codegenSourceModule(srcFileUrl, ngFactoryOutputCtx));
|
|
||||||
generatedFiles.push(this._codegenSourceModule(srcFileUrl, jitSummaryOutputCtx));
|
|
||||||
|
|
||||||
// create stubs for external stylesheets (always empty, as users should not import anything from
|
// create stubs for external stylesheets (always empty, as users should not import anything from
|
||||||
// the generated code)
|
// the generated code)
|
||||||
directives.forEach((dirType) => {
|
directives.forEach((dirType) => {
|
||||||
const compMeta = this._metadataResolver.getDirectiveMetadata(<any>dirType);
|
const compMeta = this._metadataResolver.getDirectiveMetadata(<any>dirType);
|
||||||
|
|
||||||
|
partialJitStubRequired = true;
|
||||||
|
|
||||||
if (!compMeta.isComponent) {
|
if (!compMeta.isComponent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Note: compMeta is a component and therefore template is non null.
|
// Note: compMeta is a component and therefore template is non null.
|
||||||
compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => {
|
compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => {
|
||||||
generatedFiles.push(this._codegenSourceModule(
|
const styleContext = this._createOutputContext(_stylesModuleUrl(
|
||||||
stylesheetMeta.moduleUrl !,
|
stylesheetMeta.moduleUrl !, this._styleCompiler.needsStyleShim(compMeta), fileSuffix));
|
||||||
this._createOutputContext(_stylesModuleUrl(
|
_createTypeReferenceStub(styleContext, Identifiers.ComponentFactory);
|
||||||
stylesheetMeta.moduleUrl !, this._styleCompiler.needsStyleShim(compMeta),
|
generatedFiles.push(this._codegenSourceModule(stylesheetMeta.moduleUrl !, styleContext));
|
||||||
fileSuffix))));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
partialFactoryStubRequired = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If we need all the stubs to be generated then insert an arbitrary reference into the stub
|
||||||
|
if ((partialFactoryStubRequired || !partial) && ngFactoryOutputCtx.statements.length <= 0) {
|
||||||
|
_createTypeReferenceStub(ngFactoryOutputCtx, Identifiers.ComponentFactory);
|
||||||
|
}
|
||||||
|
if ((partialJitStubRequired || !partial || (pipes && pipes.length > 0)) &&
|
||||||
|
jitSummaryOutputCtx.statements.length <= 0) {
|
||||||
|
_createTypeReferenceStub(jitSummaryOutputCtx, Identifiers.ComponentFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: we are creating stub ngfactory/ngsummary for all source files,
|
||||||
|
// as the real calculation requires almost the same logic as producing the real content for
|
||||||
|
// them. Our pipeline will filter out empty ones at the end. Because of this filter, however,
|
||||||
|
// stub references to the reference type needs to be generated even if the user cannot
|
||||||
|
// refer to type from the `.d.ts` file to prevent the file being elided from the emit.
|
||||||
|
generatedFiles.push(this._codegenSourceModule(srcFileUrl, ngFactoryOutputCtx));
|
||||||
|
if (this._enableSummariesForJit) {
|
||||||
|
generatedFiles.push(this._codegenSourceModule(srcFileUrl, jitSummaryOutputCtx));
|
||||||
|
}
|
||||||
|
|
||||||
return generatedFiles;
|
return generatedFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,6 +388,10 @@ export class AotCompiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _createTypeReferenceStub(outputCtx: OutputContext, reference: o.ExternalReference) {
|
||||||
|
outputCtx.statements.push(o.importExpr(reference).toStmt());
|
||||||
|
}
|
||||||
|
|
||||||
function _resolveStyleStatements(
|
function _resolveStyleStatements(
|
||||||
symbolResolver: StaticSymbolResolver, compileResult: CompiledStylesheet, needsShim: boolean,
|
symbolResolver: StaticSymbolResolver, compileResult: CompiledStylesheet, needsShim: boolean,
|
||||||
fileSuffix: string): void {
|
fileSuffix: string): void {
|
||||||
|
@ -100,7 +100,6 @@ function createSummaryForJitFunction(
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ToJsonSerializer extends ValueTransformer {
|
class ToJsonSerializer extends ValueTransformer {
|
||||||
// Note: This only contains symbols without members.
|
// Note: This only contains symbols without members.
|
||||||
symbols: StaticSymbol[] = [];
|
symbols: StaticSymbol[] = [];
|
||||||
|
@ -128,10 +128,6 @@ export function hostViewClassName(compType: any): string {
|
|||||||
return `HostView_${identifierName({reference: compType})}`;
|
return `HostView_${identifierName({reference: compType})}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dirWrapperClassName(dirType: any) {
|
|
||||||
return `Wrapper_${identifierName({reference: dirType})}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function componentFactoryName(compType: any): string {
|
export function componentFactoryName(compType: any): string {
|
||||||
return `${identifierName({reference: compType})}NgFactory`;
|
return `${identifierName({reference: compType})}NgFactory`;
|
||||||
}
|
}
|
||||||
|
@ -69,5 +69,5 @@ export * from './selector';
|
|||||||
export * from './style_compiler';
|
export * from './style_compiler';
|
||||||
export * from './template_parser/template_parser';
|
export * from './template_parser/template_parser';
|
||||||
export {ViewCompiler} from './view_compiler/view_compiler';
|
export {ViewCompiler} from './view_compiler/view_compiler';
|
||||||
export {isSyntaxError, syntaxError} from './util';
|
export {getParseErrors, isSyntaxError, syntaxError} from './util';
|
||||||
// This file only reexports content of the `src` folder. Keep it that way.
|
// This file only reexports content of the `src` folder. Keep it that way.
|
||||||
|
@ -23,8 +23,7 @@ import {NgModuleResolver} from './ng_module_resolver';
|
|||||||
import {PipeResolver} from './pipe_resolver';
|
import {PipeResolver} from './pipe_resolver';
|
||||||
import {ElementSchemaRegistry} from './schema/element_schema_registry';
|
import {ElementSchemaRegistry} from './schema/element_schema_registry';
|
||||||
import {SummaryResolver} from './summary_resolver';
|
import {SummaryResolver} from './summary_resolver';
|
||||||
import {getUrlScheme} from './url_resolver';
|
import {SyncAsync, ValueTransformer, noUndefined, syntaxError, visitValue} from './util';
|
||||||
import {MODULE_SUFFIX, SyncAsync, ValueTransformer, noUndefined, syntaxError, visitValue} from './util';
|
|
||||||
|
|
||||||
export type ErrorCollector = (error: any, type?: any) => void;
|
export type ErrorCollector = (error: any, type?: any) => void;
|
||||||
export const ERROR_COLLECTOR_TOKEN = new InjectionToken('ErrorCollector');
|
export const ERROR_COLLECTOR_TOKEN = new InjectionToken('ErrorCollector');
|
||||||
|
@ -118,12 +118,14 @@ export class ParseError {
|
|||||||
public span: ParseSourceSpan, public msg: string,
|
public span: ParseSourceSpan, public msg: string,
|
||||||
public level: ParseErrorLevel = ParseErrorLevel.ERROR) {}
|
public level: ParseErrorLevel = ParseErrorLevel.ERROR) {}
|
||||||
|
|
||||||
toString(): string {
|
contextualMessage(): string {
|
||||||
const ctx = this.span.start.getContext(100, 3);
|
const ctx = this.span.start.getContext(100, 3);
|
||||||
const contextStr =
|
return ctx ? ` ("${ctx.before}[${ParseErrorLevel[this.level]} ->]${ctx.after}")` : '';
|
||||||
ctx ? ` ("${ctx.before}[${ParseErrorLevel[this.level]} ->]${ctx.after}")` : '';
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
const details = this.span.details ? `, ${this.span.details}` : '';
|
const details = this.span.details ? `, ${this.span.details}` : '';
|
||||||
return `${this.msg}${contextStr}: ${this.span.start}${details}`;
|
return `${this.msg}${this.contextualMessage()}: ${this.span.start}${details}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ export class TemplateParser {
|
|||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
const errorString = errors.join('\n');
|
const errorString = errors.join('\n');
|
||||||
throw syntaxError(`Template parse errors:\n${errorString}`);
|
throw syntaxError(`Template parse errors:\n${errorString}`, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {template: result.templateAst !, pipes: result.usedPipes !};
|
return {template: result.templateAst !, pipes: result.usedPipes !};
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import {ɵisPromise as isPromise} from '@angular/core';
|
import {ɵisPromise as isPromise} from '@angular/core';
|
||||||
|
|
||||||
import * as o from './output/output_ast';
|
import * as o from './output/output_ast';
|
||||||
|
import {ParseError} from './parse_util';
|
||||||
|
|
||||||
export const MODULE_SUFFIX = '';
|
export const MODULE_SUFFIX = '';
|
||||||
|
|
||||||
@ -96,21 +97,26 @@ export const SyncAsync = {
|
|||||||
all: <T>(syncAsyncValues: SyncAsync<T>[]): SyncAsync<T[]> => {
|
all: <T>(syncAsyncValues: SyncAsync<T>[]): SyncAsync<T[]> => {
|
||||||
return syncAsyncValues.some(isPromise) ? Promise.all(syncAsyncValues) : syncAsyncValues as T[];
|
return syncAsyncValues.some(isPromise) ? Promise.all(syncAsyncValues) : syncAsyncValues as T[];
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function syntaxError(msg: string, parseErrors?: ParseError[]): Error {
|
||||||
|
const error = Error(msg);
|
||||||
|
(error as any)[ERROR_SYNTAX_ERROR] = true;
|
||||||
|
if (parseErrors) (error as any)[ERROR_PARSE_ERRORS] = parseErrors;
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function syntaxError(msg: string):
|
|
||||||
Error {
|
|
||||||
const error = Error(msg);
|
|
||||||
(error as any)[ERROR_SYNTAX_ERROR] = true;
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ERROR_SYNTAX_ERROR = 'ngSyntaxError';
|
const ERROR_SYNTAX_ERROR = 'ngSyntaxError';
|
||||||
|
const ERROR_PARSE_ERRORS = 'ngParseErrors';
|
||||||
|
|
||||||
export function isSyntaxError(error: Error): boolean {
|
export function isSyntaxError(error: Error): boolean {
|
||||||
return (error as any)[ERROR_SYNTAX_ERROR];
|
return (error as any)[ERROR_SYNTAX_ERROR];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getParseErrors(error: Error): ParseError[] {
|
||||||
|
return (error as any)[ERROR_PARSE_ERRORS] || [];
|
||||||
|
}
|
||||||
|
|
||||||
export function escapeRegExp(s: string): string {
|
export function escapeRegExp(s: string): string {
|
||||||
return s.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
|
return s.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,9 @@ describe('aot stubs', () => {
|
|||||||
it('should create empty .ngfactory and .ngsummary files for every source file', () => {
|
it('should create empty .ngfactory and .ngsummary files for every source file', () => {
|
||||||
const appDir = {'app.ts': `export const x = 1;`};
|
const appDir = {'app.ts': `export const x = 1;`};
|
||||||
const rootDir = {'app': appDir};
|
const rootDir = {'app': appDir};
|
||||||
const {genFiles} =
|
const {genFiles} = compile(
|
||||||
compile([rootDir, angularFiles], {postCompile: expectNoDiagnostics, stubsOnly: true});
|
[rootDir, angularFiles],
|
||||||
|
{postCompile: expectNoDiagnostics, stubsOnly: true, enableSummariesForJit: true});
|
||||||
expect(genFiles.find((f) => f.genFileUrl === '/app/app.ngfactory.ts')).toBeTruthy();
|
expect(genFiles.find((f) => f.genFileUrl === '/app/app.ngfactory.ts')).toBeTruthy();
|
||||||
expect(genFiles.find((f) => f.genFileUrl === '/app/app.ngsummary.ts')).toBeTruthy();
|
expect(genFiles.find((f) => f.genFileUrl === '/app/app.ngsummary.ts')).toBeTruthy();
|
||||||
});
|
});
|
||||||
@ -64,6 +65,8 @@ describe('aot stubs', () => {
|
|||||||
`
|
`
|
||||||
};
|
};
|
||||||
const rootDir = {'app': appDir};
|
const rootDir = {'app': appDir};
|
||||||
compile([rootDir, angularFiles], {postCompile: expectNoDiagnostics, stubsOnly: true});
|
compile(
|
||||||
|
[rootDir, angularFiles],
|
||||||
|
{postCompile: expectNoDiagnostics, stubsOnly: true, enableSummariesForJit: true});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export {MetadataWriterHost} from './src/compiler_host';
|
export {MetadataWriterHost} from './src/compiler_host';
|
||||||
export {CodegenExtension, UserError, main} from './src/main';
|
export {CodegenExtension, UserError, createBundleIndexHost, main} from './src/main';
|
||||||
|
|
||||||
export {default as AngularCompilerOptions} from './src/options';
|
export {default as AngularCompilerOptions} from './src/options';
|
||||||
export * from './src/bundler';
|
export * from './src/bundler';
|
||||||
|
@ -33,6 +33,38 @@ export interface CodegenExtension {
|
|||||||
host: ts.CompilerHost): Promise<string[]>;
|
host: ts.CompilerHost): Promise<string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createBundleIndexHost(
|
||||||
|
ngOptions: NgOptions, rootFiles: string[],
|
||||||
|
host: ts.CompilerHost): {host: ts.CompilerHost, indexName?: string, errors?: ts.Diagnostic[]} {
|
||||||
|
const files = rootFiles.filter(f => !DTS.test(f));
|
||||||
|
if (files.length != 1) {
|
||||||
|
return {
|
||||||
|
host,
|
||||||
|
errors: [{
|
||||||
|
file: null,
|
||||||
|
start: null,
|
||||||
|
length: null,
|
||||||
|
messageText:
|
||||||
|
'Angular compiler option "flatModuleIndex" requires one and only one .ts file in the "files" field.',
|
||||||
|
category: ts.DiagnosticCategory.Error,
|
||||||
|
code: 0
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const file = files[0];
|
||||||
|
const indexModule = file.replace(/\.ts$/, '');
|
||||||
|
const bundler =
|
||||||
|
new MetadataBundler(indexModule, ngOptions.flatModuleId, new CompilerHostAdapter(host));
|
||||||
|
const metadataBundle = bundler.getMetadataBundle();
|
||||||
|
const metadata = JSON.stringify(metadataBundle.metadata);
|
||||||
|
const name =
|
||||||
|
path.join(path.dirname(indexModule), ngOptions.flatModuleOutFile.replace(JS_EXT, '.ts'));
|
||||||
|
const libraryIndex = `./${path.basename(indexModule)}`;
|
||||||
|
const content = privateEntriesToIndex(libraryIndex, metadataBundle.privates);
|
||||||
|
host = new SyntheticIndexHost(host, {name, content, metadata});
|
||||||
|
return {host, indexName: name};
|
||||||
|
}
|
||||||
|
|
||||||
export function main(
|
export function main(
|
||||||
project: string | VinylFile, cliOptions: CliOptions, codegen?: CodegenExtension,
|
project: string | VinylFile, cliOptions: CliOptions, codegen?: CodegenExtension,
|
||||||
options?: ts.CompilerOptions): Promise<any> {
|
options?: ts.CompilerOptions): Promise<any> {
|
||||||
@ -71,32 +103,11 @@ export function main(
|
|||||||
// If the compilation is a flat module index then produce the flat module index
|
// If the compilation is a flat module index then produce the flat module index
|
||||||
// metadata and the synthetic flat module index.
|
// metadata and the synthetic flat module index.
|
||||||
if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) {
|
if (ngOptions.flatModuleOutFile && !ngOptions.skipMetadataEmit) {
|
||||||
const files = parsed.fileNames.filter(f => !DTS.test(f));
|
const {host: bundleHost, indexName, errors} =
|
||||||
if (files.length != 1) {
|
createBundleIndexHost(ngOptions, rootFileNames, host);
|
||||||
check([{
|
if (errors) check(errors);
|
||||||
file: null,
|
if (indexName) addGeneratedFileName(indexName);
|
||||||
start: null,
|
host = bundleHost;
|
||||||
length: null,
|
|
||||||
messageText:
|
|
||||||
'Angular compiler option "flatModuleIndex" requires one and only one .ts file in the "files" field.',
|
|
||||||
category: ts.DiagnosticCategory.Error,
|
|
||||||
code: 0
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
const file = files[0];
|
|
||||||
const indexModule = file.replace(/\.ts$/, '');
|
|
||||||
const bundler =
|
|
||||||
new MetadataBundler(indexModule, ngOptions.flatModuleId, new CompilerHostAdapter(host));
|
|
||||||
if (diagnostics) console.time('NG flat module index');
|
|
||||||
const metadataBundle = bundler.getMetadataBundle();
|
|
||||||
if (diagnostics) console.timeEnd('NG flat module index');
|
|
||||||
const metadata = JSON.stringify(metadataBundle.metadata);
|
|
||||||
const name =
|
|
||||||
path.join(path.dirname(indexModule), ngOptions.flatModuleOutFile.replace(JS_EXT, '.ts'));
|
|
||||||
const libraryIndex = `./${path.basename(indexModule)}`;
|
|
||||||
const content = privateEntriesToIndex(libraryIndex, metadataBundle.privates);
|
|
||||||
host = new SyntheticIndexHost(host, {name, content, metadata});
|
|
||||||
addGeneratedFileName(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tsickleCompilerHostOptions:
|
const tsickleCompilerHostOptions:
|
||||||
|
Reference in New Issue
Block a user