@rangojs/router 0.0.0-experimental.130 → 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 +56 -14
- 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/fresh.ts +8 -9
- package/src/router/segment-resolution/helpers.ts +11 -10
- package/src/router/segment-resolution/loader-cache.ts +13 -0
- package/src/router/segment-resolution/revalidation.ts +4 -4
- 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
package/src/ssr/index.tsx
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { renderSegments } from "../segment-system.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
filterSegmentOrder,
|
|
5
|
+
filterRouteSegmentIds,
|
|
6
|
+
} from "../browser/react/filter-segment-order.js";
|
|
4
7
|
import { ThemeProvider } from "../theme/ThemeProvider.js";
|
|
5
8
|
import { NonceContext } from "../browser/react/nonce-context.js";
|
|
6
9
|
import { NavigationStoreContext } from "../browser/react/context.js";
|
|
@@ -166,9 +169,7 @@ function createSsrEventController(opts: {
|
|
|
166
169
|
const handleState = {
|
|
167
170
|
data: opts.handleData ?? {},
|
|
168
171
|
segmentOrder: filterSegmentOrder(rawMatched),
|
|
169
|
-
routeSegmentIds: rawMatched
|
|
170
|
-
(id) => !id.includes(".@") && !/D\d+\./.test(id),
|
|
171
|
-
),
|
|
172
|
+
routeSegmentIds: filterRouteSegmentIds(rawMatched),
|
|
172
173
|
};
|
|
173
174
|
const state: DerivedNavigationState = {
|
|
174
175
|
state: "idle",
|
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
} from "./flight.js";
|
|
50
50
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
51
51
|
import type { CacheProfile } from "../cache/profile-registry.js";
|
|
52
|
+
import type { ThemeConfig } from "../theme/types.js";
|
|
53
|
+
import { resolveThemeConfig } from "../theme/constants.js";
|
|
52
54
|
import {
|
|
53
55
|
deserializeFlight,
|
|
54
56
|
makeClientManifest,
|
|
@@ -114,6 +116,13 @@ export interface RenderHandlerOptions<TEnv = any> {
|
|
|
114
116
|
* `"use cache: profileName"` resolution once a `cacheStore` is wired.
|
|
115
117
|
*/
|
|
116
118
|
cacheProfiles?: Record<string, CacheProfile>;
|
|
119
|
+
/**
|
|
120
|
+
* Theme config in the same shape `createRouter({ theme })` takes (e.g. `true`
|
|
121
|
+
* or `{ themes: [...] }`). Without it `ctx.theme`/`ctx.setTheme` are inert,
|
|
122
|
+
* mirroring an app with no theme configured. Pass one to exercise a handler
|
|
123
|
+
* that reads `ctx.theme` or writes the theme cookie via `ctx.setTheme(...)`.
|
|
124
|
+
*/
|
|
125
|
+
theme?: ThemeConfig | true;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
/** Result of {@link renderHandler}. */
|
|
@@ -225,6 +234,8 @@ export async function renderHandler<TEnv = any>(
|
|
|
225
234
|
version: opts.stateCookie?.version,
|
|
226
235
|
cacheStore: opts.cacheStore,
|
|
227
236
|
cacheProfiles: opts.cacheProfiles,
|
|
237
|
+
themeConfig:
|
|
238
|
+
opts.theme === undefined ? undefined : resolveThemeConfig(opts.theme),
|
|
228
239
|
});
|
|
229
240
|
|
|
230
241
|
const loaderSeeds = new Map<unknown, unknown>(opts.loaders ?? []);
|
|
@@ -243,6 +243,81 @@ export function findStatementEnd(code: string, pos: number): number {
|
|
|
243
243
|
return i;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
export
|
|
247
|
-
|
|
246
|
+
export { escapeRegExp } from "../../regex-escape.js";
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Given an index pointing just past a `create*` callee identifier, return the
|
|
250
|
+
* index of the call's opening `(`, accounting for an optional generic type
|
|
251
|
+
* argument list `<...>` with arbitrarily nested `<>`. Returns -1 when the next
|
|
252
|
+
* non-trivia token is neither `<` nor `(` (i.e. not a call site).
|
|
253
|
+
*
|
|
254
|
+
* Replaces the single-level `<[^>]*>` regex assumption, which stopped at the
|
|
255
|
+
* first `>` and so never reached `(` for `createRouter<Config<Env>>(`.
|
|
256
|
+
*
|
|
257
|
+
* Whitespace, comments, and strings between the callee and `(` are skipped via
|
|
258
|
+
* skipStringOrComment. The `<...>` scan only balances angle brackets; it does
|
|
259
|
+
* not attempt to parse full TS type syntax (sufficient for locating the paren).
|
|
260
|
+
*/
|
|
261
|
+
export function findCallParenAfterGenerics(
|
|
262
|
+
code: string,
|
|
263
|
+
afterCalleeIndex: number,
|
|
264
|
+
): number {
|
|
265
|
+
let i = afterCalleeIndex;
|
|
266
|
+
// Skip leading whitespace/comments/strings before `<` or `(`.
|
|
267
|
+
while (i < code.length) {
|
|
268
|
+
const skipped = skipStringOrComment(code, i);
|
|
269
|
+
if (skipped > i) {
|
|
270
|
+
i = skipped;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (/\s/.test(code[i])) {
|
|
274
|
+
i++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (i >= code.length) return -1;
|
|
281
|
+
|
|
282
|
+
if (code[i] === "(") return i;
|
|
283
|
+
|
|
284
|
+
if (code[i] === "<") {
|
|
285
|
+
let depth = 0;
|
|
286
|
+
while (i < code.length) {
|
|
287
|
+
const skipped = skipStringOrComment(code, i);
|
|
288
|
+
if (skipped > i) {
|
|
289
|
+
i = skipped;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const ch = code[i];
|
|
293
|
+
if (ch === "<") {
|
|
294
|
+
depth++;
|
|
295
|
+
} else if (ch === ">") {
|
|
296
|
+
depth--;
|
|
297
|
+
if (depth === 0) {
|
|
298
|
+
i++;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
i++;
|
|
303
|
+
}
|
|
304
|
+
if (depth !== 0) return -1;
|
|
305
|
+
// After the balanced generic list, skip trivia and expect `(`.
|
|
306
|
+
while (i < code.length) {
|
|
307
|
+
const skipped = skipStringOrComment(code, i);
|
|
308
|
+
if (skipped > i) {
|
|
309
|
+
i = skipped;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (/\s/.test(code[i])) {
|
|
313
|
+
i++;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
if (i < code.length && code[i] === "(") return i;
|
|
319
|
+
return -1;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return -1;
|
|
248
323
|
}
|
|
@@ -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)
|