diff --git a/tools/@angular/tsc-wrapped/src/evaluator.ts b/tools/@angular/tsc-wrapped/src/evaluator.ts index 7e568d829c..9e4d418ef0 100644 --- a/tools/@angular/tsc-wrapped/src/evaluator.ts +++ b/tools/@angular/tsc-wrapped/src/evaluator.ts @@ -178,6 +178,9 @@ export class Evaluator { case ts.SyntaxKind.NullKeyword: case ts.SyntaxKind.TrueKeyword: case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.TemplateHead: + case ts.SyntaxKind.TemplateMiddle: + case ts.SyntaxKind.TemplateTail: return true; case ts.SyntaxKind.ParenthesizedExpression: const parenthesizedExpression = node; @@ -211,6 +214,10 @@ export class Evaluator { return true; } break; + case ts.SyntaxKind.TemplateExpression: + const templateExpression = node; + return templateExpression.templateSpans.every( + span => this.isFoldableWorker(span.expression, folding)); } } return false; @@ -436,9 +443,11 @@ export class Evaluator { } return recordEntry(typeReference, node); case ts.SyntaxKind.NoSubstitutionTemplateLiteral: - return (node).text; case ts.SyntaxKind.StringLiteral: - return (node).text; + case ts.SyntaxKind.TemplateHead: + case ts.SyntaxKind.TemplateTail: + case ts.SyntaxKind.TemplateMiddle: + return (node).text; case ts.SyntaxKind.NumericLiteral: return parseFloat((node).text); case ts.SyntaxKind.AnyKeyword: @@ -575,6 +584,36 @@ export class Evaluator { case ts.SyntaxKind.FunctionExpression: case ts.SyntaxKind.ArrowFunction: return recordEntry(errorSymbol('Function call not supported', node), node); + case ts.SyntaxKind.TaggedTemplateExpression: + return recordEntry( + errorSymbol('Tagged template expressions are not supported in metadata', node), node); + case ts.SyntaxKind.TemplateExpression: + const templateExpression = node; + if (this.isFoldable(node)) { + return templateExpression.templateSpans.reduce( + (previous, current) => previous + this.evaluateNode(current.expression) + + this.evaluateNode(current.literal), + this.evaluateNode(templateExpression.head)); + } else { + return templateExpression.templateSpans.reduce((previous, current) => { + const expr = this.evaluateNode(current.expression); + const literal = this.evaluateNode(current.literal); + if (isFoldableError(expr)) return expr; + if (isFoldableError(literal)) return literal; + if (typeof previous === 'string' && typeof expr === 'string' && + typeof literal === 'string') { + return previous + expr + literal; + } + let result = expr; + if (previous !== '') { + result = {__symbolic: 'binop', operator: '+', left: previous, right: expr}; + } + if (literal != '') { + result = {__symbolic: 'binop', operator: '+', left: result, right: literal}; + } + return result; + }, this.evaluateNode(templateExpression.head)); + } } return recordEntry(errorSymbol('Expression form not supported', node), node); } diff --git a/tools/@angular/tsc-wrapped/test/collector.spec.ts b/tools/@angular/tsc-wrapped/test/collector.spec.ts index 896e2f4cbf..0dddee2509 100644 --- a/tools/@angular/tsc-wrapped/test/collector.spec.ts +++ b/tools/@angular/tsc-wrapped/test/collector.spec.ts @@ -633,6 +633,80 @@ describe('Collector', () => { }); }); + describe('with interpolations', () => { + function createSource(text: string): ts.SourceFile { + return ts.createSourceFile('', text, ts.ScriptTarget.Latest, true); + } + + function e(expr: string, prefix?: string) { + const source = createSource(`${prefix || ''} export let value = ${expr};`); + const metadata = collector.getMetadata(source); + return expect(metadata.metadata['value']); + } + + it('should be able to collect a raw interpolated string', + () => { e('`simple value`').toBe('simple value'); }); + + it('should be able to interpolate a single value', + () => { e('`${foo}`', 'const foo = "foo value"').toBe('foo value'); }); + + it('should be able to interpolate multiple values', () => { + e('`foo:${foo}, bar:${bar}, end`', 'const foo = "foo"; const bar = "bar";') + .toBe('foo:foo, bar:bar, end'); + }); + + it('should be able to interpolate with an imported reference', () => { + e('`external:${external}`', 'import {external} from "./external";').toEqual({ + __symbolic: 'binop', + operator: '+', + left: 'external:', + right: { + __symbolic: 'reference', + module: './external', + name: 'external', + } + }); + }); + + it('should simplify a redundant template', () => { + e('`${external}`', 'import {external} from "./external";') + .toEqual({__symbolic: 'reference', module: './external', name: 'external'}); + }); + + it('should be able to collect complex template with imported references', () => { + e('`foo:${foo}, bar:${bar}, end`', 'import {foo, bar} from "./external";').toEqual({ + __symbolic: 'binop', + operator: '+', + left: { + __symbolic: 'binop', + operator: '+', + left: { + __symbolic: 'binop', + operator: '+', + left: { + __symbolic: 'binop', + operator: '+', + left: 'foo:', + right: {__symbolic: 'reference', module: './external', name: 'foo'} + }, + right: ', bar:' + }, + right: {__symbolic: 'reference', module: './external', name: 'bar'} + }, + right: ', end' + }); + }); + + it('should reject a tagged literal', () => { + e('tag`some value`').toEqual({ + __symbolic: 'error', + message: 'Tagged template expressions are not supported in metadata', + line: 0, + character: 20 + }); + }); + }); + describe('in strict mode', () => { it('should throw if an error symbol is collecting a reference to a non-exported symbol', () => { const source = program.getSourceFile('/local-symbol-ref.ts');