@lingui/cli
Version:
Lingui CLI to extract messages, compile catalogs, and manage translation workflows
167 lines (166 loc) • 5.67 kB
JavaScript
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;