UNPKG

@lingui/cli

Version:

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

135 lines (134 loc) 5.94 kB
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" }; }); }, });