vite-intlayer 8.12.1 → 8.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/IntlayerCompilerPlugin.mjs.map +1 -1
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/intlayerMinifyPlugin.mjs.map +1 -1
- package/dist/esm/intlayerNitroHandler.mjs +107 -0
- package/dist/esm/intlayerNitroHandler.mjs.map +1 -0
- package/dist/esm/intlayerOptimizePlugin.mjs +2 -2
- package/dist/esm/intlayerOptimizePlugin.mjs.map +1 -1
- package/dist/esm/intlayerPlugin.mjs.map +1 -1
- package/dist/esm/intlayerProxyPlugin.mjs +146 -77
- package/dist/esm/intlayerProxyPlugin.mjs.map +1 -1
- package/dist/esm/intlayerPrunePlugin.mjs.map +1 -1
- package/dist/esm/intlayerVueAsyncPlugin.mjs.map +1 -1
- package/dist/esm/pruneContext.mjs.map +1 -1
- package/dist/types/IntlayerCompilerPlugin.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/intlayerMinifyPlugin.d.ts.map +1 -1
- package/dist/types/intlayerNitroHandler.d.ts +57 -0
- package/dist/types/intlayerNitroHandler.d.ts.map +1 -0
- package/dist/types/intlayerOptimizePlugin.d.ts.map +1 -1
- package/dist/types/intlayerPlugin.d.ts.map +1 -1
- package/dist/types/intlayerProxyPlugin.d.ts +55 -6
- package/dist/types/intlayerProxyPlugin.d.ts.map +1 -1
- package/dist/types/intlayerPrunePlugin.d.ts.map +1 -1
- package/dist/types/intlayerVueAsyncPlugin.d.ts.map +1 -1
- package/dist/types/pruneContext.d.ts.map +1 -1
- package/package.json +20 -11
|
@@ -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
|
-
*
|
|
11
|
+
* Creates a standalone, framework-agnostic locale-routing middleware.
|
|
10
12
|
*
|
|
11
|
-
* This
|
|
12
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
23
|
+
* // server/middleware/intlayerProxy.ts
|
|
24
|
+
* import { fromNodeMiddleware } from 'h3';
|
|
25
|
+
* import { createIntlayerProxyHandler } from 'vite-intlayer';
|
|
21
26
|
*
|
|
22
|
-
* export default
|
|
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
|
|
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
|
-
*
|
|
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:
|
|
128
|
-
* - locale:
|
|
129
|
-
* - currentPath:(e.g
|
|
130
|
-
* - search:
|
|
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
|
-
*
|
|
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,123 @@ const intlayerProxy = (configOptions, options) => {
|
|
|
248
255
|
rewriteUrl(req, res, searchParams ? `${internalUrl}${searchParams}` : internalUrl, pathLocale);
|
|
249
256
|
return next();
|
|
250
257
|
};
|
|
251
|
-
return {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
+
const parsedUrl = parse(req.url ?? "/", true);
|
|
260
|
+
const originalPath = parsedUrl.pathname ?? "/";
|
|
261
|
+
const searchParams = parsedUrl.search ?? "";
|
|
262
|
+
const pathLocale = getPathLocale(originalPath);
|
|
263
|
+
if ((options?.ignore?.(req) ?? false) || originalPath.startsWith("/node_modules") || originalPath.startsWith("/@") || originalPath.startsWith("/_")) return next();
|
|
264
|
+
if (originalPath.match(/\.[a-zA-Z0-9]+$/)) {
|
|
265
|
+
if (pathLocale) req.url = `${originalPath.slice(`/${pathLocale}`.length) || "/"}${searchParams}`;
|
|
266
|
+
return next();
|
|
267
|
+
}
|
|
268
|
+
const storageLocale = getStorageLocale(req);
|
|
269
|
+
const effectiveStorageLocale = pathLocale && supportedLocales.includes(pathLocale) ? pathLocale : storageLocale;
|
|
270
|
+
const originalUrl = req.url;
|
|
271
|
+
if (process.env["INTLAYER_ROUTING_DOMAINS"] !== "false" && !noPrefix && pathLocale && domains) {
|
|
272
|
+
const localeDomain = domains[pathLocale];
|
|
273
|
+
if (localeDomain) {
|
|
274
|
+
const reqHost = (req.headers["host"] ?? "").split(":")[0] ?? "";
|
|
275
|
+
if (normalizeDomainHostname(localeDomain) !== reqHost) {
|
|
276
|
+
const rawPath = originalPath.slice(`/${pathLocale}`.length) || "/";
|
|
277
|
+
redirectUrl(res, `${/^https?:\/\//.test(localeDomain) ? localeDomain : `https://${localeDomain}`}${rawPath}${searchParams}`, "domain-routing", originalUrl);
|
|
293
278
|
return;
|
|
294
279
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (process.env["INTLAYER_ROUTING_DOMAINS"] !== "false" && !noPrefix && !pathLocale) {
|
|
283
|
+
const domainLocale = getLocaleFromDomain((req.headers["host"] ?? "").split(":")[0] ?? "");
|
|
284
|
+
if (domainLocale) {
|
|
285
|
+
const internalPath = `/${domainLocale}${getCanonicalPath(originalPath, domainLocale, rewriteRules)}`;
|
|
286
|
+
rewriteUrl(req, res, searchParams ? `${internalPath}${searchParams}` : internalPath, domainLocale);
|
|
287
|
+
return next();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (noPrefix) {
|
|
291
|
+
handleNoPrefix({
|
|
292
|
+
req,
|
|
293
|
+
res,
|
|
294
|
+
next,
|
|
295
|
+
originalPath,
|
|
296
|
+
searchParams,
|
|
297
|
+
storageLocale: effectiveStorageLocale,
|
|
298
|
+
originalUrl
|
|
305
299
|
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
handlePrefix({
|
|
303
|
+
req,
|
|
304
|
+
res,
|
|
305
|
+
next,
|
|
306
|
+
originalPath,
|
|
307
|
+
searchParams,
|
|
308
|
+
pathLocale,
|
|
309
|
+
storageLocale: effectiveStorageLocale,
|
|
310
|
+
originalUrl
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Vite plugin that provides locale-based routing middleware for **all environments**:
|
|
316
|
+
* development, preview, and production SSR (Nitro / TanStack Start).
|
|
317
|
+
*
|
|
318
|
+
* - **Dev** (`vite dev`): registered via `configureServer`.
|
|
319
|
+
* - **Preview** (`vite preview`): registered via `configurePreviewServer`.
|
|
320
|
+
* - **Production Nitro** (`vite build`): automatically injected via the `.nitro` module
|
|
321
|
+
* property that `nitro/vite` reads and pushes into `nitroConfig.modules`. The module
|
|
322
|
+
* registers `intlayerNitroHandler` as a Nitro server middleware — no extra user config
|
|
323
|
+
* needed.
|
|
324
|
+
*
|
|
325
|
+
* If you need custom config options or an `ignore` predicate in production, bypass
|
|
326
|
+
* auto-injection and create a server middleware file manually:
|
|
327
|
+
*
|
|
328
|
+
* ```ts
|
|
329
|
+
* // server/middleware/intlayerProxy.ts
|
|
330
|
+
* import { fromNodeMiddleware } from 'h3';
|
|
331
|
+
* import { createIntlayerProxyHandler } from 'vite-intlayer';
|
|
332
|
+
*
|
|
333
|
+
* export default fromNodeMiddleware(
|
|
334
|
+
* createIntlayerProxyHandler(myConfig, { ignore: (req) => req.url?.startsWith('/api') })
|
|
335
|
+
* );
|
|
336
|
+
* ```
|
|
337
|
+
*
|
|
338
|
+
* @param configOptions - Optional configuration for Intlayer.
|
|
339
|
+
* @param options - Plugin-specific options, like ignoring certain paths.
|
|
340
|
+
* @returns A Vite plugin.
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```ts
|
|
344
|
+
* import { intlayerProxy } from 'vite-intlayer';
|
|
345
|
+
*
|
|
346
|
+
* export default defineConfig({
|
|
347
|
+
* plugins: [intlayerProxy()],
|
|
348
|
+
* });
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
const intlayerProxy = (configOptions, options) => {
|
|
352
|
+
const handler = createIntlayerProxyHandler(configOptions, options);
|
|
353
|
+
const logger = getAppLogger(getConfiguration(configOptions));
|
|
354
|
+
return {
|
|
355
|
+
name: "vite-intlayer-middleware-plugin",
|
|
356
|
+
nitro: {
|
|
357
|
+
name: "intlayer-proxy",
|
|
358
|
+
setup(nitro) {
|
|
359
|
+
if (nitro.options.dev) return;
|
|
360
|
+
const handlerPath = fileURLToPath(new URL("./intlayerNitroHandler.mjs", import.meta.url));
|
|
361
|
+
nitro.options.handlers.push({
|
|
362
|
+
route: "/**",
|
|
363
|
+
handler: handlerPath,
|
|
364
|
+
middleware: true
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
configureServer: (server) => {
|
|
369
|
+
logger(`Intlayer proxy ${colorize("enabled", ANSIColors.GREEN)}`, { level: "info" });
|
|
370
|
+
server.middlewares.use(handler);
|
|
371
|
+
},
|
|
372
|
+
configurePreviewServer: (server) => {
|
|
373
|
+
logger(`Intlayer proxy ${colorize("enabled", ANSIColors.GREEN)}`, { level: "info" });
|
|
374
|
+
server.middlewares.use(handler);
|
|
306
375
|
}
|
|
307
376
|
};
|
|
308
377
|
};
|
|
@@ -321,9 +390,9 @@ const intlayerProxy = (configOptions, options) => {
|
|
|
321
390
|
const intlayerMiddleware = intlayerProxy;
|
|
322
391
|
/**
|
|
323
392
|
* @deprecated Rename to intlayerProxy instead
|
|
324
|
-
*
|
|
393
|
+
*
|
|
325
394
|
* A Vite plugin that integrates a logic similar to the Next.js intlayer middleware.
|
|
326
|
-
|
|
395
|
+
*
|
|
327
396
|
* ```ts
|
|
328
397
|
* // Example usage of the plugin in a Vite configuration
|
|
329
398
|
* export default defineConfig({
|
|
@@ -334,5 +403,5 @@ const intlayerMiddleware = intlayerProxy;
|
|
|
334
403
|
const intLayerMiddlewarePlugin = intlayerProxy;
|
|
335
404
|
|
|
336
405
|
//#endregion
|
|
337
|
-
export { intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy };
|
|
406
|
+
export { createIntlayerProxyHandler, intLayerMiddlewarePlugin, intlayerMiddleware, intlayerProxy };
|
|
338
407
|
//# 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 // 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 // Bypass special Vite/server endpoints and node_modules\n if (\n // Custom ignore function\n (options?.ignore?.(req) ?? false) ||\n originalPath.startsWith('/node_modules') ||\n /**\n * /^@vite/ # HMR client and helpers\n * /^@fs/ # file-system import serving\n * /^@id/ # virtual module ids\n * /^@tanstack/start-router-manifest # Tanstack Start Router manifest\n */\n originalPath.startsWith('/@') ||\n /**\n * /^__vite_ping$ # health ping\n * /^__open-in-editor$\n * /^__manifest$ # Remix/RR7 lazyRouteDiscovery\n */\n originalPath.startsWith('/_')\n ) {\n return next();\n }\n\n // Static file requests (e.g. /assets/video.mp4): bypass locale routing.\n // If the URL carries a locale prefix (e.g. /fr/assets/video.mp4),\n // rewrite the request internally to the unprefixed path (/assets/video.mp4)\n // so the file can be served correctly from the public directory.\n if (originalPath.match(/\\.[a-zA-Z0-9]+$/)) {\n if (pathLocale) {\n const pathWithoutLocale =\n originalPath.slice(`/${pathLocale}`.length) || '/';\n req.url = `${pathWithoutLocale}${searchParams}`;\n }\n return next();\n }\n\n // Attempt to read the locale from storage (cookies, localStorage, etc.)\n const storageLocale = getStorageLocale(req);\n\n // CRITICAL FIX: If there's a valid pathLocale, it takes precedence over storage\n // This prevents race conditions when cookies are stale during locale switches\n const effectiveStorageLocale =\n pathLocale && supportedLocales.includes(pathLocale)\n ? pathLocale\n : storageLocale;\n\n // Store original URL for redirect tracking\n const originalUrl = req.url;\n\n // Domain routing: if the path locale is mapped to a different domain, redirect there.\n // e.g. intlayer.org/zh/about → https://intlayer.zh/about\n if (\n process.env['INTLAYER_ROUTING_DOMAINS'] !== 'false' &&\n !noPrefix &&\n pathLocale &&\n domains\n ) {\n const localeDomain = domains[pathLocale as keyof typeof domains];\n if (localeDomain) {\n const reqHost = (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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerPrunePlugin.mjs","names":[],"sources":["../../src/intlayerPrunePlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { PruneContext } from '@intlayer/babel';\nimport { formatPath, runOnce } from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\n// Dictionary JSON types\n\n/**\n * A compiled intlayer translation node – used in static dictionaries where\n * all locales are bundled in a single file.\n *\n * Structure:\n * { nodeType: \"translation\", translation: { en: { field1, field2 }, fr: {…} } }\n */\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\n/**\n * Compiled intlayer dictionary as stored in a `.json` file.\n *\n * Two content shapes are supported (see `pruneStaticDictionaryContent` and\n * `pruneDynamicDictionaryContent`).\n */\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string; // present in per-locale dynamic dictionary files\n localIds?: string[];\n [extraKey: string]: unknown;\n};\n\n// Type guards\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// Pruning logic\n\n/**\n * Result of a prune attempt.\n *\n * `wasRecognised` is `false` when the content structure did not match any\n * known shape – the caller should log a warning and skip the file entirely.\n */\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/**\n * Prune a **static** dictionary JSON (all locales in one file).\n *\n * Shape A – the whole `content` is a single translation node:\n * { nodeType: \"translation\", translation: { en: { f1, f2 }, fr: { f1, f2 } } }\n * → prune the field objects inside each locale.\n *\n * Shape B – `content` is a plain record of fields, each being a translated node:\n * { field1: { nodeType: \"translation\", … }, field2: { … } }\n * → prune the top-level keys of `content` directly.\n *\n * Returns `{ wasRecognised: false }` when neither shape matches.\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n const localeValuesAreRecords = isPlainRecord(firstLocaleValue);\n\n if (localeValuesAreRecords) {\n const prunedTranslationByLocale: Record<string, unknown> = {};\n\n for (const [localeName, localeContent] of Object.entries(\n content.translation\n )) {\n if (!isPlainRecord(localeContent)) {\n // Locale value is not a record (e.g. a primitive) – keep as-is\n prunedTranslationByLocale[localeName] = localeContent;\n continue;\n }\n\n const prunedLocaleFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(localeContent)) {\n if (usedFieldNames.has(fieldName)) {\n prunedLocaleFields[fieldName] = fieldValue;\n }\n }\n prunedTranslationByLocale[localeName] = prunedLocaleFields;\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslationByLocale },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n const prunedContentFields: Record<string, unknown> = {};\n\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContentFields[fieldName] = fieldValue;\n }\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContentFields as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Prune a **dynamic / per-locale** dictionary JSON (one file per locale).\n *\n * Structure:\n * { key, content: { field1: value, field2: value }, locale: \"en\" }\n *\n * The `content` here is already the flat, locale-specific record, so we\n * prune its top-level keys directly.\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n\n const prunedContentFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContentFields[fieldName] = fieldValue;\n }\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContentFields as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n};\n\n/**\n * Returns the Vite plugin that removes unused content fields from compiled\n * dictionary JSON files during a production build.\n *\n * Targets:\n * - `<dictionariesDir>/**\\/*.json` – static all-locale dictionaries\n * - `<dynamicDictionariesDir>/**\\/*.json` – per-locale dynamic dictionaries\n * - `<fetchDictionariesDir>/**\\/*.json` – per-locale fetch dictionaries\n *\n * Decision table for each dictionary JSON:\n *\n * | condition | action |\n * |------------------------------------------------|-----------------|\n * | key in `dictionariesWithEdgeCases` | skip (warn once)|\n * | JSON parse error / missing key field | skip + warn |\n * | unrecognised content structure | skip + warn |\n * | analysis incomplete + key not in usage map | skip + warn |\n * | usage = 'all' (spread / untracked variable) | skip prune |\n * | usage = Set<string> | prune fields |\n *\n * Pruned dictionaries are returned as compact JSON (minification is handled\n * separately by `intlayerMinifyPlugin`).\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Shared state produced by the usage analyser that\n * runs inside `intlayerOptimizePlugin`.\n */\nexport const intlayerPrune = (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext\n): PluginOption[] => {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, purge } = intlayerConfig.build;\n const editorEnabled = intlayerConfig.editor.enabled;\n\n const {\n dictionariesDir,\n dynamicDictionariesDir,\n fetchDictionariesDir,\n baseDir,\n } = intlayerConfig.system;\n\n /**\n * Tracks dictionary keys whose \"pruned fields\" log has already been emitted\n * during this build session. Using an in-memory Set (instead of `runOnce`\n * file locks) avoids race conditions when client and SSR environments run\n * transforms concurrently — JavaScript's single-threaded event loop ensures\n * the `.has` / `.add` pair is always atomic.\n */\n const loggedPrunedDictionaryKeys = new Set<string>();\n\n /**\n * Accumulated statistics for the build summary.\n */\n const prunedFieldsCountPerDictionary = new Map<string, number>();\n\n const isDictionaryJsonFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.endsWith('.json') &&\n (absoluteFilePath.startsWith(dictionariesDir) ||\n absoluteFilePath.startsWith(dynamicDictionariesDir) ||\n absoluteFilePath.startsWith(fetchDictionariesDir));\n\n const isDynamicOrFetchDictionaryFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.startsWith(dynamicDictionariesDir) ||\n absoluteFilePath.startsWith(fetchDictionariesDir);\n\n const isPruneEnabled = (\n _config: unknown,\n env: { command: string }\n ): boolean => {\n const isBuildCommand = env.command === 'build';\n const isOptimizeActive =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isBuildCommand) return false;\n if (!isOptimizeActive) return false;\n if (!purge) return false;\n\n if (editorEnabled) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-purge-editor-warning.lock'\n ),\n () =>\n logger([\n 'Dictionary purge is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n '— the editor requires full dictionary content.',\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n return false;\n }\n\n runOnce(\n join(baseDir, '.intlayer', 'cache', 'intlayer-purge-plugin-enabled.lock'),\n () => logger(['Dictionary purge', colorize('enabled', ANSIColors.GREEN)]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n };\n\n const prunePlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-prune',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM conversion\n enforce: 'pre',\n apply: isPruneEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!isDictionaryJsonFile(absoluteFilePath)) return null;\n\n // Parse JSON\n let parsedDictionary: CompiledDictionaryJson;\n try {\n parsedDictionary = JSON.parse(rawJsonCode) as CompiledDictionaryJson;\n } catch {\n // Malformed JSON – leave it for Vite to report the error\n return null;\n }\n\n const { key: dictionaryKey } = parsedDictionary;\n\n if (!dictionaryKey) {\n logger(\n [\n `Dictionary file`,\n formatPath(absoluteFilePath),\n `is missing a \"key\" field. Skipping prune for this file.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Skip keys already marked as edge cases─\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) {\n return null;\n }\n\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n\n // No usage entry in the map─\n if (!fieldUsage) {\n if (pruneContext.hasUnparsableSourceFiles) {\n // At least one source file failed to parse; the unparsable file might\n // reference this key, so we cannot safely prune it.\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Skipping prune for dictionary`,\n colorizeKey(dictionaryKey),\n `: analysis is incomplete due to earlier source-file parse failures.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Analysis was complete but this key was never referenced in any source\n // file – the dictionary is either unused or loaded dynamically by key.\n // Leave the content unchanged (the minify plugin will compact it).\n return null;\n }\n\n // Usage is 'all': at least one call-site consumes all fields─\n if (fieldUsage === 'all') {\n return null; // nothing to prune\n }\n\n // Prune\n const isDynamicOrFetch = isDynamicOrFetchDictionaryFile(absoluteFilePath);\n\n const { prunedDictionary, wasRecognised } = isDynamicOrFetch\n ? pruneDynamicDictionaryContent(parsedDictionary, fieldUsage)\n : pruneStaticDictionaryContent(parsedDictionary, fieldUsage);\n\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Unrecognised content structure in dictionary`,\n colorizeKey(dictionaryKey),\n `(file:`,\n `${formatPath(absoluteFilePath)}).`,\n `Skipping prune for this dictionary.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Log pruned fields\n const originalContent = parsedDictionary.content;\n let originalFieldNames: string[];\n\n if (isTranslationNode(originalContent)) {\n // Shape A – fields live inside each locale object\n const firstLocaleValue = Object.values(originalContent.translation)[0];\n originalFieldNames = isPlainRecord(firstLocaleValue)\n ? Object.keys(firstLocaleValue)\n : [];\n } else if (isPlainRecord(originalContent)) {\n // Shape B / dynamic – flat content record\n originalFieldNames = Object.keys(originalContent);\n } else {\n originalFieldNames = [];\n }\n\n const removedFieldNames = originalFieldNames.filter(\n (fieldName) => !fieldUsage.has(fieldName)\n );\n\n if (removedFieldNames.length > 0) {\n prunedFieldsCountPerDictionary.set(\n dictionaryKey,\n removedFieldNames.length\n );\n\n if (!loggedPrunedDictionaryKeys.has(dictionaryKey)) {\n loggedPrunedDictionaryKeys.add(dictionaryKey);\n logger(\n [\n `Pruned`,\n colorizeNumber(removedFieldNames.length),\n `unused field${removedFieldNames.length === 1 ? '' : 's'} from`,\n `${colorizeKey(dictionaryKey)}:`,\n removedFieldNames\n .map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT))\n .join(', '),\n ],\n { isVerbose: true }\n );\n }\n }\n\n return { code: JSON.stringify(prunedDictionary), map: null };\n },\n\n /**\n * Log a summary of all fields removed during this build.\n */\n buildEnd: () => {\n runOnce(\n join(baseDir, '.intlayer', 'cache', 'intlayer-prune-summary.lock'),\n () => {\n const totalPrunedFieldsCount = [\n ...prunedFieldsCountPerDictionary.values(),\n ].reduce((a, b) => a + b, 0);\n const totalPrunedDictionariesCount =\n prunedFieldsCountPerDictionary.size;\n\n if (totalPrunedFieldsCount > 0) {\n logger([\n `Pruned`,\n colorizeNumber(totalPrunedFieldsCount),\n `unused field${totalPrunedFieldsCount === 1 ? '' : 's'} across`,\n colorizeNumber(totalPrunedDictionariesCount),\n `dictionar${totalPrunedDictionariesCount === 1 ? 'y' : 'ies'}.`,\n ]);\n }\n },\n { cacheTimeoutMs: 1000 * 5 }\n );\n },\n };\n\n return [prunePlugin];\n};\n"],"mappings":";;;;;;AA2CA,MAAM,qBAAqB,UACzB,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,aAAa,iBAChD,OAAQ,MAAkC,gBAAgB;AAE5D,MAAM,iBAAiB,UACrB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;;;;;;;;;;;;;;AA4BrE,MAAM,gCACJ,YACA,mBACgB;CAChB,MAAM,EAAE,YAAY;CAGpB,IAAI,kBAAkB,OAAO,GAAG;EAC9B,MAAM,mBAAmB,OAAO,OAAO,QAAQ,WAAW,EAAE;EAG5D,IAF+B,cAAc,gBAEpB,GAAG;GAC1B,MAAM,4BAAqD,CAAC;GAE5D,KAAK,MAAM,CAAC,YAAY,kBAAkB,OAAO,QAC/C,QAAQ,WACV,GAAG;IACD,IAAI,CAAC,cAAc,aAAa,GAAG;KAEjC,0BAA0B,cAAc;KACxC;IACF;IAEA,MAAM,qBAA8C,CAAC;IACrD,KAAK,MAAM,CAAC,WAAW,eAAe,OAAO,QAAQ,aAAa,GAChE,IAAI,eAAe,IAAI,SAAS,GAC9B,mBAAmB,aAAa;IAGpC,0BAA0B,cAAc;GAC1C;GAEA,OAAO;IACL,kBAAkB;KAChB,GAAG;KACH,SAAS;MAAE,GAAG;MAAS,aAAa;KAA0B;IAChE;IACA,eAAe;GACjB;EACF;CACF;CAGA,IAAI,cAAc,OAAO,KAAK,CAAC,kBAAkB,OAAO,GAAG;EACzD,MAAM,sBAA+C,CAAC;EAEtD,KAAK,MAAM,CAAC,WAAW,eAAe,OAAO,QAAQ,OAAO,GAC1D,IAAI,eAAe,IAAI,SAAS,GAC9B,oBAAoB,aAAa;EAIrC,OAAO;GACL,kBAAkB;IAChB,GAAG;IACH,SAAS;GACX;GACA,eAAe;EACjB;CACF;CAEA,OAAO;EAAE,kBAAkB;EAAY,eAAe;CAAM;AAC9D;;;;;;;;;;AAWA,MAAM,iCACJ,YACA,mBACgB;CAChB,MAAM,EAAE,YAAY;CAEpB,IAAI,CAAC,cAAc,OAAO,GACxB,OAAO;EAAE,kBAAkB;EAAY,eAAe;CAAM;CAG9D,MAAM,sBAA+C,CAAC;CACtD,KAAK,MAAM,CAAC,WAAW,eAAe,OAAO,QAAQ,OAAO,GAC1D,IAAI,eAAe,IAAI,SAAS,GAC9B,oBAAoB,aAAa;CAIrC,OAAO;EACL,kBAAkB;GAChB,GAAG;GACH,SAAS;EACX;EACA,eAAe;CACjB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,iBACX,gBACA,iBACmB;CACnB,MAAM,SAAS,aAAa,cAAc;CAE1C,MAAM,EAAE,UAAU,UAAU,eAAe;CAC3C,MAAM,gBAAgB,eAAe,OAAO;CAE5C,MAAM,EACJ,iBACA,wBACA,sBACA,YACE,eAAe;;;;;;;;CASnB,MAAM,6CAA6B,IAAI,IAAY;;;;CAKnD,MAAM,iDAAiC,IAAI,IAAoB;CAE/D,MAAM,wBAAwB,qBAC5B,iBAAiB,SAAS,OAAO,MAChC,iBAAiB,WAAW,eAAe,KAC1C,iBAAiB,WAAW,sBAAsB,KAClD,iBAAiB,WAAW,oBAAoB;CAEpD,MAAM,kCAAkC,qBACtC,iBAAiB,WAAW,sBAAsB,KAClD,iBAAiB,WAAW,oBAAoB;CAElD,MAAM,kBACJ,SACA,QACY;EACZ,MAAM,iBAAiB,IAAI,YAAY;EACvC,MAAM,mBACH,aAAa,UAAa,kBAAmB,aAAa;EAE7D,IAAI,CAAC,gBAAgB,OAAO;EAC5B,IAAI,CAAC,kBAAkB,OAAO;EAC9B,IAAI,CAAC,OAAO,OAAO;EAEnB,IAAI,eAAe;GACjB,QACE,KACE,SACA,aACA,SACA,oCACF,SAEE,OAAO;IACL;IACA,SAAS,YAAY,WAAW,SAAS;IACzC;IACA,SAAS,kBAAkB,WAAW,IAAI;IAC1C;IACA,SAAS,QAAQ,WAAW,SAAS;IACrC;GACF,CAAC,GACH,EAAE,gBAAgB,MAAO,GAAG,CAC9B;GACA,OAAO;EACT;EAEA,QACE,KAAK,SAAS,aAAa,SAAS,oCAAoC,SAClE,OAAO,CAAC,oBAAoB,SAAS,WAAW,WAAW,KAAK,CAAC,CAAC,GACxE,EAAE,gBAAgB,MAAO,GAAG,CAC9B;EAEA,OAAO;CACT;CAyKA,OAAO,CAAC;EAtKN,MAAM;EAEN,SAAS;EACT,OAAO;EAEP,YAAY,aAAa,aAAa;GACpC,MAAM,mBAAmB,SAAS,MAAM,KAAK,CAAC,EAAE;GAEhD,IAAI,CAAC,qBAAqB,gBAAgB,GAAG,OAAO;GAGpD,IAAI;GACJ,IAAI;IACF,mBAAmB,KAAK,MAAM,WAAW;GAC3C,QAAQ;IAEN,OAAO;GACT;GAEA,MAAM,EAAE,KAAK,kBAAkB;GAE/B,IAAI,CAAC,eAAe;IAClB,OACE;KACE;KACA,WAAW,gBAAgB;KAC3B;IACF,GACA,EAAE,OAAO,OAAO,CAClB;IACA,OAAO;GACT;GAGA,IAAI,aAAa,0BAA0B,IAAI,aAAa,GAC1D,OAAO;GAGT,MAAM,aACJ,aAAa,6BAA6B,IAAI,aAAa;GAG7D,IAAI,CAAC,YAAY;IACf,IAAI,aAAa,0BAA0B;KAGzC,aAAa,0BAA0B,IAAI,aAAa;KACxD,OACE;MACE;MACA,YAAY,aAAa;MACzB;KACF,GACA,EAAE,OAAO,OAAO,CAClB;KACA,OAAO;IACT;IAKA,OAAO;GACT;GAGA,IAAI,eAAe,OACjB,OAAO;GAMT,MAAM,EAAE,kBAAkB,kBAFD,+BAA+B,gBAEG,IACvD,8BAA8B,kBAAkB,UAAU,IAC1D,6BAA6B,kBAAkB,UAAU;GAE7D,IAAI,CAAC,eAAe;IAClB,aAAa,0BAA0B,IAAI,aAAa;IACxD,OACE;KACE;KACA,YAAY,aAAa;KACzB;KACA,GAAG,WAAW,gBAAgB,EAAE;KAChC;IACF,GACA,EAAE,OAAO,OAAO,CAClB;IACA,OAAO;GACT;GAGA,MAAM,kBAAkB,iBAAiB;GACzC,IAAI;GAEJ,IAAI,kBAAkB,eAAe,GAAG;IAEtC,MAAM,mBAAmB,OAAO,OAAO,gBAAgB,WAAW,EAAE;IACpE,qBAAqB,cAAc,gBAAgB,IAC/C,OAAO,KAAK,gBAAgB,IAC5B,CAAC;GACP,OAAO,IAAI,cAAc,eAAe,GAEtC,qBAAqB,OAAO,KAAK,eAAe;QAEhD,qBAAqB,CAAC;GAGxB,MAAM,oBAAoB,mBAAmB,QAC1C,cAAc,CAAC,WAAW,IAAI,SAAS,CAC1C;GAEA,IAAI,kBAAkB,SAAS,GAAG;IAChC,+BAA+B,IAC7B,eACA,kBAAkB,MACpB;IAEA,IAAI,CAAC,2BAA2B,IAAI,aAAa,GAAG;KAClD,2BAA2B,IAAI,aAAa;KAC5C,OACE;MACE;MACA,eAAe,kBAAkB,MAAM;MACvC,eAAe,kBAAkB,WAAW,IAAI,KAAK,IAAI;MACzD,GAAG,YAAY,aAAa,EAAE;MAC9B,kBACG,KAAK,cAAc,SAAS,WAAW,WAAW,UAAU,CAAC,EAC7D,KAAK,IAAI;KACd,GACA,EAAE,WAAW,KAAK,CACpB;IACF;GACF;GAEA,OAAO;IAAE,MAAM,KAAK,UAAU,gBAAgB;IAAG,KAAK;GAAK;EAC7D;;;;EAKA,gBAAgB;GACd,QACE,KAAK,SAAS,aAAa,SAAS,6BAA6B,SAC3D;IACJ,MAAM,yBAAyB,CAC7B,GAAG,+BAA+B,OAAO,CAC3C,EAAE,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC;IAC3B,MAAM,+BACJ,+BAA+B;IAEjC,IAAI,yBAAyB,GAC3B,OAAO;KACL;KACA,eAAe,sBAAsB;KACrC,eAAe,2BAA2B,IAAI,KAAK,IAAI;KACvD,eAAe,4BAA4B;KAC3C,YAAY,iCAAiC,IAAI,MAAM,MAAM;IAC/D,CAAC;GAEL,GACA,EAAE,gBAAgB,MAAO,EAAE,CAC7B;EACF;CAGgB,CAAC;AACrB"}
|
|
1
|
+
{"version":3,"file":"intlayerPrunePlugin.mjs","names":[],"sources":["../../src/intlayerPrunePlugin.ts"],"sourcesContent":["import { join } from 'node:path';\nimport type { PruneContext } from '@intlayer/babel';\nimport { formatPath, runOnce } from '@intlayer/chokidar/utils';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n colorizeKey,\n colorizeNumber,\n getAppLogger,\n} from '@intlayer/config/logger';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\n// Dictionary JSON types\n\n/**\n * A compiled intlayer translation node – used in static dictionaries where\n * all locales are bundled in a single file.\n *\n * Structure:\n * { nodeType: \"translation\", translation: { en: { field1, field2 }, fr: {…} } }\n */\ntype TranslationNode = {\n nodeType: 'translation';\n translation: Record<string, unknown>;\n};\n\n/**\n * Compiled intlayer dictionary as stored in a `.json` file.\n *\n * Two content shapes are supported (see `pruneStaticDictionaryContent` and\n * `pruneDynamicDictionaryContent`).\n */\ntype CompiledDictionaryJson = {\n key: string;\n content: TranslationNode | Record<string, unknown>;\n locale?: string; // present in per-locale dynamic dictionary files\n localIds?: string[];\n [extraKey: string]: unknown;\n};\n\n// Type guards\n\nconst isTranslationNode = (value: unknown): value is TranslationNode =>\n typeof value === 'object' &&\n value !== null &&\n (value as Record<string, unknown>).nodeType === 'translation' &&\n typeof (value as Record<string, unknown>).translation === 'object';\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n// Pruning logic\n\n/**\n * Result of a prune attempt.\n *\n * `wasRecognised` is `false` when the content structure did not match any\n * known shape – the caller should log a warning and skip the file entirely.\n */\ntype PruneResult = {\n prunedDictionary: CompiledDictionaryJson;\n wasRecognised: boolean;\n};\n\n/**\n * Prune a **static** dictionary JSON (all locales in one file).\n *\n * Shape A – the whole `content` is a single translation node:\n * { nodeType: \"translation\", translation: { en: { f1, f2 }, fr: { f1, f2 } } }\n * → prune the field objects inside each locale.\n *\n * Shape B – `content` is a plain record of fields, each being a translated node:\n * { field1: { nodeType: \"translation\", … }, field2: { … } }\n * → prune the top-level keys of `content` directly.\n *\n * Returns `{ wasRecognised: false }` when neither shape matches.\n */\nconst pruneStaticDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n // Shape A\n if (isTranslationNode(content)) {\n const firstLocaleValue = Object.values(content.translation)[0];\n const localeValuesAreRecords = isPlainRecord(firstLocaleValue);\n\n if (localeValuesAreRecords) {\n const prunedTranslationByLocale: Record<string, unknown> = {};\n\n for (const [localeName, localeContent] of Object.entries(\n content.translation\n )) {\n if (!isPlainRecord(localeContent)) {\n // Locale value is not a record (e.g. a primitive) – keep as-is\n prunedTranslationByLocale[localeName] = localeContent;\n continue;\n }\n\n const prunedLocaleFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(localeContent)) {\n if (usedFieldNames.has(fieldName)) {\n prunedLocaleFields[fieldName] = fieldValue;\n }\n }\n prunedTranslationByLocale[localeName] = prunedLocaleFields;\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: { ...content, translation: prunedTranslationByLocale },\n },\n wasRecognised: true,\n };\n }\n }\n\n // Shape B\n if (isPlainRecord(content) && !isTranslationNode(content)) {\n const prunedContentFields: Record<string, unknown> = {};\n\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContentFields[fieldName] = fieldValue;\n }\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContentFields as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n }\n\n return { prunedDictionary: dictionary, wasRecognised: false };\n};\n\n/**\n * Prune a **dynamic / per-locale** dictionary JSON (one file per locale).\n *\n * Structure:\n * { key, content: { field1: value, field2: value }, locale: \"en\" }\n *\n * The `content` here is already the flat, locale-specific record, so we\n * prune its top-level keys directly.\n */\nconst pruneDynamicDictionaryContent = (\n dictionary: CompiledDictionaryJson,\n usedFieldNames: Set<string>\n): PruneResult => {\n const { content } = dictionary;\n\n if (!isPlainRecord(content)) {\n return { prunedDictionary: dictionary, wasRecognised: false };\n }\n\n const prunedContentFields: Record<string, unknown> = {};\n for (const [fieldName, fieldValue] of Object.entries(content)) {\n if (usedFieldNames.has(fieldName)) {\n prunedContentFields[fieldName] = fieldValue;\n }\n }\n\n return {\n prunedDictionary: {\n ...dictionary,\n content: prunedContentFields as CompiledDictionaryJson['content'],\n },\n wasRecognised: true,\n };\n};\n\n/**\n * Returns the Vite plugin that removes unused content fields from compiled\n * dictionary JSON files during a production build.\n *\n * Targets:\n * - `<dictionariesDir>/**\\/*.json` – static all-locale dictionaries\n * - `<dynamicDictionariesDir>/**\\/*.json` – per-locale dynamic dictionaries\n * - `<fetchDictionariesDir>/**\\/*.json` – per-locale fetch dictionaries\n *\n * Decision table for each dictionary JSON:\n *\n * | condition | action |\n * |------------------------------------------------|-----------------|\n * | key in `dictionariesWithEdgeCases` | skip (warn once)|\n * | JSON parse error / missing key field | skip + warn |\n * | unrecognised content structure | skip + warn |\n * | analysis incomplete + key not in usage map | skip + warn |\n * | usage = 'all' (spread / untracked variable) | skip prune |\n * | usage = Set<string> | prune fields |\n *\n * Pruned dictionaries are returned as compact JSON (minification is handled\n * separately by `intlayerMinifyPlugin`).\n *\n * @param intlayerConfig - Resolved intlayer configuration.\n * @param pruneContext - Shared state produced by the usage analyser that\n * runs inside `intlayerOptimizePlugin`.\n */\nexport const intlayerPrune = (\n intlayerConfig: IntlayerConfig,\n pruneContext: PruneContext\n): PluginOption[] => {\n const logger = getAppLogger(intlayerConfig);\n\n const { optimize, purge } = intlayerConfig.build;\n const editorEnabled = intlayerConfig.editor.enabled;\n\n const {\n dictionariesDir,\n dynamicDictionariesDir,\n fetchDictionariesDir,\n baseDir,\n } = intlayerConfig.system;\n\n /**\n * Tracks dictionary keys whose \"pruned fields\" log has already been emitted\n * during this build session. Using an in-memory Set (instead of `runOnce`\n * file locks) avoids race conditions when client and SSR environments run\n * transforms concurrently — JavaScript's single-threaded event loop ensures\n * the `.has` / `.add` pair is always atomic.\n */\n const loggedPrunedDictionaryKeys = new Set<string>();\n\n /**\n * Accumulated statistics for the build summary.\n */\n const prunedFieldsCountPerDictionary = new Map<string, number>();\n\n const isDictionaryJsonFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.endsWith('.json') &&\n (absoluteFilePath.startsWith(dictionariesDir) ||\n absoluteFilePath.startsWith(dynamicDictionariesDir) ||\n absoluteFilePath.startsWith(fetchDictionariesDir));\n\n const isDynamicOrFetchDictionaryFile = (absoluteFilePath: string): boolean =>\n absoluteFilePath.startsWith(dynamicDictionariesDir) ||\n absoluteFilePath.startsWith(fetchDictionariesDir);\n\n const isPruneEnabled = (\n _config: unknown,\n env: { command: string }\n ): boolean => {\n const isBuildCommand = env.command === 'build';\n const isOptimizeActive =\n (optimize === undefined && isBuildCommand) || optimize === true;\n\n if (!isBuildCommand) return false;\n if (!isOptimizeActive) return false;\n if (!purge) return false;\n\n if (editorEnabled) {\n runOnce(\n join(\n baseDir,\n '.intlayer',\n 'cache',\n 'intlayer-purge-editor-warning.lock'\n ),\n () =>\n logger([\n 'Dictionary purge is',\n colorize('disabled', ANSIColors.GREY_DARK),\n 'because',\n colorize('editor.enabled', ANSIColors.BLUE),\n 'is',\n colorize('true', ANSIColors.GREY_DARK),\n '— the editor requires full dictionary content.',\n ]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n return false;\n }\n\n runOnce(\n join(baseDir, '.intlayer', 'cache', 'intlayer-purge-plugin-enabled.lock'),\n () => logger(['Dictionary purge', colorize('enabled', ANSIColors.GREEN)]),\n { cacheTimeoutMs: 1000 * 10 }\n );\n\n return true;\n };\n\n const prunePlugin: PluginOption = {\n name: 'vite-intlayer-dictionary-prune',\n // 'pre' so we receive raw JSON before Vite's built-in JSON → ESM conversion\n enforce: 'pre',\n apply: isPruneEnabled,\n\n transform: (rawJsonCode, moduleId) => {\n const absoluteFilePath = moduleId.split('?', 1)[0];\n\n if (!isDictionaryJsonFile(absoluteFilePath)) return null;\n\n // Parse JSON\n let parsedDictionary: CompiledDictionaryJson;\n try {\n parsedDictionary = JSON.parse(rawJsonCode) as CompiledDictionaryJson;\n } catch {\n // Malformed JSON – leave it for Vite to report the error\n return null;\n }\n\n const { key: dictionaryKey } = parsedDictionary;\n\n if (!dictionaryKey) {\n logger(\n [\n `Dictionary file`,\n formatPath(absoluteFilePath),\n `is missing a \"key\" field. Skipping prune for this file.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Skip keys already marked as edge cases─\n if (pruneContext.dictionariesWithEdgeCases.has(dictionaryKey)) {\n return null;\n }\n\n const fieldUsage =\n pruneContext.dictionaryKeyToFieldUsageMap.get(dictionaryKey);\n\n // No usage entry in the map─\n if (!fieldUsage) {\n if (pruneContext.hasUnparsableSourceFiles) {\n // At least one source file failed to parse; the unparsable file might\n // reference this key, so we cannot safely prune it.\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Skipping prune for dictionary`,\n colorizeKey(dictionaryKey),\n `: analysis is incomplete due to earlier source-file parse failures.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Analysis was complete but this key was never referenced in any source\n // file – the dictionary is either unused or loaded dynamically by key.\n // Leave the content unchanged (the minify plugin will compact it).\n return null;\n }\n\n // Usage is 'all': at least one call-site consumes all fields─\n if (fieldUsage === 'all') {\n return null; // nothing to prune\n }\n\n // Prune\n const isDynamicOrFetch = isDynamicOrFetchDictionaryFile(absoluteFilePath);\n\n const { prunedDictionary, wasRecognised } = isDynamicOrFetch\n ? pruneDynamicDictionaryContent(parsedDictionary, fieldUsage)\n : pruneStaticDictionaryContent(parsedDictionary, fieldUsage);\n\n if (!wasRecognised) {\n pruneContext.dictionariesWithEdgeCases.add(dictionaryKey);\n logger(\n [\n `Unrecognised content structure in dictionary`,\n colorizeKey(dictionaryKey),\n `(file:`,\n `${formatPath(absoluteFilePath)}).`,\n `Skipping prune for this dictionary.`,\n ],\n { level: 'warn' }\n );\n return null;\n }\n\n // Log pruned fields\n const originalContent = parsedDictionary.content;\n let originalFieldNames: string[];\n\n if (isTranslationNode(originalContent)) {\n // Shape A – fields live inside each locale object\n const firstLocaleValue = Object.values(originalContent.translation)[0];\n originalFieldNames = isPlainRecord(firstLocaleValue)\n ? Object.keys(firstLocaleValue)\n : [];\n } else if (isPlainRecord(originalContent)) {\n // Shape B / dynamic – flat content record\n originalFieldNames = Object.keys(originalContent);\n } else {\n originalFieldNames = [];\n }\n\n const removedFieldNames = originalFieldNames.filter(\n (fieldName) => !fieldUsage.has(fieldName)\n );\n\n if (removedFieldNames.length > 0) {\n prunedFieldsCountPerDictionary.set(\n dictionaryKey,\n removedFieldNames.length\n );\n\n if (!loggedPrunedDictionaryKeys.has(dictionaryKey)) {\n loggedPrunedDictionaryKeys.add(dictionaryKey);\n logger(\n [\n `Pruned`,\n colorizeNumber(removedFieldNames.length),\n `unused field${removedFieldNames.length === 1 ? '' : 's'} from`,\n `${colorizeKey(dictionaryKey)}:`,\n removedFieldNames\n .map((fieldName) => colorize(fieldName, ANSIColors.GREY_LIGHT))\n .join(', '),\n ],\n { isVerbose: true }\n );\n }\n }\n\n return { code: JSON.stringify(prunedDictionary), map: null };\n },\n\n /**\n * Log a summary of all fields removed during this build.\n */\n buildEnd: () => {\n runOnce(\n join(baseDir, '.intlayer', 'cache', 'intlayer-prune-summary.lock'),\n () => {\n const totalPrunedFieldsCount = [\n ...prunedFieldsCountPerDictionary.values(),\n ].reduce((a, b) => a + b, 0);\n const totalPrunedDictionariesCount =\n prunedFieldsCountPerDictionary.size;\n\n if (totalPrunedFieldsCount > 0) {\n logger([\n `Pruned`,\n colorizeNumber(totalPrunedFieldsCount),\n `unused field${totalPrunedFieldsCount === 1 ? '' : 's'} across`,\n colorizeNumber(totalPrunedDictionariesCount),\n `dictionar${totalPrunedDictionariesCount === 1 ? 'y' : 'ies'}.`,\n ]);\n }\n },\n { cacheTimeoutMs: 1000 * 5 }\n );\n },\n };\n\n return [prunePlugin];\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intlayerVueAsyncPlugin.mjs","names":[],"sources":["../../src/intlayerVueAsyncPlugin.ts"],"sourcesContent":["import type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\nexport const intlayerVueAsyncPlugin = (\n configuration: IntlayerConfig,\n filesList: string[]\n): PluginOption => {\n const { optimize } = configuration.build;\n const importMode =\n configuration.build.importMode ?? configuration.dictionary?.importMode;\n\n return {\n /**\n * On vue, we pre-insert the 'await' to the useIntlayer call\n * It will trigger the transformation of the async call by the vue compiler\n *\n * Then the second plugin will make the second transformation to replace the useIntlayer call by the useDictionaryDynamic call\n */\n name: 'vite-intlayer-simple-transform',\n enforce: 'pre', // Run before Vue so Vue sees the 'await'\n apply: (_config, env) => {\n // Only apply babel plugin if optimize is enabled\n\n const isBuild = env.command === 'build';\n const isEnabled =\n (optimize === undefined && isBuild) || optimize === true;\n const isAsync = importMode === 'dynamic' || importMode === 'fetch';\n\n return isEnabled && isAsync;\n },\n\n transform(code, id) {\n // Only process .vue files\n // The await injection is only needed for Vue to trigger async component compilation\n if (!id.endsWith('.vue')) return null;\n\n /**\n * Transform file as\n * .../HelloWorld.vue?vue&type=script&setup=true&lang.ts\n * Into\n * .../HelloWorld.vue\n *\n * Prevention for virtual file\n */\n const filename = id.split('?', 1)[0];\n\n if (!filesList.includes(filename)) return null;\n\n // Check if the file actually uses the composable to avoid unnecessary work\n if (!code.includes('useIntlayer')) return null;\n\n // Add 'await' to the function call\n // Matches: useIntlayer(args) -> await useIntlayer(args)\n // Note: Since we aliased the import above, 'useIntlayer' now refers to 'useDictionaryAsync'\n const transformedCode = code.replace(\n /(\\s+|=\\s*)useIntlayer\\s*\\(/g,\n '$1await useIntlayer('\n );\n\n return {\n code: transformedCode,\n map: null, // Simple string replace doesn't strictly need a sourcemap for this case\n };\n },\n };\n};\n"],"mappings":";AAGA,MAAa,0BACX,eACA,cACiB;CACjB,MAAM,EAAE,aAAa,cAAc;CACnC,MAAM,aACJ,cAAc,MAAM,cAAc,cAAc,YAAY;
|
|
1
|
+
{"version":3,"file":"intlayerVueAsyncPlugin.mjs","names":[],"sources":["../../src/intlayerVueAsyncPlugin.ts"],"sourcesContent":["import type { IntlayerConfig } from '@intlayer/types/config';\nimport type { PluginOption } from 'vite';\n\nexport const intlayerVueAsyncPlugin = (\n configuration: IntlayerConfig,\n filesList: string[]\n): PluginOption => {\n const { optimize } = configuration.build;\n const importMode =\n configuration.build.importMode ?? configuration.dictionary?.importMode;\n\n return {\n /**\n * On vue, we pre-insert the 'await' to the useIntlayer call\n * It will trigger the transformation of the async call by the vue compiler\n *\n * Then the second plugin will make the second transformation to replace the useIntlayer call by the useDictionaryDynamic call\n */\n name: 'vite-intlayer-simple-transform',\n enforce: 'pre', // Run before Vue so Vue sees the 'await'\n apply: (_config, env) => {\n // Only apply babel plugin if optimize is enabled\n\n const isBuild = env.command === 'build';\n const isEnabled =\n (optimize === undefined && isBuild) || optimize === true;\n const isAsync = importMode === 'dynamic' || importMode === 'fetch';\n\n return isEnabled && isAsync;\n },\n\n transform(code, id) {\n // Only process .vue files\n // The await injection is only needed for Vue to trigger async component compilation\n if (!id.endsWith('.vue')) return null;\n\n /**\n * Transform file as\n * .../HelloWorld.vue?vue&type=script&setup=true&lang.ts\n * Into\n * .../HelloWorld.vue\n *\n * Prevention for virtual file\n */\n const filename = id.split('?', 1)[0];\n\n if (!filesList.includes(filename)) return null;\n\n // Check if the file actually uses the composable to avoid unnecessary work\n if (!code.includes('useIntlayer')) return null;\n\n // Add 'await' to the function call\n // Matches: useIntlayer(args) -> await useIntlayer(args)\n // Note: Since we aliased the import above, 'useIntlayer' now refers to 'useDictionaryAsync'\n const transformedCode = code.replace(\n /(\\s+|=\\s*)useIntlayer\\s*\\(/g,\n '$1await useIntlayer('\n );\n\n return {\n code: transformedCode,\n map: null, // Simple string replace doesn't strictly need a sourcemap for this case\n };\n },\n };\n};\n"],"mappings":";AAGA,MAAa,0BACX,eACA,cACiB;CACjB,MAAM,EAAE,aAAa,cAAc;CACnC,MAAM,aACJ,cAAc,MAAM,cAAc,cAAc,YAAY;AAE9D,QAAO;;;;;;;EAOL,MAAM;EACN,SAAS;EACT,QAAQ,SAAS,QAAQ;GAGvB,MAAM,UAAU,IAAI,YAAY;AAKhC,WAHG,aAAa,UAAa,WAAY,aAAa,UACtC,eAAe,aAAa,eAAe;;EAK7D,UAAU,MAAM,IAAI;AAGlB,OAAI,CAAC,GAAG,SAAS,OAAO,CAAE,QAAO;;;;;;;;;GAUjC,MAAM,WAAW,GAAG,MAAM,KAAK,EAAE,CAAC;AAElC,OAAI,CAAC,UAAU,SAAS,SAAS,CAAE,QAAO;AAG1C,OAAI,CAAC,KAAK,SAAS,cAAc,CAAE,QAAO;AAU1C,UAAO;IACL,MANsB,KAAK,QAC3B,+BACA,uBAIqB;IACrB,KAAK;IACN;;EAEJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pruneContext.mjs","names":[],"sources":["../../src/pruneContext.ts"],"sourcesContent":["// Re-export PruneContext types from @intlayer/babel so both the vite plugins\n// and the babel plugins share the same type definitions.\n\nimport type { PruneContext } from '@intlayer/babel';\n\n// createPruneContext is kept here as a local runtime value so that\n// vite-intlayer does not depend on @intlayer/babel's dist being built\n// with the new exports before this plugin can load.\nexport const createPruneContext = (): PruneContext => ({\n dictionaryKeyToFieldUsageMap: new Map(),\n dictionariesWithEdgeCases: new Set(),\n hasUnparsableSourceFiles: false,\n dictionaryKeysWithUntrackedBindings: new Map(),\n dictionaryKeyToFieldRenameMap: new Map(),\n dictionaryKeysWithOpaqueTopLevelFields: new Map(),\n dictionariesSkippingFieldRename: new Set(),\n pendingFrameworkAnalysis: new Map(),\n});\n"],"mappings":";AAQA,MAAa,4BAA0C;CACrD,8CAA8B,IAAI,
|
|
1
|
+
{"version":3,"file":"pruneContext.mjs","names":[],"sources":["../../src/pruneContext.ts"],"sourcesContent":["// Re-export PruneContext types from @intlayer/babel so both the vite plugins\n// and the babel plugins share the same type definitions.\n\nimport type { PruneContext } from '@intlayer/babel';\n\n// createPruneContext is kept here as a local runtime value so that\n// vite-intlayer does not depend on @intlayer/babel's dist being built\n// with the new exports before this plugin can load.\nexport const createPruneContext = (): PruneContext => ({\n dictionaryKeyToFieldUsageMap: new Map(),\n dictionariesWithEdgeCases: new Set(),\n hasUnparsableSourceFiles: false,\n dictionaryKeysWithUntrackedBindings: new Map(),\n dictionaryKeyToFieldRenameMap: new Map(),\n dictionaryKeysWithOpaqueTopLevelFields: new Map(),\n dictionariesSkippingFieldRename: new Set(),\n pendingFrameworkAnalysis: new Map(),\n});\n"],"mappings":";AAQA,MAAa,4BAA0C;CACrD,8CAA8B,IAAI,KAAK;CACvC,2CAA2B,IAAI,KAAK;CACpC,0BAA0B;CAC1B,qDAAqC,IAAI,KAAK;CAC9C,+CAA+B,IAAI,KAAK;CACxC,wDAAwC,IAAI,KAAK;CACjD,iDAAiC,IAAI,KAAK;CAC1C,0CAA0B,IAAI,KAAK;CACpC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IntlayerCompilerPlugin.d.ts","names":[],"sources":["../../src/IntlayerCompilerPlugin.ts"],"mappings":";;;;;;;AA8BA;KAAY,uBAAA;;;;EAIV,aAAA,GAAgB,uBAAA;EAKQ;;;EAAxB,cAAA,GAAiB,OAAA,CAAQ,cAAA;AAAA
|
|
1
|
+
{"version":3,"file":"IntlayerCompilerPlugin.d.ts","names":[],"sources":["../../src/IntlayerCompilerPlugin.ts"],"mappings":";;;;;;;AA8BA;KAAY,uBAAA;;;;EAIV,aAAA,GAAgB,uBAAA;EAKQ;;;EAAxB,cAAA,GAAiB,OAAA,CAAQ,cAAA;AAAA;;;;;AAuB3B;;;;;;;;;;;;;;;;cAAa,gBAAA,GACX,OAAA,GAAU,uBAAA,KACT,YAAA"}
|