@inlang/paraglide-js 2.26.0 → 2.28.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.
- package/README.md +11 -1
- package/dist/bundler-plugins/per-locale-build/index.js +14 -7
- package/dist/bundler-plugins/per-locale-build/index.js.map +1 -1
- package/dist/bundler-plugins/per-locale-build/vite-environments.test.js +237 -4
- package/dist/bundler-plugins/per-locale-build/vite-environments.test.js.map +1 -1
- package/dist/bundler-plugins/unplugin.d.ts +1 -1
- package/dist/bundler-plugins/unplugin.js +79 -10
- package/dist/bundler-plugins/unplugin.js.map +1 -1
- package/dist/bundler-plugins/unplugin.test.js +334 -1
- package/dist/bundler-plugins/unplugin.test.js.map +1 -1
- package/dist/bundler-plugins/vite.js +2 -1
- package/dist/bundler-plugins/vite.js.map +1 -1
- package/dist/bundler-plugins/vite.test.js +1 -1
- package/dist/bundler-plugins/vite.test.js.map +1 -1
- package/dist/cli/commands/compile/command.js +13 -0
- package/dist/cli/commands/compile/command.js.map +1 -1
- package/dist/cli/commands/compile/command.test.js +87 -3
- package/dist/cli/commands/compile/command.test.js.map +1 -1
- package/dist/cli/commands/init/command.d.ts +5 -0
- package/dist/cli/commands/init/command.js +216 -44
- package/dist/cli/commands/init/command.js.map +1 -1
- package/dist/cli/commands/init/command.test.d.ts +1 -0
- package/dist/cli/commands/init/command.test.js +123 -0
- package/dist/cli/commands/init/command.test.js.map +1 -0
- package/dist/cli/defaults.js +2 -2
- package/dist/cli/defaults.js.map +1 -1
- package/dist/cli/init-options.d.ts +11 -0
- package/dist/cli/init-options.js +2 -0
- package/dist/cli/init-options.js.map +1 -0
- package/dist/cli/steps/add-inlang-check.d.ts +19 -0
- package/dist/cli/steps/add-inlang-check.js +60 -0
- package/dist/cli/steps/add-inlang-check.js.map +1 -0
- package/dist/cli/steps/add-inlang-check.test.d.ts +1 -0
- package/dist/cli/steps/add-inlang-check.test.js +88 -0
- package/dist/cli/steps/add-inlang-check.test.js.map +1 -0
- package/dist/cli/steps/add-message-lint.d.ts +34 -0
- package/dist/cli/steps/add-message-lint.js +105 -0
- package/dist/cli/steps/add-message-lint.js.map +1 -0
- package/dist/cli/steps/add-message-lint.test.d.ts +1 -0
- package/dist/cli/steps/add-message-lint.test.js +74 -0
- package/dist/cli/steps/add-message-lint.test.js.map +1 -0
- package/dist/cli/steps/add-vite-plugin.d.ts +18 -0
- package/dist/cli/steps/add-vite-plugin.js +34 -6
- package/dist/cli/steps/add-vite-plugin.js.map +1 -1
- package/dist/cli/steps/add-vite-plugin.test.js +25 -9
- package/dist/cli/steps/add-vite-plugin.test.js.map +1 -1
- package/dist/cli/steps/agents-md-suggestion.d.ts +11 -0
- package/dist/cli/steps/agents-md-suggestion.js +19 -0
- package/dist/cli/steps/agents-md-suggestion.js.map +1 -0
- package/dist/cli/steps/agents-md-suggestion.test.d.ts +1 -0
- package/dist/cli/steps/agents-md-suggestion.test.js +36 -0
- package/dist/cli/steps/agents-md-suggestion.test.js.map +1 -0
- package/dist/cli/steps/check-for-uncomitted-changes.d.ts +3 -0
- package/dist/cli/steps/check-for-uncomitted-changes.js +15 -3
- package/dist/cli/steps/check-for-uncomitted-changes.js.map +1 -1
- package/dist/cli/steps/detect-bundler.d.ts +1 -0
- package/dist/cli/steps/detect-bundler.js +7 -1
- package/dist/cli/steps/detect-bundler.js.map +1 -1
- package/dist/cli/steps/initialize-inlang-project.d.ts +4 -3
- package/dist/cli/steps/initialize-inlang-project.js +60 -157
- package/dist/cli/steps/initialize-inlang-project.js.map +1 -1
- package/dist/cli/steps/maybe-add-machine-translation.d.ts +4 -0
- package/dist/cli/steps/maybe-add-machine-translation.js +14 -8
- package/dist/cli/steps/maybe-add-machine-translation.js.map +1 -1
- package/dist/cli/steps/maybe-add-sherlock.d.ts +2 -0
- package/dist/cli/steps/maybe-add-sherlock.js +13 -7
- package/dist/cli/steps/maybe-add-sherlock.js.map +1 -1
- package/dist/cli/steps/maybe-add-sherlock.test.d.ts +1 -0
- package/dist/cli/steps/maybe-add-sherlock.test.js +44 -0
- package/dist/cli/steps/maybe-add-sherlock.test.js.map +1 -0
- package/dist/cli/steps/prompt-for-outdir.d.ts +2 -0
- package/dist/cli/steps/prompt-for-outdir.js +4 -0
- package/dist/cli/steps/prompt-for-outdir.js.map +1 -1
- package/dist/cli/steps/update-package-json.js +8 -13
- package/dist/cli/steps/update-package-json.js.map +1 -1
- package/dist/cli/steps/update-ts-config.d.ts +6 -0
- package/dist/cli/steps/update-ts-config.js +27 -4
- package/dist/cli/steps/update-ts-config.js.map +1 -1
- package/dist/cli/utils.d.ts +11 -0
- package/dist/cli/utils.js +21 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/cli/utils.test.d.ts +1 -0
- package/dist/cli/utils.test.js +16 -0
- package/dist/cli/utils.test.js.map +1 -0
- package/dist/compiler/compile-annotation.d.ts +48 -1
- package/dist/compiler/compile-annotation.js +243 -10
- package/dist/compiler/compile-annotation.js.map +1 -1
- package/dist/compiler/compile-bundle.d.ts +7 -2
- package/dist/compiler/compile-bundle.js +97 -3
- package/dist/compiler/compile-bundle.js.map +1 -1
- package/dist/compiler/compile-bundle.test.js +232 -19
- package/dist/compiler/compile-bundle.test.js.map +1 -1
- package/dist/compiler/compile-lifecycle.test.d.ts +1 -0
- package/dist/compiler/compile-lifecycle.test.js +69 -0
- package/dist/compiler/compile-lifecycle.test.js.map +1 -0
- package/dist/compiler/compile-local-variable.d.ts +3 -0
- package/dist/compiler/compile-local-variable.js +5 -2
- package/dist/compiler/compile-local-variable.js.map +1 -1
- package/dist/compiler/compile-local-variable.test.js +55 -15
- package/dist/compiler/compile-local-variable.test.js.map +1 -1
- package/dist/compiler/compile-message.d.ts +29 -1
- package/dist/compiler/compile-message.exact-number.test.d.ts +1 -0
- package/dist/compiler/compile-message.exact-number.test.js +786 -0
- package/dist/compiler/compile-message.exact-number.test.js.map +1 -0
- package/dist/compiler/compile-message.js +162 -66
- package/dist/compiler/compile-message.js.map +1 -1
- package/dist/compiler/compile-message.locals.test.d.ts +1 -0
- package/dist/compiler/compile-message.locals.test.js +498 -0
- package/dist/compiler/compile-message.locals.test.js.map +1 -0
- package/dist/compiler/compile-message.test.js +113 -22
- package/dist/compiler/compile-message.test.js.map +1 -1
- package/dist/compiler/compile-pattern.d.ts +5 -0
- package/dist/compiler/compile-pattern.js +6 -18
- package/dist/compiler/compile-pattern.js.map +1 -1
- package/dist/compiler/compile-pattern.test.js +169 -5
- package/dist/compiler/compile-pattern.test.js.map +1 -1
- package/dist/compiler/compile-project.js +5 -1
- package/dist/compiler/compile-project.js.map +1 -1
- package/dist/compiler/compile-project.test.js +1260 -185
- package/dist/compiler/compile-project.test.js.map +1 -1
- package/dist/compiler/compile.js +36 -17
- package/dist/compiler/compile.js.map +1 -1
- package/dist/compiler/compile.test.js +71 -1
- package/dist/compiler/compile.test.js.map +1 -1
- package/dist/compiler/compiler-options.d.ts +41 -0
- package/dist/compiler/compiler-options.js.map +1 -1
- package/dist/compiler/create-readme.d.ts +4 -1
- package/dist/compiler/create-readme.js +33 -125
- package/dist/compiler/create-readme.js.map +1 -1
- package/dist/compiler/emit-ts-declarations.interop.test.js +2 -2
- package/dist/compiler/emit-ts-declarations.interop.test.js.map +1 -1
- package/dist/compiler/emit-ts-declarations.test.js +4 -4
- package/dist/compiler/emit-ts-declarations.test.js.map +1 -1
- package/dist/compiler/index.d.ts +1 -1
- package/dist/compiler/index.js +1 -1
- package/dist/compiler/index.js.map +1 -1
- package/dist/compiler/input-alias.d.ts +21 -0
- package/dist/compiler/input-alias.js +42 -0
- package/dist/compiler/input-alias.js.map +1 -0
- package/dist/compiler/input-alias.test.d.ts +1 -0
- package/dist/compiler/input-alias.test.js +58 -0
- package/dist/compiler/input-alias.test.js.map +1 -0
- package/dist/compiler/message-locals.d.ts +46 -0
- package/dist/compiler/message-locals.js +129 -0
- package/dist/compiler/message-locals.js.map +1 -0
- package/dist/compiler/output-structure/locale-modules.js +7 -1
- package/dist/compiler/output-structure/locale-modules.js.map +1 -1
- package/dist/compiler/output-structure/locale-modules.test.js +35 -0
- package/dist/compiler/output-structure/locale-modules.test.js.map +1 -1
- package/dist/compiler/output-structure/message-modules.js +6 -4
- package/dist/compiler/output-structure/message-modules.js.map +1 -1
- package/dist/compiler/output-structure/message-modules.test.js +35 -0
- package/dist/compiler/output-structure/message-modules.test.js.map +1 -1
- package/dist/compiler/registry.d.ts +12 -0
- package/dist/compiler/registry.js +158 -31
- package/dist/compiler/registry.js.map +1 -1
- package/dist/compiler/registry.test.d.ts +1 -0
- package/dist/compiler/registry.test.js +291 -0
- package/dist/compiler/registry.test.js.map +1 -0
- package/dist/compiler/runtime/create-runtime.d.ts +2 -0
- package/dist/compiler/runtime/create-runtime.js +9 -0
- package/dist/compiler/runtime/create-runtime.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-cookie.js +8 -1
- package/dist/compiler/runtime/extract-locale-from-cookie.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-cookie.test.js +26 -0
- package/dist/compiler/runtime/extract-locale-from-cookie.test.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-header.js +47 -5
- package/dist/compiler/runtime/extract-locale-from-header.js.map +1 -1
- package/dist/compiler/runtime/extract-locale-from-header.test.js +58 -0
- package/dist/compiler/runtime/extract-locale-from-header.test.js.map +1 -1
- package/dist/compiler/runtime/get-locale.js +7 -1
- package/dist/compiler/runtime/get-locale.js.map +1 -1
- package/dist/compiler/runtime/get-locale.test.js +95 -0
- package/dist/compiler/runtime/get-locale.test.js.map +1 -1
- package/dist/compiler/runtime/localize-href.d.ts +8 -0
- package/dist/compiler/runtime/localize-href.js +60 -0
- package/dist/compiler/runtime/localize-href.js.map +1 -1
- package/dist/compiler/runtime/localize-href.test.js +227 -1
- package/dist/compiler/runtime/localize-href.test.js.map +1 -1
- package/dist/compiler/runtime/set-locale.js +24 -6
- package/dist/compiler/runtime/set-locale.js.map +1 -1
- package/dist/compiler/runtime/set-locale.test.js +182 -0
- package/dist/compiler/runtime/set-locale.test.js.map +1 -1
- package/dist/compiler/runtime/type.d.ts +2 -0
- package/dist/compiler/runtime/type.js.map +1 -1
- package/dist/compiler/runtime/type.test.js +34 -0
- package/dist/compiler/runtime/type.test.js.map +1 -1
- package/dist/compiler/runtime/variables.d.ts +2 -0
- package/dist/compiler/runtime/variables.js +7 -0
- package/dist/compiler/runtime/variables.js.map +1 -1
- package/dist/compiler/server/create-server-file.js +24 -0
- package/dist/compiler/server/create-server-file.js.map +1 -1
- package/dist/compiler/server/create-server-file.test.js +8 -2
- package/dist/compiler/server/create-server-file.test.js.map +1 -1
- package/dist/compiler/server/middleware.d.ts +8 -2
- package/dist/compiler/server/middleware.js +204 -11
- package/dist/compiler/server/middleware.js.map +1 -1
- package/dist/compiler/server/middleware.test.js +750 -3
- package/dist/compiler/server/middleware.test.js.map +1 -1
- package/dist/eslint/index.d.ts +4 -0
- package/dist/eslint/index.js +8 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/eslint/no-dynamic-message-access.d.ts +3 -0
- package/dist/eslint/no-dynamic-message-access.js +150 -0
- package/dist/eslint/no-dynamic-message-access.js.map +1 -0
- package/dist/eslint/no-dynamic-message-access.test.d.ts +1 -0
- package/dist/eslint/no-dynamic-message-access.test.js +126 -0
- package/dist/eslint/no-dynamic-message-access.test.js.map +1 -0
- package/dist/eslint/oxlint.test.d.ts +1 -0
- package/dist/eslint/oxlint.test.js +64 -0
- package/dist/eslint/oxlint.test.js.map +1 -0
- package/dist/services/config/config-schema.d.ts +2 -0
- package/dist/services/config/config-schema.js +2 -0
- package/dist/services/config/config-schema.js.map +1 -1
- package/dist/services/config/load-paraglide-config.test.js +12 -0
- package/dist/services/config/load-paraglide-config.test.js.map +1 -1
- package/dist/services/env-variables/index.js +1 -1
- package/dist/services/env-variables/index.js.map +1 -1
- package/dist/services/file-handling/write-output.bench.js +51 -11
- package/dist/services/file-handling/write-output.bench.js.map +1 -1
- package/dist/services/file-handling/write-output.js +60 -20
- package/dist/services/file-handling/write-output.js.map +1 -1
- package/dist/services/file-handling/write-output.test.js +175 -0
- package/dist/services/file-handling/write-output.test.js.map +1 -1
- package/package.json +15 -5
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { test, expect } from "vitest";
|
|
1
|
+
import { test, expect, describe, beforeAll, vi } from "vitest";
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
import { createServer } from "node:http";
|
|
2
4
|
import { createParaglide } from "../create-paraglide.js";
|
|
3
5
|
import { newProject } from "@inlang/sdk";
|
|
4
6
|
test("sets the locale and origin", async () => {
|
|
@@ -93,6 +95,7 @@ test("skips i18n middleware behavior for excluded routeStrategies", async () =>
|
|
|
93
95
|
});
|
|
94
96
|
const response = await runtime.paraglideMiddleware(request, ({ locale, request }) => new Response(`${locale}|${request.url}`));
|
|
95
97
|
expect(response.status).toBe(200);
|
|
98
|
+
expect(response.headers.get("Vary")).toBe(null);
|
|
96
99
|
expect(await response.text()).toBe("en|https://example.com/api/data");
|
|
97
100
|
});
|
|
98
101
|
test("localized URLs still match routeStrategies after delocalization", async () => {
|
|
@@ -426,7 +429,7 @@ test("sets Vary: Accept-Language header when preferredLanguage strategy is used
|
|
|
426
429
|
// Should have Vary header when preferredLanguage strategy is used
|
|
427
430
|
expect(response.headers.get("Vary")).toBe("Accept-Language");
|
|
428
431
|
});
|
|
429
|
-
test("
|
|
432
|
+
test("sets Vary: Cookie without Accept-Language for a cookie redirect", async () => {
|
|
430
433
|
const runtime = await createParaglide({
|
|
431
434
|
blob: await newProject({
|
|
432
435
|
settings: {
|
|
@@ -460,9 +463,337 @@ test("does not set Vary header when preferredLanguage strategy is not used", asy
|
|
|
460
463
|
expect(response instanceof Response).toBe(true);
|
|
461
464
|
expect(response.status).toBe(307); // Redirect status code
|
|
462
465
|
expect(response.headers.get("Location")).toBe("https://example.com/fr/some-path");
|
|
463
|
-
|
|
466
|
+
expect(response.headers.get("Vary")).toBe("Cookie");
|
|
467
|
+
});
|
|
468
|
+
test.each([
|
|
469
|
+
{
|
|
470
|
+
strategy: ["cookie", "url", "baseLocale"],
|
|
471
|
+
vary: "Cookie",
|
|
472
|
+
locale: "fr",
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
strategy: ["preferredLanguage", "url", "baseLocale"],
|
|
476
|
+
vary: "Accept-Language",
|
|
477
|
+
locale: "fr",
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
strategy: ["url", "cookie", "preferredLanguage", "baseLocale"],
|
|
481
|
+
vary: "Cookie, Accept-Language",
|
|
482
|
+
locale: "fr",
|
|
483
|
+
},
|
|
484
|
+
{ strategy: ["url", "baseLocale"], vary: null, locale: "en" },
|
|
485
|
+
].flatMap((scenario) => [
|
|
486
|
+
{
|
|
487
|
+
...scenario,
|
|
488
|
+
method: "GET",
|
|
489
|
+
destination: "document",
|
|
490
|
+
status: 307,
|
|
491
|
+
body: "",
|
|
492
|
+
location: `https://example.com/${scenario.locale}/page?campaign=example`,
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
...scenario,
|
|
496
|
+
method: "GET",
|
|
497
|
+
destination: "",
|
|
498
|
+
status: 200,
|
|
499
|
+
body: scenario.locale,
|
|
500
|
+
location: null,
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
...scenario,
|
|
504
|
+
method: "HEAD",
|
|
505
|
+
destination: "",
|
|
506
|
+
status: 200,
|
|
507
|
+
body: "",
|
|
508
|
+
location: null,
|
|
509
|
+
},
|
|
510
|
+
]))("sets Vary from $strategy for $method with destination '$destination'", async ({ strategy, vary, method, destination, status, body, location }) => {
|
|
511
|
+
const runtime = await createParaglide({
|
|
512
|
+
blob: await newProject({
|
|
513
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
514
|
+
}),
|
|
515
|
+
strategy: ["url", "cookie", "preferredLanguage", "baseLocale"],
|
|
516
|
+
routeStrategies: [{ match: "/page", strategy }],
|
|
517
|
+
urlPatterns: [
|
|
518
|
+
{
|
|
519
|
+
pattern: "https://example.com/:path(.*)?",
|
|
520
|
+
localized: [
|
|
521
|
+
["en", "https://example.com/en/:path(.*)?"],
|
|
522
|
+
["fr", "https://example.com/fr/:path(.*)?"],
|
|
523
|
+
],
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
});
|
|
527
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/page?campaign=example", {
|
|
528
|
+
method,
|
|
529
|
+
headers: {
|
|
530
|
+
cookie: "PARAGLIDE_LOCALE=fr",
|
|
531
|
+
"Accept-Language": "fr",
|
|
532
|
+
"Sec-Fetch-Dest": destination,
|
|
533
|
+
},
|
|
534
|
+
}), ({ locale }) => new Response(method === "HEAD" ? null : locale), {
|
|
535
|
+
onRedirect: (response) => {
|
|
536
|
+
expect(response.headers.get("Vary")).toBe(vary);
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
expect(response.headers.get("Vary")).toBe(vary);
|
|
540
|
+
expect(response.status).toBe(status);
|
|
541
|
+
expect(response.headers.get("Location")).toBe(location);
|
|
542
|
+
expect(await response.text()).toBe(body);
|
|
543
|
+
});
|
|
544
|
+
test.each([
|
|
545
|
+
["Accept-Encoding", "Accept-Encoding, Cookie, Accept-Language"],
|
|
546
|
+
["accept-encoding, cookie", "accept-encoding, cookie, Accept-Language"],
|
|
547
|
+
[
|
|
548
|
+
"Accept-Encoding, ACCEPT-LANGUAGE",
|
|
549
|
+
"Accept-Encoding, ACCEPT-LANGUAGE, Cookie",
|
|
550
|
+
],
|
|
551
|
+
["cookie, ACCEPT-LANGUAGE", "cookie, ACCEPT-LANGUAGE"],
|
|
552
|
+
["*", "*"],
|
|
553
|
+
["Origin, *", "Origin, *"],
|
|
554
|
+
])("merges negotiated response Vary with %s", async (vary, expected) => {
|
|
555
|
+
const runtime = await createParaglide({
|
|
556
|
+
blob: await newProject({
|
|
557
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
558
|
+
}),
|
|
559
|
+
strategy: ["cookie", "preferredLanguage", "baseLocale"],
|
|
560
|
+
});
|
|
561
|
+
const headers = new Headers({ Vary: vary, "Content-Type": "text/plain" });
|
|
562
|
+
headers.append("Set-Cookie", "theme=dark; Path=/");
|
|
563
|
+
headers.append("Set-Cookie", "session=example; Path=/");
|
|
564
|
+
const original = new Response("fr", {
|
|
565
|
+
status: 202,
|
|
566
|
+
statusText: "Accepted",
|
|
567
|
+
headers,
|
|
568
|
+
});
|
|
569
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/page", {
|
|
570
|
+
headers: { cookie: "PARAGLIDE_LOCALE=fr" },
|
|
571
|
+
}), () => original);
|
|
572
|
+
expect(response.headers.get("Vary")).toBe(expected);
|
|
573
|
+
expect(response).toBe(original);
|
|
574
|
+
expect(response.headers.get("Content-Type")).toBe("text/plain");
|
|
575
|
+
expect(response.headers.getSetCookie()).toEqual(headers.getSetCookie());
|
|
576
|
+
expect(response.status).toBe(202);
|
|
577
|
+
expect(response.statusText).toBe("Accepted");
|
|
578
|
+
expect(response.body).toBe(original.body);
|
|
579
|
+
expect(await response.text()).toBe("fr");
|
|
580
|
+
});
|
|
581
|
+
test("varies negotiated responses even when preference headers are absent", async () => {
|
|
582
|
+
const runtime = await createParaglide({
|
|
583
|
+
blob: await newProject({
|
|
584
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
585
|
+
}),
|
|
586
|
+
strategy: ["cookie", "preferredLanguage", "baseLocale"],
|
|
587
|
+
});
|
|
588
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), ({ locale }) => new Response(locale));
|
|
589
|
+
expect(response.headers.get("Vary")).toBe("Cookie, Accept-Language");
|
|
590
|
+
expect(await response.text()).toBe("en");
|
|
591
|
+
});
|
|
592
|
+
test.each([
|
|
593
|
+
["https://example.com/fr/page", "fr"],
|
|
594
|
+
["https://example.com/page", "en"],
|
|
595
|
+
])("preserves Vary when the effective URL determines the locale: %s", async (url, locale) => {
|
|
596
|
+
const runtime = await createParaglide({
|
|
597
|
+
blob: await newProject({
|
|
598
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
599
|
+
}),
|
|
600
|
+
strategy: ["url", "cookie", "preferredLanguage", "baseLocale"],
|
|
601
|
+
});
|
|
602
|
+
const original = new Response(locale, {
|
|
603
|
+
headers: { Vary: "Accept-Encoding" },
|
|
604
|
+
});
|
|
605
|
+
const response = await runtime.paraglideMiddleware(new Request("http://internal/page", {
|
|
606
|
+
headers: { cookie: "PARAGLIDE_LOCALE=fr" },
|
|
607
|
+
}), ({ locale: resolved }) => {
|
|
608
|
+
expect(resolved).toBe(locale);
|
|
609
|
+
return original;
|
|
610
|
+
}, { effectiveRequestUrl: url });
|
|
611
|
+
expect(response).toBe(original);
|
|
612
|
+
expect(response.headers.get("Vary")).toBe("Accept-Encoding");
|
|
613
|
+
});
|
|
614
|
+
test.each([
|
|
615
|
+
{
|
|
616
|
+
strategy: ["cookie", "url", "preferredLanguage", "baseLocale"],
|
|
617
|
+
vary: "Cookie",
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
strategy: ["preferredLanguage", "url", "cookie", "baseLocale"],
|
|
621
|
+
vary: "Accept-Language",
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
strategy: ["cookie", "preferredLanguage", "url", "baseLocale"],
|
|
625
|
+
vary: "Cookie, Accept-Language",
|
|
626
|
+
},
|
|
627
|
+
].flatMap((scenario) => ["/page", "/en/page"].flatMap((path) => ["GET", "HEAD"].flatMap((method) => ["", "en", "fr"].map((preference) => ({
|
|
628
|
+
...scenario,
|
|
629
|
+
path,
|
|
630
|
+
method,
|
|
631
|
+
preference,
|
|
632
|
+
}))))))("varies $method $path for $strategy with preference '$preference'", async ({ strategy, vary, path, method, preference }) => {
|
|
633
|
+
const runtime = await createParaglide({
|
|
634
|
+
blob: await newProject({
|
|
635
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
636
|
+
}),
|
|
637
|
+
strategy,
|
|
638
|
+
});
|
|
639
|
+
const response = await runtime.paraglideMiddleware(new Request(`https://example.com${path}`, {
|
|
640
|
+
method,
|
|
641
|
+
headers: preference
|
|
642
|
+
? {
|
|
643
|
+
Cookie: `PARAGLIDE_LOCALE=${preference}`,
|
|
644
|
+
"Accept-Language": preference,
|
|
645
|
+
}
|
|
646
|
+
: {},
|
|
647
|
+
}), ({ locale }) => {
|
|
648
|
+
expect(locale).toBe(preference || "en");
|
|
649
|
+
return new Response(method === "HEAD" ? null : locale);
|
|
650
|
+
});
|
|
651
|
+
expect(response.status).toBe(200);
|
|
652
|
+
expect(response.headers.get("Vary")).toBe(vary);
|
|
653
|
+
expect(await response.text()).toBe(method === "HEAD" ? "" : preference || "en");
|
|
654
|
+
});
|
|
655
|
+
test("preserves mutable response subclasses with the default strategy", async () => {
|
|
656
|
+
const runtime = await createParaglide({
|
|
657
|
+
blob: await newProject({
|
|
658
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
659
|
+
}),
|
|
660
|
+
});
|
|
661
|
+
class CustomResponse extends Response {
|
|
662
|
+
marker = "custom";
|
|
663
|
+
}
|
|
664
|
+
const original = new CustomResponse("page");
|
|
665
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => original);
|
|
666
|
+
expect(response).toBe(original);
|
|
667
|
+
expect(response).toBeInstanceOf(CustomResponse);
|
|
668
|
+
expect(response.marker).toBe("custom");
|
|
669
|
+
expect(response.headers.get("Vary")).toBe("Cookie");
|
|
670
|
+
});
|
|
671
|
+
test("preserves Response.error() without adding Vary", async () => {
|
|
672
|
+
const runtime = await createParaglide({
|
|
673
|
+
blob: await newProject({
|
|
674
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
675
|
+
}),
|
|
676
|
+
});
|
|
677
|
+
const original = Response.error();
|
|
678
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => original);
|
|
679
|
+
expect(response).toBe(original);
|
|
680
|
+
expect(response.status).toBe(0);
|
|
464
681
|
expect(response.headers.get("Vary")).toBe(null);
|
|
465
682
|
});
|
|
683
|
+
test("propagates unexpected header update errors", async () => {
|
|
684
|
+
const runtime = await createParaglide({
|
|
685
|
+
blob: await newProject({
|
|
686
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
687
|
+
}),
|
|
688
|
+
});
|
|
689
|
+
const original = new Response("page");
|
|
690
|
+
const failure = new Error("Cannot update headers");
|
|
691
|
+
original.headers.set = () => {
|
|
692
|
+
throw failure;
|
|
693
|
+
};
|
|
694
|
+
await expect(runtime.paraglideMiddleware(new Request("https://example.com/page"), () => original)).rejects.toBe(failure);
|
|
695
|
+
});
|
|
696
|
+
test("adds Vary to a resolved response with immutable headers", async () => {
|
|
697
|
+
const runtime = await createParaglide({
|
|
698
|
+
blob: await newProject({
|
|
699
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
700
|
+
}),
|
|
701
|
+
strategy: ["cookie", "baseLocale"],
|
|
702
|
+
});
|
|
703
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => Response.redirect("https://example.com/login", 302));
|
|
704
|
+
expect(response.status).toBe(302);
|
|
705
|
+
expect(response.headers.get("Location")).toBe("https://example.com/login");
|
|
706
|
+
expect(response.headers.get("Vary")).toBe("Cookie");
|
|
707
|
+
});
|
|
708
|
+
test.each(["/page", "/redirect"])("preserves fetch response metadata when adding Vary for %s", async (path) => {
|
|
709
|
+
const runtime = await createParaglide({
|
|
710
|
+
blob: await newProject({
|
|
711
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
712
|
+
}),
|
|
713
|
+
});
|
|
714
|
+
const server = createServer((request, response) => {
|
|
715
|
+
if (request.url === "/redirect") {
|
|
716
|
+
response.writeHead(302, { Location: "/page" });
|
|
717
|
+
response.end();
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
response.writeHead(202, "Accepted", { Vary: "Accept-Encoding" });
|
|
721
|
+
response.end("page");
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
server.listen(0, "127.0.0.1");
|
|
725
|
+
await once(server, "listening");
|
|
726
|
+
try {
|
|
727
|
+
const address = server.address();
|
|
728
|
+
if (!address || typeof address === "string") {
|
|
729
|
+
throw new Error("Expected a TCP server address");
|
|
730
|
+
}
|
|
731
|
+
const origin = `http://127.0.0.1:${address.port}`;
|
|
732
|
+
const original = await fetch(`${origin}${path}`);
|
|
733
|
+
const metadata = {
|
|
734
|
+
url: `${origin}/page`,
|
|
735
|
+
redirected: path === "/redirect",
|
|
736
|
+
type: "basic",
|
|
737
|
+
};
|
|
738
|
+
expect(original).toMatchObject(metadata);
|
|
739
|
+
expect(() => original.headers.set("Vary", "Cookie")).toThrow(TypeError);
|
|
740
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => original);
|
|
741
|
+
const clone = response.clone();
|
|
742
|
+
const secondClone = clone.clone();
|
|
743
|
+
for (const result of [response, clone, secondClone]) {
|
|
744
|
+
expect
|
|
745
|
+
.soft({
|
|
746
|
+
url: result.url,
|
|
747
|
+
redirected: result.redirected,
|
|
748
|
+
type: result.type,
|
|
749
|
+
})
|
|
750
|
+
.toEqual(metadata);
|
|
751
|
+
expect(result.status).toBe(202);
|
|
752
|
+
expect(result.statusText).toBe("Accepted");
|
|
753
|
+
expect(result.headers.get("Vary")).toBe("Accept-Encoding, Cookie");
|
|
754
|
+
expect(await result.text()).toBe("page");
|
|
755
|
+
}
|
|
756
|
+
expect(original.headers.get("Vary")).toBe("Accept-Encoding");
|
|
757
|
+
expect(() => response.clone()).toThrow(TypeError);
|
|
758
|
+
}
|
|
759
|
+
finally {
|
|
760
|
+
server.closeAllConnections();
|
|
761
|
+
await new Promise((resolve, reject) => {
|
|
762
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
test("allows overriding copied response properties", async () => {
|
|
767
|
+
const runtime = await createParaglide({
|
|
768
|
+
blob: await newProject({
|
|
769
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
770
|
+
}),
|
|
771
|
+
});
|
|
772
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => Response.redirect("https://example.com/login", 302));
|
|
773
|
+
for (const result of [new Response(), response, response.clone()]) {
|
|
774
|
+
result.clone = () => new Response("instrumented");
|
|
775
|
+
expect(await result.clone().text()).toBe("instrumented");
|
|
776
|
+
expect(Reflect.deleteProperty(result, "clone")).toBe(true);
|
|
777
|
+
Object.defineProperties(result, {
|
|
778
|
+
url: { value: "https://example.com/upstream" },
|
|
779
|
+
redirected: { value: true },
|
|
780
|
+
type: { value: "basic" },
|
|
781
|
+
});
|
|
782
|
+
expect(result.url).toBe("https://example.com/upstream");
|
|
783
|
+
expect(result.redirected).toBe(true);
|
|
784
|
+
expect(result.type).toBe("basic");
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
test("preserves non-Response results from resolve with cookie negotiation", async () => {
|
|
788
|
+
const runtime = await createParaglide({
|
|
789
|
+
blob: await newProject({
|
|
790
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
791
|
+
}),
|
|
792
|
+
strategy: ["cookie", "baseLocale"],
|
|
793
|
+
});
|
|
794
|
+
const result = { handled: true };
|
|
795
|
+
expect(await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => result)).toBe(result);
|
|
796
|
+
});
|
|
466
797
|
test("does not call onRedirect callback when there is no redirecting", async () => {
|
|
467
798
|
const runtime = await createParaglide({
|
|
468
799
|
blob: await newProject({
|
|
@@ -836,6 +1167,422 @@ test("only redirects if the request.headers.get('Sec-Fetch-Dest') === 'document'
|
|
|
836
1167
|
// Middleware should be called since no redirect for API requests
|
|
837
1168
|
expect(apiMiddlewareResolveWasCalled).toBe(true);
|
|
838
1169
|
});
|
|
1170
|
+
describe("navigation redirects", () => {
|
|
1171
|
+
test.each([
|
|
1172
|
+
["/fr/page", "fr", null],
|
|
1173
|
+
["/page", "en", "Cookie, Accept-Language, Accept, Sec-Fetch-Dest"],
|
|
1174
|
+
])("varies navigation responses according to URL resolution for %s", async (path, locale, expectedVary) => {
|
|
1175
|
+
const instance = await createParaglide({
|
|
1176
|
+
blob: await newProject({
|
|
1177
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
1178
|
+
}),
|
|
1179
|
+
strategy: ["url", "cookie", "preferredLanguage", "baseLocale"],
|
|
1180
|
+
urlPatterns: [
|
|
1181
|
+
{
|
|
1182
|
+
pattern: "https://example.com/:path(.*)?",
|
|
1183
|
+
localized: [
|
|
1184
|
+
["en", "https://example.com/en/:path(.*)?"],
|
|
1185
|
+
["fr", "https://example.com/fr/:path(.*)?"],
|
|
1186
|
+
],
|
|
1187
|
+
},
|
|
1188
|
+
],
|
|
1189
|
+
});
|
|
1190
|
+
const response = await instance.paraglideMiddleware(new Request(`https://example.com${path}`, {
|
|
1191
|
+
headers: {
|
|
1192
|
+
Cookie: "PARAGLIDE_LOCALE=en",
|
|
1193
|
+
"Accept-Language": "en",
|
|
1194
|
+
Accept: "text/html",
|
|
1195
|
+
"Sec-Fetch-Dest": "empty",
|
|
1196
|
+
},
|
|
1197
|
+
}), ({ locale }) => new Response(locale), { redirect: "navigation" });
|
|
1198
|
+
expect(response.headers.get("Vary")).toBe(expectedVary);
|
|
1199
|
+
expect(await response.text()).toBe(locale);
|
|
1200
|
+
});
|
|
1201
|
+
let runtime;
|
|
1202
|
+
const vary = "Cookie, Accept-Language, Accept, Sec-Fetch-Dest";
|
|
1203
|
+
beforeAll(async () => {
|
|
1204
|
+
runtime = await createParaglide({
|
|
1205
|
+
blob: await newProject({
|
|
1206
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
1207
|
+
}),
|
|
1208
|
+
strategy: ["preferredLanguage", "cookie", "url"],
|
|
1209
|
+
routeStrategies: [
|
|
1210
|
+
{ match: "/api/:path(.*)?", exclude: true },
|
|
1211
|
+
{ match: "/no-url", strategy: ["preferredLanguage"] },
|
|
1212
|
+
],
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
test.each([
|
|
1216
|
+
["GET", undefined, "text/html", 307],
|
|
1217
|
+
["HEAD", undefined, "text/html", 307],
|
|
1218
|
+
["GET", undefined, "*/*", 307],
|
|
1219
|
+
["GET", undefined, "text/*", 307],
|
|
1220
|
+
[
|
|
1221
|
+
"GET",
|
|
1222
|
+
undefined,
|
|
1223
|
+
"application/json, TEXT/HTML; charset=utf-8; Q=0.5",
|
|
1224
|
+
307,
|
|
1225
|
+
],
|
|
1226
|
+
["GET", undefined, "text/html;q=0.001", 307],
|
|
1227
|
+
["GET", undefined, "text/html;q=0, */*", 200],
|
|
1228
|
+
["GET", undefined, "*/*, text/html;q=0", 200],
|
|
1229
|
+
["GET", undefined, "text/*;q=0, */*", 200],
|
|
1230
|
+
["GET", undefined, "text/html;q=0.5, text/*;q=0", 307],
|
|
1231
|
+
["GET", undefined, "text/html;q=invalid", 200],
|
|
1232
|
+
["GET", undefined, "text/html;q=2", 200],
|
|
1233
|
+
["GET", undefined, "text/html;q=-1", 200],
|
|
1234
|
+
["GET", undefined, "text/html;q=0.1234", 200],
|
|
1235
|
+
["GET", undefined, 'text/html;profile="a,b";q=0, */*;q=0', 200],
|
|
1236
|
+
["GET", undefined, 'text/html;profile="x;q=0";q=1', 307],
|
|
1237
|
+
["GET", undefined, 'text/html;profile="x;q=1";q=0', 200],
|
|
1238
|
+
["GET", undefined, 'text/html;profile="a\\\",b";q=0, */*;q=0', 200],
|
|
1239
|
+
["GET", undefined, 'text/html;profile="a\\\";q=0";q=1', 307],
|
|
1240
|
+
["GET", undefined, 'text/html;profile="a\\\\";q=0, */*;q=0', 200],
|
|
1241
|
+
["GET", undefined, 'text/html;profile="unterminated', 200],
|
|
1242
|
+
["GET", undefined, "application/json", 200],
|
|
1243
|
+
["GET", undefined, "application/xhtml+xml", 200],
|
|
1244
|
+
["GET", undefined, "text/html-invalid", 200],
|
|
1245
|
+
["GET", undefined, "", 200],
|
|
1246
|
+
["GET", undefined, undefined, 200],
|
|
1247
|
+
["GET", "empty", "text/html", 200],
|
|
1248
|
+
["GET", "", "text/html", 200],
|
|
1249
|
+
["GET", "script", "text/html", 200],
|
|
1250
|
+
["GET", "document", "application/json", 307],
|
|
1251
|
+
["HEAD", "document", undefined, 307],
|
|
1252
|
+
["POST", undefined, "text/html", 200],
|
|
1253
|
+
["POST", "document", "text/html", 200],
|
|
1254
|
+
["PUT", "document", "text/html", 200],
|
|
1255
|
+
["OPTIONS", undefined, "text/html", 200],
|
|
1256
|
+
])("%s with destination %s and Accept %s returns %i", async (method, destination, accept, status) => {
|
|
1257
|
+
const headers = new Headers({
|
|
1258
|
+
"Accept-Language": "fr",
|
|
1259
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1260
|
+
});
|
|
1261
|
+
if (destination !== undefined)
|
|
1262
|
+
headers.set("Sec-Fetch-Dest", destination);
|
|
1263
|
+
if (accept !== undefined)
|
|
1264
|
+
headers.set("Accept", accept);
|
|
1265
|
+
const body = method === "POST" ? "name=Alice" : undefined;
|
|
1266
|
+
const request = new Request("https://example.com/about?from=test", {
|
|
1267
|
+
method,
|
|
1268
|
+
headers,
|
|
1269
|
+
body,
|
|
1270
|
+
});
|
|
1271
|
+
const resolve = vi.fn(async ({ request: localized }) => {
|
|
1272
|
+
expect(localized.method).toBe(method);
|
|
1273
|
+
expect(await localized.text()).toBe(body ?? "");
|
|
1274
|
+
return new Response(null);
|
|
1275
|
+
});
|
|
1276
|
+
const onRedirect = vi.fn();
|
|
1277
|
+
const response = await runtime.paraglideMiddleware(request, resolve, {
|
|
1278
|
+
redirect: "navigation",
|
|
1279
|
+
onRedirect,
|
|
1280
|
+
});
|
|
1281
|
+
expect(response.status).toBe(status);
|
|
1282
|
+
expect(resolve).toHaveBeenCalledTimes(status === 307 ? 0 : 1);
|
|
1283
|
+
expect(onRedirect).toHaveBeenCalledTimes(status === 307 ? 1 : 0);
|
|
1284
|
+
expect(response.headers.get("Location")).toBe(status === 307 ? "https://example.com/fr/about?from=test" : null);
|
|
1285
|
+
expect(response.headers.get("Vary")).toBe(method === "GET" || method === "HEAD" ? vary : "Cookie, Accept-Language");
|
|
1286
|
+
expect(response.body).toBeNull();
|
|
1287
|
+
expect(request.bodyUsed).toBe(false);
|
|
1288
|
+
expect(await request.text()).toBe(body ?? "");
|
|
1289
|
+
});
|
|
1290
|
+
test.each([undefined, "document", null, "nav"])("keeps default behavior for redirect=%s", async (redirect) => {
|
|
1291
|
+
for (const method of ["GET", "HEAD", "POST"]) {
|
|
1292
|
+
for (const destination of [undefined, "document", "empty"]) {
|
|
1293
|
+
const headers = new Headers({
|
|
1294
|
+
Accept: "text/html",
|
|
1295
|
+
"Accept-Language": "fr",
|
|
1296
|
+
});
|
|
1297
|
+
if (destination)
|
|
1298
|
+
headers.set("Sec-Fetch-Dest", destination);
|
|
1299
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/about", { method, headers }), () => new Response(null), redirect === undefined
|
|
1300
|
+
? undefined
|
|
1301
|
+
: { redirect: redirect });
|
|
1302
|
+
expect(response.status).toBe(destination === "document" ? 307 : 200);
|
|
1303
|
+
expect(response.headers.get("Vary")).toBe("Cookie, Accept-Language");
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
test.each(["GET", "HEAD"])("separates cached 200 and 307 responses for %s", async (method) => {
|
|
1308
|
+
for (const field of ["Accept-Language", "Cookie"]) {
|
|
1309
|
+
for (const locale of ["en", "fr"]) {
|
|
1310
|
+
for (const previous of [
|
|
1311
|
+
"",
|
|
1312
|
+
"Accept-Encoding",
|
|
1313
|
+
vary.toLowerCase(),
|
|
1314
|
+
"*",
|
|
1315
|
+
]) {
|
|
1316
|
+
const downstream = new Response(method === "HEAD" ? null : "ok", {
|
|
1317
|
+
headers: {
|
|
1318
|
+
Vary: previous,
|
|
1319
|
+
"Cache-Control": "public, max-age=600",
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
const response = await runtime.paraglideMiddleware(new Request("http://internal.example.com/about", {
|
|
1323
|
+
method,
|
|
1324
|
+
headers: {
|
|
1325
|
+
Accept: "text/html",
|
|
1326
|
+
[field]: field === "Cookie" ? `PARAGLIDE_LOCALE=${locale}` : locale,
|
|
1327
|
+
},
|
|
1328
|
+
}), () => downstream, {
|
|
1329
|
+
redirect: "navigation",
|
|
1330
|
+
effectiveRequestUrl: "https://example.com/about?from=test",
|
|
1331
|
+
});
|
|
1332
|
+
expect(response.status).toBe(locale === "fr" ? 307 : 200);
|
|
1333
|
+
expect(response.headers.get("Location")).toBe(locale === "fr" ? "https://example.com/fr/about?from=test" : null);
|
|
1334
|
+
const expected = previous === "Accept-Encoding"
|
|
1335
|
+
? `${previous}, ${vary}`
|
|
1336
|
+
: previous || vary;
|
|
1337
|
+
expect(response.headers.get("Vary")).toBe(locale === "fr" ? vary : expected);
|
|
1338
|
+
if (locale === "en") {
|
|
1339
|
+
expect(response.headers.get("Cache-Control")).toBe("public, max-age=600");
|
|
1340
|
+
expect(await response.text()).toBe(method === "HEAD" ? "" : "ok");
|
|
1341
|
+
expect(response).toBe(downstream);
|
|
1342
|
+
expect(downstream.headers.get("Vary")).toBe(expected);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
test("preserves response identity and custom state when adding Vary", async () => {
|
|
1349
|
+
class FrameworkResponse extends Response {
|
|
1350
|
+
context = { compression: "manual", framework: "example" };
|
|
1351
|
+
}
|
|
1352
|
+
const downstream = new FrameworkResponse("ok", {
|
|
1353
|
+
status: 201,
|
|
1354
|
+
statusText: "Created",
|
|
1355
|
+
headers: { Vary: "Accept-Encoding", "X-Framework": "example" },
|
|
1356
|
+
});
|
|
1357
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/about", {
|
|
1358
|
+
headers: { Accept: "application/json" },
|
|
1359
|
+
}), () => downstream, { redirect: "navigation" });
|
|
1360
|
+
expect(response).toBe(downstream);
|
|
1361
|
+
expect(response).toBeInstanceOf(FrameworkResponse);
|
|
1362
|
+
expect(downstream.context).toEqual({
|
|
1363
|
+
compression: "manual",
|
|
1364
|
+
framework: "example",
|
|
1365
|
+
});
|
|
1366
|
+
expect(response.status).toBe(201);
|
|
1367
|
+
expect(response.statusText).toBe("Created");
|
|
1368
|
+
expect(response.headers.get("X-Framework")).toBe("example");
|
|
1369
|
+
expect(response.headers.get("Vary")).toBe(`Accept-Encoding, ${vary}`);
|
|
1370
|
+
expect(await response.text()).toBe("ok");
|
|
1371
|
+
});
|
|
1372
|
+
test("preserves callback returns with unusable bodies", async () => {
|
|
1373
|
+
const consumed = new Response("ok");
|
|
1374
|
+
await consumed.text();
|
|
1375
|
+
const locked = new Response("ok");
|
|
1376
|
+
const reader = locked.body.getReader();
|
|
1377
|
+
try {
|
|
1378
|
+
for (const value of [undefined, {}, Response.error(), consumed, locked]) {
|
|
1379
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/about", {
|
|
1380
|
+
headers: { Accept: "application/json" },
|
|
1381
|
+
}), async () => value, { redirect: "navigation" });
|
|
1382
|
+
expect(response).toBe(value);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
finally {
|
|
1386
|
+
reader.releaseLock();
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
test("preserves a fetched body when immutable headers require a replacement", async () => {
|
|
1390
|
+
const downstream = await fetch("data:text/plain,ok");
|
|
1391
|
+
expect(() => downstream.headers.set("Vary", "Accept")).toThrow();
|
|
1392
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/about", {
|
|
1393
|
+
headers: { Accept: "application/json" },
|
|
1394
|
+
}), () => downstream, { redirect: "navigation" });
|
|
1395
|
+
expect(response).not.toBe(downstream);
|
|
1396
|
+
expect(response.status).toBe(downstream.status);
|
|
1397
|
+
expect(response.statusText).toBe(downstream.statusText);
|
|
1398
|
+
expect(response.headers.get("Content-Type")).toBe("text/plain");
|
|
1399
|
+
expect(response.headers.get("Vary")).toBe(vary);
|
|
1400
|
+
expect(downstream.headers.get("Vary")).toBeNull();
|
|
1401
|
+
expect(await response.text()).toBe("ok");
|
|
1402
|
+
});
|
|
1403
|
+
test("varies immutable headers only on routes using URL strategy", async () => {
|
|
1404
|
+
for (const path of ["/about", "/api/data", "/no-url"]) {
|
|
1405
|
+
const downstream = Response.redirect("https://example.com/login", 303);
|
|
1406
|
+
const response = await runtime.paraglideMiddleware(new Request(`https://example.com${path}`, {
|
|
1407
|
+
headers: {
|
|
1408
|
+
Accept: path === "/about" ? "application/json" : "text/html",
|
|
1409
|
+
Cookie: "PARAGLIDE_LOCALE=fr",
|
|
1410
|
+
"Accept-Language": "fr",
|
|
1411
|
+
},
|
|
1412
|
+
}), () => downstream, { redirect: "navigation" });
|
|
1413
|
+
expect(response.status).toBe(303);
|
|
1414
|
+
expect(response.statusText).toBe(downstream.statusText);
|
|
1415
|
+
expect(response.headers.get("Location")).toBe("https://example.com/login");
|
|
1416
|
+
expect(response.headers.get("Vary")).toBe(path === "/about" ? vary : path === "/no-url" ? "Accept-Language" : null);
|
|
1417
|
+
expect(downstream.headers.get("Vary")).toBeNull();
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
});
|
|
1421
|
+
test.each([
|
|
1422
|
+
["/fr/about", "application/json", false, "en", 200],
|
|
1423
|
+
["/fr/about", "text/html", false, "en", 200],
|
|
1424
|
+
["/fr/about/", "application/json", false, "en", 200],
|
|
1425
|
+
["/fr/about/", "text/html", false, "en", 307],
|
|
1426
|
+
["/fr/about", "text/html", true, "fr", 200],
|
|
1427
|
+
["/fr/about", "text/html", true, "en", 307],
|
|
1428
|
+
])("navigation cache for %s with Accept %s, custom strategy=%s and preference=%s", async (path, accept, custom, preference, status) => {
|
|
1429
|
+
const runtime = await createParaglide({
|
|
1430
|
+
blob: await newProject({
|
|
1431
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
1432
|
+
}),
|
|
1433
|
+
strategy: [
|
|
1434
|
+
"url",
|
|
1435
|
+
"cookie",
|
|
1436
|
+
"preferredLanguage",
|
|
1437
|
+
"baseLocale",
|
|
1438
|
+
...(custom ? ["custom-header"] : []),
|
|
1439
|
+
],
|
|
1440
|
+
trailingSlash: "never",
|
|
1441
|
+
});
|
|
1442
|
+
if (custom)
|
|
1443
|
+
runtime.defineCustomServerStrategy("custom-header", {
|
|
1444
|
+
getLocale: (request) => request?.headers.get("Accept-Language") ?? undefined,
|
|
1445
|
+
});
|
|
1446
|
+
for (const method of ["GET", "HEAD"]) {
|
|
1447
|
+
const downstream = new Response(method === "HEAD" ? null : "ok", {
|
|
1448
|
+
headers: {
|
|
1449
|
+
Vary: "Accept-Encoding",
|
|
1450
|
+
"Cache-Control": "public, max-age=600",
|
|
1451
|
+
},
|
|
1452
|
+
});
|
|
1453
|
+
const response = await runtime.paraglideMiddleware(new Request("http://internal.example.com/about", {
|
|
1454
|
+
method,
|
|
1455
|
+
headers: {
|
|
1456
|
+
Accept: accept,
|
|
1457
|
+
Cookie: "PARAGLIDE_LOCALE=en",
|
|
1458
|
+
"Accept-Language": preference,
|
|
1459
|
+
},
|
|
1460
|
+
}), () => downstream, {
|
|
1461
|
+
redirect: "navigation",
|
|
1462
|
+
effectiveRequestUrl: `https://example.com${path}`,
|
|
1463
|
+
});
|
|
1464
|
+
expect(response.status).toBe(status);
|
|
1465
|
+
expect(response.headers.get("Location")).toBe(status === 307
|
|
1466
|
+
? `https://example.com${custom ? "/about" : "/fr/about"}`
|
|
1467
|
+
: null);
|
|
1468
|
+
const fields = custom
|
|
1469
|
+
? "Cookie, Accept-Language, Accept, Sec-Fetch-Dest"
|
|
1470
|
+
: "Accept, Sec-Fetch-Dest";
|
|
1471
|
+
const needsVary = custom || path.endsWith("/");
|
|
1472
|
+
expect(response.headers.get("Vary")).toBe(status === 307
|
|
1473
|
+
? fields
|
|
1474
|
+
: needsVary
|
|
1475
|
+
? `Accept-Encoding, ${fields}`
|
|
1476
|
+
: "Accept-Encoding");
|
|
1477
|
+
if (!needsVary)
|
|
1478
|
+
expect(response).toBe(downstream);
|
|
1479
|
+
if (status === 200) {
|
|
1480
|
+
expect(response.headers.get("Cache-Control")).toBe("public, max-age=600");
|
|
1481
|
+
expect(await response.text()).toBe(method === "HEAD" ? "" : "ok");
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
test.each(["document", "navigation"])("custom server strategies retain preference Vary in %s mode", async (redirect) => {
|
|
1486
|
+
const runtime = await createParaglide({
|
|
1487
|
+
blob: await newProject({
|
|
1488
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
1489
|
+
}),
|
|
1490
|
+
strategy: ["url", "cookie", "preferredLanguage", "custom-header"],
|
|
1491
|
+
});
|
|
1492
|
+
runtime.defineCustomServerStrategy("custom-header", {
|
|
1493
|
+
getLocale: (request) => request?.headers.get("Accept-Language") ?? undefined,
|
|
1494
|
+
});
|
|
1495
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/fr/page", {
|
|
1496
|
+
headers: { Accept: "application/json", "Accept-Language": "en" },
|
|
1497
|
+
}), ({ locale }) => new Response(locale), { redirect });
|
|
1498
|
+
expect(await response.text()).toBe("en");
|
|
1499
|
+
expect(response.headers.get("Vary")).toBe(redirect === "navigation"
|
|
1500
|
+
? "Cookie, Accept-Language, Accept, Sec-Fetch-Dest"
|
|
1501
|
+
: "Cookie, Accept-Language");
|
|
1502
|
+
});
|
|
1503
|
+
test.each([
|
|
1504
|
+
"sync undefined",
|
|
1505
|
+
"async undefined",
|
|
1506
|
+
"object",
|
|
1507
|
+
"object with headers",
|
|
1508
|
+
"error",
|
|
1509
|
+
"consumed body",
|
|
1510
|
+
"locked body",
|
|
1511
|
+
"html",
|
|
1512
|
+
].flatMap((kind) => [false, true].flatMap((experimentalMiddlewareLocaleSplitting) => ["document", "navigation"].map((redirect) => ({
|
|
1513
|
+
kind,
|
|
1514
|
+
experimentalMiddlewareLocaleSplitting,
|
|
1515
|
+
redirect,
|
|
1516
|
+
})))))("$redirect redirects handle callback returns: $kind with locale splitting=$experimentalMiddlewareLocaleSplitting", async ({ kind, experimentalMiddlewareLocaleSplitting, redirect }) => {
|
|
1517
|
+
const runtime = await createParaglide({
|
|
1518
|
+
blob: await newProject({
|
|
1519
|
+
settings: { baseLocale: "en", locales: ["en", "fr"] },
|
|
1520
|
+
}),
|
|
1521
|
+
strategy: ["preferredLanguage", "url"],
|
|
1522
|
+
experimentalMiddlewareLocaleSplitting,
|
|
1523
|
+
});
|
|
1524
|
+
let downstream;
|
|
1525
|
+
let reader;
|
|
1526
|
+
if (kind === "object")
|
|
1527
|
+
downstream = { handled: true };
|
|
1528
|
+
if (kind === "object with headers") {
|
|
1529
|
+
downstream = { headers: new Headers({ "Content-Type": "text/html" }) };
|
|
1530
|
+
}
|
|
1531
|
+
if (kind === "error")
|
|
1532
|
+
downstream = Response.error();
|
|
1533
|
+
if (["consumed body", "locked body", "html"].includes(kind)) {
|
|
1534
|
+
const response = new Response("<html><head></head><body>ok</body></html>", {
|
|
1535
|
+
headers: { "Content-Type": "text/html" },
|
|
1536
|
+
});
|
|
1537
|
+
if (kind === "consumed body")
|
|
1538
|
+
await response.text();
|
|
1539
|
+
if (kind === "locked body")
|
|
1540
|
+
reader = response.body.getReader();
|
|
1541
|
+
downstream = response;
|
|
1542
|
+
}
|
|
1543
|
+
try {
|
|
1544
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/about", {
|
|
1545
|
+
headers: { Accept: "application/json", "Accept-Language": "fr" },
|
|
1546
|
+
}), kind === "async undefined" ? async () => downstream : () => downstream, { redirect });
|
|
1547
|
+
if (kind === "html") {
|
|
1548
|
+
const htmlResponse = response;
|
|
1549
|
+
expect(htmlResponse.headers.get("Vary")).toBe(redirect === "navigation"
|
|
1550
|
+
? "Accept-Language, Accept, Sec-Fetch-Dest"
|
|
1551
|
+
: "Accept-Language");
|
|
1552
|
+
const html = await htmlResponse.text();
|
|
1553
|
+
expect(html).toContain("<body>ok</body>");
|
|
1554
|
+
expect(html.includes("globalThis.__paraglide.ssr")).toBe(experimentalMiddlewareLocaleSplitting);
|
|
1555
|
+
}
|
|
1556
|
+
else
|
|
1557
|
+
expect(response).toBe(downstream);
|
|
1558
|
+
}
|
|
1559
|
+
finally {
|
|
1560
|
+
reader?.releaseLock();
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
test("locale splitting accepts responses created before the global Response is replaced", async () => {
|
|
1564
|
+
const runtime = await createParaglide({
|
|
1565
|
+
blob: await newProject({
|
|
1566
|
+
settings: { baseLocale: "en", locales: ["en", "de"] },
|
|
1567
|
+
}),
|
|
1568
|
+
strategy: ["globalVariable", "baseLocale"],
|
|
1569
|
+
experimentalMiddlewareLocaleSplitting: true,
|
|
1570
|
+
});
|
|
1571
|
+
const NativeResponse = Response;
|
|
1572
|
+
const downstream = new NativeResponse("<html><head></head><body>ok</body></html>", { headers: { "Content-Type": "text/html" } });
|
|
1573
|
+
vi.stubGlobal("Response", class extends NativeResponse {
|
|
1574
|
+
});
|
|
1575
|
+
try {
|
|
1576
|
+
expect(downstream).not.toBeInstanceOf(Response);
|
|
1577
|
+
const response = await runtime.paraglideMiddleware(new Request("https://example.com/page"), () => downstream);
|
|
1578
|
+
const html = await response.text();
|
|
1579
|
+
expect(html).toContain("globalThis.__paraglide.ssr");
|
|
1580
|
+
expect(html).toContain("<body>ok</body>");
|
|
1581
|
+
}
|
|
1582
|
+
finally {
|
|
1583
|
+
vi.unstubAllGlobals();
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
839
1586
|
// https://github.com/opral/inlang-paraglide-js/issues/477
|
|
840
1587
|
test("does not catch errors thrown by downstream resolve call", async () => {
|
|
841
1588
|
const runtime = await createParaglide({
|