@inlang/paraglide-js 2.23.2 → 2.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/bundler-plugins/per-locale-build/vite-environments.js +12 -7
  2. package/dist/bundler-plugins/per-locale-build/vite-environments.js.map +1 -1
  3. package/dist/bundler-plugins/unplugin.js +134 -4
  4. package/dist/bundler-plugins/unplugin.js.map +1 -1
  5. package/dist/bundler-plugins/unplugin.test.js +77 -0
  6. package/dist/bundler-plugins/unplugin.test.js.map +1 -1
  7. package/dist/cli/commands/compile/command.js +8 -1
  8. package/dist/cli/commands/compile/command.js.map +1 -1
  9. package/dist/cli/commands/compile/command.test.js +45 -0
  10. package/dist/cli/commands/compile/command.test.js.map +1 -1
  11. package/dist/compiler/compile.js +0 -15
  12. package/dist/compiler/compile.js.map +1 -1
  13. package/dist/compiler/compile.test.js +0 -52
  14. package/dist/compiler/compile.test.js.map +1 -1
  15. package/dist/compiler/compiler-options.d.ts +8 -0
  16. package/dist/compiler/compiler-options.js.map +1 -1
  17. package/dist/compiler/runtime/create-runtime.d.ts +1 -0
  18. package/dist/compiler/runtime/create-runtime.js +11 -0
  19. package/dist/compiler/runtime/create-runtime.js.map +1 -1
  20. package/dist/compiler/runtime/exec-url-pattern.d.ts +9 -0
  21. package/dist/compiler/runtime/exec-url-pattern.js +31 -0
  22. package/dist/compiler/runtime/exec-url-pattern.js.map +1 -0
  23. package/dist/compiler/runtime/extract-locale-from-url.js +8 -3
  24. package/dist/compiler/runtime/extract-locale-from-url.js.map +1 -1
  25. package/dist/compiler/runtime/generate-static-localized-urls.js +12 -6
  26. package/dist/compiler/runtime/generate-static-localized-urls.js.map +1 -1
  27. package/dist/compiler/runtime/generate-static-localized-urls.test.js +46 -0
  28. package/dist/compiler/runtime/generate-static-localized-urls.test.js.map +1 -1
  29. package/dist/compiler/runtime/localize-href.bench.d.ts +1 -0
  30. package/dist/compiler/runtime/localize-href.bench.js +77 -0
  31. package/dist/compiler/runtime/localize-href.bench.js.map +1 -0
  32. package/dist/compiler/runtime/localize-href.test.js +43 -0
  33. package/dist/compiler/runtime/localize-href.test.js.map +1 -1
  34. package/dist/compiler/runtime/localize-url.d.ts +14 -0
  35. package/dist/compiler/runtime/localize-url.js +376 -18
  36. package/dist/compiler/runtime/localize-url.js.map +1 -1
  37. package/dist/compiler/runtime/localize-url.test.js +163 -0
  38. package/dist/compiler/runtime/localize-url.test.js.map +1 -1
  39. package/dist/compiler/runtime/normalize-trailing-slash.d.ts +10 -0
  40. package/dist/compiler/runtime/normalize-trailing-slash.js +23 -0
  41. package/dist/compiler/runtime/normalize-trailing-slash.js.map +1 -0
  42. package/dist/compiler/runtime/route-strategy.js +4 -2
  43. package/dist/compiler/runtime/route-strategy.js.map +1 -1
  44. package/dist/compiler/runtime/should-redirect.js +4 -1
  45. package/dist/compiler/runtime/should-redirect.js.map +1 -1
  46. package/dist/compiler/runtime/should-redirect.test.js +30 -2
  47. package/dist/compiler/runtime/should-redirect.test.js.map +1 -1
  48. package/dist/compiler/runtime/type.d.ts +1 -0
  49. package/dist/compiler/runtime/type.js.map +1 -1
  50. package/dist/compiler/runtime/variables.d.ts +6 -0
  51. package/dist/compiler/runtime/variables.js +6 -0
  52. package/dist/compiler/runtime/variables.js.map +1 -1
  53. package/dist/compiler/server/middleware.js +6 -2
  54. package/dist/compiler/server/middleware.js.map +1 -1
  55. package/dist/compiler/server/middleware.test.js +34 -0
  56. package/dist/compiler/server/middleware.test.js.map +1 -1
  57. package/dist/index.test.d.ts +1 -0
  58. package/dist/index.test.js +9 -0
  59. package/dist/index.test.js.map +1 -0
  60. package/dist/services/env-variables/index.js +1 -1
  61. package/dist/services/env-variables/index.js.map +1 -1
  62. package/dist/services/file-handling/write-output.bench.d.ts +1 -0
  63. package/dist/services/file-handling/write-output.bench.js +24 -0
  64. package/dist/services/file-handling/write-output.bench.js.map +1 -0
  65. package/dist/services/file-handling/write-output.js +16 -20
  66. package/dist/services/file-handling/write-output.js.map +1 -1
  67. package/dist/services/file-handling/write-output.test.js +27 -0
  68. package/dist/services/file-handling/write-output.test.js.map +1 -1
  69. package/package.json +2 -2
  70. package/dist/services/account/index.d.ts +0 -14
  71. package/dist/services/account/index.js +0 -43
  72. package/dist/services/account/index.js.map +0 -1
@@ -148,6 +148,23 @@ test("default url patterns to improve out of the box experience", async () => {
148
148
  expect(runtime.localizeHref("http://localhost:5173/about", { locale: "de" })).toBe("http://localhost:5173/de/about");
149
149
  expect(runtime.deLocalizeHref("http://localhost:5173/de/about")).toBe("http://localhost:5173/about");
150
150
  });
151
+ test("normalizes localized root hrefs and anchors", async () => {
152
+ const runtime = await createParaglide({
153
+ blob: await newProject({
154
+ settings: {
155
+ baseLocale: "en",
156
+ locales: ["en", "de"],
157
+ },
158
+ }),
159
+ isServer: "false",
160
+ strategy: ["url"],
161
+ trailingSlash: "never",
162
+ });
163
+ globalThis.window = { location: new URL("http://example.com") };
164
+ expect(runtime.localizeHref("/", { locale: "de" })).toBe("/de");
165
+ expect(runtime.localizeHref("/#section", { locale: "de" })).toBe("/de#section");
166
+ expect(runtime.deLocalizeHref("/de/")).toBe("/");
167
+ });
151
168
  test("normalizes mixed-case explicit locales in localizeHref", async () => {
152
169
  const runtime = await createParaglide({
153
170
  blob: await newProject({
@@ -161,4 +178,30 @@ test("normalizes mixed-case explicit locales in localizeHref", async () => {
161
178
  expect(runtime.localizeHref("/about", { locale: "DE" })).toBe("/de/about");
162
179
  expect(runtime.localizeHref("/de/about", { locale: "EN" })).toBe("/about");
163
180
  });
181
+ test("keeps custom URLPattern path expressions on the generic fallback", async () => {
182
+ const runtime = await createParaglide({
183
+ blob: await newProject({
184
+ settings: {
185
+ baseLocale: "en",
186
+ locales: ["en", "de"],
187
+ },
188
+ }),
189
+ strategy: ["url"],
190
+ urlPatterns: [
191
+ {
192
+ pattern: "/:slug(.)?",
193
+ localized: [
194
+ ["en", "/:slug(.)?"],
195
+ ["de", "/de/:slug(.)?"],
196
+ ],
197
+ },
198
+ ],
199
+ });
200
+ runtime.overwriteGetLocale(() => "en");
201
+ runtime.overwriteGetUrlOrigin(() => "https://example.com");
202
+ // `(.)` matches exactly one character in URLPattern. A prefix-only
203
+ // implementation must not broaden this route into a catch-all.
204
+ expect(runtime.localizeHref("/a", { locale: "de" })).toBe("/de/a");
205
+ expect(runtime.localizeHref("/about", { locale: "de" })).toBe("/about");
206
+ });
164
207
  //# sourceMappingURL=localize-href.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"localize-href.test.js","sourceRoot":"","sources":["../../../src/compiler/runtime/localize-href.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { test, expect } from \"vitest\";\nimport { createParaglide } from \"../create-paraglide.js\";\nimport { newProject } from \"@inlang/sdk\";\n\ntest(\"uses the locale from getLocale() if no locale is provided\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\", \"globalVariable\", \"baseLocale\"],\n\t\turlPatterns: [\n\t\t\t{\n\t\t\t\tpattern: \"http://:domain(.*)/:locale(de|en)?/:path(.*)?\",\n\t\t\t\tlocalized: [\n\t\t\t\t\t[\"de\", \"http://:domain(.*)/de/:path(.*)?\"],\n\t\t\t\t\t[\"en\", \"http://:domain(.*)/:path(.*)?\"],\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t});\n\n\truntime.overwriteGetLocale(() => \"de\");\n\n\texpect(runtime.localizeHref(\"/hello\")).toBe(\"/de/hello\");\n\texpect(runtime.deLocalizeHref(\"/de/hello\")).toBe(\"/hello\");\n});\n\ntest(\"returns an absolute href if the provided href is absolute\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\", \"globalVariable\", \"baseLocale\"],\n\t\turlPatterns: [\n\t\t\t{\n\t\t\t\tpattern: \"http://:domain(.*)/:locale(de|en)?/:path(.*)?\",\n\t\t\t\tlocalized: [\n\t\t\t\t\t[\"de\", \"http://:domain(.*)/de/:path(.*)?\"],\n\t\t\t\t\t[\"en\", \"http://:domain(.*)/:path(.*)?\"],\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t});\n\n\texpect(\n\t\truntime.localizeHref(\"http://example.com/hello\", { locale: \"de\" })\n\t).toBe(\"http://example.com/de/hello\");\n\texpect(runtime.deLocalizeHref(\"http://example.com/de/hello\")).toBe(\n\t\t\"http://example.com/hello\"\n\t);\n});\n\n// useful if domain based localization is used for example\ntest(\"returns an absolute href if the provided href is relative but the origin of the localized href differs\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\", \"globalVariable\", \"baseLocale\"],\n\t\turlPatterns: [\n\t\t\t{\n\t\t\t\tpattern: \"http://example.com/:path(.*)?\",\n\t\t\t\tlocalized: [\n\t\t\t\t\t[\"de\", \"http://de.example.com/:path(.*)?\"],\n\t\t\t\t\t[\"en\", \"http://example.com/:path(.*)?\"],\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t});\n\n\t// simulating routing from current en page to de page\n\truntime.overwriteGetLocale(() => \"en\");\n\truntime.overwriteGetUrlOrigin(() => \"http://example.com\");\n\n\texpect(runtime.localizeHref(\"/hello\", { locale: \"de\" })).toBe(\n\t\t\"http://de.example.com/hello\"\n\t);\n\texpect(runtime.deLocalizeHref(\"http://de.example.com/hello\")).toBe(\n\t\t\"http://example.com/hello\"\n\t);\n\n\t// simulating routing from current de page to en page\n\truntime.overwriteGetLocale(() => \"de\");\n\n\texpect(runtime.localizeHref(\"/hello\", { locale: \"en\" })).toBe(\n\t\t\"http://example.com/hello\"\n\t);\n\texpect(runtime.deLocalizeHref(\"http://example.com/hello\")).toBe(\n\t\t\"http://example.com/hello\"\n\t);\n});\n\n// https://github.com/opral/inlang-paraglide-js/issues/362\ntest(\"adding a base path\", async () => {\n\tconst base = \"shop\";\n\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\"],\n\t\turlPatterns: [\n\t\t\t{\n\t\t\t\tpattern: `/{${base}/}?:path(.*)?`,\n\t\t\t\tlocalized: [\n\t\t\t\t\t[\"de\", `/{${base}/}?de/:path(.*)?`],\n\t\t\t\t\t[\"en\", `/{${base}/}?:path(.*)?`],\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t});\n\n\t// simulating the current locale to be en\n\truntime.overwriteGetLocale(() => \"en\");\n\n\texpect(runtime.localizeHref(\"/about\")).toBe(\"/shop/about\");\n\texpect(runtime.deLocalizeHref(\"/about\")).toBe(\"/shop/about\");\n\n\t// simulating current locale to be de\n\truntime.overwriteGetLocale(() => \"de\");\n\texpect(runtime.localizeHref(\"/about\")).toBe(\"/shop/de/about\");\n\texpect(runtime.deLocalizeHref(\"/de/about\")).toBe(\"/shop/about\");\n});\n\ntest(\"default url patterns to improve out of the box experience\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\", \"fr\"],\n\t\t\t},\n\t\t}),\n\t\tisServer: \"false\",\n\t\tstrategy: [\"url\"],\n\t\turlPatterns: undefined,\n\t});\n\n\t// polyfilling window\n\tglobalThis.window = { location: new URL(\"http://example.com\") } as any;\n\n\texpect(runtime.urlPatterns).toStrictEqual([\n\t\t{\n\t\t\tpattern: \":protocol://:domain(.*)::port?/:path(.*)?\",\n\t\t\tlocalized: [\n\t\t\t\t[\"de\", \":protocol://:domain(.*)::port?/de/:path(.*)?\"],\n\t\t\t\t[\"fr\", \":protocol://:domain(.*)::port?/fr/:path(.*)?\"],\n\t\t\t\t[\"en\", \":protocol://:domain(.*)::port?/:path(.*)?\"],\n\t\t\t],\n\t\t},\n\t]);\n\n\t// expecting the baseLocale to not be included in the url\n\texpect(runtime.localizeHref(\"/about\", { locale: \"en\" })).toBe(\"/about\");\n\texpect(runtime.localizeHref(\"/about\", { locale: \"de\" })).toBe(\"/de/about\");\n\n\t// expecting root path to work\n\texpect(runtime.localizeHref(\"/\", { locale: \"en\" })).toBe(\"/\");\n\texpect(runtime.localizeHref(\"/\", { locale: \"de\" })).toBe(\"/de/\");\n\n\t// expecting de-localization to work\n\texpect(runtime.deLocalizeHref(\"/de/\")).toBe(\"/\");\n\texpect(runtime.deLocalizeHref(\"/de/about\")).toBe(\"/about\");\n\texpect(runtime.deLocalizeHref(\"/about\")).toBe(\"/about\");\n\n\t// expecting localhost to work (for development)\n\texpect(runtime.localizeHref(\"http://localhost:5173/\", { locale: \"de\" })).toBe(\n\t\t\"http://localhost:5173/de/\"\n\t);\n\n\t// de-localizing localhost\n\texpect(runtime.deLocalizeHref(\"http://localhost:5173/de\")).toBe(\n\t\t\"http://localhost:5173/\"\n\t);\n\n\t// pathnames\n\texpect(\n\t\truntime.localizeHref(\"http://localhost:5173/about\", { locale: \"de\" })\n\t).toBe(\"http://localhost:5173/de/about\");\n\n\texpect(runtime.deLocalizeHref(\"http://localhost:5173/de/about\")).toBe(\n\t\t\"http://localhost:5173/about\"\n\t);\n});\n\ntest(\"normalizes mixed-case explicit locales in localizeHref\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\"],\n\t});\n\n\texpect(runtime.localizeHref(\"/about\", { locale: \"DE\" })).toBe(\"/de/about\");\n\n\texpect(runtime.localizeHref(\"/de/about\", { locale: \"EN\" })).toBe(\"/about\");\n});\n"]}
1
+ {"version":3,"file":"localize-href.test.js","sourceRoot":"","sources":["../../../src/compiler/runtime/localize-href.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { test, expect } from \"vitest\";\nimport { createParaglide } from \"../create-paraglide.js\";\nimport { newProject } from \"@inlang/sdk\";\n\ntest(\"uses the locale from getLocale() if no locale is provided\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\", \"globalVariable\", \"baseLocale\"],\n\t\turlPatterns: [\n\t\t\t{\n\t\t\t\tpattern: \"http://:domain(.*)/:locale(de|en)?/:path(.*)?\",\n\t\t\t\tlocalized: [\n\t\t\t\t\t[\"de\", \"http://:domain(.*)/de/:path(.*)?\"],\n\t\t\t\t\t[\"en\", \"http://:domain(.*)/:path(.*)?\"],\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t});\n\n\truntime.overwriteGetLocale(() => \"de\");\n\n\texpect(runtime.localizeHref(\"/hello\")).toBe(\"/de/hello\");\n\texpect(runtime.deLocalizeHref(\"/de/hello\")).toBe(\"/hello\");\n});\n\ntest(\"returns an absolute href if the provided href is absolute\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\", \"globalVariable\", \"baseLocale\"],\n\t\turlPatterns: [\n\t\t\t{\n\t\t\t\tpattern: \"http://:domain(.*)/:locale(de|en)?/:path(.*)?\",\n\t\t\t\tlocalized: [\n\t\t\t\t\t[\"de\", \"http://:domain(.*)/de/:path(.*)?\"],\n\t\t\t\t\t[\"en\", \"http://:domain(.*)/:path(.*)?\"],\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t});\n\n\texpect(\n\t\truntime.localizeHref(\"http://example.com/hello\", { locale: \"de\" })\n\t).toBe(\"http://example.com/de/hello\");\n\texpect(runtime.deLocalizeHref(\"http://example.com/de/hello\")).toBe(\n\t\t\"http://example.com/hello\"\n\t);\n});\n\n// useful if domain based localization is used for example\ntest(\"returns an absolute href if the provided href is relative but the origin of the localized href differs\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\", \"globalVariable\", \"baseLocale\"],\n\t\turlPatterns: [\n\t\t\t{\n\t\t\t\tpattern: \"http://example.com/:path(.*)?\",\n\t\t\t\tlocalized: [\n\t\t\t\t\t[\"de\", \"http://de.example.com/:path(.*)?\"],\n\t\t\t\t\t[\"en\", \"http://example.com/:path(.*)?\"],\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t});\n\n\t// simulating routing from current en page to de page\n\truntime.overwriteGetLocale(() => \"en\");\n\truntime.overwriteGetUrlOrigin(() => \"http://example.com\");\n\n\texpect(runtime.localizeHref(\"/hello\", { locale: \"de\" })).toBe(\n\t\t\"http://de.example.com/hello\"\n\t);\n\texpect(runtime.deLocalizeHref(\"http://de.example.com/hello\")).toBe(\n\t\t\"http://example.com/hello\"\n\t);\n\n\t// simulating routing from current de page to en page\n\truntime.overwriteGetLocale(() => \"de\");\n\n\texpect(runtime.localizeHref(\"/hello\", { locale: \"en\" })).toBe(\n\t\t\"http://example.com/hello\"\n\t);\n\texpect(runtime.deLocalizeHref(\"http://example.com/hello\")).toBe(\n\t\t\"http://example.com/hello\"\n\t);\n});\n\n// https://github.com/opral/inlang-paraglide-js/issues/362\ntest(\"adding a base path\", async () => {\n\tconst base = \"shop\";\n\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\"],\n\t\turlPatterns: [\n\t\t\t{\n\t\t\t\tpattern: `/{${base}/}?:path(.*)?`,\n\t\t\t\tlocalized: [\n\t\t\t\t\t[\"de\", `/{${base}/}?de/:path(.*)?`],\n\t\t\t\t\t[\"en\", `/{${base}/}?:path(.*)?`],\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t});\n\n\t// simulating the current locale to be en\n\truntime.overwriteGetLocale(() => \"en\");\n\n\texpect(runtime.localizeHref(\"/about\")).toBe(\"/shop/about\");\n\texpect(runtime.deLocalizeHref(\"/about\")).toBe(\"/shop/about\");\n\n\t// simulating current locale to be de\n\truntime.overwriteGetLocale(() => \"de\");\n\texpect(runtime.localizeHref(\"/about\")).toBe(\"/shop/de/about\");\n\texpect(runtime.deLocalizeHref(\"/de/about\")).toBe(\"/shop/about\");\n});\n\ntest(\"default url patterns to improve out of the box experience\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\", \"fr\"],\n\t\t\t},\n\t\t}),\n\t\tisServer: \"false\",\n\t\tstrategy: [\"url\"],\n\t\turlPatterns: undefined,\n\t});\n\n\t// polyfilling window\n\tglobalThis.window = { location: new URL(\"http://example.com\") } as any;\n\n\texpect(runtime.urlPatterns).toStrictEqual([\n\t\t{\n\t\t\tpattern: \":protocol://:domain(.*)::port?/:path(.*)?\",\n\t\t\tlocalized: [\n\t\t\t\t[\"de\", \":protocol://:domain(.*)::port?/de/:path(.*)?\"],\n\t\t\t\t[\"fr\", \":protocol://:domain(.*)::port?/fr/:path(.*)?\"],\n\t\t\t\t[\"en\", \":protocol://:domain(.*)::port?/:path(.*)?\"],\n\t\t\t],\n\t\t},\n\t]);\n\n\t// expecting the baseLocale to not be included in the url\n\texpect(runtime.localizeHref(\"/about\", { locale: \"en\" })).toBe(\"/about\");\n\texpect(runtime.localizeHref(\"/about\", { locale: \"de\" })).toBe(\"/de/about\");\n\n\t// expecting root path to work\n\texpect(runtime.localizeHref(\"/\", { locale: \"en\" })).toBe(\"/\");\n\texpect(runtime.localizeHref(\"/\", { locale: \"de\" })).toBe(\"/de/\");\n\n\t// expecting de-localization to work\n\texpect(runtime.deLocalizeHref(\"/de/\")).toBe(\"/\");\n\texpect(runtime.deLocalizeHref(\"/de/about\")).toBe(\"/about\");\n\texpect(runtime.deLocalizeHref(\"/about\")).toBe(\"/about\");\n\n\t// expecting localhost to work (for development)\n\texpect(runtime.localizeHref(\"http://localhost:5173/\", { locale: \"de\" })).toBe(\n\t\t\"http://localhost:5173/de/\"\n\t);\n\n\t// de-localizing localhost\n\texpect(runtime.deLocalizeHref(\"http://localhost:5173/de\")).toBe(\n\t\t\"http://localhost:5173/\"\n\t);\n\n\t// pathnames\n\texpect(\n\t\truntime.localizeHref(\"http://localhost:5173/about\", { locale: \"de\" })\n\t).toBe(\"http://localhost:5173/de/about\");\n\n\texpect(runtime.deLocalizeHref(\"http://localhost:5173/de/about\")).toBe(\n\t\t\"http://localhost:5173/about\"\n\t);\n});\n\ntest(\"normalizes localized root hrefs and anchors\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tisServer: \"false\",\n\t\tstrategy: [\"url\"],\n\t\ttrailingSlash: \"never\",\n\t});\n\n\tglobalThis.window = { location: new URL(\"http://example.com\") } as any;\n\n\texpect(runtime.localizeHref(\"/\", { locale: \"de\" })).toBe(\"/de\");\n\texpect(runtime.localizeHref(\"/#section\", { locale: \"de\" })).toBe(\n\t\t\"/de#section\"\n\t);\n\texpect(runtime.deLocalizeHref(\"/de/\")).toBe(\"/\");\n});\n\ntest(\"normalizes mixed-case explicit locales in localizeHref\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\"],\n\t});\n\n\texpect(runtime.localizeHref(\"/about\", { locale: \"DE\" })).toBe(\"/de/about\");\n\n\texpect(runtime.localizeHref(\"/de/about\", { locale: \"EN\" })).toBe(\"/about\");\n});\n\ntest(\"keeps custom URLPattern path expressions on the generic fallback\", async () => {\n\tconst runtime = await createParaglide({\n\t\tblob: await newProject({\n\t\t\tsettings: {\n\t\t\t\tbaseLocale: \"en\",\n\t\t\t\tlocales: [\"en\", \"de\"],\n\t\t\t},\n\t\t}),\n\t\tstrategy: [\"url\"],\n\t\turlPatterns: [\n\t\t\t{\n\t\t\t\tpattern: \"/:slug(.)?\",\n\t\t\t\tlocalized: [\n\t\t\t\t\t[\"en\", \"/:slug(.)?\"],\n\t\t\t\t\t[\"de\", \"/de/:slug(.)?\"],\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t});\n\truntime.overwriteGetLocale(() => \"en\");\n\truntime.overwriteGetUrlOrigin(() => \"https://example.com\");\n\n\t// `(.)` matches exactly one character in URLPattern. A prefix-only\n\t// implementation must not broaden this route into a catch-all.\n\texpect(runtime.localizeHref(\"/a\", { locale: \"de\" })).toBe(\"/de/a\");\n\texpect(runtime.localizeHref(\"/about\", { locale: \"de\" })).toBe(\"/about\");\n});\n"]}
@@ -93,3 +93,17 @@ export declare function deLocalizeUrl(url: string | URL): URL;
93
93
  * @returns {Record<string, string | null | undefined>} An object containing all named groups from the match.
94
94
  */
95
95
  export declare function aggregateGroups(match: any): Record<string, string | null | undefined>;
96
+ export type FastPathPattern = {
97
+ protocol: string | undefined;
98
+ hostname: string | undefined;
99
+ port: string | undefined;
100
+ pathnamePrefix: string;
101
+ pathMode: "segments" | "catch-all-optional" | "catch-all-required";
102
+ };
103
+ export type FastPathRoute = {
104
+ base: FastPathPattern;
105
+ localized: Array<{
106
+ locale: string;
107
+ pattern: FastPathPattern;
108
+ }>;
109
+ };
@@ -2,7 +2,9 @@ import { extractLocaleFromUrl } from "./extract-locale-from-url.js";
2
2
  import { getLocale } from "./get-locale.js";
3
3
  import { getUrlOrigin } from "./get-url-origin.js";
4
4
  import { assertIsLocale, toLocale } from "./check-locale.js";
5
- import { baseLocale, TREE_SHAKE_DEFAULT_URL_PATTERN_USED, urlPatterns, } from "./variables.js";
5
+ import { baseLocale, TREE_SHAKE_DEFAULT_URL_PATTERN_USED, trailingSlash, urlPatterns, } from "./variables.js";
6
+ import { normalizeTrailingSlash } from "./normalize-trailing-slash.js";
7
+ import { execUrlPattern } from "./exec-url-pattern.js";
6
8
  /**
7
9
  * Lower-level URL localization function, primarily used in server contexts.
8
10
  *
@@ -54,12 +56,24 @@ export function localizeUrl(url, options) {
54
56
  if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
55
57
  return localizeUrlDefaultPattern(url, targetLocale);
56
58
  }
57
- const urlObj = typeof url === "string" ? new URL(url) : url;
59
+ const originalUrl = typeof url === "string" ? new URL(url) : url;
60
+ const urlObj = trailingSlash === undefined
61
+ ? originalUrl
62
+ : normalizeTrailingSlash(new URL(originalUrl));
58
63
  // Iterate over URL patterns
59
64
  for (const element of urlPatterns) {
65
+ // Most applications use a locale prefix (and sometimes a fixed domain)
66
+ // with a trailing catch-all path. Those routes do not need URLPattern's
67
+ // parser or matcher on every call. Keep the generic matcher below as the
68
+ // fallback for every other URLPattern shape. This stays inside the outer
69
+ // loop so specific patterns retain precedence over a later catch-all.
70
+ const fastPathLocalized = localizeUrlFastPath(urlObj, targetLocale, element);
71
+ if (fastPathLocalized !== undefined) {
72
+ return fastPathLocalized;
73
+ }
60
74
  // match localized patterns
61
75
  for (const [, localizedPattern] of element.localized) {
62
- const match = new URLPattern(localizedPattern, urlObj.href).exec(urlObj.href);
76
+ const match = execUrlPattern(getUrlPattern(localizedPattern, urlObj), urlObj);
63
77
  if (!match) {
64
78
  continue;
65
79
  }
@@ -68,19 +82,19 @@ export function localizeUrl(url, options) {
68
82
  continue;
69
83
  }
70
84
  const localizedUrl = fillPattern(targetPattern, aggregateGroups(match), urlObj.origin);
71
- return fillMissingUrlParts(localizedUrl, match);
85
+ return normalizeTrailingSlash(fillMissingUrlParts(localizedUrl, match));
72
86
  }
73
- const unlocalizedMatch = new URLPattern(element.pattern, urlObj.href).exec(urlObj.href);
87
+ const unlocalizedMatch = execUrlPattern(getUrlPattern(element.pattern, urlObj), urlObj);
74
88
  if (unlocalizedMatch) {
75
89
  const targetPattern = element.localized.find(([locale]) => locale === targetLocale)?.[1];
76
90
  if (targetPattern) {
77
91
  const localizedUrl = fillPattern(targetPattern, aggregateGroups(unlocalizedMatch), urlObj.origin);
78
- return fillMissingUrlParts(localizedUrl, unlocalizedMatch);
92
+ return normalizeTrailingSlash(fillMissingUrlParts(localizedUrl, unlocalizedMatch));
79
93
  }
80
94
  }
81
95
  }
82
96
  // If no match found, return the original URL
83
- return urlObj;
97
+ return originalUrl;
84
98
  }
85
99
  /**
86
100
  * https://github.com/opral/inlang-paraglide-js/issues/381
@@ -90,11 +104,11 @@ export function localizeUrl(url, options) {
90
104
  * @returns {URL}
91
105
  */
92
106
  function localizeUrlDefaultPattern(url, locale) {
93
- const urlObj = typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url);
107
+ const urlObj = normalizeTrailingSlash(typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url));
94
108
  const currentLocale = extractLocaleFromUrl(urlObj);
95
109
  // If current locale matches target locale, no change needed
96
110
  if (currentLocale === locale) {
97
- return urlObj;
111
+ return normalizeTrailingSlash(urlObj);
98
112
  }
99
113
  const pathSegments = urlObj.pathname.split("/").filter(Boolean);
100
114
  // If current path starts with a locale, remove it
@@ -109,7 +123,7 @@ function localizeUrlDefaultPattern(url, locale) {
109
123
  // For other locales, add prefix
110
124
  urlObj.pathname = "/" + locale + "/" + pathSegments.join("/");
111
125
  }
112
- return urlObj;
126
+ return normalizeTrailingSlash(urlObj);
113
127
  }
114
128
  /**
115
129
  * Low-level URL de-localization function, primarily used in server contexts.
@@ -154,28 +168,35 @@ export function deLocalizeUrl(url) {
154
168
  if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
155
169
  return deLocalizeUrlDefaultPattern(url);
156
170
  }
157
- const urlObj = typeof url === "string" ? new URL(url) : url;
171
+ const originalUrl = typeof url === "string" ? new URL(url) : url;
172
+ const urlObj = trailingSlash === undefined
173
+ ? originalUrl
174
+ : normalizeTrailingSlash(new URL(originalUrl));
158
175
  // Iterate over URL patterns
159
176
  for (const element of urlPatterns) {
177
+ const fastPathDeLocalized = deLocalizeUrlFastPath(urlObj, element);
178
+ if (fastPathDeLocalized !== undefined) {
179
+ return fastPathDeLocalized;
180
+ }
160
181
  // Iterate over localized versions
161
182
  for (const [, localizedPattern] of element.localized) {
162
- const match = new URLPattern(localizedPattern, urlObj.href).exec(urlObj.href);
183
+ const match = execUrlPattern(getUrlPattern(localizedPattern, urlObj), urlObj);
163
184
  if (match) {
164
185
  // Convert localized URL back to the base pattern
165
186
  const groups = aggregateGroups(match);
166
187
  const baseUrl = fillPattern(element.pattern, groups, urlObj.origin);
167
- return fillMissingUrlParts(baseUrl, match);
188
+ return normalizeTrailingSlash(fillMissingUrlParts(baseUrl, match));
168
189
  }
169
190
  }
170
191
  // match unlocalized pattern
171
- const unlocalizedMatch = new URLPattern(element.pattern, urlObj.href).exec(urlObj.href);
192
+ const unlocalizedMatch = execUrlPattern(getUrlPattern(element.pattern, urlObj), urlObj);
172
193
  if (unlocalizedMatch) {
173
194
  const baseUrl = fillPattern(element.pattern, aggregateGroups(unlocalizedMatch), urlObj.origin);
174
- return fillMissingUrlParts(baseUrl, unlocalizedMatch);
195
+ return normalizeTrailingSlash(fillMissingUrlParts(baseUrl, unlocalizedMatch));
175
196
  }
176
197
  }
177
198
  // no match found return the original url
178
- return urlObj;
199
+ return originalUrl;
179
200
  }
180
201
  /**
181
202
  * De-localizes a URL using the default pattern (/:locale/*)
@@ -183,13 +204,13 @@ export function deLocalizeUrl(url) {
183
204
  * @returns {URL}
184
205
  */
185
206
  function deLocalizeUrlDefaultPattern(url) {
186
- const urlObj = typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url);
207
+ const urlObj = normalizeTrailingSlash(typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url));
187
208
  const pathSegments = urlObj.pathname.split("/").filter(Boolean);
188
209
  // If first segment is a locale, remove it
189
210
  if (pathSegments.length > 0 && toLocale(pathSegments[0])) {
190
211
  urlObj.pathname = "/" + pathSegments.slice(1).join("/");
191
212
  }
192
- return urlObj;
213
+ return normalizeTrailingSlash(urlObj);
193
214
  }
194
215
  /**
195
216
  * Takes matches of implicit wildcards in the UrlPattern (when a part is missing
@@ -314,4 +335,341 @@ export function aggregateGroups(match) {
314
335
  ...match.username.groups,
315
336
  };
316
337
  }
338
+ /** @type {Map<string, URLPattern>} */
339
+ const urlPatternCache = new Map();
340
+ const URL_PATTERN_CACHE_LIMIT = 128;
341
+ const ABSOLUTE_URL_PATTERN = /^(?:[A-Za-z][A-Za-z\d+.-]*|:[A-Za-z][A-Za-z\d_-]*):\/\//;
342
+ /**
343
+ * URLPattern's base URL affects relative patterns. Absolute patterns only
344
+ * depend on the pattern itself, while root-relative patterns also depend on
345
+ * the URL origin. Other relative patterns are deliberately not cached because
346
+ * their semantics depend on the complete base URL.
347
+ *
348
+ * @param {string} pattern
349
+ * @param {URL} url
350
+ * @returns {URLPattern}
351
+ */
352
+ function getUrlPattern(pattern, url) {
353
+ const isAbsolutePattern = ABSOLUTE_URL_PATTERN.test(pattern);
354
+ const isRootRelativePattern = pattern.startsWith("/");
355
+ if (!isAbsolutePattern && !isRootRelativePattern) {
356
+ return new URLPattern(pattern, url.href);
357
+ }
358
+ const key = isAbsolutePattern
359
+ ? pattern
360
+ : JSON.stringify([url.origin, pattern]);
361
+ const cached = urlPatternCache.get(key);
362
+ if (cached !== undefined) {
363
+ // Refresh the entry so frequently used patterns stay in the bounded cache.
364
+ urlPatternCache.delete(key);
365
+ urlPatternCache.set(key, cached);
366
+ return cached;
367
+ }
368
+ const compiled = new URLPattern(pattern, url.href);
369
+ if (urlPatternCache.size >= URL_PATTERN_CACHE_LIMIT) {
370
+ const oldestKey = urlPatternCache.keys().next().value;
371
+ if (oldestKey !== undefined)
372
+ urlPatternCache.delete(oldestKey);
373
+ }
374
+ urlPatternCache.set(key, compiled);
375
+ return compiled;
376
+ }
377
+ /**
378
+ * A small, deliberately conservative subset of URLPattern routing.
379
+ *
380
+ * The compiler emits routes such as `/:path(.*)?`, `/de/:path*`, or
381
+ * `https://example.com/:path*`. For those routes matching is equivalent to a
382
+ * pathname prefix check and (optionally) an origin check. Everything that has
383
+ * a dynamic host, a custom path regexp, or another URLPattern modifier keeps
384
+ * using the generic implementation above.
385
+ *
386
+ * @typedef {{
387
+ * protocol: string | undefined;
388
+ * hostname: string | undefined;
389
+ * port: string | undefined;
390
+ * pathnamePrefix: string;
391
+ * pathMode: "segments" | "catch-all-optional" | "catch-all-required";
392
+ * }} FastPathPattern
393
+ * @typedef {{
394
+ * base: FastPathPattern;
395
+ * localized: Array<{ locale: string; pattern: FastPathPattern }>;
396
+ * }} FastPathRoute
397
+ */
398
+ /** @type {WeakMap<object, FastPathRoute | null>} */
399
+ const fastPathRouteCache = new WeakMap();
400
+ /**
401
+ * @param {URL} urlObj
402
+ * @param {string} targetLocale
403
+ * @param {{ pattern: string; localized: Array<[string, string]> }} element
404
+ * @returns {URL | undefined}
405
+ */
406
+ function localizeUrlFastPath(urlObj, targetLocale, element) {
407
+ const route = getFastPathRoute(element);
408
+ if (route === null)
409
+ return undefined;
410
+ // Preserve URLPattern's ordering: localized patterns are checked before
411
+ // the unlocalized pattern, and the first matching pattern wins.
412
+ for (const localized of route.localized) {
413
+ const suffix = matchFastPathPattern(localized.pattern, urlObj);
414
+ if (suffix === undefined)
415
+ continue;
416
+ const target = route.localized.find((candidate) => candidate.locale === targetLocale)?.pattern;
417
+ if (target === undefined)
418
+ continue;
419
+ return applyFastPathPattern(target, suffix, urlObj);
420
+ }
421
+ const suffix = matchFastPathPattern(route.base, urlObj);
422
+ if (suffix !== undefined) {
423
+ const target = route.localized.find((candidate) => candidate.locale === targetLocale)?.pattern;
424
+ if (target !== undefined) {
425
+ return applyFastPathPattern(target, suffix, urlObj);
426
+ }
427
+ }
428
+ return undefined;
429
+ }
430
+ /**
431
+ * @param {URL} urlObj
432
+ * @param {{ pattern: string; localized: Array<[string, string]> }} element
433
+ * @returns {URL | undefined}
434
+ */
435
+ function deLocalizeUrlFastPath(urlObj, element) {
436
+ const route = getFastPathRoute(element);
437
+ if (route === null)
438
+ return undefined;
439
+ for (const localized of route.localized) {
440
+ const suffix = matchFastPathPattern(localized.pattern, urlObj);
441
+ if (suffix !== undefined) {
442
+ return applyFastPathPattern(route.base, suffix, urlObj);
443
+ }
444
+ }
445
+ const suffix = matchFastPathPattern(route.base, urlObj);
446
+ if (suffix !== undefined) {
447
+ return applyFastPathPattern(route.base, suffix, urlObj);
448
+ }
449
+ return undefined;
450
+ }
451
+ /**
452
+ * @param {{ pattern: string; localized: Array<[string, string]> }} element
453
+ * @returns {FastPathRoute | null}
454
+ */
455
+ function getFastPathRoute(element) {
456
+ const cached = fastPathRouteCache.get(element);
457
+ if (cached !== undefined)
458
+ return cached;
459
+ const base = parseFastPathPattern(element.pattern);
460
+ if (base === undefined) {
461
+ fastPathRouteCache.set(element, null);
462
+ return null;
463
+ }
464
+ const localized = [];
465
+ for (const [locale, pattern] of element.localized) {
466
+ const parsed = parseFastPathPattern(pattern);
467
+ if (parsed === undefined || parsed.pathMode !== base.pathMode) {
468
+ fastPathRouteCache.set(element, null);
469
+ return null;
470
+ }
471
+ localized.push({ locale, pattern: parsed });
472
+ }
473
+ const route = { base, localized };
474
+ fastPathRouteCache.set(element, route);
475
+ return route;
476
+ }
477
+ /**
478
+ * Parse only catch-all path patterns. In particular, do not treat `:path(.)?`
479
+ * as a catch-all: URLPattern's `(.)` means exactly one character and cannot be
480
+ * represented by this prefix matcher without changing routing semantics.
481
+ *
482
+ * @param {string} pattern
483
+ * @returns {FastPathPattern | undefined}
484
+ */
485
+ function parseFastPathPattern(pattern) {
486
+ const wildcard = pattern.match(/\/:path(?:\(\.\*\)(?:\?)?|\*)$/);
487
+ if (wildcard === null || wildcard.index === undefined)
488
+ return undefined;
489
+ const prefix = pattern.slice(0, wildcard.index);
490
+ const originAndPath = parseFastPathOriginAndPath(prefix);
491
+ if (originAndPath === undefined)
492
+ return undefined;
493
+ const pathMode = wildcard[0].endsWith("*")
494
+ ? "segments"
495
+ : wildcard[0].endsWith("?")
496
+ ? "catch-all-optional"
497
+ : "catch-all-required";
498
+ return { ...originAndPath, pathMode };
499
+ }
500
+ /**
501
+ * @param {string} prefix
502
+ * @returns {FastPathPattern | undefined}
503
+ */
504
+ function parseFastPathOriginAndPath(prefix) {
505
+ if (prefix === "" || prefix.startsWith("/")) {
506
+ if (hasUrlPatternSyntax(prefix))
507
+ return undefined;
508
+ return {
509
+ protocol: undefined,
510
+ hostname: undefined,
511
+ port: undefined,
512
+ pathnamePrefix: normalizePathPrefix(prefix),
513
+ pathMode: "catch-all-optional",
514
+ };
515
+ }
516
+ // A `:protocol://host` pattern is common in generated configurations. The
517
+ // protocol is intentionally left unconstrained, just like URLPattern.
518
+ const dynamicProtocol = prefix.match(/^:protocol:\/\/([^/]+)(\/.*)?$/);
519
+ const staticOrigin = prefix.match(/^([A-Za-z][A-Za-z\d+.-]*):\/\/([^/]+)(\/.*)?$/);
520
+ if (dynamicProtocol !== null) {
521
+ const dynamicHost = dynamicProtocol[1] ?? "";
522
+ const hostMatch = dynamicHost.match(/^([^:(){}?*+]+)(?::(\d+))?$/);
523
+ if (hostMatch === null ||
524
+ (dynamicProtocol[2] !== undefined &&
525
+ hasUrlPatternSyntax(dynamicProtocol[2]))) {
526
+ return undefined;
527
+ }
528
+ return {
529
+ protocol: undefined,
530
+ hostname: (hostMatch[1] ?? "").toLowerCase(),
531
+ port: hostMatch[2],
532
+ pathnamePrefix: normalizePathPrefix(dynamicProtocol[2] ?? ""),
533
+ pathMode: "catch-all-optional",
534
+ };
535
+ }
536
+ if (staticOrigin === null)
537
+ return undefined;
538
+ const host = staticOrigin[2] ?? "";
539
+ const pathname = staticOrigin[3];
540
+ const hostMatch = host.match(/^([^:(){}?*+]+)(?::(\d+))?$/);
541
+ if (hostMatch === null ||
542
+ (pathname !== undefined && hasUrlPatternSyntax(pathname))) {
543
+ return undefined;
544
+ }
545
+ return {
546
+ protocol: `${staticOrigin[1]}:`,
547
+ hostname: (hostMatch[1] ?? "").toLowerCase(),
548
+ port: normalizePatternPort(hostMatch[2], `${staticOrigin[1]}:`),
549
+ pathnamePrefix: normalizePathPrefix(pathname ?? ""),
550
+ pathMode: "catch-all-optional",
551
+ };
552
+ }
553
+ /**
554
+ * @param {string | undefined} port
555
+ * @param {string} protocol
556
+ * @returns {string | undefined}
557
+ */
558
+ function normalizePatternPort(port, protocol) {
559
+ if (port === "80" && protocol === "http:")
560
+ return "";
561
+ if (port === "443" && protocol === "https:")
562
+ return "";
563
+ return port;
564
+ }
565
+ /**
566
+ * @param {string} value
567
+ * @returns {boolean}
568
+ */
569
+ function hasUrlPatternSyntax(value) {
570
+ return /[:(){}?*+]/.test(value);
571
+ }
572
+ /**
573
+ * @param {string} pathname
574
+ * @returns {string}
575
+ */
576
+ function normalizePathPrefix(pathname) {
577
+ if (pathname === "" || pathname === "/")
578
+ return "/";
579
+ return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
580
+ }
581
+ /**
582
+ * @param {FastPathPattern} pattern
583
+ * @param {URL} urlObj
584
+ * @returns {string | undefined}
585
+ */
586
+ function matchFastPathPattern(pattern, urlObj) {
587
+ if (pattern.protocol !== undefined && pattern.protocol !== urlObj.protocol) {
588
+ return undefined;
589
+ }
590
+ if (pattern.hostname !== undefined &&
591
+ pattern.hostname !== urlObj.hostname.toLowerCase()) {
592
+ return undefined;
593
+ }
594
+ if (pattern.hostname !== undefined) {
595
+ const expectedPort = pattern.port ?? defaultPortForProtocol(pattern.protocol ?? urlObj.protocol);
596
+ if (expectedPort !== urlObj.port)
597
+ return undefined;
598
+ }
599
+ const prefix = pattern.pathnamePrefix;
600
+ if (prefix === "/") {
601
+ if (urlObj.pathname.startsWith("//"))
602
+ return undefined;
603
+ if (pattern.pathMode === "segments") {
604
+ return isNonEmptyPathSegments(urlObj.pathname)
605
+ ? urlObj.pathname
606
+ : undefined;
607
+ }
608
+ return urlObj.pathname;
609
+ }
610
+ if (urlObj.pathname === prefix) {
611
+ return pattern.pathMode === "catch-all-required" ? undefined : "";
612
+ }
613
+ if (urlObj.pathname.startsWith(`${prefix}/`)) {
614
+ const suffix = urlObj.pathname.slice(prefix.length);
615
+ if (suffix.startsWith("//"))
616
+ return undefined;
617
+ if (pattern.pathMode === "segments" &&
618
+ !isNonEmptyPathSegments(suffix)) {
619
+ return undefined;
620
+ }
621
+ return suffix;
622
+ }
623
+ return undefined;
624
+ }
625
+ /**
626
+ * @param {string} pathname
627
+ * @returns {boolean}
628
+ */
629
+ function isNonEmptyPathSegments(pathname) {
630
+ return pathname.length > 1 && !pathname.endsWith("/") && !pathname.includes("//");
631
+ }
632
+ /**
633
+ * URLPattern treats the default port as empty in URL instances.
634
+ *
635
+ * @param {string} protocol
636
+ * @returns {string}
637
+ */
638
+ function defaultPortForProtocol(protocol) {
639
+ if (protocol === "http:")
640
+ return "";
641
+ if (protocol === "https:")
642
+ return "";
643
+ return "";
644
+ }
645
+ /**
646
+ * @param {FastPathPattern} pattern
647
+ * @param {string} suffix
648
+ * @param {URL} source
649
+ * @returns {URL}
650
+ */
651
+ function applyFastPathPattern(pattern, suffix, source) {
652
+ const localized = new URL(source.href);
653
+ if (pattern.protocol !== undefined)
654
+ localized.protocol = pattern.protocol;
655
+ if (pattern.hostname !== undefined) {
656
+ localized.hostname = pattern.hostname;
657
+ localized.port =
658
+ pattern.port ?? defaultPortForProtocol(localized.protocol);
659
+ }
660
+ localized.pathname = joinFastPathPrefix(pattern.pathnamePrefix, suffix);
661
+ return localized;
662
+ }
663
+ /**
664
+ * @param {string} prefix
665
+ * @param {string} suffix
666
+ * @returns {string}
667
+ */
668
+ function joinFastPathPrefix(prefix, suffix) {
669
+ if (prefix === "/")
670
+ return suffix === "" ? "/" : suffix;
671
+ if (suffix === "")
672
+ return prefix;
673
+ return `${prefix}${suffix.startsWith("/") ? suffix : `/${suffix}`}`;
674
+ }
317
675
  //# sourceMappingURL=localize-url.js.map