@inlang/paraglide-js 2.3.1 → 2.4.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.
@@ -1,2 +1,2 @@
1
- export declare const paraglideVitePlugin: (options: import("../index.js").CompilerOptions) => import("unplugin").VitePlugin<any> | import("unplugin").VitePlugin<any>[];
1
+ export declare const paraglideVitePlugin: (options: import("../index.js").CompilerOptions) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
2
2
  //# sourceMappingURL=vite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/vite.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,+HAAoC,CAAC"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/vite.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,+GAAoC,CAAC"}
@@ -69,7 +69,7 @@ export const compileProject = async (args) => {
69
69
  for (const [filename, content] of Object.entries(output)) {
70
70
  if (optionsWithDefaults.includeEslintDisableComment) {
71
71
  if (filename.endsWith(".js")) {
72
- output[filename] = `// eslint-disable\n${content}`;
72
+ output[filename] = `/* eslint-disable */\n${content}`;
73
73
  }
74
74
  }
75
75
  }
@@ -70,7 +70,7 @@ test("handles message bundles with a : in the id", async () => {
70
70
  },
71
71
  }),
72
72
  });
73
- await insertBundleNested(project.db, createBundleNested({
73
+ await insertBundleNested(project, createBundleNested({
74
74
  id: "hello:world",
75
75
  messages: [
76
76
  {
@@ -97,7 +97,7 @@ test("can emit message bundles with more than 255 characters", async () => {
97
97
  },
98
98
  }),
99
99
  });
100
- await insertBundleNested(project.db, createBundleNested({
100
+ await insertBundleNested(project, createBundleNested({
101
101
  // 300 characters long id
102
102
  id: "a".repeat(300),
103
103
  messages: [
@@ -199,7 +199,7 @@ describe.each([
199
199
  settings: { locales: ["en", "de"], baseLocale: "en" },
200
200
  }),
201
201
  });
202
- await insertBundleNested(project.db, createBundleNested({
202
+ await insertBundleNested(project, createBundleNested({
203
203
  id: "plural_test",
204
204
  declarations: [
205
205
  { type: "input-variable", name: "count" },
@@ -333,7 +333,7 @@ describe.each([
333
333
  settings: { locales: ["en", "de", "en-US"], baseLocale: "en" },
334
334
  }),
335
335
  });
336
- await insertBundleNested(project.db, createBundleNested({
336
+ await insertBundleNested(project, createBundleNested({
337
337
  id: "missingInGerman",
338
338
  messages: [
339
339
  {
@@ -363,7 +363,7 @@ describe.each([
363
363
  }),
364
364
  });
365
365
  // Add test messages
366
- await insertBundleNested(project.db, createBundleNested({
366
+ await insertBundleNested(project, createBundleNested({
367
367
  id: "greeting",
368
368
  messages: [
369
369
  {
@@ -380,7 +380,7 @@ describe.each([
380
380
  },
381
381
  ],
382
382
  }));
383
- await insertBundleNested(project.db, createBundleNested({
383
+ await insertBundleNested(project, createBundleNested({
384
384
  id: "farewell",
385
385
  messages: [
386
386
  {
@@ -473,7 +473,7 @@ describe.each([
473
473
  settings: { locales: ["en", "de"], baseLocale: "en" },
474
474
  }),
475
475
  });
476
- await insertBundleNested(project.db, createBundleNested({
476
+ await insertBundleNested(project, createBundleNested({
477
477
  id: "$502.23-hello_world",
478
478
  messages: [
479
479
  {
@@ -499,7 +499,7 @@ describe.each([
499
499
  settings: { locales: ["en", "en-US"], baseLocale: "en" },
500
500
  }),
501
501
  });
502
- await insertBundleNested(project.db, createBundleNested({
502
+ await insertBundleNested(project, createBundleNested({
503
503
  id: "exists_in_both",
504
504
  messages: [
505
505
  {
@@ -523,7 +523,7 @@ describe.each([
523
523
  },
524
524
  ],
525
525
  }));
526
- await insertBundleNested(project.db, createBundleNested({
526
+ await insertBundleNested(project, createBundleNested({
527
527
  id: "missing_in_en_US",
528
528
  messages: [
529
529
  {
@@ -552,7 +552,7 @@ describe.each([
552
552
  settings: { locales: ["en"], baseLocale: "en" },
553
553
  }),
554
554
  });
555
- await insertBundleNested(project.db, createBundleNested({
555
+ await insertBundleNested(project, createBundleNested({
556
556
  id: "happy🍌",
557
557
  messages: [
558
558
  {
@@ -578,7 +578,7 @@ describe.each([
578
578
  }),
579
579
  });
580
580
  // Create two bundles with the same name but different case
581
- await insertBundleNested(project.db, createBundleNested({
581
+ await insertBundleNested(project, createBundleNested({
582
582
  id: "Helloworld",
583
583
  messages: [
584
584
  {
@@ -593,7 +593,7 @@ describe.each([
593
593
  },
594
594
  ],
595
595
  }));
596
- await insertBundleNested(project.db, createBundleNested({
596
+ await insertBundleNested(project, createBundleNested({
597
597
  id: "helloworld",
598
598
  messages: [
599
599
  {
@@ -883,7 +883,7 @@ const mockBundles = [
883
883
  },
884
884
  ];
885
885
  for (const bundle of mockBundles) {
886
- await insertBundleNested(project.db, bundle);
886
+ await insertBundleNested(project, bundle);
887
887
  }
888
888
  function createBundleNested(args) {
889
889
  return {
@@ -237,7 +237,7 @@ test("includes eslint-disable comment", async () => {
237
237
  fs: fs,
238
238
  });
239
239
  const messages = await fs.promises.readFile("/output/messages.js", "utf8");
240
- expect(messages).toContain("// eslint-disable");
240
+ expect(messages).toContain("/* eslint-disable */");
241
241
  await compile({
242
242
  project: "/project.inlang",
243
243
  outdir: "/output",
@@ -245,7 +245,7 @@ test("includes eslint-disable comment", async () => {
245
245
  fs: fs,
246
246
  });
247
247
  const messagesWithoutComment = await fs.promises.readFile("/output/messages.js", "utf8");
248
- expect(messagesWithoutComment).not.toContain("// eslint-disable");
248
+ expect(messagesWithoutComment).not.toContain("/* eslint-disable */");
249
249
  });
250
250
  test("default compiler options should include cookied, variable and baseLocale to ensure easy try out of paraglide js, working both in server and browser environemnts", () => {
251
251
  // someone trying out paraglide js should be able to call `getLocale()` and `setLocale()`
@@ -298,16 +298,29 @@ test("emits warnings for modules that couldn't be imported via http", async () =
298
298
  const mock = vi.fn();
299
299
  consola.mockTypes(() => mock);
300
300
  const fs = memfs().fs;
301
+ const fetchMock = vi.fn().mockRejectedValue(new TypeError("network error"));
302
+ vi.stubGlobal("fetch", fetchMock);
303
+ const errorsSpy = vi.spyOn(project.errors, "get").mockResolvedValue([
304
+ {
305
+ message: "Failed to import module https://example.com/non-existent-paraglide-plugin.js",
306
+ },
307
+ ]);
301
308
  // save project to directory to test loading
302
309
  await saveProjectToDirectory({
303
310
  project,
304
311
  path: "/project.inlang",
305
312
  fs: fs.promises,
306
313
  });
307
- await compile({
308
- project: "/project.inlang",
309
- outdir: "/output",
310
- fs: fs,
311
- });
314
+ try {
315
+ await compile({
316
+ project: "/project.inlang",
317
+ outdir: "/output",
318
+ fs: fs,
319
+ });
320
+ }
321
+ finally {
322
+ vi.unstubAllGlobals();
323
+ errorsSpy.mockRestore();
324
+ }
312
325
  expect(mock).toHaveBeenCalled();
313
326
  });
@@ -1 +1 @@
1
- {"version":3,"file":"create-runtime.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/create-runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,EAAE;QAChB,QAAQ,EAAE,WAAW,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC;QACnD,UAAU,EAAE,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC;QACvD,YAAY,EAAE,WAAW,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC;QAC3D,YAAY,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;QAC9C,WAAW,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,CAAC;QAC7C,qCAAqC,EAAE,eAAe,CAAC,uCAAuC,CAAC,CAAC;QAChG,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;QACtC,eAAe,EAAE,eAAe,CAAC,iBAAiB,CAAC,CAAC;QACpD,wBAAwB,EAAE,WAAW,CACpC,eAAe,CAAC,0BAA0B,CAAC,CAC3C,CAAC;KACF,CAAC;CACF,GAAG,MAAM,CA0IT"}
1
+ {"version":3,"file":"create-runtime.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/create-runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,EAAE;QAChB,QAAQ,EAAE,WAAW,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC;QACnD,UAAU,EAAE,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC;QACvD,YAAY,EAAE,WAAW,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC;QAC3D,YAAY,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;QAC9C,WAAW,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,CAAC;QAC7C,qCAAqC,EAAE,eAAe,CAAC,uCAAuC,CAAC,CAAC;QAChG,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;QACtC,eAAe,EAAE,eAAe,CAAC,iBAAiB,CAAC,CAAC;QACpD,wBAAwB,EAAE,WAAW,CACpC,eAAe,CAAC,0BAA0B,CAAC,CAC3C,CAAC;KACF,CAAC;CACF,GAAG,MAAM,CA4IT"}
@@ -74,6 +74,8 @@ ${injectCode("./extract-locale-from-url.js")}
74
74
 
75
75
  ${injectCode("./localize-url.js")}
76
76
 
77
+ ${injectCode("./should-redirect.js")}
78
+
77
79
  ${injectCode("./localize-href.js")}
78
80
 
79
81
  ${injectCode("./track-message-call.js")}
@@ -1,3 +1,6 @@
1
+ /**
2
+ * @typedef {(newLocale: Locale, options?: { reload?: boolean }) => void | Promise<void>} SetLocaleFn
3
+ */
1
4
  /**
2
5
  * Set the locale.
3
6
  *
@@ -15,10 +18,11 @@
15
18
  * @example
16
19
  * setLocale('en', { reload: false });
17
20
  *
18
- * @type {(newLocale: Locale, options?: { reload?: boolean }) => Promise<void> | void}
21
+ * @type {SetLocaleFn}
19
22
  */
20
- export let setLocale: (newLocale: Locale, options?: {
23
+ export let setLocale: SetLocaleFn;
24
+ export function overwriteSetLocale(fn: SetLocaleFn): void;
25
+ export type SetLocaleFn = (newLocale: Locale, options?: {
21
26
  reload?: boolean;
22
- }) => Promise<void> | void;
23
- export function overwriteSetLocale(fn: (newLocale: Locale) => void): void;
27
+ }) => void | Promise<void>;
24
28
  //# sourceMappingURL=set-locale.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"set-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/set-locale.js"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;;;;;;GAkBG;AACH,sBAFU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAmGnF;AAgBK,uCAFI,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,QAIrC"}
1
+ {"version":3,"file":"set-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/set-locale.js"],"names":[],"mappings":"AAgCA;;GAEG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,sBAFU,WAAW,CAyGnB;AAgBK,uCAFI,WAAW,QAIrB;0BA/IY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC"}
@@ -18,6 +18,9 @@ const navigateOrReload = (newLocation) => {
18
18
  window.location.reload();
19
19
  }
20
20
  };
21
+ /**
22
+ * @typedef {(newLocale: Locale, options?: { reload?: boolean }) => void | Promise<void>} SetLocaleFn
23
+ */
21
24
  /**
22
25
  * Set the locale.
23
26
  *
@@ -35,7 +38,7 @@ const navigateOrReload = (newLocation) => {
35
38
  * @example
36
39
  * setLocale('en', { reload: false });
37
40
  *
38
- * @type {(newLocale: Locale, options?: { reload?: boolean }) => Promise<void> | void}
41
+ * @type {SetLocaleFn}
39
42
  */
40
43
  export let setLocale = (newLocale, options) => {
41
44
  const optionsWithDefaults = {
@@ -44,6 +47,7 @@ export let setLocale = (newLocale, options) => {
44
47
  };
45
48
  // locale is already set
46
49
  // https://github.com/opral/inlang-paraglide-js/issues/430
50
+ /** @type {Locale | undefined} */
47
51
  let currentLocale;
48
52
  try {
49
53
  currentLocale = getLocale();
@@ -102,31 +106,33 @@ export let setLocale = (newLocale, options) => {
102
106
  else if (isCustomStrategy(strat) && customClientStrategies.has(strat)) {
103
107
  const handler = customClientStrategies.get(strat);
104
108
  if (handler) {
105
- const result = handler.setLocale(newLocale);
106
- // Handle async setLocale - fire and forget
109
+ let result = handler.setLocale(newLocale);
110
+ // Handle async setLocale
107
111
  if (result instanceof Promise) {
108
- result.catch((error) => {
109
- error.message = `Custom strategy "${strat}" setLocale failed: ${error.message}`;
112
+ result = result.catch((error) => {
113
+ throw new Error(`Custom strategy "${strat}" setLocale failed.`, {
114
+ cause: error,
115
+ });
110
116
  });
111
117
  customSetLocalePromises.push(result);
112
118
  }
113
119
  }
114
120
  }
115
121
  }
116
- if (!isServer &&
117
- optionsWithDefaults.reload &&
118
- window.location &&
119
- newLocale !== currentLocale) {
120
- if (customSetLocalePromises.length) {
121
- // Wait for any async custom setLocale functions
122
- return Promise.all(customSetLocalePromises).then(() => {
123
- navigateOrReload(newLocation);
124
- });
125
- }
126
- else {
122
+ const runReload = () => {
123
+ if (!isServer &&
124
+ optionsWithDefaults.reload &&
125
+ window.location &&
126
+ newLocale !== currentLocale) {
127
127
  navigateOrReload(newLocation);
128
128
  }
129
+ };
130
+ if (customSetLocalePromises.length) {
131
+ return Promise.all(customSetLocalePromises).then(() => {
132
+ runReload();
133
+ });
129
134
  }
135
+ runReload();
130
136
  return;
131
137
  };
132
138
  /**
@@ -141,8 +147,8 @@ export let setLocale = (newLocale, options) => {
141
147
  * return Cookies.set('locale', newLocale)
142
148
  * });
143
149
  *
144
- * @param {(newLocale: Locale) => void} fn
150
+ * @param {SetLocaleFn} fn
145
151
  */
146
152
  export const overwriteSetLocale = (fn) => {
147
- setLocale = fn;
153
+ setLocale = /** @type {SetLocaleFn} */ (fn);
148
154
  };
@@ -155,6 +155,31 @@ test("when strategy precedes URL, it should set the locale and re-direct to the
155
155
  expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=en; path=/; max-age=34560000");
156
156
  expect(globalThis.window.location.href).toBe("https://example.com/en/some-path");
157
157
  });
158
+ test("overwriteSetLocale receives the options object", async () => {
159
+ const runtime = await createParaglide({
160
+ blob: await newProject({
161
+ settings: {
162
+ baseLocale: "en",
163
+ locales: ["en", "fr"],
164
+ },
165
+ }),
166
+ strategy: ["cookie"],
167
+ cookieName: "PARAGLIDE_LOCALE",
168
+ });
169
+ // Provide minimal browser globals to avoid strategy branches failing.
170
+ /** @ts-expect-error - browser shim for tests */
171
+ globalThis.document = { cookie: "" };
172
+ globalThis.window = {
173
+ location: {
174
+ href: "https://example.com/en",
175
+ reload: vi.fn(),
176
+ },
177
+ };
178
+ const spy = vi.fn();
179
+ runtime.overwriteSetLocale(spy);
180
+ runtime.setLocale("fr", { reload: false });
181
+ expect(spy).toHaveBeenCalledWith("fr", { reload: false });
182
+ });
158
183
  // https://github.com/opral/inlang-paraglide-js/issues/430
159
184
  test("should not reload when setting locale to current locale", async () => {
160
185
  // @ts-expect-error - global variable definition
@@ -317,6 +342,31 @@ test("calls setLocale on multiple custom strategies", async () => {
317
342
  expect(customLocale1).toBe("de");
318
343
  expect(customLocale2).toBe("de");
319
344
  });
345
+ test("setLocale should return a promise if any custom setLocale function is async", async () => {
346
+ let customLocale1 = "en";
347
+ const runtime = await createParaglide({
348
+ blob: await newProject({
349
+ settings: {
350
+ baseLocale: "en",
351
+ locales: ["en", "fr", "de"],
352
+ },
353
+ }),
354
+ strategy: ["custom-async", "baseLocale"],
355
+ isServer: "false",
356
+ });
357
+ runtime.defineCustomClientStrategy("custom-async", {
358
+ getLocale: () => customLocale1,
359
+ setLocale: async (locale) => {
360
+ customLocale1 = locale;
361
+ },
362
+ });
363
+ const setLocalePromise = runtime.setLocale("de");
364
+ const setLocalePromiseWithoutReload = runtime.setLocale("de", {
365
+ reload: false,
366
+ });
367
+ expect(setLocalePromise).toBeInstanceOf(Promise);
368
+ expect(setLocalePromiseWithoutReload).toBeInstanceOf(Promise);
369
+ });
320
370
  test("awaits async setLocale functions to resolve in custom strategy", async () => {
321
371
  let customLocale1 = "en";
322
372
  globalThis.window = {
@@ -408,7 +458,9 @@ test("reload should not run if async setLocale function rejects in custom strate
408
458
  throw new Error("fetch error");
409
459
  },
410
460
  });
411
- await expect(() => runtime.setLocale("de")).rejects.toThrowError(`Custom strategy "custom-async" setLocale failed: fetch error`);
461
+ const error = expect(() => runtime.setLocale("de")).rejects;
462
+ await error.toThrowError(`Custom strategy "custom-async" setLocale failed.`);
463
+ await error.toMatchObject({ cause: { message: "fetch error" } });
412
464
  // Verify that reload was never called
413
465
  expect(window.location.reload).toHaveBeenCalledTimes(0);
414
466
  expect(customLocale1).toBe("en");
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @typedef {object} ShouldRedirectServerInput
3
+ * @property {Request} request
4
+ * @property {string | URL} [url]
5
+ * @property {ReturnType<typeof assertIsLocale>} [locale]
6
+ *
7
+ * @typedef {object} ShouldRedirectClientInput
8
+ * @property {undefined} [request]
9
+ * @property {string | URL} [url]
10
+ * @property {ReturnType<typeof assertIsLocale>} [locale]
11
+ *
12
+ * @typedef {ShouldRedirectServerInput | ShouldRedirectClientInput} ShouldRedirectInput
13
+ *
14
+ * @typedef {object} ShouldRedirectResult
15
+ * @property {boolean} shouldRedirect - Indicates whether the consumer should perform a redirect.
16
+ * @property {ReturnType<typeof assertIsLocale>} locale - Locale resolved using the configured strategies.
17
+ * @property {URL | undefined} redirectUrl - Destination URL when a redirect is required.
18
+ */
19
+ /**
20
+ * Determines whether a redirect is required to align the current URL with the active locale.
21
+ *
22
+ * This helper mirrors the logic that powers `paraglideMiddleware`, but works in both server
23
+ * and client environments. It evaluates the configured strategies in order, computes the
24
+ * canonical localized URL, and reports when the current URL does not match.
25
+ *
26
+ * When called in the browser without arguments, the current `window.location.href` is used.
27
+ *
28
+ * @example
29
+ * // Client side usage (e.g. TanStack Router beforeLoad hook)
30
+ * async function beforeLoad({ location }) {
31
+ * const decision = await shouldRedirect({ url: location.href });
32
+ *
33
+ * if (decision.shouldRedirect) {
34
+ * throw redirect({ to: decision.redirectUrl.href });
35
+ * }
36
+ * }
37
+ *
38
+ * @example
39
+ * // Server side usage with a Request
40
+ * export async function handle(request) {
41
+ * const decision = await shouldRedirect({ request });
42
+ *
43
+ * if (decision.shouldRedirect) {
44
+ * return Response.redirect(decision.redirectUrl, 307);
45
+ * }
46
+ *
47
+ * return render(request, decision.locale);
48
+ * }
49
+ *
50
+ * @param {ShouldRedirectInput} [input]
51
+ * @returns {Promise<ShouldRedirectResult>}
52
+ */
53
+ export function shouldRedirect(input?: ShouldRedirectInput): Promise<ShouldRedirectResult>;
54
+ export type ShouldRedirectServerInput = {
55
+ request: Request;
56
+ url?: string | URL | undefined;
57
+ locale?: ReturnType<typeof assertIsLocale>;
58
+ };
59
+ export type ShouldRedirectClientInput = {
60
+ request?: undefined;
61
+ url?: string | URL | undefined;
62
+ locale?: ReturnType<typeof assertIsLocale>;
63
+ };
64
+ export type ShouldRedirectInput = ShouldRedirectServerInput | ShouldRedirectClientInput;
65
+ export type ShouldRedirectResult = {
66
+ /**
67
+ * - Indicates whether the consumer should perform a redirect.
68
+ */
69
+ shouldRedirect: boolean;
70
+ /**
71
+ * - Locale resolved using the configured strategies.
72
+ */
73
+ locale: ReturnType<typeof assertIsLocale>;
74
+ /**
75
+ * - Destination URL when a redirect is required.
76
+ */
77
+ redirectUrl: URL | undefined;
78
+ };
79
+ import { assertIsLocale } from "./assert-is-locale.js";
80
+ //# sourceMappingURL=should-redirect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"should-redirect.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/should-redirect.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,uCAHW,mBAAmB,GACjB,OAAO,CAAC,oBAAoB,CAAC,CAsBzC;;aAvEa,OAAO;;aAEP,UAAU,CAAC,OAAO,cAAc,CAAC;;;cAGjC,SAAS;;aAET,UAAU,CAAC,OAAO,cAAc,CAAC;;kCAElC,yBAAyB,GAAG,yBAAyB;;;;;oBAGpD,OAAO;;;;YACP,UAAU,CAAC,OAAO,cAAc,CAAC;;;;iBACjC,GAAG,GAAG,SAAS;;+BAnBE,uBAAuB"}
@@ -0,0 +1,119 @@
1
+ import { localizeUrl } from "./localize-url.js";
2
+ import { getLocale } from "./get-locale.js";
3
+ import { getUrlOrigin } from "./get-url-origin.js";
4
+ import { extractLocaleFromRequestAsync } from "./extract-locale-from-request-async.js";
5
+ import { assertIsLocale } from "./assert-is-locale.js";
6
+ import { strategy } from "./variables.js";
7
+ /**
8
+ * @typedef {object} ShouldRedirectServerInput
9
+ * @property {Request} request
10
+ * @property {string | URL} [url]
11
+ * @property {ReturnType<typeof assertIsLocale>} [locale]
12
+ *
13
+ * @typedef {object} ShouldRedirectClientInput
14
+ * @property {undefined} [request]
15
+ * @property {string | URL} [url]
16
+ * @property {ReturnType<typeof assertIsLocale>} [locale]
17
+ *
18
+ * @typedef {ShouldRedirectServerInput | ShouldRedirectClientInput} ShouldRedirectInput
19
+ *
20
+ * @typedef {object} ShouldRedirectResult
21
+ * @property {boolean} shouldRedirect - Indicates whether the consumer should perform a redirect.
22
+ * @property {ReturnType<typeof assertIsLocale>} locale - Locale resolved using the configured strategies.
23
+ * @property {URL | undefined} redirectUrl - Destination URL when a redirect is required.
24
+ */
25
+ /**
26
+ * Determines whether a redirect is required to align the current URL with the active locale.
27
+ *
28
+ * This helper mirrors the logic that powers `paraglideMiddleware`, but works in both server
29
+ * and client environments. It evaluates the configured strategies in order, computes the
30
+ * canonical localized URL, and reports when the current URL does not match.
31
+ *
32
+ * When called in the browser without arguments, the current `window.location.href` is used.
33
+ *
34
+ * @example
35
+ * // Client side usage (e.g. TanStack Router beforeLoad hook)
36
+ * async function beforeLoad({ location }) {
37
+ * const decision = await shouldRedirect({ url: location.href });
38
+ *
39
+ * if (decision.shouldRedirect) {
40
+ * throw redirect({ to: decision.redirectUrl.href });
41
+ * }
42
+ * }
43
+ *
44
+ * @example
45
+ * // Server side usage with a Request
46
+ * export async function handle(request) {
47
+ * const decision = await shouldRedirect({ request });
48
+ *
49
+ * if (decision.shouldRedirect) {
50
+ * return Response.redirect(decision.redirectUrl, 307);
51
+ * }
52
+ *
53
+ * return render(request, decision.locale);
54
+ * }
55
+ *
56
+ * @param {ShouldRedirectInput} [input]
57
+ * @returns {Promise<ShouldRedirectResult>}
58
+ */
59
+ export async function shouldRedirect(input = {}) {
60
+ const locale = /** @type {ReturnType<typeof assertIsLocale>} */ (await resolveLocale(input));
61
+ if (!strategy.includes("url")) {
62
+ return { shouldRedirect: false, locale, redirectUrl: undefined };
63
+ }
64
+ const currentUrl = resolveUrl(input);
65
+ const localizedUrl = localizeUrl(currentUrl.href, { locale });
66
+ const shouldRedirectToLocalizedUrl = normalizeUrl(localizedUrl.href) !== normalizeUrl(currentUrl.href);
67
+ return {
68
+ shouldRedirect: shouldRedirectToLocalizedUrl,
69
+ locale,
70
+ redirectUrl: shouldRedirectToLocalizedUrl ? localizedUrl : undefined,
71
+ };
72
+ }
73
+ /**
74
+ * Resolves the locale either from the provided input or by using the configured strategies.
75
+ *
76
+ * @param {ShouldRedirectInput} input
77
+ * @returns {Promise<ReturnType<typeof assertIsLocale>>}
78
+ */
79
+ async function resolveLocale(input) {
80
+ if (input.locale) {
81
+ return assertIsLocale(input.locale);
82
+ }
83
+ if (input.request) {
84
+ return extractLocaleFromRequestAsync(input.request);
85
+ }
86
+ return getLocale();
87
+ }
88
+ /**
89
+ * Resolves the current URL from the provided input or runtime context.
90
+ *
91
+ * @param {ShouldRedirectInput} input
92
+ * @returns {URL}
93
+ */
94
+ function resolveUrl(input) {
95
+ if (input.request) {
96
+ return new URL(input.request.url);
97
+ }
98
+ if (input.url instanceof URL) {
99
+ return new URL(input.url.href);
100
+ }
101
+ if (typeof input.url === "string") {
102
+ return new URL(input.url, getUrlOrigin());
103
+ }
104
+ if (typeof window !== "undefined" && window?.location?.href) {
105
+ return new URL(window.location.href);
106
+ }
107
+ throw new Error("shouldRedirect() requires either a request, an absolute URL, or must run in a browser environment.");
108
+ }
109
+ /**
110
+ * Normalize url for comparison by stripping the trailing slash.
111
+ *
112
+ * @param {string} url
113
+ * @returns {string}
114
+ */
115
+ function normalizeUrl(url) {
116
+ const urlObj = new URL(url);
117
+ urlObj.pathname = urlObj.pathname.replace(/\/$/, "");
118
+ return urlObj.href;
119
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=should-redirect.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"should-redirect.test.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/should-redirect.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,119 @@
1
+ import { expect, test } from "vitest";
2
+ import { createParaglide } from "../create-paraglide.js";
3
+ import { newProject } from "@inlang/sdk";
4
+ test("shouldRedirect redirects to the strategy-preferred locale on the server", async () => {
5
+ const runtime = await createParaglide({
6
+ blob: await newProject({
7
+ settings: {
8
+ baseLocale: "en",
9
+ locales: ["en", "fr"],
10
+ },
11
+ }),
12
+ strategy: ["cookie", "url"],
13
+ cookieName: "PARAGLIDE_LOCALE",
14
+ urlPatterns: [
15
+ {
16
+ pattern: "https://example.com/:path(.*)?",
17
+ localized: [
18
+ ["en", "https://example.com/en/:path(.*)?"],
19
+ ["fr", "https://example.com/fr/:path(.*)?"],
20
+ ],
21
+ },
22
+ ],
23
+ });
24
+ const request = new Request("https://example.com/en/dashboard", {
25
+ headers: {
26
+ cookie: "PARAGLIDE_LOCALE=fr",
27
+ },
28
+ });
29
+ const decision = await runtime.shouldRedirect({ request });
30
+ expect(decision.shouldRedirect).toBe(true);
31
+ expect(decision.redirectUrl?.href).toBe("https://example.com/fr/dashboard");
32
+ expect(decision.locale).toBe("fr");
33
+ });
34
+ test("shouldRedirect does nothing when the URL already matches", async () => {
35
+ const runtime = await createParaglide({
36
+ blob: await newProject({
37
+ settings: {
38
+ baseLocale: "en",
39
+ locales: ["en", "fr"],
40
+ },
41
+ }),
42
+ strategy: ["cookie", "url"],
43
+ cookieName: "PARAGLIDE_LOCALE",
44
+ urlPatterns: [
45
+ {
46
+ pattern: "https://example.com/:path(.*)?",
47
+ localized: [
48
+ ["en", "https://example.com/en/:path(.*)?"],
49
+ ["fr", "https://example.com/fr/:path(.*)?"],
50
+ ],
51
+ },
52
+ ],
53
+ });
54
+ const request = new Request("https://example.com/fr/dashboard", {
55
+ headers: {
56
+ cookie: "PARAGLIDE_LOCALE=fr",
57
+ },
58
+ });
59
+ const decision = await runtime.shouldRedirect({ request });
60
+ expect(decision.shouldRedirect).toBe(false);
61
+ expect(decision.redirectUrl).toBeUndefined();
62
+ expect(decision.locale).toBe("fr");
63
+ });
64
+ test("shouldRedirect falls back to the browser URL when no input is provided", async () => {
65
+ const runtime = await createParaglide({
66
+ blob: await newProject({
67
+ settings: {
68
+ baseLocale: "en",
69
+ locales: ["en", "de"],
70
+ },
71
+ }),
72
+ strategy: ["url", "globalVariable"],
73
+ isServer: "false",
74
+ urlPatterns: undefined,
75
+ });
76
+ const originalWindow = globalThis.window;
77
+ try {
78
+ globalThis.window = {
79
+ location: {
80
+ href: "https://example.com/en/profile",
81
+ origin: "https://example.com",
82
+ },
83
+ };
84
+ runtime.overwriteGetLocale(() => "de");
85
+ const decision = await runtime.shouldRedirect();
86
+ expect(decision.shouldRedirect).toBe(true);
87
+ expect(decision.redirectUrl?.href).toBe("https://example.com/de/profile");
88
+ expect(decision.locale).toBe("de");
89
+ }
90
+ finally {
91
+ if (originalWindow === undefined) {
92
+ Reflect.deleteProperty(globalThis, "window");
93
+ }
94
+ else {
95
+ globalThis.window = originalWindow;
96
+ }
97
+ }
98
+ });
99
+ test("shouldRedirect never suggests a redirect without the url strategy", async () => {
100
+ const runtime = await createParaglide({
101
+ blob: await newProject({
102
+ settings: {
103
+ baseLocale: "en",
104
+ locales: ["en", "fr"],
105
+ },
106
+ }),
107
+ strategy: ["cookie"],
108
+ cookieName: "PARAGLIDE_LOCALE",
109
+ });
110
+ const request = new Request("https://example.com/en/dashboard", {
111
+ headers: {
112
+ cookie: "PARAGLIDE_LOCALE=fr",
113
+ },
114
+ });
115
+ const decision = await runtime.shouldRedirect({ request });
116
+ expect(decision.shouldRedirect).toBe(false);
117
+ expect(decision.redirectUrl).toBeUndefined();
118
+ expect(decision.locale).toBe("fr");
119
+ });
@@ -25,6 +25,7 @@ export type Runtime = {
25
25
  deLocalizeHref: typeof import("./localize-href.js").deLocalizeHref;
26
26
  localizeUrl: typeof import("./localize-url.js").localizeUrl;
27
27
  deLocalizeUrl: typeof import("./localize-url.js").deLocalizeUrl;
28
+ shouldRedirect: typeof import("./should-redirect.js").shouldRedirect;
28
29
  extractLocaleFromUrl: typeof import("./extract-locale-from-url.js").extractLocaleFromUrl;
29
30
  extractLocaleFromRequest: typeof import("./extract-locale-from-request.js").extractLocaleFromRequest;
30
31
  extractLocaleFromRequestAsync: typeof import("./extract-locale-from-request-async.js").extractLocaleFromRequestAsync;
@@ -1 +1 @@
1
- {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/type.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG;IACrB,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,OAAO,EAAE,cAAc,gBAAgB,EAAE,OAAO,CAAC;IACjD,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,YAAY,EAAE,cAAc,gBAAgB,EAAE,YAAY,CAAC;IAC3D,WAAW,EAAE,cAAc,gBAAgB,EAAE,WAAW,CAAC;IACzD,wBAAwB,EAAE,cAAc,gBAAgB,EAAE,wBAAwB,CAAC;IACnF,uBAAuB,EAAE,cAAc,gBAAgB,EAAE,uBAAuB,CAAC;IACjF,qCAAqC,EAAE,cAAc,gBAAgB,EAAE,qCAAqC,CAAC;IAC7G,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,YAAY,EAAE,cAAc,qBAAqB,EAAE,YAAY,CAAC;IAChE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,qBAAqB,EAAE,cAAc,qBAAqB,EAAE,qBAAqB,CAAC;IAClF,gCAAgC,EAAE,cAAc,gBAAgB,EAAE,gCAAgC,CAAC;IACnG,cAAc,EAAE,cAAc,uBAAuB,EAAE,cAAc,CAAC;IACtE,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,YAAY,EAAE,cAAc,oBAAoB,EAAE,YAAY,CAAC;IAC/D,cAAc,EAAE,cAAc,oBAAoB,EAAE,cAAc,CAAC;IACnE,WAAW,EAAE,cAAc,mBAAmB,EAAE,WAAW,CAAC;IAC5D,aAAa,EAAE,cAAc,mBAAmB,EAAE,aAAa,CAAC;IAChE,oBAAoB,EAAE,cAAc,8BAA8B,EAAE,oBAAoB,CAAC;IACzF,wBAAwB,EAAE,cAAc,kCAAkC,EAAE,wBAAwB,CAAC;IACrG,6BAA6B,EAAE,cAAc,wCAAwC,EAAE,6BAA6B,CAAC;IACrH,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,0BAA0B,EAAE,cAAc,oCAAoC,EAAE,0BAA0B,CAAC;IAC3G,2BAA2B,EAAE,cAAc,qCAAqC,EAAE,2BAA2B,CAAC;IAC9G,gBAAgB,EAAE,cAAc,yBAAyB,EAAE,gBAAgB,CAAC;IAC5E,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;IACtF,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;CACtF,CAAC"}
1
+ {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/type.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG;IACrB,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,OAAO,EAAE,cAAc,gBAAgB,EAAE,OAAO,CAAC;IACjD,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,YAAY,EAAE,cAAc,gBAAgB,EAAE,YAAY,CAAC;IAC3D,WAAW,EAAE,cAAc,gBAAgB,EAAE,WAAW,CAAC;IACzD,wBAAwB,EAAE,cAAc,gBAAgB,EAAE,wBAAwB,CAAC;IACnF,uBAAuB,EAAE,cAAc,gBAAgB,EAAE,uBAAuB,CAAC;IACjF,qCAAqC,EAAE,cAAc,gBAAgB,EAAE,qCAAqC,CAAC;IAC7G,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,YAAY,EAAE,cAAc,qBAAqB,EAAE,YAAY,CAAC;IAChE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,qBAAqB,EAAE,cAAc,qBAAqB,EAAE,qBAAqB,CAAC;IAClF,gCAAgC,EAAE,cAAc,gBAAgB,EAAE,gCAAgC,CAAC;IACnG,cAAc,EAAE,cAAc,uBAAuB,EAAE,cAAc,CAAC;IACtE,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,YAAY,EAAE,cAAc,oBAAoB,EAAE,YAAY,CAAC;IAC/D,cAAc,EAAE,cAAc,oBAAoB,EAAE,cAAc,CAAC;IACnE,WAAW,EAAE,cAAc,mBAAmB,EAAE,WAAW,CAAC;IAC5D,aAAa,EAAE,cAAc,mBAAmB,EAAE,aAAa,CAAC;IAChE,cAAc,EAAE,cAAc,sBAAsB,EAAE,cAAc,CAAC;IACrE,oBAAoB,EAAE,cAAc,8BAA8B,EAAE,oBAAoB,CAAC;IACzF,wBAAwB,EAAE,cAAc,kCAAkC,EAAE,wBAAwB,CAAC;IACrG,6BAA6B,EAAE,cAAc,wCAAwC,EAAE,6BAA6B,CAAC;IACrH,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,0BAA0B,EAAE,cAAc,oCAAoC,EAAE,0BAA0B,CAAC;IAC3G,2BAA2B,EAAE,cAAc,qCAAqC,EAAE,2BAA2B,CAAC;IAC9G,gBAAgB,EAAE,cAAc,yBAAyB,EAAE,gBAAgB,CAAC;IAC5E,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;IACtF,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;CACtF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/compiler/server/middleware.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,oCA9Ca,CAAC,WAEH,OAAO,WACP,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,cAAc,EAAE,MAAM,CAAA;CAAE,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,cACrF;IAAE,UAAU,EAAC,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAE,GACzC,OAAO,CAAC,QAAQ,CAAC,CA0I7B"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/compiler/server/middleware.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,oCA9Ca,CAAC,WAEH,OAAO,WACP,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,cAAc,EAAE,MAAM,CAAA;CAAE,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,cACrF;IAAE,UAAU,EAAC,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAE,GACzC,OAAO,CAAC,QAAQ,CAAC,CAyI7B"}
@@ -68,30 +68,29 @@ export async function paraglideMiddleware(request, resolve, callbacks) {
68
68
  else if (!runtime.serverAsyncLocalStorage) {
69
69
  runtime.overwriteServerAsyncLocalStorage(createMockAsyncLocalStorage());
70
70
  }
71
- const locale = await runtime.extractLocaleFromRequestAsync(request);
71
+ const decision = await runtime.shouldRedirect({ request });
72
+ const locale = decision.locale;
72
73
  const origin = new URL(request.url).origin;
73
74
  // if the client makes a request to a URL that doesn't match
74
75
  // the localizedUrl, redirect the client to the localized URL
75
76
  if (request.headers.get("Sec-Fetch-Dest") === "document" &&
76
- runtime.strategy.includes("url")) {
77
- const localizedUrl = runtime.localizeUrl(request.url, { locale });
78
- if (normalizeURL(localizedUrl.href) !== normalizeURL(request.url)) {
79
- // Create headers object with Vary header if preferredLanguage strategy is used
80
- /** @type {Record<string, string>} */
81
- const headers = {};
82
- if (runtime.strategy.includes("preferredLanguage")) {
83
- headers["Vary"] = "Accept-Language";
84
- }
85
- const response = new Response(null, {
86
- status: 307,
87
- headers: {
88
- Location: localizedUrl.href,
89
- ...headers,
90
- },
91
- });
92
- callbacks?.onRedirect(response);
93
- return response;
77
+ decision.shouldRedirect &&
78
+ decision.redirectUrl) {
79
+ // Create headers object with Vary header if preferredLanguage strategy is used
80
+ /** @type {Record<string, string>} */
81
+ const headers = {};
82
+ if (runtime.strategy.includes("preferredLanguage")) {
83
+ headers["Vary"] = "Accept-Language";
94
84
  }
85
+ const response = new Response(null, {
86
+ status: 307,
87
+ headers: {
88
+ Location: decision.redirectUrl.href,
89
+ ...headers,
90
+ },
91
+ });
92
+ callbacks?.onRedirect(response);
93
+ return response;
95
94
  }
96
95
  // If the strategy includes "url", we need to de-localize the URL
97
96
  // before passing it to the server middleware.
@@ -135,18 +134,6 @@ export async function paraglideMiddleware(request, resolve, callbacks) {
135
134
  }
136
135
  return response;
137
136
  }
138
- /**
139
- * Normalize url for comparison.
140
- * Strips trailing slash
141
- * @param {string} url
142
- * @returns {string} normalized url string
143
- */
144
- function normalizeURL(url) {
145
- const urlObj = new URL(url);
146
- // // strip trailing slash from pathname
147
- urlObj.pathname = urlObj.pathname.replace(/\/$/, "");
148
- return urlObj.href;
149
- }
150
137
  /**
151
138
  * Creates a mock AsyncLocalStorage implementation for environments where
152
139
  * native AsyncLocalStorage is not available or disabled.
@@ -1,5 +1,5 @@
1
1
  export const ENV_VARIABLES = {
2
2
  PARJS_APP_ID: "library.inlang.paraglideJs",
3
3
  PARJS_POSTHOG_TOKEN: undefined,
4
- PARJS_PACKAGE_VERSION: "2.3.0",
4
+ PARJS_PACKAGE_VERSION: "2.4.0",
5
5
  };
@@ -45,8 +45,7 @@ test("should create any missing directories", async () => {
45
45
  test("should only write once if the output hasn't changed", async () => {
46
46
  const { writeOutput } = await import("./write-output.js");
47
47
  const fs = mockFs({});
48
- // @ts-expect-error - spy
49
- fs.writeFile = vi.spyOn(fs, "writeFile");
48
+ const writeFileSpy = vi.spyOn(fs, "writeFile");
50
49
  const hashes = await writeOutput({
51
50
  directory: "/output",
52
51
  output: { "test.txt": "test" },
@@ -60,13 +59,12 @@ test("should only write once if the output hasn't changed", async () => {
60
59
  });
61
60
  expect(hashes).toEqual(hashes2);
62
61
  expect(await fs.readFile("/output/test.txt", { encoding: "utf-8" })).toBe("test");
63
- expect(fs.writeFile).toHaveBeenCalledTimes(1);
62
+ expect(writeFileSpy).toHaveBeenCalledTimes(1);
64
63
  });
65
64
  test("should write again if the output has changed", async () => {
66
65
  const { writeOutput } = await import("./write-output.js");
67
66
  const fs = mockFs({});
68
- // @ts-expect-error - spy
69
- fs.writeFile = vi.spyOn(fs, "writeFile");
67
+ const writeFileSpy = vi.spyOn(fs, "writeFile");
70
68
  const hashes = await writeOutput({
71
69
  directory: "/output",
72
70
  output: { "test.txt": "test" },
@@ -79,13 +77,12 @@ test("should write again if the output has changed", async () => {
79
77
  previousOutputHashes: hashes,
80
78
  });
81
79
  expect(await fs.readFile("/output/test.txt", { encoding: "utf-8" })).toBe("test2");
82
- expect(fs.writeFile).toHaveBeenCalledTimes(2);
80
+ expect(writeFileSpy).toHaveBeenCalledTimes(2);
83
81
  });
84
82
  test("should write files if output has partially changed", async () => {
85
83
  const { writeOutput } = await import("./write-output.js");
86
84
  const fs = mockFs({});
87
- // @ts-expect-error - spy
88
- fs.writeFile = vi.spyOn(fs, "writeFile");
85
+ const writeFileSpy = vi.spyOn(fs, "writeFile");
89
86
  const hashes = await writeOutput({
90
87
  directory: "/output",
91
88
  output: { "file1.txt": "test", "file2.txt": "test" },
@@ -97,8 +94,8 @@ test("should write files if output has partially changed", async () => {
97
94
  fs,
98
95
  previousOutputHashes: hashes,
99
96
  });
100
- expect(fs.writeFile).toHaveBeenCalledWith("/output/file2.txt", "test2");
101
- expect(fs.writeFile).toHaveBeenCalledTimes(3);
97
+ expect(writeFileSpy).toHaveBeenCalledWith("/output/file2.txt", "test2");
98
+ expect(writeFileSpy).toHaveBeenCalledTimes(3);
102
99
  });
103
100
  test("should delete files that have been removed from the output", async () => {
104
101
  const { writeOutput } = await import("./write-output.js");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inlang/paraglide-js",
3
3
  "type": "module",
4
- "version": "2.3.1",
4
+ "version": "2.4.0",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public",
@@ -35,21 +35,19 @@
35
35
  "@inlang/recommend-sherlock": "0.2.1"
36
36
  },
37
37
  "devDependencies": {
38
- "@eslint/js": "^9.18.0",
39
38
  "@rollup/plugin-virtual": "3.0.2",
40
39
  "@ts-morph/bootstrap": "0.26.0",
41
40
  "@types/node": "^22.10.6",
42
- "@vitest/coverage-v8": "2.1.8",
43
- "eslint": "^9.18.0",
41
+ "@vitest/coverage-v8": "3.1.4",
44
42
  "memfs": "4.17.0",
43
+ "oxlint": "^1.14.0",
45
44
  "prettier": "^3.4.2",
46
45
  "rolldown": "1.0.0-beta.1",
47
46
  "typedoc": "0.28.12",
48
47
  "typedoc-plugin-markdown": "4.7.0",
49
48
  "typedoc-plugin-missing-exports": "4.0.0",
50
49
  "typescript": "5.8.3",
51
- "typescript-eslint": "^8.20.0",
52
- "vitest": "2.1.8",
50
+ "vitest": "3.1.4",
53
51
  "@inlang/plugin-message-format": "4.0.0",
54
52
  "@opral/tsconfig": "1.1.0"
55
53
  },
@@ -85,7 +83,7 @@
85
83
  "test": "npm run env-variables && tsc --noEmit && vitest run --coverage ./src/**/*",
86
84
  "test:watch": "npm run env-variables && vitest --watch ./src/**/*",
87
85
  "env-variables": "node ./src/services/env-variables/create-index-file.js",
88
- "lint": "eslint ./src --fix",
86
+ "lint": "oxlint --config .oxlintrc.json --fix",
89
87
  "format": "prettier ./src --write",
90
88
  "clean": "rm -rf ./dist ./node_modules"
91
89
  }