paramour 0.2.0 → 0.3.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/LICENSE +21 -0
- package/dist/codec.d.ts +48 -7
- package/dist/codec.js +10 -1
- package/dist/describe.d.ts +20 -0
- package/dist/describe.js +51 -0
- package/dist/errors.d.ts +6 -2
- package/dist/errors.js +6 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/p.d.ts +7 -0
- package/dist/p.js +86 -1
- package/dist/path.js +7 -11
- package/dist/search.d.ts +32 -4
- package/dist/search.js +47 -20
- package/package.json +10 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jason Paff
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/codec.d.ts
CHANGED
|
@@ -9,25 +9,41 @@ export type Arity = "many" | "single";
|
|
|
9
9
|
/**
|
|
10
10
|
* A bidirectional wire codec.
|
|
11
11
|
*
|
|
12
|
-
* `Out` is the decoded in-memory type. `P`, `C`, and `
|
|
12
|
+
* `Out` is the decoded in-memory type. `P`, `C`, `A`, and `E` are type-state:
|
|
13
13
|
* modifier methods are conditionally `never`, so illegal chains
|
|
14
14
|
* (`.optional().default()`, double `.catch()`) fail to compile (design-02 D3).
|
|
15
|
+
* `E` carries `~defaultElides` as a literal after `.default()` (NQ6a).
|
|
15
16
|
* Presence modifiers are also `never` for arity-"many" codecs: absent and `[]`
|
|
16
17
|
* are the same wire state (S6/P6), so `.default()`/`.optional()` could never
|
|
17
18
|
* round-trip there.
|
|
18
19
|
*
|
|
19
20
|
* `.default()` and `.catch()` accept either a value or a zero-arg factory;
|
|
20
21
|
* factories are invoked per decode/encode, so reference-typed defaults can be
|
|
21
|
-
* isolated per call
|
|
22
|
+
* isolated per call. Array values are shallow-copied per call for the same
|
|
23
|
+
* isolation; other plain object values are returned by reference.
|
|
22
24
|
*
|
|
23
25
|
* Properties prefixed `~` are internal machinery, not public API. For
|
|
24
26
|
* arity-"many" codecs the element functions operate on single elements of
|
|
25
27
|
* `Out` (which is an array type).
|
|
26
28
|
*/
|
|
27
|
-
export interface Codec<Out, P extends Presence = "required", C extends boolean = false, A extends Arity = "single"> {
|
|
28
|
-
readonly catch: C extends false ? (fallback: (() => Out) | Out) => Codec<Out, P, true, A> : never;
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
export interface Codec<Out, P extends Presence = "required", C extends boolean = false, A extends Arity = "single", E extends boolean = boolean> {
|
|
30
|
+
readonly catch: C extends false ? (fallback: (() => Out) | Out) => Codec<Out, P, true, A, E> : never;
|
|
31
|
+
/**
|
|
32
|
+
* Overloaded so the value/factory split is visible in type-state (NQ6a):
|
|
33
|
+
* the factory overload comes FIRST and must stay first. The runtime
|
|
34
|
+
* {@link isFactory} check treats ANY function as a factory, so a function
|
|
35
|
+
* argument must either match the factory overload or fail to compile
|
|
36
|
+
* ({@link NonFactoryValue}) — E=true is only ever inferred for arguments
|
|
37
|
+
* the runtime will also treat as values. The one statically-invisible
|
|
38
|
+
* residue: an argument whose static type is not a function (e.g.
|
|
39
|
+
* `unknown`) but holds one at runtime lands on the runtime's factory
|
|
40
|
+
* branch anyway; no type-level check can see that.
|
|
41
|
+
*/
|
|
42
|
+
readonly default: A extends "single" ? P extends "required" ? {
|
|
43
|
+
(value: () => Out): Codec<Out, "defaulted", C, A, false>;
|
|
44
|
+
<V extends Out>(value: NonFactoryValue<V> & V): Codec<Out, "defaulted", C, A, true>;
|
|
45
|
+
} : never : never;
|
|
46
|
+
readonly optional: A extends "single" ? P extends "required" ? () => Codec<Out, "optional", C, A, E> : never : never;
|
|
31
47
|
readonly "~arity": A;
|
|
32
48
|
/** Stored as a thunk regardless of the form passed to `.catch()`. */
|
|
33
49
|
readonly "~catchValue": (() => Out) | undefined;
|
|
@@ -38,10 +54,22 @@ export interface Codec<Out, P extends Presence = "required", C extends boolean =
|
|
|
38
54
|
* re-serialized per encode. Factory defaults never elide: a time-varying
|
|
39
55
|
* factory would elide an explicitly-passed value that later decodes as a
|
|
40
56
|
* different one.
|
|
57
|
+
*
|
|
58
|
+
* Literal-typed via `E` (NQ6a) so derived surfaces (`@paramour-js/nuqs`)
|
|
59
|
+
* can give value-defaulted keys non-nullable reads while keeping
|
|
60
|
+
* factory-defaulted keys honestly nullable. A hand-written
|
|
61
|
+
* `Codec<…, "defaulted">` leaves `E` at its `boolean` default, which
|
|
62
|
+
* consumers must treat as the factory (nullable) branch — the safe
|
|
63
|
+
* reading.
|
|
41
64
|
*/
|
|
42
|
-
readonly "~defaultElides":
|
|
65
|
+
readonly "~defaultElides": E;
|
|
43
66
|
/** Stored as a thunk regardless of the form passed to `.default()`. */
|
|
44
67
|
readonly "~defaultValue": (() => Out) | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Element codec of a composite list codec (currently `p.csv`) — the
|
|
70
|
+
* per-segment scalar; undefined for every non-composite kind (CV6).
|
|
71
|
+
*/
|
|
72
|
+
readonly "~element": AnyCodec | undefined;
|
|
45
73
|
/** Members of a `p.enum` codec; undefined for every other kind. */
|
|
46
74
|
readonly "~enumMembers": readonly string[] | undefined;
|
|
47
75
|
/**
|
|
@@ -66,11 +94,24 @@ export type ParamCodec = Codec<any, "required", boolean>;
|
|
|
66
94
|
*/
|
|
67
95
|
export type Presence = "defaulted" | "optional" | "required";
|
|
68
96
|
export type PresenceOf<C extends AnyCodec> = C["~presence"];
|
|
97
|
+
/**
|
|
98
|
+
* Rejects value-form `.default()` arguments whose static type includes any
|
|
99
|
+
* function member: runtime {@link isFactory} would treat them as factories,
|
|
100
|
+
* so letting them infer the value branch would let type-state assert an
|
|
101
|
+
* elision (`E = true`) the runtime never performs (NQ6a). Non-distributive
|
|
102
|
+
* on purpose — a union with a function member is rejected whole, since its
|
|
103
|
+
* runtime branch is unknowable at compile time.
|
|
104
|
+
*/
|
|
105
|
+
type NonFactoryValue<V> = [Extract<V, (...args: never[]) => unknown>] extends [
|
|
106
|
+
never
|
|
107
|
+
] ? unknown : never;
|
|
69
108
|
/** Internal factory used by the `p.*` builders. */
|
|
70
109
|
export declare function createCodec<Out, A extends Arity = "single">(impl: {
|
|
71
110
|
arity?: A;
|
|
111
|
+
element?: AnyCodec;
|
|
72
112
|
enumMembers?: readonly string[];
|
|
73
113
|
kind?: string;
|
|
74
114
|
parseElement: (raw: string) => unknown;
|
|
75
115
|
serializeElement: (value: unknown) => string;
|
|
76
116
|
}): Codec<Out, "required", false, A>;
|
|
117
|
+
export {};
|
package/dist/codec.js
CHANGED
|
@@ -6,6 +6,7 @@ export function createCodec(impl) {
|
|
|
6
6
|
catchValue: undefined,
|
|
7
7
|
defaultElides: false,
|
|
8
8
|
defaultValue: undefined,
|
|
9
|
+
element: impl.element,
|
|
9
10
|
enumMembers: impl.enumMembers,
|
|
10
11
|
kind: impl.kind ?? "custom",
|
|
11
12
|
parseElement: impl.parseElement,
|
|
@@ -60,6 +61,7 @@ function build(state) {
|
|
|
60
61
|
"~caught": state.catchValue !== undefined,
|
|
61
62
|
"~defaultElides": state.defaultElides,
|
|
62
63
|
"~defaultValue": state.defaultValue,
|
|
64
|
+
"~element": state.element,
|
|
63
65
|
"~enumMembers": state.enumMembers,
|
|
64
66
|
"~kind": state.kind,
|
|
65
67
|
"~parseElement": state.parseElement,
|
|
@@ -90,8 +92,15 @@ function serializeDefault(serializeElement, value) {
|
|
|
90
92
|
* the one chokepoint where a throwing user factory is branded ParamourError.
|
|
91
93
|
*/
|
|
92
94
|
function toThunk(stored, what) {
|
|
93
|
-
if (!isFactory(stored))
|
|
95
|
+
if (!isFactory(stored)) {
|
|
96
|
+
// Array values are handed out as fresh shallow copies: a consumer
|
|
97
|
+
// mutating a decoded fallback must not pollute later decodes or shift
|
|
98
|
+
// D8 elision (p.csv makes array defaults idiomatic — CV5). Non-array
|
|
99
|
+
// reference values stay by-reference; use a factory to isolate those.
|
|
100
|
+
if (Array.isArray(stored))
|
|
101
|
+
return () => stored.slice();
|
|
94
102
|
return () => stored;
|
|
103
|
+
}
|
|
95
104
|
return () => {
|
|
96
105
|
try {
|
|
97
106
|
return stored();
|
package/dist/describe.d.ts
CHANGED
|
@@ -22,10 +22,17 @@ export interface CodecDescription {
|
|
|
22
22
|
readonly arity: Arity;
|
|
23
23
|
readonly caught: boolean;
|
|
24
24
|
readonly defaultValue?: CodecDefaultDescription;
|
|
25
|
+
/**
|
|
26
|
+
* Nested description of a composite list codec's element scalar (CV6;
|
|
27
|
+
* currently `p.csv`).
|
|
28
|
+
*/
|
|
29
|
+
readonly element?: CodecDescription;
|
|
25
30
|
readonly enumMembers?: readonly string[];
|
|
26
31
|
readonly kind: string;
|
|
27
32
|
readonly presence: Presence;
|
|
28
33
|
}
|
|
34
|
+
/** Rendering styles accepted by {@link formatCodecDescription}. */
|
|
35
|
+
export type CodecFormatStyle = "compact" | "verbose";
|
|
29
36
|
/** A param codec plus the dynamic-segment kind that hosts it. */
|
|
30
37
|
export interface ParamDescription extends CodecDescription {
|
|
31
38
|
readonly segmentKind: "catchall" | "optional-catchall" | "single";
|
|
@@ -61,3 +68,16 @@ export declare function describeCodec(codec: AnyCodec): CodecDescription;
|
|
|
61
68
|
* both router brands — reflection only needs the data core.
|
|
62
69
|
*/
|
|
63
70
|
export declare function describeRoute(route: AnyRoute): RouteDescription;
|
|
71
|
+
/**
|
|
72
|
+
* One-line label for a {@link CodecDescription} — THE shared walk over the
|
|
73
|
+
* description's fields, so every consumer (the devtools panel's shape
|
|
74
|
+
* column, `paramour list`'s annotations) renders the same structure and a
|
|
75
|
+
* future field lands everywhere at once. Two skins over one walk:
|
|
76
|
+
*
|
|
77
|
+
* - `"compact"`: `enum(asc|desc)? =asc catch`, `csv<enum(a|b)>`, `string[]`
|
|
78
|
+
* — `?` for optional presence, the default's wire form (`=3`) or `=ƒ()`
|
|
79
|
+
* for a factory default, bare `catch`.
|
|
80
|
+
* - `"verbose"`: `enum(asc, desc) (optional) (default: asc) (catch)` —
|
|
81
|
+
* parenthesized annotations in fixed order: presence, default, catch.
|
|
82
|
+
*/
|
|
83
|
+
export declare function formatCodecDescription(description: CodecDescription, style: CodecFormatStyle): string;
|
package/dist/describe.js
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export function describeCodec(codec) {
|
|
6
6
|
const defaultValue = describeDefault(codec);
|
|
7
|
+
const element = codec["~element"];
|
|
7
8
|
const enumMembers = codec["~enumMembers"];
|
|
8
9
|
return {
|
|
9
10
|
arity: codec["~arity"],
|
|
10
11
|
caught: codec["~caught"],
|
|
11
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.
|
|
15
|
+
...(element === undefined ? {} : { element: describeCodec(element) }),
|
|
12
16
|
...(enumMembers === undefined ? {} : { enumMembers }),
|
|
13
17
|
kind: codec["~kind"],
|
|
14
18
|
presence: codec["~presence"],
|
|
@@ -42,6 +46,53 @@ export function describeRoute(route) {
|
|
|
42
46
|
search: describeSearch(route["~search"]),
|
|
43
47
|
};
|
|
44
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* One-line label for a {@link CodecDescription} — THE shared walk over the
|
|
51
|
+
* description's fields, so every consumer (the devtools panel's shape
|
|
52
|
+
* column, `paramour list`'s annotations) renders the same structure and a
|
|
53
|
+
* future field lands everywhere at once. Two skins over one walk:
|
|
54
|
+
*
|
|
55
|
+
* - `"compact"`: `enum(asc|desc)? =asc catch`, `csv<enum(a|b)>`, `string[]`
|
|
56
|
+
* — `?` for optional presence, the default's wire form (`=3`) or `=ƒ()`
|
|
57
|
+
* for a factory default, bare `catch`.
|
|
58
|
+
* - `"verbose"`: `enum(asc, desc) (optional) (default: asc) (catch)` —
|
|
59
|
+
* parenthesized annotations in fixed order: presence, default, catch.
|
|
60
|
+
*/
|
|
61
|
+
export function formatCodecDescription(description, style) {
|
|
62
|
+
const memberSeparator = style === "compact" ? "|" : ", ";
|
|
63
|
+
const kindLabel = (part) => part.enumMembers === undefined
|
|
64
|
+
? part.kind
|
|
65
|
+
: `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 += "[]";
|
|
71
|
+
if (style === "compact") {
|
|
72
|
+
if (description.presence === "optional")
|
|
73
|
+
label += "?";
|
|
74
|
+
if (description.defaultValue !== undefined) {
|
|
75
|
+
label +=
|
|
76
|
+
description.defaultValue.kind === "value"
|
|
77
|
+
? ` =${description.defaultValue.wire}`
|
|
78
|
+
: " =ƒ()";
|
|
79
|
+
}
|
|
80
|
+
if (description.caught)
|
|
81
|
+
label += " catch";
|
|
82
|
+
return label;
|
|
83
|
+
}
|
|
84
|
+
const notes = [];
|
|
85
|
+
if (description.presence === "optional")
|
|
86
|
+
notes.push("(optional)");
|
|
87
|
+
if (description.defaultValue !== undefined) {
|
|
88
|
+
notes.push(description.defaultValue.kind === "value"
|
|
89
|
+
? `(default: ${description.defaultValue.wire})`
|
|
90
|
+
: "(default: factory)");
|
|
91
|
+
}
|
|
92
|
+
if (description.caught)
|
|
93
|
+
notes.push("(catch)");
|
|
94
|
+
return [label, ...notes].join(" ");
|
|
95
|
+
}
|
|
45
96
|
/**
|
|
46
97
|
* Value-form defaults re-serialize the live value (the D8 ethos — never a
|
|
47
98
|
* stale snapshot); a throwing serialize here means the default was mutated
|
package/dist/errors.d.ts
CHANGED
|
@@ -54,8 +54,12 @@ export declare class SerializeError extends ParamourError {
|
|
|
54
54
|
*/
|
|
55
55
|
export declare function describeType(value: unknown): string;
|
|
56
56
|
/**
|
|
57
|
-
* Best-effort human-readable message for a foreign (non-paramour) throw
|
|
58
|
-
*
|
|
57
|
+
* Best-effort human-readable message for a foreign (non-paramour) throw:
|
|
58
|
+
* an `Error`'s message, else a {@link showValue}-hardened `String()` — safe
|
|
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.
|
|
59
63
|
*/
|
|
60
64
|
export declare function foreignMessage(error: unknown): string;
|
|
61
65
|
/**
|
package/dist/errors.js
CHANGED
|
@@ -107,8 +107,12 @@ export function describeType(value) {
|
|
|
107
107
|
return value === null ? "null" : typeof value;
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
|
-
* Best-effort human-readable message for a foreign (non-paramour) throw
|
|
111
|
-
*
|
|
110
|
+
* Best-effort human-readable message for a foreign (non-paramour) throw:
|
|
111
|
+
* an `Error`'s message, else a {@link showValue}-hardened `String()` — safe
|
|
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.
|
|
112
116
|
*/
|
|
113
117
|
export function foreignMessage(error) {
|
|
114
118
|
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
|
-
export { type CodecDefaultDescription, type CodecDescription, describeCodec, describeRoute, type ParamDescription, type RouteDescription, type SearchDescription, } from "./describe.js";
|
|
3
|
-
export { type Issue, ParamourError, ParamsDecodeError, ParseError, type RouteDecodeError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
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";
|
|
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
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";
|
|
8
8
|
export { safeDecodeParams, safeDecodeSearch } from "./safe-decode.js";
|
|
9
|
-
export { buildSearchString, decodeSearch, encodeSearch, type InferSearchInput, type InferSearchOutput, rawSearch, type RawSearch, type SearchConfig, type SearchOutputOf, type SearchSource, searchToString, } from "./search.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";
|
|
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
|
-
export { describeCodec, describeRoute, } from "./describe.js";
|
|
3
|
-
export { ParamourError, ParamsDecodeError, ParseError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
|
|
2
|
+
export { describeCodec, describeRoute, formatCodecDescription, } from "./describe.js";
|
|
3
|
+
export { foreignMessage, 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, rawSearch, searchToString, } from "./search.js";
|
|
9
|
+
export { buildSearchString, decodeSearch, encodeSearch, isRawSearch, parseValue, rawSearch, searchToString, serializeValue, } from "./search.js";
|
|
10
10
|
export { standardSearchSchema, } from "./standard-schema.js";
|
package/dist/p.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ import { type Codec } from "./codec.js";
|
|
|
6
6
|
*/
|
|
7
7
|
export declare const p: {
|
|
8
8
|
boolean(): Codec<boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* 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
|
|
12
|
+
* repeated-key format (CV7: both are first-class; csv is the one-key
|
|
13
|
+
* packing). Elements are strings unless an element codec is given.
|
|
14
|
+
*/
|
|
15
|
+
csv<E = string>(element?: Codec<E>): Codec<E[]>;
|
|
9
16
|
custom<Out>(codec: {
|
|
10
17
|
/** Reflection name shown by describeCodec/`paramour list` (default "custom"). */
|
|
11
18
|
label?: string;
|
package/dist/p.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createCodec } from "./codec.js";
|
|
2
|
-
import { foreignMessage, ParseError, rebrandForeign, SerializeError, showValue, } from "./errors.js";
|
|
2
|
+
import { foreignMessage, ParamourError, ParseError, rebrandForeign, SerializeError, showValue, } from "./errors.js";
|
|
3
3
|
import { runStandardSchemaSync } from "./schema.js";
|
|
4
4
|
// Wire grammars per wire-format spec §4. `Number()` alone is too loose
|
|
5
5
|
// (accepts hex, trims whitespace), hence explicit anchored patterns.
|
|
@@ -86,6 +86,12 @@ function serializeFiniteNumber(value) {
|
|
|
86
86
|
function stringifyJson(value) {
|
|
87
87
|
return rebrandForeign(() => JSON.stringify(value), (error) => new SerializeError("Value is not JSON-serializable", { cause: error }));
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
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
|
|
92
|
+
* exist yet while the module initializes.
|
|
93
|
+
*/
|
|
94
|
+
let defaultCsvElement;
|
|
89
95
|
/**
|
|
90
96
|
* The `p.*` wire-codec builders (DESIGN §5 layer 1, design-02 D9).
|
|
91
97
|
* Each codec defines how one value crosses the URL boundary, both directions.
|
|
@@ -109,6 +115,85 @@ export const p = {
|
|
|
109
115
|
},
|
|
110
116
|
});
|
|
111
117
|
},
|
|
118
|
+
/**
|
|
119
|
+
* 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
|
|
121
|
+
* repeated-key format (CV7: both are first-class; csv is the one-key
|
|
122
|
+
* packing). Elements are strings unless an element codec is given.
|
|
123
|
+
*/
|
|
124
|
+
csv(element) {
|
|
125
|
+
// 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());
|
|
134
|
+
if (inner["~element"] !== undefined) {
|
|
135
|
+
throw new ParamourError("p.csv() elements cannot themselves be csv lists");
|
|
136
|
+
}
|
|
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
|
+
const parseInner = inner["~parseElement"];
|
|
143
|
+
const serializeInner = inner["~serializeElement"];
|
|
144
|
+
return createCodec({
|
|
145
|
+
element: inner,
|
|
146
|
+
kind: "csv",
|
|
147
|
+
parseElement: (raw) => {
|
|
148
|
+
// CV3: the empty wire string is [] — checked before split, because
|
|
149
|
+
// "".split(",") is [""], which the strict grammar would reject.
|
|
150
|
+
if (raw === "")
|
|
151
|
+
return [];
|
|
152
|
+
const result = [];
|
|
153
|
+
for (const segment of raw.split(",")) {
|
|
154
|
+
// CV3: strict grammar — "a,,b", trailing "a,", and a lone "," are
|
|
155
|
+
// ParseErrors, recoverable via the LIST's .catch() (D2).
|
|
156
|
+
if (segment === "") {
|
|
157
|
+
throw new ParseError(`"${raw}" contains an empty list element`);
|
|
158
|
+
}
|
|
159
|
+
// CV3: the first element failure aborts the list parse — the
|
|
160
|
+
// element codec's own ParseError propagates unwrapped.
|
|
161
|
+
result.push(parseInner(segment));
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
},
|
|
165
|
+
serializeElement: (value) => {
|
|
166
|
+
if (!Array.isArray(value)) {
|
|
167
|
+
throw new SerializeError(`Expected an array, got ${showValue(value)}`);
|
|
168
|
+
}
|
|
169
|
+
const parts = [];
|
|
170
|
+
for (const item of value) {
|
|
171
|
+
const serialized = serializeInner(item);
|
|
172
|
+
// A plain-JS custom element serializer can return a non-string;
|
|
173
|
+
// enforce the string contract here — as search.ts/path.ts do at
|
|
174
|
+
// their ~serializeElement call sites — so the CV4 guards below
|
|
175
|
+
// cannot throw raw TypeErrors.
|
|
176
|
+
if (typeof serialized !== "string") {
|
|
177
|
+
throw new SerializeError(`List element serializer must return a string, got ${typeof serialized}`);
|
|
178
|
+
}
|
|
179
|
+
// CV4: an empty segment on re-parse; [""] is deliberately
|
|
180
|
+
// unrepresentable — the empty wire string already means [].
|
|
181
|
+
if (serialized === "") {
|
|
182
|
+
throw new SerializeError(`List element ${showValue(item)} serializes to the empty string`);
|
|
183
|
+
}
|
|
184
|
+
// CV4: would mis-split on re-parse.
|
|
185
|
+
if (serialized.includes(",")) {
|
|
186
|
+
throw new SerializeError(`List element serialization "${serialized}" contains a comma`);
|
|
187
|
+
}
|
|
188
|
+
parts.push(serialized);
|
|
189
|
+
}
|
|
190
|
+
// [] joins to "" (CV5) — which is also why .default([])'s
|
|
191
|
+
// define-time pre-serialization succeeds and D8 elision compares
|
|
192
|
+
// [] against the empty wire form.
|
|
193
|
+
return parts.join(",");
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
},
|
|
112
197
|
custom(codec) {
|
|
113
198
|
// Paramour's own errors are never downgraded: ANY ParamourError thrown
|
|
114
199
|
// by user parse/serialize code — config-level failures (async schema,
|
package/dist/path.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describeType, ParamourError, ParamsDecodeError, ParseError, SerializeError, } from "./errors.js";
|
|
2
|
-
import { encodeComponent, readInputValue } from "./search.js";
|
|
2
|
+
import { encodeComponent, readInputValue, serializeValue } from "./search.js";
|
|
3
3
|
// Anchored per the wire-format spec's regex ethos; name charset excludes
|
|
4
4
|
// brackets so nesting can't smuggle through. Match order mirrors the type
|
|
5
5
|
// grammar: `[[...` before `[...` before `[`.
|
|
@@ -394,18 +394,14 @@ function serializeDynamicSegment(codec, segment, value) {
|
|
|
394
394
|
};
|
|
395
395
|
}
|
|
396
396
|
/**
|
|
397
|
-
* Serializes one segment value into its wire-string form.
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
* static surfaces must skip it.
|
|
397
|
+
* Serializes one segment value into its wire-string form. The string
|
|
398
|
+
* contract is search.ts's shared {@link serializeValue}; the path surface
|
|
399
|
+
* layers R4 on top (a segment additionally can't be empty).
|
|
400
|
+
* Percent-encoding is deliberately NOT applied here: that is encodeParams'
|
|
401
|
+
* byte-layer step (S7), and the static surfaces must skip it.
|
|
403
402
|
*/
|
|
404
403
|
function serializeSegmentValue(codec, name, value) {
|
|
405
|
-
const serialized = codec
|
|
406
|
-
if (typeof serialized !== "string") {
|
|
407
|
-
throw new SerializeError(`serializer for route param "${name}" must return a string, got ${typeof serialized}`);
|
|
408
|
-
}
|
|
404
|
+
const serialized = serializeValue(codec, `route param "${name}"`, value);
|
|
409
405
|
if (serialized === "") {
|
|
410
406
|
// R4: "" would produce "//" or a vanishing segment — same rationale as R3.
|
|
411
407
|
throw new SerializeError(`route param "${name}" serialized to an empty string, which cannot form a path segment`);
|
package/dist/search.d.ts
CHANGED
|
@@ -112,12 +112,28 @@ export declare function encodeComponent(text: string): string;
|
|
|
112
112
|
*/
|
|
113
113
|
export declare function encodeSearch<S extends SearchSlot>(config: S, input: SearchInputOf<S>): [string, string][];
|
|
114
114
|
/**
|
|
115
|
-
* Runtime discriminant for the `search:` slot (design-04 SS2): the
|
|
116
|
-
* `~kind` marker is unambiguous against a codec map
|
|
117
|
-
*
|
|
118
|
-
* barrel
|
|
115
|
+
* Runtime discriminant for the `search:` slot (design-04 SS2): probes the
|
|
116
|
+
* `~kind` marker's VALUE, which is unambiguous against a codec map — a map
|
|
117
|
+
* key literally named "~kind" would hold a codec object, never the marker
|
|
118
|
+
* string. Exported from the package barrel so derived surfaces
|
|
119
|
+
* (`@paramour-js/nuqs`) share the discriminant instead of duplicating the
|
|
120
|
+
* literal.
|
|
119
121
|
*/
|
|
120
122
|
export declare function isRawSearch(config: SearchSlot): config is RawSearch<StandardSchemaV1>;
|
|
123
|
+
/**
|
|
124
|
+
* Invokes a codec's element parser on one wire string — the parse twin of
|
|
125
|
+
* {@link serializeValue}, and the ONE sanctioned way to run a parse WITHOUT
|
|
126
|
+
* the codec's `.catch()` recovery applied (decodeSearch always recovers a
|
|
127
|
+
* caught failure, so a probe through it cannot tell "parsed cleanly" from
|
|
128
|
+
* "failed and was caught"). Exists for reflection-driven tooling — the
|
|
129
|
+
* devtools panel's catch-attribution probe (design-12 DT7) — and any other
|
|
130
|
+
* derived surface that must observe the raw parse outcome. A parse failure
|
|
131
|
+
* throws the codec's own {@link ParseError}; foreign throws from a custom
|
|
132
|
+
* codec propagate unwrapped, matching decodeSearch's taxonomy. For
|
|
133
|
+
* arity-"many" codecs this parses ONE element of the repeated-key array,
|
|
134
|
+
* not the whole array (the same contract as `~parseElement` itself).
|
|
135
|
+
*/
|
|
136
|
+
export declare function parseValue(codec: AnyCodec, raw: string): unknown;
|
|
121
137
|
/**
|
|
122
138
|
* The whole-object search escape hatch (design-04 SS1, maintainer ruling):
|
|
123
139
|
* an explicit, greppable wrapper around a bare Standard Schema so a route's
|
|
@@ -152,4 +168,16 @@ export declare function readInputValue(values: Record<string, unknown>, key: str
|
|
|
152
168
|
export declare function requireSearchConfig(config: SearchSlot): void;
|
|
153
169
|
/** Convenience: encode + build in one step. */
|
|
154
170
|
export declare function searchToString<S extends SearchSlot>(config: S, input: SearchInputOf<S>): string;
|
|
171
|
+
/**
|
|
172
|
+
* Invokes a codec's serializer and enforces its string contract: a custom
|
|
173
|
+
* codec written in plain JS can return undefined, which would otherwise
|
|
174
|
+
* reach the byte layer as the literal text "undefined" — or, worse, match
|
|
175
|
+
* an absent default and silently drop the param. `label` names the value's
|
|
176
|
+
* site in the error (`search param "q"`, `route param "id"`, …). Exported
|
|
177
|
+
* from the package barrel as the ONE implementation of this contract:
|
|
178
|
+
* path.ts segments and derived surfaces (`@paramour-js/nuqs` eq/
|
|
179
|
+
* clearOnDefault) share it so their judgment stays identical to D8
|
|
180
|
+
* elision's by construction, not by parallel copies.
|
|
181
|
+
*/
|
|
182
|
+
export declare function serializeValue(codec: AnyCodec, label: string, value: unknown): string;
|
|
155
183
|
export {};
|
package/dist/search.js
CHANGED
|
@@ -164,7 +164,10 @@ export function encodeSearch(config, input) {
|
|
|
164
164
|
}
|
|
165
165
|
// Array codecs cannot carry defaults, so no elision applies.
|
|
166
166
|
for (const element of value) {
|
|
167
|
-
pairs.push([
|
|
167
|
+
pairs.push([
|
|
168
|
+
key,
|
|
169
|
+
serializeValue(codec, `search param "${key}"`, element),
|
|
170
|
+
]);
|
|
168
171
|
}
|
|
169
172
|
continue;
|
|
170
173
|
}
|
|
@@ -174,13 +177,14 @@ export function encodeSearch(config, input) {
|
|
|
174
177
|
}
|
|
175
178
|
continue; // absent optional/defaulted param → key omitted (S3)
|
|
176
179
|
}
|
|
177
|
-
const serialized = serializeValue(codec, key
|
|
180
|
+
const serialized = serializeValue(codec, `search param "${key}"`, value);
|
|
178
181
|
// D8 elision, gated on an elidable default existing — an ungated
|
|
179
182
|
// comparison would let a (contract-violating) serialize that returns
|
|
180
183
|
// undefined match undefined and silently drop the param.
|
|
181
184
|
if (codec["~defaultElides"] &&
|
|
182
185
|
codec["~defaultValue"] !== undefined &&
|
|
183
|
-
serialized ===
|
|
186
|
+
serialized ===
|
|
187
|
+
serializeValue(codec, `search param "${key}"`, codec["~defaultValue"]())) {
|
|
184
188
|
continue;
|
|
185
189
|
}
|
|
186
190
|
pairs.push([key, serialized]);
|
|
@@ -188,14 +192,32 @@ export function encodeSearch(config, input) {
|
|
|
188
192
|
return pairs;
|
|
189
193
|
}
|
|
190
194
|
/**
|
|
191
|
-
* Runtime discriminant for the `search:` slot (design-04 SS2): the
|
|
192
|
-
* `~kind` marker is unambiguous against a codec map
|
|
193
|
-
*
|
|
194
|
-
* barrel
|
|
195
|
+
* Runtime discriminant for the `search:` slot (design-04 SS2): probes the
|
|
196
|
+
* `~kind` marker's VALUE, which is unambiguous against a codec map — a map
|
|
197
|
+
* key literally named "~kind" would hold a codec object, never the marker
|
|
198
|
+
* string. Exported from the package barrel so derived surfaces
|
|
199
|
+
* (`@paramour-js/nuqs`) share the discriminant instead of duplicating the
|
|
200
|
+
* literal.
|
|
195
201
|
*/
|
|
196
202
|
export function isRawSearch(config) {
|
|
197
203
|
return "~kind" in config && config["~kind"] === "raw-search";
|
|
198
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Invokes a codec's element parser on one wire string — the parse twin of
|
|
207
|
+
* {@link serializeValue}, and the ONE sanctioned way to run a parse WITHOUT
|
|
208
|
+
* the codec's `.catch()` recovery applied (decodeSearch always recovers a
|
|
209
|
+
* caught failure, so a probe through it cannot tell "parsed cleanly" from
|
|
210
|
+
* "failed and was caught"). Exists for reflection-driven tooling — the
|
|
211
|
+
* devtools panel's catch-attribution probe (design-12 DT7) — and any other
|
|
212
|
+
* derived surface that must observe the raw parse outcome. A parse failure
|
|
213
|
+
* throws the codec's own {@link ParseError}; foreign throws from a custom
|
|
214
|
+
* codec propagate unwrapped, matching decodeSearch's taxonomy. For
|
|
215
|
+
* arity-"many" codecs this parses ONE element of the repeated-key array,
|
|
216
|
+
* not the whole array (the same contract as `~parseElement` itself).
|
|
217
|
+
*/
|
|
218
|
+
export function parseValue(codec, raw) {
|
|
219
|
+
return codec["~parseElement"](raw);
|
|
220
|
+
}
|
|
199
221
|
/**
|
|
200
222
|
* The whole-object search escape hatch (design-04 SS1, maintainer ruling):
|
|
201
223
|
* an explicit, greppable wrapper around a bare Standard Schema so a route's
|
|
@@ -255,6 +277,24 @@ export function requireSearchConfig(config) {
|
|
|
255
277
|
export function searchToString(config, input) {
|
|
256
278
|
return buildSearchString(encodeSearch(config, input));
|
|
257
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Invokes a codec's serializer and enforces its string contract: a custom
|
|
282
|
+
* codec written in plain JS can return undefined, which would otherwise
|
|
283
|
+
* reach the byte layer as the literal text "undefined" — or, worse, match
|
|
284
|
+
* an absent default and silently drop the param. `label` names the value's
|
|
285
|
+
* site in the error (`search param "q"`, `route param "id"`, …). Exported
|
|
286
|
+
* from the package barrel as the ONE implementation of this contract:
|
|
287
|
+
* path.ts segments and derived surfaces (`@paramour-js/nuqs` eq/
|
|
288
|
+
* clearOnDefault) share it so their judgment stays identical to D8
|
|
289
|
+
* elision's by construction, not by parallel copies.
|
|
290
|
+
*/
|
|
291
|
+
export function serializeValue(codec, label, value) {
|
|
292
|
+
const serialized = codec["~serializeElement"](value);
|
|
293
|
+
if (typeof serialized !== "string") {
|
|
294
|
+
throw new SerializeError(`serializer for ${label} must return a string, got ${typeof serialized}`);
|
|
295
|
+
}
|
|
296
|
+
return serialized;
|
|
297
|
+
}
|
|
258
298
|
/**
|
|
259
299
|
* The `RawSearch` decode path (design-04 SS3/SS4). The schema receives EVERY
|
|
260
300
|
* source key, normalized to Next's own `searchParams` shape — P8's
|
|
@@ -459,16 +499,3 @@ function requireRawSearchString(key, value) {
|
|
|
459
499
|
}
|
|
460
500
|
return value;
|
|
461
501
|
}
|
|
462
|
-
/**
|
|
463
|
-
* Invokes a codec's serializer and enforces its string contract: a custom
|
|
464
|
-
* codec written in plain JS can return undefined, which would otherwise
|
|
465
|
-
* reach the byte layer as the literal text "undefined" — or, worse, match
|
|
466
|
-
* an absent default and silently drop the param.
|
|
467
|
-
*/
|
|
468
|
-
function serializeValue(codec, key, value) {
|
|
469
|
-
const serialized = codec["~serializeElement"](value);
|
|
470
|
-
if (typeof serialized !== "string") {
|
|
471
|
-
throw new SerializeError(`serializer for search param "${key}" must return a string, got ${typeof serialized}`);
|
|
472
|
-
}
|
|
473
|
-
return serialized;
|
|
474
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paramour",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -8,10 +8,6 @@
|
|
|
8
8
|
"default": "./dist/index.js"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"build": "tsc -p tsconfig.build.json",
|
|
13
|
-
"typecheck": "tsc --noEmit"
|
|
14
|
-
},
|
|
15
11
|
"description": "Type-safe routing companion for the Next.js App Router: validated route params and search params, typed path building, and explicit URL serialization.",
|
|
16
12
|
"keywords": [
|
|
17
13
|
"nextjs",
|
|
@@ -37,6 +33,10 @@
|
|
|
37
33
|
"files": [
|
|
38
34
|
"dist"
|
|
39
35
|
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"provenance": true
|
|
39
|
+
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@standard-schema/spec": "^1.1.0"
|
|
42
42
|
},
|
|
@@ -46,5 +46,9 @@
|
|
|
46
46
|
"fast-check": "^4.8.0",
|
|
47
47
|
"valibot": "^1.4.2",
|
|
48
48
|
"zod": "^4.4.3"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -p tsconfig.build.json",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
49
53
|
}
|
|
50
|
-
}
|
|
54
|
+
}
|