@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1fa245e2

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 (136) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +76 -18
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +558 -319
  5. package/package.json +16 -15
  6. package/skills/cache-guide/SKILL.md +32 -0
  7. package/skills/caching/SKILL.md +45 -4
  8. package/skills/links/SKILL.md +3 -1
  9. package/skills/loader/SKILL.md +53 -43
  10. package/skills/middleware/SKILL.md +2 -0
  11. package/skills/parallel/SKILL.md +126 -0
  12. package/skills/prerender/SKILL.md +110 -68
  13. package/skills/route/SKILL.md +31 -0
  14. package/skills/router-setup/SKILL.md +87 -2
  15. package/skills/typesafety/SKILL.md +10 -0
  16. package/src/__internal.ts +1 -1
  17. package/src/browser/app-version.ts +14 -0
  18. package/src/browser/event-controller.ts +5 -0
  19. package/src/browser/navigation-bridge.ts +19 -13
  20. package/src/browser/navigation-client.ts +115 -58
  21. package/src/browser/navigation-store.ts +43 -8
  22. package/src/browser/navigation-transaction.ts +11 -9
  23. package/src/browser/partial-update.ts +80 -15
  24. package/src/browser/prefetch/cache.ts +57 -5
  25. package/src/browser/prefetch/fetch.ts +38 -23
  26. package/src/browser/prefetch/queue.ts +92 -20
  27. package/src/browser/prefetch/resource-ready.ts +77 -0
  28. package/src/browser/react/Link.tsx +53 -9
  29. package/src/browser/react/NavigationProvider.tsx +40 -4
  30. package/src/browser/react/context.ts +7 -2
  31. package/src/browser/react/use-handle.ts +9 -58
  32. package/src/browser/react/use-router.ts +21 -8
  33. package/src/browser/rsc-router.tsx +134 -59
  34. package/src/browser/scroll-restoration.ts +41 -42
  35. package/src/browser/segment-reconciler.ts +6 -1
  36. package/src/browser/server-action-bridge.ts +8 -6
  37. package/src/browser/types.ts +36 -5
  38. package/src/build/generate-manifest.ts +6 -6
  39. package/src/build/generate-route-types.ts +3 -0
  40. package/src/build/route-types/include-resolution.ts +8 -1
  41. package/src/build/route-types/router-processing.ts +223 -74
  42. package/src/build/route-types/scan-filter.ts +8 -1
  43. package/src/cache/cache-runtime.ts +15 -11
  44. package/src/cache/cache-scope.ts +48 -7
  45. package/src/cache/cf/cf-cache-store.ts +453 -11
  46. package/src/cache/cf/index.ts +5 -1
  47. package/src/cache/document-cache.ts +17 -7
  48. package/src/cache/index.ts +1 -0
  49. package/src/cache/taint.ts +55 -0
  50. package/src/client.tsx +2 -56
  51. package/src/context-var.ts +72 -2
  52. package/src/debug.ts +2 -2
  53. package/src/handle.ts +40 -0
  54. package/src/index.rsc.ts +3 -1
  55. package/src/index.ts +8 -0
  56. package/src/prerender/store.ts +5 -4
  57. package/src/prerender.ts +138 -77
  58. package/src/reverse.ts +22 -1
  59. package/src/route-definition/dsl-helpers.ts +73 -25
  60. package/src/route-definition/helpers-types.ts +10 -6
  61. package/src/route-definition/index.ts +3 -0
  62. package/src/route-definition/redirect.ts +11 -3
  63. package/src/route-definition/resolve-handler-use.ts +149 -0
  64. package/src/route-map-builder.ts +7 -1
  65. package/src/route-types.ts +11 -0
  66. package/src/router/content-negotiation.ts +100 -1
  67. package/src/router/find-match.ts +4 -2
  68. package/src/router/handler-context.ts +79 -23
  69. package/src/router/intercept-resolution.ts +11 -4
  70. package/src/router/lazy-includes.ts +4 -1
  71. package/src/router/loader-resolution.ts +122 -10
  72. package/src/router/logging.ts +5 -2
  73. package/src/router/manifest.ts +9 -3
  74. package/src/router/match-api.ts +124 -189
  75. package/src/router/match-middleware/background-revalidation.ts +30 -2
  76. package/src/router/match-middleware/cache-lookup.ts +88 -16
  77. package/src/router/match-middleware/cache-store.ts +53 -10
  78. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  79. package/src/router/match-middleware/segment-resolution.ts +61 -5
  80. package/src/router/match-result.ts +22 -6
  81. package/src/router/metrics.ts +6 -1
  82. package/src/router/middleware-types.ts +6 -8
  83. package/src/router/middleware.ts +4 -6
  84. package/src/router/navigation-snapshot.ts +182 -0
  85. package/src/router/prerender-match.ts +110 -10
  86. package/src/router/preview-match.ts +30 -102
  87. package/src/router/request-classification.ts +310 -0
  88. package/src/router/route-snapshot.ts +245 -0
  89. package/src/router/router-context.ts +6 -1
  90. package/src/router/router-interfaces.ts +36 -4
  91. package/src/router/router-options.ts +37 -11
  92. package/src/router/segment-resolution/fresh.ts +183 -20
  93. package/src/router/segment-resolution/helpers.ts +29 -24
  94. package/src/router/segment-resolution/loader-cache.ts +1 -0
  95. package/src/router/segment-resolution/revalidation.ts +412 -297
  96. package/src/router/segment-wrappers.ts +2 -0
  97. package/src/router/types.ts +1 -0
  98. package/src/router.ts +59 -6
  99. package/src/rsc/handler.ts +460 -368
  100. package/src/rsc/manifest-init.ts +5 -1
  101. package/src/rsc/progressive-enhancement.ts +4 -0
  102. package/src/rsc/rsc-rendering.ts +5 -0
  103. package/src/rsc/server-action.ts +2 -0
  104. package/src/rsc/ssr-setup.ts +2 -2
  105. package/src/rsc/types.ts +8 -1
  106. package/src/segment-system.tsx +140 -4
  107. package/src/server/context.ts +140 -14
  108. package/src/server/loader-registry.ts +9 -8
  109. package/src/server/request-context.ts +144 -18
  110. package/src/ssr/index.tsx +4 -0
  111. package/src/static-handler.ts +18 -6
  112. package/src/types/cache-types.ts +4 -4
  113. package/src/types/handler-context.ts +137 -33
  114. package/src/types/loader-types.ts +36 -9
  115. package/src/types/route-entry.ts +8 -1
  116. package/src/types/segments.ts +2 -0
  117. package/src/urls/path-helper-types.ts +9 -2
  118. package/src/urls/path-helper.ts +48 -13
  119. package/src/urls/pattern-types.ts +12 -0
  120. package/src/urls/response-types.ts +16 -6
  121. package/src/use-loader.tsx +73 -4
  122. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  123. package/src/vite/discovery/discover-routers.ts +5 -1
  124. package/src/vite/discovery/prerender-collection.ts +14 -1
  125. package/src/vite/discovery/state.ts +13 -6
  126. package/src/vite/index.ts +4 -0
  127. package/src/vite/plugin-types.ts +51 -79
  128. package/src/vite/plugins/expose-action-id.ts +1 -3
  129. package/src/vite/plugins/performance-tracks.ts +88 -0
  130. package/src/vite/plugins/refresh-cmd.ts +88 -26
  131. package/src/vite/plugins/version-plugin.ts +13 -1
  132. package/src/vite/rango.ts +163 -211
  133. package/src/vite/router-discovery.ts +153 -42
  134. package/src/vite/utils/banner.ts +3 -3
  135. package/src/vite/utils/prerender-utils.ts +18 -0
  136. package/src/vite/utils/shared-utils.ts +3 -2
@@ -292,7 +292,7 @@ function exposeActionId() {
292
292
  }
293
293
  if (!rscPluginApi) {
294
294
  throw new Error(
295
- "[rsc-router] Could not find @vitejs/plugin-rsc. @rangojs/router requires the Vite RSC plugin.\nThe RSC plugin should be included automatically. If you disabled it with\nrango({ rsc: false }), add rsc() before rango() in your config."
295
+ "[rsc-router] Could not find @vitejs/plugin-rsc. @rangojs/router requires the Vite RSC plugin, which is included automatically by rango()."
296
296
  );
297
297
  }
298
298
  if (!isBuild) return;
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
1745
1745
  // package.json
1746
1746
  var package_default = {
1747
1747
  name: "@rangojs/router",
1748
- version: "0.0.0-experimental.1b930379",
1748
+ version: "0.0.0-experimental.1fa245e2",
1749
1749
  description: "Django-inspired RSC router with composable URL patterns",
1750
1750
  keywords: [
1751
1751
  "react",
@@ -1887,7 +1887,7 @@ var package_default = {
1887
1887
  "test:unit:watch": "vitest"
1888
1888
  },
1889
1889
  dependencies: {
1890
- "@vitejs/plugin-rsc": "^0.5.14",
1890
+ "@vitejs/plugin-rsc": "^0.5.19",
1891
1891
  "magic-string": "^0.30.17",
1892
1892
  picomatch: "^4.0.3",
1893
1893
  "rsc-html-stream": "^0.0.7"
@@ -2095,31 +2095,7 @@ declare global {
2095
2095
  }
2096
2096
 
2097
2097
  // src/build/route-types/scan-filter.ts
2098
- import { join, relative } from "node:path";
2099
2098
  import picomatch from "picomatch";
2100
- var DEFAULT_EXCLUDE_PATTERNS = [
2101
- "**/__tests__/**",
2102
- "**/__mocks__/**",
2103
- "**/dist/**",
2104
- "**/coverage/**",
2105
- "**/*.test.{ts,tsx,js,jsx}",
2106
- "**/*.spec.{ts,tsx,js,jsx}"
2107
- ];
2108
- function createScanFilter(root, opts) {
2109
- const { include, exclude } = opts;
2110
- const hasInclude = include && include.length > 0;
2111
- const hasCustomExclude = exclude !== void 0;
2112
- if (!hasInclude && !hasCustomExclude) return void 0;
2113
- const effectiveExclude = exclude ?? DEFAULT_EXCLUDE_PATTERNS;
2114
- const includeMatcher = hasInclude ? picomatch(include) : null;
2115
- const excludeMatcher = effectiveExclude.length > 0 ? picomatch(effectiveExclude) : null;
2116
- return (absolutePath) => {
2117
- const rel = relative(root, absolutePath);
2118
- if (excludeMatcher && excludeMatcher(rel)) return false;
2119
- if (includeMatcher) return includeMatcher(rel);
2120
- return true;
2121
- };
2122
- }
2123
2099
 
2124
2100
  // src/build/route-types/per-module-writer.ts
2125
2101
  import ts4 from "typescript";
@@ -2341,7 +2317,7 @@ function buildRouteMapFromBlock(block, fullSource, filePath, visited, searchSche
2341
2317
  }
2342
2318
  return routeMap;
2343
2319
  }
2344
- function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut) {
2320
+ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagnosticsOut, inlineBlock) {
2345
2321
  visited = visited ?? /* @__PURE__ */ new Set();
2346
2322
  const realPath = resolve2(filePath);
2347
2323
  const key = variableName ? `${realPath}:${variableName}` : realPath;
@@ -2357,7 +2333,9 @@ function buildCombinedRouteMapWithSearch(filePath, variableName, visited, diagno
2357
2333
  return { routes: {}, searchSchemas: {} };
2358
2334
  }
2359
2335
  let block;
2360
- if (variableName) {
2336
+ if (inlineBlock) {
2337
+ block = inlineBlock;
2338
+ } else if (variableName) {
2361
2339
  const extracted = extractUrlsBlockForVariable(source, variableName);
2362
2340
  if (!extracted) return { routes: {}, searchSchemas: {} };
2363
2341
  block = extracted;
@@ -2386,7 +2364,7 @@ import {
2386
2364
  readdirSync
2387
2365
  } from "node:fs";
2388
2366
  import {
2389
- join as join2,
2367
+ join,
2390
2368
  dirname as dirname2,
2391
2369
  resolve as resolve3,
2392
2370
  sep,
@@ -2406,7 +2384,7 @@ function countPublicRouteEntries(source) {
2406
2384
  }
2407
2385
  var ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
2408
2386
  function isRoutableSourceFile(name) {
2409
- return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.");
2387
+ return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
2410
2388
  }
2411
2389
  function findRouterFilesRecursive(dir, filter, results) {
2412
2390
  let entries;
@@ -2421,9 +2399,10 @@ function findRouterFilesRecursive(dir, filter, results) {
2421
2399
  const childDirs = [];
2422
2400
  const routerFilesInDir = [];
2423
2401
  for (const entry of entries) {
2424
- const fullPath = join2(dir, entry.name);
2402
+ const fullPath = join(dir, entry.name);
2425
2403
  if (entry.isDirectory()) {
2426
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
2404
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage" || entry.name === "__tests__" || entry.name === "__mocks__" || entry.name.startsWith("."))
2405
+ continue;
2427
2406
  childDirs.push(fullPath);
2428
2407
  continue;
2429
2408
  }
@@ -2475,7 +2454,7 @@ Router root: ${conflict.ancestor}
2475
2454
  Nested router: ${conflict.nested}
2476
2455
  Move the nested router into a sibling directory or configure it as a separate app root.`;
2477
2456
  }
2478
- function extractUrlsVariableFromRouter(code) {
2457
+ function extractUrlsFromRouter(code) {
2479
2458
  const sourceFile = ts5.createSourceFile(
2480
2459
  "router.tsx",
2481
2460
  code,
@@ -2489,24 +2468,70 @@ function extractUrlsVariableFromRouter(code) {
2489
2468
  const callee = node.expression;
2490
2469
  return ts5.isIdentifier(callee) && callee.text === "createRouter";
2491
2470
  }
2471
+ function isInlineBuilder(node) {
2472
+ return ts5.isArrowFunction(node) || ts5.isFunctionExpression(node);
2473
+ }
2474
+ function isRoutesOnCreateRouter(node) {
2475
+ if (!ts5.isPropertyAccessExpression(node.expression) || node.expression.name.text !== "routes")
2476
+ return false;
2477
+ let inner = node.expression.expression;
2478
+ while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
2479
+ inner = inner.expression.expression;
2480
+ }
2481
+ return isCreateRouterCall(inner);
2482
+ }
2492
2483
  function visit(node) {
2493
2484
  if (result) return;
2494
- if (ts5.isCallExpression(node) && ts5.isPropertyAccessExpression(node.expression) && node.expression.name.text === "routes" && node.arguments.length >= 1 && ts5.isIdentifier(node.arguments[0])) {
2495
- let inner = node.expression.expression;
2496
- while (ts5.isCallExpression(inner) && ts5.isPropertyAccessExpression(inner.expression)) {
2497
- inner = inner.expression.expression;
2498
- }
2499
- if (isCreateRouterCall(inner)) {
2500
- result = node.arguments[0].text;
2501
- return;
2485
+ if (ts5.isCallExpression(node) && node.arguments.length >= 1 && isRoutesOnCreateRouter(node)) {
2486
+ const arg = node.arguments[0];
2487
+ if (ts5.isIdentifier(arg)) {
2488
+ result = { kind: "variable", name: arg.text };
2489
+ } else if (isInlineBuilder(arg)) {
2490
+ result = { kind: "inline", block: arg.getText(sourceFile) };
2502
2491
  }
2492
+ return;
2503
2493
  }
2504
2494
  if (isCreateRouterCall(node)) {
2505
2495
  const callExpr = node;
2506
- for (const arg of callExpr.arguments) {
2496
+ for (const callArg of callExpr.arguments) {
2497
+ if (ts5.isObjectLiteralExpression(callArg)) {
2498
+ for (const prop of callArg.properties) {
2499
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls") {
2500
+ if (ts5.isIdentifier(prop.initializer)) {
2501
+ result = { kind: "variable", name: prop.initializer.text };
2502
+ } else if (isInlineBuilder(prop.initializer)) {
2503
+ result = {
2504
+ kind: "inline",
2505
+ block: prop.initializer.getText(sourceFile)
2506
+ };
2507
+ }
2508
+ return;
2509
+ }
2510
+ }
2511
+ }
2512
+ }
2513
+ }
2514
+ ts5.forEachChild(node, visit);
2515
+ }
2516
+ visit(sourceFile);
2517
+ return result;
2518
+ }
2519
+ function extractBasenameFromRouter(code) {
2520
+ const sourceFile = ts5.createSourceFile(
2521
+ "router.tsx",
2522
+ code,
2523
+ ts5.ScriptTarget.Latest,
2524
+ true,
2525
+ ts5.ScriptKind.TSX
2526
+ );
2527
+ let result;
2528
+ function visit(node) {
2529
+ if (result !== void 0) return;
2530
+ if (ts5.isCallExpression(node) && ts5.isIdentifier(node.expression) && node.expression.text === "createRouter") {
2531
+ for (const arg of node.arguments) {
2507
2532
  if (ts5.isObjectLiteralExpression(arg)) {
2508
2533
  for (const prop of arg.properties) {
2509
- if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "urls" && ts5.isIdentifier(prop.initializer)) {
2534
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.name) && prop.name.text === "basename" && ts5.isStringLiteral(prop.initializer)) {
2510
2535
  result = prop.initializer.text;
2511
2536
  return;
2512
2537
  }
@@ -2519,6 +2544,19 @@ function extractUrlsVariableFromRouter(code) {
2519
2544
  visit(sourceFile);
2520
2545
  return result;
2521
2546
  }
2547
+ function applyBasenameToRoutes(result, basename3) {
2548
+ const prefixed = {};
2549
+ for (const [name, pattern] of Object.entries(result.routes)) {
2550
+ if (pattern === "/") {
2551
+ prefixed[name] = basename3;
2552
+ } else if (basename3.endsWith("/") && pattern.startsWith("/")) {
2553
+ prefixed[name] = basename3 + pattern.slice(1);
2554
+ } else {
2555
+ prefixed[name] = basename3 + pattern;
2556
+ }
2557
+ }
2558
+ return { routes: prefixed, searchSchemas: result.searchSchemas };
2559
+ }
2522
2560
  function buildCombinedRouteMapForRouterFile(routerFilePath) {
2523
2561
  let routerSource;
2524
2562
  try {
@@ -2526,19 +2564,40 @@ function buildCombinedRouteMapForRouterFile(routerFilePath) {
2526
2564
  } catch {
2527
2565
  return { routes: {}, searchSchemas: {} };
2528
2566
  }
2529
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
2530
- if (!urlsVarName) {
2567
+ const extraction = extractUrlsFromRouter(routerSource);
2568
+ if (!extraction) {
2531
2569
  return { routes: {}, searchSchemas: {} };
2532
2570
  }
2533
- const imported = resolveImportedVariable(routerSource, urlsVarName);
2534
- if (imported) {
2535
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
2536
- if (!targetFile) {
2537
- return { routes: {}, searchSchemas: {} };
2571
+ const rawBasename = extractBasenameFromRouter(routerSource);
2572
+ const basename3 = rawBasename ? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "") : void 0;
2573
+ let result;
2574
+ if (extraction.kind === "inline") {
2575
+ result = buildCombinedRouteMapWithSearch(
2576
+ routerFilePath,
2577
+ void 0,
2578
+ void 0,
2579
+ void 0,
2580
+ extraction.block
2581
+ );
2582
+ } else {
2583
+ const imported = resolveImportedVariable(routerSource, extraction.name);
2584
+ if (imported) {
2585
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
2586
+ if (!targetFile) {
2587
+ return { routes: {}, searchSchemas: {} };
2588
+ }
2589
+ result = buildCombinedRouteMapWithSearch(
2590
+ targetFile,
2591
+ imported.exportedName
2592
+ );
2593
+ } else {
2594
+ result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
2538
2595
  }
2539
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
2540
2596
  }
2541
- return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
2597
+ if (basename3) {
2598
+ result = applyBasenameToRoutes(result, basename3);
2599
+ }
2600
+ return result;
2542
2601
  }
2543
2602
  function findRouterFiles(root, filter) {
2544
2603
  const result = [];
@@ -2547,7 +2606,7 @@ function findRouterFiles(root, filter) {
2547
2606
  }
2548
2607
  function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
2549
2608
  try {
2550
- const oldCombinedPath = join2(root, "src", "named-routes.gen.ts");
2609
+ const oldCombinedPath = join(root, "src", "named-routes.gen.ts");
2551
2610
  if (existsSync3(oldCombinedPath)) {
2552
2611
  unlinkSync(oldCombinedPath);
2553
2612
  console.log(
@@ -2563,31 +2622,21 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
2563
2622
  throw new Error(formatNestedRouterConflictError(nestedRouterConflict));
2564
2623
  }
2565
2624
  for (const routerFilePath of routerFilePaths) {
2566
- let routerSource;
2567
- try {
2568
- routerSource = readFileSync2(routerFilePath, "utf-8");
2569
- } catch {
2570
- continue;
2571
- }
2572
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
2573
- if (!urlsVarName) continue;
2574
- let result;
2575
- const imported = resolveImportedVariable(routerSource, urlsVarName);
2576
- if (imported) {
2577
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
2578
- if (!targetFile) continue;
2579
- result = buildCombinedRouteMapWithSearch(
2580
- targetFile,
2581
- imported.exportedName
2582
- );
2583
- } else {
2584
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
2625
+ const result = buildCombinedRouteMapForRouterFile(routerFilePath);
2626
+ if (Object.keys(result.routes).length === 0 && Object.keys(result.searchSchemas).length === 0) {
2627
+ let routerSource;
2628
+ try {
2629
+ routerSource = readFileSync2(routerFilePath, "utf-8");
2630
+ } catch {
2631
+ continue;
2632
+ }
2633
+ if (!extractUrlsFromRouter(routerSource)) continue;
2585
2634
  }
2586
2635
  const routerBasename = pathBasename(routerFilePath).replace(
2587
2636
  /\.(tsx?|jsx?)$/,
2588
2637
  ""
2589
2638
  );
2590
- const outPath = join2(
2639
+ const outPath = join(
2591
2640
  dirname2(routerFilePath),
2592
2641
  `${routerBasename}.named-routes.gen.ts`
2593
2642
  );
@@ -2717,8 +2766,9 @@ function createVersionPlugin() {
2717
2766
  let isDev = false;
2718
2767
  let server = null;
2719
2768
  const clientModuleSignatures = /* @__PURE__ */ new Map();
2769
+ let versionCounter = 0;
2720
2770
  const bumpVersion = (reason) => {
2721
- currentVersion = Date.now().toString(16);
2771
+ currentVersion = Date.now().toString(16) + String(++versionCounter);
2722
2772
  console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
2723
2773
  const rscEnv = server?.environments?.rsc;
2724
2774
  const versionMod = rscEnv?.moduleGraph?.getModuleById(
@@ -2774,6 +2824,9 @@ function createVersionPlugin() {
2774
2824
  if (!isDev) return;
2775
2825
  const isRscModule = this.environment?.name === "rsc";
2776
2826
  if (!isRscModule) return;
2827
+ if (ctx.modules.length === 1 && ctx.modules[0].id === "\0" + VIRTUAL_IDS.version) {
2828
+ return;
2829
+ }
2777
2830
  if (isCodeModule(ctx.file)) {
2778
2831
  const filePath = normalizeModuleId(ctx.file);
2779
2832
  const previousSignature = clientModuleSignatures.get(filePath);
@@ -2803,6 +2856,68 @@ function createVersionPlugin() {
2803
2856
 
2804
2857
  // src/vite/utils/shared-utils.ts
2805
2858
  import * as Vite from "vite";
2859
+
2860
+ // src/vite/plugins/performance-tracks.ts
2861
+ import { readFile } from "node:fs/promises";
2862
+ var RSDW_PATCH_RE = /((?:var|let|const)\s+\w+\s*=\s*root\._children\s*,\s*(\w+)\s*=\s*root\._debugInfo\s*[;,])/;
2863
+ function buildPatchReplacement(match, debugInfoVar) {
2864
+ return `${match}
2865
+ if (${debugInfoVar} && 0 === ${debugInfoVar}.length && "fulfilled" === root.status) {
2866
+ var _resolved = "function" === typeof resolveLazy ? resolveLazy(root.value) : root.value;
2867
+ if ("object" === typeof _resolved && null !== _resolved && isArrayImpl(_resolved._debugInfo)) {
2868
+ ${debugInfoVar} = _resolved._debugInfo;
2869
+ }
2870
+ }`;
2871
+ }
2872
+ function patchRsdwClientDebugInfoRecovery(code) {
2873
+ const match = code.match(RSDW_PATCH_RE);
2874
+ if (!match) {
2875
+ return { code, debugInfoVar: null };
2876
+ }
2877
+ return {
2878
+ code: code.replace(match[1], buildPatchReplacement(match[1], match[2])),
2879
+ debugInfoVar: match[2]
2880
+ };
2881
+ }
2882
+ function performanceTracksOptimizeDepsPlugin() {
2883
+ return {
2884
+ name: "@rangojs/router:performance-tracks-optimize-deps",
2885
+ setup(build) {
2886
+ build.onLoad(
2887
+ {
2888
+ filter: /react-server-dom-webpack-client\.browser\.(development|production)\.js$/
2889
+ },
2890
+ async (args) => {
2891
+ const code = await readFile(args.path, "utf8");
2892
+ const patched = patchRsdwClientDebugInfoRecovery(code);
2893
+ return {
2894
+ contents: patched.code,
2895
+ loader: "js"
2896
+ };
2897
+ }
2898
+ );
2899
+ }
2900
+ };
2901
+ }
2902
+ function performanceTracksPlugin() {
2903
+ return {
2904
+ name: "@rangojs/router:performance-tracks",
2905
+ transform(code, id) {
2906
+ if (!id.includes("react-server-dom") || !id.includes("client")) return;
2907
+ const patched = patchRsdwClientDebugInfoRecovery(code);
2908
+ if (!patched.debugInfoVar) return;
2909
+ if (process.env.INTERNAL_RANGO_DEBUG)
2910
+ console.log(
2911
+ "[perf-tracks] patched RSDW client (var:",
2912
+ patched.debugInfoVar,
2913
+ ")"
2914
+ );
2915
+ return patched.code;
2916
+ }
2917
+ };
2918
+ }
2919
+
2920
+ // src/vite/utils/shared-utils.ts
2806
2921
  var versionEsbuildPlugin = {
2807
2922
  name: "@rangojs/router-version",
2808
2923
  setup(build) {
@@ -2820,7 +2935,7 @@ var versionEsbuildPlugin = {
2820
2935
  }
2821
2936
  };
2822
2937
  var sharedEsbuildOptions = {
2823
- plugins: [versionEsbuildPlugin]
2938
+ plugins: [versionEsbuildPlugin, performanceTracksOptimizeDepsPlugin()]
2824
2939
  };
2825
2940
  function createVirtualEntriesPlugin(entries, routerPathRef) {
2826
2941
  const virtualModules = {};
@@ -2903,11 +3018,11 @@ ${dim} \u2571${reset} ${bold}\u2554\u2550\u2557${reset}${dim} * \u2
2903
3018
  ${dim} ${reset}${bold}\u2551 \u2551${reset} ${bold}\u2554\u2550\u2557${reset}${dim} * \u2727. \u2571${reset}
2904
3019
  ${dim} ${reset}${bold}\u2554\u2557 \u2551 \u2551 \u2551 \u2551${reset}${dim} * \u2571${reset}
2905
3020
  ${dim} ${reset}${bold}\u2551\u2551 \u2551 \u2551 \u2551 \u2551 \u2566\u2550\u2557\u2554\u2550\u2557\u2554\u2557\u2554\u2554\u2550\u2557\u2554\u2550\u2557${reset}${dim} \u2727 \u2726${reset}
2906
- ${dim} ${reset}${bold}\u2550\u2563\u2551 \u2551 \u2560\u2550\u255D \u2551 \u2560\u2566\u255D\u2560\u2550\u2563\u2551\u2551\u2551\u2551 \u2566\u2551 \u2551${reset}${dim} * \u2727${reset}
3021
+ ${dim} ${reset}${bold}\u2551\u2551 \u2551 \u2560\u2550\u255D \u2551 \u2560\u2566\u255D\u2560\u2550\u2563\u2551\u2551\u2551\u2551 \u2566\u2551 \u2551${reset}${dim} * \u2727${reset}
2907
3022
  ${dim} ${reset}${bold}\u2551\u255A\u2550\u255D \u2554\u2550\u2550\u2550\u255D \u2569\u255A\u2550\u2569 \u2569\u255D\u255A\u255D\u255A\u2550\u255D\u255A\u2550\u255D${reset}${dim} \u2726 . *${reset}
2908
3023
  ${dim} ${reset}${bold}\u255A\u2550\u2550\u2557 \u2551${reset}${dim} * RSC Wrangler \u2727 \u2726${reset}
2909
- ${dim} * ${reset}${bold}\u2551 \u2560\u2550${reset}${dim} * \u2727. \u2571${reset}
2910
- ${bold}\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2569\u2550\u2550\u2550${reset}${dim} \u2726 *${reset}
3024
+ ${dim} * ${reset}${bold}\u2551 \u2551${reset}${dim} * \u2727. \u2571${reset}
3025
+ ${dim} ${reset}${bold}\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550${reset}${dim} \u2726 *${reset}
2911
3026
 
2912
3027
  v${version} \xB7 ${preset} \xB7 ${mode}
2913
3028
  `;
@@ -3026,6 +3141,8 @@ function createCjsToEsmPlugin() {
3026
3141
  import { createServer as createViteServer } from "vite";
3027
3142
  import { resolve as resolve8 } from "node:path";
3028
3143
  import { readFileSync as readFileSync6 } from "node:fs";
3144
+ import { createRequire } from "node:module";
3145
+ import { pathToFileURL } from "node:url";
3029
3146
 
3030
3147
  // src/vite/plugins/virtual-stub-plugin.ts
3031
3148
  function createVirtualStubPlugin() {
@@ -3052,7 +3169,7 @@ function createVirtualStubPlugin() {
3052
3169
  }
3053
3170
 
3054
3171
  // src/vite/plugins/client-ref-hashing.ts
3055
- import { relative as relative2 } from "node:path";
3172
+ import { relative } from "node:path";
3056
3173
  import { createHash as createHash2 } from "node:crypto";
3057
3174
  var CLIENT_PKG_PROXY_PREFIX = "/@id/__x00__virtual:vite-rsc/client-package-proxy/";
3058
3175
  var CLIENT_IN_SERVER_PKG_PROXY_PREFIX = "/@id/__x00__virtual:vite-rsc/client-in-server-package-proxy/";
@@ -3065,10 +3182,10 @@ function computeProductionHash(projectRoot, refKey) {
3065
3182
  const absPath = decodeURIComponent(
3066
3183
  refKey.slice(CLIENT_IN_SERVER_PKG_PROXY_PREFIX.length)
3067
3184
  );
3068
- toHash = relative2(projectRoot, absPath).replaceAll("\\", "/");
3185
+ toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
3069
3186
  } else if (refKey.startsWith(FS_PREFIX)) {
3070
3187
  const absPath = refKey.slice(FS_PREFIX.length - 1);
3071
- toHash = relative2(projectRoot, absPath).replaceAll("\\", "/");
3188
+ toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
3072
3189
  } else if (refKey.startsWith("/")) {
3073
3190
  toHash = refKey.slice(1);
3074
3191
  } else {
@@ -3209,8 +3326,8 @@ function createDiscoveryState(entryPath, opts) {
3209
3326
  perRouterManifestDataMap: /* @__PURE__ */ new Map(),
3210
3327
  prerenderManifestEntries: null,
3211
3328
  staticManifestEntries: null,
3212
- handlerChunkInfo: null,
3213
- staticHandlerChunkInfo: null,
3329
+ handlerChunkInfoMap: /* @__PURE__ */ new Map(),
3330
+ staticHandlerChunkInfoMap: /* @__PURE__ */ new Map(),
3214
3331
  rscEntryFileName: null,
3215
3332
  resolvedPrerenderModules: void 0,
3216
3333
  resolvedStaticModules: void 0,
@@ -3293,8 +3410,17 @@ function jsonParseExpression(value) {
3293
3410
  }
3294
3411
 
3295
3412
  // src/context-var.ts
3413
+ var NON_CACHEABLE_KEYS = /* @__PURE__ */ Symbol.for(
3414
+ "rango:non-cacheable-keys"
3415
+ );
3416
+ function getNonCacheableKeys(variables) {
3417
+ if (!variables[NON_CACHEABLE_KEYS]) {
3418
+ variables[NON_CACHEABLE_KEYS] = /* @__PURE__ */ new Set();
3419
+ }
3420
+ return variables[NON_CACHEABLE_KEYS];
3421
+ }
3296
3422
  var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
3297
- function contextSet(variables, keyOrVar, value) {
3423
+ function contextSet(variables, keyOrVar, value, options) {
3298
3424
  if (typeof keyOrVar === "string") {
3299
3425
  if (FORBIDDEN_KEYS.has(keyOrVar)) {
3300
3426
  throw new Error(
@@ -3302,8 +3428,14 @@ function contextSet(variables, keyOrVar, value) {
3302
3428
  );
3303
3429
  }
3304
3430
  variables[keyOrVar] = value;
3431
+ if (options?.cache === false) {
3432
+ getNonCacheableKeys(variables).add(keyOrVar);
3433
+ }
3305
3434
  } else {
3306
3435
  variables[keyOrVar.key] = value;
3436
+ if (options?.cache === false) {
3437
+ getNonCacheableKeys(variables).add(keyOrVar.key);
3438
+ }
3307
3439
  }
3308
3440
  }
3309
3441
 
@@ -3326,6 +3458,7 @@ function encodePathParam(value) {
3326
3458
  }
3327
3459
  function substituteRouteParams(pattern, params, encode = encodeURIComponent) {
3328
3460
  let result = pattern;
3461
+ let hadOmittedOptional = false;
3329
3462
  for (const [key, value] of Object.entries(params)) {
3330
3463
  const escaped = escapeRegExp2(key);
3331
3464
  result = result.replace(
@@ -3334,6 +3467,15 @@ function substituteRouteParams(pattern, params, encode = encodeURIComponent) {
3334
3467
  );
3335
3468
  result = result.replace(`*${key}`, encode(value));
3336
3469
  }
3470
+ result = result.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\?/g, () => {
3471
+ hadOmittedOptional = true;
3472
+ return "";
3473
+ });
3474
+ if (hadOmittedOptional) {
3475
+ const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
3476
+ result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
3477
+ if (hadTrailingSlash && !result.endsWith("/")) result += "/";
3478
+ }
3337
3479
  return result;
3338
3480
  }
3339
3481
  async function runWithConcurrency(items, concurrency, fn) {
@@ -3445,11 +3587,12 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3445
3587
  for (const { manifest } of allManifests) {
3446
3588
  if (!manifest.prerenderRoutes) continue;
3447
3589
  const defs = manifest._prerenderDefs || {};
3590
+ const passthroughSet = new Set(manifest.passthroughRoutes || []);
3448
3591
  for (const routeName of manifest.prerenderRoutes) {
3449
3592
  const pattern = manifest.routeManifest[routeName];
3450
3593
  if (!pattern) continue;
3451
3594
  const def = defs[routeName];
3452
- const isPassthroughRoute = !!def?.options?.passthrough;
3595
+ const isPassthroughRoute = passthroughSet.has(routeName);
3453
3596
  const hasDynamic = pattern.includes(":") || pattern.includes("*");
3454
3597
  if (!hasDynamic) {
3455
3598
  entries.push({
@@ -3462,12 +3605,20 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3462
3605
  if (def?.getParams) {
3463
3606
  try {
3464
3607
  const buildVars = {};
3608
+ const buildEnv = state.resolvedBuildEnv;
3465
3609
  const getParamsCtx = {
3466
3610
  build: true,
3611
+ dev: !state.isBuildMode,
3467
3612
  set: ((keyOrVar, value) => {
3468
3613
  contextSet(buildVars, keyOrVar, value);
3469
3614
  }),
3470
- reverse: getParamsReverse
3615
+ reverse: getParamsReverse,
3616
+ get env() {
3617
+ if (buildEnv !== void 0) return buildEnv;
3618
+ throw new Error(
3619
+ "[rsc-router] ctx.env is not available during build-time getParams(). Configure buildEnv in your rango() plugin options to enable build-time env access."
3620
+ );
3621
+ }
3471
3622
  };
3472
3623
  const paramsList = await def.getParams(getParamsCtx);
3473
3624
  const concurrency = def.options?.concurrency ?? 1;
@@ -3546,7 +3697,8 @@ async function expandPrerenderRoutes(state, rscEnv, registry, allManifests) {
3546
3697
  entry.urlPath,
3547
3698
  {},
3548
3699
  entry.buildVars,
3549
- entry.isPassthroughRoute
3700
+ entry.isPassthroughRoute,
3701
+ state.resolvedBuildEnv
3550
3702
  );
3551
3703
  if (!result) continue;
3552
3704
  if (result.passthrough) {
@@ -3670,7 +3822,9 @@ async function renderStaticHandlers(state, rscEnv, registry) {
3670
3822
  const result = await routerInstance.renderStaticSegment(
3671
3823
  def.handler,
3672
3824
  def.$$id,
3673
- def.$$routePrefix
3825
+ def.$$routePrefix,
3826
+ state.resolvedBuildEnv,
3827
+ !state.isBuildMode
3674
3828
  );
3675
3829
  if (result) {
3676
3830
  const hasHandles = Object.keys(result.handles).length > 0;
@@ -3795,7 +3949,11 @@ async function discoverRouters(state, rscEnv) {
3795
3949
  if (!router.urlpatterns || !generateManifestFull) {
3796
3950
  continue;
3797
3951
  }
3798
- const manifest = generateManifestFull(router.urlpatterns, routerMountIndex);
3952
+ const manifest = generateManifestFull(
3953
+ router.urlpatterns,
3954
+ routerMountIndex,
3955
+ router.__basename ? { urlPrefix: router.__basename } : void 0
3956
+ );
3799
3957
  routerMountIndex++;
3800
3958
  allManifests.push({ id, manifest });
3801
3959
  const routeCount = Object.keys(manifest.routeManifest).length;
@@ -3937,7 +4095,7 @@ async function discoverRouters(state, rscEnv) {
3937
4095
  }
3938
4096
 
3939
4097
  // src/vite/discovery/route-types-writer.ts
3940
- import { dirname as dirname3, basename, join as join3, resolve as resolve6 } from "node:path";
4098
+ import { dirname as dirname3, basename, join as join2, resolve as resolve6 } from "node:path";
3941
4099
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
3942
4100
  function filterUserNamedRoutes(manifest) {
3943
4101
  const filtered = {};
@@ -3958,7 +4116,7 @@ function writeCombinedRouteTypesWithTracking(state, opts) {
3958
4116
  /\.(tsx?|jsx?)$/,
3959
4117
  ""
3960
4118
  );
3961
- const outPath = join3(routerDir, `${routerBasename}.named-routes.gen.ts`);
4119
+ const outPath = join2(routerDir, `${routerBasename}.named-routes.gen.ts`);
3962
4120
  try {
3963
4121
  preContent.set(outPath, readFileSync4(outPath, "utf-8"));
3964
4122
  } catch {
@@ -3971,7 +4129,7 @@ function writeCombinedRouteTypesWithTracking(state, opts) {
3971
4129
  /\.(tsx?|jsx?)$/,
3972
4130
  ""
3973
4131
  );
3974
- const outPath = join3(routerDir, `${routerBasename}.named-routes.gen.ts`);
4132
+ const outPath = join2(routerDir, `${routerBasename}.named-routes.gen.ts`);
3975
4133
  if (!existsSync5(outPath)) continue;
3976
4134
  try {
3977
4135
  const content = readFileSync4(outPath, "utf-8");
@@ -3988,7 +4146,7 @@ function writeRouteTypesFiles(state) {
3988
4146
  const entryDir = dirname3(
3989
4147
  resolve6(state.projectRoot, state.resolvedEntryPath)
3990
4148
  );
3991
- const oldCombinedPath = join3(entryDir, "named-routes.gen.ts");
4149
+ const oldCombinedPath = join2(entryDir, "named-routes.gen.ts");
3992
4150
  if (existsSync5(oldCombinedPath)) {
3993
4151
  unlinkSync2(oldCombinedPath);
3994
4152
  console.log(
@@ -4013,7 +4171,7 @@ Set an explicit \`id\` on createRouter() or check the call site.`
4013
4171
  }
4014
4172
  const routerDir = dirname3(sourceFile);
4015
4173
  const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
4016
- const outPath = join3(routerDir, `${routerBasename}.named-routes.gen.ts`);
4174
+ const outPath = join2(routerDir, `${routerBasename}.named-routes.gen.ts`);
4017
4175
  const userRoutes = filterUserNamedRoutes(routeManifest);
4018
4176
  let effectiveSearchSchemas = routeSearchSchemas;
4019
4177
  if ((!effectiveSearchSchemas || Object.keys(effectiveSearchSchemas).length === 0) && sourceFile) {
@@ -4078,7 +4236,7 @@ function supplementGenFilesWithRuntimeRoutes(state) {
4078
4236
  }
4079
4237
  const routerDir = dirname3(sourceFile);
4080
4238
  const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
4081
- const outPath = join3(routerDir, `${routerBasename}.named-routes.gen.ts`);
4239
+ const outPath = join2(routerDir, `${routerBasename}.named-routes.gen.ts`);
4082
4240
  const source = generateRouteTypesSource(
4083
4241
  mergedRoutes,
4084
4242
  Object.keys(mergedSearchSchemas).length > 0 ? mergedSearchSchemas : void 0
@@ -4092,7 +4250,7 @@ function supplementGenFilesWithRuntimeRoutes(state) {
4092
4250
  }
4093
4251
 
4094
4252
  // src/vite/discovery/virtual-module-codegen.ts
4095
- import { dirname as dirname4, basename as basename2, join as join4 } from "node:path";
4253
+ import { dirname as dirname4, basename as basename2, join as join3 } from "node:path";
4096
4254
  function generateRoutesManifestModule(state) {
4097
4255
  const hasManifest = state.mergedRouteManifest && Object.keys(state.mergedRouteManifest).length > 0;
4098
4256
  if (hasManifest) {
@@ -4107,7 +4265,7 @@ function generateRoutesManifestModule(state) {
4107
4265
  /\.(tsx?|jsx?)$/,
4108
4266
  ""
4109
4267
  );
4110
- const genPath = join4(
4268
+ const genPath = join3(
4111
4269
  routerDir,
4112
4270
  `${routerBasename}.named-routes.gen.js`
4113
4271
  ).replaceAll("\\", "/");
@@ -4204,7 +4362,7 @@ function generatePerRouterModule(state, routerId) {
4204
4362
  /\.(tsx?|jsx?)$/,
4205
4363
  ""
4206
4364
  );
4207
- const genPath = join4(
4365
+ const genPath = join3(
4208
4366
  routerDir,
4209
4367
  `${routerBasename}.named-routes.gen.js`
4210
4368
  ).replaceAll("\\", "/");
@@ -4244,48 +4402,45 @@ function postprocessBundle(state) {
4244
4402
  );
4245
4403
  const evictionTargets = [
4246
4404
  {
4247
- info: state.handlerChunkInfo,
4405
+ infos: state.handlerChunkInfoMap.values(),
4248
4406
  fnName: "Prerender",
4249
4407
  brand: "prerenderHandler",
4250
4408
  label: "handler code from RSC bundle"
4251
4409
  },
4252
4410
  {
4253
- info: state.staticHandlerChunkInfo,
4411
+ infos: state.staticHandlerChunkInfoMap.values(),
4254
4412
  fnName: "Static",
4255
4413
  brand: "staticHandler",
4256
4414
  label: "static handler code"
4257
4415
  }
4258
4416
  ];
4259
4417
  for (const target of evictionTargets) {
4260
- if (!target.info) continue;
4261
- const chunkPath = resolve7(
4262
- state.projectRoot,
4263
- "dist/rsc",
4264
- target.info.fileName
4265
- );
4266
- try {
4267
- const code = readFileSync5(chunkPath, "utf-8");
4268
- const result = evictHandlerCode(
4269
- code,
4270
- target.info.exports,
4271
- target.fnName,
4272
- target.brand
4273
- );
4274
- if (result) {
4275
- writeFileSync4(chunkPath, result.code);
4276
- const savedKB = (result.savedBytes / 1024).toFixed(1);
4277
- console.log(
4278
- `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${target.info.fileName}`
4418
+ for (const info of target.infos) {
4419
+ const chunkPath = resolve7(state.projectRoot, "dist/rsc", info.fileName);
4420
+ try {
4421
+ const code = readFileSync5(chunkPath, "utf-8");
4422
+ const result = evictHandlerCode(
4423
+ code,
4424
+ info.exports,
4425
+ target.fnName,
4426
+ target.brand
4427
+ );
4428
+ if (result) {
4429
+ writeFileSync4(chunkPath, result.code);
4430
+ const savedKB = (result.savedBytes / 1024).toFixed(1);
4431
+ console.log(
4432
+ `[rsc-router] Evicted ${target.label} (${savedKB} KB saved): ${info.fileName}`
4433
+ );
4434
+ }
4435
+ } catch (replaceErr) {
4436
+ console.warn(
4437
+ `[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`
4279
4438
  );
4280
4439
  }
4281
- } catch (replaceErr) {
4282
- console.warn(
4283
- `[rsc-router] Failed to evict ${target.label}: ${replaceErr.message}`
4284
- );
4285
4440
  }
4286
4441
  }
4287
- state.handlerChunkInfo = null;
4288
- state.staticHandlerChunkInfo = null;
4442
+ state.handlerChunkInfoMap.clear();
4443
+ state.staticHandlerChunkInfoMap.clear();
4289
4444
  if (hasPrerenderData && existsSync6(rscEntryPath)) {
4290
4445
  const rscCode = readFileSync5(rscEntryPath, "utf-8");
4291
4446
  if (!rscCode.includes("__prerender-manifest.js")) {
@@ -4328,7 +4483,7 @@ function postprocessBundle(state) {
4328
4483
  }
4329
4484
  if (hasStaticData && existsSync6(rscEntryPath)) {
4330
4485
  const rscCode = readFileSync5(rscEntryPath, "utf-8");
4331
- if (!rscCode.includes("__STATIC_MANIFEST")) {
4486
+ if (!rscCode.includes("__static-manifest.js")) {
4332
4487
  try {
4333
4488
  const manifestEntries = [];
4334
4489
  let totalBytes = copyStagedBuildAssets(
@@ -4397,8 +4552,67 @@ async function createTempRscServer(state, options = {}) {
4397
4552
  ]
4398
4553
  });
4399
4554
  }
4555
+ async function resolveBuildEnv(option, factoryCtx) {
4556
+ if (!option) return null;
4557
+ if (option === "auto") {
4558
+ if (factoryCtx.preset !== "cloudflare") {
4559
+ throw new Error(
4560
+ '[rsc-router] buildEnv: "auto" is only supported with preset: "cloudflare". Use a factory function or plain object for other presets.'
4561
+ );
4562
+ }
4563
+ try {
4564
+ const userRequire = createRequire(
4565
+ resolve8(factoryCtx.root, "package.json")
4566
+ );
4567
+ const wranglerPath = userRequire.resolve("wrangler");
4568
+ const { getPlatformProxy } = await import(pathToFileURL(wranglerPath).href);
4569
+ const proxy = await getPlatformProxy();
4570
+ return {
4571
+ env: proxy.env,
4572
+ dispose: proxy.dispose
4573
+ };
4574
+ } catch (err) {
4575
+ throw new Error(
4576
+ `[rsc-router] buildEnv: "auto" requires wrangler to be installed.
4577
+ Install it with: pnpm add -D wrangler
4578
+ ${err.message}`
4579
+ );
4580
+ }
4581
+ }
4582
+ if (typeof option === "function") {
4583
+ return await option(factoryCtx);
4584
+ }
4585
+ return { env: option };
4586
+ }
4587
+ async function acquireBuildEnv(s, command, mode) {
4588
+ const option = s.opts?.buildEnv;
4589
+ if (!option) return false;
4590
+ const result = await resolveBuildEnv(option, {
4591
+ root: s.projectRoot,
4592
+ mode,
4593
+ command,
4594
+ preset: s.opts?.preset ?? "node"
4595
+ });
4596
+ if (!result) return false;
4597
+ s.resolvedBuildEnv = result.env;
4598
+ s.buildEnvDispose = result.dispose ?? null;
4599
+ return true;
4600
+ }
4601
+ async function releaseBuildEnv(s) {
4602
+ if (s.buildEnvDispose) {
4603
+ try {
4604
+ await s.buildEnvDispose();
4605
+ } catch (err) {
4606
+ console.warn(`[rsc-router] buildEnv dispose failed: ${err.message}`);
4607
+ }
4608
+ s.buildEnvDispose = null;
4609
+ }
4610
+ s.resolvedBuildEnv = void 0;
4611
+ }
4400
4612
  function createRouterDiscoveryPlugin(entryPath, opts) {
4401
4613
  const s = createDiscoveryState(entryPath, opts);
4614
+ let viteCommand = "build";
4615
+ let viteMode = "production";
4402
4616
  return {
4403
4617
  name: "@rangojs/router:discovery",
4404
4618
  config() {
@@ -4407,31 +4621,13 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
4407
4621
  __RANGO_DEBUG__: JSON.stringify(!!process.env.INTERNAL_RANGO_DEBUG)
4408
4622
  }
4409
4623
  };
4410
- if (opts?.enableBuildPrerender) {
4411
- config.environments = {
4412
- rsc: {
4413
- build: {
4414
- rollupOptions: {
4415
- output: {
4416
- manualChunks(id) {
4417
- if (s.resolvedPrerenderModules?.has(id)) {
4418
- return "__prerender-handlers";
4419
- }
4420
- if (s.resolvedStaticModules?.has(id)) {
4421
- return "__static-handlers";
4422
- }
4423
- }
4424
- }
4425
- }
4426
- }
4427
- }
4428
- };
4429
- }
4430
4624
  return config;
4431
4625
  },
4432
4626
  configResolved(config) {
4433
4627
  s.projectRoot = config.root;
4434
4628
  s.isBuildMode = config.command === "build";
4629
+ viteCommand = config.command;
4630
+ viteMode = config.mode;
4435
4631
  s.userResolveAlias = config.resolve.alias;
4436
4632
  if (!s.resolvedEntryPath && opts?.routerPathRef?.path) {
4437
4633
  s.resolvedEntryPath = opts.routerPathRef.path;
@@ -4445,12 +4641,6 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
4445
4641
  s.resolvedEntryPath = entries[0];
4446
4642
  }
4447
4643
  }
4448
- if (opts?.include || opts?.exclude) {
4449
- s.scanFilter = createScanFilter(s.projectRoot, {
4450
- include: opts.include,
4451
- exclude: opts.exclude
4452
- });
4453
- }
4454
4644
  if (opts?.staticRouteTypesGeneration !== false) {
4455
4645
  s.cachedRouterFiles = findRouterFiles(s.projectRoot, s.scanFilter);
4456
4646
  writeCombinedRouteTypesWithTracking(s, { preserveIfLarger: true });
@@ -4482,6 +4672,8 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
4482
4672
  });
4483
4673
  prerenderTempServer = null;
4484
4674
  }
4675
+ releaseBuildEnv(s).catch(() => {
4676
+ });
4485
4677
  });
4486
4678
  async function getOrCreateTempServer() {
4487
4679
  if (prerenderNodeRegistry) {
@@ -4512,6 +4704,7 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
4512
4704
  if (!rscEnv?.runner) {
4513
4705
  s.devServerOrigin = getDevServerOrigin();
4514
4706
  try {
4707
+ await acquireBuildEnv(s, viteCommand, viteMode);
4515
4708
  const tempRscEnv = await getOrCreateTempServer();
4516
4709
  if (tempRscEnv) {
4517
4710
  await discoverRouters(s, tempRscEnv);
@@ -4527,6 +4720,7 @@ ${err.stack}`
4527
4720
  return;
4528
4721
  }
4529
4722
  try {
4723
+ await acquireBuildEnv(s, viteCommand, viteMode);
4530
4724
  const serverMod = await rscEnv.runner.import(
4531
4725
  "@rangojs/router/server"
4532
4726
  );
@@ -4613,7 +4807,10 @@ ${err.stack}`
4613
4807
  pathname,
4614
4808
  {},
4615
4809
  void 0,
4616
- wantPassthrough
4810
+ wantPassthrough,
4811
+ s.resolvedBuildEnv,
4812
+ true
4813
+ // devMode: check getParams for passthrough routes
4617
4814
  );
4618
4815
  if (!result) continue;
4619
4816
  if (result.passthrough) continue;
@@ -4749,6 +4946,7 @@ ${err.stack}`
4749
4946
  resetStagedBuildAssets(s.projectRoot);
4750
4947
  s.prerenderManifestEntries = null;
4751
4948
  s.staticManifestEntries = null;
4949
+ await acquireBuildEnv(s, viteCommand, viteMode);
4752
4950
  let tempServer = null;
4753
4951
  globalThis.__rscRouterDiscoveryActive = true;
4754
4952
  try {
@@ -4788,6 +4986,7 @@ ${details}`
4788
4986
  if (tempServer) {
4789
4987
  await tempServer.close();
4790
4988
  }
4989
+ await releaseBuildEnv(s);
4791
4990
  }
4792
4991
  },
4793
4992
  // Virtual module: provides the pre-generated route manifest as a JS module
@@ -4830,20 +5029,30 @@ ${details}`
4830
5029
  }
4831
5030
  if (!s.resolvedPrerenderModules?.size && !s.resolvedStaticModules?.size)
4832
5031
  return;
5032
+ s.handlerChunkInfoMap.clear();
5033
+ s.staticHandlerChunkInfoMap.clear();
4833
5034
  for (const [fileName, chunk] of Object.entries(bundle)) {
4834
5035
  if (chunk.type !== "chunk") continue;
4835
- if (fileName.includes("__prerender-handlers") && s.resolvedPrerenderModules?.size) {
5036
+ if (s.resolvedPrerenderModules?.size) {
4836
5037
  const handlers = extractHandlerExportsFromChunk(
4837
5038
  chunk.code,
4838
5039
  s.resolvedPrerenderModules,
4839
5040
  "Prerender",
4840
- true
5041
+ false
4841
5042
  );
4842
5043
  if (handlers.length > 0) {
4843
- s.handlerChunkInfo = { fileName, exports: handlers };
5044
+ const existing = s.handlerChunkInfoMap.get(fileName);
5045
+ if (existing) {
5046
+ existing.exports.push(...handlers);
5047
+ } else {
5048
+ s.handlerChunkInfoMap.set(fileName, {
5049
+ fileName,
5050
+ exports: handlers
5051
+ });
5052
+ }
4844
5053
  }
4845
5054
  }
4846
- if (fileName.includes("__static-handlers") && s.resolvedStaticModules?.size) {
5055
+ if (s.resolvedStaticModules?.size) {
4847
5056
  const handlers = extractHandlerExportsFromChunk(
4848
5057
  chunk.code,
4849
5058
  s.resolvedStaticModules,
@@ -4851,7 +5060,15 @@ ${details}`
4851
5060
  false
4852
5061
  );
4853
5062
  if (handlers.length > 0) {
4854
- s.staticHandlerChunkInfo = { fileName, exports: handlers };
5063
+ const existing = s.staticHandlerChunkInfoMap.get(fileName);
5064
+ if (existing) {
5065
+ existing.exports.push(...handlers);
5066
+ } else {
5067
+ s.staticHandlerChunkInfoMap.set(fileName, {
5068
+ fileName,
5069
+ exports: handlers
5070
+ });
5071
+ }
4855
5072
  }
4856
5073
  }
4857
5074
  }
@@ -4878,8 +5095,16 @@ async function rango(options) {
4878
5095
  const showBanner = resolvedOptions.banner ?? true;
4879
5096
  const plugins = [];
4880
5097
  const rangoAliases = getPackageAliases();
4881
- const excludeDeps = getExcludeDeps();
4882
- let rscEntryPath = null;
5098
+ const excludeDeps = [
5099
+ ...getExcludeDeps(),
5100
+ // The public browser entry re-exports the RSDW browser client.
5101
+ // Excluding both keeps Vite from freezing the unpatched bundle into
5102
+ // .vite/deps before our source transforms run.
5103
+ "@vitejs/plugin-rsc/browser",
5104
+ // Keep the browser RSDW client out of Vite's dep optimizer so our
5105
+ // cjs-to-esm transform can patch the real file.
5106
+ "@vitejs/plugin-rsc/vendor/react-server-dom/client.browser"
5107
+ ];
4883
5108
  const routerRef = { path: void 0 };
4884
5109
  const prerenderEnabled = true;
4885
5110
  if (preset === "cloudflare") {
@@ -4975,6 +5200,7 @@ async function rango(options) {
4975
5200
  }
4976
5201
  });
4977
5202
  plugins.push(createVirtualEntriesPlugin(finalEntries));
5203
+ plugins.push(performanceTracksPlugin());
4978
5204
  plugins.push(
4979
5205
  rsc({
4980
5206
  entries: finalEntries,
@@ -4983,153 +5209,122 @@ async function rango(options) {
4983
5209
  );
4984
5210
  plugins.push(clientRefDedup());
4985
5211
  } else {
4986
- const nodeOptions = resolvedOptions;
4987
- routerRef.path = nodeOptions.router;
4988
- if (!routerRef.path) {
4989
- plugins.push({
4990
- name: "@rangojs/router:auto-discover",
4991
- config(userConfig) {
4992
- if (routerRef.path) return;
4993
- const root = userConfig.root ? resolve9(process.cwd(), userConfig.root) : process.cwd();
4994
- const filter = createScanFilter(root, {
4995
- include: resolvedOptions.include,
4996
- exclude: resolvedOptions.exclude
4997
- });
4998
- const candidates = findRouterFiles(root, filter);
4999
- if (candidates.length === 1) {
5000
- const abs = candidates[0];
5001
- routerRef.path = (abs.startsWith(root) ? "./" + abs.slice(root.length + 1) : abs).replaceAll("\\", "/");
5002
- } else if (candidates.length > 1) {
5003
- const list = candidates.map(
5004
- (f) => " - " + (f.startsWith(root) ? f.slice(root.length + 1) : f)
5005
- ).join("\n");
5006
- throw new Error(
5007
- `[rsc-router] Multiple routers found. Specify \`router\` to choose one:
5008
- ${list}`
5009
- );
5010
- }
5212
+ plugins.push({
5213
+ name: "@rangojs/router:auto-discover",
5214
+ config(userConfig) {
5215
+ if (routerRef.path) return;
5216
+ const root = userConfig.root ? resolve9(process.cwd(), userConfig.root) : process.cwd();
5217
+ const candidates = findRouterFiles(root);
5218
+ if (candidates.length === 1) {
5219
+ const abs = candidates[0];
5220
+ routerRef.path = (abs.startsWith(root) ? "./" + abs.slice(root.length + 1) : abs).replaceAll("\\", "/");
5221
+ } else if (candidates.length > 1) {
5222
+ const list = candidates.map(
5223
+ (f) => " - " + (f.startsWith(root) ? f.slice(root.length + 1) : f)
5224
+ ).join("\n");
5225
+ throw new Error(`[rsc-router] Multiple routers found:
5226
+ ${list}`);
5011
5227
  }
5012
- });
5013
- }
5014
- const rscOption = nodeOptions.rsc ?? true;
5015
- if (rscOption !== false) {
5016
- const { default: rsc } = await import("@vitejs/plugin-rsc");
5017
- const userEntries = typeof rscOption === "boolean" ? {} : rscOption.entries || {};
5018
- const finalEntries = {
5019
- client: userEntries.client ?? VIRTUAL_IDS.browser,
5020
- ssr: userEntries.ssr ?? VIRTUAL_IDS.ssr,
5021
- rsc: userEntries.rsc ?? VIRTUAL_IDS.rsc
5022
- };
5023
- rscEntryPath = userEntries.rsc ?? null;
5024
- let hasWarnedDuplicate = false;
5025
- plugins.push({
5026
- name: "@rangojs/router:rsc-integration",
5027
- enforce: "pre",
5028
- config() {
5029
- const useVirtualClient = finalEntries.client === VIRTUAL_IDS.browser;
5030
- const useVirtualSSR = finalEntries.ssr === VIRTUAL_IDS.ssr;
5031
- const useVirtualRSC = finalEntries.rsc === VIRTUAL_IDS.rsc;
5032
- return {
5033
- // Exclude rsc-router modules from optimization to prevent module duplication
5034
- // This ensures the same Context instance is used by both browser entry and RSC proxy modules
5035
- optimizeDeps: {
5036
- exclude: excludeDeps,
5037
- esbuildOptions: sharedEsbuildOptions
5038
- },
5039
- build: {
5040
- rollupOptions: { onwarn }
5041
- },
5042
- resolve: {
5043
- alias: rangoAliases
5044
- },
5045
- environments: {
5046
- client: {
5047
- build: {
5048
- rollupOptions: {
5049
- output: {
5050
- manualChunks: getManualChunks
5051
- }
5052
- }
5053
- },
5054
- // Always exclude rsc-router modules, conditionally add virtual entry
5055
- optimizeDeps: {
5056
- // Pre-bundle React and rsc-html-stream to prevent late discovery
5057
- // triggering ERR_OUTDATED_OPTIMIZED_DEP on cold starts
5058
- include: [
5059
- "react",
5060
- "react-dom",
5061
- "react/jsx-runtime",
5062
- "react/jsx-dev-runtime",
5063
- "rsc-html-stream/client"
5064
- ],
5065
- exclude: excludeDeps,
5066
- esbuildOptions: sharedEsbuildOptions,
5067
- ...useVirtualClient && {
5068
- // Tell Vite to scan the virtual entry for dependencies
5069
- entries: [VIRTUAL_IDS.browser]
5070
- }
5071
- }
5072
- },
5073
- ...useVirtualSSR && {
5074
- ssr: {
5075
- optimizeDeps: {
5076
- entries: [VIRTUAL_IDS.ssr],
5077
- // Pre-bundle all SSR deps to prevent late discovery triggering ERR_OUTDATED_OPTIMIZED_DEP
5078
- include: [
5079
- "react",
5080
- "react-dom",
5081
- "react-dom/server.edge",
5082
- "react-dom/static.edge",
5083
- "react/jsx-runtime",
5084
- "react/jsx-dev-runtime",
5085
- "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge"
5086
- ],
5087
- exclude: excludeDeps,
5088
- esbuildOptions: sharedEsbuildOptions
5228
+ }
5229
+ });
5230
+ const finalEntries = {
5231
+ client: VIRTUAL_IDS.browser,
5232
+ ssr: VIRTUAL_IDS.ssr,
5233
+ rsc: VIRTUAL_IDS.rsc
5234
+ };
5235
+ const { default: rsc } = await import("@vitejs/plugin-rsc");
5236
+ let hasWarnedDuplicate = false;
5237
+ plugins.push({
5238
+ name: "@rangojs/router:rsc-integration",
5239
+ enforce: "pre",
5240
+ config() {
5241
+ return {
5242
+ optimizeDeps: {
5243
+ exclude: excludeDeps,
5244
+ esbuildOptions: sharedEsbuildOptions
5245
+ },
5246
+ build: {
5247
+ rollupOptions: { onwarn }
5248
+ },
5249
+ resolve: {
5250
+ alias: rangoAliases
5251
+ },
5252
+ environments: {
5253
+ client: {
5254
+ build: {
5255
+ rollupOptions: {
5256
+ output: {
5257
+ manualChunks: getManualChunks
5089
5258
  }
5090
5259
  }
5091
5260
  },
5092
- ...useVirtualRSC && {
5093
- rsc: {
5094
- optimizeDeps: {
5095
- entries: [VIRTUAL_IDS.rsc],
5096
- // Pre-bundle all RSC deps to prevent late discovery triggering ERR_OUTDATED_OPTIMIZED_DEP
5097
- include: [
5098
- "react",
5099
- "react/jsx-runtime",
5100
- "react/jsx-dev-runtime",
5101
- "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge"
5102
- ],
5103
- esbuildOptions: sharedEsbuildOptions
5104
- }
5105
- }
5261
+ optimizeDeps: {
5262
+ include: [
5263
+ "react",
5264
+ "react-dom",
5265
+ "react/jsx-runtime",
5266
+ "react/jsx-dev-runtime",
5267
+ "rsc-html-stream/client"
5268
+ ],
5269
+ exclude: excludeDeps,
5270
+ esbuildOptions: sharedEsbuildOptions,
5271
+ entries: [VIRTUAL_IDS.browser]
5272
+ }
5273
+ },
5274
+ ssr: {
5275
+ optimizeDeps: {
5276
+ entries: [VIRTUAL_IDS.ssr],
5277
+ include: [
5278
+ "react",
5279
+ "react-dom",
5280
+ "react-dom/server.edge",
5281
+ "react-dom/static.edge",
5282
+ "react/jsx-runtime",
5283
+ "react/jsx-dev-runtime",
5284
+ "@vitejs/plugin-rsc/vendor/react-server-dom/client.edge"
5285
+ ],
5286
+ exclude: excludeDeps,
5287
+ esbuildOptions: sharedEsbuildOptions
5288
+ }
5289
+ },
5290
+ rsc: {
5291
+ optimizeDeps: {
5292
+ entries: [VIRTUAL_IDS.rsc],
5293
+ include: [
5294
+ "react",
5295
+ "react/jsx-runtime",
5296
+ "react/jsx-dev-runtime",
5297
+ "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge"
5298
+ ],
5299
+ esbuildOptions: sharedEsbuildOptions
5106
5300
  }
5107
5301
  }
5108
- };
5109
- },
5110
- configResolved(config) {
5111
- if (showBanner) {
5112
- const mode = config.command === "serve" ? process.argv.includes("preview") ? "preview" : "dev" : "build";
5113
- printBanner(mode, "node", rangoVersion);
5114
- }
5115
- const rscMinimalCount = config.plugins.filter(
5116
- (p) => p.name === "rsc:minimal"
5117
- ).length;
5118
- if (rscMinimalCount > 1 && !hasWarnedDuplicate) {
5119
- hasWarnedDuplicate = true;
5120
- console.warn(
5121
- "[rsc-router] Duplicate @vitejs/plugin-rsc detected. Remove rsc() from your config or use rango({ rsc: false }) for manual configuration."
5122
- );
5123
5302
  }
5303
+ };
5304
+ },
5305
+ configResolved(config) {
5306
+ if (showBanner) {
5307
+ const mode = config.command === "serve" ? process.argv.includes("preview") ? "preview" : "dev" : "build";
5308
+ printBanner(mode, "node", rangoVersion);
5124
5309
  }
5125
- });
5126
- plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
5127
- plugins.push(
5128
- rsc({
5129
- entries: finalEntries
5130
- })
5131
- );
5132
- }
5310
+ const rscMinimalCount = config.plugins.filter(
5311
+ (p) => p.name === "rsc:minimal"
5312
+ ).length;
5313
+ if (rscMinimalCount > 1 && !hasWarnedDuplicate) {
5314
+ hasWarnedDuplicate = true;
5315
+ console.warn(
5316
+ "[rsc-router] Duplicate @vitejs/plugin-rsc detected. Remove rsc() from your vite config \u2014 rango() includes it automatically."
5317
+ );
5318
+ }
5319
+ }
5320
+ });
5321
+ plugins.push(createVirtualEntriesPlugin(finalEntries, routerRef));
5322
+ plugins.push(performanceTracksPlugin());
5323
+ plugins.push(
5324
+ rsc({
5325
+ entries: finalEntries
5326
+ })
5327
+ );
5133
5328
  plugins.push(clientRefDedup());
5134
5329
  }
5135
5330
  plugins.push({
@@ -5157,18 +5352,16 @@ ${list}`
5157
5352
  plugins.push(createVersionPlugin());
5158
5353
  const discoveryEntryPath = preset !== "cloudflare" ? routerRef.path : void 0;
5159
5354
  const discoveryRouterRef = preset !== "cloudflare" ? routerRef : void 0;
5160
- const injectorEntryPath = rscEntryPath ?? (preset === "cloudflare" ? void 0 : null);
5161
- if (injectorEntryPath !== null) {
5162
- plugins.push(createVersionInjectorPlugin(injectorEntryPath));
5355
+ if (preset === "cloudflare") {
5356
+ plugins.push(createVersionInjectorPlugin(void 0));
5163
5357
  }
5164
5358
  plugins.push(createCjsToEsmPlugin());
5165
5359
  plugins.push(
5166
5360
  createRouterDiscoveryPlugin(discoveryEntryPath, {
5167
5361
  routerPathRef: discoveryRouterRef,
5168
5362
  enableBuildPrerender: prerenderEnabled,
5169
- staticRouteTypesGeneration: resolvedOptions.staticRouteTypesGeneration,
5170
- include: resolvedOptions.include,
5171
- exclude: resolvedOptions.exclude
5363
+ buildEnv: options?.buildEnv,
5364
+ preset
5172
5365
  })
5173
5366
  );
5174
5367
  return plugins;
@@ -5181,29 +5374,75 @@ function poke() {
5181
5374
  apply: "serve",
5182
5375
  configureServer(server) {
5183
5376
  const stdin = process.stdin;
5184
- const previousRawMode = stdin.isTTY ? stdin.isRaw : null;
5377
+ const debug = process.env.RANGO_POKE_DEBUG === "1";
5378
+ const triggerReload = (source) => {
5379
+ server.hot.send({ type: "full-reload", path: "*" });
5380
+ server.config.logger.info(` browser reload (${source})`, {
5381
+ timestamp: true
5382
+ });
5383
+ };
5384
+ const toBuffer = (chunk) => {
5385
+ return typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
5386
+ };
5387
+ const formatChunk = (chunk) => {
5388
+ const data = toBuffer(chunk);
5389
+ const hex = Array.from(data).map((byte) => `0x${byte.toString(16).padStart(2, "0")}`).join(" ");
5390
+ const ascii = Array.from(data).map((byte) => {
5391
+ if (byte >= 32 && byte <= 126) return String.fromCharCode(byte);
5392
+ if (byte === 10) return "\\n";
5393
+ if (byte === 13) return "\\r";
5394
+ if (byte === 9) return "\\t";
5395
+ return ".";
5396
+ }).join("");
5397
+ return `len=${data.length} hex=[${hex}] ascii="${ascii}"`;
5398
+ };
5399
+ const readCtrlR = (chunk) => {
5400
+ const data = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
5401
+ return data.length === 1 && data[0] === 18;
5402
+ };
5403
+ const readSubmittedCommands = (chunk) => {
5404
+ const text = toBuffer(chunk).toString("utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
5405
+ if (!text.includes("\n")) return [];
5406
+ const lines = text.split("\n");
5407
+ lines.pop();
5408
+ return lines;
5409
+ };
5410
+ if (debug) {
5411
+ server.config.logger.info(
5412
+ ` poke debug enabled (isTTY=${stdin.isTTY ? "yes" : "no"}, isRaw=${stdin.isTTY ? stdin.isRaw ? "yes" : "no" : "n/a"})`,
5413
+ { timestamp: true }
5414
+ );
5415
+ }
5185
5416
  if (stdin.isTTY) {
5186
- stdin.setRawMode(true);
5417
+ server.config.logger.info(
5418
+ " poke ready: press e + enter to reload browser (ctrl+r also works when available)",
5419
+ { timestamp: true }
5420
+ );
5187
5421
  }
5188
5422
  const onData = (data) => {
5189
- if (data.length !== 1) return;
5190
- if (data[0] === 3) {
5191
- process.emit("SIGINT", "SIGINT");
5192
- return;
5193
- }
5194
- if (data[0] === 18) {
5195
- server.hot.send({ type: "full-reload", path: "*" });
5196
- server.config.logger.info(" browser reload (ctrl+r)", {
5423
+ if (debug) {
5424
+ server.config.logger.info(` poke stdin ${formatChunk(data)}`, {
5197
5425
  timestamp: true
5198
5426
  });
5199
5427
  }
5428
+ if (readCtrlR(data)) {
5429
+ triggerReload("ctrl+r");
5430
+ return;
5431
+ }
5432
+ for (const command of readSubmittedCommands(data)) {
5433
+ if (command === "e") {
5434
+ triggerReload("e+enter");
5435
+ return;
5436
+ }
5437
+ if (command === "\x1Br") {
5438
+ triggerReload("option+r+enter");
5439
+ return;
5440
+ }
5441
+ }
5200
5442
  };
5201
5443
  stdin.on("data", onData);
5202
5444
  server.httpServer?.on("close", () => {
5203
5445
  stdin.off("data", onData);
5204
- if (stdin.isTTY && previousRawMode !== null) {
5205
- stdin.setRawMode(previousRawMode);
5206
- }
5207
5446
  });
5208
5447
  }
5209
5448
  };