@formatjs/ts-transformer
Version:
TS Compiler transformer for formatjs
516 lines (515 loc) • 20.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.transformWithTs = transformWithTs;
exports.transform = transform;
const tslib_1 = require("tslib");
const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
const json_stable_stringify_1 = tslib_1.__importDefault(require("json-stable-stringify"));
const typescript = tslib_1.__importStar(require("typescript"));
const console_utils_1 = require("./console_utils");
const interpolate_name_1 = require("./interpolate-name");
const MESSAGE_DESC_KEYS = [
'id',
'defaultMessage',
'description',
];
function primitiveToTSNode(factory, v) {
return typeof v === 'string'
? factory.createStringLiteral(v)
: typeof v === 'number'
? factory.createNumericLiteral(v + '')
: typeof v === 'boolean'
? v
? factory.createTrue()
: factory.createFalse()
: undefined;
}
function isValidIdentifier(k) {
try {
new Function(`return {${k}:1}`);
return true;
}
catch (e) {
return false;
}
}
function objToTSNode(factory, obj) {
if (typeof obj === 'object' && !obj) {
return factory.createNull();
}
const props = Object.entries(obj)
.filter(([_, v]) => typeof v !== 'undefined')
.map(([k, v]) => factory.createPropertyAssignment(isValidIdentifier(k) ? k : factory.createStringLiteral(k), primitiveToTSNode(factory, v) ||
(Array.isArray(v)
? factory.createArrayLiteralExpression(v
.filter(n => typeof n !== 'undefined')
.map(n => objToTSNode(factory, n)))
: objToTSNode(factory, v))));
return factory.createObjectLiteralExpression(props);
}
function messageASTToTSNode(factory, ast) {
return factory.createArrayLiteralExpression(ast.map(el => objToTSNode(factory, el)));
}
function literalToObj(ts, n) {
if (ts.isNumericLiteral(n)) {
return +n.text;
}
if (ts.isStringLiteral(n)) {
return n.text;
}
if (n.kind === ts.SyntaxKind.TrueKeyword) {
return true;
}
if (n.kind === ts.SyntaxKind.FalseKeyword) {
return false;
}
}
function objectLiteralExpressionToObj(ts, obj) {
return obj.properties.reduce((all, prop) => {
if (ts.isPropertyAssignment(prop) && prop.name) {
if (ts.isIdentifier(prop.name)) {
all[prop.name.escapedText.toString()] = literalToObj(ts, prop.initializer);
}
else if (ts.isStringLiteral(prop.name)) {
all[prop.name.text] = literalToObj(ts, prop.initializer);
}
}
return all;
}, {});
}
const DEFAULT_OPTS = {
onMsgExtracted: () => undefined,
onMetaExtracted: () => undefined,
};
function isMultipleMessageDecl(ts, node) {
return (ts.isIdentifier(node.expression) &&
node.expression.text === 'defineMessages');
}
function isSingularMessageDecl(ts, node, additionalComponentNames) {
const compNames = new Set([
'FormattedMessage',
'defineMessage',
'formatMessage',
'$formatMessage',
'$t',
...additionalComponentNames,
]);
let fnName = '';
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
fnName = node.expression.text;
}
else if (ts.isJsxOpeningElement(node) && ts.isIdentifier(node.tagName)) {
fnName = node.tagName.text;
}
else if (ts.isJsxSelfClosingElement(node) &&
ts.isIdentifier(node.tagName)) {
fnName = node.tagName.text;
}
return compNames.has(fnName);
}
function evaluateStringConcat(ts, node) {
const { right, left } = node;
if (!ts.isStringLiteral(right)) {
return ['', false];
}
if (ts.isStringLiteral(left)) {
return [left.text + right.text, true];
}
if (ts.isBinaryExpression(left)) {
const [result, isStatic] = evaluateStringConcat(ts, left);
return [result + right.text, isStatic];
}
return ['', false];
}
function extractMessageDescriptor(ts, node, { overrideIdFn, extractSourceLocation, preserveWhitespace }, sf) {
let properties = undefined;
if (ts.isObjectLiteralExpression(node)) {
properties = node.properties;
}
else if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
properties = node.attributes.properties;
}
const msg = { id: '' };
if (!properties) {
return;
}
properties.forEach(prop => {
const { name } = prop;
const initializer = ts.isPropertyAssignment(prop) || ts.isJsxAttribute(prop)
? prop.initializer
: undefined;
if (name && ts.isIdentifier(name) && initializer) {
// {id: 'id'}
if (ts.isStringLiteral(initializer)) {
switch (name.text) {
case 'id':
msg.id = initializer.text;
break;
case 'defaultMessage':
msg.defaultMessage = initializer.text;
break;
case 'description':
msg.description = initializer.text;
break;
}
}
// {id: `id`}
else if (ts.isNoSubstitutionTemplateLiteral(initializer)) {
switch (name.text) {
case 'id':
msg.id = initializer.text;
break;
case 'defaultMessage':
msg.defaultMessage = initializer.text;
break;
case 'description':
msg.description = initializer.text;
break;
}
}
// {id: dedent`id`}
else if (ts.isTaggedTemplateExpression(initializer)) {
const { template } = initializer;
if (!ts.isNoSubstitutionTemplateLiteral(template)) {
throw new Error('Tagged template expression must be no substitution');
}
switch (name.text) {
case 'id':
msg.id = template.text;
break;
case 'defaultMessage':
msg.defaultMessage = template.text;
break;
case 'description':
msg.description = template.text;
break;
}
}
else if (ts.isJsxExpression(initializer) && initializer.expression) {
// <FormattedMessage foo={'barbaz'} />
if (ts.isStringLiteral(initializer.expression)) {
switch (name.text) {
case 'id':
msg.id = initializer.expression.text;
break;
case 'defaultMessage':
msg.defaultMessage = initializer.expression.text;
break;
case 'description':
msg.description = initializer.expression.text;
break;
}
}
// description={{custom: 1}}
else if (ts.isObjectLiteralExpression(initializer.expression) &&
name.text === 'description') {
msg.description = objectLiteralExpressionToObj(ts, initializer.expression);
}
// <FormattedMessage foo={`bar`} />
else if (ts.isNoSubstitutionTemplateLiteral(initializer.expression)) {
const { expression } = initializer;
switch (name.text) {
case 'id':
msg.id = expression.text;
break;
case 'defaultMessage':
msg.defaultMessage = expression.text;
break;
case 'description':
msg.description = expression.text;
break;
}
}
// <FormattedMessage foo={dedent`dedent Hello World!`} />
else if (ts.isTaggedTemplateExpression(initializer.expression)) {
const { expression: { template }, } = initializer;
if (!ts.isNoSubstitutionTemplateLiteral(template)) {
throw new Error('Tagged template expression must be no substitution');
}
switch (name.text) {
case 'id':
msg.id = template.text;
break;
case 'defaultMessage':
msg.defaultMessage = template.text;
break;
case 'description':
msg.description = template.text;
break;
}
}
// <FormattedMessage foo={'bar' + 'baz'} />
else if (ts.isBinaryExpression(initializer.expression)) {
const { expression } = initializer;
const [result, isStatic] = evaluateStringConcat(ts, expression);
if (isStatic) {
switch (name.text) {
case 'id':
msg.id = result;
break;
case 'defaultMessage':
msg.defaultMessage = result;
break;
case 'description':
msg.description = result;
break;
}
}
}
}
// {defaultMessage: 'asd' + bar'}
else if (ts.isBinaryExpression(initializer)) {
const [result, isStatic] = evaluateStringConcat(ts, initializer);
if (isStatic) {
switch (name.text) {
case 'id':
msg.id = result;
break;
case 'defaultMessage':
msg.defaultMessage = result;
break;
case 'description':
msg.description = result;
break;
}
}
}
// description: {custom: 1}
else if (ts.isObjectLiteralExpression(initializer) &&
name.text === 'description') {
msg.description = objectLiteralExpressionToObj(ts, initializer);
}
}
});
// We extracted nothing
if (!msg.defaultMessage && !msg.id) {
return;
}
if (msg.defaultMessage && !preserveWhitespace) {
msg.defaultMessage = msg.defaultMessage.trim().replace(/\s+/gm, ' ');
}
if (msg.defaultMessage && overrideIdFn) {
switch (typeof overrideIdFn) {
case 'string':
if (!msg.id) {
msg.id = (0, interpolate_name_1.interpolateName)({ resourcePath: sf.fileName }, overrideIdFn, {
content: msg.description
? `${msg.defaultMessage}#${typeof msg.description === 'string'
? msg.description
: (0, json_stable_stringify_1.default)(msg.description)}`
: msg.defaultMessage,
});
}
break;
case 'function':
msg.id = overrideIdFn(msg.id, msg.defaultMessage, msg.description, sf.fileName);
break;
}
}
if (extractSourceLocation) {
return {
...msg,
file: sf.fileName,
start: node.pos,
end: node.end,
};
}
return msg;
}
/**
* Check if node is `foo.bar.formatMessage` node
* @param node
* @param sf
*/
function isMemberMethodFormatMessageCall(ts, node, additionalFunctionNames) {
const fnNames = new Set([
'formatMessage',
'$formatMessage',
...additionalFunctionNames,
]);
const method = node.expression;
// Handle foo.formatMessage()
if (ts.isPropertyAccessExpression(method)) {
return fnNames.has(method.name.text);
}
// Handle formatMessage()
return ts.isIdentifier(method) && fnNames.has(method.text);
}
function extractMessageFromJsxComponent(ts, factory, node, opts, sf) {
const { onMsgExtracted } = opts;
if (!isSingularMessageDecl(ts, node, opts.additionalComponentNames || [])) {
return node;
}
const msg = extractMessageDescriptor(ts, node, opts, sf);
if (!msg) {
return node;
}
if (typeof onMsgExtracted === 'function') {
onMsgExtracted(sf.fileName, [msg]);
}
const newProps = generateNewProperties(ts, factory, node.attributes, {
defaultMessage: opts.removeDefaultMessage
? undefined
: msg.defaultMessage,
id: msg.id,
}, opts.ast);
if (ts.isJsxOpeningElement(node)) {
return factory.updateJsxOpeningElement(node, node.tagName, node.typeArguments, factory.createJsxAttributes(newProps));
}
return factory.updateJsxSelfClosingElement(node, node.tagName, node.typeArguments, factory.createJsxAttributes(newProps));
}
function setAttributesInObject(ts, factory, node, msg, ast) {
const newProps = [
factory.createPropertyAssignment('id', factory.createStringLiteral(msg.id)),
...(msg.defaultMessage
? [
factory.createPropertyAssignment('defaultMessage', ast
? messageASTToTSNode(factory, (0, icu_messageformat_parser_1.parse)(msg.defaultMessage))
: factory.createStringLiteral(msg.defaultMessage)),
]
: []),
];
for (const prop of node.properties) {
if (ts.isPropertyAssignment(prop) &&
ts.isIdentifier(prop.name) &&
MESSAGE_DESC_KEYS.includes(prop.name.text)) {
continue;
}
if (ts.isPropertyAssignment(prop)) {
newProps.push(prop);
}
}
return factory.createObjectLiteralExpression(factory.createNodeArray(newProps));
}
function generateNewProperties(ts, factory, node, msg, ast) {
const newProps = [
factory.createJsxAttribute(factory.createIdentifier('id'), factory.createStringLiteral(msg.id)),
...(msg.defaultMessage
? [
factory.createJsxAttribute(factory.createIdentifier('defaultMessage'), ast
? factory.createJsxExpression(undefined, messageASTToTSNode(factory, (0, icu_messageformat_parser_1.parse)(msg.defaultMessage)))
: factory.createStringLiteral(msg.defaultMessage)),
]
: []),
];
for (const prop of node.properties) {
if (ts.isJsxAttribute(prop) &&
ts.isIdentifier(prop.name) &&
MESSAGE_DESC_KEYS.includes(prop.name.text)) {
continue;
}
if (ts.isJsxAttribute(prop)) {
newProps.push(prop);
}
}
return newProps;
}
function extractMessagesFromCallExpression(ts, factory, node, opts, sf) {
const { onMsgExtracted, additionalFunctionNames } = opts;
if (isMultipleMessageDecl(ts, node)) {
const [arg, ...restArgs] = node.arguments;
let descriptorsObj;
if (ts.isObjectLiteralExpression(arg)) {
descriptorsObj = arg;
}
else if (ts.isAsExpression(arg) &&
ts.isObjectLiteralExpression(arg.expression)) {
descriptorsObj = arg.expression;
}
if (descriptorsObj) {
const properties = descriptorsObj.properties;
const msgs = properties
.filter((prop) => ts.isPropertyAssignment(prop))
.map(prop => ts.isObjectLiteralExpression(prop.initializer) &&
extractMessageDescriptor(ts, prop.initializer, opts, sf))
.filter((msg) => !!msg);
if (!msgs.length) {
return node;
}
(0, console_utils_1.debug)('Multiple messages extracted from "%s": %s', sf.fileName, msgs);
if (typeof onMsgExtracted === 'function') {
onMsgExtracted(sf.fileName, msgs);
}
const clonedProperties = factory.createNodeArray(properties.map((prop, i) => {
if (!ts.isPropertyAssignment(prop) ||
!ts.isObjectLiteralExpression(prop.initializer)) {
return prop;
}
return factory.createPropertyAssignment(prop.name, setAttributesInObject(ts, factory, prop.initializer, {
defaultMessage: opts.removeDefaultMessage
? undefined
: msgs[i].defaultMessage,
id: msgs[i] ? msgs[i].id : '',
}, opts.ast));
}));
const clonedDescriptorsObj = factory.createObjectLiteralExpression(clonedProperties);
return factory.updateCallExpression(node, node.expression, node.typeArguments, [clonedDescriptorsObj, ...restArgs]);
}
}
else if (isSingularMessageDecl(ts, node, opts.additionalComponentNames || []) ||
isMemberMethodFormatMessageCall(ts, node, additionalFunctionNames || [])) {
const [descriptorsObj, ...restArgs] = node.arguments;
if (ts.isObjectLiteralExpression(descriptorsObj)) {
const msg = extractMessageDescriptor(ts, descriptorsObj, opts, sf);
if (!msg) {
return node;
}
(0, console_utils_1.debug)('Message extracted from "%s": %s', sf.fileName, msg);
if (typeof onMsgExtracted === 'function') {
onMsgExtracted(sf.fileName, [msg]);
}
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
setAttributesInObject(ts, factory, descriptorsObj, {
defaultMessage: opts.removeDefaultMessage
? undefined
: msg.defaultMessage,
id: msg.id,
}, opts.ast),
...restArgs,
]);
}
}
return node;
}
const PRAGMA_REGEX = /^\/\/ @([^\s]*) (.*)$/m;
function getVisitor(ts, ctx, sf, opts) {
const visitor = (node) => {
const newNode = ts.isCallExpression(node)
? extractMessagesFromCallExpression(ts, ctx.factory, node, opts, sf)
: ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)
? extractMessageFromJsxComponent(ts, ctx.factory, node, opts, sf)
: node;
return ts.visitEachChild(newNode, visitor, ctx);
};
return visitor;
}
function transformWithTs(ts, opts) {
opts = { ...DEFAULT_OPTS, ...opts };
(0, console_utils_1.debug)('Transforming options', opts);
const transformFn = ctx => {
return sf => {
const pragmaResult = PRAGMA_REGEX.exec(sf.text);
if (pragmaResult) {
(0, console_utils_1.debug)('Pragma found', pragmaResult);
const [, pragma, kvString] = pragmaResult;
if (pragma === opts.pragma) {
const kvs = kvString.split(' ');
const result = {};
for (const kv of kvs) {
const [k, v] = kv.split(':');
result[k] = v;
}
(0, console_utils_1.debug)('Pragma extracted', result);
if (typeof opts.onMetaExtracted === 'function') {
opts.onMetaExtracted(sf.fileName, result);
}
}
}
return ts.visitEachChild(sf, getVisitor(ts, ctx, sf, opts), ctx);
};
};
return transformFn;
}
function transform(opts) {
return transformWithTs(typescript, opts);
}