paramour 0.3.0 → 0.5.0

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/codec.d.ts CHANGED
@@ -66,8 +66,8 @@ export interface Codec<Out, P extends Presence = "required", C extends boolean =
66
66
  /** Stored as a thunk regardless of the form passed to `.default()`. */
67
67
  readonly "~defaultValue": (() => Out) | undefined;
68
68
  /**
69
- * Element codec of a composite list codec (currently `p.csv`) — the
70
- * per-segment scalar; undefined for every non-composite kind (CV6).
69
+ * Element codec of a composite list codec (`p.csv`, `p.array`) — the
70
+ * per-segment/per-key scalar; undefined for every non-composite kind (CV6).
71
71
  */
72
72
  readonly "~element": AnyCodec | undefined;
73
73
  /** Members of a `p.enum` codec; undefined for every other kind. */
@@ -24,7 +24,7 @@ export interface CodecDescription {
24
24
  readonly defaultValue?: CodecDefaultDescription;
25
25
  /**
26
26
  * Nested description of a composite list codec's element scalar (CV6;
27
- * currently `p.csv`).
27
+ * `p.csv` and `p.array`).
28
28
  */
29
29
  readonly element?: CodecDescription;
30
30
  readonly enumMembers?: readonly string[];
package/dist/describe.js CHANGED
@@ -10,8 +10,9 @@ export function describeCodec(codec) {
10
10
  arity: codec["~arity"],
11
11
  caught: codec["~caught"],
12
12
  ...(defaultValue === undefined ? {} : { defaultValue }),
13
- // Recursion terminates: nested csv is rejected at construction (CV2),
14
- // and element codecs are unmodified scalars with no element of their own.
13
+ // Recursion terminates: composite nesting is bounded at construction —
14
+ // csv rejects nested csv (CV2) and array rejects arity-many inners
15
+ // (PP1), so the deepest legal chain is array<csv<scalar>>.
15
16
  ...(element === undefined ? {} : { element: describeCodec(element) }),
16
17
  ...(enumMembers === undefined ? {} : { enumMembers }),
17
18
  kind: codec["~kind"],
@@ -63,11 +64,22 @@ export function formatCodecDescription(description, style) {
63
64
  const kindLabel = (part) => part.enumMembers === undefined
64
65
  ? part.kind
65
66
  : `enum(${part.enumMembers.join(memberSeparator)})`;
66
- let label = description.element === undefined
67
- ? kindLabel(description)
68
- : `${description.kind}<${kindLabel(description.element)}>`;
69
- if (description.arity === "many")
70
- label += "[]";
67
+ // Composite labels: a one-key list wraps its element (`csv<integer>`); a
68
+ // repeated-key list IS its element, pluralized (`integer[]`) — the
69
+ // "array" kind never appears in a label, the `[]` carries it. The elision
70
+ // keys on the kind, NOT arity: consumers force arity "many" onto
71
+ // non-array descriptions (render.ts's catch-all params), where a csv
72
+ // wrapper must survive as `csv<integer>[]`. Recursive so `array<csv<E>>`
73
+ // renders `csv<E>[]`.
74
+ const shapeLabel = (part) => {
75
+ const base = part.element === undefined
76
+ ? kindLabel(part)
77
+ : part.kind === "array"
78
+ ? shapeLabel(part.element)
79
+ : `${part.kind}<${shapeLabel(part.element)}>`;
80
+ return part.arity === "many" ? `${base}[]` : base;
81
+ };
82
+ let label = shapeLabel(description);
71
83
  if (style === "compact") {
72
84
  if (description.presence === "optional")
73
85
  label += "?";
package/dist/errors.d.ts CHANGED
@@ -57,9 +57,10 @@ export declare function describeType(value: unknown): string;
57
57
  * Best-effort human-readable message for a foreign (non-paramour) throw:
58
58
  * an `Error`'s message, else a {@link showValue}-hardened `String()` — safe
59
59
  * even for values whose primitive conversion itself throws (null-prototype
60
- * objects, `Symbol.toPrimitive` throwers). Public via the barrel so derived
61
- * tooling that catches user-code throws (the devtools panel's edit preview)
62
- * shares the hardening instead of re-implementing it minus the guard.
60
+ * objects, `Symbol.toPrimitive` throwers). Public via `paramour/internal` so
61
+ * derived tooling that catches user-code throws (the devtools panel's edit
62
+ * preview) shares the hardening instead of re-implementing it minus the
63
+ * guard.
63
64
  */
64
65
  export declare function foreignMessage(error: unknown): string;
65
66
  /**
package/dist/errors.js CHANGED
@@ -110,9 +110,10 @@ export function describeType(value) {
110
110
  * Best-effort human-readable message for a foreign (non-paramour) throw:
111
111
  * an `Error`'s message, else a {@link showValue}-hardened `String()` — safe
112
112
  * even for values whose primitive conversion itself throws (null-prototype
113
- * objects, `Symbol.toPrimitive` throwers). Public via the barrel so derived
114
- * tooling that catches user-code throws (the devtools panel's edit preview)
115
- * shares the hardening instead of re-implementing it minus the guard.
113
+ * objects, `Symbol.toPrimitive` throwers). Public via `paramour/internal` so
114
+ * derived tooling that catches user-code throws (the devtools panel's edit
115
+ * preview) shares the hardening instead of re-implementing it minus the
116
+ * guard.
116
117
  */
117
118
  export function foreignMessage(error) {
118
119
  return error instanceof Error ? error.message : showValue(error);
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export { type AnyCodec, type Arity, type Codec, type OutputOf, type ParamCodec, type Presence, type PresenceOf, } from "./codec.js";
2
2
  export { type CodecDefaultDescription, type CodecDescription, type CodecFormatStyle, describeCodec, describeRoute, formatCodecDescription, type ParamDescription, type RouteDescription, type SearchDescription, } from "./describe.js";
3
- export { foreignMessage, type Issue, ParamourError, ParamsDecodeError, ParseError, type RouteDecodeError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
3
+ export { type Issue, ParamourError, ParamsDecodeError, ParseError, type RouteDecodeError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
4
4
  export { href, type Href, type HrefArgs, type InferHrefInput, type StaticHrefOptions, } from "./href.js";
5
5
  export { p } from "./p.js";
6
6
  export { buildPath, decodeParams, type DecodeParamsOptions, encodeParams, encodeStaticParams, type InferStaticParams, type ParamsSource, } from "./path.js";
7
- export { type AnyAppRoute, type AnyPagesRoute, type AnyRoute, type AppRoute, defineAppRoute, definePagesRoute, type InferRouteParams, type PagesContext, type PagesRoute, type ParamourRegister, type ParamsConfig, type ParamsProps, type RegisteredAppRoutePaths, type RegisteredPagesRoutePaths, type RegisteredStaticAppRoutePaths, type RegisteredStaticPagesRoutePaths, type RegisteredStaticRoutePaths, type Route, type RouteProps, type RouterKind, type SafeResult, type SearchProps, } from "./route.js";
7
+ export { type AnyAppRoute, type AnyPagesRoute, type AnyRoute, type AppRoute, defineAppRoute, definePagesRoute, type InferRouteParams, type PagesContext, type PagesRoute, type ParamourRegister, type ParamsConfig, type ParamsProps, type ParamsPropsInput, type RegisteredAppRoutePaths, type RegisteredPagesRoutePaths, type RegisteredStaticAppRoutePaths, type RegisteredStaticPagesRoutePaths, type RegisteredStaticRoutePaths, type Route, type RouteProps, type RoutePropsInput, type RouterKind, type SafeResult, type SearchProps, type SearchPropsInput, } from "./route.js";
8
8
  export { safeDecodeParams, safeDecodeSearch } from "./safe-decode.js";
9
- export { buildSearchString, decodeSearch, encodeSearch, type InferSearchInput, type InferSearchOutput, isRawSearch, parseValue, rawSearch, type RawSearch, type SearchConfig, type SearchOutputOf, type SearchSource, searchToString, serializeValue, } from "./search.js";
9
+ export { buildSearchString, decodeSearch, encodeSearch, type InferSearchInput, type InferSearchOutput, isRawSearch, rawSearch, type RawSearch, type SearchConfig, type SearchOutputOf, type SearchSource, searchToString, serializeValue, } from "./search.js";
10
10
  export { standardSearchSchema, type StandardSearchSchema, } from "./standard-schema.js";
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  export {} from "./codec.js";
2
2
  export { describeCodec, describeRoute, formatCodecDescription, } from "./describe.js";
3
- export { foreignMessage, ParamourError, ParamsDecodeError, ParseError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
3
+ export { ParamourError, ParamsDecodeError, ParseError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
4
4
  export { href, } from "./href.js";
5
5
  export { p } from "./p.js";
6
6
  export { buildPath, decodeParams, encodeParams, encodeStaticParams, } from "./path.js";
7
7
  export { defineAppRoute, definePagesRoute, } from "./route.js";
8
8
  export { safeDecodeParams, safeDecodeSearch } from "./safe-decode.js";
9
- export { buildSearchString, decodeSearch, encodeSearch, isRawSearch, parseValue, rawSearch, searchToString, serializeValue, } from "./search.js";
9
+ export { buildSearchString, decodeSearch, encodeSearch, isRawSearch, rawSearch, searchToString, serializeValue, } from "./search.js";
10
10
  export { standardSearchSchema, } from "./standard-schema.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The `paramour/internal` entry: unstable helpers for derived tooling
3
+ * (devtools, adapters), NOT for app authors and NOT covered by the public
4
+ * API's stability expectations. These live off the main barrel on purpose —
5
+ * the docs' Reference section documents the app-author surface, and these
6
+ * two exist solely so reflection-driven consumers (the devtools panel's
7
+ * catch-attribution probe and edit preview, design-12 DT7) share core's
8
+ * implementation instead of re-deriving it.
9
+ */
10
+ export { foreignMessage } from "./errors.js";
11
+ export { parseValue } from "./search.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The `paramour/internal` entry: unstable helpers for derived tooling
3
+ * (devtools, adapters), NOT for app authors and NOT covered by the public
4
+ * API's stability expectations. These live off the main barrel on purpose —
5
+ * the docs' Reference section documents the app-author surface, and these
6
+ * two exist solely so reflection-driven consumers (the devtools panel's
7
+ * catch-attribution probe and edit preview, design-12 DT7) share core's
8
+ * implementation instead of re-deriving it.
9
+ */
10
+ export { foreignMessage } from "./errors.js";
11
+ export { parseValue } from "./search.js";
package/dist/p.d.ts CHANGED
@@ -5,10 +5,17 @@ import { type Codec } from "./codec.js";
5
5
  * Each codec defines how one value crosses the URL boundary, both directions.
6
6
  */
7
7
  export declare const p: {
8
+ /**
9
+ * A repeated-key list (`?tags=a&tags=b`, design-13 PP1): arity "many", so
10
+ * presence modifiers are unavailable — absent and `[]` are the same wire
11
+ * state (S6/P6) — unlike `p.csv`'s one-key packing (CV7). Elements are
12
+ * strings unless an element codec is given.
13
+ */
14
+ array<E = string>(element?: Codec<E>): Codec<E[], "required", false, "many">;
8
15
  boolean(): Codec<boolean>;
9
16
  /**
10
17
  * A comma-separated scalar list in ONE wire value (design-11 CV1): arity
11
- * "single", so the full modifier set applies — unlike `p.stringArray`'s
18
+ * "single", so the full modifier set applies — unlike `p.array`'s
12
19
  * repeated-key format (CV7: both are first-class; csv is the one-key
13
20
  * packing). Elements are strings unless an element codec is given.
14
21
  */
@@ -20,11 +27,21 @@ export declare const p: {
20
27
  serialize: (value: Out) => string;
21
28
  }): Codec<Out>;
22
29
  enum<const M extends readonly [string, ...string[]]>(members: M): Codec<M[number]>;
30
+ /**
31
+ * A 1-based-on-wire / 0-based-in-memory integer for pagination-style
32
+ * params (`?page=1` ↔ index 0, design-13 PP5). Deliberately stricter than
33
+ * nuqs's `parseAsIndex`, whose below-floor behavior is unspecified: wire
34
+ * values below 1 are a ParseError (recoverable via `.catch()`, like any
35
+ * other malformed input), and a negative in-memory index — which cannot
36
+ * round-trip through the 1-based wire floor — is a SerializeError at
37
+ * link-build time (the RL1 ethos). The optional schema validates the
38
+ * in-memory (0-based) value on both sides.
39
+ */
40
+ index<S extends StandardSchemaV1<number, number>>(schema?: S): Codec<S extends undefined ? number : StandardSchemaV1.InferOutput<S>>;
23
41
  integer<S extends StandardSchemaV1<number, number>>(schema?: S): Codec<S extends undefined ? number : StandardSchemaV1.InferOutput<S>>;
24
42
  isoDate(): Codec<Date>;
25
43
  json<S extends StandardSchemaV1>(schema: S): Codec<StandardSchemaV1.InferOutput<S>>;
26
44
  number<S extends StandardSchemaV1<number, number>>(schema?: S): Codec<S extends undefined ? number : StandardSchemaV1.InferOutput<S>>;
27
45
  string<S extends StandardSchemaV1<string, string>>(schema?: S): Codec<S extends undefined ? string : StandardSchemaV1.InferOutput<S>>;
28
- stringArray(): Codec<string[], "required", false, "many">;
29
46
  timestamp(): Codec<Date>;
30
47
  };
package/dist/p.js CHANGED
@@ -73,12 +73,46 @@ function refineForSerialize(schema, value) {
73
73
  }
74
74
  return result.value;
75
75
  }
76
+ /**
77
+ * Shared element admission for `p.array`/`p.csv` (PP1/CV2): resolves the
78
+ * no-arg default and mirrors the type-state exclusions — presence, catch,
79
+ * and arity-many inners — for JS consumers (the RL1 ethos). Only the
80
+ * element's parse/serialize functions are captured by the list builders, so
81
+ * an accepted modifier would be silently dropped, not applied; one guard
82
+ * keeps that runtime mirror of the type-state in a single place.
83
+ */
84
+ function resolveListElement(element, builder) {
85
+ const inner = element ?? (defaultListElement ??= p.string());
86
+ if (inner["~arity"] === "many" ||
87
+ inner["~caught"] ||
88
+ inner["~presence"] !== "required") {
89
+ throw new ParamourError(`${builder}() elements cannot carry modifiers (.optional()/.default()/.catch()) or be array codecs`);
90
+ }
91
+ return inner;
92
+ }
76
93
  function serializeFiniteNumber(value) {
77
94
  if (typeof value !== "number" || !Number.isFinite(value)) {
78
95
  throw new SerializeError(`Expected a finite number, got ${showValue(value)}`);
79
96
  }
80
97
  return String(value);
81
98
  }
99
+ /**
100
+ * Serialize-side twin of {@link parseIntegerElement}, shared by `p.integer`
101
+ * and `p.index`: schema refinement plus the finite and safe-integer guards.
102
+ * Returns the refined NUMBER — `p.integer` stringifies it as-is, `p.index`
103
+ * shifts it into the 1-based wire form first — so the happy path pays no
104
+ * throwaway stringification.
105
+ */
106
+ function serializeIntegerElement(schema, value) {
107
+ const refined = schema ? refineForSerialize(schema, value) : value;
108
+ if (typeof refined !== "number" || !Number.isFinite(refined)) {
109
+ throw new SerializeError(`Expected a finite number, got ${showValue(refined)}`);
110
+ }
111
+ if (!Number.isSafeInteger(refined)) {
112
+ throw new SerializeError(`${String(refined)} is not a safe integer`);
113
+ }
114
+ return refined;
115
+ }
82
116
  /**
83
117
  * JSON.stringify throws raw TypeErrors (circular refs, BigInt) and lets
84
118
  * toJSON() exceptions escape; wrap them so the ParamourError contract holds.
@@ -87,16 +121,43 @@ function stringifyJson(value) {
87
121
  return rebrandForeign(() => JSON.stringify(value), (error) => new SerializeError("Value is not JSON-serializable", { cause: error }));
88
122
  }
89
123
  /**
90
- * Shared element for no-arg `p.csv()` — codecs are immutable, so one
91
- * schemaless string codec serves every list. Lazily built: `p` does not
124
+ * Shared element for no-arg `p.csv()`/`p.array()` — codecs are immutable, so
125
+ * one schemaless string codec serves every list. Lazily built: `p` does not
92
126
  * exist yet while the module initializes.
93
127
  */
94
- let defaultCsvElement;
128
+ let defaultListElement;
95
129
  /**
96
130
  * The `p.*` wire-codec builders (DESIGN §5 layer 1, design-02 D9).
97
131
  * Each codec defines how one value crosses the URL boundary, both directions.
98
132
  */
99
133
  export const p = {
134
+ /**
135
+ * A repeated-key list (`?tags=a&tags=b`, design-13 PP1): arity "many", so
136
+ * presence modifiers are unavailable — absent and `[]` are the same wire
137
+ * state (S6/P6) — unlike `p.csv`'s one-key packing (CV7). Elements are
138
+ * strings unless an element codec is given.
139
+ */
140
+ array(element) {
141
+ // PP1: same element-by-composition shape as p.csv (CV2) — presence,
142
+ // catch, and arity-many inners are excluded by the parameter type;
143
+ // resolveListElement mirrors that type-state for JS consumers. Unlike
144
+ // csv there is no nested-composite special case: a csv element is a
145
+ // legal whole-value scalar per repeated key (?m=a,b&m=c,d), and
146
+ // repeated-key values have no separator for element serializations to
147
+ // collide with, so no CV4 twin is needed either.
148
+ const inner = resolveListElement(element, "p.array");
149
+ // The element functions already have the arity-"many" per-element
150
+ // contract (one wire value per array item), so they pass through as-is;
151
+ // the string-return contract on custom serializers is enforced at the
152
+ // search.ts/path.ts call sites, same as using the element directly.
153
+ return createCodec({
154
+ arity: "many",
155
+ element: inner,
156
+ kind: "array",
157
+ parseElement: inner["~parseElement"],
158
+ serializeElement: inner["~serializeElement"],
159
+ });
160
+ },
100
161
  boolean() {
101
162
  return createCodec({
102
163
  kind: "boolean",
@@ -117,28 +178,26 @@ export const p = {
117
178
  },
118
179
  /**
119
180
  * A comma-separated scalar list in ONE wire value (design-11 CV1): arity
120
- * "single", so the full modifier set applies — unlike `p.stringArray`'s
181
+ * "single", so the full modifier set applies — unlike `p.array`'s
121
182
  * repeated-key format (CV7: both are first-class; csv is the one-key
122
183
  * packing). Elements are strings unless an element codec is given.
123
184
  */
124
185
  csv(element) {
125
186
  // CV2: presence, catch, and arity-many inners are excluded by the
126
- // parameter type (the D3 philosophy); these guards mirror that
127
- // type-state for JS consumers (the RL1 ethos) — only the element
128
- // functions are captured below, so an accepted modifier would be
129
- // silently dropped, not applied. Nesting is detected structurally via
130
- // ~element (never via ~kind, which is reflection-only and free-form for
131
- // p.custom labels). Comma-emitting p.custom inners are undetectable
132
- // here and are caught by the CV4 serialize guard instead.
133
- const inner = element ?? (defaultCsvElement ??= p.string());
187
+ // parameter type (the D3 philosophy); resolveListElement mirrors that
188
+ // type-state for JS consumers (the RL1 ethos). Nesting is detected
189
+ // structurally via ~element (never via ~kind, which is reflection-only
190
+ // and free-form for p.custom labels). Comma-emitting p.custom inners
191
+ // are undetectable here and are caught by the CV4 serialize guard
192
+ // instead.
193
+ const inner = resolveListElement(element, "p.csv");
194
+ // The shared guard ran first: p.array also carries ~element (PP1), and
195
+ // the arity guard owns the "array codecs" wording — the ~element guard
196
+ // here is then specifically the nested-csv (arity-"single" composite)
197
+ // case.
134
198
  if (inner["~element"] !== undefined) {
135
199
  throw new ParamourError("p.csv() elements cannot themselves be csv lists");
136
200
  }
137
- if (inner["~arity"] === "many" ||
138
- inner["~caught"] ||
139
- inner["~presence"] !== "required") {
140
- throw new ParamourError("p.csv() elements cannot carry modifiers (.optional()/.default()/.catch()) or be array codecs");
141
- }
142
201
  const parseInner = inner["~parseElement"];
143
202
  const serializeInner = inner["~serializeElement"];
144
203
  return createCodec({
@@ -226,21 +285,48 @@ export const p = {
226
285
  },
227
286
  });
228
287
  },
229
- integer(schema) {
288
+ /**
289
+ * A 1-based-on-wire / 0-based-in-memory integer for pagination-style
290
+ * params (`?page=1` ↔ index 0, design-13 PP5). Deliberately stricter than
291
+ * nuqs's `parseAsIndex`, whose below-floor behavior is unspecified: wire
292
+ * values below 1 are a ParseError (recoverable via `.catch()`, like any
293
+ * other malformed input), and a negative in-memory index — which cannot
294
+ * round-trip through the 1-based wire floor — is a SerializeError at
295
+ * link-build time (the RL1 ethos). The optional schema validates the
296
+ * in-memory (0-based) value on both sides.
297
+ */
298
+ index(schema) {
230
299
  return createCodec({
231
- kind: "integer",
300
+ kind: "index",
232
301
  parseElement: (raw) => {
233
- const value = parseIntegerElement(raw);
302
+ const wire = parseIntegerElement(raw);
303
+ if (wire < 1) {
304
+ throw new ParseError(`"${raw}" is below the 1-based wire floor of 1`);
305
+ }
306
+ const value = wire - 1;
234
307
  return schema ? refine(schema, value) : value;
235
308
  },
236
309
  serializeElement: (value) => {
237
- const refined = schema ? refineForSerialize(schema, value) : value;
238
- const serialized = serializeFiniteNumber(refined);
239
- if (!Number.isSafeInteger(refined)) {
240
- throw new SerializeError(`${serialized} is not a safe integer`);
310
+ const index = serializeIntegerElement(schema, value);
311
+ if (index < 0) {
312
+ throw new SerializeError(`${String(index)} is negative and cannot round-trip through the 1-based wire form`);
241
313
  }
242
- return serialized;
314
+ const wire = index + 1;
315
+ if (!Number.isSafeInteger(wire)) {
316
+ throw new SerializeError(`${String(index)} is outside the 1-based wire form's safe integer range`);
317
+ }
318
+ return String(wire);
319
+ },
320
+ });
321
+ },
322
+ integer(schema) {
323
+ return createCodec({
324
+ kind: "integer",
325
+ parseElement: (raw) => {
326
+ const value = parseIntegerElement(raw);
327
+ return schema ? refine(schema, value) : value;
243
328
  },
329
+ serializeElement: (value) => String(serializeIntegerElement(schema, value)),
244
330
  });
245
331
  },
246
332
  isoDate() {
@@ -311,19 +397,6 @@ export const p = {
311
397
  },
312
398
  });
313
399
  },
314
- stringArray() {
315
- return createCodec({
316
- arity: "many",
317
- kind: "string",
318
- parseElement: (raw) => raw,
319
- serializeElement: (value) => {
320
- if (typeof value !== "string") {
321
- throw new SerializeError("Expected an array of strings");
322
- }
323
- return value;
324
- },
325
- });
326
- },
327
400
  timestamp() {
328
401
  return createCodec({
329
402
  kind: "timestamp",
package/dist/route.d.ts CHANGED
@@ -27,20 +27,20 @@ export interface AppRoute<Path extends string, PC extends ParamsConfig<Path>, SC
27
27
  * FIRST — a params grammar failure means the URL doesn't denote this
28
28
  * route at all (morally a 404), so it throws before search is decoded.
29
29
  */
30
- parse(props: RouteProps): Promise<{
30
+ parse(props: RoutePropsInput): Promise<{
31
31
  params: ParamsOutput<Path, PC>;
32
32
  search: SearchOutputOf<SC>;
33
33
  }>;
34
34
  /** Bare params object (RL6) — layout props are structurally assignable. */
35
- parseParams(props: ParamsProps): Promise<ParamsOutput<Path, PC>>;
35
+ parseParams(props: ParamsPropsInput): Promise<ParamsOutput<Path, PC>>;
36
36
  /** Bare search object (RL6) — the search half alone. */
37
- parseSearch(props: SearchProps): Promise<SearchOutputOf<SC>>;
38
- safeParse(props: RouteProps): Promise<SafeResult<{
37
+ parseSearch(props: SearchPropsInput): Promise<SearchOutputOf<SC>>;
38
+ safeParse(props: RoutePropsInput): Promise<SafeResult<{
39
39
  params: ParamsOutput<Path, PC>;
40
40
  search: SearchOutputOf<SC>;
41
41
  }>>;
42
- safeParseParams(props: ParamsProps): Promise<SafeResult<ParamsOutput<Path, PC>>>;
43
- safeParseSearch(props: SearchProps): Promise<SafeResult<SearchOutputOf<SC>>>;
42
+ safeParseParams(props: ParamsPropsInput): Promise<SafeResult<ParamsOutput<Path, PC>>>;
43
+ safeParseSearch(props: SearchPropsInput): Promise<SafeResult<SearchOutputOf<SC>>>;
44
44
  }
45
45
  /** Names of `[...name]` catch-all segments in the path literal (RL3). */
46
46
  export type CatchAllNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[...${infer Name}]` ? NonEmptyName<Name> : never : never;
@@ -52,7 +52,15 @@ export type CatchAllNames<Path extends string> = Segments<Path> extends infer S
52
52
  export type ConformParams<Path extends string, PC> = PC & Record<Exclude<keyof PC, PathParamNames<Path>>, never>;
53
53
  /** Decoded params object type for a route (RL3); see {@link ParamsOutput}. */
54
54
  export type InferRouteParams<R extends AnyRoute> = ParamsOutput<R["path"], R["~params"]>;
55
- /** Accepts Next 15/16's promised props and plain objects alike (RL6). */
55
+ /**
56
+ * Accepts promised props and plain objects alike (RL6). This width lives on
57
+ * the parse INPUT surface ({@link RoutePropsInput} and friends), not on the
58
+ * annotation types: every supported Next (peer `>=15`) delivers page props
59
+ * as promises, and Next 15.5's generated `.next/types` page check requires
60
+ * a page's `params` prop to be `Promise<any> | undefined` — a sync arm in
61
+ * {@link RouteProps} fails `next build` there. Hand-built sync props (tests,
62
+ * server code calling `parse` directly) stay legal via the input types.
63
+ */
56
64
  export type MaybePromise<T> = Promise<T> | T;
57
65
  /**
58
66
  * RL3: an empty name (`[]`, `[...]`, `[[...]]`) is not a token — it falls
@@ -145,6 +153,14 @@ export type ParamsOutput<Path extends string, PC> = {
145
153
  * global doesn't exist in fresh clones before `next dev` first runs.
146
154
  */
147
155
  export interface ParamsProps {
156
+ readonly params?: Promise<ParamsSource>;
157
+ }
158
+ /**
159
+ * What `parseParams` ACCEPTS (RL6): {@link ParamsProps} plus plain sync
160
+ * objects — see {@link MaybePromise} for why the annotation type is
161
+ * promise-only while the parse input stays wide.
162
+ */
163
+ export interface ParamsPropsInput {
148
164
  readonly params?: MaybePromise<ParamsSource>;
149
165
  }
150
166
  /** Every dynamic segment name in the path literal (RL3). */
@@ -218,9 +234,18 @@ export type RouteConfig<Path extends string, PC extends ParamsConfig<Path>, SC e
218
234
  readonly params: ConformParams<Path, PC>;
219
235
  readonly search?: SC;
220
236
  };
221
- /** Full page-props contract (RL6): Next's `PageProps` is structurally assignable. */
237
+ /**
238
+ * Full page-props contract (RL6): the type a page annotates its props with.
239
+ * Next's `PageProps` is structurally assignable, and both members are
240
+ * promise-only so the annotation survives Next 15.5's generated page check
241
+ * (see {@link MaybePromise}). Deliberately NOT Next's generated `PageProps`
242
+ * global — core stays framework-agnostic.
243
+ */
222
244
  export interface RouteProps extends ParamsProps, SearchProps {
223
245
  }
246
+ /** What `parse`/`safeParse` ACCEPT (RL6): {@link RouteProps} plus sync props. */
247
+ export interface RoutePropsInput extends ParamsPropsInput, SearchPropsInput {
248
+ }
224
249
  /** Which router a route belongs to (PR3) — the value of the `~router` brand. */
225
250
  export type RouterKind = "app" | "pages";
226
251
  /**
@@ -241,6 +266,10 @@ export type SafeResult<T> = {
241
266
  * shape is the same as the params side's, hence the shared source type.
242
267
  */
243
268
  export interface SearchProps {
269
+ readonly searchParams?: Promise<ParamsSource>;
270
+ }
271
+ /** Sync-accepting twin of {@link SearchProps} — see {@link ParamsPropsInput}. */
272
+ export interface SearchPropsInput {
244
273
  readonly searchParams?: MaybePromise<ParamsSource>;
245
274
  }
246
275
  /**
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "paramour",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
8
8
  "default": "./dist/index.js"
9
+ },
10
+ "./internal": {
11
+ "types": "./dist/internal.d.ts",
12
+ "default": "./dist/internal.js"
9
13
  }
10
14
  },
15
+ "engines": {
16
+ "node": ">=22.13.0"
17
+ },
11
18
  "description": "Type-safe routing companion for the Next.js App Router: validated route params and search params, typed path building, and explicit URL serialization.",
12
19
  "keywords": [
13
20
  "nextjs",
@@ -49,6 +56,7 @@
49
56
  },
50
57
  "scripts": {
51
58
  "build": "tsc -p tsconfig.build.json",
59
+ "check:publish": "publint --strict && attw --pack . --profile esm-only",
52
60
  "typecheck": "tsc --noEmit"
53
61
  }
54
62
  }