@rangojs/router 0.0.0-experimental.131 → 0.0.0-experimental.132

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 (51) hide show
  1. package/dist/bin/rango.js +69 -24
  2. package/dist/vite/index.js +182 -41
  3. package/package.json +6 -3
  4. package/src/browser/connection-warmup.ts +134 -0
  5. package/src/browser/event-controller.ts +5 -4
  6. package/src/browser/partial-update.ts +32 -16
  7. package/src/browser/react/NavigationProvider.tsx +6 -83
  8. package/src/browser/react/filter-segment-order.ts +17 -0
  9. package/src/browser/react/use-link-status.ts +10 -2
  10. package/src/browser/react/use-navigation.ts +10 -2
  11. package/src/build/route-types/ast-route-extraction.ts +15 -8
  12. package/src/build/route-types/include-resolution.ts +109 -21
  13. package/src/build/route-types/per-module-writer.ts +15 -2
  14. package/src/cache/cache-key-utils.ts +29 -13
  15. package/src/cache/cf/cf-cache-store.ts +129 -5
  16. package/src/decode-loader-results.ts +11 -1
  17. package/src/encode-kv.ts +49 -0
  18. package/src/handles/meta.ts +5 -1
  19. package/src/host/cookie-handler.ts +2 -21
  20. package/src/prerender/param-hash.ts +6 -5
  21. package/src/regex-escape.ts +8 -0
  22. package/src/route-definition/dsl-helpers.ts +6 -2
  23. package/src/router/error-handling.ts +32 -1
  24. package/src/router/handler-context.ts +6 -1
  25. package/src/router/instrument.ts +14 -10
  26. package/src/router/intercept-resolution.ts +16 -1
  27. package/src/router/loader-resolution.ts +49 -19
  28. package/src/router/match-middleware/background-revalidation.ts +6 -0
  29. package/src/router/match-middleware/cache-store.ts +6 -0
  30. package/src/router/middleware.ts +67 -27
  31. package/src/router/pattern-matching.ts +3 -9
  32. package/src/router/revalidation.ts +65 -23
  33. package/src/router/router-context.ts +1 -0
  34. package/src/router/router-options.ts +3 -3
  35. package/src/router/segment-resolution/loader-cache.ts +13 -0
  36. package/src/router/segment-wrappers.ts +3 -0
  37. package/src/router/trie-matching.ts +74 -20
  38. package/src/router.ts +2 -2
  39. package/src/rsc/progressive-enhancement.ts +20 -0
  40. package/src/rsc/server-action.ts +124 -47
  41. package/src/search-params.ts +8 -6
  42. package/src/segment-system.tsx +7 -1
  43. package/src/server/cookie-parse.ts +32 -0
  44. package/src/server/handle-store.ts +14 -14
  45. package/src/server/request-context.ts +5 -26
  46. package/src/ssr/index.tsx +5 -4
  47. package/src/testing/render-handler.ts +11 -0
  48. package/src/vite/plugins/expose-id-utils.ts +77 -2
  49. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -5
  50. package/src/vite/plugins/expose-ids/router-transform.ts +82 -12
  51. package/src/vite/utils/prerender-utils.ts +1 -3
@@ -5,6 +5,7 @@ import {
5
5
  findStatementEnd,
6
6
  buildExportMap,
7
7
  escapeRegExp,
8
+ findCallParenAfterGenerics,
8
9
  } from "../expose-id-utils.js";
9
10
  import { codeMatchIndices } from "../../../build/route-types/source-scan.js";
10
11
  import type { CreateExportBinding } from "./types.js";
@@ -50,18 +51,42 @@ export function isExportOnlyFile(
50
51
  return true;
51
52
  }
52
53
 
54
+ // Matches the callee identifier only. The optional generic argument list
55
+ // (which may be nested, e.g. createLoader<A<B>>(...)) and the call paren are
56
+ // resolved per match via findCallParenAfterGenerics, so a nested `>` no longer
57
+ // defeats the scan the way `<[^>]*>` did (it stopped at the first `>`).
53
58
  function createCallPattern(fnNames: string[]): RegExp {
54
- return new RegExp(
55
- `\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
56
- "g",
59
+ return new RegExp(`\\b(?:${fnNames.map(escapeRegExp).join("|")})\\b`, "g");
60
+ }
61
+
62
+ /**
63
+ * Byte offsets of every create*-call site in real code: a callee-identifier
64
+ * match that is actually followed by a call `(` (after an optional nested
65
+ * generic list). Non-call references (type positions, the import specifier
66
+ * itself) yield -1 from findCallParenAfterGenerics and are dropped.
67
+ */
68
+ function createCallStartIndices(code: string, fnNames: string[]): number[] {
69
+ return codeMatchIndices(code, createCallPattern(fnNames)).filter(
70
+ (index) =>
71
+ findCallParenAfterGenerics(
72
+ code,
73
+ index + matchedNameLength(code, index),
74
+ ) !== -1,
57
75
  );
58
76
  }
59
77
 
78
+ // Length of the identifier match at `index` (run of identifier chars).
79
+ function matchedNameLength(code: string, index: number): number {
80
+ let i = index;
81
+ while (i < code.length && /[A-Za-z0-9_$]/.test(code[i])) i++;
82
+ return i - index;
83
+ }
84
+
60
85
  export function countCreateCallsForNames(
61
86
  code: string,
62
87
  fnNames: string[],
63
88
  ): number {
64
- return codeMatchIndices(code, createCallPattern(fnNames)).length;
89
+ return createCallStartIndices(code, fnNames).length;
65
90
  }
66
91
 
67
92
  export function offsetToLineColumn(
@@ -86,7 +111,7 @@ export function findUnsupportedCreateCallSites(
86
111
  supportedBindings: CreateExportBinding[],
87
112
  ): Array<{ line: number; column: number }> {
88
113
  const supported = new Set(supportedBindings.map((b) => b.callExprStart));
89
- return codeMatchIndices(code, createCallPattern(fnNames))
114
+ return createCallStartIndices(code, fnNames)
90
115
  .filter((index) => !supported.has(index))
91
116
  .map((index) => offsetToLineColumn(code, index));
92
117
  }
@@ -2,26 +2,65 @@ import type { Plugin } from "vite";
2
2
  import MagicString from "magic-string";
3
3
  import path from "node:path";
4
4
  import { createHash } from "node:crypto";
5
- import { normalizePath, findMatchingParen } from "../expose-id-utils.js";
6
- import { getImportedFnNames } from "./export-analysis.js";
5
+ import {
6
+ normalizePath,
7
+ findMatchingParen,
8
+ findCallParenAfterGenerics,
9
+ skipStringOrComment,
10
+ } from "../expose-id-utils.js";
11
+ import { escapeRegExp } from "../../../regex-escape.js";
12
+ import {
13
+ getImportedFnNames,
14
+ buildUnsupportedShapeWarning,
15
+ } from "./export-analysis.js";
7
16
  import { codeMatchIndices } from "../../../build/route-types/source-scan.js";
8
17
  import { createRangoDebugger, createCounter, NS } from "../../debug.js";
9
18
 
10
19
  const debug = createRangoDebugger(NS.transform);
11
20
 
21
+ /**
22
+ * Advance past leading whitespace and a single leading line/block comment so a
23
+ * `createRouter(/* opts *\/ { ... })` call still resolves to its `{`. Only the
24
+ * first comment-or-whitespace run is skipped (enough for the common prefix
25
+ * case); a non-trivia token after it stops the scan.
26
+ */
27
+ function skipLeadingTrivia(code: string, start: number, end: number): number {
28
+ let i = start;
29
+ while (i < end) {
30
+ const skipped = skipStringOrComment(code, i);
31
+ // skipStringOrComment only advances for strings/comments, not whitespace.
32
+ if (skipped > i) {
33
+ i = skipped;
34
+ continue;
35
+ }
36
+ if (/\s/.test(code[i])) {
37
+ i++;
38
+ continue;
39
+ }
40
+ break;
41
+ }
42
+ return i;
43
+ }
44
+
12
45
  export function transformRouter(
13
46
  code: string,
14
47
  filePath: string,
15
48
  routerFnNames: string[],
16
49
  absolutePath?: string,
50
+ warn?: (message: string) => void,
17
51
  ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
52
+ // Match only the callee identifier; the generic list (which may be nested,
53
+ // e.g. createRouter<Config<Env>>(...)) and the opening paren are located
54
+ // separately via findCallParenAfterGenerics so a nested `>` does not defeat
55
+ // the scan (a `<[^>]*>` regex stopped at the first `>`).
18
56
  const pat = new RegExp(
19
- `\\b(?:${routerFnNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\s*(?:<[^>]*>)?\\s*\\(`,
57
+ `\\b(?:${routerFnNames.map(escapeRegExp).join("|")})\\b`,
20
58
  "g",
21
59
  );
22
60
  let match: RegExpExecArray | null;
23
61
  const s = new MagicString(code);
24
62
  let changed = false;
63
+ const unsupportedSites: Array<{ line: number; column: number }> = [];
25
64
 
26
65
  // Compute the import path for the generated route names file.
27
66
  // filePath is relative to project root (e.g., "src/router.tsx")
@@ -40,32 +79,58 @@ export function transformRouter(
40
79
  while ((match = pat.exec(code)) !== null) {
41
80
  if (!codeOffsets.has(match.index)) continue;
42
81
  const callStart = match.index;
43
- const parenPos = match.index + match[0].length - 1;
82
+ const calleeEnd = match.index + match[0].length;
83
+
84
+ // Resolve the call's opening paren, skipping an optional (possibly nested)
85
+ // generic argument list. A non-call reference (e.g. a bare identifier or a
86
+ // type position) yields -1 and is skipped.
87
+ const parenPos = findCallParenAfterGenerics(code, calleeEnd);
88
+ if (parenPos === -1) continue;
44
89
 
45
90
  const closeParen = findMatchingParen(code, parenPos + 1);
46
91
  const callArgs = code.slice(parenPos + 1, closeParen);
47
92
 
48
93
  if (callArgs.includes("$$id")) continue;
49
94
 
95
+ const sourceFilePath = absolutePath ?? filePath;
50
96
  const lineNumber = code.slice(0, callStart).split("\n").length;
51
97
  const hash = createHash("sha256")
52
98
  .update(`${filePath}:${lineNumber}`)
53
99
  .digest("hex")
54
100
  .slice(0, 8);
55
-
56
- changed = true;
57
- const sourceFilePath = absolutePath ?? filePath;
58
101
  const injected = ` $$id: "${hash}", $$sourceFile: "${sourceFilePath}", $$routeNames: ${routeNamesVar},`;
59
102
 
60
- const afterParen = callArgs.trimStart();
61
- if (afterParen.startsWith("{")) {
62
- const bracePos = code.indexOf("{", parenPos + 1);
63
- s.appendRight(bracePos + 1, injected);
64
- } else if (afterParen.startsWith(")")) {
103
+ // Skip a leading comment/whitespace run so `createRouter(/* c *\/ {...})`
104
+ // and a newline-then-comment prefix still resolve to the object literal.
105
+ // findMatchingParen returns one past the close `)`, so the matching paren
106
+ // sits at closeParen - 1 and empty args resolve to that `)`.
107
+ const argsContentStart = skipLeadingTrivia(code, parenPos + 1, closeParen);
108
+ const firstArgChar = code[argsContentStart];
109
+
110
+ if (firstArgChar === "{") {
111
+ changed = true;
112
+ s.appendRight(argsContentStart + 1, injected);
113
+ } else if (argsContentStart >= closeParen - 1) {
114
+ // Empty args: createRouter(). Wrap a fresh config object.
115
+ changed = true;
65
116
  s.appendRight(parenPos + 1, `{${injected} }`);
117
+ } else {
118
+ // Unsupported argument shape (bare identifier, spread, call, etc.). No
119
+ // stable $$id can be injected here. Record it so the plugin can warn —
120
+ // and crucially do NOT mark changed, so a dead named-routes.gen import is
121
+ // not prepended for a call we never touched.
122
+ const lastNl = code.lastIndexOf("\n", callStart - 1);
123
+ const column = callStart - (lastNl + 1) + 1;
124
+ unsupportedSites.push({ line: lineNumber, column });
66
125
  }
67
126
  }
68
127
 
128
+ if (unsupportedSites.length > 0 && warn) {
129
+ warn(
130
+ buildUnsupportedShapeWarning(filePath, "createRouter", unsupportedSites),
131
+ );
132
+ }
133
+
69
134
  if (!changed) return null;
70
135
 
71
136
  s.prepend(
@@ -110,11 +175,16 @@ export function exposeRouterId(): Plugin {
110
175
  try {
111
176
  const filePath = normalizePath(path.relative(projectRoot, id));
112
177
  const routerFnNames = getImportedFnNames(code, "createRouter");
178
+ const warn =
179
+ typeof this.warn === "function"
180
+ ? (message: string) => this.warn(message)
181
+ : undefined;
113
182
  return transformRouter(
114
183
  code,
115
184
  filePath,
116
185
  routerFnNames,
117
186
  normalizePath(id),
187
+ warn,
118
188
  );
119
189
  } finally {
120
190
  counter?.record(id, performance.now() - start);
@@ -9,9 +9,7 @@ import {
9
9
  } from "node:fs";
10
10
  import { resolve } from "node:path";
11
11
 
12
- export function escapeRegExp(str: string): string {
13
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14
- }
12
+ import { escapeRegExp } from "../../regex-escape.js";
15
13
 
16
14
  export function encodePathParam(value: unknown): string {
17
15
  return String(value)