Skip to content

Commit 24ffb0b

Browse files
committed
Support tagged template literals in code generator
1 parent 323be39 commit 24ffb0b

File tree

12 files changed

+182
-31
lines changed

12 files changed

+182
-31
lines changed

packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export class Esm5RenderingFormatter extends EsmRenderingFormatter {
6666
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
6767
const node = translateStatement(
6868
stmt, importManager,
69-
{downlevelLocalizedStrings: true, downlevelVariableDeclarations: true});
69+
{downlevelTaggedTemplates: true, downlevelVariableDeclarations: true});
7070
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
7171

7272
return code;

packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class TestRenderingFormatter implements RenderingFormatter {
6666
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
6767
const node = translateStatement(
6868
stmt, importManager,
69-
{downlevelLocalizedStrings: this.isEs5, downlevelVariableDeclarations: this.isEs5});
69+
{downlevelTaggedTemplates: this.isEs5, downlevelVariableDeclarations: this.isEs5});
7070
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
7171

7272
return `// TRANSPILED\n${code}`;

packages/compiler-cli/src/ngtsc/transform/src/transform.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ function transformIvySourceFile(
280280
const constants =
281281
constantPool.statements.map(stmt => translateStatement(stmt, importManager, {
282282
recordWrappedNodeExpr,
283-
downlevelLocalizedStrings: downlevelTranslatedCode,
283+
downlevelTaggedTemplates: downlevelTranslatedCode,
284284
downlevelVariableDeclarations: downlevelTranslatedCode,
285285
}));
286286

packages/compiler-cli/src/ngtsc/translator/src/translator.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,21 @@ const BINARY_OPERATORS = new Map<o.BinaryOperator, BinaryOperator>([
3838
export type RecordWrappedNodeExprFn<TExpression> = (expr: TExpression) => void;
3939

4040
export interface TranslatorOptions<TExpression> {
41-
downlevelLocalizedStrings?: boolean;
41+
downlevelTaggedTemplates?: boolean;
4242
downlevelVariableDeclarations?: boolean;
4343
recordWrappedNodeExpr?: RecordWrappedNodeExprFn<TExpression>;
4444
}
4545

4646
export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.ExpressionVisitor,
4747
o.StatementVisitor {
48-
private downlevelLocalizedStrings: boolean;
48+
private downlevelTaggedTemplates: boolean;
4949
private downlevelVariableDeclarations: boolean;
5050
private recordWrappedNodeExpr: RecordWrappedNodeExprFn<TExpression>;
5151

5252
constructor(
5353
private factory: AstFactory<TStatement, TExpression>,
5454
private imports: ImportGenerator<TExpression>, options: TranslatorOptions<TExpression>) {
55-
this.downlevelLocalizedStrings = options.downlevelLocalizedStrings === true;
55+
this.downlevelTaggedTemplates = options.downlevelTaggedTemplates === true;
5656
this.downlevelVariableDeclarations = options.downlevelVariableDeclarations === true;
5757
this.recordWrappedNodeExpr = options.recordWrappedNodeExpr || (() => {});
5858
}
@@ -168,6 +168,19 @@ export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.E
168168
ast.sourceSpan);
169169
}
170170

171+
visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): TExpression {
172+
const elements: TemplateElement[] = [];
173+
for (let i = 0; i < ast.template.elements.length; i++) {
174+
elements.push(createTemplateElement(ast.serializeTemplateElement(i)));
175+
}
176+
return this.setSourceMapRange(
177+
this.createTaggedTemplateFunctionCall(ast.tag.visitExpression(this, context), {
178+
elements,
179+
expressions: ast.template.expressions.map(e => e.visitExpression(this, context))
180+
}),
181+
ast.sourceSpan);
182+
}
183+
171184
visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): TExpression {
172185
return this.factory.createNewExpression(
173186
ast.classExpr.visitExpression(this, context),
@@ -202,13 +215,15 @@ export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.E
202215
}
203216

204217
const localizeTag = this.factory.createIdentifier('$localize');
218+
return this.setSourceMapRange(
219+
this.createTaggedTemplateFunctionCall(localizeTag, {elements, expressions}),
220+
ast.sourceSpan);
221+
}
205222

206-
// Now choose which implementation to use to actually create the necessary AST nodes.
207-
const localizeCall = this.downlevelLocalizedStrings ?
208-
this.createES5TaggedTemplateFunctionCall(localizeTag, {elements, expressions}) :
209-
this.factory.createTaggedTemplate(localizeTag, {elements, expressions});
210-
211-
return this.setSourceMapRange(localizeCall, ast.sourceSpan);
223+
private createTaggedTemplateFunctionCall(
224+
tag: TExpression, template: TemplateLiteral<TExpression>): TExpression {
225+
return this.downlevelTaggedTemplates ? this.createES5TaggedTemplateFunctionCall(tag, template) :
226+
this.factory.createTaggedTemplate(tag, template);
212227
}
213228

214229
/**

packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ export class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor
9898
throw new Error('Method not implemented.');
9999
}
100100

101+
visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): never {
102+
throw new Error('Method not implemented.');
103+
}
104+
101105
visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): never {
102106
throw new Error('Method not implemented.');
103107
}

packages/compiler-cli/src/transformers/node_emitter.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LeadingComment, leadingComment, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, LocalizedString, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, TypeofExpr, UnaryOperator, UnaryOperatorExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
9+
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LeadingComment, leadingComment, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, LocalizedString, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, TaggedTemplateExpr, ThrowStmt, TryCatchStmt, TypeofExpr, UnaryOperator, UnaryOperatorExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
1010
import * as ts from 'typescript';
1111

1212
import {attachComments} from '../ngtsc/translator';
@@ -544,6 +544,25 @@ export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
544544
expr.args.map(arg => arg.visitExpression(this, null))));
545545
}
546546

547+
visitTaggedTemplateExpr(expr: TaggedTemplateExpr): RecordedNode<ts.TaggedTemplateExpression> {
548+
const elementCount = expr.template.elements.length;
549+
const templateSpans: ts.TemplateSpan[] = [];
550+
for (let i = 1; i < elementCount; i++) {
551+
const element = expr.serializeTemplateElement(i),
552+
literal = i < elementCount - 1 ? ts.createTemplateMiddle(element.cooked, element.raw) :
553+
ts.createTemplateTail(element.cooked, element.raw);
554+
templateSpans.push(ts.createTemplateSpan(
555+
expr.template.expressions[i - 1].visitExpression(this, null), literal));
556+
}
557+
const headElement = expr.serializeTemplateElement(0);
558+
return this.postProcess(
559+
expr,
560+
ts.createTaggedTemplate(
561+
expr.tag.visitExpression(this, null), /* typeArguments */ undefined,
562+
ts.createTemplateExpression(
563+
ts.createTemplateHead(headElement.cooked, headElement.raw), templateSpans)));
564+
}
565+
547566
visitInstantiateExpr(expr: InstantiateExpr): RecordedNode<ts.NewExpression> {
548567
return this.postProcess(
549568
expr,

packages/compiler/src/compiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export * from './ml_parser/tags';
7878
export {LexerRange} from './ml_parser/lexer';
7979
export * from './ml_parser/xml_parser';
8080
export {NgModuleCompiler} from './ng_module_compiler';
81-
export {ArrayType, AssertNotNull, DYNAMIC_TYPE, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, NONE_TYPE, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, STRING_TYPE, TypeofExpr, collectExternalReferences, jsDocComment, leadingComment, LeadingComment, JSDocComment, UnaryOperator, UnaryOperatorExpr, LocalizedString} from './output/output_ast';
81+
export {ArrayType, AssertNotNull, DYNAMIC_TYPE, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinType, BuiltinTypeName, BuiltinVar, CastExpr, ClassField, ClassMethod, ClassStmt, CommaExpr, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, literalMap, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, NONE_TYPE, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, StatementVisitor, TaggedTemplateExpr, TemplateLiteral, TemplateLiteralElement, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr, StmtModifier, Statement, STRING_TYPE, TypeofExpr, collectExternalReferences, jsDocComment, leadingComment, LeadingComment, JSDocComment, UnaryOperator, UnaryOperatorExpr, LocalizedString} from './output/output_ast';
8282
export {EmitterVisitorContext} from './output/abstract_emitter';
8383
export {JitEvaluator} from './output/output_jit';
8484
export * from './output/ts_emitter';

packages/compiler/src/constant_pool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ class KeyVisitor implements o.ExpressionVisitor {
324324
visitWritePropExpr = invalid;
325325
visitInvokeMethodExpr = invalid;
326326
visitInvokeFunctionExpr = invalid;
327+
visitTaggedTemplateExpr = invalid;
327328
visitInstantiateExpr = invalid;
328329
visitConditionalExpr = invalid;
329330
visitNotExpr = invalid;

packages/compiler/src/output/abstract_emitter.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,17 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
344344
ctx.print(expr, `)`);
345345
return null;
346346
}
347+
visitTaggedTemplateExpr(expr: o.TaggedTemplateExpr, ctx: EmitterVisitorContext): any {
348+
expr.tag.visitExpression(this, ctx);
349+
ctx.print(expr, '`' + expr.serializeTemplateElement(0).raw);
350+
for (let i = 1; i < expr.template.elements.length; i++) {
351+
ctx.print(expr, '${');
352+
expr.template.expressions[i].visitExpression(this, ctx);
353+
ctx.print(expr, `}${expr.serializeTemplateElement(i - 1).raw}`);
354+
}
355+
ctx.print(expr, '`');
356+
return null;
357+
}
347358
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): any {
348359
throw new Error('Abstract emitter cannot visit WrappedNodeExpr.');
349360
}

packages/compiler/src/output/abstract_js_emitter.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@
1010
import {AbstractEmitterVisitor, CATCH_ERROR_VAR, CATCH_STACK_VAR, EmitterVisitorContext, escapeIdentifier} from './abstract_emitter';
1111
import * as o from './output_ast';
1212

13+
/**
14+
* In TypeScript, tagged template functions expect a "template object", which is an array of
15+
* "cooked" strings plus a `raw` property that contains an array of "raw" strings. This is
16+
* typically constructed with a function called `__makeTemplateObject(cooked, raw)`, but it may not
17+
* be available in all environments.
18+
*
19+
* This is a JavaScript polyfill that uses __makeTemplateObject when it available, but otherwise
20+
* creates an inline helper with the same functionality.
21+
*
22+
* In the inline function, if `Object.defineProperty` is available we use that to attach the `raw`
23+
* array.
24+
*/
25+
const makeTemplateObjectPolyfill =
26+
'(this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})';
27+
1328
export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
1429
constructor() {
1530
super(false);
@@ -115,6 +130,30 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
115130
}
116131
return null;
117132
}
133+
visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, ctx: EmitterVisitorContext): any {
134+
// The following convoluted piece of code is effectively the downlevelled equivalent of
135+
// ```
136+
// tag`...`
137+
// ```
138+
// which is effectively like:
139+
// ```
140+
// tag(__makeTemplateObject(cooked, raw), expression1, expression2, ...);
141+
// ```
142+
ast.tag.visitExpression(this, ctx);
143+
ctx.print(ast, `(${makeTemplateObjectPolyfill}(`);
144+
const parts: o.CookedRawString[] = [];
145+
for (let i = 0; i < ast.template.elements.length; i++) {
146+
parts.push(ast.serializeTemplateElement(i));
147+
}
148+
ctx.print(ast, `[${parts.map(part => escapeIdentifier(part.cooked, false)).join(', ')}], `);
149+
ctx.print(ast, `[${parts.map(part => escapeIdentifier(part.raw, false)).join(', ')}])`);
150+
ast.template.expressions.forEach(expression => {
151+
ctx.print(ast, ', ');
152+
expression.visitExpression(this, ctx);
153+
});
154+
ctx.print(ast, ')');
155+
return null;
156+
}
118157
visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any {
119158
ctx.print(ast, `function${ast.name ? ' ' + ast.name : ''}(`);
120159
this._visitParams(ast.params, ctx);
@@ -161,19 +200,7 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
161200
// ```
162201
// $localize(__makeTemplateObject(cooked, raw), expression1, expression2, ...);
163202
// ```
164-
//
165-
// The `$localize` function expects a "template object", which is an array of "cooked" strings
166-
// plus a `raw` property that contains an array of "raw" strings.
167-
//
168-
// In some environments a helper function called `__makeTemplateObject(cooked, raw)` might be
169-
// available, in which case we use that. Otherwise we must create our own helper function
170-
// inline.
171-
//
172-
// In the inline function, if `Object.defineProperty` is available we use that to attach the
173-
// `raw` array.
174-
ctx.print(
175-
ast,
176-
'$localize((this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})(');
203+
ctx.print(ast, `$localize(${makeTemplateObjectPolyfill}(`);
177204
const parts = [ast.serializeI18nHead()];
178205
for (let i = 1; i < ast.messageParts.length; i++) {
179206
parts.push(ast.serializeI18nTemplatePart(i));

0 commit comments

Comments
 (0)