@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.
- package/dist/app.d.ts +48 -49
- package/dist/app.js +11 -12
- package/dist/cli-args.d.ts +4 -4
- package/dist/cli-args.js +4 -4
- package/dist/cli-inputs.d.ts +6 -7
- package/dist/cli-inputs.js +6 -7
- package/dist/cli.js +1 -1
- package/dist/collisions.d.ts +8 -8
- package/dist/collisions.js +9 -9
- package/dist/commands/generate.d.ts +7 -7
- package/dist/commands/generate.js +16 -16
- package/dist/commands/init.js +1 -1
- package/dist/config.d.ts +10 -10
- package/dist/config.js +4 -4
- package/dist/devtools-seam.d.ts +19 -19
- package/dist/devtools-seam.js +3 -3
- package/dist/doctor/checks.js +1 -1
- package/dist/emit.d.ts +12 -12
- package/dist/emit.js +13 -13
- package/dist/generate.d.ts +14 -14
- package/dist/generate.js +11 -11
- package/dist/list/discover-route-defs.d.ts +4 -4
- package/dist/list/discover-route-defs.js +4 -4
- package/dist/lock.d.ts +8 -9
- package/dist/lock.js +11 -12
- package/dist/navigation-adapter.d.ts +17 -18
- package/dist/navigation-adapter.js +4 -4
- package/dist/observe.d.ts +14 -14
- package/dist/observe.js +4 -4
- package/dist/pages.d.ts +30 -31
- package/dist/pages.js +10 -10
- package/dist/run-cli.d.ts +1 -1
- package/dist/run-cli.js +1 -1
- package/dist/scan-app.d.ts +10 -10
- package/dist/scan-app.js +30 -30
- package/dist/scan-pages.d.ts +6 -6
- package/dist/scan-pages.js +28 -26
- package/dist/scan.d.ts +13 -10
- package/dist/scan.js +7 -7
- package/dist/select.d.ts +40 -40
- package/dist/select.js +30 -30
- package/dist/testing.d.ts +28 -32
- package/dist/testing.js +15 -15
- package/dist/watch.d.ts +12 -12
- package/dist/watch.js +13 -13
- package/dist/with-typed-routes.d.ts +11 -10
- package/dist/with-typed-routes.js +42 -39
- 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
|
|
5
|
-
*
|
|
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
|
|
14
|
-
*
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `route.parseContext(ctx)`
|
|
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`
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
27
|
+
* untouched, and `PENDING` itself is one referentially stable object.
|
|
28
28
|
*
|
|
29
|
-
* Devtools instrumentation
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
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" }
|
|
42
|
-
*
|
|
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}
|
|
49
|
-
* through `options.select
|
|
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}
|
|
55
|
-
* through `options.select
|
|
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
|
|
84
|
-
*
|
|
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
|
|
91
|
-
* `parseContext`'s server-side subtraction (core route.ts
|
|
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
|
|
106
|
-
*
|
|
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
|
|
112
|
+
* across provider presence.
|
|
113
113
|
*/
|
|
114
114
|
const realPagesAdapter = { useRouter };
|
|
115
115
|
/**
|
|
116
|
-
* `useRouter` with the one failure the brand cannot catch translated
|
|
117
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/scan-app.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Dirent } from "node:fs";
|
|
2
|
-
/** Next's default `pageExtensions` — extensions only, no leading dot
|
|
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
|
|
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
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
27
|
-
*
|
|
28
|
-
* page"), and two route handlers at the same path — both throw
|
|
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
|
|
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 `(.)`/`(..)`/`(...)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
21
|
-
*
|
|
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
|
|
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
|
|
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
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
|
62
|
-
*
|
|
63
|
-
* page"), and two route handlers at the same path — both throw
|
|
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
|
-
//
|
|
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)
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
103
|
-
//
|
|
104
|
-
// handler typing is
|
|
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
|
|
115
|
-
: `app route collision at "${path}": ${existing} and ${file} both declare a route handler at the same path, which Next refuses to build
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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).
|
package/dist/scan-pages.d.ts
CHANGED
|
@@ -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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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[];
|
package/dist/scan-pages.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
|
17
|
-
*
|
|
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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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,
|
|
66
|
-
// to the directory guard below and stay
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
90
|
-
// shared
|
|
91
|
-
// false for the link Dirent, so the subtree is
|
|
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
|
|
96
|
-
//
|
|
97
|
-
//
|
|
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
|
|
3
|
-
*
|
|
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
|
|
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
|
|
17
|
-
*
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
8
|
-
*
|
|
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
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
|
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" })),
|