@catmint/vite 0.0.0-prealpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +339 -0
  2. package/dist/boundary.d.ts +15 -0
  3. package/dist/boundary.d.ts.map +1 -0
  4. package/dist/boundary.js +193 -0
  5. package/dist/boundary.js.map +1 -0
  6. package/dist/build-entries.d.ts +81 -0
  7. package/dist/build-entries.d.ts.map +1 -0
  8. package/dist/build-entries.js +1139 -0
  9. package/dist/build-entries.js.map +1 -0
  10. package/dist/cors-utils.d.ts +22 -0
  11. package/dist/cors-utils.d.ts.map +1 -0
  12. package/dist/cors-utils.js +36 -0
  13. package/dist/cors-utils.js.map +1 -0
  14. package/dist/dev-server.d.ts +34 -0
  15. package/dist/dev-server.d.ts.map +1 -0
  16. package/dist/dev-server.js +1683 -0
  17. package/dist/dev-server.js.map +1 -0
  18. package/dist/env-transform.d.ts +16 -0
  19. package/dist/env-transform.d.ts.map +1 -0
  20. package/dist/env-transform.js +125 -0
  21. package/dist/env-transform.js.map +1 -0
  22. package/dist/error-page.d.ts +19 -0
  23. package/dist/error-page.d.ts.map +1 -0
  24. package/dist/error-page.js +152 -0
  25. package/dist/error-page.js.map +1 -0
  26. package/dist/index.d.ts +92 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +100 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/mdx-transform.d.ts +33 -0
  31. package/dist/mdx-transform.d.ts.map +1 -0
  32. package/dist/mdx-transform.js +86 -0
  33. package/dist/mdx-transform.js.map +1 -0
  34. package/dist/middleware.d.ts +13 -0
  35. package/dist/middleware.d.ts.map +1 -0
  36. package/dist/middleware.js +155 -0
  37. package/dist/middleware.js.map +1 -0
  38. package/dist/resolve-utils.d.ts +17 -0
  39. package/dist/resolve-utils.d.ts.map +1 -0
  40. package/dist/resolve-utils.js +51 -0
  41. package/dist/resolve-utils.js.map +1 -0
  42. package/dist/route-gen.d.ts +12 -0
  43. package/dist/route-gen.d.ts.map +1 -0
  44. package/dist/route-gen.js +221 -0
  45. package/dist/route-gen.js.map +1 -0
  46. package/dist/server-fn-transform.d.ts +12 -0
  47. package/dist/server-fn-transform.d.ts.map +1 -0
  48. package/dist/server-fn-transform.js +394 -0
  49. package/dist/server-fn-transform.js.map +1 -0
  50. package/dist/utils.d.ts +56 -0
  51. package/dist/utils.d.ts.map +1 -0
  52. package/dist/utils.js +198 -0
  53. package/dist/utils.js.map +1 -0
  54. package/package.json +37 -0
@@ -0,0 +1,1139 @@
1
+ // @catmint/vite/build-entries — Virtual entry modules for production builds
2
+ //
3
+ // Provides virtual modules consumed by catmint build:
4
+ //
5
+ // Legacy (non-RSC) entries:
6
+ // 1. virtual:catmint/client-entry
7
+ // Client-side hydration entry with per-route code splitting.
8
+ // 2. virtual:catmint/server-entry
9
+ // Server-side render function using renderToPipeableStream.
10
+ //
11
+ // RSC entries (three-environment build):
12
+ // 3. virtual:catmint/rsc-entry
13
+ // RSC environment entry: imports pages+layouts, exports render(url)
14
+ // that builds element tree and calls renderToReadableStream from
15
+ // @vitejs/plugin-rsc/rsc to produce an RSC flight stream.
16
+ // 4. virtual:catmint/ssr-rsc-entry
17
+ // SSR environment entry for RSC mode: exports renderToHtml(rscStream)
18
+ // that converts flight stream to HTML, plus endpoint handling.
19
+ // 5. virtual:catmint/client-rsc-entry
20
+ // Client environment entry for RSC mode: reads embedded RSC payload
21
+ // from the HTML and hydrates via hydrateRoot.
22
+ import { dirname, join, relative, resolve, sep, posix } from "node:path";
23
+ import { existsSync, readdirSync, statSync } from "node:fs";
24
+ import { deterministicHash, toPosixPath, CLIENT_NAVIGATION_RUNTIME, } from "./utils.js";
25
+ const CLIENT_ENTRY_ID = "virtual:catmint/client-entry";
26
+ const SERVER_ENTRY_ID = "virtual:catmint/server-entry";
27
+ const RSC_ENTRY_ID = "virtual:catmint/rsc-entry";
28
+ const SSR_RSC_ENTRY_ID = "virtual:catmint/ssr-rsc-entry";
29
+ const CLIENT_RSC_ENTRY_ID = "virtual:catmint/client-rsc-entry";
30
+ const RESOLVED_CLIENT_ID = "\0" + CLIENT_ENTRY_ID;
31
+ const RESOLVED_SERVER_ID = "\0" + SERVER_ENTRY_ID;
32
+ const RESOLVED_RSC_ID = "\0" + RSC_ENTRY_ID;
33
+ const RESOLVED_SSR_RSC_ID = "\0" + SSR_RSC_ENTRY_ID;
34
+ const RESOLVED_CLIENT_RSC_ID = "\0" + CLIENT_RSC_ENTRY_ID;
35
+ /**
36
+ * Scan the app directory for status page files (e.g. 404.tsx, 500.tsx).
37
+ *
38
+ * Recursively walks the app directory looking for files named `{code}.tsx`
39
+ * (or `.jsx`, `.ts`, `.js`) where `code` is a 3-digit HTTP status code.
40
+ * Returns them sorted from most specific (deepest) to least specific (root).
41
+ */
42
+ export function scanStatusPages(appDir) {
43
+ const STATUS_PAGE_REGEX = /^(\d{3})\.(tsx|jsx|ts|js)$/;
44
+ const normalizedAppDir = resolve(appDir);
45
+ const results = [];
46
+ function walk(dir) {
47
+ let entries;
48
+ try {
49
+ entries = readdirSync(dir);
50
+ }
51
+ catch {
52
+ return;
53
+ }
54
+ for (const entry of entries) {
55
+ const fullPath = join(dir, entry);
56
+ const match = STATUS_PAGE_REGEX.exec(entry);
57
+ if (match) {
58
+ const statusCode = parseInt(match[1], 10);
59
+ // Compute URL prefix: /app/admin/404.tsx → "/admin"
60
+ const relDir = relative(normalizedAppDir, dir);
61
+ const urlPrefix = relDir
62
+ ? "/" + relDir.split(sep).join(posix.sep)
63
+ : "/";
64
+ results.push({ statusCode, filePath: fullPath, urlPrefix });
65
+ }
66
+ try {
67
+ if (statSync(fullPath).isDirectory()) {
68
+ // Skip node_modules and hidden dirs
69
+ if (!entry.startsWith(".") && entry !== "node_modules") {
70
+ walk(fullPath);
71
+ }
72
+ }
73
+ }
74
+ catch {
75
+ // Skip inaccessible entries
76
+ }
77
+ }
78
+ }
79
+ walk(normalizedAppDir);
80
+ // Sort: most specific (longest prefix) first, so resolution picks the nearest
81
+ results.sort((a, b) => b.urlPrefix.length - a.urlPrefix.length);
82
+ return results;
83
+ }
84
+ /**
85
+ * Layout file names to check, in priority order.
86
+ */
87
+ const LAYOUT_FILENAMES = ["layout.tsx", "layout.jsx", "layout.ts", "layout.js"];
88
+ /**
89
+ * Walk up from a page file's directory to the app root, collecting
90
+ * layout files in order from outermost (root) to innermost.
91
+ */
92
+ function collectLayouts(pageFilePath, appDir) {
93
+ const layouts = [];
94
+ let dir = dirname(pageFilePath);
95
+ const normalizedAppDir = resolve(appDir);
96
+ while (dir.startsWith(normalizedAppDir)) {
97
+ for (const filename of LAYOUT_FILENAMES) {
98
+ const layoutPath = join(dir, filename);
99
+ if (existsSync(layoutPath)) {
100
+ layouts.unshift(layoutPath);
101
+ break;
102
+ }
103
+ }
104
+ if (dir === normalizedAppDir)
105
+ break;
106
+ dir = dirname(dir);
107
+ }
108
+ return layouts;
109
+ }
110
+ /**
111
+ * Convert an absolute file path to a Vite-resolvable import path
112
+ * relative to the project root (e.g. '/app/layout.tsx').
113
+ */
114
+ function toVitePath(filePath, root) {
115
+ return "/" + relative(root, filePath).split(sep).join(posix.sep);
116
+ }
117
+ /**
118
+ * Deduplicate an array preserving order.
119
+ */
120
+ function unique(arr) {
121
+ return [...new Set(arr)];
122
+ }
123
+ /**
124
+ * Create the build entries plugin.
125
+ *
126
+ * Must be provided with routes and appDir from the build command.
127
+ * Only active during production builds (not in dev mode).
128
+ */
129
+ export function buildEntriesPlugin(options) {
130
+ const { routes, endpoints, appDir, serverFnFiles, statusPages, softNavigation = true, i18n: i18nConfig = null, } = options;
131
+ let root = "";
132
+ return {
133
+ name: "catmint:build-entries",
134
+ configResolved(config) {
135
+ root = config.root;
136
+ },
137
+ resolveId(id) {
138
+ if (id === CLIENT_ENTRY_ID)
139
+ return RESOLVED_CLIENT_ID;
140
+ if (id === SERVER_ENTRY_ID)
141
+ return RESOLVED_SERVER_ID;
142
+ if (id === RSC_ENTRY_ID)
143
+ return RESOLVED_RSC_ID;
144
+ if (id === SSR_RSC_ENTRY_ID)
145
+ return RESOLVED_SSR_RSC_ID;
146
+ if (id === CLIENT_RSC_ENTRY_ID)
147
+ return RESOLVED_CLIENT_RSC_ID;
148
+ return null;
149
+ },
150
+ load(id) {
151
+ if (id === RESOLVED_CLIENT_ID) {
152
+ return generateClientEntry(routes, appDir, root, i18nConfig);
153
+ }
154
+ if (id === RESOLVED_SERVER_ID) {
155
+ return generateServerEntry(routes, endpoints, appDir, root, i18nConfig);
156
+ }
157
+ if (id === RESOLVED_RSC_ID) {
158
+ return generateRscEntry(routes, appDir, root, i18nConfig, statusPages ?? []);
159
+ }
160
+ if (id === RESOLVED_SSR_RSC_ID) {
161
+ return generateSsrRscEntry(endpoints, root, serverFnFiles ?? []);
162
+ }
163
+ if (id === RESOLVED_CLIENT_RSC_ID) {
164
+ return generateClientRscEntry(softNavigation);
165
+ }
166
+ return null;
167
+ },
168
+ };
169
+ }
170
+ /**
171
+ * Generate the client-side hydration entry module.
172
+ *
173
+ * Creates a route map where each route lazily imports its page component.
174
+ * At runtime, matches the current URL against the route patterns and
175
+ * hydrates with the correct page + layout chain.
176
+ *
177
+ * Layout components are eagerly imported since they're shared across
178
+ * many routes and needed immediately for hydration.
179
+ */
180
+ function generateClientEntry(routes, appDir, root, i18nConfig = null) {
181
+ // Collect all unique layout files across all routes
182
+ const allLayoutPaths = [];
183
+ const routeLayoutMap = new Map();
184
+ for (const route of routes) {
185
+ const layouts = collectLayouts(route.filePath, appDir);
186
+ routeLayoutMap.set(route.pattern, layouts);
187
+ for (const lp of layouts) {
188
+ if (!allLayoutPaths.includes(lp)) {
189
+ allLayoutPaths.push(lp);
190
+ }
191
+ }
192
+ }
193
+ const lines = [
194
+ "// Auto-generated client entry by catmint build",
195
+ "// Per-route code splitting with lazy page imports",
196
+ "",
197
+ "import { hydrateRoot } from 'react-dom/client'",
198
+ "import { createElement, lazy, Suspense } from 'react'",
199
+ "import { AppProviders } from 'catmint/runtime/app-providers'",
200
+ "",
201
+ ];
202
+ // Eagerly import all layouts (shared across routes)
203
+ for (let i = 0; i < allLayoutPaths.length; i++) {
204
+ const vitePath = toVitePath(allLayoutPaths[i], root);
205
+ lines.push(`import Layout_${i} from '${vitePath}'`);
206
+ }
207
+ lines.push("");
208
+ // Create the route map with lazy page imports
209
+ lines.push("const routeMap = {");
210
+ for (const route of routes) {
211
+ const vitePath = toVitePath(route.filePath, root);
212
+ // Use JSON.stringify for the key to handle patterns with special chars
213
+ lines.push(` ${JSON.stringify(route.pattern)}: {`);
214
+ lines.push(` page: () => import('${vitePath}'),`);
215
+ // Map this route's layouts to their indices in allLayoutPaths
216
+ const layouts = routeLayoutMap.get(route.pattern) ?? [];
217
+ const layoutIndices = layouts.map((lp) => allLayoutPaths.indexOf(lp));
218
+ lines.push(` layouts: [${layoutIndices.map((i) => `Layout_${i}`).join(", ")}],`);
219
+ lines.push(` },`);
220
+ }
221
+ lines.push("}");
222
+ lines.push("");
223
+ // Serialize i18n config for injection into generated code
224
+ const i18nConfigJson = i18nConfig ? JSON.stringify(i18nConfig) : "null";
225
+ // Generate the route matching and hydration logic
226
+ lines.push(`
227
+ // Match the current URL to a route pattern
228
+ function matchRoute(pathname) {
229
+ // First try exact match
230
+ if (routeMap[pathname]) return { route: routeMap[pathname], params: {} }
231
+
232
+ // Try pattern matching for dynamic routes
233
+ for (const [pattern, route] of Object.entries(routeMap)) {
234
+ const params = matchPattern(pattern, pathname)
235
+ if (params) return { route, params }
236
+ }
237
+ return null
238
+ }
239
+
240
+ // Match a URL pattern like '/user/:id' or '/docs/*slug' against a pathname
241
+ function matchPattern(pattern, pathname) {
242
+ const patternParts = pattern.split('/').filter(Boolean)
243
+ const pathParts = pathname.split('/').filter(Boolean)
244
+
245
+ const params = {}
246
+
247
+ for (let i = 0; i < patternParts.length; i++) {
248
+ const pp = patternParts[i]
249
+
250
+ // Catch-all segment
251
+ if (pp.startsWith('*')) {
252
+ const paramName = pp.endsWith('?') ? pp.slice(1, -1) : pp.slice(1)
253
+ params[paramName] = pathParts.slice(i)
254
+ return params
255
+ }
256
+
257
+ // Dynamic segment
258
+ if (pp.startsWith(':')) {
259
+ if (i >= pathParts.length) return null
260
+ params[pp.slice(1)] = pathParts[i]
261
+ continue
262
+ }
263
+
264
+ // Static segment
265
+ if (i >= pathParts.length || pp !== pathParts[i]) return null
266
+ }
267
+
268
+ // Optional catch-all: pattern has fewer parts and last is optional
269
+ if (patternParts.length < pathParts.length) return null
270
+ if (patternParts.length > pathParts.length) {
271
+ // Check if remaining are optional catch-all
272
+ const remaining = patternParts[pathParts.length]
273
+ if (remaining && remaining.startsWith('*') && remaining.endsWith('?')) {
274
+ const paramName = remaining.slice(1, -1)
275
+ params[paramName] = []
276
+ return params
277
+ }
278
+ return null
279
+ }
280
+
281
+ return params
282
+ }
283
+
284
+ // Hydrate the current page
285
+ var __i18nConfig = ${i18nConfigJson}
286
+ var pathname = window.location.pathname
287
+ var matched = matchRoute(pathname)
288
+
289
+ if (matched) {
290
+ matched.route.page().then(function(pageMod) {
291
+ var Page = pageMod.default
292
+ var layouts = matched.route.layouts
293
+
294
+ // Build component tree: outermost layout wraps innermost wraps page
295
+ var element = createElement(Page, null)
296
+ for (var i = layouts.length - 1; i >= 0; i--) {
297
+ element = createElement(layouts[i], null, element)
298
+ }
299
+
300
+ // Wrap with framework providers
301
+ var providerProps = { pathname: pathname, params: matched.params }
302
+ if (__i18nConfig) providerProps.i18nConfig = __i18nConfig
303
+ element = createElement(AppProviders, providerProps, element)
304
+
305
+ hydrateRoot(document, element)
306
+ })
307
+ } else {
308
+ console.warn('[catmint] No matching route for', pathname)
309
+ }
310
+ `);
311
+ return lines.join("\n");
312
+ }
313
+ /**
314
+ * Generate the server-side render entry module (legacy, non-RSC mode).
315
+ *
316
+ * Exports a `render(url, options)` function that:
317
+ * 1. Matches the URL to a route
318
+ * 2. Loads the page + layout chain
319
+ * 3. Renders with renderToPipeableStream
320
+ * 4. Returns a pipeable stream
321
+ */
322
+ function generateServerEntry(routes, endpoints, appDir, root, i18nConfig = null) {
323
+ // Collect all unique layout files
324
+ const allLayoutPaths = [];
325
+ const routeLayoutMap = new Map();
326
+ for (const route of routes) {
327
+ const layouts = collectLayouts(route.filePath, appDir);
328
+ routeLayoutMap.set(route.pattern, layouts);
329
+ for (const lp of layouts) {
330
+ if (!allLayoutPaths.includes(lp)) {
331
+ allLayoutPaths.push(lp);
332
+ }
333
+ }
334
+ }
335
+ const lines = [
336
+ "// Auto-generated server entry by catmint build",
337
+ "",
338
+ "import { createElement } from 'react'",
339
+ "import { renderToPipeableStream } from 'react-dom/server'",
340
+ "import { AppProviders } from 'catmint/runtime/app-providers'",
341
+ "",
342
+ ];
343
+ // Import all page components
344
+ for (let i = 0; i < routes.length; i++) {
345
+ const vitePath = toVitePath(routes[i].filePath, root);
346
+ lines.push(`import Page_${i} from '${vitePath}'`);
347
+ }
348
+ lines.push("");
349
+ // Import all layout components
350
+ for (let i = 0; i < allLayoutPaths.length; i++) {
351
+ const vitePath = toVitePath(allLayoutPaths[i], root);
352
+ lines.push(`import Layout_${i} from '${vitePath}'`);
353
+ }
354
+ lines.push("");
355
+ // Create the route map
356
+ lines.push("const routeMap = {");
357
+ for (let i = 0; i < routes.length; i++) {
358
+ const route = routes[i];
359
+ const layouts = routeLayoutMap.get(route.pattern) ?? [];
360
+ const layoutIndices = layouts.map((lp) => allLayoutPaths.indexOf(lp));
361
+ lines.push(` ${JSON.stringify(route.pattern)}: {`);
362
+ lines.push(` page: Page_${i},`);
363
+ lines.push(` layouts: [${layoutIndices.map((j) => `Layout_${j}`).join(", ")}],`);
364
+ lines.push(` },`);
365
+ }
366
+ lines.push("}");
367
+ lines.push("");
368
+ // Route matching function
369
+ lines.push(ROUTE_MATCH_CODE);
370
+ // Serialize i18n config for injection into generated code
371
+ const i18nConfigJson = i18nConfig ? JSON.stringify(i18nConfig) : "null";
372
+ // Export the render function
373
+ lines.push(`
374
+ var __i18nConfig = ${i18nConfigJson}
375
+
376
+ /**
377
+ * Render a URL to a pipeable stream.
378
+ *
379
+ * @param {string} url - The URL pathname to render
380
+ * @param {object} [options] - Render options
381
+ * @param {string[]} [options.bootstrapModules] - Client JS modules to inject as <script> tags
382
+ * @returns {{ stream: import('react-dom/server').PipeableStream, statusCode: number } | null}
383
+ */
384
+ export function render(url, options) {
385
+ const pathname = new URL(url, 'http://localhost').pathname
386
+ const matched = matchRoute(pathname)
387
+
388
+ if (!matched) return null
389
+
390
+ const { route, params } = matched
391
+ const { page: Page, layouts } = route
392
+
393
+ // Build component tree: Layout0(Layout1(...(Page)))
394
+ let element = createElement(Page, null)
395
+ for (let i = layouts.length - 1; i >= 0; i--) {
396
+ element = createElement(layouts[i], null, element)
397
+ }
398
+
399
+ // Wrap with framework providers
400
+ var providerProps = { pathname: pathname, params: params }
401
+ if (__i18nConfig) providerProps.i18nConfig = __i18nConfig
402
+ element = createElement(AppProviders, providerProps, element)
403
+
404
+ const streamOptions = {}
405
+ if (options && options.bootstrapModules) {
406
+ streamOptions.bootstrapModules = options.bootstrapModules
407
+ }
408
+
409
+ const stream = renderToPipeableStream(element, streamOptions)
410
+
411
+ return { stream, statusCode: 200, params }
412
+ }
413
+
414
+ /**
415
+ * Check if a URL matches any page route.
416
+ */
417
+ export function hasRoute(url) {
418
+ const pathname = new URL(url, 'http://localhost').pathname
419
+ return matchRoute(pathname) !== null
420
+ }
421
+
422
+ /**
423
+ * Get all registered route patterns.
424
+ */
425
+ export function getRoutePatterns() {
426
+ return Object.keys(routeMap)
427
+ }
428
+ `);
429
+ // --- Endpoint handling ---
430
+ if (endpoints.length > 0) {
431
+ lines.push("");
432
+ // Import all endpoint modules
433
+ for (let i = 0; i < endpoints.length; i++) {
434
+ const vitePath = toVitePath(endpoints[i].filePath, root);
435
+ lines.push(`import * as Endpoint_${i} from '${vitePath}'`);
436
+ }
437
+ lines.push("");
438
+ // Create the endpoint route map
439
+ lines.push("const endpointMap = {");
440
+ for (let i = 0; i < endpoints.length; i++) {
441
+ const ep = endpoints[i];
442
+ lines.push(` ${JSON.stringify(ep.pattern)}: {`);
443
+ lines.push(` mod: Endpoint_${i},`);
444
+ lines.push(` methods: ${JSON.stringify(ep.methods)},`);
445
+ lines.push(` },`);
446
+ }
447
+ lines.push("}");
448
+ lines.push(`
449
+
450
+ function matchEndpoint(pathname) {
451
+ if (endpointMap[pathname]) return { endpoint: endpointMap[pathname], params: {} }
452
+ for (const [pattern, endpoint] of Object.entries(endpointMap)) {
453
+ const params = matchPattern(pattern, pathname)
454
+ if (params) return { endpoint, params }
455
+ }
456
+ return null
457
+ }
458
+
459
+ /**
460
+ * Handle an API endpoint request.
461
+ */
462
+ export async function handleEndpoint(url, method, request) {
463
+ const pathname = new URL(url, 'http://localhost').pathname
464
+ const matched = matchEndpoint(pathname)
465
+
466
+ if (!matched) return null
467
+
468
+ const { endpoint, params } = matched
469
+ const mod = endpoint.mod
470
+
471
+ const handler = mod[method] || mod.ANY || mod.default
472
+ if (!handler) return null
473
+
474
+ const ctx = { params }
475
+ const response = await handler(request, ctx)
476
+ return { response }
477
+ }
478
+
479
+ /**
480
+ * Check if a URL matches any endpoint route.
481
+ */
482
+ export function hasEndpoint(url) {
483
+ const pathname = new URL(url, 'http://localhost').pathname
484
+ return matchEndpoint(pathname) !== null
485
+ }
486
+ `);
487
+ }
488
+ else {
489
+ lines.push(`
490
+ export async function handleEndpoint() { return null }
491
+ export function hasEndpoint() { return false }
492
+ `);
493
+ }
494
+ return lines.join("\n");
495
+ }
496
+ // --------------------------------------------------------------------------
497
+ // RSC build entry generators
498
+ // --------------------------------------------------------------------------
499
+ /**
500
+ * Generate the RSC environment entry module.
501
+ *
502
+ * This module runs in the RSC environment (with `react-server` resolve conditions).
503
+ * It imports all pages and layouts, and exports a `render(url)` function that:
504
+ * 1. Matches the URL to a route
505
+ * 2. Builds the React element tree (page + layouts)
506
+ * 3. Calls `renderToReadableStream` from @vitejs/plugin-rsc/rsc
507
+ * 4. Returns the RSC flight ReadableStream
508
+ *
509
+ * The RSC plugin automatically handles "use client" directives, replacing
510
+ * client components with client reference proxies in this environment.
511
+ */
512
+ function generateRscEntry(routes, appDir, root, i18nConfig = null, statusPages = []) {
513
+ const allLayoutPaths = [];
514
+ const routeLayoutMap = new Map();
515
+ for (const route of routes) {
516
+ const layouts = collectLayouts(route.filePath, appDir);
517
+ routeLayoutMap.set(route.pattern, layouts);
518
+ for (const lp of layouts) {
519
+ if (!allLayoutPaths.includes(lp)) {
520
+ allLayoutPaths.push(lp);
521
+ }
522
+ }
523
+ }
524
+ const lines = [
525
+ "// Auto-generated RSC entry by catmint build",
526
+ "// Runs in the RSC environment with react-server resolve conditions",
527
+ "",
528
+ "import { createElement } from 'react'",
529
+ "import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc'",
530
+ "import { AppProviders } from 'catmint/runtime/app-providers'",
531
+ "",
532
+ ];
533
+ // Import all page components (default export + full module for generateMetadata)
534
+ for (let i = 0; i < routes.length; i++) {
535
+ const vitePath = toVitePath(routes[i].filePath, root);
536
+ lines.push(`import Page_${i}, * as PageMod_${i} from '${vitePath}'`);
537
+ }
538
+ lines.push("");
539
+ // Import all layout components (default export + full module for generateMetadata)
540
+ for (let i = 0; i < allLayoutPaths.length; i++) {
541
+ const vitePath = toVitePath(allLayoutPaths[i], root);
542
+ lines.push(`import Layout_${i}, * as LayoutMod_${i} from '${vitePath}'`);
543
+ }
544
+ lines.push("");
545
+ // Create the route map (includes module references for generateMetadata)
546
+ lines.push("const routeMap = {");
547
+ for (let i = 0; i < routes.length; i++) {
548
+ const route = routes[i];
549
+ const layouts = routeLayoutMap.get(route.pattern) ?? [];
550
+ const layoutIndices = layouts.map((lp) => allLayoutPaths.indexOf(lp));
551
+ lines.push(` ${JSON.stringify(route.pattern)}: {`);
552
+ lines.push(` page: Page_${i},`);
553
+ lines.push(` pageMod: PageMod_${i},`);
554
+ lines.push(` layouts: [${layoutIndices.map((j) => `Layout_${j}`).join(", ")}],`);
555
+ lines.push(` layoutMods: [${layoutIndices.map((j) => `LayoutMod_${j}`).join(", ")}],`);
556
+ lines.push(` },`);
557
+ }
558
+ lines.push("}");
559
+ // Serialize i18n config for injection into generated code
560
+ const i18nConfigJson = i18nConfig ? JSON.stringify(i18nConfig) : "null";
561
+ // Route matching + render function (with generateMetadata support)
562
+ lines.push(`
563
+ ${ROUTE_MATCH_CODE}
564
+
565
+ var __i18nConfig = ${i18nConfigJson}
566
+
567
+ /**
568
+ * Merge multiple HeadConfig objects (last wins for title and meta keys).
569
+ */
570
+ function __mergeHeadConfigs(configs) {
571
+ var merged = {}
572
+ var metaMap = new Map()
573
+ var links = []
574
+
575
+ for (var i = 0; i < configs.length; i++) {
576
+ var config = configs[i]
577
+ if (config.title !== undefined) merged.title = config.title
578
+ if (config.meta) {
579
+ for (var j = 0; j < config.meta.length; j++) {
580
+ var meta = config.meta[j]
581
+ var key = meta.name ? 'name:' + meta.name : meta.property ? 'property:' + meta.property : 'content:' + meta.content
582
+ metaMap.set(key, meta)
583
+ }
584
+ }
585
+ if (config.link) links = links.concat(config.link)
586
+ }
587
+
588
+ if (metaMap.size > 0) merged.meta = Array.from(metaMap.values())
589
+ if (links.length > 0) merged.link = links
590
+
591
+ return merged
592
+ }
593
+
594
+ /**
595
+ * Resolve generateMetadata exports from page and layout modules.
596
+ * Layouts first (outermost to innermost), page last (page wins on conflict).
597
+ */
598
+ async function __resolveGenerateMetadata(pageMod, layoutMods, params, url) {
599
+ var urlObj = new URL(url, 'http://localhost')
600
+ var search = {}
601
+ for (var [key, value] of urlObj.searchParams.entries()) {
602
+ search[key] = value
603
+ }
604
+
605
+ var metadataArgs = { params: params || {}, search: search }
606
+ var configs = []
607
+
608
+ for (var i = 0; i < layoutMods.length; i++) {
609
+ if (typeof layoutMods[i].generateMetadata === 'function') {
610
+ try {
611
+ var config = await layoutMods[i].generateMetadata(metadataArgs)
612
+ if (config && typeof config === 'object') configs.push(config)
613
+ } catch (e) { /* skip */ }
614
+ }
615
+ }
616
+
617
+ if (typeof pageMod.generateMetadata === 'function') {
618
+ try {
619
+ var config = await pageMod.generateMetadata(metadataArgs)
620
+ if (config && typeof config === 'object') configs.push(config)
621
+ } catch (e) { /* skip */ }
622
+ }
623
+
624
+ if (configs.length === 0) return undefined
625
+ return __mergeHeadConfigs(configs)
626
+ }
627
+
628
+ /**
629
+ * Render a URL to an RSC flight ReadableStream with optional head metadata.
630
+ *
631
+ * @param {string} url - The URL pathname to render
632
+ * @returns {Promise<{ stream: ReadableStream<Uint8Array>, headConfig?: object } | null>}
633
+ */
634
+ export async function render(url) {
635
+ const pathname = new URL(url, 'http://localhost').pathname
636
+ const matched = matchRoute(pathname)
637
+
638
+ if (!matched) return null
639
+
640
+ const { route, params } = matched
641
+ const { page: Page, pageMod, layouts, layoutMods } = route
642
+
643
+ // Resolve generateMetadata from page + layout modules
644
+ var headConfig = await __resolveGenerateMetadata(pageMod, layoutMods, params, url)
645
+
646
+ // Build component tree: Layout0(Layout1(...(Page)))
647
+ let element = createElement(Page, null)
648
+ for (let i = layouts.length - 1; i >= 0; i--) {
649
+ element = createElement(layouts[i], null, element)
650
+ }
651
+
652
+ // Wrap with framework providers (AppProviders is a client reference in RSC env)
653
+ var providerProps = { pathname: pathname, params: params }
654
+ if (__i18nConfig) providerProps.i18nConfig = __i18nConfig
655
+ if (headConfig) providerProps.headConfig = headConfig
656
+ element = createElement(AppProviders, providerProps, element)
657
+
658
+ return { stream: renderToReadableStream(element), headConfig: headConfig }
659
+ }
660
+
661
+ /**
662
+ * Check if a URL matches any page route.
663
+ */
664
+ export function hasRoute(url) {
665
+ const pathname = new URL(url, 'http://localhost').pathname
666
+ return matchRoute(pathname) !== null
667
+ }
668
+ `);
669
+ // --- Status page rendering ---
670
+ if (statusPages.length > 0) {
671
+ lines.push("");
672
+ lines.push("// Status page imports");
673
+ // Import each status page component (default + module namespace for generateMetadata)
674
+ for (let i = 0; i < statusPages.length; i++) {
675
+ const vitePath = toVitePath(statusPages[i].filePath, root);
676
+ lines.push(`import StatusPage_${i}, * as StatusPageMod_${i} from '${vitePath}'`);
677
+ }
678
+ lines.push("");
679
+ // Collect layouts for each status page
680
+ const statusPageLayoutMap = new Map();
681
+ for (let i = 0; i < statusPages.length; i++) {
682
+ const layouts = collectLayouts(statusPages[i].filePath, appDir);
683
+ statusPageLayoutMap.set(i, layouts);
684
+ // Add any new layout paths that aren't already tracked
685
+ for (const lp of layouts) {
686
+ if (!allLayoutPaths.includes(lp)) {
687
+ const vitePath = toVitePath(lp, root);
688
+ lines.push(`import Layout_${allLayoutPaths.length}, * as LayoutMod_${allLayoutPaths.length} from '${vitePath}'`);
689
+ allLayoutPaths.push(lp);
690
+ }
691
+ }
692
+ }
693
+ lines.push("");
694
+ // Build the status page map: { [statusCode]: [{ urlPrefix, component, componentMod, layouts, layoutMods }] }
695
+ // Entries are ordered most-specific first (longest urlPrefix first)
696
+ lines.push("const statusPageMap = {");
697
+ const byCode = new Map();
698
+ for (let i = 0; i < statusPages.length; i++) {
699
+ const sp = statusPages[i];
700
+ if (!byCode.has(sp.statusCode))
701
+ byCode.set(sp.statusCode, []);
702
+ byCode.get(sp.statusCode).push({ index: i, urlPrefix: sp.urlPrefix });
703
+ }
704
+ for (const [code, entries] of byCode) {
705
+ // Already sorted most-specific first from scanStatusPages
706
+ lines.push(` ${code}: [`);
707
+ for (const entry of entries) {
708
+ const layouts = statusPageLayoutMap.get(entry.index) ?? [];
709
+ const layoutIndices = layouts.map((lp) => allLayoutPaths.indexOf(lp));
710
+ lines.push(` {`);
711
+ lines.push(` urlPrefix: ${JSON.stringify(entry.urlPrefix)},`);
712
+ lines.push(` component: StatusPage_${entry.index},`);
713
+ lines.push(` componentMod: StatusPageMod_${entry.index},`);
714
+ lines.push(` layouts: [${layoutIndices.map((j) => `Layout_${j}`).join(", ")}],`);
715
+ lines.push(` layoutMods: [${layoutIndices.map((j) => `LayoutMod_${j}`).join(", ")}],`);
716
+ lines.push(` },`);
717
+ }
718
+ lines.push(` ],`);
719
+ }
720
+ lines.push("}");
721
+ lines.push(`
722
+
723
+ /**
724
+ * Render a status page to an RSC flight ReadableStream with optional head metadata.
725
+ *
726
+ * Finds the most specific status page for the given code and pathname,
727
+ * renders it through the RSC pipeline with layouts and AppProviders.
728
+ *
729
+ * @param {number} statusCode - HTTP status code (e.g. 404, 500)
730
+ * @param {string} pathname - The request pathname (for resolution + provider props)
731
+ * @param {object} [data] - Optional data to pass as props to the status page
732
+ * @returns {Promise<{ stream: ReadableStream<Uint8Array>, headConfig?: object } | null>}
733
+ */
734
+ export async function renderStatusPage(statusCode, pathname, data) {
735
+ var candidates = statusPageMap[statusCode]
736
+ if (!candidates || candidates.length === 0) return null
737
+
738
+ // Find the most specific status page whose urlPrefix matches the pathname
739
+ var match = null
740
+ for (var i = 0; i < candidates.length; i++) {
741
+ var c = candidates[i]
742
+ if (c.urlPrefix === "/" || pathname.startsWith(c.urlPrefix + "/") || pathname === c.urlPrefix) {
743
+ match = c
744
+ break
745
+ }
746
+ }
747
+ if (!match) return null
748
+
749
+ var StatusPage = match.component
750
+ var layouts = match.layouts
751
+
752
+ // Resolve generateMetadata from status page + layout modules
753
+ var headConfig = await __resolveGenerateMetadata(match.componentMod, match.layoutMods, {}, pathname)
754
+
755
+ // Build component tree with status props
756
+ var statusProps = { status: statusCode }
757
+ if (data) { for (var k in data) statusProps[k] = data[k] }
758
+ var element = createElement(StatusPage, statusProps)
759
+
760
+ // Wrap with layouts
761
+ for (var i = layouts.length - 1; i >= 0; i--) {
762
+ element = createElement(layouts[i], null, element)
763
+ }
764
+
765
+ // Wrap with AppProviders
766
+ var providerProps = { pathname: pathname, params: {} }
767
+ if (__i18nConfig) providerProps.i18nConfig = __i18nConfig
768
+ if (headConfig) providerProps.headConfig = headConfig
769
+ element = createElement(AppProviders, providerProps, element)
770
+
771
+ return { stream: renderToReadableStream(element), headConfig: headConfig }
772
+ }
773
+ `);
774
+ }
775
+ else {
776
+ lines.push(`
777
+ export function renderStatusPage() { return null }
778
+ `);
779
+ }
780
+ return lines.join("\n");
781
+ }
782
+ /**
783
+ * Generate the SSR environment entry for RSC mode.
784
+ *
785
+ * This module runs in the SSR environment. It exports:
786
+ * - `renderToHtml(rscStream)` — takes an RSC flight stream, converts it
787
+ * to HTML via react-dom/server, and injects the RSC payload for client hydration
788
+ * - Endpoint handling functions (handleEndpoint, hasEndpoint)
789
+ *
790
+ * The RSC plugin handles "use client" references via its SSR module transforms,
791
+ * so client components are resolved to their correct chunk references.
792
+ */
793
+ function generateSsrRscEntry(endpoints, root, serverFnFiles) {
794
+ const lines = [
795
+ "// Auto-generated SSR-RSC entry by catmint build",
796
+ "// Runs in the SSR environment for RSC → HTML conversion",
797
+ "",
798
+ "import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'",
799
+ "import { renderToReadableStream } from 'react-dom/server.edge'",
800
+ "import { injectRSCPayload } from 'rsc-html-stream/server'",
801
+ "import assetsManifest from 'virtual:vite-rsc/assets-manifest'",
802
+ "",
803
+ ];
804
+ // Import server function modules at the top (alongside other imports)
805
+ if (serverFnFiles.length > 0) {
806
+ lines.push("// Server function module imports");
807
+ for (let i = 0; i < serverFnFiles.length; i++) {
808
+ const vitePath = toVitePath(serverFnFiles[i].filePath, root);
809
+ lines.push(`import * as ServerFn_${i} from '${vitePath}'`);
810
+ }
811
+ lines.push("");
812
+ }
813
+ // Export the RSC → HTML rendering function
814
+ lines.push(`
815
+ /**
816
+ * Escape a string for safe insertion into an HTML attribute value.
817
+ */
818
+ function __escapeHtmlAttr(str) {
819
+ return str
820
+ .replace(/&/g, '&amp;')
821
+ .replace(/"/g, '&quot;')
822
+ .replace(/</g, '&lt;')
823
+ .replace(/>/g, '&gt;')
824
+ }
825
+
826
+ /**
827
+ * Serialize a HeadConfig into HTML tags for injection into <head>.
828
+ */
829
+ function __renderHeadToString(config) {
830
+ var parts = []
831
+
832
+ if (config.title !== undefined) {
833
+ parts.push('<title>' + __escapeHtmlAttr(config.title) + '</title>')
834
+ }
835
+
836
+ if (config.meta) {
837
+ for (var i = 0; i < config.meta.length; i++) {
838
+ var meta = config.meta[i]
839
+ var attrs = []
840
+ if (meta.name) attrs.push('name="' + __escapeHtmlAttr(meta.name) + '"')
841
+ if (meta.property) attrs.push('property="' + __escapeHtmlAttr(meta.property) + '"')
842
+ attrs.push('content="' + __escapeHtmlAttr(meta.content) + '"')
843
+ parts.push('<meta ' + attrs.join(' ') + '>')
844
+ }
845
+ }
846
+
847
+ if (config.link) {
848
+ for (var i = 0; i < config.link.length; i++) {
849
+ var linkDef = config.link[i]
850
+ var attrs = [
851
+ 'rel="' + __escapeHtmlAttr(linkDef.rel) + '"',
852
+ 'href="' + __escapeHtmlAttr(linkDef.href) + '"',
853
+ ]
854
+ for (var key in linkDef) {
855
+ if (key !== 'rel' && key !== 'href') {
856
+ attrs.push(key + '="' + __escapeHtmlAttr(linkDef[key]) + '"')
857
+ }
858
+ }
859
+ parts.push('<link ' + attrs.join(' ') + '>')
860
+ }
861
+ }
862
+
863
+ return parts.join('\\n')
864
+ }
865
+
866
+ /**
867
+ * Strip existing <title> from SSR-rendered HTML so the framework-injected
868
+ * title from generateMetadata takes precedence.
869
+ */
870
+ function __stripExistingHeadTags(html, config) {
871
+ var result = html
872
+ if (config.title !== undefined) {
873
+ result = result.replace(/<title>[^<]*<\\/title>/i, '')
874
+ }
875
+ return result
876
+ }
877
+
878
+ /**
879
+ * Convert an RSC flight stream to an HTML ReadableStream.
880
+ *
881
+ * 1. Tees the RSC stream — one fork for SSR, one for browser injection
882
+ * 2. createFromReadableStream → React VDOM
883
+ * 3. renderToReadableStream → HTML stream (with bootstrapScriptContent for client JS)
884
+ * 4. injectRSCPayload → embeds RSC payload as <script> tags in HTML
885
+ * 5. If headConfig is provided, injects <title>/<meta>/<link> tags before </head>
886
+ *
887
+ * The bootstrapScriptContent comes from the RSC assets manifest and tells the
888
+ * browser to load the client entry JS module for hydration.
889
+ *
890
+ * @param {ReadableStream} rscStream - The RSC flight stream
891
+ * @param {object} [headConfig] - Optional HeadConfig to inject into <head>
892
+ * @returns {Promise<ReadableStream>} HTML stream with embedded RSC payload
893
+ */
894
+ export async function renderToHtml(rscStream, headConfig) {
895
+ const [rscForSsr, rscForBrowser] = rscStream.tee()
896
+ const root = createFromReadableStream(rscForSsr)
897
+ const htmlStream = await renderToReadableStream(root, {
898
+ bootstrapScriptContent: assetsManifest.bootstrapScriptContent,
899
+ })
900
+ var finalStream = htmlStream.pipeThrough(injectRSCPayload(rscForBrowser))
901
+
902
+ // If headConfig is provided, inject head tags before </head>
903
+ if (headConfig) {
904
+ var headTags = __renderHeadToString(headConfig)
905
+ var decoder = new TextDecoder()
906
+ var encoder = new TextEncoder()
907
+ var injected = false
908
+ var buffer = ''
909
+
910
+ finalStream = finalStream.pipeThrough(new TransformStream({
911
+ transform(chunk, controller) {
912
+ if (injected) {
913
+ controller.enqueue(chunk)
914
+ return
915
+ }
916
+ buffer += decoder.decode(chunk, { stream: true })
917
+ var idx = buffer.indexOf('</head>')
918
+ if (idx !== -1) {
919
+ injected = true
920
+ var before = __stripExistingHeadTags(buffer.slice(0, idx), headConfig)
921
+ var after = buffer.slice(idx)
922
+ controller.enqueue(encoder.encode(before + headTags + '\\n' + after))
923
+ buffer = ''
924
+ }
925
+ },
926
+ flush(controller) {
927
+ if (!injected && buffer) {
928
+ controller.enqueue(encoder.encode(buffer + headTags))
929
+ } else if (buffer) {
930
+ controller.enqueue(encoder.encode(buffer))
931
+ }
932
+ }
933
+ }))
934
+ }
935
+
936
+ return finalStream
937
+ }
938
+ `);
939
+ // Route matching function (needed for endpoint matching)
940
+ lines.push(ROUTE_MATCH_CODE);
941
+ // --- Endpoint handling ---
942
+ if (endpoints.length > 0) {
943
+ lines.push("");
944
+ for (let i = 0; i < endpoints.length; i++) {
945
+ const vitePath = toVitePath(endpoints[i].filePath, root);
946
+ lines.push(`import * as Endpoint_${i} from '${vitePath}'`);
947
+ }
948
+ lines.push("");
949
+ lines.push("const endpointMap = {");
950
+ for (let i = 0; i < endpoints.length; i++) {
951
+ const ep = endpoints[i];
952
+ lines.push(` ${JSON.stringify(ep.pattern)}: {`);
953
+ lines.push(` mod: Endpoint_${i},`);
954
+ lines.push(` methods: ${JSON.stringify(ep.methods)},`);
955
+ lines.push(` },`);
956
+ }
957
+ lines.push("}");
958
+ lines.push(`
959
+
960
+ function matchEndpoint(pathname) {
961
+ if (endpointMap[pathname]) return { endpoint: endpointMap[pathname], params: {} }
962
+ for (const [pattern, endpoint] of Object.entries(endpointMap)) {
963
+ const params = matchPattern(pattern, pathname)
964
+ if (params) return { endpoint, params }
965
+ }
966
+ return null
967
+ }
968
+
969
+ export async function handleEndpoint(url, method, request) {
970
+ const pathname = new URL(url, 'http://localhost').pathname
971
+ const matched = matchEndpoint(pathname)
972
+
973
+ if (!matched) return null
974
+
975
+ const { endpoint, params } = matched
976
+ const mod = endpoint.mod
977
+
978
+ const handler = mod[method] || mod.ANY || mod.default
979
+ if (!handler) return null
980
+
981
+ const ctx = { params }
982
+ const response = await handler(request, ctx)
983
+ return { response }
984
+ }
985
+
986
+ export function hasEndpoint(url) {
987
+ const pathname = new URL(url, 'http://localhost').pathname
988
+ return matchEndpoint(pathname) !== null
989
+ }
990
+ `);
991
+ }
992
+ else {
993
+ lines.push(`
994
+ export async function handleEndpoint() { return null }
995
+ export function hasEndpoint() { return false }
996
+ `);
997
+ }
998
+ // --- Server function RPC handling ---
999
+ if (serverFnFiles.length > 0) {
1000
+ lines.push("");
1001
+ // Build the hash → function lookup map
1002
+ lines.push("// Hash → function lookup for server function RPC");
1003
+ lines.push("const serverFnMap = {");
1004
+ for (let i = 0; i < serverFnFiles.length; i++) {
1005
+ const file = serverFnFiles[i];
1006
+ const relativePath = toPosixPath(relative(root, file.filePath));
1007
+ for (const exportName of file.exports) {
1008
+ const hash = deterministicHash(`${relativePath}:${exportName}`);
1009
+ const basePath = relativePath.replace(/\.(fn\.ts|fn\.tsx?)$/, "");
1010
+ const key = `${basePath}/${hash}`;
1011
+ if (exportName === "default") {
1012
+ lines.push(` ${JSON.stringify(key)}: ServerFn_${i}.default,`);
1013
+ }
1014
+ else {
1015
+ lines.push(` ${JSON.stringify(key)}: ServerFn_${i}.${exportName},`);
1016
+ }
1017
+ }
1018
+ }
1019
+ lines.push("}");
1020
+ lines.push(`
1021
+
1022
+ /**
1023
+ * Handle a server function RPC call.
1024
+ *
1025
+ * @param {string} pathname - The URL pathname (e.g. '/__catmint/fn/app/examples/server-fn/actions/c875b945')
1026
+ * @param {unknown} body - The parsed request body (JSON input to the function)
1027
+ * @returns {Promise<{ result: unknown } | null>} The function result, or null if no match
1028
+ */
1029
+ export async function handleServerFn(pathname, body) {
1030
+ const prefix = '/__catmint/fn/'
1031
+ if (!pathname.startsWith(prefix)) return null
1032
+
1033
+ const key = pathname.slice(prefix.length)
1034
+ const fn = serverFnMap[key]
1035
+ if (!fn) return null
1036
+ if (typeof fn !== 'function') return null
1037
+
1038
+ const result = await fn(body)
1039
+ return { result }
1040
+ }
1041
+ `);
1042
+ }
1043
+ else {
1044
+ lines.push(`
1045
+ export async function handleServerFn() { return null }
1046
+ `);
1047
+ }
1048
+ return lines.join("\n");
1049
+ }
1050
+ /**
1051
+ * Generate the client entry for RSC mode.
1052
+ *
1053
+ * This module runs in the browser. It:
1054
+ * 1. Reads the embedded RSC payload from the HTML via rsc-html-stream/client
1055
+ * 2. Deserializes it via createFromReadableStream from @vitejs/plugin-rsc/browser
1056
+ * 3. Hydrates the document via hydrateRoot from react-dom/client
1057
+ * 4. If soft navigation is enabled, installs click handlers to intercept
1058
+ * relative link clicks and perform RSC-powered client-side transitions
1059
+ *
1060
+ * The RSC plugin handles client reference resolution automatically — when
1061
+ * the browser encounters a client reference in the RSC payload, it loads
1062
+ * the appropriate client component chunk.
1063
+ */
1064
+ function generateClientRscEntry(softNavigation) {
1065
+ return `// Auto-generated client RSC entry by catmint build
1066
+ // Hydrates from embedded RSC payload in the HTML
1067
+
1068
+ import { rscStream } from 'rsc-html-stream/client'
1069
+ import { createFromReadableStream } from '@vitejs/plugin-rsc/browser'
1070
+ import { hydrateRoot } from 'react-dom/client'
1071
+
1072
+ let root = null
1073
+
1074
+ ${CLIENT_NAVIGATION_RUNTIME}
1075
+
1076
+ async function hydrate() {
1077
+ const initialRoot = await createFromReadableStream(rscStream)
1078
+ root = hydrateRoot(document, initialRoot)
1079
+ }
1080
+
1081
+ hydrate().then(function() {
1082
+ if (${softNavigation ? "true" : "false"}) {
1083
+ setupClientNavigation(root)
1084
+ }
1085
+ })
1086
+ `;
1087
+ }
1088
+ // --------------------------------------------------------------------------
1089
+ // Shared route matching code (used by both RSC and legacy entries)
1090
+ // --------------------------------------------------------------------------
1091
+ /**
1092
+ * Route matching JavaScript code shared between entry generators.
1093
+ * Extracted to avoid duplication.
1094
+ */
1095
+ const ROUTE_MATCH_CODE = `
1096
+ function matchRoute(pathname) {
1097
+ if (routeMap[pathname]) return { route: routeMap[pathname], params: {} }
1098
+ for (const [pattern, route] of Object.entries(routeMap)) {
1099
+ const params = matchPattern(pattern, pathname)
1100
+ if (params) return { route, params }
1101
+ }
1102
+ return null
1103
+ }
1104
+
1105
+ function matchPattern(pattern, pathname) {
1106
+ const patternParts = pattern.split('/').filter(Boolean)
1107
+ const pathParts = pathname.split('/').filter(Boolean)
1108
+ const params = {}
1109
+
1110
+ for (let i = 0; i < patternParts.length; i++) {
1111
+ const pp = patternParts[i]
1112
+ if (pp.startsWith('*')) {
1113
+ const paramName = pp.endsWith('?') ? pp.slice(1, -1) : pp.slice(1)
1114
+ params[paramName] = pathParts.slice(i)
1115
+ return params
1116
+ }
1117
+ if (pp.startsWith(':')) {
1118
+ if (i >= pathParts.length) return null
1119
+ params[pp.slice(1)] = pathParts[i]
1120
+ continue
1121
+ }
1122
+ if (i >= pathParts.length || pp !== pathParts[i]) return null
1123
+ }
1124
+
1125
+ if (patternParts.length < pathParts.length) return null
1126
+ if (patternParts.length > pathParts.length) {
1127
+ const remaining = patternParts[pathParts.length]
1128
+ if (remaining && remaining.startsWith('*') && remaining.endsWith('?')) {
1129
+ const paramName = remaining.slice(1, -1)
1130
+ params[paramName] = []
1131
+ return params
1132
+ }
1133
+ return null
1134
+ }
1135
+
1136
+ return params
1137
+ }
1138
+ `;
1139
+ //# sourceMappingURL=build-entries.js.map