2017-01-26 12:35:49 -05:00
|
|
|
/**
|
|
|
|
* @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
|
|
|
|
*/
|
|
|
|
|
2017-05-17 14:21:08 -04:00
|
|
|
import {AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler, toTypeScript} from '@angular/compiler';
|
2017-09-13 19:55:42 -04:00
|
|
|
import {MetadataBundlerHost} from '@angular/compiler-cli/src/metadata/bundler';
|
|
|
|
import {MetadataCollector} from '@angular/compiler-cli/src/metadata/collector';
|
|
|
|
import {ModuleMetadata} from '@angular/compiler-cli/src/metadata/index';
|
2017-01-26 12:35:49 -05:00
|
|
|
import * as fs from 'fs';
|
|
|
|
import * as path from 'path';
|
|
|
|
import * as ts from 'typescript';
|
|
|
|
|
2017-08-23 13:22:17 -04:00
|
|
|
export interface MetadataProvider { getMetadata(source: ts.SourceFile): ModuleMetadata|undefined; }
|
|
|
|
|
2017-04-26 12:24:42 -04:00
|
|
|
let nodeModulesPath: string;
|
|
|
|
let angularSourcePath: string;
|
|
|
|
let rootPath: string;
|
|
|
|
|
|
|
|
calcPathsOnDisc();
|
|
|
|
|
|
|
|
export type MockFileOrDirectory = string | MockDirectory;
|
2017-01-26 12:35:49 -05:00
|
|
|
|
|
|
|
export type MockDirectory = {
|
2017-04-26 12:24:42 -04:00
|
|
|
[name: string]: MockFileOrDirectory | undefined;
|
2017-01-26 12:35:49 -05:00
|
|
|
};
|
|
|
|
|
2017-04-26 12:24:42 -04:00
|
|
|
export function isDirectory(data: MockFileOrDirectory | undefined): data is MockDirectory {
|
2017-01-26 12:35:49 -05:00
|
|
|
return typeof data !== 'string';
|
|
|
|
}
|
|
|
|
|
|
|
|
const NODE_MODULES = '/node_modules/';
|
|
|
|
const IS_GENERATED = /\.(ngfactory|ngstyle)$/;
|
|
|
|
const angularts = /@angular\/(\w|\/|-)+\.tsx?$/;
|
|
|
|
const rxjs = /\/rxjs\//;
|
|
|
|
const tsxfile = /\.tsx$/;
|
|
|
|
export const settings: ts.CompilerOptions = {
|
|
|
|
target: ts.ScriptTarget.ES5,
|
|
|
|
declaration: true,
|
|
|
|
module: ts.ModuleKind.CommonJS,
|
|
|
|
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
|
|
emitDecoratorMetadata: true,
|
|
|
|
experimentalDecorators: true,
|
|
|
|
removeComments: false,
|
|
|
|
noImplicitAny: false,
|
|
|
|
skipLibCheck: true,
|
2017-05-11 13:15:54 -04:00
|
|
|
strictNullChecks: true,
|
2017-01-26 12:35:49 -05:00
|
|
|
lib: ['lib.es2015.d.ts', 'lib.dom.d.ts'],
|
|
|
|
types: []
|
|
|
|
};
|
|
|
|
|
2017-03-20 19:31:11 -04:00
|
|
|
export interface EmitterOptions {
|
|
|
|
emitMetadata: boolean;
|
2017-04-26 12:24:42 -04:00
|
|
|
mockData?: MockDirectory;
|
|
|
|
}
|
|
|
|
|
|
|
|
function calcPathsOnDisc() {
|
|
|
|
const moduleFilename = module.filename.replace(/\\/g, '/');
|
|
|
|
const distIndex = moduleFilename.indexOf('/dist/all');
|
|
|
|
if (distIndex >= 0) {
|
|
|
|
rootPath = moduleFilename.substr(0, distIndex);
|
|
|
|
nodeModulesPath = path.join(rootPath, 'node_modules');
|
|
|
|
angularSourcePath = path.join(rootPath, 'packages');
|
|
|
|
}
|
2017-03-20 19:31:11 -04:00
|
|
|
}
|
2017-02-14 16:33:06 -05:00
|
|
|
|
2017-04-26 12:24:42 -04:00
|
|
|
|
2017-01-26 12:35:49 -05:00
|
|
|
export class EmittingCompilerHost implements ts.CompilerHost {
|
2017-02-14 16:33:06 -05:00
|
|
|
private addedFiles = new Map<string, string>();
|
2017-01-26 12:35:49 -05:00
|
|
|
private writtenFiles = new Map<string, string>();
|
|
|
|
private scriptNames: string[];
|
|
|
|
private root = '/';
|
|
|
|
private collector = new MetadataCollector();
|
|
|
|
|
2017-02-14 16:33:06 -05:00
|
|
|
constructor(scriptNames: string[], private options: EmitterOptions) {
|
2017-04-26 12:24:42 -04:00
|
|
|
// Rewrite references to scripts with '@angular' to its corresponding location in
|
|
|
|
// the source tree.
|
|
|
|
this.scriptNames = scriptNames.map(f => this.effectiveName(f));
|
|
|
|
this.root = rootPath;
|
|
|
|
}
|
|
|
|
|
|
|
|
public writtenAngularFiles(target = new Map<string, string>()): Map<string, string> {
|
|
|
|
this.written.forEach((value, key) => {
|
|
|
|
const path = `/node_modules/@angular${key.substring(angularSourcePath.length)}`;
|
|
|
|
target.set(path, value);
|
|
|
|
});
|
|
|
|
return target;
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
|
2017-02-14 16:33:06 -05:00
|
|
|
public addScript(fileName: string, content: string) {
|
|
|
|
const scriptName = this.effectiveName(fileName);
|
|
|
|
this.addedFiles.set(scriptName, content);
|
|
|
|
this.scriptNames.push(scriptName);
|
|
|
|
}
|
|
|
|
|
|
|
|
public override(fileName: string, content: string) {
|
|
|
|
const scriptName = this.effectiveName(fileName);
|
|
|
|
this.addedFiles.set(scriptName, content);
|
|
|
|
}
|
|
|
|
|
|
|
|
public addWrittenFile(fileName: string, content: string) {
|
|
|
|
this.writtenFiles.set(this.effectiveName(fileName), content);
|
|
|
|
}
|
|
|
|
|
2017-01-26 12:35:49 -05:00
|
|
|
public getWrittenFiles(): {name: string, content: string}[] {
|
|
|
|
return Array.from(this.writtenFiles).map(f => ({name: f[0], content: f[1]}));
|
|
|
|
}
|
|
|
|
|
|
|
|
public get scripts(): string[] { return this.scriptNames; }
|
|
|
|
|
|
|
|
public get written(): Map<string, string> { return this.writtenFiles; }
|
|
|
|
|
2017-02-14 16:33:06 -05:00
|
|
|
public effectiveName(fileName: string): string {
|
2017-03-08 18:53:45 -05:00
|
|
|
const prefix = '@angular/';
|
|
|
|
return fileName.startsWith('@angular/') ?
|
2017-04-26 12:24:42 -04:00
|
|
|
path.join(angularSourcePath, fileName.substr(prefix.length)) :
|
2017-03-08 18:53:45 -05:00
|
|
|
fileName;
|
2017-02-14 16:33:06 -05:00
|
|
|
}
|
|
|
|
|
2017-01-26 12:35:49 -05:00
|
|
|
// ts.ModuleResolutionHost
|
2017-02-14 16:33:06 -05:00
|
|
|
fileExists(fileName: string): boolean {
|
2017-03-20 19:31:11 -04:00
|
|
|
return this.addedFiles.has(fileName) || open(fileName, this.options.mockData) != null ||
|
|
|
|
fs.existsSync(fileName);
|
2017-02-14 16:33:06 -05:00
|
|
|
}
|
2017-01-26 12:35:49 -05:00
|
|
|
|
|
|
|
readFile(fileName: string): string {
|
2017-03-20 19:31:11 -04:00
|
|
|
const result = this.addedFiles.get(fileName) || open(fileName, this.options.mockData);
|
2017-02-14 16:33:06 -05:00
|
|
|
if (result) return result;
|
2017-03-20 19:31:11 -04:00
|
|
|
|
2017-01-26 12:35:49 -05:00
|
|
|
let basename = path.basename(fileName);
|
|
|
|
if (/^lib.*\.d\.ts$/.test(basename)) {
|
|
|
|
let libPath = ts.getDefaultLibFilePath(settings);
|
|
|
|
return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
|
|
|
|
}
|
|
|
|
return fs.readFileSync(fileName, 'utf8');
|
|
|
|
}
|
|
|
|
|
|
|
|
directoryExists(directoryName: string): boolean {
|
2017-03-20 19:31:11 -04:00
|
|
|
return directoryExists(directoryName, this.options.mockData) ||
|
|
|
|
(fs.existsSync(directoryName) && fs.statSync(directoryName).isDirectory());
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
getCurrentDirectory(): string { return this.root; }
|
|
|
|
|
|
|
|
getDirectories(dir: string): string[] {
|
2017-03-20 19:31:11 -04:00
|
|
|
const result = open(dir, this.options.mockData);
|
|
|
|
if (result && typeof result !== 'string') {
|
|
|
|
return Object.keys(result);
|
|
|
|
}
|
2017-01-26 12:35:49 -05:00
|
|
|
return fs.readdirSync(dir).filter(p => {
|
|
|
|
const name = path.join(dir, p);
|
|
|
|
const stat = fs.statSync(name);
|
|
|
|
return stat && stat.isDirectory();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// ts.CompilerHost
|
|
|
|
getSourceFile(
|
|
|
|
fileName: string, languageVersion: ts.ScriptTarget,
|
|
|
|
onError?: (message: string) => void): ts.SourceFile {
|
|
|
|
const content = this.readFile(fileName);
|
|
|
|
if (content) {
|
2017-02-14 16:33:06 -05:00
|
|
|
return ts.createSourceFile(fileName, content, languageVersion, /* setParentNodes */ true);
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
2017-03-24 12:59:58 -04:00
|
|
|
throw new Error(`File not found '${fileName}'.`);
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
|
|
|
|
|
|
|
|
writeFile: ts.WriteFileCallback =
|
|
|
|
(fileName: string, data: string, writeByteOrderMark: boolean,
|
2017-12-22 12:36:47 -05:00
|
|
|
onError?: (message: string) => void, sourceFiles?: ReadonlyArray<ts.SourceFile>) => {
|
2017-02-14 16:33:06 -05:00
|
|
|
this.addWrittenFile(fileName, data);
|
|
|
|
if (this.options.emitMetadata && sourceFiles && sourceFiles.length && DTS.test(fileName)) {
|
2017-01-26 12:35:49 -05:00
|
|
|
const metadataFilePath = fileName.replace(DTS, '.metadata.json');
|
|
|
|
const metadata = this.collector.getMetadata(sourceFiles[0]);
|
2017-02-14 16:33:06 -05:00
|
|
|
if (metadata) {
|
|
|
|
this.addWrittenFile(metadataFilePath, JSON.stringify(metadata));
|
|
|
|
}
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getCanonicalFileName(fileName: string): string {
|
|
|
|
return fileName;
|
|
|
|
}
|
|
|
|
useCaseSensitiveFileNames(): boolean { return false; }
|
|
|
|
getNewLine(): string { return '\n'; }
|
|
|
|
}
|
|
|
|
|
|
|
|
export class MockCompilerHost implements ts.CompilerHost {
|
|
|
|
scriptNames: string[];
|
|
|
|
|
2017-03-30 17:51:29 -04:00
|
|
|
public overrides = new Map<string, string>();
|
|
|
|
public writtenFiles = new Map<string, string>();
|
2017-01-26 12:35:49 -05:00
|
|
|
private sourceFiles = new Map<string, ts.SourceFile>();
|
|
|
|
private assumeExists = new Set<string>();
|
|
|
|
private traces: string[] = [];
|
|
|
|
|
2017-04-26 12:24:42 -04:00
|
|
|
constructor(scriptNames: string[], private data: MockDirectory) {
|
2017-01-26 12:35:49 -05:00
|
|
|
this.scriptNames = scriptNames.slice(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test API
|
|
|
|
override(fileName: string, content: string) {
|
|
|
|
if (content) {
|
|
|
|
this.overrides.set(fileName, content);
|
|
|
|
} else {
|
|
|
|
this.overrides.delete(fileName);
|
|
|
|
}
|
|
|
|
this.sourceFiles.delete(fileName);
|
|
|
|
}
|
|
|
|
|
|
|
|
addScript(fileName: string, content: string) {
|
|
|
|
this.overrides.set(fileName, content);
|
|
|
|
this.scriptNames.push(fileName);
|
|
|
|
this.sourceFiles.delete(fileName);
|
|
|
|
}
|
|
|
|
|
|
|
|
assumeFileExists(fileName: string) { this.assumeExists.add(fileName); }
|
|
|
|
|
|
|
|
remove(files: string[]) {
|
|
|
|
// Remove the files from the list of scripts.
|
|
|
|
const fileSet = new Set(files);
|
|
|
|
this.scriptNames = this.scriptNames.filter(f => fileSet.has(f));
|
|
|
|
|
|
|
|
// Remove files from written files
|
|
|
|
files.forEach(f => this.writtenFiles.delete(f));
|
|
|
|
}
|
|
|
|
|
|
|
|
// ts.ModuleResolutionHost
|
|
|
|
fileExists(fileName: string): boolean {
|
|
|
|
if (this.overrides.has(fileName) || this.writtenFiles.has(fileName) ||
|
|
|
|
this.assumeExists.has(fileName)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
const effectiveName = this.getEffectiveName(fileName);
|
|
|
|
if (effectiveName == fileName) {
|
2017-05-23 16:40:50 -04:00
|
|
|
return open(fileName, this.data) != null;
|
|
|
|
}
|
|
|
|
if (fileName.match(rxjs)) {
|
|
|
|
return fs.existsSync(effectiveName);
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
2017-05-23 16:40:50 -04:00
|
|
|
return false;
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
|
2017-03-24 12:59:58 -04:00
|
|
|
readFile(fileName: string): string { return this.getFileContent(fileName) !; }
|
2017-01-26 12:35:49 -05:00
|
|
|
|
|
|
|
trace(s: string): void { this.traces.push(s); }
|
|
|
|
|
|
|
|
getCurrentDirectory(): string { return '/'; }
|
|
|
|
|
|
|
|
getDirectories(dir: string): string[] {
|
|
|
|
const effectiveName = this.getEffectiveName(dir);
|
|
|
|
if (effectiveName === dir) {
|
|
|
|
const data = find(dir, this.data);
|
|
|
|
if (isDirectory(data)) {
|
|
|
|
return Object.keys(data).filter(k => isDirectory(data[k]));
|
|
|
|
}
|
|
|
|
}
|
2017-03-24 12:59:58 -04:00
|
|
|
return [];
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// ts.CompilerHost
|
|
|
|
getSourceFile(
|
|
|
|
fileName: string, languageVersion: ts.ScriptTarget,
|
|
|
|
onError?: (message: string) => void): ts.SourceFile {
|
|
|
|
let result = this.sourceFiles.get(fileName);
|
|
|
|
if (!result) {
|
|
|
|
const content = this.getFileContent(fileName);
|
|
|
|
if (content) {
|
|
|
|
result = ts.createSourceFile(fileName, content, languageVersion);
|
|
|
|
this.sourceFiles.set(fileName, result);
|
|
|
|
}
|
|
|
|
}
|
2017-03-24 12:59:58 -04:00
|
|
|
return result !;
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
getDefaultLibFileName(options: ts.CompilerOptions): string { return 'lib.d.ts'; }
|
|
|
|
|
|
|
|
writeFile: ts.WriteFileCallback =
|
|
|
|
(fileName: string, data: string, writeByteOrderMark: boolean) => {
|
|
|
|
this.writtenFiles.set(fileName, data);
|
|
|
|
this.sourceFiles.delete(fileName);
|
|
|
|
}
|
|
|
|
|
|
|
|
getCanonicalFileName(fileName: string): string {
|
|
|
|
return fileName;
|
|
|
|
}
|
|
|
|
useCaseSensitiveFileNames(): boolean { return false; }
|
|
|
|
getNewLine(): string { return '\n'; }
|
|
|
|
|
|
|
|
// Private methods
|
|
|
|
private getFileContent(fileName: string): string|undefined {
|
|
|
|
if (this.overrides.has(fileName)) {
|
|
|
|
return this.overrides.get(fileName);
|
|
|
|
}
|
|
|
|
if (this.writtenFiles.has(fileName)) {
|
|
|
|
return this.writtenFiles.get(fileName);
|
|
|
|
}
|
|
|
|
let basename = path.basename(fileName);
|
|
|
|
if (/^lib.*\.d\.ts$/.test(basename)) {
|
|
|
|
let libPath = ts.getDefaultLibFilePath(settings);
|
|
|
|
return fs.readFileSync(path.join(path.dirname(libPath), basename), 'utf8');
|
2017-05-23 16:40:50 -04:00
|
|
|
}
|
|
|
|
let effectiveName = this.getEffectiveName(fileName);
|
|
|
|
if (effectiveName === fileName) {
|
|
|
|
return open(fileName, this.data);
|
|
|
|
}
|
|
|
|
if (fileName.match(rxjs) && fs.existsSync(fileName)) {
|
|
|
|
return fs.readFileSync(fileName, 'utf8');
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private getEffectiveName(name: string): string {
|
|
|
|
const node_modules = 'node_modules';
|
|
|
|
const rxjs = '/rxjs';
|
|
|
|
if (name.startsWith('/' + node_modules)) {
|
2017-04-26 12:24:42 -04:00
|
|
|
if (nodeModulesPath && name.startsWith('/' + node_modules + rxjs)) {
|
|
|
|
return path.join(nodeModulesPath, name.substr(node_modules.length + 1));
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
|
|
|
const DTS = /\.d\.ts$/;
|
|
|
|
const GENERATED_FILES = /\.ngfactory\.ts$|\.ngstyle\.ts$/;
|
|
|
|
|
|
|
|
export class MockAotCompilerHost implements AotCompilerHost {
|
|
|
|
private metadataVisible: boolean = true;
|
|
|
|
private dtsAreSource: boolean = true;
|
2017-08-15 20:06:09 -04:00
|
|
|
private resolveModuleNameHost: ts.ModuleResolutionHost;
|
2017-01-26 12:35:49 -05:00
|
|
|
|
2017-08-23 13:22:17 -04:00
|
|
|
constructor(
|
|
|
|
private tsHost: MockCompilerHost,
|
|
|
|
private metadataProvider: MetadataProvider = new MetadataCollector()) {
|
2017-08-15 20:06:09 -04:00
|
|
|
this.resolveModuleNameHost = Object.create(tsHost);
|
|
|
|
this.resolveModuleNameHost.fileExists = (fileName) => {
|
|
|
|
fileName = stripNgResourceSuffix(fileName);
|
|
|
|
return tsHost.fileExists(fileName);
|
|
|
|
};
|
|
|
|
}
|
2017-01-26 12:35:49 -05:00
|
|
|
|
|
|
|
hideMetadata() { this.metadataVisible = false; }
|
|
|
|
|
|
|
|
tsFilesOnly() { this.dtsAreSource = false; }
|
|
|
|
|
|
|
|
// StaticSymbolResolverHost
|
2017-04-14 17:40:56 -04:00
|
|
|
getMetadataFor(modulePath: string): {[key: string]: any}[]|undefined {
|
2017-01-26 12:35:49 -05:00
|
|
|
if (!this.tsHost.fileExists(modulePath)) {
|
2017-04-14 17:40:56 -04:00
|
|
|
return undefined;
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
if (DTS.test(modulePath)) {
|
|
|
|
if (this.metadataVisible) {
|
|
|
|
const metadataPath = modulePath.replace(DTS, '.metadata.json');
|
|
|
|
if (this.tsHost.fileExists(metadataPath)) {
|
|
|
|
let result = JSON.parse(this.tsHost.readFile(metadataPath));
|
|
|
|
return Array.isArray(result) ? result : [result];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const sf = this.tsHost.getSourceFile(modulePath, ts.ScriptTarget.Latest);
|
2017-08-23 13:22:17 -04:00
|
|
|
const metadata = this.metadataProvider.getMetadata(sf);
|
2017-01-26 12:35:49 -05:00
|
|
|
return metadata ? [metadata] : [];
|
|
|
|
}
|
2017-04-14 17:40:56 -04:00
|
|
|
return undefined;
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
moduleNameToFileName(moduleName: string, containingFile: string): string|null {
|
|
|
|
if (!containingFile || !containingFile.length) {
|
|
|
|
if (moduleName.indexOf('.') === 0) {
|
|
|
|
throw new Error('Resolution of relative paths requires a containing file.');
|
|
|
|
}
|
|
|
|
// Any containing file gives the same result for absolute imports
|
|
|
|
containingFile = path.join('/', 'index.ts');
|
|
|
|
}
|
|
|
|
moduleName = moduleName.replace(EXT, '');
|
|
|
|
const resolved = ts.resolveModuleName(
|
|
|
|
moduleName, containingFile.replace(/\\/g, '/'),
|
2017-08-15 20:06:09 -04:00
|
|
|
{baseDir: '/', genDir: '/'}, this.resolveModuleNameHost)
|
2017-01-26 12:35:49 -05:00
|
|
|
.resolvedModule;
|
|
|
|
return resolved ? resolved.resolvedFileName : null;
|
|
|
|
}
|
|
|
|
|
2018-01-10 12:55:03 -05:00
|
|
|
getOutputName(filePath: string) { return filePath; }
|
|
|
|
|
2017-08-15 20:06:09 -04:00
|
|
|
resourceNameToFileName(resourceName: string, containingFile: string) {
|
|
|
|
// Note: we convert package paths into relative paths to be compatible with the the
|
|
|
|
// previous implementation of UrlResolver.
|
|
|
|
if (resourceName && resourceName.charAt(0) !== '.' && !path.isAbsolute(resourceName)) {
|
|
|
|
resourceName = `./${resourceName}`;
|
|
|
|
}
|
|
|
|
const filePathWithNgResource =
|
|
|
|
this.moduleNameToFileName(addNgResourceSuffix(resourceName), containingFile);
|
|
|
|
return filePathWithNgResource ? stripNgResourceSuffix(filePathWithNgResource) : null;
|
|
|
|
}
|
|
|
|
|
2017-01-26 12:35:49 -05:00
|
|
|
// AotSummaryResolverHost
|
|
|
|
loadSummary(filePath: string): string|null { return this.tsHost.readFile(filePath); }
|
|
|
|
|
|
|
|
isSourceFile(sourceFilePath: string): boolean {
|
|
|
|
return !GENERATED_FILES.test(sourceFilePath) &&
|
|
|
|
(this.dtsAreSource || !DTS.test(sourceFilePath));
|
|
|
|
}
|
|
|
|
|
2017-08-15 17:41:48 -04:00
|
|
|
toSummaryFileName(filePath: string): string { return filePath.replace(EXT, '') + '.d.ts'; }
|
|
|
|
|
|
|
|
fromSummaryFileName(filePath: string): string { return filePath; }
|
2017-01-26 12:35:49 -05:00
|
|
|
|
|
|
|
// AotCompilerHost
|
2017-09-21 21:05:07 -04:00
|
|
|
fileNameToModuleName(importedFile: string, containingFile: string): string {
|
2017-01-26 12:35:49 -05:00
|
|
|
return importedFile.replace(EXT, '');
|
|
|
|
}
|
|
|
|
|
2017-05-17 18:39:08 -04:00
|
|
|
loadResource(path: string): string {
|
2017-03-20 19:31:11 -04:00
|
|
|
if (this.tsHost.fileExists(path)) {
|
2017-05-17 18:39:08 -04:00
|
|
|
return this.tsHost.readFile(path);
|
2017-03-20 19:31:11 -04:00
|
|
|
} else {
|
2017-05-17 18:39:08 -04:00
|
|
|
throw new Error(`Resource ${path} not found.`);
|
2017-03-20 19:31:11 -04:00
|
|
|
}
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-15 16:30:40 -05:00
|
|
|
export class MockMetadataBundlerHost implements MetadataBundlerHost {
|
|
|
|
private collector = new MetadataCollector();
|
|
|
|
|
|
|
|
constructor(private host: ts.CompilerHost) {}
|
|
|
|
|
2017-07-18 15:52:48 -04:00
|
|
|
getMetadataFor(moduleName: string): ModuleMetadata|undefined {
|
2017-02-15 16:30:40 -05:00
|
|
|
const source = this.host.getSourceFile(moduleName + '.ts', ts.ScriptTarget.Latest);
|
2017-12-22 12:36:47 -05:00
|
|
|
return source && this.collector.getMetadata(source);
|
2017-02-15 16:30:40 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-26 12:24:42 -04:00
|
|
|
function find(fileName: string, data: MockFileOrDirectory | undefined): MockFileOrDirectory|
|
|
|
|
undefined {
|
2017-03-20 19:31:11 -04:00
|
|
|
if (!data) return undefined;
|
2017-05-23 16:40:50 -04:00
|
|
|
const names = fileName.split('/');
|
2017-01-26 12:35:49 -05:00
|
|
|
if (names.length && !names[0].length) names.shift();
|
2017-04-26 12:24:42 -04:00
|
|
|
let current: MockFileOrDirectory|undefined = data;
|
2017-05-23 16:40:50 -04:00
|
|
|
for (const name of names) {
|
|
|
|
if (typeof current !== 'object') {
|
2017-01-26 12:35:49 -05:00
|
|
|
return undefined;
|
2017-05-23 16:40:50 -04:00
|
|
|
}
|
|
|
|
current = current[name];
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
|
|
|
return current;
|
|
|
|
}
|
|
|
|
|
2017-04-26 12:24:42 -04:00
|
|
|
function open(fileName: string, data: MockFileOrDirectory | undefined): string|undefined {
|
2017-01-26 12:35:49 -05:00
|
|
|
let result = find(fileName, data);
|
|
|
|
if (typeof result === 'string') {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
2017-04-26 12:24:42 -04:00
|
|
|
function directoryExists(dirname: string, data: MockFileOrDirectory | undefined): boolean {
|
2017-01-26 12:35:49 -05:00
|
|
|
let result = find(dirname, data);
|
2017-03-24 12:59:58 -04:00
|
|
|
return !!result && typeof result !== 'string';
|
2017-01-26 12:35:49 -05:00
|
|
|
}
|
2017-04-26 12:24:42 -04:00
|
|
|
|
|
|
|
export type MockFileArray = {
|
|
|
|
fileName: string,
|
|
|
|
content: string
|
|
|
|
}[];
|
|
|
|
|
|
|
|
export type MockData = MockDirectory | Map<string, string>| (MockDirectory | Map<string, string>)[];
|
|
|
|
|
|
|
|
export function toMockFileArray(data: MockData, target: MockFileArray = []): MockFileArray {
|
|
|
|
if (data instanceof Map) {
|
|
|
|
mapToMockFileArray(data, target);
|
|
|
|
} else if (Array.isArray(data)) {
|
|
|
|
data.forEach(entry => toMockFileArray(entry, target));
|
|
|
|
} else {
|
|
|
|
mockDirToFileArray(data, '', target);
|
|
|
|
}
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
|
|
|
function mockDirToFileArray(dir: MockDirectory, path: string, target: MockFileArray) {
|
|
|
|
Object.keys(dir).forEach((localFileName) => {
|
|
|
|
const value = dir[localFileName] !;
|
|
|
|
const fileName = `${path}/${localFileName}`;
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
target.push({fileName, content: value});
|
|
|
|
} else {
|
|
|
|
mockDirToFileArray(value, fileName, target);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function mapToMockFileArray(files: Map<string, string>, target: MockFileArray) {
|
|
|
|
files.forEach((content, fileName) => { target.push({fileName, content}); });
|
|
|
|
}
|
|
|
|
|
|
|
|
export function arrayToMockMap(arr: MockFileArray): Map<string, string> {
|
|
|
|
const map = new Map<string, string>();
|
|
|
|
arr.forEach(({fileName, content}) => { map.set(fileName, content); });
|
|
|
|
return map;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function arrayToMockDir(arr: MockFileArray): MockDirectory {
|
|
|
|
const rootDir: MockDirectory = {};
|
|
|
|
arr.forEach(({fileName, content}) => {
|
|
|
|
let pathParts = fileName.split('/');
|
|
|
|
// trim trailing slash
|
|
|
|
let startIndex = pathParts[0] ? 0 : 1;
|
|
|
|
// get/create the directory
|
|
|
|
let currentDir = rootDir;
|
|
|
|
for (let i = startIndex; i < pathParts.length - 1; i++) {
|
|
|
|
const pathPart = pathParts[i];
|
|
|
|
let localDir = <MockDirectory>currentDir[pathPart];
|
|
|
|
if (!localDir) {
|
|
|
|
currentDir[pathPart] = localDir = {};
|
|
|
|
}
|
|
|
|
currentDir = localDir;
|
|
|
|
}
|
|
|
|
// write the file
|
|
|
|
currentDir[pathParts[pathParts.length - 1]] = content;
|
|
|
|
});
|
|
|
|
return rootDir;
|
|
|
|
}
|
|
|
|
|
|
|
|
const minCoreIndex = `
|
|
|
|
export * from './src/application_module';
|
|
|
|
export * from './src/change_detection';
|
|
|
|
export * from './src/metadata';
|
|
|
|
export * from './src/di/metadata';
|
feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.
Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".
Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.
Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.
Additionally, this commit adds several unit and integration tests of various
flavors to test this change.
PR Close #22005
2018-02-02 13:33:48 -05:00
|
|
|
export * from './src/di/injectable';
|
2017-04-26 12:24:42 -04:00
|
|
|
export * from './src/di/injector';
|
|
|
|
export * from './src/di/injection_token';
|
|
|
|
export * from './src/linker';
|
|
|
|
export * from './src/render';
|
|
|
|
export * from './src/codegen_private_exports';
|
|
|
|
`;
|
|
|
|
|
2017-11-20 13:21:17 -05:00
|
|
|
export function setup(
|
|
|
|
options: {compileAngular: boolean, compileAnimations: boolean, compileCommon?: boolean} = {
|
|
|
|
compileAngular: true,
|
|
|
|
compileAnimations: true,
|
|
|
|
compileCommon: false,
|
|
|
|
}) {
|
2017-04-26 12:24:42 -04:00
|
|
|
let angularFiles = new Map<string, string>();
|
|
|
|
|
|
|
|
beforeAll(() => {
|
|
|
|
if (options.compileAngular) {
|
|
|
|
const emittingHost = new EmittingCompilerHost([], {emitMetadata: true});
|
|
|
|
emittingHost.addScript('@angular/core/index.ts', minCoreIndex);
|
|
|
|
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
|
2017-11-20 13:21:17 -05:00
|
|
|
emittingProgram.emit();
|
|
|
|
emittingHost.writtenAngularFiles(angularFiles);
|
|
|
|
}
|
|
|
|
if (options.compileCommon) {
|
|
|
|
const emittingHost =
|
|
|
|
new EmittingCompilerHost(['@angular/common/index.ts'], {emitMetadata: true});
|
|
|
|
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
|
2017-04-26 12:24:42 -04:00
|
|
|
emittingProgram.emit();
|
|
|
|
emittingHost.writtenAngularFiles(angularFiles);
|
|
|
|
}
|
2017-07-13 19:16:56 -04:00
|
|
|
if (options.compileAnimations) {
|
|
|
|
const emittingHost =
|
|
|
|
new EmittingCompilerHost(['@angular/animations/index.ts'], {emitMetadata: true});
|
|
|
|
const emittingProgram = ts.createProgram(emittingHost.scripts, settings, emittingHost);
|
|
|
|
emittingProgram.emit();
|
|
|
|
emittingHost.writtenAngularFiles(angularFiles);
|
|
|
|
}
|
2017-04-26 12:24:42 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
return angularFiles;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function expectNoDiagnostics(program: ts.Program) {
|
|
|
|
function fileInfo(diagnostic: ts.Diagnostic): string {
|
|
|
|
if (diagnostic.file) {
|
|
|
|
return `${diagnostic.file.fileName}(${diagnostic.start}): `;
|
|
|
|
}
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
function chars(len: number, ch: string): string { return new Array(len).fill(ch).join(''); }
|
|
|
|
|
|
|
|
function lineNoOf(offset: number, text: string): number {
|
|
|
|
let result = 1;
|
|
|
|
for (let i = 0; i < offset; i++) {
|
|
|
|
if (text[i] == '\n') result++;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function lineInfo(diagnostic: ts.Diagnostic): string {
|
|
|
|
if (diagnostic.file) {
|
2017-08-02 19:26:42 -04:00
|
|
|
const start = diagnostic.start !;
|
|
|
|
let end = diagnostic.start ! + diagnostic.length !;
|
2017-04-26 12:24:42 -04:00
|
|
|
const source = diagnostic.file.text;
|
|
|
|
let lineStart = start;
|
|
|
|
let lineEnd = end;
|
|
|
|
while (lineStart > 0 && source[lineStart] != '\n') lineStart--;
|
|
|
|
if (lineStart < start) lineStart++;
|
|
|
|
while (lineEnd < source.length && source[lineEnd] != '\n') lineEnd++;
|
|
|
|
let line = source.substring(lineStart, lineEnd);
|
|
|
|
const lineIndex = line.indexOf('/n');
|
|
|
|
if (lineIndex > 0) {
|
|
|
|
line = line.substr(0, lineIndex);
|
|
|
|
end = start + lineIndex;
|
|
|
|
}
|
|
|
|
const lineNo = lineNoOf(start, source) + ': ';
|
|
|
|
return '\n' + lineNo + line + '\n' + chars(start - lineStart + lineNo.length, ' ') +
|
|
|
|
chars(end - start, '^');
|
|
|
|
}
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2017-12-22 12:36:47 -05:00
|
|
|
function expectNoDiagnostics(diagnostics: ReadonlyArray<ts.Diagnostic>) {
|
2017-04-26 12:24:42 -04:00
|
|
|
if (diagnostics && diagnostics.length) {
|
|
|
|
throw new Error(
|
|
|
|
'Errors from TypeScript:\n' +
|
2018-01-25 11:52:10 -05:00
|
|
|
diagnostics
|
|
|
|
.map(
|
|
|
|
d =>
|
|
|
|
`${fileInfo(d)}${ts.flattenDiagnosticMessageText(d.messageText, '\n')}${lineInfo(d)}`)
|
|
|
|
.join(' \n'));
|
2017-04-26 12:24:42 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
expectNoDiagnostics(program.getOptionsDiagnostics());
|
|
|
|
expectNoDiagnostics(program.getSyntacticDiagnostics());
|
|
|
|
expectNoDiagnostics(program.getSemanticDiagnostics());
|
|
|
|
}
|
|
|
|
|
2017-06-01 13:13:50 -04:00
|
|
|
export function isSource(fileName: string): boolean {
|
2017-08-16 12:00:03 -04:00
|
|
|
return !isDts(fileName) && /\.ts$/.test(fileName);
|
|
|
|
}
|
|
|
|
|
|
|
|
function isDts(fileName: string): boolean {
|
|
|
|
return /\.d.ts$/.test(fileName);
|
|
|
|
}
|
|
|
|
|
|
|
|
function isSourceOrDts(fileName: string): boolean {
|
|
|
|
return /\.ts$/.test(fileName);
|
2017-04-26 12:24:42 -04:00
|
|
|
}
|
|
|
|
|
2017-05-11 13:26:02 -04:00
|
|
|
export function compile(
|
|
|
|
rootDirs: MockData, options: {
|
|
|
|
emit?: boolean,
|
|
|
|
useSummaries?: boolean,
|
|
|
|
preCompile?: (program: ts.Program) => void,
|
|
|
|
postCompile?: (program: ts.Program) => void,
|
|
|
|
}& AotCompilerOptions = {},
|
2017-05-17 18:39:08 -04:00
|
|
|
tsOptions: ts.CompilerOptions = {}): {genFiles: GeneratedFile[], outDir: MockDirectory} {
|
|
|
|
// when using summaries, always emit so the next step can use the results.
|
|
|
|
const emit = options.emit || options.useSummaries;
|
2017-05-23 16:40:50 -04:00
|
|
|
const preCompile = options.preCompile || (() => {});
|
2017-05-17 18:39:08 -04:00
|
|
|
const postCompile = options.postCompile || expectNoDiagnostics;
|
|
|
|
const rootDirArr = toMockFileArray(rootDirs);
|
2017-08-16 12:00:03 -04:00
|
|
|
const scriptNames = rootDirArr.map(entry => entry.fileName)
|
|
|
|
.filter(options.useSummaries ? isSource : isSourceOrDts);
|
2017-05-17 18:39:08 -04:00
|
|
|
|
|
|
|
const host = new MockCompilerHost(scriptNames, arrayToMockDir(rootDirArr));
|
|
|
|
const aotHost = new MockAotCompilerHost(host);
|
|
|
|
if (options.useSummaries) {
|
|
|
|
aotHost.hideMetadata();
|
|
|
|
aotHost.tsFilesOnly();
|
|
|
|
}
|
|
|
|
const tsSettings = {...settings, ...tsOptions};
|
|
|
|
const program = ts.createProgram(host.scriptNames.slice(0), tsSettings, host);
|
2017-05-23 16:40:50 -04:00
|
|
|
preCompile(program);
|
2017-10-20 12:46:41 -04:00
|
|
|
const {compiler, reflector} = createAotCompiler(aotHost, options, (err) => { throw err; });
|
2017-05-23 16:40:50 -04:00
|
|
|
const analyzedModules =
|
|
|
|
compiler.analyzeModulesSync(program.getSourceFiles().map(sf => sf.fileName));
|
2017-09-12 12:40:28 -04:00
|
|
|
const genFiles = compiler.emitAllImpls(analyzedModules);
|
2017-05-17 18:39:08 -04:00
|
|
|
genFiles.forEach((file) => {
|
|
|
|
const source = file.source || toTypeScript(file);
|
2017-10-12 19:09:49 -04:00
|
|
|
if (isSource(file.genFileUrl)) {
|
|
|
|
host.addScript(file.genFileUrl, source);
|
2017-05-17 18:39:08 -04:00
|
|
|
} else {
|
2017-10-12 19:09:49 -04:00
|
|
|
host.override(file.genFileUrl, source);
|
2017-04-26 12:24:42 -04:00
|
|
|
}
|
|
|
|
});
|
2017-05-17 18:39:08 -04:00
|
|
|
const newProgram = ts.createProgram(host.scriptNames.slice(0), tsSettings, host);
|
2017-05-23 16:40:50 -04:00
|
|
|
postCompile(newProgram);
|
2017-05-17 18:39:08 -04:00
|
|
|
if (emit) {
|
|
|
|
newProgram.emit();
|
|
|
|
}
|
|
|
|
let outDir: MockDirectory = {};
|
|
|
|
if (emit) {
|
2017-10-12 19:09:49 -04:00
|
|
|
const dtsFilesWithGenFiles = new Set<string>(genFiles.map(gf => gf.srcFileUrl).filter(isDts));
|
2017-08-16 12:00:03 -04:00
|
|
|
outDir =
|
|
|
|
arrayToMockDir(toMockFileArray([host.writtenFiles, host.overrides])
|
|
|
|
.filter((entry) => !isSource(entry.fileName))
|
|
|
|
.concat(rootDirArr.filter(e => dtsFilesWithGenFiles.has(e.fileName))));
|
2017-05-17 18:39:08 -04:00
|
|
|
}
|
|
|
|
return {genFiles, outDir};
|
2017-04-26 12:24:42 -04:00
|
|
|
}
|
2017-08-15 20:06:09 -04:00
|
|
|
|
|
|
|
function stripNgResourceSuffix(fileName: string): string {
|
|
|
|
return fileName.replace(/\.\$ngresource\$.*/, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
function addNgResourceSuffix(fileName: string): string {
|
|
|
|
return `${fileName}.$ngresource$`;
|
|
|
|
}
|