@paramour-js/next 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin/paramour.js +19 -0
- package/dist/app.d.ts +76 -0
- package/dist/app.js +32 -0
- package/dist/cli-args.d.ts +28 -0
- package/dist/cli-args.js +35 -0
- package/dist/cli-inputs.d.ts +32 -0
- package/dist/cli-inputs.js +80 -0
- package/dist/cli-io.d.ts +15 -0
- package/dist/cli-io.js +16 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +5 -0
- package/dist/collisions.d.ts +37 -0
- package/dist/collisions.js +80 -0
- package/dist/commands/doctor.d.ts +7 -0
- package/dist/commands/doctor.js +56 -0
- package/dist/commands/generate.d.ts +11 -0
- package/dist/commands/generate.js +222 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +205 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.js +94 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +99 -0
- package/dist/doctor/checks.d.ts +16 -0
- package/dist/doctor/checks.js +231 -0
- package/dist/emit.d.ts +35 -0
- package/dist/emit.js +74 -0
- package/dist/generate.d.ts +70 -0
- package/dist/generate.js +106 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/init/scaffold.d.ts +39 -0
- package/dist/init/scaffold.js +244 -0
- package/dist/init/wrap-next-config.d.ts +41 -0
- package/dist/init/wrap-next-config.js +99 -0
- package/dist/list/discover-route-defs.d.ts +52 -0
- package/dist/list/discover-route-defs.js +121 -0
- package/dist/list/render.d.ts +35 -0
- package/dist/list/render.js +132 -0
- package/dist/lock.d.ts +29 -0
- package/dist/lock.js +88 -0
- package/dist/pages.d.ts +49 -0
- package/dist/pages.js +62 -0
- package/dist/run-cli.d.ts +11 -0
- package/dist/run-cli.js +53 -0
- package/dist/scan-app.d.ts +30 -0
- package/dist/scan-app.js +149 -0
- package/dist/scan-pages.d.ts +10 -0
- package/dist/scan-pages.js +102 -0
- package/dist/scan.d.ts +32 -0
- package/dist/scan.js +77 -0
- package/dist/select.d.ts +94 -0
- package/dist/select.js +195 -0
- package/dist/watch.d.ts +39 -0
- package/dist/watch.js +87 -0
- package/dist/with-typed-routes.d.ts +44 -0
- package/dist/with-typed-routes.js +200 -0
- package/package.json +67 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One `paramour doctor` finding. `fail` means a verification the user cares
|
|
3
|
+
* about is untrue (exit 1, same class as check-drift); `warn` is advisory
|
|
4
|
+
* and never affects the exit code.
|
|
5
|
+
*/
|
|
6
|
+
export interface DoctorCheck {
|
|
7
|
+
detail?: string[];
|
|
8
|
+
label: string;
|
|
9
|
+
status: "fail" | "pass" | "warn";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* @internal The check battery, in report order. Each check degrades
|
|
13
|
+
* independently — doctor exists to diagnose broken setups, so a throwing
|
|
14
|
+
* probe becomes a finding, never a crash.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runDoctorChecks(projectRoot: string): Promise<DoctorCheck[]>;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { resolveInputs } from "../cli-inputs.js";
|
|
4
|
+
import { message } from "../cli-io.js";
|
|
5
|
+
import { loadConfigFile } from "../config.js";
|
|
6
|
+
import { checkArtifact, formatRouteDiff, } from "../generate.js";
|
|
7
|
+
import { tsconfigCheck } from "../init/scaffold.js";
|
|
8
|
+
import { detectWrapState, findNextConfig } from "../init/wrap-next-config.js";
|
|
9
|
+
import { discoverRouteDefinitions, routeKey, } from "../list/discover-route-defs.js";
|
|
10
|
+
import { scanRoutes } from "../scan.js";
|
|
11
|
+
/**
|
|
12
|
+
* @internal The check battery, in report order. Each check degrades
|
|
13
|
+
* independently — doctor exists to diagnose broken setups, so a throwing
|
|
14
|
+
* probe becomes a finding, never a crash.
|
|
15
|
+
*/
|
|
16
|
+
export async function runDoctorChecks(projectRoot) {
|
|
17
|
+
const checks = [];
|
|
18
|
+
// 1. Config file parses and validates.
|
|
19
|
+
let config = {};
|
|
20
|
+
try {
|
|
21
|
+
const loaded = await loadConfigFile(projectRoot);
|
|
22
|
+
config = loaded?.config ?? {};
|
|
23
|
+
checks.push({
|
|
24
|
+
label: loaded === undefined
|
|
25
|
+
? "config: no paramour.config file — defaults in effect"
|
|
26
|
+
: `config: ${basename(loaded.path)} is valid`,
|
|
27
|
+
status: "pass",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
checks.push({
|
|
32
|
+
detail: [message(error)],
|
|
33
|
+
label: "config: invalid",
|
|
34
|
+
status: "fail",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// 2. Route directories resolve (PR8 discovery, config dirs honored).
|
|
38
|
+
let inputs;
|
|
39
|
+
try {
|
|
40
|
+
inputs = await resolveInputs({}, projectRoot, config);
|
|
41
|
+
const dirs = [inputs.appDir, inputs.pagesDir]
|
|
42
|
+
.filter((dir) => dir !== undefined)
|
|
43
|
+
.map((dir) => `${relative(projectRoot, dir).replaceAll("\\", "/")}/`);
|
|
44
|
+
checks.push({
|
|
45
|
+
label: `route directories: ${dirs.join(", ")}`,
|
|
46
|
+
status: "pass",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
checks.push({
|
|
51
|
+
detail: [message(error)],
|
|
52
|
+
label: "route directories: not found",
|
|
53
|
+
status: "fail",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// 3. Artifact exists and is current (the `check` engine).
|
|
57
|
+
let routes;
|
|
58
|
+
if (inputs === undefined) {
|
|
59
|
+
checks.push({
|
|
60
|
+
label: "artifact: skipped (no route directories)",
|
|
61
|
+
status: "warn",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const artifactRel = relative(projectRoot, inputs.artifactPath).replaceAll("\\", "/");
|
|
66
|
+
try {
|
|
67
|
+
routes = scanRoutes(inputs, inputs.pageExtensions);
|
|
68
|
+
const result = checkArtifact(inputs);
|
|
69
|
+
if (result.upToDate) {
|
|
70
|
+
checks.push({
|
|
71
|
+
label: `artifact: ${artifactRel} is up to date`,
|
|
72
|
+
status: "pass",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
checks.push({
|
|
77
|
+
detail: [
|
|
78
|
+
...formatRouteDiff(result.app, result.pages),
|
|
79
|
+
"run `paramour generate` and commit the result",
|
|
80
|
+
],
|
|
81
|
+
label: result.missingFile
|
|
82
|
+
? `artifact: ${artifactRel} is missing`
|
|
83
|
+
: `artifact: ${artifactRel} is out of date`,
|
|
84
|
+
status: "fail",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
checks.push({
|
|
90
|
+
detail: [message(error)],
|
|
91
|
+
label: "artifact: check failed",
|
|
92
|
+
status: "fail",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// 4. next.config wraps withTypedRoutes — warn-level: CLI-only workflows
|
|
97
|
+
// (generate in a package script, check in CI) are legitimate.
|
|
98
|
+
const nextConfig = findNextConfig(projectRoot);
|
|
99
|
+
if (nextConfig === undefined) {
|
|
100
|
+
checks.push({
|
|
101
|
+
detail: ["`paramour init` can create and wrap one"],
|
|
102
|
+
label: "next.config: none found",
|
|
103
|
+
status: "warn",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const name = basename(nextConfig.path);
|
|
108
|
+
try {
|
|
109
|
+
const state = await detectWrapState(readFileSync(nextConfig.path, "utf8"));
|
|
110
|
+
if (state === "wrapped") {
|
|
111
|
+
checks.push({
|
|
112
|
+
label: `next.config: ${name} wraps withTypedRoutes`,
|
|
113
|
+
status: "pass",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
checks.push({
|
|
118
|
+
detail: [
|
|
119
|
+
state === "unparseable"
|
|
120
|
+
? "could not parse it to verify"
|
|
121
|
+
: "dev/build auto-regeneration is off; `paramour init` can wrap it (CLI-only workflows are fine)",
|
|
122
|
+
],
|
|
123
|
+
label: `next.config: ${name} does not wrap withTypedRoutes`,
|
|
124
|
+
status: "warn",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
checks.push({
|
|
130
|
+
detail: [message(error)],
|
|
131
|
+
label: `next.config: could not read ${name}`,
|
|
132
|
+
status: "warn",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// 5. Version alignment between the two packages.
|
|
137
|
+
checks.push(versionCheck(projectRoot));
|
|
138
|
+
// 6. tsconfig covers the artifact (init's warn-level heuristic).
|
|
139
|
+
// resolve, not join — an absolute outFile must win, as it does in
|
|
140
|
+
// resolveInputs.
|
|
141
|
+
const artifactPath = inputs?.artifactPath ??
|
|
142
|
+
resolve(projectRoot, config.outFile ?? "paramour-env.d.ts");
|
|
143
|
+
const coverage = tsconfigCheck(projectRoot, artifactPath);
|
|
144
|
+
checks.push({
|
|
145
|
+
...(coverage.detail === undefined ? {} : { detail: [coverage.detail] }),
|
|
146
|
+
label: `tsconfig: ${coverage.label}`,
|
|
147
|
+
status: coverage.ok ? "pass" : "warn",
|
|
148
|
+
});
|
|
149
|
+
// 7. Route-definition discovery health (list's engine).
|
|
150
|
+
checks.push(await discoveryCheck(projectRoot, config, routes));
|
|
151
|
+
return checks;
|
|
152
|
+
}
|
|
153
|
+
async function discoveryCheck(projectRoot, config, routes) {
|
|
154
|
+
try {
|
|
155
|
+
const discovery = await discoverRouteDefinitions(projectRoot, {
|
|
156
|
+
routeFiles: config.routeFiles,
|
|
157
|
+
});
|
|
158
|
+
const files = new Set(discovery.definitions.map((definition) => definition.file));
|
|
159
|
+
const detail = [];
|
|
160
|
+
if (routes !== undefined) {
|
|
161
|
+
const defined = new Set(discovery.definitions.map((definition) => routeKey(definition.route["~router"], definition.route.path)));
|
|
162
|
+
const all = [
|
|
163
|
+
...routes.appRoutes.map((path) => routeKey("app", path)),
|
|
164
|
+
...routes.pagesRoutes.map((path) => routeKey("pages", path)),
|
|
165
|
+
];
|
|
166
|
+
const covered = all.filter((key) => defined.has(key)).length;
|
|
167
|
+
detail.push(`${String(covered)} of ${String(all.length)} filesystem routes have definitions`);
|
|
168
|
+
}
|
|
169
|
+
for (const failure of discovery.loadFailures) {
|
|
170
|
+
detail.push(`failed to load ${failure.file}: ${failure.message}`);
|
|
171
|
+
}
|
|
172
|
+
for (const duplicate of discovery.duplicates) {
|
|
173
|
+
detail.push(`duplicate definition of ${duplicate.path} (${duplicate.router}) in ${duplicate.file} — ${duplicate.firstFile} wins`);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
detail,
|
|
177
|
+
label: `route definitions: ${String(discovery.definitions.length)} found in ${String(files.size)} module${files.size === 1 ? "" : "s"}`,
|
|
178
|
+
status: discovery.loadFailures.length > 0 || discovery.duplicates.length > 0
|
|
179
|
+
? "warn"
|
|
180
|
+
: "pass",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
detail: [message(error)],
|
|
186
|
+
label: "route definitions: discovery failed",
|
|
187
|
+
status: "warn",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function readVersion(projectRoot, name) {
|
|
192
|
+
// Walks upward like Node resolution: workspaces hoist dependencies to a
|
|
193
|
+
// parent node_modules, so a single project-root read hard-fails healthy
|
|
194
|
+
// monorepo setups.
|
|
195
|
+
for (let dir = projectRoot;; dir = dirname(dir)) {
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(readFileSync(join(dir, "node_modules", ...name.split("/"), "package.json"), "utf8"));
|
|
198
|
+
return typeof parsed.version === "string" ? parsed.version : undefined;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
if (dirname(dir) === dir)
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function versionCheck(projectRoot) {
|
|
207
|
+
const core = readVersion(projectRoot, "paramour");
|
|
208
|
+
const next = readVersion(projectRoot, "@paramour-js/next");
|
|
209
|
+
const missing = [
|
|
210
|
+
...(next === undefined ? ["@paramour-js/next"] : []),
|
|
211
|
+
...(core === undefined ? ["paramour"] : []),
|
|
212
|
+
];
|
|
213
|
+
if (missing.length > 0) {
|
|
214
|
+
return {
|
|
215
|
+
detail: ["install dependencies and re-run"],
|
|
216
|
+
label: `versions: ${missing.join(", ")} not resolvable in node_modules`,
|
|
217
|
+
status: "fail",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (core !== next) {
|
|
221
|
+
return {
|
|
222
|
+
detail: ["align the two versions — they release in lockstep"],
|
|
223
|
+
label: `versions: paramour ${core ?? ""} != @paramour-js/next ${next ?? ""}`,
|
|
224
|
+
status: "warn",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
label: `versions: paramour and @paramour-js/next are both ${core ?? ""}`,
|
|
229
|
+
status: "pass",
|
|
230
|
+
};
|
|
231
|
+
}
|
package/dist/emit.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Result of {@link writeIfChanged}. */
|
|
2
|
+
export interface WriteIfChangedResult {
|
|
3
|
+
/**
|
|
4
|
+
* Prior file content, or `null` when the file did not exist — the input
|
|
5
|
+
* for TR4's drift warning (which paths appeared/disappeared).
|
|
6
|
+
*/
|
|
7
|
+
previousContent: null | string;
|
|
8
|
+
/**
|
|
9
|
+
* `false` on a byte-identical no-op — the signal `--check` (TR7) and the
|
|
10
|
+
* watch loop (TR5) hang off.
|
|
11
|
+
*/
|
|
12
|
+
written: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Input of {@link emitArtifact} — one union per router (PR9). */
|
|
15
|
+
export interface EmitRoutes {
|
|
16
|
+
appRoutes: readonly string[];
|
|
17
|
+
pagesRoutes: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Artifact text for the per-router route unions (TR3/PR9): deterministic —
|
|
21
|
+
* sorted, deduped, LF-only, trailing newline, no timestamps. Sorting/deduping
|
|
22
|
+
* happens here as well as in the scanner so byte-identity never depends on
|
|
23
|
+
* who the caller was. Always the leading-pipe multiline union form, even for
|
|
24
|
+
* one path — one shape, no count-dependent formatting. An empty union omits
|
|
25
|
+
* its member entirely (TR3's absent-not-`never` rule, applied per router in
|
|
26
|
+
* PR9): a pages-only project keeps world-A's permissive `string` fallback
|
|
27
|
+
* for `defineAppRoute`, and vice versa.
|
|
28
|
+
*/
|
|
29
|
+
export declare function emitArtifact(routes: EmitRoutes): string;
|
|
30
|
+
/**
|
|
31
|
+
* Compare-before-write (TR3): a no-op regeneration must not touch the file —
|
|
32
|
+
* that property is what prevents TS-server churn, watch-loop feedback, and
|
|
33
|
+
* concurrent-generator races (TR6).
|
|
34
|
+
*/
|
|
35
|
+
export declare function writeIfChanged(filePath: string, content: string): WriteIfChangedResult;
|
package/dist/emit.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
const HEADER = "// Generated by @paramour-js/next. Do not edit — regenerate with `paramour generate`.";
|
|
4
|
+
/**
|
|
5
|
+
* Artifact text for the per-router route unions (TR3/PR9): deterministic —
|
|
6
|
+
* sorted, deduped, LF-only, trailing newline, no timestamps. Sorting/deduping
|
|
7
|
+
* happens here as well as in the scanner so byte-identity never depends on
|
|
8
|
+
* who the caller was. Always the leading-pipe multiline union form, even for
|
|
9
|
+
* one path — one shape, no count-dependent formatting. An empty union omits
|
|
10
|
+
* its member entirely (TR3's absent-not-`never` rule, applied per router in
|
|
11
|
+
* PR9): a pages-only project keeps world-A's permissive `string` fallback
|
|
12
|
+
* for `defineAppRoute`, and vice versa.
|
|
13
|
+
*/
|
|
14
|
+
export function emitArtifact(routes) {
|
|
15
|
+
const members = ["appRoutes", "pagesRoutes"].flatMap((member) => {
|
|
16
|
+
const sorted = [...new Set(routes[member])].sort();
|
|
17
|
+
if (sorted.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
// The union's closing `;` attaches to the last member line. Paths go
|
|
20
|
+
// through JSON.stringify, not raw `"${path}"`: a dirname may legally
|
|
21
|
+
// contain a quote or backslash (Linux/macOS), which unescaped would emit
|
|
22
|
+
// invalid TS or silently declare the wrong route. JSON string escaping is
|
|
23
|
+
// a subset of TS string-literal escaping (output stays valid TS) and is
|
|
24
|
+
// deterministic, so TR3's byte-identity guarantee holds.
|
|
25
|
+
return [
|
|
26
|
+
` ${member}:`,
|
|
27
|
+
`${sorted.map((path) => ` | ${JSON.stringify(path)}`).join("\n")};`,
|
|
28
|
+
];
|
|
29
|
+
});
|
|
30
|
+
if (members.length === 0) {
|
|
31
|
+
// TR3: no routes yet → NO members (explicitly not `never`, which would
|
|
32
|
+
// make every route-constructor call an error). The empty merge keeps
|
|
33
|
+
// the documented world-A `string` fallback (RL8).
|
|
34
|
+
return [
|
|
35
|
+
HEADER,
|
|
36
|
+
'import "paramour";',
|
|
37
|
+
"",
|
|
38
|
+
'declare module "paramour" {',
|
|
39
|
+
" // TR3: empty merge — no routes discovered yet; the pre-generation",
|
|
40
|
+
" // `string` fallback stays in effect.",
|
|
41
|
+
" // eslint-disable-next-line @typescript-eslint/no-empty-object-type",
|
|
42
|
+
" interface ParamourRegister {}",
|
|
43
|
+
"}",
|
|
44
|
+
"",
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
return [
|
|
48
|
+
HEADER,
|
|
49
|
+
'import "paramour";',
|
|
50
|
+
"",
|
|
51
|
+
'declare module "paramour" {',
|
|
52
|
+
" interface ParamourRegister {",
|
|
53
|
+
...members,
|
|
54
|
+
" }",
|
|
55
|
+
"}",
|
|
56
|
+
"",
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Compare-before-write (TR3): a no-op regeneration must not touch the file —
|
|
61
|
+
* that property is what prevents TS-server churn, watch-loop feedback, and
|
|
62
|
+
* concurrent-generator races (TR6).
|
|
63
|
+
*/
|
|
64
|
+
export function writeIfChanged(filePath, content) {
|
|
65
|
+
const previousContent = existsSync(filePath)
|
|
66
|
+
? readFileSync(filePath, "utf8")
|
|
67
|
+
: null;
|
|
68
|
+
if (previousContent === content)
|
|
69
|
+
return { previousContent, written: false };
|
|
70
|
+
// TR3's outFile escape hatch may point into a not-yet-existing directory.
|
|
71
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
72
|
+
writeFileSync(filePath, content);
|
|
73
|
+
return { previousContent, written: true };
|
|
74
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared generation engine (TR9): `withTypedRoutes` and the CLI drive
|
|
3
|
+
* these same functions, so wrapper and `paramour generate` cannot drift on
|
|
4
|
+
* what a pass produces. Everything here is @internal — not barrel API.
|
|
5
|
+
*/
|
|
6
|
+
/** Result of {@link checkArtifact}. */
|
|
7
|
+
export interface CheckResult {
|
|
8
|
+
/** App-router drift — split per router so the report names it (PR9). */
|
|
9
|
+
app: RouterDrift;
|
|
10
|
+
/** `true` when the artifact file does not exist at all. */
|
|
11
|
+
missingFile: boolean;
|
|
12
|
+
/** Pages-router drift. */
|
|
13
|
+
pages: RouterDrift;
|
|
14
|
+
/** `true` on a byte-identical artifact — the only non-drift state. */
|
|
15
|
+
upToDate: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Inputs to one generation/check pass; either dir may be absent (PR1). */
|
|
18
|
+
export interface GenerateInputs {
|
|
19
|
+
appDir?: string | undefined;
|
|
20
|
+
artifactPath: string;
|
|
21
|
+
pageExtensions: readonly string[];
|
|
22
|
+
pagesDir?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Result of {@link generate}. */
|
|
25
|
+
export interface GenerateResult {
|
|
26
|
+
/** The freshly scanned app-route union (sorted). */
|
|
27
|
+
appRoutes: string[];
|
|
28
|
+
/** The freshly scanned pages-route union (sorted). */
|
|
29
|
+
pagesRoutes: string[];
|
|
30
|
+
/** Prior artifact content, `null` when the file did not exist. */
|
|
31
|
+
previousContent: null | string;
|
|
32
|
+
/** `false` on a byte-identical no-op (TR3 write-if-changed). */
|
|
33
|
+
written: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** One router's appeared/disappeared route paths (TR4/TR7 drift). */
|
|
36
|
+
export interface RouterDrift {
|
|
37
|
+
/** Routes on disk (scan) that the artifact lacks. */
|
|
38
|
+
appeared: string[];
|
|
39
|
+
/** Routes in the artifact that no longer exist on disk. */
|
|
40
|
+
disappeared: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `--check` (TR7): scan to memory and byte-compare against disk — never
|
|
44
|
+
* writes. A missing artifact is drift, not an error: that is exactly the
|
|
45
|
+
* CI-degrades-to-world-A case the committed file exists to prevent (TR3).
|
|
46
|
+
*/
|
|
47
|
+
export declare function checkArtifact(inputs: GenerateInputs): CheckResult;
|
|
48
|
+
/**
|
|
49
|
+
* Per-router drift of a completed {@link generate} pass against the artifact
|
|
50
|
+
* it replaced — the wrapper's build-phase drift report (TR4).
|
|
51
|
+
*/
|
|
52
|
+
export declare function diffGenerated(result: GenerateResult): {
|
|
53
|
+
app: RouterDrift;
|
|
54
|
+
pages: RouterDrift;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The ` + /new (app)` / ` - /old (pages)` lines of a drift report
|
|
58
|
+
* (TR4/TR7) — each line names the router its path moved in (PR9).
|
|
59
|
+
*/
|
|
60
|
+
export declare function formatRouteDiff(app: RouterDrift, pages: RouterDrift): string[];
|
|
61
|
+
/** One generation pass: scan → emit → write-if-changed (TR3). */
|
|
62
|
+
export declare function generate(inputs: GenerateInputs): GenerateResult;
|
|
63
|
+
/**
|
|
64
|
+
* Per-router route paths in a previously emitted artifact; both empty for a
|
|
65
|
+
* missing file or the empty merge.
|
|
66
|
+
*/
|
|
67
|
+
export declare function parseArtifactRoutes(previousContent: null | string): {
|
|
68
|
+
appRoutes: Set<string>;
|
|
69
|
+
pagesRoutes: Set<string>;
|
|
70
|
+
};
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { emitArtifact, writeIfChanged } from "./emit.js";
|
|
3
|
+
import { scanRoutes } from "./scan.js";
|
|
4
|
+
/**
|
|
5
|
+
* Reads the per-router unions back out of a previously emitted artifact for
|
|
6
|
+
* drift diffs (TR4/TR7). Only ever applied to text this package generated
|
|
7
|
+
* (TR3 deterministic form), so line-anchored matches on the member headers
|
|
8
|
+
* and union members are exact, not heuristic. `\s*$` on both tolerates a
|
|
9
|
+
* CRLF-resaved artifact.
|
|
10
|
+
*/
|
|
11
|
+
const MEMBER_HEADER = /^\s*(appRoutes|pagesRoutes):\s*$/;
|
|
12
|
+
const UNION_MEMBER = /^\s*\| "(.*)";?\s*$/;
|
|
13
|
+
/**
|
|
14
|
+
* `--check` (TR7): scan to memory and byte-compare against disk — never
|
|
15
|
+
* writes. A missing artifact is drift, not an error: that is exactly the
|
|
16
|
+
* CI-degrades-to-world-A case the committed file exists to prevent (TR3).
|
|
17
|
+
*/
|
|
18
|
+
export function checkArtifact(inputs) {
|
|
19
|
+
const routes = scanRoutes(inputs, inputs.pageExtensions);
|
|
20
|
+
const expected = emitArtifact(routes);
|
|
21
|
+
const current = existsSync(inputs.artifactPath)
|
|
22
|
+
? readFileSync(inputs.artifactPath, "utf8")
|
|
23
|
+
: null;
|
|
24
|
+
if (current === expected) {
|
|
25
|
+
return {
|
|
26
|
+
app: { appeared: [], disappeared: [] },
|
|
27
|
+
missingFile: false,
|
|
28
|
+
pages: { appeared: [], disappeared: [] },
|
|
29
|
+
upToDate: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const previous = parseArtifactRoutes(current);
|
|
33
|
+
return {
|
|
34
|
+
app: diffRouter(routes.appRoutes, previous.appRoutes),
|
|
35
|
+
missingFile: current === null,
|
|
36
|
+
pages: diffRouter(routes.pagesRoutes, previous.pagesRoutes),
|
|
37
|
+
upToDate: false,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Per-router drift of a completed {@link generate} pass against the artifact
|
|
42
|
+
* it replaced — the wrapper's build-phase drift report (TR4).
|
|
43
|
+
*/
|
|
44
|
+
export function diffGenerated(result) {
|
|
45
|
+
const previous = parseArtifactRoutes(result.previousContent);
|
|
46
|
+
return {
|
|
47
|
+
app: diffRouter(result.appRoutes, previous.appRoutes),
|
|
48
|
+
pages: diffRouter(result.pagesRoutes, previous.pagesRoutes),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The ` + /new (app)` / ` - /old (pages)` lines of a drift report
|
|
53
|
+
* (TR4/TR7) — each line names the router its path moved in (PR9).
|
|
54
|
+
*/
|
|
55
|
+
export function formatRouteDiff(app, pages) {
|
|
56
|
+
return [
|
|
57
|
+
...app.appeared.map((path) => ` + ${path} (app)`),
|
|
58
|
+
...pages.appeared.map((path) => ` + ${path} (pages)`),
|
|
59
|
+
...app.disappeared.map((path) => ` - ${path} (app)`),
|
|
60
|
+
...pages.disappeared.map((path) => ` - ${path} (pages)`),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
/** One generation pass: scan → emit → write-if-changed (TR3). */
|
|
64
|
+
export function generate(inputs) {
|
|
65
|
+
const routes = scanRoutes(inputs, inputs.pageExtensions);
|
|
66
|
+
return {
|
|
67
|
+
...routes,
|
|
68
|
+
...writeIfChanged(inputs.artifactPath, emitArtifact(routes)),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Per-router route paths in a previously emitted artifact; both empty for a
|
|
73
|
+
* missing file or the empty merge.
|
|
74
|
+
*/
|
|
75
|
+
export function parseArtifactRoutes(previousContent) {
|
|
76
|
+
const appRoutes = new Set();
|
|
77
|
+
const pagesRoutes = new Set();
|
|
78
|
+
if (previousContent === null)
|
|
79
|
+
return { appRoutes, pagesRoutes };
|
|
80
|
+
let current;
|
|
81
|
+
for (const line of previousContent.split("\n")) {
|
|
82
|
+
const header = MEMBER_HEADER.exec(line);
|
|
83
|
+
if (header !== null) {
|
|
84
|
+
current = header[1] === "appRoutes" ? appRoutes : pagesRoutes;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const member = UNION_MEMBER.exec(line);
|
|
88
|
+
if (member !== null) {
|
|
89
|
+
const [, path] = member;
|
|
90
|
+
if (path !== undefined)
|
|
91
|
+
current?.add(path);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Any other line ends the member block — union members are contiguous
|
|
95
|
+
// in the TR3 deterministic form.
|
|
96
|
+
current = undefined;
|
|
97
|
+
}
|
|
98
|
+
return { appRoutes, pagesRoutes };
|
|
99
|
+
}
|
|
100
|
+
function diffRouter(fresh, previous) {
|
|
101
|
+
const freshSet = new Set(fresh);
|
|
102
|
+
return {
|
|
103
|
+
appeared: fresh.filter((path) => !previous.has(path)),
|
|
104
|
+
disappeared: [...previous].filter((path) => !freshSet.has(path)),
|
|
105
|
+
};
|
|
106
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { RouteCollisionError } from "./collisions.js";
|
|
2
|
+
export { type ParamourConfig } from "./config.js";
|
|
3
|
+
export { emitArtifact, type EmitRoutes, writeIfChanged, type WriteIfChangedResult, } from "./emit.js";
|
|
4
|
+
export { type AcquireLockResult, acquireWatcherLock } from "./lock.js";
|
|
5
|
+
export { DEFAULT_PAGE_EXTENSIONS, scanAppRoutes } from "./scan-app.js";
|
|
6
|
+
export { scanPagesRoutes } from "./scan-pages.js";
|
|
7
|
+
export { resolveRouteDirs, type RouteDirs, scanRoutes, type ScanRoutesResult, } from "./scan.js";
|
|
8
|
+
export { DEFAULT_DEBOUNCE_MS, type RouteDirsWatcher, watchRouteDirs, type WatchRouteDirsOptions, } from "./watch.js";
|
|
9
|
+
export { withTypedRoutes, type WithTypedRoutesOptions, } from "./with-typed-routes.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { RouteCollisionError } from "./collisions.js";
|
|
2
|
+
export {} from "./config.js";
|
|
3
|
+
export { emitArtifact, writeIfChanged, } from "./emit.js";
|
|
4
|
+
export { acquireWatcherLock } from "./lock.js";
|
|
5
|
+
export { DEFAULT_PAGE_EXTENSIONS, scanAppRoutes } from "./scan-app.js";
|
|
6
|
+
export { scanPagesRoutes } from "./scan-pages.js";
|
|
7
|
+
export { resolveRouteDirs, scanRoutes, } from "./scan.js";
|
|
8
|
+
export { DEFAULT_DEBOUNCE_MS, watchRouteDirs, } from "./watch.js";
|
|
9
|
+
export { withTypedRoutes, } from "./with-typed-routes.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** One line of init's detect-and-verify summary; `ok: false` renders ⚠. */
|
|
2
|
+
export interface SetupCheck {
|
|
3
|
+
detail?: string;
|
|
4
|
+
label: string;
|
|
5
|
+
ok: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Insert `"paramour": "paramour generate"` into a package.json's scripts,
|
|
9
|
+
* preserving the file's own indentation and trailing-newline choice.
|
|
10
|
+
* Throws on malformed JSON — a broken package.json is init's one hard
|
|
11
|
+
* prerequisite failure.
|
|
12
|
+
*/
|
|
13
|
+
export declare function addPackageScript(text: string): {
|
|
14
|
+
changed: boolean;
|
|
15
|
+
text: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The detect-and-verify summary: route dirs discoverable, both packages
|
|
19
|
+
* declared, tsconfig covering the artifact. Warn-level throughout — a fresh
|
|
20
|
+
* project legitimately fails several of these, so none affect init's exit
|
|
21
|
+
* code.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkSetup(projectRoot: string, artifactPath: string): SetupCheck[];
|
|
24
|
+
/** The starter `paramour.config.ts` — every field commented-out defaults. */
|
|
25
|
+
export declare function paramourConfigTemplate(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Tolerant-enough JSONC → JSON for tsconfig reads: strips line and block
|
|
28
|
+
* comments outside strings, then trailing commas. Heuristic by design — the
|
|
29
|
+
* one consumer is a warn-level check.
|
|
30
|
+
*/
|
|
31
|
+
export declare function stripJsonComments(text: string): string;
|
|
32
|
+
/** Exported for `doctor`, which reports the same heuristic as its own check. */
|
|
33
|
+
export declare function tsconfigCheck(projectRoot: string, artifactPath: string): SetupCheck;
|
|
34
|
+
/**
|
|
35
|
+
* Does a tsconfig `include` pattern cover the artifact? NOT a glob engine:
|
|
36
|
+
* exact match, directory prefix, or a `**` pattern whose extension can
|
|
37
|
+
* match a `.d.ts` (Next's default globstar-`.ts` include is the target case).
|
|
38
|
+
*/
|
|
39
|
+
export declare function tsconfigPatternCovers(pattern: string, artifactRel: string): boolean;
|