angular-cn/packages/compiler/test/render3/mock_compile.ts

344 lines
15 KiB
TypeScript

/**
* @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 {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileIdentifierMetadata, CompileMetadataResolver, CompileNgModuleMetadata, CompilePipeSummary, CompileTypeMetadata, CompilerConfig, DEFAULT_INTERPOLATION_CONFIG, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, ParseError, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver, templateSourceUrl} from '@angular/compiler';
import {ViewEncapsulation} from '@angular/core';
import * as ts from 'typescript';
import {NgAnalyzedModules} from '../../src/aot/compiler';
import {ConstantPool} from '../../src/constant_pool';
import {ParserError} from '../../src/expression_parser/ast';
import * as o from '../../src/output/output_ast';
import {ModuleKind, compileModuleBackPatch} from '../../src/render3/r3_back_patch_compiler';
import {compileModuleFactory} from '../../src/render3/r3_module_factory_compiler';
import {compilePipe} from '../../src/render3/r3_pipe_compiler';
import {OutputMode} from '../../src/render3/r3_types';
import {compileComponent, compileDirective} from '../../src/render3/r3_view_compiler';
import {BindingParser} from '../../src/template_parser/binding_parser';
import {OutputContext} from '../../src/util';
import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToMockDir, expectNoDiagnostics, settings, setup, toMockFileArray} from '../aot/test_util';
const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
const OPERATOR =
/!|%|\*|\/|\^|\&|\&\&\|\||\|\||\(|\)|\{|\}|\[|\]|:|;|\.|<|<=|>|>=|=|==|===|!=|!==|=>|\+|\+\+|-|--|@|,|\.|\.\.\./;
const STRING = /\'[^'\n]*\'|"[^'\n]*"|`[^`]*`/;
const NUMBER = /[0-9]+/;
const ELLIPSIS = '…';
const TOKEN = new RegExp(
`^((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source}|${ELLIPSIS})`);
const WHITESPACE = /^\s+/;
type Piece = string | RegExp;
const IDENT = /[A-Za-z$_][A-Za-z0-9$_]*/;
const SKIP = /(?:.|\n|\r)*/;
const MATCHING_IDENT = /^\$.*\$$/;
function tokenize(text: string): Piece[] {
function matches(exp: RegExp): string|false {
const m = text.match(exp);
if (!m) return false;
text = text.substr(m[0].length);
return m[0];
}
function next(): string {
const result = matches(TOKEN);
if (!result) {
throw Error(`Invalid test, no token found for '${text.substr(0, 30)}...'`);
}
matches(WHITESPACE);
return result;
}
const pieces: Piece[] = [];
matches(WHITESPACE);
while (text) {
const token = next();
if (token === 'IDENT') {
pieces.push(IDENT);
} else if (token === ELLIPSIS) {
pieces.push(SKIP);
} else {
pieces.push(token);
}
}
return pieces;
}
const contextWidth = 100;
export function expectEmit(source: string, emitted: string, description: string) {
const pieces = tokenize(emitted);
const expr = r(...pieces);
if (!expr.test(source)) {
let last: number = 0;
for (let i = 1; i < pieces.length; i++) {
const t = r(...pieces.slice(0, i));
const m = source.match(t);
const expected = pieces[i - 1] == IDENT ? '<IDENT>' : pieces[i - 1];
if (!m) {
const contextPieceWidth = contextWidth / 2;
fail(
`${description}: Expected to find ${expected} '${source.substr(0,last)}[<---HERE expected "${expected}"]${source.substr(last)}'`);
return;
} else {
last = (m.index || 0) + m[0].length;
}
}
fail(
`Test helper failure: Expected expression failed but the reporting logic could not find where it failed in: ${source}`);
}
}
const IDENT_LIKE = /^[a-z][A-Z]/;
const SPECIAL_RE_CHAR = /\/|\(|\)|\||\*|\+|\[|\]|\{|\}|\$/g;
function r(...pieces: (string | RegExp)[]): RegExp {
const results: string[] = [];
let first = true;
let group = 0;
const groups = new Map<string, number>();
for (const piece of pieces) {
if (!first)
results.push(`\\s${typeof piece === 'string' && IDENT_LIKE.test(piece) ? '+' : '*'}`);
first = false;
if (typeof piece === 'string') {
if (MATCHING_IDENT.test(piece)) {
const matchGroup = groups.get(piece);
if (!matchGroup) {
results.push('(' + IDENT.source + ')');
const newGroup = ++group;
groups.set(piece, newGroup);
} else {
results.push(`\\${matchGroup}`);
}
} else {
results.push(piece.replace(SPECIAL_RE_CHAR, s => '\\' + s));
}
} else {
results.push('(?:' + piece.source + ')');
}
}
return new RegExp(results.join(''));
}
function doCompile(
data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {},
errorCollector: (error: any, fileName?: string) => void = error => { throw error; },
compileAction: (
outputCtx: OutputContext, analyzedModules: NgAnalyzedModules,
resolver: CompileMetadataResolver, htmlParser: HtmlParser, templateParser: TemplateParser,
hostBindingParser: BindingParser, reflector: StaticReflector) => void) {
const testFiles = toMockFileArray(data);
const scripts = testFiles.map(entry => entry.fileName);
const angularFilesArray = toMockFileArray(angularFiles);
const files = arrayToMockDir([...testFiles, ...angularFilesArray]);
const mockCompilerHost = new MockCompilerHost(scripts, files);
const compilerHost = new MockAotCompilerHost(mockCompilerHost);
const program = ts.createProgram(scripts, {...settings}, mockCompilerHost);
expectNoDiagnostics(program);
// TODO(chuckj): Replace with a variant of createAotCompiler() when the r3_view_compiler is
// integrated
const translations = options.translations || '';
const urlResolver = createAotUrlResolver(compilerHost);
const symbolCache = new StaticSymbolCache();
const summaryResolver = new AotSummaryResolver(compilerHost, symbolCache);
const symbolResolver = new StaticSymbolResolver(compilerHost, symbolCache, summaryResolver);
const staticReflector =
new StaticReflector(summaryResolver, symbolResolver, [], [], errorCollector);
const htmlParser = new I18NHtmlParser(
new HtmlParser(), translations, options.i18nFormat, options.missingTranslation, console);
const config = new CompilerConfig({
defaultEncapsulation: ViewEncapsulation.Emulated,
useJit: false,
missingTranslation: options.missingTranslation,
preserveWhitespaces: options.preserveWhitespaces,
strictInjectionParameters: options.strictInjectionParameters,
});
const normalizer = new DirectiveNormalizer(
{get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config);
const expressionParser = new Parser(new Lexer());
const elementSchemaRegistry = new DomElementSchemaRegistry();
const templateParser = new TemplateParser(
config, staticReflector, expressionParser, elementSchemaRegistry, htmlParser, console, []);
const resolver = new CompileMetadataResolver(
config, htmlParser, new NgModuleResolver(staticReflector),
new DirectiveResolver(staticReflector), new PipeResolver(staticReflector), summaryResolver,
elementSchemaRegistry, normalizer, console, symbolCache, staticReflector, errorCollector);
// Create the TypeScript program
const sourceFiles = program.getSourceFiles().map(sf => sf.fileName);
// Analyze the modules
// TODO(chuckj): Eventually this should not be necessary as the ts.SourceFile should be sufficient
// to generate a template definition.
const analyzedModules = analyzeNgModules(sourceFiles, compilerHost, symbolResolver, resolver);
const pipesOrDirectives = Array.from(analyzedModules.ngModuleByPipeOrDirective.keys());
const fakeOutputContext: OutputContext = {
genFilePath: 'fakeFactory.ts',
statements: [],
importExpr(symbol: StaticSymbol, typeParams: o.Type[]) {
if (!(symbol instanceof StaticSymbol)) {
if (!symbol) {
throw new Error('Invalid: undefined passed to as a symbol');
}
throw new Error(`Invalid: ${(symbol as any).constructor.name} is not a symbol`);
}
return (symbol.members || [])
.reduce(
(expr, member) => expr.prop(member),
<o.Expression>o.importExpr(new o.ExternalReference(symbol.filePath, symbol.name)));
},
constantPool: new ConstantPool()
};
const errors: ParseError[] = [];
const hostBindingParser = new BindingParser(
expressionParser, DEFAULT_INTERPOLATION_CONFIG, elementSchemaRegistry, [], errors);
// Load all directives and pipes
for (const pipeOrDirective of pipesOrDirectives) {
const module = analyzedModules.ngModuleByPipeOrDirective.get(pipeOrDirective) !;
resolver.loadNgModuleDirectiveAndPipeMetadata(module.type.reference, true);
}
compileAction(
fakeOutputContext, analyzedModules, resolver, htmlParser, templateParser, hostBindingParser,
staticReflector);
fakeOutputContext.statements.unshift(...fakeOutputContext.constantPool.statements);
const emitter = new TypeScriptEmitter();
const moduleName = compilerHost.fileNameToModuleName(
fakeOutputContext.genFilePath, fakeOutputContext.genFilePath);
const result = emitter.emitStatementsAndContext(
fakeOutputContext.genFilePath, fakeOutputContext.statements, '', false,
/* referenceFilter */ undefined,
/* importFilter */ e => e.moduleName != null && e.moduleName.startsWith('/app'));
if (errors.length) {
throw new Error('Unexpected errors:' + errors.map(e => e.toString()).join(', '));
}
return {source: result.sourceText, outputContext: fakeOutputContext};
}
export function compile(
data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {},
errorCollector: (error: any, fileName?: string) => void = error => { throw error;}) {
return doCompile(
data, angularFiles, options, errorCollector,
(outputCtx: OutputContext, analyzedModules: NgAnalyzedModules,
resolver: CompileMetadataResolver, htmlParser: HtmlParser, templateParser: TemplateParser,
hostBindingParser: BindingParser, reflector: StaticReflector) => {
const pipesOrDirectives = Array.from(analyzedModules.ngModuleByPipeOrDirective.keys());
for (const pipeOrDirective of pipesOrDirectives) {
const module = analyzedModules.ngModuleByPipeOrDirective.get(pipeOrDirective);
if (!module || !module.type.reference.filePath.startsWith('/app')) {
continue;
}
if (resolver.isDirective(pipeOrDirective)) {
const metadata = resolver.getDirectiveMetadata(pipeOrDirective);
if (metadata.isComponent) {
const fakeUrl = 'ng://fake-template-url.html';
const htmlAst = htmlParser.parse(metadata.template !.template !, fakeUrl);
const directives = module.transitiveModule.directives.map(
dir => resolver.getDirectiveSummary(dir.reference));
const pipes = module.transitiveModule.pipes.map(
pipe => resolver.getPipeSummary(pipe.reference));
const parsedTemplate = templateParser.parse(
metadata, htmlAst, directives, pipes, module.schemas, fakeUrl, false);
compileComponent(
outputCtx, metadata, pipes, parsedTemplate.template, reflector, hostBindingParser,
OutputMode.PartialClass);
} else {
compileDirective(
outputCtx, metadata, reflector, hostBindingParser, OutputMode.PartialClass);
}
} else if (resolver.isPipe(pipeOrDirective)) {
const metadata = resolver.getPipeMetadata(pipeOrDirective);
if (metadata) {
compilePipe(outputCtx, metadata, reflector, OutputMode.PartialClass);
}
}
}
});
}
const DTS = /\.d\.ts$/;
const EXT = /(\.\w+)+$/;
const NONE_WORD = /\W/g;
const NODE_MODULES = /^.*\/node_modules\//;
function getBackPatchFunctionName(type: CompileTypeMetadata) {
const filePath = (type.reference.filePath as string)
.replace(EXT, '')
.replace(NODE_MODULES, '')
.replace(NONE_WORD, '_');
return `ngBackPatch_${filePath.split('/').filter(s => !!s).join('_')}_${type.reference.name}`;
}
function getBackPatchReference(type: CompileTypeMetadata): o.Expression {
return o.variable(getBackPatchFunctionName(type));
}
export function backPatch(
data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {},
errorCollector: (error: any, fileName?: string) => void = error => { throw error;}) {
return doCompile(
data, angularFiles, options, errorCollector,
(outputCtx: OutputContext, analyzedModules: NgAnalyzedModules,
resolver: CompileMetadataResolver, htmlParser: HtmlParser, templateParser: TemplateParser,
hostBindingParser: BindingParser, reflector: StaticReflector) => {
const parseTemplate =
(compMeta: CompileDirectiveMetadata, ngModule: CompileNgModuleMetadata,
directiveIdentifiers: CompileIdentifierMetadata[]) => {
const directives =
directiveIdentifiers.map(dir => resolver.getDirectiveSummary(dir.reference));
const pipes = ngModule.transitiveModule.pipes.map(
pipe => resolver.getPipeSummary(pipe.reference));
return templateParser.parse(
compMeta, compMeta.template !.htmlAst !, directives, pipes, ngModule.schemas,
templateSourceUrl(ngModule.type, compMeta, compMeta.template !), true);
};
for (const module of analyzedModules.ngModules) {
compileModuleBackPatch(
outputCtx, getBackPatchFunctionName(module.type), module,
DTS.test(module.type.reference.filePath) ? ModuleKind.Renderer2 :
ModuleKind.Renderer3,
getBackPatchReference, parseTemplate, reflector, resolver);
}
});
}
export function createFactories(
data: MockDirectory, context: MockData, options: AotCompilerOptions = {},
errorCollector: (error: any, fileName?: string) => void = error => { throw error;}) {
return doCompile(
data, context, options, errorCollector,
(outputCtx: OutputContext, analyzedModules: NgAnalyzedModules,
resolver: CompileMetadataResolver, htmlParser: HtmlParser, templateParser: TemplateParser,
hostBindingParser: BindingParser, reflector: StaticReflector) => {
for (const module of analyzedModules.ngModules) {
compileModuleFactory(outputCtx, module, getBackPatchReference, resolver);
}
});
}