diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index 969d696157..c279b9cec3 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -618,7 +618,11 @@ export function expectNoDiagnostics(program: ts.Program) { if (diagnostics && diagnostics.length) { throw new Error( 'Errors from TypeScript:\n' + - diagnostics.map(d => `${fileInfo(d)}${d.messageText}${lineInfo(d)}`).join(' \n')); + diagnostics + .map( + d => + `${fileInfo(d)}${ts.flattenDiagnosticMessageText(d.messageText, '\n')}${lineInfo(d)}`) + .join(' \n')); } } expectNoDiagnostics(program.getOptionsDiagnostics()); diff --git a/packages/compiler/test/render3/mock_compile.ts b/packages/compiler/test/render3/mock_compile.ts new file mode 100644 index 0000000000..28cb8f1f5d --- /dev/null +++ b/packages/compiler/test/render3/mock_compile.ts @@ -0,0 +1,241 @@ +/** + * @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, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler'; +import {ViewEncapsulation} from '@angular/core'; +import * as ts from 'typescript'; + +import {ConstantPool} from '../../src/constant_pool'; +import * as o from '../../src/output/output_ast'; +import {compileComponent, compileDirective} from '../../src/render3/r3_view_compiler'; +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 ? '' : 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(); + 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('')); +} + +export function compile( + data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {}, + errorCollector: (error: any, fileName?: string) => void = error => { throw error;}) { + 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, + enableLegacyTemplate: options.enableLegacyTemplate === true, + 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 directives = 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.importExpr(new o.ExternalReference(symbol.filePath, symbol.name))); + }, + constantPool: new ConstantPool() + }; + + // Load All directives + for (const directive of directives) { + const module = analyzedModules.ngModuleByPipeOrDirective.get(directive) !; + resolver.loadNgModuleDirectiveAndPipeMetadata(module.type.reference, true); + } + + // Compile the directives. + for (const directive of directives) { + const module = analyzedModules.ngModuleByPipeOrDirective.get(directive); + if (!module || !module.type.reference.filePath.startsWith('/app')) { + continue; + } + if (resolver.isDirective(directive)) { + const metadata = resolver.getDirectiveMetadata(directive); + 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(fakeOutputContext, metadata, parsedTemplate.template, staticReflector); + } else { + compileDirective(fakeOutputContext, metadata, 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')); + + return {source: result.sourceText, outputContext: fakeOutputContext}; +} diff --git a/packages/compiler/test/render3/mock_compiler_spec.ts b/packages/compiler/test/render3/mock_compiler_spec.ts new file mode 100644 index 0000000000..4c7e44145c --- /dev/null +++ b/packages/compiler/test/render3/mock_compiler_spec.ts @@ -0,0 +1,154 @@ +/** + * @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, setup} from '../aot/test_util'; +import {compile, expectEmit} from './mock_compile'; + +describe('mock_compiler', () => { + // This produces a MockDirectory of the file needed to compile an Angular application. + // This setup is performed in a beforeAll which populates the map returned. + const angularFiles = setup({ + compileAngular: true, + compileAnimations: false, + compileCommon: true, + }); + + describe('compiling', () => { + // To use compile you need to supply the files in a MockDirectory that can be merged + // with a set of "environment" files such as the angular files. + it('should be able to compile a simple application', () => { + const files = { + app: { + 'hello.component.ts': ` + import {Component, Input} from '@angular/core'; + + @Component({template: 'Hello {{name}}!'}) + export class HelloComponent { + @Input() name: string = 'world'; + } + `, + 'hello.module.ts': ` + import {NgModule} from '@angular/core'; + import {HelloComponent} from './hello.component'; + + @NgModule({declarations: [HelloComponent]}) + export class HelloModule {} + ` + } + }; + const result = compile(files, angularFiles); + + // result.source contains just the emitted factory declarations regardless of the original + // module. + expect(result.source).toContain('Hello'); + + // The output context is also returned if the actual output ast is needed. + expect(result.outputContext.statements.length).toBeGreaterThan(0); + }); + }); + + describe('expecting emitted output', () => { + it('should be able to find a simple expression in the output', () => { + const files = { + app: { + 'hello.component.ts': ` + import {Component, Input} from '@angular/core'; + + @Component({template: 'Hello {{name}}! Your name as {{name.length}} characters'}) + export class HelloComponent { + @Input() name: string = 'world'; + } + `, + 'hello.module.ts': ` + import {NgModule} from '@angular/core'; + import {HelloComponent} from './hello.component'; + + @NgModule({declarations: [HelloComponent]}) + export class HelloModule {} + ` + } + }; + + const result = compile(files, angularFiles); + + // The expression can expected directly. + expectEmit(result.source, 'name.length', 'name length expression not found'); + + // Whitespace is not significant + expectEmit( + result.source, 'name \n\n . \n length', + 'name length expression not found (whitespace)'); + }); + }); + + it('should be able to skip untested regions', () => { + const files = { + app: { + 'hello.component.ts': ` + import {Component, Input} from '@angular/core'; + + @Component({template: 'Hello {{name}}! Your name as {{name.length}} characters'}) + export class HelloComponent { + @Input() name: string = 'world'; + } + `, + 'hello.module.ts': ` + import {NgModule} from '@angular/core'; + import {HelloComponent} from './hello.component'; + + @NgModule({declarations: [HelloComponent]}) + export class HelloModule {} + ` + } + }; + + const result = compile(files, angularFiles); + + // The special character … means anything can be generated between the two sections allowing + // skipping sections of the output that are not under test. The ellipsis unicode char (…) is + // used instead of '...' because '...' is legal JavaScript (the spread operator) and might + // need to be tested. + expectEmit(result.source, 'ctx.name … ctx.name.length', 'could not find correct length access'); + }); + + it('should be able to enforce consistent identifiers', () => { + const files = { + app: { + 'hello.component.ts': ` + import {Component, Input} from '@angular/core'; + + @Component({template: 'Hello {{name}}! Your name as {{name.length}} characters'}) + export class HelloComponent { + @Input() name: string = 'world'; + } + `, + 'hello.module.ts': ` + import {NgModule} from '@angular/core'; + import {HelloComponent} from './hello.component'; + + @NgModule({declarations: [HelloComponent]}) + export class HelloModule {} + ` + } + }; + + const result = compile(files, angularFiles); + + // IDENT can be used a wild card for any identifier + expectEmit(result.source, 'IDENT.name', 'could not find context access'); + + // $$ can be used as a wild-card but all the content matched by the identifiers must + // match each other. + // This is useful if the code generator is free to invent a name but should use the name + // consistently. + expectEmit( + result.source, '$ctx$.$name$ … $ctx$.$name$.length', + 'could not find correct length access'); + }); +}); \ No newline at end of file diff --git a/packages/compiler/test/render3/r3_view_compiler_spec.ts b/packages/compiler/test/render3/r3_view_compiler_spec.ts index e3fe2a4ee5..408c836655 100644 --- a/packages/compiler/test/render3/r3_view_compiler_spec.ts +++ b/packages/compiler/test/render3/r3_view_compiler_spec.ts @@ -6,15 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler'; -import {ViewEncapsulation} from '@angular/core'; -import * as ts from 'typescript'; - -import {ConstantPool} from '../../src/constant_pool'; -import * as o from '../../src/output/output_ast'; -import {compileComponent, compileDirective} from '../../src/render3/r3_view_compiler'; -import {OutputContext} from '../../src/util'; -import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToMockDir, settings, setup, toMockFileArray} from '../aot/test_util'; +import {MockDirectory, setup} from '../aot/test_util'; +import {compile, expectEmit} from './mock_compile'; describe('r3_view_compiler', () => { const angularFiles = setup({ @@ -112,7 +105,7 @@ describe('r3_view_compiler', () => { selector: 'my-app', template: ' {{list[0]}} {{list[1]}} {{list[2]}} {{list[3]}} {{list[4]}} {{list[5]}} {{list[6]}} {{list[7]}} {{list[8]}} ' }) - export class MyApp implements OnInit { + export class MyApp { list: any[] = []; } @@ -121,7 +114,7 @@ describe('r3_view_compiler', () => { } }; - const bV_call = `IDENT.ɵbV([' ',ctx.list[0],' ',ctx.list[1],' ',ctx.list[2],' ',ctx.list[3], + const bV_call = `$r3$.ɵbV([' ',ctx.list[0],' ',ctx.list[1],' ',ctx.list[2],' ',ctx.list[3], ' ',ctx.list[4],' ',ctx.list[5],' ',ctx.list[6],' ',ctx.list[7],' ',ctx.list[8], ' '])`; const result = compile(files, angularFiles); @@ -157,27 +150,26 @@ describe('r3_view_compiler', () => { // The template should look like this (where IDENT is a wild card for an identifier): const template = ` + const $c1$ = ['class', 'my-app', 'title', 'Hello']; + … template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, 'div', IDENT); - IDENT.ɵT(1, 'Hello '); - IDENT.ɵE(2, 'b'); - IDENT.ɵT(3, 'World'); - IDENT.ɵe(); - IDENT.ɵT(4, '!'); - IDENT.ɵe(); + $r3$.ɵE(0, 'div', $c1$); + $r3$.ɵT(1, 'Hello '); + $r3$.ɵE(2, 'b'); + $r3$.ɵT(3, 'World'); + $r3$.ɵe(); + $r3$.ɵT(4, '!'); + $r3$.ɵe(); } } `; - // The compiler should also emit a const array like this: - const constants = `const IDENT = ['class', 'my-app', 'title', 'Hello'];`; const result = compile(files, angularFiles); expectEmit(result.source, factory, 'Incorrect factory'); expectEmit(result.source, template, 'Incorrect template'); - expectEmit(result.source, constants, 'Incorrect shared constants'); }); }); }); @@ -206,20 +198,20 @@ describe('r3_view_compiler', () => { // ChildComponent definition should be: const ChildComponentDefinition = ` - static ngComponentDef = IDENT.ɵdefineComponent({ + static ngComponentDef = $r3$.ɵdefineComponent({ type: ChildComponent, tag: 'child', factory: function ChildComponent_Factory() { return new ChildComponent(); }, template: function ChildComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵT(0, 'child-view'); + $r3$.ɵT(0, 'child-view'); } } });`; // SomeDirective definition should be: const SomeDirectiveDefinition = ` - static ngDirectiveDef = IDENT.ɵdefineDirective({ + static ngDirectiveDef = $r3$.ɵdefineDirective({ type: SomeDirective, factory: function SomeDirective_Factory() {return new SomeDirective(); } }); @@ -227,28 +219,27 @@ describe('r3_view_compiler', () => { // MyComponent definition should be: const MyComponentDefinition = ` - static ngComponentDef = IDENT.ɵdefineComponent({ + const $c1$ = ['some-directive', '']; + const $c2$ = [SomeDirective]; + … + static ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, tag: 'my-component', factory: function MyComponent_Factory() { return new MyComponent(); }, template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, ChildComponent, IDENT, IDENT); - IDENT.ɵe(); - IDENT.ɵT(3, '!'); + $r3$.ɵE(0, ChildComponent, IDENT, IDENT); + $r3$.ɵe(); + $r3$.ɵT(3, '!'); } ChildComponent.ngComponentDef.h(1, 0); SomeDirective.ngDirectiveDef.h(2, 0); - IDENT.ɵr(1, 0); - IDENT.ɵr(2, 0); + $r3$.ɵr(1, 0); + $r3$.ɵr(2, 0); } }); `; - // The following constants should be emitted as well. - const AttributesConstant = `const IDENT = ['some-directive', ''];`; - - const DirectivesConstant = `const IDENT = [SomeDirective];`; const result = compile(files, angularFiles); const source = result.source; @@ -256,8 +247,6 @@ describe('r3_view_compiler', () => { expectEmit(source, ChildComponentDefinition, 'Incorrect ChildComponent.ngComponentDef'); expectEmit(source, SomeDirectiveDefinition, 'Incorrect SomeDirective.ngDirectiveDef'); expectEmit(source, MyComponentDefinition, 'Incorrect MyComponentDefinition.ngComponentDef'); - expectEmit(source, AttributesConstant, 'Incorrect shared attributes constant'); - expectEmit(source, DirectivesConstant, 'Incorrect share directives constant'); }); it('should support structural directives', () => { @@ -286,47 +275,46 @@ describe('r3_view_compiler', () => { }; const IfDirectiveDefinition = ` - static ngDirectiveDef = IDENT.ɵdefineDirective({ + static ngDirectiveDef = $r3$.ɵdefineDirective({ type: IfDirective, - factory: function IfDirective_Factory() { return new IfDirective(IDENT.ɵinjectTemplateRef()); } + factory: function IfDirective_Factory() { return new IfDirective($r3$.ɵinjectTemplateRef()); } });`; const MyComponentDefinition = ` - static ngComponentDef = IDENT.ɵdefineComponent({ + const $c1$ = ['foo', '']; + const $c2$ = [IfDirective]; + … + static ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, tag: 'my-component', factory: function MyComponent_Factory() { return new MyComponent(); }, template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, 'ul', null, null, IDENT); - IDENT.ɵC(2, IDENT, MyComponent_IfDirective_Template_2); - IDENT.ɵe(); + $r3$.ɵE(0, 'ul', null, null, $c1$); + $r3$.ɵC(2, $c2$, MyComponent_IfDirective_Template_2); + $r3$.ɵe(); } - const IDENT = IDENT.ɵm(1); + const $foo$ = $r3$.ɵm(1); IfDirective.ngDirectiveDef.h(3,2); - IDENT.ɵcR(2); - IDENT.ɵr(3,2); - IDENT.ɵcr(); + $r3$.ɵcR(2); + $r3$.ɵr(3,2); + $r3$.ɵcr(); function MyComponent_IfDirective_Template_2(ctx0: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, 'li'); - IDENT.ɵT(1); - IDENT.ɵe(); + $r3$.ɵE(0, 'li'); + $r3$.ɵT(1); + $r3$.ɵe(); } - IDENT.ɵt(1, IDENT.ɵb2('', ctx.salutation, ' ', IDENT, '')); + $r3$.ɵt(1, $r3$.ɵb2('', ctx.salutation, ' ', $foo$, '')); } } });`; - const locals = `const IDENT = ['foo', ''];`; - const directives = `const IDENT = [IfDirective];`; const result = compile(files, angularFiles); const source = result.source; expectEmit(source, IfDirectiveDefinition, 'Incorrect IfDirective.ngDirectiveDef'); expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); - expectEmit(source, locals, 'Incorrect share locals constant'); - expectEmit(source, directives, 'Incorrect shared directive constant'); }); it('should support content projection', () => { @@ -359,50 +347,49 @@ describe('r3_view_compiler', () => { }; const SimpleComponentDefinition = ` - static ngComponentDef = IDENT.ɵdefineComponent({ + static ngComponentDef = $r3$.ɵdefineComponent({ type: SimpleComponent, tag: 'simple', factory: function SimpleComponent_Factory() { return new SimpleComponent(); }, template: function SimpleComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵpD(0); - IDENT.ɵE(1, 'div'); - IDENT.ɵP(2, 0); - IDENT.ɵe(); + $r3$.ɵpD(0); + $r3$.ɵE(1, 'div'); + $r3$.ɵP(2, 0); + $r3$.ɵe(); } } });`; const ComplexComponentDefinition = ` - static ngComponentDef = IDENT.ɵdefineComponent({ + const $c1$ = [[[['span', 'title', 'tofirst'], null]], [[['span', 'title', 'tosecond'], null]]]; + const $c2$ = ['id','first']; + const $c3$ = ['id','second']; + … + static ngComponentDef = $r3$.ɵdefineComponent({ type: ComplexComponent, tag: 'complex', factory: function ComplexComponent_Factory() { return new ComplexComponent(); }, template: function ComplexComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵpD(0, IDENT); - IDENT.ɵE(1, 'div', IDENT); - IDENT.ɵP(2, 0, 1); - IDENT.ɵe(); - IDENT.ɵE(3, 'div', IDENT); - IDENT.ɵP(4, 0, 2); - IDENT.ɵe(); + $r3$.ɵpD(0, $c1$); + $r3$.ɵE(1, 'div', $c2$); + $r3$.ɵP(2, 0, 1); + $r3$.ɵe(); + $r3$.ɵE(3, 'div', $c3$); + $r3$.ɵP(4, 0, 2); + $r3$.ɵe(); } } }); `; - const ComplexComponent_ProjectionConst = ` - const IDENT = [[[['span', 'title', 'tofirst'], null]], [[['span', 'title', 'tosecond'], null]]]; - `; - const result = compile(files, angularFiles); const source = result.source; expectEmit(result.source, SimpleComponentDefinition, 'Incorrect SimpleComponent definition'); expectEmit( result.source, ComplexComponentDefinition, 'Incorrect ComplexComponent definition'); - expectEmit(result.source, ComplexComponent_ProjectionConst, 'Incorrect projection const'); }); it('local reference', () => { @@ -421,29 +408,28 @@ describe('r3_view_compiler', () => { }; const MyComponentDefinition = ` - static ngComponentDef = IDENT.ɵdefineComponent({ + const $c1$ = ['user', '']; + … + static ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, tag: 'my-component', factory: function MyComponent_Factory() { return new MyComponent(); }, template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, 'input', null, null, IDENT); - IDENT.ɵe(); - IDENT.ɵT(2); + $r3$.ɵE(0, 'input', null, null, $c1$); + $r3$.ɵe(); + $r3$.ɵT(2); } - const IDENT = IDENT.ɵm(1); - IDENT.ɵt(2, IDENT.ɵb1('Hello ', IDENT.value, '!')); + const $user$ = $r3$.ɵm(1); + $r3$.ɵt(2, $r3$.ɵb1('Hello ', $user$.value, '!')); } }); `; - const locals = `const IDENT = ['user', ''];`; - const result = compile(files, angularFiles); const source = result.source; expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef'); - expectEmit(source, locals, 'Incorrect locals constant definition'); }); describe('lifecycle hooks', () => { @@ -484,7 +470,7 @@ describe('r3_view_compiler', () => { name2 = '2'; } - @NgModule({declarations: [LifecycleComp, SimpleLayout]} + @NgModule({declarations: [LifecycleComp, SimpleLayout]}) export class LifecycleModule {} ` } @@ -492,33 +478,35 @@ describe('r3_view_compiler', () => { it('should gen hooks with a few simple components', () => { const LifecycleCompDefinition = ` - static ngComponentDef = IDENT.ɵdefineComponent({ + static ngComponentDef = $r3$.ɵdefineComponent({ type: LifecycleComp, tag: 'lifecycle-comp', factory: function LifecycleComp_Factory() { return new LifecycleComp(); }, - template: function LifecycleComp_Template(ctx: any, cm: boolean) {}, + template: function LifecycleComp_Template(ctx: IDENT, cm: IDENT) {}, inputs: {nameMin: 'name'}, - features: [IDENT.ɵNgOnChangesFeature(LifecycleComp)] + features: [$r3$.ɵNgOnChangesFeature(LifecycleComp)] });`; const SimpleLayoutDefinition = ` - static ngComponentDef = IDENT.ɵdefineComponent({ + const $c1$ = LifecycleComp.ngComponentDef; + … + static ngComponentDef = $r3$.ɵdefineComponent({ type: SimpleLayout, tag: 'simple-layout', factory: function SimpleLayout_Factory() { return new SimpleLayout(); }, - template: function SimpleLayout_Template(ctx: any, cm: boolean) { + template: function SimpleLayout_Template(ctx: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, LifecycleComp); - IDENT.ɵe(); - IDENT.ɵE(2, LifecycleComp); - IDENT.ɵe(); + $r3$.ɵE(0, LifecycleComp); + $r3$.ɵe(); + $r3$.ɵE(2, LifecycleComp); + $r3$.ɵe(); } - IDENT.ɵp(0, 'name', IDENT.ɵb(ctx.name1)); - IDENT.ɵp(2, 'name', IDENT.ɵb(ctx.name2)); - IDENT.h(1, 0); - IDENT.h(3, 2); - IDENT.ɵr(1, 0); - IDENT.ɵr(3, 2); + $r3$.ɵp(0, 'name', $r3$.ɵb(ctx.name1)); + $r3$.ɵp(2, 'name', $r3$.ɵb(ctx.name2)); + $c1$.h(1, 0); + $c1$.h(3, 2); + $r3$.ɵr(1, 0); + $r3$.ɵr(3, 2); } });`; @@ -610,41 +598,43 @@ describe('r3_view_compiler', () => { // TODO(chuckj): Enforce this when the directives are specified const ForDirectiveDefinition = ` - static ngDirectiveDef = IDENT.ɵdefineDirective({ + static ngDirectiveDef = $r3$.ɵdefineDirective({ type: ForOfDirective, factory: function ForOfDirective_Factory() { - return new ForOfDirective(IDENT.ɵinjectViewContainerRef(), IDENT.ɵinjectTemplateRef()); + return new ForOfDirective($r3$.ɵinjectViewContainerRef(), $r3$.ɵinjectTemplateRef()); }, - features: [IDENT.ɵNgOnChangesFeature(NgForOf)], + features: [$r3$.ɵNgOnChangesFeature(NgForOf)], inputs: {forOf: 'forOf'} }); `; const MyComponentDefinition = ` - static ngComponentDef = IDENT.ɵdefineComponent({ + const $c1$ = [ForOfDirective]; + … + static ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, tag: 'my-component', factory: function MyComponent_Factory() { return new MyComponent(); }, template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, 'ul'); - IDENT.ɵC(1, IDENT, MyComponent_ForOfDirective_Template_1); - IDENT.ɵe(); + $r3$.ɵE(0, 'ul'); + $r3$.ɵC(1, $c1$, MyComponent_ForOfDirective_Template_1); + $r3$.ɵe(); } - IDENT.ɵp(1, 'forOf', IDENT.ɵb(ctx.items)); + $r3$.ɵp(1, 'forOf', $r3$.ɵb(ctx.items)); ForOfDirective.ngDirectiveDef.h(2, 1); - IDENT.ɵcR(1); - IDENT.ɵr(2, 1); - IDENT.ɵcr(); + $r3$.ɵcR(1); + $r3$.ɵr(2, 1); + $r3$.ɵcr(); function MyComponent_ForOfDirective_Template_1(ctx0: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, 'li'); - IDENT.ɵT(1); - IDENT.ɵe(); + $r3$.ɵE(0, 'li'); + $r3$.ɵT(1); + $r3$.ɵe(); } - const IDENT = ctx0.$implicit; - IDENT.ɵt(1, IDENT.ɵb1('', IDENT.name, '')); + const $item$ = ctx0.$implicit; + $r3$.ɵt(1, $r3$.ɵb1('', $item$.name, '')); } } }); @@ -681,7 +671,7 @@ describe('r3_view_compiler', () => { \` }) export class MyComponent { - items: Item[] = [ + items = [ {name: 'one', infos: [{description: '11'}, {description: '12'}]}, {name: 'two', infos: [{description: '21'}, {description: '22'}]} ]; @@ -696,50 +686,53 @@ describe('r3_view_compiler', () => { }; const MyComponentDefinition = ` - static ngComponentDef = IDENT.ɵdefineComponent({ + const $c1$ = [ForOfDirective]; + const $c2$ = ForOfDirective.ngDirectiveDef; + … + static ngComponentDef = $r3$.ɵdefineComponent({ type: MyComponent, tag: 'my-component', factory: function MyComponent_Factory() { return new MyComponent(); }, template: function MyComponent_Template(ctx: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, 'ul'); - IDENT.ɵC(1, IDENT, MyComponent_ForOfDirective_Template_1); - IDENT.ɵe(); + $r3$.ɵE(0, 'ul'); + $r3$.ɵC(1, $c1$, MyComponent_ForOfDirective_Template_1); + $r3$.ɵe(); } - IDENT.ɵp(1, 'forOf', IDENT.ɵb(ctx.items)); - IDENT.h(2,1); - IDENT.ɵcR(1); - IDENT.ɵr(2, 1); - IDENT.ɵcr(); + $r3$.ɵp(1, 'forOf', $r3$.ɵb(ctx.items)); + $c2$.h(2,1); + $r3$.ɵcR(1); + $r3$.ɵr(2, 1); + $r3$.ɵcr(); function MyComponent_ForOfDirective_Template_1(ctx0: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, 'li'); - IDENT.ɵE(1, 'div'); - IDENT.ɵT(2); - IDENT.ɵe(); - IDENT.ɵE(3, 'ul'); - IDENT.ɵC(4, IDENT, MyComponent_ForOfDirective_ForOfDirective_Template_4); - IDENT.ɵe(); - IDENT.ɵe(); + $r3$.ɵE(0, 'li'); + $r3$.ɵE(1, 'div'); + $r3$.ɵT(2); + $r3$.ɵe(); + $r3$.ɵE(3, 'ul'); + $r3$.ɵC(4, $c1$, MyComponent_ForOfDirective_ForOfDirective_Template_4); + $r3$.ɵe(); + $r3$.ɵe(); } - const IDENT = ctx0.$implicit; - IDENT.ɵp(4, 'forOf', IDENT.ɵb(IDENT.infos)); - IDENT.h(5,4); - IDENT.ɵt(2, IDENT.ɵb1('', IDENT.name, '')); - IDENT.ɵcR(4); - IDENT.ɵr(5, 4); - IDENT.ɵcr(); + const $item$ = ctx0.$implicit; + $r3$.ɵp(4, 'forOf', $r3$.ɵb(IDENT.infos)); + $c2$.h(5,4); + $r3$.ɵt(2, $r3$.ɵb1('', IDENT.name, '')); + $r3$.ɵcR(4); + $r3$.ɵr(5, 4); + $r3$.ɵcr(); function MyComponent_ForOfDirective_ForOfDirective_Template_4( ctx1: IDENT, cm: IDENT) { if (cm) { - IDENT.ɵE(0, 'li'); - IDENT.ɵT(1); - IDENT.ɵe(); + $r3$.ɵE(0, 'li'); + $r3$.ɵT(1); + $r3$.ɵe(); } - const IDENT = ctx1.$implicit; - IDENT.ɵt(1, IDENT.ɵb2(' ', IDENT.name, ': ', IDENT.description, ' ')); + const $info$ = ctx1.$implicit; + $r3$.ɵt(1, $r3$.ɵb2(' ', $item$.name, ': ', $info$.description, ' ')); } } } @@ -752,207 +745,3 @@ describe('r3_view_compiler', () => { }); }); }); - -const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/; -const OPERATOR = - /!|%|\*|\/|\^|\&|\&\&\|\||\|\||\(|\)|\{|\}|\[|\]|:|;|\.|<|<=|>|>=|=|==|===|!=|!==|=>|\+|\+\+|-|--|@|,|\.|\.\.\./; -const STRING = /\'[^'\n]*\'|"[^'\n]*"|`[^`]*`/; -const NUMBER = /[0-9]+/; -const TOKEN = new RegExp( - `^((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source})`); -const WHITESPACE = /^\s+/; - -type Piece = string | RegExp; - -const IDENT = /[A-Za-z$_][A-Za-z0-9$_]*/; - -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 { - pieces.push(token); - } - } - return pieces; -} - -const contextWidth = 100; -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++) { - let t = r(...pieces.slice(0, i)); - let m = source.match(t); - let expected = pieces[i - 1] == 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 { - let results: string[] = []; - let first = true; - for (const piece of pieces) { - if (!first) - results.push(`\\s${typeof piece === 'string' && IDENT_LIKE.test(piece) ? '+' : '*'}`); - first = false; - if (typeof piece === 'string') { - results.push(piece.replace(SPECIAL_RE_CHAR, s => '\\' + s)); - } else { - results.push('(' + piece.source + ')'); - } - } - return new RegExp(results.join('')); -} - -function compile( - data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {}, - errorCollector: (error: any, fileName?: string) => void = error => { throw error; }) { - 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); - - // 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, - enableLegacyTemplate: options.enableLegacyTemplate === true, - 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 directives = 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.importExpr(new o.ExternalReference(symbol.filePath, symbol.name))); - }, - constantPool: new ConstantPool() - }; - - // Load All directives - for (const directive of directives) { - const module = analyzedModules.ngModuleByPipeOrDirective.get(directive) !; - resolver.loadNgModuleDirectiveAndPipeMetadata(module.type.reference, true); - } - - // Compile the directives. - for (const directive of directives) { - const module = analyzedModules.ngModuleByPipeOrDirective.get(directive); - if (!module || !module.type.reference.filePath.startsWith('/app')) { - continue; - } - if (resolver.isDirective(directive)) { - const metadata = resolver.getDirectiveMetadata(directive); - 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(fakeOutputContext, metadata, parsedTemplate.template, staticReflector); - } else { - compileDirective(fakeOutputContext, metadata, 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')); - - return {source: result.sourceText, outputContext: fakeOutputContext}; -} \ No newline at end of file