mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
fix(web): interpret encoded Next route paths
This commit is contained in:
1 parent
087acf26e8
commit
0dea08fb07
3 files changed
+152
-5
No files matched your search
@@ -421,3 +421,86 @@ it.each([
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("decodes captured Next script URL segments once while retaining source URL identity", () => {
|
||||
const url =
|
||||
"https://example.test/base/_next/static/chunks/app/%28shop%29/blog/%5Bid%5D/page-abc.js";
|
||||
const result = build([
|
||||
source("captured-page", "", url),
|
||||
source("app/blog/%5Bid%5D/page.js", ""),
|
||||
]);
|
||||
expect(result.routes.map(({ path }) => path)).toEqual([
|
||||
"/base/blog/:id",
|
||||
"/blog/%5Bid%5D",
|
||||
]);
|
||||
expect(result.sources.find((entry) => entry.url === url)?.url).toBe(url);
|
||||
expect(result.occurrences).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
origin: "source-url",
|
||||
unprefixed_path: "/blog/:id",
|
||||
inferred_base_path: "/base",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["team%2Fadmin", "team%5Cadmin", "%E0%A4%A"])(
|
||||
"keeps malformed Next URL segment %s unknown alongside usable script facts",
|
||||
(segment) => {
|
||||
const url = `https://example.test/_next/static/chunks/app/${segment}/page-abc.js`;
|
||||
const result = build([source("page", "router.push('/usable');", url)]);
|
||||
expect(result.routes.map(({ path }) => path)).toEqual(["/usable"]);
|
||||
expect(result.sources[0]?.url).toBe(url);
|
||||
expect(result.coverage.candidate_enumeration).toBe("partial");
|
||||
expect(result.unknowns).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "interpretation",
|
||||
reason: expect.stringContaining("Next script URL"),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves literal percent encoding after one script URL decode", () => {
|
||||
const result = build([
|
||||
source(
|
||||
"page",
|
||||
"",
|
||||
"https://example.test/_next/static/chunks/app/%255Bid%255D/page-abc.js",
|
||||
),
|
||||
]);
|
||||
expect(result.routes.map(({ path }) => path)).toEqual(["/%5Bid%5D"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"page%3Fnotes.js",
|
||||
"page%23notes.js",
|
||||
"page.js%3Fnotes",
|
||||
"page.js%23notes",
|
||||
])("keeps encoded Next script filename delimiters literal: %s", (leaf) => {
|
||||
const url = `https://example.test/_next/static/chunks/app/dashboard/${leaf}`;
|
||||
const result = build([source("captured-page", "", url)]);
|
||||
expect(result.routes).toEqual([]);
|
||||
expect(result.sources[0]?.url).toBe(url);
|
||||
});
|
||||
|
||||
it.each(["?cache=1", "#section", "?cache=1#section"])(
|
||||
"parses actual Next script URL suffix %s before classifying the filename",
|
||||
(suffix) => {
|
||||
const url = `https://example.test/_next/static/chunks/app/dashboard/page-abc.js${suffix}`;
|
||||
const result = build([source("captured-page", "", url)]);
|
||||
expect(result.routes.map(({ path }) => path)).toEqual(["/dashboard"]);
|
||||
expect(result.sources[0]?.url).toBe(url);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["page?notes.js", "page#notes.js", "page.js?notes", "page.js#notes"])(
|
||||
"keeps filesystem Next page filename delimiters literal: %s",
|
||||
(leaf) => {
|
||||
const result = build([source(`app/dashboard/${leaf}`, "")]);
|
||||
expect(result.routes).toEqual([]);
|
||||
},
|
||||
);
|
||||
@@ -62,10 +62,10 @@ export const classifyNextAppRoutePath = (
|
||||
): NextAppRoutePath | undefined => {
|
||||
if (raw.includes("\\") || context.kind === "source-reference") return;
|
||||
let value = raw;
|
||||
if (
|
||||
const urlPath =
|
||||
context.kind === "script-url" ||
|
||||
(context.kind === "source-literal" && raw.includes("://"))
|
||||
) {
|
||||
(context.kind === "source-literal" && raw.includes("://"));
|
||||
if (urlPath) {
|
||||
try {
|
||||
value = new URL(raw).pathname;
|
||||
} catch {
|
||||
@@ -118,11 +118,36 @@ export const classifyNextAppRoutePath = (
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (urlPath) {
|
||||
// Captured script URLs encode filesystem segments. Decode once here, after
|
||||
// selecting the Next asset root; source paths retain their literal spelling.
|
||||
if (/%(?:2f|5c)/iu.test(relative))
|
||||
return {
|
||||
kind: "unknown",
|
||||
reason: "A Next script URL contains an encoded path separator.",
|
||||
};
|
||||
try {
|
||||
relative = decodeURIComponent(relative);
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof URIError)) throw cause;
|
||||
return {
|
||||
kind: "unknown",
|
||||
reason: "A Next script URL contains malformed percent encoding.",
|
||||
};
|
||||
}
|
||||
}
|
||||
const match =
|
||||
/^(?:(.*?)\/)?page(?:-([^/?#]+)\.js|\.(?:[cm]?jsx?|tsx?)|)(?:[?#].*)?$/u.exec(
|
||||
/^(?:(.*?)\/)?page(?:-([^/?#]+)\.js|\.(?:[cm]?jsx?|tsx?)|)([?#].*)?$/u.exec(
|
||||
relative,
|
||||
);
|
||||
if (match === null || (match[2] !== undefined && !chunk)) return;
|
||||
if (
|
||||
match === null ||
|
||||
(match[2] !== undefined && !chunk) ||
|
||||
// URL parsing already removed URL suffixes. Any decoded delimiter is a
|
||||
// filename byte, as it is in an inventoried filesystem source path.
|
||||
(match[3] !== undefined && (urlPath || context.kind === "source-path"))
|
||||
)
|
||||
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