@paramour-js/next 0.4.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/README.md +1 -1
- package/dist/app.d.ts +48 -49
- package/dist/app.js +11 -12
- package/dist/cli-args.d.ts +4 -4
- package/dist/cli-args.js +4 -4
- package/dist/cli-inputs.d.ts +6 -7
- package/dist/cli-inputs.js +6 -7
- package/dist/cli.js +1 -1
- package/dist/collisions.d.ts +8 -8
- package/dist/collisions.js +9 -9
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.js +12 -7
- package/dist/commands/generate.d.ts +55 -7
- package/dist/commands/generate.js +29 -22
- package/dist/commands/init.d.ts +40 -0
- package/dist/commands/init.js +111 -19
- package/dist/commands/list.d.ts +21 -0
- package/dist/commands/list.js +12 -7
- package/dist/commands/skills.d.ts +45 -0
- package/dist/commands/skills.js +222 -0
- package/dist/config.d.ts +17 -10
- package/dist/config.js +17 -4
- package/dist/devtools-seam.d.ts +19 -19
- package/dist/devtools-seam.js +3 -3
- package/dist/doctor/checks.js +7 -3
- package/dist/emit.d.ts +12 -12
- package/dist/emit.js +13 -13
- package/dist/generate.d.ts +14 -14
- package/dist/generate.js +11 -11
- package/dist/init/agents-md.d.ts +32 -0
- package/dist/init/agents-md.js +78 -0
- package/dist/init/scaffold.js +54 -0
- package/dist/list/discover-route-defs.d.ts +4 -4
- package/dist/list/discover-route-defs.js +4 -4
- package/dist/lock.d.ts +8 -9
- package/dist/lock.js +11 -12
- package/dist/navigation-adapter.d.ts +17 -18
- package/dist/navigation-adapter.js +4 -4
- package/dist/observe.d.ts +14 -14
- package/dist/observe.js +4 -4
- package/dist/pages.d.ts +30 -31
- package/dist/pages.js +10 -10
- package/dist/run-cli.d.ts +3 -1
- package/dist/run-cli.js +7 -3
- package/dist/scan-app.d.ts +10 -10
- package/dist/scan-app.js +30 -30
- package/dist/scan-pages.d.ts +6 -6
- package/dist/scan-pages.js +28 -26
- package/dist/scan.d.ts +13 -10
- package/dist/scan.js +7 -7
- package/dist/select.d.ts +40 -40
- package/dist/select.js +30 -30
- package/dist/skills/doctor.d.ts +11 -0
- package/dist/skills/doctor.js +71 -0
- package/dist/skills/manifest.d.ts +41 -0
- package/dist/skills/manifest.js +96 -0
- package/dist/skills/packaged.d.ts +21 -0
- package/dist/skills/packaged.js +32 -0
- package/dist/skills/sync.d.ts +84 -0
- package/dist/skills/sync.js +136 -0
- package/dist/skills/targets.d.ts +29 -0
- package/dist/skills/targets.js +73 -0
- package/dist/testing.d.ts +28 -32
- package/dist/testing.js +15 -15
- package/dist/watch.d.ts +12 -12
- package/dist/watch.js +13 -13
- package/dist/with-typed-routes.d.ts +11 -10
- package/dist/with-typed-routes.js +42 -39
- package/package.json +4 -3
- package/skills/paramour/SKILL.md +43 -0
- package/skills/paramour/references/authoring.md +113 -0
- package/skills/paramour/references/migration.md +141 -0
- package/skills/paramour/references/reference.md +96 -0
- package/skills/paramour/references/setup.md +139 -0
package/dist/lock.d.ts
CHANGED
|
@@ -12,18 +12,17 @@ export interface AcquireLockResult {
|
|
|
12
12
|
release?: () => void;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Cross-process single-writer guard
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* points, not here.
|
|
15
|
+
* Cross-process single-writer guard: a best-effort pidfile lock. On startup:
|
|
16
|
+
* read lock → liveness-probe the owner → decline if alive, (over)write and
|
|
17
|
+
* acquire if dead or absent. Deliberately best-effort, not correct — the
|
|
18
|
+
* deterministic write-if-changed output means two live watchers produce
|
|
19
|
+
* identical bytes; imperfect locking costs a log line, not corruption. Hence
|
|
20
|
+
* no flock semantics, atomic-rename dances, or PID-reuse paranoia. The
|
|
21
|
+
* in-process singleton guard lives at the composition points, not here.
|
|
23
22
|
*/
|
|
24
23
|
export declare function acquireWatcherLock(lockPath: string): AcquireLockResult;
|
|
25
24
|
/**
|
|
26
|
-
* The one canonical pidfile location
|
|
25
|
+
* The one canonical pidfile location: CLI-vs-wrapper dedupe only works
|
|
27
26
|
* because both paths compute the lock from the same project root.
|
|
28
27
|
*/
|
|
29
28
|
export declare function watcherLockPath(projectRoot: string): string;
|
package/dist/lock.js
CHANGED
|
@@ -3,14 +3,13 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
/** Strict anchored PID parse — anything else is a stale/corrupt lock. */
|
|
4
4
|
const PID_RE = /^\d+$/;
|
|
5
5
|
/**
|
|
6
|
-
* Cross-process single-writer guard
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* points, not here.
|
|
6
|
+
* Cross-process single-writer guard: a best-effort pidfile lock. On startup:
|
|
7
|
+
* read lock → liveness-probe the owner → decline if alive, (over)write and
|
|
8
|
+
* acquire if dead or absent. Deliberately best-effort, not correct — the
|
|
9
|
+
* deterministic write-if-changed output means two live watchers produce
|
|
10
|
+
* identical bytes; imperfect locking costs a log line, not corruption. Hence
|
|
11
|
+
* no flock semantics, atomic-rename dances, or PID-reuse paranoia. The
|
|
12
|
+
* in-process singleton guard lives at the composition points, not here.
|
|
14
13
|
*/
|
|
15
14
|
export function acquireWatcherLock(lockPath) {
|
|
16
15
|
const ownerPid = readOwnerPid(lockPath);
|
|
@@ -35,8 +34,8 @@ export function acquireWatcherLock(lockPath) {
|
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
36
|
catch {
|
|
38
|
-
// Best-effort
|
|
39
|
-
//
|
|
37
|
+
// Best-effort: a leftover lock self-heals via the liveness probe on
|
|
38
|
+
// the next startup.
|
|
40
39
|
}
|
|
41
40
|
};
|
|
42
41
|
const reraise = (signal) => {
|
|
@@ -57,13 +56,13 @@ export function acquireWatcherLock(lockPath) {
|
|
|
57
56
|
return { acquired: true, release };
|
|
58
57
|
}
|
|
59
58
|
/**
|
|
60
|
-
* The one canonical pidfile location
|
|
59
|
+
* The one canonical pidfile location: CLI-vs-wrapper dedupe only works
|
|
61
60
|
* because both paths compute the lock from the same project root.
|
|
62
61
|
*/
|
|
63
62
|
export function watcherLockPath(projectRoot) {
|
|
64
63
|
return join(projectRoot, "node_modules", ".cache", "paramour", "watcher.lock");
|
|
65
64
|
}
|
|
66
|
-
/** `true` when `pid` is a live process
|
|
65
|
+
/** `true` when `pid` is a live process — the liveness probe. */
|
|
67
66
|
function isAlive(pid) {
|
|
68
67
|
try {
|
|
69
68
|
process.kill(pid, 0);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { ParamsSource } from "paramour";
|
|
2
2
|
/**
|
|
3
|
-
* Adapter seam for the client hooks' framework reads
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* shared leaf.
|
|
3
|
+
* Adapter seam for the client hooks' framework reads: each flavor's hooks
|
|
4
|
+
* resolve Next through a React context so the /testing entry can override
|
|
5
|
+
* the reads without runner-specific module mocking. Deliberately NO
|
|
6
|
+
* `"use client"` directive — app.ts (which carries one) and pages.ts (which
|
|
7
|
+
* must not) both import from here, like observe.ts/select.ts; the directive
|
|
8
|
+
* belongs on the entry modules, not a shared leaf.
|
|
10
9
|
*
|
|
11
|
-
* This module is Next-free on purpose
|
|
10
|
+
* This module is Next-free on purpose: the contexts default to `null`
|
|
12
11
|
* and each flavor ENTRY supplies its own real-Next fallback
|
|
13
12
|
* (`useContext(ctx) ?? realAdapter`), so neither this module nor the
|
|
14
13
|
* /testing entry ever drags a `next/*` specifier into its graph. The
|
|
@@ -20,11 +19,11 @@ import type { ParamsSource } from "paramour";
|
|
|
20
19
|
/**
|
|
21
20
|
* App-flavor adapter: EXACTLY the ambient view of `next/navigation` declared
|
|
22
21
|
* in `src/types/next-navigation.d.ts` — that ambient is the contract of
|
|
23
|
-
* record, and this interface must stay in lockstep with it (
|
|
22
|
+
* record, and this interface must stay in lockstep with it (the
|
|
24
23
|
* `examples/next-compat` pins guard the real-Next side). `useParams()`'s
|
|
25
24
|
* `null` arm is the outside-App-Router-tree state (Next #48058 family) the
|
|
26
25
|
* hooks deliberately tolerate; `useRouter().replace`/`usePathname` are the
|
|
27
|
-
* devtools `navigate` capability's write path and resolution base
|
|
26
|
+
* devtools `navigate` capability's write path and resolution base.
|
|
28
27
|
*/
|
|
29
28
|
export interface AppNavigationAdapter {
|
|
30
29
|
useParams(): null | ParamsSource;
|
|
@@ -37,10 +36,10 @@ export interface AppNavigationAdapter {
|
|
|
37
36
|
/**
|
|
38
37
|
* Pages-flavor adapter: EXACTLY the ambient view of `next/router.js`
|
|
39
38
|
* declared in `src/types/next-router.d.ts` — that ambient is the contract of
|
|
40
|
-
* record, and this interface must stay in lockstep with it
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
39
|
+
* record, and this interface must stay in lockstep with it. The ambient's
|
|
40
|
+
* throw-on-unmounted behavior under `app/` is part of the contract: adapter
|
|
41
|
+
* implementations reproduce it by THROWING from `useRouter()`, which
|
|
42
|
+
* pages.ts translates.
|
|
44
43
|
*/
|
|
45
44
|
export interface PagesNavigationAdapter {
|
|
46
45
|
useRouter(): {
|
|
@@ -51,10 +50,10 @@ export interface PagesNavigationAdapter {
|
|
|
51
50
|
};
|
|
52
51
|
}
|
|
53
52
|
/**
|
|
54
|
-
* The `null` default is load-bearing
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* The `null` default is load-bearing: with no provider mounted, app.ts falls
|
|
54
|
+
* back to its real `next/navigation` adapter — zero markup and zero behavior
|
|
55
|
+
* change in production.
|
|
57
56
|
*/
|
|
58
57
|
export declare const AppNavigationContext: import("react").Context<AppNavigationAdapter | null>;
|
|
59
|
-
/** The pages twin; `null` default load-bearing for the same
|
|
58
|
+
/** The pages twin; its `null` default is load-bearing for the same reasons. */
|
|
60
59
|
export declare const PagesNavigationContext: import("react").Context<PagesNavigationAdapter | null>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createContext } from "react";
|
|
2
2
|
/**
|
|
3
|
-
* The `null` default is load-bearing
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* The `null` default is load-bearing: with no provider mounted, app.ts falls
|
|
4
|
+
* back to its real `next/navigation` adapter — zero markup and zero behavior
|
|
5
|
+
* change in production.
|
|
6
6
|
*/
|
|
7
7
|
export const AppNavigationContext = createContext(null);
|
|
8
|
-
/** The pages twin; `null` default load-bearing for the same
|
|
8
|
+
/** The pages twin; its `null` default is load-bearing for the same reasons. */
|
|
9
9
|
export const PagesNavigationContext = createContext(null);
|
package/dist/observe.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { AnyRoute, ParamsSource, RouterKind } from "paramour";
|
|
2
2
|
import type { ParamourHookId, ParamourNavigate, ParamourObservationResult, ParamourSearchWire } from "./devtools-seam.js";
|
|
3
3
|
/**
|
|
4
|
-
* Shared devtools seam wiring for the six read hooks
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Shared devtools seam wiring for the six read hooks: the navigate builders
|
|
5
|
+
* (one per router flavor) and the per-hook emitter that owns WHEN an
|
|
6
|
+
* observation goes out. Deliberately NO `"use client"` directive — app.ts
|
|
7
|
+
* (which carries one) and pages.ts (which must not) both import from here,
|
|
8
|
+
* like select.ts.
|
|
9
9
|
*
|
|
10
10
|
* Emission policy: the hooks call {@link DevtoolsEmitter.observe} from
|
|
11
|
-
* inside the `useStableResult` compute — the
|
|
12
|
-
*
|
|
11
|
+
* inside the `useStableResult` compute — the fingerprint cache miss IS the
|
|
12
|
+
* decode-change dedup, and only render-phase can report the
|
|
13
13
|
* OrThrow hooks' error observation before the rethrow. `observe` alone
|
|
14
14
|
* would leave one staleness hole: a component that survives a navigation
|
|
15
15
|
* whose decode is unchanged (`/product/1?q=a` → `/product/2?q=a` in a
|
|
@@ -18,10 +18,10 @@ import type { ParamourHookId, ParamourNavigate, ParamourObservationResult, Param
|
|
|
18
18
|
* navigate back to the old resource. {@link DevtoolsEmitter.refresh},
|
|
19
19
|
* called render-phase after the stable result returns, closes it: when the
|
|
20
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
|
|
22
|
-
*
|
|
21
|
+
* re-emits the CACHED result with the fresh spec — decode stability is
|
|
22
|
+
* untouched, only the seam payload is renewed.
|
|
23
23
|
*
|
|
24
|
-
* Production erasure
|
|
24
|
+
* Production erasure: every call site keeps a literal
|
|
25
25
|
* `process.env.NODE_ENV` guard the bundler constant-folds, and both emitter
|
|
26
26
|
* methods early-return behind the same literal guard, so the
|
|
27
27
|
* `emitObservation` import above is dead in a production bundle and drops
|
|
@@ -51,15 +51,15 @@ export interface ObservationSpec {
|
|
|
51
51
|
readonly wire: () => ParamourSearchWire | Readonly<ParamsSource>;
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
* App-flavor navigate capability
|
|
55
|
-
*
|
|
54
|
+
* App-flavor navigate capability: `next/navigation`'s `replace` returns void
|
|
55
|
+
* and resolves the basePath-/locale-relative join itself.
|
|
56
56
|
*/
|
|
57
57
|
export declare function makeAppNavigate(router: {
|
|
58
58
|
replace: (href: string) => void;
|
|
59
59
|
}, pathname: string): ParamourNavigate;
|
|
60
60
|
/**
|
|
61
|
-
* Pages-flavor navigate capability
|
|
62
|
-
*
|
|
61
|
+
* Pages-flavor navigate capability: `next/router`'s `replace` returns a
|
|
62
|
+
* promise that REJECTS on routine navigation aborts (rapid re-commits
|
|
63
63
|
* from the panel), marked with next's `cancelled` discriminant — those must
|
|
64
64
|
* not surface as unhandled rejections. Anything else is a real failure
|
|
65
65
|
* (render error, route-info error) silently discarding the user's edit, so
|
package/dist/observe.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useRef } from "react";
|
|
2
2
|
import { emitObservation } from "./devtools-seam.js";
|
|
3
3
|
/**
|
|
4
|
-
* App-flavor navigate capability
|
|
5
|
-
*
|
|
4
|
+
* App-flavor navigate capability: `next/navigation`'s `replace` returns void
|
|
5
|
+
* and resolves the basePath-/locale-relative join itself.
|
|
6
6
|
*/
|
|
7
7
|
export function makeAppNavigate(router, pathname) {
|
|
8
8
|
return (search) => {
|
|
@@ -10,8 +10,8 @@ export function makeAppNavigate(router, pathname) {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Pages-flavor navigate capability
|
|
14
|
-
*
|
|
13
|
+
* Pages-flavor navigate capability: `next/router`'s `replace` returns a
|
|
14
|
+
* promise that REJECTS on routine navigation aborts (rapid re-commits
|
|
15
15
|
* from the panel), marked with next's `cancelled` discriminant — those must
|
|
16
16
|
* not surface as unhandled rejections. Anything else is a real failure
|
|
17
17
|
* (render error, route-info error) silently discarding the user's edit, so
|
package/dist/pages.d.ts
CHANGED
|
@@ -2,57 +2,56 @@ import { type AnyPagesRoute, type InferRouteParams, type SafeResult, type Search
|
|
|
2
2
|
import { type SelectOptions } from "./select.js";
|
|
3
3
|
export type { SelectOptions } from "./select.js";
|
|
4
4
|
/**
|
|
5
|
-
* Pages Router hooks
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Pages Router hooks. Deliberately NO `"use client"` directive on this
|
|
6
|
+
* module: the directive is an App Router (RSC graph) concept, meaningless in
|
|
7
|
+
* a `pages/` bundle.
|
|
8
8
|
*
|
|
9
9
|
* `useRouter().query` is one merged bag (route params + search), and on a
|
|
10
10
|
* statically-optimized page it is `{}` until `router.isReady` flips after
|
|
11
11
|
* hydration — a platform fact the result type admits as a third state
|
|
12
|
-
* instead of papering over
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* instead of papering over. On `getServerSideProps` pages the FIRST render
|
|
13
|
+
* is already `isReady: true` with a populated query, so the `pending` arm
|
|
14
|
+
* never surfaces there.
|
|
15
15
|
*
|
|
16
|
-
* Deliberately NO `OrThrow` variants
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `route.parseContext(ctx)`
|
|
16
|
+
* Deliberately NO `OrThrow` variants: throwing on `pending` would flash the
|
|
17
|
+
* error boundary on every statically-optimized page's first render, and
|
|
18
|
+
* returning `T | undefined` would make the name a lie. The three-state union
|
|
19
|
+
* forcing the check IS the feature — and users who know their page has
|
|
20
|
+
* `getServerSideProps` should be reading typed props from
|
|
21
|
+
* `route.parseContext(ctx)` rather than reaching for a client hook.
|
|
22
22
|
*
|
|
23
|
-
* Both hooks are gated to `AnyPagesRoute`
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Both hooks are gated to `AnyPagesRoute` and share the /app hooks'
|
|
24
|
+
* layering: raw-slice stabilization keyed on the declared slice of `query`
|
|
25
|
+
* (+ `isReady`), then an optional `{ select }` projection with
|
|
26
26
|
* result-equality checking — the `pending` arm passes through the selector
|
|
27
|
-
* untouched
|
|
27
|
+
* untouched, and `PENDING` itself is one referentially stable object.
|
|
28
28
|
*
|
|
29
|
-
* Devtools instrumentation
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* `process.env.NODE_ENV !== "production"` (DT6).
|
|
29
|
+
* Devtools instrumentation: each hook reports through the shared emitter in
|
|
30
|
+
* observe.ts — `observe` from inside the `useStableResult` compute callback
|
|
31
|
+
* (the fingerprint cache miss IS the decode-change dedup; see app.ts's
|
|
32
|
+
* fuller account), `refresh` after it for pathname moves under an unchanged
|
|
33
|
+
* decode. The `pending` arm emits as a first-class observation, keyed by
|
|
34
|
+
* `PENDING_FINGERPRINT` so the pre-`isReady` render reports exactly once.
|
|
35
|
+
* Every emit sits behind `process.env.NODE_ENV !== "production"`.
|
|
37
36
|
*/
|
|
38
37
|
/**
|
|
39
|
-
* Three-state result for the pages hooks
|
|
38
|
+
* Three-state result for the pages hooks: core's `SafeResult` plus a
|
|
40
39
|
* `pending` member for the pre-`isReady` render of a statically-optimized
|
|
41
|
-
* page. Literally `SafeResult<T> | { status: "pending" }
|
|
42
|
-
*
|
|
40
|
+
* page. Literally `SafeResult<T> | { status: "pending" }`, so both routers'
|
|
41
|
+
* results destructure identically.
|
|
43
42
|
*/
|
|
44
43
|
export type RouterResult<T> = SafeResult<T> | {
|
|
45
44
|
status: "pending";
|
|
46
45
|
};
|
|
47
46
|
/**
|
|
48
|
-
* Decoded route params as a {@link RouterResult}
|
|
49
|
-
* through `options.select
|
|
47
|
+
* Decoded route params as a {@link RouterResult}, optionally projected
|
|
48
|
+
* through `options.select`.
|
|
50
49
|
*/
|
|
51
50
|
export declare function useRouteParams<R extends AnyPagesRoute>(route: R): RouterResult<InferRouteParams<R>>;
|
|
52
51
|
export declare function useRouteParams<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<InferRouteParams<R>, U>): RouterResult<U>;
|
|
53
52
|
/**
|
|
54
|
-
* Decoded search params as a {@link RouterResult}
|
|
55
|
-
* through `options.select
|
|
53
|
+
* Decoded search params as a {@link RouterResult}, optionally projected
|
|
54
|
+
* through `options.select`.
|
|
56
55
|
*/
|
|
57
56
|
export declare function useSearch<R extends AnyPagesRoute>(route: R): RouterResult<SearchOutputOf<R["~search"]>>;
|
|
58
57
|
export declare function useSearch<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<SearchOutputOf<R["~search"]>, U>): RouterResult<U>;
|
package/dist/pages.js
CHANGED
|
@@ -80,15 +80,15 @@ export function useSearch(route, options) {
|
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
82
|
* `asPath`'s path part: basePath-/locale-relative — exactly what
|
|
83
|
-
* `replace()` expects back — so the panel's search-only string
|
|
84
|
-
*
|
|
83
|
+
* `replace()` expects back — so the panel's search-only string resolves
|
|
84
|
+
* without doubling a configured basePath.
|
|
85
85
|
*/
|
|
86
86
|
function asPathPathname(asPath) {
|
|
87
87
|
return asPath.split(/[#?]/)[0] ?? "/";
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
90
|
-
* `query` minus the route's own path-param names
|
|
91
|
-
* `parseContext`'s server-side subtraction (core route.ts
|
|
90
|
+
* `query` minus the route's own path-param names — the client twin of
|
|
91
|
+
* `parseContext`'s server-side subtraction (core route.ts). Entries →
|
|
92
92
|
* fromEntries so a hostile `?__proto__=` key stays an ordinary own property
|
|
93
93
|
* (decodeParams's ethos). Names come from the define-time `~segments` token
|
|
94
94
|
* cache, so nothing re-tokenizes per render.
|
|
@@ -102,19 +102,19 @@ function omitPathParams(query, route) {
|
|
|
102
102
|
return Object.fromEntries(Object.entries(query).filter(([key]) => !names.has(key)));
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
|
-
* Real-Next fallback for the adapter seam
|
|
106
|
-
*
|
|
105
|
+
* Real-Next fallback for the adapter seam: the /testing provider overrides
|
|
106
|
+
* the read through {@link PagesNavigationContext}; with
|
|
107
107
|
* no provider mounted the context's `null` default resolves here, so
|
|
108
108
|
* production behavior (and this module's `next/router.js`-only bundle
|
|
109
109
|
* graph, per dist.test.ts) is unchanged. The adapter is resolved via an
|
|
110
110
|
* unconditional `useContext` BEFORE the try below, exactly where the direct
|
|
111
111
|
* call previously sat, keeping hook call order identical across renders and
|
|
112
|
-
* across provider presence
|
|
112
|
+
* across provider presence.
|
|
113
113
|
*/
|
|
114
114
|
const realPagesAdapter = { useRouter };
|
|
115
115
|
/**
|
|
116
|
-
* `useRouter` with the one failure the brand cannot catch translated
|
|
117
|
-
*
|
|
116
|
+
* `useRouter` with the one failure the brand cannot catch translated: in a
|
|
117
|
+
* hybrid project a component rendered under `app/` can legally hold a
|
|
118
118
|
* pages-branded route, but `next/router` has no mount there and throws
|
|
119
119
|
* "NextRouter was not mounted" — a message pointing at the wrong fix
|
|
120
120
|
* (component placement is invisible to the type system). Rethrow a
|
|
@@ -130,7 +130,7 @@ function usePagesRouter() {
|
|
|
130
130
|
catch (error) {
|
|
131
131
|
if (error instanceof Error &&
|
|
132
132
|
error.message.includes("NextRouter was not mounted")) {
|
|
133
|
-
throw new ParamourError('pages hooks were rendered under the App Router, where next/router is never mounted — import this component\'s hooks from "@paramour-js/next/app" and pass it an app route instead
|
|
133
|
+
throw new ParamourError('pages hooks were rendered under the App Router, where next/router is never mounted — import this component\'s hooks from "@paramour-js/next/app" and pass it an app route instead', { cause: error });
|
|
134
134
|
}
|
|
135
135
|
throw error;
|
|
136
136
|
}
|
package/dist/run-cli.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type CliIo } from "./cli-io.js";
|
|
2
2
|
export { type CliIo } from "./cli-io.js";
|
|
3
|
+
type Command = (argv: readonly string[], io: CliIo) => Promise<number>;
|
|
4
|
+
export declare const COMMANDS: Record<string, Command>;
|
|
3
5
|
/**
|
|
4
|
-
* @internal The CLI dispatcher
|
|
6
|
+
* @internal The CLI dispatcher, in-process testable: returns the exit
|
|
5
7
|
* code instead of exiting. The exit-code contract holds across every
|
|
6
8
|
* command: 0 success, 1 "the thing you asked me to verify is not true"
|
|
7
9
|
* (`check`/`generate --check` drift, `doctor` failures) ONLY, 2
|
package/dist/run-cli.js
CHANGED
|
@@ -3,14 +3,17 @@ import { runDoctor } from "./commands/doctor.js";
|
|
|
3
3
|
import { runGenerate } from "./commands/generate.js";
|
|
4
4
|
import { runInit } from "./commands/init.js";
|
|
5
5
|
import { runList } from "./commands/list.js";
|
|
6
|
+
import { runSkills } from "./commands/skills.js";
|
|
6
7
|
export {} from "./cli-io.js";
|
|
7
|
-
// Alphabetical; the unknown-command message derives from these keys
|
|
8
|
-
|
|
8
|
+
// Alphabetical; the unknown-command message derives from these keys, and the
|
|
9
|
+
// skill-drift test pins them against the skill's CLI table.
|
|
10
|
+
export const COMMANDS = {
|
|
9
11
|
check: (argv, io) => runGenerate(argv, io, "check"),
|
|
10
12
|
doctor: runDoctor,
|
|
11
13
|
generate: (argv, io) => runGenerate(argv, io, "generate"),
|
|
12
14
|
init: runInit,
|
|
13
15
|
list: runList,
|
|
16
|
+
skills: (argv, io) => Promise.resolve(runSkills(argv, io)),
|
|
14
17
|
};
|
|
15
18
|
const USAGE = [
|
|
16
19
|
"Usage: paramour <command> [options]",
|
|
@@ -21,11 +24,12 @@ const USAGE = [
|
|
|
21
24
|
" generate generate paramour-env.d.ts from the app and pages directories",
|
|
22
25
|
" init set up paramour in this project",
|
|
23
26
|
" list print every route with its params/search shape",
|
|
27
|
+
" skills install or verify the bundled agent skill for detected agent tools",
|
|
24
28
|
"",
|
|
25
29
|
"Run `paramour <command> --help` for that command's options.",
|
|
26
30
|
].join("\n");
|
|
27
31
|
/**
|
|
28
|
-
* @internal The CLI dispatcher
|
|
32
|
+
* @internal The CLI dispatcher, in-process testable: returns the exit
|
|
29
33
|
* code instead of exiting. The exit-code contract holds across every
|
|
30
34
|
* command: 0 success, 1 "the thing you asked me to verify is not true"
|
|
31
35
|
* (`check`/`generate --check` drift, `doctor` failures) ONLY, 2
|
package/dist/scan-app.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Dirent } from "node:fs";
|
|
2
|
-
/** Next's default `pageExtensions` — extensions only, no leading dot
|
|
2
|
+
/** Next's default `pageExtensions` — extensions only, no leading dot. */
|
|
3
3
|
export declare const DEFAULT_PAGE_EXTENSIONS: readonly ["tsx", "ts", "jsx", "js"];
|
|
4
4
|
/**
|
|
5
5
|
* Whether a directory entry should be treated as a FILE for routing: a real
|
|
6
6
|
* file, or a symlink whose target is a regular file. `Dirent.isFile()` is
|
|
7
7
|
* false for a symlink even when it points at a file, yet Next resolves and
|
|
8
8
|
* serves symlinked `page`/`route` files (common in pnpm-linked monorepos), so
|
|
9
|
-
* dropping them would omit routes that Next actually serves (Bug 4
|
|
9
|
+
* dropping them would omit routes that Next actually serves (Bug 4). A
|
|
10
10
|
* symlink to a DIRECTORY returns false — directory symlinks stay not-followed,
|
|
11
11
|
* the existing v1 stance — and a broken link (statSync throws ENOENT) also
|
|
12
12
|
* returns false, i.e. is skipped silently, matching Next's own tolerance of
|
|
@@ -15,16 +15,16 @@ export declare const DEFAULT_PAGE_EXTENSIONS: readonly ["tsx", "ts", "jsx", "js"
|
|
|
15
15
|
export declare function resolvesToFile(entry: Dirent, dir: string): boolean;
|
|
16
16
|
/**
|
|
17
17
|
* Walk an app dir and return the sorted union of URL-shaped route paths —
|
|
18
|
-
* exactly the strings `defineAppRoute` accepts
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* exactly the strings `defineAppRoute` accepts. Pure `fs.readdir` recursion;
|
|
19
|
+
* no dependency on Next internals. Two page files resolving to one URL path
|
|
20
|
+
* — `(a)/x` + `(b)/x` group twins, or `page.tsx` + `page.jsx` extension
|
|
21
|
+
* twins — throw a {@link RouteCollisionError} instead of being deduped, the
|
|
22
|
+
* same stance the pages scanner takes: that state is Next's own build error,
|
|
23
23
|
* and deduping would emit an artifact for a project that cannot build.
|
|
24
24
|
*
|
|
25
25
|
* `route.<ext>` handlers are scanned but never emitted (handler typing is
|
|
26
|
-
* deferred
|
|
27
|
-
*
|
|
28
|
-
* page"), and two route handlers at the same path — both throw
|
|
26
|
+
* deferred). They exist only to catch the states Next refuses to build: a
|
|
27
|
+
* page and a route handler at the same URL path ("conflicting route and
|
|
28
|
+
* page"), and two route handlers at the same path — both throw.
|
|
29
29
|
*/
|
|
30
30
|
export declare function scanAppRoutes(appDir: string, pageExtensions?: readonly string[]): string[];
|
package/dist/scan-app.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { assertNoStructuralCollisions, RouteCollisionError, } from "./collisions.js";
|
|
4
|
-
/** Next's default `pageExtensions` — extensions only, no leading dot
|
|
4
|
+
/** Next's default `pageExtensions` — extensions only, no leading dot. */
|
|
5
5
|
export const DEFAULT_PAGE_EXTENSIONS = ["tsx", "ts", "jsx", "js"];
|
|
6
6
|
/**
|
|
7
|
-
* Interception markers `(.)`/`(..)`/`(...)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Interception markers `(.)`/`(..)`/`(...)`. A prefix match, so chained
|
|
8
|
+
* forms like `(..)(..)segment` are caught too; tested BEFORE the route-group
|
|
9
|
+
* test so `(.)foo` is never misread as a group.
|
|
10
10
|
*/
|
|
11
11
|
const INTERCEPTION_PREFIX = /^\(\.{1,3}\)/;
|
|
12
12
|
/**
|
|
@@ -17,18 +17,18 @@ const INTERCEPTION_PREFIX = /^\(\.{1,3}\)/;
|
|
|
17
17
|
* `/_settings`. The escape is defined for the LEADING position only. Because
|
|
18
18
|
* RFC 3986 percent-encoding is case-insensitive on its hex digits (and this
|
|
19
19
|
* could not be pinned against Next's source from here), both `%5F` and `%5f`
|
|
20
|
-
* are decoded defensively (Bug 8
|
|
21
|
-
*
|
|
20
|
+
* are decoded defensively (Bug 8). The fs name stays raw for error messages;
|
|
21
|
+
* only the emitted URL segment is decoded.
|
|
22
22
|
*/
|
|
23
23
|
const LEADING_ESCAPED_UNDERSCORE = /^%5[Ff]/;
|
|
24
|
-
/** Route groups `(group)` — stripped from the emitted path
|
|
24
|
+
/** Route groups `(group)` — stripped from the emitted path. */
|
|
25
25
|
const ROUTE_GROUP = /^\(.*\)$/;
|
|
26
26
|
/**
|
|
27
27
|
* Whether a directory entry should be treated as a FILE for routing: a real
|
|
28
28
|
* file, or a symlink whose target is a regular file. `Dirent.isFile()` is
|
|
29
29
|
* false for a symlink even when it points at a file, yet Next resolves and
|
|
30
30
|
* serves symlinked `page`/`route` files (common in pnpm-linked monorepos), so
|
|
31
|
-
* dropping them would omit routes that Next actually serves (Bug 4
|
|
31
|
+
* dropping them would omit routes that Next actually serves (Bug 4). A
|
|
32
32
|
* symlink to a DIRECTORY returns false — directory symlinks stay not-followed,
|
|
33
33
|
* the existing v1 stance — and a broken link (statSync throws ENOENT) also
|
|
34
34
|
* returns false, i.e. is skipped silently, matching Next's own tolerance of
|
|
@@ -50,17 +50,17 @@ export function resolvesToFile(entry, dir) {
|
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
52
|
* Walk an app dir and return the sorted union of URL-shaped route paths —
|
|
53
|
-
* exactly the strings `defineAppRoute` accepts
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
53
|
+
* exactly the strings `defineAppRoute` accepts. Pure `fs.readdir` recursion;
|
|
54
|
+
* no dependency on Next internals. Two page files resolving to one URL path
|
|
55
|
+
* — `(a)/x` + `(b)/x` group twins, or `page.tsx` + `page.jsx` extension
|
|
56
|
+
* twins — throw a {@link RouteCollisionError} instead of being deduped, the
|
|
57
|
+
* same stance the pages scanner takes: that state is Next's own build error,
|
|
58
58
|
* and deduping would emit an artifact for a project that cannot build.
|
|
59
59
|
*
|
|
60
60
|
* `route.<ext>` handlers are scanned but never emitted (handler typing is
|
|
61
|
-
* deferred
|
|
62
|
-
*
|
|
63
|
-
* page"), and two route handlers at the same path — both throw
|
|
61
|
+
* deferred). They exist only to catch the states Next refuses to build: a
|
|
62
|
+
* page and a route handler at the same URL path ("conflicting route and
|
|
63
|
+
* page"), and two route handlers at the same path — both throw.
|
|
64
64
|
*/
|
|
65
65
|
export function scanAppRoutes(appDir, pageExtensions = DEFAULT_PAGE_EXTENSIONS) {
|
|
66
66
|
// Path → the fs path (relative to appDir) that produced it, so a collision
|
|
@@ -71,20 +71,20 @@ export function scanAppRoutes(appDir, pageExtensions = DEFAULT_PAGE_EXTENSIONS)
|
|
|
71
71
|
const pageFileNames = new Set(pageExtensions.map((ext) => `page.${ext}`));
|
|
72
72
|
const routeFileNames = new Set(pageExtensions.map((ext) => `route.${ext}`));
|
|
73
73
|
walk(appDir, [], [], pageFileNames, routeFileNames, out, routeOut);
|
|
74
|
-
//
|
|
74
|
+
// A page and a route handler resolving to one URL path is Next's
|
|
75
75
|
// "conflicting route and page" build error — no valid artifact exists, so
|
|
76
76
|
// throw rather than emit the page and silently drop the handler. Sorted so
|
|
77
77
|
// the reported pair is deterministic across platforms.
|
|
78
78
|
for (const [path, routeFile] of [...routeOut].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
79
79
|
const pageFile = out.get(path);
|
|
80
80
|
if (pageFile !== undefined) {
|
|
81
|
-
throw new RouteCollisionError(`app route collision at "${path}": page ${pageFile} and route handler ${routeFile} resolve to the same path, which Next refuses to build (conflicting route and page)
|
|
81
|
+
throw new RouteCollisionError(`app route collision at "${path}": page ${pageFile} and route handler ${routeFile} resolve to the same path, which Next refuses to build (conflicting route and page)`);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
// Code-unit sort, never localeCompare — locale independence feeds
|
|
84
|
+
// Code-unit sort, never localeCompare — locale independence feeds the
|
|
85
85
|
// byte-identical-on-every-OS guarantee.
|
|
86
86
|
const paths = [...out.keys()].sort();
|
|
87
|
-
//
|
|
87
|
+
// Structural collisions (different slug names, optional-catch-all
|
|
88
88
|
// specificity) — non-equal strings the Map above cannot catch.
|
|
89
89
|
assertNoStructuralCollisions(paths.map((path) => ({ path, router: "app" })));
|
|
90
90
|
return paths;
|
|
@@ -97,11 +97,11 @@ function walk(dir, urlSegments, fsSegments, pageFileNames, routeFileNames, out,
|
|
|
97
97
|
const name = entry.name;
|
|
98
98
|
// A real file, or a symlink whose target is a file (Bug 4). Directory
|
|
99
99
|
// symlinks fall through to the directory guard below, which is false for
|
|
100
|
-
// a symlink Dirent, so their subtree is skipped — not followed
|
|
100
|
+
// a symlink Dirent, so their subtree is skipped — not followed.
|
|
101
101
|
if (resolvesToFile(entry, dir)) {
|
|
102
|
-
// Exact, case-sensitive `page.<ext>` / `route.<ext>` match
|
|
103
|
-
//
|
|
104
|
-
// handler typing is
|
|
102
|
+
// Exact, case-sensitive `page.<ext>` / `route.<ext>` match. Pages are
|
|
103
|
+
// emitted; route handlers are tracked separately (never emitted —
|
|
104
|
+
// handler typing is deferred) purely to detect the build errors above.
|
|
105
105
|
const isPage = pageFileNames.has(name);
|
|
106
106
|
const isRoute = !isPage && routeFileNames.has(name);
|
|
107
107
|
if (isPage || isRoute) {
|
|
@@ -111,19 +111,19 @@ function walk(dir, urlSegments, fsSegments, pageFileNames, routeFileNames, out,
|
|
|
111
111
|
const existing = target.get(path);
|
|
112
112
|
if (existing !== undefined) {
|
|
113
113
|
throw new RouteCollisionError(isPage
|
|
114
|
-
? `app route collision at "${path}": ${existing} and ${file} resolve to the same path
|
|
115
|
-
: `app route collision at "${path}": ${existing} and ${file} both declare a route handler at the same path, which Next refuses to build
|
|
114
|
+
? `app route collision at "${path}": ${existing} and ${file} resolve to the same path`
|
|
115
|
+
: `app route collision at "${path}": ${existing} and ${file} both declare a route handler at the same path, which Next refuses to build`);
|
|
116
116
|
}
|
|
117
117
|
target.set(path, file);
|
|
118
118
|
}
|
|
119
119
|
continue;
|
|
120
120
|
}
|
|
121
|
-
// Symlinked directories are deliberately not followed (
|
|
121
|
+
// Symlinked directories are deliberately not followed (the v1 stance):
|
|
122
122
|
// `resolvesToFile` returned false and `isDirectory()` is false for the
|
|
123
123
|
// link Dirent, so the subtree is skipped here.
|
|
124
124
|
if (!entry.isDirectory())
|
|
125
125
|
continue;
|
|
126
|
-
//
|
|
126
|
+
// Skip rules: private folders, parallel slots, interception routes —
|
|
127
127
|
// each skips the entire subtree, pages at any depth included. The `_`
|
|
128
128
|
// test reads the raw fs name, so `%5F`-escaped folders (which do NOT
|
|
129
129
|
// start with `_`) are correctly NOT skipped (Bug 8).
|
|
@@ -134,12 +134,12 @@ function walk(dir, urlSegments, fsSegments, pageFileNames, routeFileNames, out,
|
|
|
134
134
|
if (INTERCEPTION_PREFIX.test(name))
|
|
135
135
|
continue;
|
|
136
136
|
if (ROUTE_GROUP.test(name)) {
|
|
137
|
-
// Group stripped: recurse with the SAME url segments
|
|
137
|
+
// Group stripped: recurse with the SAME url segments.
|
|
138
138
|
walk(join(dir, name), urlSegments, [...fsSegments, name], pageFileNames, routeFileNames, out, routeOut);
|
|
139
139
|
continue;
|
|
140
140
|
}
|
|
141
141
|
// Dynamic segments `[id]` / `[...slug]` / `[[...slug]]` pass through
|
|
142
|
-
// verbatim
|
|
142
|
+
// verbatim as URL-shaped literals. A leading `%5F` decodes to `_`
|
|
143
143
|
// for the emitted URL segment so it string-matches the served URL; the fs
|
|
144
144
|
// name stays raw for error messages, and the decoded form participates in
|
|
145
145
|
// collision detection via the `out` Map key (Bug 8).
|