@lingui/cli
Version:
Lingui CLI to extract messages, compile catalogs, and manage translation workflows
135 lines (134 loc) • 5.94 kB
JavaScript
import path from "path";
import { buildIncludeDepsFilter } from "../buildIncludeDepsFilter.js";
import { DEFAULT_EXCLUDE_EXTENSIONS } from "../constants.js";
import { transformAsync } from "@babel/core";
import fs from "fs";
import { babelRe, getBabelParserOptions } from "../../api/extractors/babel.js";
import linguiMacroPlugin from "@lingui/babel-plugin-lingui-macro";
import { buildContentFilterRe } from "../buildContentFilter.js";
function createExtRegExp(extensions) {
return new RegExp("\\.(?:" + extensions.join("|") + ")(?:\\?.*)?$");
}
// esbuild metafile reports entryPoint as cwd-relative with forward slashes.
// Resolve to absolute and filter to only user-specified entries
// (dynamic imports also get entryPoint set in metafile).
function resolveIfUserEntry(metaEntryPoint, entryPointSet) {
const resolved = path.resolve(metaEntryPoint);
return entryPointSet.has(resolved) ? resolved : undefined;
}
export function createEsbuildBundler(options) {
return {
async bundle(entryPoints, outDir, linguiConfig) {
let esbuild;
try {
esbuild = await import("esbuild");
}
catch {
throw new Error(`"esbuild" is required for createEsbuildBundler but is not installed. ` +
`Install it with: npm install esbuild`);
}
const includeDeps = options?.includeDeps || [];
const excludeExtensions = options?.excludeExtensions || DEFAULT_EXCLUDE_EXTENSIONS;
const shouldInclude = buildIncludeDepsFilter(includeDeps);
let esbuildOptions = {
entryPoints,
outExtension: { ".js": ".jsx" },
jsx: "preserve",
bundle: true,
platform: "node",
target: ["esnext"],
format: "esm",
splitting: true,
treeShaking: true,
outdir: outDir,
sourcemap: "inline",
sourceRoot: outDir,
sourcesContent: false,
metafile: true,
plugins: [
pluginLinguiMacro({ linguiConfig }),
{
name: "externalize-deps",
setup(build) {
build.onResolve({ filter: /^[^.#/].*/ }, async (args) => {
if (shouldInclude(args.path) || args.kind === "entry-point") {
return { external: false };
}
return { external: true };
});
},
},
{
name: "externalize-files",
setup(build) {
build.onResolve({ filter: createExtRegExp(excludeExtensions) }, () => ({
external: true,
}));
// for some dynamic imports, for example
// await import(`../locales/${locale}.po`)
// esbuild skips resolve, because files crawled from the disk
// to exclude those files from build, load an empty object instead
build.onLoad({ filter: createExtRegExp(excludeExtensions) }, () => {
return {
contents: JSON.stringify({}),
loader: "json",
};
});
},
},
],
};
if (options?.resolveEsbuildOptions) {
esbuildOptions = options.resolveEsbuildOptions(esbuildOptions);
}
const bundleResult = await esbuild.build(esbuildOptions);
const metafile = bundleResult.metafile;
const entryPointSet = new Set(entryPoints.map((ep) => path.resolve(ep)));
const allOutputPaths = new Set(Object.keys(metafile.outputs));
const chunks = Object.entries(metafile.outputs).map(([outputPath, meta]) => ({
id: outputPath,
filePath: outputPath,
entryPoint: meta.entryPoint
? resolveIfUserEntry(meta.entryPoint, entryPointSet)
: undefined,
imports: meta.imports
.filter((imp) => allOutputPaths.has(imp.path))
.map((imp) => imp.path),
}));
return { chunks };
},
};
}
const pluginLinguiMacro = (options) => ({
name: "linguiMacro",
setup(build) {
build.onLoad({ filter: babelRe, namespace: "" }, async (args) => {
const filename = path.relative(process.cwd(), args.path);
const contents = await fs.promises.readFile(args.path, "utf8");
const hasMacroRe = buildContentFilterRe(options.linguiConfig);
if (!hasMacroRe.test(contents)) {
// let esbuild process file as usual
return undefined;
}
const result = await transformAsync(contents, {
babelrc: false,
configFile: false,
filename,
sourceMaps: "inline",
parserOpts: {
plugins: getBabelParserOptions(filename, {}),
},
plugins: [
[
linguiMacroPlugin,
{
descriptorFields: "all",
linguiConfig: options.linguiConfig,
},
],
],
});
return { contents: result.code, loader: "tsx" };
});
},
});