vite-intlayer 8.12.0 → 8.12.2

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.
@@ -3,6 +3,6 @@ import { intlayerMinify } from "./intlayerMinifyPlugin.mjs";
3
3
  import { intlayerPrune } from "./intlayerPrunePlugin.mjs";
4
4
  import { intlayerOptimize } from "./intlayerOptimizePlugin.mjs";
5
5
  import { intLayerPlugin, intlayer, intlayerPlugin } from "./intlayerPlugin.mjs";
6
- import { intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy } from "./intlayerProxyPlugin.mjs";
6
+ import { createIntlayerProxyHandler, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy } from "./intlayerProxyPlugin.mjs";
7
7
 
8
- export { intLayerMiddlewarePlugin, intLayerPlugin, intlayer, intlayerCompiler, intlayerMiddleware, intlayerMinify, intlayerOptimize, intlayerPlugin, intlayerProxy, intlayerPrune };
8
+ export { createIntlayerProxyHandler, intLayerMiddlewarePlugin, intLayerPlugin, intlayer, intlayerCompiler, intlayerMiddleware, intlayerMinify, intlayerOptimize, intlayerPlugin, intlayerProxy, intlayerPrune };
@@ -0,0 +1,107 @@
1
+ import { createIntlayerProxyHandler } from "./intlayerProxyPlugin.mjs";
2
+ import * as ANSIColors from "@intlayer/config/colors";
3
+ import { colorize, getAppLogger } from "@intlayer/config/logger";
4
+ import { getConfiguration } from "@intlayer/config/node";
5
+
6
+ //#region src/intlayerNitroHandler.ts
7
+ getAppLogger(getConfiguration())(`Intlayer proxy ${colorize("enabled", ANSIColors.GREEN)}`, { level: "info" });
8
+ const nodeMiddleware = createIntlayerProxyHandler();
9
+ /**
10
+ * Native h3 v2 event handler for Nitro production servers (TanStack Start, Nuxt, etc.).
11
+ *
12
+ * Unlike `fromNodeMiddleware` (h3 v1 API), this handler uses the Web Fetch API event
13
+ * model exclusively and is therefore compatible with ALL Nitro presets — including Bun
14
+ * and Deno — where `event.node` is `undefined` and `fromNodeMiddleware` crashes with
15
+ * "undefined is not an object (evaluating 'event.node.req')".
16
+ *
17
+ * It bridges h3 v2 events to the Node.js-style `createIntlayerProxyHandler` middleware
18
+ * via lightweight IncomingMessage / ServerResponse shims:
19
+ *
20
+ * - **Redirect** (301 / 5xx): builds a Web API `Response` and returns it — Nitro sends
21
+ * the correct HTTP response to the browser.
22
+ * - **Rewrite** (`next()` + modified `req.url`): replaces `event.url` with the rewritten
23
+ * URL so `event.path` (a getter) returns the new pathname for downstream handlers and
24
+ * the Nitro router.
25
+ * - **Pass-through** (`next()`, URL unchanged): returns `undefined` — Nitro proceeds to
26
+ * the next handler / route.
27
+ */
28
+ var intlayerNitroHandler_default = async (event) => new Promise((resolve) => {
29
+ const initialPath = event.path;
30
+ /**
31
+ * Minimal IncomingMessage shim.
32
+ *
33
+ * Only the fields actually read by createIntlayerProxyHandler are populated:
34
+ * - url : the current pathname + search, modified for rewrites
35
+ * - headers.cookie : locale cookie detection
36
+ * - headers.host : domain-based locale routing
37
+ * - headers.accept-language : browser Accept-Language fallback
38
+ * - headers.x-forwarded-* : forwarded host/proto for reverse-proxy setups
39
+ *
40
+ * headers must be a mutable plain object because setLocaleInStorageServer
41
+ * writes Set-Cookie back via req.headers[name] = value.
42
+ */
43
+ const fakeReq = {
44
+ url: initialPath,
45
+ method: "GET",
46
+ headers: {
47
+ cookie: event.headers.get("cookie") ?? "",
48
+ host: event.headers.get("host") ?? "",
49
+ "accept-language": event.headers.get("accept-language") ?? "",
50
+ "x-forwarded-host": event.headers.get("x-forwarded-host") ?? "",
51
+ "x-forwarded-proto": event.headers.get("x-forwarded-proto") ?? ""
52
+ }
53
+ };
54
+ let responseStatusCode = 200;
55
+ const accumulatedHeaders = {};
56
+ /**
57
+ * Minimal ServerResponse shim.
58
+ *
59
+ * Implements only the methods that createIntlayerProxyHandler invokes:
60
+ * writeHead() — status + Location header for 301 redirects
61
+ * setHeader() — Set-Cookie written by setLocaleInStorageServer
62
+ * getHeader() — defensive read-back (not strictly required but safe)
63
+ * end() — finalises the response; for redirects this returns a
64
+ * Web API Response object that Nitro sends to the client
65
+ */
66
+ const fakeRes = {
67
+ writeHead(statusCode, headersArg) {
68
+ responseStatusCode = statusCode;
69
+ if (headersArg && typeof headersArg === "object") for (const [key, value] of Object.entries(headersArg)) accumulatedHeaders[key.toLowerCase()] = Array.isArray(value) ? value[0] ?? "" : String(value);
70
+ return fakeRes;
71
+ },
72
+ setHeader(name, value) {
73
+ accumulatedHeaders[name.toLowerCase()] = Array.isArray(value) ? value[0] ?? "" : String(value);
74
+ return fakeRes;
75
+ },
76
+ getHeader(name) {
77
+ return accumulatedHeaders[name.toLowerCase()];
78
+ },
79
+ getHeaders() {
80
+ return { ...accumulatedHeaders };
81
+ },
82
+ end(body) {
83
+ const webHeaders = new Headers();
84
+ for (const [key, value] of Object.entries(accumulatedHeaders)) webHeaders.set(key, value);
85
+ resolve(new Response(responseStatusCode >= 300 && responseStatusCode < 400 ? null : typeof body === "string" ? body : null, {
86
+ status: responseStatusCode,
87
+ headers: webHeaders
88
+ }));
89
+ return fakeRes;
90
+ },
91
+ headersSent: false
92
+ };
93
+ nodeMiddleware(fakeReq, fakeRes, () => {
94
+ const rewrittenPath = fakeReq.url;
95
+ if (rewrittenPath !== initialPath) try {
96
+ event.url = new URL(rewrittenPath, event.url.origin);
97
+ } catch {
98
+ console.error("[intlayer-proxy] URL rewrite failed — invalid path:", rewrittenPath);
99
+ }
100
+ if (Object.keys(accumulatedHeaders).length > 0) for (const [key, value] of Object.entries(accumulatedHeaders)) event.res.headers.set(key, value);
101
+ resolve(void 0);
102
+ });
103
+ });
104
+
105
+ //#endregion
106
+ export { intlayerNitroHandler_default as default };
107
+ //# sourceMappingURL=intlayerNitroHandler.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intlayerNitroHandler.mjs","names":[],"sources":["../../src/intlayerNitroHandler.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { createIntlayerProxyHandler } from './intlayerProxyPlugin';\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 = getConfiguration();\nconst logger = getAppLogger(intlayerConfig);\nlogger(`Intlayer proxy ${colorize('enabled', ANSIColors.GREEN)}`, {\n level: 'info',\n});\n\nconst nodeMiddleware = createIntlayerProxyHandler();\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 `createIntlayerProxyHandler` 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 createIntlayerProxyHandler 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 *\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 } 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 createIntlayerProxyHandler 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":";;;;;;AAyCe,aADQ,iBACkB,CACpC,EAAE,kBAAkB,SAAS,WAAW,WAAW,KAAK,KAAK,EAChE,OAAO,OACT,CAAC;AAED,MAAM,iBAAiB,2BAA2B;;;;;;;;;;;;;;;;;;;;AAqBlD,mCAAe,OAAO,UACpB,IAAI,SAA0B,YAAY;CACxC,MAAM,cAAc,MAAM;;;;;;;;;;;;;;CAe1B,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;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,EAAE,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,7 +1,7 @@
1
1
  import { intlayerVueAsyncPlugin } from "./intlayerVueAsyncPlugin.mjs";
2
2
  import { readFile, readdir } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { INTLAYER_USAGE_REGEX, SOURCE_FILE_REGEX, analyzeFieldUsageInFile, buildNestedRenameMapFromContent, optimizeSourceFile, renameFieldsInSourceFile } from "@intlayer/babel";
4
+ import { INTLAYER_OR_COMPAT_USAGE_REGEX, INTLAYER_USAGE_REGEX, SOURCE_FILE_REGEX, analyzeFieldUsageInFile, buildNestedRenameMapFromContent, optimizeSourceFile, renameFieldsInSourceFile } from "@intlayer/babel";
5
5
  import * as ANSIColors from "@intlayer/config/colors";
6
6
  import { colorize, colorizeKey, colorizeNumber, getAppLogger } from "@intlayer/config/logger";
7
7
  import { buildComponentFilesList, formatPath, runOnce } from "@intlayer/chokidar/utils";
@@ -74,7 +74,7 @@ const intlayerOptimize = async (intlayerConfig, pruneContext) => {
74
74
  } catch {
75
75
  return;
76
76
  }
77
- if (!INTLAYER_USAGE_REGEX.test(sourceCode)) return;
77
+ if (!INTLAYER_OR_COMPAT_USAGE_REGEX.test(sourceCode)) return;
78
78
  try {
79
79
  await analyzeFieldUsageInFile(sourceFilePath, sourceCode, pruneContext);
80
80
  } catch (parseError) {
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerOptimizePlugin.mjs","names":[],"sources":["../../src/intlayerOptimizePlugin.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport {\n analyzeFieldUsageInFile,\n buildNestedRenameMapFromContent,\n INTLAYER_USAGE_REGEX,\n optimizeSourceFile,\n type PruneContext,\n renameFieldsInSourceFile,\n SOURCE_FILE_REGEX,\n} from '@intlayer/babel';\nimport {\n buildComponentFilesList,\n formatPath,\n runOnce,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { IMPORT_MODE } from '@intlayer/config/defaultValues';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport type { PluginOption } from 'vite';\nimport { intlayerVueAsyncPlugin } from './intlayerVueAsyncPlugin';\n\n// Plugin\n\n/**\n * Returns the Vite plugins responsible for the build optimisation step.\n *\n * Contains three internal plugins:\n *\n * 1. Vue async plugin – handles Vue SFC async script blocks.\n * 2. Usage analyser (`vite-intlayer-usage-analyzer`) – pre-scans every\n * component source file during `buildStart` to build the field-usage map\n * in `pruneContext`. This runs before any `transform` calls so the\n * downstream prune plugin always has complete data.\n * 3. Babel transform (`vite-intlayer-babel-transform`) – rewrites\n * `useIntlayer('key')` / `getIntlayer('key')` calls into\n * `useDictionary(_hash)` / `getDictionary(_hash)` and injects the\n * corresponding JSON (or dynamic `.mjs`) imports. Also applies field-name\n * renaming when `build.minify` is enabled.\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Shared mutable state written here and read by the\n * prune and minify plugins. Pass `null` to skip\n * analysis (e.g. when both `purge` and `minify` are\n * disabled).\n */\nexport const intlayerOptimize = async (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext | null\n): Promise<PluginOption[]> => {\n try {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, purge, minify } = intlayerConfig.build;\n const editorEnabled = intlayerConfig.editor.enabled;\n\n const importMode =\n intlayerConfig.build.importMode ?? intlayerConfig.dictionary?.importMode;\n\n const {\n dictionariesDir,\n dynamicDictionariesDir,\n unmergedDictionariesDir,\n fetchDictionariesDir,\n mainDir,\n baseDir,\n } = intlayerConfig.system;\n\n const dictionariesEntryPath = join(mainDir, 'dictionaries.mjs');\n const unmergedDictionariesEntryPath = join(\n mainDir,\n 'unmerged_dictionaries.mjs'\n );\n const dynamicDictionariesEntryPath = join(\n mainDir,\n 'dynamic_dictionaries.mjs'\n );\n\n const componentFilesList = buildComponentFilesList(intlayerConfig);\n\n const transformableFilesList = [\n ...componentFilesList,\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n ];\n\n const dictionaries = getDictionaries(intlayerConfig);\n\n const dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch'\n > = {};\n (Object.values(dictionaries) as Dictionary[]).forEach((dictionary) => {\n dictionaryKeyToImportModeMap[dictionary.key] =\n dictionary.importMode ?? importMode ?? IMPORT_MODE;\n });\n\n const isBuildOptimizeEnabled = (\n _config: unknown,\n env: { command: string }\n ) => {\n const isBuildCommand = env.command === 'build';\n return (optimize === undefined && isBuildCommand) || optimize === true;\n };\n\n const isAnalysisEnabled = (_config: unknown, env: { command: string }) =>\n !editorEnabled &&\n (!!purge || !!minify) &&\n isBuildOptimizeEnabled(_config, env);\n\n let partiallyMinifiedDictionariesCount = 0;\n\n return [\n intlayerVueAsyncPlugin(intlayerConfig, transformableFilesList),\n\n // Plugin 1: Usage analyser\n {\n name: 'vite-intlayer-usage-analyzer',\n enforce: 'pre',\n apply: isAnalysisEnabled,\n\n buildStart: async () => {\n if (!pruneContext) return;\n\n // Phase 1: Babel-based field-usage analysis for all component files\n await Promise.all(\n componentFilesList.map(async (sourceFilePath) => {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) return;\n\n let sourceCode: string;\n try {\n sourceCode = await readFile(sourceFilePath, 'utf-8');\n } catch {\n return; // unreadable file – skip silently\n }\n\n if (!INTLAYER_USAGE_REGEX.test(sourceCode)) return;\n\n // For Vue/Svelte SFCs, the usage analyzer expects the raw script\n // content. `analyzeFieldUsageInFile` handles block extraction\n // internally via `extractScriptBlocks`.\n try {\n await analyzeFieldUsageInFile(\n sourceFilePath,\n sourceCode,\n pruneContext\n );\n } catch (parseError) {\n pruneContext.hasUnparsableSourceFiles = true;\n logger(\n [\n `Could not parse`,\n formatPath(sourceFilePath),\n `for field-usage analysis.`,\n 'Dictionaries whose usage cannot be confirmed will not be pruned.',\n parseError instanceof Error\n ? `(${parseError.message})`\n : String(parseError),\n ],\n { level: 'warn' }\n );\n }\n })\n );\n\n // Phase 2: Framework-specific analysis for Vue / Svelte / Astro SFC\n // bindings that Babel scope analysis cannot resolve:\n // Vue → `.value` ref-accessor indirection\n // Svelte → `$` reactive store prefix\n // Astro → frontmatter variables referenced in the HTML template\n if (pruneContext.pendingFrameworkAnalysis.size > 0) {\n const vuePending = new Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >();\n const sveltePending = new Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >();\n const astroPending = new Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >();\n\n for (const [\n filePath,\n entries,\n ] of pruneContext.pendingFrameworkAnalysis) {\n if (filePath.endsWith('.vue')) {\n vuePending.set(filePath, entries);\n } else if (filePath.endsWith('.svelte')) {\n sveltePending.set(filePath, entries);\n } else if (filePath.endsWith('.astro')) {\n astroPending.set(filePath, entries);\n }\n }\n\n /** Merge framework-extracted field usage into pruneContext. */\n const mergeFrameworkResult = (\n dictionaryKey: string,\n fields: Set<string> | undefined\n ): void => {\n if (fields && fields.size > 0) {\n // The Babel rename plugin cannot update source-code property\n // accesses for SFC indirect patterns → suppress field renaming.\n pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);\n\n const existing =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (existing === 'all') return;\n\n const merged =\n existing instanceof Set\n ? new Set([...existing, ...fields])\n : new Set(fields);\n pruneContext.dictionaryKeyToFieldUsageMap.set(\n dictionaryKey,\n merged\n );\n } else {\n pruneContext.dictionaryKeyToFieldUsageMap.set(\n dictionaryKey,\n 'all'\n );\n }\n };\n\n // Vue files\n if (vuePending.size > 0) {\n let extractVueIntlayerFieldUsage:\n | ((\n code: string,\n vars: { variableName: string; dictionaryKey: string }[]\n ) => Map<string, Set<string>>)\n | null = null;\n\n try {\n const vueCompiler = await import('@intlayer/vue-compiler');\n extractVueIntlayerFieldUsage =\n vueCompiler.extractVueIntlayerFieldUsage;\n } catch {\n // @intlayer/vue-compiler not installed – fall back to 'all'\n }\n\n for (const [filePath, entries] of vuePending) {\n if (!extractVueIntlayerFieldUsage) {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n let fileCode: string;\n try {\n fileCode = await readFile(filePath, 'utf-8');\n } catch {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n const result = extractVueIntlayerFieldUsage(fileCode, entries);\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(\n dictionaryKey,\n result.get(dictionaryKey)\n );\n }\n }\n }\n\n // Svelte files\n if (sveltePending.size > 0) {\n let extractSvelteIntlayerFieldUsage:\n | ((\n code: string,\n vars: { variableName: string; dictionaryKey: string }[]\n ) => Map<string, Set<string>>)\n | null = null;\n\n try {\n const svelteCompiler = await import(\n '@intlayer/svelte-compiler'\n );\n extractSvelteIntlayerFieldUsage =\n svelteCompiler.extractSvelteIntlayerFieldUsage;\n } catch {\n // @intlayer/svelte-compiler not installed – fall back to 'all'\n }\n\n for (const [filePath, entries] of sveltePending) {\n if (!extractSvelteIntlayerFieldUsage) {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n let fileCode: string;\n try {\n fileCode = await readFile(filePath, 'utf-8');\n } catch {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n const result = extractSvelteIntlayerFieldUsage(\n fileCode,\n entries\n );\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(\n dictionaryKey,\n result.get(dictionaryKey)\n );\n }\n }\n }\n\n // Astro files\n // Frontmatter variables are used in the HTML template, which is not\n // visible to Babel's scope analysis. Scan the template section for\n // `variableName.fieldName` accesses using a lightweight regex pass.\n if (astroPending.size > 0) {\n for (const [filePath, entries] of astroPending) {\n let fileCode: string;\n try {\n fileCode = await readFile(filePath, 'utf-8');\n } catch {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n // Extract only the template (everything after the closing ---).\n // The frontmatter was already handled by Babel in Phase 1.\n const fenceMatch = /^---\\r?\\n[\\s\\S]*?\\r?\\n---/.exec(fileCode);\n const template = fenceMatch\n ? fileCode.slice(fenceMatch.index + fenceMatch[0].length)\n : fileCode;\n\n for (const { variableName, dictionaryKey } of entries) {\n const escapedVar = variableName.replace(\n /[.*+?^${}()|[\\]\\\\]/g,\n '\\\\$&'\n );\n const fieldRe = new RegExp(\n `\\\\b${escapedVar}\\\\.([a-zA-Z_$][a-zA-Z0-9_$]*)`,\n 'g'\n );\n const foundFields = new Set<string>();\n let m = fieldRe.exec(template);\n while (m !== null) {\n foundFields.add(m[1]);\n m = fieldRe.exec(template);\n }\n mergeFrameworkResult(\n dictionaryKey,\n foundFields.size > 0 ? foundFields : undefined\n );\n }\n }\n }\n }\n\n // Phase 3: Warn about untracked bindings (plain variable assignments)\n for (const [\n dictionaryKey,\n sourceFilePaths,\n ] of pruneContext.dictionaryKeysWithUntrackedBindings) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be purged or minified.`,\n `\\n Reason: the result of`,\n `${colorize(`useIntlayer(`, ANSIColors.GREY_LIGHT)}${colorizeKey(\n `'${dictionaryKey}'`\n )}${colorize(`)`, ANSIColors.GREY_LIGHT)}`,\n `is assigned to a plain variable in:`,\n ...sourceFilePaths.map(\n (filePath) => `\\n - ${formatPath(filePath)}`\n ),\n ],\n { level: 'warn' }\n );\n }\n\n // Phase 4: Build field-rename map for minification\n // Reads each compiled dictionary JSON to discover the full nested\n // user-defined field structure, then builds a NestedRenameMap that\n // assigns short alphabetic aliases at every level.\n if (minify) {\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n\n // Fetch-mode dictionaries are served from a remote API using\n // original field names – renaming would break the client/server\n // contract.\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch')\n continue;\n\n // SFC indirect access: skip field rename for these dictionaries\n // to avoid a JSON ↔ source mismatch at runtime.\n if (\n pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey)\n )\n continue;\n\n // Read dictionary content (static JSON first, then dynamic per-locale)\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(\n dictionariesDir,\n `${dictionaryKey}.json`\n );\n try {\n const raw = await readFile(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n try {\n const dynamicDir = join(\n dynamicDictionariesDir,\n dictionaryKey\n );\n const localeFiles = await readdir(dynamicDir);\n const firstJsonFile = localeFiles.find((f) =>\n f.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = await readFile(\n join(dynamicDir, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary file not readable – skip rename for this key\n }\n }\n\n if (!dictionaryContent) continue;\n\n // Build the rename map from ALL user-defined fields in the\n // dictionary — not just the ones statically consumed by source\n // files. Using the full set ensures that:\n // 1. Every field in the compiled JSON is renamed (even if\n // pruned-out fields still appear when purge is disabled).\n // 2. The short-name assignment is stable: the alphabetical\n // order of all fields determines each short name, so adding\n // or removing a consumer never changes names for others.\n // 3. There is no source ↔ JSON mismatch: both sides use the\n // identical map regardless of which subset is consumed.\n const nestedRenameMap =\n buildNestedRenameMapFromContent(dictionaryContent);\n\n // Skip dictionaries whose opaque fields have nested user-defined\n // structure – renaming those sub-keys would silently break child\n // components that consume the field value as-is.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(\n dictionaryKey\n );\n\n if (opaqueFieldMap) {\n const dangerousEntries = [...opaqueFieldMap.entries()].filter(\n ([fieldName]) =>\n (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0\n );\n if (dangerousEntries.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousEntries.flatMap(([fieldName, locations]) => [\n `\\n Opaque field:`,\n colorize(`'${fieldName}'`, ANSIColors.BLUE),\n `(nested keys preserved for stability).`,\n ...locations.map(\n (loc) => `\\n at ${formatPath(loc)}`\n ),\n ]),\n ],\n { level: 'warn', isVerbose: true }\n );\n\n // Disable renaming for the children of opaque fields to prevent\n // breaking components that receive the field as a prop.\n for (const [fieldName] of dangerousEntries) {\n const entry = nestedRenameMap.get(fieldName);\n if (entry) {\n entry.children = new Map();\n }\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n\n if (partiallyMinifiedDictionariesCount > 0) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-partial-minify-summary.lock'\n ),\n () => {\n logger([\n `Partially minified`,\n colorizeNumber(partiallyMinifiedDictionariesCount),\n `dictionar${partiallyMinifiedDictionariesCount === 1 ? 'y' : 'ies'}`,\n `(preserved nested keys for opaque fields).`,\n ]);\n },\n { cacheTimeoutMs: 1000 * 5 }\n );\n }\n }\n },\n },\n\n // Plugin 2: Babel transform\n {\n name: 'vite-intlayer-babel-transform',\n enforce: 'post', // Run after framework transformations (e.g. Vue SFC)\n apply: (_config, env) => {\n const isBuildCommand = env.command === 'build';\n const isEnabled =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isBuildCommand || !isEnabled) return false;\n\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-optimize-plugin-enabled.lock'\n ),\n () =>\n logger([\n `Build optimization ${colorize('enabled', ANSIColors.GREEN)}`,\n colorize('(import mode:', ANSIColors.GREY_DARK),\n colorize(importMode ?? IMPORT_MODE, ANSIColors.BLUE),\n colorize(')', ANSIColors.GREY_DARK),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n },\n\n transform: async (sourceCode, moduleId) => {\n // Strip query parameters added by Vue/Svelte loaders\n // e.g. \"HelloWorld.vue?vue&type=script&setup=true&lang.ts\" → \"HelloWorld.vue\"\n const sourceFilePath = moduleId.split('?', 1)[0];\n\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) return null;\n if (!transformableFilesList.includes(sourceFilePath)) return null;\n\n const isDictionaryEntryFile = [\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n ].includes(sourceFilePath);\n\n const isUsingIntlayer = INTLAYER_USAGE_REGEX.test(sourceCode);\n if (!isUsingIntlayer && !isDictionaryEntryFile) return null;\n\n // Step 1: Field rename (must run before the optimize pass, which\n // replaces useIntlayer → useDictionary and erases the dictionary key)\n let codeToOptimize = sourceCode;\n\n if (pruneContext && isUsingIntlayer) {\n const renamedCode = await renameFieldsInSourceFile(\n sourceFilePath,\n sourceCode,\n pruneContext\n );\n if (renamedCode) {\n codeToOptimize = renamedCode;\n }\n }\n\n // Step 2: Optimize (useIntlayer('key') → useDictionary(_hash))\n const transformResult = await optimizeSourceFile(\n codeToOptimize,\n sourceFilePath,\n {\n optimize,\n dictionariesDir,\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n unmergedDictionariesDir,\n dynamicDictionariesDir,\n dynamicDictionariesEntryPath,\n fetchDictionariesDir,\n fetchDictionariesEntryPath: join(\n mainDir,\n 'fetch_dictionaries.mjs'\n ),\n importMode,\n filesList: transformableFilesList,\n replaceDictionaryEntry: true,\n dictionaryModeMap: dictionaryKeyToImportModeMap,\n }\n );\n\n if (!transformResult) return null;\n\n return {\n code: transformResult.code,\n map: transformResult.map as any,\n };\n },\n },\n ];\n } catch (pluginInitError) {\n console.warn(\n '[vite-intlayer] Failed to initialise optimization plugin:',\n pluginInitError\n );\n return [];\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"intlayerOptimizePlugin.mjs","names":[],"sources":["../../src/intlayerOptimizePlugin.ts"],"sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport {\n analyzeFieldUsageInFile,\n buildNestedRenameMapFromContent,\n INTLAYER_OR_COMPAT_USAGE_REGEX,\n INTLAYER_USAGE_REGEX,\n optimizeSourceFile,\n type PruneContext,\n renameFieldsInSourceFile,\n SOURCE_FILE_REGEX,\n} from '@intlayer/babel';\nimport {\n buildComponentFilesList,\n formatPath,\n runOnce,\n} from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { IMPORT_MODE } from '@intlayer/config/defaultValues';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport { getDictionaries } from '@intlayer/dictionaries-entry';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport type { PluginOption } from 'vite';\nimport { intlayerVueAsyncPlugin } from './intlayerVueAsyncPlugin';\n\n// Plugin\n\n/**\n * Returns the Vite plugins responsible for the build optimisation step.\n *\n * Contains three internal plugins:\n *\n * 1. Vue async plugin – handles Vue SFC async script blocks.\n * 2. Usage analyser (`vite-intlayer-usage-analyzer`) – pre-scans every\n * component source file during `buildStart` to build the field-usage map\n * in `pruneContext`. This runs before any `transform` calls so the\n * downstream prune plugin always has complete data.\n * 3. Babel transform (`vite-intlayer-babel-transform`) – rewrites\n * `useIntlayer('key')` / `getIntlayer('key')` calls into\n * `useDictionary(_hash)` / `getDictionary(_hash)` and injects the\n * corresponding JSON (or dynamic `.mjs`) imports. Also applies field-name\n * renaming when `build.minify` is enabled.\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Shared mutable state written here and read by the\n * prune and minify plugins. Pass `null` to skip\n * analysis (e.g. when both `purge` and `minify` are\n * disabled).\n */\nexport const intlayerOptimize = async (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext | null\n): Promise<PluginOption[]> => {\n try {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, purge, minify } = intlayerConfig.build;\n const editorEnabled = intlayerConfig.editor.enabled;\n\n const importMode =\n intlayerConfig.build.importMode ?? intlayerConfig.dictionary?.importMode;\n\n const {\n dictionariesDir,\n dynamicDictionariesDir,\n unmergedDictionariesDir,\n fetchDictionariesDir,\n mainDir,\n baseDir,\n } = intlayerConfig.system;\n\n const dictionariesEntryPath = join(mainDir, 'dictionaries.mjs');\n const unmergedDictionariesEntryPath = join(\n mainDir,\n 'unmerged_dictionaries.mjs'\n );\n const dynamicDictionariesEntryPath = join(\n mainDir,\n 'dynamic_dictionaries.mjs'\n );\n\n const componentFilesList = buildComponentFilesList(intlayerConfig);\n\n const transformableFilesList = [\n ...componentFilesList,\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n ];\n\n const dictionaries = getDictionaries(intlayerConfig);\n\n const dictionaryKeyToImportModeMap: Record<\n string,\n 'static' | 'dynamic' | 'fetch'\n > = {};\n (Object.values(dictionaries) as Dictionary[]).forEach((dictionary) => {\n dictionaryKeyToImportModeMap[dictionary.key] =\n dictionary.importMode ?? importMode ?? IMPORT_MODE;\n });\n\n const isBuildOptimizeEnabled = (\n _config: unknown,\n env: { command: string }\n ) => {\n const isBuildCommand = env.command === 'build';\n return (optimize === undefined && isBuildCommand) || optimize === true;\n };\n\n const isAnalysisEnabled = (_config: unknown, env: { command: string }) =>\n !editorEnabled &&\n (!!purge || !!minify) &&\n isBuildOptimizeEnabled(_config, env);\n\n let partiallyMinifiedDictionariesCount = 0;\n\n return [\n intlayerVueAsyncPlugin(intlayerConfig, transformableFilesList),\n\n // Plugin 1: Usage analyser\n {\n name: 'vite-intlayer-usage-analyzer',\n enforce: 'pre',\n apply: isAnalysisEnabled,\n\n buildStart: async () => {\n if (!pruneContext) return;\n\n // Phase 1: Babel-based field-usage analysis for all component files\n await Promise.all(\n componentFilesList.map(async (sourceFilePath) => {\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) return;\n\n let sourceCode: string;\n try {\n sourceCode = await readFile(sourceFilePath, 'utf-8');\n } catch {\n return; // unreadable file – skip silently\n }\n\n if (!INTLAYER_OR_COMPAT_USAGE_REGEX.test(sourceCode)) return;\n\n // For Vue/Svelte SFCs, the usage analyzer expects the raw script\n // content. `analyzeFieldUsageInFile` handles block extraction\n // internally via `extractScriptBlocks`.\n try {\n await analyzeFieldUsageInFile(\n sourceFilePath,\n sourceCode,\n pruneContext\n );\n } catch (parseError) {\n pruneContext.hasUnparsableSourceFiles = true;\n logger(\n [\n `Could not parse`,\n formatPath(sourceFilePath),\n `for field-usage analysis.`,\n 'Dictionaries whose usage cannot be confirmed will not be pruned.',\n parseError instanceof Error\n ? `(${parseError.message})`\n : String(parseError),\n ],\n { level: 'warn' }\n );\n }\n })\n );\n\n // Phase 2: Framework-specific analysis for Vue / Svelte / Astro SFC\n // bindings that Babel scope analysis cannot resolve:\n // Vue → `.value` ref-accessor indirection\n // Svelte → `$` reactive store prefix\n // Astro → frontmatter variables referenced in the HTML template\n if (pruneContext.pendingFrameworkAnalysis.size > 0) {\n const vuePending = new Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >();\n const sveltePending = new Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >();\n const astroPending = new Map<\n string,\n { variableName: string; dictionaryKey: string }[]\n >();\n\n for (const [\n filePath,\n entries,\n ] of pruneContext.pendingFrameworkAnalysis) {\n if (filePath.endsWith('.vue')) {\n vuePending.set(filePath, entries);\n } else if (filePath.endsWith('.svelte')) {\n sveltePending.set(filePath, entries);\n } else if (filePath.endsWith('.astro')) {\n astroPending.set(filePath, entries);\n }\n }\n\n /** Merge framework-extracted field usage into pruneContext. */\n const mergeFrameworkResult = (\n dictionaryKey: string,\n fields: Set<string> | undefined\n ): void => {\n if (fields && fields.size > 0) {\n // The Babel rename plugin cannot update source-code property\n // accesses for SFC indirect patterns → suppress field renaming.\n pruneContext.dictionariesSkippingFieldRename.add(dictionaryKey);\n\n const existing =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n if (existing === 'all') return;\n\n const merged =\n existing instanceof Set\n ? new Set([...existing, ...fields])\n : new Set(fields);\n pruneContext.dictionaryKeyToFieldUsageMap.set(\n dictionaryKey,\n merged\n );\n } else {\n pruneContext.dictionaryKeyToFieldUsageMap.set(\n dictionaryKey,\n 'all'\n );\n }\n };\n\n // Vue files\n if (vuePending.size > 0) {\n let extractVueIntlayerFieldUsage:\n | ((\n code: string,\n vars: { variableName: string; dictionaryKey: string }[]\n ) => Map<string, Set<string>>)\n | null = null;\n\n try {\n const vueCompiler = await import('@intlayer/vue-compiler');\n extractVueIntlayerFieldUsage =\n vueCompiler.extractVueIntlayerFieldUsage;\n } catch {\n // @intlayer/vue-compiler not installed – fall back to 'all'\n }\n\n for (const [filePath, entries] of vuePending) {\n if (!extractVueIntlayerFieldUsage) {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n let fileCode: string;\n try {\n fileCode = await readFile(filePath, 'utf-8');\n } catch {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n const result = extractVueIntlayerFieldUsage(fileCode, entries);\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(\n dictionaryKey,\n result.get(dictionaryKey)\n );\n }\n }\n }\n\n // Svelte files\n if (sveltePending.size > 0) {\n let extractSvelteIntlayerFieldUsage:\n | ((\n code: string,\n vars: { variableName: string; dictionaryKey: string }[]\n ) => Map<string, Set<string>>)\n | null = null;\n\n try {\n const svelteCompiler = await import(\n '@intlayer/svelte-compiler'\n );\n extractSvelteIntlayerFieldUsage =\n svelteCompiler.extractSvelteIntlayerFieldUsage;\n } catch {\n // @intlayer/svelte-compiler not installed – fall back to 'all'\n }\n\n for (const [filePath, entries] of sveltePending) {\n if (!extractSvelteIntlayerFieldUsage) {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n let fileCode: string;\n try {\n fileCode = await readFile(filePath, 'utf-8');\n } catch {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n const result = extractSvelteIntlayerFieldUsage(\n fileCode,\n entries\n );\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(\n dictionaryKey,\n result.get(dictionaryKey)\n );\n }\n }\n }\n\n // Astro files\n // Frontmatter variables are used in the HTML template, which is not\n // visible to Babel's scope analysis. Scan the template section for\n // `variableName.fieldName` accesses using a lightweight regex pass.\n if (astroPending.size > 0) {\n for (const [filePath, entries] of astroPending) {\n let fileCode: string;\n try {\n fileCode = await readFile(filePath, 'utf-8');\n } catch {\n for (const { dictionaryKey } of entries) {\n mergeFrameworkResult(dictionaryKey, undefined);\n }\n continue;\n }\n\n // Extract only the template (everything after the closing ---).\n // The frontmatter was already handled by Babel in Phase 1.\n const fenceMatch = /^---\\r?\\n[\\s\\S]*?\\r?\\n---/.exec(fileCode);\n const template = fenceMatch\n ? fileCode.slice(fenceMatch.index + fenceMatch[0].length)\n : fileCode;\n\n for (const { variableName, dictionaryKey } of entries) {\n const escapedVar = variableName.replace(\n /[.*+?^${}()|[\\]\\\\]/g,\n '\\\\$&'\n );\n const fieldRe = new RegExp(\n `\\\\b${escapedVar}\\\\.([a-zA-Z_$][a-zA-Z0-9_$]*)`,\n 'g'\n );\n const foundFields = new Set<string>();\n let m = fieldRe.exec(template);\n while (m !== null) {\n foundFields.add(m[1]);\n m = fieldRe.exec(template);\n }\n mergeFrameworkResult(\n dictionaryKey,\n foundFields.size > 0 ? foundFields : undefined\n );\n }\n }\n }\n }\n\n // Phase 3: Warn about untracked bindings (plain variable assignments)\n for (const [\n dictionaryKey,\n sourceFilePaths,\n ] of pruneContext.dictionaryKeysWithUntrackedBindings) {\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `cannot be purged or minified.`,\n `\\n Reason: the result of`,\n `${colorize(`useIntlayer(`, ANSIColors.GREY_LIGHT)}${colorizeKey(\n `'${dictionaryKey}'`\n )}${colorize(`)`, ANSIColors.GREY_LIGHT)}`,\n `is assigned to a plain variable in:`,\n ...sourceFilePaths.map(\n (filePath) => `\\n - ${formatPath(filePath)}`\n ),\n ],\n { level: 'warn' }\n );\n }\n\n // Phase 4: Build field-rename map for minification\n // Reads each compiled dictionary JSON to discover the full nested\n // user-defined field structure, then builds a NestedRenameMap that\n // assigns short alphabetic aliases at every level.\n if (minify) {\n for (const [\n dictionaryKey,\n fieldUsage,\n ] of pruneContext.dictionaryKeyToFieldUsageMap) {\n if (fieldUsage === 'all') continue;\n\n // Fetch-mode dictionaries are served from a remote API using\n // original field names – renaming would break the client/server\n // contract.\n if (dictionaryKeyToImportModeMap[dictionaryKey] === 'fetch')\n continue;\n\n // SFC indirect access: skip field rename for these dictionaries\n // to avoid a JSON ↔ source mismatch at runtime.\n if (\n pruneContext.dictionariesSkippingFieldRename.has(dictionaryKey)\n )\n continue;\n\n // Read dictionary content (static JSON first, then dynamic per-locale)\n let dictionaryContent: unknown = null;\n\n const staticJsonPath = join(\n dictionariesDir,\n `${dictionaryKey}.json`\n );\n try {\n const raw = await readFile(staticJsonPath, 'utf-8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n } catch {\n try {\n const dynamicDir = join(\n dynamicDictionariesDir,\n dictionaryKey\n );\n const localeFiles = await readdir(dynamicDir);\n const firstJsonFile = localeFiles.find((f) =>\n f.endsWith('.json')\n );\n if (firstJsonFile) {\n const raw = await readFile(\n join(dynamicDir, firstJsonFile),\n 'utf-8'\n );\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n dictionaryContent = parsed.content;\n }\n } catch {\n // Dictionary file not readable – skip rename for this key\n }\n }\n\n if (!dictionaryContent) continue;\n\n // Build the rename map from ALL user-defined fields in the\n // dictionary — not just the ones statically consumed by source\n // files. Using the full set ensures that:\n // 1. Every field in the compiled JSON is renamed (even if\n // pruned-out fields still appear when purge is disabled).\n // 2. The short-name assignment is stable: the alphabetical\n // order of all fields determines each short name, so adding\n // or removing a consumer never changes names for others.\n // 3. There is no source ↔ JSON mismatch: both sides use the\n // identical map regardless of which subset is consumed.\n const nestedRenameMap =\n buildNestedRenameMapFromContent(dictionaryContent);\n\n // Skip dictionaries whose opaque fields have nested user-defined\n // structure – renaming those sub-keys would silently break child\n // components that consume the field value as-is.\n const opaqueFieldMap =\n pruneContext.dictionaryKeysWithOpaqueTopLevelFields.get(\n dictionaryKey\n );\n\n if (opaqueFieldMap) {\n const dangerousEntries = [...opaqueFieldMap.entries()].filter(\n ([fieldName]) =>\n (nestedRenameMap.get(fieldName)?.children.size ?? 0) > 0\n );\n if (dangerousEntries.length > 0) {\n partiallyMinifiedDictionariesCount += 1;\n\n logger(\n [\n `Dictionary`,\n colorizeKey(dictionaryKey),\n `partially minified.`,\n ...dangerousEntries.flatMap(([fieldName, locations]) => [\n `\\n Opaque field:`,\n colorize(`'${fieldName}'`, ANSIColors.BLUE),\n `(nested keys preserved for stability).`,\n ...locations.map(\n (loc) => `\\n at ${formatPath(loc)}`\n ),\n ]),\n ],\n { level: 'warn', isVerbose: true }\n );\n\n // Disable renaming for the children of opaque fields to prevent\n // breaking components that receive the field as a prop.\n for (const [fieldName] of dangerousEntries) {\n const entry = nestedRenameMap.get(fieldName);\n if (entry) {\n entry.children = new Map();\n }\n }\n }\n }\n\n if (nestedRenameMap.size > 0) {\n pruneContext.dictionaryKeyToFieldRenameMap.set(\n dictionaryKey,\n nestedRenameMap\n );\n }\n }\n\n if (partiallyMinifiedDictionariesCount > 0) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-partial-minify-summary.lock'\n ),\n () => {\n logger([\n `Partially minified`,\n colorizeNumber(partiallyMinifiedDictionariesCount),\n `dictionar${partiallyMinifiedDictionariesCount === 1 ? 'y' : 'ies'}`,\n `(preserved nested keys for opaque fields).`,\n ]);\n },\n { cacheTimeoutMs: 1000 * 5 }\n );\n }\n }\n },\n },\n\n // Plugin 2: Babel transform\n {\n name: 'vite-intlayer-babel-transform',\n enforce: 'post', // Run after framework transformations (e.g. Vue SFC)\n apply: (_config, env) => {\n const isBuildCommand = env.command === 'build';\n const isEnabled =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isBuildCommand || !isEnabled) return false;\n\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-optimize-plugin-enabled.lock'\n ),\n () =>\n logger([\n `Build optimization ${colorize('enabled', ANSIColors.GREEN)}`,\n colorize('(import mode:', ANSIColors.GREY_DARK),\n colorize(importMode ?? IMPORT_MODE, ANSIColors.BLUE),\n colorize(')', ANSIColors.GREY_DARK),\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n },\n\n transform: async (sourceCode, moduleId) => {\n // Strip query parameters added by Vue/Svelte loaders\n // e.g. \"HelloWorld.vue?vue&type=script&setup=true&lang.ts\" → \"HelloWorld.vue\"\n const sourceFilePath = moduleId.split('?', 1)[0];\n\n if (!SOURCE_FILE_REGEX.test(sourceFilePath)) return null;\n if (!transformableFilesList.includes(sourceFilePath)) return null;\n\n const isDictionaryEntryFile = [\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n ].includes(sourceFilePath);\n\n const isUsingIntlayer = INTLAYER_USAGE_REGEX.test(sourceCode);\n if (!isUsingIntlayer && !isDictionaryEntryFile) return null;\n\n // Step 1: Field rename (must run before the optimize pass, which\n // replaces useIntlayer → useDictionary and erases the dictionary key)\n let codeToOptimize = sourceCode;\n\n if (pruneContext && isUsingIntlayer) {\n const renamedCode = await renameFieldsInSourceFile(\n sourceFilePath,\n sourceCode,\n pruneContext\n );\n if (renamedCode) {\n codeToOptimize = renamedCode;\n }\n }\n\n // Step 2: Optimize (useIntlayer('key') → useDictionary(_hash))\n const transformResult = await optimizeSourceFile(\n codeToOptimize,\n sourceFilePath,\n {\n optimize,\n dictionariesDir,\n dictionariesEntryPath,\n unmergedDictionariesEntryPath,\n unmergedDictionariesDir,\n dynamicDictionariesDir,\n dynamicDictionariesEntryPath,\n fetchDictionariesDir,\n fetchDictionariesEntryPath: join(\n mainDir,\n 'fetch_dictionaries.mjs'\n ),\n importMode,\n filesList: transformableFilesList,\n replaceDictionaryEntry: true,\n dictionaryModeMap: dictionaryKeyToImportModeMap,\n }\n );\n\n if (!transformResult) return null;\n\n return {\n code: transformResult.code,\n map: transformResult.map as any,\n };\n },\n },\n ];\n } catch (pluginInitError) {\n console.warn(\n '[vite-intlayer] Failed to initialise optimization plugin:',\n pluginInitError\n );\n return [];\n }\n};\n"],"mappings":"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"}
@@ -1,30 +1,37 @@
1
+ import * as ANSIColors from "@intlayer/config/colors";
2
+ import { colorize, getAppLogger } from "@intlayer/config/logger";
1
3
  import { getConfiguration } from "@intlayer/config/node";
2
4
  import { ROUTING_MODE } from "@intlayer/config/defaultValues";
3
- import { parse } from "node:url";
5
+ import { fileURLToPath, parse } from "node:url";
4
6
  import { getCanonicalPath, getLocalizedPath, getRewriteRules, localeDetector } from "@intlayer/core/localization";
5
7
  import { getCookie, getLocaleFromStorageServer, setLocaleInStorageServer } from "@intlayer/core/utils";
6
8
 
7
9
  //#region src/intlayerProxyPlugin.ts
8
10
  /**
9
- * Vite plugin that provides a development middleware for locale-based routing.
11
+ * Creates a standalone, framework-agnostic locale-routing middleware.
10
12
  *
11
- * This plugin mimics the behavior of the Intlayer middleware in Next.js,
12
- * handling locale detection, redirects, and rewrites during development.
13
+ * This function contains all the locale detection, redirect, and rewrite logic.
14
+ * It is intentionally separated from the Vite plugin so the same handler can be
15
+ * used in every environment:
13
16
  *
14
- * @param configOptions - Optional configuration for Intlayer.
15
- * @param options - Plugin-specific options, like ignoring certain paths.
16
- * @returns A Vite plugin.
17
+ * - **Dev**: wired up automatically by `intlayerProxy` via `configureServer`
18
+ * - **Preview**: wired up automatically by `intlayerProxy` via `configurePreviewServer`
19
+ * - **Production (Nitro / TanStack Start)**: create `server/middleware/intlayerProxy.ts`:
17
20
  *
18
21
  * @example
19
22
  * ```ts
20
- * import { intlayerProxy } from 'vite-intlayer';
23
+ * // server/middleware/intlayerProxy.ts
24
+ * import { fromNodeMiddleware } from 'h3';
25
+ * import { createIntlayerProxyHandler } from 'vite-intlayer';
21
26
  *
22
- * export default defineConfig({
23
- * plugins: [intlayerProxy()],
24
- * });
27
+ * export default fromNodeMiddleware(createIntlayerProxyHandler());
25
28
  * ```
29
+ *
30
+ * @param configOptions - Optional Intlayer configuration overrides.
31
+ * @param options - Plugin-specific options, such as path ignoring.
32
+ * @returns A Connect-compatible `(req, res, next) => void` middleware.
26
33
  */
27
- const intlayerProxy = (configOptions, options) => {
34
+ const createIntlayerProxyHandler = (configOptions, options) => {
28
35
  const { internationalization, routing } = getConfiguration(configOptions);
29
36
  const { locales: supportedLocales, defaultLocale } = internationalization;
30
37
  const { basePath = "", mode = ROUTING_MODE, rewrite, domains } = routing;
@@ -70,6 +77,7 @@ const intlayerProxy = (configOptions, options) => {
70
77
  };
71
78
  /**
72
79
  * Extracts the locale from the URL pathname if present as the first segment.
80
+ * e.g. if pathname is /en/some/page or /en, checks if "en" is in supportedLocales.
73
81
  */
74
82
  const getPathLocale = (pathname) => {
75
83
  const firstSegment = pathname.split("/").filter(Boolean)[0];
@@ -112,8 +120,9 @@ const intlayerProxy = (configOptions, options) => {
112
120
  return res.end();
113
121
  };
114
122
  /**
115
- * "Rewrite" the request internally by adjusting req.url;
116
- * we also set the locale in the response header if needed.
123
+ * "Rewrite" the request internally by adjusting req.url.
124
+ * Also sets the locale in the response/request headers via storage to mimic
125
+ * Next.js's behaviour of propagating the detected locale downstream.
117
126
  */
118
127
  const rewriteUrl = (req, res, newUrl, locale) => {
119
128
  if (req.url !== newUrl) req.url = newUrl;
@@ -124,10 +133,10 @@ const intlayerProxy = (configOptions, options) => {
124
133
  };
125
134
  /**
126
135
  * Constructs a new path string, optionally including a locale prefix, basePath, and search parameters.
127
- * - basePath: (e.g., '/myapp')
128
- * - locale: (e.g., 'en')
129
- * - currentPath:(e.g., '/products/shoes')
130
- * - search: (e.g., '?foo=bar')
136
+ * - basePath: (e.g. '/myapp')
137
+ * - locale: (e.g. 'en')
138
+ * - currentPath: (e.g. '/products/shoes')
139
+ * - search: (e.g. '?foo=bar')
131
140
  */
132
141
  const constructPath = (locale, currentPath, search) => {
133
142
  const pathWithoutPrefix = currentPath.startsWith(`/${locale}`) ? currentPath.slice(`/${locale}`.length) : currentPath;
@@ -168,9 +177,7 @@ const intlayerProxy = (configOptions, options) => {
168
177
  return next();
169
178
  };
170
179
  /**
171
- * The main prefix logic:
172
- * - If there's no pathLocale in the URL, we might want to detect & redirect or rewrite
173
- * - If there is a pathLocale, handle storage mismatch or default locale special cases
180
+ * The main prefix logic.
174
181
  */
175
182
  const handlePrefix = ({ req, res, next, originalPath, searchParams, pathLocale, storageLocale, originalUrl }) => {
176
183
  if (!pathLocale) {
@@ -197,7 +204,7 @@ const intlayerProxy = (configOptions, options) => {
197
204
  };
198
205
  /**
199
206
  * Handles requests where the locale is missing from the URL pathname.
200
- * We detect a locale from storage / headers / default, then either redirect or rewrite.
207
+ * Detects a locale from storage / headers / default, then either redirects or rewrites.
201
208
  */
202
209
  const handleMissingPathLocale = ({ req, res, next, originalPath, searchParams, storageLocale, originalUrl }) => {
203
210
  let locale = storageLocale ?? localeDetector(req.headers, supportedLocales, defaultLocale);
@@ -248,61 +255,119 @@ const intlayerProxy = (configOptions, options) => {
248
255
  rewriteUrl(req, res, searchParams ? `${internalUrl}${searchParams}` : internalUrl, pathLocale);
249
256
  return next();
250
257
  };
251
- return {
252
- name: "vite-intlayer-middleware-plugin",
253
- configureServer: (server) => {
254
- server.middlewares.use((req, res, next) => {
255
- if ((options?.ignore?.(req) ?? false) || req.url?.startsWith("/node_modules") || req.url?.startsWith("/@") || req.url?.startsWith("/_") || req.url?.split("?")[0].match(/\.[a-z]+$/i)) return next();
256
- const parsedUrl = parse(req.url ?? "/", true);
257
- const originalPath = parsedUrl.pathname ?? "/";
258
- const searchParams = parsedUrl.search ?? "";
259
- const pathLocale = getPathLocale(originalPath);
260
- const storageLocale = getStorageLocale(req);
261
- const effectiveStorageLocale = pathLocale && supportedLocales.includes(pathLocale) ? pathLocale : storageLocale;
262
- const originalUrl = req.url;
263
- if (process.env["INTLAYER_ROUTING_DOMAINS"] !== "false" && !noPrefix && pathLocale && domains) {
264
- const localeDomain = domains[pathLocale];
265
- if (localeDomain) {
266
- const reqHost = (req.headers["host"] ?? "").split(":")[0];
267
- if (normalizeDomainHostname(localeDomain) !== reqHost) {
268
- const rawPath = originalPath.slice(`/${pathLocale}`.length) || "/";
269
- redirectUrl(res, `${/^https?:\/\//.test(localeDomain) ? localeDomain : `https://${localeDomain}`}${rawPath}${searchParams}`, "domain-routing", originalUrl);
270
- return;
271
- }
272
- }
273
- }
274
- if (process.env["INTLAYER_ROUTING_DOMAINS"] !== "false" && !noPrefix && !pathLocale) {
275
- const reqHost = (req.headers["host"] ?? "").split(":")[0];
276
- const domainLocale = getLocaleFromDomain(reqHost);
277
- if (domainLocale) {
278
- const internalPath = `/${domainLocale}${getCanonicalPath(originalPath, domainLocale, rewriteRules)}`;
279
- rewriteUrl(req, res, searchParams ? `${internalPath}${searchParams}` : internalPath, domainLocale);
280
- return next();
281
- }
282
- }
283
- if (noPrefix) {
284
- handleNoPrefix({
285
- req,
286
- res,
287
- next,
288
- originalPath,
289
- searchParams,
290
- storageLocale: effectiveStorageLocale,
291
- originalUrl
292
- });
258
+ return (req, res, next) => {
259
+ if ((options?.ignore?.(req) ?? false) || req.url?.startsWith("/node_modules") || req.url?.startsWith("/@") || req.url?.startsWith("/_") || req.url?.split("?")[0]?.match(/\.[a-z]+$/i)) return next();
260
+ const parsedUrl = parse(req.url ?? "/", true);
261
+ const originalPath = parsedUrl.pathname ?? "/";
262
+ const searchParams = parsedUrl.search ?? "";
263
+ const pathLocale = getPathLocale(originalPath);
264
+ const storageLocale = getStorageLocale(req);
265
+ const effectiveStorageLocale = pathLocale && supportedLocales.includes(pathLocale) ? pathLocale : storageLocale;
266
+ const originalUrl = req.url;
267
+ if (process.env["INTLAYER_ROUTING_DOMAINS"] !== "false" && !noPrefix && pathLocale && domains) {
268
+ const localeDomain = domains[pathLocale];
269
+ if (localeDomain) {
270
+ const reqHost = (req.headers["host"] ?? "").split(":")[0] ?? "";
271
+ if (normalizeDomainHostname(localeDomain) !== reqHost) {
272
+ const rawPath = originalPath.slice(`/${pathLocale}`.length) || "/";
273
+ redirectUrl(res, `${/^https?:\/\//.test(localeDomain) ? localeDomain : `https://${localeDomain}`}${rawPath}${searchParams}`, "domain-routing", originalUrl);
293
274
  return;
294
275
  }
295
- handlePrefix({
296
- req,
297
- res,
298
- next,
299
- originalPath,
300
- searchParams,
301
- pathLocale,
302
- storageLocale: effectiveStorageLocale,
303
- originalUrl
304
- });
276
+ }
277
+ }
278
+ if (process.env["INTLAYER_ROUTING_DOMAINS"] !== "false" && !noPrefix && !pathLocale) {
279
+ const domainLocale = getLocaleFromDomain((req.headers["host"] ?? "").split(":")[0] ?? "");
280
+ if (domainLocale) {
281
+ const internalPath = `/${domainLocale}${getCanonicalPath(originalPath, domainLocale, rewriteRules)}`;
282
+ rewriteUrl(req, res, searchParams ? `${internalPath}${searchParams}` : internalPath, domainLocale);
283
+ return next();
284
+ }
285
+ }
286
+ if (noPrefix) {
287
+ handleNoPrefix({
288
+ req,
289
+ res,
290
+ next,
291
+ originalPath,
292
+ searchParams,
293
+ storageLocale: effectiveStorageLocale,
294
+ originalUrl
305
295
  });
296
+ return;
297
+ }
298
+ handlePrefix({
299
+ req,
300
+ res,
301
+ next,
302
+ originalPath,
303
+ searchParams,
304
+ pathLocale,
305
+ storageLocale: effectiveStorageLocale,
306
+ originalUrl
307
+ });
308
+ };
309
+ };
310
+ /**
311
+ * Vite plugin that provides locale-based routing middleware for **all environments**:
312
+ * development, preview, and production SSR (Nitro / TanStack Start).
313
+ *
314
+ * - **Dev** (`vite dev`): registered via `configureServer`.
315
+ * - **Preview** (`vite preview`): registered via `configurePreviewServer`.
316
+ * - **Production Nitro** (`vite build`): automatically injected via the `.nitro` module
317
+ * property that `nitro/vite` reads and pushes into `nitroConfig.modules`. The module
318
+ * registers `intlayerNitroHandler` as a Nitro server middleware — no extra user config
319
+ * needed.
320
+ *
321
+ * If you need custom config options or an `ignore` predicate in production, bypass
322
+ * auto-injection and create a server middleware file manually:
323
+ *
324
+ * ```ts
325
+ * // server/middleware/intlayerProxy.ts
326
+ * import { fromNodeMiddleware } from 'h3';
327
+ * import { createIntlayerProxyHandler } from 'vite-intlayer';
328
+ *
329
+ * export default fromNodeMiddleware(
330
+ * createIntlayerProxyHandler(myConfig, { ignore: (req) => req.url?.startsWith('/api') })
331
+ * );
332
+ * ```
333
+ *
334
+ * @param configOptions - Optional configuration for Intlayer.
335
+ * @param options - Plugin-specific options, like ignoring certain paths.
336
+ * @returns A Vite plugin.
337
+ *
338
+ * @example
339
+ * ```ts
340
+ * import { intlayerProxy } from 'vite-intlayer';
341
+ *
342
+ * export default defineConfig({
343
+ * plugins: [intlayerProxy()],
344
+ * });
345
+ * ```
346
+ */
347
+ const intlayerProxy = (configOptions, options) => {
348
+ const handler = createIntlayerProxyHandler(configOptions, options);
349
+ const logger = getAppLogger(getConfiguration(configOptions));
350
+ return {
351
+ name: "vite-intlayer-middleware-plugin",
352
+ nitro: {
353
+ name: "intlayer-proxy",
354
+ setup(nitro) {
355
+ if (nitro.options.dev) return;
356
+ const handlerPath = fileURLToPath(new URL("./intlayerNitroHandler.mjs", import.meta.url));
357
+ nitro.options.handlers.push({
358
+ route: "/**",
359
+ handler: handlerPath,
360
+ middleware: true
361
+ });
362
+ }
363
+ },
364
+ configureServer: (server) => {
365
+ logger(`Intlayer proxy ${colorize("enabled", ANSIColors.GREEN)}`, { level: "info" });
366
+ server.middlewares.use(handler);
367
+ },
368
+ configurePreviewServer: (server) => {
369
+ logger(`Intlayer proxy ${colorize("enabled", ANSIColors.GREEN)}`, { level: "info" });
370
+ server.middlewares.use(handler);
306
371
  }
307
372
  };
308
373
  };
@@ -321,9 +386,9 @@ const intlayerProxy = (configOptions, options) => {
321
386
  const intlayerMiddleware = intlayerProxy;
322
387
  /**
323
388
  * @deprecated Rename to intlayerProxy instead
324
- *
389
+ *
325
390
  * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.
326
-
391
+ *
327
392
  * ```ts
328
393
  * // Example usage of the plugin in a Vite configuration
329
394
  * export default defineConfig({
@@ -334,5 +399,5 @@ const intlayerMiddleware = intlayerProxy;
334
399
  const intLayerMiddlewarePlugin = intlayerProxy;
335
400
 
336
401
  //#endregion
337
- export { intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy };
402
+ export { createIntlayerProxyHandler, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy };
338
403
  //# sourceMappingURL=intlayerProxyPlugin.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerProxyPlugin.mjs","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { parse } from 'node:url';\nimport { ROUTING_MODE } from '@intlayer/config/defaultValues';\n\n// ── Tree-shake constants ──────────────────────────────────────────────────────\n// When these env vars are injected at build time, bundlers eliminate the\n// branches guarded by these constants.\n\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport {\n getCanonicalPath,\n getLocalizedPath,\n getRewriteRules,\n localeDetector,\n} from '@intlayer/core/localization';\nimport {\n getCookie,\n getLocaleFromStorageServer,\n setLocaleInStorageServer,\n} from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\n/* @ts-ignore - Vite types error */\nimport type { Connect, Plugin } from 'vite';\n\ntype 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: [ intlayerProxyPlugin({ 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\n/**\n * Vite plugin that provides a development middleware for locale-based routing.\n *\n * This plugin mimics the behavior of the Intlayer middleware in Next.js,\n * handling locale detection, redirects, and rewrites during development.\n *\n * @param configOptions - Optional configuration for Intlayer.\n * @param options - Plugin-specific options, like ignoring certain paths.\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 */\nexport const intlayerProxy = (\n configOptions?: GetConfigurationOptions,\n options?: IntlayerProxyPluginOptions\n): Plugin => {\n const intlayerConfig = getConfiguration(configOptions);\n\n const { internationalization, routing } = intlayerConfig;\n const { locales: supportedLocales, defaultLocale } = internationalization;\n\n const { basePath = '', mode = ROUTING_MODE, rewrite, domains } = routing;\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 process.env['INTLAYER_ROUTING_MODE'] !== 'prefix-no-default'\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 * Strips the protocol from a domain string, returning only the hostname.\n */\n const normalizeDomainHostname = (domain: string): string => {\n try {\n return /^https?:\\/\\//.test(domain) ? new URL(domain).hostname : domain;\n } catch {\n return domain;\n }\n };\n\n /**\n * Returns the locale exclusively mapped to a given hostname via `routing.domains`,\n * or undefined if zero or more than one locale share that hostname.\n */\n const getLocaleFromDomain = (hostname: string): Locale | undefined => {\n if (!domains) return undefined;\n const matching = Object.entries(domains).filter(\n ([, domain]) => normalizeDomainHostname(domain!) === hostname\n );\n return matching.length === 1 ? (matching[0][0] as Locale) : undefined;\n };\n\n /* --------------------------------------------------------------------\n * Helper & Utility Functions\n * --------------------------------------------------------------------\n */\n\n /**\n * Retrieves the locale from storage (cookies, localStorage, sessionStorage).\n */\n const getStorageLocale = (req: IncomingMessage): Locale | undefined => {\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 * Extracts the locale from the URL pathname if present as the first segment.\n */\n const getPathLocale = (pathname: string): Locale | undefined => {\n // e.g. if pathname is /en/some/page or /en\n // we check if \"en\" is in your supportedLocales\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 * Writes a 301 redirect response with the given new URL.\n */\n const redirectUrl = (\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n reason?: string,\n originalUrl?: string\n ) => {\n if (originalUrl) {\n if (originalUrl === newUrl) {\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 key = `${originalUrl} -> ${newUrl}`;\n const prev = redirectCounts.get(key);\n const count =\n prev && now - prev.lastSeen < REDIRECT_TTL_MS ? prev.count + 1 : 1;\n\n redirectCounts.set(key, { count, lastSeen: now });\n\n if (count > MAX_REDIRECTS) {\n console.error('[REDIRECT LOOP DETECTED!]', {\n originalUrl,\n redirectCount: count,\n lastRedirectTo: newUrl,\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 [key, entry] of redirectCounts) {\n if (now - entry.lastSeen >= REDIRECT_TTL_MS) redirectCounts.delete(key);\n }\n }\n\n res.writeHead(301, { Location: newUrl });\n return res.end();\n };\n\n /**\n * \"Rewrite\" the request internally by adjusting req.url;\n * we also set the locale in the response header if needed.\n */\n const rewriteUrl = (\n req: Connect.IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n locale?: Locale\n ) => {\n if (req.url !== newUrl) {\n req.url = newUrl;\n }\n // If you want to mimic Next.js's behavior of setting a header for the locale:\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 if present\n const pathWithoutPrefix = currentPath.startsWith(`/${locale}`)\n ? currentPath.slice(`/${locale}`.length)\n : currentPath;\n\n // Ensure basePath always starts with '/', and remove trailing slash if needed\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 = currentPath.startsWith(`/${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 // 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 // Determine the best locale\n let locale = storageLocale ?? defaultLocale;\n\n // Use fallback to localeDetector if no storage locale\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 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 return redirectUrl(res, redirectPath, undefined, originalUrl);\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 the existing locale matches the detected locale, no redirect needed\n if (existingLocale === locale) {\n // For internal routing, we need to add the locale prefix so the framework can match [locale] param\n const internalPath = `/${locale}${canonicalPath}`;\n const rewritePath = `${internalPath}${searchParams ?? ''}`;\n\n // Rewrite internally (URL stays the same in browser, but internally routes to /[locale]/path)\n rewriteUrl(req, res, rewritePath, locale);\n return next();\n }\n\n // Locale param missing or doesn't match - redirect to add/update it\n const search = appendLocaleSearchIfNeeded(searchParams, locale);\n const redirectPath = search\n ? `${originalPath}${search}`\n : `${originalPath}${searchParams ?? ''}`;\n\n // Redirect to add/update the locale search param (URL changes in browser)\n return redirectUrl(res, redirectPath, undefined, originalUrl);\n }\n\n // For no-prefix mode (not search-params), add locale prefix internally for routing\n const internalPath = `/${locale}${canonicalPath}`;\n\n // Add search params if needed\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 browser, but internally routes to /[locale]/path)\n rewriteUrl(req, res, rewritePath, locale);\n\n return next();\n };\n\n /**\n * The main prefix logic:\n * - If there's no pathLocale in the URL, we might want to detect & redirect or rewrite\n * - If there is a pathLocale, handle storage mismatch or default locale special cases\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 is missing, handle\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 // If pathLocale exists, handle it\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 * We detect a locale from storage / headers / default, then either redirect or rewrite.\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\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, fallback\n if (!supportedLocales.includes(locale)) {\n locale = defaultLocale;\n }\n\n // Resolve to canonical path.\n // If user visits /a-propos (implied 'fr'), we resolve to /about\n const canonicalPath = getCanonicalPath(originalPath, locale, rewriteRules);\n\n // Determine target localized path for redirection\n // /about + 'fr' -> /a-propos\n const targetLocalizedPathResult = getLocalizedPath(\n canonicalPath,\n locale,\n rewriteRules\n );\n const targetLocalizedPath =\n typeof targetLocalizedPathResult === 'string'\n ? targetLocalizedPathResult\n : targetLocalizedPathResult.path;\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, do a 301 redirect\n // so that the user sees the locale in the URL.\n if (prefixDefault || locale !== defaultLocale) {\n return redirectUrl(res, newPath, undefined, 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 Vite serves index.html 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 // In prefix modes, respect the URL path locale\n // The path locale takes precedence, and we'll update storage to match\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 default AND the path locale is the default locale -> remove it\n if (!prefixDefault && pathLocale === defaultLocale) {\n const targetLocalizedPathResult = getLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n const targetLocalizedPath =\n typeof targetLocalizedPathResult === 'string'\n ? targetLocalizedPathResult\n : targetLocalizedPathResult.path;\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 return redirectUrl(\n res,\n fullPath + (searchParams ?? ''),\n undefined,\n originalUrl\n );\n }\n\n // If we do prefix default or pathLocale != default, keep as is, but rewrite to canonical internally\n const internalUrl = `/${pathLocale}${canonicalPath}`;\n const newPath = searchParams\n ? `${internalUrl}${searchParams}`\n : internalUrl;\n\n rewriteUrl(req, res, newPath, pathLocale);\n return next();\n };\n\n return {\n name: 'vite-intlayer-middleware-plugin',\n configureServer: (server) => {\n server.middlewares.use((req, res, next) => {\n // Bypass assets and special Vite endpoints\n if (\n // Custom ignore function\n (options?.ignore?.(req) ?? false) ||\n req.url?.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 req.url?.startsWith('/@') ||\n /**\n * /^__vite_ping$ # health ping\n * /^__open-in-editor$\n * /^__manifest$ # Remix/RR7 lazyRouteDiscovery\n */\n req.url?.startsWith('/_') ||\n /**\n * ./myFile.js\n */\n req.url?.split('?')[0].match(/\\.[a-z]+$/i) // checks for file extensions\n ) {\n return next();\n }\n\n // Parse original URL for path and query\n const parsedUrl = parse(req.url ?? '/', true);\n const originalPath = parsedUrl.pathname ?? '/';\n const searchParams = parsedUrl.search ?? '';\n\n // Check if there's a locale prefix in the path FIRST\n const pathLocale = getPathLocale(originalPath);\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 = (req.headers['host'] ?? '').split(':')[0];\n const domainHost = normalizeDomainHostname(localeDomain);\n if (domainHost !== reqHost) {\n const rawPath =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n const targetOrigin = /^https?:\\/\\//.test(localeDomain)\n ? localeDomain\n : `https://${localeDomain}`;\n redirectUrl(\n res,\n `${targetOrigin}${rawPath}${searchParams}`,\n 'domain-routing',\n originalUrl\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 = (req.headers['host'] ?? '').split(':')[0];\n const domainLocale = getLocaleFromDomain(reqHost);\n if (domainLocale) {\n const canonicalPath = getCanonicalPath(\n originalPath,\n domainLocale,\n rewriteRules\n );\n const internalPath = `/${domainLocale}${canonicalPath}`;\n rewriteUrl(\n req,\n res,\n searchParams ? `${internalPath}${searchParams}` : internalPath,\n domainLocale\n );\n return next();\n }\n }\n\n // If noPrefix is true, we skip prefix logic altogether\n if (noPrefix) {\n handleNoPrefix({\n req,\n res,\n next,\n originalPath,\n searchParams,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n return;\n }\n\n // Otherwise, handle prefix logic\n handlePrefix({\n req,\n res,\n next,\n originalPath,\n searchParams,\n pathLocale,\n storageLocale: effectiveStorageLocale,\n originalUrl,\n });\n });\n },\n };\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":"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"}
1
+ {"version":3,"file":"intlayerProxyPlugin.mjs","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { fileURLToPath, parse } from 'node:url';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport { ROUTING_MODE } from '@intlayer/config/defaultValues';\nimport { colorize, getAppLogger } from '@intlayer/config/logger';\nimport {\n type GetConfigurationOptions,\n getConfiguration,\n} from '@intlayer/config/node';\nimport {\n getCanonicalPath,\n getLocalizedPath,\n getRewriteRules,\n localeDetector,\n} from '@intlayer/core/localization';\nimport {\n getCookie,\n getLocaleFromStorageServer,\n setLocaleInStorageServer,\n} from '@intlayer/core/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\n/* @ts-ignore - Vite types error */\nimport type { Connect, Plugin } from 'vite';\n\ntype 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: [ intlayerProxyPlugin({ 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\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 */\ntype NodeMiddleware = (\n req: IncomingMessage,\n res: ServerResponse<IncomingMessage>,\n next: () => void\n) => void;\n\n/**\n * Creates a standalone, framework-agnostic locale-routing middleware.\n *\n * This function contains all the locale detection, redirect, and rewrite logic.\n * It is intentionally separated from the Vite plugin so the same handler can be\n * 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 configOptions - Optional Intlayer configuration overrides.\n * @param options - Plugin-specific options, such as path ignoring.\n * @returns A Connect-compatible `(req, res, next) => void` middleware.\n */\nexport const createIntlayerProxyHandler = (\n configOptions?: GetConfigurationOptions,\n options?: IntlayerProxyPluginOptions\n): NodeMiddleware => {\n const intlayerConfig = getConfiguration(configOptions);\n\n const { internationalization, routing } = intlayerConfig;\n const { locales: supportedLocales, defaultLocale } = internationalization;\n\n const { basePath = '', mode = ROUTING_MODE, rewrite, domains } = routing;\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 process.env['INTLAYER_ROUTING_MODE'] !== 'prefix-no-default'\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 * Strips the protocol from a domain string, returning only the hostname.\n */\n const normalizeDomainHostname = (domain: string): string => {\n try {\n return /^https?:\\/\\//.test(domain) ? new URL(domain).hostname : domain;\n } catch {\n return domain;\n }\n };\n\n /**\n * Returns the locale exclusively mapped to a given hostname via `routing.domains`,\n * or undefined if zero or more than one locale share that hostname.\n */\n const getLocaleFromDomain = (hostname: string): Locale | undefined => {\n if (!domains) return undefined;\n const matching = Object.entries(domains).filter(\n ([, domain]) => normalizeDomainHostname(domain!) === hostname\n );\n return matching.length === 1 ? (matching[0]![0] as Locale) : undefined;\n };\n\n /* --------------------------------------------------------------------\n * Helper & Utility Functions\n * --------------------------------------------------------------------\n */\n\n /**\n * Retrieves the locale from storage (cookies, localStorage, sessionStorage).\n */\n const getStorageLocale = (req: IncomingMessage): Locale | undefined => {\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 * 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 * Writes a 301 redirect response with the given new URL.\n */\n const redirectUrl = (\n res: ServerResponse<IncomingMessage>,\n newUrl: string,\n reason?: string,\n originalUrl?: string\n ) => {\n if (originalUrl) {\n if (originalUrl === newUrl) {\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 key = `${originalUrl} -> ${newUrl}`;\n const prev = redirectCounts.get(key);\n const count =\n prev && now - prev.lastSeen < REDIRECT_TTL_MS ? prev.count + 1 : 1;\n\n redirectCounts.set(key, { count, lastSeen: now });\n\n if (count > MAX_REDIRECTS) {\n console.error('[REDIRECT LOOP DETECTED!]', {\n originalUrl,\n redirectCount: count,\n lastRedirectTo: newUrl,\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 [key, entry] of redirectCounts) {\n if (now - entry.lastSeen >= REDIRECT_TTL_MS) redirectCounts.delete(key);\n }\n }\n\n res.writeHead(301, { Location: newUrl });\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 if (req.url !== newUrl) {\n req.url = newUrl;\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 = currentPath.startsWith(`/${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 = currentPath.startsWith(`/${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 // 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 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 return redirectUrl(res, redirectPath, undefined, originalUrl);\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 const internalPath = `/${locale}${canonicalPath}`;\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(res, redirectPath, undefined, 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 const internalPath = `/${locale}${canonicalPath}`;\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 // 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 targetLocalizedPathResult = getLocalizedPath(\n canonicalPath,\n locale,\n rewriteRules\n );\n const targetLocalizedPath =\n typeof targetLocalizedPathResult === 'string'\n ? targetLocalizedPathResult\n : targetLocalizedPathResult.path;\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 301 redirect so the user sees the locale in the URL\n if (prefixDefault || locale !== defaultLocale) {\n return redirectUrl(res, newPath, undefined, 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 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 targetLocalizedPathResult = getLocalizedPath(\n canonicalPath,\n pathLocale,\n rewriteRules\n );\n const targetLocalizedPath =\n typeof targetLocalizedPathResult === 'string'\n ? targetLocalizedPathResult\n : targetLocalizedPathResult.path;\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 return redirectUrl(\n res,\n fullPath + (searchParams ?? ''),\n undefined,\n originalUrl\n );\n }\n\n // If we do prefix the default or pathLocale !== default, keep as-is\n // but rewrite to canonical internally\n const internalUrl = `/${pathLocale}${canonicalPath}`;\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 // Bypass assets and special Vite/server endpoints\n if (\n // Custom ignore function\n (options?.ignore?.(req) ?? false) ||\n req.url?.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 req.url?.startsWith('/@') ||\n /**\n * /^__vite_ping$ # health ping\n * /^__open-in-editor$\n * /^__manifest$ # Remix/RR7 lazyRouteDiscovery\n */\n req.url?.startsWith('/_') ||\n /**\n * ./myFile.js\n */\n req.url?.split('?')[0]?.match(/\\.[a-z]+$/i) // checks for file extensions\n ) {\n return next();\n }\n\n // Parse original URL for path and query\n const parsedUrl = parse(req.url ?? '/', true);\n const originalPath = parsedUrl.pathname ?? '/';\n const searchParams = parsedUrl.search ?? '';\n\n // Check if there's a locale prefix in the path FIRST\n const pathLocale = getPathLocale(originalPath);\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 = (req.headers['host'] ?? '').split(':')[0] ?? '';\n const domainHost = normalizeDomainHostname(localeDomain);\n if (domainHost !== reqHost) {\n const rawPath = originalPath.slice(`/${pathLocale}`.length) || '/';\n const targetOrigin = /^https?:\\/\\//.test(localeDomain)\n ? localeDomain\n : `https://${localeDomain}`;\n redirectUrl(\n res,\n `${targetOrigin}${rawPath}${searchParams}`,\n 'domain-routing',\n originalUrl\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 = (req.headers['host'] ?? '').split(':')[0] ?? '';\n const domainLocale = getLocaleFromDomain(reqHost);\n if (domainLocale) {\n const canonicalPath = getCanonicalPath(\n originalPath,\n domainLocale,\n rewriteRules\n );\n const internalPath = `/${domainLocale}${canonicalPath}`;\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\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(myConfig, { ignore: (req) => req.url?.startsWith('/api') })\n * );\n * ```\n *\n * @param configOptions - Optional configuration for Intlayer.\n * @param options - Plugin-specific options, like ignoring certain paths.\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 */\nexport const intlayerProxy = (\n configOptions?: GetConfigurationOptions,\n options?: IntlayerProxyPluginOptions\n): Plugin => {\n const handler = createIntlayerProxyHandler(configOptions, options);\n const intlayerConfig = getConfiguration(configOptions);\n const logger = getAppLogger(intlayerConfig);\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 nitro.options.handlers.push({\n route: '/**',\n handler: handlerPath,\n middleware: true,\n });\n },\n };\n\n return {\n name: 'vite-intlayer-middleware-plugin',\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 logger(`Intlayer proxy ${colorize('enabled', ANSIColors.GREEN)}`, {\n level: 'info',\n });\n server.middlewares.use(handler);\n },\n // Vite preview server\n configurePreviewServer: (server) => {\n logger(`Intlayer proxy ${colorize('enabled', ANSIColors.GREEN)}`, {\n level: 'info',\n });\n server.middlewares.use(handler);\n },\n } as 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":"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"}
@@ -2,6 +2,6 @@ import { IntlayerCompilerOptions, intlayerCompiler } from "./IntlayerCompilerPlu
2
2
  import { intlayerMinify } from "./intlayerMinifyPlugin.js";
3
3
  import { intlayerOptimize } from "./intlayerOptimizePlugin.js";
4
4
  import { intLayerPlugin, intlayer, intlayerPlugin } from "./intlayerPlugin.js";
5
- import { intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy } from "./intlayerProxyPlugin.js";
5
+ import { createIntlayerProxyHandler, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy } from "./intlayerProxyPlugin.js";
6
6
  import { intlayerPrune } from "./intlayerPrunePlugin.js";
7
- export { IntlayerCompilerOptions, intLayerMiddlewarePlugin, intLayerPlugin, intlayer, intlayerCompiler, intlayerMiddleware, intlayerMinify, intlayerOptimize, intlayerPlugin, intlayerProxy, intlayerPrune };
7
+ export { IntlayerCompilerOptions, createIntlayerProxyHandler, intLayerMiddlewarePlugin, intLayerPlugin, intlayer, intlayerCompiler, intlayerMiddleware, intlayerMinify, intlayerOptimize, intlayerPlugin, intlayerProxy, intlayerPrune };
@@ -0,0 +1,57 @@
1
+ //#region src/intlayerNitroHandler.d.ts
2
+ /**
3
+ * Minimal duck-type for h3 v2's H3Event.
4
+ *
5
+ * We intentionally avoid importing from 'h3' to keep this file runtime-agnostic —
6
+ * Nitro bundles h3 internally and provides the populated event at runtime. Using a
7
+ * structural type here means the file compiles without h3 in devDependencies and
8
+ * works with any h3 v2-compatible runtime (Bun, Deno, Node).
9
+ */
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
+ /**
17
+ * Full URL object — a **plain property** (not a getter) on H3Event, safe to
18
+ * replace for internal URL rewrites. After assignment, `event.path` will
19
+ * automatically reflect the new pathname + search via the getter.
20
+ */
21
+ url: URL;
22
+ /**
23
+ * Web Fetch API Headers — always populated in h3 v2 regardless of preset
24
+ * (Node, Bun, Deno). Use `.get(name)` instead of bracket-access.
25
+ */
26
+ readonly headers: Headers;
27
+ /**
28
+ * Lazy response object — created on first access; its `headers` carry outgoing
29
+ * response headers (e.g. Set-Cookie) that h3 merges into the HTTP response.
30
+ */
31
+ readonly res: {
32
+ readonly headers: Headers;
33
+ };
34
+ };
35
+ /**
36
+ * Native h3 v2 event handler for Nitro production servers (TanStack Start, Nuxt, etc.).
37
+ *
38
+ * Unlike `fromNodeMiddleware` (h3 v1 API), this handler uses the Web Fetch API event
39
+ * model exclusively and is therefore compatible with ALL Nitro presets — including Bun
40
+ * and Deno — where `event.node` is `undefined` and `fromNodeMiddleware` crashes with
41
+ * "undefined is not an object (evaluating 'event.node.req')".
42
+ *
43
+ * It bridges h3 v2 events to the Node.js-style `createIntlayerProxyHandler` middleware
44
+ * via lightweight IncomingMessage / ServerResponse shims:
45
+ *
46
+ * - **Redirect** (301 / 5xx): builds a Web API `Response` and returns it — Nitro sends
47
+ * the correct HTTP response to the browser.
48
+ * - **Rewrite** (`next()` + modified `req.url`): replaces `event.url` with the rewritten
49
+ * URL so `event.path` (a getter) returns the new pathname for downstream handlers and
50
+ * the Nitro router.
51
+ * - **Pass-through** (`next()`, URL unchanged): returns `undefined` — Nitro proceeds to
52
+ * the next handler / route.
53
+ */
54
+ declare const _default: (event: H3EventLike) => Promise<Response | void>;
55
+ //#endregion
56
+ export { _default as default };
57
+ //# sourceMappingURL=intlayerNitroHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intlayerNitroHandler.d.ts","names":[],"sources":["../../src/intlayerNitroHandler.ts"],"mappings":";;;;;;;;;KAcK,WAAA;EAKM;;;;EAAA,SAAA,IAAA;EAgBA;;;;AACkB;EAX3B,GAAA,EAAK,GAAA;EA0C4D;;;;EAAA,SArCxD,OAAA,EAAS,OAAA;EAqC8B;;;;EAAA,SAhCvC,GAAA;IAAA,SACE,OAAA,EAAS,OAAA;EAAA;AAAA;;;;;;;;;;;;;;;;;;;;cAAO,QAAA,GA+BP,KAAA,EAAO,WAAA,KAAc,OAAA,CAAQ,QAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerOptimizePlugin.d.ts","names":[],"sources":["../../src/intlayerOptimizePlugin.ts"],"mappings":";;;;;;;AAsDA;;;;;;;;;;;;;;;;AAGuB;;;;cAHV,gBAAA,GACX,cAAA,EAAgB,cAAA,EAChB,YAAA,EAAc,YAAA,YACb,OAAA,CAAQ,YAAA"}
1
+ {"version":3,"file":"intlayerOptimizePlugin.d.ts","names":[],"sources":["../../src/intlayerOptimizePlugin.ts"],"mappings":";;;;;;;AAuDA;;;;;;;;;;;;;;;;AAGuB;;;;cAHV,gBAAA,GACX,cAAA,EAAgB,cAAA,EAChB,YAAA,EAAc,YAAA,YACb,OAAA,CAAQ,YAAA"}
@@ -1,6 +1,6 @@
1
1
  import { GetConfigurationOptions } from "@intlayer/config/node";
2
2
  import { Plugin } from "vite";
3
- import { IncomingMessage } from "node:http";
3
+ import { IncomingMessage, ServerResponse } from "node:http";
4
4
 
5
5
  //#region src/intlayerProxyPlugin.d.ts
6
6
  type IntlayerProxyPluginOptions = {
@@ -20,10 +20,59 @@ type IntlayerProxyPluginOptions = {
20
20
  ignore?: (req: IncomingMessage) => boolean | undefined;
21
21
  };
22
22
  /**
23
- * Vite plugin that provides a development middleware for locale-based routing.
23
+ * A Node.js-compatible Connect middleware function.
24
+ * Compatible with Vite dev/preview server, Node.js http, Express, and h3's
25
+ * `fromNodeMiddleware` wrapper for Nitro/TanStack Start production use.
26
+ */
27
+ type NodeMiddleware = (req: IncomingMessage, res: ServerResponse<IncomingMessage>, next: () => void) => void;
28
+ /**
29
+ * Creates a standalone, framework-agnostic locale-routing middleware.
30
+ *
31
+ * This function contains all the locale detection, redirect, and rewrite logic.
32
+ * It is intentionally separated from the Vite plugin so the same handler can be
33
+ * used in every environment:
34
+ *
35
+ * - **Dev**: wired up automatically by `intlayerProxy` via `configureServer`
36
+ * - **Preview**: wired up automatically by `intlayerProxy` via `configurePreviewServer`
37
+ * - **Production (Nitro / TanStack Start)**: create `server/middleware/intlayerProxy.ts`:
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * // server/middleware/intlayerProxy.ts
42
+ * import { fromNodeMiddleware } from 'h3';
43
+ * import { createIntlayerProxyHandler } from 'vite-intlayer';
24
44
  *
25
- * This plugin mimics the behavior of the Intlayer middleware in Next.js,
26
- * handling locale detection, redirects, and rewrites during development.
45
+ * export default fromNodeMiddleware(createIntlayerProxyHandler());
46
+ * ```
47
+ *
48
+ * @param configOptions - Optional Intlayer configuration overrides.
49
+ * @param options - Plugin-specific options, such as path ignoring.
50
+ * @returns A Connect-compatible `(req, res, next) => void` middleware.
51
+ */
52
+ declare const createIntlayerProxyHandler: (configOptions?: GetConfigurationOptions, options?: IntlayerProxyPluginOptions) => NodeMiddleware;
53
+ /**
54
+ * Vite plugin that provides locale-based routing middleware for **all environments**:
55
+ * development, preview, and production SSR (Nitro / TanStack Start).
56
+ *
57
+ * - **Dev** (`vite dev`): registered via `configureServer`.
58
+ * - **Preview** (`vite preview`): registered via `configurePreviewServer`.
59
+ * - **Production Nitro** (`vite build`): automatically injected via the `.nitro` module
60
+ * property that `nitro/vite` reads and pushes into `nitroConfig.modules`. The module
61
+ * registers `intlayerNitroHandler` as a Nitro server middleware — no extra user config
62
+ * needed.
63
+ *
64
+ * If you need custom config options or an `ignore` predicate in production, bypass
65
+ * auto-injection and create a server middleware file manually:
66
+ *
67
+ * ```ts
68
+ * // server/middleware/intlayerProxy.ts
69
+ * import { fromNodeMiddleware } from 'h3';
70
+ * import { createIntlayerProxyHandler } from 'vite-intlayer';
71
+ *
72
+ * export default fromNodeMiddleware(
73
+ * createIntlayerProxyHandler(myConfig, { ignore: (req) => req.url?.startsWith('/api') })
74
+ * );
75
+ * ```
27
76
  *
28
77
  * @param configOptions - Optional configuration for Intlayer.
29
78
  * @param options - Plugin-specific options, like ignoring certain paths.
@@ -56,7 +105,7 @@ declare const intlayerMiddleware: (configOptions?: GetConfigurationOptions, opti
56
105
  * @deprecated Rename to intlayerProxy instead
57
106
  *
58
107
  * A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.
59
-
108
+ *
60
109
  * ```ts
61
110
  * // Example usage of the plugin in a Vite configuration
62
111
  * export default defineConfig({
@@ -66,5 +115,5 @@ declare const intlayerMiddleware: (configOptions?: GetConfigurationOptions, opti
66
115
  */
67
116
  declare const intLayerMiddlewarePlugin: (configOptions?: GetConfigurationOptions, options?: IntlayerProxyPluginOptions) => Plugin;
68
117
  //#endregion
69
- export { intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy };
118
+ export { createIntlayerProxyHandler, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy };
70
119
  //# sourceMappingURL=intlayerProxyPlugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"intlayerProxyPlugin.d.ts","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"mappings":";;;;;KA2BK,0BAAA;;AAFuC;;;;;;;;AAgBZ;AAsBhC;;;EAtBE,MAAA,IAAU,GAAA,EAAK,eAAe;AAAA;;;;;;;;;;AAyvB/B;AAcD;;;;;;;;;cAjvBa,aAAA,GACX,aAAA,GAAgB,uBAAA,EAChB,OAAA,GAAU,0BAAA,KACT,MAAA;;;;;AA8uB4C;AAc/C;;;;;;;cAda,kBAAA,GAAkB,aAAA,GAhvBb,uBAAA,EAAuB,OAAA,GAC7B,0BAAA,KACT,MAAA;;;;;;;AA4vBkD;;;;;;cAAxC,wBAAA,GAAwB,aAAA,GA9vBnB,uBAAA,EAAuB,OAAA,GAC7B,0BAAA,KACT,MAAA"}
1
+ {"version":3,"file":"intlayerProxyPlugin.d.ts","names":[],"sources":["../../src/intlayerProxyPlugin.ts"],"mappings":";;;;;KAwBK,0BAAA;;AAFuC;;;;;;;;AAgBZ;AAAA;;;EAA9B,MAAA,IAAU,GAAA,EAAK,eAAe;AAAA;;;;;;KAQ3B,cAAA,IACH,GAAA,EAAK,eAAA,EACL,GAAA,EAAK,cAAA,CAAe,eAAA,GACpB,IAAA;;;;;AAAgB;AA2BlB;;;;;;;;;;;;;;AAotBC;AAuCD;;;;cA3vBa,0BAAA,GACX,aAAA,GAAgB,uBAAA,EAChB,OAAA,GAAU,0BAAA,KACT,cAAA;;;;;;;;;;AA8zBF;AAcD;;;;;;;;;;;;;;AAA+C;AAc/C;;;;;;;;;;;;cAlGa,aAAA,GACX,aAAA,GAAgB,uBAAA,EAChB,OAAA,GAAU,0BAAA,KACT,MAAA;;AA+FkD;;;;;;;;;;;cAdxC,kBAAA,GAAkB,aAAA,GAnFb,uBAAA,EAAuB,OAAA,GAC7B,0BAAA,KACT,MAAA;;;;;;;;;;;;;cA+FU,wBAAA,GAAwB,aAAA,GAjGnB,uBAAA,EAAuB,OAAA,GAC7B,0BAAA,KACT,MAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-intlayer",
3
- "version": "8.12.0",
3
+ "version": "8.12.2",
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": [
@@ -42,6 +42,11 @@
42
42
  "import": "./dist/esm/index.mjs",
43
43
  "default": "./dist/esm/index.mjs"
44
44
  },
45
+ "./nitro-handler": {
46
+ "types": "./dist/types/intlayerNitroHandler.d.ts",
47
+ "import": "./dist/esm/intlayerNitroHandler.mjs",
48
+ "default": "./dist/esm/intlayerNitroHandler.mjs"
49
+ },
45
50
  "./package.json": "./package.json"
46
51
  },
47
52
  "main": "./dist/esm/index.mjs",
@@ -76,12 +81,12 @@
76
81
  "typecheck": "tsc --noEmit --project tsconfig.types.json"
77
82
  },
78
83
  "dependencies": {
79
- "@intlayer/babel": "8.12.0",
80
- "@intlayer/chokidar": "8.12.0",
81
- "@intlayer/config": "8.12.0",
82
- "@intlayer/core": "8.12.0",
83
- "@intlayer/dictionaries-entry": "8.12.0",
84
- "@intlayer/types": "8.12.0"
84
+ "@intlayer/babel": "8.12.2",
85
+ "@intlayer/chokidar": "8.12.2",
86
+ "@intlayer/config": "8.12.2",
87
+ "@intlayer/core": "8.12.2",
88
+ "@intlayer/dictionaries-entry": "8.12.2",
89
+ "@intlayer/types": "8.12.2"
85
90
  },
86
91
  "devDependencies": {
87
92
  "@types/node": "25.9.1",
@@ -95,8 +100,9 @@
95
100
  },
96
101
  "peerDependencies": {
97
102
  "@babel/core": ">=6.0.0",
98
- "@intlayer/svelte-compiler": "8.12.0",
99
- "@intlayer/vue-compiler": "8.12.0",
103
+ "@intlayer/svelte-compiler": "8.12.2",
104
+ "@intlayer/vue-compiler": "8.12.2",
105
+ "h3": ">=1.0.0",
100
106
  "vite": ">=4.0.0"
101
107
  },
102
108
  "peerDependenciesMeta": {
@@ -108,6 +114,9 @@
108
114
  },
109
115
  "@intlayer/vue-compiler": {
110
116
  "optional": true
117
+ },
118
+ "h3": {
119
+ "optional": true
111
120
  }
112
121
  },
113
122
  "engines": {