@pyreon/zero 0.12.10 → 0.12.11
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/lib/ai.js +13 -2
- package/lib/ai.js.map +1 -1
- package/lib/{fs-router-BkbIWqek.js → fs-router-3xzp-4Wj.js} +4 -2
- package/lib/fs-router-3xzp-4Wj.js.map +1 -0
- package/lib/fs-router-CQ7Zxeca.js +955 -0
- package/lib/fs-router-CQ7Zxeca.js.map +1 -0
- package/lib/seo.js +2 -2
- package/lib/server.js +4 -4
- package/lib/server.js.map +1 -1
- package/lib/types/index.d.ts +49 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/server.d.ts +55 -3
- package/lib/types/server.d.ts.map +1 -1
- package/package.json +19 -10
- package/src/fs-router.ts +1009 -50
- package/src/types.ts +50 -0
- package/src/vite-plugin.ts +11 -5
- package/lib/fs-router-BkbIWqek.js.map +0 -1
- package/lib/fs-router-Dil4IKZR.js +0 -290
- package/lib/fs-router-Dil4IKZR.js.map +0 -1
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region \0rolldown/runtime.js
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __exportAll = (all, no_symbols) => {
|
|
7
|
+
let target = {};
|
|
8
|
+
for (var name in all) {
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
if (!no_symbols) {
|
|
15
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
16
|
+
}
|
|
17
|
+
return target;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/fs-router.ts
|
|
22
|
+
var fs_router_exports = /* @__PURE__ */ __exportAll({
|
|
23
|
+
detectRouteExports: () => detectRouteExports,
|
|
24
|
+
filePathToUrlPath: () => filePathToUrlPath,
|
|
25
|
+
generateMiddlewareModule: () => generateMiddlewareModule,
|
|
26
|
+
generateRouteModule: () => generateRouteModule,
|
|
27
|
+
generateRouteModuleFromRoutes: () => generateRouteModuleFromRoutes,
|
|
28
|
+
hasAnyMetaExport: () => hasAnyMetaExport,
|
|
29
|
+
parseFileRoutes: () => parseFileRoutes,
|
|
30
|
+
scanRouteFiles: () => scanRouteFiles,
|
|
31
|
+
scanRouteFilesWithExports: () => scanRouteFilesWithExports,
|
|
32
|
+
stripTypeAssertions: () => stripTypeAssertions
|
|
33
|
+
});
|
|
34
|
+
const ROUTE_EXTENSIONS = [
|
|
35
|
+
".tsx",
|
|
36
|
+
".jsx",
|
|
37
|
+
".ts",
|
|
38
|
+
".js"
|
|
39
|
+
];
|
|
40
|
+
/** Names whose top-level export presence we care about. */
|
|
41
|
+
const ROUTE_EXPORT_NAMES = [
|
|
42
|
+
"loader",
|
|
43
|
+
"guard",
|
|
44
|
+
"meta",
|
|
45
|
+
"renderMode",
|
|
46
|
+
"error",
|
|
47
|
+
"middleware"
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Detect which optional metadata exports a route file source declares.
|
|
51
|
+
*
|
|
52
|
+
* Walks the source character-by-character, tracking string-literal and
|
|
53
|
+
* comment state, then collects top-level `export …` statements. This is
|
|
54
|
+
* more accurate than regex (no false matches inside string literals,
|
|
55
|
+
* template literals, or comments) and lighter than a full AST parse
|
|
56
|
+
* (no oxc/babel dependency, ~1µs per file).
|
|
57
|
+
*
|
|
58
|
+
* Recognizes:
|
|
59
|
+
* • `export const NAME = …`
|
|
60
|
+
* • `export let NAME = …`
|
|
61
|
+
* • `export var NAME = …`
|
|
62
|
+
* • `export function NAME(…)`
|
|
63
|
+
* • `export async function NAME(…)`
|
|
64
|
+
* • `export { NAME }` and `export { localName as NAME }`
|
|
65
|
+
* • `export { NAME } from '…'` (re-export)
|
|
66
|
+
*
|
|
67
|
+
* Names checked: loader, guard, meta, renderMode, error, middleware.
|
|
68
|
+
*/
|
|
69
|
+
function detectRouteExports(source) {
|
|
70
|
+
const found = /* @__PURE__ */ new Set();
|
|
71
|
+
const tokens = scanTopLevelExportTokens(source);
|
|
72
|
+
for (const tok of tokens) if (tok.kind === "declaration") {
|
|
73
|
+
if (ROUTE_EXPORT_NAMES.includes(tok.name)) found.add(tok.name);
|
|
74
|
+
} else for (const name of tok.names) if (ROUTE_EXPORT_NAMES.includes(name)) found.add(name);
|
|
75
|
+
const rawMeta = found.has("meta") ? extractLiteralExport(source, "meta") : void 0;
|
|
76
|
+
const rawRenderMode = found.has("renderMode") ? extractLiteralExport(source, "renderMode") : void 0;
|
|
77
|
+
const cleanMeta = rawMeta !== void 0 ? stripTypeAssertions(rawMeta) : void 0;
|
|
78
|
+
const cleanRenderMode = rawRenderMode !== void 0 ? stripTypeAssertions(rawRenderMode) : void 0;
|
|
79
|
+
const metaLiteral = cleanMeta !== void 0 && isPureLiteral(cleanMeta) ? cleanMeta : void 0;
|
|
80
|
+
const renderModeLiteral = cleanRenderMode !== void 0 && isPureLiteral(cleanRenderMode) ? cleanRenderMode : void 0;
|
|
81
|
+
return {
|
|
82
|
+
hasLoader: found.has("loader"),
|
|
83
|
+
hasGuard: found.has("guard"),
|
|
84
|
+
hasMeta: found.has("meta"),
|
|
85
|
+
hasRenderMode: found.has("renderMode"),
|
|
86
|
+
hasError: found.has("error"),
|
|
87
|
+
hasMiddleware: found.has("middleware"),
|
|
88
|
+
...metaLiteral !== void 0 ? { metaLiteral } : {},
|
|
89
|
+
...renderModeLiteral !== void 0 ? { renderModeLiteral } : {}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Extract the literal initializer of an `export const NAME = …` statement
|
|
94
|
+
* as a raw text slice — used by the route generator to inline `meta` and
|
|
95
|
+
* `renderMode` values into the generated routes module.
|
|
96
|
+
*
|
|
97
|
+
* Walks the source character-by-character respecting strings, template
|
|
98
|
+
* literals, comments, and brace/bracket/paren nesting. The slice runs
|
|
99
|
+
* from the first non-whitespace character after `=` to the matching
|
|
100
|
+
* end-of-expression terminator (`;`, newline at depth 0, or top-level
|
|
101
|
+
* `export`). Whatever the slice contains is handed to V8 verbatim by
|
|
102
|
+
* embedding it inside `{ … }` in the generated module — which means
|
|
103
|
+
* the original source must already be valid JavaScript (which it is,
|
|
104
|
+
* since the route file compiles).
|
|
105
|
+
*
|
|
106
|
+
* Returns `undefined` when extraction fails for any reason — the
|
|
107
|
+
* generator falls back to a static module import in that case.
|
|
108
|
+
*/
|
|
109
|
+
function extractLiteralExport(source, name) {
|
|
110
|
+
const len = source.length;
|
|
111
|
+
let i = 0;
|
|
112
|
+
let depth = 0;
|
|
113
|
+
const isIdCont = (c) => /[A-Za-z0-9_$]/.test(c);
|
|
114
|
+
const skipWs = (p) => {
|
|
115
|
+
while (p < len && /\s/.test(source[p])) p++;
|
|
116
|
+
return p;
|
|
117
|
+
};
|
|
118
|
+
while (i < len) {
|
|
119
|
+
const ch = source[i];
|
|
120
|
+
const next = source[i + 1] ?? "";
|
|
121
|
+
if (ch === "/" && next === "/") {
|
|
122
|
+
while (i < len && source[i] !== "\n") i++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (ch === "/" && next === "*") {
|
|
126
|
+
i += 2;
|
|
127
|
+
while (i < len - 1 && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
128
|
+
i += 2;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (ch === "\"" || ch === "'") {
|
|
132
|
+
const quote = ch;
|
|
133
|
+
i++;
|
|
134
|
+
while (i < len && source[i] !== quote) if (source[i] === "\\") i += 2;
|
|
135
|
+
else i++;
|
|
136
|
+
i++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ch === "`") {
|
|
140
|
+
i++;
|
|
141
|
+
while (i < len && source[i] !== "`") {
|
|
142
|
+
if (source[i] === "\\") {
|
|
143
|
+
i += 2;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (source[i] === "$" && source[i + 1] === "{") {
|
|
147
|
+
i += 2;
|
|
148
|
+
let exprDepth = 1;
|
|
149
|
+
while (i < len && exprDepth > 0) {
|
|
150
|
+
const c = source[i];
|
|
151
|
+
if (c === "{") exprDepth++;
|
|
152
|
+
else if (c === "}") exprDepth--;
|
|
153
|
+
if (exprDepth === 0) {
|
|
154
|
+
i++;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
i++;
|
|
162
|
+
}
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === "{") {
|
|
167
|
+
depth++;
|
|
168
|
+
i++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (ch === "}") {
|
|
172
|
+
depth--;
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (depth === 0 && ch === "e") {
|
|
177
|
+
if (source.slice(i, i + 6) === "export" && !isIdCont(source[i + 6] ?? "")) {
|
|
178
|
+
let p = skipWs(i + 6);
|
|
179
|
+
if (source.slice(p, p + 5) === "const" && !isIdCont(source[p + 5] ?? "")) {
|
|
180
|
+
p = skipWs(p + 5);
|
|
181
|
+
if (source.slice(p, p + name.length) === name && !isIdCont(source[p + name.length] ?? "")) {
|
|
182
|
+
p = skipWs(p + name.length);
|
|
183
|
+
if (source[p] === "=") {
|
|
184
|
+
p = skipWs(p + 1);
|
|
185
|
+
return readExpressionUntilEnd(source, p);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
i = i + 6;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Read a JavaScript expression starting at `start` and return the raw
|
|
198
|
+
* text up to (but not including) its end. The end is whichever comes
|
|
199
|
+
* first of:
|
|
200
|
+
* • a `;` at depth 0
|
|
201
|
+
* • a newline at depth 0 that is not inside a string/template
|
|
202
|
+
* • the next top-level `export` / `const` / `function` keyword
|
|
203
|
+
* • end of file
|
|
204
|
+
*
|
|
205
|
+
* Tracks `()`, `[]`, and `{}` nesting plus string/template/comment
|
|
206
|
+
* state so depth-0 boundaries are detected correctly even for nested
|
|
207
|
+
* objects, arrays, and tagged templates.
|
|
208
|
+
*/
|
|
209
|
+
function readExpressionUntilEnd(source, start) {
|
|
210
|
+
const len = source.length;
|
|
211
|
+
let i = start;
|
|
212
|
+
let depth = 0;
|
|
213
|
+
while (i < len) {
|
|
214
|
+
const ch = source[i];
|
|
215
|
+
const next = source[i + 1] ?? "";
|
|
216
|
+
if (depth === 0) {
|
|
217
|
+
if (ch === ";") return source.slice(start, i).trim() || void 0;
|
|
218
|
+
if (ch === "\n") {
|
|
219
|
+
const trimmed = source.slice(start, i).trim();
|
|
220
|
+
if (trimmed.length === 0) {
|
|
221
|
+
i++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
return trimmed;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (ch === "/" && next === "/") {
|
|
228
|
+
while (i < len && source[i] !== "\n") i++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (ch === "/" && next === "*") {
|
|
232
|
+
i += 2;
|
|
233
|
+
while (i < len - 1 && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
234
|
+
i += 2;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (ch === "\"" || ch === "'") {
|
|
238
|
+
const quote = ch;
|
|
239
|
+
i++;
|
|
240
|
+
while (i < len && source[i] !== quote) if (source[i] === "\\") i += 2;
|
|
241
|
+
else i++;
|
|
242
|
+
i++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (ch === "`") {
|
|
246
|
+
i++;
|
|
247
|
+
while (i < len && source[i] !== "`") {
|
|
248
|
+
if (source[i] === "\\") {
|
|
249
|
+
i += 2;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (source[i] === "$" && source[i + 1] === "{") {
|
|
253
|
+
i += 2;
|
|
254
|
+
let exprDepth = 1;
|
|
255
|
+
while (i < len && exprDepth > 0) {
|
|
256
|
+
const c = source[i];
|
|
257
|
+
if (c === "{") exprDepth++;
|
|
258
|
+
else if (c === "}") exprDepth--;
|
|
259
|
+
if (exprDepth === 0) {
|
|
260
|
+
i++;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
i++;
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
i++;
|
|
268
|
+
}
|
|
269
|
+
i++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (ch === "{" || ch === "[" || ch === "(") {
|
|
273
|
+
depth++;
|
|
274
|
+
i++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (ch === "}" || ch === "]" || ch === ")") {
|
|
278
|
+
depth--;
|
|
279
|
+
if (depth < 0) return source.slice(start, i).trim() || void 0;
|
|
280
|
+
i++;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
i++;
|
|
284
|
+
}
|
|
285
|
+
const trimmed = source.slice(start).trim();
|
|
286
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* True if `text` is a pure JS literal — only string/number/boolean/null
|
|
290
|
+
* literals plus the structural punctuation needed to compose them into
|
|
291
|
+
* objects, arrays, and tuples. Identifiers, operators, function calls,
|
|
292
|
+
* template-literal expression slots, and references to other names all
|
|
293
|
+
* disqualify the expression.
|
|
294
|
+
*
|
|
295
|
+
* Walks the source character-by-character, tracking string/template/
|
|
296
|
+
* comment state. Inside a string or template head (no `${}` slot) every
|
|
297
|
+
* character is fine; outside strings, only the structural symbols
|
|
298
|
+
* `{}[](),:` plus whitespace, digits, the literal keywords `true`,
|
|
299
|
+
* `false`, `null`, and `-` (for negative numbers) are allowed.
|
|
300
|
+
*
|
|
301
|
+
* The check is conservative on purpose — anything fancier than a flat
|
|
302
|
+
* literal falls back to the static-import path, which still works,
|
|
303
|
+
* just at the cost of one un-split chunk.
|
|
304
|
+
*/
|
|
305
|
+
function isPureLiteral(text) {
|
|
306
|
+
const len = text.length;
|
|
307
|
+
let i = 0;
|
|
308
|
+
while (i < len) {
|
|
309
|
+
const ch = text[i];
|
|
310
|
+
if (ch === "\"" || ch === "'") {
|
|
311
|
+
const quote = ch;
|
|
312
|
+
i++;
|
|
313
|
+
while (i < len && text[i] !== quote) if (text[i] === "\\") i += 2;
|
|
314
|
+
else i++;
|
|
315
|
+
i++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (ch === "`") {
|
|
319
|
+
i++;
|
|
320
|
+
while (i < len && text[i] !== "`") {
|
|
321
|
+
if (text[i] === "\\") {
|
|
322
|
+
i += 2;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (text[i] === "$" && text[i + 1] === "{") return false;
|
|
326
|
+
i++;
|
|
327
|
+
}
|
|
328
|
+
i++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (/\s/.test(ch)) {
|
|
332
|
+
i++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (ch === "{" || ch === "}" || ch === "[" || ch === "]" || ch === "," || ch === ":") {
|
|
336
|
+
i++;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (/[0-9]/.test(ch) || ch === "-" && /[0-9]/.test(text[i + 1] ?? "")) {
|
|
340
|
+
while (i < len && /[0-9a-fA-Fxob.eE+\-_]/.test(text[i])) i++;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (text.slice(i, i + 4) === "true" && !isIdContChar(text[i + 4] ?? "")) {
|
|
344
|
+
i += 4;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (text.slice(i, i + 5) === "false" && !isIdContChar(text[i + 5] ?? "")) {
|
|
348
|
+
i += 5;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (text.slice(i, i + 4) === "null" && !isIdContChar(text[i + 4] ?? "")) {
|
|
352
|
+
i += 4;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (text.slice(i, i + 9) === "undefined" && !isIdContChar(text[i + 9] ?? "")) {
|
|
356
|
+
i += 9;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (/[A-Za-z_$]/.test(ch)) {
|
|
360
|
+
let end = i + 1;
|
|
361
|
+
while (end < len && isIdContChar(text[end])) end++;
|
|
362
|
+
let after = end;
|
|
363
|
+
while (after < len && /\s/.test(text[after])) after++;
|
|
364
|
+
if (text[after] === ":") {
|
|
365
|
+
i = end;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
function isIdContChar(c) {
|
|
375
|
+
return /[A-Za-z0-9_$]/.test(c);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Strip TypeScript type-only suffixes (`as const`, `as SomeType`,
|
|
379
|
+
* `satisfies SomeType`) from a literal expression so the generated
|
|
380
|
+
* JS module is syntactically valid.
|
|
381
|
+
*
|
|
382
|
+
* The route file is TypeScript so authors freely write
|
|
383
|
+
* `export const renderMode = 'ssg' as const` — but the generated
|
|
384
|
+
* `virtual:zero/routes` module is JavaScript and can't keep the cast.
|
|
385
|
+
* Strip from the rightmost top-level `as` or `satisfies` keyword.
|
|
386
|
+
*/
|
|
387
|
+
function stripTypeAssertions(literal) {
|
|
388
|
+
let result = literal.trim();
|
|
389
|
+
let depth = 0;
|
|
390
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
391
|
+
const ch = result[i];
|
|
392
|
+
if (ch === ")" || ch === "]" || ch === "}") depth++;
|
|
393
|
+
else if (ch === "(" || ch === "[" || ch === "{") depth--;
|
|
394
|
+
if (depth !== 0) continue;
|
|
395
|
+
if (i >= 4 && result[i - 3] === " " && result[i - 2] === "a" && result[i - 1] === "s" && result[i] === " ") {
|
|
396
|
+
result = result.slice(0, i - 3).trim();
|
|
397
|
+
i = result.length;
|
|
398
|
+
depth = 0;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (i >= 11 && result.slice(i - 10, i + 1) === " satisfies ") {
|
|
402
|
+
result = result.slice(0, i - 10).trim();
|
|
403
|
+
i = result.length;
|
|
404
|
+
depth = 0;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
function scanTopLevelExportTokens(source) {
|
|
411
|
+
const tokens = [];
|
|
412
|
+
const len = source.length;
|
|
413
|
+
let i = 0;
|
|
414
|
+
let depth = 0;
|
|
415
|
+
const isIdStart = (c) => /[A-Za-z_$]/.test(c);
|
|
416
|
+
const isIdCont = (c) => /[A-Za-z0-9_$]/.test(c);
|
|
417
|
+
const readIdentifier = (p) => {
|
|
418
|
+
if (p >= len || !isIdStart(source[p])) return null;
|
|
419
|
+
let end = p + 1;
|
|
420
|
+
while (end < len && isIdCont(source[end])) end++;
|
|
421
|
+
return [source.slice(p, end), end];
|
|
422
|
+
};
|
|
423
|
+
const skipWs = (p) => {
|
|
424
|
+
while (p < len && /\s/.test(source[p])) p++;
|
|
425
|
+
return p;
|
|
426
|
+
};
|
|
427
|
+
const matchKeyword = (p, keyword) => {
|
|
428
|
+
if (source.slice(p, p + keyword.length) !== keyword) return -1;
|
|
429
|
+
const after = p + keyword.length;
|
|
430
|
+
if (after < len && isIdCont(source[after])) return -1;
|
|
431
|
+
if (p > 0 && isIdCont(source[p - 1])) return -1;
|
|
432
|
+
return after;
|
|
433
|
+
};
|
|
434
|
+
while (i < len) {
|
|
435
|
+
const ch = source[i];
|
|
436
|
+
const next = source[i + 1] ?? "";
|
|
437
|
+
if (ch === "/" && next === "/") {
|
|
438
|
+
while (i < len && source[i] !== "\n") i++;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (ch === "/" && next === "*") {
|
|
442
|
+
i += 2;
|
|
443
|
+
while (i < len - 1 && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
444
|
+
i += 2;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (ch === "\"" || ch === "'") {
|
|
448
|
+
const quote = ch;
|
|
449
|
+
i++;
|
|
450
|
+
while (i < len && source[i] !== quote) if (source[i] === "\\") i += 2;
|
|
451
|
+
else i++;
|
|
452
|
+
i++;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (ch === "`") {
|
|
456
|
+
i++;
|
|
457
|
+
while (i < len && source[i] !== "`") {
|
|
458
|
+
if (source[i] === "\\") {
|
|
459
|
+
i += 2;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (source[i] === "$" && source[i + 1] === "{") {
|
|
463
|
+
i += 2;
|
|
464
|
+
let exprDepth = 1;
|
|
465
|
+
while (i < len && exprDepth > 0) {
|
|
466
|
+
const c = source[i];
|
|
467
|
+
if (c === "{") exprDepth++;
|
|
468
|
+
else if (c === "}") exprDepth--;
|
|
469
|
+
if (exprDepth === 0) {
|
|
470
|
+
i++;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
i++;
|
|
474
|
+
}
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
i++;
|
|
478
|
+
}
|
|
479
|
+
i++;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (ch === "{") {
|
|
483
|
+
depth++;
|
|
484
|
+
i++;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (ch === "}") {
|
|
488
|
+
depth--;
|
|
489
|
+
i++;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (depth === 0 && ch === "e") {
|
|
493
|
+
const afterExport = matchKeyword(i, "export");
|
|
494
|
+
if (afterExport > 0) {
|
|
495
|
+
let p = skipWs(afterExport);
|
|
496
|
+
const afterDefault = matchKeyword(p, "default");
|
|
497
|
+
if (afterDefault > 0) {
|
|
498
|
+
i = afterDefault;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (source[p] === "{") {
|
|
502
|
+
p++;
|
|
503
|
+
const names = [];
|
|
504
|
+
while (p < len && source[p] !== "}") {
|
|
505
|
+
p = skipWs(p);
|
|
506
|
+
if (source[p] === "}") break;
|
|
507
|
+
const id = readIdentifier(p);
|
|
508
|
+
if (!id) {
|
|
509
|
+
p++;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const [first, afterFirst] = id;
|
|
513
|
+
let exportedName = first;
|
|
514
|
+
const afterAs = matchKeyword(skipWs(afterFirst), "as");
|
|
515
|
+
if (afterAs > 0) {
|
|
516
|
+
const alias = readIdentifier(skipWs(afterAs));
|
|
517
|
+
if (alias) {
|
|
518
|
+
exportedName = alias[0];
|
|
519
|
+
p = alias[1];
|
|
520
|
+
} else p = afterFirst;
|
|
521
|
+
} else p = afterFirst;
|
|
522
|
+
names.push(exportedName);
|
|
523
|
+
p = skipWs(p);
|
|
524
|
+
if (source[p] === ",") p++;
|
|
525
|
+
}
|
|
526
|
+
tokens.push({
|
|
527
|
+
kind: "list",
|
|
528
|
+
names
|
|
529
|
+
});
|
|
530
|
+
i = p + 1;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const afterAsync = matchKeyword(p, "async");
|
|
534
|
+
if (afterAsync > 0) p = skipWs(afterAsync);
|
|
535
|
+
let foundDecl = false;
|
|
536
|
+
for (const kw of [
|
|
537
|
+
"const",
|
|
538
|
+
"let",
|
|
539
|
+
"var",
|
|
540
|
+
"function"
|
|
541
|
+
]) {
|
|
542
|
+
const afterKw = matchKeyword(p, kw);
|
|
543
|
+
if (afterKw > 0) {
|
|
544
|
+
const id = readIdentifier(skipWs(afterKw));
|
|
545
|
+
if (id) {
|
|
546
|
+
tokens.push({
|
|
547
|
+
kind: "declaration",
|
|
548
|
+
name: id[0]
|
|
549
|
+
});
|
|
550
|
+
i = id[1];
|
|
551
|
+
foundDecl = true;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (!foundDecl) i = afterExport;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
i++;
|
|
561
|
+
}
|
|
562
|
+
return tokens;
|
|
563
|
+
}
|
|
564
|
+
/** All-false exports record. Used when source detection fails. */
|
|
565
|
+
const EMPTY_EXPORTS = {
|
|
566
|
+
hasLoader: false,
|
|
567
|
+
hasGuard: false,
|
|
568
|
+
hasMeta: false,
|
|
569
|
+
hasRenderMode: false,
|
|
570
|
+
hasError: false,
|
|
571
|
+
hasMiddleware: false
|
|
572
|
+
};
|
|
573
|
+
/**
|
|
574
|
+
* True if a route file declares ANY metadata export.
|
|
575
|
+
* Used by the code generator to decide whether to emit a static
|
|
576
|
+
* `import * as mod` (for metadata access) instead of lazy().
|
|
577
|
+
*/
|
|
578
|
+
function hasAnyMetaExport(exports) {
|
|
579
|
+
return exports.hasLoader || exports.hasGuard || exports.hasMeta || exports.hasRenderMode || exports.hasError || exports.hasMiddleware;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Parse a set of file paths (relative to routes dir) into FileRoute objects.
|
|
583
|
+
*
|
|
584
|
+
* @param files Array of file paths like ["index.tsx", "users/[id].tsx"]
|
|
585
|
+
* @param defaultMode Default rendering mode from config
|
|
586
|
+
* @param exportsMap Optional map of filePath → detected exports. When
|
|
587
|
+
* provided, the resulting FileRoute objects carry export info that the
|
|
588
|
+
* code generator uses to optimize imports (skip metadata namespace
|
|
589
|
+
* imports for routes that only export `default`).
|
|
590
|
+
*/
|
|
591
|
+
function parseFileRoutes(files, defaultMode = "ssr", exportsMap) {
|
|
592
|
+
return files.filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext))).map((filePath) => {
|
|
593
|
+
const route = parseFilePath(filePath, defaultMode);
|
|
594
|
+
const exp = exportsMap?.get(filePath);
|
|
595
|
+
return exp ? {
|
|
596
|
+
...route,
|
|
597
|
+
exports: exp
|
|
598
|
+
} : route;
|
|
599
|
+
}).sort(sortRoutes);
|
|
600
|
+
}
|
|
601
|
+
function parseFilePath(filePath, defaultMode) {
|
|
602
|
+
let route = filePath;
|
|
603
|
+
for (const ext of ROUTE_EXTENSIONS) if (route.endsWith(ext)) {
|
|
604
|
+
route = route.slice(0, -ext.length);
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
const fileName = getFileName(route);
|
|
608
|
+
const isLayout = fileName === "_layout";
|
|
609
|
+
const isError = fileName === "_error";
|
|
610
|
+
const isLoading = fileName === "_loading";
|
|
611
|
+
const isNotFound = fileName === "_404" || fileName === "_not-found";
|
|
612
|
+
const isCatchAll = route.includes("[...");
|
|
613
|
+
const parts = route.split("/");
|
|
614
|
+
parts.pop();
|
|
615
|
+
const dirPath = parts.filter((s) => !(s.startsWith("(") && s.endsWith(")"))).join("/");
|
|
616
|
+
const urlPath = filePathToUrlPath(route);
|
|
617
|
+
return {
|
|
618
|
+
filePath,
|
|
619
|
+
urlPath,
|
|
620
|
+
dirPath,
|
|
621
|
+
depth: urlPath === "/" ? 0 : urlPath.split("/").filter(Boolean).length,
|
|
622
|
+
isLayout,
|
|
623
|
+
isError,
|
|
624
|
+
isLoading,
|
|
625
|
+
isNotFound,
|
|
626
|
+
isCatchAll,
|
|
627
|
+
renderMode: defaultMode
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Convert a file path (without extension) to a URL path pattern.
|
|
632
|
+
*
|
|
633
|
+
* Examples:
|
|
634
|
+
* "index" → "/"
|
|
635
|
+
* "about" → "/about"
|
|
636
|
+
* "users/index" → "/users"
|
|
637
|
+
* "users/[id]" → "/users/:id"
|
|
638
|
+
* "blog/[...slug]" → "/blog/:slug*"
|
|
639
|
+
* "(auth)/login" → "/login" (group stripped)
|
|
640
|
+
* "_layout" → "/" (layout marker)
|
|
641
|
+
*/
|
|
642
|
+
function filePathToUrlPath(filePath) {
|
|
643
|
+
const segments = filePath.split("/");
|
|
644
|
+
const urlSegments = [];
|
|
645
|
+
for (const seg of segments) {
|
|
646
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue;
|
|
647
|
+
if (seg === "_layout" || seg === "_error" || seg === "_loading" || seg === "_404" || seg === "_not-found") continue;
|
|
648
|
+
if (seg === "index") continue;
|
|
649
|
+
const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/);
|
|
650
|
+
if (catchAll) {
|
|
651
|
+
urlSegments.push(`:${catchAll[1]}*`);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const dynamic = seg.match(/^\[(\w+)\]$/);
|
|
655
|
+
if (dynamic) {
|
|
656
|
+
urlSegments.push(`:${dynamic[1]}`);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
urlSegments.push(seg);
|
|
660
|
+
}
|
|
661
|
+
return `/${urlSegments.join("/")}` || "/";
|
|
662
|
+
}
|
|
663
|
+
/** Sort routes: static before dynamic, catch-all last. */
|
|
664
|
+
function sortRoutes(a, b) {
|
|
665
|
+
if (a.isCatchAll !== b.isCatchAll) return a.isCatchAll ? 1 : -1;
|
|
666
|
+
if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1;
|
|
667
|
+
const aDynamic = a.urlPath.includes(":");
|
|
668
|
+
if (aDynamic !== b.urlPath.includes(":")) return aDynamic ? 1 : -1;
|
|
669
|
+
return a.urlPath.localeCompare(b.urlPath);
|
|
670
|
+
}
|
|
671
|
+
function getFileName(filePath) {
|
|
672
|
+
const parts = filePath.split("/");
|
|
673
|
+
return parts[parts.length - 1] ?? "";
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Group flat file routes into a directory tree.
|
|
677
|
+
*/
|
|
678
|
+
function getOrCreateChild(node, segment) {
|
|
679
|
+
let child = node.children.get(segment);
|
|
680
|
+
if (!child) {
|
|
681
|
+
child = {
|
|
682
|
+
pages: [],
|
|
683
|
+
children: /* @__PURE__ */ new Map()
|
|
684
|
+
};
|
|
685
|
+
node.children.set(segment, child);
|
|
686
|
+
}
|
|
687
|
+
return child;
|
|
688
|
+
}
|
|
689
|
+
function resolveNode(root, dirPath) {
|
|
690
|
+
let node = root;
|
|
691
|
+
if (dirPath) for (const segment of dirPath.split("/")) node = getOrCreateChild(node, segment);
|
|
692
|
+
return node;
|
|
693
|
+
}
|
|
694
|
+
function placeRoute(node, route) {
|
|
695
|
+
if (route.isLayout) node.layout = route;
|
|
696
|
+
else if (route.isError) node.error = route;
|
|
697
|
+
else if (route.isLoading) node.loading = route;
|
|
698
|
+
else if (route.isNotFound) node.notFound = route;
|
|
699
|
+
else node.pages.push(route);
|
|
700
|
+
}
|
|
701
|
+
function buildRouteTree(routes) {
|
|
702
|
+
const root = {
|
|
703
|
+
pages: [],
|
|
704
|
+
children: /* @__PURE__ */ new Map()
|
|
705
|
+
};
|
|
706
|
+
for (const route of routes) placeRoute(resolveNode(root, route.dirPath), route);
|
|
707
|
+
return root;
|
|
708
|
+
}
|
|
709
|
+
function generateRouteModule(files, routesDir, options) {
|
|
710
|
+
const exportsMap = /* @__PURE__ */ new Map();
|
|
711
|
+
for (const filePath of files) {
|
|
712
|
+
if (!ROUTE_EXTENSIONS.some((ext) => filePath.endsWith(ext))) continue;
|
|
713
|
+
try {
|
|
714
|
+
const source = readFileSync(join(routesDir, filePath), "utf-8");
|
|
715
|
+
exportsMap.set(filePath, detectRouteExports(source));
|
|
716
|
+
} catch {
|
|
717
|
+
exportsMap.set(filePath, EMPTY_EXPORTS);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return generateRouteModuleFromRoutes(parseFileRoutes(files, void 0, exportsMap), routesDir, options);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Lower-level entry point that accepts pre-parsed FileRoute[] (so callers
|
|
724
|
+
* can attach `.exports` info from source detection). Use this when you've
|
|
725
|
+
* already read the files and want optimal output.
|
|
726
|
+
*/
|
|
727
|
+
function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
728
|
+
const tree = buildRouteTree(routes);
|
|
729
|
+
const imports = [];
|
|
730
|
+
let importCounter = 0;
|
|
731
|
+
const useStaticOnly = options?.staticImports ?? false;
|
|
732
|
+
let needsLazyImport = false;
|
|
733
|
+
function nextImport(filePath, exportName = "default") {
|
|
734
|
+
const name = `_${importCounter++}`;
|
|
735
|
+
const fullPath = `${routesDir}/${filePath}`;
|
|
736
|
+
if (exportName === "default") imports.push(`import ${name} from "${fullPath}"`);
|
|
737
|
+
else imports.push(`import { ${exportName} as ${name} } from "${fullPath}"`);
|
|
738
|
+
return name;
|
|
739
|
+
}
|
|
740
|
+
function nextModuleImport(filePath) {
|
|
741
|
+
const name = `_m${importCounter++}`;
|
|
742
|
+
const fullPath = `${routesDir}/${filePath}`;
|
|
743
|
+
imports.push(`import * as ${name} from "${fullPath}"`);
|
|
744
|
+
return name;
|
|
745
|
+
}
|
|
746
|
+
function nextLazy(filePath, loadingName, errorName) {
|
|
747
|
+
const name = `_${importCounter++}`;
|
|
748
|
+
const fullPath = `${routesDir}/${filePath}`;
|
|
749
|
+
needsLazyImport = true;
|
|
750
|
+
const opts = [];
|
|
751
|
+
if (loadingName) opts.push(`loading: ${loadingName}`);
|
|
752
|
+
if (errorName) opts.push(`error: ${errorName}`);
|
|
753
|
+
const optsStr = opts.length > 0 ? `, { ${opts.join(", ")} }` : "";
|
|
754
|
+
imports.push(`const ${name} = lazy(() => import("${fullPath}")${optsStr})`);
|
|
755
|
+
return name;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Emit a `meta: { ... }` prop using the literal initializers captured
|
|
759
|
+
* from the route file source. Either or both of `metaLiteral` and
|
|
760
|
+
* `renderModeLiteral` may be present; the result is always a single
|
|
761
|
+
* inline object literal.
|
|
762
|
+
*/
|
|
763
|
+
function emitInlineMeta(exp, props, indent) {
|
|
764
|
+
if (!exp.hasMeta && !exp.hasRenderMode) return;
|
|
765
|
+
const parts = [];
|
|
766
|
+
if (exp.hasMeta && exp.metaLiteral !== void 0) parts.push(`...(${exp.metaLiteral})`);
|
|
767
|
+
if (exp.hasRenderMode && exp.renderModeLiteral !== void 0) parts.push(`renderMode: ${exp.renderModeLiteral}`);
|
|
768
|
+
if (parts.length > 0) props.push(`${indent} meta: { ${parts.join(", ")} }`);
|
|
769
|
+
}
|
|
770
|
+
function generatePageRoute(page, indent, loadingName, errorName, notFoundName) {
|
|
771
|
+
const exp = page.exports ?? EMPTY_EXPORTS;
|
|
772
|
+
const props = [`${indent} path: ${JSON.stringify(page.urlPath)}`];
|
|
773
|
+
const hasMeta = hasAnyMetaExport(exp);
|
|
774
|
+
if (useStaticOnly) if (hasMeta) {
|
|
775
|
+
const mod = nextModuleImport(page.filePath);
|
|
776
|
+
props.push(`${indent} component: ${mod}.default`);
|
|
777
|
+
if (exp.hasLoader) props.push(`${indent} loader: ${mod}.loader`);
|
|
778
|
+
if (exp.hasGuard) props.push(`${indent} beforeEnter: ${mod}.guard`);
|
|
779
|
+
if (exp.hasMeta || exp.hasRenderMode) {
|
|
780
|
+
const metaParts = [];
|
|
781
|
+
if (exp.hasMeta) metaParts.push(`...${mod}.meta`);
|
|
782
|
+
if (exp.hasRenderMode) metaParts.push(`renderMode: ${mod}.renderMode`);
|
|
783
|
+
props.push(`${indent} meta: { ${metaParts.join(", ")} }`);
|
|
784
|
+
}
|
|
785
|
+
if (errorName) {
|
|
786
|
+
const errorRef = exp.hasError ? `${mod}.error || ${errorName}` : errorName;
|
|
787
|
+
props.push(`${indent} errorComponent: ${errorRef}`);
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
const comp = nextImport(page.filePath, "default");
|
|
791
|
+
props.push(`${indent} component: ${comp}`);
|
|
792
|
+
if (errorName) props.push(`${indent} errorComponent: ${errorName}`);
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
const inlineableMeta = (!exp.hasMeta || exp.metaLiteral !== void 0) && (!exp.hasRenderMode || exp.renderModeLiteral !== void 0);
|
|
796
|
+
const needsFunctionExports = exp.hasLoader || exp.hasGuard || exp.hasError;
|
|
797
|
+
if (hasMeta && inlineableMeta && !needsFunctionExports) {
|
|
798
|
+
const comp = nextLazy(page.filePath, loadingName, errorName);
|
|
799
|
+
props.push(`${indent} component: ${comp}`);
|
|
800
|
+
emitInlineMeta(exp, props, indent);
|
|
801
|
+
if (errorName) props.push(`${indent} errorComponent: ${errorName}`);
|
|
802
|
+
} else if (hasMeta && inlineableMeta) {
|
|
803
|
+
const comp = nextLazy(page.filePath, loadingName, errorName);
|
|
804
|
+
const fullPath = `${routesDir}/${page.filePath}`;
|
|
805
|
+
props.push(`${indent} component: ${comp}`);
|
|
806
|
+
if (exp.hasLoader) props.push(`${indent} loader: (ctx) => import("${fullPath}").then((m) => m.loader(ctx))`);
|
|
807
|
+
if (exp.hasGuard) props.push(`${indent} beforeEnter: (to, from) => import("${fullPath}").then((m) => m.guard(to, from))`);
|
|
808
|
+
emitInlineMeta(exp, props, indent);
|
|
809
|
+
if (errorName) {
|
|
810
|
+
const errorRef = exp.hasError ? `lazy(() => import("${fullPath}").then((m) => ({ default: m.error })))` : errorName;
|
|
811
|
+
if (exp.hasError) needsLazyImport = true;
|
|
812
|
+
props.push(`${indent} errorComponent: ${errorRef}`);
|
|
813
|
+
}
|
|
814
|
+
} else if (hasMeta) {
|
|
815
|
+
const mod = nextModuleImport(page.filePath);
|
|
816
|
+
props.push(`${indent} component: ${mod}.default`);
|
|
817
|
+
if (exp.hasLoader) props.push(`${indent} loader: ${mod}.loader`);
|
|
818
|
+
if (exp.hasGuard) props.push(`${indent} beforeEnter: ${mod}.guard`);
|
|
819
|
+
if (exp.hasMeta || exp.hasRenderMode) {
|
|
820
|
+
const metaParts = [];
|
|
821
|
+
if (exp.hasMeta) metaParts.push(`...${mod}.meta`);
|
|
822
|
+
if (exp.hasRenderMode) metaParts.push(`renderMode: ${mod}.renderMode`);
|
|
823
|
+
props.push(`${indent} meta: { ${metaParts.join(", ")} }`);
|
|
824
|
+
}
|
|
825
|
+
if (errorName) {
|
|
826
|
+
const errorRef = exp.hasError ? `${mod}.error || ${errorName}` : errorName;
|
|
827
|
+
props.push(`${indent} errorComponent: ${errorRef}`);
|
|
828
|
+
}
|
|
829
|
+
} else {
|
|
830
|
+
const comp = nextLazy(page.filePath, loadingName, errorName);
|
|
831
|
+
props.push(`${indent} component: ${comp}`);
|
|
832
|
+
if (errorName) props.push(`${indent} errorComponent: ${errorName}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (notFoundName) props.push(`${indent} notFoundComponent: ${notFoundName}`);
|
|
836
|
+
return `${indent}{\n${props.join(",\n")}\n${indent}}`;
|
|
837
|
+
}
|
|
838
|
+
function wrapWithLayout(node, children, indent, errorName, notFoundName) {
|
|
839
|
+
const layout = node.layout;
|
|
840
|
+
const exp = layout.exports ?? EMPTY_EXPORTS;
|
|
841
|
+
const hasMeta = hasAnyMetaExport(exp);
|
|
842
|
+
let layoutComp;
|
|
843
|
+
let layoutMod;
|
|
844
|
+
if (hasMeta) {
|
|
845
|
+
layoutMod = nextModuleImport(layout.filePath);
|
|
846
|
+
layoutComp = `${layoutMod}.layout`;
|
|
847
|
+
} else layoutComp = nextImport(layout.filePath, "layout");
|
|
848
|
+
const props = [`${indent}path: ${JSON.stringify(layout.urlPath)}`, `${indent}component: ${layoutComp}`];
|
|
849
|
+
if (layoutMod !== void 0) {
|
|
850
|
+
if (exp.hasLoader) props.push(`${indent}loader: ${layoutMod}.loader`);
|
|
851
|
+
if (exp.hasGuard) props.push(`${indent}beforeEnter: ${layoutMod}.guard`);
|
|
852
|
+
if (exp.hasMeta || exp.hasRenderMode) {
|
|
853
|
+
const metaParts = [];
|
|
854
|
+
if (exp.hasMeta) metaParts.push(`...${layoutMod}.meta`);
|
|
855
|
+
if (exp.hasRenderMode) metaParts.push(`renderMode: ${layoutMod}.renderMode`);
|
|
856
|
+
props.push(`${indent}meta: { ${metaParts.join(", ")} }`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (errorName) props.push(`${indent}errorComponent: ${errorName}`);
|
|
860
|
+
if (notFoundName) props.push(`${indent}notFoundComponent: ${notFoundName}`);
|
|
861
|
+
if (children.length > 0) props.push(`${indent}children: [\n${children.join(",\n")}\n${indent}]`);
|
|
862
|
+
return `${indent}{\n${props.map((p) => ` ${p}`).join(",\n")}\n${indent}}`;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Generate route definitions for a tree node.
|
|
866
|
+
*/
|
|
867
|
+
function generateNode(node, depth) {
|
|
868
|
+
const indent = " ".repeat(depth + 1);
|
|
869
|
+
const errorName = node.error ? nextImport(node.error.filePath) : void 0;
|
|
870
|
+
const loadingName = node.loading ? nextImport(node.loading.filePath) : void 0;
|
|
871
|
+
const notFoundName = node.notFound ? nextImport(node.notFound.filePath) : void 0;
|
|
872
|
+
const childRouteDefs = [];
|
|
873
|
+
for (const [, childNode] of node.children) childRouteDefs.push(...generateNode(childNode, depth + 1));
|
|
874
|
+
const allChildren = [...node.pages.map((page) => generatePageRoute(page, indent, loadingName, errorName, notFoundName)), ...childRouteDefs];
|
|
875
|
+
if (node.layout) return [wrapWithLayout(node, allChildren, indent, errorName, notFoundName)];
|
|
876
|
+
return allChildren;
|
|
877
|
+
}
|
|
878
|
+
const routeDefs = generateNode(tree, 0);
|
|
879
|
+
const lines = [];
|
|
880
|
+
if (needsLazyImport) lines.push(`import { lazy } from "@pyreon/router"`, "");
|
|
881
|
+
lines.push(...imports, "");
|
|
882
|
+
lines.push(`function clean(routes) {`, ` return routes.map(r => {`, ` const c = {}`, ` for (const k in r) if (r[k] !== undefined) c[k] = r[k]`, ` if (c.children) c.children = clean(c.children)`, ` return c`, ` })`, `}`, "", `export const routes = clean([`, routeDefs.join(",\n"), `])`);
|
|
883
|
+
return lines.join("\n");
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Generate a virtual module that maps URL patterns to their middleware exports.
|
|
887
|
+
* Used by the server entry to dispatch per-route middleware.
|
|
888
|
+
*/
|
|
889
|
+
function generateMiddlewareModule(files, routesDir) {
|
|
890
|
+
const routes = parseFileRoutes(files);
|
|
891
|
+
const imports = [];
|
|
892
|
+
const entries = [];
|
|
893
|
+
let counter = 0;
|
|
894
|
+
for (const route of routes) {
|
|
895
|
+
if (route.isLayout || route.isError || route.isLoading || route.isNotFound) continue;
|
|
896
|
+
const name = `_mw${counter++}`;
|
|
897
|
+
const fullPath = `${routesDir}/${route.filePath}`;
|
|
898
|
+
imports.push(`import { middleware as ${name} } from "${fullPath}"`);
|
|
899
|
+
entries.push(` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`);
|
|
900
|
+
}
|
|
901
|
+
return [
|
|
902
|
+
...imports,
|
|
903
|
+
"",
|
|
904
|
+
`export const routeMiddleware = [`,
|
|
905
|
+
entries.join(",\n"),
|
|
906
|
+
`].filter(e => e.middleware)`
|
|
907
|
+
].join("\n");
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Scan a directory for route files.
|
|
911
|
+
* Returns paths relative to the routes directory.
|
|
912
|
+
*/
|
|
913
|
+
async function scanRouteFiles(routesDir) {
|
|
914
|
+
const { readdir } = await import("node:fs/promises");
|
|
915
|
+
const { relative } = await import("node:path");
|
|
916
|
+
const files = [];
|
|
917
|
+
async function walk(dir) {
|
|
918
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
919
|
+
for (const entry of entries) {
|
|
920
|
+
const fullPath = join(dir, entry.name);
|
|
921
|
+
if (entry.isDirectory()) await walk(fullPath);
|
|
922
|
+
else if (ROUTE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) files.push(relative(routesDir, fullPath));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
await walk(routesDir);
|
|
926
|
+
return files;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Scan route files AND read each one to detect optional metadata exports
|
|
930
|
+
* (loader, guard, meta, renderMode, error, middleware).
|
|
931
|
+
*
|
|
932
|
+
* Returns FileRoute[] with `.exports` populated, ready to feed into
|
|
933
|
+
* `generateRouteModuleFromRoutes()` for optimal output:
|
|
934
|
+
* • lazy() for components without metadata (best code splitting)
|
|
935
|
+
* • Direct property access for components with metadata (no _pick)
|
|
936
|
+
* • No spurious IMPORT_IS_UNDEFINED warnings
|
|
937
|
+
*/
|
|
938
|
+
async function scanRouteFilesWithExports(routesDir, defaultMode = "ssr") {
|
|
939
|
+
const { readFile } = await import("node:fs/promises");
|
|
940
|
+
const files = await scanRouteFiles(routesDir);
|
|
941
|
+
const exportsMap = /* @__PURE__ */ new Map();
|
|
942
|
+
await Promise.all(files.map(async (filePath) => {
|
|
943
|
+
try {
|
|
944
|
+
const source = await readFile(join(routesDir, filePath), "utf-8");
|
|
945
|
+
exportsMap.set(filePath, detectRouteExports(source));
|
|
946
|
+
} catch {
|
|
947
|
+
exportsMap.set(filePath, EMPTY_EXPORTS);
|
|
948
|
+
}
|
|
949
|
+
}));
|
|
950
|
+
return parseFileRoutes(files, defaultMode, exportsMap);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
//#endregion
|
|
954
|
+
export { generateRouteModuleFromRoutes as a, scanRouteFilesWithExports as c, generateRouteModule as i, fs_router_exports as n, parseFileRoutes as o, generateMiddlewareModule as r, scanRouteFiles as s, filePathToUrlPath as t };
|
|
955
|
+
//# sourceMappingURL=fs-router-CQ7Zxeca.js.map
|