Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ export class Esm5RenderingFormatter extends EsmRenderingFormatter {
*/
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
const node = translateStatement(
stmt, importManager,
{downlevelLocalizedStrings: true, downlevelVariableDeclarations: true});
stmt, importManager, {downlevelTaggedTemplates: true, downlevelVariableDeclarations: true});
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);

return code;
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class TestRenderingFormatter implements RenderingFormatter {
printStatement(stmt: Statement, sourceFile: ts.SourceFile, importManager: ImportManager): string {
const node = translateStatement(
stmt, importManager,
{downlevelLocalizedStrings: this.isEs5, downlevelVariableDeclarations: this.isEs5});
{downlevelTaggedTemplates: this.isEs5, downlevelVariableDeclarations: this.isEs5});
const code = this.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);

return `// TRANSPILED\n${code}`;
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/ngtsc/transform/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ function transformIvySourceFile(
const constants =
constantPool.statements.map(stmt => translateStatement(stmt, importManager, {
recordWrappedNodeExpr,
downlevelLocalizedStrings: downlevelTranslatedCode,
downlevelTaggedTemplates: downlevelTranslatedCode,
downlevelVariableDeclarations: downlevelTranslatedCode,
}));

Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/ngtsc/translator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export {Context} from './src/context';
export {ImportManager} from './src/import_manager';
export {ExpressionTranslatorVisitor, RecordWrappedNodeExprFn, TranslatorOptions} from './src/translator';
export {translateType} from './src/type_translator';
export {attachComments, TypeScriptAstFactory} from './src/typescript_ast_factory';
export {attachComments, createTemplateMiddle, createTemplateTail, TypeScriptAstFactory} from './src/typescript_ast_factory';
export {translateExpression, translateStatement} from './src/typescript_translator';
33 changes: 24 additions & 9 deletions packages/compiler-cli/src/ngtsc/translator/src/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '@angular/compiler';
import {createTaggedTemplate} from 'typescript';

import {AstFactory, BinaryOperator, ObjectLiteralProperty, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './api/ast_factory';
import {ImportGenerator} from './api/import_generator';
Expand Down Expand Up @@ -38,21 +39,21 @@ const BINARY_OPERATORS = new Map<o.BinaryOperator, BinaryOperator>([
export type RecordWrappedNodeExprFn<TExpression> = (expr: TExpression) => void;

export interface TranslatorOptions<TExpression> {
downlevelLocalizedStrings?: boolean;
downlevelTaggedTemplates?: boolean;
downlevelVariableDeclarations?: boolean;
recordWrappedNodeExpr?: RecordWrappedNodeExprFn<TExpression>;
}

export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.ExpressionVisitor,
o.StatementVisitor {
private downlevelLocalizedStrings: boolean;
private downlevelTaggedTemplates: boolean;
private downlevelVariableDeclarations: boolean;
private recordWrappedNodeExpr: RecordWrappedNodeExprFn<TExpression>;

constructor(
private factory: AstFactory<TStatement, TExpression>,
private imports: ImportGenerator<TExpression>, options: TranslatorOptions<TExpression>) {
this.downlevelLocalizedStrings = options.downlevelLocalizedStrings === true;
this.downlevelTaggedTemplates = options.downlevelTaggedTemplates === true;
this.downlevelVariableDeclarations = options.downlevelVariableDeclarations === true;
this.recordWrappedNodeExpr = options.recordWrappedNodeExpr || (() => {});
}
Expand Down Expand Up @@ -168,6 +169,19 @@ export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.E
ast.sourceSpan);
}

visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): TExpression {
return this.setSourceMapRange(
this.createTaggedTemplateExpression(ast.tag.visitExpression(this, context), {
elements: ast.template.elements.map(e => createTemplateElement({
cooked: e.text,
raw: e.rawText,
range: e.sourceSpan ?? ast.sourceSpan,
})),
expressions: ast.template.expressions.map(e => e.visitExpression(this, context))
}),
ast.sourceSpan);
}

visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): TExpression {
return this.factory.createNewExpression(
ast.classExpr.visitExpression(this, context),
Expand Down Expand Up @@ -202,13 +216,14 @@ export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.E
}

const localizeTag = this.factory.createIdentifier('$localize');
return this.setSourceMapRange(
this.createTaggedTemplateExpression(localizeTag, {elements, expressions}), ast.sourceSpan);
}

// Now choose which implementation to use to actually create the necessary AST nodes.
const localizeCall = this.downlevelLocalizedStrings ?
this.createES5TaggedTemplateFunctionCall(localizeTag, {elements, expressions}) :
this.factory.createTaggedTemplate(localizeTag, {elements, expressions});

return this.setSourceMapRange(localizeCall, ast.sourceSpan);
private createTaggedTemplateExpression(tag: TExpression, template: TemplateLiteral<TExpression>):
TExpression {
return this.downlevelTaggedTemplates ? this.createES5TaggedTemplateFunctionCall(tag, template) :
this.factory.createTaggedTemplate(tag, template);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ export class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor
throw new Error('Method not implemented.');
}

visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): never {
throw new Error('Method not implemented.');
}

visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): never {
throw new Error('Method not implemented.');
}
Expand Down
6 changes: 5 additions & 1 deletion packages/compiler-cli/src/transformers/node_emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

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';
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';
import * as ts from 'typescript';

import {attachComments} from '../ngtsc/translator';
Expand Down Expand Up @@ -544,6 +544,10 @@ export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
expr.args.map(arg => arg.visitExpression(this, null))));
}

visitTaggedTemplateExpr(expr: TaggedTemplateExpr): RecordedNode<ts.TaggedTemplateExpression> {
throw new Error('tagged templates are not supported in pre-ivy mode.');
}

visitInstantiateExpr(expr: InstantiateExpr): RecordedNode<ts.NewExpression> {
return this.postProcess(
expr,
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export * from './ml_parser/tags';
export {LexerRange} from './ml_parser/lexer';
export * from './ml_parser/xml_parser';
export {NgModuleCompiler} from './ng_module_compiler';
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';
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';
export {EmitterVisitorContext} from './output/abstract_emitter';
export {JitEvaluator} from './output/output_jit';
export * from './output/ts_emitter';
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/constant_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ class KeyVisitor implements o.ExpressionVisitor {
visitWritePropExpr = invalid;
visitInvokeMethodExpr = invalid;
visitInvokeFunctionExpr = invalid;
visitTaggedTemplateExpr = invalid;
visitInstantiateExpr = invalid;
visitConditionalExpr = invalid;
visitNotExpr = invalid;
Expand Down
11 changes: 11 additions & 0 deletions packages/compiler/src/output/abstract_emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,17 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
ctx.print(expr, `)`);
return null;
}
visitTaggedTemplateExpr(expr: o.TaggedTemplateExpr, ctx: EmitterVisitorContext): any {
expr.tag.visitExpression(this, ctx);
ctx.print(expr, '`' + expr.template.elements[0].rawText);
for (let i = 1; i < expr.template.elements.length; i++) {
ctx.print(expr, '${');
expr.template.expressions[i - 1].visitExpression(this, ctx);
ctx.print(expr, `}${expr.template.elements[i].rawText}`);
}
ctx.print(expr, '`');
return null;
}
visitWrappedNodeExpr(ast: o.WrappedNodeExpr<any>, ctx: EmitterVisitorContext): any {
throw new Error('Abstract emitter cannot visit WrappedNodeExpr.');
}
Expand Down
50 changes: 37 additions & 13 deletions packages/compiler/src/output/abstract_js_emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@
import {AbstractEmitterVisitor, CATCH_ERROR_VAR, CATCH_STACK_VAR, EmitterVisitorContext, escapeIdentifier} from './abstract_emitter';
import * as o from './output_ast';

/**
* In TypeScript, tagged template functions expect a "template object", which is an array of
* "cooked" strings plus a `raw` property that contains an array of "raw" strings. This is
* typically constructed with a function called `__makeTemplateObject(cooked, raw)`, but it may not
* be available in all environments.
*
* This is a JavaScript polyfill that uses __makeTemplateObject when it's available, but otherwise
* creates an inline helper with the same functionality.
*
* In the inline function, if `Object.defineProperty` is available we use that to attach the `raw`
* array.
*/
const makeTemplateObjectPolyfill =
'(this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's strange that we need to provide this polyfill ourselves.

Shouldn't it come from tslib?

Could we check if the current file has import for this method and if not add an import from tslib? Is that feasible @petebacondarwin @alxhub ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this PR were aimed at Ivy only, I think it would definitely be possible. The way to do this would be to treat it as an ExternalExpr that refers to {moduleName: 'tslib', name: 'makeTemplateObject'} (or whatever the tslib export is).

Then, we'll have to ensure that in JIT mode (which is where AbstractJsEmitterVisitor ends up being used in Ivy), that external reference will have be made resolvable by the R3JitReflector to the real tslib function.

View Engine might be able to handle emitting such an import, but I've not ever tried that so I don't know for sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR was originally sent we've now decided not to support Trusted Types in ViewEngine, and I believe the $localize tag was already Ivy-only. So I don't think there's anything preventing us from dropping the ViewEngine-specific parts from this PR and following @alxhub's proposal. I'd be happy to see how far I can take this but might need some guidance with the R3JitReflector part.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, how about this - let's remove the VE code paths and land this PR as is. @bjarkler can the follow up with @alxhub on fixing the polyfill issue in a separate PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good @IgorMinar. I've now removed the VE-specific code.


export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
constructor() {
super(false);
Expand Down Expand Up @@ -115,6 +130,27 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
}
return null;
}
visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, ctx: EmitterVisitorContext): any {
// The following convoluted piece of code is effectively the downlevelled equivalent of
// ```
// tag`...`
// ```
// which is effectively like:
// ```
// tag(__makeTemplateObject(cooked, raw), expression1, expression2, ...);
// ```
const elements = ast.template.elements;
ast.tag.visitExpression(this, ctx);
ctx.print(ast, `(${makeTemplateObjectPolyfill}(`);
ctx.print(ast, `[${elements.map(part => escapeIdentifier(part.text, false)).join(', ')}], `);
ctx.print(ast, `[${elements.map(part => escapeIdentifier(part.rawText, false)).join(', ')}])`);
ast.template.expressions.forEach(expression => {
ctx.print(ast, ', ');
expression.visitExpression(this, ctx);
});
ctx.print(ast, ')');
return null;
}
visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any {
ctx.print(ast, `function${ast.name ? ' ' + ast.name : ''}(`);
this._visitParams(ast.params, ctx);
Expand Down Expand Up @@ -161,19 +197,7 @@ export abstract class AbstractJsEmitterVisitor extends AbstractEmitterVisitor {
// ```
// $localize(__makeTemplateObject(cooked, raw), expression1, expression2, ...);
// ```
//
// The `$localize` function expects a "template object", which is an array of "cooked" strings
// plus a `raw` property that contains an array of "raw" strings.
//
// In some environments a helper function called `__makeTemplateObject(cooked, raw)` might be
// available, in which case we use that. Otherwise we must create our own helper function
// inline.
//
// In the inline function, if `Object.defineProperty` is available we use that to attach the
// `raw` array.
ctx.print(
ast,
'$localize((this&&this.__makeTemplateObject||function(e,t){return Object.defineProperty?Object.defineProperty(e,"raw",{value:t}):e.raw=t,e})(');
ctx.print(ast, `$localize(${makeTemplateObjectPolyfill}(`);
const parts = [ast.serializeI18nHead()];
for (let i = 1; i < ast.messageParts.length; i++) {
parts.push(ast.serializeI18nTemplatePart(i));
Expand Down
Loading