@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,200 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { RouteCollisionError } from "./collisions.js";
|
|
3
|
+
import { diffGenerated, formatRouteDiff, generate, } from "./generate.js";
|
|
4
|
+
import { acquireWatcherLock, watcherLockPath, } from "./lock.js";
|
|
5
|
+
import { DEFAULT_PAGE_EXTENSIONS } from "./scan-app.js";
|
|
6
|
+
import { resolveRouteDirs } from "./scan.js";
|
|
7
|
+
import { watchRouteDirs } from "./watch.js";
|
|
8
|
+
/**
|
|
9
|
+
* Phase constants from `next/constants`, hardcoded so the package stays
|
|
10
|
+
* hermetic (TR4 hermeticity ruling): the values are stable, documented
|
|
11
|
+
* public API, and importing them would make `next` a runtime dependency.
|
|
12
|
+
*/
|
|
13
|
+
const PHASE_DEVELOPMENT_SERVER = "phase-development-server";
|
|
14
|
+
const PHASE_PRODUCTION_BUILD = "phase-production-build";
|
|
15
|
+
/**
|
|
16
|
+
* TR6 guard 1 — the in-process singleton, keyed by route dirs + artifact
|
|
17
|
+
* path. Load-bearing even for a single `next dev`: the spike-#1 census found
|
|
18
|
+
* Turbopack dev invokes the config function twice in the same process.
|
|
19
|
+
*/
|
|
20
|
+
const devWatcherTeardowns = new Map();
|
|
21
|
+
/** Messages already logged — "log once" (TR5) across repeat evaluations. */
|
|
22
|
+
const warnedOnce = new Set();
|
|
23
|
+
/** @internal Test seam: the number of live dev-watcher singletons. */
|
|
24
|
+
export function devWatcherCountForTests() {
|
|
25
|
+
return devWatcherTeardowns.size;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* @internal Test seam: close every dev watcher, release the pidfile locks,
|
|
29
|
+
* and clear the log-once state. Also required between tests on Windows —
|
|
30
|
+
* an open watch handle blocks temp-dir removal.
|
|
31
|
+
*/
|
|
32
|
+
export function resetDevWatchersForTests() {
|
|
33
|
+
for (const teardown of devWatcherTeardowns.values())
|
|
34
|
+
teardown();
|
|
35
|
+
devWatcherTeardowns.clear();
|
|
36
|
+
warnedOnce.clear();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Wrap a Next config with route-registry generation (TR4). Returns the
|
|
40
|
+
* config-function form; Next's phase argument is the mode discriminator:
|
|
41
|
+
*
|
|
42
|
+
* - production build → one generation pass before the config is returned
|
|
43
|
+
* (the build type-checks against fresh routes); drift warns loudly, or
|
|
44
|
+
* fails the build under `strict: true`.
|
|
45
|
+
* - dev server → one immediate generation pass, then the debounced watcher
|
|
46
|
+
* (TR5) behind both single-writer guards (TR6).
|
|
47
|
+
* - every other phase → pass-through, no generation.
|
|
48
|
+
*
|
|
49
|
+
* Two states throw during config evaluation instead of degrading to
|
|
50
|
+
* stale-types mode, both phases alike, because Next itself has no valid
|
|
51
|
+
* build for them: an app↔pages route collision (PR9), and discovery's
|
|
52
|
+
* populated-ignored-dir config error (spike-2 ruling — Next is silently
|
|
53
|
+
* serving none of those pages).
|
|
54
|
+
*/
|
|
55
|
+
export function withTypedRoutes(config, options = {}) {
|
|
56
|
+
return async (phase, ctx) => {
|
|
57
|
+
const resolved = typeof config === "function" ? await config(phase, ctx) : config;
|
|
58
|
+
if (phase !== PHASE_DEVELOPMENT_SERVER && phase !== PHASE_PRODUCTION_BUILD)
|
|
59
|
+
return resolved;
|
|
60
|
+
// The dev server and every build worker evaluate the config with the
|
|
61
|
+
// project root as cwd (spike-#1 census); TR7's CLI flags are the home
|
|
62
|
+
// for anything more configurable than this.
|
|
63
|
+
const projectRoot = process.cwd();
|
|
64
|
+
const artifactPath = resolve(projectRoot, options.outFile ?? "paramour-env.d.ts");
|
|
65
|
+
const pageExtensions = resolved.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;
|
|
66
|
+
// May throw the spike-2 config error — deliberately not caught (above).
|
|
67
|
+
const dirs = resolveRouteDirs(projectRoot, pageExtensions);
|
|
68
|
+
if (dirs.appDir === undefined && dirs.pagesDir === undefined) {
|
|
69
|
+
// §7.3: codegen is never load-bearing — a config wrapper must not
|
|
70
|
+
// take down `next dev`/`next build` over a missing route dir.
|
|
71
|
+
warnOnce(`paramour: no route directory (app/, pages/, src/app/, or src/pages/) under ${projectRoot}; route generation skipped`);
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
if (phase === PHASE_PRODUCTION_BUILD) {
|
|
75
|
+
generateForBuild(dirs, pageExtensions, artifactPath, options.strict ?? false);
|
|
76
|
+
return resolved;
|
|
77
|
+
}
|
|
78
|
+
generateSafely(dirs, pageExtensions, artifactPath);
|
|
79
|
+
startDevWatcher(projectRoot, dirs, pageExtensions, artifactPath);
|
|
80
|
+
return resolved;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build-phase pass (TR4): regenerate, then warn loudly on drift — naming the
|
|
85
|
+
* paths that appeared/disappeared and the router they moved in — but
|
|
86
|
+
* continue; `strict` upgrades drift to a thrown error *after* the file is
|
|
87
|
+
* already corrected. A missing artifact counts as drift: that is exactly the
|
|
88
|
+
* CI-degrades-to-world-A scenario the committed file exists to prevent
|
|
89
|
+
* (TR3). A route collision is NOT incidental failure and rethrows (PR9).
|
|
90
|
+
*/
|
|
91
|
+
function generateForBuild(dirs, pageExtensions, artifactPath, strict) {
|
|
92
|
+
let result;
|
|
93
|
+
try {
|
|
94
|
+
result = generate({ ...dirs, artifactPath, pageExtensions });
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
// PR9: Next fails this build anyway; surfacing the collision from the
|
|
98
|
+
// config evaluation names the actual problem instead of leaving a stale
|
|
99
|
+
// artifact to confuse the type errors that follow.
|
|
100
|
+
if (error instanceof RouteCollisionError)
|
|
101
|
+
throw error;
|
|
102
|
+
// §7.3 again: incidental generation failure is stale types, not a
|
|
103
|
+
// broken build. Only *drift* is allowed to fail a strict build.
|
|
104
|
+
console.warn("paramour: route generation failed; building with stale route types", error);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!result.written)
|
|
108
|
+
return;
|
|
109
|
+
const drift = diffGenerated(result);
|
|
110
|
+
const message = [
|
|
111
|
+
result.previousContent === null
|
|
112
|
+
? `paramour: ${artifactPath} was missing and has been generated.`
|
|
113
|
+
: `paramour: ${artifactPath} was out of date and has been regenerated.`,
|
|
114
|
+
...formatRouteDiff(drift.app, drift.pages),
|
|
115
|
+
"Commit the regenerated file, or run `paramour generate` before building.",
|
|
116
|
+
].join("\n");
|
|
117
|
+
if (strict)
|
|
118
|
+
throw new Error(message);
|
|
119
|
+
console.warn(message);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Dev-phase generation (TR4): failure warns and continues (§7.3) — except a
|
|
123
|
+
* route collision, which throws from the config evaluation here exactly as
|
|
124
|
+
* in the build phase (PR9; only the running WATCHER treats it non-fatally).
|
|
125
|
+
*/
|
|
126
|
+
function generateSafely(dirs, pageExtensions, artifactPath) {
|
|
127
|
+
try {
|
|
128
|
+
generate({ ...dirs, artifactPath, pageExtensions });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error instanceof RouteCollisionError)
|
|
132
|
+
throw error;
|
|
133
|
+
console.warn("paramour: route generation failed; dev continues with stale route types", error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Start the dev watcher behind both TR6 guards. Failure at any layer leaves
|
|
138
|
+
* dev running in stale-types mode — never fatal (TR5).
|
|
139
|
+
*/
|
|
140
|
+
function startDevWatcher(projectRoot, dirs, pageExtensions, artifactPath) {
|
|
141
|
+
const watchedDirs = [dirs.appDir, dirs.pagesDir].filter((dir) => dir !== undefined);
|
|
142
|
+
const key = `${watchedDirs.join(" ")} ${artifactPath}`;
|
|
143
|
+
if (devWatcherTeardowns.has(key))
|
|
144
|
+
return;
|
|
145
|
+
let lock;
|
|
146
|
+
try {
|
|
147
|
+
lock = acquireWatcherLock(watcherLockPath(projectRoot));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
// §7.3: a corrupt lock location (e.g. a directory at the pidfile path)
|
|
151
|
+
// must not take down `next dev` — stale-types mode, like every other
|
|
152
|
+
// watcher-layer failure.
|
|
153
|
+
warnOnce("paramour: dev watcher failed; dev continues with stale route types", error);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!lock.acquired) {
|
|
157
|
+
// TR6: another live process owns the watcher (e.g. `paramour generate
|
|
158
|
+
// --watch` beside `next dev`). Initial generation above already ran, so
|
|
159
|
+
// dev is still correct from second zero.
|
|
160
|
+
console.warn(`paramour: watcher already running (pid ${String(lock.ownerPid)})`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const watcher = watchRouteDirs(watchedDirs, {
|
|
164
|
+
ignorePaths: [artifactPath],
|
|
165
|
+
onError: (error) => {
|
|
166
|
+
warnOnce("paramour: dev watcher failed; dev continues with stale route types", error);
|
|
167
|
+
},
|
|
168
|
+
onRescan: () => {
|
|
169
|
+
try {
|
|
170
|
+
generate({ ...dirs, artifactPath, pageExtensions });
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error instanceof RouteCollisionError) {
|
|
174
|
+
// PR9's watch exception: a mid-watch collision is usually a file
|
|
175
|
+
// mid-move — log loudly every time (not once: it stays broken
|
|
176
|
+
// until fixed), keep the last good artifact, keep running (TR5).
|
|
177
|
+
console.warn(`paramour: ${error.message}; dev continues with the last good artifact`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
throw error; // routed to onError by the watcher (TR5 non-fatal)
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
// A failed watchRouteDirs returns a no-op handle and is still registered:
|
|
185
|
+
// retrying on every config re-evaluation would just warn repeatedly.
|
|
186
|
+
devWatcherTeardowns.set(key, () => {
|
|
187
|
+
watcher.close();
|
|
188
|
+
lock.release?.();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/** TR5 "log once": repeat evaluations/events don't spam the dev console. */
|
|
192
|
+
function warnOnce(message, detail) {
|
|
193
|
+
if (warnedOnce.has(message))
|
|
194
|
+
return;
|
|
195
|
+
warnedOnce.add(message);
|
|
196
|
+
if (detail === undefined)
|
|
197
|
+
console.warn(message);
|
|
198
|
+
else
|
|
199
|
+
console.warn(message, detail);
|
|
200
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@paramour-js/next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"default": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"./app": {
|
|
11
|
+
"types": "./dist/app.d.ts",
|
|
12
|
+
"default": "./dist/app.js"
|
|
13
|
+
},
|
|
14
|
+
"./pages": {
|
|
15
|
+
"types": "./dist/pages.d.ts",
|
|
16
|
+
"default": "./dist/pages.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"paramour": "./bin/paramour.js"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.18.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"jiti": "^2.7.0",
|
|
27
|
+
"magicast": "^0.3.5",
|
|
28
|
+
"tinyglobby": "^0.2.15",
|
|
29
|
+
"paramour": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"next": ">=15",
|
|
33
|
+
"react": ">=18.2.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@standard-schema/spec": "^1.1.0",
|
|
37
|
+
"@testing-library/dom": "^10.4.0",
|
|
38
|
+
"@testing-library/react": "^16.1.0",
|
|
39
|
+
"@types/node": "^24.0.0",
|
|
40
|
+
"@types/react": "^19.2.0",
|
|
41
|
+
"happy-dom": "^15.11.0",
|
|
42
|
+
"react": "^19.2.0",
|
|
43
|
+
"react-dom": "^19.2.0",
|
|
44
|
+
"typescript": "^6.0.3",
|
|
45
|
+
"zod": "^4.4.3"
|
|
46
|
+
},
|
|
47
|
+
"description": "Next.js integration for paramour: withTypedRoutes, App and Pages Router hooks, PageProps glue, and the codegen CLI.",
|
|
48
|
+
"author": "Jason Paff <jasonpaff@gmail.com>",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/JasonPaff/paramour.git",
|
|
53
|
+
"directory": "packages/next"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/JasonPaff/paramour#readme",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/JasonPaff/paramour/issues"
|
|
58
|
+
},
|
|
59
|
+
"files": [
|
|
60
|
+
"bin",
|
|
61
|
+
"dist"
|
|
62
|
+
],
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsc -p tsconfig.build.json",
|
|
65
|
+
"typecheck": "tsc --noEmit"
|
|
66
|
+
}
|
|
67
|
+
}
|