diff --git a/packages/cli/src/extract-experimental/buildIncludeDepsFilter.test.ts b/packages/cli/src/extract-experimental/buildIncludeDepsFilter.test.ts new file mode 100644 index 000000000..377b415df --- /dev/null +++ b/packages/cli/src/extract-experimental/buildIncludeDepsFilter.test.ts @@ -0,0 +1,13 @@ +import { buildIncludeDepsFilter } from "./buildIncludeDepsFilter" + +describe("buildExternalizeFilter", () => { + it("should not externalize packages from includeDeps", () => { + const isIncluded = buildIncludeDepsFilter(["package1", "package3/subpath"]) + + expect(isIncluded("package1")).toBeTruthy() + expect(isIncluded("package1/subpath")).toBeTruthy() + expect(isIncluded("package2")).toBeFalsy() + expect(isIncluded("package3/subpath")).toBeTruthy() + expect(isIncluded("package3/subpath/subpath")).toBeTruthy() + }) +}) diff --git a/packages/cli/src/extract-experimental/buildIncludeDepsFilter.ts b/packages/cli/src/extract-experimental/buildIncludeDepsFilter.ts new file mode 100644 index 000000000..2547a2c36 --- /dev/null +++ b/packages/cli/src/extract-experimental/buildIncludeDepsFilter.ts @@ -0,0 +1,9 @@ +function createPackageRegExp(packageName: string) { + return new RegExp("^" + packageName + "(?:\\/.+)?") +} + +export function buildIncludeDepsFilter(includeDeps: string[]) { + const include = includeDeps.map(createPackageRegExp) + + return (id: string) => include.some((regExp) => regExp.test(id)) +} diff --git a/packages/cli/src/extract-experimental/bundleSource.ts b/packages/cli/src/extract-experimental/bundleSource.ts index 1f4a6f232..d8e2eebce 100644 --- a/packages/cli/src/extract-experimental/bundleSource.ts +++ b/packages/cli/src/extract-experimental/bundleSource.ts @@ -1,6 +1,7 @@ import { LinguiConfigNormalized } from "@lingui/conf" import { BuildOptions } from "esbuild" import { pluginLinguiMacro } from "./linguiEsbuildPlugin" +import { buildIncludeDepsFilter } from "./buildIncludeDepsFilter" function createExtRegExp(extensions: string[]) { return new RegExp("\\.(?:" + extensions.join("|") + ")(?:\\?.*)?$") @@ -49,11 +50,29 @@ export async function bundleSource( sourcemap: "inline", sourceRoot: outDir, sourcesContent: false, - packages: "external", metafile: true, plugins: [ pluginLinguiMacro({ linguiConfig }), + { + name: "externalize-deps", + setup(build) { + const shouldInclude = buildIncludeDepsFilter(config.includeDeps || []) + + // considers all import paths that "look like" package imports in the original source code to be package imports. + // Specifically import paths that don't start with a path segment of / or . or .. are considered to be package imports. + // The only two exceptions to this rule are subpath imports (which start with a # character) and deps specified in the `includeDeps` + build.onResolve({ filter: /^[^.#/].*/ }, async (args) => { + if (shouldInclude(args.path)) { + return { external: false } + } + + return { + external: true, + } + }) + }, + }, { name: "externalize-files", setup(build) { diff --git a/packages/cli/test/extractor-experimental-template/expected/about.page.en.js b/packages/cli/test/extractor-experimental-template/expected/about.page.en.js index 7707d434d..47e4c5d8c 100644 --- a/packages/cli/test/extractor-experimental-template/expected/about.page.en.js +++ b/packages/cli/test/extractor-experimental-template/expected/about.page.en.js @@ -1 +1 @@ -/*eslint-disable*/module.exports={messages:JSON.parse("{\"1TzdHc\":[\"aliased module message\"],\"8Pj7KC\":[\"JSX: about page message\"],\"LGGfGX\":[\"header message\"],\"u5PTM8\":[\"about page message\"]}")}; \ No newline at end of file +/*eslint-disable*/module.exports={messages:JSON.parse("{\"1TzdHc\":[\"aliased module message\"],\"5iuJq+\":[\"subpath import module message\"],\"8Pj7KC\":[\"JSX: about page message\"],\"LGGfGX\":[\"header message\"],\"u5PTM8\":[\"about page message\"]}")}; \ No newline at end of file diff --git a/packages/cli/test/extractor-experimental-template/expected/about.page.messages.pot b/packages/cli/test/extractor-experimental-template/expected/about.page.messages.pot index 3b01c9627..460ad9a51 100644 --- a/packages/cli/test/extractor-experimental-template/expected/about.page.messages.pot +++ b/packages/cli/test/extractor-experimental-template/expected/about.page.messages.pot @@ -6,7 +6,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: @lingui/cli\n" -#: fixtures/pages/about.page.tsx:16 +#: fixtures/pages/about.page.tsx:19 msgid "about page message" msgstr "" @@ -18,6 +18,10 @@ msgstr "" msgid "header message" msgstr "" -#: fixtures/pages/about.page.tsx:19 +#: fixtures/pages/about.page.tsx:22 msgid "JSX: about page message" msgstr "" + +#: fixtures/components/subpath-import.ts:3 +msgid "subpath import module message" +msgstr "" diff --git a/packages/cli/test/extractor-experimental-template/expected/about.page.pl.js b/packages/cli/test/extractor-experimental-template/expected/about.page.pl.js index 7707d434d..47e4c5d8c 100644 --- a/packages/cli/test/extractor-experimental-template/expected/about.page.pl.js +++ b/packages/cli/test/extractor-experimental-template/expected/about.page.pl.js @@ -1 +1 @@ -/*eslint-disable*/module.exports={messages:JSON.parse("{\"1TzdHc\":[\"aliased module message\"],\"8Pj7KC\":[\"JSX: about page message\"],\"LGGfGX\":[\"header message\"],\"u5PTM8\":[\"about page message\"]}")}; \ No newline at end of file +/*eslint-disable*/module.exports={messages:JSON.parse("{\"1TzdHc\":[\"aliased module message\"],\"5iuJq+\":[\"subpath import module message\"],\"8Pj7KC\":[\"JSX: about page message\"],\"LGGfGX\":[\"header message\"],\"u5PTM8\":[\"about page message\"]}")}; \ No newline at end of file diff --git a/packages/cli/test/extractor-experimental-template/fixtures/components/subpath-import.ts b/packages/cli/test/extractor-experimental-template/fixtures/components/subpath-import.ts new file mode 100644 index 000000000..3a650575d --- /dev/null +++ b/packages/cli/test/extractor-experimental-template/fixtures/components/subpath-import.ts @@ -0,0 +1,3 @@ +import { t } from "@lingui/core/macro" + +export const msg = t`subpath import module message` diff --git a/packages/cli/test/extractor-experimental-template/fixtures/pages/about.page.tsx b/packages/cli/test/extractor-experimental-template/fixtures/pages/about.page.tsx index 72630c149..01e536aad 100644 --- a/packages/cli/test/extractor-experimental-template/fixtures/pages/about.page.tsx +++ b/packages/cli/test/extractor-experimental-template/fixtures/pages/about.page.tsx @@ -13,10 +13,13 @@ import styles2 from "./styles.css?inline" // should respect tsconfig path aliases import { msg as msg2 } from "@alias" +// should respect package.json subpath imports +import { msg as msg3 } from "#subpath-dep" + const msg = t`about page message` export default function Page() { return JSX: about page message } -console.log(msg, headerMsg, bla, styles, styles2, msg2) +console.log(msg, headerMsg, bla, styles, styles2, msg2, msg3) diff --git a/packages/cli/test/extractor-experimental-template/lingui.config.ts b/packages/cli/test/extractor-experimental-template/lingui.config.ts index 6110511ce..b465e51eb 100644 --- a/packages/cli/test/extractor-experimental-template/lingui.config.ts +++ b/packages/cli/test/extractor-experimental-template/lingui.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ extractor: { entries: ["/fixtures/pages/**/*.page.{ts,tsx}"], output: "/actual/{entryName}.{locale}", + includeDeps: ["@alias"], }, }, }) diff --git a/packages/cli/test/extractor-experimental-template/package.json b/packages/cli/test/extractor-experimental-template/package.json index 3a655ff6a..972318aeb 100644 --- a/packages/cli/test/extractor-experimental-template/package.json +++ b/packages/cli/test/extractor-experimental-template/package.json @@ -2,6 +2,9 @@ "name": "extractor-experimental-template", "version": "1.0.0", "private": true, + "imports": { + "#subpath-dep": "./fixtures/components/subpath-import.ts" + }, "dependencies": { "@lingui/core": "*", "@lingui/react": "*", diff --git a/packages/cli/test/extractor-experimental-template/tsconfig.json b/packages/cli/test/extractor-experimental-template/tsconfig.json index 8c2301e65..84ab6623e 100644 --- a/packages/cli/test/extractor-experimental-template/tsconfig.json +++ b/packages/cli/test/extractor-experimental-template/tsconfig.json @@ -1,15 +1,12 @@ { "compilerOptions": { - "module": "commonjs", - "target": "es5", + "module": "NodeNext", + "target": "ESNext", + "jsx": "preserve", "baseUrl": "./", "paths": { - "@alias": [ - "./fixtures/components/aliased-module.ts" - ] + "@alias": ["./fixtures/components/aliased-module.ts"] } }, - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/packages/cli/test/index.test.ts b/packages/cli/test/index.test.ts index ea16996e7..e9b1f87d5 100644 --- a/packages/cli/test/index.test.ts +++ b/packages/cli/test/index.test.ts @@ -84,18 +84,18 @@ describe("E2E Extractor Test", () => { expect(replaceDuration(getConsoleMockCalls(console.log))) .toMatchInlineSnapshot(` - Done in ms - Catalog statistics for actual/{locale}: - ┌─────────────┬─────────────┬─────────┐ - │ Language │ Total count │ Missing │ - ├─────────────┼─────────────┼─────────┤ - │ en (source) │ 10 │ - │ - │ pl │ 10 │ 10 │ - └─────────────┴─────────────┴─────────┘ + Done in ms + Catalog statistics for actual/{locale}: + ┌─────────────┬─────────────┬─────────┐ + │ Language │ Total count │ Missing │ + ├─────────────┼─────────────┼─────────┤ + │ en (source) │ 10 │ - │ + │ pl │ 10 │ 10 │ + └─────────────┴─────────────┴─────────┘ - (Use "yarn extract" to update catalogs with new messages.) - (Use "yarn compile" to compile catalogs for production. Alternatively, use bundler plugins: https://lingui.dev/ref/cli#compiling-catalogs-in-ci) - `) + (Use "yarn extract" to update catalogs with new messages.) + (Use "yarn compile" to compile catalogs for production. Alternatively, use bundler plugins: https://lingui.dev/ref/cli#compiling-catalogs-in-ci) + `) }) compareFolders(actualPath, expectedPath) @@ -222,7 +222,7 @@ describe("E2E Extractor Test", () => { Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use at your own risk. Catalog statistics for fixtures/pages/about.page.tsx: - 4 message(s) extracted + 5 message(s) extracted Catalog statistics for fixtures/pages/index.page.ts: 1 message(s) extracted @@ -455,18 +455,18 @@ describe("E2E Extractor Test", () => { expect(replaceDuration(getConsoleMockCalls(console.log))) .toMatchInlineSnapshot(` - Done in ms - Catalog statistics for actual/{locale}: - ┌─────────────┬─────────────┬─────────┐ - │ Language │ Total count │ Missing │ - ├─────────────┼─────────────┼─────────┤ - │ en (source) │ 10 │ - │ - │ pl │ 10 │ 10 │ - └─────────────┴─────────────┴─────────┘ - - (Use "yarn extract" to update catalogs with new messages.) - (Use "yarn compile" to compile catalogs for production. Alternatively, use bundler plugins: https://lingui.dev/ref/cli#compiling-catalogs-in-ci) - `) + Done in ms + Catalog statistics for actual/{locale}: + ┌─────────────┬─────────────┬─────────┐ + │ Language │ Total count │ Missing │ + ├─────────────┼─────────────┼─────────┤ + │ en (source) │ 10 │ - │ + │ pl │ 10 │ 10 │ + └─────────────┴─────────────┴─────────┘ + + (Use "yarn extract" to update catalogs with new messages.) + (Use "yarn compile" to compile catalogs for production. Alternatively, use bundler plugins: https://lingui.dev/ref/cli#compiling-catalogs-in-ci) + `) }) }) }) diff --git a/packages/conf/src/types.ts b/packages/conf/src/types.ts index 07e26c2ad..8839fa849 100644 --- a/packages/conf/src/types.ts +++ b/packages/conf/src/types.ts @@ -138,25 +138,25 @@ export type ExperimentalExtractorOptions = { entries: string[] /** - * Explicitly include some dependency for extraction. - * For example, you can include all monorepo's packages as - * ["@mycompany/"] - */ - includeDeps?: string[] - - /** - * By default all dependencies from package.json would be ecxluded from analyzing. - * If something was not properly discovered you can add it here. + * List of package name patterns to include for extraction. * - * Note: it automatically matches also sub imports + * For example, to include all packages from your monorepo: * - * "next" would match "next" and "next/head" + * ["@mycompany"] + * + * By default, all imports that look like package imports are ignored. + * This means imports that do not start with `/`, `./`, `../`, or `#` + * (used for subpath imports). TypeScript path aliases are also ignored + * because they look like package imports. + * + * Add here the packages you want to include. */ - excludeDeps?: string[] + includeDeps?: string[] /** - * svg, jpg and other files which might be imported in application should be exluded from analysis. - * By default extractor provides a comprehensive list of extensions. If you feel like somthing is missing in this list please fill an issue on GitHub + * svg, jpg and other files which might be imported in application should be excluded from analysis. + * By default, extractor provides a comprehensive list of extensions. If you feel like something + * is missing in this list please fill an issue on GitHub * * NOTE: changing this param will override default list of extensions. */