perf(compiler): make the creation of `ts.Program` faster. (#19275)

We now create 2 programs with exactly the same fileNames and
exactly the same `import` / `export` declarations,
allowing TS to reuse the structure of first program
completely. When passing in an oldProgram and the files didn’t change,
TS can also reuse the old program completely.

This is possible buy adding generated files to TS
in `host.geSourceFile` via `ts.SourceFile.referencedFiles`.

This commit also:
- has a minor side effect on how we generate shared stylesheets:
  - previously every import in a stylesheet would generate a new
    `.ngstyles.ts` file.
  - now, we only generate 1 `.ngstyles.ts` file per entry in `@Component.styleUrls`.
  This was required as we need to be able to determine the program files
  without loading the resources (which can be async).
- makes all angular related methods in `CompilerHost`
  optional, allowing to just use a regular `ts.CompilerHost` as `CompilerHost`.
- simplifies the logic around `Compiler.analyzeNgModules` by introducing `NgAnalyzedFile`.

Perf impact: 1.5s improvement in compiling angular io
PR Close #19275
This commit is contained in:
Tobias Bosch 2017-09-12 09:40:28 -07:00 committed by Igor Minar
parent 9d2236a4b5
commit edd5f5a333
35 changed files with 1929 additions and 1536 deletions

View File

@ -38,8 +38,7 @@ else
sed -i -E 's/ng build/ng build --prod --build-optimizer/g' package.json
sed -i -E 's/ng test/ng test --single-run/g' package.json
# workaround for https://github.com/angular/angular-cli/issues/7401
sed -i -E 's/"@angular\/cli\"\: \".*\"/"@angular\/cli": "https:\/\/github.com\/angular\/cli-builds"/g' package.json
sed -i -E 's/"typescript\"\: \".*\"/"typescript": "2.4.2"/g' package.json
yarn add \
file:../../dist/packages-dist/compiler-cli \

View File

@ -22,8 +22,6 @@ const GENERATED_FILES = /\.ngfactory\.ts$|\.ngstyle\.ts$|\.ngsummary\.ts$/;
const GENERATED_OR_DTS_FILES = /\.d\.ts$|\.ngfactory\.ts$|\.ngstyle\.ts$|\.ngsummary\.ts$/;
const SHALLOW_IMPORT = /^((\w|-)+|(@(\w|-)+(\/(\w|-)+)+))$/;
export interface MetadataProvider { getMetadata(source: ts.SourceFile): ModuleMetadata|undefined; }
export interface BaseAotCompilerHostContext extends ts.ModuleResolutionHost {
readResource?(fileName: string): Promise<string>|string;
}
@ -35,9 +33,7 @@ export abstract class BaseAotCompilerHost<C extends BaseAotCompilerHostContext>
private flatModuleIndexNames = new Set<string>();
private flatModuleIndexRedirectNames = new Set<string>();
constructor(
protected program: ts.Program, protected options: CompilerOptions, protected context: C,
protected metadataProvider: MetadataProvider = new MetadataCollector()) {}
constructor(protected options: CompilerOptions, protected context: C) {}
abstract moduleNameToFileName(m: string, containingFile: string): string|null;
@ -49,17 +45,7 @@ export abstract class BaseAotCompilerHost<C extends BaseAotCompilerHostContext>
abstract fromSummaryFileName(fileName: string, referringLibFileName: string): string;
protected getSourceFile(filePath: string): ts.SourceFile {
const sf = this.program.getSourceFile(filePath);
if (!sf) {
if (this.context.fileExists(filePath)) {
const sourceText = this.context.readFile(filePath);
return ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
}
throw new Error(`Source file ${filePath} not present in program.`);
}
return sf;
}
abstract getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined;
getMetadataFor(filePath: string): ModuleMetadata[]|undefined {
if (!this.context.fileExists(filePath)) {
@ -82,8 +68,7 @@ export abstract class BaseAotCompilerHost<C extends BaseAotCompilerHostContext>
}
}
const sf = this.getSourceFile(filePath);
const metadata = this.metadataProvider.getMetadata(sf);
const metadata = this.getMetadataForSourceFile(filePath);
return metadata ? [metadata] : [];
}
@ -122,7 +107,7 @@ export abstract class BaseAotCompilerHost<C extends BaseAotCompilerHostContext>
v3Metadata.metadata[prop] = v1Metadata.metadata[prop];
}
const exports = this.metadataProvider.getMetadata(this.getSourceFile(dtsFilePath));
const exports = this.getMetadataForSourceFile(dtsFilePath);
if (exports) {
for (let prop in exports.metadata) {
if (!v3Metadata.metadata[prop]) {
@ -233,6 +218,7 @@ export interface CompilerHostContext extends ts.ModuleResolutionHost {
// TODO(tbosch): remove this once G3 uses the transformer compiler!
export class CompilerHost extends BaseAotCompilerHost<CompilerHostContext> {
protected metadataProvider: MetadataCollector;
protected basePath: string;
private moduleFileNames = new Map<string, string|null>();
private isGenDirChildOfRootDir: boolean;
@ -241,10 +227,10 @@ export class CompilerHost extends BaseAotCompilerHost<CompilerHostContext> {
private urlResolver: UrlResolver;
constructor(
program: ts.Program, options: CompilerOptions, context: CompilerHostContext,
collectorOptions?: CollectorOptions,
metadataProvider: MetadataProvider = new MetadataCollector(collectorOptions)) {
super(program, options, context, metadataProvider);
protected program: ts.Program, options: CompilerOptions, context: CompilerHostContext,
collectorOptions?: CollectorOptions) {
super(options, context);
this.metadataProvider = new MetadataCollector(collectorOptions);
// normalize the path so that it never ends with '/'.
this.basePath = path.normalize(path.join(this.options.basePath !, '.')).replace(/\\/g, '/');
this.genDir = path.normalize(path.join(this.options.genDir !, '.')).replace(/\\/g, '/');
@ -274,6 +260,19 @@ export class CompilerHost extends BaseAotCompilerHost<CompilerHostContext> {
this.urlResolver = createOfflineCompileUrlResolver();
}
getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined {
let sf = this.program.getSourceFile(filePath);
if (!sf) {
if (this.context.fileExists(filePath)) {
const sourceText = this.context.readFile(filePath);
sf = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
} else {
throw new Error(`Source file ${filePath} not present in program.`);
}
}
return this.metadataProvider.getMetadata(sf);
}
toSummaryFileName(fileName: string, referringSrcFileName: string): string {
return fileName.replace(EXT, '') + '.d.ts';
}

View File

@ -13,7 +13,7 @@ import {DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from '../transformers/api';
import {GENERATED_FILES} from '../transformers/util';
export interface TypeCheckHost {
ngSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null;
parseSourceSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null;
}
export function translateDiagnostics(host: TypeCheckHost, untranslatedDiagnostics: ts.Diagnostic[]):
@ -49,7 +49,7 @@ export function translateDiagnostics(host: TypeCheckHost, untranslatedDiagnostic
function sourceSpanOf(host: TypeCheckHost, source: ts.SourceFile, start: number): ParseSourceSpan|
null {
const {line, character} = ts.getLineAndCharacterOfPosition(source, start);
return host.ngSpanOf(source.fileName, line, character);
return host.parseSourceSpanOf(source.fileName, line, character);
}
function diagnosticMessageToString(message: ts.DiagnosticMessageChain | string): string {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotSummaryResolver, CompileMetadataResolver, CompilePipeSummary, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, extractProgramSymbols} from '@angular/compiler';
import {AotSummaryResolver, CompileMetadataResolver, CompilePipeSummary, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';

View File

@ -14,7 +14,7 @@ Angular modules and Typescript as this will indirectly add a dependency
to the language service.
*/
export {CompilerHost, CompilerHostContext, MetadataProvider, ModuleResolutionHostAdapter, NodeCompilerHostContext} from './compiler_host';
export {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter, NodeCompilerHostContext} from './compiler_host';
export {DiagnosticTemplateInfo, ExpressionDiagnostic, getExpressionDiagnostics, getExpressionScope, getTemplateExpressionDiagnostics} from './diagnostics/expression_diagnostics';
export {AstType, DiagnosticKind, ExpressionDiagnosticsContext, TypeDiagnostic} from './diagnostics/expression_type';
export {BuiltinType, DeclarationKind, Definition, Location, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './diagnostics/symbols';

View File

@ -55,11 +55,11 @@ export interface CompilerOptions extends ts.CompilerOptions {
}
export interface CompilerHost extends ts.CompilerHost {
moduleNameToFileName(moduleName: string, containingFile?: string): string|null;
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null;
resourceNameToFileName(resourceName: string, containingFilePath: string): string|null;
toSummaryFileName(fileName: string, referringSrcFileName: string): string;
fromSummaryFileName(fileName: string, referringLibFileName: string): string;
moduleNameToFileName?(moduleName: string, containingFile?: string): string|null;
fileNameToModuleName?(importedFilePath: string, containingFilePath: string): string;
resourceNameToFileName?(resourceName: string, containingFilePath: string): string|null;
toSummaryFileName?(fileName: string, referringSrcFileName: string): string;
fromSummaryFileName?(fileName: string, referringLibFileName: string): string;
readResource?(fileName: string): Promise<string>|string;
}

View File

@ -131,9 +131,7 @@ export class PathMappedCompilerHost extends CompilerHost {
return this.readMetadata(metadataPath, rootedPath);
}
} else {
const sf = this.getSourceFile(rootedPath);
sf.fileName = sf.fileName;
const metadata = this.metadataProvider.getMetadata(sf);
const metadata = this.getMetadataForSourceFile(rootedPath);
return metadata ? [metadata] : [];
}
}

View File

@ -141,12 +141,6 @@ export function performCompilation({rootNames, options, host, oldProgram, emitCa
customTransformers?: api.CustomTransformers,
emitFlags?: api.EmitFlags
}): PerformCompilationResult {
const [major, minor] = ts.version.split('.');
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
}
let program: api.Program|undefined;
let emitResult: ts.EmitResult|undefined;
let allDiagnostics: Diagnostics = [];

View File

@ -53,8 +53,8 @@ export interface PerformWatchHost {
export function createPerformWatchHost(
configFileName: string, reportDiagnostics: (diagnostics: Diagnostics) => void,
existingOptions?: ts.CompilerOptions,
createEmitCallback?: (options: api.CompilerOptions) => api.TsEmitCallback): PerformWatchHost {
existingOptions?: ts.CompilerOptions, createEmitCallback?: (options: api.CompilerOptions) =>
api.TsEmitCallback | undefined): PerformWatchHost {
return {
reportDiagnostics: reportDiagnostics,
createCompilerHost: options => createCompilerHost({options}),

View File

@ -135,17 +135,17 @@ export interface CompilerHost extends ts.CompilerHost {
* 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;
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`.
*/
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null;
fileNameToModuleName?(importedFilePath: string, containingFilePath: string): string;
/**
* Converts a file path for a resource that is used in a source file or another resource
* into a filepath.
*/
resourceNameToFileName(resourceName: string, containingFilePath: string): string|null;
resourceNameToFileName?(resourceName: string, containingFilePath: string): string|null;
/**
* Converts a file name into a representation that should be stored in a summary file.
* This has to include changing the suffix as well.
@ -154,12 +154,12 @@ export interface CompilerHost extends ts.CompilerHost {
*
* @param referringSrcFileName the soure file that refers to fileName
*/
toSummaryFileName(fileName: string, referringSrcFileName: string): string;
toSummaryFileName?(fileName: string, referringSrcFileName: string): string;
/**
* Converts a fileName that was processed by `toSummaryFileName` back into a real fileName
* given the fileName of the library that is referrig to it.
*/
fromSummaryFileName(fileName: string, referringLibFileName: string): string;
fromSummaryFileName?(fileName: string, referringLibFileName: string): string;
/**
* 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
@ -267,7 +267,7 @@ export interface Program {
*
* Angular structural information is required to emit files.
*/
emit({emitFlags, cancellationToken, customTransformers, emitCallback}: {
emit({emitFlags, cancellationToken, customTransformers, emitCallback}?: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,

View File

@ -6,11 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/
import {syntaxError} from '@angular/compiler';
import {EmitterVisitorContext, ExternalReference, GeneratedFile, ParseSourceSpan, TypeScriptEmitter, collectExternalReferences, syntaxError} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import {BaseAotCompilerHost} from '../compiler_host';
import {TypeCheckHost} from '../diagnostics/translate_diagnostics';
import {ModuleMetadata} from '../metadata/index';
import {CompilerHost, CompilerOptions} from './api';
import {GENERATED_FILES} from './util';
const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-)+|(@(\w|-)+\/(\w|-)+))/;
const DTS = /\.d\.ts$/;
@ -19,58 +24,122 @@ const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
export function createCompilerHost(
{options, tsHost = ts.createCompilerHost(options, true)}:
{options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost {
const mixin = new CompilerHostMixin(tsHost, options);
const host = Object.create(tsHost);
host.moduleNameToFileName = mixin.moduleNameToFileName.bind(mixin);
host.fileNameToModuleName = mixin.fileNameToModuleName.bind(mixin);
host.toSummaryFileName = mixin.toSummaryFileName.bind(mixin);
host.fromSummaryFileName = mixin.fromSummaryFileName.bind(mixin);
host.resourceNameToFileName = mixin.resourceNameToFileName.bind(mixin);
// 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;
return tsHost;
}
class CompilerHostMixin {
private rootDirs: string[];
private basePath: string;
private moduleResolutionHost: ModuleFilenameResolutionHost;
private moduleResolutionCache: ts.ModuleResolutionCache;
export interface MetadataProvider {
getMetadata(sourceFile: ts.SourceFile): ModuleMetadata|undefined;
}
constructor(private context: ts.CompilerHost, private options: CompilerOptions) {
// normalize the path so that it never ends with '/'.
this.basePath = normalizePath(this.options.basePath !);
this.rootDirs = (this.options.rootDirs || [
this.options.basePath !
]).map(p => path.resolve(this.basePath, normalizePath(p)));
this.moduleResolutionHost = createModuleFilenameResolverHost(context);
interface GenSourceFile {
externalReferences: Set<string>;
sourceFile: ts.SourceFile;
emitCtx: EmitterVisitorContext;
}
/**
* Implements the following hosts based on an api.CompilerHost:
* - ts.CompilerHost to be consumed by a ts.Program
* - AotCompilerHost for @angular/compiler
* - TypeCheckHost for mapping ts errors to ng errors (via translateDiagnostics)
*/
export class TsCompilerAotCompilerTypeCheckHostAdapter extends
BaseAotCompilerHost<CompilerHost> implements ts.CompilerHost,
TypeCheckHost {
private rootDirs: string[];
private moduleResolutionCache: ts.ModuleResolutionCache;
private originalSourceFiles = new Map<string, ts.SourceFile>();
private generatedSourceFiles = new Map<string, GenSourceFile>();
private generatedCodeFor = new Set<string>();
private emitter = new TypeScriptEmitter();
getCancellationToken: () => ts.CancellationToken;
getDefaultLibLocation: () => string;
trace: (s: string) => void;
getDirectories: (path: string) => string[];
directoryExists?: (directoryName: string) => boolean;
constructor(
private rootFiles: string[], options: CompilerOptions, context: CompilerHost,
private metadataProvider: MetadataProvider,
private codeGenerator: (fileName: string) => GeneratedFile[]) {
super(options, context);
this.moduleResolutionCache = ts.createModuleResolutionCache(
this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context));
const basePath = this.options.basePath !;
this.rootDirs =
(this.options.rootDirs || [this.options.basePath !]).map(p => path.resolve(basePath, p));
if (context.getDirectories) {
this.getDirectories = path => context.getDirectories !(path);
}
if (context.directoryExists) {
this.directoryExists = directoryName => context.directoryExists !(directoryName);
}
if (context.getCancellationToken) {
this.getCancellationToken = () => context.getCancellationToken !();
}
if (context.getDefaultLibLocation) {
this.getDefaultLibLocation = () => context.getDefaultLibLocation !();
}
if (context.trace) {
this.trace = s => context.trace !(s);
}
if (context.fileNameToModuleName) {
this.fileNameToModuleName = context.fileNameToModuleName.bind(context);
}
// Note: don't copy over context.moduleNameToFileName as we first
// normalize undefined containingFile to a filled containingFile.
if (context.resourceNameToFileName) {
this.resourceNameToFileName = context.resourceNameToFileName.bind(context);
}
if (context.toSummaryFileName) {
this.toSummaryFileName = context.toSummaryFileName.bind(context);
}
if (context.fromSummaryFileName) {
this.fromSummaryFileName = context.fromSummaryFileName.bind(context);
}
}
moduleNameToFileName(m: string, containingFile: string): string|null {
private resolveModuleName(moduleName: string, containingFile: string): ts.ResolvedModule
|undefined {
const rm = ts.resolveModuleName(
moduleName, containingFile, this.options, this, this.moduleResolutionCache)
.resolvedModule;
if (rm && this.isSourceFile(rm.resolvedFileName)) {
// Case: generateCodeForLibraries = true and moduleName is
// a .d.ts file in a node_modules folder.
// Need to set isExternalLibraryImport to false so that generated files for that file
// are emitted.
rm.isExternalLibraryImport = false;
}
return rm;
}
// Note: We implement this method so that TypeScript and Angular share the same
// ts.ModuleResolutionCache
// and that we can tell ts.Program about our different opinion about
// ResolvedModule.isExternalLibraryImport
// (see our isSourceFile method).
resolveModuleNames(moduleNames: string[], containingFile: string): ts.ResolvedModule[] {
// TODO(tbosch): this seems to be a typing error in TypeScript,
// as it contains assertions that the result contains the same number of entries
// as the given module names.
return <ts.ResolvedModule[]>moduleNames.map(
moduleName => this.resolveModuleName(moduleName, containingFile));
}
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 = path.join(this.basePath, 'index.ts');
containingFile = this.rootFiles[0];
}
const resolved = ts.resolveModuleName(
m, containingFile.replace(/\\/g, '/'), this.options,
this.moduleResolutionHost, this.moduleResolutionCache)
.resolvedModule;
if (resolved) {
if (this.options.traceResolution) {
console.error('resolve', m, containingFile, '=>', resolved.resolvedFileName);
}
return resolved.resolvedFileName;
if (this.context.moduleNameToFileName) {
return this.context.moduleNameToFileName(m, containingFile);
}
return null;
const resolved = this.resolveModuleName(m, containingFile);
return resolved ? resolved.resolvedFileName : null;
}
/**
@ -97,11 +166,6 @@ class CompilerHostMixin {
'fileNameToModuleName 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 it so that the `resolve` method works!
if (!this.moduleResolutionHost.fileExists(importedFile)) {
this.moduleResolutionHost.assumeFileExists(importedFile);
}
// drop extension
importedFile = importedFile.replace(EXT, '');
const importedFilePackagName = getPackageName(importedFile);
@ -109,8 +173,8 @@ class CompilerHostMixin {
let moduleName: string;
if (importedFilePackagName === containingFilePackageName) {
const rootedContainingFile = stripRootDir(this.rootDirs, containingFile);
const rootedImportedFile = stripRootDir(this.rootDirs, importedFile);
const rootedContainingFile = relativeToRootDirs(containingFile, this.rootDirs);
const rootedImportedFile = relativeToRootDirs(importedFile, this.rootDirs);
if (rootedContainingFile !== containingFile && rootedImportedFile !== importedFile) {
// if both files are contained in the `rootDirs`, then strip the rootDirs
@ -127,18 +191,6 @@ class CompilerHostMixin {
return moduleName;
}
toSummaryFileName(fileName: string, referringSrcFileName: string): string {
return this.fileNameToModuleName(fileName, referringSrcFileName);
}
fromSummaryFileName(fileName: string, referringLibFileName: string): string {
const resolved = this.moduleNameToFileName(fileName, referringLibFileName);
if (!resolved) {
throw new Error(`Could not resolve ${fileName} from ${referringLibFileName}`);
}
return resolved;
}
resourceNameToFileName(resourceName: string, containingFile: string): string|null {
// Note: we convert package paths into relative paths to be compatible with the the
// previous implementation of UrlResolver.
@ -152,45 +204,193 @@ class CompilerHostMixin {
this.moduleNameToFileName(addNgResourceSuffix(resourceName), containingFile);
return filePathWithNgResource ? stripNgResourceSuffix(filePathWithNgResource) : null;
}
}
interface ModuleFilenameResolutionHost extends ts.ModuleResolutionHost {
assumeFileExists(fileName: string): void;
}
toSummaryFileName(fileName: string, referringSrcFileName: string): string {
return this.fileNameToModuleName(fileName, referringSrcFileName);
}
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 DefaultModuleFilenameResolver
// and it should be able to resolve summary file names.
resolveModuleNameHost.fileExists = (fileName: string): boolean => {
fromSummaryFileName(fileName: string, referringLibFileName: string): string {
const resolved = this.moduleNameToFileName(fileName, referringLibFileName);
if (!resolved) {
throw new Error(`Could not resolve ${fileName} from ${referringLibFileName}`);
}
return resolved;
}
parseSourceSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null {
const data = this.generatedSourceFiles.get(fileName);
if (data && data.emitCtx) {
return data.emitCtx.spanOf(line, character);
}
return null;
}
private getOriginalSourceFile(
filePath: string, languageVersion?: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined): ts.SourceFile|undefined {
let sf = this.originalSourceFiles.get(filePath);
if (sf) {
return sf;
}
if (!languageVersion) {
languageVersion = this.options.target || ts.ScriptTarget.Latest;
}
sf = this.context.getSourceFile(filePath, languageVersion, onError);
this.originalSourceFiles.set(filePath, sf);
return sf;
}
getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined {
const sf = this.getOriginalSourceFile(filePath);
if (!sf) {
return undefined;
}
return this.metadataProvider.getMetadata(sf);
}
updateGeneratedFile(genFile: GeneratedFile): ts.SourceFile|null {
if (!genFile.stmts) {
return null;
}
const oldGenFile = this.generatedSourceFiles.get(genFile.genFileUrl);
if (!oldGenFile) {
throw new Error(`Illegal State: previous GeneratedFile not found for ${genFile.genFileUrl}.`);
}
const newRefs = genFileExternalReferences(genFile);
const oldRefs = oldGenFile.externalReferences;
let refsAreEqual = oldRefs.size === newRefs.size;
if (refsAreEqual) {
newRefs.forEach(r => refsAreEqual = refsAreEqual && oldRefs.has(r));
}
if (!refsAreEqual) {
throw new Error(
`Illegal State: external references changed in ${genFile.genFileUrl}.\nOld: ${Array.from(oldRefs)}.\nNew: ${Array.from(newRefs)}`);
}
return this.addGeneratedFile(genFile, newRefs);
}
private addGeneratedFile(genFile: GeneratedFile, externalReferences: Set<string>): ts.SourceFile
|null {
if (!genFile.stmts) {
return null;
}
const {sourceText, context} = this.emitter.emitStatementsAndContext(
genFile.srcFileUrl, genFile.genFileUrl, genFile.stmts, /* preamble */ '',
/* emitSourceMaps */ false);
const sf = ts.createSourceFile(
genFile.genFileUrl, sourceText, this.options.target || ts.ScriptTarget.Latest);
this.generatedSourceFiles.set(genFile.genFileUrl, {
sourceFile: sf,
emitCtx: context, externalReferences,
});
return sf;
}
private ensureCodeGeneratedFor(fileName: string): void {
if (this.generatedCodeFor.has(fileName)) {
return;
}
this.generatedCodeFor.add(fileName);
const baseNameFromGeneratedFile = this._getBaseNameForGeneratedFile(fileName);
if (baseNameFromGeneratedFile) {
return this.ensureCodeGeneratedFor(baseNameFromGeneratedFile);
}
const sf = this.getOriginalSourceFile(fileName, this.options.target || ts.ScriptTarget.Latest);
if (!sf) {
return;
}
const genFileNames: string[] = [];
if (this.isSourceFile(fileName)) {
// Note: we can't exit early here,
// as we might need to clear out old changes to `SourceFile.referencedFiles`
// that were created by a previous run, given an original CompilerHost
// that caches source files.
const genFiles = this.codeGenerator(fileName);
genFiles.forEach(genFile => {
const sf = this.addGeneratedFile(genFile, genFileExternalReferences(genFile));
if (sf) {
genFileNames.push(sf.fileName);
}
});
}
addReferencesToSourceFile(sf, genFileNames);
}
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined): ts.SourceFile {
this.ensureCodeGeneratedFor(fileName);
const genFile = this.generatedSourceFiles.get(fileName);
if (genFile) {
return genFile.sourceFile;
}
// TODO(tbosch): TypeScript's typings for getSourceFile are incorrect,
// as it can very well return undefined.
return this.getOriginalSourceFile(fileName, languageVersion, onError) !;
}
fileExists(fileName: string): boolean {
fileName = stripNgResourceSuffix(fileName);
if (assumedExists.has(fileName)) {
// Note: Don't rely on this.generatedSourceFiles here,
// as it might not have been filled yet.
if (this._getBaseNameForGeneratedFile(fileName)) {
return true;
}
return this.originalSourceFiles.has(fileName) || this.context.fileExists(fileName);
}
if (host.fileExists(fileName)) {
return true;
private _getBaseNameForGeneratedFile(genFileName: string): string|null {
const genMatch = GENERATED_FILES.exec(genFileName);
if (genMatch) {
const [, base, genSuffix, suffix] = genMatch;
// Note: on-the-fly generated files always have a `.ts` suffix,
// but the file from which we generated it can be a `.ts`/ `.d.ts`
// (see options.generateCodeForLibraries).
// It can also be a `.css` file in case of a `.css.ngstyle.ts` file
if (suffix === 'ts') {
const baseNames =
genSuffix.indexOf('ngstyle') >= 0 ? [base] : [`${base}.ts`, `${base}.d.ts`];
return baseNames.find(
baseName => this.isSourceFile(baseName) && this.fileExists(baseName)) ||
null;
}
}
return null;
}
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);
readFile = (fileName: string) => this.context.readFile(fileName);
getDefaultLibFileName = (options: ts.CompilerOptions) =>
this.context.getDefaultLibFileName(options);
getCurrentDirectory = () => this.context.getCurrentDirectory();
getCanonicalFileName = (fileName: string) => this.context.getCanonicalFileName(fileName);
useCaseSensitiveFileNames = () => this.context.useCaseSensitiveFileNames();
getNewLine = () => this.context.getNewLine();
// 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;
realPath = (p: string) => p;
writeFile = this.context.writeFile.bind(this.context);
}
return resolveModuleNameHost;
function genFileExternalReferences(genFile: GeneratedFile): Set<string> {
return new Set(collectExternalReferences(genFile.stmts !).map(er => er.moduleName !));
}
function addReferencesToSourceFile(sf: ts.SourceFile, genFileNames: string[]) {
// Note: as we modify ts.SourceFiles we need to keep the original
// value for `referencedFiles` around in cache the original host is caching ts.SourceFiles.
// Note: cloning the ts.SourceFile is expensive as the nodes in have parent pointers,
// i.e. we would also need to clone and adjust all nodes.
let originalReferencedFiles: ts.FileReference[]|undefined = (sf as any).originalReferencedFiles;
if (!originalReferencedFiles) {
originalReferencedFiles = sf.referencedFiles;
(sf as any).originalReferencedFiles = originalReferencedFiles;
}
const newReferencedFiles = [...originalReferencedFiles];
genFileNames.forEach(gf => newReferencedFiles.push({fileName: gf, pos: 0, end: 0}));
sf.referencedFiles = newReferencedFiles;
}
function dotRelative(from: string, to: string): string {
@ -206,16 +406,13 @@ function getPackageName(filePath: string): string|null {
return match ? match[1] : null;
}
function stripRootDir(rootDirs: string[], fileName: string): string {
if (!fileName) return fileName;
// NB: the rootDirs should have been sorted longest-first
for (const dir of rootDirs) {
if (fileName.indexOf(dir) === 0) {
fileName = fileName.substring(dir.length);
break;
}
export function relativeToRootDirs(filePath: string, rootDirs: string[]): string {
if (!filePath) return filePath;
for (const dir of rootDirs || []) {
const rel = path.relative(dir, filePath);
if (rel.indexOf('.') != 0) return rel;
}
return fileName;
return filePath;
}
function stripNodeModulesPrefix(filePath: string): string {
@ -227,10 +424,6 @@ function getNodeModulesPrefix(filePath: string): string|null {
return match ? match[1] : null;
}
function normalizePath(p: string): string {
return path.normalize(path.join(p, '.')).replace(/\\/g, '/');
}
function stripNgResourceSuffix(fileName: string): string {
return fileName.replace(/\.\$ngresource\$.*/, '');
}

View File

@ -6,19 +6,19 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, MessageBundle, NgAnalyzedModules, ParseSourceSpan, Serializer, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError, toTypeScript} from '@angular/compiler';
import {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedModules, ParseSourceSpan, Serializer, StubEmitFlags, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isSyntaxError} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {BaseAotCompilerHost} from '../compiler_host';
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
import {createBundleIndexHost} from '../metadata/index';
import {ModuleMetadata, createBundleIndexHost} from '../metadata/index';
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
import {TsCompilerAotCompilerTypeCheckHostAdapter} from './compiler_host';
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {GENERATED_FILES} from './util';
import {GENERATED_FILES, StructureIsReused, tsStructureIsReused} from './util';
const emptyModules: NgAnalyzedModules = {
ngModules: [],
@ -34,17 +34,13 @@ const defaultEmitCallback: TsEmitCallback =
class AngularCompilerProgram implements Program {
private tsProgram: ts.Program;
private aotCompilerHost: AotCompilerHost;
private compiler: AotCompiler;
private srcNames: string[];
private metadataCache: LowerMetadataCache;
// Lazily initialized fields
private _typeCheckHost: TypeCheckHost;
private _compiler: AotCompiler;
private _tsProgram: ts.Program;
private _analyzedModules: NgAnalyzedModules|undefined;
private _structuralDiagnostics: Diagnostic[] = [];
private _stubs: GeneratedFile[]|undefined;
private _stubFiles: string[]|undefined;
private _programWithStubsHost: ts.CompilerHost&TypeCheckHost|undefined;
private _structuralDiagnostics: Diagnostic[]|undefined;
private _programWithStubs: ts.Program|undefined;
private _generatedFiles: GeneratedFile[]|undefined;
private _generatedFileDiagnostics: Diagnostic[]|undefined;
@ -53,7 +49,13 @@ class AngularCompilerProgram implements Program {
constructor(
private rootNames: string[], private options: CompilerOptions, private host: CompilerHost,
oldProgram?: Program) {
private oldProgram?: Program) {
const [major, minor] = ts.version.split('.');
if (Number(major) < 2 || (Number(major) === 2 && Number(minor) < 4)) {
throw new Error('The Angular Compiler requires TypeScript >= 2.4.');
}
this.rootNames = rootNames = rootNames.filter(r => !GENERATED_FILES.test(r));
if (options.flatModuleOutFile) {
const {host: bundleHost, indexName, errors} = createBundleIndexHost(options, rootNames, host);
if (errors) {
@ -67,26 +69,13 @@ class AngularCompilerProgram implements Program {
})));
} else {
rootNames.push(indexName !);
this.host = host = bundleHost;
this.host = bundleHost;
}
}
const oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
this.tsProgram = ts.createProgram(rootNames, options, host, oldTsProgram);
this.srcNames =
this.tsProgram.getSourceFiles()
.map(sf => sf.fileName)
.filter(f => !f.match(/\.ngfactory\.[\w.]+$|\.ngstyle\.[\w.]+$|\.ngsummary\.[\w.]+$/));
this.metadataCache = new LowerMetadataCache({quotedNames: true}, !!options.strictMetadataEmit);
this.aotCompilerHost =
new AotCompilerHostImpl(this.tsProgram, options, host, this.metadataCache);
const aotOptions = getAotCompilerOptions(options);
this.compiler = createAotCompiler(this.aotCompilerHost, aotOptions).compiler;
}
// Program implementation
getTsProgram(): ts.Program { return this.programWithStubs; }
getTsProgram(): ts.Program { return this.tsProgram; }
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken) {
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
@ -121,23 +110,28 @@ class AngularCompilerProgram implements Program {
}
loadNgStructureAsync(): Promise<void> {
return this.compiler.analyzeModulesAsync(this.rootNames)
if (this._analyzedModules) {
throw new Error('Angular structure already loaded');
}
const {tmpProgram, analyzedFiles, hostAdapter} = this._createProgramWithBasicStubs();
return this._compiler.loadFilesAsync(analyzedFiles)
.catch(this.catchAnalysisError.bind(this))
.then(analyzedModules => {
if (this._analyzedModules) {
throw new Error('Angular structure loaded both synchronously and asynchronsly');
}
this._analyzedModules = analyzedModules;
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, hostAdapter);
});
}
emit({emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback}: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback
}): ts.EmitResult {
emit(
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback}: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback
} = {}): ts.EmitResult {
if (emitFlags & EmitFlags.I18nBundle) {
const locale = this.options.i18nOutLocale || null;
const file = this.options.i18nOutFile || null;
@ -145,60 +139,95 @@ class AngularCompilerProgram implements Program {
const bundle = this.compiler.emitMessageBundle(this.analyzedModules, locale);
i18nExtract(format, file, this.host, this.options, bundle);
}
if (emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Summary)) {
return emitCallback({
program: this.programWithStubs,
host: this.host,
options: this.options,
targetSourceFile: undefined,
writeFile:
createWriteFileCallback(emitFlags, this.host, this.metadataCache, this.generatedFiles),
cancellationToken,
emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS,
customTransformers: this.calculateTransforms(customTransformers)
const outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}> = [];
if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Summary)) ===
0) {
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
}
const emitResult = emitCallback({
program: this.tsProgram,
host: this.host,
options: this.options,
targetSourceFile: undefined,
writeFile: createWriteFileCallback(this.options, this.host, outSrcMapping), cancellationToken,
emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS,
customTransformers: this.calculateTransforms(customTransformers)
});
if (!outSrcMapping.length) {
// if no files were emitted by TypeScript, also don't emit .json files
return emitResult;
}
const srcToOutPath = this.createSrcToOutPathMapper(outSrcMapping);
if (!this.options.skipTemplateCodegen) {
this.generatedFiles.forEach(gf => {
if (gf.source) {
this.host.writeFile(srcToOutPath(gf.genFileUrl), gf.source, false);
}
});
}
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
if (emitFlags & EmitFlags.Metadata) {
this.tsProgram.getSourceFiles().forEach(sf => {
if (!sf.isDeclarationFile && !GENERATED_FILES.test(sf.fileName)) {
const metadata = this.metadataCache.getMetadata(sf);
const metadataText = JSON.stringify([metadata]);
this.host.writeFile(
srcToOutPath(sf.fileName.replace(/\.ts$/, '.metadata.json')), metadataText, false);
}
});
}
return emitResult;
}
// Private members
private get compiler(): AotCompiler {
if (!this._compiler) {
this.initSync();
}
return this._compiler !;
}
private get analyzedModules(): NgAnalyzedModules {
return this._analyzedModules || (this._analyzedModules = this.analyzeModules());
if (!this._analyzedModules) {
this.initSync();
}
return this._analyzedModules !;
}
private get structuralDiagnostics(): Diagnostic[] {
return this.analyzedModules && this._structuralDiagnostics;
if (!this._structuralDiagnostics) {
this.initSync();
}
return this._structuralDiagnostics !;
}
private get stubs(): GeneratedFile[] {
return this._stubs || (this._stubs = this.generateStubs());
private get tsProgram(): ts.Program {
if (!this._tsProgram) {
this.initSync();
}
return this._tsProgram !;
}
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&TypeCheckHost {
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 typeCheckHost(): TypeCheckHost {
if (!this._typeCheckHost) {
this.initSync();
}
return this._typeCheckHost !;
}
private get generatedFiles(): GeneratedFile[] {
return this._generatedFiles || (this._generatedFiles = this.generateFiles());
if (!this._generatedFiles) {
this.generateFiles();
}
return this._generatedFiles !;
}
private get generatedFileDiagnostics(): Diagnostic[]|undefined {
return this.generatedFiles && this._generatedFileDiagnostics !;
if (!this._generatedFileDiagnostics) {
this.generateFiles();
}
return this._generatedFileDiagnostics !;
}
private get semanticDiagnostics(): {ts: ts.Diagnostic[], ng: Diagnostic[]} {
@ -211,9 +240,7 @@ class AngularCompilerProgram implements Program {
if (!this.options.disableExpressionLowering) {
beforeTs.push(getExpressionLoweringTransformFactory(this.metadataCache));
}
if (!this.options.skipTemplateCodegen) {
beforeTs.push(getAngularEmitterTransformFactory(this.generatedFiles));
}
beforeTs.push(getAngularEmitterTransformFactory(this.generatedFiles));
if (customTransformers && customTransformers.beforeTs) {
beforeTs.push(...customTransformers.beforeTs);
}
@ -221,6 +248,91 @@ class AngularCompilerProgram implements Program {
return {before: beforeTs, after: afterTs};
}
private createSrcToOutPathMapper(outSrcMappings:
Array<{sourceFile: ts.SourceFile, outFileName: string}>):
(srcFileName: string) => string {
let srcToOutPath: (srcFileName: string) => string;
if (this.options.outDir) {
// TODO(tbosch): talk to TypeScript team to expose their logic for calculating the `rootDir`
// if none was specified.
if (outSrcMappings.length === 0) {
throw new Error(`Can't calculate the rootDir without at least one outSrcMapping. `);
}
const firstEntry = outSrcMappings[0];
const entrySrcDir = path.dirname(firstEntry.sourceFile.fileName);
const entryOutDir = path.dirname(firstEntry.outFileName);
const commonSuffix = longestCommonSuffix(entrySrcDir, entryOutDir);
const rootDir = entrySrcDir.substring(0, entrySrcDir.length - commonSuffix.length);
srcToOutPath = (srcFileName) =>
path.resolve(this.options.outDir, path.relative(rootDir, srcFileName));
} else {
srcToOutPath = (srcFileName) => srcFileName;
}
return srcToOutPath;
}
private initSync() {
if (this._analyzedModules) {
return;
}
const {tmpProgram, analyzedFiles, hostAdapter} = this._createProgramWithBasicStubs();
let analyzedModules: NgAnalyzedModules;
try {
analyzedModules = this._compiler.loadFilesSync(analyzedFiles);
} catch (e) {
analyzedModules = this.catchAnalysisError(e);
}
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, hostAdapter);
}
private _createProgramWithBasicStubs(): {
tmpProgram: ts.Program,
analyzedFiles: NgAnalyzedFile[],
hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter
} {
if (this._analyzedModules) {
throw new Error(`Internal Error: already initalized!`);
}
const analyzedFiles: NgAnalyzedFile[] = [];
const codegen = (fileName: string) => {
if (this._analyzedModules) {
throw new Error(`Internal Error: already initalized!`);
}
const analyzedFile = this._compiler.analyzeFile(fileName);
analyzedFiles.push(analyzedFile);
const debug = fileName.endsWith('application_ref.ts');
return this._compiler.emitBasicStubs(analyzedFile);
};
const hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter(
this.rootNames, this.options, this.host, this.metadataCache, codegen);
const aotOptions = getAotCompilerOptions(this.options);
this._compiler = createAotCompiler(hostAdapter, aotOptions).compiler;
this._typeCheckHost = hostAdapter;
this._structuralDiagnostics = [];
const oldTsProgram = this.oldProgram ? this.oldProgram.getTsProgram() : undefined;
// Note: This is important to not produce a memory leak!
this.oldProgram = undefined;
const tmpProgram = ts.createProgram(this.rootNames, this.options, hostAdapter, oldTsProgram);
return {tmpProgram, analyzedFiles, hostAdapter};
}
private _updateProgramWithTypeCheckStubs(
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules,
hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter) {
this._analyzedModules = analyzedModules;
const genFiles = this._compiler.emitTypeCheckStubs(analyzedModules);
genFiles.forEach(gf => hostAdapter.updateGeneratedFile(gf));
this._tsProgram = ts.createProgram(this.rootNames, this.options, hostAdapter, tmpProgram);
// Note: the new ts program should be completely reusable by TypeScript as:
// - we cache all the files in the hostAdapter
// - new new stubs use the exactly same imports/exports as the old once (we assert that in
// hostAdapter.updateGeneratedFile).
if (tsStructureIsReused(tmpProgram) !== StructureIsReused.Completely) {
throw new Error(`Internal Error: The structure of the program changed during codegen.`);
}
}
private catchAnalysisError(e: any): NgAnalyzedModules {
if (isSyntaxError(e)) {
const parserErrors = getParseErrors(e);
@ -241,78 +353,32 @@ class AngularCompilerProgram implements Program {
code: DEFAULT_ERROR_CODE
}];
}
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.compiler.emitAllStubs(this.analyzedModules);
}
private generateFiles() {
const diags: Diagnostic[] = this._generatedFileDiagnostics = [];
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;
this._generatedFiles = this.compiler.emitAllImpls(this.analyzedModules);
} catch (e) {
this._generatedFiles = [];
if (isSyntaxError(e)) {
this._generatedFileDiagnostics = [{
diags.push({
messageText: e.message,
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
}];
});
return [];
}
throw e;
}
}
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);
}
private generateSemanticDiagnostics(): {ts: ts.Diagnostic[], ng: Diagnostic[]} {
return translateDiagnostics(
this.programWithStubsHost, this.programWithStubs.getSemanticDiagnostics());
}
}
class AotCompilerHostImpl extends BaseAotCompilerHost<CompilerHost> {
moduleNameToFileName(m: string, containingFile: string): string|null {
return this.context.moduleNameToFileName(m, containingFile);
}
fileNameToModuleName(importedFile: string, containingFile: string): string|null {
return this.context.fileNameToModuleName(importedFile, containingFile);
}
resourceNameToFileName(resourceName: string, containingFile: string): string|null {
return this.context.resourceNameToFileName(resourceName, containingFile);
}
toSummaryFileName(fileName: string, referringSrcFileName: string): string {
return this.context.toSummaryFileName(fileName, referringSrcFileName);
}
fromSummaryFileName(fileName: string, referringLibFileName: string): string {
return this.context.fromSummaryFileName(fileName, referringLibFileName);
return translateDiagnostics(this.typeCheckHost, this.tsProgram.getSemanticDiagnostics());
}
}
@ -359,70 +425,20 @@ function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions {
};
}
function writeMetadata(
host: ts.CompilerHost, emitFilePath: string, sourceFile: ts.SourceFile,
metadataCache: LowerMetadataCache, onError?: (message: string) => void) {
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 = metadataCache.getMetadata(collectableFile);
if (metadata) {
const metadataText = JSON.stringify([metadata]);
host.writeFile(path, metadataText, false, onError, [sourceFile]);
}
}
}
function writeNgSummaryJson(
host: ts.CompilerHost, emitFilePath: string, sourceFile: ts.SourceFile,
generatedFilesByName: Map<string, GeneratedFile>, onError?: (message: string) => void) {
// Note: some files have an empty .ngfactory.js/.d.ts file but still need
// .ngsummary.json files (e.g. directives / pipes).
// We write the ngSummary when we try to emit the .ngfactory.js files
// and not the regular .js files as the latter are not emitted when
// we generate code for a npm library which ships .js / .d.ts / .metadata.json files.
if (/\.ngfactory.js$/.test(emitFilePath)) {
const emitPath = emitFilePath.replace(/\.ngfactory\.js$/, '.ngsummary.json');
const genFilePath = sourceFile.fileName.replace(/\.ngfactory\.ts$/, '.ngsummary.json');
const genFile = generatedFilesByName.get(genFilePath);
if (genFile) {
host.writeFile(emitPath, genFile.source !, false, onError, [sourceFile]);
}
}
}
function createWriteFileCallback(
emitFlags: EmitFlags, host: ts.CompilerHost, metadataCache: LowerMetadataCache,
generatedFiles: GeneratedFile[]) {
const generatedFilesByName = new Map<string, GeneratedFile>();
generatedFiles.forEach(f => generatedFilesByName.set(f.genFileUrl, f));
options: {skipTemplateCodegen?: boolean}, host: ts.CompilerHost,
outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}>) {
return (fileName: string, data: string, writeByteOrderMark: boolean,
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null;
const isGenerated = GENERATED_FILES.test(fileName);
if (sourceFile) {
const isGenerated = GENERATED_FILES.test(fileName);
if (isGenerated) {
writeNgSummaryJson(host, fileName, sourceFile, generatedFilesByName, onError);
}
if (!isGenerated && (emitFlags & EmitFlags.Metadata)) {
writeMetadata(host, fileName, sourceFile, metadataCache, onError);
}
if (isGenerated) {
const genFile = generatedFilesByName.get(sourceFile.fileName);
if (!genFile || !genFile.stmts || !genFile.stmts.length) {
// Don't emit empty generated files
return;
}
}
outSrcMapping.push({outFileName: fileName, sourceFile});
}
if (isGenerated && options.skipTemplateCodegen) {
// Always generate the files if requested to ensure we capture any diagnostic errors but only
// don't emit them.
return;
}
host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
};
@ -447,86 +463,12 @@ function getNgOptionDiagnostics(options: CompilerOptions): Diagnostic[] {
return [];
}
function createProgramWithStubsHost(
generatedFiles: GeneratedFile[], originalProgram: ts.Program,
originalHost: ts.CompilerHost): ts.CompilerHost&TypeCheckHost {
interface FileData {
g: GeneratedFile;
s?: ts.SourceFile;
emitCtx?: EmitterVisitorContext;
function longestCommonSuffix(a: string, b: string): string {
let len = 0;
while (a.charCodeAt(a.length - 1 - len) === b.charCodeAt(b.length - 1 - len)) {
len++;
}
return new class implements ts.CompilerHost, TypeCheckHost {
private generatedFiles: Map<string, FileData>;
private emitter = new TypeScriptEmitter();
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);
}
}
ngSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null {
const data = this.generatedFiles.get(fileName);
if (data && data.emitCtx) {
return data.emitCtx.spanOf(line, character);
}
return null;
}
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined): ts.SourceFile {
const data = this.generatedFiles.get(fileName);
if (data) {
if (!data.s) {
const {sourceText, context} = this.emitter.emitStatementsAndContext(
data.g.srcFileUrl, data.g.genFileUrl, data.g.stmts !,
/* preamble */ undefined, /* emitSourceMaps */ undefined,
/* referenceFilter */ undefined);
data.emitCtx = context;
data.s = ts.createSourceFile(fileName, sourceText, languageVersion);
}
return data.s;
}
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();
realPath = (p: string) => p;
fileExists = (fileName: string) =>
this.generatedFiles.has(fileName) || originalHost.fileExists(fileName);
};
return a.substring(a.length - len);
}
export function i18nExtract(

View File

@ -6,4 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
export const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/;
export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2}
// Note: This is an internal property in TypeScript. Use it only for assertions and tests.
export function tsStructureIsReused(program: ts.Program): StructureIsReused {
return (program as any).structureIsReused;
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompilerHost, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler';
import {AotCompilerHost, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, InterpolationConfig, JitSummaryResolver, Lexer, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, Parser, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, TemplateParser, analyzeNgModules, createOfflineCompileUrlResolver} from '@angular/compiler';
import {ViewEncapsulation, ɵConsole as Console} from '@angular/core';
import {CompilerHostContext} from 'compiler-cli';
import * as fs from 'fs';
@ -175,9 +175,10 @@ export class DiagnosticContext {
new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config);
result = this._resolver = new CompileMetadataResolver(
config, moduleResolver, directiveResolver, pipeResolver, new JitSummaryResolver(),
elementSchemaRegistry, directiveNormalizer, new Console(), staticSymbolCache,
this.reflector, (error, type) => this.collectError(error, type && type.filePath));
config, htmlParser, moduleResolver, directiveResolver, pipeResolver,
new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(),
staticSymbolCache, this.reflector,
(error, type) => this.collectError(error, type && type.filePath));
}
return result;
}
@ -186,12 +187,9 @@ export class DiagnosticContext {
let analyzedModules = this._analyzedModules;
if (!analyzedModules) {
const analyzeHost = {isSourceFile(filePath: string) { return true; }};
const programSymbols = extractProgramSymbols(
this.staticSymbolResolver, this.program.getSourceFiles().map(sf => sf.fileName),
analyzeHost);
const programFiles = this.program.getSourceFiles().map(sf => sf.fileName);
analyzedModules = this._analyzedModules =
analyzeNgModules(programSymbols, analyzeHost, this.staticSymbolResolver, this.resolver);
analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, this.resolver);
}
return analyzedModules;
}

View File

@ -111,7 +111,7 @@ export class MockCompilerHost implements ts.CompilerHost {
fileName: string, languageVersion: ts.ScriptTarget,
onError?: (message: string) => void): ts.SourceFile {
const sourceText = this.context.readFile(fileName);
if (sourceText) {
if (sourceText != null) {
return ts.createSourceFile(fileName, sourceText, languageVersion);
} else {
return undefined !;

View File

@ -363,7 +363,6 @@ describe('ngc transformer command-line', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"enableSummariesForJit": true
},
"include": ["src/**/*.ts"]
}`);
@ -379,7 +378,6 @@ describe('ngc transformer command-line', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"enableSummariesForJit": true,
"skipTemplateCodegen": true
},
"compilerOptions": {
@ -410,7 +408,6 @@ describe('ngc transformer command-line', () => {
writeConfig(`{
"extends": "./tsconfig-base.json",
"angularCompilerOptions": {
"enableSummariesForJit": true,
"skipTemplateCodegen": false
},
"compilerOptions": {
@ -826,8 +823,7 @@ describe('ngc transformer command-line', () => {
write('lib1/tsconfig-lib1.json', `{
"extends": "../tsconfig-base.json",
"angularCompilerOptions": {
"generateCodeForLibraries": false,
"enableSummariesForJit": true
"generateCodeForLibraries": false
},
"compilerOptions": {
"rootDir": ".",
@ -849,8 +845,7 @@ describe('ngc transformer command-line', () => {
write('lib2/tsconfig-lib2.json', `{
"extends": "../tsconfig-base.json",
"angularCompilerOptions": {
"generateCodeForLibraries": false,
"enableSummariesForJit": true
"generateCodeForLibraries": false
},
"compilerOptions": {
"rootDir": ".",

View File

@ -6,59 +6,78 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as compiler from '@angular/compiler';
import * as ts from 'typescript';
import {MetadataCollector} from '../../src/metadata/collector';
import {CompilerHost, CompilerOptions} from '../../src/transformers/api';
import {createCompilerHost} from '../../src/transformers/compiler_host';
import {TsCompilerAotCompilerTypeCheckHostAdapter, createCompilerHost} from '../../src/transformers/compiler_host';
import {Directory, Entry, MockAotContext, MockCompilerHost} from '../mocks';
const dummyModule = 'export let foo: any[];';
const aGeneratedFile = new compiler.GeneratedFile(
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
[new compiler.DeclareVarStmt('x', new compiler.LiteralExpr(1))]);
const aGeneratedFileText = `var x:any = 1;\n`;
describe('NgCompilerHost', () => {
function createHost(
{files = {}, options = {basePath: '/tmp'}}: {files?: Directory,
options?: CompilerOptions} = {}) {
let codeGenerator: jasmine.Spy;
beforeEach(() => { codeGenerator = jasmine.createSpy('codeGenerator').and.returnValue([]); });
function createNgHost({files = {}}: {files?: Directory} = {}): CompilerHost {
const context = new MockAotContext('/tmp/', files);
const tsHost = new MockCompilerHost(context);
return createCompilerHost({tsHost, options});
return new MockCompilerHost(context) as ts.CompilerHost;
}
function createHost({
files = {},
options = {
basePath: '/tmp',
moduleResolution: ts.ModuleResolutionKind.NodeJs,
},
ngHost = createNgHost({files}),
}: {files?: Directory, options?: CompilerOptions, ngHost?: CompilerHost} = {}) {
return new TsCompilerAotCompilerTypeCheckHostAdapter(
['/tmp/index.ts'], options, ngHost, new MetadataCollector(), codeGenerator);
}
describe('fileNameToModuleName', () => {
let ngHost: CompilerHost;
beforeEach(() => { ngHost = createHost(); });
let host: TsCompilerAotCompilerTypeCheckHostAdapter;
beforeEach(() => { host = createHost(); });
it('should use a package import when accessing a package from a source file', () => {
expect(ngHost.fileNameToModuleName('/tmp/node_modules/@angular/core.d.ts', '/tmp/main.ts'))
expect(host.fileNameToModuleName('/tmp/node_modules/@angular/core.d.ts', '/tmp/main.ts'))
.toBe('@angular/core');
});
it('should use a package import when accessing a package from another package', () => {
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/mod1/index.d.ts', '/tmp/node_modules/mod2/index.d.ts'))
.toBe('mod1/index');
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/@angular/core/index.d.ts',
'/tmp/node_modules/@angular/common/index.d.ts'))
.toBe('@angular/core/index');
});
it('should use a relative import when accessing a file in the same package', () => {
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/mod/a/child.d.ts', '/tmp/node_modules/mod/index.d.ts'))
.toBe('./a/child');
expect(ngHost.fileNameToModuleName(
expect(host.fileNameToModuleName(
'/tmp/node_modules/@angular/core/src/core.d.ts',
'/tmp/node_modules/@angular/core/index.d.ts'))
.toBe('./src/core');
});
it('should use a relative import when accessing a source file from a source file', () => {
expect(ngHost.fileNameToModuleName('/tmp/src/a/child.ts', '/tmp/src/index.ts'))
expect(host.fileNameToModuleName('/tmp/src/a/child.ts', '/tmp/src/index.ts'))
.toBe('./a/child');
});
it('should support multiple rootDirs when accessing a source file form a source file', () => {
const ngHostWithMultipleRoots = createHost({
const hostWithMultipleRoots = createHost({
options: {
basePath: '/tmp/',
rootDirs: [
@ -68,59 +87,202 @@ describe('NgCompilerHost', () => {
}
});
// both files are in the rootDirs
expect(ngHostWithMultipleRoots.fileNameToModuleName('/tmp/src/b/b.ts', '/tmp/src/a/a.ts'))
expect(hostWithMultipleRoots.fileNameToModuleName('/tmp/src/b/b.ts', '/tmp/src/a/a.ts'))
.toBe('./b');
// one file is not in the rootDirs
expect(ngHostWithMultipleRoots.fileNameToModuleName('/tmp/src/c/c.ts', '/tmp/src/a/a.ts'))
expect(hostWithMultipleRoots.fileNameToModuleName('/tmp/src/c/c.ts', '/tmp/src/a/a.ts'))
.toBe('../c/c');
});
it('should error if accessing a source file from a package', () => {
expect(
() => ngHost.fileNameToModuleName(
() => host.fileNameToModuleName(
'/tmp/src/a/child.ts', '/tmp/node_modules/@angular/core.d.ts'))
.toThrowError(
'Trying to import a source file from a node_modules package: import /tmp/src/a/child.ts from /tmp/node_modules/@angular/core.d.ts');
'Trying to import a source file from a node_modules package: ' +
'import /tmp/src/a/child.ts from /tmp/node_modules/@angular/core.d.ts');
});
it('should use the provided implementation if any', () => {
const ngHost = createNgHost();
ngHost.fileNameToModuleName = () => 'someResult';
const host = createHost({ngHost});
expect(host.fileNameToModuleName('a', 'b')).toBe('someResult');
});
});
describe('moduleNameToFileName', () => {
it('should resolve a package import without a containing file', () => {
const ngHost = createHost(
{files: {'tmp': {'node_modules': {'@angular': {'core': {'index.d.ts': dummyModule}}}}}});
expect(ngHost.moduleNameToFileName('@angular/core'))
.toBe('/tmp/node_modules/@angular/core/index.d.ts');
it('should resolve an import using the containing file', () => {
const host = createHost({files: {'tmp': {'src': {'a': {'child.d.ts': dummyModule}}}}});
expect(host.moduleNameToFileName('./a/child', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.d.ts');
});
it('should resolve an import using the containing file', () => {
const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.d.ts': dummyModule}}}}});
expect(ngHost.moduleNameToFileName('./a/child', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.d.ts');
it('should allow to skip the containg file for package imports', () => {
const host =
createHost({files: {'tmp': {'node_modules': {'@core': {'index.d.ts': dummyModule}}}}});
expect(host.moduleNameToFileName('@core/index')).toBe('/tmp/node_modules/@core/index.d.ts');
});
it('should use the provided implementation if any', () => {
const ngHost = createNgHost();
ngHost.moduleNameToFileName = () => 'someResult';
const host = createHost({ngHost});
expect(host.moduleNameToFileName('a', 'b')).toBe('someResult');
});
});
describe('resourceNameToFileName', () => {
it('should resolve a relative import', () => {
const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(ngHost.resourceNameToFileName('./a/child.html', '/tmp/src/index.ts'))
const host = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(host.resourceNameToFileName('./a/child.html', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.html');
expect(ngHost.resourceNameToFileName('./a/non-existing.html', '/tmp/src/index.ts'))
.toBe(null);
expect(host.resourceNameToFileName('./a/non-existing.html', '/tmp/src/index.ts')).toBe(null);
});
it('should resolve package paths as relative paths', () => {
const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(ngHost.resourceNameToFileName('a/child.html', '/tmp/src/index.ts'))
const host = createHost({files: {'tmp': {'src': {'a': {'child.html': '<div>'}}}}});
expect(host.resourceNameToFileName('a/child.html', '/tmp/src/index.ts'))
.toBe('/tmp/src/a/child.html');
});
it('should resolve absolute paths as package paths', () => {
const ngHost = createHost({files: {'tmp': {'node_modules': {'a': {'child.html': '<div>'}}}}});
expect(ngHost.resourceNameToFileName('/a/child.html', ''))
const host = createHost({files: {'tmp': {'node_modules': {'a': {'child.html': '<div>'}}}}});
expect(host.resourceNameToFileName('/a/child.html', ''))
.toBe('/tmp/node_modules/a/child.html');
});
it('should use the provided implementation if any', () => {
const ngHost = createNgHost();
ngHost.resourceNameToFileName = () => 'someResult';
const host = createHost({ngHost});
expect(host.resourceNameToFileName('a', 'b')).toBe('someResult');
});
});
describe('getSourceFile', () => {
it('should cache source files by name', () => {
const host = createHost({files: {'tmp': {'src': {'index.ts': ``}}}});
const sf1 = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
const sf2 = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf1).toBe(sf2);
});
it('should generate code when asking for the base name and add it as referencedFiles', () => {
codeGenerator.and.returnValue([aGeneratedFile]);
const host = createHost({
files: {
'tmp': {
'src': {
'index.ts': `
/// <reference path="main.ts"/>
`
}
}
}
});
const sf = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(aGeneratedFileText);
// the codegen should have been cached
expect(codeGenerator).toHaveBeenCalledTimes(1);
});
it('should generate code when asking for the generated name first', () => {
codeGenerator.and.returnValue([aGeneratedFile]);
const host = createHost({
files: {
'tmp': {
'src': {
'index.ts': `
/// <reference path="main.ts"/>
`
}
}
}
});
const genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(aGeneratedFileText);
const sf = host.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
// the codegen should have been cached
expect(codeGenerator).toHaveBeenCalledTimes(1);
});
it('should clear old generated references if the original host cached them', () => {
const ngHost = createNgHost();
const sfText = `
/// <reference path="main.ts"/>
`;
const sf = ts.createSourceFile('/tmp/src/index.ts', sfText, ts.ScriptTarget.Latest);
ngHost.getSourceFile = () => sf;
codeGenerator.and.returnValue(
[new compiler.GeneratedFile('/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', [])]);
const host1 = createHost({ngHost});
host1.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles.length).toBe(2);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
expect(sf.referencedFiles[1].fileName).toBe('/tmp/src/index.ngfactory.ts');
codeGenerator.and.returnValue([]);
const host2 = createHost({ngHost});
host2.getSourceFile('/tmp/src/index.ts', ts.ScriptTarget.Latest);
expect(sf.referencedFiles.length).toBe(1);
expect(sf.referencedFiles[0].fileName).toBe('main.ts');
});
});
describe('updateSourceFile', () => {
it('should update source files', () => {
codeGenerator.and.returnValue([aGeneratedFile]);
const host = createHost({files: {'tmp': {'src': {'index.ts': ''}}}});
let genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(aGeneratedFileText);
host.updateGeneratedFile(new compiler.GeneratedFile(
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
[new compiler.DeclareVarStmt('x', new compiler.LiteralExpr(2))]));
genSf = host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(genSf.text).toBe(`var x:any = 2;\n`);
});
it('should error if the imports changed', () => {
codeGenerator.and.returnValue(
[new compiler.GeneratedFile('/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts', [
new compiler.DeclareVarStmt(
'x', new compiler.ExternalExpr(new compiler.ExternalReference('aModule', 'aName')))
])]);
const host = createHost({files: {'tmp': {'src': {'index.ts': ''}}}});
host.getSourceFile('/tmp/src/index.ngfactory.ts', ts.ScriptTarget.Latest);
expect(
() => host.updateGeneratedFile(new compiler.GeneratedFile(
'/tmp/src/index.ts', '/tmp/src/index.ngfactory.ts',
[new compiler.DeclareVarStmt(
'x', new compiler.ExternalExpr(
new compiler.ExternalReference('otherModule', 'aName')))])))
.toThrowError([
`Illegal State: external references changed in /tmp/src/index.ngfactory.ts.`,
`Old: aModule.`, `New: otherModule`
].join('\n'));
});
});
});

View File

@ -0,0 +1,294 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ng from '@angular/compiler-cli';
import {makeTempDir} from '@angular/tsc-wrapped/test/test_support';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {StructureIsReused, tsStructureIsReused} from '../../src/transformers/util';
function getNgRootDir() {
const moduleFilename = module.filename.replace(/\\/g, '/');
const distIndex = moduleFilename.indexOf('/dist/all');
return moduleFilename.substr(0, distIndex);
}
describe('ng program', () => {
let basePath: string;
let write: (fileName: string, content: string) => void;
let errorSpy: jasmine.Spy&((s: string) => void);
function writeFiles(...mockDirs: {[fileName: string]: string}[]) {
mockDirs.forEach(
(dir) => { Object.keys(dir).forEach((fileName) => { write(fileName, dir[fileName]); }); });
}
function createCompilerOptions(overrideOptions: ng.CompilerOptions = {}): ng.CompilerOptions {
return {
basePath,
'experimentalDecorators': true,
'skipLibCheck': true,
'strict': true,
'types': [],
'outDir': path.resolve(basePath, 'built'),
'rootDir': basePath,
'baseUrl': basePath,
'declaration': true,
'target': ts.ScriptTarget.ES5,
'module': ts.ModuleKind.ES2015,
'moduleResolution': ts.ModuleResolutionKind.NodeJs,
'lib': [
path.resolve(basePath, 'node_modules/typescript/lib/lib.es6.d.ts'),
path.resolve(basePath, 'node_modules/typescript/lib/lib.dom.d.ts')
],
'typeRoots': [path.resolve(basePath, 'node_modules/@types')], ...overrideOptions,
};
}
function expectNoDiagnostics(options: ng.CompilerOptions, p: ng.Program) {
const diags: ng.Diagnostics =
[...p.getTsSemanticDiagnostics(), ...p.getNgSemanticDiagnostics()];
if (diags.length > 0) {
console.error('Diagnostics: ' + ng.formatDiagnostics(options, diags));
throw new Error('Expected no diagnostics.');
}
}
beforeEach(() => {
errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error);
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'});
};
const ngRootDir = getNgRootDir();
const nodeModulesPath = path.resolve(basePath, 'node_modules');
fs.mkdirSync(nodeModulesPath);
fs.symlinkSync(
path.resolve(ngRootDir, 'dist', 'all', '@angular'),
path.resolve(nodeModulesPath, '@angular'));
fs.symlinkSync(
path.resolve(ngRootDir, 'node_modules', 'rxjs'), path.resolve(nodeModulesPath, 'rxjs'));
fs.symlinkSync(
path.resolve(ngRootDir, 'node_modules', 'typescript'),
path.resolve(nodeModulesPath, 'typescript'));
});
describe('reuse of old ts program', () => {
const files = {
'src/util.ts': `export const x = 1;`,
'src/main.ts': `
import {NgModule, Component} from '@angular/core';
import {x} from './util';
@Component({selector: 'comp', templateUrl: './main.html'})
export class MyComp {}
@NgModule()
export class MyModule {}
`,
'src/main.html': `Hello world`,
};
function expectResuse(newFiles: {[fileName: string]: string}, reuseLevel: StructureIsReused) {
writeFiles(files);
const options1 = createCompilerOptions();
const host1 = ng.createCompilerHost({options: options1});
const rootNames1 = [path.resolve(basePath, 'src/main.ts')];
const p1 = ng.createProgram({rootNames: rootNames1, options: options1, host: host1});
expectNoDiagnostics(options1, p1);
// Note: we recreate the options, rootNames and the host
// to check that TS checks against values, and not references!
writeFiles(newFiles);
const options2 = {...options1};
const host2 = ng.createCompilerHost({options: options2});
const rootNames2 = [...rootNames1];
const p2 =
ng.createProgram({rootNames: rootNames2, options: options2, host: host2, oldProgram: p1});
expectNoDiagnostics(options1, p2);
expect(tsStructureIsReused(p1.getTsProgram())).toBe(reuseLevel);
}
it('should reuse completely if nothing changed',
() => { expectResuse({}, StructureIsReused.Completely); });
it('should resuse if a template or a ts file changed', () => {
expectResuse(
{
'src/main.html': `Some other text`,
'src/util.ts': `export const x = 2;`,
},
StructureIsReused.Completely);
});
it('should not reuse if an import changed', () => {
expectResuse(
{
'src/util.ts': `
import {Injectable} from '@angular/core';
export const x = 2;
`,
},
StructureIsReused.SafeModules);
});
});
it('should typecheck templates even if skipTemplateCodegen is set', () => {
writeFiles({
'src/main.ts': `
import {NgModule, Component} from '@angular/core';
@Component({selector: 'mycomp', template: '{{nonExistent}}'})
export class MyComp {}
@NgModule({declarations: [MyComp]})
export class MyModule {}
`
});
const options = createCompilerOptions({skipTemplateCodegen: true});
const host = ng.createCompilerHost({options});
const program =
ng.createProgram({rootNames: [path.resolve(basePath, 'src/main.ts')], options, host});
const diags = program.getNgSemanticDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toBe(`Property 'nonExistent' does not exist on type 'MyComp'.`);
});
it('should be able to use asynchronously loaded resources', (done) => {
writeFiles({
'src/main.ts': `
import {NgModule, Component} from '@angular/core';
@Component({selector: 'mycomp', templateUrl: './main.html'})
export class MyComp {}
@NgModule({declarations: [MyComp]})
export class MyModule {}
`,
// Note: we need to be able to resolve the template synchronously,
// only the content is delivered asynchronously.
'src/main.html': '',
});
const options = createCompilerOptions();
const host = ng.createCompilerHost({options});
host.readResource = () => Promise.resolve('Hello world!');
const program =
ng.createProgram({rootNames: [path.resolve(basePath, 'src/main.ts')], options, host});
program.loadNgStructureAsync().then(() => {
program.emit();
const factory = fs.readFileSync(path.resolve(basePath, 'built/src/main.ngfactory.js'));
expect(factory).toContain('Hello world!');
done();
});
});
});
function appComponentSource(): string {
return `
import {Component, Pipe, Directive} from '@angular/core';
export interface Person {
name: string;
address: Address;
}
export interface Address {
street: string;
city: string;
state: string;
zip: string;
}
@Component({
templateUrl: './app.component.html'
})
export class AppComponent {
name = 'Angular';
person: Person;
people: Person[];
maybePerson?: Person;
getName(): string { return this.name; }
getPerson(): Person { return this.person; }
getMaybePerson(): Person | undefined { return this.maybePerson; }
}
@Pipe({
name: 'aPipe',
})
export class APipe {
transform(n: number): number { return n + 1; }
}
@Directive({
selector: '[aDir]',
exportAs: 'aDir'
})
export class ADirective {
name = 'ADirective';
}
`;
}
const QUICKSTART = {
'src/app.component.ts': appComponentSource(),
'src/app.component.html': '<h1>Hello {{name}}</h1>',
'src/app.module.ts': `
import { NgModule } from '@angular/core';
import { AppComponent, APipe, ADirective } from './app.component';
@NgModule({
declarations: [ AppComponent, APipe, ADirective ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
`
};
const LOWERING_QUICKSTART = {
'src/app.component.ts': appComponentSource(),
'src/app.component.html': '<h1>Hello {{name}}</h1>',
'src/app.module.ts': `
import { NgModule, Component } from '@angular/core';
import { AppComponent, APipe, ADirective } from './app.component';
class Foo {}
@Component({
template: '',
providers: [
{provide: 'someToken', useFactory: () => new Foo()}
]
})
export class Bar {}
@NgModule({
declarations: [ AppComponent, APipe, ADirective, Bar ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
`
};
function expectNoDiagnostics(diagnostics: ng.Diagnostics) {
if (diagnostics && diagnostics.length) {
throw new Error(ng.formatDiagnostics({}, diagnostics));
}
}

View File

@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, createHostComponentMeta, flatten, identifierName, sourceUrl, templateSourceUrl} from '../compile_metadata';
import {CompileDirectiveMetadata, CompileDirectiveSummary, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileNgModuleSummary, CompilePipeMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileSummaryKind, CompileTypeMetadata, CompileTypeSummary, componentFactoryName, flatten, identifierName, sourceUrl, templateSourceUrl} from '../compile_metadata';
import {CompilerConfig} from '../config';
import {ViewEncapsulation} from '../core';
import {MessageBundle} from '../i18n/message_bundle';
import {Identifiers, createTokenForExternalReference} from '../identifiers';
import {CompileMetadataResolver} from '../metadata_resolver';
@ -21,7 +22,7 @@ import {CompiledStylesheet, StyleCompiler} from '../style_compiler';
import {SummaryResolver} from '../summary_resolver';
import {TemplateAst} from '../template_parser/template_ast';
import {TemplateParser} from '../template_parser/template_parser';
import {OutputContext, syntaxError} from '../util';
import {OutputContext, ValueVisitor, syntaxError, visitValue} from '../util';
import {TypeCheckCompiler} from '../view_compiler/type_check_compiler';
import {ViewCompileResult, ViewCompiler} from '../view_compiler/view_compiler';
@ -33,6 +34,12 @@ import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolv
import {createForJitStub, serializeSummaries} from './summary_serializer';
import {ngfactoryFilePath, splitTypescriptSuffix, summaryFileName, summaryForJitFileName, summaryForJitName} from './util';
export enum StubEmitFlags {
Basic = 1 << 0,
TypeCheck = 1 << 1,
All = TypeCheck | Basic
}
export class AotCompiler {
private _templateAstCache =
new Map<StaticSymbol, {template: TemplateAst[], pipes: CompilePipeSummary[]}>();
@ -40,20 +47,20 @@ export class AotCompiler {
constructor(
private _config: CompilerConfig, private _host: AotCompilerHost,
private _reflector: StaticReflector, private _metadataResolver: CompileMetadataResolver,
private _htmlParser: HtmlParser, private _templateParser: TemplateParser,
private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler,
private _typeCheckCompiler: TypeCheckCompiler, private _ngModuleCompiler: NgModuleCompiler,
private _outputEmitter: OutputEmitter,
private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler,
private _viewCompiler: ViewCompiler, private _typeCheckCompiler: TypeCheckCompiler,
private _ngModuleCompiler: NgModuleCompiler, private _outputEmitter: OutputEmitter,
private _summaryResolver: SummaryResolver<StaticSymbol>, private _localeId: string|null,
private _translationFormat: string|null, private _enableSummariesForJit: boolean|null,
private _symbolResolver: StaticSymbolResolver) {}
private _translationFormat: string|null,
/** TODO(tbosch): remove this flag as it is always on in the new ngc */
private _enableSummariesForJit: boolean|null, private _symbolResolver: StaticSymbolResolver) {
}
clearCache() { this._metadataResolver.clearCache(); }
analyzeModulesSync(rootFiles: string[]): NgAnalyzedModules {
const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host);
const analyzeResult = analyzeAndValidateNgModules(
programSymbols, this._host, this._symbolResolver, this._metadataResolver);
rootFiles, this._host, this._symbolResolver, this._metadataResolver);
analyzeResult.ngModules.forEach(
ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
ngModule.type.reference, true));
@ -61,9 +68,8 @@ export class AotCompiler {
}
analyzeModulesAsync(rootFiles: string[]): Promise<NgAnalyzedModules> {
const programSymbols = extractProgramSymbols(this._symbolResolver, rootFiles, this._host);
const analyzeResult = analyzeAndValidateNgModules(
programSymbols, this._host, this._symbolResolver, this._metadataResolver);
rootFiles, this._host, this._symbolResolver, this._metadataResolver);
return Promise
.all(analyzeResult.ngModules.map(
ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
@ -71,21 +77,183 @@ export class AotCompiler {
.then(() => analyzeResult);
}
emitAllStubs(analyzeResult: NgAnalyzedModules): GeneratedFile[] {
const {files, ngModuleByPipeOrDirective} = analyzeResult;
const sourceModules = files.map(
file => this._compileStubFile(
file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules));
return flatten(sourceModules);
analyzeFile(fileName: string): NgAnalyzedFile {
return analyzeFile(this._host, this._symbolResolver, this._metadataResolver, fileName);
}
emitAllImpls(analyzeResult: NgAnalyzedModules): GeneratedFile[] {
const {ngModuleByPipeOrDirective, files} = analyzeResult;
const sourceModules = files.map(
file => this._compileImplFile(
file.srcUrl, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules,
file.injectables));
return flatten(sourceModules);
emitBasicStubs(file: NgAnalyzedFile): GeneratedFile[] {
return this._emitStubs(file, StubEmitFlags.Basic);
}
emitTypeCheckStubs(files: NgAnalyzedModules): GeneratedFile[] {
const generatedFiles: GeneratedFile[] = [];
files.files.forEach(
file => this._emitStubs(file, StubEmitFlags.TypeCheck)
.forEach(genFile => generatedFiles.push(genFile)));
return generatedFiles;
}
loadFilesAsync(files: NgAnalyzedFile[]): Promise<NgAnalyzedModules> {
const loadingPromises: Promise<NgAnalyzedModules>[] = [];
files.forEach(
file => file.ngModules.forEach(
ngModule =>
loadingPromises.push(this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
ngModule.type.reference, false))));
return Promise.all(loadingPromises).then(_ => mergeAndValidateNgFiles(files));
}
loadFilesSync(files: NgAnalyzedFile[]): NgAnalyzedModules {
files.forEach(
file => file.ngModules.forEach(
ngModule => this._metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
ngModule.type.reference, true)));
return mergeAndValidateNgFiles(files);
}
private _emitStubs(file: NgAnalyzedFile, emitFlags: StubEmitFlags): GeneratedFile[] {
return [
...this._createNgFactoryStub(file, emitFlags),
...this._createExternalStyleSheetNgFactoryStubs(file, emitFlags),
...this._createNgSummaryStub(file, emitFlags)
];
}
private _createNgFactoryStub(file: NgAnalyzedFile, emitFlags: StubEmitFlags): GeneratedFile[] {
const generatedFiles: GeneratedFile[] = [];
const outputCtx = this._createOutputContext(ngfactoryFilePath(file.fileName, true));
file.ngModules.forEach((ngModuleMeta, ngModuleIndex) => {
// Note: the code below needs to executed for StubEmitFlags.Basic and StubEmitFlags.TypeCheck,
// so we don't change the .ngfactory file too much when adding the typecheck block.
// create exports that user code can reference
this._ngModuleCompiler.createStub(outputCtx, ngModuleMeta.type.reference);
// add references to the symbols from the metadata.
// These can be used by the type check block for components,
// and they also cause TypeScript to include these files into the program too,
// which will make them part of the analyzedFiles.
const externalReferences: StaticSymbol[] = [
...ngModuleMeta.declaredDirectives.map(d => d.reference),
...ngModuleMeta.declaredPipes.map(d => d.reference),
...ngModuleMeta.importedModules.map(m => m.type.reference),
...ngModuleMeta.exportedModules.map(m => m.type.reference),
];
const externalReferenceVars = new Map<any, string>();
externalReferences.forEach((ref, typeIndex) => {
if (this._host.isSourceFile(ref.filePath)) {
externalReferenceVars.set(ref, `_decl${ngModuleIndex}_${typeIndex}`);
}
});
externalReferenceVars.forEach((varName, reference) => {
outputCtx.statements.push(
o.variable(varName)
.set(o.NULL_EXPR.cast(o.DYNAMIC_TYPE))
.toDeclStmt(o.expressionType(outputCtx.importExpr(reference))));
});
if (emitFlags & StubEmitFlags.TypeCheck) {
// add the typecheck block for all components of the NgModule
ngModuleMeta.declaredDirectives.forEach((dirId) => {
const compMeta = this._metadataResolver.getDirectiveMetadata(dirId.reference);
if (!compMeta.isComponent) {
return;
}
this._createTypeCheckBlock(
outputCtx, ngModuleMeta, this._metadataResolver.getHostComponentMetadata(compMeta),
[compMeta.type], externalReferenceVars);
this._createTypeCheckBlock(
outputCtx, ngModuleMeta, compMeta, ngModuleMeta.transitiveModule.directives,
externalReferenceVars);
});
}
});
// make sure we create a .ngfactory if we have a least one component
// in the file.
// Only do this for StubEmitFlags.Basic, as adding a type check block
// does not change this file (as we generate type check blocks based on NgModules).
if (outputCtx.statements.length === 0 && (emitFlags & StubEmitFlags.Basic) &&
file.directives.some(
dir => this._metadataResolver.getNonNormalizedDirectiveMetadata(
dir) !.metadata.isComponent)) {
_createEmptyStub(outputCtx);
}
// make sure we create a .ngfactory if we reexport a non source file.
// Only do this for StubEmitFlags.Basic, as adding a type check block
// does not change this file (as we generate type check blocks based on NgModules).
if (outputCtx.statements.length === 0 && (emitFlags & StubEmitFlags.Basic) &&
file.exportsNonSourceFiles) {
_createEmptyStub(outputCtx);
}
if (outputCtx.statements.length > 0) {
generatedFiles.push(this._codegenSourceModule(file.fileName, outputCtx));
}
return generatedFiles;
}
private _createExternalStyleSheetNgFactoryStubs(file: NgAnalyzedFile, emitFlags: StubEmitFlags):
GeneratedFile[] {
const generatedFiles: GeneratedFile[] = [];
if (!(emitFlags & StubEmitFlags.Basic)) {
// note: stylesheet stubs don't change when we produce type check stubs
return generatedFiles;
}
const fileSuffix = splitTypescriptSuffix(file.fileName, true)[1];
file.directives.forEach((dirSymbol) => {
const compMeta =
this._metadataResolver.getNonNormalizedDirectiveMetadata(dirSymbol) !.metadata;
if (!compMeta.isComponent) {
return;
}
// Note: compMeta is a component and therefore template is non null.
compMeta.template !.styleUrls.forEach((styleUrl) => {
const normalizedUrl = this._host.resourceNameToFileName(styleUrl, file.fileName);
if (!normalizedUrl) {
throw new Error(`Couldn't resolve resource ${styleUrl} relative to ${file.fileName}`);
}
const encapsulation =
compMeta.template !.encapsulation || this._config.defaultEncapsulation;
const outputCtx = this._createOutputContext(_stylesModuleUrl(
normalizedUrl, encapsulation === ViewEncapsulation.Emulated, fileSuffix));
_createEmptyStub(outputCtx);
generatedFiles.push(this._codegenSourceModule(normalizedUrl, outputCtx));
});
});
return generatedFiles;
}
private _createNgSummaryStub(file: NgAnalyzedFile, emitFlags: StubEmitFlags): GeneratedFile[] {
const generatedFiles: GeneratedFile[] = [];
// note: .ngsummary.js stubs don't change when we produce type check stubs
if (!this._enableSummariesForJit || !(emitFlags & StubEmitFlags.Basic)) {
return generatedFiles;
}
if (file.directives.length || file.injectables.length || file.ngModules.length ||
file.pipes.length || file.exportsNonSourceFiles) {
const outputCtx = this._createOutputContext(summaryForJitFileName(file.fileName, true));
file.ngModules.forEach(ngModule => {
// create exports that user code can reference
createForJitStub(outputCtx, ngModule.type.reference);
});
if (outputCtx.statements.length === 0) {
_createEmptyStub(outputCtx);
}
generatedFiles.push(this._codegenSourceModule(file.fileName, outputCtx));
}
return generatedFiles;
}
private _createTypeCheckBlock(
ctx: OutputContext, moduleMeta: CompileNgModuleMetadata, compMeta: CompileDirectiveMetadata,
directives: CompileIdentifierMetadata[], externalReferenceVars: Map<any, string>) {
const {template: parsedTemplate, pipes: usedPipes} =
this._parseTemplate(compMeta, moduleMeta, directives);
ctx.statements.push(...this._typeCheckCompiler.compileComponent(
compMeta, parsedTemplate, usedPipes, externalReferenceVars));
}
emitMessageBundle(analyzeResult: NgAnalyzedModules, locale: string|null): MessageBundle {
@ -107,7 +275,8 @@ export class AotCompiler {
const html = compMeta.template !.template !;
const interpolationConfig =
InterpolationConfig.fromArray(compMeta.template !.interpolation);
errors.push(...messageBundle.updateFromTemplate(html, file.srcUrl, interpolationConfig) !);
errors.push(
...messageBundle.updateFromTemplate(html, file.fileName, interpolationConfig) !);
});
});
@ -118,69 +287,18 @@ export class AotCompiler {
return messageBundle;
}
private _compileStubFile(
srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], pipes: StaticSymbol[],
ngModules: StaticSymbol[]): GeneratedFile[] {
const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1];
const generatedFiles: GeneratedFile[] = [];
const ngFactoryOutputCtx = this._createOutputContext(ngfactoryFilePath(srcFileUrl, true));
const jitSummaryOutputCtx = this._createOutputContext(summaryForJitFileName(srcFileUrl, true));
// create exports that user code can reference
ngModules.forEach((ngModuleReference) => {
this._ngModuleCompiler.createStub(ngFactoryOutputCtx, ngModuleReference);
createForJitStub(jitSummaryOutputCtx, ngModuleReference);
});
// create stubs for external stylesheets (always empty, as users should not import anything from
// the generated code)
directives.forEach((dirType) => {
const compMeta = this._metadataResolver.getDirectiveMetadata(<any>dirType);
if (!compMeta.isComponent) {
return;
}
const ngModule = ngModuleByPipeOrDirective.get(dirType);
if (!ngModule) {
throw new Error(
`Internal Error: cannot determine the module for component ${identifierName(compMeta.type)}!`);
}
this._compileComponentTypeCheckBlock(
ngFactoryOutputCtx, compMeta, ngModule, ngModule.transitiveModule.directives);
// Note: compMeta is a component and therefore template is non null.
compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => {
const styleContext = this._createOutputContext(_stylesModuleUrl(
stylesheetMeta.moduleUrl !, this._styleCompiler.needsStyleShim(compMeta), fileSuffix));
_createStub(styleContext);
generatedFiles.push(this._codegenSourceModule(stylesheetMeta.moduleUrl !, styleContext));
});
});
if (ngFactoryOutputCtx.statements.length <= 0) {
_createStub(ngFactoryOutputCtx);
}
if (jitSummaryOutputCtx.statements.length <= 0) {
_createStub(jitSummaryOutputCtx);
}
// 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;
emitAllImpls(analyzeResult: NgAnalyzedModules): GeneratedFile[] {
const {ngModuleByPipeOrDirective, files} = analyzeResult;
const sourceModules = files.map(
file => this._compileImplFile(
file.fileName, ngModuleByPipeOrDirective, file.directives, file.pipes, file.ngModules,
file.injectables));
return flatten(sourceModules);
}
private _compileImplFile(
srcFileUrl: string, ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>,
directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: StaticSymbol[],
directives: StaticSymbol[], pipes: StaticSymbol[], ngModules: CompileNgModuleMetadata[],
injectables: StaticSymbol[]): GeneratedFile[] {
const fileSuffix = splitTypescriptSuffix(srcFileUrl, true)[1];
const generatedFiles: GeneratedFile[] = [];
@ -191,7 +309,7 @@ export class AotCompiler {
...this._createSummary(srcFileUrl, directives, pipes, ngModules, injectables, outputCtx));
// compile all ng modules
ngModules.forEach((ngModuleType) => this._compileModule(outputCtx, ngModuleType));
ngModules.forEach((ngModuleMeta) => this._compileModule(outputCtx, ngModuleMeta));
// compile components
directives.forEach((dirType) => {
@ -228,7 +346,7 @@ export class AotCompiler {
private _createSummary(
srcFileName: string, directives: StaticSymbol[], pipes: StaticSymbol[],
ngModules: StaticSymbol[], injectables: StaticSymbol[],
ngModules: CompileNgModuleMetadata[], injectables: StaticSymbol[],
ngFactoryCtx: OutputContext): GeneratedFile[] {
const symbolSummaries = this._symbolResolver.getSymbolsOf(srcFileName)
.map(symbol => this._symbolResolver.resolveSymbol(symbol));
@ -238,10 +356,11 @@ export class AotCompiler {
CompileTypeMetadata
}[] =
[
...ngModules.map(ref => ({
summary: this._metadataResolver.getNgModuleSummary(ref) !,
metadata: this._metadataResolver.getNgModuleMetadata(ref) !
})),
...ngModules.map(
meta => ({
summary: this._metadataResolver.getNgModuleSummary(meta.type.reference) !,
metadata: this._metadataResolver.getNgModuleMetadata(meta.type.reference) !
})),
...directives.map(ref => ({
summary: this._metadataResolver.getDirectiveSummary(ref) !,
metadata: this._metadataResolver.getDirectiveMetadata(ref) !
@ -273,8 +392,7 @@ export class AotCompiler {
return [summaryJson];
}
private _compileModule(outputCtx: OutputContext, ngModuleType: StaticSymbol): void {
const ngModule = this._metadataResolver.getNgModuleMetadata(ngModuleType) !;
private _compileModule(outputCtx: OutputContext, ngModule: CompileNgModuleMetadata): void {
const providers: CompileProviderMetadata[] = [];
if (this._localeId) {
@ -298,10 +416,7 @@ export class AotCompiler {
private _compileComponentFactory(
outputCtx: OutputContext, compMeta: CompileDirectiveMetadata,
ngModule: CompileNgModuleMetadata, fileSuffix: string): void {
const hostType = this._metadataResolver.getHostComponentType(compMeta.type.reference);
const hostMeta = createHostComponentMeta(
hostType, compMeta, this._metadataResolver.getHostComponentViewClass(hostType),
this._htmlParser);
const hostMeta = this._metadataResolver.getHostComponentMetadata(compMeta);
const hostViewFactoryVar =
this._compileComponent(outputCtx, hostMeta, ngModule, [compMeta.type], null, fileSuffix)
.viewClassVar;
@ -336,26 +451,6 @@ export class AotCompiler {
[o.StmtModifier.Final, o.StmtModifier.Exported]));
}
private _parseTemplate(
compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata,
directiveIdentifiers: CompileIdentifierMetadata[]):
{template: TemplateAst[], pipes: CompilePipeSummary[]} {
let result = this._templateAstCache.get(compMeta.type.reference);
if (result) {
return result;
}
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
const directives =
directiveIdentifiers.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference));
const pipes = ngModule.transitiveModule.pipes.map(
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
result = this._templateParser.parse(
compMeta, compMeta.template !.htmlAst !, directives, pipes, ngModule.schemas,
templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces);
this._templateAstCache.set(compMeta.type.reference, result);
return result;
}
private _compileComponent(
outputCtx: OutputContext, compMeta: CompileDirectiveMetadata,
ngModule: CompileNgModuleMetadata, directiveIdentifiers: CompileIdentifierMetadata[],
@ -373,12 +468,23 @@ export class AotCompiler {
return viewResult;
}
private _compileComponentTypeCheckBlock(
outputCtx: OutputContext, compMeta: CompileDirectiveMetadata,
ngModule: CompileNgModuleMetadata, directiveIdentifiers: CompileIdentifierMetadata[]) {
const {template: parsedTemplate, pipes: usedPipes} =
this._parseTemplate(compMeta, ngModule, directiveIdentifiers);
this._typeCheckCompiler.compileComponent(outputCtx, compMeta, parsedTemplate, usedPipes);
private _parseTemplate(
compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata,
directiveIdentifiers: CompileIdentifierMetadata[]):
{template: TemplateAst[], pipes: CompilePipeSummary[]} {
if (this._templateAstCache.has(compMeta.type.reference)) {
return this._templateAstCache.get(compMeta.type.reference) !;
}
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
const directives =
directiveIdentifiers.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference));
const pipes = ngModule.transitiveModule.pipes.map(
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const result = this._templateParser.parse(
compMeta, compMeta.template !.htmlAst !, directives, pipes, ngModule.schemas,
templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces);
this._templateAstCache.set(compMeta.type.reference, result);
return result;
}
private _createOutputContext(genFilePath: string): OutputContext {
@ -433,13 +539,14 @@ export class AotCompiler {
}
}
function _createStub(outputCtx: OutputContext) {
function _createEmptyStub(outputCtx: OutputContext) {
// Note: We need to produce at least one import statement so that
// TypeScript knows that the file is an es6 module. Otherwise our generated
// exports / imports won't be emitted properly by TypeScript.
outputCtx.statements.push(o.importExpr(Identifiers.ComponentFactory).toStmt());
}
function _resolveStyleStatements(
symbolResolver: StaticSymbolResolver, compileResult: CompiledStylesheet, needsShim: boolean,
fileSuffix: string): void {
@ -456,180 +563,164 @@ function _stylesModuleUrl(stylesheetUrl: string, shim: boolean, suffix: string):
export interface NgAnalyzedModules {
ngModules: CompileNgModuleMetadata[];
ngModuleByPipeOrDirective: Map<StaticSymbol, CompileNgModuleMetadata>;
files: Array<{
srcUrl: string,
directives: StaticSymbol[],
pipes: StaticSymbol[],
ngModules: StaticSymbol[],
injectables: StaticSymbol[]
}>;
files: NgAnalyzedFile[];
symbolsMissingModule?: StaticSymbol[];
}
export interface NgAnalyzedFile {
fileName: string;
directives: StaticSymbol[];
pipes: StaticSymbol[];
ngModules: CompileNgModuleMetadata[];
injectables: StaticSymbol[];
exportsNonSourceFiles: boolean;
}
export interface NgAnalyzeModulesHost { isSourceFile(filePath: string): boolean; }
// Returns all the source files and a mapping from modules to directives
export function analyzeNgModules(
programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost,
staticSymbolResolver: StaticSymbolResolver,
fileNames: string[], host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver,
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
const programStaticSymbolsWithDecorators = programStaticSymbols.filter(
symbol => !symbol.filePath.endsWith('.d.ts') ||
staticSymbolResolver.hasDecorators(symbol.filePath));
const {ngModules, symbolsMissingModule} =
_createNgModules(programStaticSymbolsWithDecorators, host, metadataResolver);
return _analyzeNgModules(
programStaticSymbols, programStaticSymbolsWithDecorators, ngModules, symbolsMissingModule,
metadataResolver);
const files = _analyzeFilesIncludingNonProgramFiles(
fileNames, host, staticSymbolResolver, metadataResolver);
return mergeAnalyzedFiles(files);
}
export function analyzeAndValidateNgModules(
programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost,
staticSymbolResolver: StaticSymbolResolver,
fileNames: string[], host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver,
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
const result =
analyzeNgModules(programStaticSymbols, host, staticSymbolResolver, metadataResolver);
if (result.symbolsMissingModule && result.symbolsMissingModule.length) {
const messages = result.symbolsMissingModule.map(
return validateAnalyzedModules(
analyzeNgModules(fileNames, host, staticSymbolResolver, metadataResolver));
}
function validateAnalyzedModules(analyzedModules: NgAnalyzedModules): NgAnalyzedModules {
if (analyzedModules.symbolsMissingModule && analyzedModules.symbolsMissingModule.length) {
const messages = analyzedModules.symbolsMissingModule.map(
s =>
`Cannot determine the module for class ${s.name} in ${s.filePath}! Add ${s.name} to the NgModule to fix it.`);
throw syntaxError(messages.join('\n'));
}
return result;
return analyzedModules;
}
function _analyzeNgModules(
programSymbols: StaticSymbol[], programSymbolsWithDecorators: StaticSymbol[],
ngModuleMetas: CompileNgModuleMetadata[], symbolsMissingModule: StaticSymbol[],
metadataResolver: CompileMetadataResolver): NgAnalyzedModules {
const moduleMetasByRef = new Map<any, CompileNgModuleMetadata>();
ngModuleMetas.forEach((ngModule) => moduleMetasByRef.set(ngModule.type.reference, ngModule));
const ngModuleByPipeOrDirective = new Map<StaticSymbol, CompileNgModuleMetadata>();
const ngModulesByFile = new Map<string, StaticSymbol[]>();
const ngDirectivesByFile = new Map<string, StaticSymbol[]>();
const ngPipesByFile = new Map<string, StaticSymbol[]>();
const ngInjectablesByFile = new Map<string, StaticSymbol[]>();
const filePaths = new Set<string>();
// Analyzes all of the program files,
// including files that are not part of the program
// but are referenced by an NgModule.
function _analyzeFilesIncludingNonProgramFiles(
fileNames: string[], host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver,
metadataResolver: CompileMetadataResolver): NgAnalyzedFile[] {
const seenFiles = new Set<string>();
const files: NgAnalyzedFile[] = [];
// Make sure we produce an analyzed file for each input file
programSymbols.forEach((symbol) => {
const filePath = symbol.filePath;
filePaths.add(filePath);
});
programSymbolsWithDecorators.forEach((symbol) => {
if (metadataResolver.isInjectable(symbol)) {
const filePath = symbol.filePath;
ngInjectablesByFile.set(filePath, (ngInjectablesByFile.get(filePath) || []).concat(symbol));
const visitFile = (fileName: string) => {
if (seenFiles.has(fileName) || !host.isSourceFile(fileName)) {
return false;
}
});
// Looping over all modules to construct:
// - a map from file to modules `ngModulesByFile`,
// - a map from file to directives `ngDirectivesByFile`,
// - a map from file to pipes `ngPipesByFile`,
// - a map from directive/pipe to module `ngModuleByPipeOrDirective`.
ngModuleMetas.forEach((ngModuleMeta) => {
const srcFileUrl = ngModuleMeta.type.reference.filePath;
filePaths.add(srcFileUrl);
ngModulesByFile.set(
srcFileUrl, (ngModulesByFile.get(srcFileUrl) || []).concat(ngModuleMeta.type.reference));
ngModuleMeta.declaredDirectives.forEach((dirIdentifier) => {
const fileUrl = dirIdentifier.reference.filePath;
filePaths.add(fileUrl);
ngDirectivesByFile.set(
fileUrl, (ngDirectivesByFile.get(fileUrl) || []).concat(dirIdentifier.reference));
ngModuleByPipeOrDirective.set(dirIdentifier.reference, ngModuleMeta);
seenFiles.add(fileName);
const analyzedFile = analyzeFile(host, staticSymbolResolver, metadataResolver, fileName);
files.push(analyzedFile);
analyzedFile.ngModules.forEach(ngModule => {
ngModule.transitiveModule.modules.forEach(modMeta => visitFile(modMeta.reference.filePath));
});
ngModuleMeta.declaredPipes.forEach((pipeIdentifier) => {
const fileUrl = pipeIdentifier.reference.filePath;
filePaths.add(fileUrl);
ngPipesByFile.set(
fileUrl, (ngPipesByFile.get(fileUrl) || []).concat(pipeIdentifier.reference));
ngModuleByPipeOrDirective.set(pipeIdentifier.reference, ngModuleMeta);
});
});
const files: {
srcUrl: string,
directives: StaticSymbol[],
pipes: StaticSymbol[],
ngModules: StaticSymbol[],
injectables: StaticSymbol[]
}[] = [];
filePaths.forEach((srcUrl) => {
const directives = ngDirectivesByFile.get(srcUrl) || [];
const pipes = ngPipesByFile.get(srcUrl) || [];
const ngModules = ngModulesByFile.get(srcUrl) || [];
const injectables = ngInjectablesByFile.get(srcUrl) || [];
files.push({srcUrl, directives, pipes, ngModules, injectables});
});
return {
// map directive/pipe to module
ngModuleByPipeOrDirective,
// list modules and directives for every source file
files,
ngModules: ngModuleMetas, symbolsMissingModule
};
fileNames.forEach((fileName) => visitFile(fileName));
return files;
}
export function extractProgramSymbols(
staticSymbolResolver: StaticSymbolResolver, files: string[],
host: NgAnalyzeModulesHost): StaticSymbol[] {
const staticSymbols: StaticSymbol[] = [];
files.filter(fileName => host.isSourceFile(fileName)).forEach(sourceFile => {
staticSymbolResolver.getSymbolsOf(sourceFile).forEach((symbol) => {
export function analyzeFile(
host: NgAnalyzeModulesHost, staticSymbolResolver: StaticSymbolResolver,
metadataResolver: CompileMetadataResolver, fileName: string): NgAnalyzedFile {
const directives: StaticSymbol[] = [];
const pipes: StaticSymbol[] = [];
const injectables: StaticSymbol[] = [];
const ngModules: CompileNgModuleMetadata[] = [];
const hasDecorators = staticSymbolResolver.hasDecorators(fileName);
let exportsNonSourceFiles = false;
// Don't analyze .d.ts files that have no decorators as a shortcut
// to speed up the analysis. This prevents us from
// resolving the references in these files.
// Note: exportsNonSourceFiles is only needed when compiling with summaries,
// which is not the case when .d.ts files are treated as input files.
if (!fileName.endsWith('.d.ts') || hasDecorators) {
staticSymbolResolver.getSymbolsOf(fileName).forEach((symbol) => {
const resolvedSymbol = staticSymbolResolver.resolveSymbol(symbol);
const symbolMeta = resolvedSymbol.metadata;
if (symbolMeta) {
if (symbolMeta.__symbolic != 'error') {
// Ignore symbols that are only included to record error information.
staticSymbols.push(resolvedSymbol.symbol);
if (!symbolMeta || symbolMeta.__symbolic === 'error') {
return;
}
exportsNonSourceFiles =
exportsNonSourceFiles || isValueExportingNonSourceFile(host, symbolMeta);
if (symbolMeta.__symbolic === 'class') {
if (metadataResolver.isDirective(symbol)) {
directives.push(symbol);
} else if (metadataResolver.isPipe(symbol)) {
pipes.push(symbol);
} else if (metadataResolver.isInjectable(symbol)) {
injectables.push(symbol);
} else {
const ngModule = metadataResolver.getNgModuleMetadata(symbol, false);
if (ngModule) {
ngModules.push(ngModule);
}
}
}
});
});
return staticSymbols;
}
// Load the NgModules and check
// that all directives / pipes that are present in the program
// are also declared by a module.
function _createNgModules(
programStaticSymbols: StaticSymbol[], host: NgAnalyzeModulesHost,
metadataResolver: CompileMetadataResolver):
{ngModules: CompileNgModuleMetadata[], symbolsMissingModule: StaticSymbol[]} {
const ngModules = new Map<any, CompileNgModuleMetadata>();
const programPipesAndDirectives: StaticSymbol[] = [];
const ngModulePipesAndDirective = new Set<StaticSymbol>();
const addNgModule = (staticSymbol: any) => {
if (ngModules.has(staticSymbol) || !host.isSourceFile(staticSymbol.filePath)) {
return false;
}
const ngModule = metadataResolver.getNgModuleMetadata(staticSymbol, false);
if (ngModule) {
ngModules.set(ngModule.type.reference, ngModule);
ngModule.declaredDirectives.forEach((dir) => ngModulePipesAndDirective.add(dir.reference));
ngModule.declaredPipes.forEach((pipe) => ngModulePipesAndDirective.add(pipe.reference));
// For every input module add the list of transitively included modules
ngModule.transitiveModule.modules.forEach(modMeta => addNgModule(modMeta.reference));
}
return !!ngModule;
}
return {
fileName, directives, pipes, ngModules, injectables, exportsNonSourceFiles,
};
programStaticSymbols.forEach((staticSymbol) => {
if (!addNgModule(staticSymbol) &&
(metadataResolver.isDirective(staticSymbol) || metadataResolver.isPipe(staticSymbol))) {
programPipesAndDirectives.push(staticSymbol);
}
function isValueExportingNonSourceFile(host: NgAnalyzeModulesHost, metadata: any): boolean {
let exportsNonSourceFiles = false;
class Visitor implements ValueVisitor {
visitArray(arr: any[], context: any): any { arr.forEach(v => visitValue(v, this, context)); }
visitStringMap(map: {[key: string]: any}, context: any): any {
Object.keys(map).forEach((key) => visitValue(map[key], this, context));
}
visitPrimitive(value: any, context: any): any {}
visitOther(value: any, context: any): any {
if (value instanceof StaticSymbol && !host.isSourceFile(value.filePath)) {
exportsNonSourceFiles = true;
}
}
}
visitValue(metadata, new Visitor(), null);
return exportsNonSourceFiles;
}
export function mergeAnalyzedFiles(analyzedFiles: NgAnalyzedFile[]): NgAnalyzedModules {
const allNgModules: CompileNgModuleMetadata[] = [];
const ngModuleByPipeOrDirective = new Map<StaticSymbol, CompileNgModuleMetadata>();
const allPipesAndDirectives = new Set<StaticSymbol>();
analyzedFiles.forEach(af => {
af.ngModules.forEach(ngModule => {
allNgModules.push(ngModule);
ngModule.declaredDirectives.forEach(
d => ngModuleByPipeOrDirective.set(d.reference, ngModule));
ngModule.declaredPipes.forEach(p => ngModuleByPipeOrDirective.set(p.reference, ngModule));
});
af.directives.forEach(d => allPipesAndDirectives.add(d));
af.pipes.forEach(p => allPipesAndDirectives.add(p));
});
// Throw an error if any of the program pipe or directives is not declared by a module
const symbolsMissingModule =
programPipesAndDirectives.filter(s => !ngModulePipesAndDirective.has(s));
return {ngModules: Array.from(ngModules.values()), symbolsMissingModule};
const symbolsMissingModule: StaticSymbol[] = [];
allPipesAndDirectives.forEach(ref => {
if (!ngModuleByPipeOrDirective.has(ref)) {
symbolsMissingModule.push(ref);
}
});
return {
ngModules: allNgModules,
ngModuleByPipeOrDirective,
symbolsMissingModule,
files: analyzedFiles
};
}
function mergeAndValidateNgFiles(files: NgAnalyzedFile[]): NgAnalyzedModules {
return validateAnalyzedModules(mergeAnalyzedFiles(files));
}

View File

@ -77,17 +77,16 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom
const tmplParser = new TemplateParser(
config, staticReflector, expressionParser, elementSchemaRegistry, htmlParser, console, []);
const resolver = new CompileMetadataResolver(
config, new NgModuleResolver(staticReflector), new DirectiveResolver(staticReflector),
new PipeResolver(staticReflector), summaryResolver, elementSchemaRegistry, normalizer,
console, symbolCache, staticReflector);
config, htmlParser, new NgModuleResolver(staticReflector),
new DirectiveResolver(staticReflector), new PipeResolver(staticReflector), summaryResolver,
elementSchemaRegistry, normalizer, console, symbolCache, staticReflector);
// TODO(vicb): do not pass options.i18nFormat here
const viewCompiler = new ViewCompiler(config, staticReflector, elementSchemaRegistry);
const typeCheckCompiler = new TypeCheckCompiler(options, staticReflector);
const compiler = new AotCompiler(
config, compilerHost, staticReflector, resolver, htmlParser, tmplParser,
new StyleCompiler(urlResolver), viewCompiler, typeCheckCompiler,
new NgModuleCompiler(staticReflector), new TypeScriptEmitter(), summaryResolver,
options.locale || null, options.i18nFormat || null, options.enableSummariesForJit || null,
symbolResolver);
config, compilerHost, staticReflector, resolver, tmplParser, new StyleCompiler(urlResolver),
viewCompiler, typeCheckCompiler, new NgModuleCompiler(staticReflector),
new TypeScriptEmitter(), summaryResolver, options.locale || null, options.i18nFormat || null,
options.enableSummariesForJit || null, symbolResolver);
return {compiler, reflector: staticReflector};
}

View File

@ -252,7 +252,7 @@ export class StaticSymbolResolver {
/**
* hasDecorators checks a file's metadata for the presense of decorators without evalutating the
* metada.
* metadata.
*
* @param filePath the absolute path to examine for decorators.
* @returns true if any class in the file has a decorator.
@ -435,7 +435,8 @@ export class StaticSymbolResolver {
ResolvedStaticSymbol {
sourceSymbol.assertNoMembers();
targetSymbol.assertNoMembers();
if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath)) {
if (this.summaryResolver.isLibraryFile(sourceSymbol.filePath) &&
this.summaryResolver.isLibraryFile(targetSymbol.filePath)) {
// This case is for an ng library importing symbols from a plain ts library
// transitively.
// Note: We rely on the fact that we discover symbols in the direction

View File

@ -9,10 +9,7 @@
import {StaticSymbol} from './aot/static_symbol';
import {ChangeDetectionStrategy, SchemaMetadata, Type, ViewEncapsulation} from './core';
import {LifecycleHooks} from './lifecycle_reflector';
import * as html from './ml_parser/ast';
import {HtmlParser} from './ml_parser/html_parser';
import {ParseTreeResult as HtmlParseTreeResult} from './ml_parser/parser';
import {CssSelector} from './selector';
import {splitAtColon, stringify} from './util';
@ -505,51 +502,6 @@ export class CompileDirectiveMetadata {
}
}
/**
* Construct {@link CompileDirectiveMetadata} from {@link ComponentTypeMetadata} and a selector.
*/
export function createHostComponentMeta(
hostTypeReference: any, compMeta: CompileDirectiveMetadata,
hostViewType: StaticSymbol | ProxyClass, htmlParser: HtmlParser): CompileDirectiveMetadata {
const template = CssSelector.parse(compMeta.selector !)[0].getMatchingElementTemplate();
const templateUrl = '';
const htmlAst = htmlParser.parse(template, templateUrl);
return CompileDirectiveMetadata.create({
isHost: true,
type: {reference: hostTypeReference, diDeps: [], lifecycleHooks: []},
template: new CompileTemplateMetadata({
encapsulation: ViewEncapsulation.None,
template,
templateUrl,
htmlAst,
styles: [],
styleUrls: [],
ngContentSelectors: [],
animations: [],
isInline: true,
externalStylesheets: [],
interpolation: null,
preserveWhitespaces: false,
}),
exportAs: null,
changeDetection: ChangeDetectionStrategy.Default,
inputs: [],
outputs: [],
host: {},
isComponent: true,
selector: '*',
providers: [],
viewProviders: [],
queries: [],
viewQueries: [],
componentViewType: hostViewType,
rendererType:
{id: '__Host__', encapsulation: ViewEncapsulation.None, styles: [], data: {}} as object,
entryComponents: [],
componentFactory: null
});
}
export interface CompilePipeSummary extends CompileTypeSummary {
type: CompileTypeMetadata;
name: string;

View File

@ -63,7 +63,7 @@ export * from './ml_parser/html_tags';
export * from './ml_parser/interpolation_config';
export * from './ml_parser/tags';
export {NgModuleCompiler} from './ng_module_compiler';
export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement} from './output/output_ast';
export {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, collectExternalReferences} from './output/output_ast';
export {EmitterVisitorContext} from './output/abstract_emitter';
export * from './output/ts_emitter';
export * from './parse_util';

View File

@ -12,6 +12,7 @@ import {ViewEncapsulation} from './core';
import * as html from './ml_parser/ast';
import {HtmlParser} from './ml_parser/html_parser';
import {InterpolationConfig} from './ml_parser/interpolation_config';
import {ParseTreeResult as HtmlParseTreeResult} from './ml_parser/parser';
import {ResourceLoader} from './resource_loader';
import {extractStyleUrls, isStyleUrlResolvable} from './style_url_resolver';
import {PreparsedElementType, preparseElement} from './template_parser/template_preparser';
@ -88,12 +89,12 @@ export class DirectiveNormalizer {
}
return SyncAsync.then(
this.normalizeTemplateOnly(prenormData),
(result: CompileTemplateMetadata) => this.normalizeExternalStylesheets(result));
this._preParseTemplate(prenormData),
(preparsedTemplate) => this._normalizeTemplateMetadata(prenormData, preparsedTemplate));
}
normalizeTemplateOnly(prenomData: PrenormalizedTemplateMetadata):
SyncAsync<CompileTemplateMetadata> {
private _preParseTemplate(prenomData: PrenormalizedTemplateMetadata):
SyncAsync<PreparsedTemplate> {
let template: SyncAsync<string>;
let templateUrl: string;
if (prenomData.template != null) {
@ -104,12 +105,12 @@ export class DirectiveNormalizer {
template = this._fetch(templateUrl);
}
return SyncAsync.then(
template, (template) => this.normalizeLoadedTemplate(prenomData, template, templateUrl));
template, (template) => this._preparseLoadedTemplate(prenomData, template, templateUrl));
}
normalizeLoadedTemplate(
private _preparseLoadedTemplate(
prenormData: PrenormalizedTemplateMetadata, template: string,
templateAbsUrl: string): CompileTemplateMetadata {
templateAbsUrl: string): PreparsedTemplate {
const isInline = !!prenormData.template;
const interpolationConfig = InterpolationConfig.fromArray(prenormData.interpolation !);
const rootNodesAndErrors = this._htmlParser.parse(
@ -123,69 +124,98 @@ export class DirectiveNormalizer {
throw syntaxError(`Template parse errors:\n${errorString}`);
}
const templateMetadataStyles = this.normalizeStylesheet(new CompileStylesheetMetadata({
styles: prenormData.styles,
styleUrls: prenormData.styleUrls,
moduleUrl: prenormData.moduleUrl
}));
const templateMetadataStyles = this._normalizeStylesheet(new CompileStylesheetMetadata(
{styles: prenormData.styles, moduleUrl: prenormData.moduleUrl}));
const visitor = new TemplatePreparseVisitor();
html.visitAll(visitor, rootNodesAndErrors.rootNodes);
const templateStyles = this.normalizeStylesheet(new CompileStylesheetMetadata(
const templateStyles = this._normalizeStylesheet(new CompileStylesheetMetadata(
{styles: visitor.styles, styleUrls: visitor.styleUrls, moduleUrl: templateAbsUrl}));
const styles = templateMetadataStyles.styles.concat(templateStyles.styles);
const inlineStyleUrls = templateMetadataStyles.styleUrls.concat(templateStyles.styleUrls);
const styleUrls = this
._normalizeStylesheet(new CompileStylesheetMetadata(
{styleUrls: prenormData.styleUrls, moduleUrl: prenormData.moduleUrl}))
.styleUrls;
return {
template,
templateUrl: templateAbsUrl, isInline,
htmlAst: rootNodesAndErrors, styles, inlineStyleUrls, styleUrls,
ngContentSelectors: visitor.ngContentSelectors,
};
}
private _normalizeTemplateMetadata(
prenormData: PrenormalizedTemplateMetadata,
preparsedTemplate: PreparsedTemplate): SyncAsync<CompileTemplateMetadata> {
return SyncAsync.then(
this._loadMissingExternalStylesheets(
preparsedTemplate.styleUrls.concat(preparsedTemplate.inlineStyleUrls)),
(externalStylesheets) => this._normalizeLoadedTemplateMetadata(
prenormData, preparsedTemplate, externalStylesheets));
}
private _normalizeLoadedTemplateMetadata(
prenormData: PrenormalizedTemplateMetadata, preparsedTemplate: PreparsedTemplate,
stylesheets: Map<string, CompileStylesheetMetadata>): CompileTemplateMetadata {
// Algorithm:
// - produce exactly 1 entry per original styleUrl in
// CompileTemplateMetadata.externalStylesheets whith all styles inlined
// - inline all styles that are referenced by the template into CompileTemplateMetadata.styles.
// Reason: be able to determine how many stylesheets there are even without loading
// the template nor the stylesheets, so we can create a stub for TypeScript always synchronously
// (as resouce loading may be async)
const styles = [...preparsedTemplate.styles];
this._inlineStyles(preparsedTemplate.inlineStyleUrls, stylesheets, styles);
const styleUrls = preparsedTemplate.styleUrls;
const externalStylesheets = styleUrls.map(styleUrl => {
const stylesheet = stylesheets.get(styleUrl) !;
const styles = [...stylesheet.styles];
this._inlineStyles(stylesheet.styleUrls, stylesheets, styles);
return new CompileStylesheetMetadata({moduleUrl: styleUrl, styles: styles});
});
let encapsulation = prenormData.encapsulation;
if (encapsulation == null) {
encapsulation = this._config.defaultEncapsulation;
}
const styles = templateMetadataStyles.styles.concat(templateStyles.styles);
const styleUrls = templateMetadataStyles.styleUrls.concat(templateStyles.styleUrls);
if (encapsulation === ViewEncapsulation.Emulated && styles.length === 0 &&
styleUrls.length === 0) {
encapsulation = ViewEncapsulation.None;
}
return new CompileTemplateMetadata({
encapsulation,
template,
templateUrl: templateAbsUrl,
htmlAst: rootNodesAndErrors, styles, styleUrls,
ngContentSelectors: visitor.ngContentSelectors,
template: preparsedTemplate.template,
templateUrl: preparsedTemplate.templateUrl,
htmlAst: preparsedTemplate.htmlAst, styles, styleUrls,
ngContentSelectors: preparsedTemplate.ngContentSelectors,
animations: prenormData.animations,
interpolation: prenormData.interpolation, isInline,
externalStylesheets: [],
interpolation: prenormData.interpolation,
isInline: preparsedTemplate.isInline, externalStylesheets,
preserveWhitespaces: preserveWhitespacesDefault(
prenormData.preserveWhitespaces, this._config.preserveWhitespaces),
});
}
normalizeExternalStylesheets(templateMeta: CompileTemplateMetadata):
SyncAsync<CompileTemplateMetadata> {
return SyncAsync.then(
this._loadMissingExternalStylesheets(templateMeta.styleUrls),
(externalStylesheets) => new CompileTemplateMetadata({
encapsulation: templateMeta.encapsulation,
template: templateMeta.template,
templateUrl: templateMeta.templateUrl,
htmlAst: templateMeta.htmlAst,
styles: templateMeta.styles,
styleUrls: templateMeta.styleUrls,
externalStylesheets: externalStylesheets,
ngContentSelectors: templateMeta.ngContentSelectors,
animations: templateMeta.animations,
interpolation: templateMeta.interpolation,
isInline: templateMeta.isInline,
preserveWhitespaces: templateMeta.preserveWhitespaces,
}));
private _inlineStyles(
styleUrls: string[], stylesheets: Map<string, CompileStylesheetMetadata>,
targetStyles: string[]) {
styleUrls.forEach(styleUrl => {
const stylesheet = stylesheets.get(styleUrl) !;
stylesheet.styles.forEach(style => targetStyles.push(style));
this._inlineStyles(stylesheet.styleUrls, stylesheets, targetStyles);
});
}
private _loadMissingExternalStylesheets(
styleUrls: string[],
loadedStylesheets:
Map<string, CompileStylesheetMetadata> = new Map<string, CompileStylesheetMetadata>()):
SyncAsync<CompileStylesheetMetadata[]> {
SyncAsync<Map<string, CompileStylesheetMetadata>> {
return SyncAsync.then(
SyncAsync.all(styleUrls.filter((styleUrl) => !loadedStylesheets.has(styleUrl))
.map(
@ -193,16 +223,16 @@ export class DirectiveNormalizer {
this._fetch(styleUrl),
(loadedStyle) => {
const stylesheet =
this.normalizeStylesheet(new CompileStylesheetMetadata(
this._normalizeStylesheet(new CompileStylesheetMetadata(
{styles: [loadedStyle], moduleUrl: styleUrl}));
loadedStylesheets.set(styleUrl, stylesheet);
return this._loadMissingExternalStylesheets(
stylesheet.styleUrls, loadedStylesheets);
}))),
(_) => Array.from(loadedStylesheets.values()));
(_) => loadedStylesheets);
}
normalizeStylesheet(stylesheet: CompileStylesheetMetadata): CompileStylesheetMetadata {
private _normalizeStylesheet(stylesheet: CompileStylesheetMetadata): CompileStylesheetMetadata {
const moduleUrl = stylesheet.moduleUrl !;
const allStyleUrls = stylesheet.styleUrls.filter(isStyleUrlResolvable)
.map(url => this._urlResolver.resolve(moduleUrl, url));
@ -218,6 +248,17 @@ export class DirectiveNormalizer {
}
}
interface PreparsedTemplate {
template: string;
templateUrl: string;
isInline: boolean;
htmlAst: HtmlParseTreeResult;
styles: string[];
inlineStyleUrls: string[];
styleUrls: string[];
ngContentSelectors: string[];
}
class TemplatePreparseVisitor implements html.Visitor {
ngContentSelectors: string[] = [];
styles: string[] = [];

View File

@ -10,7 +10,7 @@
/**
* Extract i18n messages from source code
*/
import {analyzeAndValidateNgModules, extractProgramSymbols} from '../aot/compiler';
import {analyzeAndValidateNgModules} from '../aot/compiler';
import {createAotUrlResolver} from '../aot/compiler_factory';
import {StaticReflector} from '../aot/static_reflector';
import {StaticSymbolCache} from '../aot/static_symbol';
@ -56,9 +56,8 @@ export class Extractor {
private messageBundle: MessageBundle, private metadataResolver: CompileMetadataResolver) {}
extract(rootFiles: string[]): Promise<MessageBundle> {
const programSymbols = extractProgramSymbols(this.staticSymbolResolver, rootFiles, this.host);
const {files, ngModules} = analyzeAndValidateNgModules(
programSymbols, this.host, this.staticSymbolResolver, this.metadataResolver);
rootFiles, this.host, this.staticSymbolResolver, this.metadataResolver);
return Promise
.all(ngModules.map(
ngModule => this.metadataResolver.loadNgModuleDirectiveAndPipeMetadata(
@ -79,7 +78,7 @@ export class Extractor {
const interpolationConfig =
InterpolationConfig.fromArray(compMeta.template !.interpolation);
errors.push(...this.messageBundle.updateFromTemplate(
html, file.srcUrl, interpolationConfig) !);
html, file.fileName, interpolationConfig) !);
});
});
@ -108,9 +107,9 @@ export class Extractor {
{get: (url: string) => host.loadResource(url)}, urlResolver, htmlParser, config);
const elementSchemaRegistry = new DomElementSchemaRegistry();
const resolver = new CompileMetadataResolver(
config, new NgModuleResolver(staticReflector), new DirectiveResolver(staticReflector),
new PipeResolver(staticReflector), summaryResolver, elementSchemaRegistry, normalizer,
console, symbolCache, staticReflector);
config, htmlParser, new NgModuleResolver(staticReflector),
new DirectiveResolver(staticReflector), new PipeResolver(staticReflector), summaryResolver,
elementSchemaRegistry, normalizer, console, symbolCache, staticReflector);
// TODO(vicb): implicit tags & attributes
const messageBundle = new MessageBundle(htmlParser, [], {}, locale);

View File

@ -6,18 +6,18 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileProviderMetadata, CompileStylesheetMetadata, CompileTypeSummary, ProviderMeta, ProxyClass, createHostComponentMeta, identifierName, ngModuleJitUrl, sharedStylesheetJitUrl, templateJitUrl, templateSourceUrl} from '../compile_metadata';
import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompilePipeSummary, CompileProviderMetadata, CompileStylesheetMetadata, CompileTypeSummary, ProviderMeta, ProxyClass, identifierName, ngModuleJitUrl, sharedStylesheetJitUrl, templateJitUrl, templateSourceUrl} from '../compile_metadata';
import {CompileReflector} from '../compile_reflector';
import {CompilerConfig} from '../config';
import {Type} from '../core';
import {CompileMetadataResolver} from '../metadata_resolver';
import {HtmlParser} from '../ml_parser/html_parser';
import {NgModuleCompiler} from '../ng_module_compiler';
import * as ir from '../output/output_ast';
import {interpretStatements} from '../output/output_interpreter';
import {jitStatements} from '../output/output_jit';
import {CompiledStylesheet, StyleCompiler} from '../style_compiler';
import {SummaryResolver} from '../summary_resolver';
import {TemplateAst} from '../template_parser/template_ast';
import {TemplateParser} from '../template_parser/template_parser';
import {Console, OutputContext, SyncAsync, stringify} from '../util';
import {ViewCompiler} from '../view_compiler/view_compiler';
@ -44,11 +44,11 @@ export class JitCompiler {
private _sharedStylesheetCount = 0;
constructor(
private _metadataResolver: CompileMetadataResolver, private _htmlParser: HtmlParser,
private _templateParser: TemplateParser, private _styleCompiler: StyleCompiler,
private _viewCompiler: ViewCompiler, private _ngModuleCompiler: NgModuleCompiler,
private _summaryResolver: SummaryResolver<Type>, private _reflector: CompileReflector,
private _compilerConfig: CompilerConfig, private _console: Console,
private _metadataResolver: CompileMetadataResolver, private _templateParser: TemplateParser,
private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler,
private _ngModuleCompiler: NgModuleCompiler, private _summaryResolver: SummaryResolver<Type>,
private _reflector: CompileReflector, private _compilerConfig: CompilerConfig,
private _console: Console,
private getExtraNgModuleProviders: (ngModule: any) => CompileProviderMetadata[]) {}
compileModuleSync(moduleType: Type): object {
@ -227,9 +227,8 @@ export class JitCompiler {
const compMeta = this._metadataResolver.getDirectiveMetadata(compType);
assertComponent(compMeta);
const hostClass = this._metadataResolver.getHostComponentType(compType);
const hostMeta = createHostComponentMeta(
hostClass, compMeta, (compMeta.componentFactory as any).viewDefFactory, this._htmlParser);
const hostMeta = this._metadataResolver.getHostComponentMetadata(
compMeta, (compMeta.componentFactory as any).viewDefFactory);
compiledTemplate =
new CompiledTemplate(true, compMeta.type, hostMeta, ngModule, [compMeta.type]);
this._compiledHostTemplateCache.set(compType, compiledTemplate);
@ -257,21 +256,16 @@ export class JitCompiler {
const externalStylesheetsByModuleUrl = new Map<string, CompiledStylesheet>();
const outputContext = createOutputContext();
const componentStylesheet = this._styleCompiler.compileComponent(outputContext, compMeta);
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => {
const compiledStylesheet =
this._styleCompiler.compileStyles(createOutputContext(), compMeta, stylesheetMeta);
externalStylesheetsByModuleUrl.set(stylesheetMeta.moduleUrl !, compiledStylesheet);
});
this._resolveStylesCompileResult(componentStylesheet, externalStylesheetsByModuleUrl);
const directives =
template.directives.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference));
const pipes = template.ngModule.transitiveModule.pipes.map(
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template !.template !, directives, pipes, template.ngModule.schemas,
templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !),
preserveWhitespaces);
const {template: parsedTemplate, pipes: usedPipes} =
this._parseTemplate(compMeta, template.ngModule, template.directives);
const compileResult = this._viewCompiler.compileComponent(
outputContext, compMeta, parsedTemplate, ir.variable(componentStylesheet.stylesVar),
usedPipes);
@ -282,6 +276,21 @@ export class JitCompiler {
template.compiled(viewClass, rendererType);
}
private _parseTemplate(
compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata,
directiveIdentifiers: CompileIdentifierMetadata[]):
{template: TemplateAst[], pipes: CompilePipeSummary[]} {
// Note: ! is ok here as components always have a template.
const preserveWhitespaces = compMeta.template !.preserveWhitespaces;
const directives =
directiveIdentifiers.map(dir => this._metadataResolver.getDirectiveSummary(dir.reference));
const pipes = ngModule.transitiveModule.pipes.map(
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
return this._templateParser.parse(
compMeta, compMeta.template !.htmlAst !, directives, pipes, ngModule.schemas,
templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces);
}
private _resolveStylesCompileResult(
result: CompiledStylesheet, externalStylesheetsByModuleUrl: Map<string, CompiledStylesheet>) {
result.dependencies.forEach((dep, i) => {

View File

@ -12,14 +12,16 @@ import {assertArrayOfStrings, assertInterpolationSymbols} from './assertions';
import * as cpl from './compile_metadata';
import {CompileReflector} from './compile_reflector';
import {CompilerConfig} from './config';
import {ChangeDetectionStrategy, Component, Directive, ModuleWithProviders, Provider, Query, SchemaMetadata, Type, createAttribute, createComponent, createHost, createInject, createInjectable, createInjectionToken, createOptional, createSelf, createSkipSelf} from './core';
import {ChangeDetectionStrategy, Component, Directive, ModuleWithProviders, Provider, Query, SchemaMetadata, Type, ViewEncapsulation, createAttribute, createComponent, createHost, createInject, createInjectable, createInjectionToken, createOptional, createSelf, createSkipSelf} from './core';
import {DirectiveNormalizer} from './directive_normalizer';
import {DirectiveResolver} from './directive_resolver';
import {Identifiers} from './identifiers';
import {getAllLifecycleHooks} from './lifecycle_reflector';
import {HtmlParser} from './ml_parser/html_parser';
import {NgModuleResolver} from './ng_module_resolver';
import {PipeResolver} from './pipe_resolver';
import {ElementSchemaRegistry} from './schema/element_schema_registry';
import {CssSelector} from './selector';
import {SummaryResolver} from './summary_resolver';
import {Console, SyncAsync, ValueTransformer, isPromise, noUndefined, resolveForwardRef, stringify, syntaxError, visitValue} from './util';
@ -44,9 +46,9 @@ export class CompileMetadataResolver {
private _ngModuleOfTypes = new Map<Type, Type>();
constructor(
private _config: CompilerConfig, private _ngModuleResolver: NgModuleResolver,
private _directiveResolver: DirectiveResolver, private _pipeResolver: PipeResolver,
private _summaryResolver: SummaryResolver<any>,
private _config: CompilerConfig, private _htmlParser: HtmlParser,
private _ngModuleResolver: NgModuleResolver, private _directiveResolver: DirectiveResolver,
private _pipeResolver: PipeResolver, private _summaryResolver: SummaryResolver<any>,
private _schemaRegistry: ElementSchemaRegistry,
private _directiveNormalizer: DirectiveNormalizer, private _console: Console,
private _staticSymbolCache: StaticSymbolCache, private _reflector: CompileReflector,
@ -167,6 +169,54 @@ export class CompileMetadataResolver {
return typeSummary && typeSummary.summaryKind === kind ? typeSummary : null;
}
getHostComponentMetadata(
compMeta: cpl.CompileDirectiveMetadata,
hostViewType?: StaticSymbol|cpl.ProxyClass): cpl.CompileDirectiveMetadata {
const hostType = this.getHostComponentType(compMeta.type.reference);
if (!hostViewType) {
hostViewType = this.getHostComponentViewClass(hostType);
}
// Note: ! is ok here as this method should only be called with normalized directive
// metadata, which always fills in the selector.
const template = CssSelector.parse(compMeta.selector !)[0].getMatchingElementTemplate();
const templateUrl = '';
const htmlAst = this._htmlParser.parse(template, templateUrl);
return cpl.CompileDirectiveMetadata.create({
isHost: true,
type: {reference: hostType, diDeps: [], lifecycleHooks: []},
template: new cpl.CompileTemplateMetadata({
encapsulation: ViewEncapsulation.None,
template,
templateUrl,
htmlAst,
styles: [],
styleUrls: [],
ngContentSelectors: [],
animations: [],
isInline: true,
externalStylesheets: [],
interpolation: null,
preserveWhitespaces: false,
}),
exportAs: null,
changeDetection: ChangeDetectionStrategy.Default,
inputs: [],
outputs: [],
host: {},
isComponent: true,
selector: '*',
providers: [],
viewProviders: [],
queries: [],
viewQueries: [],
componentViewType: hostViewType,
rendererType:
{id: '__Host__', encapsulation: ViewEncapsulation.None, styles: [], data: {}} as object,
entryComponents: [],
componentFactory: null
});
}
loadDirectiveMetadata(ngModuleType: any, directiveType: any, isSync: boolean): SyncAsync<null> {
if (this._directiveCache.has(directiveType)) {
return null;

View File

@ -924,85 +924,109 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor {
export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor {
visitReadVarExpr(ast: ReadVarExpr, context: any): any { return ast; }
visitWriteVarExpr(expr: WriteVarExpr, context: any): any {
expr.value.visitExpression(this, context);
return expr;
visitType(ast: Type, context: any): any { return ast; }
visitExpression(ast: Expression, context: any): any {
if (ast.type) {
ast.type.visitType(this, context);
}
return ast;
}
visitWriteKeyExpr(expr: WriteKeyExpr, context: any): any {
expr.receiver.visitExpression(this, context);
expr.index.visitExpression(this, context);
expr.value.visitExpression(this, context);
return expr;
visitBuiltintType(type: BuiltinType, context: any): any { return this.visitType(type, context); }
visitExpressionType(type: ExpressionType, context: any): any {
type.value.visitExpression(this, context);
return this.visitType(type, context);
}
visitWritePropExpr(expr: WritePropExpr, context: any): any {
expr.receiver.visitExpression(this, context);
expr.value.visitExpression(this, context);
return expr;
visitArrayType(type: ArrayType, context: any): any { return this.visitType(type, context); }
visitMapType(type: MapType, context: any): any { return this.visitType(type, context); }
visitReadVarExpr(ast: ReadVarExpr, context: any): any {
return this.visitExpression(ast, context);
}
visitWriteVarExpr(ast: WriteVarExpr, context: any): any {
ast.value.visitExpression(this, context);
return this.visitExpression(ast, context);
}
visitWriteKeyExpr(ast: WriteKeyExpr, context: any): any {
ast.receiver.visitExpression(this, context);
ast.index.visitExpression(this, context);
ast.value.visitExpression(this, context);
return this.visitExpression(ast, context);
}
visitWritePropExpr(ast: WritePropExpr, context: any): any {
ast.receiver.visitExpression(this, context);
ast.value.visitExpression(this, context);
return this.visitExpression(ast, context);
}
visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): any {
ast.receiver.visitExpression(this, context);
this.visitAllExpressions(ast.args, context);
return ast;
return this.visitExpression(ast, context);
}
visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any {
ast.fn.visitExpression(this, context);
this.visitAllExpressions(ast.args, context);
return ast;
return this.visitExpression(ast, context);
}
visitInstantiateExpr(ast: InstantiateExpr, context: any): any {
ast.classExpr.visitExpression(this, context);
this.visitAllExpressions(ast.args, context);
return ast;
return this.visitExpression(ast, context);
}
visitLiteralExpr(ast: LiteralExpr, context: any): any {
return this.visitExpression(ast, context);
}
visitExternalExpr(ast: ExternalExpr, context: any): any {
if (ast.typeParams) {
ast.typeParams.forEach(type => type.visitType(this, context));
}
return this.visitExpression(ast, context);
}
visitLiteralExpr(ast: LiteralExpr, context: any): any { return ast; }
visitExternalExpr(ast: ExternalExpr, context: any): any { return ast; }
visitConditionalExpr(ast: ConditionalExpr, context: any): any {
ast.condition.visitExpression(this, context);
ast.trueCase.visitExpression(this, context);
ast.falseCase !.visitExpression(this, context);
return ast;
return this.visitExpression(ast, context);
}
visitNotExpr(ast: NotExpr, context: any): any {
ast.condition.visitExpression(this, context);
return ast;
return this.visitExpression(ast, context);
}
visitAssertNotNullExpr(ast: AssertNotNull, context: any): any {
ast.condition.visitExpression(this, context);
return ast;
return this.visitExpression(ast, context);
}
visitCastExpr(ast: CastExpr, context: any): any {
ast.value.visitExpression(this, context);
return ast;
return this.visitExpression(ast, context);
}
visitFunctionExpr(ast: FunctionExpr, context: any): any {
this.visitAllStatements(ast.statements, context);
return ast;
return this.visitExpression(ast, context);
}
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any {
ast.lhs.visitExpression(this, context);
ast.rhs.visitExpression(this, context);
return ast;
return this.visitExpression(ast, context);
}
visitReadPropExpr(ast: ReadPropExpr, context: any): any {
ast.receiver.visitExpression(this, context);
return ast;
return this.visitExpression(ast, context);
}
visitReadKeyExpr(ast: ReadKeyExpr, context: any): any {
ast.receiver.visitExpression(this, context);
ast.index.visitExpression(this, context);
return ast;
return this.visitExpression(ast, context);
}
visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): any {
this.visitAllExpressions(ast.entries, context);
return ast;
return this.visitExpression(ast, context);
}
visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any {
ast.entries.forEach((entry) => entry.value.visitExpression(this, context));
return ast;
return this.visitExpression(ast, context);
}
visitCommaExpr(ast: CommaExpr, context: any): any {
this.visitAllExpressions(ast.parts, context);
return this.visitExpression(ast, context);
}
visitAllExpressions(exprs: Expression[], context: any): void {
exprs.forEach(expr => expr.visitExpression(this, context));
@ -1010,10 +1034,16 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor
visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): any {
stmt.value.visitExpression(this, context);
if (stmt.type) {
stmt.type.visitType(this, context);
}
return stmt;
}
visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): any {
this.visitAllStatements(stmt.statements, context);
if (stmt.type) {
stmt.type.visitType(this, context);
}
return stmt;
}
visitExpressionStmt(stmt: ExpressionStatement, context: any): any {
@ -1078,6 +1108,20 @@ class _ReadVarVisitor extends RecursiveAstVisitor {
}
}
export function collectExternalReferences(stmts: Statement[]): ExternalReference[] {
const visitor = new _FindExternalReferencesVisitor();
visitor.visitAllStatements(stmts, null);
return visitor.externalReferences;
}
class _FindExternalReferencesVisitor extends RecursiveAstVisitor {
externalReferences: ExternalReference[] = [];
visitExternalExpr(e: ExternalExpr, context: any) {
this.externalReferences.push(e.value);
return super.visitExternalExpr(e, context);
}
}
export function applySourceSpanToStatementIfNeeded(
stmt: Statement, sourceSpan: ParseSourceSpan | null): Statement {
if (!sourceSpan) {

View File

@ -17,8 +17,6 @@ import * as o from '../output/output_ast';
import {convertValueToOutputAst} from '../output/value_util';
import {ParseSourceSpan} from '../parse_util';
import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, ProviderAstType, QueryMatch, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast';
import {OutputContext} from '../util';
/**
* Generates code that is used to type check templates.
@ -26,23 +24,31 @@ import {OutputContext} from '../util';
export class TypeCheckCompiler {
constructor(private options: AotCompilerOptions, private reflector: StaticReflector) {}
/**
* Important notes:
* - This must not produce new `import` statements, but only refer to types outside
* of the file via the variables provided via externalReferenceVars.
* This allows Typescript to reuse the old program's structure as no imports have changed.
* - This must not produce any exports, as this would pollute the .d.ts file
* and also violate the point above.
*/
compileComponent(
outputCtx: OutputContext, component: CompileDirectiveMetadata, template: TemplateAst[],
usedPipes: CompilePipeSummary[]): void {
component: CompileDirectiveMetadata, template: TemplateAst[], usedPipes: CompilePipeSummary[],
externalReferenceVars: Map<StaticSymbol, string>): o.Statement[] {
const pipes = new Map<string, StaticSymbol>();
usedPipes.forEach(p => pipes.set(p.name, p.type.reference));
let embeddedViewCount = 0;
const viewBuilderFactory = (parent: ViewBuilder | null): ViewBuilder => {
const embeddedViewIndex = embeddedViewCount++;
return new ViewBuilder(
this.options, this.reflector, outputCtx, parent, component.type.reference,
embeddedViewIndex, pipes, viewBuilderFactory);
this.options, this.reflector, externalReferenceVars, parent, component.type.reference,
component.isHost, embeddedViewIndex, pipes, viewBuilderFactory);
};
const visitor = viewBuilderFactory(null);
visitor.visitAll([], template);
outputCtx.statements.push(...visitor.build());
return visitor.build();
}
}
@ -60,9 +66,9 @@ interface Expression {
value: AST;
}
const DYNAMIC_VAR_NAME = '_any';
class ViewBuilder implements TemplateAstVisitor, LocalResolver {
private outputVarTypes = new Map<string, OutputVarType>();
private outputVarNames = new Map<OutputVarType, string>();
private refOutputVars = new Map<string, OutputVarType>();
private variables: VariableAst[] = [];
private children: ViewBuilder[] = [];
@ -71,16 +77,23 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
constructor(
private options: AotCompilerOptions, private reflector: StaticReflector,
private outputCtx: OutputContext, private parent: ViewBuilder|null,
private component: StaticSymbol, private embeddedViewIndex: number,
private pipes: Map<string, StaticSymbol>, private viewBuilderFactory: ViewBuilderFactory) {}
private externalReferenceVars: Map<StaticSymbol, string>, private parent: ViewBuilder|null,
private component: StaticSymbol, private isHostComponent: boolean,
private embeddedViewIndex: number, private pipes: Map<string, StaticSymbol>,
private viewBuilderFactory: ViewBuilderFactory) {}
private getOrAddOutputVar(type: o.BuiltinTypeName|StaticSymbol): string {
let varName = this.outputVarNames.get(type);
private getOutputVar(type: o.BuiltinTypeName|StaticSymbol): string {
let varName: string|undefined;
if (type === this.component && this.isHostComponent) {
varName = DYNAMIC_VAR_NAME;
} else if (type instanceof StaticSymbol) {
varName = this.externalReferenceVars.get(type);
} else {
varName = DYNAMIC_VAR_NAME;
}
if (!varName) {
varName = `_v${this.outputVarNames.size}`;
this.outputVarNames.set(type, varName);
this.outputVarTypes.set(varName, type);
throw new Error(
`Illegal State: referring to a type without a variable ${JSON.stringify(type)}`);
}
return varName;
}
@ -92,15 +105,15 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
build(targetStatements: o.Statement[] = []): o.Statement[] {
this.children.forEach((child) => child.build(targetStatements));
const viewStmts: o.Statement[] = [];
const viewStmts: o.Statement[] =
[o.variable(DYNAMIC_VAR_NAME).set(o.NULL_EXPR).toDeclStmt(o.DYNAMIC_TYPE)];
let bindingCount = 0;
this.updates.forEach((expression) => {
const {sourceSpan, context, value} = this.preprocessUpdateExpression(expression);
const bindingId = `${bindingCount++}`;
const nameResolver = context === this.component ? this : null;
const {stmts, currValExpr} = convertPropertyBinding(
nameResolver, o.variable(this.getOrAddOutputVar(context)), value, bindingId);
nameResolver, o.variable(this.getOutputVar(context)), value, bindingId);
stmts.push(new o.ExpressionStatement(currValExpr));
viewStmts.push(...stmts.map(
(stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan)));
@ -110,21 +123,13 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
const bindingId = `${bindingCount++}`;
const nameResolver = context === this.component ? this : null;
const {stmts} = convertActionBinding(
nameResolver, o.variable(this.getOrAddOutputVar(context)), value, bindingId);
nameResolver, o.variable(this.getOutputVar(context)), value, bindingId);
viewStmts.push(...stmts.map(
(stmt: o.Statement) => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan)));
});
const viewName = `_View_${this.component.name}_${this.embeddedViewIndex}`;
const params: o.FnParam[] = [];
this.outputVarNames.forEach((varName, varType) => {
const outputType = varType instanceof StaticSymbol ?
o.expressionType(this.outputCtx.importExpr(varType)) :
new o.BuiltinType(varType);
params.push(new o.FnParam(varName, outputType));
});
const viewFactory = new o.DeclareFunctionStmt(viewName, params, viewStmts);
const viewFactory = new o.DeclareFunctionStmt(viewName, [], viewStmts);
targetStatements.push(viewFactory);
return targetStatements;
}
@ -211,7 +216,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
getLocal(name: string): o.Expression|null {
if (name == EventHandlerVars.event.name) {
return o.variable(this.getOrAddOutputVar(o.BuiltinTypeName.Dynamic));
return o.variable(this.getOutputVar(o.BuiltinTypeName.Dynamic));
}
for (let currBuilder: ViewBuilder|null = this; currBuilder; currBuilder = currBuilder.parent) {
let outputVarType: OutputVarType|undefined;
@ -225,7 +230,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
}
}
if (outputVarType != null) {
return o.variable(this.getOrAddOutputVar(outputVarType));
return o.variable(this.getOutputVar(outputVarType));
}
}
return null;
@ -237,7 +242,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
throw new Error(
`Illegal State: Could not find pipe ${name} in template of ${this.component}`);
}
return this.getOrAddOutputVar(pipe);
return this.getOutputVar(pipe);
}
private preprocessUpdateExpression(expression: Expression): Expression {
@ -264,7 +269,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver {
if (this.options.fullTemplateTypeCheck) {
return o.variable(this.pipeOutputVar(name)).callMethod('transform', args);
} else {
return o.variable(this.getOrAddOutputVar(o.BuiltinTypeName.Dynamic));
return o.variable(this.getOutputVar(o.BuiltinTypeName.Dynamic));
}
},
},

View File

@ -1,72 +0,0 @@
/**
* @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 {MockDirectory, compile, expectNoDiagnostics, setup, toMockFileArray} from './test_util';
describe('aot stubs', () => {
let angularFiles = setup();
it('should create empty .ngfactory and .ngsummary files for every source file', () => {
const appDir = {'app.ts': `export const x = 1;`};
const rootDir = {'app': appDir};
const {genFiles} = compile(
[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.ngsummary.ts')).toBeTruthy();
});
it('should create empty .ngstyle files for imported css files', () => {
const appDir = {
'app.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
template: '',
styleUrls: ['./style.css']
})
export class MyComp {}
@NgModule({
declarations: [MyComp]
})
export class MyModule {}
export const x = 1;
`,
'style.css': ''
};
const rootDir = {'app': appDir};
const {genFiles} =
compile([rootDir, angularFiles], {postCompile: expectNoDiagnostics, stubsOnly: true});
expect(genFiles.find((f) => f.genFileUrl === '/app/style.css.shim.ngstyle.ts')).toBeTruthy();
});
it('should create stub exports for NgModules of the right type', () => {
const appDir = {
'app.module.ts': `
import { NgModule } from '@angular/core';
@NgModule()
export class MyModule {}
`,
'app.boot.ts': `
import {NgModuleFactory} from '@angular/core';
import {MyModuleNgFactory} from './app.module.ngfactory';
import {MyModuleNgSummary} from './app.module.ngsummary';
import {MyModule} from './app.module';
export const factory: NgModuleFactory<MyModule> = MyModuleNgFactory;
export const summary: () => any[] = MyModuleNgSummary;
`
};
const rootDir = {'app': appDir};
compile(
[rootDir, angularFiles],
{postCompile: expectNoDiagnostics, stubsOnly: true, enableSummariesForJit: true});
});
});

View File

@ -633,7 +633,6 @@ export function compile(
useSummaries?: boolean,
preCompile?: (program: ts.Program) => void,
postCompile?: (program: ts.Program) => void,
stubsOnly?: boolean,
}& AotCompilerOptions = {},
tsOptions: ts.CompilerOptions = {}): {genFiles: GeneratedFile[], outDir: MockDirectory} {
// when using summaries, always emit so the next step can use the results.
@ -656,8 +655,7 @@ export function compile(
const {compiler, reflector} = createAotCompiler(aotHost, options);
const analyzedModules =
compiler.analyzeModulesSync(program.getSourceFiles().map(sf => sf.fileName));
const genFiles = options.stubsOnly ? compiler.emitAllStubs(analyzedModules) :
compiler.emitAllImpls(analyzedModules);
const genFiles = compiler.emitAllImpls(analyzedModules);
genFiles.forEach((file) => {
const source = file.source || toTypeScript(file);
if (isSource(file.genFileUrl)) {

View File

@ -10,23 +10,18 @@ import {CompileStylesheetMetadata, CompileTemplateMetadata} from '@angular/compi
import {CompilerConfig, preserveWhitespacesDefault} from '@angular/compiler/src/config';
import {DirectiveNormalizer} from '@angular/compiler/src/directive_normalizer';
import {ResourceLoader} from '@angular/compiler/src/resource_loader';
import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock';
import {ViewEncapsulation} from '@angular/core/src/metadata/view';
import {TestBed} from '@angular/core/testing';
import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal';
import {TestBed, inject} from '@angular/core/testing';
import {noUndefined} from '../src/util';
import {SpyResourceLoader} from './spies';
import {TEST_COMPILER_PROVIDERS} from './test_bindings';
const SOME_MODULE_URL = 'package:some/module/a.js';
const SOME_HTTP_MODULE_URL = 'http://some/module/a.js';
function normalizeTemplate(normalizer: DirectiveNormalizer, o: {
ngModuleType?: any; componentType?: any; moduleUrl?: string; template?: string | null;
templateUrl?: string | null;
styles?: string[];
moduleUrl?: string; template?: string | null; templateUrl?: string | null; styles?: string[];
styleUrls?: string[];
interpolation?: [string, string] | null;
encapsulation?: ViewEncapsulation | null;
@ -34,9 +29,9 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: {
preserveWhitespaces?: boolean | null;
}) {
return normalizer.normalizeTemplate({
ngModuleType: noUndefined(o.ngModuleType),
componentType: noUndefined(o.componentType),
moduleUrl: noUndefined(o.moduleUrl),
ngModuleType: null,
componentType: SomeComp,
moduleUrl: noUndefined(o.moduleUrl || SOME_MODULE_URL),
template: noUndefined(o.template),
templateUrl: noUndefined(o.templateUrl),
styles: noUndefined(o.styles),
@ -48,131 +43,39 @@ function normalizeTemplate(normalizer: DirectiveNormalizer, o: {
});
}
function normalizeTemplateOnly(normalizer: DirectiveNormalizer, o: {
ngModuleType?: any; componentType?: any; moduleUrl?: string; template?: string | null;
templateUrl?: string | null;
styles?: string[];
styleUrls?: string[];
interpolation?: [string, string] | null;
encapsulation?: ViewEncapsulation | null;
animations?: CompileAnimationEntryMetadata[];
preserveWhitespaces?: boolean | null;
}) {
return normalizer.normalizeTemplateOnly({
ngModuleType: noUndefined(o.ngModuleType),
componentType: noUndefined(o.componentType),
moduleUrl: noUndefined(o.moduleUrl),
template: noUndefined(o.template),
templateUrl: noUndefined(o.templateUrl),
styles: noUndefined(o.styles),
styleUrls: noUndefined(o.styleUrls),
interpolation: noUndefined(o.interpolation),
encapsulation: noUndefined(o.encapsulation),
animations: noUndefined(o.animations),
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
});
}
function compileTemplateMetadata({encapsulation, template, templateUrl, styles, styleUrls,
externalStylesheets, animations, ngContentSelectors,
interpolation, isInline, preserveWhitespaces}: {
encapsulation?: ViewEncapsulation | null,
template?: string | null,
templateUrl?: string | null,
styles?: string[],
styleUrls?: string[],
externalStylesheets?: CompileStylesheetMetadata[],
ngContentSelectors?: string[],
animations?: any[],
interpolation?: [string, string] | null,
isInline?: boolean,
preserveWhitespaces?: boolean | null
}): CompileTemplateMetadata {
return new CompileTemplateMetadata({
encapsulation: encapsulation || null,
template: template || null,
templateUrl: templateUrl || null,
htmlAst: null,
styles: styles || [],
styleUrls: styleUrls || [],
externalStylesheets: externalStylesheets || [],
ngContentSelectors: ngContentSelectors || [],
animations: animations || [],
interpolation: interpolation || null,
isInline: !!isInline,
preserveWhitespaces: preserveWhitespacesDefault(noUndefined(preserveWhitespaces)),
});
}
function normalizeLoadedTemplate(
normalizer: DirectiveNormalizer, o: {
ngModuleType?: any; componentType?: any; moduleUrl?: string; template?: string | null;
templateUrl?: string | null;
styles?: string[];
styleUrls?: string[];
interpolation?: [string, string] | null;
encapsulation?: ViewEncapsulation | null;
animations?: CompileAnimationEntryMetadata[];
preserveWhitespaces?: boolean;
},
template: string, templateAbsUrl: string) {
return normalizer.normalizeLoadedTemplate(
{
ngModuleType: o.ngModuleType || null,
componentType: o.componentType || null,
moduleUrl: o.moduleUrl || '',
template: o.template || null,
templateUrl: o.templateUrl || null,
styles: o.styles || [],
styleUrls: o.styleUrls || [],
interpolation: o.interpolation || null,
encapsulation: o.encapsulation || null,
animations: o.animations || [],
preserveWhitespaces: noUndefined(o.preserveWhitespaces),
},
template, templateAbsUrl);
}
export function main() {
describe('DirectiveNormalizer', () => {
beforeEach(() => { TestBed.configureCompiler({providers: TEST_COMPILER_PROVIDERS}); });
let resourceLoaderSpy: jasmine.Spy;
describe('normalizeDirective', () => {
beforeEach(() => {
resourceLoaderSpy =
jasmine.createSpy('get').and.callFake((url: string) => `resource(${url})`);
const resourceLoader = {get: resourceLoaderSpy};
TestBed.configureCompiler({
providers:
[...TEST_COMPILER_PROVIDERS, {provide: ResourceLoader, useValue: resourceLoader}]
});
});
describe('normalizeTemplate', () => {
it('should throw if no template was specified',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizeTemplate(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
}))
expect(() => normalizeTemplate(normalizer, {}))
.toThrowError('No template specified for component SomeComp');
}));
it('should throw if template is not a string',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizeTemplate(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
template: <any>{}
}))
expect(() => normalizeTemplate(normalizer, {template: <any>{}}))
.toThrowError('The template specified for component SomeComp is not a string');
}));
it('should throw if templateUrl is not a string',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizeTemplate(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
templateUrl: <any>{}
}))
expect(() => normalizeTemplate(normalizer, {templateUrl: <any>{}}))
.toThrowError('The templateUrl specified for component SomeComp is not a string');
}));
it('should throw if both template and templateUrl are defined',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizeTemplate(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
template: '',
templateUrl: '',
}))
@ -181,29 +84,21 @@ export function main() {
it('should throw if preserveWhitespaces is not a boolean',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
expect(() => normalizeTemplate(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
template: '',
preserveWhitespaces: <any>'WRONG',
}))
.toThrowError(
'The preserveWhitespaces option for component SomeComp must be a boolean');
}));
});
describe('normalizeTemplateOnly sync', () => {
describe('inline template', () => {
it('should store the template',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = <CompileTemplateMetadata>normalizeTemplateOnly(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: 'a',
templateUrl: null,
styles: [],
styleUrls: []
});
expect(template.template).toEqual('a');
expect(template.templateUrl).toEqual('package:some/module/a.js');
@ -212,46 +107,26 @@ export function main() {
it('should resolve styles on the annotation against the moduleUrl',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = <CompileTemplateMetadata>normalizeTemplateOnly(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: '',
templateUrl: null,
styles: [],
styleUrls: ['test.css']
});
const template = <CompileTemplateMetadata>normalizeTemplate(
normalizer, {template: '', styleUrls: ['test.css']});
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
}));
it('should resolve styles in the template against the moduleUrl',
it('should resolve styles in the template against the moduleUrl and add them to the styles',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = <CompileTemplateMetadata>normalizeTemplateOnly(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: '<style>@import test.css</style>',
templateUrl: null,
styles: [],
styleUrls: []
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '<style>template @import test.css</style>',
styles: ['direct'],
});
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
expect(template.styles).toEqual([
'direct', 'template ', 'resource(package:some/module/test.css)'
]);
}));
it('should use ViewEncapsulation.Emulated by default',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = <CompileTemplateMetadata>normalizeTemplateOnly(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: '',
templateUrl: null,
styles: [],
styleUrls: ['test.css']
});
const template = <CompileTemplateMetadata>normalizeTemplate(
normalizer, {template: '', styleUrls: ['test.css']});
expect(template.encapsulation).toEqual(ViewEncapsulation.Emulated);
}));
@ -260,195 +135,105 @@ export function main() {
[CompilerConfig, DirectiveNormalizer],
(config: CompilerConfig, normalizer: DirectiveNormalizer) => {
config.defaultEncapsulation = ViewEncapsulation.None;
const template = <CompileTemplateMetadata>normalizeTemplateOnly(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: undefined,
template: '',
templateUrl: undefined,
styles: [],
styleUrls: ['test.css']
});
const template = <CompileTemplateMetadata>normalizeTemplate(
normalizer, {template: '', styleUrls: ['test.css']});
expect(template.encapsulation).toEqual(ViewEncapsulation.None);
}));
});
describe('templateUrl', () => {
it('should load a template from a url that is resolved against moduleUrl',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = <CompileTemplateMetadata>normalizeTemplate(
normalizer, {templateUrl: 'sometplurl.html', styleUrls: ['test.css']});
expect(template.template).toEqual('resource(package:some/module/sometplurl.html)');
expect(template.templateUrl).toEqual('package:some/module/sometplurl.html');
expect(template.isInline).toBe(false);
}));
it('should load a template from a url that is resolved against moduleUrl',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: MockResourceLoader) => {
resourceLoader.expect('package:some/module/sometplurl.html', 'a');
(<Promise<CompileTemplateMetadata>>normalizeTemplateOnly(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: null,
templateUrl: 'sometplurl.html',
styles: [],
styleUrls: ['test.css']
})).then((template) => {
expect(template.template).toEqual('a');
expect(template.templateUrl).toEqual('package:some/module/sometplurl.html');
expect(template.isInline).toBe(false);
async.done();
});
resourceLoader.flush();
}));
it('should resolve styles on the annotation against the moduleUrl',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = <CompileTemplateMetadata>normalizeTemplate(
normalizer, {templateUrl: 'tpl/sometplurl.html', styleUrls: ['test.css']});
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
}));
it('should resolve styles on the annotation against the moduleUrl',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: MockResourceLoader) => {
resourceLoader.expect('package:some/module/tpl/sometplurl.html', '');
(<Promise<CompileTemplateMetadata>>normalizeTemplateOnly(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: null,
templateUrl: 'tpl/sometplurl.html',
styles: [],
styleUrls: ['test.css']
})).then((template) => {
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
async.done();
});
resourceLoader.flush();
}));
it('should resolve styles in the template against the templateUrl and add them to the styles',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
resourceLoaderSpy.and.callFake((url: string) => {
switch (url) {
case 'package:some/module/tpl/sometplurl.html':
return '<style>template @import test.css</style>';
default:
return `resource(${url})`;
}
});
const template = <CompileTemplateMetadata>normalizeTemplate(
normalizer, {templateUrl: 'tpl/sometplurl.html', styles: ['direct']});
expect(template.styles).toEqual([
'direct', 'template ', 'resource(package:some/module/tpl/test.css)'
]);
}));
it('should resolve styles in the template against the templateUrl',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: MockResourceLoader) => {
resourceLoader.expect(
'package:some/module/tpl/sometplurl.html', '<style>@import test.css</style>');
(<Promise<CompileTemplateMetadata>>normalizeTemplateOnly(normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
template: null,
templateUrl: 'tpl/sometplurl.html',
styles: [],
styleUrls: []
})).then((template) => {
expect(template.styleUrls).toEqual(['package:some/module/tpl/test.css']);
async.done();
});
resourceLoader.flush();
}));
});
describe('normalizeExternalStylesheets', () => {
beforeEach(() => { TestBed.configureCompiler({providers: [SpyResourceLoader.PROVIDE]}); });
describe('externalStylesheets', () => {
it('should load an external stylesheet',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: SpyResourceLoader) => {
programResourceLoaderSpy(resourceLoader, {'package:some/module/test.css': 'a'});
(<Promise<CompileTemplateMetadata>>normalizer.normalizeExternalStylesheets(
compileTemplateMetadata({
template: '',
templateUrl: '',
styleUrls: ['package:some/module/test.css']
})))
.then((template) => {
expect(template.externalStylesheets.length).toBe(1);
expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({
moduleUrl: 'package:some/module/test.css',
styles: ['a'],
styleUrls: []
}));
async.done();
});
}));
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = <CompileTemplateMetadata>normalizeTemplate(
normalizer, {template: '', styleUrls: ['package:some/module/test.css']});
expect(template.externalStylesheets.length).toBe(1);
expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({
moduleUrl: 'package:some/module/test.css',
styles: ['resource(package:some/module/test.css)'],
}));
}));
it('should load stylesheets referenced by external stylesheets',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: SpyResourceLoader) => {
programResourceLoaderSpy(resourceLoader, {
'package:some/module/test.css': 'a@import "test2.css"',
'package:some/module/test2.css': 'b'
});
(<Promise<CompileTemplateMetadata>>normalizer.normalizeExternalStylesheets(
compileTemplateMetadata({
template: '',
templateUrl: '',
styleUrls: ['package:some/module/test.css']
})))
.then((template) => {
expect(template.externalStylesheets.length).toBe(2);
expect(template.externalStylesheets[0]).toEqual(new CompileStylesheetMetadata({
moduleUrl: 'package:some/module/test.css',
styles: ['a'],
styleUrls: ['package:some/module/test2.css']
}));
expect(template.externalStylesheets[1]).toEqual(new CompileStylesheetMetadata({
moduleUrl: 'package:some/module/test2.css',
styles: ['b'],
styleUrls: []
}));
async.done();
});
}));
it('should load stylesheets referenced by external stylesheets and inline them',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
resourceLoaderSpy.and.callFake((url: string) => {
switch (url) {
case 'package:some/module/test.css':
return 'a@import "test2.css"';
case 'package:some/module/test2.css':
return 'b';
default:
throw new Error(`Unexpected url ${url}`);
}
});
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '',
styleUrls: ['package:some/module/test.css'],
});
expect(template.externalStylesheets.length).toBe(1);
expect(template.externalStylesheets[0])
.toEqual(new CompileStylesheetMetadata(
{moduleUrl: 'package:some/module/test.css', styles: ['a', 'b'], styleUrls: []}));
}));
});
describe('caching', () => {
it('should work for templateUrl',
inject(
[AsyncTestCompleter, DirectiveNormalizer, ResourceLoader],
(async: AsyncTestCompleter, normalizer: DirectiveNormalizer,
resourceLoader: MockResourceLoader) => {
resourceLoader.expect('package:some/module/cmp.html', 'a');
const prenormMeta = {
ngModuleType: null as any,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
templateUrl: 'cmp.html',
};
Promise
.all([
normalizeTemplateOnly(normalizer, prenormMeta),
normalizeTemplateOnly(normalizer, prenormMeta)
])
.then((templates: CompileTemplateMetadata[]) => {
expect(templates[0].template).toEqual('a');
expect(templates[1].template).toEqual('a');
async.done();
});
resourceLoader.flush();
}));
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const prenormMeta = {
templateUrl: 'cmp.html',
};
const template1 = <CompileTemplateMetadata>normalizeTemplate(normalizer, prenormMeta);
const template2 = <CompileTemplateMetadata>normalizeTemplate(normalizer, prenormMeta);
expect(template1.template).toEqual('resource(package:some/module/cmp.html)');
expect(template2.template).toEqual('resource(package:some/module/cmp.html)');
expect(resourceLoaderSpy).toHaveBeenCalledTimes(1);
}));
});
describe('normalizeLoadedTemplate', () => {
it('should store the viewEncapsulation in the result',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const viewEncapsulation = ViewEncapsulation.Native;
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: viewEncapsulation,
styles: [],
styleUrls: []
},
'', 'package:some/module/');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
encapsulation: viewEncapsulation,
template: '',
});
expect(template.encapsulation).toBe(viewEncapsulation);
}));
@ -456,281 +241,166 @@ export function main() {
inject(
[DirectiveNormalizer, CompilerConfig],
(normalizer: DirectiveNormalizer, config: CompilerConfig) => {
const template = normalizeLoadedTemplate(normalizer, {}, '', '');
const template =
<CompileTemplateMetadata>normalizeTemplate(normalizer, {template: ''});
expect(template.preserveWhitespaces).toBe(config.preserveWhitespaces);
}));
it('should store the preserveWhitespaces=false in the result',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template =
normalizeLoadedTemplate(normalizer, {preserveWhitespaces: false}, '', '');
const template = <CompileTemplateMetadata>normalizeTemplate(
normalizer, {preserveWhitespaces: false, template: ''});
expect(template.preserveWhitespaces).toBe(false);
}));
it('should store the preserveWhitespaces=true in the result',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template =
normalizeLoadedTemplate(normalizer, {preserveWhitespaces: true}, '', '');
const template = <CompileTemplateMetadata>normalizeTemplate(
normalizer, {preserveWhitespaces: true, template: ''});
expect(template.preserveWhitespaces).toBe(true);
}));
it('should keep the template as html',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'a', 'package:some/module/');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: 'a',
});
expect(template.template).toEqual('a');
}));
it('should collect ngContent',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<ng-content select="a"></ng-content>', 'package:some/module/');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '<ng-content select="a"></ng-content>',
});
expect(template.ngContentSelectors).toEqual(['a']);
}));
it('should normalize ngContent wildcard selector',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<ng-content></ng-content><ng-content select></ng-content><ng-content select="*"></ng-content>',
'package:some/module/');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template:
'<ng-content></ng-content><ng-content select></ng-content><ng-content select="*"></ng-content>',
});
expect(template.ngContentSelectors).toEqual(['*', '*', '*']);
}));
it('should collect top level styles in the template',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<style>a</style>', 'package:some/module/');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '<style>a</style>',
});
expect(template.styles).toEqual(['a']);
}));
it('should collect styles inside in elements',
it('should collect styles inside elements',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<div><style>a</style></div>', 'package:some/module/');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '<div><style>a</style></div>',
});
expect(template.styles).toEqual(['a']);
}));
it('should collect styleUrls in the template',
it('should collect styleUrls in the template and add them to the styles',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<link rel="stylesheet" href="aUrl">', 'package:some/module/');
expect(template.styleUrls).toEqual(['package:some/module/aUrl']);
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '<link rel="stylesheet" href="aUrl">',
});
expect(template.styles).toEqual(['resource(package:some/module/aUrl)']);
expect(template.styleUrls).toEqual([]);
}));
it('should collect styleUrls in elements',
it('should collect styleUrls in elements and add them to the styles',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<div><link rel="stylesheet" href="aUrl"></div>', 'package:some/module/');
expect(template.styleUrls).toEqual(['package:some/module/aUrl']);
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '<div><link rel="stylesheet" href="aUrl"></div>',
});
expect(template.styles).toEqual(['resource(package:some/module/aUrl)']);
expect(template.styleUrls).toEqual([]);
}));
it('should ignore link elements with non stylesheet rel attribute',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<link href="b" rel="a">', 'package:some/module/');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '<link href="b" rel="a">',
});
expect(template.styleUrls).toEqual([]);
}));
it('should ignore link elements with absolute urls but non package: scheme',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<link href="http://some/external.css" rel="stylesheet">', 'package:some/module/');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '<link href="http://some/external.css" rel="stylesheet">',
});
expect(template.styleUrls).toEqual([]);
}));
it('should extract @import style urls into styleAbsUrl',
it('should extract @import style urls and add them to the styles',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: ['@import "test.css";'],
styleUrls: []
},
'', 'package:some/module/id');
expect(template.styles).toEqual(['']);
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
styles: ['@import "test.css";'],
template: '',
});
expect(template.styles).toEqual(['', 'resource(package:some/module/test.css)']);
expect(template.styleUrls).toEqual([]);
}));
it('should not resolve relative urls in inline styles',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: ['.foo{background-image: url(\'double.jpg\');'],
styleUrls: []
},
'', 'package:some/module/id');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
styles: ['.foo{background-image: url(\'double.jpg\');'],
template: '',
});
expect(template.styles).toEqual(['.foo{background-image: url(\'double.jpg\');']);
}));
it('should resolve relative style urls in styleUrls',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: ['test.css']
},
'', 'package:some/module/id');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
styleUrls: ['test.css'],
template: '',
});
expect(template.styles).toEqual([]);
expect(template.styleUrls).toEqual(['package:some/module/test.css']);
}));
it('should resolve relative style urls in styleUrls with http directive url',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_HTTP_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: ['test.css']
},
'', 'http://some/module/id');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
moduleUrl: SOME_HTTP_MODULE_URL,
styleUrls: ['test.css'],
template: '',
});
expect(template.styles).toEqual([]);
expect(template.styleUrls).toEqual(['http://some/module/test.css']);
}));
it('should normalize ViewEncapsulation.Emulated to ViewEncapsulation.None if there are no styles nor stylesheets',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: ViewEncapsulation.Emulated,
styles: [],
styleUrls: []
},
'', 'package:some/module/id');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
encapsulation: ViewEncapsulation.Emulated,
template: '',
});
expect(template.encapsulation).toEqual(ViewEncapsulation.None);
}));
it('should ignore ng-content in elements with ngNonBindable',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<div ngNonBindable><ng-content select="a"></ng-content></div>',
'package:some/module/');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '<div ngNonBindable><ng-content select="a"></ng-content></div>',
});
expect(template.ngContentSelectors).toEqual([]);
}));
it('should still collect <style> in elements with ngNonBindable',
inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => {
const template = normalizeLoadedTemplate(
normalizer, {
ngModuleType: null,
componentType: SomeComp,
moduleUrl: SOME_MODULE_URL,
encapsulation: null,
styles: [],
styleUrls: []
},
'<div ngNonBindable><style>div {color:red}</style></div>', 'package:some/module/');
const template = <CompileTemplateMetadata>normalizeTemplate(normalizer, {
template: '<div ngNonBindable><style>div {color:red}</style></div>',
});
expect(template.styles).toEqual(['div {color:red}']);
}));
});
});
}
function programResourceLoaderSpy(spy: SpyResourceLoader, results: {[key: string]: string}) {
spy.spy('get').and.callFake((url: string): Promise<any> => {
const result = results[url];
if (result) {
return Promise.resolve(result);
} else {
return Promise.reject(`Unknown mock url ${url}`);
}
});
}
class SomeComp {}

View File

@ -0,0 +1,25 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '../../src/output/output_ast';
export function main() {
describe('OutputAst', () => {
describe('collectExternalReferences', () => {
it('should find expressions of variable types', () => {
const ref1 = new o.ExternalReference('aModule', 'name1');
const ref2 = new o.ExternalReference('aModule', 'name2');
const stmt =
o.variable('test').set(o.NULL_EXPR).toDeclStmt(o.importType(ref1, [o.importType(
ref2) !]));
expect(o.collectExternalReferences([stmt])).toEqual([ref1, ref2]);
});
});
});
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, createOfflineCompileUrlResolver, extractProgramSymbols} from '@angular/compiler';
import {AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, InterpolationConfig, JitSummaryResolver, NgAnalyzedModules, NgModuleResolver, ParseTreeResult, PipeResolver, ResourceLoader, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, SummaryResolver, analyzeNgModules, createOfflineCompileUrlResolver} from '@angular/compiler';
import {CompilerOptions, getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from '@angular/compiler-cli/src/language_services';
import {ViewEncapsulation, ɵConsole as Console} from '@angular/core';
import * as fs from 'fs';
@ -104,9 +104,10 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
new DirectiveNormalizer(resourceLoader, urlResolver, htmlParser, config);
result = this._resolver = new CompileMetadataResolver(
config, moduleResolver, directiveResolver, pipeResolver, new JitSummaryResolver(),
elementSchemaRegistry, directiveNormalizer, new Console(), this._staticSymbolCache,
this.reflector, (error, type) => this.collectError(error, type && type.filePath));
config, htmlParser, moduleResolver, directiveResolver, pipeResolver,
new JitSummaryResolver(), elementSchemaRegistry, directiveNormalizer, new Console(),
this._staticSymbolCache, this.reflector,
(error, type) => this.collectError(error, type && type.filePath));
}
return result;
}
@ -146,12 +147,10 @@ export class TypeScriptServiceHost implements LanguageServiceHost {
let analyzedModules = this.analyzedModules;
if (!analyzedModules) {
const analyzeHost = {isSourceFile(filePath: string) { return true; }};
const programSymbols = extractProgramSymbols(
this.staticSymbolResolver, this.program.getSourceFiles().map(sf => sf.fileName),
analyzeHost);
const programFiles = this.program.getSourceFiles().map(sf => sf.fileName);
analyzedModules = this.analyzedModules =
analyzeNgModules(programSymbols, analyzeHost, this.staticSymbolResolver, this.resolver);
analyzeNgModules(programFiles, analyzeHost, this.staticSymbolResolver, this.resolver);
}
return analyzedModules;
}

View File

@ -34,13 +34,12 @@ export class CompilerImpl implements Compiler {
private _delegate: JitCompiler;
constructor(
private _injector: Injector, private _metadataResolver: CompileMetadataResolver,
htmlParser: HtmlParser, templateParser: TemplateParser, styleCompiler: StyleCompiler,
viewCompiler: ViewCompiler, ngModuleCompiler: NgModuleCompiler,
summaryResolver: SummaryResolver<Type<any>>, compileReflector: CompileReflector,
compilerConfig: CompilerConfig, console: Console) {
templateParser: TemplateParser, styleCompiler: StyleCompiler, viewCompiler: ViewCompiler,
ngModuleCompiler: NgModuleCompiler, summaryResolver: SummaryResolver<Type<any>>,
compileReflector: CompileReflector, compilerConfig: CompilerConfig, console: Console) {
this._delegate = new JitCompiler(
_metadataResolver, htmlParser, templateParser, styleCompiler, viewCompiler,
ngModuleCompiler, summaryResolver, compileReflector, compilerConfig, console,
_metadataResolver, templateParser, styleCompiler, viewCompiler, ngModuleCompiler,
summaryResolver, compileReflector, compilerConfig, console,
this.getExtraNgModuleProviders.bind(this));
}
@ -128,7 +127,7 @@ export const COMPILER_PROVIDERS = <StaticProvider[]>[
I18NHtmlParser, Console]
},
{ provide: DirectiveNormalizer, deps: [ResourceLoader, UrlResolver, HtmlParser, CompilerConfig]},
{ provide: CompileMetadataResolver, deps: [CompilerConfig, NgModuleResolver,
{ provide: CompileMetadataResolver, deps: [CompilerConfig, HtmlParser, NgModuleResolver,
DirectiveResolver, PipeResolver,
SummaryResolver,
ElementSchemaRegistry,
@ -142,7 +141,7 @@ export const COMPILER_PROVIDERS = <StaticProvider[]>[
{ provide: NgModuleCompiler, deps: [CompileReflector] },
{ provide: CompilerConfig, useValue: new CompilerConfig()},
{ provide: Compiler, useClass: CompilerImpl, deps: [Injector, CompileMetadataResolver,
HtmlParser, TemplateParser, StyleCompiler,
TemplateParser, StyleCompiler,
ViewCompiler, NgModuleCompiler,
SummaryResolver, CompileReflector, CompilerConfig,
Console]},