@rangojs/router 0.0.0-experimental.113 → 0.0.0-experimental.115

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 (36) hide show
  1. package/dist/bin/rango.js +73 -2
  2. package/dist/vite/index.js +193 -19
  3. package/package.json +18 -17
  4. package/skills/hooks/SKILL.md +3 -3
  5. package/skills/links/SKILL.md +10 -10
  6. package/skills/rango/SKILL.md +1 -0
  7. package/skills/react-compiler/SKILL.md +168 -0
  8. package/skills/view-transitions/SKILL.md +85 -3
  9. package/src/browser/react/use-reverse.ts +19 -12
  10. package/src/build/route-types/router-processing.ts +14 -1
  11. package/src/build/route-types/source-scan.ts +118 -0
  12. package/src/handle.ts +3 -5
  13. package/src/loader.rsc.ts +2 -5
  14. package/src/loader.ts +2 -5
  15. package/src/missing-id-error.ts +68 -0
  16. package/src/reverse.ts +16 -13
  17. package/src/route-definition/dsl-helpers.ts +5 -2
  18. package/src/route-definition/helpers-types.ts +31 -19
  19. package/src/router/router-options.ts +24 -0
  20. package/src/router/segment-resolution/fresh.ts +17 -4
  21. package/src/router/segment-resolution/revalidation.ts +17 -4
  22. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  23. package/src/router/types.ts +8 -0
  24. package/src/router.ts +2 -0
  25. package/src/segment-system.tsx +18 -2
  26. package/src/types/segments.ts +18 -1
  27. package/src/urls/path-helper-types.ts +9 -1
  28. package/src/vite/debug.ts +1 -0
  29. package/src/vite/index.ts +2 -0
  30. package/src/vite/plugin-types.ts +67 -0
  31. package/src/vite/plugins/expose-ids/export-analysis.ts +68 -12
  32. package/src/vite/plugins/expose-internal-ids.ts +12 -4
  33. package/src/vite/rango.ts +12 -0
  34. package/src/vite/router-discovery.ts +14 -2
  35. package/src/vite/utils/client-chunks.ts +156 -0
  36. package/src/vite/utils/shared-utils.ts +10 -3
package/dist/bin/rango.js CHANGED
@@ -576,6 +576,75 @@ var init_per_module_writer = __esm({
576
576
  }
577
577
  });
578
578
 
579
+ // src/build/route-types/source-scan.ts
580
+ function isLineTerminator(ch) {
581
+ const c = ch.charCodeAt(0);
582
+ return c === 10 || c === 13 || c === 8232 || c === 8233;
583
+ }
584
+ function makeCodeClassifier(code) {
585
+ const n = code.length;
586
+ let i = 0;
587
+ let skipStart = -1;
588
+ let skipEnd = -1;
589
+ return (q) => {
590
+ if (q >= skipStart && q < skipEnd) return false;
591
+ while (i < n && i <= q) {
592
+ const c = code[i];
593
+ const d = i + 1 < n ? code[i + 1] : "";
594
+ let end = -1;
595
+ if (c === "/" && d === "/") {
596
+ let j = i + 2;
597
+ while (j < n && !isLineTerminator(code[j])) j++;
598
+ end = j;
599
+ } else if (c === "/" && d === "*") {
600
+ let j = i + 2;
601
+ while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++;
602
+ end = Math.min(n, j + 2);
603
+ } else if (c === '"' || c === "'" || c === "`") {
604
+ let j = i + 1;
605
+ while (j < n) {
606
+ if (code[j] === "\\") {
607
+ j += 2;
608
+ continue;
609
+ }
610
+ if (code[j] === c) {
611
+ j++;
612
+ break;
613
+ }
614
+ j++;
615
+ }
616
+ end = j;
617
+ }
618
+ if (end >= 0) {
619
+ if (q < end) {
620
+ skipStart = i;
621
+ skipEnd = end;
622
+ return false;
623
+ }
624
+ i = end;
625
+ } else {
626
+ i++;
627
+ }
628
+ }
629
+ return true;
630
+ };
631
+ }
632
+ function firstCodeMatchIndex(code, pattern) {
633
+ const inCode = makeCodeClassifier(code);
634
+ pattern.lastIndex = 0;
635
+ let m;
636
+ while ((m = pattern.exec(code)) !== null) {
637
+ if (inCode(m.index)) return m.index;
638
+ if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
639
+ }
640
+ return -1;
641
+ }
642
+ var init_source_scan = __esm({
643
+ "src/build/route-types/source-scan.ts"() {
644
+ "use strict";
645
+ }
646
+ });
647
+
579
648
  // src/build/route-types/router-processing.ts
580
649
  import {
581
650
  readFileSync as readFileSync3,
@@ -630,7 +699,7 @@ function findRouterFilesRecursive(dir, filter, results) {
630
699
  if (filter && !filter(fullPath)) continue;
631
700
  try {
632
701
  const source = readFileSync3(fullPath, "utf-8");
633
- if (ROUTER_CALL_PATTERN.test(source)) {
702
+ if (ROUTER_CALL_PATTERN.test(source) && firstCodeMatchIndex(source, ROUTER_CALL_PATTERN_G) >= 0) {
634
703
  routerFilesInDir.push(fullPath);
635
704
  }
636
705
  } catch {
@@ -971,15 +1040,17 @@ function writeCombinedRouteTypes(root, knownRouterFiles, opts) {
971
1040
  }
972
1041
  }
973
1042
  }
974
- var ROUTER_CALL_PATTERN;
1043
+ var ROUTER_CALL_PATTERN, ROUTER_CALL_PATTERN_G;
975
1044
  var init_router_processing = __esm({
976
1045
  "src/build/route-types/router-processing.ts"() {
977
1046
  "use strict";
978
1047
  init_codegen();
1048
+ init_source_scan();
979
1049
  init_include_resolution();
980
1050
  init_per_module_writer();
981
1051
  init_route_name();
982
1052
  ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
1053
+ ROUTER_CALL_PATTERN_G = /\bcreateRouter\s*[<(]/g;
983
1054
  }
984
1055
  });
985
1056
 
@@ -200,7 +200,8 @@ var NS = {
200
200
  prerender: "rango:prerender",
201
201
  build: "rango:build",
202
202
  dev: "rango:dev",
203
- transform: "rango:transform"
203
+ transform: "rango:transform",
204
+ chunks: "rango:chunks"
204
205
  };
205
206
  if (process.env.INTERNAL_RANGO_DEBUG) {
206
207
  const existing = debugFactory.disable();
@@ -744,6 +745,83 @@ var STRICT_CREATE_CONFIGS = [
744
745
 
745
746
  // src/vite/plugins/expose-ids/export-analysis.ts
746
747
  import { parseAst } from "vite";
748
+
749
+ // src/build/route-types/source-scan.ts
750
+ function isLineTerminator(ch) {
751
+ const c = ch.charCodeAt(0);
752
+ return c === 10 || c === 13 || c === 8232 || c === 8233;
753
+ }
754
+ function makeCodeClassifier(code) {
755
+ const n = code.length;
756
+ let i = 0;
757
+ let skipStart = -1;
758
+ let skipEnd = -1;
759
+ return (q) => {
760
+ if (q >= skipStart && q < skipEnd) return false;
761
+ while (i < n && i <= q) {
762
+ const c = code[i];
763
+ const d = i + 1 < n ? code[i + 1] : "";
764
+ let end = -1;
765
+ if (c === "/" && d === "/") {
766
+ let j = i + 2;
767
+ while (j < n && !isLineTerminator(code[j])) j++;
768
+ end = j;
769
+ } else if (c === "/" && d === "*") {
770
+ let j = i + 2;
771
+ while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++;
772
+ end = Math.min(n, j + 2);
773
+ } else if (c === '"' || c === "'" || c === "`") {
774
+ let j = i + 1;
775
+ while (j < n) {
776
+ if (code[j] === "\\") {
777
+ j += 2;
778
+ continue;
779
+ }
780
+ if (code[j] === c) {
781
+ j++;
782
+ break;
783
+ }
784
+ j++;
785
+ }
786
+ end = j;
787
+ }
788
+ if (end >= 0) {
789
+ if (q < end) {
790
+ skipStart = i;
791
+ skipEnd = end;
792
+ return false;
793
+ }
794
+ i = end;
795
+ } else {
796
+ i++;
797
+ }
798
+ }
799
+ return true;
800
+ };
801
+ }
802
+ function firstCodeMatchIndex(code, pattern) {
803
+ const inCode = makeCodeClassifier(code);
804
+ pattern.lastIndex = 0;
805
+ let m;
806
+ while ((m = pattern.exec(code)) !== null) {
807
+ if (inCode(m.index)) return m.index;
808
+ if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
809
+ }
810
+ return -1;
811
+ }
812
+ function codeMatchIndices(code, pattern) {
813
+ const inCode = makeCodeClassifier(code);
814
+ const indices = [];
815
+ pattern.lastIndex = 0;
816
+ let m;
817
+ while ((m = pattern.exec(code)) !== null) {
818
+ if (inCode(m.index)) indices.push(m.index);
819
+ if (pattern.lastIndex <= m.index) pattern.lastIndex = m.index + 1;
820
+ }
821
+ return indices;
822
+ }
823
+
824
+ // src/vite/plugins/expose-ids/export-analysis.ts
747
825
  function isExportOnlyFile(code, bindings) {
748
826
  if (bindings.length === 0) return false;
749
827
  const knownLocals = /* @__PURE__ */ new Set();
@@ -772,12 +850,30 @@ function isExportOnlyFile(code, bindings) {
772
850
  }
773
851
  return true;
774
852
  }
775
- function countCreateCallsForNames(code, fnNames) {
776
- const pattern = new RegExp(
853
+ function createCallPattern(fnNames) {
854
+ return new RegExp(
777
855
  `\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
778
856
  "g"
779
857
  );
780
- return (code.match(pattern) || []).length;
858
+ }
859
+ function countCreateCallsForNames(code, fnNames) {
860
+ return codeMatchIndices(code, createCallPattern(fnNames)).length;
861
+ }
862
+ function offsetToLineColumn(code, index) {
863
+ let line = 1;
864
+ let lineStart = 0;
865
+ const end = Math.min(index, code.length);
866
+ for (let i = 0; i < end; i++) {
867
+ if (code[i] === "\n") {
868
+ line++;
869
+ lineStart = i + 1;
870
+ }
871
+ }
872
+ return { line, column: index - lineStart + 1 };
873
+ }
874
+ function findUnsupportedCreateCallSites(code, fnNames, supportedBindings) {
875
+ const supported = new Set(supportedBindings.map((b) => b.callExprStart));
876
+ return codeMatchIndices(code, createCallPattern(fnNames)).filter((index) => !supported.has(index)).map((index) => offsetToLineColumn(code, index));
781
877
  }
782
878
  function getImportedFnNames(code, importedName) {
783
879
  const importPattern = /import\s*\{([^}]*)\}\s*from\s*["']@rangojs\/router(?:\/[^"']*)?["']/g;
@@ -936,9 +1032,20 @@ function collectCreateExportBindings(code, fnNames, program) {
936
1032
  }
937
1033
  return bindings;
938
1034
  }
939
- function buildUnsupportedShapeWarning(filePath, fnName) {
940
- return [
941
- `[rango] Unsupported ${fnName} shape in "${filePath}".`,
1035
+ function buildUnsupportedShapeWarning(filePath, fnName, sites = []) {
1036
+ const lines = [`[rango] Unsupported ${fnName} shape in "${filePath}".`];
1037
+ if (sites.length === 1) {
1038
+ const s = sites[0];
1039
+ lines.push(
1040
+ `The ${fnName}(...) call at ${filePath}:${s.line}:${s.column} has no stable $$id injected \u2014 it is not in a supported shape.`
1041
+ );
1042
+ } else if (sites.length > 1) {
1043
+ lines.push(
1044
+ `These ${fnName}(...) calls have no stable $$id injected \u2014 they are not in a supported shape:`
1045
+ );
1046
+ for (const s of sites) lines.push(` - ${filePath}:${s.line}:${s.column}`);
1047
+ }
1048
+ lines.push(
942
1049
  `Supported shapes are:`,
943
1050
  ` - export const X = ${fnName}(...)`,
944
1051
  ` - const X = ${fnName}(...); export { X }`,
@@ -946,7 +1053,8 @@ function buildUnsupportedShapeWarning(filePath, fnName) {
946
1053
  `Potentially unsupported forms include:`,
947
1054
  ` - export let/var X = ${fnName}(...)`,
948
1055
  ` - inline ${fnName}(...) calls`
949
- ].join("\n");
1056
+ );
1057
+ return lines.join("\n");
950
1058
  }
951
1059
 
952
1060
  // src/vite/plugins/expose-ids/loader-transform.ts
@@ -1366,13 +1474,16 @@ ${lazyImports.join(",\n")}
1366
1474
  const hasCode = cfg.fnName === "createLoader" ? hasLoaderCode : cfg.fnName === "createHandle" ? hasHandleCode : hasLocationStateCode;
1367
1475
  if (!hasCode) continue;
1368
1476
  const fnNames = getFnNames(cfg.fnName);
1369
- const totalCalls = countCreateCallsForNames(code, fnNames);
1370
- const supportedBindings = getBindings(code, fnNames).length;
1371
- if (totalCalls <= supportedBindings) continue;
1477
+ const sites = findUnsupportedCreateCallSites(
1478
+ code,
1479
+ fnNames,
1480
+ getBindings(code, fnNames)
1481
+ );
1482
+ if (sites.length === 0) continue;
1372
1483
  const warnKey = `${id}::${cfg.fnName}`;
1373
1484
  if (unsupportedShapeWarnings.has(warnKey)) continue;
1374
1485
  unsupportedShapeWarnings.add(warnKey);
1375
- this.warn(buildUnsupportedShapeWarning(filePath, cfg.fnName));
1486
+ this.warn(buildUnsupportedShapeWarning(filePath, cfg.fnName, sites));
1376
1487
  }
1377
1488
  if (hasLoaderCode && isRscEnv) {
1378
1489
  const fnNames = getFnNames("createLoader");
@@ -2019,7 +2130,7 @@ import { resolve } from "node:path";
2019
2130
  // package.json
2020
2131
  var package_default = {
2021
2132
  name: "@rangojs/router",
2022
- version: "0.0.0-experimental.113",
2133
+ version: "0.0.0-experimental.115",
2023
2134
  description: "Django-inspired RSC router with composable URL patterns",
2024
2135
  keywords: [
2025
2136
  "react",
@@ -2677,6 +2788,7 @@ function countPublicRouteEntries(source) {
2677
2788
  return count;
2678
2789
  }
2679
2790
  var ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
2791
+ var ROUTER_CALL_PATTERN_G = /\bcreateRouter\s*[<(]/g;
2680
2792
  function isRoutableSourceFile(name) {
2681
2793
  return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !name.includes(".gen.") && !name.includes(".test.") && !name.includes(".spec.");
2682
2794
  }
@@ -2704,7 +2816,7 @@ function findRouterFilesRecursive(dir, filter, results) {
2704
2816
  if (filter && !filter(fullPath)) continue;
2705
2817
  try {
2706
2818
  const source = readFileSync2(fullPath, "utf-8");
2707
- if (ROUTER_CALL_PATTERN.test(source)) {
2819
+ if (ROUTER_CALL_PATTERN.test(source) && firstCodeMatchIndex(source, ROUTER_CALL_PATTERN_G) >= 0) {
2708
2820
  routerFilesInDir.push(fullPath);
2709
2821
  }
2710
2822
  } catch {
@@ -3323,12 +3435,65 @@ function getManualChunks(id) {
3323
3435
  return "react";
3324
3436
  }
3325
3437
  const packageName = getPublishedPackageName();
3326
- if (normalized.includes(`node_modules/${packageName}/`) || normalized.includes("packages/rsc-router/") || normalized.includes("packages/rangojs-router/")) {
3438
+ if (normalized.includes(`node_modules/${packageName}/`) || /\/packages\/(rsc-router|rangojs-router)\/(src|dist)\//.test(normalized)) {
3327
3439
  return "router";
3328
3440
  }
3329
3441
  return void 0;
3330
3442
  }
3331
3443
 
3444
+ // src/vite/utils/client-chunks.ts
3445
+ var debugChunks = createRangoDebugger(NS.chunks);
3446
+ function isSharedRuntime(meta) {
3447
+ return [meta.id, meta.normalizedId].some(
3448
+ (path6) => path6.includes("/node_modules/") || /\/@rangojs\/router\//.test(path6) || /\/packages\/(rangojs-router|rsc-router)\/(src|dist)\//.test(path6)
3449
+ );
3450
+ }
3451
+ function sanitizeGroup(name) {
3452
+ return name.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "") || "app";
3453
+ }
3454
+ var ROUTE_ROOT_DIRS = /* @__PURE__ */ new Set([
3455
+ "routes",
3456
+ "route",
3457
+ "pages",
3458
+ "page",
3459
+ "app",
3460
+ "features",
3461
+ "feature",
3462
+ "views",
3463
+ "view",
3464
+ "handlers",
3465
+ "urls",
3466
+ "modules",
3467
+ "screens",
3468
+ "sections"
3469
+ ]);
3470
+ function directoryClientChunks(meta) {
3471
+ if (isSharedRuntime(meta)) {
3472
+ return void 0;
3473
+ }
3474
+ const segments = meta.normalizedId.split("/").filter(Boolean);
3475
+ const dirCount = segments.length - 1;
3476
+ if (dirCount >= 1) {
3477
+ for (let i = 0; i < dirCount - 1; i++) {
3478
+ if (ROUTE_ROOT_DIRS.has(segments[i].toLowerCase())) {
3479
+ const group = `app-${sanitizeGroup(segments[i + 1])}`;
3480
+ debugChunks?.("split %s -> %s", meta.normalizedId, group);
3481
+ return group;
3482
+ }
3483
+ }
3484
+ }
3485
+ debugChunks?.(
3486
+ "shared %s (no route-root marker; inherits default grouping)",
3487
+ meta.normalizedId
3488
+ );
3489
+ return void 0;
3490
+ }
3491
+ function resolveClientChunks(option) {
3492
+ if (!option) return void 0;
3493
+ if (option === true) return directoryClientChunks;
3494
+ return option;
3495
+ }
3496
+
3332
3497
  // src/vite/utils/banner.ts
3333
3498
  var rangoVersion = package_default.version;
3334
3499
  var _bannerPrinted = false;
@@ -5946,8 +6111,12 @@ ${err.stack}`
5946
6111
  const trimmed = source.trimStart();
5947
6112
  const isUseClient = trimmed.startsWith('"use client"') || trimmed.startsWith("'use client'");
5948
6113
  if (!inRecoveryMode && isUseClient) return;
5949
- const hasUrls = source.includes("urls(");
5950
- const hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
6114
+ let hasUrls = source.includes("urls(");
6115
+ let hasCreateRouter = /\bcreateRouter\s*[<(]/.test(source);
6116
+ if (hasUrls) hasUrls = firstCodeMatchIndex(source, /urls\(/g) >= 0;
6117
+ if (hasCreateRouter) {
6118
+ hasCreateRouter = firstCodeMatchIndex(source, /\bcreateRouter\s*[<(]/g) >= 0;
6119
+ }
5951
6120
  if (!inRecoveryMode && !hasUrls && !hasCreateRouter) return;
5952
6121
  if (inRecoveryMode) {
5953
6122
  debugDiscovery?.(
@@ -6259,6 +6428,9 @@ async function rango(options) {
6259
6428
  const resolvedOptions = options ?? { preset: "node" };
6260
6429
  const preset = resolvedOptions.preset ?? "node";
6261
6430
  const showBanner = resolvedOptions.banner ?? true;
6431
+ const clientChunks = resolveClientChunks(
6432
+ resolvedOptions.clientChunks ?? true
6433
+ );
6262
6434
  debugConfig?.("rango(%s) setup start", preset);
6263
6435
  const plugins = [];
6264
6436
  const rangoAliases = { ...getPackageAliases(), ...getVendorAliases() };
@@ -6384,7 +6556,8 @@ async function rango(options) {
6384
6556
  plugins.push(
6385
6557
  rsc({
6386
6558
  entries: finalEntries,
6387
- serverHandler: false
6559
+ serverHandler: false,
6560
+ clientChunks
6388
6561
  })
6389
6562
  );
6390
6563
  plugins.push(clientRefDedup());
@@ -6514,7 +6687,8 @@ ${list}`);
6514
6687
  plugins.push(performanceTracksPlugin());
6515
6688
  plugins.push(
6516
6689
  rsc({
6517
- entries: finalEntries
6690
+ entries: finalEntries,
6691
+ clientChunks
6518
6692
  })
6519
6693
  );
6520
6694
  plugins.push(clientRefDedup());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.113",
3
+ "version": "0.0.0-experimental.115",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -132,6 +132,16 @@
132
132
  "access": "public",
133
133
  "tag": "experimental"
134
134
  },
135
+ "scripts": {
136
+ "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
137
+ "prepublishOnly": "pnpm build",
138
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
139
+ "test": "playwright test",
140
+ "test:ui": "playwright test --ui",
141
+ "test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
142
+ "test:unit": "vitest run",
143
+ "test:unit:watch": "vitest"
144
+ },
135
145
  "dependencies": {
136
146
  "@types/debug": "^4.1.12",
137
147
  "@vitejs/plugin-rsc": "^0.5.26",
@@ -142,17 +152,17 @@
142
152
  },
143
153
  "devDependencies": {
144
154
  "@playwright/test": "^1.49.1",
155
+ "@shared/e2e": "workspace:*",
145
156
  "@types/node": "^24.10.1",
146
- "@types/react": "^19.2.7",
147
- "@types/react-dom": "^19.2.3",
157
+ "@types/react": "catalog:",
158
+ "@types/react-dom": "catalog:",
148
159
  "esbuild": "^0.27.0",
149
160
  "jiti": "^2.6.1",
150
- "react": "^19.2.6",
151
- "react-dom": "^19.2.6",
161
+ "react": "catalog:",
162
+ "react-dom": "catalog:",
152
163
  "tinyexec": "^0.3.2",
153
164
  "typescript": "^5.3.0",
154
- "vitest": "^4.0.0",
155
- "@shared/e2e": "0.0.1"
165
+ "vitest": "^4.0.0"
156
166
  },
157
167
  "peerDependencies": {
158
168
  "@cloudflare/vite-plugin": "^1.38.0",
@@ -168,14 +178,5 @@
168
178
  "vite": {
169
179
  "optional": true
170
180
  }
171
- },
172
- "scripts": {
173
- "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm dlx esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
174
- "typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
175
- "test": "playwright test",
176
- "test:ui": "playwright test --ui",
177
- "test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
178
- "test:unit": "vitest run",
179
- "test:unit:watch": "vitest"
180
181
  }
181
- }
182
+ }
@@ -868,7 +868,7 @@ function MountInfo() {
868
868
 
869
869
  ### useReverse(routes)
870
870
 
871
- Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse(".name", params?)`. Auto-fills params from `useParams()`; explicit params override.
871
+ Mount-aware local reverse for client components. Import the generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("name", params?)` — the leading dot is optional. Auto-fills params from `useParams()`; explicit params override.
872
872
 
873
873
  > Per-module `*.gen.ts` files are **CLI opt-in and not Vite-watched** — run `rango generate <urls-file>` (or wire it into `predev`) and re-run it whenever the module's routes change. See `/links` for the full generated-file setup and exposure-boundary rules.
874
874
 
@@ -881,8 +881,8 @@ function BlogNav() {
881
881
  const reverse = useReverse(blogRoutes);
882
882
  return (
883
883
  <nav>
884
- <Link to={reverse(".index")}>Blog</Link>
885
- <Link to={reverse(".post", { postId: "hello" })}>Post</Link>
884
+ <Link to={reverse("index")}>Blog</Link>
885
+ <Link to={reverse("post", { postId: "hello" })}>Post</Link>
886
886
  </nav>
887
887
  );
888
888
  }
@@ -13,7 +13,7 @@ argument-hint: [ctx.reverse|href|useHref|useMount|useReverse|scopedReverse]
13
13
  **On the client, two patterns:**
14
14
 
15
15
  1. **Receive URLs as props / loader data / action return.** The default. The server has the full route manifest and handler context — generate URLs there and hand strings to client components.
16
- 2. **`useReverse(routes)`.** Import a generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse(".name", params?)`. Mount-aware via `useMount()`, auto-fills params from `useParams()`, fully typed from the imported map. Use this when a client component needs to generate URLs into a known module without round-tripping through the server.
16
+ 2. **`useReverse(routes)`.** Import a generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("name", params?)` (the leading dot is optional). Mount-aware via `useMount()`, auto-fills params from `useParams()`, fully typed from the imported map. Use this when a client component needs to generate URLs into a known module without round-tripping through the server.
17
17
 
18
18
  `ctx.reverse()` itself is **server-only** — it depends on the full route manifest and handler context. Client components never import or call it.
19
19
 
@@ -273,7 +273,7 @@ function MountInfo() {
273
273
 
274
274
  Hook that returns a typed local reverse function for a `routes` map imported from a generated `.gen.ts` next to a `urls()` module. The route map is the **exposure boundary** — `useReverse` only knows about names in that map, never the full app manifest.
275
275
 
276
- > Import the per-module `routes` (e.g. `urls/blog.gen.ts`), **not** `router.named-routes.gen.ts`. The named-routes file is the whole app manifest and is server-only data importing it into a client component pulls every route name into the client bundle.
276
+ > **Which map?** `useReverse` accepts any routes map. Prefer the per-module `routes` (e.g. `urls/blog.gen.ts`): it gives **mount-aware** local `.name` reverse (auto-prefixes the `include()` mount) and only that module's names enter the client bundle. You _can_ instead pass `router.named-routes.gen.ts` (`NamedRoutes`) for **global** names (`blog.post`; the leading dot is optional) — it is a plain importable map and works on the client (it is **not** server-only)but its paths are **absolute** while `useReverse` mount-prefixes, so it is correct only at the root mount (under a non-root mount it double-prefixes), and importing it pulls every route name and pattern in the app into the client bundle (a small names-to-paths map — not components or loaders), versus the per-module map which exposes only one module's names. So the per-module map is preferred for in-module links; the named-routes map is the escape hatch for global names.
277
277
 
278
278
  ```tsx
279
279
  "use client";
@@ -285,8 +285,8 @@ export function BlogNav() {
285
285
 
286
286
  return (
287
287
  <nav>
288
- <Link to={reverse(".index")}>Blog</Link>
289
- <Link to={reverse(".post", { postId: "hello" })}>Post</Link>
288
+ <Link to={reverse("index")}>Blog</Link>
289
+ <Link to={reverse("post", { postId: "hello" })}>Post</Link>
290
290
  </nav>
291
291
  );
292
292
  }
@@ -294,7 +294,7 @@ export function BlogNav() {
294
294
 
295
295
  ### How it resolves
296
296
 
297
- 1. Strips the leading `.` and looks up the name in the imported `routes` map.
297
+ 1. Strips an optional leading `.` and looks up the name in the imported `routes` map.
298
298
  2. Joins the local pattern with the surrounding `useMount()` value — the include's URL pattern.
299
299
  3. Substitutes params: explicit params from the call, then auto-filled from `useParams()` for anything still unresolved (mount params like `:tenantId` flow in this way).
300
300
  4. Appends a query string if a search object is passed and the route has a `search` schema.
@@ -362,14 +362,14 @@ reverse(".search", {}, { q: "hello world", page: 2 });
362
362
 
363
363
  ### Errors
364
364
 
365
- - Unknown name: throws `Unknown local route: ".not-a-route"`.
365
+ - Unknown name: throws `Unknown route: ".not-a-route"`.
366
366
  - Missing required param: throws `Missing param "postId" for route ".detail"`.
367
367
 
368
368
  Both happen synchronously during `reverse()` — wrap calls in try/catch (or an ErrorBoundary if the throw happens during render) when you need to surface them as UI.
369
369
 
370
- ### Names are dot-only on the client
370
+ ### The leading dot is optional
371
371
 
372
- `useReverse` accepts only `.name` (and dotted variants like `.nested.index`). There is no global namespace on the client the import IS the scope. To link into a different module, import that module's `routes`:
372
+ `reverse("post")` and `reverse(".post")` resolve **identically** the leading dot is cosmetic. The map you import IS the scope, so there is no separate global namespace to disambiguate and the dot carries no meaning; it exists only as a readability convention and for parity with `ctx.reverse(".name")` on the server. To link into a different module, import that module's `routes`:
373
373
 
374
374
  ```tsx
375
375
  import { routes as blogRoutes } from "../urls/blog.gen.js";
@@ -380,8 +380,8 @@ function CrossNav() {
380
380
  const shop = useReverse(shopRoutes);
381
381
  return (
382
382
  <nav>
383
- <Link to={blog(".index")}>Blog</Link>
384
- <Link to={shop(".cart")}>Cart</Link>
383
+ <Link to={blog("index")}>Blog</Link>
384
+ <Link to={shop("cart")}>Cart</Link>
385
385
  </nav>
386
386
  );
387
387
  }
@@ -220,6 +220,7 @@ Grouped by concern — read when you need to…
220
220
  | `/tailwind` | Set up Tailwind CSS v4 with `?url` imports |
221
221
  | `/view-transitions` | React View Transitions on layouts, routes, and parallel slots |
222
222
  | `/breadcrumbs` | Built-in Breadcrumbs handle for breadcrumb navigation |
223
+ | `/react-compiler` | Enable React Compiler (opt-in) the vite-rsc way; client-only scope |
223
224
 
224
225
  **Observability & production health**:
225
226