vite-intlayer 9.5.4 → 9.5.6
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 +2 -2
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/intlayerNitroHandler.mjs +18 -82
- package/dist/esm/intlayerNitroHandler.mjs.map +1 -1
- package/dist/esm/intlayerProxyHandler.mjs +14 -2
- package/dist/esm/intlayerProxyHandler.mjs.map +1 -1
- package/dist/esm/intlayerProxyPlugin.mjs +87 -25
- package/dist/esm/intlayerProxyPlugin.mjs.map +1 -1
- package/dist/esm/intlayerWebProxyHandler.mjs +113 -0
- package/dist/esm/intlayerWebProxyHandler.mjs.map +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/intlayerNitroHandler.d.ts +3 -8
- package/dist/types/intlayerNitroHandler.d.ts.map +1 -1
- package/dist/types/intlayerProxyHandler.d.ts +12 -0
- package/dist/types/intlayerProxyHandler.d.ts.map +1 -1
- package/dist/types/intlayerProxyPlugin.d.ts +24 -5
- package/dist/types/intlayerProxyPlugin.d.ts.map +1 -1
- package/dist/types/intlayerWebProxyHandler.d.ts +66 -0
- package/dist/types/intlayerWebProxyHandler.d.ts.map +1 -0
- package/package.json +14 -9
package/README.md
CHANGED
|
@@ -188,7 +188,8 @@ Explore our comprehensive documentation to get started with Intlayer and learn h
|
|
|
188
188
|
<li><a href="https://intlayer.org/doc/environment/create-react-app" rel=''>React CRA</a></li>
|
|
189
189
|
<li><a href="https://intlayer.org/doc/environment/vite-and-react" rel=''>Vite + React</a></li>
|
|
190
190
|
<li><a href="https://intlayer.org/doc/environment/vite-and-react" rel=''>Vite + React using Compiler</a></li>
|
|
191
|
-
<li><a href="https://intlayer.org/doc/environment/vite-and-react/
|
|
191
|
+
<li><a href="https://intlayer.org/doc/environment/vite-and-react/react-router-v7" rel=''>React-router-v7</a></li>
|
|
192
|
+
<li><a href="https://intlayer.org/doc/environment/remix-3" rel=''>Remix 3</a></li>
|
|
192
193
|
<li><a href="https://intlayer.org/doc/environment/tanstack-start" rel=''>Tanstack start</a>
|
|
193
194
|
<ul>
|
|
194
195
|
<li><a href="https://intlayer.org/doc/environment/tanstack-start/solid" rel=''>Solid</a></li>
|
|
@@ -200,7 +201,6 @@ Explore our comprehensive documentation to get started with Intlayer and learn h
|
|
|
200
201
|
<li><a href="https://intlayer.org/doc/environment/astro/vue" rel=''>Vue</a></li>
|
|
201
202
|
<li><a href="https://intlayer.org/doc/environment/astro/svelte" rel=''>Svelte</a></li>
|
|
202
203
|
<li><a href="https://intlayer.org/doc/environment/astro/solid" rel=''>Solid</a></li>
|
|
203
|
-
<li><a href="https://intlayer.org/doc/environment/astro/vanilla" rel=''>Vanilla JS</a></li>
|
|
204
204
|
<li><a href="https://intlayer.org/doc/environment/astro/lit" rel=''>Lit</a></li>
|
|
205
205
|
</ul>
|
|
206
206
|
</li>
|
package/dist/esm/index.mjs
CHANGED
|
@@ -3,8 +3,8 @@ import { createDictionaryModuleParser, intlayerChunk, resolveBoundaries, toBound
|
|
|
3
3
|
import { intlayerMinify } from "./intlayerMinifyPlugin.mjs";
|
|
4
4
|
import { intlayerOptimize } from "./intlayerOptimizePlugin.mjs";
|
|
5
5
|
import { intlayerPreload } from "./intlayerPreloadPlugin.mjs";
|
|
6
|
-
import { createIntlayerProxyHandler, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy } from "./intlayerProxyPlugin.mjs";
|
|
6
|
+
import { createIntlayerProxyHandler, createRedirectOnlyMiddleware, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy, resolveNitroHandlerPath } from "./intlayerProxyPlugin.mjs";
|
|
7
7
|
import { intlayerPrune } from "./intlayerPrunePlugin.mjs";
|
|
8
8
|
import { INTLAYER_NO_EXTERNAL_PATTERN, intLayerPlugin, intlayer, intlayerPlugin } from "./intlayerPlugin.mjs";
|
|
9
9
|
|
|
10
|
-
export { INTLAYER_NO_EXTERNAL_PATTERN, createDictionaryModuleParser, createIntlayerProxyHandler, intLayerMiddlewarePlugin, intLayerPlugin, intlayer, intlayerChunk, intlayerCompiler, intlayerMiddleware, intlayerMinify, intlayerOptimize, intlayerPlugin, intlayerPreload, intlayerProxy, intlayerPrune, resolveBoundaries, toBoundaryName };
|
|
10
|
+
export { INTLAYER_NO_EXTERNAL_PATTERN, createDictionaryModuleParser, createIntlayerProxyHandler, createRedirectOnlyMiddleware, intLayerMiddlewarePlugin, intLayerPlugin, intlayer, intlayerChunk, intlayerCompiler, intlayerMiddleware, intlayerMinify, intlayerOptimize, intlayerPlugin, intlayerPreload, intlayerProxy, intlayerPrune, resolveBoundaries, resolveNitroHandlerPath, toBoundaryName };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createProxyHandler } from "./intlayerProxyHandler.mjs";
|
|
1
|
+
import { createProxyHandler, isPrerenderProcess } from "./intlayerProxyHandler.mjs";
|
|
2
|
+
import { runProxyOnWebRequest } from "./intlayerWebProxyHandler.mjs";
|
|
2
3
|
import { getAppLogger } from "@intlayer/config/logger";
|
|
3
4
|
import * as builtConfiguration from "@intlayer/config/built";
|
|
4
5
|
import { formatProxyEnabledMessage } from "@intlayer/core/localization";
|
|
@@ -15,7 +16,8 @@ import { formatProxyEnabledMessage } from "@intlayer/core/localization";
|
|
|
15
16
|
* it - which is exactly what a Nitro deployment output is.
|
|
16
17
|
*/
|
|
17
18
|
const intlayerConfig = builtConfiguration;
|
|
18
|
-
getAppLogger(intlayerConfig)
|
|
19
|
+
const logger = getAppLogger(intlayerConfig);
|
|
20
|
+
if (!isPrerenderProcess()) logger(formatProxyEnabledMessage(false), { level: "info" });
|
|
19
21
|
const nodeMiddleware = createProxyHandler({ configuration: intlayerConfig });
|
|
20
22
|
/**
|
|
21
23
|
* Native h3 v2 event handler for Nitro production servers (TanStack Start, Nuxt, etc.).
|
|
@@ -25,8 +27,8 @@ const nodeMiddleware = createProxyHandler({ configuration: intlayerConfig });
|
|
|
25
27
|
* and Deno — where `event.node` is `undefined` and `fromNodeMiddleware` crashes with
|
|
26
28
|
* "undefined is not an object (evaluating 'event.node.req')".
|
|
27
29
|
*
|
|
28
|
-
* It bridges h3 v2 events to the Node.js-style proxy middleware
|
|
29
|
-
*
|
|
30
|
+
* It bridges h3 v2 events to the Node.js-style proxy middleware through
|
|
31
|
+
* `runProxyOnWebRequest`:
|
|
30
32
|
*
|
|
31
33
|
* - **Redirect** (301 / 5xx): builds a Web API `Response` and returns it — Nitro sends
|
|
32
34
|
* the correct HTTP response to the browser.
|
|
@@ -36,85 +38,19 @@ const nodeMiddleware = createProxyHandler({ configuration: intlayerConfig });
|
|
|
36
38
|
* - **Pass-through** (`next()`, URL unchanged): returns `undefined` — Nitro proceeds to
|
|
37
39
|
* the next handler / route.
|
|
38
40
|
*/
|
|
39
|
-
var intlayerNitroHandler_default =
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
*
|
|
44
|
-
* Only the fields actually read by the proxy middleware are populated:
|
|
45
|
-
* - url : the current pathname + search, modified for rewrites
|
|
46
|
-
* - headers.cookie : locale cookie detection
|
|
47
|
-
* - headers.host : domain-based locale routing
|
|
48
|
-
* - headers.accept-language : browser Accept-Language fallback
|
|
49
|
-
* - headers.x-forwarded-* : forwarded host/proto for reverse-proxy setups
|
|
50
|
-
* - headers.x-nitro-prerender : marks a request issued by Nitro's
|
|
51
|
-
* prerenderer, which keeps the generated page URL-driven
|
|
52
|
-
*
|
|
53
|
-
* headers must be a mutable plain object because setLocaleInStorageServer
|
|
54
|
-
* writes Set-Cookie back via req.headers[name] = value.
|
|
55
|
-
*/
|
|
56
|
-
const fakeReq = {
|
|
57
|
-
url: initialPath,
|
|
58
|
-
method: "GET",
|
|
59
|
-
headers: {
|
|
60
|
-
cookie: event.headers.get("cookie") ?? "",
|
|
61
|
-
host: event.headers.get("host") ?? "",
|
|
62
|
-
"accept-language": event.headers.get("accept-language") ?? "",
|
|
63
|
-
"x-forwarded-host": event.headers.get("x-forwarded-host") ?? "",
|
|
64
|
-
"x-forwarded-proto": event.headers.get("x-forwarded-proto") ?? "",
|
|
65
|
-
"x-nitro-prerender": event.headers.get("x-nitro-prerender") ?? ""
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
let responseStatusCode = 200;
|
|
69
|
-
const accumulatedHeaders = {};
|
|
70
|
-
/**
|
|
71
|
-
* Minimal ServerResponse shim.
|
|
72
|
-
*
|
|
73
|
-
* Implements only the methods that the proxy middleware invokes:
|
|
74
|
-
* writeHead() — status + Location header for 301 redirects
|
|
75
|
-
* setHeader() — Set-Cookie written by setLocaleInStorageServer
|
|
76
|
-
* getHeader() — defensive read-back (not strictly required but safe)
|
|
77
|
-
* end() — finalises the response; for redirects this returns a
|
|
78
|
-
* Web API Response object that Nitro sends to the client
|
|
79
|
-
*/
|
|
80
|
-
const fakeRes = {
|
|
81
|
-
writeHead(statusCode, headersArg) {
|
|
82
|
-
responseStatusCode = statusCode;
|
|
83
|
-
if (headersArg && typeof headersArg === "object") for (const [key, value] of Object.entries(headersArg)) accumulatedHeaders[key.toLowerCase()] = Array.isArray(value) ? value[0] ?? "" : String(value);
|
|
84
|
-
return fakeRes;
|
|
85
|
-
},
|
|
86
|
-
setHeader(name, value) {
|
|
87
|
-
accumulatedHeaders[name.toLowerCase()] = Array.isArray(value) ? value[0] ?? "" : String(value);
|
|
88
|
-
return fakeRes;
|
|
89
|
-
},
|
|
90
|
-
getHeader(name) {
|
|
91
|
-
return accumulatedHeaders[name.toLowerCase()];
|
|
92
|
-
},
|
|
93
|
-
getHeaders() {
|
|
94
|
-
return { ...accumulatedHeaders };
|
|
95
|
-
},
|
|
96
|
-
end(body) {
|
|
97
|
-
const webHeaders = new Headers();
|
|
98
|
-
for (const [key, value] of Object.entries(accumulatedHeaders)) webHeaders.set(key, value);
|
|
99
|
-
resolve(new Response(responseStatusCode >= 300 && responseStatusCode < 400 ? null : typeof body === "string" ? body : null, {
|
|
100
|
-
status: responseStatusCode,
|
|
101
|
-
headers: webHeaders
|
|
102
|
-
}));
|
|
103
|
-
return fakeRes;
|
|
104
|
-
},
|
|
105
|
-
headersSent: false
|
|
106
|
-
};
|
|
107
|
-
nodeMiddleware(fakeReq, fakeRes, () => {
|
|
108
|
-
const rewrittenPath = fakeReq.url;
|
|
109
|
-
if (rewrittenPath !== initialPath) try {
|
|
110
|
-
event.url = new URL(rewrittenPath, event.url.origin);
|
|
111
|
-
} catch {
|
|
112
|
-
console.error("[intlayer-proxy] URL rewrite failed — invalid path:", rewrittenPath);
|
|
113
|
-
}
|
|
114
|
-
if (Object.keys(accumulatedHeaders).length > 0) for (const [key, value] of Object.entries(accumulatedHeaders)) event.res.headers.set(key, value);
|
|
115
|
-
resolve(void 0);
|
|
41
|
+
var intlayerNitroHandler_default = (event) => {
|
|
42
|
+
const outcome = runProxyOnWebRequest(nodeMiddleware, {
|
|
43
|
+
url: event.url,
|
|
44
|
+
headers: event.headers
|
|
116
45
|
});
|
|
117
|
-
|
|
46
|
+
if (outcome.kind === "response") return outcome.response;
|
|
47
|
+
if (outcome.rewrittenPath) try {
|
|
48
|
+
event.url = new URL(outcome.rewrittenPath, event.url.origin);
|
|
49
|
+
} catch {
|
|
50
|
+
console.error("[intlayer-proxy] URL rewrite failed — invalid path:", outcome.rewrittenPath);
|
|
51
|
+
}
|
|
52
|
+
for (const [name, value] of outcome.headers) event.res.headers.append(name, value);
|
|
53
|
+
};
|
|
118
54
|
|
|
119
55
|
//#endregion
|
|
120
56
|
export { intlayerNitroHandler_default as default };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerNitroHandler.mjs","names":[],"sources":["../../src/intlayerNitroHandler.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\n/**\n * The configuration as a plain JSON object.\n *\n * Bundlers alias `@intlayer/config/built` to the generated\n * `.intlayer/config/configuration.mjs`, which inlines the resolved\n * configuration at build time. Loading it through `@intlayer/config/node`\n * instead would transpile `intlayer.config.ts` with `esbuild` on every server\n * start, making the built server unable to boot without `node_modules` next to\n * it - which is exactly what a Nitro deployment output is.\n */\nimport * as builtConfiguration from '@intlayer/config/built';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport { formatProxyEnabledMessage } from '@intlayer/core/localization';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createProxyHandler } from './intlayerProxyHandler';\n\n/**\n * Minimal duck-type for h3 v2's H3Event.\n *\n * We intentionally avoid importing from 'h3' to keep this file runtime-agnostic —\n * Nitro bundles h3 internally and provides the populated event at runtime. Using a\n * structural type here means the file compiles without h3 in devDependencies and\n * works with any h3 v2-compatible runtime (Bun, Deno, Node).\n */\ntype H3EventLike = {\n /**\n * pathname + search — a computed getter on H3Event:\n * `return this.url.pathname + this.url.search`\n */\n readonly path: string;\n /**\n * Full URL object — a **plain property** (not a getter) on H3Event, safe to\n * replace for internal URL rewrites. After assignment, `event.path` will\n * automatically reflect the new pathname + search via the getter.\n */\n url: URL;\n /**\n * Web Fetch API Headers — always populated in h3 v2 regardless of preset\n * (Node, Bun, Deno). Use `.get(name)` instead of bracket-access.\n */\n readonly headers: Headers;\n /**\n * Lazy response object — created on first access; its `headers` carry outgoing\n * response headers (e.g. Set-Cookie) that h3 merges into the HTTP response.\n */\n readonly res: {\n readonly headers: Headers;\n };\n};\n\nconst intlayerConfig = builtConfiguration as unknown as IntlayerConfig;\nconst logger = getAppLogger(intlayerConfig);\n// A Nitro server is a production server, so the stored locale always drives\n// redirects here — hence the `false`. This runs once per server process; the\n// Vite preview server deliberately does not add a second proxy layer on top of\n// this one (see `configurePreviewServer` in `intlayerProxyPlugin`).\nlogger(formatProxyEnabledMessage(false), {\n level: 'info',\n});\n\nconst nodeMiddleware = createProxyHandler({ configuration: intlayerConfig });\n\n/**\n * Native h3 v2 event handler for Nitro production servers (TanStack Start, Nuxt, etc.).\n *\n * Unlike `fromNodeMiddleware` (h3 v1 API), this handler uses the Web Fetch API event\n * model exclusively and is therefore compatible with ALL Nitro presets — including Bun\n * and Deno — where `event.node` is `undefined` and `fromNodeMiddleware` crashes with\n * \"undefined is not an object (evaluating 'event.node.req')\".\n *\n * It bridges h3 v2 events to the Node.js-style proxy middleware\n * via lightweight IncomingMessage / ServerResponse shims:\n *\n * - **Redirect** (301 / 5xx): builds a Web API `Response` and returns it — Nitro sends\n * the correct HTTP response to the browser.\n * - **Rewrite** (`next()` + modified `req.url`): replaces `event.url` with the rewritten\n * URL so `event.path` (a getter) returns the new pathname for downstream handlers and\n * the Nitro router.\n * - **Pass-through** (`next()`, URL unchanged): returns `undefined` — Nitro proceeds to\n * the next handler / route.\n */\nexport default async (event: H3EventLike): Promise<Response | void> =>\n new Promise<Response | void>((resolve) => {\n const initialPath = event.path;\n\n /**\n * Minimal IncomingMessage shim.\n *\n * Only the fields actually read by the proxy middleware are populated:\n * - url : the current pathname + search, modified for rewrites\n * - headers.cookie : locale cookie detection\n * - headers.host : domain-based locale routing\n * - headers.accept-language : browser Accept-Language fallback\n * - headers.x-forwarded-* : forwarded host/proto for reverse-proxy setups\n * - headers.x-nitro-prerender : marks a request issued by Nitro's\n * prerenderer, which keeps the generated page URL-driven\n *\n * headers must be a mutable plain object because setLocaleInStorageServer\n * writes Set-Cookie back via req.headers[name] = value.\n */\n const fakeReq = {\n url: initialPath,\n method: 'GET',\n headers: {\n cookie: event.headers.get('cookie') ?? '',\n host: event.headers.get('host') ?? '',\n 'accept-language': event.headers.get('accept-language') ?? '',\n 'x-forwarded-host': event.headers.get('x-forwarded-host') ?? '',\n 'x-forwarded-proto': event.headers.get('x-forwarded-proto') ?? '',\n 'x-nitro-prerender': event.headers.get('x-nitro-prerender') ?? '',\n } as Record<string, string>,\n } as unknown as IncomingMessage;\n\n let responseStatusCode = 200;\n const accumulatedHeaders: Record<string, string> = {};\n\n /**\n * Minimal ServerResponse shim.\n *\n * Implements only the methods that the proxy middleware invokes:\n * writeHead() — status + Location header for 301 redirects\n * setHeader() — Set-Cookie written by setLocaleInStorageServer\n * getHeader() — defensive read-back (not strictly required but safe)\n * end() — finalises the response; for redirects this returns a\n * Web API Response object that Nitro sends to the client\n */\n const fakeRes = {\n writeHead(\n statusCode: number,\n headersArg?: Record<string, string | string[] | number> | string\n ) {\n // Capture the status code and any headers supplied alongside writeHead.\n responseStatusCode = statusCode;\n if (headersArg && typeof headersArg === 'object') {\n for (const [key, value] of Object.entries(headersArg)) {\n accumulatedHeaders[key.toLowerCase()] = Array.isArray(value)\n ? (value[0] ?? '')\n : String(value);\n }\n }\n return fakeRes;\n },\n setHeader(name: string, value: string | number | string[]) {\n // Capture Set-Cookie and other outgoing headers.\n accumulatedHeaders[name.toLowerCase()] = Array.isArray(value)\n ? (value[0] ?? '')\n : String(value);\n return fakeRes;\n },\n getHeader(name: string) {\n return accumulatedHeaders[name.toLowerCase()];\n },\n getHeaders() {\n return { ...accumulatedHeaders };\n },\n end(body?: string | Buffer | null) {\n // Build a Web API Response from accumulated status + headers + body.\n // For 3xx redirects the body is intentionally null.\n const webHeaders = new Headers();\n for (const [key, value] of Object.entries(accumulatedHeaders)) {\n webHeaders.set(key, value);\n }\n const isRedirect =\n responseStatusCode >= 300 && responseStatusCode < 400;\n resolve(\n new Response(\n isRedirect ? null : typeof body === 'string' ? body : null,\n {\n status: responseStatusCode,\n headers: webHeaders,\n }\n )\n );\n return fakeRes;\n },\n headersSent: false,\n } as unknown as ServerResponse<IncomingMessage>;\n\n nodeMiddleware(fakeReq, fakeRes, () => {\n // Middleware called next() — either a URL rewrite or a true pass-through.\n const rewrittenPath = fakeReq.url as string;\n\n if (rewrittenPath !== initialPath) {\n // The middleware rewrote the URL (e.g. /about → /en/about for locale prefix).\n // Replace event.url so that event.path (the getter: url.pathname + url.search)\n // returns the new path and the Nitro router matches the correct route.\n //\n // event.url is a plain property on h3 v2's H3Event (not a getter), so direct\n // assignment is safe. We use event.url.origin as the base so relative paths\n // resolve correctly; for path-only requests origin defaults to http://localhost.\n try {\n event.url = new URL(rewrittenPath, event.url.origin);\n } catch {\n console.error(\n '[intlayer-proxy] URL rewrite failed — invalid path:',\n rewrittenPath\n );\n }\n }\n\n // Forward any Set-Cookie or custom headers set by setLocaleInStorageServer to\n // the h3 v2 response object so they are included in the outgoing HTTP response.\n // Accessing event.res lazily creates the H3EventResponse (no cost if empty).\n if (Object.keys(accumulatedHeaders).length > 0) {\n for (const [key, value] of Object.entries(accumulatedHeaders)) {\n event.res.headers.set(key, value);\n }\n }\n\n resolve(undefined);\n });\n });\n"],"mappings":";;;;;;;;;;;;;;;;AAmDA,MAAM,iBAAiB;AACR,aAAa,cAKvB,CAAC,CAAC,0BAA0B,KAAK,GAAG,EACvC,OAAO,OACT,CAAC;AAED,MAAM,iBAAiB,mBAAmB,EAAE,eAAe,eAAe,CAAC;;;;;;;;;;;;;;;;;;;;AAqB3E,mCAAe,OAAO,UACpB,IAAI,SAA0B,YAAY;CACxC,MAAM,cAAc,MAAM;;;;;;;;;;;;;;;;CAiB1B,MAAM,UAAU;EACd,KAAK;EACL,QAAQ;EACR,SAAS;GACP,QAAQ,MAAM,QAAQ,IAAI,QAAQ,KAAK;GACvC,MAAM,MAAM,QAAQ,IAAI,MAAM,KAAK;GACnC,mBAAmB,MAAM,QAAQ,IAAI,iBAAiB,KAAK;GAC3D,oBAAoB,MAAM,QAAQ,IAAI,kBAAkB,KAAK;GAC7D,qBAAqB,MAAM,QAAQ,IAAI,mBAAmB,KAAK;GAC/D,qBAAqB,MAAM,QAAQ,IAAI,mBAAmB,KAAK;EACjE;CACF;CAEA,IAAI,qBAAqB;CACzB,MAAM,qBAA6C,CAAC;;;;;;;;;;;CAYpD,MAAM,UAAU;EACd,UACE,YACA,YACA;GAEA,qBAAqB;GACrB,IAAI,cAAc,OAAO,eAAe,UACtC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,GAClD,mBAAmB,IAAI,YAAY,KAAK,MAAM,QAAQ,KAAK,IACtD,MAAM,MAAM,KACb,OAAO,KAAK;GAGpB,OAAO;EACT;EACA,UAAU,MAAc,OAAmC;GAEzD,mBAAmB,KAAK,YAAY,KAAK,MAAM,QAAQ,KAAK,IACvD,MAAM,MAAM,KACb,OAAO,KAAK;GAChB,OAAO;EACT;EACA,UAAU,MAAc;GACtB,OAAO,mBAAmB,KAAK,YAAY;EAC7C;EACA,aAAa;GACX,OAAO,EAAE,GAAG,mBAAmB;EACjC;EACA,IAAI,MAA+B;GAGjC,MAAM,aAAa,IAAI,QAAQ;GAC/B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,kBAAkB,GAC1D,WAAW,IAAI,KAAK,KAAK;GAI3B,QACE,IAAI,SAFJ,sBAAsB,OAAO,qBAAqB,MAGnC,OAAO,OAAO,SAAS,WAAW,OAAO,MACtD;IACE,QAAQ;IACR,SAAS;GACX,CACF,CACF;GACA,OAAO;EACT;EACA,aAAa;CACf;CAEA,eAAe,SAAS,eAAe;EAErC,MAAM,gBAAgB,QAAQ;EAE9B,IAAI,kBAAkB,aAQpB,IAAI;GACF,MAAM,MAAM,IAAI,IAAI,eAAe,MAAM,IAAI,MAAM;EACrD,QAAQ;GACN,QAAQ,MACN,uDACA,aACF;EACF;EAMF,IAAI,OAAO,KAAK,kBAAkB,CAAC,CAAC,SAAS,GAC3C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,kBAAkB,GAC1D,MAAM,IAAI,QAAQ,IAAI,KAAK,KAAK;EAIpC,QAAQ,MAAS;CACnB,CAAC;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"intlayerNitroHandler.mjs","names":[],"sources":["../../src/intlayerNitroHandler.ts"],"sourcesContent":["/**\n * The configuration as a plain JSON object.\n *\n * Bundlers alias `@intlayer/config/built` to the generated\n * `.intlayer/config/configuration.mjs`, which inlines the resolved\n * configuration at build time. Loading it through `@intlayer/config/node`\n * instead would transpile `intlayer.config.ts` with `esbuild` on every server\n * start, making the built server unable to boot without `node_modules` next to\n * it - which is exactly what a Nitro deployment output is.\n */\nimport * as builtConfiguration from '@intlayer/config/built';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport { formatProxyEnabledMessage } from '@intlayer/core/localization';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { createProxyHandler, isPrerenderProcess } from './intlayerProxyHandler';\nimport { runProxyOnWebRequest } from './intlayerWebProxyHandler';\n\n/**\n * Minimal duck-type for h3 v2's H3Event.\n *\n * We intentionally avoid importing from 'h3' to keep this file runtime-agnostic —\n * Nitro bundles h3 internally and provides the populated event at runtime. Using a\n * structural type here means the file compiles without h3 in devDependencies and\n * works with any h3 v2-compatible runtime (Bun, Deno, Node).\n */\ntype H3EventLike = {\n /**\n * Full URL object — a **plain property** (not a getter) on H3Event, safe to\n * replace for internal URL rewrites. After assignment, `event.path` will\n * automatically reflect the new pathname + search via the getter.\n */\n url: URL;\n /**\n * Web Fetch API Headers — always populated in h3 v2 regardless of preset\n * (Node, Bun, Deno). Use `.get(name)` instead of bracket-access.\n */\n readonly headers: Headers;\n /**\n * Lazy response object — created on first access; its `headers` carry outgoing\n * response headers (e.g. Set-Cookie) that h3 merges into the HTTP response.\n */\n readonly res: {\n readonly headers: Headers;\n };\n};\n\nconst intlayerConfig = builtConfiguration as unknown as IntlayerConfig;\nconst logger = getAppLogger(intlayerConfig);\n// A Nitro server is a production server, so the stored locale drives redirects\n// here — hence the `false`. Announced once per server process, except while a\n// build prerenders through this server: the build already announced the proxy\n// (see `announceBuild` in `intlayerProxyPlugin`), and every request of that\n// pass is a prerender request anyway.\nif (!isPrerenderProcess()) {\n logger(formatProxyEnabledMessage(false), { level: 'info' });\n}\n\nconst nodeMiddleware = createProxyHandler({ configuration: intlayerConfig });\n\n/**\n * Native h3 v2 event handler for Nitro production servers (TanStack Start, Nuxt, etc.).\n *\n * Unlike `fromNodeMiddleware` (h3 v1 API), this handler uses the Web Fetch API event\n * model exclusively and is therefore compatible with ALL Nitro presets — including Bun\n * and Deno — where `event.node` is `undefined` and `fromNodeMiddleware` crashes with\n * \"undefined is not an object (evaluating 'event.node.req')\".\n *\n * It bridges h3 v2 events to the Node.js-style proxy middleware through\n * `runProxyOnWebRequest`:\n *\n * - **Redirect** (301 / 5xx): builds a Web API `Response` and returns it — Nitro sends\n * the correct HTTP response to the browser.\n * - **Rewrite** (`next()` + modified `req.url`): replaces `event.url` with the rewritten\n * URL so `event.path` (a getter) returns the new pathname for downstream handlers and\n * the Nitro router.\n * - **Pass-through** (`next()`, URL unchanged): returns `undefined` — Nitro proceeds to\n * the next handler / route.\n */\nexport default (event: H3EventLike): Response | undefined => {\n const outcome = runProxyOnWebRequest(nodeMiddleware, {\n url: event.url,\n headers: event.headers,\n });\n\n if (outcome.kind === 'response') return outcome.response;\n\n if (outcome.rewrittenPath) {\n // Replace event.url so that event.path (the getter: url.pathname +\n // url.search) returns the new path and the Nitro router matches the\n // correct route. event.url is a plain property on h3 v2's H3Event (not a\n // getter), so direct assignment is safe.\n try {\n event.url = new URL(outcome.rewrittenPath, event.url.origin);\n } catch {\n console.error(\n '[intlayer-proxy] URL rewrite failed — invalid path:',\n outcome.rewrittenPath\n );\n }\n }\n\n // Forward Set-Cookie / custom headers written by setLocaleInStorageServer to\n // the h3 v2 response. Accessing event.res lazily creates the H3EventResponse\n // (no cost if empty).\n for (const [name, value] of outcome.headers) {\n event.res.headers.append(name, value);\n }\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AA8CA,MAAM,iBAAiB;AACvB,MAAM,SAAS,aAAa,cAAc;AAM1C,IAAI,CAAC,mBAAmB,GACtB,OAAO,0BAA0B,KAAK,GAAG,EAAE,OAAO,OAAO,CAAC;AAG5D,MAAM,iBAAiB,mBAAmB,EAAE,eAAe,eAAe,CAAC;;;;;;;;;;;;;;;;;;;;AAqB3E,oCAAgB,UAA6C;CAC3D,MAAM,UAAU,qBAAqB,gBAAgB;EACnD,KAAK,MAAM;EACX,SAAS,MAAM;CACjB,CAAC;CAED,IAAI,QAAQ,SAAS,YAAY,OAAO,QAAQ;CAEhD,IAAI,QAAQ,eAKV,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,QAAQ,eAAe,MAAM,IAAI,MAAM;CAC7D,QAAQ;EACN,QAAQ,MACN,uDACA,QAAQ,aACV;CACF;CAMF,KAAK,MAAM,CAAC,MAAM,UAAU,QAAQ,SAClC,MAAM,IAAI,QAAQ,OAAO,MAAM,KAAK;AAExC"}
|
|
@@ -15,6 +15,18 @@ const NITRO_PRERENDER_HEADER = "x-nitro-prerender";
|
|
|
15
15
|
*/
|
|
16
16
|
const TANSTACK_PRERENDER_ENV_VAR = "TSS_PRERENDERING";
|
|
17
17
|
/**
|
|
18
|
+
* Detects a process that only exists to prerender pages: a TanStack Start
|
|
19
|
+
* build (flag set on the build process, which loads the server in-process) or
|
|
20
|
+
* Nitro's own prerenderer (a dedicated build where Nitro defines
|
|
21
|
+
* `import.meta.prerender`).
|
|
22
|
+
*
|
|
23
|
+
* Prerendered pages are served to every visitor, so such a process never lets
|
|
24
|
+
* the stored locale drive a redirect — see {@link isPrerenderRequest} for the
|
|
25
|
+
* per-request counterpart, which also covers a prerenderer hitting a
|
|
26
|
+
* long-lived server.
|
|
27
|
+
*/
|
|
28
|
+
const isPrerenderProcess = () => process.env[TANSTACK_PRERENDER_ENV_VAR] === "true" || import.meta.prerender === true;
|
|
29
|
+
/**
|
|
18
30
|
* Detects requests issued by a prerenderer rather than by a real visitor.
|
|
19
31
|
*
|
|
20
32
|
* Prerendered pages are written to disk and then served to every visitor, so
|
|
@@ -31,7 +43,7 @@ const TANSTACK_PRERENDER_ENV_VAR = "TSS_PRERENDERING";
|
|
|
31
43
|
* isPrerenderRequest({ headers: { 'x-nitro-prerender': '/about' } }); // true
|
|
32
44
|
* ```
|
|
33
45
|
*/
|
|
34
|
-
const isPrerenderRequest = (req) => Boolean(req.headers[NITRO_PRERENDER_HEADER]) ||
|
|
46
|
+
const isPrerenderRequest = (req) => Boolean(req.headers[NITRO_PRERENDER_HEADER]) || isPrerenderProcess();
|
|
35
47
|
/**
|
|
36
48
|
* Decodes a request pathname so it can be matched against the rewrite rules.
|
|
37
49
|
*
|
|
@@ -500,5 +512,5 @@ const createProxyHandler = (options) => {
|
|
|
500
512
|
};
|
|
501
513
|
|
|
502
514
|
//#endregion
|
|
503
|
-
export { createProxyHandler };
|
|
515
|
+
export { createProxyHandler, isPrerenderProcess };
|
|
504
516
|
//# sourceMappingURL=intlayerProxyHandler.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerProxyHandler.mjs","names":[],"sources":["../../src/intlayerProxyHandler.ts"],"sourcesContent":["/**\n * Runtime half of the Intlayer proxy: locale detection, redirects and rewrites.\n *\n * Nothing here may import `@intlayer/config/node`. This module is bundled into\n * production servers (see `intlayerNitroHandler`), and the node configuration\n * loader transpiles `intlayer.config.ts` with `esbuild` at runtime, which makes\n * the built server depend on `esbuild` being resolvable next to it.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { ROUTING_MODE } from '@intlayer/config/defaultValues';\nimport {\n getCanonicalPath,\n getDomainHostname,\n getDomainOrigin,\n getInternalPath,\n getLocaleFromDomain,\n getRewriteRules,\n isProxyStorageLocaleEnabled,\n localeDetector,\n resolveLocalizedPath,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport {\n getCookie,\n getLocaleFromStorageServer,\n setLocaleInStorageServer,\n} from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n/* @ts-ignore - Vite types error */\nimport type { Connect } from 'vite';\n\n/**\n * Header Nitro's prerenderer sets on every request it issues to the server it\n * is generating pages from. Its value is the route being generated.\n */\nconst NITRO_PRERENDER_HEADER = 'x-nitro-prerender';\n\n/**\n * Environment flag TanStack Start sets on the build process while it prerenders\n * pages through a Vite preview server. The preview server — and therefore this\n * middleware — runs in that same process, so the flag is readable here.\n */\nconst TANSTACK_PRERENDER_ENV_VAR = 'TSS_PRERENDERING';\n\n/**\n * Detects requests issued by a prerenderer rather than by a real visitor.\n *\n * Prerendered pages are written to disk and then served to every visitor, so\n * their content must depend on the URL alone. Anything client-specific — a\n * locale cookie, a persisted locale on the response — would either be baked\n * into a static file or make the generated page depend on the machine running\n * the build.\n *\n * @param req - The incoming request.\n * @returns `true` when the request comes from a prerender pass.\n *\n * @example\n * ```ts\n * isPrerenderRequest({ headers: { 'x-nitro-prerender': '/about' } }); // true\n * ```\n */\nconst isPrerenderRequest = (req: IncomingMessage): boolean =>\n Boolean(req.headers[NITRO_PRERENDER_HEADER]) ||\n process.env[TANSTACK_PRERENDER_ENV_VAR] === 'true';\n\n/**\n * Decodes a request pathname so it can be matched against the rewrite rules.\n *\n * A rule is written with the characters the locale actually uses\n * (`/doc/релизы/v8`) while the browser sends them percent-encoded, so without\n * this every non-ASCII localized URL misses its own rule and is treated as an\n * unlocalized path. `decodeURI` leaves the reserved escapes (`%2F`, `%3F`,\n * `%23`) encoded, which keeps the segmentation of the path intact, and a\n * malformed escape is handed back untouched rather than throwing.\n */\nconst decodePathname = (pathname: string): string => {\n try {\n return decodeURI(pathname);\n } catch {\n return pathname;\n }\n};\n\n/**\n * Re-encodes a decoded path before it is written back to `req.url`, leaving the\n * query string as it was received.\n *\n * The inverse of {@link decodePathname}: `encodeURI` escapes exactly what it\n * decoded, so characters that are legal in a path — `+` in\n * `/doc/intlayer_with_vite+react`, for instance — survive the round trip, and\n * an already-encoded path is left as is.\n */\nconst encodePathname = (path: string): string => {\n const queryIndex = path.indexOf('?');\n const pathname = queryIndex === -1 ? path : path.slice(0, queryIndex);\n const search = queryIndex === -1 ? '' : path.slice(queryIndex);\n\n return `${encodeURI(pathname)}${search}`;\n};\n\n/**\n * Splits a raw request target (`req.url`) into its pathname and search string.\n *\n * Replaces the deprecated `url.parse`, and unlike `new URL(requestUrl, base)`\n * it never reinterprets the leading `//` of a request target as an authority,\n * so `//example.com/page` stays the pathname it was sent as. The fragment is\n * dropped first — a browser never sends one, but a hand-written request may.\n */\nconst splitRequestTarget = (\n requestUrl: string\n): { pathname: string; search: string } => {\n const hashIndex = requestUrl.indexOf('#');\n const target = hashIndex === -1 ? requestUrl : requestUrl.slice(0, hashIndex);\n const queryIndex = target.indexOf('?');\n\n return queryIndex === -1\n ? { pathname: target, search: '' }\n : {\n pathname: target.slice(0, queryIndex),\n search: target.slice(queryIndex),\n };\n};\n\n/**\n * A Node.js-compatible Connect middleware function.\n * Compatible with Vite dev/preview server, Node.js http, Express, and h3's\n * `fromNodeMiddleware` wrapper for Nitro/TanStack Start production use.\n */\nexport type NodeMiddleware = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n next: () => void\n) => void;\n\nexport type IntlayerProxyHandlerOptions = {\n /**\n * The resolved Intlayer configuration driving locale detection and routing.\n */\n configuration: IntlayerConfig;\n /**\n * A function that allows you to ignore specific requests from the intlayer proxy.\n *\n * @param req - The incoming request.\n * @returns A boolean value indicating whether to ignore the request.\n */\n ignore?: (req: IncomingMessage) => boolean | undefined;\n /**\n * Whether a development or preview server is serving the app.\n *\n * It only matters in the proxy's auto mode, where a dev server keeps locale\n * routing URL-driven by ignoring the stored locale as a redirect source.\n *\n * @default false\n */\n isDevServer?: boolean;\n};\n\n/**\n * Creates the locale-routing middleware from an already-resolved configuration.\n *\n * This is the runtime core shared by every environment - dev server, preview\n * server and the production Nitro server. It deliberately receives the\n * configuration instead of loading it: loading it would pull\n * `@intlayer/config/node` - and therefore `esbuild` - into the server bundle,\n * which then fails to boot wherever `node_modules` is not shipped next to the\n * build output.\n *\n * @param options - The resolved configuration plus optional handler behaviour.\n * @returns A Connect-compatible `(req, res, next) => void` middleware.\n *\n * @see `createIntlayerProxyHandler` for the public, configuration-loading entry point.\n */\nexport const createProxyHandler = (\n options: IntlayerProxyHandlerOptions\n): NodeMiddleware => {\n const {\n configuration: intlayerConfig,\n ignore,\n isDevServer = false,\n } = options;\n\n const { internationalization, routing } = intlayerConfig;\n const { locales: supportedLocales, defaultLocale } = internationalization;\n\n const {\n basePath = '',\n mode = ROUTING_MODE,\n rewrite,\n domains,\n enableProxy,\n } = routing;\n\n // In auto mode a dev/preview server ignores the stored locale when resolving\n // which locale a request maps to, so a lingering cookie cannot keep pulling\n // navigation to another locale while developing. Prefix redirects, locale\n // persistence and `Accept-Language` detection are unaffected.\n const canUseStorageLocale = isProxyStorageLocaleEnabled(\n resolveProxyMode(enableProxy),\n isDevServer\n );\n\n type RedirectCounter = { count: number; lastSeen: number };\n const redirectCounts = new Map<string, RedirectCounter>();\n const MAX_REDIRECTS = 10;\n const REDIRECT_TTL_MS = 2_000;\n\n // Derived flags from routing.mode\n const noPrefix =\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params');\n const prefixDefault =\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all'\n ) && mode === 'prefix-all';\n\n const rewriteRules =\n process.env.INTLAYER_ROUTING_REWRITE_RULES !== 'false'\n ? getRewriteRules(rewrite, 'url')\n : undefined;\n\n /**\n * Resolves the locale a localized (\"pretty\") path belongs to.\n *\n * A path such as `/contacto` only exists because a rewrite rule maps it to\n * the Spanish locale, so it declares its locale just as explicitly as an\n * `/es` prefix would. Without this, a visitor whose `Accept-Language` (or\n * stored locale) says `en` gets `/contacto` canonicalized against the\n * English rules only, which never matches — the proxy then treats it as an\n * unlocalized path and redirects to `/en/contacto`, a URL no locale owns.\n *\n * Only patterns that differ from their canonical form are considered: a rule\n * whose localized path equals the canonical one (`en: '/contact'`) carries no\n * locale signal, and matching on it would hijack the default locale's\n * unprefixed URLs.\n *\n * @param path - The request pathname, without any locale prefix.\n * @returns The locale declared by the path, or `undefined` when it declares none.\n */\n const getRewriteLocale = (path: string): Locale | undefined =>\n rewriteRules\n ? supportedLocales.find(\n (candidate) =>\n getCanonicalPath(path, candidate, rewriteRules) !== path\n )\n : undefined;\n\n /**\n * Extracts the hostname from a request's `Host` header, handling IPv6\n * literals (`[::1]:5173` → `::1`) that a plain `split(':')` would mangle.\n */\n const getRequestHostname = (req: IncomingMessage): string => {\n const host = req.headers.host ?? '';\n const ipv6Match = host.match(/^\\[([^\\]]+)\\]/);\n if (ipv6Match) return ipv6Match[1]!;\n return host.split(':')[0] ?? '';\n };\n\n /* --------------------------------------------------------------------\n * Helper & Utility Functions\n * --------------------------------------------------------------------\n */\n\n /**\n * Retrieves the locale from storage (cookies, localStorage, sessionStorage).\n *\n * Returns `undefined` when the stored locale is not allowed to drive locale\n * resolution (auto mode on a dev/preview server, or a prerender pass, whose\n * output must stay URL-driven), which makes every caller fall through to\n * `Accept-Language` detection and then the default locale.\n */\n const getStorageLocale = (req: IncomingMessage): Locale | undefined => {\n if (!canUseStorageLocale || isPrerenderRequest(req)) return undefined;\n\n const locale = getLocaleFromStorageServer({\n getCookie: (name: string) => getCookie(name, req.headers.cookie),\n });\n return locale;\n };\n\n /**\n * Appends locale to search params when routing mode is 'search-params'.\n */\n const appendLocaleSearchIfNeeded = (\n search: string | undefined,\n locale: Locale\n ): string | undefined => {\n if (\n (process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params') ||\n mode !== 'search-params'\n )\n return search;\n\n const params = new URLSearchParams(search ?? '');\n\n params.set('locale', locale);\n\n return `?${params.toString()}`;\n };\n\n /**\n * Checks whether a pathname starts with the given locale as a full path\n * segment (`/fr` or `/fr/...`). A bare `startsWith('/fr')` would also match\n * unrelated paths like `/friends`, causing wrong prefix stripping and\n * self-redirect loops.\n */\n const hasLocaleSegmentPrefix = (pathname: string, locale: Locale): boolean =>\n pathname === `/${locale}` || pathname.startsWith(`/${locale}/`);\n\n /**\n * Extracts the locale from the URL pathname if present as the first segment.\n * e.g. if pathname is /en/some/page or /en, checks if \"en\" is in supportedLocales.\n */\n const getPathLocale = (pathname: string): Locale | undefined => {\n const segments = pathname.split('/').filter(Boolean);\n const firstSegment = segments[0];\n if (firstSegment && supportedLocales.includes(firstSegment as Locale)) {\n return firstSegment as Locale;\n }\n return undefined;\n };\n\n /**\n * Persists the resolved locale onto the outgoing response as a cookie (and\n * header, per `routing.storage`). Only the cookie survives a client redirect,\n * so this is what carries an explicitly-selected locale across a\n * prefix-stripping redirect. Enabled cookie/header targets are resolved by\n * {@link setLocaleInStorageServer} from the config; disabled ones are no-ops.\n */\n const persistLocaleOnResponse = (\n res: ServerResponse<IncomingMessage>,\n locale: Locale\n ) => {\n setLocaleInStorageServer(locale, {\n setCookieStore: (name, value, attributes) => {\n const parts: string[] = [`${name}=${encodeURIComponent(value)}`];\n if (attributes.path) parts.push(`Path=${attributes.path}`);\n if (attributes.domain) parts.push(`Domain=${attributes.domain}`);\n if (typeof attributes.expires === 'number')\n parts.push(`Expires=${new Date(attributes.expires).toUTCString()}`);\n if (attributes.secure) parts.push('Secure');\n if (attributes.httpOnly) parts.push('HttpOnly');\n if (attributes.sameSite) parts.push(`SameSite=${attributes.sameSite}`);\n\n const cookieString = parts.join('; ');\n const existing = res.getHeader('Set-Cookie');\n const cookies = Array.isArray(existing)\n ? existing.map(String)\n : existing !== undefined\n ? [String(existing)]\n : [];\n cookies.push(cookieString);\n res.setHeader('Set-Cookie', cookies);\n },\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n },\n });\n };\n\n /**\n * Identifies the client issuing a request, so redirect-loop tracking is\n * scoped per client. Without this, concurrent visitors hitting the same\n * legitimate redirect (e.g. `/` → `/fr/`) would share one counter and trip\n * the loop detector under normal production traffic.\n */\n const getClientKey = (req: IncomingMessage): string => {\n const forwardedFor = req.headers['x-forwarded-for'];\n const forwardedIp = (\n Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor\n )\n ?.split(',')[0]\n ?.trim();\n\n return forwardedIp || req.socket?.remoteAddress || 'unknown';\n };\n\n /**\n * Strips any origin from a redirect target, keeping only path, search and\n * hash. Prevents open redirects: user-controlled paths like `//evil.com/x`\n * would otherwise be echoed verbatim into the `Location` header and be\n * interpreted by browsers as a protocol-relative cross-origin URL.\n */\n const toSameOriginUrl = (url: string): string => {\n try {\n const parsed = new URL(url, 'http://intlayer-internal');\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n return url;\n }\n };\n\n type RedirectOptions = {\n /** Human-readable reason logged when a redirect loop is detected. */\n reason?: string;\n /** The URL of the incoming request, used for redirect-loop tracking. */\n originalUrl?: string;\n /**\n * When provided, the locale is persisted (cookie/header, per config) on\n * the redirect response so the follow-up request resolves the same locale.\n */\n persistLocale?: Locale;\n /**\n * HTTP status for the redirect. Defaults to 302: locale redirects depend\n * on mutable state (cookie, Accept-Language) and must not be cached by\n * the browser as permanent. Domain-routing redirects pass 301 explicitly.\n */\n status?: number;\n /**\n * Allows a cross-origin `Location` target. Only the domain-routing\n * redirect sets this; all other targets are sanitized to same-origin.\n */\n allowCrossOrigin?: boolean;\n };\n\n /**\n * Writes a redirect response with the given new URL.\n */\n const redirectUrl = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n options?: RedirectOptions\n ) => {\n const {\n reason,\n originalUrl,\n persistLocale,\n status = 302,\n allowCrossOrigin = false,\n } = options ?? {};\n\n const targetUrl = allowCrossOrigin ? newUrl : toSameOriginUrl(newUrl);\n\n if (originalUrl) {\n if (originalUrl === targetUrl) {\n console.error('[REDIRECT LOOP DETECTED!]', { originalUrl, reason });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${originalUrl} redirects to itself`\n );\n }\n\n const now = Date.now();\n const trackingKey = `${getClientKey(req)}|${originalUrl} -> ${targetUrl}`;\n const previousEntry = redirectCounts.get(trackingKey);\n const count =\n previousEntry && now - previousEntry.lastSeen < REDIRECT_TTL_MS\n ? previousEntry.count + 1\n : 1;\n\n redirectCounts.set(trackingKey, { count, lastSeen: now });\n\n if (count > MAX_REDIRECTS) {\n console.error('[REDIRECT LOOP DETECTED!]', {\n originalUrl,\n redirectCount: count,\n lastRedirectTo: targetUrl,\n reason,\n });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${count} redirects from ${originalUrl}`\n );\n }\n\n for (const [entryKey, entry] of redirectCounts) {\n if (now - entry.lastSeen >= REDIRECT_TTL_MS)\n redirectCounts.delete(entryKey);\n }\n }\n\n // A prerendered response is stored as a static file and replayed to every\n // visitor, so persisting the locale on it would hand the locale of the\n // build machine to everyone hitting that page.\n if (persistLocale && !isPrerenderRequest(req)) {\n persistLocaleOnResponse(res, persistLocale);\n }\n\n res.writeHead(status, { Location: targetUrl });\n return res.end();\n };\n\n /**\n * \"Rewrite\" the request internally by adjusting req.url.\n * Also sets the locale in the response/request headers via storage to mimic\n * Next.js's behaviour of propagating the detected locale downstream.\n */\n const rewriteUrl = (\n req: Connect.IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n locale?: Locale\n ) => {\n const encodedUrl = encodePathname(newUrl);\n\n if (req.url !== encodedUrl) {\n req.url = encodedUrl;\n }\n if (locale) {\n setLocaleInStorageServer(locale, {\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n req.headers[name] = value;\n },\n });\n }\n };\n\n /**\n * Constructs a new path string, optionally including a locale prefix, basePath, and search parameters.\n * - basePath: (e.g. '/myapp')\n * - locale: (e.g. 'en')\n * - currentPath: (e.g. '/products/shoes')\n * - search: (e.g. '?foo=bar')\n */\n const constructPath = (\n locale: Locale,\n currentPath: string,\n search?: string\n ) => {\n // Strip any incoming locale prefix to avoid double-prefixing\n const pathWithoutPrefix = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath.slice(`/${locale}`.length)\n : currentPath;\n\n // Ensure basePath always starts with '/' and has no trailing slash\n const cleanBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n // In 'search-params' and 'no-prefix' modes, do not prefix the path with the locale\n if (\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params')\n ) {\n const newPath = search\n ? `${pathWithoutPrefix || '/'}${search}`\n : pathWithoutPrefix || '/';\n return newPath;\n }\n\n // Check if path already starts with locale to avoid double-prefixing\n const pathWithLocalePrefix = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath\n : `/${locale}${currentPath}`;\n\n let newPath = `${normalizedBasePath}${pathWithLocalePrefix}`;\n\n // Special case: if prefixDefault is false and locale is defaultLocale, remove the locale prefix\n if (!prefixDefault && locale === defaultLocale) {\n newPath = `${normalizedBasePath}${pathWithoutPrefix || '/'}`;\n }\n\n // Never emit a trailing slash (`/fr/` for the root path): the framework's\n // trailing-slash normalisation would redirect it back and forth with this\n // proxy, creating an infinite redirect loop.\n if (newPath.length > 1 && newPath.endsWith('/')) {\n newPath = newPath.slice(0, -1);\n }\n\n // Append search parameters if provided\n if (search) {\n newPath += search;\n }\n\n return newPath;\n };\n\n /* --------------------------------------------------------------------\n * Handlers that mirror Next.js style logic\n * --------------------------------------------------------------------\n */\n\n /**\n * If `noPrefix` is true, we never prefix the locale in the URL.\n * We simply rewrite the request to the same path, but with the best-chosen locale\n * in a header or search params if desired.\n */\n const handleNoPrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n const pathLocale = getPathLocale(originalPath);\n\n // Determine the best locale: prefer cookie/storage, fall back to Accept-Language detection\n let locale = storageLocale ?? defaultLocale;\n\n // Use localeDetector if no storage locale is available\n if (!storageLocale) {\n const detectedLocale = localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n );\n locale = detectedLocale as Locale;\n }\n\n // Guard against a stale/unsupported locale coming from storage or detection\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // Without a prefix, a localized path is the only locale signal the URL\n // carries, so it takes precedence over storage / `Accept-Language`.\n if (!pathLocale) {\n locale = getRewriteLocale(originalPath) ?? locale;\n }\n\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n\n const canonicalPath = getCanonicalPath(\n pathWithoutLocale,\n pathLocale,\n rewriteRules\n );\n\n const search = appendLocaleSearchIfNeeded(searchParams, pathLocale);\n\n const redirectPath = search\n ? `${canonicalPath}${search}`\n : `${canonicalPath}${searchParams ?? ''}`;\n\n // Persist the explicitly-requested locale: stripping the prefix drops\n // the only locale signal from the URL, so without this the follow-up\n // request would fall back to cookie / Accept-Language detection and\n // could resolve a different locale.\n return redirectUrl(req, res, redirectPath, {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // In search-params mode, we need to redirect to add the locale search param\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params'\n ) {\n // Check if locale search param already exists and matches the detected locale\n const existingSearchParams = new URLSearchParams(searchParams ?? '');\n const existingLocale = existingSearchParams.get('locale');\n\n if (existingLocale === locale) {\n // Rewrite internally — URL stays the same in the browser, but the framework\n // sees /[locale]/path so the [locale] route param is populated correctly.\n // getInternalPath collapses the root path to `/${locale}` — a trailing\n // slash (`/fr/`) would trigger the framework's trailing-slash\n // normalisation redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n const rewritePath = `${internalPath}${searchParams ?? ''}`;\n\n rewriteUrl(req, res, rewritePath, locale);\n return next();\n }\n\n // Locale param missing or doesn't match — redirect to add/update it (URL changes in browser)\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const redirectPath = search\n ? `${originalPath}${search}`\n : `${originalPath}${searchParams ?? ''}`;\n\n return redirectUrl(req, res, redirectPath, { originalUrl });\n }\n\n // For no-prefix mode (not search-params), add locale prefix internally for routing\n // so the framework can match the [locale] route param without exposing it in the URL.\n // getInternalPath collapses the root path to `/${locale}` — a trailing slash\n // (`/fr/`) would trigger the framework's trailing-slash normalisation\n // redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const rewritePath = search\n ? `${internalPath}${search}`\n : `${internalPath}${searchParams ?? ''}`;\n\n // Rewrite internally — URL stays the same in the browser\n rewriteUrl(req, res, rewritePath, locale);\n\n return next();\n };\n\n /**\n * The main prefix logic.\n */\n const handlePrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale?: Locale;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n if (!pathLocale) {\n handleMissingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n });\n return;\n }\n\n handleExistingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n originalUrl,\n });\n };\n\n /**\n * Handles requests where the locale is missing from the URL pathname.\n * Detects a locale from storage / headers / default, then either redirects or rewrites.\n */\n const handleMissingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n // Choose the best locale: cookie/storage → Accept-Language detection → defaultLocale\n let locale = (storageLocale ??\n localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n )) as Locale;\n\n // If still invalid, fall back to defaultLocale\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // A localized path names its own locale, and the URL always outranks the\n // stored / negotiated one — otherwise /a-propos read as 'en' resolves to\n // no rule and gets redirected to the ownerless /en/a-propos.\n locale = getRewriteLocale(originalPath) ?? locale;\n\n // Resolve to canonical path.\n // If user visits /a-propos (implied 'fr'), this resolves to /about\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // Determine target localized path for redirection.\n // /about + 'fr' → /a-propos\n const { path: targetLocalizedPath } = resolveLocalizedPath(\n canonicalPath,\n locale,\n rewriteRules\n );\n\n // Construct new path, preserving original search params\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const newPath = constructPath(locale, targetLocalizedPath, search);\n\n // If we always prefix default or if this is not the default locale,\n // do a redirect so the user sees the locale in the URL\n if (prefixDefault || locale !== defaultLocale) {\n return redirectUrl(req, res, newPath, { originalUrl });\n }\n\n // If we do NOT prefix the default locale, pass through the canonical path unchanged.\n // Rewriting to `/${locale}${canonicalPath}` (e.g. /en/) causes TanStack Start to issue a\n // trailing-slash normalisation redirect (/en/ → /en), which the proxy then strips back to /,\n // creating an infinite redirect loop.\n // Because {-$locale} is an optional segment, the framework matches the un-prefixed URL with\n // locale=undefined and falls back to defaultLocale via `params.locale ?? defaultLocale`.\n // searchParams MUST be preserved here — dropping them causes the framework (e.g. TanStack Start) to\n // see a URL with no search params, trigger a validateSearch normalisation redirect to the prefixed URL\n // (e.g. /en?page=1&...), which the middleware then strips back to /?..., creating an infinite loop.\n rewriteUrl(req, res, `${canonicalPath}${searchParams}`, locale);\n return next();\n };\n\n /**\n * Handles requests where the locale prefix is present in the pathname.\n */\n const handleExistingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale: Locale;\n originalUrl?: string;\n }) => {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n\n // Identify the canonical path (internal path).\n // Ex: /a-propos (from URL) → /about (canonical)\n const canonicalPath = getCanonicalPath(rawPath, pathLocale, rewriteRules);\n\n // When rewrite rules are configured and the URL is already a valid localized pretty URL\n // (e.g. /fr/essais which maps to canonical /fr/tests), do NOT redirect to canonical.\n //\n // Why: the SPA router (Solid, React Router, Vue Router…) is expected to define routes using\n // the localized paths (e.g. <Route path=\"/essais\">) so the browser URL must stay as-is.\n // A 301 redirect to canonical would:\n // 1. Change the browser URL to the canonical form (/fr/tests)\n // 2. Break subsequent client-side navigation because <A> links produced by getLocalizedUrl\n // point back to the localized URL (/fr/essais) which then has no matching route.\n //\n // We set the locale header and call next() so the server serves the page at the pretty URL.\n if (canonicalPath !== rawPath) {\n const newPath = searchParams\n ? `${originalPath}${searchParams}`\n : originalPath;\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n }\n\n // The URL is in canonical form (e.g. /fr/about) while the locale owns a\n // different pretty URL for it (fr: '/a-propos'). Redirect so the visible\n // URL is the one the locale owns — the same one `getLocalizedUrl` emits\n // and the one the SPA router declares a route for. Skipped when the\n // default-locale prefix has to be stripped anyway: `handleDefaultLocaleRedirect`\n // resolves the localized path itself, so redirecting here first would\n // cost a second round trip.\n if (prefixDefault || pathLocale !== defaultLocale) {\n const { path: targetLocalizedPath, isRewritten } = resolveLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n\n if (isRewritten && targetLocalizedPath !== rawPath) {\n const search = appendLocaleSearchIfNeeded(searchParams, pathLocale);\n\n return redirectUrl(\n req,\n res,\n constructPath(pathLocale, targetLocalizedPath, search),\n { originalUrl }\n );\n }\n }\n\n handleDefaultLocaleRedirect({\n req,\n res,\n next,\n searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\n });\n };\n\n /**\n * If the path locale is the default locale but we don't want to prefix the default, remove it.\n */\n const handleDefaultLocaleRedirect = ({\n req,\n res,\n next,\n searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n searchParams: string;\n pathLocale: Locale;\n canonicalPath: string;\n originalUrl?: string;\n }) => {\n // If we don't prefix the default locale AND the path locale IS the default → strip the prefix\n if (!prefixDefault && pathLocale === defaultLocale) {\n const { path: targetLocalizedPath } = resolveLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n\n // Construct path without prefix\n const cleanBasePath = basePath.startsWith('/')\n ? basePath\n : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n let finalPath = targetLocalizedPath;\n if (finalPath.startsWith('/')) finalPath = finalPath.slice(1);\n\n const fullPath = `${normalizedBasePath}/${finalPath}`.replace(\n /\\/+/g,\n '/'\n );\n\n // Persist the explicitly-requested default locale. Stripping the prefix\n // (e.g. /es → /) drops the only locale signal from the URL, so without\n // this the follow-up request to the canonical path would fall back to\n // Accept-Language detection and could resolve a different locale.\n return redirectUrl(req, res, fullPath + (searchParams ?? ''), {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n // If we do prefix the default or pathLocale !== default, keep as-is\n // but rewrite to canonical internally.\n // Never emit a trailing slash (`/fr/` for the bare `/fr` URL): rewriting\n // `/fr` to `/fr/` makes the framework issue a trailing-slash normalisation\n // redirect back to `/fr`, which this proxy rewrites again — an infinite\n // redirect loop. `getInternalPath` collapses the root path to `/${locale}`.\n const internalUrl = getInternalPath(canonicalPath, pathLocale);\n const newPath = searchParams\n ? `${internalUrl}${searchParams}`\n : internalUrl;\n\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n };\n\n return (req, res, next) => {\n // Parse original URL for path and query\n const { pathname, search } = splitRequestTarget(req.url ?? '/');\n // Decoded so the rewrite rules — written with the locale's own characters —\n // match; `rewriteUrl` re-encodes whatever is written back to `req.url`.\n const originalPath = decodePathname(pathname || '/');\n const searchParams = search;\n\n // Check if there's a locale prefix in the path FIRST\n const pathLocale = getPathLocale(originalPath);\n\n // Bypass special Vite/server endpoints and node_modules\n if (\n // Custom ignore function\n (ignore?.(req) ?? false) ||\n originalPath.startsWith('/node_modules') ||\n /**\n * /^@vite/ # HMR client and helpers\n * /^@fs/ # file-system import serving\n * /^@id/ # virtual module ids\n * /^@tanstack/start-router-manifest # Tanstack Start Router manifest\n */\n originalPath.startsWith('/@') ||\n /**\n * /^__vite_ping$ # health ping\n * /^__open-in-editor$\n * /^__manifest$ # Remix/RR7 lazyRouteDiscovery\n */\n originalPath.startsWith('/_')\n ) {\n return next();\n }\n\n // Static file requests (e.g. /assets/video.mp4): bypass locale routing.\n // If the URL carries a locale prefix (e.g. /fr/assets/video.mp4),\n // rewrite the request internally to the unprefixed path (/assets/video.mp4)\n // so the file can be served correctly from the public directory.\n if (originalPath.match(/\\.[a-zA-Z0-9]+$/)) {\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n req.url = encodePathname(`${pathWithoutLocale}${searchParams}`);\n }\n return next();\n }\n\n // Attempt to read the locale from storage (cookies, localStorage, etc.)\n const storageLocale = getStorageLocale(req);\n\n // CRITICAL FIX: If there's a valid pathLocale, it takes precedence over storage\n // This prevents race conditions when cookies are stale during locale switches\n const effectiveStorageLocale =\n pathLocale && supportedLocales.includes(pathLocale)\n ? pathLocale\n : storageLocale;\n\n // Store original URL for redirect tracking\n const originalUrl = req.url;\n\n // Domain routing: if the path locale is mapped to a different domain, redirect there.\n // e.g. intlayer.org/zh/about → https://intlayer.zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n pathLocale &&\n domains\n ) {\n const localeDomain = domains[pathLocale as keyof typeof domains];\n if (localeDomain) {\n const reqHost = getRequestHostname(req);\n const domainHost = getDomainHostname(localeDomain);\n if (domainHost !== reqHost) {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n const targetOrigin = getDomainOrigin(localeDomain);\n // Domain mapping is stable config, so a cacheable 301 is intended\n // here — unlike locale-detection redirects, which use 302.\n redirectUrl(req, res, `${targetOrigin}${rawPath}${searchParams}`, {\n reason: 'domain-routing',\n originalUrl,\n status: 301,\n allowCrossOrigin: true,\n });\n return;\n }\n }\n }\n\n // Domain routing: if the current hostname is exclusively mapped to one locale,\n // treat it as that locale without a URL prefix.\n // e.g. intlayer.zh/about → internally rewrite to /zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n !pathLocale\n ) {\n const reqHost = getRequestHostname(req);\n const domainLocale = getLocaleFromDomain(reqHost, domains);\n if (domainLocale) {\n const canonicalPath = getCanonicalPath(\n originalPath,\n domainLocale,\n rewriteRules\n );\n\n // Default locale without forced prefixing: pass the canonical path\n // through unchanged, mirroring handleMissingPathLocale. Rewriting `/`\n // to `/en/` makes TanStack Start issue a trailing-slash normalisation\n // redirect (/en/ → /en), whose prefix the proxy then strips back to /,\n // creating an infinite redirect loop.\n if (!prefixDefault && domainLocale === defaultLocale) {\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n `${canonicalPath}${searchParams}`,\n domainLocale\n );\n return next();\n }\n\n // Never emit a trailing slash (`/zh/`): the framework would redirect\n // it to `/zh`, which this proxy rewrites to `/zh/` again — a loop.\n const internalPath = getInternalPath(canonicalPath, domainLocale);\n\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n searchParams ? `${internalPath}${searchParams}` : internalPath,\n domainLocale\n );\n return next();\n }\n }\n\n if (noPrefix) {\n handleNoPrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n return;\n }\n\n handlePrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"intlayerProxyHandler.mjs","names":[],"sources":["../../src/intlayerProxyHandler.ts"],"sourcesContent":["/**\n * Runtime half of the Intlayer proxy: locale detection, redirects and rewrites.\n *\n * Nothing here may import `@intlayer/config/node`. This module is bundled into\n * production servers (see `intlayerNitroHandler`), and the node configuration\n * loader transpiles `intlayer.config.ts` with `esbuild` at runtime, which makes\n * the built server depend on `esbuild` being resolvable next to it.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { ROUTING_MODE } from '@intlayer/config/defaultValues';\nimport {\n getCanonicalPath,\n getDomainHostname,\n getDomainOrigin,\n getInternalPath,\n getLocaleFromDomain,\n getRewriteRules,\n isProxyStorageLocaleEnabled,\n localeDetector,\n resolveLocalizedPath,\n resolveProxyMode,\n} from '@intlayer/core/localization';\nimport {\n getCookie,\n getLocaleFromStorageServer,\n setLocaleInStorageServer,\n} from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\n/* @ts-ignore - Vite types error */\nimport type { Connect } from 'vite';\n\n/**\n * Header Nitro's prerenderer sets on every request it issues to the server it\n * is generating pages from. Its value is the route being generated.\n */\nconst NITRO_PRERENDER_HEADER = 'x-nitro-prerender';\n\n/**\n * Environment flag TanStack Start sets on the build process while it prerenders\n * pages through a Vite preview server. The preview server — and therefore this\n * middleware — runs in that same process, so the flag is readable here.\n */\nconst TANSTACK_PRERENDER_ENV_VAR = 'TSS_PRERENDERING';\n\n/**\n * Detects a process that only exists to prerender pages: a TanStack Start\n * build (flag set on the build process, which loads the server in-process) or\n * Nitro's own prerenderer (a dedicated build where Nitro defines\n * `import.meta.prerender`).\n *\n * Prerendered pages are served to every visitor, so such a process never lets\n * the stored locale drive a redirect — see {@link isPrerenderRequest} for the\n * per-request counterpart, which also covers a prerenderer hitting a\n * long-lived server.\n */\nexport const isPrerenderProcess = (): boolean =>\n process.env[TANSTACK_PRERENDER_ENV_VAR] === 'true' ||\n (import.meta as { prerender?: boolean }).prerender === true;\n\n/**\n * Detects requests issued by a prerenderer rather than by a real visitor.\n *\n * Prerendered pages are written to disk and then served to every visitor, so\n * their content must depend on the URL alone. Anything client-specific — a\n * locale cookie, a persisted locale on the response — would either be baked\n * into a static file or make the generated page depend on the machine running\n * the build.\n *\n * @param req - The incoming request.\n * @returns `true` when the request comes from a prerender pass.\n *\n * @example\n * ```ts\n * isPrerenderRequest({ headers: { 'x-nitro-prerender': '/about' } }); // true\n * ```\n */\nconst isPrerenderRequest = (req: IncomingMessage): boolean =>\n Boolean(req.headers[NITRO_PRERENDER_HEADER]) || isPrerenderProcess();\n\n/**\n * Decodes a request pathname so it can be matched against the rewrite rules.\n *\n * A rule is written with the characters the locale actually uses\n * (`/doc/релизы/v8`) while the browser sends them percent-encoded, so without\n * this every non-ASCII localized URL misses its own rule and is treated as an\n * unlocalized path. `decodeURI` leaves the reserved escapes (`%2F`, `%3F`,\n * `%23`) encoded, which keeps the segmentation of the path intact, and a\n * malformed escape is handed back untouched rather than throwing.\n */\nconst decodePathname = (pathname: string): string => {\n try {\n return decodeURI(pathname);\n } catch {\n return pathname;\n }\n};\n\n/**\n * Re-encodes a decoded path before it is written back to `req.url`, leaving the\n * query string as it was received.\n *\n * The inverse of {@link decodePathname}: `encodeURI` escapes exactly what it\n * decoded, so characters that are legal in a path — `+` in\n * `/doc/intlayer_with_vite+react`, for instance — survive the round trip, and\n * an already-encoded path is left as is.\n */\nconst encodePathname = (path: string): string => {\n const queryIndex = path.indexOf('?');\n const pathname = queryIndex === -1 ? path : path.slice(0, queryIndex);\n const search = queryIndex === -1 ? '' : path.slice(queryIndex);\n\n return `${encodeURI(pathname)}${search}`;\n};\n\n/**\n * Splits a raw request target (`req.url`) into its pathname and search string.\n *\n * Replaces the deprecated `url.parse`, and unlike `new URL(requestUrl, base)`\n * it never reinterprets the leading `//` of a request target as an authority,\n * so `//example.com/page` stays the pathname it was sent as. The fragment is\n * dropped first — a browser never sends one, but a hand-written request may.\n */\nconst splitRequestTarget = (\n requestUrl: string\n): { pathname: string; search: string } => {\n const hashIndex = requestUrl.indexOf('#');\n const target = hashIndex === -1 ? requestUrl : requestUrl.slice(0, hashIndex);\n const queryIndex = target.indexOf('?');\n\n return queryIndex === -1\n ? { pathname: target, search: '' }\n : {\n pathname: target.slice(0, queryIndex),\n search: target.slice(queryIndex),\n };\n};\n\n/**\n * A Node.js-compatible Connect middleware function.\n * Compatible with Vite dev/preview server, Node.js http, Express, and h3's\n * `fromNodeMiddleware` wrapper for Nitro/TanStack Start production use.\n */\nexport type NodeMiddleware = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n next: () => void\n) => void;\n\nexport type IntlayerProxyHandlerOptions = {\n /**\n * The resolved Intlayer configuration driving locale detection and routing.\n */\n configuration: IntlayerConfig;\n /**\n * A function that allows you to ignore specific requests from the intlayer proxy.\n *\n * @param req - The incoming request.\n * @returns A boolean value indicating whether to ignore the request.\n */\n ignore?: (req: IncomingMessage) => boolean | undefined;\n /**\n * Whether a development or preview server is serving the app.\n *\n * It only matters in the proxy's auto mode, where a dev server keeps locale\n * routing URL-driven by ignoring the stored locale as a redirect source.\n *\n * @default false\n */\n isDevServer?: boolean;\n};\n\n/**\n * Creates the locale-routing middleware from an already-resolved configuration.\n *\n * This is the runtime core shared by every environment - dev server, preview\n * server and the production Nitro server. It deliberately receives the\n * configuration instead of loading it: loading it would pull\n * `@intlayer/config/node` - and therefore `esbuild` - into the server bundle,\n * which then fails to boot wherever `node_modules` is not shipped next to the\n * build output.\n *\n * @param options - The resolved configuration plus optional handler behaviour.\n * @returns A Connect-compatible `(req, res, next) => void` middleware.\n *\n * @see `createIntlayerProxyHandler` for the public, configuration-loading entry point.\n */\nexport const createProxyHandler = (\n options: IntlayerProxyHandlerOptions\n): NodeMiddleware => {\n const {\n configuration: intlayerConfig,\n ignore,\n isDevServer = false,\n } = options;\n\n const { internationalization, routing } = intlayerConfig;\n const { locales: supportedLocales, defaultLocale } = internationalization;\n\n const {\n basePath = '',\n mode = ROUTING_MODE,\n rewrite,\n domains,\n enableProxy,\n } = routing;\n\n // In auto mode a dev/preview server ignores the stored locale when resolving\n // which locale a request maps to, so a lingering cookie cannot keep pulling\n // navigation to another locale while developing. Prefix redirects, locale\n // persistence and `Accept-Language` detection are unaffected.\n const canUseStorageLocale = isProxyStorageLocaleEnabled(\n resolveProxyMode(enableProxy),\n isDevServer\n );\n\n type RedirectCounter = { count: number; lastSeen: number };\n const redirectCounts = new Map<string, RedirectCounter>();\n const MAX_REDIRECTS = 10;\n const REDIRECT_TTL_MS = 2_000;\n\n // Derived flags from routing.mode\n const noPrefix =\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params');\n const prefixDefault =\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'prefix-all'\n ) && mode === 'prefix-all';\n\n const rewriteRules =\n process.env.INTLAYER_ROUTING_REWRITE_RULES !== 'false'\n ? getRewriteRules(rewrite, 'url')\n : undefined;\n\n /**\n * Resolves the locale a localized (\"pretty\") path belongs to.\n *\n * A path such as `/contacto` only exists because a rewrite rule maps it to\n * the Spanish locale, so it declares its locale just as explicitly as an\n * `/es` prefix would. Without this, a visitor whose `Accept-Language` (or\n * stored locale) says `en` gets `/contacto` canonicalized against the\n * English rules only, which never matches — the proxy then treats it as an\n * unlocalized path and redirects to `/en/contacto`, a URL no locale owns.\n *\n * Only patterns that differ from their canonical form are considered: a rule\n * whose localized path equals the canonical one (`en: '/contact'`) carries no\n * locale signal, and matching on it would hijack the default locale's\n * unprefixed URLs.\n *\n * @param path - The request pathname, without any locale prefix.\n * @returns The locale declared by the path, or `undefined` when it declares none.\n */\n const getRewriteLocale = (path: string): Locale | undefined =>\n rewriteRules\n ? supportedLocales.find(\n (candidate) =>\n getCanonicalPath(path, candidate, rewriteRules) !== path\n )\n : undefined;\n\n /**\n * Extracts the hostname from a request's `Host` header, handling IPv6\n * literals (`[::1]:5173` → `::1`) that a plain `split(':')` would mangle.\n */\n const getRequestHostname = (req: IncomingMessage): string => {\n const host = req.headers.host ?? '';\n const ipv6Match = host.match(/^\\[([^\\]]+)\\]/);\n if (ipv6Match) return ipv6Match[1]!;\n return host.split(':')[0] ?? '';\n };\n\n /* --------------------------------------------------------------------\n * Helper & Utility Functions\n * --------------------------------------------------------------------\n */\n\n /**\n * Retrieves the locale from storage (cookies, localStorage, sessionStorage).\n *\n * Returns `undefined` when the stored locale is not allowed to drive locale\n * resolution (auto mode on a dev/preview server, or a prerender pass, whose\n * output must stay URL-driven), which makes every caller fall through to\n * `Accept-Language` detection and then the default locale.\n */\n const getStorageLocale = (req: IncomingMessage): Locale | undefined => {\n if (!canUseStorageLocale || isPrerenderRequest(req)) return undefined;\n\n const locale = getLocaleFromStorageServer({\n getCookie: (name: string) => getCookie(name, req.headers.cookie),\n });\n return locale;\n };\n\n /**\n * Appends locale to search params when routing mode is 'search-params'.\n */\n const appendLocaleSearchIfNeeded = (\n search: string | undefined,\n locale: Locale\n ): string | undefined => {\n if (\n (process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params') ||\n mode !== 'search-params'\n )\n return search;\n\n const params = new URLSearchParams(search ?? '');\n\n params.set('locale', locale);\n\n return `?${params.toString()}`;\n };\n\n /**\n * Checks whether a pathname starts with the given locale as a full path\n * segment (`/fr` or `/fr/...`). A bare `startsWith('/fr')` would also match\n * unrelated paths like `/friends`, causing wrong prefix stripping and\n * self-redirect loops.\n */\n const hasLocaleSegmentPrefix = (pathname: string, locale: Locale): boolean =>\n pathname === `/${locale}` || pathname.startsWith(`/${locale}/`);\n\n /**\n * Extracts the locale from the URL pathname if present as the first segment.\n * e.g. if pathname is /en/some/page or /en, checks if \"en\" is in supportedLocales.\n */\n const getPathLocale = (pathname: string): Locale | undefined => {\n const segments = pathname.split('/').filter(Boolean);\n const firstSegment = segments[0];\n if (firstSegment && supportedLocales.includes(firstSegment as Locale)) {\n return firstSegment as Locale;\n }\n return undefined;\n };\n\n /**\n * Persists the resolved locale onto the outgoing response as a cookie (and\n * header, per `routing.storage`). Only the cookie survives a client redirect,\n * so this is what carries an explicitly-selected locale across a\n * prefix-stripping redirect. Enabled cookie/header targets are resolved by\n * {@link setLocaleInStorageServer} from the config; disabled ones are no-ops.\n */\n const persistLocaleOnResponse = (\n res: ServerResponse<IncomingMessage>,\n locale: Locale\n ) => {\n setLocaleInStorageServer(locale, {\n setCookieStore: (name, value, attributes) => {\n const parts: string[] = [`${name}=${encodeURIComponent(value)}`];\n if (attributes.path) parts.push(`Path=${attributes.path}`);\n if (attributes.domain) parts.push(`Domain=${attributes.domain}`);\n if (typeof attributes.expires === 'number')\n parts.push(`Expires=${new Date(attributes.expires).toUTCString()}`);\n if (attributes.secure) parts.push('Secure');\n if (attributes.httpOnly) parts.push('HttpOnly');\n if (attributes.sameSite) parts.push(`SameSite=${attributes.sameSite}`);\n\n const cookieString = parts.join('; ');\n const existing = res.getHeader('Set-Cookie');\n const cookies = Array.isArray(existing)\n ? existing.map(String)\n : existing !== undefined\n ? [String(existing)]\n : [];\n cookies.push(cookieString);\n res.setHeader('Set-Cookie', cookies);\n },\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n },\n });\n };\n\n /**\n * Identifies the client issuing a request, so redirect-loop tracking is\n * scoped per client. Without this, concurrent visitors hitting the same\n * legitimate redirect (e.g. `/` → `/fr/`) would share one counter and trip\n * the loop detector under normal production traffic.\n */\n const getClientKey = (req: IncomingMessage): string => {\n const forwardedFor = req.headers['x-forwarded-for'];\n const forwardedIp = (\n Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor\n )\n ?.split(',')[0]\n ?.trim();\n\n return forwardedIp || req.socket?.remoteAddress || 'unknown';\n };\n\n /**\n * Strips any origin from a redirect target, keeping only path, search and\n * hash. Prevents open redirects: user-controlled paths like `//evil.com/x`\n * would otherwise be echoed verbatim into the `Location` header and be\n * interpreted by browsers as a protocol-relative cross-origin URL.\n */\n const toSameOriginUrl = (url: string): string => {\n try {\n const parsed = new URL(url, 'http://intlayer-internal');\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n return url;\n }\n };\n\n type RedirectOptions = {\n /** Human-readable reason logged when a redirect loop is detected. */\n reason?: string;\n /** The URL of the incoming request, used for redirect-loop tracking. */\n originalUrl?: string;\n /**\n * When provided, the locale is persisted (cookie/header, per config) on\n * the redirect response so the follow-up request resolves the same locale.\n */\n persistLocale?: Locale;\n /**\n * HTTP status for the redirect. Defaults to 302: locale redirects depend\n * on mutable state (cookie, Accept-Language) and must not be cached by\n * the browser as permanent. Domain-routing redirects pass 301 explicitly.\n */\n status?: number;\n /**\n * Allows a cross-origin `Location` target. Only the domain-routing\n * redirect sets this; all other targets are sanitized to same-origin.\n */\n allowCrossOrigin?: boolean;\n };\n\n /**\n * Writes a redirect response with the given new URL.\n */\n const redirectUrl = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n options?: RedirectOptions\n ) => {\n const {\n reason,\n originalUrl,\n persistLocale,\n status = 302,\n allowCrossOrigin = false,\n } = options ?? {};\n\n const targetUrl = allowCrossOrigin ? newUrl : toSameOriginUrl(newUrl);\n\n if (originalUrl) {\n if (originalUrl === targetUrl) {\n console.error('[REDIRECT LOOP DETECTED!]', { originalUrl, reason });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${originalUrl} redirects to itself`\n );\n }\n\n const now = Date.now();\n const trackingKey = `${getClientKey(req)}|${originalUrl} -> ${targetUrl}`;\n const previousEntry = redirectCounts.get(trackingKey);\n const count =\n previousEntry && now - previousEntry.lastSeen < REDIRECT_TTL_MS\n ? previousEntry.count + 1\n : 1;\n\n redirectCounts.set(trackingKey, { count, lastSeen: now });\n\n if (count > MAX_REDIRECTS) {\n console.error('[REDIRECT LOOP DETECTED!]', {\n originalUrl,\n redirectCount: count,\n lastRedirectTo: targetUrl,\n reason,\n });\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n return res.end(\n `Redirect loop detected: ${count} redirects from ${originalUrl}`\n );\n }\n\n for (const [entryKey, entry] of redirectCounts) {\n if (now - entry.lastSeen >= REDIRECT_TTL_MS)\n redirectCounts.delete(entryKey);\n }\n }\n\n // A prerendered response is stored as a static file and replayed to every\n // visitor, so persisting the locale on it would hand the locale of the\n // build machine to everyone hitting that page.\n if (persistLocale && !isPrerenderRequest(req)) {\n persistLocaleOnResponse(res, persistLocale);\n }\n\n res.writeHead(status, { Location: targetUrl });\n return res.end();\n };\n\n /**\n * \"Rewrite\" the request internally by adjusting req.url.\n * Also sets the locale in the response/request headers via storage to mimic\n * Next.js's behaviour of propagating the detected locale downstream.\n */\n const rewriteUrl = (\n req: Connect.IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n locale?: Locale\n ) => {\n const encodedUrl = encodePathname(newUrl);\n\n if (req.url !== encodedUrl) {\n req.url = encodedUrl;\n }\n if (locale) {\n setLocaleInStorageServer(locale, {\n setHeader: (name: string, value: string) => {\n res.setHeader(name, value);\n req.headers[name] = value;\n },\n });\n }\n };\n\n /**\n * Constructs a new path string, optionally including a locale prefix, basePath, and search parameters.\n * - basePath: (e.g. '/myapp')\n * - locale: (e.g. 'en')\n * - currentPath: (e.g. '/products/shoes')\n * - search: (e.g. '?foo=bar')\n */\n const constructPath = (\n locale: Locale,\n currentPath: string,\n search?: string\n ) => {\n // Strip any incoming locale prefix to avoid double-prefixing\n const pathWithoutPrefix = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath.slice(`/${locale}`.length)\n : currentPath;\n\n // Ensure basePath always starts with '/' and has no trailing slash\n const cleanBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n // In 'search-params' and 'no-prefix' modes, do not prefix the path with the locale\n if (\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'no-prefix'\n ) &&\n mode === 'no-prefix') ||\n (!(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params')\n ) {\n const newPath = search\n ? `${pathWithoutPrefix || '/'}${search}`\n : pathWithoutPrefix || '/';\n return newPath;\n }\n\n // Check if path already starts with locale to avoid double-prefixing\n const pathWithLocalePrefix = hasLocaleSegmentPrefix(currentPath, locale)\n ? currentPath\n : `/${locale}${currentPath}`;\n\n let newPath = `${normalizedBasePath}${pathWithLocalePrefix}`;\n\n // Special case: if prefixDefault is false and locale is defaultLocale, remove the locale prefix\n if (!prefixDefault && locale === defaultLocale) {\n newPath = `${normalizedBasePath}${pathWithoutPrefix || '/'}`;\n }\n\n // Never emit a trailing slash (`/fr/` for the root path): the framework's\n // trailing-slash normalisation would redirect it back and forth with this\n // proxy, creating an infinite redirect loop.\n if (newPath.length > 1 && newPath.endsWith('/')) {\n newPath = newPath.slice(0, -1);\n }\n\n // Append search parameters if provided\n if (search) {\n newPath += search;\n }\n\n return newPath;\n };\n\n /* --------------------------------------------------------------------\n * Handlers that mirror Next.js style logic\n * --------------------------------------------------------------------\n */\n\n /**\n * If `noPrefix` is true, we never prefix the locale in the URL.\n * We simply rewrite the request to the same path, but with the best-chosen locale\n * in a header or search params if desired.\n */\n const handleNoPrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n const pathLocale = getPathLocale(originalPath);\n\n // Determine the best locale: prefer cookie/storage, fall back to Accept-Language detection\n let locale = storageLocale ?? defaultLocale;\n\n // Use localeDetector if no storage locale is available\n if (!storageLocale) {\n const detectedLocale = localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n );\n locale = detectedLocale as Locale;\n }\n\n // Guard against a stale/unsupported locale coming from storage or detection\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // Without a prefix, a localized path is the only locale signal the URL\n // carries, so it takes precedence over storage / `Accept-Language`.\n if (!pathLocale) {\n locale = getRewriteLocale(originalPath) ?? locale;\n }\n\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n\n const canonicalPath = getCanonicalPath(\n pathWithoutLocale,\n pathLocale,\n rewriteRules\n );\n\n const search = appendLocaleSearchIfNeeded(searchParams, pathLocale);\n\n const redirectPath = search\n ? `${canonicalPath}${search}`\n : `${canonicalPath}${searchParams ?? ''}`;\n\n // Persist the explicitly-requested locale: stripping the prefix drops\n // the only locale signal from the URL, so without this the follow-up\n // request would fall back to cookie / Accept-Language detection and\n // could resolve a different locale.\n return redirectUrl(req, res, redirectPath, {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // In search-params mode, we need to redirect to add the locale search param\n if (\n !(\n process.env.INTLAYER_ROUTING_MODE &&\n process.env.INTLAYER_ROUTING_MODE !== 'search-params'\n ) &&\n mode === 'search-params'\n ) {\n // Check if locale search param already exists and matches the detected locale\n const existingSearchParams = new URLSearchParams(searchParams ?? '');\n const existingLocale = existingSearchParams.get('locale');\n\n if (existingLocale === locale) {\n // Rewrite internally — URL stays the same in the browser, but the framework\n // sees /[locale]/path so the [locale] route param is populated correctly.\n // getInternalPath collapses the root path to `/${locale}` — a trailing\n // slash (`/fr/`) would trigger the framework's trailing-slash\n // normalisation redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n const rewritePath = `${internalPath}${searchParams ?? ''}`;\n\n rewriteUrl(req, res, rewritePath, locale);\n return next();\n }\n\n // Locale param missing or doesn't match — redirect to add/update it (URL changes in browser)\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const redirectPath = search\n ? `${originalPath}${search}`\n : `${originalPath}${searchParams ?? ''}`;\n\n return redirectUrl(req, res, redirectPath, { originalUrl });\n }\n\n // For no-prefix mode (not search-params), add locale prefix internally for routing\n // so the framework can match the [locale] route param without exposing it in the URL.\n // getInternalPath collapses the root path to `/${locale}` — a trailing slash\n // (`/fr/`) would trigger the framework's trailing-slash normalisation\n // redirect and loop with this proxy.\n const internalPath = getInternalPath(canonicalPath, locale);\n\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const rewritePath = search\n ? `${internalPath}${search}`\n : `${internalPath}${searchParams ?? ''}`;\n\n // Rewrite internally — URL stays the same in the browser\n rewriteUrl(req, res, rewritePath, locale);\n\n return next();\n };\n\n /**\n * The main prefix logic.\n */\n const handlePrefix = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale?: Locale;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n if (!pathLocale) {\n handleMissingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n });\n return;\n }\n\n handleExistingPathLocale({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n originalUrl,\n });\n };\n\n /**\n * Handles requests where the locale is missing from the URL pathname.\n * Detects a locale from storage / headers / default, then either redirects or rewrites.\n */\n const handleMissingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n storageLocale?: Locale;\n originalUrl?: string;\n }) => {\n // Choose the best locale: cookie/storage → Accept-Language detection → defaultLocale\n let locale = (storageLocale ??\n localeDetector(\n req.headers as Record<string, string>,\n supportedLocales,\n defaultLocale\n )) as Locale;\n\n // If still invalid, fall back to defaultLocale\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // A localized path names its own locale, and the URL always outranks the\n // stored / negotiated one — otherwise /a-propos read as 'en' resolves to\n // no rule and gets redirected to the ownerless /en/a-propos.\n locale = getRewriteLocale(originalPath) ?? locale;\n\n // Resolve to canonical path.\n // If user visits /a-propos (implied 'fr'), this resolves to /about\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // Determine target localized path for redirection.\n // /about + 'fr' → /a-propos\n const { path: targetLocalizedPath } = resolveLocalizedPath(\n canonicalPath,\n locale,\n rewriteRules\n );\n\n // Construct new path, preserving original search params\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const newPath = constructPath(locale, targetLocalizedPath, search);\n\n // If we always prefix default or if this is not the default locale,\n // do a redirect so the user sees the locale in the URL\n if (prefixDefault || locale !== defaultLocale) {\n return redirectUrl(req, res, newPath, { originalUrl });\n }\n\n // If we do NOT prefix the default locale, pass through the canonical path unchanged.\n // Rewriting to `/${locale}${canonicalPath}` (e.g. /en/) causes TanStack Start to issue a\n // trailing-slash normalisation redirect (/en/ → /en), which the proxy then strips back to /,\n // creating an infinite redirect loop.\n // Because {-$locale} is an optional segment, the framework matches the un-prefixed URL with\n // locale=undefined and falls back to defaultLocale via `params.locale ?? defaultLocale`.\n // searchParams MUST be preserved here — dropping them causes the framework (e.g. TanStack Start) to\n // see a URL with no search params, trigger a validateSearch normalisation redirect to the prefixed URL\n // (e.g. /en?page=1&...), which the middleware then strips back to /?..., creating an infinite loop.\n rewriteUrl(req, res, `${canonicalPath}${searchParams}`, locale);\n return next();\n };\n\n /**\n * Handles requests where the locale prefix is present in the pathname.\n */\n const handleExistingPathLocale = ({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n originalPath: string;\n searchParams: string;\n pathLocale: Locale;\n originalUrl?: string;\n }) => {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n\n // Identify the canonical path (internal path).\n // Ex: /a-propos (from URL) → /about (canonical)\n const canonicalPath = getCanonicalPath(rawPath, pathLocale, rewriteRules);\n\n // When rewrite rules are configured and the URL is already a valid localized pretty URL\n // (e.g. /fr/essais which maps to canonical /fr/tests), do NOT redirect to canonical.\n //\n // Why: the SPA router (Solid, React Router, Vue Router…) is expected to define routes using\n // the localized paths (e.g. <Route path=\"/essais\">) so the browser URL must stay as-is.\n // A 301 redirect to canonical would:\n // 1. Change the browser URL to the canonical form (/fr/tests)\n // 2. Break subsequent client-side navigation because <A> links produced by getLocalizedUrl\n // point back to the localized URL (/fr/essais) which then has no matching route.\n //\n // We set the locale header and call next() so the server serves the page at the pretty URL.\n if (canonicalPath !== rawPath) {\n const newPath = searchParams\n ? `${originalPath}${searchParams}`\n : originalPath;\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n }\n\n // The URL is in canonical form (e.g. /fr/about) while the locale owns a\n // different pretty URL for it (fr: '/a-propos'). Redirect so the visible\n // URL is the one the locale owns — the same one `getLocalizedUrl` emits\n // and the one the SPA router declares a route for. Skipped when the\n // default-locale prefix has to be stripped anyway: `handleDefaultLocaleRedirect`\n // resolves the localized path itself, so redirecting here first would\n // cost a second round trip.\n if (prefixDefault || pathLocale !== defaultLocale) {\n const { path: targetLocalizedPath, isRewritten } = resolveLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n\n if (isRewritten && targetLocalizedPath !== rawPath) {\n const search = appendLocaleSearchIfNeeded(searchParams, pathLocale);\n\n return redirectUrl(\n req,\n res,\n constructPath(pathLocale, targetLocalizedPath, search),\n { originalUrl }\n );\n }\n }\n\n handleDefaultLocaleRedirect({\n req,\n res,\n next,\n searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\n });\n };\n\n /**\n * If the path locale is the default locale but we don't want to prefix the default, remove it.\n */\n const handleDefaultLocaleRedirect = ({\n req,\n res,\n next,\n searchParams,\n pathLocale,\n canonicalPath,\n originalUrl,\n }: {\n req: Connect.IncomingMessage;\n res: ServerResponse<IncomingMessage>;\n next: Connect.NextFunction;\n searchParams: string;\n pathLocale: Locale;\n canonicalPath: string;\n originalUrl?: string;\n }) => {\n // If we don't prefix the default locale AND the path locale IS the default → strip the prefix\n if (!prefixDefault && pathLocale === defaultLocale) {\n const { path: targetLocalizedPath } = resolveLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n\n // Construct path without prefix\n const cleanBasePath = basePath.startsWith('/')\n ? basePath\n : `/${basePath}`;\n const normalizedBasePath = cleanBasePath.endsWith('/')\n ? cleanBasePath.slice(0, -1)\n : cleanBasePath;\n\n let finalPath = targetLocalizedPath;\n if (finalPath.startsWith('/')) finalPath = finalPath.slice(1);\n\n const fullPath = `${normalizedBasePath}/${finalPath}`.replace(\n /\\/+/g,\n '/'\n );\n\n // Persist the explicitly-requested default locale. Stripping the prefix\n // (e.g. /es → /) drops the only locale signal from the URL, so without\n // this the follow-up request to the canonical path would fall back to\n // Accept-Language detection and could resolve a different locale.\n return redirectUrl(req, res, fullPath + (searchParams ?? ''), {\n originalUrl,\n persistLocale: pathLocale,\n });\n }\n\n // If we do prefix the default or pathLocale !== default, keep as-is\n // but rewrite to canonical internally.\n // Never emit a trailing slash (`/fr/` for the bare `/fr` URL): rewriting\n // `/fr` to `/fr/` makes the framework issue a trailing-slash normalisation\n // redirect back to `/fr`, which this proxy rewrites again — an infinite\n // redirect loop. `getInternalPath` collapses the root path to `/${locale}`.\n const internalUrl = getInternalPath(canonicalPath, pathLocale);\n const newPath = searchParams\n ? `${internalUrl}${searchParams}`\n : internalUrl;\n\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n };\n\n return (req, res, next) => {\n // Parse original URL for path and query\n const { pathname, search } = splitRequestTarget(req.url ?? '/');\n // Decoded so the rewrite rules — written with the locale's own characters —\n // match; `rewriteUrl` re-encodes whatever is written back to `req.url`.\n const originalPath = decodePathname(pathname || '/');\n const searchParams = search;\n\n // Check if there's a locale prefix in the path FIRST\n const pathLocale = getPathLocale(originalPath);\n\n // Bypass special Vite/server endpoints and node_modules\n if (\n // Custom ignore function\n (ignore?.(req) ?? false) ||\n originalPath.startsWith('/node_modules') ||\n /**\n * /^@vite/ # HMR client and helpers\n * /^@fs/ # file-system import serving\n * /^@id/ # virtual module ids\n * /^@tanstack/start-router-manifest # Tanstack Start Router manifest\n */\n originalPath.startsWith('/@') ||\n /**\n * /^__vite_ping$ # health ping\n * /^__open-in-editor$\n * /^__manifest$ # Remix/RR7 lazyRouteDiscovery\n */\n originalPath.startsWith('/_')\n ) {\n return next();\n }\n\n // Static file requests (e.g. /assets/video.mp4): bypass locale routing.\n // If the URL carries a locale prefix (e.g. /fr/assets/video.mp4),\n // rewrite the request internally to the unprefixed path (/assets/video.mp4)\n // so the file can be served correctly from the public directory.\n if (originalPath.match(/\\.[a-zA-Z0-9]+$/)) {\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n req.url = encodePathname(`${pathWithoutLocale}${searchParams}`);\n }\n return next();\n }\n\n // Attempt to read the locale from storage (cookies, localStorage, etc.)\n const storageLocale = getStorageLocale(req);\n\n // CRITICAL FIX: If there's a valid pathLocale, it takes precedence over storage\n // This prevents race conditions when cookies are stale during locale switches\n const effectiveStorageLocale =\n pathLocale && supportedLocales.includes(pathLocale)\n ? pathLocale\n : storageLocale;\n\n // Store original URL for redirect tracking\n const originalUrl = req.url;\n\n // Domain routing: if the path locale is mapped to a different domain, redirect there.\n // e.g. intlayer.org/zh/about → https://intlayer.zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n pathLocale &&\n domains\n ) {\n const localeDomain = domains[pathLocale as keyof typeof domains];\n if (localeDomain) {\n const reqHost = getRequestHostname(req);\n const domainHost = getDomainHostname(localeDomain);\n if (domainHost !== reqHost) {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n const targetOrigin = getDomainOrigin(localeDomain);\n // Domain mapping is stable config, so a cacheable 301 is intended\n // here — unlike locale-detection redirects, which use 302.\n redirectUrl(req, res, `${targetOrigin}${rawPath}${searchParams}`, {\n reason: 'domain-routing',\n originalUrl,\n status: 301,\n allowCrossOrigin: true,\n });\n return;\n }\n }\n }\n\n // Domain routing: if the current hostname is exclusively mapped to one locale,\n // treat it as that locale without a URL prefix.\n // e.g. intlayer.zh/about → internally rewrite to /zh/about\n if (\n process.env.INTLAYER_ROUTING_DOMAINS !== 'false' &&\n !noPrefix &&\n !pathLocale\n ) {\n const reqHost = getRequestHostname(req);\n const domainLocale = getLocaleFromDomain(reqHost, domains);\n if (domainLocale) {\n const canonicalPath = getCanonicalPath(\n originalPath,\n domainLocale,\n rewriteRules\n );\n\n // Default locale without forced prefixing: pass the canonical path\n // through unchanged, mirroring handleMissingPathLocale. Rewriting `/`\n // to `/en/` makes TanStack Start issue a trailing-slash normalisation\n // redirect (/en/ → /en), whose prefix the proxy then strips back to /,\n // creating an infinite redirect loop.\n if (!prefixDefault && domainLocale === defaultLocale) {\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n `${canonicalPath}${searchParams}`,\n domainLocale\n );\n return next();\n }\n\n // Never emit a trailing slash (`/zh/`): the framework would redirect\n // it to `/zh`, which this proxy rewrites to `/zh/` again — a loop.\n const internalPath = getInternalPath(canonicalPath, domainLocale);\n\n rewriteUrl(\n req as Connect.IncomingMessage,\n res,\n searchParams ? `${internalPath}${searchParams}` : internalPath,\n domainLocale\n );\n return next();\n }\n }\n\n if (noPrefix) {\n handleNoPrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n return;\n }\n\n handlePrefix({\n req: req as Connect.IncomingMessage,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n };\n};\n"],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@ import { createPrimaryInstanceGuard } from "./dedupePlugin.mjs";
|
|
|
2
2
|
import { createProxyHandler } from "./intlayerProxyHandler.mjs";
|
|
3
3
|
import { getAppLogger } from "@intlayer/config/logger";
|
|
4
4
|
import { getConfiguration } from "@intlayer/config/node";
|
|
5
|
+
import { normalizePath } from "@intlayer/config/utils";
|
|
5
6
|
import { formatProxyEnabledMessage, isProxyStorageLocaleEnabled, resolveProxyMode } from "@intlayer/core/localization";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
@@ -14,6 +15,27 @@ const PROXY_PLUGIN_NAME = "vite-intlayer-middleware-plugin";
|
|
|
14
15
|
*/
|
|
15
16
|
const NITRO_PREVIEW_PLUGIN_NAME = "nitro:preview";
|
|
16
17
|
/**
|
|
18
|
+
* Detects a throwaway Vite server that never receives a browser request.
|
|
19
|
+
*
|
|
20
|
+
* Astro's `sync` step (run by `astro build`) boots a middleware-mode server
|
|
21
|
+
* with HMR and its WebSocket server disabled, only to load the content config,
|
|
22
|
+
* and tears it down right after. Announcing the proxy there reads as if the
|
|
23
|
+
* static build were locale-routed, so the banner is skipped for such servers.
|
|
24
|
+
*/
|
|
25
|
+
const isThrowawayServer = ({ config: { server } }) => Boolean(server.middlewareMode) && server.hmr === false && server.ws === false;
|
|
26
|
+
/**
|
|
27
|
+
* Restricts a proxy middleware to its redirects: a request the proxy lets
|
|
28
|
+
* through continues with the URL the browser sent, any internal rewrite
|
|
29
|
+
* undone, for a downstream server that runs the full proxy itself.
|
|
30
|
+
*/
|
|
31
|
+
const createRedirectOnlyMiddleware = (handler) => (req, res, next) => {
|
|
32
|
+
const originalUrl = req.url;
|
|
33
|
+
handler(req, res, () => {
|
|
34
|
+
req.url = originalUrl;
|
|
35
|
+
next();
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
17
39
|
* Creates a standalone, framework-agnostic locale-routing middleware.
|
|
18
40
|
*
|
|
19
41
|
* It loads the Intlayer configuration and hands it to the runtime handler in
|
|
@@ -81,50 +103,90 @@ const createIntlayerProxyHandler = (options) => createProxyHandler({
|
|
|
81
103
|
*
|
|
82
104
|
* @deprecated Since Intlayer v9, `intlayerProxy()` is bundled directly into the `intlayer()` plugin and enabled by default through the `routing.enableProxy` option (unset by default, which selects auto mode). Registering it separately as shown below is now optional.
|
|
83
105
|
*/
|
|
106
|
+
/**
|
|
107
|
+
* Absolute path of the built Nitro middleware, relative to this module.
|
|
108
|
+
*
|
|
109
|
+
* Nitro inlines the path as an import specifier in its virtual routing module,
|
|
110
|
+
* so Windows backslashes must become forward slashes or the import breaks.
|
|
111
|
+
*
|
|
112
|
+
* @param moduleUrl - `import.meta.url` of the calling module
|
|
113
|
+
* @param toPath - `file:` URL to path converter (overridable for tests)
|
|
114
|
+
*/
|
|
115
|
+
const resolveNitroHandlerPath = (moduleUrl, toPath = fileURLToPath) => normalizePath(toPath(new URL("./intlayerNitroHandler.mjs", moduleUrl)));
|
|
84
116
|
const intlayerProxy = (options) => {
|
|
85
|
-
const handler = createIntlayerProxyHandler({
|
|
86
|
-
...options,
|
|
87
|
-
isDevServer: true
|
|
88
|
-
});
|
|
89
117
|
const intlayerConfig = getConfiguration(options?.configOptions);
|
|
90
118
|
const logger = getAppLogger(intlayerConfig);
|
|
91
|
-
const
|
|
119
|
+
const proxyMode = resolveProxyMode(intlayerConfig.routing.enableProxy);
|
|
120
|
+
const createHandler = (isDevServer) => createProxyHandler({
|
|
121
|
+
configuration: intlayerConfig,
|
|
122
|
+
ignore: options?.ignore,
|
|
123
|
+
isDevServer
|
|
124
|
+
});
|
|
92
125
|
/**
|
|
93
126
|
* Logs that the proxy is serving requests, spelling out when auto mode has
|
|
94
127
|
* suppressed the stored locale so the reported state matches the behaviour.
|
|
95
128
|
*/
|
|
96
|
-
const logProxyEnabled = () => logger(formatProxyEnabledMessage(
|
|
129
|
+
const logProxyEnabled = (isDevServer) => logger(formatProxyEnabledMessage(!isProxyStorageLocaleEnabled(proxyMode, isDevServer)), { level: "info" });
|
|
97
130
|
const guard = createPrimaryInstanceGuard(PROXY_PLUGIN_NAME);
|
|
98
131
|
let isNitroServingPreview = false;
|
|
132
|
+
/**
|
|
133
|
+
* Nitro module injected automatically by `nitro/vite`.
|
|
134
|
+
*
|
|
135
|
+
* When a Vite plugin carries a `.nitro` property, `nitro/vite` pushes it into
|
|
136
|
+
* `nitroConfig.modules` during the build phase. The module's `setup` hook adds
|
|
137
|
+
* our locale-routing handler to Nitro's server pipeline, making locale detection
|
|
138
|
+
* work in production SSR builds (TanStack Start, Nuxt, etc.) without any extra
|
|
139
|
+
* user configuration.
|
|
140
|
+
*
|
|
141
|
+
* @see https://github.com/nitrojs/nitro (nitro/vite source, line ~402)
|
|
142
|
+
*/
|
|
143
|
+
const nitroModule = {
|
|
144
|
+
name: "intlayer-proxy",
|
|
145
|
+
setup(nitro) {
|
|
146
|
+
if (nitro.options.dev) return;
|
|
147
|
+
const handlerPath = resolveNitroHandlerPath(import.meta.url);
|
|
148
|
+
if (nitro.options.handlers.some((existingHandler) => existingHandler.handler === handlerPath)) return;
|
|
149
|
+
nitro.options.handlers.push({
|
|
150
|
+
route: "/**",
|
|
151
|
+
handler: handlerPath,
|
|
152
|
+
middleware: true
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
let hasAnnouncedBuild = false;
|
|
157
|
+
/**
|
|
158
|
+
* Announces the proxy at build time. A build never reads the stored locale:
|
|
159
|
+
* it only prerenders pages served to every visitor, and a static output has
|
|
160
|
+
* no server to redirect from until `preview` or a host mounts the handler.
|
|
161
|
+
*/
|
|
162
|
+
const announceBuild = () => {
|
|
163
|
+
if (hasAnnouncedBuild) return;
|
|
164
|
+
hasAnnouncedBuild = true;
|
|
165
|
+
logger(formatProxyEnabledMessage(true, "build"), { level: "info" });
|
|
166
|
+
};
|
|
99
167
|
const plugin = {
|
|
100
168
|
name: PROXY_PLUGIN_NAME,
|
|
169
|
+
enforce: "pre",
|
|
101
170
|
configResolved: (config) => {
|
|
102
171
|
guard.resolve(config);
|
|
103
172
|
isNitroServingPreview = config.plugins.some((registeredPlugin) => registeredPlugin.name === NITRO_PREVIEW_PLUGIN_NAME);
|
|
173
|
+
if (guard.isPrimary && config.command === "build") announceBuild();
|
|
104
174
|
},
|
|
105
|
-
nitro:
|
|
106
|
-
name: "intlayer-proxy",
|
|
107
|
-
setup(nitro) {
|
|
108
|
-
if (nitro.options.dev) return;
|
|
109
|
-
const handlerPath = fileURLToPath(new URL("./intlayerNitroHandler.mjs", import.meta.url));
|
|
110
|
-
if (nitro.options.handlers.some((existingHandler) => existingHandler.handler === handlerPath)) return;
|
|
111
|
-
nitro.options.handlers.push({
|
|
112
|
-
route: "/**",
|
|
113
|
-
handler: handlerPath,
|
|
114
|
-
middleware: true
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
},
|
|
175
|
+
nitro: nitroModule,
|
|
118
176
|
configureServer: (server) => {
|
|
119
177
|
if (!guard.isPrimary) return;
|
|
120
|
-
logProxyEnabled();
|
|
121
|
-
server.middlewares.use(
|
|
178
|
+
if (!isThrowawayServer(server)) logProxyEnabled(true);
|
|
179
|
+
server.middlewares.use(createHandler(true));
|
|
122
180
|
},
|
|
123
181
|
configurePreviewServer: (server) => {
|
|
124
182
|
if (!guard.isPrimary) return;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
183
|
+
const previewHandler = createHandler(false);
|
|
184
|
+
if (!isNitroServingPreview) {
|
|
185
|
+
logProxyEnabled(false);
|
|
186
|
+
server.middlewares.use(previewHandler);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
server.middlewares.use(createRedirectOnlyMiddleware(previewHandler));
|
|
128
190
|
}
|
|
129
191
|
};
|
|
130
192
|
guard.setPlugin(plugin);
|
|
@@ -158,5 +220,5 @@ const intlayerMiddleware = intlayerProxy;
|
|
|
158
220
|
const intLayerMiddlewarePlugin = intlayerProxy;
|
|
159
221
|
|
|
160
222
|
//#endregion
|
|
161
|
-
export { createIntlayerProxyHandler, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy };
|
|
223
|
+
export { createIntlayerProxyHandler, createRedirectOnlyMiddleware, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy, resolveNitroHandlerPath };
|
|
162
224
|
//# sourceMappingURL=intlayerProxyPlugin.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerProxyPlugin.mjs","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"sourcesContent":["import type { IncomingMessage } from 'node:http';\nimport { fileURLToPath } from 'node:url';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport {\n formatProxyEnabledMessage,\n isProxyStorageLocaleEnabled,\n resolveProxyMode,\n} from '@intlayer/core/localization';\n/* @ts-ignore - Vite types error */\nimport type { Plugin } from 'vite';\nimport { createPrimaryInstanceGuard } from './dedupePlugin';\nimport {\n createProxyHandler,\n type NodeMiddleware,\n} from './intlayerProxyHandler';\n\nconst PROXY_PLUGIN_NAME = 'vite-intlayer-middleware-plugin';\n\n/**\n * Name of the Vite plugin `nitro/vite` registers in preview mode. It mounts the\n * built Nitro server — which already carries the Intlayer proxy as a Nitro\n * middleware — inside the Vite preview server.\n */\nconst NITRO_PREVIEW_PLUGIN_NAME = 'nitro:preview';\n\nexport type IntlayerProxyPluginOptions = {\n /**\n * A function that allows you to ignore specific requests from the intlayer proxy.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * plugins: [ intlayerProxy({ ignore: (req) => req.url?.startsWith('/api') }) ],\n * });\n * ```\n *\n * @param req - The incoming request.\n * @returns A boolean value indicating whether to ignore the request.\n */\n ignore?: (req: IncomingMessage) => boolean | undefined;\n /**\n * Optional Intlayer configuration overrides forwarded to `getConfiguration`.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * plugins: [ intlayerProxy({ configOptions: { override: { ... } } }) ],\n * });\n * ```\n */\n configOptions?: GetConfigurationOptions;\n /**\n * Whether a development or preview server is serving the app.\n *\n * Set internally by the plugin from `configureServer` /\n * `configurePreviewServer`. It only matters in the proxy's auto mode, where\n * a dev server keeps locale routing URL-driven by ignoring the stored locale\n * as a redirect source.\n *\n * Defaults to `false` so that mounting `createIntlayerProxyHandler()`\n * manually — the documented production Nitro setup — keeps full behaviour.\n *\n * @default false\n */\n isDevServer?: boolean;\n};\n\n/**\n * Creates a standalone, framework-agnostic locale-routing middleware.\n *\n * It loads the Intlayer configuration and hands it to the runtime handler in\n * `intlayerProxyHandler`, so it can be used in every environment:\n *\n * - **Dev**: wired up automatically by `intlayerProxy` via `configureServer`\n * - **Preview**: wired up automatically by `intlayerProxy` via `configurePreviewServer`\n * - **Production (Nitro / TanStack Start)**: create `server/middleware/intlayerProxy.ts`:\n *\n * @example\n * ```ts\n * // server/middleware/intlayerProxy.ts\n * import { fromNodeMiddleware } from 'h3';\n * import { createIntlayerProxyHandler } from 'vite-intlayer';\n *\n * export default fromNodeMiddleware(createIntlayerProxyHandler());\n * ```\n *\n * @param options - Plugin-specific options, such as path ignoring and Intlayer configuration overrides.\n * @returns A Connect-compatible `(req, res, next) => void` middleware.\n */\nexport const createIntlayerProxyHandler = (\n options?: IntlayerProxyPluginOptions\n): NodeMiddleware =>\n createProxyHandler({\n configuration: getConfiguration(options?.configOptions),\n ignore: options?.ignore,\n isDevServer: options?.isDevServer ?? false,\n });\n\n/**\n * Vite plugin that provides locale-based routing middleware for **all environments**:\n * development, preview, and production SSR (Nitro / TanStack Start).\n *\n * - **Dev** (`vite dev`): registered via `configureServer`.\n * - **Preview** (`vite preview`): registered via `configurePreviewServer`.\n * - **Production Nitro** (`vite build`): automatically injected via the `.nitro` module\n * property that `nitro/vite` reads and pushes into `nitroConfig.modules`. The module\n * registers `intlayerNitroHandler` as a Nitro server middleware — no extra user config\n * needed.\n *\n * If you need custom config options or an `ignore` predicate in production, bypass\n * auto-injection and create a server middleware file manually:\n *\n * ```ts\n * // server/middleware/intlayerProxy.ts\n * import { fromNodeMiddleware } from 'h3';\n * import { createIntlayerProxyHandler } from 'vite-intlayer';\n *\n * export default fromNodeMiddleware(\n * createIntlayerProxyHandler({\n * ignore: (req) => req.url?.startsWith('/api'),\n * configOptions: myConfig,\n * })\n * );\n * ```\n *\n * @param options - Plugin-specific options, like ignoring certain paths and Intlayer configuration overrides.\n * @returns A Vite plugin.\n *\n * @example\n * ```ts\n * import { intlayerProxy } from 'vite-intlayer';\n *\n * export default defineConfig({\n * plugins: [intlayerProxy()],\n * });\n * ```\n *\n * @deprecated Since Intlayer v9, `intlayerProxy()` is bundled directly into the `intlayer()` plugin and enabled by default through the `routing.enableProxy` option (unset by default, which selects auto mode). Registering it separately as shown below is now optional.\n */\nexport const intlayerProxy = (options?: IntlayerProxyPluginOptions): Plugin => {\n // Dev and preview servers run the same handler; both are \"dev servers\" as far\n // as auto mode is concerned, so a single instance covers both hooks.\n const handler = createIntlayerProxyHandler({ ...options, isDevServer: true });\n const intlayerConfig = getConfiguration(options?.configOptions);\n const logger = getAppLogger(intlayerConfig);\n\n // Both hooks below serve a dev or preview server, hence the hard-coded true.\n const isStorageLocaleSuppressed = !isProxyStorageLocaleEnabled(\n resolveProxyMode(intlayerConfig.routing.enableProxy),\n true\n );\n\n /**\n * Logs that the proxy is serving requests, spelling out when auto mode has\n * suppressed the stored locale so the reported state matches the behaviour.\n */\n const logProxyEnabled = () =>\n logger(formatProxyEnabledMessage(isStorageLocaleSuppressed), {\n level: 'info',\n });\n\n // Ensures the proxy registers its middleware only once, even when it is\n // registered both via `intlayer()` (which now bundles it) and a manual\n // `intlayerProxy()` call.\n const guard = createPrimaryInstanceGuard(PROXY_PLUGIN_NAME);\n\n // Set during `configResolved`: `nitro/vite` serves the *built* Nitro server\n // from inside the preview server, and that build already carries the proxy as\n // a Nitro middleware (see `nitroModule` below).\n let isNitroServingPreview = false;\n\n /**\n * Nitro module injected automatically by `nitro/vite`.\n *\n * When a Vite plugin carries a `.nitro` property, `nitro/vite` pushes it into\n * `nitroConfig.modules` during the build phase. The module's `setup` hook adds\n * our locale-routing handler to Nitro's server pipeline, making locale detection\n * work in production SSR builds (TanStack Start, Nuxt, etc.) without any extra\n * user configuration.\n *\n * @see https://github.com/nitrojs/nitro (nitro/vite source, line ~402)\n */\n const nitroModule = {\n name: 'intlayer-proxy',\n setup(nitro: {\n options: {\n dev: boolean;\n handlers: {\n route: string;\n handler: string;\n middleware: boolean;\n }[];\n };\n }) {\n // In dev mode, locale routing is already handled by configureServer (Vite dev server).\n // The Nitro dev server uses h3 v2's Web Fetch API event model which is incompatible\n // with fromNodeMiddleware (h3 v1) and would cause double-execution anyway.\n // Only inject for production builds where Nitro is the actual HTTP server.\n if (nitro.options.dev) return;\n\n const handlerPath = fileURLToPath(\n new URL('./intlayerNitroHandler.mjs', import.meta.url)\n );\n\n // Skip if an identical handler was already registered by another instance\n // (e.g. both `intlayer()` and a manual `intlayerProxy()`).\n const alreadyRegistered = nitro.options.handlers.some(\n (existingHandler) => existingHandler.handler === handlerPath\n );\n if (alreadyRegistered) return;\n\n nitro.options.handlers.push({\n route: '/**',\n handler: handlerPath,\n middleware: true,\n });\n },\n };\n\n const plugin = {\n name: PROXY_PLUGIN_NAME,\n // Decide whether this is the primary instance before registering middleware.\n configResolved: (config: { plugins: readonly { name: string }[] }) => {\n guard.resolve(config);\n isNitroServingPreview = config.plugins.some(\n (registeredPlugin) =>\n registeredPlugin.name === NITRO_PREVIEW_PLUGIN_NAME\n );\n },\n // Injected into nitroConfig.modules by the `nitro/vite` plugin so the\n // locale-routing middleware is registered in the production Nitro server.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n nitro: nitroModule as any,\n // Vite dev server\n configureServer: (server) => {\n if (!guard.isPrimary) return;\n logProxyEnabled();\n server.middlewares.use(handler);\n },\n // Vite preview server\n configurePreviewServer: (server) => {\n if (!guard.isPrimary) return;\n // With Nitro, the preview server forwards every request to the built\n // Nitro server, whose pipeline already starts with this proxy. Adding a\n // second layer here would run locale resolution twice on the same\n // request: the first pass rewrites `req.url` (and the locale request\n // header), the second pass reads that rewritten URL as if it came from\n // the browser and can redirect it back — the redirect ping-pong that\n // shows up as \"max redirects reached\" while prerendering.\n if (isNitroServingPreview) return;\n logProxyEnabled();\n server.middlewares.use(handler);\n },\n } as Plugin;\n\n // Register the plugin object so the dedupe guard can identify the primary\n // instance by reference during `configResolved`.\n guard.setPlugin(plugin);\n\n return plugin;\n};\n\n/**\n * @deprecated Rename to intlayerProxy instead\n *\n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerMiddleware() ],\n * });\n * ```\n */\nexport const intlayerMiddleware = intlayerProxy;\n\n/**\n * @deprecated Rename to intlayerProxy instead\n *\n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerMiddleware() ],\n * });\n * ```\n */\nexport const intLayerMiddlewarePlugin = intlayerProxy;\n"],"mappings":";;;;;;;;AAoBA,MAAM,oBAAoB;;;;;;AAO1B,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;AAkElC,MAAa,8BACX,YAEA,mBAAmB;CACjB,eAAe,iBAAiB,SAAS,aAAa;CACtD,QAAQ,SAAS;CACjB,aAAa,SAAS,eAAe;AACvC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CH,MAAa,iBAAiB,YAAiD;CAG7E,MAAM,UAAU,2BAA2B;EAAE,GAAG;EAAS,aAAa;CAAK,CAAC;CAC5E,MAAM,iBAAiB,iBAAiB,SAAS,aAAa;CAC9D,MAAM,SAAS,aAAa,cAAc;CAG1C,MAAM,4BAA4B,CAAC,4BACjC,iBAAiB,eAAe,QAAQ,WAAW,GACnD,IACF;;;;;CAMA,MAAM,wBACJ,OAAO,0BAA0B,yBAAyB,GAAG,EAC3D,OAAO,OACT,CAAC;CAKH,MAAM,QAAQ,2BAA2B,iBAAiB;CAK1D,IAAI,wBAAwB;CAkD5B,MAAM,SAAS;EACb,MAAM;EAEN,iBAAiB,WAAqD;GACpE,MAAM,QAAQ,MAAM;GACpB,wBAAwB,OAAO,QAAQ,MACpC,qBACC,iBAAiB,SAAS,yBAC9B;EACF;EAIA,OAAO;GAjDP,MAAM;GACN,MAAM,OASH;IAKD,IAAI,MAAM,QAAQ,KAAK;IAEvB,MAAM,cAAc,cAClB,IAAI,IAAI,8BAA8B,YAAY,GAAG,CACvD;IAOA,IAH0B,MAAM,QAAQ,SAAS,MAC9C,oBAAoB,gBAAgB,YAAY,WAE/B,GAAG;IAEvB,MAAM,QAAQ,SAAS,KAAK;KAC1B,OAAO;KACP,SAAS;KACT,YAAY;IACd,CAAC;GACH;EAgBiB;EAEjB,kBAAkB,WAAW;GAC3B,IAAI,CAAC,MAAM,WAAW;GACtB,gBAAgB;GAChB,OAAO,YAAY,IAAI,OAAO;EAChC;EAEA,yBAAyB,WAAW;GAClC,IAAI,CAAC,MAAM,WAAW;GAQtB,IAAI,uBAAuB;GAC3B,gBAAgB;GAChB,OAAO,YAAY,IAAI,OAAO;EAChC;CACF;CAIA,MAAM,UAAU,MAAM;CAEtB,OAAO;AACT;;;;;;;;;;;;;AAcA,MAAa,qBAAqB;;;;;;;;;;;;;AAclC,MAAa,2BAA2B"}
|
|
1
|
+
{"version":3,"file":"intlayerProxyPlugin.mjs","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"sourcesContent":["import type { IncomingMessage } from 'node:http';\nimport { fileURLToPath } from 'node:url';\nimport { getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport { normalizePath } from '@intlayer/config/utils';\nimport {\n formatProxyEnabledMessage,\n isProxyStorageLocaleEnabled,\n resolveProxyMode,\n} from '@intlayer/core/localization';\n/* @ts-ignore - Vite types error */\nimport type { Plugin } from 'vite';\nimport { createPrimaryInstanceGuard } from './dedupePlugin';\nimport {\n createProxyHandler,\n type NodeMiddleware,\n} from './intlayerProxyHandler';\n\nconst PROXY_PLUGIN_NAME = 'vite-intlayer-middleware-plugin';\n\n/**\n * Name of the Vite plugin `nitro/vite` registers in preview mode. It mounts the\n * built Nitro server — which already carries the Intlayer proxy as a Nitro\n * middleware — inside the Vite preview server.\n */\nconst NITRO_PREVIEW_PLUGIN_NAME = 'nitro:preview';\n\n/**\n * Server config of a Vite dev server, narrowed to the fields inspected below.\n */\ntype DevServerLike = {\n config: { server: { middlewareMode?: unknown; hmr?: unknown; ws?: unknown } };\n};\n\n/**\n * Detects a throwaway Vite server that never receives a browser request.\n *\n * Astro's `sync` step (run by `astro build`) boots a middleware-mode server\n * with HMR and its WebSocket server disabled, only to load the content config,\n * and tears it down right after. Announcing the proxy there reads as if the\n * static build were locale-routed, so the banner is skipped for such servers.\n */\nconst isThrowawayServer = ({ config: { server } }: DevServerLike): boolean =>\n Boolean(server.middlewareMode) && server.hmr === false && server.ws === false;\n\n/**\n * Restricts a proxy middleware to its redirects: a request the proxy lets\n * through continues with the URL the browser sent, any internal rewrite\n * undone, for a downstream server that runs the full proxy itself.\n */\nexport const createRedirectOnlyMiddleware =\n (handler: NodeMiddleware): NodeMiddleware =>\n (req, res, next) => {\n const originalUrl = req.url;\n\n handler(req, res, () => {\n req.url = originalUrl;\n next();\n });\n };\n\nexport type IntlayerProxyPluginOptions = {\n /**\n * A function that allows you to ignore specific requests from the intlayer proxy.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * plugins: [ intlayerProxy({ ignore: (req) => req.url?.startsWith('/api') }) ],\n * });\n * ```\n *\n * @param req - The incoming request.\n * @returns A boolean value indicating whether to ignore the request.\n */\n ignore?: (req: IncomingMessage) => boolean | undefined;\n /**\n * Optional Intlayer configuration overrides forwarded to `getConfiguration`.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * plugins: [ intlayerProxy({ configOptions: { override: { ... } } }) ],\n * });\n * ```\n */\n configOptions?: GetConfigurationOptions;\n /**\n * Whether a development server is serving the app.\n *\n * Set internally by the plugin from `configureServer`. It only matters in\n * the proxy's auto mode, where a dev server keeps locale routing URL-driven\n * by ignoring the stored locale as a redirect source.\n *\n * A preview server (`vite preview`) serves the production build, so it is\n * *not* a dev server: the stored locale drives redirects there, as it does\n * on the deployed app.\n *\n * Defaults to `false` so that mounting `createIntlayerProxyHandler()`\n * manually — the documented production Nitro setup — keeps full behaviour.\n *\n * @default false\n */\n isDevServer?: boolean;\n};\n\n/**\n * Creates a standalone, framework-agnostic locale-routing middleware.\n *\n * It loads the Intlayer configuration and hands it to the runtime handler in\n * `intlayerProxyHandler`, so it can be used in every environment:\n *\n * - **Dev**: wired up automatically by `intlayerProxy` via `configureServer`\n * - **Preview**: wired up automatically by `intlayerProxy` via `configurePreviewServer`\n * - **Production (Nitro / TanStack Start)**: create `server/middleware/intlayerProxy.ts`:\n *\n * @example\n * ```ts\n * // server/middleware/intlayerProxy.ts\n * import { fromNodeMiddleware } from 'h3';\n * import { createIntlayerProxyHandler } from 'vite-intlayer';\n *\n * export default fromNodeMiddleware(createIntlayerProxyHandler());\n * ```\n *\n * @param options - Plugin-specific options, such as path ignoring and Intlayer configuration overrides.\n * @returns A Connect-compatible `(req, res, next) => void` middleware.\n */\nexport const createIntlayerProxyHandler = (\n options?: IntlayerProxyPluginOptions\n): NodeMiddleware =>\n createProxyHandler({\n configuration: getConfiguration(options?.configOptions),\n ignore: options?.ignore,\n isDevServer: options?.isDevServer ?? false,\n });\n\n/**\n * Vite plugin that provides locale-based routing middleware for **all environments**:\n * development, preview, and production SSR (Nitro / TanStack Start).\n *\n * - **Dev** (`vite dev`): registered via `configureServer`.\n * - **Preview** (`vite preview`): registered via `configurePreviewServer`.\n * - **Production Nitro** (`vite build`): automatically injected via the `.nitro` module\n * property that `nitro/vite` reads and pushes into `nitroConfig.modules`. The module\n * registers `intlayerNitroHandler` as a Nitro server middleware — no extra user config\n * needed.\n *\n * If you need custom config options or an `ignore` predicate in production, bypass\n * auto-injection and create a server middleware file manually:\n *\n * ```ts\n * // server/middleware/intlayerProxy.ts\n * import { fromNodeMiddleware } from 'h3';\n * import { createIntlayerProxyHandler } from 'vite-intlayer';\n *\n * export default fromNodeMiddleware(\n * createIntlayerProxyHandler({\n * ignore: (req) => req.url?.startsWith('/api'),\n * configOptions: myConfig,\n * })\n * );\n * ```\n *\n * @param options - Plugin-specific options, like ignoring certain paths and Intlayer configuration overrides.\n * @returns A Vite plugin.\n *\n * @example\n * ```ts\n * import { intlayerProxy } from 'vite-intlayer';\n *\n * export default defineConfig({\n * plugins: [intlayerProxy()],\n * });\n * ```\n *\n * @deprecated Since Intlayer v9, `intlayerProxy()` is bundled directly into the `intlayer()` plugin and enabled by default through the `routing.enableProxy` option (unset by default, which selects auto mode). Registering it separately as shown below is now optional.\n */\n/**\n * Absolute path of the built Nitro middleware, relative to this module.\n *\n * Nitro inlines the path as an import specifier in its virtual routing module,\n * so Windows backslashes must become forward slashes or the import breaks.\n *\n * @param moduleUrl - `import.meta.url` of the calling module\n * @param toPath - `file:` URL to path converter (overridable for tests)\n */\nexport const resolveNitroHandlerPath = (\n moduleUrl: string,\n toPath: (url: URL) => string = fileURLToPath\n): string =>\n normalizePath(toPath(new URL('./intlayerNitroHandler.mjs', moduleUrl)));\n\nexport const intlayerProxy = (options?: IntlayerProxyPluginOptions): Plugin => {\n const intlayerConfig = getConfiguration(options?.configOptions);\n const logger = getAppLogger(intlayerConfig);\n const proxyMode = resolveProxyMode(intlayerConfig.routing.enableProxy);\n\n // The dev server is the only place auto mode suppresses the stored locale.\n // The preview server serves the production build (`vite build` output), so\n // it runs the production handler: a locale cookie must redirect there\n // exactly as it will once deployed, otherwise `vite preview` cannot be used\n // to check that behaviour. Built on demand: a build mounts neither.\n const createHandler = (isDevServer: boolean): NodeMiddleware =>\n createProxyHandler({\n configuration: intlayerConfig,\n ignore: options?.ignore,\n isDevServer,\n });\n\n /**\n * Logs that the proxy is serving requests, spelling out when auto mode has\n * suppressed the stored locale so the reported state matches the behaviour.\n */\n const logProxyEnabled = (isDevServer: boolean) =>\n logger(\n formatProxyEnabledMessage(\n !isProxyStorageLocaleEnabled(proxyMode, isDevServer)\n ),\n { level: 'info' }\n );\n\n // Ensures the proxy registers its middleware only once, even when it is\n // registered both via `intlayer()` (which now bundles it) and a manual\n // `intlayerProxy()` call.\n const guard = createPrimaryInstanceGuard(PROXY_PLUGIN_NAME);\n\n // Set during `configResolved`: `nitro/vite` serves the *built* Nitro server\n // from inside the preview server, and that build already carries the proxy as\n // a Nitro middleware (see `nitroModule` below).\n let isNitroServingPreview = false;\n\n /**\n * Nitro module injected automatically by `nitro/vite`.\n *\n * When a Vite plugin carries a `.nitro` property, `nitro/vite` pushes it into\n * `nitroConfig.modules` during the build phase. The module's `setup` hook adds\n * our locale-routing handler to Nitro's server pipeline, making locale detection\n * work in production SSR builds (TanStack Start, Nuxt, etc.) without any extra\n * user configuration.\n *\n * @see https://github.com/nitrojs/nitro (nitro/vite source, line ~402)\n */\n const nitroModule = {\n name: 'intlayer-proxy',\n setup(nitro: {\n options: {\n dev: boolean;\n handlers: {\n route: string;\n handler: string;\n middleware: boolean;\n }[];\n };\n }) {\n // In dev mode, locale routing is already handled by configureServer (Vite dev server).\n // The Nitro dev server uses h3 v2's Web Fetch API event model which is incompatible\n // with fromNodeMiddleware (h3 v1) and would cause double-execution anyway.\n // Only inject for production builds where Nitro is the actual HTTP server.\n if (nitro.options.dev) return;\n\n const handlerPath = resolveNitroHandlerPath(import.meta.url);\n\n // Skip if an identical handler was already registered by another instance\n // (e.g. both `intlayer()` and a manual `intlayerProxy()`).\n const alreadyRegistered = nitro.options.handlers.some(\n (existingHandler) => existingHandler.handler === handlerPath\n );\n if (alreadyRegistered) return;\n\n nitro.options.handlers.push({\n route: '/**',\n handler: handlerPath,\n middleware: true,\n });\n },\n };\n\n // `configResolved` runs once per Vite build the plugin instance takes part\n // in (Astro runs a server and a client build from the same instances).\n let hasAnnouncedBuild = false;\n\n /**\n * Announces the proxy at build time. A build never reads the stored locale:\n * it only prerenders pages served to every visitor, and a static output has\n * no server to redirect from until `preview` or a host mounts the handler.\n */\n const announceBuild = () => {\n if (hasAnnouncedBuild) return;\n hasAnnouncedBuild = true;\n logger(formatProxyEnabledMessage(true, 'build'), { level: 'info' });\n };\n\n const plugin = {\n name: PROXY_PLUGIN_NAME,\n // The preview layer must be mounted ahead of the `nitro:preview`\n // middleware, which answers every request itself, whatever the order of\n // the plugins in the user's Vite config.\n enforce: 'pre',\n // Decide whether this is the primary instance before registering middleware.\n configResolved: (config: {\n command: 'build' | 'serve';\n plugins: readonly { name: string }[];\n }) => {\n guard.resolve(config);\n isNitroServingPreview = config.plugins.some(\n (registeredPlugin) =>\n registeredPlugin.name === NITRO_PREVIEW_PLUGIN_NAME\n );\n if (guard.isPrimary && config.command === 'build') announceBuild();\n },\n // Injected into nitroConfig.modules by the `nitro/vite` plugin so the\n // locale-routing middleware is registered in the production Nitro server.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n nitro: nitroModule as any,\n // Vite dev server\n configureServer: (server) => {\n if (!guard.isPrimary) return;\n if (!isThrowawayServer(server)) logProxyEnabled(true);\n server.middlewares.use(createHandler(true));\n },\n // Vite preview server\n configurePreviewServer: (server) => {\n if (!guard.isPrimary) return;\n\n const previewHandler = createHandler(false);\n\n if (!isNitroServingPreview) {\n logProxyEnabled(false);\n server.middlewares.use(previewHandler);\n return;\n }\n\n // The built Nitro server announces the proxy itself when it loads, so\n // nothing is logged here.\n //\n // With Nitro, the preview server forwards every request to the built\n // Nitro server, whose pipeline already starts with this proxy — but\n // Nitro's preview serves the prerendered pages straight from disk,\n // ahead of that pipeline, so a stored locale would be ignored on exactly\n // the pages a visitor lands on. Redirects are therefore decided here,\n // and everything else reaches Nitro with its URL untouched: letting a\n // rewrite through would make the Nitro pass read `/en/about` as if the\n // browser had sent it and redirect it back to `/about` — the ping-pong\n // that shows up as \"max redirects reached\" while prerendering.\n server.middlewares.use(createRedirectOnlyMiddleware(previewHandler));\n },\n } as Plugin;\n\n // Register the plugin object so the dedupe guard can identify the primary\n // instance by reference during `configResolved`.\n guard.setPlugin(plugin);\n\n return plugin;\n};\n\n/**\n * @deprecated Rename to intlayerProxy instead\n *\n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerMiddleware() ],\n * });\n * ```\n */\nexport const intlayerMiddleware = intlayerProxy;\n\n/**\n * @deprecated Rename to intlayerProxy instead\n *\n * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.\n *\n * ```ts\n * // Example usage of the plugin in a Vite configuration\n * export default defineConfig({\n * plugins: [ intlayerMiddleware() ],\n * });\n * ```\n */\nexport const intLayerMiddlewarePlugin = intlayerProxy;\n"],"mappings":";;;;;;;;;AAqBA,MAAM,oBAAoB;;;;;;AAO1B,MAAM,4BAA4B;;;;;;;;;AAiBlC,MAAM,qBAAqB,EAAE,QAAQ,EAAE,eACrC,QAAQ,OAAO,cAAc,KAAK,OAAO,QAAQ,SAAS,OAAO,OAAO;;;;;;AAO1E,MAAa,gCACV,aACA,KAAK,KAAK,SAAS;CAClB,MAAM,cAAc,IAAI;CAExB,QAAQ,KAAK,WAAW;EACtB,IAAI,MAAM;EACV,KAAK;CACP,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;;;AAqEF,MAAa,8BACX,YAEA,mBAAmB;CACjB,eAAe,iBAAiB,SAAS,aAAa;CACtD,QAAQ,SAAS;CACjB,aAAa,SAAS,eAAe;AACvC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDH,MAAa,2BACX,WACA,SAA+B,kBAE/B,cAAc,OAAO,IAAI,IAAI,8BAA8B,SAAS,CAAC,CAAC;AAExE,MAAa,iBAAiB,YAAiD;CAC7E,MAAM,iBAAiB,iBAAiB,SAAS,aAAa;CAC9D,MAAM,SAAS,aAAa,cAAc;CAC1C,MAAM,YAAY,iBAAiB,eAAe,QAAQ,WAAW;CAOrE,MAAM,iBAAiB,gBACrB,mBAAmB;EACjB,eAAe;EACf,QAAQ,SAAS;EACjB;CACF,CAAC;;;;;CAMH,MAAM,mBAAmB,gBACvB,OACE,0BACE,CAAC,4BAA4B,WAAW,WAAW,CACrD,GACA,EAAE,OAAO,OAAO,CAClB;CAKF,MAAM,QAAQ,2BAA2B,iBAAiB;CAK1D,IAAI,wBAAwB;;;;;;;;;;;;CAa5B,MAAM,cAAc;EAClB,MAAM;EACN,MAAM,OASH;GAKD,IAAI,MAAM,QAAQ,KAAK;GAEvB,MAAM,cAAc,wBAAwB,YAAY,GAAG;GAO3D,IAH0B,MAAM,QAAQ,SAAS,MAC9C,oBAAoB,gBAAgB,YAAY,WAE/B,GAAG;GAEvB,MAAM,QAAQ,SAAS,KAAK;IAC1B,OAAO;IACP,SAAS;IACT,YAAY;GACd,CAAC;EACH;CACF;CAIA,IAAI,oBAAoB;;;;;;CAOxB,MAAM,sBAAsB;EAC1B,IAAI,mBAAmB;EACvB,oBAAoB;EACpB,OAAO,0BAA0B,MAAM,OAAO,GAAG,EAAE,OAAO,OAAO,CAAC;CACpE;CAEA,MAAM,SAAS;EACb,MAAM;EAIN,SAAS;EAET,iBAAiB,WAGX;GACJ,MAAM,QAAQ,MAAM;GACpB,wBAAwB,OAAO,QAAQ,MACpC,qBACC,iBAAiB,SAAS,yBAC9B;GACA,IAAI,MAAM,aAAa,OAAO,YAAY,SAAS,cAAc;EACnE;EAIA,OAAO;EAEP,kBAAkB,WAAW;GAC3B,IAAI,CAAC,MAAM,WAAW;GACtB,IAAI,CAAC,kBAAkB,MAAM,GAAG,gBAAgB,IAAI;GACpD,OAAO,YAAY,IAAI,cAAc,IAAI,CAAC;EAC5C;EAEA,yBAAyB,WAAW;GAClC,IAAI,CAAC,MAAM,WAAW;GAEtB,MAAM,iBAAiB,cAAc,KAAK;GAE1C,IAAI,CAAC,uBAAuB;IAC1B,gBAAgB,KAAK;IACrB,OAAO,YAAY,IAAI,cAAc;IACrC;GACF;GAcA,OAAO,YAAY,IAAI,6BAA6B,cAAc,CAAC;EACrE;CACF;CAIA,MAAM,UAAU,MAAM;CAEtB,OAAO;AACT;;;;;;;;;;;;;AAcA,MAAa,qBAAqB;;;;;;;;;;;;;AAclC,MAAa,2BAA2B"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createProxyHandler } from "./intlayerProxyHandler.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/intlayerWebProxyHandler.ts
|
|
4
|
+
/**
|
|
5
|
+
* Header names the proxy reads from the incoming request.
|
|
6
|
+
*/
|
|
7
|
+
const FORWARDED_REQUEST_HEADERS = [
|
|
8
|
+
"cookie",
|
|
9
|
+
"host",
|
|
10
|
+
"accept-language",
|
|
11
|
+
"x-forwarded-host",
|
|
12
|
+
"x-forwarded-proto",
|
|
13
|
+
"x-nitro-prerender"
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Runs a Node-style proxy middleware against a Web request.
|
|
17
|
+
*
|
|
18
|
+
* The proxy answers synchronously, so no promise is involved. Only the fields
|
|
19
|
+
* it actually touches are shimmed:
|
|
20
|
+
* - `req.url` / `req.headers` (mutable plain object: `setLocaleInStorageServer`
|
|
21
|
+
* writes the locale header back through it)
|
|
22
|
+
* - `res.writeHead` / `res.setHeader` / `res.getHeader` / `res.end` for
|
|
23
|
+
* redirects and cookies
|
|
24
|
+
*
|
|
25
|
+
* @param nodeMiddleware - The middleware returned by `createProxyHandler`.
|
|
26
|
+
* @param request - The incoming Web request.
|
|
27
|
+
* @returns The proxy outcome, see {@link WebProxyOutcome}.
|
|
28
|
+
*/
|
|
29
|
+
const runProxyOnWebRequest = (nodeMiddleware, request) => {
|
|
30
|
+
const initialPath = `${request.url.pathname}${request.url.search}`;
|
|
31
|
+
const requestHeaders = {};
|
|
32
|
+
for (const headerName of FORWARDED_REQUEST_HEADERS) requestHeaders[headerName] = request.headers.get(headerName) ?? "";
|
|
33
|
+
const fakeRequest = {
|
|
34
|
+
url: initialPath,
|
|
35
|
+
method: "GET",
|
|
36
|
+
headers: requestHeaders
|
|
37
|
+
};
|
|
38
|
+
let outcome;
|
|
39
|
+
let statusCode = 200;
|
|
40
|
+
const responseHeaders = new Headers();
|
|
41
|
+
const setHeader = (name, value) => {
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
responseHeaders.delete(name);
|
|
44
|
+
for (const item of value) responseHeaders.append(name, item);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
responseHeaders.set(name, String(value));
|
|
48
|
+
};
|
|
49
|
+
const fakeResponse = {
|
|
50
|
+
writeHead(status, headers) {
|
|
51
|
+
statusCode = status;
|
|
52
|
+
if (headers && typeof headers === "object") for (const [name, value] of Object.entries(headers)) setHeader(name, value);
|
|
53
|
+
return fakeResponse;
|
|
54
|
+
},
|
|
55
|
+
setHeader(name, value) {
|
|
56
|
+
setHeader(name, value);
|
|
57
|
+
return fakeResponse;
|
|
58
|
+
},
|
|
59
|
+
getHeader(name) {
|
|
60
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
61
|
+
const cookies = responseHeaders.getSetCookie();
|
|
62
|
+
return cookies.length > 0 ? cookies : void 0;
|
|
63
|
+
}
|
|
64
|
+
return responseHeaders.get(name) ?? void 0;
|
|
65
|
+
},
|
|
66
|
+
end(body) {
|
|
67
|
+
outcome = {
|
|
68
|
+
kind: "response",
|
|
69
|
+
response: new Response(!(statusCode >= 300 && statusCode < 400) && typeof body === "string" ? body : null, {
|
|
70
|
+
status: statusCode,
|
|
71
|
+
headers: responseHeaders
|
|
72
|
+
})
|
|
73
|
+
};
|
|
74
|
+
return fakeResponse;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
nodeMiddleware(fakeRequest, fakeResponse, () => {
|
|
78
|
+
const currentPath = fakeRequest.url;
|
|
79
|
+
outcome = {
|
|
80
|
+
kind: "next",
|
|
81
|
+
rewrittenPath: currentPath === initialPath ? void 0 : currentPath,
|
|
82
|
+
headers: responseHeaders
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
return outcome ?? {
|
|
86
|
+
kind: "next",
|
|
87
|
+
rewrittenPath: void 0,
|
|
88
|
+
headers: responseHeaders
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Creates a proxy handler for runtimes that expose Web `Request`s rather than
|
|
93
|
+
* Node's `IncomingMessage`.
|
|
94
|
+
*
|
|
95
|
+
* @param options - Same options as `createProxyHandler`; `configuration` must
|
|
96
|
+
* be the built configuration (`@intlayer/config/built`), never one loaded
|
|
97
|
+
* through `@intlayer/config/node`.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const proxy = createIntlayerWebProxyHandler({ configuration });
|
|
102
|
+
* const outcome = proxy({ url: new URL(request.url), headers: request.headers });
|
|
103
|
+
* if (outcome.kind === 'response') return outcome.response;
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
const createIntlayerWebProxyHandler = (options) => {
|
|
107
|
+
const nodeMiddleware = createProxyHandler(options);
|
|
108
|
+
return (request) => runProxyOnWebRequest(nodeMiddleware, request);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
export { createIntlayerWebProxyHandler, runProxyOnWebRequest };
|
|
113
|
+
//# sourceMappingURL=intlayerWebProxyHandler.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intlayerWebProxyHandler.mjs","names":[],"sources":["../../src/intlayerWebProxyHandler.ts"],"sourcesContent":["/**\n * Web Fetch API bridge for the Intlayer proxy.\n *\n * The proxy itself is a Node-style middleware (`intlayerProxyHandler`). Runtimes\n * that only expose a `Request` (Astro middleware, h3 v2 events, Workers…) run it\n * through the lightweight `IncomingMessage` / `ServerResponse` shims below and\n * read the outcome back as plain data.\n *\n * Nothing here may import `@intlayer/config/node`: this module is bundled into\n * production servers.\n */\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport {\n createProxyHandler,\n type IntlayerProxyHandlerOptions,\n type NodeMiddleware,\n} from './intlayerProxyHandler';\n\n/**\n * Subset of a Web request the proxy needs.\n */\nexport type WebProxyRequest = {\n /** Absolute URL of the incoming request. */\n url: URL;\n /** Incoming request headers. */\n headers: Headers;\n};\n\n/**\n * Outcome of running the proxy on a request.\n *\n * - `response`: the proxy answered the request itself (a locale redirect, or a\n * 5xx when a redirect loop was detected). Send it as is.\n * - `next`: the request continues down the pipeline. `rewrittenPath` is set\n * when the proxy changed the internal path (e.g. `/about` → `/en/about`), and\n * `headers` carries the outgoing headers it produced (typically `Set-Cookie`\n * persisting the resolved locale) to append to the final response.\n */\nexport type WebProxyOutcome =\n | { kind: 'response'; response: Response }\n | { kind: 'next'; rewrittenPath: string | undefined; headers: Headers };\n\n/**\n * Header names the proxy reads from the incoming request.\n */\nconst FORWARDED_REQUEST_HEADERS = [\n 'cookie',\n 'host',\n 'accept-language',\n 'x-forwarded-host',\n 'x-forwarded-proto',\n 'x-nitro-prerender',\n] as const;\n\ntype NodeHeaderValue = string | number | readonly string[];\n\n/**\n * Runs a Node-style proxy middleware against a Web request.\n *\n * The proxy answers synchronously, so no promise is involved. Only the fields\n * it actually touches are shimmed:\n * - `req.url` / `req.headers` (mutable plain object: `setLocaleInStorageServer`\n * writes the locale header back through it)\n * - `res.writeHead` / `res.setHeader` / `res.getHeader` / `res.end` for\n * redirects and cookies\n *\n * @param nodeMiddleware - The middleware returned by `createProxyHandler`.\n * @param request - The incoming Web request.\n * @returns The proxy outcome, see {@link WebProxyOutcome}.\n */\nexport const runProxyOnWebRequest = (\n nodeMiddleware: NodeMiddleware,\n request: WebProxyRequest\n): WebProxyOutcome => {\n const initialPath = `${request.url.pathname}${request.url.search}`;\n\n const requestHeaders: Record<string, string> = {};\n for (const headerName of FORWARDED_REQUEST_HEADERS) {\n requestHeaders[headerName] = request.headers.get(headerName) ?? '';\n }\n\n const fakeRequest = {\n url: initialPath,\n method: 'GET',\n headers: requestHeaders,\n } as unknown as IncomingMessage;\n\n let outcome: WebProxyOutcome | undefined;\n let statusCode = 200;\n const responseHeaders = new Headers();\n\n const setHeader = (name: string, value: NodeHeaderValue) => {\n if (Array.isArray(value)) {\n // Several `Set-Cookie` values must stay separate headers.\n responseHeaders.delete(name);\n for (const item of value) responseHeaders.append(name, item);\n return;\n }\n responseHeaders.set(name, String(value));\n };\n\n const fakeResponse = {\n writeHead(status: number, headers?: Record<string, NodeHeaderValue>) {\n statusCode = status;\n if (headers && typeof headers === 'object') {\n for (const [name, value] of Object.entries(headers)) {\n setHeader(name, value);\n }\n }\n return fakeResponse;\n },\n setHeader(name: string, value: NodeHeaderValue) {\n setHeader(name, value);\n return fakeResponse;\n },\n getHeader(name: string) {\n if (name.toLowerCase() === 'set-cookie') {\n const cookies = responseHeaders.getSetCookie();\n return cookies.length > 0 ? cookies : undefined;\n }\n return responseHeaders.get(name) ?? undefined;\n },\n end(body?: string | Buffer | null) {\n const isRedirect = statusCode >= 300 && statusCode < 400;\n outcome = {\n kind: 'response',\n response: new Response(\n !isRedirect && typeof body === 'string' ? body : null,\n { status: statusCode, headers: responseHeaders }\n ),\n };\n return fakeResponse;\n },\n } as unknown as ServerResponse<IncomingMessage>;\n\n nodeMiddleware(fakeRequest, fakeResponse, () => {\n const currentPath = fakeRequest.url as string;\n outcome = {\n kind: 'next',\n rewrittenPath: currentPath === initialPath ? undefined : currentPath,\n headers: responseHeaders,\n };\n });\n\n // The proxy either ends the response or calls `next()` on every path.\n return (\n outcome ?? {\n kind: 'next',\n rewrittenPath: undefined,\n headers: responseHeaders,\n }\n );\n};\n\n/**\n * Handler running the Intlayer proxy on Web requests.\n */\nexport type IntlayerWebProxyHandler = (\n request: WebProxyRequest\n) => WebProxyOutcome;\n\n/**\n * Creates a proxy handler for runtimes that expose Web `Request`s rather than\n * Node's `IncomingMessage`.\n *\n * @param options - Same options as `createProxyHandler`; `configuration` must\n * be the built configuration (`@intlayer/config/built`), never one loaded\n * through `@intlayer/config/node`.\n *\n * @example\n * ```ts\n * const proxy = createIntlayerWebProxyHandler({ configuration });\n * const outcome = proxy({ url: new URL(request.url), headers: request.headers });\n * if (outcome.kind === 'response') return outcome.response;\n * ```\n */\nexport const createIntlayerWebProxyHandler = (\n options: IntlayerProxyHandlerOptions\n): IntlayerWebProxyHandler => {\n const nodeMiddleware = createProxyHandler(options);\n\n return (request) => runProxyOnWebRequest(nodeMiddleware, request);\n};\n"],"mappings":";;;;;;AA8CA,MAAM,4BAA4B;CAChC;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;AAkBA,MAAa,wBACX,gBACA,YACoB;CACpB,MAAM,cAAc,GAAG,QAAQ,IAAI,WAAW,QAAQ,IAAI;CAE1D,MAAM,iBAAyC,CAAC;CAChD,KAAK,MAAM,cAAc,2BACvB,eAAe,cAAc,QAAQ,QAAQ,IAAI,UAAU,KAAK;CAGlE,MAAM,cAAc;EAClB,KAAK;EACL,QAAQ;EACR,SAAS;CACX;CAEA,IAAI;CACJ,IAAI,aAAa;CACjB,MAAM,kBAAkB,IAAI,QAAQ;CAEpC,MAAM,aAAa,MAAc,UAA2B;EAC1D,IAAI,MAAM,QAAQ,KAAK,GAAG;GAExB,gBAAgB,OAAO,IAAI;GAC3B,KAAK,MAAM,QAAQ,OAAO,gBAAgB,OAAO,MAAM,IAAI;GAC3D;EACF;EACA,gBAAgB,IAAI,MAAM,OAAO,KAAK,CAAC;CACzC;CAEA,MAAM,eAAe;EACnB,UAAU,QAAgB,SAA2C;GACnE,aAAa;GACb,IAAI,WAAW,OAAO,YAAY,UAChC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAChD,UAAU,MAAM,KAAK;GAGzB,OAAO;EACT;EACA,UAAU,MAAc,OAAwB;GAC9C,UAAU,MAAM,KAAK;GACrB,OAAO;EACT;EACA,UAAU,MAAc;GACtB,IAAI,KAAK,YAAY,MAAM,cAAc;IACvC,MAAM,UAAU,gBAAgB,aAAa;IAC7C,OAAO,QAAQ,SAAS,IAAI,UAAU;GACxC;GACA,OAAO,gBAAgB,IAAI,IAAI,KAAK;EACtC;EACA,IAAI,MAA+B;GAEjC,UAAU;IACR,MAAM;IACN,UAAU,IAAI,SACZ,EAJe,cAAc,OAAO,aAAa,QAIlC,OAAO,SAAS,WAAW,OAAO,MACjD;KAAE,QAAQ;KAAY,SAAS;IAAgB,CACjD;GACF;GACA,OAAO;EACT;CACF;CAEA,eAAe,aAAa,oBAAoB;EAC9C,MAAM,cAAc,YAAY;EAChC,UAAU;GACR,MAAM;GACN,eAAe,gBAAgB,cAAc,SAAY;GACzD,SAAS;EACX;CACF,CAAC;CAGD,OACE,WAAW;EACT,MAAM;EACN,eAAe;EACf,SAAS;CACX;AAEJ;;;;;;;;;;;;;;;;AAwBA,MAAa,iCACX,YAC4B;CAC5B,MAAM,iBAAiB,mBAAmB,OAAO;CAEjD,QAAQ,YAAY,qBAAqB,gBAAgB,OAAO;AAClE"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { IntlayerCompilerOptions, intlayerCompiler } from "./IntlayerCompilerPlu
|
|
|
2
2
|
import { ChunkingContext, DictionaryModule, ModuleGraphNode, createDictionaryModuleParser, intlayerChunk, resolveBoundaries, toBoundaryName } from "./intlayerChunkPlugin.js";
|
|
3
3
|
import { intlayerMinify } from "./intlayerMinifyPlugin.js";
|
|
4
4
|
import { intlayerOptimize } from "./intlayerOptimizePlugin.js";
|
|
5
|
-
import { IntlayerProxyPluginOptions, createIntlayerProxyHandler, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy } from "./intlayerProxyPlugin.js";
|
|
5
|
+
import { IntlayerProxyPluginOptions, createIntlayerProxyHandler, createRedirectOnlyMiddleware, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy, resolveNitroHandlerPath } from "./intlayerProxyPlugin.js";
|
|
6
6
|
import { INTLAYER_NO_EXTERNAL_PATTERN, IntlayerPluginOptions, intLayerPlugin, intlayer, intlayerPlugin } from "./intlayerPlugin.js";
|
|
7
7
|
import { intlayerPreload } from "./intlayerPreloadPlugin.js";
|
|
8
8
|
import { intlayerPrune } from "./intlayerPrunePlugin.js";
|
|
9
|
-
export { ChunkingContext, DictionaryModule, INTLAYER_NO_EXTERNAL_PATTERN, IntlayerCompilerOptions, IntlayerPluginOptions, IntlayerProxyPluginOptions, ModuleGraphNode, createDictionaryModuleParser, createIntlayerProxyHandler, intLayerMiddlewarePlugin, intLayerPlugin, intlayer, intlayerChunk, intlayerCompiler, intlayerMiddleware, intlayerMinify, intlayerOptimize, intlayerPlugin, intlayerPreload, intlayerProxy, intlayerPrune, resolveBoundaries, toBoundaryName };
|
|
9
|
+
export { ChunkingContext, DictionaryModule, INTLAYER_NO_EXTERNAL_PATTERN, IntlayerCompilerOptions, IntlayerPluginOptions, IntlayerProxyPluginOptions, ModuleGraphNode, createDictionaryModuleParser, createIntlayerProxyHandler, createRedirectOnlyMiddleware, intLayerMiddlewarePlugin, intLayerPlugin, intlayer, intlayerChunk, intlayerCompiler, intlayerMiddleware, intlayerMinify, intlayerOptimize, intlayerPlugin, intlayerPreload, intlayerProxy, intlayerPrune, resolveBoundaries, resolveNitroHandlerPath, toBoundaryName };
|
|
@@ -8,11 +8,6 @@
|
|
|
8
8
|
* works with any h3 v2-compatible runtime (Bun, Deno, Node).
|
|
9
9
|
*/
|
|
10
10
|
type H3EventLike = {
|
|
11
|
-
/**
|
|
12
|
-
* pathname + search — a computed getter on H3Event:
|
|
13
|
-
* `return this.url.pathname + this.url.search`
|
|
14
|
-
*/
|
|
15
|
-
readonly path: string;
|
|
16
11
|
/**
|
|
17
12
|
* Full URL object — a **plain property** (not a getter) on H3Event, safe to
|
|
18
13
|
* replace for internal URL rewrites. After assignment, `event.path` will
|
|
@@ -40,8 +35,8 @@ type H3EventLike = {
|
|
|
40
35
|
* and Deno — where `event.node` is `undefined` and `fromNodeMiddleware` crashes with
|
|
41
36
|
* "undefined is not an object (evaluating 'event.node.req')".
|
|
42
37
|
*
|
|
43
|
-
* It bridges h3 v2 events to the Node.js-style proxy middleware
|
|
44
|
-
*
|
|
38
|
+
* It bridges h3 v2 events to the Node.js-style proxy middleware through
|
|
39
|
+
* `runProxyOnWebRequest`:
|
|
45
40
|
*
|
|
46
41
|
* - **Redirect** (301 / 5xx): builds a Web API `Response` and returns it — Nitro sends
|
|
47
42
|
* the correct HTTP response to the browser.
|
|
@@ -51,7 +46,7 @@ type H3EventLike = {
|
|
|
51
46
|
* - **Pass-through** (`next()`, URL unchanged): returns `undefined` — Nitro proceeds to
|
|
52
47
|
* the next handler / route.
|
|
53
48
|
*/
|
|
54
|
-
declare function _default(event: H3EventLike):
|
|
49
|
+
declare function _default(event: H3EventLike): Response | undefined;
|
|
55
50
|
//#endregion
|
|
56
51
|
export { _default as default };
|
|
57
52
|
//# sourceMappingURL=intlayerNitroHandler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerNitroHandler.d.ts","names":[],"sources":["../../src/intlayerNitroHandler.ts"],"mappings":";;;;;;;;;KAyBK
|
|
1
|
+
{"version":3,"file":"intlayerNitroHandler.d.ts","names":[],"sources":["../../src/intlayerNitroHandler.ts"],"mappings":";;;;;;;;;KAyBK;;;;;;EAMH,KAAK;;;;;WAKI,SAAS;;;;;WAKT;aACE,SAAS;;;;;;;;;;;;;;;;;;;;;;iBAoCN,SAAA,OAAO,cAAc"}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { IntlayerConfig } from "@intlayer/types/config";
|
|
2
2
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
3
|
//#region src/intlayerProxyHandler.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Detects a process that only exists to prerender pages: a TanStack Start
|
|
6
|
+
* build (flag set on the build process, which loads the server in-process) or
|
|
7
|
+
* Nitro's own prerenderer (a dedicated build where Nitro defines
|
|
8
|
+
* `import.meta.prerender`).
|
|
9
|
+
*
|
|
10
|
+
* Prerendered pages are served to every visitor, so such a process never lets
|
|
11
|
+
* the stored locale drive a redirect — see {@link isPrerenderRequest} for the
|
|
12
|
+
* per-request counterpart, which also covers a prerenderer hitting a
|
|
13
|
+
* long-lived server.
|
|
14
|
+
*/
|
|
15
|
+
export declare const isPrerenderProcess: () => boolean;
|
|
4
16
|
/**
|
|
5
17
|
* A Node.js-compatible Connect middleware function.
|
|
6
18
|
* Compatible with Vite dev/preview server, Node.js http, Express, and h3's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerProxyHandler.d.ts","names":[],"sources":["../../src/intlayerProxyHandler.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"intlayerProxyHandler.d.ts","names":[],"sources":["../../src/intlayerProxyHandler.ts"],"mappings":";;;;;;;;;;;;;;qBAyDa;;;;;;YAuFD,kBACV,KAAK,iBACL,KAAK,eAAe,kBACpB;YAGU;;;;EAIV,eAAe;;;;;;;EAOf,UAAU,KAAK;;;;;;;;;EASf;;;;;;;;;;;;;;;;;qBAkBW,qBAAkB,SACpB,gCACR"}
|
|
@@ -3,6 +3,12 @@ import { GetConfigurationOptions } from "@intlayer/config/node";
|
|
|
3
3
|
import { Plugin } from "vite";
|
|
4
4
|
import { IncomingMessage } from "node:http";
|
|
5
5
|
//#region src/intlayerProxyPlugin.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Restricts a proxy middleware to its redirects: a request the proxy lets
|
|
8
|
+
* through continues with the URL the browser sent, any internal rewrite
|
|
9
|
+
* undone, for a downstream server that runs the full proxy itself.
|
|
10
|
+
*/
|
|
11
|
+
export declare const createRedirectOnlyMiddleware: (handler: NodeMiddleware) => NodeMiddleware;
|
|
6
12
|
export type IntlayerProxyPluginOptions = {
|
|
7
13
|
/**
|
|
8
14
|
* A function that allows you to ignore specific requests from the intlayer proxy.
|
|
@@ -30,12 +36,15 @@ export type IntlayerProxyPluginOptions = {
|
|
|
30
36
|
*/
|
|
31
37
|
configOptions?: GetConfigurationOptions;
|
|
32
38
|
/**
|
|
33
|
-
* Whether a development
|
|
39
|
+
* Whether a development server is serving the app.
|
|
34
40
|
*
|
|
35
|
-
* Set internally by the plugin from `configureServer
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
41
|
+
* Set internally by the plugin from `configureServer`. It only matters in
|
|
42
|
+
* the proxy's auto mode, where a dev server keeps locale routing URL-driven
|
|
43
|
+
* by ignoring the stored locale as a redirect source.
|
|
44
|
+
*
|
|
45
|
+
* A preview server (`vite preview`) serves the production build, so it is
|
|
46
|
+
* *not* a dev server: the stored locale drives redirects there, as it does
|
|
47
|
+
* on the deployed app.
|
|
39
48
|
*
|
|
40
49
|
* Defaults to `false` so that mounting `createIntlayerProxyHandler()`
|
|
41
50
|
* manually — the documented production Nitro setup — keeps full behaviour.
|
|
@@ -108,6 +117,16 @@ export declare const createIntlayerProxyHandler: (options?: IntlayerProxyPluginO
|
|
|
108
117
|
*
|
|
109
118
|
* @deprecated Since Intlayer v9, `intlayerProxy()` is bundled directly into the `intlayer()` plugin and enabled by default through the `routing.enableProxy` option (unset by default, which selects auto mode). Registering it separately as shown below is now optional.
|
|
110
119
|
*/
|
|
120
|
+
/**
|
|
121
|
+
* Absolute path of the built Nitro middleware, relative to this module.
|
|
122
|
+
*
|
|
123
|
+
* Nitro inlines the path as an import specifier in its virtual routing module,
|
|
124
|
+
* so Windows backslashes must become forward slashes or the import breaks.
|
|
125
|
+
*
|
|
126
|
+
* @param moduleUrl - `import.meta.url` of the calling module
|
|
127
|
+
* @param toPath - `file:` URL to path converter (overridable for tests)
|
|
128
|
+
*/
|
|
129
|
+
export declare const resolveNitroHandlerPath: (moduleUrl: string, toPath?: (url: URL) => string) => string;
|
|
111
130
|
export declare const intlayerProxy: (options?: IntlayerProxyPluginOptions) => Plugin;
|
|
112
131
|
/**
|
|
113
132
|
* @deprecated Rename to intlayerProxy instead
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerProxyPlugin.d.ts","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"intlayerProxyPlugin.d.ts","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"mappings":";;;;;;;;;;qBAqDa,+BAA4B,SAC7B,mBAAiB;YAUjB;;;;;;;;;;;;;;EAcV,UAAU,KAAK;;;;;;;;;;;EAWf,gBAAgB;;;;;;;;;;;;;;;;;EAiBhB;;;;;;;;;;;;;;;;;;;;;;;;qBAyBW,6BAA0B,UAC3B,+BACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAyDU,0BAAuB,mBACjB,UACR,KAAK;qBAIH,gBAAa,UAAc,+BAA6B;;;;;;;;;;;;;qBA+KxD,2BAAkB;;;;;;;;;;;;;qBAclB,iCAAwB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { IntlayerProxyHandlerOptions, NodeMiddleware } from "./intlayerProxyHandler.js";
|
|
2
|
+
//#region src/intlayerWebProxyHandler.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Subset of a Web request the proxy needs.
|
|
5
|
+
*/
|
|
6
|
+
export type WebProxyRequest = {
|
|
7
|
+
/** Absolute URL of the incoming request. */
|
|
8
|
+
url: URL;
|
|
9
|
+
/** Incoming request headers. */
|
|
10
|
+
headers: Headers;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Outcome of running the proxy on a request.
|
|
14
|
+
*
|
|
15
|
+
* - `response`: the proxy answered the request itself (a locale redirect, or a
|
|
16
|
+
* 5xx when a redirect loop was detected). Send it as is.
|
|
17
|
+
* - `next`: the request continues down the pipeline. `rewrittenPath` is set
|
|
18
|
+
* when the proxy changed the internal path (e.g. `/about` → `/en/about`), and
|
|
19
|
+
* `headers` carries the outgoing headers it produced (typically `Set-Cookie`
|
|
20
|
+
* persisting the resolved locale) to append to the final response.
|
|
21
|
+
*/
|
|
22
|
+
export type WebProxyOutcome = {
|
|
23
|
+
kind: 'response';
|
|
24
|
+
response: Response;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'next';
|
|
27
|
+
rewrittenPath: string | undefined;
|
|
28
|
+
headers: Headers;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Runs a Node-style proxy middleware against a Web request.
|
|
32
|
+
*
|
|
33
|
+
* The proxy answers synchronously, so no promise is involved. Only the fields
|
|
34
|
+
* it actually touches are shimmed:
|
|
35
|
+
* - `req.url` / `req.headers` (mutable plain object: `setLocaleInStorageServer`
|
|
36
|
+
* writes the locale header back through it)
|
|
37
|
+
* - `res.writeHead` / `res.setHeader` / `res.getHeader` / `res.end` for
|
|
38
|
+
* redirects and cookies
|
|
39
|
+
*
|
|
40
|
+
* @param nodeMiddleware - The middleware returned by `createProxyHandler`.
|
|
41
|
+
* @param request - The incoming Web request.
|
|
42
|
+
* @returns The proxy outcome, see {@link WebProxyOutcome}.
|
|
43
|
+
*/
|
|
44
|
+
export declare const runProxyOnWebRequest: (nodeMiddleware: NodeMiddleware, request: WebProxyRequest) => WebProxyOutcome;
|
|
45
|
+
/**
|
|
46
|
+
* Handler running the Intlayer proxy on Web requests.
|
|
47
|
+
*/
|
|
48
|
+
export type IntlayerWebProxyHandler = (request: WebProxyRequest) => WebProxyOutcome;
|
|
49
|
+
/**
|
|
50
|
+
* Creates a proxy handler for runtimes that expose Web `Request`s rather than
|
|
51
|
+
* Node's `IncomingMessage`.
|
|
52
|
+
*
|
|
53
|
+
* @param options - Same options as `createProxyHandler`; `configuration` must
|
|
54
|
+
* be the built configuration (`@intlayer/config/built`), never one loaded
|
|
55
|
+
* through `@intlayer/config/node`.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const proxy = createIntlayerWebProxyHandler({ configuration });
|
|
60
|
+
* const outcome = proxy({ url: new URL(request.url), headers: request.headers });
|
|
61
|
+
* if (outcome.kind === 'response') return outcome.response;
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare const createIntlayerWebProxyHandler: (options: IntlayerProxyHandlerOptions) => IntlayerWebProxyHandler;
|
|
65
|
+
//#endregion
|
|
66
|
+
//# sourceMappingURL=intlayerWebProxyHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intlayerWebProxyHandler.d.ts","names":[],"sources":["../../src/intlayerWebProxyHandler.ts"],"mappings":";;;;;YAsBY;;EAEV,KAAK;;EAEL,SAAS;;;;;;;;;;;;YAaC;EACN;EAAkB,UAAU;;EAC5B;EAAc;EAAmC,SAAS;;;;;;;;;;;;;;;;qBA8BnD,uBAAoB,gBACf,gBAAc,SACrB,oBACR;;;;YAoFS,2BACV,SAAS,oBACN;;;;;;;;;;;;;;;;qBAiBQ,gCAA6B,SAC/B,gCACR"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-intlayer",
|
|
3
|
-
"version": "9.5.
|
|
3
|
+
"version": "9.5.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A Vite plugin for seamless internationalization (i18n), providing locale detection, redirection, and environment-based configuration",
|
|
6
6
|
"keywords": [
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
"import": "./dist/esm/intlayerNitroHandler.mjs",
|
|
48
48
|
"default": "./dist/esm/intlayerNitroHandler.mjs"
|
|
49
49
|
},
|
|
50
|
+
"./web-proxy-handler": {
|
|
51
|
+
"types": "./dist/types/intlayerWebProxyHandler.d.ts",
|
|
52
|
+
"import": "./dist/esm/intlayerWebProxyHandler.mjs",
|
|
53
|
+
"default": "./dist/esm/intlayerWebProxyHandler.mjs"
|
|
54
|
+
},
|
|
50
55
|
"./package.json": "./package.json"
|
|
51
56
|
},
|
|
52
57
|
"main": "./dist/esm/index.mjs",
|
|
@@ -82,12 +87,12 @@
|
|
|
82
87
|
"typecheck": "tsc --noEmit --project tsconfig.types.json"
|
|
83
88
|
},
|
|
84
89
|
"dependencies": {
|
|
85
|
-
"@intlayer/babel": "9.5.
|
|
86
|
-
"@intlayer/config": "9.5.
|
|
87
|
-
"@intlayer/core": "9.5.
|
|
88
|
-
"@intlayer/dictionaries-entry": "9.5.
|
|
89
|
-
"@intlayer/engine": "9.5.
|
|
90
|
-
"@intlayer/types": "9.5.
|
|
90
|
+
"@intlayer/babel": "9.5.6",
|
|
91
|
+
"@intlayer/config": "9.5.6",
|
|
92
|
+
"@intlayer/core": "9.5.6",
|
|
93
|
+
"@intlayer/dictionaries-entry": "9.5.6",
|
|
94
|
+
"@intlayer/engine": "9.5.6",
|
|
95
|
+
"@intlayer/types": "9.5.6"
|
|
91
96
|
},
|
|
92
97
|
"devDependencies": {
|
|
93
98
|
"@types/node": "^22",
|
|
@@ -101,8 +106,8 @@
|
|
|
101
106
|
},
|
|
102
107
|
"peerDependencies": {
|
|
103
108
|
"@babel/core": ">=6.0.0",
|
|
104
|
-
"@intlayer/svelte-compiler": "9.5.
|
|
105
|
-
"@intlayer/vue-compiler": "9.5.
|
|
109
|
+
"@intlayer/svelte-compiler": "9.5.6",
|
|
110
|
+
"@intlayer/vue-compiler": "9.5.6",
|
|
106
111
|
"h3": ">=1.0.0",
|
|
107
112
|
"vite": ">=4.0.0"
|
|
108
113
|
},
|