mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 11:56:12 +00:00
fix(javascript): bind Next page admission to source context
This commit is contained in:
1 parent
c20b707680
commit
5ee5e49cb4
13 files changed
+532
-53
No files matched your search
@@ -1,3 +1,42 @@
|
||||
import { z } from "zod";
|
||||
import { digestSchema } from "../digests.js";
|
||||
|
||||
const packageIdentitySchema = z.strictObject({
|
||||
path: z.string().min(1),
|
||||
sha256: digestSchema,
|
||||
container_sha256: digestSchema,
|
||||
});
|
||||
export const nextAppSourceContextSchema = z.discriminatedUnion("kind", [
|
||||
z.strictObject({
|
||||
kind: z.literal("app-directory"),
|
||||
directory: z.string().min(1),
|
||||
package: packageIdentitySchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("compiled-directory"),
|
||||
directory: z.string().min(1),
|
||||
package: packageIdentitySchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("outside-app-directory"),
|
||||
package: packageIdentitySchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("unknown"),
|
||||
reason: z.string(),
|
||||
package: packageIdentitySchema.nullable(),
|
||||
}),
|
||||
]);
|
||||
export type NextAppSourceContext = z.infer<typeof nextAppSourceContextSchema>;
|
||||
export type NextAppRouteContext =
|
||||
| { readonly kind: "source-path"; readonly source: NextAppSourceContext }
|
||||
| { readonly kind: "script-url" }
|
||||
| {
|
||||
readonly kind: "source-literal";
|
||||
readonly runtime: "next" | "unknown";
|
||||
}
|
||||
| { readonly kind: "source-reference" };
|
||||
|
||||
/** URL route syntax, independent of filesystem path normalization. */
|
||||
export const routePath = (
|
||||
value: string,
|
||||
@@ -19,27 +58,71 @@ export type NextAppRoutePath =
|
||||
/** Classify a page filename without treating missing routing context as absence. */
|
||||
export const classifyNextAppRoutePath = (
|
||||
raw: string,
|
||||
context: NextAppRouteContext,
|
||||
): NextAppRoutePath | undefined => {
|
||||
if (raw.includes("\\")) return;
|
||||
if (raw.includes("\\") || context.kind === "source-reference") return;
|
||||
let value = raw;
|
||||
if (raw.includes("://")) {
|
||||
if (
|
||||
context.kind === "script-url" ||
|
||||
(context.kind === "source-literal" && raw.includes("://"))
|
||||
) {
|
||||
try {
|
||||
value = new URL(raw).pathname;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// A deployment prefix may itself contain an `app` segment. Only the chunk
|
||||
// path following Next's asset boundary can describe an App Router module.
|
||||
const assetRoot = value.indexOf("/_next/");
|
||||
if (assetRoot >= 0) value = value.slice(assetRoot + "/_next/".length);
|
||||
const match =
|
||||
/(?:^|\/)app\/(?:(.*?)\/)?page(?:-([^/?#]+)\.js|\.(?:[cm]?jsx?|tsx?)|)(?:[?#].*)?$/u.exec(
|
||||
value,
|
||||
// Ownership selects the routing root before page-segment conversion.
|
||||
let relative: string | undefined;
|
||||
let chunk = false;
|
||||
if (context.kind === "source-path") {
|
||||
if (context.source.kind === "outside-app-directory") return;
|
||||
if (
|
||||
context.source.kind === "app-directory" ||
|
||||
context.source.kind === "compiled-directory"
|
||||
) {
|
||||
const prefix = `${context.source.directory}/`;
|
||||
if (!value.startsWith(prefix)) return;
|
||||
relative = value.slice(prefix.length);
|
||||
chunk = context.source.kind === "compiled-directory";
|
||||
}
|
||||
} else if (context.kind === "script-url" || context.runtime === "next") {
|
||||
const boundary = "/_next/static/chunks/app/";
|
||||
const assetRoot = value.indexOf(boundary);
|
||||
const localChunk = ["static/chunks/app/", ".next/static/chunks/app/"].find(
|
||||
(prefix) => value.startsWith(prefix),
|
||||
);
|
||||
if (!match) return;
|
||||
if (match[2] !== undefined && !/(?:^|\/)static\/chunks\/app\//u.test(value))
|
||||
if (assetRoot >= 0) {
|
||||
relative = value.slice(assetRoot + boundary.length);
|
||||
chunk = true;
|
||||
} else if (context.kind === "source-literal" && localChunk !== undefined) {
|
||||
relative = value.slice(localChunk.length);
|
||||
chunk = true;
|
||||
} else if (context.kind === "source-literal") {
|
||||
const prefix = value.startsWith("src/app/") ? "src/app/" : "app/";
|
||||
if (value.startsWith(prefix)) relative = value.slice(prefix.length);
|
||||
}
|
||||
}
|
||||
if (relative === undefined) {
|
||||
if (
|
||||
/(?:^|\/)app\/(?:.*\/)?page(?:\.[cm]?[jt]sx?|-[^/?#]+\.js|)(?:[?#].*)?$/u.test(
|
||||
value,
|
||||
)
|
||||
)
|
||||
return {
|
||||
kind: "unknown",
|
||||
reason:
|
||||
context.kind === "source-path" && context.source.kind === "unknown"
|
||||
? context.source.reason
|
||||
: "The page-shaped path has no established Next App Router root or runtime context.",
|
||||
};
|
||||
return;
|
||||
}
|
||||
const match =
|
||||
/^(?:(.*?)\/)?page(?:-([^/?#]+)\.js|\.(?:[cm]?jsx?|tsx?)|)(?:[?#].*)?$/u.exec(
|
||||
relative,
|
||||
);
|
||||
if (match === null || (match[2] !== undefined && !chunk)) return;
|
||||
const sourceSegments = (match[1] ?? "").split("/");
|
||||
// A private folder excludes the whole page, including intercepted ancestors.
|
||||
// Keep filesystem segment spelling: only Next's explicit %5F escape is
|
||||
|
||||
Reference in new issue
Block a user