@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,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
+ }