UNPKG

@formatjs/ts-transformer

Version:

TS Compiler transformer for formatjs

516 lines (515 loc) 20.5 kB
"use strict"; 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); }