@paramour-js/next 0.4.0 → 0.4.1

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 (48) hide show
  1. package/dist/app.d.ts +48 -49
  2. package/dist/app.js +11 -12
  3. package/dist/cli-args.d.ts +4 -4
  4. package/dist/cli-args.js +4 -4
  5. package/dist/cli-inputs.d.ts +6 -7
  6. package/dist/cli-inputs.js +6 -7
  7. package/dist/cli.js +1 -1
  8. package/dist/collisions.d.ts +8 -8
  9. package/dist/collisions.js +9 -9
  10. package/dist/commands/generate.d.ts +7 -7
  11. package/dist/commands/generate.js +16 -16
  12. package/dist/commands/init.js +1 -1
  13. package/dist/config.d.ts +10 -10
  14. package/dist/config.js +4 -4
  15. package/dist/devtools-seam.d.ts +19 -19
  16. package/dist/devtools-seam.js +3 -3
  17. package/dist/doctor/checks.js +1 -1
  18. package/dist/emit.d.ts +12 -12
  19. package/dist/emit.js +13 -13
  20. package/dist/generate.d.ts +14 -14
  21. package/dist/generate.js +11 -11
  22. package/dist/list/discover-route-defs.d.ts +4 -4
  23. package/dist/list/discover-route-defs.js +4 -4
  24. package/dist/lock.d.ts +8 -9
  25. package/dist/lock.js +11 -12
  26. package/dist/navigation-adapter.d.ts +17 -18
  27. package/dist/navigation-adapter.js +4 -4
  28. package/dist/observe.d.ts +14 -14
  29. package/dist/observe.js +4 -4
  30. package/dist/pages.d.ts +30 -31
  31. package/dist/pages.js +10 -10
  32. package/dist/run-cli.d.ts +1 -1
  33. package/dist/run-cli.js +1 -1
  34. package/dist/scan-app.d.ts +10 -10
  35. package/dist/scan-app.js +30 -30
  36. package/dist/scan-pages.d.ts +6 -6
  37. package/dist/scan-pages.js +28 -26
  38. package/dist/scan.d.ts +13 -10
  39. package/dist/scan.js +7 -7
  40. package/dist/select.d.ts +40 -40
  41. package/dist/select.js +30 -30
  42. package/dist/testing.d.ts +28 -32
  43. package/dist/testing.js +15 -15
  44. package/dist/watch.d.ts +12 -12
  45. package/dist/watch.js +13 -13
  46. package/dist/with-typed-routes.d.ts +11 -10
  47. package/dist/with-typed-routes.js +42 -39
  48. package/package.json +2 -2
package/dist/observe.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { useRef } from "react";
2
2
  import { emitObservation } from "./devtools-seam.js";
3
3
  /**
4
- * App-flavor navigate capability (DT8): `next/navigation`'s `replace`
5
- * returns void and resolves the basePath-/locale-relative join itself.
4
+ * App-flavor navigate capability: `next/navigation`'s `replace` returns void
5
+ * and resolves the basePath-/locale-relative join itself.
6
6
  */
7
7
  export function makeAppNavigate(router, pathname) {
8
8
  return (search) => {
@@ -10,8 +10,8 @@ export function makeAppNavigate(router, pathname) {
10
10
  };
11
11
  }
12
12
  /**
13
- * Pages-flavor navigate capability (DT8): `next/router`'s `replace` returns
14
- * a promise that REJECTS on routine navigation aborts (rapid re-commits
13
+ * Pages-flavor navigate capability: `next/router`'s `replace` returns a
14
+ * promise that REJECTS on routine navigation aborts (rapid re-commits
15
15
  * from the panel), marked with next's `cancelled` discriminant — those must
16
16
  * not surface as unhandled rejections. Anything else is a real failure
17
17
  * (render error, route-info error) silently discarding the user's edit, so
package/dist/pages.d.ts CHANGED
@@ -2,57 +2,56 @@ import { type AnyPagesRoute, type InferRouteParams, type SafeResult, type Search
2
2
  import { type SelectOptions } from "./select.js";
3
3
  export type { SelectOptions } from "./select.js";
4
4
  /**
5
- * Pages Router hooks (design-06 PR5/PR6, design-07). Deliberately NO
6
- * `"use client"` directive on this module: the directive is an App Router
7
- * (RSC graph) concept, meaningless in a `pages/` bundle (PR2).
5
+ * Pages Router hooks. Deliberately NO `"use client"` directive on this
6
+ * module: the directive is an App Router (RSC graph) concept, meaningless in
7
+ * a `pages/` bundle.
8
8
  *
9
9
  * `useRouter().query` is one merged bag (route params + search), and on a
10
10
  * statically-optimized page it is `{}` until `router.isReady` flips after
11
11
  * hydration — a platform fact the result type admits as a third state
12
- * instead of papering over (PR5). On `getServerSideProps` pages the FIRST
13
- * render is already `isReady: true` with a populated query (design-06
14
- * spike 3), so the `pending` arm never surfaces there.
12
+ * instead of papering over. On `getServerSideProps` pages the FIRST render
13
+ * is already `isReady: true` with a populated query, so the `pending` arm
14
+ * never surfaces there.
15
15
  *
16
- * Deliberately NO `OrThrow` variants (PR6): throwing on `pending` would
17
- * flash the error boundary on every statically-optimized page's first
18
- * render, and returning `T | undefined` would make the name a lie. The
19
- * three-state union forcing the check IS the feature — and users who know
20
- * their page has `getServerSideProps` should be reading typed props from
21
- * `route.parseContext(ctx)` (PR10) rather than reaching for a client hook.
16
+ * Deliberately NO `OrThrow` variants: throwing on `pending` would flash the
17
+ * error boundary on every statically-optimized page's first render, and
18
+ * returning `T | undefined` would make the name a lie. The three-state union
19
+ * forcing the check IS the feature — and users who know their page has
20
+ * `getServerSideProps` should be reading typed props from
21
+ * `route.parseContext(ctx)` rather than reaching for a client hook.
22
22
  *
23
- * Both hooks are gated to `AnyPagesRoute` (PR3) and share the /app hooks'
24
- * design-07 layering: raw-slice stabilization keyed on the declared slice of
25
- * `query` (+ `isReady`), then an optional `{ select }` projection with
23
+ * Both hooks are gated to `AnyPagesRoute` and share the /app hooks'
24
+ * layering: raw-slice stabilization keyed on the declared slice of `query`
25
+ * (+ `isReady`), then an optional `{ select }` projection with
26
26
  * result-equality checking — the `pending` arm passes through the selector
27
- * untouched (SEL2), and `PENDING` itself is one referentially stable object.
27
+ * untouched, and `PENDING` itself is one referentially stable object.
28
28
  *
29
- * Devtools instrumentation (design-12): each hook reports through the
30
- * shared emitter in observe.ts — `observe` from inside the `useStableResult`
31
- * compute callback (DT4 — the fingerprint cache miss IS the decode-change
32
- * dedup; see app.ts's fuller account), `refresh` after it for pathname
33
- * moves under an unchanged decode (DT8). The `pending` arm emits as a
34
- * first-class observation (DT11), keyed by `PENDING_FINGERPRINT` so the
35
- * pre-`isReady` render reports exactly once. Every emit sits behind
36
- * `process.env.NODE_ENV !== "production"` (DT6).
29
+ * Devtools instrumentation: each hook reports through the shared emitter in
30
+ * observe.ts — `observe` from inside the `useStableResult` compute callback
31
+ * (the fingerprint cache miss IS the decode-change dedup; see app.ts's
32
+ * fuller account), `refresh` after it for pathname moves under an unchanged
33
+ * decode. The `pending` arm emits as a first-class observation, keyed by
34
+ * `PENDING_FINGERPRINT` so the pre-`isReady` render reports exactly once.
35
+ * Every emit sits behind `process.env.NODE_ENV !== "production"`.
37
36
  */
38
37
  /**
39
- * Three-state result for the pages hooks (PR5): core's `SafeResult` plus a
38
+ * Three-state result for the pages hooks: core's `SafeResult` plus a
40
39
  * `pending` member for the pre-`isReady` render of a statically-optimized
41
- * page. Literally `SafeResult<T> | { status: "pending" }` (PR12), so both
42
- * routers' results destructure identically.
40
+ * page. Literally `SafeResult<T> | { status: "pending" }`, so both routers'
41
+ * results destructure identically.
43
42
  */
44
43
  export type RouterResult<T> = SafeResult<T> | {
45
44
  status: "pending";
46
45
  };
47
46
  /**
48
- * Decoded route params as a {@link RouterResult} (PR5), optionally projected
49
- * through `options.select` (design-07 SEL1/SEL2).
47
+ * Decoded route params as a {@link RouterResult}, optionally projected
48
+ * through `options.select`.
50
49
  */
51
50
  export declare function useRouteParams<R extends AnyPagesRoute>(route: R): RouterResult<InferRouteParams<R>>;
52
51
  export declare function useRouteParams<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<InferRouteParams<R>, U>): RouterResult<U>;
53
52
  /**
54
- * Decoded search params as a {@link RouterResult} (PR5), optionally projected
55
- * through `options.select` (design-07 SEL1/SEL2).
53
+ * Decoded search params as a {@link RouterResult}, optionally projected
54
+ * through `options.select`.
56
55
  */
57
56
  export declare function useSearch<R extends AnyPagesRoute>(route: R): RouterResult<SearchOutputOf<R["~search"]>>;
58
57
  export declare function useSearch<R extends AnyPagesRoute, U>(route: R, options: SelectOptions<SearchOutputOf<R["~search"]>, U>): RouterResult<U>;
package/dist/pages.js CHANGED
@@ -80,15 +80,15 @@ export function useSearch(route, options) {
80
80
  }
81
81
  /**
82
82
  * `asPath`'s path part: basePath-/locale-relative — exactly what
83
- * `replace()` expects back — so the panel's search-only string (DT8)
84
- * resolves without doubling a configured basePath.
83
+ * `replace()` expects back — so the panel's search-only string resolves
84
+ * without doubling a configured basePath.
85
85
  */
86
86
  function asPathPathname(asPath) {
87
87
  return asPath.split(/[#?]/)[0] ?? "/";
88
88
  }
89
89
  /**
90
- * `query` minus the route's own path-param names (PR5) — the client twin of
91
- * `parseContext`'s server-side subtraction (core route.ts, PR10). Entries →
90
+ * `query` minus the route's own path-param names — the client twin of
91
+ * `parseContext`'s server-side subtraction (core route.ts). Entries →
92
92
  * fromEntries so a hostile `?__proto__=` key stays an ordinary own property
93
93
  * (decodeParams's ethos). Names come from the define-time `~segments` token
94
94
  * cache, so nothing re-tokenizes per render.
@@ -102,19 +102,19 @@ function omitPathParams(query, route) {
102
102
  return Object.fromEntries(Object.entries(query).filter(([key]) => !names.has(key)));
103
103
  }
104
104
  /**
105
- * Real-Next fallback for the adapter seam (design-16 TA3): the /testing
106
- * provider overrides the read through {@link PagesNavigationContext}; with
105
+ * Real-Next fallback for the adapter seam: the /testing provider overrides
106
+ * the read through {@link PagesNavigationContext}; with
107
107
  * no provider mounted the context's `null` default resolves here, so
108
108
  * production behavior (and this module's `next/router.js`-only bundle
109
109
  * graph, per dist.test.ts) is unchanged. The adapter is resolved via an
110
110
  * unconditional `useContext` BEFORE the try below, exactly where the direct
111
111
  * call previously sat, keeping hook call order identical across renders and
112
- * across provider presence (TA4).
112
+ * across provider presence.
113
113
  */
114
114
  const realPagesAdapter = { useRouter };
115
115
  /**
116
- * `useRouter` with the one failure the brand cannot catch translated (PR5):
117
- * in a hybrid project a component rendered under `app/` can legally hold a
116
+ * `useRouter` with the one failure the brand cannot catch translated: in a
117
+ * hybrid project a component rendered under `app/` can legally hold a
118
118
  * pages-branded route, but `next/router` has no mount there and throws
119
119
  * "NextRouter was not mounted" — a message pointing at the wrong fix
120
120
  * (component placement is invisible to the type system). Rethrow a
@@ -130,7 +130,7 @@ function usePagesRouter() {
130
130
  catch (error) {
131
131
  if (error instanceof Error &&
132
132
  error.message.includes("NextRouter was not mounted")) {
133
- throw new ParamourError('pages hooks were rendered under the App Router, where next/router is never mounted — import this component\'s hooks from "@paramour-js/next/app" and pass it an app route instead (PR5)', { cause: error });
133
+ throw new ParamourError('pages hooks were rendered under the App Router, where next/router is never mounted — import this component\'s hooks from "@paramour-js/next/app" and pass it an app route instead', { cause: error });
134
134
  }
135
135
  throw error;
136
136
  }
package/dist/run-cli.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type CliIo } from "./cli-io.js";
2
2
  export { type CliIo } from "./cli-io.js";
3
3
  /**
4
- * @internal The CLI dispatcher (TR7), in-process testable: returns the exit
4
+ * @internal The CLI dispatcher, in-process testable: returns the exit
5
5
  * code instead of exiting. The exit-code contract holds across every
6
6
  * command: 0 success, 1 "the thing you asked me to verify is not true"
7
7
  * (`check`/`generate --check` drift, `doctor` failures) ONLY, 2
package/dist/run-cli.js CHANGED
@@ -25,7 +25,7 @@ const USAGE = [
25
25
  "Run `paramour <command> --help` for that command's options.",
26
26
  ].join("\n");
27
27
  /**
28
- * @internal The CLI dispatcher (TR7), in-process testable: returns the exit
28
+ * @internal The CLI dispatcher, in-process testable: returns the exit
29
29
  * code instead of exiting. The exit-code contract holds across every
30
30
  * command: 0 success, 1 "the thing you asked me to verify is not true"
31
31
  * (`check`/`generate --check` drift, `doctor` failures) ONLY, 2
@@ -1,12 +1,12 @@
1
1
  import type { Dirent } from "node:fs";
2
- /** Next's default `pageExtensions` — extensions only, no leading dot (TR2). */
2
+ /** Next's default `pageExtensions` — extensions only, no leading dot. */
3
3
  export declare const DEFAULT_PAGE_EXTENSIONS: readonly ["tsx", "ts", "jsx", "js"];
4
4
  /**
5
5
  * Whether a directory entry should be treated as a FILE for routing: a real
6
6
  * file, or a symlink whose target is a regular file. `Dirent.isFile()` is
7
7
  * false for a symlink even when it points at a file, yet Next resolves and
8
8
  * serves symlinked `page`/`route` files (common in pnpm-linked monorepos), so
9
- * dropping them would omit routes that Next actually serves (Bug 4, TR2). A
9
+ * dropping them would omit routes that Next actually serves (Bug 4). A
10
10
  * symlink to a DIRECTORY returns false — directory symlinks stay not-followed,
11
11
  * the existing v1 stance — and a broken link (statSync throws ENOENT) also
12
12
  * returns false, i.e. is skipped silently, matching Next's own tolerance of
@@ -15,16 +15,16 @@ export declare const DEFAULT_PAGE_EXTENSIONS: readonly ["tsx", "ts", "jsx", "js"
15
15
  export declare function resolvesToFile(entry: Dirent, dir: string): boolean;
16
16
  /**
17
17
  * Walk an app dir and return the sorted union of URL-shaped route paths —
18
- * exactly the strings `defineAppRoute` accepts (TR2, RL2). Pure `fs.readdir`
19
- * recursion; no dependency on Next internals. Two page files resolving to
20
- * one URL path — `(a)/x` + `(b)/x` group twins, or `page.tsx` + `page.jsx`
21
- * extension twins — throw a {@link RouteCollisionError} instead of being
22
- * deduped (PR4/PR9 alignment ruling): that state is Next's own build error,
18
+ * exactly the strings `defineAppRoute` accepts. Pure `fs.readdir` recursion;
19
+ * no dependency on Next internals. Two page files resolving to one URL path
20
+ * — `(a)/x` + `(b)/x` group twins, or `page.tsx` + `page.jsx` extension
21
+ * twins — throw a {@link RouteCollisionError} instead of being deduped, the
22
+ * same stance the pages scanner takes: that state is Next's own build error,
23
23
  * and deduping would emit an artifact for a project that cannot build.
24
24
  *
25
25
  * `route.<ext>` handlers are scanned but never emitted (handler typing is
26
- * deferred, §14). They exist only to catch the states Next refuses to build:
27
- * a page and a route handler at the same URL path ("conflicting route and
28
- * page"), and two route handlers at the same path — both throw (PR9).
26
+ * deferred). They exist only to catch the states Next refuses to build: a
27
+ * page and a route handler at the same URL path ("conflicting route and
28
+ * page"), and two route handlers at the same path — both throw.
29
29
  */
30
30
  export declare function scanAppRoutes(appDir: string, pageExtensions?: readonly string[]): string[];
package/dist/scan-app.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { readdirSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { assertNoStructuralCollisions, RouteCollisionError, } from "./collisions.js";
4
- /** Next's default `pageExtensions` — extensions only, no leading dot (TR2). */
4
+ /** Next's default `pageExtensions` — extensions only, no leading dot. */
5
5
  export const DEFAULT_PAGE_EXTENSIONS = ["tsx", "ts", "jsx", "js"];
6
6
  /**
7
- * Interception markers `(.)`/`(..)`/`(...)` (TR2, RL2 / §15.5). A prefix
8
- * match, so chained forms like `(..)(..)segment` are caught too; tested
9
- * BEFORE the route-group test so `(.)foo` is never misread as a group.
7
+ * Interception markers `(.)`/`(..)`/`(...)`. A prefix match, so chained
8
+ * forms like `(..)(..)segment` are caught too; tested BEFORE the route-group
9
+ * test so `(.)foo` is never misread as a group.
10
10
  */
11
11
  const INTERCEPTION_PREFIX = /^\(\.{1,3}\)/;
12
12
  /**
@@ -17,18 +17,18 @@ const INTERCEPTION_PREFIX = /^\(\.{1,3}\)/;
17
17
  * `/_settings`. The escape is defined for the LEADING position only. Because
18
18
  * RFC 3986 percent-encoding is case-insensitive on its hex digits (and this
19
19
  * could not be pinned against Next's source from here), both `%5F` and `%5f`
20
- * are decoded defensively (Bug 8, TR2). The fs name stays raw for error
21
- * messages; only the emitted URL segment is decoded.
20
+ * are decoded defensively (Bug 8). The fs name stays raw for error messages;
21
+ * only the emitted URL segment is decoded.
22
22
  */
23
23
  const LEADING_ESCAPED_UNDERSCORE = /^%5[Ff]/;
24
- /** Route groups `(group)` — stripped from the emitted path (TR2, RL2). */
24
+ /** Route groups `(group)` — stripped from the emitted path. */
25
25
  const ROUTE_GROUP = /^\(.*\)$/;
26
26
  /**
27
27
  * Whether a directory entry should be treated as a FILE for routing: a real
28
28
  * file, or a symlink whose target is a regular file. `Dirent.isFile()` is
29
29
  * false for a symlink even when it points at a file, yet Next resolves and
30
30
  * serves symlinked `page`/`route` files (common in pnpm-linked monorepos), so
31
- * dropping them would omit routes that Next actually serves (Bug 4, TR2). A
31
+ * dropping them would omit routes that Next actually serves (Bug 4). A
32
32
  * symlink to a DIRECTORY returns false — directory symlinks stay not-followed,
33
33
  * the existing v1 stance — and a broken link (statSync throws ENOENT) also
34
34
  * returns false, i.e. is skipped silently, matching Next's own tolerance of
@@ -50,17 +50,17 @@ export function resolvesToFile(entry, dir) {
50
50
  }
51
51
  /**
52
52
  * Walk an app dir and return the sorted union of URL-shaped route paths —
53
- * exactly the strings `defineAppRoute` accepts (TR2, RL2). Pure `fs.readdir`
54
- * recursion; no dependency on Next internals. Two page files resolving to
55
- * one URL path — `(a)/x` + `(b)/x` group twins, or `page.tsx` + `page.jsx`
56
- * extension twins — throw a {@link RouteCollisionError} instead of being
57
- * deduped (PR4/PR9 alignment ruling): that state is Next's own build error,
53
+ * exactly the strings `defineAppRoute` accepts. Pure `fs.readdir` recursion;
54
+ * no dependency on Next internals. Two page files resolving to one URL path
55
+ * — `(a)/x` + `(b)/x` group twins, or `page.tsx` + `page.jsx` extension
56
+ * twins — throw a {@link RouteCollisionError} instead of being deduped, the
57
+ * same stance the pages scanner takes: that state is Next's own build error,
58
58
  * and deduping would emit an artifact for a project that cannot build.
59
59
  *
60
60
  * `route.<ext>` handlers are scanned but never emitted (handler typing is
61
- * deferred, §14). They exist only to catch the states Next refuses to build:
62
- * a page and a route handler at the same URL path ("conflicting route and
63
- * page"), and two route handlers at the same path — both throw (PR9).
61
+ * deferred). They exist only to catch the states Next refuses to build: a
62
+ * page and a route handler at the same URL path ("conflicting route and
63
+ * page"), and two route handlers at the same path — both throw.
64
64
  */
65
65
  export function scanAppRoutes(appDir, pageExtensions = DEFAULT_PAGE_EXTENSIONS) {
66
66
  // Path → the fs path (relative to appDir) that produced it, so a collision
@@ -71,20 +71,20 @@ export function scanAppRoutes(appDir, pageExtensions = DEFAULT_PAGE_EXTENSIONS)
71
71
  const pageFileNames = new Set(pageExtensions.map((ext) => `page.${ext}`));
72
72
  const routeFileNames = new Set(pageExtensions.map((ext) => `route.${ext}`));
73
73
  walk(appDir, [], [], pageFileNames, routeFileNames, out, routeOut);
74
- // PR9: a page and a route handler resolving to one URL path is Next's
74
+ // A page and a route handler resolving to one URL path is Next's
75
75
  // "conflicting route and page" build error — no valid artifact exists, so
76
76
  // throw rather than emit the page and silently drop the handler. Sorted so
77
77
  // the reported pair is deterministic across platforms.
78
78
  for (const [path, routeFile] of [...routeOut].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
79
79
  const pageFile = out.get(path);
80
80
  if (pageFile !== undefined) {
81
- throw new RouteCollisionError(`app route collision at "${path}": page ${pageFile} and route handler ${routeFile} resolve to the same path, which Next refuses to build (conflicting route and page) (PR9)`);
81
+ throw new RouteCollisionError(`app route collision at "${path}": page ${pageFile} and route handler ${routeFile} resolve to the same path, which Next refuses to build (conflicting route and page)`);
82
82
  }
83
83
  }
84
- // Code-unit sort, never localeCompare — locale independence feeds TR3's
84
+ // Code-unit sort, never localeCompare — locale independence feeds the
85
85
  // byte-identical-on-every-OS guarantee.
86
86
  const paths = [...out.keys()].sort();
87
- // PR9 structural collisions (different slug names, optional-catch-all
87
+ // Structural collisions (different slug names, optional-catch-all
88
88
  // specificity) — non-equal strings the Map above cannot catch.
89
89
  assertNoStructuralCollisions(paths.map((path) => ({ path, router: "app" })));
90
90
  return paths;
@@ -97,11 +97,11 @@ function walk(dir, urlSegments, fsSegments, pageFileNames, routeFileNames, out,
97
97
  const name = entry.name;
98
98
  // A real file, or a symlink whose target is a file (Bug 4). Directory
99
99
  // symlinks fall through to the directory guard below, which is false for
100
- // a symlink Dirent, so their subtree is skipped — not followed (TR2).
100
+ // a symlink Dirent, so their subtree is skipped — not followed.
101
101
  if (resolvesToFile(entry, dir)) {
102
- // Exact, case-sensitive `page.<ext>` / `route.<ext>` match (TR2). Pages
103
- // are emitted; route handlers are tracked separately (never emitted —
104
- // handler typing is §14) purely to detect the build errors above (PR9).
102
+ // Exact, case-sensitive `page.<ext>` / `route.<ext>` match. Pages are
103
+ // emitted; route handlers are tracked separately (never emitted —
104
+ // handler typing is deferred) purely to detect the build errors above.
105
105
  const isPage = pageFileNames.has(name);
106
106
  const isRoute = !isPage && routeFileNames.has(name);
107
107
  if (isPage || isRoute) {
@@ -111,19 +111,19 @@ function walk(dir, urlSegments, fsSegments, pageFileNames, routeFileNames, out,
111
111
  const existing = target.get(path);
112
112
  if (existing !== undefined) {
113
113
  throw new RouteCollisionError(isPage
114
- ? `app route collision at "${path}": ${existing} and ${file} resolve to the same path (PR9)`
115
- : `app route collision at "${path}": ${existing} and ${file} both declare a route handler at the same path, which Next refuses to build (PR9)`);
114
+ ? `app route collision at "${path}": ${existing} and ${file} resolve to the same path`
115
+ : `app route collision at "${path}": ${existing} and ${file} both declare a route handler at the same path, which Next refuses to build`);
116
116
  }
117
117
  target.set(path, file);
118
118
  }
119
119
  continue;
120
120
  }
121
- // Symlinked directories are deliberately not followed (TR2 v1 stance):
121
+ // Symlinked directories are deliberately not followed (the v1 stance):
122
122
  // `resolvesToFile` returned false and `isDirectory()` is false for the
123
123
  // link Dirent, so the subtree is skipped here.
124
124
  if (!entry.isDirectory())
125
125
  continue;
126
- // TR2 skip rules: private folders, parallel slots, interception routes —
126
+ // Skip rules: private folders, parallel slots, interception routes —
127
127
  // each skips the entire subtree, pages at any depth included. The `_`
128
128
  // test reads the raw fs name, so `%5F`-escaped folders (which do NOT
129
129
  // start with `_`) are correctly NOT skipped (Bug 8).
@@ -134,12 +134,12 @@ function walk(dir, urlSegments, fsSegments, pageFileNames, routeFileNames, out,
134
134
  if (INTERCEPTION_PREFIX.test(name))
135
135
  continue;
136
136
  if (ROUTE_GROUP.test(name)) {
137
- // Group stripped: recurse with the SAME url segments (TR2).
137
+ // Group stripped: recurse with the SAME url segments.
138
138
  walk(join(dir, name), urlSegments, [...fsSegments, name], pageFileNames, routeFileNames, out, routeOut);
139
139
  continue;
140
140
  }
141
141
  // Dynamic segments `[id]` / `[...slug]` / `[[...slug]]` pass through
142
- // verbatim (TR2, RL2 URL-shaped literals). A leading `%5F` decodes to `_`
142
+ // verbatim as URL-shaped literals. A leading `%5F` decodes to `_`
143
143
  // for the emitted URL segment so it string-matches the served URL; the fs
144
144
  // name stays raw for error messages, and the decoded form participates in
145
145
  // collision detection via the `out` Map key (Bug 8).
@@ -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 (PR4). A route is any file
4
- * whose extension is in `pageExtensions`, mapped by its path relative to the
5
- * dir; `index.<ext>` maps to its directory. Two files resolving to one URL
6
- * path — folder/file spelling (`blog.tsx` + `blog/index.tsx`) or extension
7
- * twins (`about.tsx` + `about.jsx`) — throw a {@link RouteCollisionError},
8
- * never dedupe: both are Next's own build errors (PR9).
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[];
@@ -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 (PR4). Deliberately a separate walker from
7
- * `scan-app.ts` — the rule sets barely overlap (routes live on FILES here,
8
- * and none of TR2's skip rules apply), and a shared walker would have to
9
- * take its rule set as a parameter to be worth having (PR8).
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 (PR4):
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 (spike 1: co-location under
17
- * `pages/` was requested, vercel/next.js#8454, and never implemented).
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 (PR4). A route is any file
29
- * whose extension is in `pageExtensions`, mapped by its path relative to the
30
- * dir; `index.<ext>` maps to its directory. Two files resolving to one URL
31
- * path — folder/file spelling (`blog.tsx` + `blog/index.tsx`) or extension
32
- * twins (`about.tsx` + `about.jsx`) — throw a {@link RouteCollisionError},
33
- * never dedupe: both are Next's own build errors (PR9).
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 TR3's
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
- // PR9 structural collisions (different slug names, optional-catch-all
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, TR2 shared posture). Directory symlinks fall through
66
- // to the directory guard below and stay not-followed.
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 (PR11 §1).
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 (PR4).
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 (PR9)`);
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 (TR2 v1 stance,
90
- // shared posture): `resolvesToFile` returned false and `isDirectory()` is
91
- // false for the link Dirent, so the subtree is skipped here.
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, PR4/§14). NO app-style
96
- // skip rules beyond this: `(group)`, `@slot`, `(.)x`, and `_`-prefixed
97
- // dirs are ordinary literal segments in the Pages Router (PR4).
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 (PR8): joint directory
3
- * discovery, delegation, and the cross-router collision checks (PR9).
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 PR9 artifact. */
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 (spike-2 ruling). Next's documented rule is one
17
- * decision, not two probes: `src/app` AND `src/pages` are both ignored
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 (PR1). After
26
- * each scanner's own intra-router checks, two cross-router passes run (PR9):
27
- * a path in BOTH unions is Next's "Conflicting app and page file" build
28
- * error, and the structural pass re-runs over the merged, labeled union so
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 (spike-2 ruling). Next's documented rule is one
8
- * decision, not two probes: `src/app` AND `src/pages` are both ignored
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 (PR1). After
51
- * each scanner's own intra-router checks, two cross-router passes run (PR9):
52
- * a path in BOTH unions is Next's "Conflicting app and page file" build
53
- * error, and the structural pass re-runs over the merged, labeled union so
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 (PR9)`);
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" })),