@inlang/paraglide-js 2.18.1 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +35 -29
  2. package/dist/bundler-plugins/unplugin.d.ts.map +1 -1
  3. package/dist/bundler-plugins/unplugin.js +241 -133
  4. package/dist/bundler-plugins/unplugin.js.map +1 -1
  5. package/dist/bundler-plugins/unplugin.test.js +184 -1
  6. package/dist/bundler-plugins/unplugin.test.js.map +1 -1
  7. package/dist/cli/commands/compile/command.d.ts.map +1 -1
  8. package/dist/cli/commands/compile/command.js +8 -3
  9. package/dist/cli/commands/compile/command.js.map +1 -1
  10. package/dist/cli/commands/compile/command.test.d.ts +2 -0
  11. package/dist/cli/commands/compile/command.test.d.ts.map +1 -0
  12. package/dist/cli/commands/compile/command.test.js +63 -0
  13. package/dist/cli/commands/compile/command.test.js.map +1 -0
  14. package/dist/cli/commands/init/command.js +2 -2
  15. package/dist/cli/commands/init/command.js.map +1 -1
  16. package/dist/compiler/compile-annotation.d.ts +13 -0
  17. package/dist/compiler/compile-annotation.d.ts.map +1 -0
  18. package/dist/compiler/compile-annotation.js +136 -0
  19. package/dist/compiler/compile-annotation.js.map +1 -0
  20. package/dist/compiler/compile-local-variable.d.ts.map +1 -1
  21. package/dist/compiler/compile-local-variable.js +1 -108
  22. package/dist/compiler/compile-local-variable.js.map +1 -1
  23. package/dist/compiler/compile-message.js +4 -0
  24. package/dist/compiler/compile-message.js.map +1 -1
  25. package/dist/compiler/compile-message.test.js +81 -0
  26. package/dist/compiler/compile-message.test.js.map +1 -1
  27. package/dist/compiler/compile-pattern.d.ts +8 -0
  28. package/dist/compiler/compile-pattern.d.ts.map +1 -1
  29. package/dist/compiler/compile-pattern.js +38 -2
  30. package/dist/compiler/compile-pattern.js.map +1 -1
  31. package/dist/compiler/compile-pattern.test.js +109 -0
  32. package/dist/compiler/compile-pattern.test.js.map +1 -1
  33. package/dist/compiler/compile-project.js +1 -1
  34. package/dist/compiler/compile-project.test.js +128 -0
  35. package/dist/compiler/compile-project.test.js.map +1 -1
  36. package/dist/compiler/compiler-options.d.ts +1 -1
  37. package/dist/compiler/create-readme.js +4 -4
  38. package/dist/compiler/emit-ts-declarations.d.ts.map +1 -1
  39. package/dist/compiler/emit-ts-declarations.js +77 -1
  40. package/dist/compiler/emit-ts-declarations.js.map +1 -1
  41. package/dist/compiler/runtime/extract-locale-from-request.js +1 -1
  42. package/dist/compiler/runtime/extract-locale-from-request.js.map +1 -1
  43. package/dist/compiler/runtime/generate-static-localized-urls.d.ts +1 -1
  44. package/dist/compiler/runtime/generate-static-localized-urls.js +1 -1
  45. package/dist/compiler/runtime/get-locale.js +4 -4
  46. package/dist/compiler/runtime/get-locale.js.map +1 -1
  47. package/dist/compiler/runtime/localize-href.d.ts +2 -2
  48. package/dist/compiler/runtime/localize-href.js +2 -2
  49. package/dist/compiler/runtime/localize-url.d.ts +2 -2
  50. package/dist/compiler/runtime/localize-url.js +2 -2
  51. package/dist/compiler/runtime/set-locale.d.ts +1 -1
  52. package/dist/compiler/runtime/set-locale.js +1 -1
  53. package/dist/compiler/runtime/should-redirect.d.ts +1 -1
  54. package/dist/compiler/runtime/should-redirect.js +1 -1
  55. package/dist/compiler/runtime/strategy.d.ts +2 -2
  56. package/dist/compiler/runtime/strategy.js +2 -2
  57. package/dist/compiler/seed-previous-compilation.d.ts +8 -0
  58. package/dist/compiler/seed-previous-compilation.d.ts.map +1 -0
  59. package/dist/compiler/seed-previous-compilation.js +9 -0
  60. package/dist/compiler/seed-previous-compilation.js.map +1 -0
  61. package/dist/compiler/server/middleware.d.ts +1 -1
  62. package/dist/compiler/server/middleware.js +1 -1
  63. package/dist/services/env-variables/index.js +1 -1
  64. package/dist/services/file-watching/tracked-fs.d.ts.map +1 -1
  65. package/dist/services/file-watching/tracked-fs.js +3 -1
  66. package/dist/services/file-watching/tracked-fs.js.map +1 -1
  67. package/package.json +8 -5
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  </p>
12
12
 
13
13
  <p align="center">
14
- <a href="https://inlang.com/m/gerre34r/library-inlang-paraglideJs"><strong>Documentation</strong></a> ·
14
+ <a href="https://paraglidejs.com"><strong>Documentation</strong></a> ·
15
15
  <a href="#quick-start"><strong>Quick Start</strong></a> ·
16
16
  <a href="https://github.com/opral/inlang-paraglide-js/issues"><strong>Report Bug</strong></a>
17
17
  </p>
@@ -26,14 +26,14 @@
26
26
  <a href="https://www.michelin.com/"><img src="https://github.com/opral/paraglide-js/blob/main/assets/used-by/michelin.svg?raw=true" alt="Michelin" height="18"></a>&nbsp;&nbsp;&nbsp;
27
27
  <a href="https://www.idealista.com/"><img src="https://github.com/opral/paraglide-js/blob/main/assets/used-by/idealista.svg?raw=true" alt="idealista" height="18"></a>&nbsp;&nbsp;&nbsp;
28
28
  <a href="https://www.architonic.com/"><img src="https://github.com/opral/paraglide-js/blob/main/assets/used-by/architonic.png?raw=true" alt="Architonic" height="18"></a>&nbsp;&nbsp;&nbsp;
29
- <a href="https://www.finanzen100.de/"><img src="https://github.com/opral/paraglide-js/blob/main/assets/used-by/finanzen100.png?raw=true" alt="Finanzen100" height="18"></a>&nbsp;&nbsp;&nbsp;
30
- <a href="https://0.email/"><img src="https://github.com/opral/paraglide-js/blob/main/assets/used-by/zero-email.svg?raw=true" alt="0.email" height="18"></a>
29
+ <a href="https://lovable.dev/"><img src="https://github.com/opral/paraglide-js/blob/main/assets/used-by/lovable.svg?raw=true" alt="Lovable" height="18"></a>&nbsp;&nbsp;&nbsp;
30
+ <a href="https://www.klaviyo.com/"><img src="https://github.com/opral/paraglide-js/blob/main/assets/used-by/klaviyo.svg?raw=true" alt="Klaviyo" height="18"></a>
31
31
  </p>
32
32
 
33
33
  <p align="center">
34
34
  <sub>Framework-authored and framework-tested</sub><br/><br/>
35
35
  <a href="https://svelte.dev/docs/cli/paraglide"><img src="https://cdn.simpleicons.org/svelte/FF3E00" alt="Svelte" height="14" /> SvelteKit's official i18n integration</a><br/>
36
- <a href="https://inlang.com/blog/tanstack-ci"><img src="https://tanstack.com/images/logos/logo-color-100.png" alt="TanStack" height="14" /> TanStack Router's e2e-tested i18n example</a>
36
+ <a href="https://paraglidejs.com/blog/tanstack-ci"><img src="https://tanstack.com/images/logos/logo-color-100.png" alt="TanStack" height="14" /> TanStack Router's e2e-tested i18n example</a>
37
37
  </p>
38
38
 
39
39
  ## Code Preview
@@ -51,7 +51,7 @@ import { m } from "./paraglide/messages.js";
51
51
  m.greeting({ name: "World" }); // "Hello World!" — fully typesafe
52
52
  ```
53
53
 
54
- The compiler turns your messages into typed ESM functions. Vite, Rollup, and other modern bundlers can tree-shake unused translations before they reach the browser. Expect [**up to 70% smaller i18n bundle sizes**](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/benchmark) compared to runtime i18n libraries (e.g. 47 KB vs 205 KB).
54
+ The compiler turns your messages into typed ESM functions. Vite, Rollup, and other modern bundlers can tree-shake unused translations before they reach the browser. Expect [**up to 70% smaller i18n bundle sizes**](https://paraglidejs.com/benchmark) compared to runtime i18n libraries (e.g. 47 KB vs 205 KB).
55
55
 
56
56
  ## Why Paraglide?
57
57
 
@@ -67,23 +67,23 @@ The compiler turns your messages into typed ESM functions. Vite, Rollup, and oth
67
67
  ## Get Started With Your Framework
68
68
 
69
69
  <p>
70
- <a href="https://inlang.com/m/gerre34r/library-inlang-paraglideJs/vite"><img src="https://cdn.simpleicons.org/react/61DAFB" alt="React" width="18" height="18" /> React</a> ·
71
- <a href="https://inlang.com/m/gerre34r/library-inlang-paraglideJs/vite"><img src="https://cdn.simpleicons.org/vuedotjs/4FC08D" alt="Vue" width="18" height="18" /> Vue</a> ·
70
+ <a href="https://paraglidejs.com/vite"><img src="https://cdn.simpleicons.org/react/61DAFB" alt="React" width="18" height="18" /> React</a> ·
71
+ <a href="https://paraglidejs.com/vite"><img src="https://cdn.simpleicons.org/vuedotjs/4FC08D" alt="Vue" width="18" height="18" /> Vue</a> ·
72
72
  <a href="https://github.com/TanStack/router/tree/main/examples/react/start-i18n-paraglide"><img src="https://tanstack.com/images/logos/logo-color-100.png" alt="TanStack" width="18" height="18" /> TanStack Start</a> ·
73
- <a href="https://inlang.com/m/gerre34r/library-inlang-paraglideJs/sveltekit"><img src="https://cdn.simpleicons.org/svelte/FF3E00" alt="Svelte" width="18" height="18" /> SvelteKit</a> ·
74
- <a href="https://inlang.com/m/gerre34r/library-inlang-paraglideJs/react-router"><img src="https://cdn.simpleicons.org/reactrouter/CA4245" alt="React Router" width="18" height="18" /> React Router</a> ·
75
- <a href="https://inlang.com/m/gerre34r/library-inlang-paraglideJs/astro"><img src="https://cdn.simpleicons.org/astro/FF5D01" alt="Astro" width="18" height="18" /> Astro</a> ·
76
- <a href="https://inlang.com/m/gerre34r/library-inlang-paraglideJs/vanilla-js-ts"><img src="https://cdn.simpleicons.org/javascript/F7DF1E" alt="JavaScript" width="18" height="18" /> Vanilla JS/TS</a>
73
+ <a href="https://paraglidejs.com/sveltekit"><img src="https://cdn.simpleicons.org/svelte/FF3E00" alt="Svelte" width="18" height="18" /> SvelteKit</a> ·
74
+ <a href="https://paraglidejs.com/react-router"><img src="https://cdn.simpleicons.org/reactrouter/CA4245" alt="React Router" width="18" height="18" /> React Router</a> ·
75
+ <a href="https://paraglidejs.com/astro"><img src="https://cdn.simpleicons.org/astro/FF5D01" alt="Astro" width="18" height="18" /> Astro</a> ·
76
+ <a href="https://paraglidejs.com/vanilla-js-ts"><img src="https://cdn.simpleicons.org/javascript/F7DF1E" alt="JavaScript" width="18" height="18" /> Vanilla JS/TS</a>
77
77
  </p>
78
78
 
79
79
  - **[TanStack Start example](https://github.com/TanStack/router/tree/main/examples/react/start-i18n-paraglide)** — SSR, localized routing, and TanStack Router integration.
80
- - **[SvelteKit guide](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/sveltekit)** — SvelteKit's official i18n integration.
81
- - **[React Router guide](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/react-router)** — SSR and client routing.
82
- - **[Astro guide](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/astro)** — static and server-rendered sites.
83
- - **[Vite guide](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/vite)** — React, Vue, Solid, or vanilla JS/TS.
80
+ - **[SvelteKit guide](https://paraglidejs.com/sveltekit)** — SvelteKit's official i18n integration.
81
+ - **[React Router guide](https://paraglidejs.com/react-router)** — SSR and client routing.
82
+ - **[Astro guide](https://paraglidejs.com/astro)** — static and server-rendered sites.
83
+ - **[Vite guide](https://paraglidejs.com/vite)** — React, Vue, Solid, or vanilla JS/TS.
84
84
 
85
85
  > [!TIP]
86
- > <img src="https://vitejs.dev/logo.svg" alt="Vite" width="16" height="16" /> **Paraglide is ideal for Vite-based apps.** Setup is one plugin, messages compile to ESM, and Vite's tree-shaking eliminates unused translations automatically. [Get started →](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/vite)
86
+ > <img src="https://vitejs.dev/logo.svg" alt="Vite" width="16" height="16" /> **Paraglide is ideal for Vite-based apps.** Setup is one plugin, messages compile to ESM, and Vite's tree-shaking eliminates unused translations automatically. [Get started →](https://paraglidejs.com/vite)
87
87
 
88
88
  ## SSR Ready
89
89
 
@@ -109,7 +109,7 @@ export function handle(request: Request) {
109
109
  }
110
110
  ```
111
111
 
112
- **[SSR Docs →](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/server-side-rendering)** · **[Middleware Docs →](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/middleware)**
112
+ **[SSR Docs →](https://paraglidejs.com/server-side-rendering)** · **[Middleware Docs →](https://paraglidejs.com/middleware)**
113
113
 
114
114
  ## Router Composition
115
115
 
@@ -129,7 +129,7 @@ localizeUrl("https://example.com/about", { locale: "de" }).href; // https://exam
129
129
 
130
130
  For routers with rewrite hooks, call `deLocalizeUrl()` on incoming URLs and `localizeUrl()` on outgoing URLs. For file-based routers, keep your file routes canonical and localize at the routing boundary.
131
131
 
132
- **[i18n Routing Docs →](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/i18n-routing)**
132
+ **[i18n Routing Docs →](https://paraglidejs.com/i18n-routing)**
133
133
 
134
134
  ### TanStack Start
135
135
 
@@ -177,7 +177,7 @@ getLocale(); // "en"
177
177
  setLocale("de"); // switches to German
178
178
  ```
179
179
 
180
- **[Full Getting Started Guide →](https://inlang.com/m/gerre34r/library-inlang-paraglideJs)**
180
+ **[Full Getting Started Guide →](https://paraglidejs.com)**
181
181
 
182
182
  ## Rich Text
183
183
 
@@ -202,7 +202,7 @@ export function ContactCta() {
202
202
 
203
203
  The markup names come from your message and are type-checked, so translators control where links and emphasis appear while your React app controls how they render.
204
204
 
205
- **[Markup Docs →](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/markup)** · **[React](https://www.npmjs.com/package/@inlang/paraglide-js-react)** · **[Svelte](https://www.npmjs.com/package/@inlang/paraglide-js-svelte)** · **[Vue](https://www.npmjs.com/package/@inlang/paraglide-js-vue)** · **[Solid](https://www.npmjs.com/package/@inlang/paraglide-js-solid)**
205
+ **[Markup Docs →](https://paraglidejs.com/markup)** · **[React](https://www.npmjs.com/package/@inlang/paraglide-js-react)** · **[Svelte](https://www.npmjs.com/package/@inlang/paraglide-js-svelte)** · **[Vue](https://www.npmjs.com/package/@inlang/paraglide-js-vue)** · **[Solid](https://www.npmjs.com/package/@inlang/paraglide-js-solid)**
206
206
 
207
207
  ## How It Works
208
208
 
@@ -248,7 +248,7 @@ m.items_in_cart({ count: 5 }); // "5 items in cart"
248
248
 
249
249
  Message format is **plugin-based** — use the default inlang format, or switch to i18next, JSON, or ICU MessageFormat via [plugins](https://inlang.com/c/plugins). If your team relies on ICU MessageFormat 1 syntax, use the [inlang-icu-messageformat-1 plugin](https://inlang.com/m/p7c8m1d2/plugin-inlang-icu-messageformat-1).
250
250
 
251
- **[Formatting Docs →](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/formatting)** · **[Pluralization & Variants Docs →](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/variants)**
251
+ **[Formatting Docs →](https://paraglidejs.com/formatting)** · **[Pluralization & Variants Docs →](https://paraglidejs.com/variants)**
252
252
 
253
253
  ## Why Compiler-First?
254
254
 
@@ -256,7 +256,7 @@ Runtime i18n libraries like i18next resolve message keys from dictionaries while
256
256
 
257
257
  That means Vite can tree-shake unused translations, TypeScript can autocomplete message keys and parameters, and your components call plain functions instead of resolving strings through a runtime lookup layer.
258
258
 
259
- In the [Paraglide benchmark](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/benchmark), typical scenarios shipped **47-144 KB with Paraglide** vs **205-422 KB with i18next**. With 5 locales, 100 used messages, and 200 total messages, Paraglide shipped **47 KB** while i18next shipped **205 KB**.
259
+ In the [Paraglide benchmark](https://paraglidejs.com/benchmark), typical scenarios shipped **47-144 KB with Paraglide** vs **205-422 KB with i18next**. With 5 locales, 100 used messages, and 200 total messages, Paraglide shipped **47 KB** while i18next shipped **205 KB**.
260
260
 
261
261
  Tree-shaking also keeps Paraglide stable as your message catalog grows. In the benchmark, using 100 messages shipped **47 KB** with Paraglide whether the project had 200, 500, or 1,000 total messages. The i18next runtime bundle grew from **205 KB** to **414 KB**.
262
262
 
@@ -273,7 +273,7 @@ Tree-shaking also keeps Paraglide stable as your message catalog grows. In the b
273
273
  | **Rich text** | ✅ Typed markup adapters | ✅ Rich-text components | Via framework wrappers |
274
274
  | **ICU MessageFormat 1** | ✅ [Via plugin](https://inlang.com/m/p7c8m1d2/plugin-inlang-icu-messageformat-1) | ✅ | Via plugin |
275
275
 
276
- **[Full Comparison →](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/comparison)**
276
+ **[Full Comparison →](https://paraglidejs.com/comparison)**
277
277
 
278
278
  ## FAQ
279
279
 
@@ -307,6 +307,12 @@ Yes. Paraglide can compile existing i18next translation files through the [i18ne
307
307
  >
308
308
  > Daniel · [Why I Replaced i18next with Paraglide JS](https://dropanote.de/en/blog/20250726-why-i-replaced-i18next-with-paraglide-js/)
309
309
 
310
+ ## Blog Posts
311
+
312
+ - [Why I Replaced i18next with Paraglide JS](https://dropanote.de/en/blog/20250726-why-i-replaced-i18next-with-paraglide-js/) — A developer's experience reducing bundle size from 40KB to 2KB
313
+ - [react-i18next Was Fine. Then I Found Paraglide.](https://brodin.dev/blog/paraglide-vs-react-i18n) — A developer's experience moving from react-i18next to Paraglide
314
+ - [Inlang / ParaglideJS blew my mind](https://dev.to/robertosnap/inlang-paraglidejs-blew-my-mind-3984) — A developer's first impressions using Paraglide JS with Remix
315
+
310
316
  ## Talks
311
317
 
312
318
  - [Paraglide JS 1.0 announcement](https://www.youtube.com/watch?v=-YES3CCAG90)
@@ -327,12 +333,12 @@ Paraglide compiles messages from [inlang](https://github.com/opral/inlang), the
327
333
 
328
334
  ## Documentation
329
335
 
330
- - [Getting Started](https://inlang.com/m/gerre34r/library-inlang-paraglideJs)
331
- - [Framework Guides](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/react-router) (React Router, SvelteKit, Astro, etc.)
332
- - [Message Syntax & Pluralization](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/variants)
333
- - [Formatting (Number/Date/Relative Time)](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/formatting)
334
- - [Routing & SSR](https://inlang.com/m/gerre34r/library-inlang-paraglideJs/server-side-rendering)
335
- - [API Reference](https://inlang.com/m/gerre34r/library-inlang-paraglideJs)
336
+ - [Getting Started](https://paraglidejs.com)
337
+ - [Framework Guides](https://paraglidejs.com/react-router) (React Router, SvelteKit, Astro, etc.)
338
+ - [Message Syntax & Pluralization](https://paraglidejs.com/variants)
339
+ - [Formatting (Number/Date/Relative Time)](https://paraglidejs.com/formatting)
340
+ - [Routing & SSR](https://paraglidejs.com/server-side-rendering)
341
+ - [API Reference](https://paraglidejs.com)
336
342
 
337
343
  ## Contributing
338
344
 
@@ -1 +1 @@
1
- {"version":3,"file":"unplugin.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/unplugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAIhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAiDvE,eAAO,MAAM,eAAe,EAAE,eAAe,CAAC,eAAe,CAqJ3D,CAAC"}
1
+ {"version":3,"file":"unplugin.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/unplugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAMhD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AA8IvE,eAAO,MAAM,eAAe,EAAE,eAAe,CAAC,eAAe,CAiN5D,CAAC"}
@@ -1,176 +1,284 @@
1
1
  import { compile } from "../compiler/compile.js";
2
- import path, { relative } from "node:path";
2
+ import { relative } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import nodeFs from "node:fs";
3
5
  import { Logger } from "../services/logger/index.js";
4
6
  import { createTrackedFs, getWatchTargets, isPathWithinDirectories, } from "../services/file-watching/tracked-fs.js";
5
7
  import { nodeNormalizePath } from "../utilities/node-normalize-path.js";
6
- import { hashDirectory } from "../services/file-handling/write-output.js";
8
+ import { seedPreviousCompilationFromOutdir } from "../compiler/seed-previous-compilation.js";
7
9
  const PLUGIN_NAME = "unplugin-paraglide-js";
8
10
  const logger = new Logger();
9
11
  /**
10
12
  * Default isServer which differs per bundler.
11
13
  */
12
14
  let isServer;
13
- let previousCompilation;
14
- const { fs: trackedFs, readFiles, clearReadFiles } = createTrackedFs();
15
+ // Module-scoped so the warm state survives plugin re-instantiation within
16
+ // one process (e.g. a vite config reload), but recreated when a different
17
+ // fs is passed — the tracked wrapper, read set, and cached compilation are
18
+ // only valid for the filesystem they were produced from.
19
+ let pluginState;
20
+ function getPluginState(args) {
21
+ if (pluginState === undefined || pluginState.baseFs !== args.fs) {
22
+ const tracked = createTrackedFs({ fs: args.fs });
23
+ pluginState = {
24
+ baseFs: args.fs,
25
+ trackedFs: tracked.fs,
26
+ readFiles: tracked.readFiles,
27
+ clearReadFiles: tracked.clearReadFiles,
28
+ previousCompilation: undefined,
29
+ previousInputsDigest: undefined,
30
+ };
31
+ }
32
+ return pluginState;
33
+ }
15
34
  function withoutCleanOutdir(args) {
16
35
  const { cleanOutdir, ...compileArgs } = args;
17
36
  void cleanOutdir;
18
37
  return compileArgs;
19
38
  }
20
39
  /**
21
- * Seed a synthetic `previousCompilation` from files already on disk in
22
- * `outdir`. This lets the first compile in a fresh process diff against
23
- * the prior build's output instead of treating it as empty — so warm
24
- * restarts produce zero writes and we never wipe `outdir` out from under
25
- * concurrent readers (SSR/prerender, sibling Vite processes).
40
+ * Hashes the files (and directory listings) the last compilation read,
41
+ * together with the options that affect the output. Returns `undefined`
42
+ * when the digest can't be computed (no tracked reads yet, an unexpected
43
+ * read error, ...) `undefined` never matches, so the caller compiles.
26
44
  *
27
- * See https://github.com/opral/inlang-paraglide-js/issues/659.
45
+ * Directory listings are included so that a message file *added* next to
46
+ * the tracked ones invalidates the digest, not only edits to known files.
47
+ * All components are length-prefixed so distinct input states can't
48
+ * produce the same hash stream.
49
+ *
50
+ * The digest is taken after the compile, by re-reading the inputs. A file
51
+ * edited *during* a compile can therefore be hashed at its new content
52
+ * while the output reflects the old one — accepted: in dev, watchChange
53
+ * recompiles on that edit, and a fresh build process always recompiles.
28
54
  */
29
- async function seedPreviousCompilation(outdir, fs) {
30
- const absoluteOutdir = path.resolve(process.cwd(), outdir);
31
- const resolvedFs = fs ?? (await import("node:fs"));
32
- const outputHashes = await hashDirectory(absoluteOutdir, resolvedFs.promises);
33
- if (!outputHashes)
55
+ async function computeInputsDigest(state, args, outputStructure) {
56
+ if (state.readFiles.size === 0) {
34
57
  return undefined;
35
- return { outputHashes };
36
- }
37
- export const unpluginFactory = (args) => ({
38
- name: PLUGIN_NAME,
39
- enforce: "pre",
40
- async buildStart() {
41
- const isProduction = process.env.NODE_ENV === "production";
42
- // default to locale-modules for development to speed up the dev server
43
- // https://github.com/opral/inlang-paraglide-js/issues/486
44
- const outputStructure = args.outputStructure ??
45
- (isProduction ? "message-modules" : "locale-modules");
46
- try {
47
- // On a fresh process, seed previousCompilation from on-disk hashes
48
- // so the first compile is a no-op when inputs are unchanged. Avoids
49
- // racing concurrent readers that wiping outdir would interrupt.
50
- const seededPrevious = previousCompilation ??
51
- (await seedPreviousCompilation(args.outdir, args.fs));
52
- previousCompilation = await compile({
53
- fs: trackedFs,
54
- previousCompilation: seededPrevious,
55
- outputStructure,
56
- isServer,
57
- ...withoutCleanOutdir(args),
58
- cleanOutdir: false,
59
- });
60
- logger.success(`Compilation complete (${outputStructure})`);
61
- }
62
- catch (error) {
63
- logger.error("Failed to compile project:", error.message);
64
- logger.info("Please check your translation files for syntax errors.");
65
- if (isProduction)
66
- throw error;
67
- }
68
- finally {
69
- // in any case add the files to watch
70
- const targets = getWatchTargets(readFiles, { outdir: args.outdir });
71
- for (const filePath of targets.files) {
72
- this.addWatchFile(filePath);
58
+ }
59
+ const targets = getWatchTargets(state.readFiles, { outdir: args.outdir });
60
+ if (targets.files.size === 0) {
61
+ return undefined;
62
+ }
63
+ const fsp = (args.fs ?? nodeFs).promises;
64
+ const hash = createHash("sha256");
65
+ try {
66
+ const { fs: _fs, ...serializableArgs } = args;
67
+ void _fs;
68
+ hash.update(JSON.stringify({ ...serializableArgs, outputStructure, isServer }));
69
+ for (const directoryPath of [...targets.directories].sort()) {
70
+ const entries = await fsp
71
+ .readdir(directoryPath)
72
+ // tracked reads include probed-but-absent paths (the SDK reads
73
+ // optional files and handles ENOENT itself) — a missing entry
74
+ // is valid input state, hash it as such
75
+ .catch(rethrowUnlessEnoent);
76
+ hash.update(`\0dir:${directoryPath.length}:${directoryPath}:`);
77
+ if (entries === undefined) {
78
+ hash.update("missing");
73
79
  }
74
- for (const directoryPath of targets.directories) {
75
- this.addWatchFile(directoryPath);
80
+ else {
81
+ for (const entry of [...entries].sort()) {
82
+ hash.update(`${entry.length}:${entry},`);
83
+ }
76
84
  }
77
85
  }
78
- },
79
- async watchChange(path) {
80
- const normalizedPath = nodeNormalizePath(path);
81
- const targets = getWatchTargets(readFiles, { outdir: args.outdir });
82
- if (targets.isIgnoredPath(normalizedPath)) {
83
- return;
84
- }
85
- const shouldCompile = targets.files.has(normalizedPath) ||
86
- isPathWithinDirectories(normalizedPath, targets.directories);
87
- if (shouldCompile === false) {
88
- return;
89
- }
90
- const isProduction = process.env.NODE_ENV === "production";
91
- // default to locale-modules for development to speed up the dev server
92
- // https://github.com/opral/inlang-paraglide-js/issues/486
93
- const outputStructure = args.outputStructure ??
94
- (isProduction ? "message-modules" : "locale-modules");
95
- const previouslyReadFiles = new Set(readFiles);
96
- try {
97
- logger.info(`Re-compiling inlang project... File "${relative(process.cwd(), path)}" has changed.`);
98
- // Clear readFiles to track fresh file reads
99
- clearReadFiles();
100
- previousCompilation = await compile({
101
- fs: trackedFs,
102
- previousCompilation,
103
- outputStructure,
104
- isServer,
105
- ...withoutCleanOutdir(args),
106
- cleanOutdir: false,
107
- });
108
- logger.success(`Re-compilation complete (${outputStructure})`);
109
- // Add any new files to watch
110
- const nextTargets = getWatchTargets(readFiles, { outdir: args.outdir });
111
- for (const filePath of nextTargets.files) {
112
- this.addWatchFile(filePath);
86
+ for (const filePath of [...targets.files].sort()) {
87
+ const content = await fsp.readFile(filePath).catch(rethrowUnlessEnoent);
88
+ hash.update(`\0file:${filePath.length}:${filePath}:`);
89
+ if (content === undefined) {
90
+ hash.update("missing");
113
91
  }
114
- for (const directoryPath of nextTargets.directories) {
115
- this.addWatchFile(directoryPath);
92
+ else {
93
+ hash.update(`${content.length}:`);
94
+ hash.update(content);
116
95
  }
117
96
  }
118
- catch (e) {
119
- clearReadFiles();
120
- for (const filePath of previouslyReadFiles) {
121
- readFiles.add(filePath);
122
- }
123
- // Reset compilation result on error
124
- previousCompilation = undefined;
125
- logger.warn("Failed to re-compile project:", e.message);
126
- }
127
- },
128
- vite: {
129
- config: {
130
- handler: () => {
131
- isServer = "import.meta.env?.SSR ?? typeof window === 'undefined'";
132
- },
133
- },
134
- configEnvironment: {
135
- handler: () => {
136
- isServer = "import.meta.env?.SSR ?? typeof window === 'undefined'";
137
- },
138
- },
139
- },
140
- webpack(compiler) {
141
- compiler.options.resolve = {
142
- ...compiler.options.resolve,
143
- fallback: {
144
- ...compiler.options.resolve?.fallback,
145
- // https://stackoverflow.com/a/72989932
146
- async_hooks: false,
147
- },
148
- };
149
- compiler.hooks.beforeRun.tapPromise(PLUGIN_NAME, async () => {
97
+ }
98
+ catch {
99
+ return undefined;
100
+ }
101
+ return hash.digest("hex");
102
+ }
103
+ function rethrowUnlessEnoent(error) {
104
+ if (error?.code === "ENOENT") {
105
+ return undefined;
106
+ }
107
+ throw error;
108
+ }
109
+ export const unpluginFactory = (args) => {
110
+ const state = getPluginState(args);
111
+ const { trackedFs, readFiles, clearReadFiles } = state;
112
+ return {
113
+ name: PLUGIN_NAME,
114
+ enforce: "pre",
115
+ async buildStart() {
150
116
  const isProduction = process.env.NODE_ENV === "production";
151
117
  // default to locale-modules for development to speed up the dev server
152
118
  // https://github.com/opral/inlang-paraglide-js/issues/486
153
119
  const outputStructure = args.outputStructure ??
154
120
  (isProduction ? "message-modules" : "locale-modules");
155
121
  try {
156
- const seededPrevious = previousCompilation ??
157
- (await seedPreviousCompilation(args.outdir, args.fs));
158
- previousCompilation = await compile({
159
- fs: trackedFs,
122
+ // `vite build` calls buildStart once per environment (client, ssr).
123
+ // Skip the expensive compile when the inputs haven't changed.
124
+ if (state.previousCompilation && state.previousInputsDigest) {
125
+ const currentDigest = await computeInputsDigest(state, args, outputStructure);
126
+ if (currentDigest === state.previousInputsDigest) {
127
+ logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
128
+ return;
129
+ }
130
+ }
131
+ // On a fresh process, seed previousCompilation from on-disk hashes
132
+ // so the first compile is a no-op when inputs are unchanged. Avoids
133
+ // racing concurrent readers that wiping outdir would interrupt.
134
+ const seededPrevious = state.previousCompilation ??
135
+ (await seedPreviousCompilationFromOutdir({
136
+ outdir: args.outdir,
137
+ fs: args.fs?.promises,
138
+ }));
139
+ state.previousCompilation = await compile({
160
140
  previousCompilation: seededPrevious,
161
141
  outputStructure,
142
+ isServer,
162
143
  ...withoutCleanOutdir(args),
163
144
  cleanOutdir: false,
145
+ // after the args spread so a user-provided fs doesn't bypass
146
+ // the read tracking (trackedFs wraps args.fs when provided)
147
+ fs: trackedFs,
164
148
  });
149
+ state.previousInputsDigest = await computeInputsDigest(state, args, outputStructure);
165
150
  logger.success(`Compilation complete (${outputStructure})`);
166
151
  }
167
152
  catch (error) {
168
- logger.warn("Failed to compile project:", error.message);
169
- logger.warn("Please check your translation files for syntax errors.");
153
+ state.previousInputsDigest = undefined;
154
+ logger.error("Failed to compile project:", error.message);
155
+ logger.info("Please check your translation files for syntax errors.");
170
156
  if (isProduction)
171
157
  throw error;
172
158
  }
173
- });
174
- },
175
- });
159
+ finally {
160
+ // in any case add the files to watch
161
+ const targets = getWatchTargets(readFiles, { outdir: args.outdir });
162
+ for (const filePath of targets.files) {
163
+ this.addWatchFile(filePath);
164
+ }
165
+ for (const directoryPath of targets.directories) {
166
+ this.addWatchFile(directoryPath);
167
+ }
168
+ }
169
+ },
170
+ async watchChange(path) {
171
+ const normalizedPath = nodeNormalizePath(path);
172
+ const targets = getWatchTargets(readFiles, { outdir: args.outdir });
173
+ if (targets.isIgnoredPath(normalizedPath)) {
174
+ return;
175
+ }
176
+ const shouldCompile = targets.files.has(normalizedPath) ||
177
+ isPathWithinDirectories(normalizedPath, targets.directories);
178
+ if (shouldCompile === false) {
179
+ return;
180
+ }
181
+ const isProduction = process.env.NODE_ENV === "production";
182
+ // default to locale-modules for development to speed up the dev server
183
+ // https://github.com/opral/inlang-paraglide-js/issues/486
184
+ const outputStructure = args.outputStructure ??
185
+ (isProduction ? "message-modules" : "locale-modules");
186
+ const previouslyReadFiles = new Set(readFiles);
187
+ try {
188
+ logger.info(`Re-compiling inlang project... File "${relative(process.cwd(), path)}" has changed.`);
189
+ // Clear readFiles to track fresh file reads
190
+ clearReadFiles();
191
+ state.previousCompilation = await compile({
192
+ previousCompilation: state.previousCompilation,
193
+ outputStructure,
194
+ isServer,
195
+ ...withoutCleanOutdir(args),
196
+ cleanOutdir: false,
197
+ fs: trackedFs,
198
+ });
199
+ state.previousInputsDigest = await computeInputsDigest(state, args, outputStructure);
200
+ logger.success(`Re-compilation complete (${outputStructure})`);
201
+ // Add any new files to watch
202
+ const nextTargets = getWatchTargets(readFiles, { outdir: args.outdir });
203
+ for (const filePath of nextTargets.files) {
204
+ this.addWatchFile(filePath);
205
+ }
206
+ for (const directoryPath of nextTargets.directories) {
207
+ this.addWatchFile(directoryPath);
208
+ }
209
+ }
210
+ catch (e) {
211
+ clearReadFiles();
212
+ for (const filePath of previouslyReadFiles) {
213
+ readFiles.add(filePath);
214
+ }
215
+ // Reset compilation result on error
216
+ state.previousCompilation = undefined;
217
+ state.previousInputsDigest = undefined;
218
+ logger.warn("Failed to re-compile project:", e.message);
219
+ }
220
+ },
221
+ vite: {
222
+ config: {
223
+ handler: () => {
224
+ isServer = "import.meta.env?.SSR ?? typeof window === 'undefined'";
225
+ },
226
+ },
227
+ configEnvironment: {
228
+ handler: () => {
229
+ isServer = "import.meta.env?.SSR ?? typeof window === 'undefined'";
230
+ },
231
+ },
232
+ },
233
+ webpack(compiler) {
234
+ compiler.options.resolve = {
235
+ ...compiler.options.resolve,
236
+ fallback: {
237
+ ...compiler.options.resolve?.fallback,
238
+ // https://stackoverflow.com/a/72989932
239
+ async_hooks: false,
240
+ },
241
+ };
242
+ compiler.hooks.beforeRun.tapPromise(PLUGIN_NAME, async () => {
243
+ const isProduction = process.env.NODE_ENV === "production";
244
+ // default to locale-modules for development to speed up the dev server
245
+ // https://github.com/opral/inlang-paraglide-js/issues/486
246
+ const outputStructure = args.outputStructure ??
247
+ (isProduction ? "message-modules" : "locale-modules");
248
+ try {
249
+ // Multi-compiler webpack setups (client + server) trigger
250
+ // beforeRun once per compiler — skip when inputs are unchanged.
251
+ if (state.previousCompilation && state.previousInputsDigest) {
252
+ const currentDigest = await computeInputsDigest(state, args, outputStructure);
253
+ if (currentDigest === state.previousInputsDigest) {
254
+ logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
255
+ return;
256
+ }
257
+ }
258
+ const seededPrevious = state.previousCompilation ??
259
+ (await seedPreviousCompilationFromOutdir({
260
+ outdir: args.outdir,
261
+ fs: args.fs?.promises,
262
+ }));
263
+ state.previousCompilation = await compile({
264
+ previousCompilation: seededPrevious,
265
+ outputStructure,
266
+ ...withoutCleanOutdir(args),
267
+ cleanOutdir: false,
268
+ fs: trackedFs,
269
+ });
270
+ state.previousInputsDigest = await computeInputsDigest(state, args, outputStructure);
271
+ logger.success(`Compilation complete (${outputStructure})`);
272
+ }
273
+ catch (error) {
274
+ state.previousInputsDigest = undefined;
275
+ logger.warn("Failed to compile project:", error.message);
276
+ logger.warn("Please check your translation files for syntax errors.");
277
+ if (isProduction)
278
+ throw error;
279
+ }
280
+ });
281
+ },
282
+ };
283
+ };
176
284
  //# sourceMappingURL=unplugin.js.map