Files
2026-08-07 18:43:42 +01:00

115 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from "node:fs";
import path from "node:path";
import react from "@vitejs/plugin-react";
import { defineConfig, type Plugin } from "vite";
import { shouldBundlePseudoLocale } from "./lib/i18n/bundling";
const LOCALES_DIR = path.resolve(__dirname, "src/locales");
/**
* One chunk per non-`en` locale, instead of one per locale-namespace file.
*
* `lib/i18n/index.ts` glob-imports the catalogs lazily, which by default emits a
* chunk per JSON file — 29 namespaces × 3 locales. i18next is seeded with every
* namespace and any surface may use any of them, so a locale is always loaded
* whole; per-namespace chunks buy no smaller download and cost a non-English
* boot 29 round trips before it can paint. Grouping makes that one request.
*
* Read from disk rather than mirroring `SUPPORTED_LOCALES`, so adding a locale
* directory needs no edit here. `en` is deliberately absent: it is statically
* imported into the entry chunk as the synchronous fallback.
*/
function localeChunkGroups() {
return fs
.readdirSync(LOCALES_DIR, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name !== "en")
.map((entry) => ({
name: `i18n-${entry.name}`,
test: new RegExp(`/src/locales/${escapeRegExp(entry.name)}/[^/]+\\.json$`),
}));
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export default defineConfig({
plugins: [react(), pseudoLocaleBundling()],
server: {
port: readPort(process.env.PORT),
strictPort: Boolean(process.env.PORT),
// Remote dev hosts (cloud VMs, LAN hostnames) are otherwise blocked by
// Vite's DNS-rebinding host check. Opt in per environment with an
// explicit comma-separated host list, e.g.
// KANDEV_VITE_ALLOWED_HOSTS="a.example.com,b.example.com".
allowedHosts: readAllowedHosts(process.env.KANDEV_VITE_ALLOWED_HOSTS),
},
preview: {
port: readPort(process.env.PORT),
strictPort: Boolean(process.env.PORT),
},
resolve: {
alias: {
"@": path.resolve(__dirname, "."),
"@kandev/ui": path.resolve(__dirname, "../packages/ui/src"),
"@kandev/theme": path.resolve(__dirname, "../packages/theme/src"),
"@kandev/types": path.resolve(__dirname, "../packages/types/src"),
},
},
build: {
outDir: "dist",
emptyOutDir: true,
rolldownOptions: {
output: {
advancedChunks: { groups: localeChunkGroups() },
},
},
},
});
/**
* Defines `__KANDEV_PSEUDO_LOCALE_BUNDLED__`, which gates the `pseudo` catalog's
* `import.meta.glob` in `lib/i18n/index.ts`. See `lib/i18n/bundling.ts` for why
* the build invocation — not `import.meta.env.PROD` — decides this.
*
* A plugin rather than a top-level `define` because only the `config` hook is
* handed `ConfigEnv.command`, and keeping the default export an object literal
* leaves `vitest.config.ts`'s `mergeConfig(viteConfig, …)` working unchanged.
*/
function pseudoLocaleBundling(): Plugin {
return {
name: "kandev:i18n-pseudo-locale-bundling",
config(_userConfig, env) {
return {
define: {
__KANDEV_PSEUDO_LOCALE_BUNDLED__: JSON.stringify(
shouldBundlePseudoLocale({ command: env.command, env: process.env }),
),
},
};
},
};
}
function readPort(value: string | undefined): number | undefined {
if (!value) return undefined;
const port = Number(value);
return Number.isInteger(port) && port > 0 ? port : undefined;
}
/**
* Reads the comma-separated KANDEV_VITE_ALLOWED_HOSTS override. Returns
* undefined (Vite's default host policy) when unset, or the explicit host
* list. Wildcards are deliberately unsupported: allowing every host would
* expose the dev server to DNS-rebinding attacks, so only named hosts can
* be opted in.
*/
function readAllowedHosts(value: string | undefined): string[] | undefined {
if (!value) return undefined;
return value
.split(",")
.map((host) => host.trim())
.filter(Boolean);
}