fix(ivy): i18n - support lazy-load template string helpers (#33097)
There are numerous approaches to downlevelling backticked template strings to ES5. This commit handles yet another one that Babel applies. PR Close #33097
This commit is contained in:
parent
1845faa66b
commit
83425fa119
|
@ -79,21 +79,29 @@ export function unwrapMessagePartsFromLocalizeCall(call: NodePath<t.CallExpressi
|
|||
}
|
||||
}
|
||||
|
||||
// Check for `__makeTemplateObject(cooked, raw)` call
|
||||
// Check for `__makeTemplateObject(cooked, raw)` or `__templateObject()` calls.
|
||||
if (cooked.isCallExpression()) {
|
||||
const arg2 = cooked.get('arguments')[1];
|
||||
if (!arg2.isExpression()) {
|
||||
throw new BabelParseError(
|
||||
arg2.node,
|
||||
'Unexpected `raw` argument to the "makeTemplateObject()" function (expected an expression).');
|
||||
let call = cooked;
|
||||
if (call.get('arguments').length === 0) {
|
||||
// No arguments so perhaps it is a `__templateObject()` call.
|
||||
// Unwrap this to get the `_taggedTemplateLiteral(cooked, raw)` call.
|
||||
call = unwrapLazyLoadHelperCall(call);
|
||||
}
|
||||
raw = arg2;
|
||||
cooked = cooked.get('arguments')[0];
|
||||
|
||||
cooked = call.get('arguments')[0];
|
||||
if (!cooked.isExpression()) {
|
||||
throw new BabelParseError(
|
||||
cooked.node,
|
||||
'Unexpected `cooked` argument to the "makeTemplateObject()" function (expected an expression).');
|
||||
}
|
||||
const arg2 = call.get('arguments')[1];
|
||||
if (arg2 && !arg2.isExpression()) {
|
||||
throw new BabelParseError(
|
||||
arg2.node,
|
||||
'Unexpected `raw` argument to the "makeTemplateObject()" function (expected an expression).');
|
||||
}
|
||||
// If there is no second argument then assume that raw and cooked are the same
|
||||
raw = arg2 !== undefined ? arg2 : cooked;
|
||||
}
|
||||
|
||||
const cookedStrings = unwrapStringLiteralArray(cooked.node);
|
||||
|
@ -153,6 +161,93 @@ export function unwrapStringLiteralArray(array: t.Expression): string[] {
|
|||
return array.elements.map((str: t.StringLiteral) => str.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* This expression is believed to be a call to a "lazy-load" template object helper function.
|
||||
* This is expected to be of the form:
|
||||
*
|
||||
* ```ts
|
||||
* function _templateObject() {
|
||||
* var e = _taggedTemplateLiteral(['cooked string', 'raw string']);
|
||||
* return _templateObject = function() { return e }, e
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* We unwrap this to return the call to `_taggedTemplateLiteral()`.
|
||||
*
|
||||
* @param call the call expression to unwrap
|
||||
* @returns the call expression
|
||||
*/
|
||||
export function unwrapLazyLoadHelperCall(call: NodePath<t.CallExpression>):
|
||||
NodePath<t.CallExpression> {
|
||||
const callee = call.get('callee');
|
||||
if (!callee.isIdentifier()) {
|
||||
throw new BabelParseError(
|
||||
callee.node,
|
||||
'Unexpected lazy-load helper call (expected a call of the form `_templateObject()`).');
|
||||
}
|
||||
const lazyLoadBinding = call.scope.getBinding(callee.node.name);
|
||||
if (!lazyLoadBinding) {
|
||||
throw new BabelParseError(callee.node, 'Missing declaration for lazy-load helper function');
|
||||
}
|
||||
const lazyLoadFn = lazyLoadBinding.path;
|
||||
if (!lazyLoadFn.isFunctionDeclaration()) {
|
||||
throw new BabelParseError(
|
||||
lazyLoadFn.node, 'Unexpected expression (expected a function declaration');
|
||||
}
|
||||
const returnedNode = getReturnedExpression(lazyLoadFn);
|
||||
|
||||
if (returnedNode.isCallExpression()) {
|
||||
return returnedNode;
|
||||
}
|
||||
|
||||
if (returnedNode.isIdentifier()) {
|
||||
const identifierName = returnedNode.node.name;
|
||||
const declaration = returnedNode.scope.getBinding(identifierName);
|
||||
if (declaration === undefined) {
|
||||
throw new BabelParseError(
|
||||
returnedNode.node, 'Missing declaration for return value from helper.');
|
||||
}
|
||||
if (!declaration.path.isVariableDeclarator()) {
|
||||
throw new BabelParseError(
|
||||
declaration.path.node,
|
||||
'Unexpected helper return value declaration (expected a variable declaration).');
|
||||
}
|
||||
const initializer = declaration.path.get('init');
|
||||
if (!initializer.isCallExpression()) {
|
||||
throw new BabelParseError(
|
||||
declaration.path.node,
|
||||
'Unexpected return value from helper (expected a call expression).');
|
||||
}
|
||||
|
||||
// Remove the lazy load helper if this is the only reference to it.
|
||||
if (lazyLoadBinding.references === 1) {
|
||||
lazyLoadFn.remove();
|
||||
}
|
||||
|
||||
return initializer;
|
||||
}
|
||||
return call;
|
||||
}
|
||||
|
||||
function getReturnedExpression(fn: NodePath<t.FunctionDeclaration>): NodePath<t.Expression> {
|
||||
const bodyStatements = fn.get('body').get('body');
|
||||
for (const statement of bodyStatements) {
|
||||
if (statement.isReturnStatement()) {
|
||||
const argument = statement.get('argument');
|
||||
if (argument.isSequenceExpression()) {
|
||||
const expressions = argument.get('expressions');
|
||||
return Array.isArray(expressions) ? expressions[expressions.length - 1] : expressions;
|
||||
} else if (argument.isExpression()) {
|
||||
return argument;
|
||||
} else {
|
||||
throw new BabelParseError(
|
||||
statement.node, 'Invalid return argument in helper function (expected an expression).');
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new BabelParseError(fn.node, 'Missing return statement in helper function.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the given `node` an array of literal strings?
|
||||
*
|
||||
|
|
|
@ -103,6 +103,33 @@ describe('makeEs5Plugin', () => {
|
|||
expect(output.code).toEqual('"try" + (40 + 2) + "me";');
|
||||
});
|
||||
|
||||
it('should handle lazy-load helper calls', () => {
|
||||
const diagnostics = new Diagnostics();
|
||||
const input = `
|
||||
function _templateObject2() {
|
||||
var e = _taggedTemplateLiteral([':escaped-colons:Welcome to the i18n app.'], ['\\\\\\:escaped-colons:Welcome to the i18n app.']);
|
||||
return _templateObject2 = function() { return e }, e
|
||||
}
|
||||
function _templateObject() {
|
||||
var e = _taggedTemplateLiteral([' Hello ', ':INTERPOLATION:! ']);
|
||||
return _templateObject = function() { return e }, e
|
||||
}
|
||||
function _taggedTemplateLiteral(e, t) {
|
||||
return t || (t = e.slice(0)),
|
||||
Object.freeze(Object.defineProperties(e, {raw: {value: Object.freeze(t)}}))
|
||||
}
|
||||
const message = $localize(_templateObject2());
|
||||
function foo() {
|
||||
console.log($localize(_templateObject(), '\ufffd0\ufffd'));
|
||||
}
|
||||
`;
|
||||
const output = transformSync(input, {plugins: [makeEs5TranslatePlugin(diagnostics, {})]}) !;
|
||||
expect(output.code).toContain('const message = ":escaped-colons:Welcome to the i18n app."');
|
||||
expect(output.code).toContain('console.log(" Hello " + \'\ufffd0\ufffd\' + "! ");');
|
||||
expect(output.code).not.toContain('templateObject');
|
||||
expect(output.code).not.toContain('templateObject2');
|
||||
});
|
||||
|
||||
it('should add diagnostic error with code-frame information if the arguments to `$localize` are missing',
|
||||
() => {
|
||||
const diagnostics = new Diagnostics();
|
||||
|
|
|
@ -9,7 +9,7 @@ import {ɵmakeTemplateObject} from '@angular/localize';
|
|||
import {NodePath, transformSync} from '@babel/core';
|
||||
import generate from '@babel/generator';
|
||||
import template from '@babel/template';
|
||||
import {Expression, Identifier, TaggedTemplateExpression, ExpressionStatement, FunctionDeclaration, CallExpression, isParenthesizedExpression, numericLiteral, binaryExpression, NumericLiteral, traverse} from '@babel/types';
|
||||
import {Expression, Identifier, TaggedTemplateExpression, ExpressionStatement, FunctionDeclaration, CallExpression, isParenthesizedExpression, numericLiteral, binaryExpression, NumericLiteral} from '@babel/types';
|
||||
import {isGlobalIdentifier, isNamedIdentifier, isStringLiteralArray, isArrayOfExpressions, unwrapStringLiteralArray, unwrapMessagePartsFromLocalizeCall, wrapInParensIfNecessary, buildLocalizeReplacement, unwrapSubstitutionsFromLocalizeCall, unwrapMessagePartsFromTemplateLiteral} from '../../../src/translate/source_files/source_file_utils';
|
||||
|
||||
describe('utils', () => {
|
||||
|
@ -71,18 +71,45 @@ describe('utils', () => {
|
|||
|
||||
describe('unwrapMessagePartsFromLocalizeCall', () => {
|
||||
it('should return an array of string literals from a direct call to a tag function', () => {
|
||||
const call = getFirstCallExpression(`$localize(['a', 'b\\t', 'c'], 1, 2)`);
|
||||
const parts = unwrapMessagePartsFromLocalizeCall(call);
|
||||
const localizeCall = getLocalizeCall(`$localize(['a', 'b\\t', 'c'], 1, 2)`);
|
||||
const parts = unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||
expect(parts).toEqual(['a', 'b\t', 'c']);
|
||||
});
|
||||
|
||||
it('should return an array of string literals from a downleveled tagged template', () => {
|
||||
let call = getFirstCallExpression(
|
||||
let localizeCall = getLocalizeCall(
|
||||
`$localize(__makeTemplateObject(['a', 'b\\t', 'c'], ['a', 'b\\\\t', 'c']), 1, 2)`);
|
||||
const parts = unwrapMessagePartsFromLocalizeCall(call);
|
||||
const parts = unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||
expect(parts).toEqual(['a', 'b\t', 'c']);
|
||||
expect(parts.raw).toEqual(['a', 'b\\t', 'c']);
|
||||
});
|
||||
|
||||
it('should return an array of string literals from a lazy load template helper', () => {
|
||||
let localizeCall = getLocalizeCall(`
|
||||
function _templateObject() {
|
||||
var e = _taggedTemplateLiteral(['a', 'b', 'c'], ['a', 'b', 'c']);
|
||||
return _templateObject = function() { return e }, e
|
||||
}
|
||||
$localize(_templateObject(), 1, 2)`);
|
||||
const parts = unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||
expect(parts).toEqual(['a', 'b', 'c']);
|
||||
expect(parts.raw).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should remove a lazy load template helper', () => {
|
||||
let localizeCall = getLocalizeCall(`
|
||||
function _templateObject() {
|
||||
var e = _taggedTemplateLiteral(['a', 'b', 'c'], ['a', 'b', 'c']);
|
||||
return _templateObject = function() { return e }, e
|
||||
}
|
||||
$localize(_templateObject(), 1, 2)`);
|
||||
const localizeStatement = localizeCall.parentPath as NodePath<ExpressionStatement>;
|
||||
const statements = localizeStatement.container as object[];
|
||||
expect(statements.length).toEqual(2);
|
||||
unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||
expect(statements.length).toEqual(1);
|
||||
expect(statements[0]).toBe(localizeStatement.node);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwrapSubstitutionsFromLocalizeCall', () => {
|
||||
|
@ -180,20 +207,15 @@ function collectExpressionsPlugin() {
|
|||
return {expressions, plugin: {visitor}};
|
||||
}
|
||||
|
||||
function getFirstCallExpression(code: string): NodePath<CallExpression> {
|
||||
let callPath: NodePath<CallExpression>|undefined = undefined;
|
||||
transformSync(code, {
|
||||
plugins: [{
|
||||
visitor: {
|
||||
CallExpression(path) {
|
||||
callPath = path;
|
||||
path.stop();
|
||||
}
|
||||
}
|
||||
}]
|
||||
function getLocalizeCall(code: string): NodePath<CallExpression> {
|
||||
let callPaths: NodePath<CallExpression>[] = [];
|
||||
transformSync(code, {plugins: [{visitor: {CallExpression(path) { callPaths.push(path); }}}]});
|
||||
const localizeCall = callPaths.find(p => {
|
||||
const callee = p.get('callee');
|
||||
return (callee.isIdentifier() && callee.node.name === '$localize');
|
||||
});
|
||||
if (callPath === undefined) {
|
||||
throw new Error('CallExpression not found in code:' + code);
|
||||
if (!localizeCall) {
|
||||
throw new Error(`$localize cannot be found in ${code}`);
|
||||
}
|
||||
return callPath;
|
||||
return localizeCall;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue