@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/bin/paramour.js +19 -0
  3. package/dist/app.d.ts +76 -0
  4. package/dist/app.js +32 -0
  5. package/dist/cli-args.d.ts +28 -0
  6. package/dist/cli-args.js +35 -0
  7. package/dist/cli-inputs.d.ts +32 -0
  8. package/dist/cli-inputs.js +80 -0
  9. package/dist/cli-io.d.ts +15 -0
  10. package/dist/cli-io.js +16 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +5 -0
  13. package/dist/collisions.d.ts +37 -0
  14. package/dist/collisions.js +80 -0
  15. package/dist/commands/doctor.d.ts +7 -0
  16. package/dist/commands/doctor.js +56 -0
  17. package/dist/commands/generate.d.ts +11 -0
  18. package/dist/commands/generate.js +222 -0
  19. package/dist/commands/init.d.ts +9 -0
  20. package/dist/commands/init.js +205 -0
  21. package/dist/commands/list.d.ts +9 -0
  22. package/dist/commands/list.js +94 -0
  23. package/dist/config.d.ts +35 -0
  24. package/dist/config.js +99 -0
  25. package/dist/doctor/checks.d.ts +16 -0
  26. package/dist/doctor/checks.js +231 -0
  27. package/dist/emit.d.ts +35 -0
  28. package/dist/emit.js +74 -0
  29. package/dist/generate.d.ts +70 -0
  30. package/dist/generate.js +106 -0
  31. package/dist/index.d.ts +9 -0
  32. package/dist/index.js +9 -0
  33. package/dist/init/scaffold.d.ts +39 -0
  34. package/dist/init/scaffold.js +244 -0
  35. package/dist/init/wrap-next-config.d.ts +41 -0
  36. package/dist/init/wrap-next-config.js +99 -0
  37. package/dist/list/discover-route-defs.d.ts +52 -0
  38. package/dist/list/discover-route-defs.js +121 -0
  39. package/dist/list/render.d.ts +35 -0
  40. package/dist/list/render.js +132 -0
  41. package/dist/lock.d.ts +29 -0
  42. package/dist/lock.js +88 -0
  43. package/dist/pages.d.ts +49 -0
  44. package/dist/pages.js +62 -0
  45. package/dist/run-cli.d.ts +11 -0
  46. package/dist/run-cli.js +53 -0
  47. package/dist/scan-app.d.ts +30 -0
  48. package/dist/scan-app.js +149 -0
  49. package/dist/scan-pages.d.ts +10 -0
  50. package/dist/scan-pages.js +102 -0
  51. package/dist/scan.d.ts +32 -0
  52. package/dist/scan.js +77 -0
  53. package/dist/select.d.ts +94 -0
  54. package/dist/select.js +195 -0
  55. package/dist/watch.d.ts +39 -0
  56. package/dist/watch.js +87 -0
  57. package/dist/with-typed-routes.d.ts +44 -0
  58. package/dist/with-typed-routes.js +200 -0
  59. 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
+ };
@@ -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
+ }
@@ -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;