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()) {
|
if (cooked.isCallExpression()) {
|
||||||
const arg2 = cooked.get('arguments')[1];
|
let call = cooked;
|
||||||
if (!arg2.isExpression()) {
|
if (call.get('arguments').length === 0) {
|
||||||
throw new BabelParseError(
|
// No arguments so perhaps it is a `__templateObject()` call.
|
||||||
arg2.node,
|
// Unwrap this to get the `_taggedTemplateLiteral(cooked, raw)` call.
|
||||||
'Unexpected `raw` argument to the "makeTemplateObject()" function (expected an expression).');
|
call = unwrapLazyLoadHelperCall(call);
|
||||||
}
|
}
|
||||||
raw = arg2;
|
|
||||||
cooked = cooked.get('arguments')[0];
|
cooked = call.get('arguments')[0];
|
||||||
if (!cooked.isExpression()) {
|
if (!cooked.isExpression()) {
|
||||||
throw new BabelParseError(
|
throw new BabelParseError(
|
||||||
cooked.node,
|
cooked.node,
|
||||||
'Unexpected `cooked` argument to the "makeTemplateObject()" function (expected an expression).');
|
'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);
|
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);
|
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?
|
* Is the given `node` an array of literal strings?
|
||||||
*
|
*
|
||||||
|
|
|
@ -103,6 +103,33 @@ describe('makeEs5Plugin', () => {
|
||||||
expect(output.code).toEqual('"try" + (40 + 2) + "me";');
|
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',
|
it('should add diagnostic error with code-frame information if the arguments to `$localize` are missing',
|
||||||
() => {
|
() => {
|
||||||
const diagnostics = new Diagnostics();
|
const diagnostics = new Diagnostics();
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {ɵmakeTemplateObject} from '@angular/localize';
|
||||||
import {NodePath, transformSync} from '@babel/core';
|
import {NodePath, transformSync} from '@babel/core';
|
||||||
import generate from '@babel/generator';
|
import generate from '@babel/generator';
|
||||||
import template from '@babel/template';
|
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';
|
import {isGlobalIdentifier, isNamedIdentifier, isStringLiteralArray, isArrayOfExpressions, unwrapStringLiteralArray, unwrapMessagePartsFromLocalizeCall, wrapInParensIfNecessary, buildLocalizeReplacement, unwrapSubstitutionsFromLocalizeCall, unwrapMessagePartsFromTemplateLiteral} from '../../../src/translate/source_files/source_file_utils';
|
||||||
|
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
|
@ -71,18 +71,45 @@ describe('utils', () => {
|
||||||
|
|
||||||
describe('unwrapMessagePartsFromLocalizeCall', () => {
|
describe('unwrapMessagePartsFromLocalizeCall', () => {
|
||||||
it('should return an array of string literals from a direct call to a tag function', () => {
|
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 localizeCall = getLocalizeCall(`$localize(['a', 'b\\t', 'c'], 1, 2)`);
|
||||||
const parts = unwrapMessagePartsFromLocalizeCall(call);
|
const parts = unwrapMessagePartsFromLocalizeCall(localizeCall);
|
||||||
expect(parts).toEqual(['a', 'b\t', 'c']);
|
expect(parts).toEqual(['a', 'b\t', 'c']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an array of string literals from a downleveled tagged template', () => {
|
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)`);
|
`$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).toEqual(['a', 'b\t', 'c']);
|
||||||
expect(parts.raw).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', () => {
|
describe('unwrapSubstitutionsFromLocalizeCall', () => {
|
||||||
|
@ -180,20 +207,15 @@ function collectExpressionsPlugin() {
|
||||||
return {expressions, plugin: {visitor}};
|
return {expressions, plugin: {visitor}};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFirstCallExpression(code: string): NodePath<CallExpression> {
|
function getLocalizeCall(code: string): NodePath<CallExpression> {
|
||||||
let callPath: NodePath<CallExpression>|undefined = undefined;
|
let callPaths: NodePath<CallExpression>[] = [];
|
||||||
transformSync(code, {
|
transformSync(code, {plugins: [{visitor: {CallExpression(path) { callPaths.push(path); }}}]});
|
||||||
plugins: [{
|
const localizeCall = callPaths.find(p => {
|
||||||
visitor: {
|
const callee = p.get('callee');
|
||||||
CallExpression(path) {
|
return (callee.isIdentifier() && callee.node.name === '$localize');
|
||||||
callPath = path;
|
|
||||||
path.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
});
|
});
|
||||||
if (callPath === undefined) {
|
if (!localizeCall) {
|
||||||
throw new Error('CallExpression not found in code:' + code);
|
throw new Error(`$localize cannot be found in ${code}`);
|
||||||
}
|
}
|
||||||
return callPath;
|
return localizeCall;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue