@pyreon/zero 0.12.10 → 0.12.12

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.
@@ -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