@paramour-js/next 0.1.2 → 0.2.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/app.d.ts +23 -0
- package/dist/app.js +133 -7
- package/dist/devtools-seam.d.ts +147 -0
- package/dist/devtools-seam.js +78 -0
- package/dist/list/render.d.ts +3 -2
- package/dist/list/render.js +5 -18
- package/dist/observe.d.ts +71 -0
- package/dist/observe.js +73 -0
- package/dist/pages.d.ts +9 -0
- package/dist/pages.js +61 -8
- package/dist/select.js +29 -7
- package/package.json +6 -2
package/dist/app.d.ts
CHANGED
|
@@ -36,6 +36,29 @@ export type { SelectOptions } from "./select.js";
|
|
|
36
36
|
* at one of these call sites is a compile error, not a runtime surprise —
|
|
37
37
|
* these hooks read Next's App-Router navigation hooks, whose pages twin has
|
|
38
38
|
* different state cardinality (`@paramour-js/next/pages`).
|
|
39
|
+
*
|
|
40
|
+
* Devtools instrumentation (design-12): each hook reports through the shared
|
|
41
|
+
* emitter in observe.ts — `observe` from inside the `useStableResult`
|
|
42
|
+
* compute callback, which runs exactly on a `(route, fingerprint)` cache
|
|
43
|
+
* miss, so the SEL4 fingerprint layer IS the decode-change dedup (DT4;
|
|
44
|
+
* StrictMode's dev double render reuses the ref cache and cannot
|
|
45
|
+
* double-emit), and `refresh` after the stable result returns, re-emitting
|
|
46
|
+
* the CACHED result when the pathname moved under an unchanged decode so
|
|
47
|
+
* the seam's `navigate`/`pathname` never go stale (DT8). Observations carry
|
|
48
|
+
* the full pre-`select` result (DT12), and the `OrThrow` hooks report the
|
|
49
|
+
* error observation BEFORE rethrowing — only render-phase can, since an
|
|
50
|
+
* effect never runs for a throwing render. Every emit sits behind
|
|
51
|
+
* `process.env.NODE_ENV !== "production"`, which bundlers constant-fold and
|
|
52
|
+
* erase along with the seam module (DT6); the spec each hook hands the
|
|
53
|
+
* emitter is built behind the same literal guard, so prod allocates
|
|
54
|
+
* nothing. `useRouter` and `usePathname` are called unconditionally in
|
|
55
|
+
* every hook (rules of hooks — a build-constant-guarded call would make
|
|
56
|
+
* hook order differ between dev and prod bundles); their cost is a
|
|
57
|
+
* referentially-stable context read each, and only the dev-only spec
|
|
58
|
+
* captures them. The `navigate` capability receives the panel's SEARCH
|
|
59
|
+
* STRING only and resolves it against `usePathname()` — basePath-/locale-
|
|
60
|
+
* relative, exactly what `router.replace` expects back (DT8; the live hash
|
|
61
|
+
* is preserved at call time).
|
|
39
62
|
*/
|
|
40
63
|
/**
|
|
41
64
|
* Decoded route params as a `SafeResult` (discriminated on `status`, PR12),
|
package/dist/app.js
CHANGED
|
@@ -1,24 +1,125 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useParams, useSearchParams } from "next/navigation";
|
|
3
|
-
import { decodeParams, decodeSearch, safeDecodeParams, safeDecodeSearch, } from "paramour";
|
|
2
|
+
import { useParams, usePathname, useRouter, useSearchParams, } from "next/navigation";
|
|
3
|
+
import { decodeParams, decodeSearch, ParamsDecodeError, safeDecodeParams, safeDecodeSearch, SearchDecodeError, } from "paramour";
|
|
4
|
+
import { searchWireSnapshot } from "./devtools-seam.js";
|
|
5
|
+
import { makeAppNavigate, useDevtoolsEmitter, } from "./observe.js";
|
|
4
6
|
import { paramsFingerprint, searchParamsFingerprint, useSelectedResult, useSelectedValue, useStableResult, } from "./select.js";
|
|
5
7
|
export function useRouteParams(route, options) {
|
|
6
8
|
const params = useParams() ?? {};
|
|
7
|
-
const
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const pathname = usePathname();
|
|
11
|
+
const emitter = useDevtoolsEmitter();
|
|
12
|
+
const spec = process.env.NODE_ENV === "production"
|
|
13
|
+
? undefined
|
|
14
|
+
: {
|
|
15
|
+
hook: "app.useRouteParams",
|
|
16
|
+
kind: "params",
|
|
17
|
+
navigate: makeAppNavigate(router, pathname),
|
|
18
|
+
pathname,
|
|
19
|
+
route,
|
|
20
|
+
routerKind: "app",
|
|
21
|
+
wire: () => ({ ...params }),
|
|
22
|
+
};
|
|
23
|
+
const result = useStableResult(route, paramsFingerprint(route, params), () => {
|
|
24
|
+
const decoded = safeDecodeParams(route, params);
|
|
25
|
+
if (process.env.NODE_ENV !== "production" && spec !== undefined) {
|
|
26
|
+
emitter.observe(spec, decoded);
|
|
27
|
+
}
|
|
28
|
+
return decoded;
|
|
29
|
+
});
|
|
30
|
+
if (process.env.NODE_ENV !== "production" && spec !== undefined) {
|
|
31
|
+
emitter.refresh(spec);
|
|
32
|
+
}
|
|
8
33
|
return useSelectedResult(result, options);
|
|
9
34
|
}
|
|
10
35
|
export function useRouteParamsOrThrow(route, options) {
|
|
11
36
|
const params = useParams() ?? {};
|
|
12
|
-
const
|
|
37
|
+
const router = useRouter();
|
|
38
|
+
const pathname = usePathname();
|
|
39
|
+
const emitter = useDevtoolsEmitter();
|
|
40
|
+
const spec = process.env.NODE_ENV === "production"
|
|
41
|
+
? undefined
|
|
42
|
+
: {
|
|
43
|
+
hook: "app.useRouteParamsOrThrow",
|
|
44
|
+
kind: "params",
|
|
45
|
+
navigate: makeAppNavigate(router, pathname),
|
|
46
|
+
pathname,
|
|
47
|
+
route,
|
|
48
|
+
routerKind: "app",
|
|
49
|
+
wire: () => ({ ...params }),
|
|
50
|
+
};
|
|
51
|
+
const value = useStableResult(route, paramsFingerprint(route, params), () => {
|
|
52
|
+
// The duplicated decode call across the prod/dev branches is the price
|
|
53
|
+
// of literal-zero prod cost — the bundler keeps exactly one branch (DT6).
|
|
54
|
+
if (process.env.NODE_ENV === "production") {
|
|
55
|
+
return decodeParams(route, params);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const data = decodeParams(route, params);
|
|
59
|
+
if (spec !== undefined) {
|
|
60
|
+
emitter.observe(spec, { data, status: "success" });
|
|
61
|
+
}
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// Report BEFORE the throw reaches the error boundary (DT4). Only the
|
|
66
|
+
// decode-error class is observed — foreign errors are not URL facts
|
|
67
|
+
// the panel explains, matching safeDecode*'s taxonomy.
|
|
68
|
+
if (error instanceof ParamsDecodeError && spec !== undefined) {
|
|
69
|
+
emitter.observe(spec, { error, status: "error" });
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (process.env.NODE_ENV !== "production" && spec !== undefined) {
|
|
75
|
+
emitter.refresh(spec);
|
|
76
|
+
}
|
|
13
77
|
return useSelectedValue(value, options);
|
|
14
78
|
}
|
|
15
79
|
export function useSearch(route, options) {
|
|
16
80
|
const searchParams = useSearchParams();
|
|
17
|
-
const
|
|
81
|
+
const router = useRouter();
|
|
82
|
+
const pathname = usePathname();
|
|
83
|
+
const emitter = useDevtoolsEmitter();
|
|
84
|
+
const spec = process.env.NODE_ENV === "production"
|
|
85
|
+
? undefined
|
|
86
|
+
: {
|
|
87
|
+
hook: "app.useSearch",
|
|
88
|
+
kind: "search",
|
|
89
|
+
navigate: makeAppNavigate(router, pathname),
|
|
90
|
+
pathname,
|
|
91
|
+
route,
|
|
92
|
+
routerKind: "app",
|
|
93
|
+
wire: () => searchWireSnapshot(searchParams),
|
|
94
|
+
};
|
|
95
|
+
const result = useStableResult(route, searchParamsFingerprint(route, searchParams), () => {
|
|
96
|
+
const decoded = safeDecodeSearch(route, searchParams);
|
|
97
|
+
if (process.env.NODE_ENV !== "production" && spec !== undefined) {
|
|
98
|
+
emitter.observe(spec, decoded);
|
|
99
|
+
}
|
|
100
|
+
return decoded;
|
|
101
|
+
});
|
|
102
|
+
if (process.env.NODE_ENV !== "production" && spec !== undefined) {
|
|
103
|
+
emitter.refresh(spec);
|
|
104
|
+
}
|
|
18
105
|
return useSelectedResult(result, options);
|
|
19
106
|
}
|
|
20
107
|
export function useSearchOrThrow(route, options) {
|
|
21
108
|
const searchParams = useSearchParams();
|
|
109
|
+
const router = useRouter();
|
|
110
|
+
const pathname = usePathname();
|
|
111
|
+
const emitter = useDevtoolsEmitter();
|
|
112
|
+
const spec = process.env.NODE_ENV === "production"
|
|
113
|
+
? undefined
|
|
114
|
+
: {
|
|
115
|
+
hook: "app.useSearchOrThrow",
|
|
116
|
+
kind: "search",
|
|
117
|
+
navigate: makeAppNavigate(router, pathname),
|
|
118
|
+
pathname,
|
|
119
|
+
route,
|
|
120
|
+
routerKind: "app",
|
|
121
|
+
wire: () => searchWireSnapshot(searchParams),
|
|
122
|
+
};
|
|
22
123
|
const value = useStableResult(route, searchParamsFingerprint(route, searchParams),
|
|
23
124
|
// decodeSearch is keyed on SearchOutputOf (design-04 SS6) — the correct
|
|
24
125
|
// public type — but AnyAppRoute erases its SC to `any`, so for a still-
|
|
@@ -26,7 +127,32 @@ export function useSearchOrThrow(route, options) {
|
|
|
26
127
|
// on the value side while staying deferred on the annotation side. The
|
|
27
128
|
// cast bridges that inference gap to the SAME (correct) type, so a
|
|
28
129
|
// rawSearch route now infers its schema output here, not a garbage
|
|
29
|
-
// {~kind, ~schema} shape.
|
|
30
|
-
()
|
|
130
|
+
// {~kind, ~schema} shape. The cast appears in both branches below — the
|
|
131
|
+
// prod/dev split (and its duplicated decode call) is the price of
|
|
132
|
+
// literal-zero prod cost; the bundler keeps exactly one branch (DT6).
|
|
133
|
+
() => {
|
|
134
|
+
if (process.env.NODE_ENV === "production") {
|
|
135
|
+
return decodeSearch(route["~search"], searchParams);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const data = decodeSearch(route["~search"], searchParams);
|
|
139
|
+
if (spec !== undefined) {
|
|
140
|
+
emitter.observe(spec, { data, status: "success" });
|
|
141
|
+
}
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// Report BEFORE the throw reaches the error boundary (DT4); only
|
|
146
|
+
// the decode-error class is observed, matching safeDecode*'s
|
|
147
|
+
// taxonomy.
|
|
148
|
+
if (error instanceof SearchDecodeError && spec !== undefined) {
|
|
149
|
+
emitter.observe(spec, { error, status: "error" });
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
if (process.env.NODE_ENV !== "production" && spec !== undefined) {
|
|
155
|
+
emitter.refresh(spec);
|
|
156
|
+
}
|
|
31
157
|
return useSelectedValue(value, options);
|
|
32
158
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { AnyRoute, ParamsSource, RouterKind, SafeResult } from "paramour";
|
|
2
|
+
/**
|
|
3
|
+
* The devtools observation seam (design-12 DT5): a dependency-free global
|
|
4
|
+
* slot the hooks push decode observations into and the devtools panel
|
|
5
|
+
* (`@paramour-js/devtools`) reads out of. This module's JSDoc is the
|
|
6
|
+
* CONTRACT OF RECORD for the slot — the panel never imports runtime code
|
|
7
|
+
* from this package (its `./devtools-seam` exports entry is types-only, so
|
|
8
|
+
* a runtime import fails module resolution); it attaches to the same
|
|
9
|
+
* `globalThis` slot by key.
|
|
10
|
+
*
|
|
11
|
+
* - Slot key: `Symbol.for("paramour.devtools.seam")` — the realm-global
|
|
12
|
+
* symbol registry, the same cross-copy identity idiom as core's error
|
|
13
|
+
* brands (RL6): a second physical copy of this module (dual-package
|
|
14
|
+
* hazard, bundler duplication) mints the SAME symbol and lands on the
|
|
15
|
+
* same slot. Either side — hooks or panel — may create the slot; both
|
|
16
|
+
* sides are create-if-absent.
|
|
17
|
+
* - The slot is DATA-ONLY (no function fields): a function property would
|
|
18
|
+
* close over whichever module copy created the slot first, letting a
|
|
19
|
+
* duplicated or version-skewed copy pin stale behavior. With plain data,
|
|
20
|
+
* every copy of the emit/attach code operates on shared state and
|
|
21
|
+
* `version` is the only skew guard needed.
|
|
22
|
+
* - Protocol: subscribe = `listeners.add(fn)`; unsubscribe =
|
|
23
|
+
* `listeners.delete(fn)`; replay = synchronously read `buffer`, then
|
|
24
|
+
* `add` — same JS thread, so nothing can be emitted between the read and
|
|
25
|
+
* the add. Emitters push to `buffer` (FIFO-capped at
|
|
26
|
+
* {@link OBSERVATION_BUFFER_CAP}) and then invoke every listener.
|
|
27
|
+
* - Production (DT6): every emit call site sits behind
|
|
28
|
+
* `process.env.NODE_ENV !== "production"`, which Next's compilers
|
|
29
|
+
* constant-fold; with the package's `sideEffects: false` the then-dead
|
|
30
|
+
* import of this module is dropped entirely. The emitted JS here imports
|
|
31
|
+
* NOTHING (the one `paramour` import is type-only) — load-bearing for
|
|
32
|
+
* that erasure.
|
|
33
|
+
*/
|
|
34
|
+
/** The `Symbol.for("paramour.devtools.seam")` slot shape — the DT5 contract. */
|
|
35
|
+
export interface ParamourDevtoolsSeam {
|
|
36
|
+
/** Capped FIFO; oldest dropped past the cap. Replay = read it. */
|
|
37
|
+
readonly buffer: ParamourObservation[];
|
|
38
|
+
/** Subscribe = add; unsubscribe = delete. Invoked synchronously per emit. */
|
|
39
|
+
readonly listeners: Set<(observation: ParamourObservation) => void>;
|
|
40
|
+
/**
|
|
41
|
+
* Bumped only when an EXISTING field's semantics change; additive fields
|
|
42
|
+
* never bump it.
|
|
43
|
+
*/
|
|
44
|
+
readonly version: 1;
|
|
45
|
+
}
|
|
46
|
+
/** Discriminant naming which hook reported (design-12 DT4). */
|
|
47
|
+
export type ParamourHookId = "app.useRouteParams" | "app.useRouteParamsOrThrow" | "app.useSearch" | "app.useSearchOrThrow" | "pages.useRouteParams" | "pages.useSearch";
|
|
48
|
+
/**
|
|
49
|
+
* Navigation capability captured from the EMITTING hook's router (design-12
|
|
50
|
+
* DT8): the panel commits URL edits through this, so it never guesses which
|
|
51
|
+
* router is live and never imports Next. The panel passes ONLY the
|
|
52
|
+
* serialized search string (`""` or `"?…"`); the hook resolves it against
|
|
53
|
+
* its OWN current pathname — `usePathname()` (App) / `asPath`'s path part
|
|
54
|
+
* (Pages), both basePath-/locale-relative, which is what `replace()`
|
|
55
|
+
* expects back. The panel reading `window.location.pathname` instead would
|
|
56
|
+
* double a configured basePath through `router.replace`. `replace`
|
|
57
|
+
* semantics — the panel's commit-to-push editing is an experiment loop, and
|
|
58
|
+
* history entries per experiment would make the back button a slog;
|
|
59
|
+
* extending to an options bag later is additive.
|
|
60
|
+
*/
|
|
61
|
+
export type ParamourNavigate = (search: string) => void;
|
|
62
|
+
/**
|
|
63
|
+
* One hook decode, reported on decode CHANGE (design-12 DT4) — and
|
|
64
|
+
* re-reported when the hook's resolution base moves under an unchanged
|
|
65
|
+
* decode (a layout surviving `/product/1?q=a` → `/product/2?q=a`), so the
|
|
66
|
+
* captured `navigate`/`pathname` never go stale while the hook is mounted.
|
|
67
|
+
*/
|
|
68
|
+
export type ParamourObservation = ParamourParamsObservation | ParamourSearchObservation;
|
|
69
|
+
/**
|
|
70
|
+
* Pre-`select` decode result (design-12 DT12): the hook's full `SafeResult`
|
|
71
|
+
* — the error arm carries the LIVE `ParamsDecodeError`/`SearchDecodeError`
|
|
72
|
+
* with its `issues` — never the user's `select` projection. `pending` is
|
|
73
|
+
* the Pages-only third state (DT11). Generic-erased on purpose: the panel
|
|
74
|
+
* treats `data` structurally.
|
|
75
|
+
*/
|
|
76
|
+
export type ParamourObservationResult = SafeResult<unknown> | {
|
|
77
|
+
readonly status: "pending";
|
|
78
|
+
};
|
|
79
|
+
/** Params decode: wire is a decode-time shallow copy of the source record. */
|
|
80
|
+
export interface ParamourParamsObservation extends ParamourObservationBase {
|
|
81
|
+
readonly kind: "params";
|
|
82
|
+
readonly wire: Readonly<ParamsSource>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Search decode: wire is decode-time `[key, value]` pairs in wire order —
|
|
86
|
+
* order is load-bearing for repeated keys (P5/S5), and pairs round-trip
|
|
87
|
+
* losslessly into the panel's raw-wire editing (DT8).
|
|
88
|
+
*/
|
|
89
|
+
export interface ParamourSearchObservation extends ParamourObservationBase {
|
|
90
|
+
readonly kind: "search";
|
|
91
|
+
readonly wire: ParamourSearchWire;
|
|
92
|
+
}
|
|
93
|
+
export type ParamourSearchWire = readonly (readonly [string, string])[];
|
|
94
|
+
interface ParamourObservationBase {
|
|
95
|
+
readonly hook: ParamourHookId;
|
|
96
|
+
readonly navigate: ParamourNavigate;
|
|
97
|
+
/**
|
|
98
|
+
* The emitting hook's OWN resolution base at decode time —
|
|
99
|
+
* `usePathname()` (App) / `asPath`'s path part (Pages), both
|
|
100
|
+
* basePath-/locale-relative like {@link ParamourNavigate}'s. The panel
|
|
101
|
+
* keys "is this session the page on screen?" on it (suffix-matched
|
|
102
|
+
* against `window.location.pathname`, which DOES carry the prefix), so it
|
|
103
|
+
* never has to reverse-engineer a configured basePath. Additive field —
|
|
104
|
+
* no `version` bump.
|
|
105
|
+
*/
|
|
106
|
+
readonly pathname: string;
|
|
107
|
+
readonly result: ParamourObservationResult;
|
|
108
|
+
/**
|
|
109
|
+
* The LIVE route object (DT5: same JS context, no serialization) — the
|
|
110
|
+
* panel calls `describeRoute`, the route's own codecs, and
|
|
111
|
+
* `buildSearchString` on it directly.
|
|
112
|
+
*/
|
|
113
|
+
readonly route: AnyRoute;
|
|
114
|
+
readonly routerKind: RouterKind;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 128: replay only needs the pre-panel-mount window. One observation per
|
|
118
|
+
* decode CHANGE per hook (DT4) means even a long pre-open session is dozens
|
|
119
|
+
* of entries, not thousands; the panel keys on route, so depth beyond
|
|
120
|
+
* "every route seen recently" adds nothing — the cap mostly bounds how many
|
|
121
|
+
* live route/result references the buffer retains.
|
|
122
|
+
*/
|
|
123
|
+
export declare const OBSERVATION_BUFFER_CAP = 128;
|
|
124
|
+
/**
|
|
125
|
+
* Pushes one observation and notifies listeners. The internal production
|
|
126
|
+
* early-return is belt-and-suspenders under DT6 (every call site is ALSO
|
|
127
|
+
* guarded, which is what the bundler erases); it makes the guard directly
|
|
128
|
+
* unit-testable and keeps a future unguarded call site failing safe.
|
|
129
|
+
*/
|
|
130
|
+
export declare function emitObservation(observation: ParamourObservation): void;
|
|
131
|
+
/**
|
|
132
|
+
* The slot, created on first touch by whichever side (hooks or panel) runs
|
|
133
|
+
* first.
|
|
134
|
+
*/
|
|
135
|
+
export declare function getParamourSeam(): ParamourDevtoolsSeam;
|
|
136
|
+
/**
|
|
137
|
+
* Pages `query` record → wire pairs; `string[]` values expand to repeated
|
|
138
|
+
* keys in array order, `undefined` values are wire absence and are skipped.
|
|
139
|
+
*/
|
|
140
|
+
export declare function recordWireSnapshot(source: ParamsSource): ParamourSearchWire;
|
|
141
|
+
/**
|
|
142
|
+
* Decode-time freeze of the (live, mutable) `URLSearchParams` into wire
|
|
143
|
+
* pairs: the observation outlives the render in the ring buffer, so it must
|
|
144
|
+
* capture what the DECODE saw, not a live view.
|
|
145
|
+
*/
|
|
146
|
+
export declare function searchWireSnapshot(source: URLSearchParams): ParamourSearchWire;
|
|
147
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 128: replay only needs the pre-panel-mount window. One observation per
|
|
3
|
+
* decode CHANGE per hook (DT4) means even a long pre-open session is dozens
|
|
4
|
+
* of entries, not thousands; the panel keys on route, so depth beyond
|
|
5
|
+
* "every route seen recently" adds nothing — the cap mostly bounds how many
|
|
6
|
+
* live route/result references the buffer retains.
|
|
7
|
+
*/
|
|
8
|
+
export const OBSERVATION_BUFFER_CAP = 128;
|
|
9
|
+
const SEAM_KEY = Symbol.for("paramour.devtools.seam");
|
|
10
|
+
const globalSlots = globalThis;
|
|
11
|
+
/**
|
|
12
|
+
* Pushes one observation and notifies listeners. The internal production
|
|
13
|
+
* early-return is belt-and-suspenders under DT6 (every call site is ALSO
|
|
14
|
+
* guarded, which is what the bundler erases); it makes the guard directly
|
|
15
|
+
* unit-testable and keeps a future unguarded call site failing safe.
|
|
16
|
+
*/
|
|
17
|
+
export function emitObservation(observation) {
|
|
18
|
+
if (process.env.NODE_ENV === "production")
|
|
19
|
+
return;
|
|
20
|
+
const seam = getParamourSeam();
|
|
21
|
+
seam.buffer.push(observation);
|
|
22
|
+
if (seam.buffer.length > OBSERVATION_BUFFER_CAP)
|
|
23
|
+
seam.buffer.shift();
|
|
24
|
+
for (const listener of seam.listeners) {
|
|
25
|
+
try {
|
|
26
|
+
listener(observation);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// A panel bug must never break app render — emit runs render-phase.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The slot, created on first touch by whichever side (hooks or panel) runs
|
|
35
|
+
* first.
|
|
36
|
+
*/
|
|
37
|
+
export function getParamourSeam() {
|
|
38
|
+
const existing = globalSlots[SEAM_KEY];
|
|
39
|
+
if (existing !== undefined)
|
|
40
|
+
return existing;
|
|
41
|
+
const created = {
|
|
42
|
+
buffer: [],
|
|
43
|
+
listeners: new Set(),
|
|
44
|
+
version: 1,
|
|
45
|
+
};
|
|
46
|
+
globalSlots[SEAM_KEY] = created;
|
|
47
|
+
return created;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Pages `query` record → wire pairs; `string[]` values expand to repeated
|
|
51
|
+
* keys in array order, `undefined` values are wire absence and are skipped.
|
|
52
|
+
*/
|
|
53
|
+
export function recordWireSnapshot(source) {
|
|
54
|
+
const pairs = [];
|
|
55
|
+
for (const [key, value] of Object.entries(source)) {
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
for (const element of value)
|
|
60
|
+
pairs.push([key, element]);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
pairs.push([key, value]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return pairs;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decode-time freeze of the (live, mutable) `URLSearchParams` into wire
|
|
70
|
+
* pairs: the observation outlives the render in the ring buffer, so it must
|
|
71
|
+
* capture what the DECODE saw, not a live view.
|
|
72
|
+
*/
|
|
73
|
+
export function searchWireSnapshot(source) {
|
|
74
|
+
const pairs = [];
|
|
75
|
+
for (const [key, value] of source)
|
|
76
|
+
pairs.push([key, value]);
|
|
77
|
+
return pairs;
|
|
78
|
+
}
|
package/dist/list/render.d.ts
CHANGED
|
@@ -27,8 +27,9 @@ export interface ListReport {
|
|
|
27
27
|
*/
|
|
28
28
|
export declare function buildListJson(report: ListReport): unknown;
|
|
29
29
|
/**
|
|
30
|
-
* `integer`, `enum(a, b)`, `string[]`, with annotations in
|
|
31
|
-
* presence, default, catch
|
|
30
|
+
* `integer`, `enum(a, b)`, `string[]`, `csv<integer>`, with annotations in
|
|
31
|
+
* fixed order: presence, default, catch — core's shared walk in its verbose
|
|
32
|
+
* skin, so the CLI and the devtools panel can never drift on the field set.
|
|
32
33
|
*/
|
|
33
34
|
export declare function formatCodec(description: CodecDescription): string;
|
|
34
35
|
/** Human report; one string per output line. */
|
package/dist/list/render.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatCodecDescription } from "paramour";
|
|
1
2
|
/**
|
|
2
3
|
* `--json` payload. Keys are alphabetical; a route with no definition
|
|
3
4
|
* carries `definition: null` rather than an absent member — friendlier to
|
|
@@ -13,26 +14,12 @@ export function buildListJson(report) {
|
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
|
-
* `integer`, `enum(a, b)`, `string[]`, with annotations in
|
|
17
|
-
* presence, default, catch
|
|
17
|
+
* `integer`, `enum(a, b)`, `string[]`, `csv<integer>`, with annotations in
|
|
18
|
+
* fixed order: presence, default, catch — core's shared walk in its verbose
|
|
19
|
+
* skin, so the CLI and the devtools panel can never drift on the field set.
|
|
18
20
|
*/
|
|
19
21
|
export function formatCodec(description) {
|
|
20
|
-
|
|
21
|
-
? description.kind
|
|
22
|
-
: `enum(${description.enumMembers.join(", ")})`;
|
|
23
|
-
if (description.arity === "many")
|
|
24
|
-
base += "[]";
|
|
25
|
-
const notes = [];
|
|
26
|
-
if (description.presence === "optional")
|
|
27
|
-
notes.push("(optional)");
|
|
28
|
-
if (description.defaultValue !== undefined) {
|
|
29
|
-
notes.push(description.defaultValue.kind === "value"
|
|
30
|
-
? `(default: ${description.defaultValue.wire})`
|
|
31
|
-
: "(default: factory)");
|
|
32
|
-
}
|
|
33
|
-
if (description.caught)
|
|
34
|
-
notes.push("(catch)");
|
|
35
|
-
return [base, ...notes].join(" ");
|
|
22
|
+
return formatCodecDescription(description, "verbose");
|
|
36
23
|
}
|
|
37
24
|
/** Human report; one string per output line. */
|
|
38
25
|
export function renderListReport(report) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { AnyRoute, ParamsSource, RouterKind } from "paramour";
|
|
2
|
+
import type { ParamourHookId, ParamourNavigate, ParamourObservationResult, ParamourSearchWire } from "./devtools-seam.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shared devtools seam wiring for the six read hooks (design-12 DT4/DT8):
|
|
5
|
+
* the navigate builders (one per router flavor) and the per-hook emitter
|
|
6
|
+
* that owns WHEN an observation goes out. Deliberately NO `"use client"`
|
|
7
|
+
* directive — app.ts (which carries one) and pages.ts (which must not,
|
|
8
|
+
* PR2) both import from here, like select.ts.
|
|
9
|
+
*
|
|
10
|
+
* Emission policy: the hooks call {@link DevtoolsEmitter.observe} from
|
|
11
|
+
* inside the `useStableResult` compute — the SEL4 fingerprint cache miss IS
|
|
12
|
+
* the decode-change dedup (DT4), and only render-phase can report the
|
|
13
|
+
* OrThrow hooks' error observation before the rethrow. `observe` alone
|
|
14
|
+
* would leave one staleness hole: a component that survives a navigation
|
|
15
|
+
* whose decode is unchanged (`/product/1?q=a` → `/product/2?q=a` in a
|
|
16
|
+
* layout) never recomputes, so its last-emitted `navigate` stays bound to
|
|
17
|
+
* the OLD pathname — committing a panel edit through it would silently
|
|
18
|
+
* navigate back to the old resource. {@link DevtoolsEmitter.refresh},
|
|
19
|
+
* called render-phase after the stable result returns, closes it: when the
|
|
20
|
+
* resolution base (or a HMR-reminted route) moved under a cached decode, it
|
|
21
|
+
* re-emits the CACHED result with the fresh spec — decode stability (SEL4)
|
|
22
|
+
* is untouched, only the seam payload is renewed.
|
|
23
|
+
*
|
|
24
|
+
* Production erasure (DT6): every call site keeps a literal
|
|
25
|
+
* `process.env.NODE_ENV` guard the bundler constant-folds, and both emitter
|
|
26
|
+
* methods early-return behind the same literal guard, so the
|
|
27
|
+
* `emitObservation` import above is dead in a production bundle and drops
|
|
28
|
+
* with `sideEffects: false`. The unconditional `useRef` here is the same
|
|
29
|
+
* bargain the hooks already make for `useRouter`/`usePathname` — hook order
|
|
30
|
+
* must not differ between dev and prod bundles.
|
|
31
|
+
*/
|
|
32
|
+
/** Per-hook emitter; see the module doc for the observe/refresh split. */
|
|
33
|
+
export interface DevtoolsEmitter {
|
|
34
|
+
readonly observe: (spec: ObservationSpec, result: ParamourObservationResult) => void;
|
|
35
|
+
readonly refresh: (spec: ObservationSpec) => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Everything one emission needs, rebuilt per render so `navigate` and
|
|
39
|
+
* `wire` always close over the CURRENT render's router/pathname/source.
|
|
40
|
+
* Call sites construct it behind their literal dev guard (undefined in
|
|
41
|
+
* prod), keeping prod allocation at zero.
|
|
42
|
+
*/
|
|
43
|
+
export interface ObservationSpec {
|
|
44
|
+
readonly hook: ParamourHookId;
|
|
45
|
+
readonly kind: "params" | "search";
|
|
46
|
+
readonly navigate: ParamourNavigate;
|
|
47
|
+
readonly pathname: string;
|
|
48
|
+
readonly route: AnyRoute;
|
|
49
|
+
readonly routerKind: RouterKind;
|
|
50
|
+
/** Decode-time wire snapshot, taken fresh at each emission. */
|
|
51
|
+
readonly wire: () => ParamourSearchWire | Readonly<ParamsSource>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* App-flavor navigate capability (DT8): `next/navigation`'s `replace`
|
|
55
|
+
* returns void and resolves the basePath-/locale-relative join itself.
|
|
56
|
+
*/
|
|
57
|
+
export declare function makeAppNavigate(router: {
|
|
58
|
+
replace: (href: string) => void;
|
|
59
|
+
}, pathname: string): ParamourNavigate;
|
|
60
|
+
/**
|
|
61
|
+
* Pages-flavor navigate capability (DT8): `next/router`'s `replace` returns
|
|
62
|
+
* a promise that REJECTS on routine navigation aborts (rapid re-commits
|
|
63
|
+
* from the panel), marked with next's `cancelled` discriminant — those must
|
|
64
|
+
* not surface as unhandled rejections. Anything else is a real failure
|
|
65
|
+
* (render error, route-info error) silently discarding the user's edit, so
|
|
66
|
+
* it is reported to the console instead of swallowed.
|
|
67
|
+
*/
|
|
68
|
+
export declare function makePagesNavigate(router: {
|
|
69
|
+
replace: (url: string) => Promise<boolean>;
|
|
70
|
+
}, pathname: string): ParamourNavigate;
|
|
71
|
+
export declare function useDevtoolsEmitter(): DevtoolsEmitter;
|
package/dist/observe.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { emitObservation } from "./devtools-seam.js";
|
|
3
|
+
/**
|
|
4
|
+
* App-flavor navigate capability (DT8): `next/navigation`'s `replace`
|
|
5
|
+
* returns void and resolves the basePath-/locale-relative join itself.
|
|
6
|
+
*/
|
|
7
|
+
export function makeAppNavigate(router, pathname) {
|
|
8
|
+
return (search) => {
|
|
9
|
+
router.replace(`${pathname}${search}${window.location.hash}`);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Pages-flavor navigate capability (DT8): `next/router`'s `replace` returns
|
|
14
|
+
* a promise that REJECTS on routine navigation aborts (rapid re-commits
|
|
15
|
+
* from the panel), marked with next's `cancelled` discriminant — those must
|
|
16
|
+
* not surface as unhandled rejections. Anything else is a real failure
|
|
17
|
+
* (render error, route-info error) silently discarding the user's edit, so
|
|
18
|
+
* it is reported to the console instead of swallowed.
|
|
19
|
+
*/
|
|
20
|
+
export function makePagesNavigate(router, pathname) {
|
|
21
|
+
return (search) => {
|
|
22
|
+
void router
|
|
23
|
+
.replace(`${pathname}${search}${window.location.hash}`)
|
|
24
|
+
.catch((error) => {
|
|
25
|
+
if (!isCancelledNavigation(error))
|
|
26
|
+
console.error(error);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function useDevtoolsEmitter() {
|
|
31
|
+
const ref = useRef(null);
|
|
32
|
+
ref.current ??= createEmitter();
|
|
33
|
+
return ref.current;
|
|
34
|
+
}
|
|
35
|
+
function createEmitter() {
|
|
36
|
+
let last = null;
|
|
37
|
+
const observe = (spec, result) => {
|
|
38
|
+
if (process.env.NODE_ENV === "production")
|
|
39
|
+
return;
|
|
40
|
+
last = { pathname: spec.pathname, result, route: spec.route };
|
|
41
|
+
// `kind` and `wire` are pairwise-correct by construction at the six
|
|
42
|
+
// call sites; the correlated union is beyond TS narrowing, hence the
|
|
43
|
+
// cast.
|
|
44
|
+
emitObservation({
|
|
45
|
+
hook: spec.hook,
|
|
46
|
+
kind: spec.kind,
|
|
47
|
+
navigate: spec.navigate,
|
|
48
|
+
pathname: spec.pathname,
|
|
49
|
+
result,
|
|
50
|
+
route: spec.route,
|
|
51
|
+
routerKind: spec.routerKind,
|
|
52
|
+
wire: spec.wire(),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
observe,
|
|
57
|
+
refresh: (spec) => {
|
|
58
|
+
if (process.env.NODE_ENV === "production")
|
|
59
|
+
return;
|
|
60
|
+
if (last === null)
|
|
61
|
+
return;
|
|
62
|
+
if (last.pathname === spec.pathname && last.route === spec.route)
|
|
63
|
+
return;
|
|
64
|
+
observe(spec, last.result);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Next's pages router marks genuine navigation aborts with `cancelled`. */
|
|
69
|
+
function isCancelledNavigation(error) {
|
|
70
|
+
return (typeof error === "object" &&
|
|
71
|
+
error !== null &&
|
|
72
|
+
error.cancelled === true);
|
|
73
|
+
}
|
package/dist/pages.d.ts
CHANGED
|
@@ -25,6 +25,15 @@ export type { SelectOptions } from "./select.js";
|
|
|
25
25
|
* `query` (+ `isReady`), then an optional `{ select }` projection with
|
|
26
26
|
* result-equality checking — the `pending` arm passes through the selector
|
|
27
27
|
* untouched (SEL2), and `PENDING` itself is one referentially stable object.
|
|
28
|
+
*
|
|
29
|
+
* Devtools instrumentation (design-12): each hook reports through the
|
|
30
|
+
* shared emitter in observe.ts — `observe` from inside the `useStableResult`
|
|
31
|
+
* compute callback (DT4 — the fingerprint cache miss IS the decode-change
|
|
32
|
+
* dedup; see app.ts's fuller account), `refresh` after it for pathname
|
|
33
|
+
* moves under an unchanged decode (DT8). The `pending` arm emits as a
|
|
34
|
+
* first-class observation (DT11), keyed by `PENDING_FINGERPRINT` so the
|
|
35
|
+
* pre-`isReady` render reports exactly once. Every emit sits behind
|
|
36
|
+
* `process.env.NODE_ENV !== "production"` (DT6).
|
|
28
37
|
*/
|
|
29
38
|
/**
|
|
30
39
|
* Three-state result for the pages hooks (PR5): core's `SafeResult` plus a
|
package/dist/pages.js
CHANGED
|
@@ -1,30 +1,83 @@
|
|
|
1
1
|
import { useRouter } from "next/router";
|
|
2
2
|
import { ParamourError, safeDecodeParams, safeDecodeSearch, } from "paramour";
|
|
3
|
+
import { recordWireSnapshot } from "./devtools-seam.js";
|
|
4
|
+
import { makePagesNavigate, useDevtoolsEmitter, } from "./observe.js";
|
|
3
5
|
import { paramsFingerprint, PENDING_FINGERPRINT, queryFingerprint, useSelectedResult, useStableResult, } from "./select.js";
|
|
4
6
|
/** Referentially stable across every pending render. */
|
|
5
7
|
const PENDING = { status: "pending" };
|
|
6
8
|
export function useRouteParams(route, options) {
|
|
7
|
-
const
|
|
9
|
+
const router = usePagesRouter();
|
|
10
|
+
const { isReady, query } = router;
|
|
11
|
+
const pathname = asPathPathname(router.asPath);
|
|
12
|
+
const emitter = useDevtoolsEmitter();
|
|
13
|
+
const spec = process.env.NODE_ENV === "production"
|
|
14
|
+
? undefined
|
|
15
|
+
: {
|
|
16
|
+
hook: "pages.useRouteParams",
|
|
17
|
+
kind: "params",
|
|
18
|
+
navigate: makePagesNavigate(router, pathname),
|
|
19
|
+
pathname,
|
|
20
|
+
route,
|
|
21
|
+
routerKind: "pages",
|
|
22
|
+
wire: () => ({ ...query }),
|
|
23
|
+
};
|
|
8
24
|
const result = useStableResult(route, isReady ? paramsFingerprint(route, query) : PENDING_FINGERPRINT, () => {
|
|
9
|
-
if (!isReady)
|
|
10
|
-
return PENDING;
|
|
11
25
|
// The merged query is a legal params source as-is: decodeParams reads
|
|
12
26
|
// only the route's own segment names, never unknown keys. R5: next/router
|
|
13
27
|
// has already percent-decoded `query`, so skip core's decode to avoid a
|
|
14
28
|
// double-decode (`/product/a%2520b` → `"a%20b"` must survive as-is).
|
|
15
|
-
|
|
29
|
+
const decoded = isReady
|
|
30
|
+
? safeDecodeParams(route, query, { percentDecode: false })
|
|
31
|
+
: PENDING;
|
|
32
|
+
if (process.env.NODE_ENV !== "production" && spec !== undefined) {
|
|
33
|
+
emitter.observe(spec, decoded);
|
|
34
|
+
}
|
|
35
|
+
return decoded;
|
|
16
36
|
});
|
|
37
|
+
if (process.env.NODE_ENV !== "production" && spec !== undefined) {
|
|
38
|
+
emitter.refresh(spec);
|
|
39
|
+
}
|
|
17
40
|
return useSelectedResult(result, options);
|
|
18
41
|
}
|
|
19
42
|
export function useSearch(route, options) {
|
|
20
|
-
const
|
|
43
|
+
const router = usePagesRouter();
|
|
44
|
+
const { isReady, query } = router;
|
|
45
|
+
const pathname = asPathPathname(router.asPath);
|
|
46
|
+
const emitter = useDevtoolsEmitter();
|
|
47
|
+
const spec = process.env.NODE_ENV === "production"
|
|
48
|
+
? undefined
|
|
49
|
+
: {
|
|
50
|
+
hook: "pages.useSearch",
|
|
51
|
+
kind: "search",
|
|
52
|
+
navigate: makePagesNavigate(router, pathname),
|
|
53
|
+
pathname,
|
|
54
|
+
route,
|
|
55
|
+
routerKind: "pages",
|
|
56
|
+
// Lazy, so the path-param subtraction only runs when an emission
|
|
57
|
+
// actually snapshots the wire.
|
|
58
|
+
wire: () => recordWireSnapshot(omitPathParams(query, route)),
|
|
59
|
+
};
|
|
21
60
|
const result = useStableResult(route, isReady ? queryFingerprint(route, query) : PENDING_FINGERPRINT, () => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
61
|
+
const source = omitPathParams(query, route);
|
|
62
|
+
const decoded = isReady ? safeDecodeSearch(route, source) : PENDING;
|
|
63
|
+
if (process.env.NODE_ENV !== "production" && spec !== undefined) {
|
|
64
|
+
emitter.observe(spec, decoded);
|
|
65
|
+
}
|
|
66
|
+
return decoded;
|
|
25
67
|
});
|
|
68
|
+
if (process.env.NODE_ENV !== "production" && spec !== undefined) {
|
|
69
|
+
emitter.refresh(spec);
|
|
70
|
+
}
|
|
26
71
|
return useSelectedResult(result, options);
|
|
27
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* `asPath`'s path part: basePath-/locale-relative — exactly what
|
|
75
|
+
* `replace()` expects back — so the panel's search-only string (DT8)
|
|
76
|
+
* resolves without doubling a configured basePath.
|
|
77
|
+
*/
|
|
78
|
+
function asPathPathname(asPath) {
|
|
79
|
+
return asPath.split(/[#?]/)[0] ?? "/";
|
|
80
|
+
}
|
|
28
81
|
/**
|
|
29
82
|
* `query` minus the route's own path-param names (PR5) — the client twin of
|
|
30
83
|
* `parseContext`'s server-side subtraction (core route.ts, PR10). Entries →
|
package/dist/select.js
CHANGED
|
@@ -116,15 +116,37 @@ export function useStableResult(route, fingerprint, compute) {
|
|
|
116
116
|
if (cached !== null &&
|
|
117
117
|
cached.route === route &&
|
|
118
118
|
cached.fingerprint === fingerprint) {
|
|
119
|
-
|
|
119
|
+
if (cached.outcome.status === "thrown")
|
|
120
|
+
throw cached.outcome.thrown;
|
|
121
|
+
return cached.outcome.value;
|
|
120
122
|
}
|
|
121
|
-
// Cleared BEFORE computing (SEL8):
|
|
122
|
-
//
|
|
123
|
-
//
|
|
123
|
+
// Cleared BEFORE computing (SEL8): no half-computed state may survive a
|
|
124
|
+
// throw, and a NEW fingerprint always recomputes — an error boundary
|
|
125
|
+
// reset after the URL is fixed can never be served a stale entry.
|
|
124
126
|
cache.current = null;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
try {
|
|
128
|
+
const value = compute();
|
|
129
|
+
cache.current = {
|
|
130
|
+
fingerprint,
|
|
131
|
+
outcome: { status: "value", value },
|
|
132
|
+
route,
|
|
133
|
+
};
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// Cache the throw under ITS fingerprint (see StableOutcome): update
|
|
138
|
+
// renders share this ref with the committed fiber, so re-render
|
|
139
|
+
// attempts while the URL stays invalid rethrow instead of re-decoding
|
|
140
|
+
// (and re-emitting). A throwing MOUNT discards its work-in-progress
|
|
141
|
+
// hooks, so replayed mounts still recompute — per-instance semantics,
|
|
142
|
+
// same as the success arm's.
|
|
143
|
+
cache.current = {
|
|
144
|
+
fingerprint,
|
|
145
|
+
outcome: { status: "thrown", thrown: error },
|
|
146
|
+
route,
|
|
147
|
+
};
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
128
150
|
}
|
|
129
151
|
/**
|
|
130
152
|
* Declared search keys of a route's `~search` slot, or `null` for a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paramour-js/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
"types": "./dist/app.d.ts",
|
|
12
12
|
"default": "./dist/app.js"
|
|
13
13
|
},
|
|
14
|
+
"./devtools-seam": {
|
|
15
|
+
"types": "./dist/devtools-seam.d.ts"
|
|
16
|
+
},
|
|
14
17
|
"./pages": {
|
|
15
18
|
"types": "./dist/pages.d.ts",
|
|
16
19
|
"default": "./dist/pages.js"
|
|
17
20
|
}
|
|
18
21
|
},
|
|
22
|
+
"sideEffects": false,
|
|
19
23
|
"bin": {
|
|
20
24
|
"paramour": "./bin/paramour.js"
|
|
21
25
|
},
|
|
@@ -26,7 +30,7 @@
|
|
|
26
30
|
"jiti": "^2.7.0",
|
|
27
31
|
"magicast": "^0.3.5",
|
|
28
32
|
"tinyglobby": "^0.2.15",
|
|
29
|
-
"paramour": "0.
|
|
33
|
+
"paramour": "0.3.0"
|
|
30
34
|
},
|
|
31
35
|
"peerDependencies": {
|
|
32
36
|
"next": ">=15",
|