@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.
@@ -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]: ResolveBaseType<BaseType<T[K]>>;
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 required params are set to their zero value (empty string / 0 / false).
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
- if (!isOptional) {
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 only: :param(a|b)
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 only: :param?
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
- : // Required: :param
37
- { name: T; optional: false; type: string };
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
@@ -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
- * Only the rango() plugin factory and its option types are part of the
5
- * public API. All other utilities are internal implementation details
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
+ }