mirror of
https://github.com/morluto/rea
synced 2026-10-28 23:11:11 -04:00
fix(javascript): test literals for Next page paths in linear time (#7748)
Every source string literal without an established Next root was tested against /(?:^|\/)app\/(?:[\s\S]*\/)?page(?:\.[cm]?[jt]sx?|-[^/?#]+\.js|)(?:[?#].*)?$/u. The unanchored search restarted at every app/ segment and rescanned to the end of the literal, and each page? or page# candidate scanned to the next line terminator before $ failed. A long literal therefore took quadratic time on the analysis thread: about 6 s for a 256 KB literal of repeated app/ segments, and 3.5 s for 224 KB of x/page? followed by a newline. The first app/ segment admits every later page segment, and . cannot cross a line terminator, so a query or fragment after the page filename starts on the last line. Test the tail after the first app/ segment one page segment at a time instead; results are unchanged.
This commit is contained in:
@@ -55,6 +55,31 @@ export type NextAppRoutePath =
|
||||
| { readonly kind: "known"; readonly path: string }
|
||||
| { readonly kind: "unknown"; readonly reason: string };
|
||||
|
||||
/**
|
||||
* Test /(?:^|\/)app\/(?:[\s\S]*\/)?page(?:\.[cm]?[jt]sx?|-[^/?#]+\.js|)(?:[?#].*)?$/u
|
||||
* in linear time; every source literal reaches this test. The first `app/`
|
||||
* segment admits every later page segment, and `.` cannot cross a line
|
||||
* terminator, so a query or fragment after the page filename starts on the
|
||||
* last line. Retrying from each `app/` or `?` instead is quadratic.
|
||||
*/
|
||||
const isPageShapedPath = (value: string): boolean => {
|
||||
const app = /(?:^|\/)app\//u.exec(value);
|
||||
if (app === null) return false;
|
||||
const tail = value.slice(app.index + app[0].length);
|
||||
const lastLineStart =
|
||||
Math.max(
|
||||
tail.lastIndexOf("\n"),
|
||||
tail.lastIndexOf("\r"),
|
||||
tail.lastIndexOf("\u2028"),
|
||||
tail.lastIndexOf("\u2029"),
|
||||
) + 1;
|
||||
for (const page of tail.matchAll(
|
||||
/(?:^|\/)page(?:\.[cm]?[jt]sx?|-[^/?#]+\.js|)(?=[?#]|$)/gu,
|
||||
))
|
||||
if (page.index + page[0].length >= lastLineStart) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
/** Classify a page filename without treating missing routing context as absence. */
|
||||
export const classifyNextAppRoutePath = (
|
||||
raw: string,
|
||||
@@ -104,11 +129,7 @@ export const classifyNextAppRoutePath = (
|
||||
}
|
||||
}
|
||||
if (relative === undefined) {
|
||||
if (
|
||||
/(?:^|\/)app\/(?:[\s\S]*\/)?page(?:\.[cm]?[jt]sx?|-[^/?#]+\.js|)(?:[?#].*)?$/u.test(
|
||||
value,
|
||||
)
|
||||
)
|
||||
if (isPageShapedPath(value))
|
||||
return {
|
||||
kind: "unknown",
|
||||
reason:
|
||||
|
||||
@@ -798,6 +798,22 @@ it.each(["app/(.)photo/_private/page.tsx", "app/_private/(.)photo/page.tsx"])(
|
||||
},
|
||||
);
|
||||
|
||||
it("tests long literals for page-shaped Next paths in linear time", () => {
|
||||
const classify = (value: string) =>
|
||||
classifyNextAppRoutePath(value, {
|
||||
kind: "source-literal",
|
||||
runtime: "unknown",
|
||||
})?.kind;
|
||||
const started = performance.now();
|
||||
expect(classify("app/".repeat(64_000))).toBeUndefined();
|
||||
expect(classify(`app/${"x/page?".repeat(32_000)}\nend`)).toBeUndefined();
|
||||
expect(performance.now() - started).toBeLessThan(1_000);
|
||||
expect(classify("lib/app/a\nb/page.tsx")).toBe("unknown");
|
||||
expect(classify("app/page-a\nb.js?v=1")).toBe("unknown");
|
||||
expect(classify("app/x/page?first\nsecond")).toBeUndefined();
|
||||
expect(classify("app/x/page.tsx/extra")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps the already-covered aliased binary fragment excluded", () => {
|
||||
const facts = analyze(
|
||||
"createBrowserRouter([{path:'/users'}]);const target={pathname:'/users/'+id};router.push(target);",
|
||||
|
||||
Reference in New Issue
Block a user