@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.
- package/dist/{index-BCwBfDfA.d.ts → index-CFBKeDvv.d.ts} +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin-CnS_mBUE.d.ts +12 -0
- package/dist/plugin-DAk44om0.js +416 -0
- package/dist/rolldown.d.ts +9 -0
- package/dist/rolldown.js +9 -0
- package/dist/rspack.d.ts +9 -0
- package/dist/rspack.js +9 -0
- package/dist/{src-BBvRqv3b.js → src-0cb8FYdn.js} +3 -0
- package/dist/vite.d.ts +5 -9
- package/dist/vite.js +5 -330
- package/package.json +11 -2
|
@@ -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-
|
|
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-
|
|
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 };
|
package/dist/rolldown.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/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 };
|
package/dist/rspack.d.ts
ADDED
|
@@ -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 };
|
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-
|
|
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
|
-
|
|
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-
|
|
2
|
-
import {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
+
"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"
|