@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.
- package/dist/bin/rango.js +69 -24
- package/dist/vite/index.js +182 -41
- package/package.json +6 -3
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/event-controller.ts +5 -4
- package/src/browser/partial-update.ts +32 -16
- package/src/browser/react/NavigationProvider.tsx +6 -83
- package/src/browser/react/filter-segment-order.ts +17 -0
- package/src/browser/react/use-link-status.ts +10 -2
- package/src/browser/react/use-navigation.ts +10 -2
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/include-resolution.ts +109 -21
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cf/cf-cache-store.ts +129 -5
- package/src/decode-loader-results.ts +11 -1
- package/src/encode-kv.ts +49 -0
- package/src/handles/meta.ts +5 -1
- package/src/host/cookie-handler.ts +2 -21
- package/src/prerender/param-hash.ts +6 -5
- package/src/regex-escape.ts +8 -0
- package/src/route-definition/dsl-helpers.ts +6 -2
- package/src/router/error-handling.ts +32 -1
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +14 -10
- package/src/router/intercept-resolution.ts +16 -1
- package/src/router/loader-resolution.ts +49 -19
- package/src/router/match-middleware/background-revalidation.ts +6 -0
- package/src/router/match-middleware/cache-store.ts +6 -0
- package/src/router/middleware.ts +67 -27
- package/src/router/pattern-matching.ts +3 -9
- package/src/router/revalidation.ts +65 -23
- package/src/router/router-context.ts +1 -0
- package/src/router/router-options.ts +3 -3
- package/src/router/segment-resolution/loader-cache.ts +13 -0
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/trie-matching.ts +74 -20
- package/src/router.ts +2 -2
- package/src/rsc/progressive-enhancement.ts +20 -0
- package/src/rsc/server-action.ts +124 -47
- package/src/search-params.ts +8 -6
- package/src/segment-system.tsx +7 -1
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/handle-store.ts +14 -14
- package/src/server/request-context.ts +5 -26
- package/src/ssr/index.tsx +5 -4
- package/src/testing/render-handler.ts +11 -0
- package/src/vite/plugins/expose-id-utils.ts +77 -2
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +82 -12
- 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
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
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 {
|
|
6
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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)
|