UNPKG

@lingui/babel-plugin-lingui-macro

Version:

Babel plugin for transforming Lingui Macros

759 lines (747 loc) 25.1 kB
'use strict'; const ast = require('./shared/babel-plugin-lingui-macro.b7bd8916.cjs'); const t = require('@babel/types'); const conf = require('@lingui/conf'); require('@lingui/message-utils/generateMessageId'); function _interopNamespaceCompat(e) { if (e && typeof e === 'object' && 'default' in e) return e; const n = Object.create(null); if (e) { for (const k in e) { n[k] = e[k]; } } n.default = e; return n; } const t__namespace = /*#__PURE__*/_interopNamespaceCompat(t); function cleanJSXElementLiteralChild(value) { const lines = value.split(/\r\n|\n|\r/); let lastNonEmptyLine = 0; for (let i = 0; i < lines.length; i++) { if (lines[i].match(/[^ \t]/)) { lastNonEmptyLine = i; } } let str = ""; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const isFirstLine = i === 0; const isLastLine = i === lines.length - 1; const isLastNonEmptyLine = i === lastNonEmptyLine; let trimmedLine = line.replace(/\t/g, " "); if (!isFirstLine) { trimmedLine = trimmedLine.replace(/^[ ]+/, ""); } if (!isLastLine) { trimmedLine = trimmedLine.replace(/[ ]+$/, ""); } if (trimmedLine) { if (!isLastNonEmptyLine) { trimmedLine += " "; } str += trimmedLine; } } return str; } var __defProp$1 = Object.defineProperty; var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$1 = (obj, key, value) => { __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; const pluralRuleRe = /(_[\d\w]+|zero|one|two|few|many|other)/; const jsx2icuExactChoice = (value) => value.replace(/_(\d+)/, "=$1").replace(/_(\w+)/, "$1"); function maybeNodeValue(node) { if (!node) return null; if (node.type === "StringLiteral") return { text: node.value, loc: node.loc }; if (node.type === "JSXAttribute") return maybeNodeValue(node.value); if (node.type === "JSXExpressionContainer") return maybeNodeValue(node.expression); if (node.type === "TemplateLiteral" && node.expressions.length === 0) return { text: node.quasis[0].value.raw, loc: node.loc }; return null; } class MacroJSX { constructor({ types }, opts) { __publicField$1(this, "types"); __publicField$1(this, "ctx"); __publicField$1(this, "replacePath", (path) => { if (!path.isJSXElement()) { return false; } const tokens = this.tokenizeNode(path, true, true); if (!tokens) { return false; } const { attributes, id, comment, context } = this.stripMacroAttributes( path ); if (!tokens.length) { throw new Error("Incorrect usage of Trans"); } const messageDescriptor = ast.createMessageDescriptorFromTokens( tokens, path.node.loc, this.ctx.stripNonEssentialProps, this.ctx.stripMessageProp, { id, context, comment } ); attributes.push(this.types.jsxSpreadAttribute(messageDescriptor)); const newNode = this.types.jsxElement( this.types.jsxOpeningElement( this.types.jsxIdentifier(this.ctx.transImportName), attributes, true ), null, [], true ); newNode.loc = path.node.loc; return newNode; }); __publicField$1(this, "attrName", (names, exclude = false) => { const namesRe = new RegExp("^(" + names.join("|") + ")$"); return (attr) => { const name = attr.name.name; return exclude ? !namesRe.test(name) : namesRe.test(name); }; }); __publicField$1(this, "stripMacroAttributes", (path) => { const { attributes } = path.node.openingElement; const id = attributes.find(this.attrName([ast.MsgDescriptorPropKey.id])); const message = attributes.find( this.attrName([ast.MsgDescriptorPropKey.message]) ); const comment = attributes.find( this.attrName([ast.MsgDescriptorPropKey.comment]) ); const context = attributes.find( this.attrName([ast.MsgDescriptorPropKey.context]) ); let reserved = [ ast.MsgDescriptorPropKey.id, ast.MsgDescriptorPropKey.message, ast.MsgDescriptorPropKey.comment, ast.MsgDescriptorPropKey.context ]; if (this.isChoiceComponent(path)) { reserved = [ ...reserved, "_\\w+", "_\\d+", "zero", "one", "two", "few", "many", "other", "value", "offset" ]; } return { id: maybeNodeValue(id), message: maybeNodeValue(message), comment: maybeNodeValue(comment), context: maybeNodeValue(context), attributes: attributes.filter(this.attrName(reserved, true)) }; }); __publicField$1(this, "tokenizeNode", (path, ignoreExpression = false, ignoreElement = false) => { if (this.isTransComponent(path)) { return this.tokenizeTrans(path); } const componentName = this.isChoiceComponent(path); if (componentName) { return [ this.tokenizeChoiceComponent( path, componentName ) ]; } if (path.isJSXElement() && !ignoreElement) { return [this.tokenizeElement(path)]; } if (!ignoreExpression) { return [this.tokenizeExpression(path)]; } }); __publicField$1(this, "tokenizeTrans", (path) => { return path.get("children").flatMap((child) => this.tokenizeChildren(child)).filter(Boolean); }); __publicField$1(this, "tokenizeChildren", (path) => { if (path.isJSXExpressionContainer()) { const exp = path.get("expression"); if (exp.isStringLiteral()) { return [this.tokenizeText(exp.node.value)]; } if (exp.isTemplateLiteral()) { return this.tokenizeTemplateLiteral(exp); } if (exp.isConditionalExpression()) { return [this.tokenizeConditionalExpression(exp)]; } if (exp.isJSXElement()) { return this.tokenizeNode(exp); } return [this.tokenizeExpression(exp)]; } else if (path.isJSXElement()) { return this.tokenizeNode(path); } else if (path.isJSXSpreadChild()) { throw new Error( "Incorrect usage of Trans: Spread could not be used as Trans children" ); } else if (path.isJSXText()) { return [this.tokenizeText(cleanJSXElementLiteralChild(path.node.value))]; } else ; }); __publicField$1(this, "tokenizeChoiceComponent", (path, componentName) => { const element = path.get("openingElement"); const format = componentName.toLowerCase(); const props = element.get("attributes").filter((attr) => { return this.attrName( [ ast.MsgDescriptorPropKey.id, ast.MsgDescriptorPropKey.comment, ast.MsgDescriptorPropKey.message, ast.MsgDescriptorPropKey.context, "key", // we remove <Trans /> react props that are not useful for translation "render", "component", "components" ], true )(attr.node); }); let token = { type: "arg", format, name: null, value: void 0, options: { offset: void 0 } }; for (const _attr of props) { if (_attr.isJSXSpreadAttribute()) { continue; } const attr = _attr; if (this.types.isJSXNamespacedName(attr.node.name)) { continue; } const name = attr.node.name.name; const value = attr.get("value"); if (name === "value") { token = { ...token, ...this.tokenizeExpression( value.isLiteral() ? value : value.get("expression") ) }; } else if (format !== "select" && name === "offset") { token.options.offset = value.isStringLiteral() || value.isNumericLiteral() ? value.node.value : value.get( "expression" ).node.value; } else { let option; if (value.isStringLiteral()) { option = value.node.extra.raw.replace( /(["'])(.*)\1/, "$2" ); } else { option = this.tokenizeChildren(value); } if (pluralRuleRe.test(name)) { token.options[jsx2icuExactChoice(name)] = option; } else { token.options[name] = option; } } } return token; }); __publicField$1(this, "tokenizeElement", (path) => { const name = this.ctx.elementIndex(); return { type: "element", name, value: { ...path.node, children: [], openingElement: { ...path.node.openingElement, selfClosing: true } }, children: this.tokenizeTrans(path) }; }); __publicField$1(this, "tokenizeExpression", (path) => { return ast.tokenizeExpression(path.node, this.ctx); }); __publicField$1(this, "tokenizeConditionalExpression", (exp) => { exp.traverse( { JSXElement: (el) => { if (this.isTransComponent(el) || this.isChoiceComponent(el)) { this.replacePath(el); el.skip(); } } }, exp.state ); return this.tokenizeExpression(exp); }); __publicField$1(this, "tokenizeText", (value) => { return { type: "text", value }; }); __publicField$1(this, "isLinguiComponent", (path, name) => { if (!path.isJSXElement()) { return false; } const config = path.context.state.get( "linguiConfig" ); const identifier = path.get("openingElement").get("name"); return config.macro.jsxPackage.some( (moduleSource) => identifier.referencesImport(moduleSource, name) ); }); __publicField$1(this, "isTransComponent", (path) => { return this.isLinguiComponent(path, ast.JsxMacroName.Trans); }); __publicField$1(this, "isChoiceComponent", (path) => { if (this.isLinguiComponent(path, ast.JsxMacroName.Plural)) { return ast.JsxMacroName.Plural; } if (this.isLinguiComponent(path, ast.JsxMacroName.Select)) { return ast.JsxMacroName.Select; } if (this.isLinguiComponent(path, ast.JsxMacroName.SelectOrdinal)) { return ast.JsxMacroName.SelectOrdinal; } }); this.types = types; this.ctx = { ...ast.createMacroJsContext( opts.isLinguiIdentifier, opts.stripNonEssentialProps, opts.stripMessageProp ), transImportName: opts.transImportName, elementIndex: ast.makeCounter() }; } tokenizeTemplateLiteral(exp) { const expressions = exp.get("expressions"); return exp.get("quasis").flatMap(({ node: text }, i) => { const value = text.value.cooked; let argTokens = []; const currExp = expressions[i]; if (currExp) { argTokens = currExp.isCallExpression() ? this.tokenizeNode(currExp) : [this.tokenizeExpression(currExp)]; } return [...value ? [this.tokenizeText(value)] : [], ...argTokens]; }); } } var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; class MacroJs { constructor(opts) { // Identifier of i18n object __publicField(this, "i18nImportName"); __publicField(this, "useLinguiImportName"); __publicField(this, "needsUseLinguiImport", false); __publicField(this, "needsI18nImport", false); __publicField(this, "_ctx"); __publicField(this, "replacePathWithMessage", (path, tokens, linguiInstance) => { return this.createI18nCall( ast.createMessageDescriptorFromTokens( tokens, path.node.loc, this._ctx.stripNonEssentialProps, this._ctx.stripMessageProp ), linguiInstance ); }); __publicField(this, "replacePath", (path) => { const ctx = this._ctx; if ( // path.isCallExpression() && ast.isDefineMessage(path.get("callee").node, ctx) ) { return ast.processDescriptor( path.get("arguments")[0].node, ctx ); } if (path.isTaggedTemplateExpression() && ast.isDefineMessage(path.get("tag").node, ctx)) { const tokens2 = ast.tokenizeTemplateLiteral(path.get("quasi").node, ctx); return ast.createMessageDescriptorFromTokens( tokens2, path.node.loc, ctx.stripNonEssentialProps, ctx.stripMessageProp ); } if (path.isTaggedTemplateExpression()) { const tag = path.get("tag"); if (tag.isCallExpression() && tag.get("arguments")[0]?.isExpression() && ast.isLinguiIdentifier(tag.get("callee").node, ast.JsMacroName.t, ctx)) { const i18nInstance = tag.get("arguments")[0].node; const tokens2 = ast.tokenizeNode(path.node, false, ctx); return this.replacePathWithMessage(path, tokens2, i18nInstance); } } if (path.isCallExpression()) { const callee = path.get("callee"); if (callee.isCallExpression() && callee.get("arguments")[0]?.isExpression() && ast.isLinguiIdentifier(callee.get("callee").node, ast.JsMacroName.t, ctx)) { const i18nInstance = callee.node.arguments[0]; return this.replaceTAsFunction( path.node, ctx, i18nInstance ); } } if (path.isCallExpression() && ast.isLinguiIdentifier(path.get("callee").node, ast.JsMacroName.t, ctx)) { this.needsI18nImport = true; return this.replaceTAsFunction(path.node, ctx); } if (path.isCallExpression() && ast.isLinguiIdentifier(path.get("callee").node, ast.JsMacroName.useLingui, ctx)) { this.needsUseLinguiImport = true; return this.processUseLingui(path, ctx); } const tokens = ast.tokenizeNode(path.node, true, ctx); if (tokens) { this.needsI18nImport = true; return this.replacePathWithMessage(path, tokens); } return false; }); /** * macro `t` is called with MessageDescriptor, after that * we create a new node to append it to i18n._ */ __publicField(this, "replaceTAsFunction", (node, ctx, linguiInstance) => { let arg = node.arguments[0]; if (t__namespace.isObjectExpression(arg)) { arg = ast.processDescriptor(arg, ctx); } return this.createI18nCall(arg, linguiInstance); }); this.i18nImportName = opts.i18nImportName; this.useLinguiImportName = opts.useLinguiImportName; this._ctx = ast.createMacroJsContext( opts.isLinguiIdentifier, opts.stripNonEssentialProps, opts.stripMessageProp ); } /** * Receives reference to `useLingui()` call * * Finds every usage of { t } destructured from the call * and process each reference as usual `t` macro. * * const { t } = useLingui() * t`Message` * * ↓ ↓ ↓ ↓ ↓ ↓ * * const { _: _t } = useLingui() * _t({id: <hash>, message: "Message"}) */ processUseLingui(path, ctx) { if (!path.parentPath.isVariableDeclarator()) { throw new Error( `\`useLingui\` macro must be used in variable declaration. Example: const { t } = useLingui() ` ); } const varDec = path.parentPath.node; if (!t__namespace.isObjectPattern(varDec.id)) { throw new Error( `You have to destructure \`t\` when using the \`useLingui\` macro, i.e: const { t } = useLingui() or const { t: _ } = useLingui() ` ); } const _property = t__namespace.isObjectPattern(varDec.id) ? varDec.id.properties.find( (property) => t__namespace.isObjectProperty(property) && t__namespace.isIdentifier(property.key) && t__namespace.isIdentifier(property.value) && property.key.name == "t" ) : null; const newNode = t__namespace.callExpression(t__namespace.identifier(this.useLinguiImportName), []); if (!_property) { return newNode; } const uniqTIdentifier = path.scope.generateUidIdentifier("t"); path.scope.getBinding(_property.value.name)?.referencePaths.forEach((refPath) => { const currentPath = refPath.parentPath; const _ctx = ast.createMacroJsContext( ctx.isLinguiIdentifier, ctx.stripNonEssentialProps, ctx.stripMessageProp ); if (currentPath.isTaggedTemplateExpression()) { const tokens = ast.tokenizeTemplateLiteral(currentPath.node, _ctx); const descriptor = ast.createMessageDescriptorFromTokens( tokens, currentPath.node.loc, _ctx.stripNonEssentialProps, _ctx.stripMessageProp ); const callExpr = t__namespace.callExpression( t__namespace.identifier(uniqTIdentifier.name), [descriptor] ); return currentPath.replaceWith(callExpr); } if (currentPath.isCallExpression() && currentPath.get("arguments")[0]?.isObjectExpression()) { const descriptor = ast.processDescriptor( currentPath.get("arguments")[0].node, _ctx ); const callExpr = t__namespace.callExpression( t__namespace.identifier(uniqTIdentifier.name), [descriptor] ); return currentPath.replaceWith(callExpr); } refPath.replaceWith(t__namespace.identifier(uniqTIdentifier.name)); }); _property.key.name = "_"; _property.value.name = uniqTIdentifier.name; return t__namespace.callExpression(t__namespace.identifier(this.useLinguiImportName), []); } createI18nCall(messageDescriptor, linguiInstance) { return t__namespace.callExpression( t__namespace.memberExpression( linguiInstance ?? t__namespace.identifier(this.i18nImportName), t__namespace.identifier("_") ), messageDescriptor ? [messageDescriptor] : [] ); } } let config; function getConfig(_config) { if (_config) { config = _config; } if (!config) { config = conf.getConfig(); } return config; } function reportUnsupportedSyntax(path, e) { const codeFrameError = path.buildCodeFrameError( `Unsupported macro usage. Please check the examples at https://lingui.dev/ref/macro#examples-of-js-macros. If you think this is a bug, fill in an issue at https://github.com/lingui/js-lingui/issues Error: ${e.message}` ); codeFrameError.stack = codeFrameError.message + "\n" + e.stack; throw codeFrameError; } function shouldStripMessageProp(opts) { if (typeof opts.stripMessageField === "boolean") { return opts.stripMessageField; } return process.env.NODE_ENV === "production" && !opts.extract; } const getIdentifierPath = (path, node) => { let foundPath; path.traverse( { Identifier: (path2) => { if (path2.node === node) { foundPath = path2; path2.stop(); } } }, path.state ); return foundPath; }; const LinguiSymbolToImportMap = { Trans: "jsxPackage", useLingui: "jsxPackage", i18n: "corePackage" }; function linguiPlugin({ types: t }) { function addImport(macroImports, state, name) { const [path] = macroImports[LinguiSymbolToImportMap[name]]; const config2 = state.get("linguiConfig"); if (!state.get("has_import_" + name)) { state.set("has_import_" + name, true); const [moduleSource, importName] = config2.runtimeConfigModule[name]; const importDecl = t.importDeclaration( [ t.importSpecifier( getSymbolIdentifier(state, name), t.identifier(importName) ) ], t.stringLiteral(moduleSource) ); importDecl.loc = path.node.loc; const [newPath] = path.insertAfter(importDecl); path.parentPath.scope.registerDeclaration(newPath); } return path.parentPath.scope.getBinding( getSymbolIdentifier(state, name).name ); } function getMacroImports(path) { const corePackage = new Set( path.get("body").filter( (statement) => statement.isImportDeclaration() && config.macro.corePackage.includes( statement.get("source").node.value ) ) ); const jsxPackage = new Set( path.get("body").filter( (statement) => statement.isImportDeclaration() && config.macro.jsxPackage.includes(statement.get("source").node.value) ) ); return { corePackage, jsxPackage, all: /* @__PURE__ */ new Set([...corePackage, ...jsxPackage]) }; } function getSymbolIdentifier(state, name) { return state.get("linguiIdentifiers")[name]; } function isLinguiIdentifier(path, node, macro) { let identPath = getIdentifierPath(path, node); if (macro === ast.JsMacroName.useLingui) { if (config.macro.jsxPackage.some( (moduleSource) => identPath.referencesImport(moduleSource, ast.JsMacroName.useLingui) )) { return true; } } else { identPath = identPath || getIdentifierPath(path.getFunctionParent(), node); if (config.macro.corePackage.some( (moduleSource) => identPath.referencesImport(moduleSource, macro) )) { return true; } } return false; } return { name: "lingui-macro-plugin", visitor: { Program: { enter(path, state) { state.set( "linguiConfig", getConfig(state.opts.linguiConfig) ); const macroImports = getMacroImports(path); if (!macroImports.all.size) { return; } state.set("linguiIdentifiers", { i18n: path.scope.generateUidIdentifier("i18n"), Trans: path.scope.generateUidIdentifier("Trans"), useLingui: path.scope.generateUidIdentifier("useLingui") }); path.traverse( { JSXElement(path2, state2) { const macro = new MacroJSX( { types: t }, { transImportName: getSymbolIdentifier(state2, "Trans").name, stripNonEssentialProps: process.env.NODE_ENV == "production" && !state2.opts.extract, stripMessageProp: shouldStripMessageProp( state2.opts ), isLinguiIdentifier: (node, macro2) => isLinguiIdentifier(path2, node, macro2) } ); let newNode; try { newNode = macro.replacePath(path2); } catch (e) { reportUnsupportedSyntax(path2, e); } if (newNode) { const [newPath] = path2.replaceWith(newNode); addImport(macroImports, state2, "Trans").reference(newPath); } }, "CallExpression|TaggedTemplateExpression"(path2, state2) { const macro = new MacroJs({ stripNonEssentialProps: process.env.NODE_ENV == "production" && !state2.opts.extract, stripMessageProp: shouldStripMessageProp( state2.opts ), i18nImportName: getSymbolIdentifier(state2, "i18n").name, useLinguiImportName: getSymbolIdentifier(state2, "useLingui").name, isLinguiIdentifier: (node, macro2) => isLinguiIdentifier(path2, node, macro2) }); let newNode; try { newNode = macro.replacePath(path2); } catch (e) { reportUnsupportedSyntax(path2, e); } if (newNode) { const [newPath] = path2.replaceWith(newNode); if (macro.needsUseLinguiImport) { addImport(macroImports, state2, "useLingui").reference( newPath ); } if (macro.needsI18nImport) { addImport(macroImports, state2, "i18n").reference(newPath); } } } }, state ); }, exit(path, state) { const macroImports = getMacroImports(path); macroImports.all.forEach((path2) => path2.remove()); } } } }; } module.exports = linguiPlugin;