343 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			343 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, 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, 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 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 HtmlParser();
 | |
|   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);
 | |
|         }
 | |
|       });
 | |
| }
 |