@ilha/router 0.3.9 → 0.4.1

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.
@@ -128,6 +128,12 @@ interface RouterBuilder {
128
128
  * was never registered via `.route()`.
129
129
  */
130
130
  attachLoader(pattern: string, loader: Loader<any>): RouterBuilder;
131
+ /**
132
+ * Return a snapshot of every registered route in match order. Useful for
133
+ * prerenderers that need to discover the filesystem routes exposed by
134
+ * `pageRouter` without reaching into router internals.
135
+ */
136
+ routes(): RouteRecord[];
131
137
  prime(): void;
132
138
  mount(target: string | Element, options?: MountOptions): () => void;
133
139
  render(url: string | URL): string;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as prefetch, B as wrapLayout, C as composeLoaders, D as isActive, E as error, F as routePath, H as getHistoryMode, I as routeSearch, L as router, M as redirect, N as routeHash, O as loader, P as routeParams, R as useRoute, S as _default, T as enableLinkInterception, U as setHistoryMode, V as HistoryMode, _ as RouteRecord, a as InferLoader, b as RouterLink, c as LinkInterceptionOptions, d as LoaderError, f as MergeLoaders, g as RenderResponse, h as Redirect, i as HydrateOptions, j as prime, k as navigate, l as Loader, m as NavigateOptions, n as ErrorHandler, o as LOADER_ENDPOINT, p as MountOptions, r as HydratableRenderOptions, s as LayoutHandler, t as AppError, u as LoaderContext, v as RouteSnapshot, w as defineLayout, x as RouterView, y as RouterBuilder, z as wrapError } from "./index-BCwBfDfA.js";
1
+ import { A as prefetch, B as wrapLayout, C as composeLoaders, D as isActive, E as error, F as routePath, H as getHistoryMode, I as routeSearch, L as router, M as redirect, N as routeHash, O as loader, P as routeParams, R as useRoute, S as _default, T as enableLinkInterception, U as setHistoryMode, V as HistoryMode, _ as RouteRecord, a as InferLoader, b as RouterLink, c as LinkInterceptionOptions, d as LoaderError, f as MergeLoaders, g as RenderResponse, h as Redirect, i as HydrateOptions, j as prime, k as navigate, l as Loader, m as NavigateOptions, n as ErrorHandler, o as LOADER_ENDPOINT, p as MountOptions, r as HydratableRenderOptions, s as LayoutHandler, t as AppError, u as LoaderContext, v as RouteSnapshot, w as defineLayout, x as RouterView, y as RouterBuilder, z as wrapError } from "./index-CFBKeDvv.js";
2
2
  export { AppError, ErrorHandler, HistoryMode, HydratableRenderOptions, HydrateOptions, InferLoader, LOADER_ENDPOINT, LayoutHandler, LinkInterceptionOptions, Loader, LoaderContext, LoaderError, MergeLoaders, MountOptions, NavigateOptions, Redirect, RenderResponse, RouteRecord, RouteSnapshot, RouterBuilder, RouterLink, RouterView, composeLoaders, _default as default, defineLayout, enableLinkInterception, error, getHistoryMode, isActive, loader, navigate, prefetch, prime, redirect, routeHash, routeParams, routePath, routeSearch, router, setHistoryMode, useRoute, wrapError, wrapLayout };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as wrapError, E as setHistoryMode, S as useRoute, T as getHistoryMode, _ as routeParams, a as RouterView, b as router, c as enableLinkInterception, d as loader, f as navigate, g as routeHash, h as redirect, i as RouterLink, l as error, m as prime, n as LoaderError, o as composeLoaders, p as prefetch, r as Redirect, s as defineLayout, t as LOADER_ENDPOINT, u as isActive, v as routePath, w as wrapLayout, x as src_default, y as routeSearch } from "./src-BBvRqv3b.js";
1
+ import { C as wrapError, E as setHistoryMode, S as useRoute, T as getHistoryMode, _ as routeParams, a as RouterView, b as router, c as enableLinkInterception, d as loader, f as navigate, g as routeHash, h as redirect, i as RouterLink, l as error, m as prime, n as LoaderError, o as composeLoaders, p as prefetch, r as Redirect, s as defineLayout, t as LOADER_ENDPOINT, u as isActive, v as routePath, w as wrapLayout, x as src_default, y as routeSearch } from "./src-0cb8FYdn.js";
2
2
  export { LOADER_ENDPOINT, LoaderError, Redirect, RouterLink, RouterView, composeLoaders, src_default as default, defineLayout, enableLinkInterception, error, getHistoryMode, isActive, loader, navigate, prefetch, prime, redirect, routeHash, routeParams, routePath, routeSearch, router, setHistoryMode, useRoute, wrapError, wrapLayout };
@@ -0,0 +1,12 @@
1
+ import * as _$unplugin from "unplugin";
2
+
3
+ //#region src/plugin.d.ts
4
+ interface IlhaPagesOptions {
5
+ /** Directory containing page files. Default: `src/pages` */
6
+ dir?: string;
7
+ /** Output path for the generated routes + registry file. Default: `.ilha/routes.ts` */
8
+ generated?: string;
9
+ }
10
+ declare const ilhaPages: _$unplugin.UnpluginInstance<IlhaPagesOptions | undefined, boolean>;
11
+ //#endregion
12
+ export { ilhaPages as n, IlhaPagesOptions as t };
@@ -0,0 +1,416 @@
1
+ import { watch } from "node:fs";
2
+ import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
3
+ import { createUnplugin } from "unplugin";
4
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
5
+ //#region src/codegen.ts
6
+ function toPosix(p) {
7
+ return p.replace(/\\/g, "/");
8
+ }
9
+ /** Files that should never be treated as pages even if they match the ts/tsx extension. */
10
+ const EXCLUDED_RE = /\.(test|spec|d)\.(ts|tsx)$/;
11
+ /**
12
+ * Match a top-of-statement `export const load`, `export let load`,
13
+ * `export function load`, or `export async function load`. Intentionally
14
+ * conservative: `export { load } from "./x"` re-exports are NOT detected
15
+ * in v1. Declare `load` directly in the file to be picked up.
16
+ */
17
+ const LOADER_EXPORT_RE = /^\s*export\s+(?:const|let|var|async\s+function|function)\s+load\b/m;
18
+ async function hasLoaderExport(file) {
19
+ try {
20
+ const stripped = (await readFile(file, "utf8")).replace(/^\s*\/\/.*$/gm, "");
21
+ return LOADER_EXPORT_RE.test(stripped);
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ function fileToSegment(name) {
27
+ if (name.startsWith("[...") && name.endsWith("]")) return `**:${name.slice(4, -1)}`;
28
+ if (name.startsWith("[") && name.endsWith("]")) return `:${name.slice(1, -1)}`;
29
+ return name;
30
+ }
31
+ /** Route-group directories like "(auth)" are transparent to the URL. */
32
+ function dirToSegment(name) {
33
+ if (name.startsWith("(") && name.endsWith(")")) return "";
34
+ return fileToSegment(name);
35
+ }
36
+ function fileToPattern(pagesDir, file) {
37
+ const rel = toPosix(relative(pagesDir, file));
38
+ const parts = rel.slice(0, -extname(rel).length).split("/");
39
+ const segments = [...parts.slice(0, -1).map(dirToSegment), fileToSegment(parts.at(-1))];
40
+ if (segments.at(-1) === "index") segments.pop();
41
+ return "/" + segments.filter(Boolean).join("/") || "/";
42
+ }
43
+ function patternToName(pattern) {
44
+ if (pattern === "/") return "index";
45
+ return pattern.replace(/^\//, "").replace(/\*\*:[^/]*/g, (m) => m.length > 3 ? m.slice(3) : "wildcard").replace(/:/g, "").replace(/\*\*/g, "wildcard").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "") || "page";
46
+ }
47
+ function specificityScore(pattern) {
48
+ if (pattern === "/") return 3;
49
+ if (pattern.includes("**")) return 0;
50
+ if (pattern.includes(":")) return 1;
51
+ return 2;
52
+ }
53
+ /** Deterministic sort: by specificity desc, then by segment count desc, then alphabetical. */
54
+ function sortEntries(entries) {
55
+ return [...entries].sort((a, b) => {
56
+ const specDiff = specificityScore(b.pattern) - specificityScore(a.pattern);
57
+ if (specDiff !== 0) return specDiff;
58
+ const segDiff = b.pattern.split("/").length - a.pattern.split("/").length;
59
+ if (segDiff !== 0) return segDiff;
60
+ return a.pattern.localeCompare(b.pattern);
61
+ });
62
+ }
63
+ function chainForFile(pagesDir, file, all, sentinel) {
64
+ const relDir = toPosix(relative(pagesDir, dirname(file)));
65
+ const parts = relDir === "" ? [] : relDir.split("/");
66
+ const dirs = [pagesDir, ...parts.map((_, i) => join(pagesDir, ...parts.slice(0, i + 1)))];
67
+ const candidatesFor = (dir) => {
68
+ const tsx = `${join(dir, sentinel)}.tsx`;
69
+ if (all.has(tsx)) return [tsx];
70
+ const ts = `${join(dir, sentinel)}.ts`;
71
+ if (all.has(ts)) return [ts];
72
+ return [];
73
+ };
74
+ return dirs.flatMap(candidatesFor);
75
+ }
76
+ const MAX_SCAN_DEPTH = 20;
77
+ async function collectFiles(dir, depth = 0) {
78
+ if (depth > MAX_SCAN_DEPTH) {
79
+ console.warn(`[ilha:pages] Max scan depth (${MAX_SCAN_DEPTH}) reached at ${dir} — skipping`);
80
+ return [];
81
+ }
82
+ const results = [];
83
+ try {
84
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
85
+ const full = join(dir, entry.name);
86
+ if (entry.isDirectory()) results.push(...await collectFiles(full, depth + 1));
87
+ else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name) && !EXCLUDED_RE.test(entry.name)) results.push(full);
88
+ }
89
+ } catch (e) {
90
+ if (e.code === "ENOENT") return [];
91
+ throw e;
92
+ }
93
+ return results;
94
+ }
95
+ async function scanPages(pagesDir) {
96
+ const all = await collectFiles(pagesDir);
97
+ const allSet = new Set(all);
98
+ const pages = all.filter((f) => !basename(f).startsWith("+"));
99
+ const layoutLoaderCache = /* @__PURE__ */ new Map();
100
+ const getLayoutHasLoader = (file) => {
101
+ let cached = layoutLoaderCache.get(file);
102
+ if (!cached) {
103
+ cached = hasLoaderExport(file);
104
+ layoutLoaderCache.set(file, cached);
105
+ }
106
+ return cached;
107
+ };
108
+ return Promise.all(pages.map(async (file) => {
109
+ const pattern = fileToPattern(pagesDir, file);
110
+ const layouts = chainForFile(pagesDir, file, allSet, "+layout");
111
+ const errors = chainForFile(pagesDir, file, allSet, "+error");
112
+ const [pageHasLoader, ...layoutFlags] = await Promise.all([hasLoaderExport(file), ...layouts.map(getLayoutHasLoader)]);
113
+ const loaderLayouts = layouts.filter((_, i) => layoutFlags[i]);
114
+ return {
115
+ file,
116
+ pattern,
117
+ name: patternToName(pattern),
118
+ layouts,
119
+ errors,
120
+ hasLoader: pageHasLoader,
121
+ loaderLayouts
122
+ };
123
+ }));
124
+ }
125
+ function validateEntries(entries, pagesDir) {
126
+ if (entries.length === 0) {
127
+ console.warn(`[ilha:pages] No pages found in ${pagesDir}`);
128
+ return;
129
+ }
130
+ const seenPatterns = /* @__PURE__ */ new Map();
131
+ const seenNames = /* @__PURE__ */ new Map();
132
+ for (const entry of entries) {
133
+ const existingPattern = seenPatterns.get(entry.pattern);
134
+ if (existingPattern) console.warn(`[ilha:pages] Duplicate route pattern "${entry.pattern}"\n first: ${existingPattern}\n second: ${entry.file}\n The first match wins — the second page will never be reached.`);
135
+ else seenPatterns.set(entry.pattern, entry.file);
136
+ const existingName = seenNames.get(entry.name);
137
+ if (existingName) console.warn(`[ilha:pages] Registry name collision: "${entry.name}" is used by both\n ${existingName}\n ${entry.file}\n Hydration may not work correctly for one of these routes.`);
138
+ else seenNames.set(entry.name, entry.file);
139
+ }
140
+ }
141
+ async function generate(pagesDir, outFile) {
142
+ const entries = sortEntries(await scanPages(pagesDir));
143
+ validateEntries(entries, pagesDir);
144
+ const rel = (abs) => {
145
+ const r = toPosix(relative(dirname(outFile), abs));
146
+ return r.startsWith(".") ? r : `./${r}`;
147
+ };
148
+ const imports = [`import { router, wrapLayout, wrapError } from "@ilha/router";`, `import type { Island } from "ilha";`];
149
+ const wrappedIslandLines = [];
150
+ const registryLines = [];
151
+ const routeLines = [];
152
+ const clientImport = (abs) => `${rel(abs)}?client`;
153
+ for (const [i, entry] of entries.entries()) {
154
+ const pageId = `_page${i}`;
155
+ imports.push(`import { default as ${pageId} } from ${JSON.stringify(clientImport(entry.file))};`);
156
+ for (const [j, l] of entry.layouts.entries()) imports.push(`import { default as _layout${i}_${j} } from ${JSON.stringify(clientImport(l))};`);
157
+ for (const [j, e] of entry.errors.entries()) imports.push(`import { default as _error${i}_${j} } from ${JSON.stringify(clientImport(e))};`);
158
+ let expr = pageId;
159
+ for (let j = entry.errors.length - 1; j >= 0; j--) expr = `wrapError(_error${i}_${j}, ${expr})`;
160
+ for (let j = entry.layouts.length - 1; j >= 0; j--) expr = `wrapLayout(_layout${i}_${j}, ${expr})`;
161
+ const wrappedId = `_wrapped${i}`;
162
+ wrappedIslandLines.push(`const ${wrappedId} = ${expr};`);
163
+ registryLines.push(` ${JSON.stringify(entry.name)}: ${wrappedId}` + (i < entries.length - 1 ? "," : ""));
164
+ routeLines.push(` .route(${JSON.stringify(entry.pattern)}, ${wrappedId})`);
165
+ }
166
+ const code = [
167
+ `// @generated by @ilha/router — do not edit`,
168
+ ``,
169
+ ...imports,
170
+ ``,
171
+ ...wrappedIslandLines,
172
+ ``,
173
+ `export const registry: Record<string, Island<any, any>> = {`,
174
+ ...registryLines,
175
+ `};`,
176
+ ``,
177
+ `export const pageRouter = router()`,
178
+ ...routeLines,
179
+ ` ;`
180
+ ].join("\n");
181
+ await mkdir(dirname(outFile), { recursive: true });
182
+ const routesChanged = await writeIfChanged(outFile, code);
183
+ const loadersFile = join(dirname(outFile), "loaders.ts");
184
+ const loadersChanged = await writeIfChanged(loadersFile, buildLoadersFile(entries, loadersFile, outFile));
185
+ if (routesChanged || loadersChanged) await generateTypes(outFile);
186
+ }
187
+ function buildLoadersFile(entries, loadersFile, routesFile) {
188
+ const relFromLoaders = (abs) => {
189
+ const r = toPosix(relative(dirname(loadersFile), abs));
190
+ return r.startsWith(".") ? r : `./${r}`;
191
+ };
192
+ const withLoaders = entries.filter((e) => e.hasLoader || e.loaderLayouts.length > 0);
193
+ if (withLoaders.length === 0) return [
194
+ `// @generated by @ilha/router — do not edit`,
195
+ `// This project has no loader exports; this file is intentionally empty.`,
196
+ ``,
197
+ `export {};`,
198
+ ``
199
+ ].join("\n");
200
+ const routesRel = relFromLoaders(routesFile).replace(/\.tsx?$/, "");
201
+ const imports = [`import { pageRouter } from ${JSON.stringify(routesRel)};`];
202
+ let needsComposeLoaders = false;
203
+ const attachLines = [];
204
+ for (const [i, entry] of withLoaders.entries()) {
205
+ const loaderIds = [];
206
+ for (const [j, layout] of entry.loaderLayouts.entries()) {
207
+ const id = `_p${i}_l${j}`;
208
+ imports.push(`import { load as ${id} } from ${JSON.stringify(relFromLoaders(layout))};`);
209
+ loaderIds.push(id);
210
+ }
211
+ if (entry.hasLoader) {
212
+ const id = `_p${i}`;
213
+ imports.push(`import { load as ${id} } from ${JSON.stringify(relFromLoaders(entry.file))};`);
214
+ loaderIds.push(id);
215
+ }
216
+ const loadersExpr = loaderIds.length === 1 ? loaderIds[0] : `composeLoaders([${loaderIds.join(", ")}])`;
217
+ if (loaderIds.length > 1) needsComposeLoaders = true;
218
+ attachLines.push(`pageRouter.attachLoader(${JSON.stringify(entry.pattern)}, ${loadersExpr});`);
219
+ }
220
+ if (needsComposeLoaders) imports.unshift(`import { composeLoaders } from "@ilha/router";`);
221
+ return [
222
+ `// @generated by @ilha/router — do not edit`,
223
+ `// Server-only. Import this module from your SSR entry to wire loaders`,
224
+ `// onto pageRouter. Importing it from the client is a no-op but wastes`,
225
+ `// bundle size — rely on the default build pipeline to keep it out.`,
226
+ ``,
227
+ ...imports,
228
+ ``,
229
+ ...attachLines,
230
+ ``
231
+ ].join("\n");
232
+ }
233
+ async function writeIfChanged(file, content) {
234
+ try {
235
+ if (await readFile(file, "utf8") === content) return false;
236
+ } catch {}
237
+ await writeFile(file, content, "utf8");
238
+ return true;
239
+ }
240
+ async function generateTypes(outFile) {
241
+ await writeIfChanged(outFile.replace(/\.tsx?$/, ".d.ts"), [
242
+ `// @generated by @ilha/router — do not edit`,
243
+ ``,
244
+ `declare module "ilha:pages" {`,
245
+ ` import type { RouterBuilder } from "@ilha/router";`,
246
+ ` export const pageRouter: RouterBuilder;`,
247
+ `}`,
248
+ ``,
249
+ `declare module "ilha:registry" {`,
250
+ ` import type { Island } from "ilha";`,
251
+ ` export const registry: Record<string, Island<any, any>>;`,
252
+ `}`,
253
+ ``,
254
+ `declare module "ilha:loaders" {`,
255
+ ` // Side-effect-only module. Importing it attaches loaders to pageRouter.`,
256
+ `}`,
257
+ ``
258
+ ].join("\n"));
259
+ }
260
+ const RESOLVED_PAGES = "\0ilha:pages";
261
+ const RESOLVED_REGISTRY = "\0ilha:registry";
262
+ const RESOLVED_LOADERS = "\0ilha:loaders";
263
+ const RESOLVED_VIRTUAL_IDS = [
264
+ RESOLVED_PAGES,
265
+ RESOLVED_REGISTRY,
266
+ RESOLVED_LOADERS
267
+ ];
268
+ /** Query suffix used on page/layout imports in the client-safe routes file. */
269
+ const CLIENT_QUERY = "?client";
270
+ function resolvePluginPaths(root, options) {
271
+ const pagesDir = resolve(root, options.dir ?? "src/pages");
272
+ const outFile = resolve(root, options.generated ?? ".ilha/routes.ts");
273
+ return {
274
+ pagesDir,
275
+ outFile,
276
+ loadersFile: join(dirname(outFile), "loaders.ts")
277
+ };
278
+ }
279
+ function createPagesPluginState(options) {
280
+ let pagesDir;
281
+ let outFile;
282
+ let loadersFile;
283
+ const setPaths = (root) => {
284
+ ({pagesDir, outFile, loadersFile} = resolvePluginPaths(root, options));
285
+ };
286
+ const regen = async () => {
287
+ try {
288
+ await generate(pagesDir, outFile);
289
+ } catch (e) {
290
+ console.error("[ilha:pages] codegen failed:", e);
291
+ }
292
+ };
293
+ const isUnderPagesDir = (file) => file === pagesDir || file.startsWith(pagesDir + sep);
294
+ const shouldRegenOnChange = (file) => {
295
+ if (!isUnderPagesDir(file)) return false;
296
+ const base = basename(file);
297
+ return base.startsWith("+") || /\.(ts|tsx)$/.test(base);
298
+ };
299
+ return {
300
+ get pagesDir() {
301
+ return pagesDir;
302
+ },
303
+ get outFile() {
304
+ return outFile;
305
+ },
306
+ get loadersFile() {
307
+ return loadersFile;
308
+ },
309
+ setPaths,
310
+ regen,
311
+ shouldRegenOnChange,
312
+ isUnderPagesDir
313
+ };
314
+ }
315
+ async function regenFromPagesChange(state, file, shouldRegen) {
316
+ if (!shouldRegen(file)) return;
317
+ await state.regen();
318
+ }
319
+ function resolvePagesId(_state, id, importer) {
320
+ if (id === "ilha:pages") return RESOLVED_PAGES;
321
+ if (id === "ilha:registry") return RESOLVED_REGISTRY;
322
+ if (id === "ilha:loaders") return RESOLVED_LOADERS;
323
+ if (id.endsWith("?client")) {
324
+ const bare = id.slice(0, -7);
325
+ return (importer ? resolve(dirname(importer.replace(/\?.*$/, "")), bare) : resolve(bare)) + CLIENT_QUERY;
326
+ }
327
+ }
328
+ function loadPagesModule(state, id) {
329
+ if (id === "\0ilha:pages") {
330
+ const spec = state.outFile.replace(/\.tsx?$/, "");
331
+ return `export { pageRouter } from ${JSON.stringify(spec)};`;
332
+ }
333
+ if (id === "\0ilha:registry") {
334
+ const spec = state.outFile.replace(/\.tsx?$/, "");
335
+ return `export { registry } from ${JSON.stringify(spec)};`;
336
+ }
337
+ if (id === "\0ilha:loaders") {
338
+ const spec = state.loadersFile.replace(/\.tsx?$/, "");
339
+ return `import ${JSON.stringify(spec)};`;
340
+ }
341
+ if (id.endsWith("?client")) {
342
+ const bare = id.slice(0, -7);
343
+ return `export { default } from ${JSON.stringify(bare)};`;
344
+ }
345
+ }
346
+ function createStructuralInvalidate(state, invalidate) {
347
+ return async (file) => {
348
+ if (!state.isUnderPagesDir(file)) return;
349
+ await state.regen();
350
+ await invalidate();
351
+ };
352
+ }
353
+ function setupRspackPagesWatcher(state, structuralInvalidate) {
354
+ const watcher = watch(state.pagesDir, { recursive: true }, (_event, filename) => {
355
+ if (!filename) return;
356
+ structuralInvalidate(join(state.pagesDir, filename));
357
+ });
358
+ return () => watcher.close();
359
+ }
360
+ const pagesFactory = (options = {}) => {
361
+ const state = createPagesPluginState(options);
362
+ return {
363
+ name: "ilha:pages",
364
+ async buildStart() {
365
+ if (!state.pagesDir) state.setPaths(process.cwd());
366
+ this.addWatchFile?.(state.pagesDir);
367
+ await state.regen();
368
+ },
369
+ async watchChange(file) {
370
+ await regenFromPagesChange(state, file, (f) => state.shouldRegenOnChange(f));
371
+ },
372
+ resolveId(id, importer) {
373
+ return resolvePagesId(state, id, importer);
374
+ },
375
+ load(id) {
376
+ return loadPagesModule(state, id);
377
+ },
378
+ vite: {
379
+ configResolved(config) {
380
+ state.setPaths(config.root);
381
+ },
382
+ configureServer(server) {
383
+ server.watcher.add(state.pagesDir);
384
+ const structuralInvalidate = createStructuralInvalidate(state, async () => {
385
+ for (const id of RESOLVED_VIRTUAL_IDS) {
386
+ const mod = server.moduleGraph.getModuleById(id);
387
+ if (mod) server.moduleGraph.invalidateModule(mod);
388
+ }
389
+ server.hot.send({ type: "full-reload" });
390
+ });
391
+ server.watcher.on("add", structuralInvalidate);
392
+ server.watcher.on("addDir", structuralInvalidate);
393
+ server.watcher.on("unlink", structuralInvalidate);
394
+ server.watcher.on("change", async (file) => {
395
+ if (state.shouldRegenOnChange(file)) await structuralInvalidate(file);
396
+ });
397
+ }
398
+ },
399
+ rspack(compiler) {
400
+ state.setPaths(compiler.options.context ?? process.cwd());
401
+ const structuralInvalidate = createStructuralInvalidate(state, () => {
402
+ if (!compiler.watching) return;
403
+ compiler.invalidate();
404
+ });
405
+ let closeWatcher;
406
+ compiler.hooks.watchRun.tap("ilha:pages", () => {
407
+ closeWatcher?.();
408
+ closeWatcher = setupRspackPagesWatcher(state, structuralInvalidate);
409
+ });
410
+ compiler.hooks.shutdown.tap("ilha:pages", () => closeWatcher?.());
411
+ }
412
+ };
413
+ };
414
+ const ilhaPages = /* @__PURE__ */ createUnplugin(pagesFactory);
415
+ //#endregion
416
+ export { ilhaPages as t };
@@ -0,0 +1,9 @@
1
+ import { B as wrapLayout, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot, z as wrapError } from "./index-CFBKeDvv.js";
2
+ import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-CnS_mBUE.js";
3
+ import * as _$unplugin from "unplugin";
4
+
5
+ //#region src/rolldown.d.ts
6
+ /** Rolldown plugin — use via `@ilha/router/rolldown`. */
7
+ declare function pages(options?: IlhaPagesOptions): _$unplugin.RolldownPlugin<any> | _$unplugin.RolldownPlugin<any>[];
8
+ //#endregion
9
+ export { type AppError, type ErrorHandler, type IlhaPagesOptions, type LayoutHandler, type RouteSnapshot, ilhaPages, pages, wrapError, wrapLayout };
@@ -0,0 +1,9 @@
1
+ import { C as wrapError, w as wrapLayout } from "./src-0cb8FYdn.js";
2
+ import { t as ilhaPages } from "./plugin-DAk44om0.js";
3
+ //#region src/rolldown.ts
4
+ /** Rolldown plugin — use via `@ilha/router/rolldown`. */
5
+ function pages(options = {}) {
6
+ return ilhaPages.rolldown(options);
7
+ }
8
+ //#endregion
9
+ export { ilhaPages, pages, wrapError, wrapLayout };
@@ -0,0 +1,9 @@
1
+ import { B as wrapLayout, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot, z as wrapError } from "./index-CFBKeDvv.js";
2
+ import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-CnS_mBUE.js";
3
+ import * as _$unplugin from "unplugin";
4
+
5
+ //#region src/rspack.d.ts
6
+ /** Rspack plugin — use via `@ilha/router/rspack`. */
7
+ declare function pages(options?: IlhaPagesOptions): _$unplugin.RspackPluginInstance;
8
+ //#endregion
9
+ export { type AppError, type ErrorHandler, type IlhaPagesOptions, type LayoutHandler, type RouteSnapshot, ilhaPages, pages, wrapError, wrapLayout };
package/dist/rspack.js ADDED
@@ -0,0 +1,9 @@
1
+ import { C as wrapError, w as wrapLayout } from "./src-0cb8FYdn.js";
2
+ import { t as ilhaPages } from "./plugin-DAk44om0.js";
3
+ //#region src/rspack.ts
4
+ /** Rspack plugin — use via `@ilha/router/rspack`. */
5
+ function pages(options = {}) {
6
+ return ilhaPages.rspack(options);
7
+ }
8
+ //#endregion
9
+ export { ilhaPages, pages, wrapError, wrapLayout };
@@ -611,6 +611,9 @@ function router() {
611
611
  if (rec) rec.loader = loader;
612
612
  return builder;
613
613
  },
614
+ routes() {
615
+ return _records.map((record) => ({ ...record }));
616
+ },
614
617
  prime,
615
618
  mount(target, { hydrate = false, registry } = {}) {
616
619
  if (!isBrowser) {
package/dist/vite.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /// <reference types="node" />
2
- import { B as wrapLayout, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot, z as wrapError } from "./index-BCwBfDfA.js";
2
+ import { B as wrapLayout, n as ErrorHandler, s as LayoutHandler, t as AppError, v as RouteSnapshot, z as wrapError } from "./index-CFBKeDvv.js";
3
+ import { n as ilhaPages, t as IlhaPagesOptions } from "./plugin-CnS_mBUE.js";
4
+ import * as fs from "node:fs";
3
5
  import * as http from "node:http";
4
6
  import { Agent, ClientRequest, ClientRequestArgs, OutgoingHttpHeaders } from "node:http";
5
7
  import { Http2SecureServer } from "node:http2";
6
- import * as fs from "node:fs";
7
8
  import { EventEmitter } from "node:events";
8
9
  import { Server as Server$1, ServerOptions as ServerOptions$1 } from "node:https";
9
10
  import * as net from "node:net";
@@ -9918,12 +9919,7 @@ interface PluginHookUtils {
9918
9919
  type ResolveFn = (id: string, importer?: string, aliasOnly?: boolean, ssr?: boolean) => Promise<string | undefined>;
9919
9920
  //#endregion
9920
9921
  //#region src/vite.d.ts
9921
- interface IlhaPagesOptions {
9922
- /** Directory containing page files. Default: `src/pages` */
9923
- dir?: string;
9924
- /** Output path for the generated routes + registry file. Default: `.ilha/routes.ts` */
9925
- generated?: string;
9926
- }
9922
+ /** Vite plugin — use via `@ilha/router/vite`. */
9927
9923
  declare function pages(options?: IlhaPagesOptions): Plugin;
9928
9924
  //#endregion
9929
- export { type AppError, type ErrorHandler, IlhaPagesOptions, type LayoutHandler, type RouteSnapshot, pages, wrapError, wrapLayout };
9925
+ export { type AppError, type ErrorHandler, type IlhaPagesOptions, type LayoutHandler, type RouteSnapshot, ilhaPages, pages, wrapError, wrapLayout };
package/dist/vite.js CHANGED
@@ -1,334 +1,9 @@
1
- import { C as wrapError, w as wrapLayout } from "./src-BBvRqv3b.js";
2
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
- import { basename, dirname, extname, join, relative, resolve } from "node:path";
1
+ import { C as wrapError, w as wrapLayout } from "./src-0cb8FYdn.js";
2
+ import { t as ilhaPages } from "./plugin-DAk44om0.js";
4
3
  //#region src/vite.ts
5
- /** Files that should never be treated as pages even if they match the ts/tsx extension. */
6
- const EXCLUDED_RE = /\.(test|spec|d)\.(ts|tsx)$/;
7
- /**
8
- * Match a top-of-statement `export const load`, `export let load`,
9
- * `export function load`, or `export async function load`. Intentionally
10
- * conservative: `export { load } from "./x"` re-exports are NOT detected
11
- * in v1. Declare `load` directly in the file to be picked up.
12
- */
13
- const LOADER_EXPORT_RE = /^\s*export\s+(?:const|let|var|async\s+function|function)\s+load\b/m;
14
- async function hasLoaderExport(file) {
15
- try {
16
- const stripped = (await readFile(file, "utf8")).replace(/^\s*\/\/.*$/gm, "");
17
- return LOADER_EXPORT_RE.test(stripped);
18
- } catch {
19
- return false;
20
- }
21
- }
22
- function fileToSegment(name) {
23
- if (name.startsWith("[...") && name.endsWith("]")) return `**:${name.slice(4, -1)}`;
24
- if (name.startsWith("[") && name.endsWith("]")) return `:${name.slice(1, -1)}`;
25
- return name;
26
- }
27
- /** Route-group directories like "(auth)" are transparent to the URL. */
28
- function dirToSegment(name) {
29
- if (name.startsWith("(") && name.endsWith(")")) return "";
30
- return fileToSegment(name);
31
- }
32
- function fileToPattern(pagesDir, file) {
33
- const rel = relative(pagesDir, file);
34
- const parts = rel.slice(0, -extname(rel).length).split("/");
35
- const segments = [...parts.slice(0, -1).map(dirToSegment), fileToSegment(parts.at(-1))];
36
- if (segments.at(-1) === "index") segments.pop();
37
- return "/" + segments.filter(Boolean).join("/") || "/";
38
- }
39
- function patternToName(pattern) {
40
- if (pattern === "/") return "index";
41
- return pattern.replace(/^\//, "").replace(/\*\*:[^/]*/g, (m) => m.length > 3 ? m.slice(3) : "wildcard").replace(/:/g, "").replace(/\*\*/g, "wildcard").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "") || "page";
42
- }
43
- function specificityScore(pattern) {
44
- if (pattern === "/") return 3;
45
- if (pattern.includes("**")) return 0;
46
- if (pattern.includes(":")) return 1;
47
- return 2;
48
- }
49
- /** Deterministic sort: by specificity desc, then by segment count desc, then alphabetical. */
50
- function sortEntries(entries) {
51
- return [...entries].sort((a, b) => {
52
- const specDiff = specificityScore(b.pattern) - specificityScore(a.pattern);
53
- if (specDiff !== 0) return specDiff;
54
- const segDiff = b.pattern.split("/").length - a.pattern.split("/").length;
55
- if (segDiff !== 0) return segDiff;
56
- return a.pattern.localeCompare(b.pattern);
57
- });
58
- }
59
- function chainForFile(pagesDir, file, all, sentinel) {
60
- const relDir = relative(pagesDir, dirname(file));
61
- const parts = relDir === "" ? [] : relDir.split("/");
62
- const dirs = [pagesDir, ...parts.map((_, i) => join(pagesDir, ...parts.slice(0, i + 1)))];
63
- const candidatesFor = (dir) => {
64
- const tsx = `${join(dir, sentinel)}.tsx`;
65
- if (all.has(tsx)) return [tsx];
66
- const ts = `${join(dir, sentinel)}.ts`;
67
- if (all.has(ts)) return [ts];
68
- return [];
69
- };
70
- return dirs.flatMap(candidatesFor);
71
- }
72
- const MAX_SCAN_DEPTH = 20;
73
- async function collectFiles(dir, depth = 0) {
74
- if (depth > MAX_SCAN_DEPTH) {
75
- console.warn(`[ilha:pages] Max scan depth (${MAX_SCAN_DEPTH}) reached at ${dir} — skipping`);
76
- return [];
77
- }
78
- const results = [];
79
- for (const entry of await readdir(dir, { withFileTypes: true })) {
80
- const full = join(dir, entry.name);
81
- if (entry.isDirectory()) results.push(...await collectFiles(full, depth + 1));
82
- else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name) && !EXCLUDED_RE.test(entry.name)) results.push(full);
83
- }
84
- return results;
85
- }
86
- async function scanPages(pagesDir) {
87
- const all = await collectFiles(pagesDir);
88
- const allSet = new Set(all);
89
- const pages = all.filter((f) => !basename(f).startsWith("+"));
90
- const layoutLoaderCache = /* @__PURE__ */ new Map();
91
- const getLayoutHasLoader = (file) => {
92
- let cached = layoutLoaderCache.get(file);
93
- if (!cached) {
94
- cached = hasLoaderExport(file);
95
- layoutLoaderCache.set(file, cached);
96
- }
97
- return cached;
98
- };
99
- return Promise.all(pages.map(async (file) => {
100
- const pattern = fileToPattern(pagesDir, file);
101
- const layouts = chainForFile(pagesDir, file, allSet, "+layout");
102
- const errors = chainForFile(pagesDir, file, allSet, "+error");
103
- const [pageHasLoader, ...layoutFlags] = await Promise.all([hasLoaderExport(file), ...layouts.map(getLayoutHasLoader)]);
104
- const loaderLayouts = layouts.filter((_, i) => layoutFlags[i]);
105
- return {
106
- file,
107
- pattern,
108
- name: patternToName(pattern),
109
- layouts,
110
- errors,
111
- hasLoader: pageHasLoader,
112
- loaderLayouts
113
- };
114
- }));
115
- }
116
- function validateEntries(entries, pagesDir) {
117
- if (entries.length === 0) {
118
- console.warn(`[ilha:pages] No pages found in ${pagesDir}`);
119
- return;
120
- }
121
- const seenPatterns = /* @__PURE__ */ new Map();
122
- const seenNames = /* @__PURE__ */ new Map();
123
- for (const entry of entries) {
124
- const existingPattern = seenPatterns.get(entry.pattern);
125
- if (existingPattern) console.warn(`[ilha:pages] Duplicate route pattern "${entry.pattern}"\n first: ${existingPattern}\n second: ${entry.file}\n The first match wins — the second page will never be reached.`);
126
- else seenPatterns.set(entry.pattern, entry.file);
127
- const existingName = seenNames.get(entry.name);
128
- if (existingName) console.warn(`[ilha:pages] Registry name collision: "${entry.name}" is used by both\n ${existingName}\n ${entry.file}\n Hydration may not work correctly for one of these routes.`);
129
- else seenNames.set(entry.name, entry.file);
130
- }
131
- }
132
- async function generate(pagesDir, outFile) {
133
- const entries = sortEntries(await scanPages(pagesDir));
134
- validateEntries(entries, pagesDir);
135
- const rel = (abs) => {
136
- const r = relative(dirname(outFile), abs);
137
- return r.startsWith(".") ? r : `./${r}`;
138
- };
139
- const imports = [`import { router, wrapLayout, wrapError } from "@ilha/router";`, `import type { Island } from "ilha";`];
140
- const wrappedIslandLines = [];
141
- const registryLines = [];
142
- const routeLines = [];
143
- const clientImport = (abs) => `${rel(abs)}?client`;
144
- for (const [i, entry] of entries.entries()) {
145
- const pageId = `_page${i}`;
146
- imports.push(`import { default as ${pageId} } from ${JSON.stringify(clientImport(entry.file))};`);
147
- for (const [j, l] of entry.layouts.entries()) imports.push(`import { default as _layout${i}_${j} } from ${JSON.stringify(clientImport(l))};`);
148
- for (const [j, e] of entry.errors.entries()) imports.push(`import { default as _error${i}_${j} } from ${JSON.stringify(clientImport(e))};`);
149
- let expr = pageId;
150
- for (let j = entry.errors.length - 1; j >= 0; j--) expr = `wrapError(_error${i}_${j}, ${expr})`;
151
- for (let j = entry.layouts.length - 1; j >= 0; j--) expr = `wrapLayout(_layout${i}_${j}, ${expr})`;
152
- const wrappedId = `_wrapped${i}`;
153
- wrappedIslandLines.push(`const ${wrappedId} = ${expr};`);
154
- registryLines.push(` ${JSON.stringify(entry.name)}: ${wrappedId}` + (i < entries.length - 1 ? "," : ""));
155
- routeLines.push(` .route(${JSON.stringify(entry.pattern)}, ${wrappedId})`);
156
- }
157
- const code = [
158
- `// @generated by @ilha/router — do not edit`,
159
- ``,
160
- ...imports,
161
- ``,
162
- ...wrappedIslandLines,
163
- ``,
164
- `export const registry: Record<string, Island<any, any>> = {`,
165
- ...registryLines,
166
- `};`,
167
- ``,
168
- `export const pageRouter = router()`,
169
- ...routeLines,
170
- ` ;`
171
- ].join("\n");
172
- await mkdir(dirname(outFile), { recursive: true });
173
- const routesChanged = await writeIfChanged(outFile, code);
174
- const loadersFile = join(dirname(outFile), "loaders.ts");
175
- const loadersChanged = await writeIfChanged(loadersFile, buildLoadersFile(entries, loadersFile, outFile));
176
- if (routesChanged || loadersChanged) await generateTypes(outFile);
177
- }
178
- function buildLoadersFile(entries, loadersFile, routesFile) {
179
- const relFromLoaders = (abs) => {
180
- const r = relative(dirname(loadersFile), abs);
181
- return r.startsWith(".") ? r : `./${r}`;
182
- };
183
- const withLoaders = entries.filter((e) => e.hasLoader || e.loaderLayouts.length > 0);
184
- if (withLoaders.length === 0) return [
185
- `// @generated by @ilha/router — do not edit`,
186
- `// This project has no loader exports; this file is intentionally empty.`,
187
- ``,
188
- `export {};`,
189
- ``
190
- ].join("\n");
191
- const routesRel = relFromLoaders(routesFile).replace(/\.ts$/, "");
192
- const imports = [`import { pageRouter } from ${JSON.stringify(routesRel)};`];
193
- let needsComposeLoaders = false;
194
- const attachLines = [];
195
- for (const [i, entry] of withLoaders.entries()) {
196
- const loaderIds = [];
197
- for (const [j, layout] of entry.loaderLayouts.entries()) {
198
- const id = `_p${i}_l${j}`;
199
- imports.push(`import { load as ${id} } from ${JSON.stringify(relFromLoaders(layout))};`);
200
- loaderIds.push(id);
201
- }
202
- if (entry.hasLoader) {
203
- const id = `_p${i}`;
204
- imports.push(`import { load as ${id} } from ${JSON.stringify(relFromLoaders(entry.file))};`);
205
- loaderIds.push(id);
206
- }
207
- const loadersExpr = loaderIds.length === 1 ? loaderIds[0] : `composeLoaders([${loaderIds.join(", ")}])`;
208
- if (loaderIds.length > 1) needsComposeLoaders = true;
209
- attachLines.push(`pageRouter.attachLoader(${JSON.stringify(entry.pattern)}, ${loadersExpr});`);
210
- }
211
- if (needsComposeLoaders) imports.unshift(`import { composeLoaders } from "@ilha/router";`);
212
- return [
213
- `// @generated by @ilha/router — do not edit`,
214
- `// Server-only. Import this module from your SSR entry to wire loaders`,
215
- `// onto pageRouter. Importing it from the client is a no-op but wastes`,
216
- `// bundle size — rely on the default build pipeline to keep it out.`,
217
- ``,
218
- ...imports,
219
- ``,
220
- ...attachLines,
221
- ``
222
- ].join("\n");
223
- }
224
- async function writeIfChanged(file, content) {
225
- try {
226
- if (await readFile(file, "utf8") === content) return false;
227
- } catch {}
228
- await writeFile(file, content, "utf8");
229
- return true;
230
- }
231
- async function generateTypes(outFile) {
232
- await writeIfChanged(outFile.replace(/\.ts$/, ".d.ts"), [
233
- `// @generated by @ilha/router — do not edit`,
234
- ``,
235
- `declare module "ilha:pages" {`,
236
- ` import type { RouterBuilder } from "@ilha/router";`,
237
- ` export const pageRouter: RouterBuilder;`,
238
- `}`,
239
- ``,
240
- `declare module "ilha:registry" {`,
241
- ` import type { Island } from "ilha";`,
242
- ` export const registry: Record<string, Island<any, any>>;`,
243
- `}`,
244
- ``,
245
- `declare module "ilha:loaders" {`,
246
- ` // Side-effect-only module. Importing it attaches loaders to pageRouter.`,
247
- `}`,
248
- ``
249
- ].join("\n"));
250
- }
251
- const VIRTUAL_PAGES = "ilha:pages";
252
- const VIRTUAL_REGISTRY = "ilha:registry";
253
- const VIRTUAL_LOADERS = "ilha:loaders";
254
- const RESOLVED_PAGES = "\0ilha:pages";
255
- const RESOLVED_REGISTRY = "\0ilha:registry";
256
- const RESOLVED_LOADERS = "\0ilha:loaders";
257
- /** Query suffix used on page/layout imports in the client-safe routes file. */
258
- const CLIENT_QUERY = "?client";
4
+ /** Vite plugin use via `@ilha/router/vite`. */
259
5
  function pages(options = {}) {
260
- let pagesDir;
261
- let outFile;
262
- let loadersFile;
263
- async function regen() {
264
- try {
265
- await generate(pagesDir, outFile);
266
- } catch (e) {
267
- console.error("[ilha:pages] codegen failed:", e);
268
- }
269
- }
270
- return {
271
- name: "ilha:pages",
272
- configResolved(config) {
273
- pagesDir = resolve(config.root, options.dir ?? "src/pages");
274
- outFile = resolve(config.root, options.generated ?? ".ilha/routes.ts");
275
- loadersFile = join(dirname(outFile), "loaders.ts");
276
- },
277
- async buildStart() {
278
- await regen();
279
- },
280
- configureServer(server) {
281
- server.watcher.add(pagesDir);
282
- const structuralInvalidate = async (file) => {
283
- if (!file.startsWith(pagesDir)) return;
284
- await regen();
285
- for (const id of [
286
- RESOLVED_PAGES,
287
- RESOLVED_REGISTRY,
288
- RESOLVED_LOADERS
289
- ]) {
290
- const mod = server.moduleGraph.getModuleById(id);
291
- if (mod) server.moduleGraph.invalidateModule(mod);
292
- }
293
- server.hot.send({ type: "full-reload" });
294
- };
295
- server.watcher.on("add", structuralInvalidate);
296
- server.watcher.on("addDir", structuralInvalidate);
297
- server.watcher.on("unlink", structuralInvalidate);
298
- server.watcher.on("change", async (file) => {
299
- if (!file.startsWith(pagesDir)) return;
300
- const base = basename(file);
301
- if (base.startsWith("+") || /\.(ts|tsx)$/.test(base)) await structuralInvalidate(file);
302
- });
303
- },
304
- resolveId(id, importer) {
305
- if (id === VIRTUAL_PAGES) return RESOLVED_PAGES;
306
- if (id === VIRTUAL_REGISTRY) return RESOLVED_REGISTRY;
307
- if (id === VIRTUAL_LOADERS) return RESOLVED_LOADERS;
308
- if (id.endsWith(CLIENT_QUERY)) {
309
- const bare = id.slice(0, -7);
310
- return (importer ? resolve(dirname(importer.replace(/\?.*$/, "")), bare) : resolve(bare)) + CLIENT_QUERY;
311
- }
312
- },
313
- load(id) {
314
- if (id === RESOLVED_PAGES) {
315
- const spec = outFile.replace(/\.tsx?$/, "");
316
- return `export { pageRouter } from ${JSON.stringify(spec)};`;
317
- }
318
- if (id === RESOLVED_REGISTRY) {
319
- const spec = outFile.replace(/\.tsx?$/, "");
320
- return `export { registry } from ${JSON.stringify(spec)};`;
321
- }
322
- if (id === RESOLVED_LOADERS) {
323
- const spec = loadersFile.replace(/\.tsx?$/, "");
324
- return `import ${JSON.stringify(spec)};`;
325
- }
326
- if (id.endsWith(CLIENT_QUERY)) {
327
- const bare = id.slice(0, -7);
328
- return `export { default } from ${JSON.stringify(bare)};`;
329
- }
330
- }
331
- };
6
+ return ilhaPages.vite(options);
332
7
  }
333
8
  //#endregion
334
- export { pages, wrapError, wrapLayout };
9
+ export { ilhaPages, pages, wrapError, wrapLayout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilha/router",
3
- "version": "0.3.9",
3
+ "version": "0.4.1",
4
4
  "description": "A tiny SPA router for Ilha",
5
5
  "license": "MIT",
6
6
  "author": "Ryuz <ryuzer@proton.me>",
@@ -23,6 +23,14 @@
23
23
  "./vite": {
24
24
  "types": "./dist/vite.d.ts",
25
25
  "import": "./dist/vite.js"
26
+ },
27
+ "./rspack": {
28
+ "types": "./dist/rspack.d.ts",
29
+ "import": "./dist/rspack.js"
30
+ },
31
+ "./rolldown": {
32
+ "types": "./dist/rolldown.d.ts",
33
+ "import": "./dist/rolldown.js"
26
34
  }
27
35
  },
28
36
  "scripts": {
@@ -31,7 +39,8 @@
31
39
  },
32
40
  "dependencies": {
33
41
  "ilha": "0.7.0",
34
- "rou3": "0.8.1"
42
+ "rou3": "0.8.1",
43
+ "unplugin": "3.0.0"
35
44
  },
36
45
  "devDependencies": {
37
46
  "vite": "^8.0.14"