@rangojs/router 0.0.0-experimental.25 → 0.0.0-experimental.26
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/README.md +1 -1
- package/dist/vite/index.js +37 -1
- package/package.json +1 -1
- package/skills/hooks/SKILL.md +0 -17
- package/skills/route/SKILL.md +2 -2
- package/skills/router-setup/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +1 -1
- package/src/browser/react/Link.tsx +5 -5
- package/src/build/route-trie.ts +19 -3
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +0 -46
- package/src/router/match-result.ts +0 -8
- package/src/router/metrics.ts +138 -34
- package/src/router/middleware-types.ts +4 -3
- package/src/router/middleware.ts +31 -2
- package/src/router/pattern-matching.ts +20 -5
- package/src/router/trie-matching.ts +20 -2
- package/src/rsc/handler.ts +104 -14
- package/src/rsc/progressive-enhancement.ts +21 -8
- package/src/rsc/rsc-rendering.ts +12 -58
- package/src/rsc/server-action.ts +2 -17
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/search-params.ts +16 -13
- package/src/types/route-config.ts +17 -8
- package/src/types/segments.ts +0 -5
- package/src/vite/index.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
package/src/search-params.ts
CHANGED
|
@@ -55,14 +55,22 @@ type Simplify<T> = { [K in keyof T]: T[K] };
|
|
|
55
55
|
/**
|
|
56
56
|
* Resolve a SearchSchema to its typed object.
|
|
57
57
|
*
|
|
58
|
+
* Both required and optional params resolve to `T | undefined` at the handler
|
|
59
|
+
* level. The required/optional distinction is a consumer-facing contract
|
|
60
|
+
* (e.g., for href() and reverse() autocomplete) — it tells callers which
|
|
61
|
+
* params the route expects, but the handler must still check for undefined
|
|
62
|
+
* since the framework cannot trust the client to send all required params.
|
|
63
|
+
*
|
|
58
64
|
* @example
|
|
59
65
|
* type S = { q: "string"; page: "number?"; sort: "string?" };
|
|
60
66
|
* type R = ResolveSearchSchema<S>;
|
|
61
|
-
* // { q: string; page?: number; sort?: string }
|
|
67
|
+
* // { q: string | undefined; page?: number; sort?: string }
|
|
62
68
|
*/
|
|
63
69
|
export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
|
|
64
70
|
{
|
|
65
|
-
[K in RequiredKeys<T> & string]:
|
|
71
|
+
[K in RequiredKeys<T> & string]:
|
|
72
|
+
| ResolveBaseType<BaseType<T[K]>>
|
|
73
|
+
| undefined;
|
|
66
74
|
} & {
|
|
67
75
|
[K in OptionalKeys<T> & string]?: ResolveBaseType<BaseType<T[K]>>;
|
|
68
76
|
}
|
|
@@ -166,7 +174,9 @@ type ExtractParamsFromPattern<T extends string> =
|
|
|
166
174
|
* - `"number"` / `"number?"` - coerced via `Number()`; NaN treated as missing
|
|
167
175
|
* - `"boolean"` / `"boolean?"` - `"true"` / `"1"` -> true, `"false"` / `"0"` / `""` -> false
|
|
168
176
|
*
|
|
169
|
-
* Missing
|
|
177
|
+
* Missing params (both required and optional) are omitted from the result
|
|
178
|
+
* (undefined). The required/optional distinction is a consumer-facing contract
|
|
179
|
+
* only — the handler must check for undefined.
|
|
170
180
|
*/
|
|
171
181
|
export function parseSearchParams<T extends SearchSchema>(
|
|
172
182
|
searchParams: URLSearchParams,
|
|
@@ -180,13 +190,7 @@ export function parseSearchParams<T extends SearchSchema>(
|
|
|
180
190
|
const raw = searchParams.get(key);
|
|
181
191
|
|
|
182
192
|
if (raw === null) {
|
|
183
|
-
|
|
184
|
-
// Required param missing: use zero value
|
|
185
|
-
if (baseType === "string") result[key] = "";
|
|
186
|
-
else if (baseType === "number") result[key] = 0;
|
|
187
|
-
else if (baseType === "boolean") result[key] = false;
|
|
188
|
-
}
|
|
189
|
-
// Optional params are omitted (undefined)
|
|
193
|
+
// Missing params are omitted (undefined) regardless of required/optional
|
|
190
194
|
continue;
|
|
191
195
|
}
|
|
192
196
|
|
|
@@ -194,11 +198,10 @@ export function parseSearchParams<T extends SearchSchema>(
|
|
|
194
198
|
result[key] = raw;
|
|
195
199
|
} else if (baseType === "number") {
|
|
196
200
|
const num = Number(raw);
|
|
197
|
-
if (Number.isNaN(num)) {
|
|
198
|
-
if (!isOptional) result[key] = 0;
|
|
199
|
-
} else {
|
|
201
|
+
if (!Number.isNaN(num)) {
|
|
200
202
|
result[key] = num;
|
|
201
203
|
}
|
|
204
|
+
// NaN treated as missing (undefined)
|
|
202
205
|
} else if (baseType === "boolean") {
|
|
203
206
|
result[key] = raw === "true" || raw === "1";
|
|
204
207
|
}
|
|
@@ -24,17 +24,26 @@ type ParseConstraint<T extends string> =
|
|
|
24
24
|
* - :param(a|b)? -> { name: "param", optional: true, type: "a" | "b" }
|
|
25
25
|
*/
|
|
26
26
|
type ExtractParamInfo<T extends string> =
|
|
27
|
-
// Optional + constrained: :param(a|b)?
|
|
28
|
-
T extends `${infer Name}(${infer Constraint})
|
|
27
|
+
// Optional + constrained (with optional suffix): :param(a|b)?suffix
|
|
28
|
+
T extends `${infer Name}(${infer Constraint})?${string}`
|
|
29
29
|
? { name: Name; optional: true; type: ParseConstraint<Constraint> }
|
|
30
|
-
: // Constrained
|
|
31
|
-
T extends `${infer Name}(${infer Constraint})`
|
|
30
|
+
: // Constrained (with optional suffix): :param(a|b)suffix
|
|
31
|
+
T extends `${infer Name}(${infer Constraint})${string}`
|
|
32
32
|
? { name: Name; optional: false; type: ParseConstraint<Constraint> }
|
|
33
|
-
: // Optional
|
|
34
|
-
T extends `${infer Name}
|
|
33
|
+
: // Optional (with optional suffix): :param?suffix
|
|
34
|
+
T extends `${infer Name}?${string}`
|
|
35
35
|
? { name: Name; optional: true; type: string }
|
|
36
|
-
: //
|
|
37
|
-
|
|
36
|
+
: // Param with dot-suffix: :param.html
|
|
37
|
+
T extends `${infer Name}.${string}`
|
|
38
|
+
? { name: Name; optional: false; type: string }
|
|
39
|
+
: // Param with dash-suffix: :param-slug
|
|
40
|
+
T extends `${infer Name}-${string}`
|
|
41
|
+
? { name: Name; optional: false; type: string }
|
|
42
|
+
: // Param with tilde-suffix: :param~v2
|
|
43
|
+
T extends `${infer Name}~${string}`
|
|
44
|
+
? { name: Name; optional: false; type: string }
|
|
45
|
+
: // Required: :param (no suffix)
|
|
46
|
+
{ name: T; optional: false; type: string };
|
|
38
47
|
|
|
39
48
|
/**
|
|
40
49
|
* Build param object from info
|
package/src/types/segments.ts
CHANGED
|
@@ -124,11 +124,6 @@ export interface MatchResult {
|
|
|
124
124
|
* Used by ctx.reverse() for local name resolution.
|
|
125
125
|
*/
|
|
126
126
|
routeName?: string;
|
|
127
|
-
/**
|
|
128
|
-
* Server-Timing header value (only present when debugPerformance is enabled)
|
|
129
|
-
* Can be added to response headers for DevTools integration
|
|
130
|
-
*/
|
|
131
|
-
serverTiming?: string;
|
|
132
127
|
/**
|
|
133
128
|
* State of named slots for this route match
|
|
134
129
|
* Key is slot name (e.g., "@modal"), value is slot state
|
package/src/vite/index.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public API for @rangojs/router/vite
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* consumed via direct imports within the package.
|
|
4
|
+
* Exports: rango() plugin factory, poke() dev utility plugin,
|
|
5
|
+
* and related option types. All other utilities are internal implementation
|
|
6
|
+
* details consumed via direct imports within the package.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export { rango } from "./rango.js";
|
|
10
|
+
export { poke } from "./plugins/refresh-cmd.js";
|
|
10
11
|
|
|
11
12
|
export type {
|
|
12
13
|
RangoNodeOptions,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vite plugin that triggers a full browser reload when Ctrl+R is pressed
|
|
5
|
+
* in the terminal running the dev server.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { poke } from "@rangojs/router/vite";
|
|
10
|
+
*
|
|
11
|
+
* export default defineConfig({
|
|
12
|
+
* plugins: [rango(), poke()],
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function poke(): Plugin {
|
|
17
|
+
return {
|
|
18
|
+
name: "vite-plugin-poke",
|
|
19
|
+
apply: "serve",
|
|
20
|
+
|
|
21
|
+
configureServer(server) {
|
|
22
|
+
const stdin = process.stdin;
|
|
23
|
+
|
|
24
|
+
// Raw mode delivers individual keystrokes as immediate single-byte
|
|
25
|
+
// events instead of waiting for Enter (cooked/line-buffered mode).
|
|
26
|
+
// Without it, Ctrl+R (0x12) is never delivered as a discrete byte.
|
|
27
|
+
// When stdin is a pipe (CI, spawned process) setRawMode is unavailable
|
|
28
|
+
// but data already arrives unbuffered, so the isTTY guard suffices.
|
|
29
|
+
const previousRawMode = stdin.isTTY ? stdin.isRaw : null;
|
|
30
|
+
if (stdin.isTTY) {
|
|
31
|
+
stdin.setRawMode(true);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const onData = (data: Buffer) => {
|
|
35
|
+
if (data.length !== 1) return;
|
|
36
|
+
|
|
37
|
+
// Ctrl+C (0x03) — defensive fallback. This plugin enables raw mode
|
|
38
|
+
// before Vite's internal stdin handler is registered (user plugins
|
|
39
|
+
// run first), so there is a brief window where Ctrl+C would be
|
|
40
|
+
// swallowed. Re-emit SIGINT so the process exits as expected.
|
|
41
|
+
if (data[0] === 0x03) {
|
|
42
|
+
process.emit("SIGINT", "SIGINT");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Ctrl+R = 0x12 in raw mode
|
|
47
|
+
if (data[0] === 0x12) {
|
|
48
|
+
server.hot.send({ type: "full-reload", path: "*" });
|
|
49
|
+
server.config.logger.info(" browser reload (ctrl+r)", {
|
|
50
|
+
timestamp: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
stdin.on("data", onData);
|
|
56
|
+
|
|
57
|
+
server.httpServer?.on("close", () => {
|
|
58
|
+
stdin.off("data", onData);
|
|
59
|
+
if (stdin.isTTY && previousRawMode !== null) {
|
|
60
|
+
stdin.setRawMode(previousRawMode);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|