@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/scan-pages.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Walk a pages dir and return the sorted union of URL-shaped route paths —
|
|
3
|
-
* exactly the strings `definePagesRoute` accepts
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* exactly the strings `definePagesRoute` accepts. A route is any file whose
|
|
4
|
+
* extension is in `pageExtensions`, mapped by its path relative to the dir;
|
|
5
|
+
* `index.<ext>` maps to its directory. Two files resolving to one URL path —
|
|
6
|
+
* folder/file spelling (`blog.tsx` + `blog/index.tsx`) or extension twins
|
|
7
|
+
* (`about.tsx` + `about.jsx`) — throw a {@link RouteCollisionError}, never
|
|
8
|
+
* dedupe: both are Next's own build errors.
|
|
9
9
|
*/
|
|
10
10
|
export declare function scanPagesRoutes(pagesDir: string, pageExtensions?: readonly string[]): string[];
|
package/dist/scan-pages.js
CHANGED
|
@@ -3,18 +3,18 @@ import { join } from "node:path";
|
|
|
3
3
|
import { assertNoStructuralCollisions, RouteCollisionError, } from "./collisions.js";
|
|
4
4
|
import { DEFAULT_PAGE_EXTENSIONS, resolvesToFile } from "./scan-app.js";
|
|
5
5
|
/**
|
|
6
|
-
* Pages Router scanner
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Pages Router scanner. Deliberately a separate walker from `scan-app.ts` —
|
|
7
|
+
* the rule sets barely overlap (routes live on FILES here, and none of the
|
|
8
|
+
* app scanner's skip rules apply), and a shared walker would have to take
|
|
9
|
+
* its rule set as a parameter to be worth having.
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
|
-
* Names special to Next at the TOP level of the pages dir only
|
|
12
|
+
* Names special to Next at the TOP level of the pages dir only:
|
|
13
13
|
* `_app`/`_document`/`_error` are framework files, `404`/`500` are error
|
|
14
14
|
* pages, not navigation targets — `href("/404")` should not type-check.
|
|
15
15
|
* Nested twins (`pages/blog/404.tsx`) are ordinary pages and route.
|
|
16
|
-
* Every other `_`-prefixed file routes too
|
|
17
|
-
*
|
|
16
|
+
* Every other `_`-prefixed file routes too — co-location under `pages/` was
|
|
17
|
+
* requested (vercel/next.js#8454) and never implemented.
|
|
18
18
|
*/
|
|
19
19
|
const TOP_LEVEL_EXCLUDED = new Set([
|
|
20
20
|
"404",
|
|
@@ -25,22 +25,22 @@ const TOP_LEVEL_EXCLUDED = new Set([
|
|
|
25
25
|
]);
|
|
26
26
|
/**
|
|
27
27
|
* Walk a pages dir and return the sorted union of URL-shaped route paths —
|
|
28
|
-
* exactly the strings `definePagesRoute` accepts
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
28
|
+
* exactly the strings `definePagesRoute` accepts. A route is any file whose
|
|
29
|
+
* extension is in `pageExtensions`, mapped by its path relative to the dir;
|
|
30
|
+
* `index.<ext>` maps to its directory. Two files resolving to one URL path —
|
|
31
|
+
* folder/file spelling (`blog.tsx` + `blog/index.tsx`) or extension twins
|
|
32
|
+
* (`about.tsx` + `about.jsx`) — throw a {@link RouteCollisionError}, never
|
|
33
|
+
* dedupe: both are Next's own build errors.
|
|
34
34
|
*/
|
|
35
35
|
export function scanPagesRoutes(pagesDir, pageExtensions = DEFAULT_PAGE_EXTENSIONS) {
|
|
36
36
|
// Path → the fs path (relative to pagesDir) that produced it, so a
|
|
37
37
|
// collision can name both files.
|
|
38
38
|
const out = new Map();
|
|
39
39
|
walk(pagesDir, [], pageExtensions, out, true);
|
|
40
|
-
// Code-unit sort, never localeCompare — locale independence feeds
|
|
40
|
+
// Code-unit sort, never localeCompare — locale independence feeds the
|
|
41
41
|
// byte-identical-on-every-OS guarantee.
|
|
42
42
|
const paths = [...out.keys()].sort();
|
|
43
|
-
//
|
|
43
|
+
// Structural collisions (different slug names, optional-catch-all
|
|
44
44
|
// specificity) — non-equal strings the Map above cannot catch.
|
|
45
45
|
assertNoStructuralCollisions(paths.map((path) => ({ path, router: "pages" })));
|
|
46
46
|
return paths;
|
|
@@ -62,10 +62,11 @@ function walk(dir, urlSegments, pageExtensions, out, isTopLevel) {
|
|
|
62
62
|
const name = entry.name;
|
|
63
63
|
// A real file, or a symlink whose target is a file: Next resolves and
|
|
64
64
|
// serves symlinked page files, so a file symlink routes exactly like a
|
|
65
|
-
// real file (Bug 4,
|
|
66
|
-
// to the directory guard below and stay
|
|
65
|
+
// real file (Bug 4, the same posture the app scanner takes). Directory
|
|
66
|
+
// symlinks fall through to the directory guard below and stay
|
|
67
|
+
// not-followed.
|
|
67
68
|
if (resolvesToFile(entry, dir)) {
|
|
68
|
-
// Declaration files match `.ts` but are never pages
|
|
69
|
+
// Declaration files match `.ts` but are never pages.
|
|
69
70
|
if (name.endsWith(".d.ts"))
|
|
70
71
|
continue;
|
|
71
72
|
const ext = matchExtension(name, pageExtensions);
|
|
@@ -75,26 +76,27 @@ function walk(dir, urlSegments, pageExtensions, out, isTopLevel) {
|
|
|
75
76
|
if (isTopLevel && TOP_LEVEL_EXCLUDED.has(base))
|
|
76
77
|
continue;
|
|
77
78
|
// `index.<ext>` maps to its directory; everything else — dynamic
|
|
78
|
-
// segments included — is a path segment of its own
|
|
79
|
+
// segments included — is a path segment of its own.
|
|
79
80
|
const segments = base === "index" ? urlSegments : [...urlSegments, base];
|
|
80
81
|
const path = segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
81
82
|
const file = [...urlSegments, name].join("/");
|
|
82
83
|
const existing = out.get(path);
|
|
83
84
|
if (existing !== undefined) {
|
|
84
|
-
throw new RouteCollisionError(`pages route collision at "${path}": ${existing} and ${file} resolve to the same path
|
|
85
|
+
throw new RouteCollisionError(`pages route collision at "${path}": ${existing} and ${file} resolve to the same path`);
|
|
85
86
|
}
|
|
86
87
|
out.set(path, file);
|
|
87
88
|
continue;
|
|
88
89
|
}
|
|
89
|
-
// Symlinked directories are deliberately not followed (
|
|
90
|
-
// shared
|
|
91
|
-
// false for the link Dirent, so the subtree is
|
|
90
|
+
// Symlinked directories are deliberately not followed (the v1 stance,
|
|
91
|
+
// shared with the app scanner): `resolvesToFile` returned false and
|
|
92
|
+
// `isDirectory()` is false for the link Dirent, so the subtree is
|
|
93
|
+
// skipped here.
|
|
92
94
|
if (!entry.isDirectory())
|
|
93
95
|
continue;
|
|
94
96
|
// `pages/api/**` is excluded — top level only, so `pages/foo/api/bar.tsx`
|
|
95
|
-
// routes (API-route typing is deferred to v1.x
|
|
96
|
-
//
|
|
97
|
-
//
|
|
97
|
+
// routes (API-route typing is deferred to v1.x). NO app-style skip rules
|
|
98
|
+
// beyond this: `(group)`, `@slot`, `(.)x`, and `_`-prefixed dirs are
|
|
99
|
+
// ordinary literal segments in the Pages Router.
|
|
98
100
|
if (isTopLevel && name === "api")
|
|
99
101
|
continue;
|
|
100
102
|
walk(join(dir, name), [...urlSegments, name], pageExtensions, out, false);
|
package/dist/scan.d.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The thin orchestrator over the two scanners
|
|
3
|
-
*
|
|
2
|
+
* The thin orchestrator over the two scanners: joint directory discovery,
|
|
3
|
+
* delegation, and the cross-router collision checks.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The two route dirs of a project; either may be absent — hybrid app/pages
|
|
7
|
+
* projects are supported, as are app-only and pages-only ones.
|
|
4
8
|
*/
|
|
5
|
-
/** The two route dirs of a project; either may be absent (PR1 hybrid). */
|
|
6
9
|
export interface RouteDirs {
|
|
7
10
|
appDir?: string | undefined;
|
|
8
11
|
pagesDir?: string | undefined;
|
|
9
12
|
}
|
|
10
|
-
/** Result of {@link scanRoutes} — the input shape of the
|
|
13
|
+
/** Result of {@link scanRoutes} — the input shape of the artifact. */
|
|
11
14
|
export interface ScanRoutesResult {
|
|
12
15
|
appRoutes: string[];
|
|
13
16
|
pagesRoutes: string[];
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
16
|
-
* Joint route-dir discovery
|
|
17
|
-
*
|
|
19
|
+
* Joint route-dir discovery. Next's documented rule is one decision, not
|
|
20
|
+
* two probes: `src/app` AND `src/pages` are both ignored
|
|
18
21
|
* whenever `app/` OR `pages/` exists at the project root. An ignored src dir
|
|
19
22
|
* that contains page files is a hard config error — Next silently serves
|
|
20
23
|
* none of those pages (and has shipped bugs in the mixed case,
|
|
@@ -22,10 +25,10 @@ export interface ScanRoutesResult {
|
|
|
22
25
|
*/
|
|
23
26
|
export declare function resolveRouteDirs(projectRoot: string, pageExtensions?: readonly string[]): RouteDirs;
|
|
24
27
|
/**
|
|
25
|
-
* Scan whichever route dirs exist and return both route unions
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
28
|
+
* Scan whichever route dirs exist and return both route unions. After each
|
|
29
|
+
* scanner's own intra-router checks, two cross-router passes run: a path in
|
|
30
|
+
* BOTH unions is Next's "Conflicting app and page file" build error, and the
|
|
31
|
+
* structural pass re-runs over the merged, labeled union so
|
|
29
32
|
* shared-prefix slug conflicts and cross-router optional-catch-all
|
|
30
33
|
* specificity are caught too.
|
|
31
34
|
*/
|
package/dist/scan.js
CHANGED
|
@@ -4,8 +4,8 @@ import { assertNoStructuralCollisions, RouteCollisionError, } from "./collisions
|
|
|
4
4
|
import { DEFAULT_PAGE_EXTENSIONS, scanAppRoutes } from "./scan-app.js";
|
|
5
5
|
import { scanPagesRoutes } from "./scan-pages.js";
|
|
6
6
|
/**
|
|
7
|
-
* Joint route-dir discovery
|
|
8
|
-
*
|
|
7
|
+
* Joint route-dir discovery. Next's documented rule is one decision, not
|
|
8
|
+
* two probes: `src/app` AND `src/pages` are both ignored
|
|
9
9
|
* whenever `app/` OR `pages/` exists at the project root. An ignored src dir
|
|
10
10
|
* that contains page files is a hard config error — Next silently serves
|
|
11
11
|
* none of those pages (and has shipped bugs in the mixed case,
|
|
@@ -47,10 +47,10 @@ export function resolveRouteDirs(projectRoot, pageExtensions = DEFAULT_PAGE_EXTE
|
|
|
47
47
|
return { appDir: rootApp, pagesDir: rootPages };
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* Scan whichever route dirs exist and return both route unions
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
50
|
+
* Scan whichever route dirs exist and return both route unions. After each
|
|
51
|
+
* scanner's own intra-router checks, two cross-router passes run: a path in
|
|
52
|
+
* BOTH unions is Next's "Conflicting app and page file" build error, and the
|
|
53
|
+
* structural pass re-runs over the merged, labeled union so
|
|
54
54
|
* shared-prefix slug conflicts and cross-router optional-catch-all
|
|
55
55
|
* specificity are caught too.
|
|
56
56
|
*/
|
|
@@ -62,7 +62,7 @@ export function scanRoutes(dirs, pageExtensions = DEFAULT_PAGE_EXTENSIONS) {
|
|
|
62
62
|
const appSet = new Set(appRoutes);
|
|
63
63
|
const shared = pagesRoutes.filter((path) => appSet.has(path));
|
|
64
64
|
if (shared.length > 0) {
|
|
65
|
-
throw new RouteCollisionError(`route collision between app/ and pages/: ${shared.map((path) => `"${path}"`).join(", ")} — Next fails the build on conflicting app and page files
|
|
65
|
+
throw new RouteCollisionError(`route collision between app/ and pages/: ${shared.map((path) => `"${path}"`).join(", ")} — Next fails the build on conflicting app and page files`);
|
|
66
66
|
}
|
|
67
67
|
assertNoStructuralCollisions([
|
|
68
68
|
...appRoutes.map((path) => ({ path, router: "app" })),
|
package/dist/select.d.ts
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
import type { AnyRoute, ParamsSource, SafeResult } from "paramour";
|
|
2
2
|
/**
|
|
3
|
-
* Shared internals of the read hooks' selector surface
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Shared internals of the read hooks' selector surface: the raw-slice
|
|
4
|
+
* stabilization layer and the selector layer. Deliberately NO `"use client"`
|
|
5
|
+
* directive: app.ts (which carries one) and pages.ts (which must not carry
|
|
6
|
+
* one) both import from here, and the directive belongs on the entry
|
|
7
|
+
* modules, not a shared leaf.
|
|
8
8
|
*
|
|
9
|
-
* Both layers are `useRef` caches mutated during render
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Both layers are `useRef` caches mutated during render — the Redux/TanStack
|
|
10
|
+
* selector pattern, and the one sanctioned departure from the hooks'
|
|
11
|
+
* pure-`useMemo` discipline: result equality needs memory across renders,
|
|
12
|
+
* which no pure memo can provide. Every cache is cleared BEFORE a compute
|
|
13
|
+
* that can throw, so a throwing decode or selector never strands a stale
|
|
14
|
+
* entry.
|
|
15
15
|
*/
|
|
16
16
|
/**
|
|
17
|
-
* Options bag accepted by every read hook
|
|
17
|
+
* Options bag accepted by every read hook.
|
|
18
18
|
*/
|
|
19
19
|
export interface SelectOptions<T, U> {
|
|
20
20
|
/**
|
|
21
|
-
* Result-equality mode for the selected value
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Result-equality mode for the selected value: `Object.is` by default —
|
|
22
|
+
* free and correct for primitive selections — with one-level `"shallow"`
|
|
23
|
+
* as the opt-in for tuple/object selections.
|
|
24
24
|
*/
|
|
25
25
|
readonly equality?: "shallow";
|
|
26
26
|
/**
|
|
27
|
-
* Pure projection of the decoded value; runs only on the success arm
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
27
|
+
* Pure projection of the decoded value; runs only on the success arm.
|
|
28
|
+
* Identity is never compared, so inline arrows are fine — and when the
|
|
29
|
+
* underlying result is reference-stable the selector is NOT re-run, so it
|
|
30
|
+
* must not read changing outside state. A throw propagates to the nearest
|
|
31
|
+
* error boundary: a selector bug is a code bug, never the `SafeResult`
|
|
32
|
+
* error arm, which is reserved for URL data problems.
|
|
33
33
|
*/
|
|
34
34
|
readonly select: (value: T) => U;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Fingerprint of the pages hooks' pre-`isReady` state
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Fingerprint of the pages hooks' pre-`isReady` state. Every real
|
|
38
|
+
* fingerprint is a `JSON.stringify`'d array (starts with `[`), so this can
|
|
39
|
+
* never collide with one.
|
|
40
40
|
*/
|
|
41
41
|
export declare const PENDING_FINGERPRINT = "pending";
|
|
42
42
|
/**
|
|
43
|
-
* Raw slice of a params source
|
|
44
|
-
*
|
|
43
|
+
* Raw slice of a params source: the route's dynamic segment names' raw
|
|
44
|
+
* values, from the define-time `~segments` token cache. Unknown keys —
|
|
45
45
|
* e.g. a parallel route's params in the same `useParams()` bag — never bust
|
|
46
46
|
* the fingerprint, because the decode never reads them.
|
|
47
47
|
*/
|
|
48
48
|
export declare function paramsFingerprint(route: AnyRoute, source: ParamsSource): string;
|
|
49
49
|
/**
|
|
50
|
-
* Raw slice of a pages `router.query` bag for the search half
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* Raw slice of a pages `router.query` bag for the search half. A codec-map
|
|
51
|
+
* route reads exactly its declared keys (query junk and the route's own path
|
|
52
|
+
* params are invisible to the decode — the two namespaces are disjoint); a
|
|
53
53
|
* `rawSearch` route has no enumerable declared-key set — the schema sees
|
|
54
|
-
* every key except the route's path params (
|
|
55
|
-
* exactly that slice is fingerprinted, in sorted-key order for
|
|
56
|
-
* independence.
|
|
54
|
+
* every key except the route's path params (by subtracting them from the
|
|
55
|
+
* bag), so exactly that slice is fingerprinted, in sorted-key order for
|
|
56
|
+
* record-order independence.
|
|
57
57
|
*/
|
|
58
58
|
export declare function queryFingerprint(route: AnyRoute, query: ParamsSource): string;
|
|
59
59
|
/**
|
|
60
|
-
* Raw slice of an app `useSearchParams()` source
|
|
60
|
+
* Raw slice of an app `useSearchParams()` source: the declared keys'
|
|
61
61
|
* `[key, value]` pairs in wire order — order is load-bearing for repeated
|
|
62
62
|
* keys (array codecs decode in wire order, P5/S5), and iterating the live
|
|
63
63
|
* pairs preserves the relative order of declared entries while `?utm_*`
|
|
@@ -66,8 +66,8 @@ export declare function queryFingerprint(route: AnyRoute, query: ParamsSource):
|
|
|
66
66
|
*/
|
|
67
67
|
export declare function searchParamsFingerprint(route: AnyRoute, source: URLSearchParams): string;
|
|
68
68
|
/**
|
|
69
|
-
* The selector layer for the safe hooks
|
|
70
|
-
* reference-stabilizes the projected WRAPPER by result equality
|
|
69
|
+
* The selector layer for the safe hooks: projects the success arm and
|
|
70
|
+
* reference-stabilizes the projected WRAPPER by result equality — a
|
|
71
71
|
* stable `data` inside a fresh wrapper would still churn every consumer.
|
|
72
72
|
* Error and pending arms pass through untouched; they are already
|
|
73
73
|
* reference-stabilized by {@link useStableResult}'s raw-slice layer.
|
|
@@ -79,16 +79,16 @@ export declare function useSelectedResult<T, U>(result: SafeResult<T> | {
|
|
|
79
79
|
status: "pending";
|
|
80
80
|
};
|
|
81
81
|
/**
|
|
82
|
-
* {@link useSelectedResult}'s twin for the `*OrThrow` hooks
|
|
83
|
-
*
|
|
82
|
+
* {@link useSelectedResult}'s twin for the `*OrThrow` hooks: same layering,
|
|
83
|
+
* no wrapper — the hook's return IS the (selected) value.
|
|
84
84
|
*/
|
|
85
85
|
export declare function useSelectedValue<T, U>(value: T, options: SelectOptions<T, U> | undefined): T | U;
|
|
86
86
|
/**
|
|
87
|
-
* The raw-slice stabilization layer
|
|
88
|
-
*
|
|
87
|
+
* The raw-slice stabilization layer: while `route` and `fingerprint` are
|
|
88
|
+
* unchanged from the previous render, the previous result — success OR
|
|
89
89
|
* error arm — is returned without recomputing, so a fresh `useSearchParams()`
|
|
90
90
|
* / `query` object whose DECLARED slice is unchanged (`?utm_source=` churn)
|
|
91
91
|
* costs neither a decode nor anyone's referential equality. This replaces
|
|
92
|
-
* the
|
|
92
|
+
* the earlier "memo keyed on Next's object reference" behavior.
|
|
93
93
|
*/
|
|
94
94
|
export declare function useStableResult<T>(route: AnyRoute, fingerprint: string, compute: () => T): T;
|
package/dist/select.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useRef } from "react";
|
|
2
2
|
/**
|
|
3
|
-
* Fingerprint of the pages hooks' pre-`isReady` state
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Fingerprint of the pages hooks' pre-`isReady` state. Every real
|
|
4
|
+
* fingerprint is a `JSON.stringify`'d array (starts with `[`), so this can
|
|
5
|
+
* never collide with one.
|
|
6
6
|
*/
|
|
7
7
|
export const PENDING_FINGERPRINT = "pending";
|
|
8
8
|
/**
|
|
9
|
-
* Raw slice of a params source
|
|
10
|
-
*
|
|
9
|
+
* Raw slice of a params source: the route's dynamic segment names' raw
|
|
10
|
+
* values, from the define-time `~segments` token cache. Unknown keys —
|
|
11
11
|
* e.g. a parallel route's params in the same `useParams()` bag — never bust
|
|
12
12
|
* the fingerprint, because the decode never reads them.
|
|
13
13
|
*/
|
|
@@ -15,13 +15,13 @@ export function paramsFingerprint(route, source) {
|
|
|
15
15
|
return recordFingerprint(dynamicSegmentNames(route), source);
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Raw slice of a pages `router.query` bag for the search half
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Raw slice of a pages `router.query` bag for the search half. A codec-map
|
|
19
|
+
* route reads exactly its declared keys (query junk and the route's own path
|
|
20
|
+
* params are invisible to the decode — the two namespaces are disjoint); a
|
|
21
21
|
* `rawSearch` route has no enumerable declared-key set — the schema sees
|
|
22
|
-
* every key except the route's path params (
|
|
23
|
-
* exactly that slice is fingerprinted, in sorted-key order for
|
|
24
|
-
* independence.
|
|
22
|
+
* every key except the route's path params (by subtracting them from the
|
|
23
|
+
* bag), so exactly that slice is fingerprinted, in sorted-key order for
|
|
24
|
+
* record-order independence.
|
|
25
25
|
*/
|
|
26
26
|
export function queryFingerprint(route, query) {
|
|
27
27
|
const declared = declaredSearchKeys(route);
|
|
@@ -34,7 +34,7 @@ export function queryFingerprint(route, query) {
|
|
|
34
34
|
return recordFingerprint(keys, query);
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Raw slice of an app `useSearchParams()` source
|
|
37
|
+
* Raw slice of an app `useSearchParams()` source: the declared keys'
|
|
38
38
|
* `[key, value]` pairs in wire order — order is load-bearing for repeated
|
|
39
39
|
* keys (array codecs decode in wire order, P5/S5), and iterating the live
|
|
40
40
|
* pairs preserves the relative order of declared entries while `?utm_*`
|
|
@@ -62,15 +62,15 @@ export function useSelectedResult(result, options) {
|
|
|
62
62
|
return result;
|
|
63
63
|
const previous = cache.current;
|
|
64
64
|
if (previous !== null && Object.is(previous.input, result.data)) {
|
|
65
|
-
// Reference-stable input ⇒ equal output by selector purity
|
|
66
|
-
//
|
|
65
|
+
// Reference-stable input ⇒ equal output by selector purity; the selector
|
|
66
|
+
// is deliberately not re-run.
|
|
67
67
|
return previous.wrapped;
|
|
68
68
|
}
|
|
69
|
-
const selected = options.select(result.data); // a throw propagates
|
|
69
|
+
const selected = options.select(result.data); // a throw propagates
|
|
70
70
|
if (previous !== null &&
|
|
71
71
|
selectedEquals(options.equality, previous.wrapped.data, selected)) {
|
|
72
|
-
// Same selection out of a new decode: keep the previous wrapper
|
|
73
|
-
//
|
|
72
|
+
// Same selection out of a new decode: keep the previous wrapper and
|
|
73
|
+
// re-key the cache so the next render takes the reference fast path.
|
|
74
74
|
previous.input = result.data;
|
|
75
75
|
return previous.wrapped;
|
|
76
76
|
}
|
|
@@ -82,8 +82,8 @@ export function useSelectedResult(result, options) {
|
|
|
82
82
|
return wrapped;
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
|
-
* {@link useSelectedResult}'s twin for the `*OrThrow` hooks
|
|
86
|
-
*
|
|
85
|
+
* {@link useSelectedResult}'s twin for the `*OrThrow` hooks: same layering,
|
|
86
|
+
* no wrapper — the hook's return IS the (selected) value.
|
|
87
87
|
*/
|
|
88
88
|
export function useSelectedValue(value, options) {
|
|
89
89
|
const cache = useRef(null);
|
|
@@ -91,9 +91,9 @@ export function useSelectedValue(value, options) {
|
|
|
91
91
|
return value;
|
|
92
92
|
const previous = cache.current;
|
|
93
93
|
if (previous !== null && Object.is(previous.input, value)) {
|
|
94
|
-
return previous.selected; //
|
|
94
|
+
return previous.selected; // selector purity: not re-run
|
|
95
95
|
}
|
|
96
|
-
const selected = options.select(value); // a throw propagates
|
|
96
|
+
const selected = options.select(value); // a throw propagates
|
|
97
97
|
if (previous !== null &&
|
|
98
98
|
selectedEquals(options.equality, previous.selected, selected)) {
|
|
99
99
|
previous.input = value;
|
|
@@ -103,12 +103,12 @@ export function useSelectedValue(value, options) {
|
|
|
103
103
|
return selected;
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
|
-
* The raw-slice stabilization layer
|
|
107
|
-
*
|
|
106
|
+
* The raw-slice stabilization layer: while `route` and `fingerprint` are
|
|
107
|
+
* unchanged from the previous render, the previous result — success OR
|
|
108
108
|
* error arm — is returned without recomputing, so a fresh `useSearchParams()`
|
|
109
109
|
* / `query` object whose DECLARED slice is unchanged (`?utm_source=` churn)
|
|
110
110
|
* costs neither a decode nor anyone's referential equality. This replaces
|
|
111
|
-
* the
|
|
111
|
+
* the earlier "memo keyed on Next's object reference" behavior.
|
|
112
112
|
*/
|
|
113
113
|
export function useStableResult(route, fingerprint, compute) {
|
|
114
114
|
const cache = useRef(null);
|
|
@@ -120,7 +120,7 @@ export function useStableResult(route, fingerprint, compute) {
|
|
|
120
120
|
throw cached.outcome.thrown;
|
|
121
121
|
return cached.outcome.value;
|
|
122
122
|
}
|
|
123
|
-
// Cleared BEFORE computing
|
|
123
|
+
// Cleared BEFORE computing: no half-computed state may survive a
|
|
124
124
|
// throw, and a NEW fingerprint always recomputes — an error boundary
|
|
125
125
|
// reset after the URL is fixed can never be served a stale entry.
|
|
126
126
|
cache.current = null;
|
|
@@ -152,7 +152,7 @@ export function useStableResult(route, fingerprint, compute) {
|
|
|
152
152
|
* Declared search keys of a route's `~search` slot, or `null` for a
|
|
153
153
|
* `rawSearch` route (whose schema owns every key, so no declared subset
|
|
154
154
|
* exists). The `~kind` marker is unambiguous against a codec map, which
|
|
155
|
-
* never carries a top-level `~`-prefixed key (
|
|
155
|
+
* never carries a top-level `~`-prefixed key (SS2).
|
|
156
156
|
*/
|
|
157
157
|
function declaredSearchKeys(route) {
|
|
158
158
|
const config = route["~search"];
|
|
@@ -185,16 +185,16 @@ function recordFingerprint(keys, source) {
|
|
|
185
185
|
Object.hasOwn(source, key) ? (source[key] ?? null) : null,
|
|
186
186
|
]));
|
|
187
187
|
}
|
|
188
|
-
/**
|
|
188
|
+
/** Result equality: `Object.is`, widened one level by `"shallow"`. */
|
|
189
189
|
function selectedEquals(equality, a, b) {
|
|
190
190
|
if (Object.is(a, b))
|
|
191
191
|
return true;
|
|
192
192
|
return equality === "shallow" && shallowEqual(a, b);
|
|
193
193
|
}
|
|
194
194
|
/**
|
|
195
|
-
* One-level equality for the `"shallow"` opt-in
|
|
196
|
-
*
|
|
197
|
-
* recursive (deep comparison in a render path is a non-goal
|
|
195
|
+
* One-level equality for the `"shallow"` opt-in: arrays element-wise, plain
|
|
196
|
+
* objects by own enumerable keys — `Object.is` at each leaf, nothing
|
|
197
|
+
* recursive (deep comparison in a render path is a non-goal).
|
|
198
198
|
*/
|
|
199
199
|
function shallowEqual(a, b) {
|
|
200
200
|
if (typeof a !== "object" ||
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DoctorCheck } from "../doctor/checks.js";
|
|
2
|
+
/**
|
|
3
|
+
* Doctor's skills battery. Intent comes from manifest presence — doctor is
|
|
4
|
+
* passive and must not nag a project that never opted into skills — unlike
|
|
5
|
+
* `skills --check`, whose intent is the detection result (it is added to CI
|
|
6
|
+
* deliberately). Findings are warn-level, never fail: stale agent guidance
|
|
7
|
+
* degrades agent output but breaks nothing at build or runtime, and a fail
|
|
8
|
+
* here would flip doctor's exit to 1 in every consumer the day after every
|
|
9
|
+
* paramour release. The CI-fatal surface is `paramour skills --check`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function skillsDoctorChecks(projectRoot: string): DoctorCheck[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { message } from "../cli-io.js";
|
|
2
|
+
import { loadPackagedSkill } from "./packaged.js";
|
|
3
|
+
import { auditTarget, FILE_STATUS_DETAIL, isOutdated } from "./sync.js";
|
|
4
|
+
import { detectTargets } from "./targets.js";
|
|
5
|
+
/**
|
|
6
|
+
* Doctor's skills battery. Intent comes from manifest presence — doctor is
|
|
7
|
+
* passive and must not nag a project that never opted into skills — unlike
|
|
8
|
+
* `skills --check`, whose intent is the detection result (it is added to CI
|
|
9
|
+
* deliberately). Findings are warn-level, never fail: stale agent guidance
|
|
10
|
+
* degrades agent output but breaks nothing at build or runtime, and a fail
|
|
11
|
+
* here would flip doctor's exit to 1 in every consumer the day after every
|
|
12
|
+
* paramour release. The CI-fatal surface is `paramour skills --check`.
|
|
13
|
+
*/
|
|
14
|
+
export function skillsDoctorChecks(projectRoot) {
|
|
15
|
+
try {
|
|
16
|
+
const detected = detectTargets(projectRoot);
|
|
17
|
+
if (detected.length === 0) {
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
label: "skills: no agent tooling detected — skipped",
|
|
21
|
+
status: "pass",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
const packaged = loadPackagedSkill();
|
|
26
|
+
const audits = detected
|
|
27
|
+
.map((target) => auditTarget(target, packaged))
|
|
28
|
+
.filter((audit) => audit.manifest !== undefined);
|
|
29
|
+
if (audits.length === 0) {
|
|
30
|
+
// Pass with an advisory label (check 1's "defaults in effect"
|
|
31
|
+
// precedent): declining skills is a legitimate steady state.
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
label: `skills: not installed (\`paramour skills\` installs agent skills for ${detected
|
|
35
|
+
.map((target) => `${target.rel.split("/")[0] ?? ""}/`)
|
|
36
|
+
.join(", ")})`,
|
|
37
|
+
status: "pass",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
return audits.map((audit) => {
|
|
42
|
+
const detail = audit.files
|
|
43
|
+
.map((file) => {
|
|
44
|
+
const note = FILE_STATUS_DETAIL[file.status];
|
|
45
|
+
return note === undefined ? undefined : `${file.relPath}: ${note}`;
|
|
46
|
+
})
|
|
47
|
+
.filter((line) => line !== undefined);
|
|
48
|
+
if (detail.length === 0) {
|
|
49
|
+
return {
|
|
50
|
+
label: `skills: ${audit.target.rel} is up to date`,
|
|
51
|
+
status: "pass",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const outdated = audit.files.some((file) => isOutdated(file.status));
|
|
55
|
+
return {
|
|
56
|
+
detail,
|
|
57
|
+
label: `skills: ${audit.target.rel} ${outdated ? "is stale" : "has local edits"}`,
|
|
58
|
+
status: "warn",
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return [
|
|
64
|
+
{
|
|
65
|
+
detail: [message(error)],
|
|
66
|
+
label: "skills: could not audit installed skills",
|
|
67
|
+
status: "warn",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sidecar stamp `paramour skills` writes next to every installed copy.
|
|
3
|
+
* It lives inside the installed skill directory (not at the project root) so
|
|
4
|
+
* deleting a tool directory is a clean uninstall — no orphaned record keeps
|
|
5
|
+
* reporting the skill as missing after a deliberate removal. Skill loaders
|
|
6
|
+
* ignore dotfiles under progressive disclosure, so the installed skill files
|
|
7
|
+
* themselves stay byte-identical to the published ones.
|
|
8
|
+
*/
|
|
9
|
+
export interface SkillManifest {
|
|
10
|
+
/** POSIX-relative path → `sha256:<hex>` of the content last synced. */
|
|
11
|
+
files: Record<string, string>;
|
|
12
|
+
skill: "paramour";
|
|
13
|
+
/**
|
|
14
|
+
* The `@paramour-js/next` version that last wrote this manifest.
|
|
15
|
+
* Informational only: staleness truth is always hash comparison, so a
|
|
16
|
+
* version bump without content changes never reads as stale.
|
|
17
|
+
*/
|
|
18
|
+
version: string;
|
|
19
|
+
}
|
|
20
|
+
export declare const MANIFEST_FILENAME = ".paramour-skills.json";
|
|
21
|
+
/**
|
|
22
|
+
* CRLF-normalized sha256. Normalizing before hashing makes every status
|
|
23
|
+
* computation immune to a consumer repo's git `autocrlf` rewriting the
|
|
24
|
+
* installed markdown on checkout — a line-ending flip is not a content edit.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hashContent(text: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Read a target's manifest; `undefined` when absent or malformed. A corrupt
|
|
29
|
+
* manifest degrades to "no provenance": byte-identical files still audit as
|
|
30
|
+
* fresh and anything else audits as locally modified, which the installer
|
|
31
|
+
* refuses to overwrite without `--force` — the safe direction. A manifest
|
|
32
|
+
* containing any file key that could escape the skill directory is treated
|
|
33
|
+
* as corrupt the same way, since those keys become deletion paths in sync.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readSkillManifest(skillDir: string): SkillManifest | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Deterministic serialization: sorted file keys, two-space indent, LF,
|
|
38
|
+
* trailing newline, no timestamps — same doctrine as the generated artifact,
|
|
39
|
+
* so a re-run that changes nothing writes nothing.
|
|
40
|
+
*/
|
|
41
|
+
export declare function renderManifest(manifest: SkillManifest): string;
|