mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
678 lines
22 KiB
TypeScript
678 lines
22 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { analyzeJavaScriptStaticSource } from "../../../tests/fixtures/javascriptStaticAnalysis.js";
|
|
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
|
|
import {
|
|
buildJavaScriptRouteInventorySteps,
|
|
javaScriptRouteInventorySchema,
|
|
type JavaScriptRouteInventoryInput,
|
|
} from "./javascriptRouteInventory.js";
|
|
|
|
const source = (
|
|
key: string,
|
|
text: string,
|
|
url: string | null = null,
|
|
): JavaScriptRouteInventoryInput => ({
|
|
key,
|
|
path: url === null ? key : null,
|
|
url,
|
|
sha256: "a".repeat(64),
|
|
container_sha256: null,
|
|
status: "complete",
|
|
reason: null,
|
|
facts: analyzeJavaScriptStaticSource(text).routes!,
|
|
...(url === null && key.startsWith("app/")
|
|
? {
|
|
next_app_context: {
|
|
kind: "app-directory" as const,
|
|
directory: "app",
|
|
package: {
|
|
path: "package.json",
|
|
sha256: "b".repeat(64),
|
|
container_sha256: "c".repeat(64),
|
|
},
|
|
},
|
|
}
|
|
: {}),
|
|
});
|
|
const build = (inputs: JavaScriptRouteInventoryInput[], bytes?: number) =>
|
|
javaScriptRouteInventorySchema.parse(
|
|
completeSemanticSteps(buildJavaScriptRouteInventorySteps(inputs, bytes)),
|
|
);
|
|
|
|
it("keeps rejected program-var paths out of independent route inventory", () => {
|
|
const result = build([
|
|
source(
|
|
"routes.js",
|
|
"var PATH='/reports/stale';globalThis.PATH='/other';createBrowserRouter([{path:PATH},{path:'/reports'}]);const independent='/reports/help';",
|
|
),
|
|
]);
|
|
expect(result.routes.map(({ path }) => path)).toEqual([
|
|
"/reports",
|
|
"/reports/help",
|
|
]);
|
|
expect(result.unknowns).toEqual([
|
|
expect.objectContaining({
|
|
kind: "interpretation",
|
|
reason: expect.stringContaining("parent segment is unknown"),
|
|
}),
|
|
]);
|
|
expect(result.coverage).toMatchObject({
|
|
source_parsing: "complete",
|
|
candidate_enumeration: "partial",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["hello world", "hello%20world"],
|
|
["team?admin", "team%3Fadmin"],
|
|
["team#admin", "team%23admin"],
|
|
] as const)(
|
|
"preserves Next filename pattern %s and its source identity",
|
|
(segment, encoded) => {
|
|
const url = `https://example.test/_next/static/chunks/app/${encoded}/page-abc.js`;
|
|
const result = build([
|
|
source(`app/${segment}/page.js`, ""),
|
|
source("captured.js", "", url),
|
|
source(
|
|
"runtime.js",
|
|
`globalThis.TURBOPACK=[];const page=${JSON.stringify(url)};`,
|
|
),
|
|
]);
|
|
expect(result.routes).toMatchObject([
|
|
{ path: `/${segment}`, source_count: 3, occurrence_count: 3 },
|
|
]);
|
|
expect(result.routes).toHaveLength(1);
|
|
expect(result.occurrences.map(({ source_id }) => source_id).sort()).toEqual(
|
|
result.sources.map(({ id }) => id).sort(),
|
|
);
|
|
expect(result.sources).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ path: `app/${segment}/page.js`, url: null }),
|
|
expect.objectContaining({ path: null, url }),
|
|
]),
|
|
);
|
|
expect(result.unknowns).toEqual([]);
|
|
expect(result.coverage).toEqual({
|
|
scope: "provided-code-sources",
|
|
source_parsing: "complete",
|
|
candidate_enumeration: "complete",
|
|
application_routes: "unknown",
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each(["%00", "%09", "%0A", "%7F"])(
|
|
"retains an interpretation gap for a Next filename containing %s",
|
|
(encoded) => {
|
|
const url = `https://example.test/_next/static/chunks/app/hello${encoded}world/page-abc.js`;
|
|
const result = build([source("captured.js", "", url)]);
|
|
expect(result.routes).toEqual([]);
|
|
expect(result.unknowns).toMatchObject([
|
|
{
|
|
source_id: result.sources[0]!.id,
|
|
kind: "interpretation",
|
|
reason: expect.stringContaining("control character"),
|
|
},
|
|
]);
|
|
expect(result.sources[0]!.url).toBe(url);
|
|
expect(result.coverage.candidate_enumeration).toBe("partial");
|
|
},
|
|
);
|
|
|
|
it.each(["app/page.js", "app/hello\nworld/page.js"])(
|
|
"does not promote page-shaped path %j without source-router applicability",
|
|
(path) => {
|
|
const unknown = source(path, "");
|
|
delete (unknown as { next_app_context?: unknown }).next_app_context;
|
|
const result = build([unknown]);
|
|
expect(result.routes).toEqual([]);
|
|
expect(result.unknowns).toMatchObject([
|
|
{
|
|
source_id: result.sources[0]!.id,
|
|
kind: "interpretation",
|
|
reason: expect.stringContaining("not measured"),
|
|
},
|
|
]);
|
|
expect(result.sources[0]!.path).toBe(path);
|
|
expect(result.coverage.candidate_enumeration).toBe("partial");
|
|
},
|
|
);
|
|
|
|
it("binds source identity to the package evidence establishing router applicability", () => {
|
|
const first = source("app/page.js", "");
|
|
const context = first.next_app_context;
|
|
if (context?.kind !== "app-directory")
|
|
throw new Error("Expected declared fixture context");
|
|
const second = {
|
|
...first,
|
|
next_app_context: {
|
|
...context,
|
|
package: { ...context.package, sha256: "d".repeat(64) },
|
|
},
|
|
};
|
|
expect(build([first]).sources[0]!.id).not.toBe(
|
|
build([second]).sources[0]!.id,
|
|
);
|
|
});
|
|
|
|
it("coalesces exact patterns while retaining independent occurrences and stable identities", () => {
|
|
const inputs = [
|
|
source("one.js", `router.push('/reports');`),
|
|
source(
|
|
"two.js",
|
|
`createBrowserRouter([{path:'/reports',children:[{path:'settings'}]}]);`,
|
|
),
|
|
];
|
|
const result = build(inputs),
|
|
repeated = build([...inputs].reverse());
|
|
expect(result.routes.map(({ path }) => path)).toEqual([
|
|
"/reports",
|
|
"/reports/settings",
|
|
]);
|
|
expect(result.routes[0]).toMatchObject({
|
|
occurrence_count: 2,
|
|
source_count: 2,
|
|
});
|
|
const reportOccurrences = result.occurrences.filter(
|
|
({ route_id }) => route_id === result.routes[0]!.id,
|
|
);
|
|
expect(
|
|
reportOccurrences
|
|
.map(({ source_id, mechanism, segments }) => ({
|
|
source: result.sources.find(({ id }) => id === source_id)?.key,
|
|
mechanism,
|
|
segments: segments.map(({ value }) => value),
|
|
}))
|
|
.sort((left, right) => left.source!.localeCompare(right.source!)),
|
|
).toEqual([
|
|
{
|
|
source: "one.js",
|
|
mechanism: "navigation-target",
|
|
segments: ["/reports"],
|
|
},
|
|
{
|
|
source: "two.js",
|
|
mechanism: "nested-route-object",
|
|
segments: ["/reports"],
|
|
},
|
|
]);
|
|
expect(new Set(result.occurrences.map(({ id }) => id)).size).toBe(
|
|
result.occurrences.length,
|
|
);
|
|
expect(result.routes.map(({ id }) => id)).toEqual(
|
|
repeated.routes.map(({ id }) => id),
|
|
);
|
|
expect(result.source_manifest_sha256).toBe(repeated.source_manifest_sha256);
|
|
expect(result.inventory_id).toBe(repeated.inventory_id);
|
|
expect(result.coverage.application_routes).toBe("unknown");
|
|
});
|
|
it("retains resolved index routes with source provenance and complete enumeration", () => {
|
|
const result = build([
|
|
source(
|
|
"routes.ts",
|
|
`const INDEX=true; createBrowserRouter([{index:(INDEX as boolean),Component:Home},{path:'/parent',children:[{index:INDEX,Component:Home}]}]);`,
|
|
),
|
|
]);
|
|
expect(result.routes.map(({ path }) => path)).toEqual(["/", "/parent"]);
|
|
expect(result.occurrences).toHaveLength(3);
|
|
expect(result.occurrences).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
route_id: result.routes[0]!.id,
|
|
unprefixed_path: "/",
|
|
origin: "source-syntax",
|
|
mechanism: "nested-route-object",
|
|
source_range: expect.objectContaining({ start: expect.any(Object) }),
|
|
}),
|
|
]),
|
|
);
|
|
expect(result.unknowns).toEqual([]);
|
|
expect(result.coverage).toMatchObject({
|
|
source_parsing: "complete",
|
|
candidate_enumeration: "complete",
|
|
application_routes: "unknown",
|
|
});
|
|
});
|
|
it.each([
|
|
"const INDEX=choose();",
|
|
"var INDEX=true;globalThis.INDEX=false;",
|
|
"var INDEX=true;const host=globalThis;host.INDEX=false;",
|
|
])(
|
|
"marks unproven index interpretation as partial with child paths: %s",
|
|
(prefix) => {
|
|
const result = build([
|
|
source(
|
|
"routes.js",
|
|
`${prefix}createBrowserRouter([{index:INDEX,children:[{path:'child'}]}]);`,
|
|
),
|
|
]);
|
|
expect(result.routes.map(({ path }) => path)).toEqual(["/child"]);
|
|
expect(result.unknowns).toEqual([
|
|
expect.objectContaining({
|
|
kind: "interpretation",
|
|
reason: expect.stringContaining("index-route flag"),
|
|
source_range: expect.objectContaining({ start: expect.any(Object) }),
|
|
}),
|
|
]);
|
|
expect(result.coverage).toMatchObject({
|
|
source_parsing: "complete",
|
|
candidate_enumeration: "partial",
|
|
application_routes: "unknown",
|
|
});
|
|
},
|
|
);
|
|
it("chooses deterministic primary provenance when sources have equal confidence", () => {
|
|
const inputs = [
|
|
source("one.js", `router.push('/reports');`),
|
|
source("two.js", `router.push('/reports');`),
|
|
];
|
|
expect(build(inputs)).toEqual(build([...inputs].reverse()));
|
|
});
|
|
it("binds ordinary constants to application segments across sources and excludes framework filesystem noise", () => {
|
|
const result = build([
|
|
source("navigation.js", `router.push('/reports');`),
|
|
source(
|
|
"runtime.js",
|
|
`globalThis.TURBOPACK=[];const routes=['/reports/:id','/ROOT/node_modules/next/process/','/_not-found'];`,
|
|
),
|
|
]);
|
|
expect(result.routes.map(({ path }) => path)).toEqual([
|
|
"/reports",
|
|
"/reports/:id",
|
|
]);
|
|
expect(result.statistics.unanchored_literals_excluded).toBe(2);
|
|
});
|
|
it.each(["api", "assets", "static", "_next"])(
|
|
"retains constants under an anchored %s segment without promoting unanchored paths",
|
|
(prefix) => {
|
|
const result = build([
|
|
source("route.js", `router.push('/${prefix}/status');`),
|
|
source(
|
|
"runtime.js",
|
|
`globalThis.TURBOPACK=[];const same='/${prefix}/status';const help='/${prefix}/help';const other='/unanchored/noise';`,
|
|
),
|
|
]);
|
|
const anchored = result.routes.find(
|
|
({ path }) => path === `/${prefix}/status`,
|
|
);
|
|
const descendant = result.routes.find(
|
|
({ path }) => path === `/${prefix}/help`,
|
|
);
|
|
expect(anchored).toMatchObject({ occurrence_count: 2, source_count: 2 });
|
|
expect(descendant).toMatchObject({
|
|
confidence: "low",
|
|
classification: "unknown",
|
|
});
|
|
expect(result.routes.some(({ path }) => path === "/unanchored/noise")).toBe(
|
|
false,
|
|
);
|
|
expect(result.coverage.application_routes).toBe("unknown");
|
|
},
|
|
);
|
|
it("retains inferred Next script bases and the unprefixed path", () => {
|
|
const result = build([
|
|
source(
|
|
"script",
|
|
`router.push('/planner');`,
|
|
"https://example.test/app/_next/static/chunks/123.js",
|
|
),
|
|
]);
|
|
expect(result.routes.map(({ path }) => path)).toEqual(["/app/planner"]);
|
|
expect(result.occurrences[0]).toMatchObject({
|
|
unprefixed_path: "/planner",
|
|
inferred_base_path: "/app",
|
|
confidence: "low",
|
|
});
|
|
expect(result.sources[0]!.url).toBe(
|
|
"https://example.test/app/_next/static/chunks/123.js",
|
|
);
|
|
expect(result.unknowns.some(({ kind }) => kind === "inferred-base")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
it("reports missing source facts as partial and preserves filename-only discoveries without fake AST ranges", () => {
|
|
const result = build([
|
|
{
|
|
...source("app/reports/[id]/page.js", ""),
|
|
status: "unavailable",
|
|
reason: "source was not selected",
|
|
facts: null,
|
|
},
|
|
]);
|
|
expect(result.coverage).toMatchObject({
|
|
source_parsing: "partial",
|
|
candidate_enumeration: "partial",
|
|
application_routes: "unknown",
|
|
});
|
|
expect(result.routes[0]!.path).toBe("/reports/:id");
|
|
expect(result.occurrences[0]).toMatchObject({
|
|
origin: "source-path",
|
|
source_range: null,
|
|
});
|
|
});
|
|
it("does not interpret the URL authority or file URL prefix as an App Router directory or routing base", () => {
|
|
const result = build([
|
|
source("script", `router.push('/reports');`, "https://app/reports/page.js"),
|
|
]);
|
|
expect(result.routes.map(({ path }) => path)).toEqual(["/reports"]);
|
|
const local = build([
|
|
source("file", `router.push('/reports');`, "file:///tmp/_next/123.js"),
|
|
]);
|
|
expect(local.sources[0]!.base_path).toBeNull();
|
|
expect(local.routes[0]!.path).toBe("/reports");
|
|
});
|
|
it("rejects forged aggregate counts and detached provenance", () => {
|
|
const result = build([source("one.js", `router.push('/reports');`)]);
|
|
expect(
|
|
javaScriptRouteInventorySchema.safeParse({
|
|
...result,
|
|
routes: result.routes.map((route) => ({
|
|
...route,
|
|
occurrence_count: 99,
|
|
})),
|
|
}).success,
|
|
).toBe(false);
|
|
expect(
|
|
javaScriptRouteInventorySchema.safeParse({ ...result, sources: [] })
|
|
.success,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("retains a bounded prefix with explicit capacity and counts only retained facts", () => {
|
|
const inputs = [
|
|
source(
|
|
"many.js",
|
|
Array.from(
|
|
{ length: 100 },
|
|
(_, index) => `router.push('/reports/${index}');`,
|
|
).join("\n"),
|
|
),
|
|
];
|
|
const result = build(inputs, 8192);
|
|
expect(result.routes.length).toBeGreaterThan(0);
|
|
expect(result.routes.length).toBeLessThan(100);
|
|
expect(result.coverage.candidate_enumeration).toBe("partial");
|
|
expect(result.capacity?.limit_bytes).toBe(8192);
|
|
expect(Buffer.byteLength(JSON.stringify(result))).toBeLessThanOrEqual(8192);
|
|
expect(() => build(inputs, 1)).toThrow(RangeError);
|
|
});
|
|
|
|
it("anchors generic constants before applying an inferred asset base", () => {
|
|
const result = build([
|
|
source(
|
|
"prefixed.js",
|
|
`globalThis.TURBOPACK=[];createBrowserRouter([{path:'/reports'}]);const paths=['/reports/:id','/noise'];`,
|
|
"https://example.test/app/_next/static/chunks/one.js",
|
|
),
|
|
]);
|
|
expect(result.routes.map((row) => row.path)).toEqual([
|
|
"/app/reports",
|
|
"/app/reports/:id",
|
|
]);
|
|
expect(result.statistics.unanchored_literals_excluded).toBe(1);
|
|
});
|
|
it("classifies typed page configuration while generic server route calls remain unknown", () => {
|
|
const result = build([
|
|
source(
|
|
"typed.js",
|
|
`defineRoutes({reports:{_$:'reports',settings:{_$:'settings'}}});app.route('/api/users');`,
|
|
),
|
|
]);
|
|
expect(
|
|
result.routes
|
|
.filter((row) => row.classification === "page")
|
|
.map((row) => row.path),
|
|
).toEqual(["/reports", "/reports/settings"]);
|
|
expect(result.routes.find((row) => row.path === "/api/users")).toMatchObject({
|
|
classification: "unknown",
|
|
classification_basis: "unknown",
|
|
});
|
|
});
|
|
|
|
it("keeps TypeScript page filename provenance separate from syntax ranges", () => {
|
|
const result = build([
|
|
source(
|
|
"app/(account)/reports/[id]/page.tsx",
|
|
"export default function Page() { return null; }",
|
|
),
|
|
]);
|
|
expect(result.routes).toHaveLength(1);
|
|
expect(result.routes[0]).toMatchObject({
|
|
path: "/reports/:id",
|
|
classification: "page",
|
|
classification_basis: "next-app-page",
|
|
});
|
|
expect(result.occurrences[0]).toMatchObject({
|
|
origin: "source-path",
|
|
source_range: null,
|
|
});
|
|
});
|
|
|
|
it("coalesces inferred deployment roots without rewriting explicit trailing slashes", () => {
|
|
const result = build([
|
|
source(
|
|
"root-call",
|
|
"router.push('/');",
|
|
"https://example.test/app/_next/static/chunks/runtime.js",
|
|
),
|
|
source(
|
|
"root-page",
|
|
"",
|
|
"https://example.test/app/_next/static/chunks/app/page.tsx",
|
|
),
|
|
source("explicit.js", "router.push('/app');router.push('/app/');"),
|
|
]);
|
|
expect(result.routes.map(({ path }) => path)).toEqual(["/app", "/app/"]);
|
|
const root = result.routes.find(({ path }) => path === "/app")!;
|
|
expect(root.source_count).toBe(3);
|
|
const page = result.occurrences.find(({ origin }) => origin === "source-url");
|
|
expect(page).toMatchObject({
|
|
route_id: root.id,
|
|
unprefixed_path: "/",
|
|
inferred_base_path: "/app",
|
|
source_range: null,
|
|
});
|
|
expect(result.unknowns.some(({ kind }) => kind === "inferred-base")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("keeps enumeration partial when a recognized navigation target is unresolved", () => {
|
|
const result = build([source("navigation.js", "router.navigate(target);")]);
|
|
expect(result.routes).toEqual([]);
|
|
expect(result.coverage.candidate_enumeration).toBe("partial");
|
|
expect(result.unknowns).toEqual([
|
|
expect.objectContaining({
|
|
kind: "interpretation",
|
|
source_id: result.sources[0]!.id,
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it.each(["/app/reports", "/reports"])(
|
|
"matches based and unbased constants against application segments for %s",
|
|
(anchor) => {
|
|
const result = build([
|
|
source(
|
|
"mixed.js",
|
|
`globalThis.TURBOPACK=[];router.push('${anchor}');const paths=['/reports/settings','/app/reports/history','/noise','/app/noise','/ROOT/node_modules/framework'];`,
|
|
"https://example.test/app/_next/static/chunks/one.js",
|
|
),
|
|
]);
|
|
expect(result.routes.map(({ path }) => path)).toEqual([
|
|
"/app/reports",
|
|
"/app/reports/history",
|
|
"/app/reports/settings",
|
|
]);
|
|
expect(result.occurrences).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
unprefixed_path: "/reports/settings",
|
|
inferred_base_path: "/app",
|
|
}),
|
|
expect.objectContaining({
|
|
unprefixed_path: "/app/reports/history",
|
|
inferred_base_path: null,
|
|
}),
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
|
|
it("keeps filename and module-derived anchors application-relative when a deployment base matches an application segment", () => {
|
|
const result = build([
|
|
source(
|
|
"page",
|
|
"",
|
|
"https://example.test/app/_next/static/chunks/app/app/page-abc.js",
|
|
),
|
|
source(
|
|
"root-runtime",
|
|
"globalThis.TURBOPACK=[];const paths=['/app/settings','/noise'];",
|
|
"https://example.test/_next/static/chunks/root.js",
|
|
),
|
|
source(
|
|
"module-runtime",
|
|
"globalThis.TURBOPACK=[];const modulePath='app/app/page.js';",
|
|
"https://example.test/app/_next/static/chunks/runtime.js",
|
|
),
|
|
]);
|
|
expect(result.routes.map(({ path }) => path)).toEqual([
|
|
"/app/app",
|
|
"/app/settings",
|
|
]);
|
|
const page = result.routes.find(({ path }) => path === "/app/app")!;
|
|
expect(page.source_count).toBe(2);
|
|
for (const origin of ["source-url", "source-syntax"])
|
|
expect(result.occurrences).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
route_id: page.id,
|
|
origin,
|
|
unprefixed_path: "/app",
|
|
inferred_base_path: "/app",
|
|
}),
|
|
]),
|
|
);
|
|
expect(result.statistics.unanchored_literals_excluded).toBe(1);
|
|
});
|
|
|
|
it("reports generated typed configurations as partial even without retained candidates", () => {
|
|
const result = build([
|
|
source("generated.js", "defineRoutes({reports:buildRoutes()});"),
|
|
]);
|
|
expect(result.routes).toEqual([]);
|
|
expect(result.coverage.candidate_enumeration).toBe("partial");
|
|
expect(result.unknowns).toHaveLength(1);
|
|
expect(result.unknowns[0]!.kind).toBe("interpretation");
|
|
});
|
|
|
|
it.each([
|
|
["app/feed/(.)photo/page.tsx", null],
|
|
[
|
|
"page.js",
|
|
"https://example.test/_next/static/chunks/app/feed/(.)photo/page-123.js",
|
|
],
|
|
] as const)(
|
|
"retains intercepted filename metadata as partial: %s",
|
|
(path, url) => {
|
|
const result = build([
|
|
source(path, "export default function Photo(){return null;}", url),
|
|
]);
|
|
expect(result.routes).toEqual([]);
|
|
expect(result.coverage.candidate_enumeration).toBe("partial");
|
|
expect(result.unknowns).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "interpretation",
|
|
reason: expect.stringContaining("intercepted"),
|
|
source_range: null,
|
|
}),
|
|
]),
|
|
);
|
|
},
|
|
);
|
|
|
|
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([]);
|
|
},
|
|
);
|