UNPKG

@lingui/cli

Version:

Lingui CLI to extract messages, compile catalogs, and manage translation workflows

167 lines (166 loc) 5.67 kB
import { DEFAULT_EXTENSIONS, transformAsync } from "@babel/core"; import linguiExtractMessages from "@lingui/babel-plugin-extract-messages"; import linguiMacroPlugin from "@lingui/babel-plugin-lingui-macro"; export const babelRe = new RegExp("\\.(" + [...DEFAULT_EXTENSIONS, ".ts", ".mts", ".cts", ".tsx"] .map((ext) => ext.slice(1)) .join("|") + ")$", "i"); const inlineSourceMapsRE = new RegExp(/\/[/*][#@]\s+sourceMappingURL=data:application\/json;(?:charset[:=]utf-8;)?base64,([A-Za-z0-9+/=]+)/i); const globalInlineSourceMapsRE = new RegExp(inlineSourceMapsRE.source, "gi"); const extractInlineSourceMap = (code) => { let base64Data; globalInlineSourceMapsRE.lastIndex = 0; for (const match of code.matchAll(globalInlineSourceMapsRE)) { base64Data = match[1]; } if (!base64Data) return null; try { const jsonString = Buffer.from(base64Data, "base64").toString("utf-8"); return JSON.parse(jsonString); } catch { return null; } }; /** * Create a source mapper which could read original positions * from either inline sourcemaps or from external passed as `sourceMaps` argument. * * Warning! You have to call destroy method after you finish working with a mapper. * * @param code source code * @param sourceMaps Raw Sourcemaps object to mapping from. Check the https://github.com/mozilla/source-map#new-sourcemapconsumerrawsourcemap */ async function createSourceMapper(code, sourceMaps) { const map = sourceMaps ?? extractInlineSourceMap(code); if (!map) { return { destroy: () => { }, originalPositionFor: (origin) => origin, }; } const { SourceMapConsumer } = await import("source-map"); const sourceMapsConsumer = await new SourceMapConsumer(map); return { destroy: () => sourceMapsConsumer.destroy(), originalPositionFor: (origin) => { const [_, line, column] = origin; const mappedPosition = sourceMapsConsumer.originalPositionFor({ line, column: column, }); return [ mappedPosition.source, mappedPosition.line, mappedPosition.column, ]; }, }; } /** * @public * * Low level function used in default Lingui extractor. * This function setup source maps and lingui plugins needed for * extraction process but leaving `parserOptions` up to userland implementation. * * * @example * ```ts * const extractor: ExtractorType = { * ... * async extract(filename, code, onMessageExtracted, ctx) { * return extractFromFileWithBabel(filename, code, onMessageExtracted, ctx, { * // https://babeljs.io/docs/babel-parser#plugins * plugins: [ * "decorators-legacy", * "typescript", * "jsx", * ], * }) * }, * } * ``` */ export async function extractFromFileWithBabel(filename, code, onMessageExtracted, ctx, parserOpts, skipMacroPlugin = false) { const mapper = await createSourceMapper(code, ctx?.sourceMaps); await transformAsync(code, { // don't generate code code: false, babelrc: false, configFile: false, filename: filename, inputSourceMap: ctx?.sourceMaps, parserOpts, plugins: [ ...(!skipMacroPlugin ? [ [ linguiMacroPlugin, { descriptorFields: "all", linguiConfig: ctx.linguiConfig, }, ], ] : []), [ linguiExtractMessages, { onMessageExtracted: (msg) => { return onMessageExtracted({ ...msg, origin: mapper.originalPositionFor(msg.origin), }); }, }, ], ], }); mapper.destroy(); } export function getBabelParserOptions(filename, parserOptions) { // https://babeljs.io/docs/en/babel-parser#latest-ecmascript-features const parserPlugins = [ "importAttributes", // stage3 "explicitResourceManagement", // stage3, "decoratorAutoAccessors", // stage3, "deferredImportEvaluation", // stage3 ]; if ([/\.ts$/, /\.mts$/, /\.cts$/, /\.tsx$/].some((r) => filename.match(r))) { parserPlugins.push("typescript"); if (parserOptions?.tsExperimentalDecorators) { parserPlugins.push("decorators-legacy"); } else { parserPlugins.push("decorators"); } } else { parserPlugins.push("decorators"); if (parserOptions?.flow) { parserPlugins.push("flow"); } } if ([/\.js$/, /\.jsx$/, /\.tsx$/].some((r) => filename.match(r))) { parserPlugins.push("jsx"); } return parserPlugins; } export function createBabelExtractor(options) { return { match(filename) { return babelRe.test(filename); }, async extract(filename, code, onMessageExtracted, ctx) { const parserOptions = options?.parserOptions ?? ctx.linguiConfig.extractorParserOptions; return extractFromFileWithBabel(filename, code, onMessageExtracted, ctx, { plugins: getBabelParserOptions(filename, parserOptions), }); }, }; } export const babelExtractor = createBabelExtractor(); export default babelExtractor;