@teyik0/furin 0.1.0-alpha.3

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.
Files changed (71) hide show
  1. package/dist/adapter/bun.d.ts +3 -0
  2. package/dist/build/client.d.ts +14 -0
  3. package/dist/build/compile-entry.d.ts +22 -0
  4. package/dist/build/entry-template.d.ts +13 -0
  5. package/dist/build/hydrate.d.ts +20 -0
  6. package/dist/build/index.d.ts +7 -0
  7. package/dist/build/index.js +2212 -0
  8. package/dist/build/route-types.d.ts +20 -0
  9. package/dist/build/scan-server.d.ts +8 -0
  10. package/dist/build/server-routes-entry.d.ts +22 -0
  11. package/dist/build/shared.d.ts +12 -0
  12. package/dist/build/types.d.ts +53 -0
  13. package/dist/cli/config.d.ts +9 -0
  14. package/dist/cli/index.d.ts +1 -0
  15. package/dist/cli/index.js +2240 -0
  16. package/dist/client.d.ts +158 -0
  17. package/dist/client.js +20 -0
  18. package/dist/config.d.ts +16 -0
  19. package/dist/config.js +23 -0
  20. package/dist/furin.d.ts +45 -0
  21. package/dist/furin.js +937 -0
  22. package/dist/internal.d.ts +18 -0
  23. package/dist/link.d.ts +119 -0
  24. package/dist/link.js +281 -0
  25. package/dist/plugin/index.d.ts +20 -0
  26. package/dist/plugin/index.js +1408 -0
  27. package/dist/plugin/transform-client.d.ts +9 -0
  28. package/dist/render/assemble.d.ts +13 -0
  29. package/dist/render/cache.d.ts +7 -0
  30. package/dist/render/element.d.ts +4 -0
  31. package/dist/render/index.d.ts +26 -0
  32. package/dist/render/loaders.d.ts +12 -0
  33. package/dist/render/shell.d.ts +17 -0
  34. package/dist/render/template.d.ts +4 -0
  35. package/dist/router.d.ts +32 -0
  36. package/dist/router.js +575 -0
  37. package/dist/runtime-env.d.ts +3 -0
  38. package/dist/tsconfig.dts.tsbuildinfo +1 -0
  39. package/dist/utils.d.ts +6 -0
  40. package/package.json +74 -0
  41. package/src/adapter/README.md +13 -0
  42. package/src/adapter/bun.ts +119 -0
  43. package/src/build/client.ts +110 -0
  44. package/src/build/compile-entry.ts +99 -0
  45. package/src/build/entry-template.ts +62 -0
  46. package/src/build/hydrate.ts +106 -0
  47. package/src/build/index.ts +120 -0
  48. package/src/build/route-types.ts +88 -0
  49. package/src/build/scan-server.ts +88 -0
  50. package/src/build/server-routes-entry.ts +38 -0
  51. package/src/build/shared.ts +80 -0
  52. package/src/build/types.ts +60 -0
  53. package/src/cli/config.ts +68 -0
  54. package/src/cli/index.ts +106 -0
  55. package/src/client.ts +237 -0
  56. package/src/config.ts +31 -0
  57. package/src/furin.ts +251 -0
  58. package/src/internal.ts +36 -0
  59. package/src/link.tsx +480 -0
  60. package/src/plugin/index.ts +80 -0
  61. package/src/plugin/transform-client.ts +372 -0
  62. package/src/render/assemble.ts +57 -0
  63. package/src/render/cache.ts +9 -0
  64. package/src/render/element.tsx +28 -0
  65. package/src/render/index.ts +312 -0
  66. package/src/render/loaders.ts +67 -0
  67. package/src/render/shell.ts +128 -0
  68. package/src/render/template.ts +54 -0
  69. package/src/router.ts +234 -0
  70. package/src/runtime-env.ts +6 -0
  71. package/src/utils.ts +68 -0
package/dist/router.js ADDED
@@ -0,0 +1,575 @@
1
+ // @bun
2
+ // src/router.ts
3
+ import { existsSync } from "fs";
4
+ import { readdir } from "fs/promises";
5
+ import { join, parse } from "path";
6
+ import { Elysia } from "elysia";
7
+
8
+ // src/internal.ts
9
+ var _compileCtx = null;
10
+ function getCompileContext() {
11
+ return _compileCtx;
12
+ }
13
+
14
+ // src/render/index.ts
15
+ import { renderToReadableStream } from "react-dom/server";
16
+
17
+ // src/render/shell.ts
18
+ function extractTitle(meta) {
19
+ if (!meta) {
20
+ return;
21
+ }
22
+ for (const entry of meta) {
23
+ if ("title" in entry) {
24
+ return entry.title;
25
+ }
26
+ }
27
+ return;
28
+ }
29
+ function isMetaTag(entry) {
30
+ return !(("title" in entry) || ("charSet" in entry) || ("script:ld+json" in entry) || ("tagName" in entry));
31
+ }
32
+ function escapeHtml(str) {
33
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
34
+ }
35
+ function safeJson(value) {
36
+ return JSON.stringify(value).replace(/</g, "\\u003c");
37
+ }
38
+ function renderAttrs(obj) {
39
+ return Object.entries(obj).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}="${escapeHtml(String(v))}"`).join(" ");
40
+ }
41
+ function buildMetaParts(meta) {
42
+ const parts = [];
43
+ const title = extractTitle(meta);
44
+ if (title) {
45
+ parts.push(`<title>${escapeHtml(title)}</title>`);
46
+ }
47
+ for (const m of meta) {
48
+ if (isMetaTag(m)) {
49
+ parts.push(`<meta ${renderAttrs(m)} />`);
50
+ }
51
+ if ("script:ld+json" in m) {
52
+ parts.push(`<script type="application/ld+json">${safeJson(m["script:ld+json"])}</script>`);
53
+ }
54
+ }
55
+ return parts;
56
+ }
57
+ function buildLinkParts(links) {
58
+ return links.map((link) => `<link ${renderAttrs(link)} />`);
59
+ }
60
+ function buildScriptParts(scripts) {
61
+ return scripts.map((script) => {
62
+ const { children, ...rest } = script;
63
+ const attrs = renderAttrs(rest);
64
+ if (children) {
65
+ return `<script ${attrs}>${children}</script>`;
66
+ }
67
+ return `<script ${attrs}></script>`;
68
+ });
69
+ }
70
+ function buildStyleParts(styles) {
71
+ return styles.map((style) => {
72
+ const typeAttr = style.type ? ` type="${escapeHtml(style.type)}"` : "";
73
+ return `<style${typeAttr}>${style.children}</style>`;
74
+ });
75
+ }
76
+ function buildHeadInjection(headData) {
77
+ const parts = [];
78
+ if (headData?.meta) {
79
+ parts.push(...buildMetaParts(headData.meta));
80
+ }
81
+ if (headData?.links) {
82
+ parts.push(...buildLinkParts(headData.links));
83
+ }
84
+ if (headData?.scripts) {
85
+ parts.push(...buildScriptParts(headData.scripts));
86
+ }
87
+ if (headData?.styles) {
88
+ parts.push(...buildStyleParts(headData.styles));
89
+ }
90
+ return parts.length > 0 ? `
91
+ ${parts.join(`
92
+ `)}
93
+ ` : "";
94
+ }
95
+ function generateIndexHtml() {
96
+ return `<!DOCTYPE html>
97
+ <html lang="en">
98
+ <head>
99
+ <meta charset="UTF-8">
100
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
101
+ <!--ssr-head-->
102
+ </head>
103
+ <body>
104
+ <div id="root"><!--ssr-outlet--></div>
105
+ <script type="module" src="./_hydrate.tsx"></script>
106
+ </body>
107
+ </html>
108
+ `;
109
+ }
110
+
111
+ // src/render/assemble.ts
112
+ function resolvePath(pattern, params) {
113
+ let path = pattern;
114
+ for (const [key, val] of Object.entries(params ?? {})) {
115
+ path = path.replace(key === "*" ? "*" : `:${key}`, val);
116
+ }
117
+ return path;
118
+ }
119
+ async function streamToString(stream) {
120
+ const reader = stream.getReader();
121
+ const decoder = new TextDecoder;
122
+ let html = "";
123
+ for (;; ) {
124
+ const { done, value } = await reader.read();
125
+ if (done) {
126
+ break;
127
+ }
128
+ html += decoder.decode(value, { stream: true });
129
+ }
130
+ html += decoder.decode();
131
+ return html;
132
+ }
133
+ function splitTemplate(template) {
134
+ const [headPre, afterHead = ""] = template.split("<!--ssr-head-->");
135
+ const [bodyPre, bodyPost = ""] = afterHead.split("<!--ssr-outlet-->");
136
+ return { headPre, bodyPre, bodyPost };
137
+ }
138
+ function assembleHTML(template, headData, reactHtml, data) {
139
+ const { headPre, bodyPre, bodyPost } = splitTemplate(template);
140
+ const dataScript = data ? `<script id="__FURIN_DATA__" type="application/json">${safeJson(data)}</script>` : "";
141
+ return headPre + headData + bodyPre + reactHtml + dataScript + bodyPost;
142
+ }
143
+
144
+ // src/render/cache.ts
145
+ var isrCache = new Map;
146
+ var ssgCache = new Map;
147
+
148
+ // src/render/element.tsx
149
+ import { jsxDEV } from "react/jsx-dev-runtime";
150
+ function buildElement(route, data, rootLayout) {
151
+ const Component = route.page.component;
152
+ let element = /* @__PURE__ */ jsxDEV(Component, {
153
+ ...data
154
+ }, undefined, false, undefined, this);
155
+ for (let i = route.routeChain.length - 1;i >= 1; i--) {
156
+ const routeEntry = route.routeChain[i];
157
+ if (routeEntry?.layout) {
158
+ const Layout = routeEntry.layout;
159
+ element = /* @__PURE__ */ jsxDEV(Layout, {
160
+ ...data,
161
+ children: element
162
+ }, undefined, false, undefined, this);
163
+ }
164
+ }
165
+ if (rootLayout.layout) {
166
+ const RootLayoutComponent = rootLayout.layout;
167
+ element = /* @__PURE__ */ jsxDEV(RootLayoutComponent, {
168
+ ...data,
169
+ children: element
170
+ }, undefined, false, undefined, this);
171
+ }
172
+ return element;
173
+ }
174
+
175
+ // src/render/loaders.ts
176
+ async function runLoaders(route, ctx, rootLayout) {
177
+ try {
178
+ const loaderMap = new Map;
179
+ const deps = (routeRef) => loaderMap.get(routeRef) ?? Promise.resolve({});
180
+ const rootData = rootLayout.loader ? await rootLayout.loader({ ...ctx }, deps) ?? {} : {};
181
+ const ctxWithRoot = { ...ctx, ...rootData };
182
+ for (let i = 1;i < route.routeChain.length; i++) {
183
+ const ancestor = route.routeChain[i];
184
+ if (ancestor?.loader) {
185
+ loaderMap.set(ancestor, Promise.resolve(ancestor.loader(ctxWithRoot, deps)).then((r) => r ?? {}));
186
+ }
187
+ }
188
+ const pagePromise = route.page?.loader ? Promise.resolve(route.page.loader(ctxWithRoot, deps)).then((r) => r ?? {}) : Promise.resolve({});
189
+ const results = await Promise.all([...loaderMap.values(), pagePromise]);
190
+ const data = Object.assign({}, rootData, ...results);
191
+ const headers = {};
192
+ Object.assign(headers, ctx.set.headers);
193
+ return { type: "data", data, headers };
194
+ } catch (err) {
195
+ if (err instanceof Response) {
196
+ return { type: "redirect", response: err };
197
+ }
198
+ throw err;
199
+ }
200
+ }
201
+
202
+ // src/render/template.ts
203
+ import { readFileSync } from "fs";
204
+ var devTemplatePromises = new Map;
205
+ var _prodTemplatePath = null;
206
+ var _prodTemplateContent = null;
207
+ function getDevTemplate(origin) {
208
+ const cached = devTemplatePromises.get(origin);
209
+ if (cached) {
210
+ return cached;
211
+ }
212
+ const promise = fetch(`${origin}/_bun_hmr_entry`).then((r) => {
213
+ if (!r.ok) {
214
+ throw new Error(`/_bun_hmr_entry returned ${r.status}`);
215
+ }
216
+ return r.text();
217
+ }).catch((err) => {
218
+ devTemplatePromises.delete(origin);
219
+ throw err;
220
+ });
221
+ devTemplatePromises.set(origin, promise);
222
+ return promise;
223
+ }
224
+ function getProductionTemplate() {
225
+ if (_prodTemplateContent !== null) {
226
+ return _prodTemplateContent;
227
+ }
228
+ if (!_prodTemplatePath) {
229
+ return null;
230
+ }
231
+ try {
232
+ _prodTemplateContent = readFileSync(_prodTemplatePath, "utf8");
233
+ return _prodTemplateContent;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+ // src/runtime-env.ts
239
+ var IS_DEV = true;
240
+
241
+ // src/render/index.ts
242
+ function catchRedirect(err) {
243
+ if (err instanceof Response) {
244
+ return err;
245
+ }
246
+ throw err;
247
+ }
248
+ async function renderForPath(route, params, root, origin) {
249
+ const resolvedPath = resolvePath(route.pattern, params);
250
+ return await renderToHTML(route, {
251
+ params,
252
+ query: {},
253
+ request: new Request(`${origin}${resolvedPath}`),
254
+ headers: {},
255
+ cookie: {},
256
+ redirect: (url, status = 302) => new Response(null, { status, headers: { Location: url } }),
257
+ set: { headers: {} },
258
+ path: resolvedPath
259
+ }, root);
260
+ }
261
+ async function renderToHTML(route, ctx, root) {
262
+ const loaderResult = await runLoaders(route, ctx, root.route);
263
+ if (loaderResult.type === "redirect") {
264
+ throw loaderResult.response;
265
+ }
266
+ const { data, headers } = loaderResult;
267
+ const componentProps = {
268
+ ...data,
269
+ params: ctx.params,
270
+ query: ctx.query,
271
+ path: ctx.path
272
+ };
273
+ const headData = buildHeadInjection(route.page?.head?.(componentProps));
274
+ const element = buildElement(route, componentProps, root.route);
275
+ const stream = await renderToReadableStream(element);
276
+ await stream.allReady;
277
+ const reactHtml = await streamToString(stream);
278
+ const template = IS_DEV ? await getDevTemplate(new URL(ctx.request.url).origin) : getProductionTemplate() ?? generateIndexHtml();
279
+ return {
280
+ html: assembleHTML(template, headData, reactHtml, data),
281
+ headers
282
+ };
283
+ }
284
+ async function prerenderSSG(route, params, root, origin = "http://localhost:3000") {
285
+ const resolvedPath = resolvePath(route.pattern, params);
286
+ const cached = ssgCache.get(resolvedPath);
287
+ if (cached && !IS_DEV) {
288
+ return cached;
289
+ }
290
+ const result = await renderForPath(route, params, root, origin);
291
+ if (!IS_DEV) {
292
+ ssgCache.set(resolvedPath, result.html);
293
+ }
294
+ return result.html;
295
+ }
296
+ async function renderSSR(route, ctx, root) {
297
+ try {
298
+ const loaderResult = await runLoaders(route, ctx, root.route);
299
+ if (loaderResult.type === "redirect") {
300
+ return loaderResult.response;
301
+ }
302
+ const { data, headers } = loaderResult;
303
+ const componentProps = { ...data, params: ctx.params, query: ctx.query, path: ctx.path };
304
+ const headData = buildHeadInjection(route.page.head?.(componentProps));
305
+ const template = IS_DEV ? await getDevTemplate(new URL(ctx.request.url).origin) : getProductionTemplate() ?? generateIndexHtml();
306
+ const { headPre, bodyPre, bodyPost } = splitTemplate(template);
307
+ const dataScript = `<script id="__FURIN_DATA__" type="application/json">${safeJson(data)}</script>`;
308
+ const element = buildElement(route, componentProps, root.route);
309
+ const reactStream = await renderToReadableStream(element);
310
+ const { readable, writable } = new TransformStream;
311
+ const writer = writable.getWriter();
312
+ const enc = new TextEncoder;
313
+ (async () => {
314
+ await writer.write(enc.encode(headPre + headData + bodyPre));
315
+ const reader = reactStream.getReader();
316
+ for (;; ) {
317
+ const { done, value } = await reader.read();
318
+ if (done) {
319
+ break;
320
+ }
321
+ await writer.write(value);
322
+ }
323
+ await writer.write(enc.encode(dataScript + bodyPost));
324
+ await writer.close();
325
+ })().catch((err) => writer.abort(err));
326
+ return new Response(readable, {
327
+ headers: {
328
+ "Content-Type": "text/html; charset=utf-8",
329
+ "Cache-Control": "no-cache, no-store, must-revalidate",
330
+ ...headers
331
+ }
332
+ });
333
+ } catch (err) {
334
+ return catchRedirect(err);
335
+ }
336
+ }
337
+ async function handleISR(route, ctx, root) {
338
+ const revalidate = route.page._route.revalidate ?? 60;
339
+ const params = ctx.params ?? {};
340
+ const cacheKey = resolvePath(route.pattern, params);
341
+ const cached = isrCache.get(cacheKey);
342
+ if (cached && !IS_DEV) {
343
+ const age = Date.now() - cached.generatedAt;
344
+ const isFresh = age < revalidate * 1000;
345
+ if (!isFresh) {
346
+ revalidateInBackground(route, params, cacheKey, revalidate, root, ctx);
347
+ }
348
+ ctx.set.headers["content-type"] = "text/html; charset=utf-8";
349
+ ctx.set.headers["cache-control"] = isFresh ? `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate}` : "public, s-maxage=0, must-revalidate";
350
+ return cached.html;
351
+ }
352
+ try {
353
+ const result = await renderToHTML(route, ctx, root);
354
+ if (!IS_DEV) {
355
+ isrCache.set(cacheKey, { html: result.html, generatedAt: Date.now(), revalidate });
356
+ }
357
+ ctx.set.headers["content-type"] = "text/html; charset=utf-8";
358
+ ctx.set.headers["cache-control"] = `public, s-maxage=${revalidate}, stale-while-revalidate=${revalidate}`;
359
+ return result.html;
360
+ } catch (err) {
361
+ return catchRedirect(err);
362
+ }
363
+ }
364
+ function revalidateInBackground(route, params, cacheKey, revalidate, root, originalCtx) {
365
+ renderForPath(route, params, root, new URL(originalCtx.request.url).origin).then((result) => {
366
+ isrCache.set(cacheKey, {
367
+ html: result.html,
368
+ generatedAt: Date.now(),
369
+ revalidate
370
+ });
371
+ }).catch((err) => {
372
+ console.error("[furin] ISR background revalidation failed:", err);
373
+ });
374
+ }
375
+
376
+ // src/utils.ts
377
+ function isFurinPage(value) {
378
+ return typeof value === "object" && value !== null && "__type" in value && value.__type === "FURIN_PAGE";
379
+ }
380
+ function isFurinRoute(value) {
381
+ return typeof value === "object" && value !== null && "__type" in value && value.__type === "FURIN_ROUTE";
382
+ }
383
+ function collectRouteChainFromRoute(route) {
384
+ const chain = [];
385
+ let current = route;
386
+ while (current) {
387
+ chain.unshift(current);
388
+ current = current.parent;
389
+ }
390
+ return chain;
391
+ }
392
+ function hasCycle(route) {
393
+ const visited = new Set;
394
+ let current = route;
395
+ while (current) {
396
+ if (visited.has(current)) {
397
+ return true;
398
+ }
399
+ visited.add(current);
400
+ current = current.parent;
401
+ }
402
+ return false;
403
+ }
404
+ function validateRouteChain(chain, root, pagePath) {
405
+ const hasRoot = chain.some((r) => r === root);
406
+ if (!hasRoot) {
407
+ const location = pagePath ? `in ${pagePath}` : "";
408
+ throw new Error(`[furin] Page ${location} must inherit from root route. ` + 'Add: import { route } from "./root"; and use route.page() or set parent: route');
409
+ }
410
+ for (const route of chain) {
411
+ if (hasCycle(route)) {
412
+ throw new Error("[furin] Cycle detected in route chain. A route cannot be its own ancestor.");
413
+ }
414
+ }
415
+ }
416
+
417
+ // src/router.ts
418
+ function createRoutePlugin(route, root) {
419
+ const { pattern, mode, routeChain } = route;
420
+ const plugins = [];
421
+ const allParams = routeChain.find((r) => r.params)?.params;
422
+ const allQuery = routeChain.find((r) => r.query)?.query;
423
+ if (allParams || allQuery) {
424
+ plugins.push(new Elysia().guard({
425
+ params: allParams,
426
+ query: allQuery
427
+ }));
428
+ }
429
+ plugins.push(new Elysia().get(pattern, async (ctx) => {
430
+ switch (mode) {
431
+ case "ssg": {
432
+ ctx.set.headers["content-type"] = "text/html; charset=utf-8";
433
+ ctx.set.headers["cache-control"] = "public, max-age=0, must-revalidate";
434
+ const origin = new URL(ctx.request.url).origin;
435
+ return await prerenderSSG(route, ctx.params, root, origin);
436
+ }
437
+ case "isr":
438
+ return handleISR(route, ctx, root);
439
+ default:
440
+ return renderSSR(route, ctx, root);
441
+ }
442
+ }));
443
+ return plugins.reduce((app, plugin) => app.use(plugin), new Elysia);
444
+ }
445
+ async function scanRootLayout(pagesDir) {
446
+ const rootPath = `${pagesDir}/root.tsx`;
447
+ const ctx = getCompileContext();
448
+ if (!(existsSync(rootPath) || ctx?.modules[rootPath])) {
449
+ throw new Error("[furin] root.tsx: not found.");
450
+ }
451
+ const mod = ctx?.modules[rootPath] ?? await import(rootPath);
452
+ const rootExport = mod.route ?? mod.default;
453
+ if (!(rootExport && isFurinRoute(rootExport))) {
454
+ throw new Error("[furin] root.tsx: createRoute() export not found.");
455
+ }
456
+ if (!rootExport.layout) {
457
+ throw new Error("[furin] root.tsx: createRoute() has no layout.");
458
+ }
459
+ return { path: rootPath, route: rootExport };
460
+ }
461
+ async function collectPageFilePaths(dir) {
462
+ const files = [];
463
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
464
+ const absolutePath = join(dir, entry.name);
465
+ if (entry.isDirectory()) {
466
+ files.push(...await collectPageFilePaths(absolutePath));
467
+ continue;
468
+ }
469
+ if (entry.isFile()) {
470
+ files.push(absolutePath);
471
+ }
472
+ }
473
+ return files;
474
+ }
475
+ async function scanPageFiles(pagesDir, root) {
476
+ const routes = [];
477
+ for (const absolutePath of await collectPageFilePaths(pagesDir)) {
478
+ if (![".tsx", ".ts", ".jsx", ".js"].some((ext) => absolutePath.endsWith(ext))) {
479
+ continue;
480
+ }
481
+ const relativePath = absolutePath.replace(`${pagesDir}/`, "");
482
+ const fileName = parse(relativePath).name;
483
+ if (fileName.startsWith("_") || fileName === "root") {
484
+ continue;
485
+ }
486
+ const ctx = getCompileContext();
487
+ const pageMod = ctx?.modules[absolutePath] ?? await import(absolutePath);
488
+ const page = pageMod.default;
489
+ if (!isFurinPage(page)) {
490
+ throw new Error(`[furin] ${relativePath}: no valid createRoute().page() export found`);
491
+ }
492
+ const routeChain = collectRouteChainFromRoute(page._route);
493
+ validateRouteChain(routeChain, root.route, relativePath);
494
+ routes.push({
495
+ pattern: filePathToPattern(relativePath),
496
+ page,
497
+ path: absolutePath,
498
+ routeChain,
499
+ mode: resolveMode(page, routeChain)
500
+ });
501
+ }
502
+ return routes;
503
+ }
504
+ async function scanPages(pagesDir) {
505
+ const ctx = getCompileContext();
506
+ if (ctx) {
507
+ return loadProdRoutes(ctx);
508
+ }
509
+ const root = await scanRootLayout(pagesDir);
510
+ const routes = await scanPageFiles(pagesDir, root);
511
+ return { root, routes };
512
+ }
513
+ function loadProdRoutes(ctx) {
514
+ const rootMod = ctx.modules[ctx.rootPath];
515
+ const rootExport = rootMod.route ?? rootMod.default;
516
+ if (!(rootExport && isFurinRoute(rootExport) && rootExport.layout)) {
517
+ throw new Error("[furin] root.tsx: createRoute() with layout not found in CompileContext.");
518
+ }
519
+ const root = { path: ctx.rootPath, route: rootExport };
520
+ const routes = [];
521
+ for (const { pattern, path, mode } of ctx.routes) {
522
+ const pageMod = ctx.modules[path];
523
+ const page = pageMod.default;
524
+ if (!isFurinPage(page)) {
525
+ throw new Error(`[furin] ${path}: invalid page module in CompileContext.`);
526
+ }
527
+ const routeChain = collectRouteChainFromRoute(page._route);
528
+ validateRouteChain(routeChain, root.route, path);
529
+ routes.push({ pattern, page, path, routeChain, mode });
530
+ }
531
+ return { root, routes };
532
+ }
533
+ function resolveMode(page, routeChain) {
534
+ const routeConfig = page._route;
535
+ if (routeConfig.mode) {
536
+ return routeConfig.mode;
537
+ }
538
+ const hasLoader = routeChain.some((r) => r.loader) || !!page.loader;
539
+ if (!hasLoader) {
540
+ return "ssg";
541
+ }
542
+ if (routeConfig.revalidate && routeConfig.revalidate > 0) {
543
+ return "isr";
544
+ }
545
+ return "ssr";
546
+ }
547
+ function filePathToPattern(path) {
548
+ const parts = path.replaceAll("\\", "/").split("/");
549
+ const segments = [];
550
+ for (const part of parts) {
551
+ const name = parse(part).name;
552
+ if (name === "index") {
553
+ continue;
554
+ }
555
+ if (name.startsWith("[") && name.endsWith("]")) {
556
+ const inner = name.slice(1, -1);
557
+ if (inner.startsWith("...")) {
558
+ segments.push("*");
559
+ continue;
560
+ }
561
+ segments.push(`:${inner}`);
562
+ continue;
563
+ }
564
+ segments.push(name);
565
+ }
566
+ return `/${segments.join("/")}`;
567
+ }
568
+ export {
569
+ scanRootLayout,
570
+ scanPages,
571
+ resolveMode,
572
+ loadProdRoutes,
573
+ filePathToPattern,
574
+ createRoutePlugin
575
+ };
@@ -0,0 +1,3 @@
1
+ export declare let IS_DEV: boolean;
2
+ /** @internal test-only — overrides IS_DEV via live binding */
3
+ export declare function __setDevMode(val: boolean): void;