mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
* fix(javascript): mark unisolated program-var route values unproven `JavaScriptRouteScan.#resolve` now records the identifier whenever a value proof depends on a program-scope `var` in a script without a proven isolated runtime. That binding is a global-object property, so `globalThis.NAME` or an alias can replace it after the source initializer. The mark lives in the shared resolver, so every consumer degrades from one fact instead of each one opting in: - path segments and navigation destinations stop asserting the source value and report an unknown interpretation; - an index flag stays unproven, as before; - a route container keeps its source route set and reports an incomplete interpretation, so `var routes=[...]` no longer loses every route. The source initializer is still followed, which is what lets a container consumer keep its facts while disclosing the gap. * fix(javascript): keep literal child routes behind an unisolated children array Rejecting the whole children configuration marked its records as claimed by the interpretation, so the automatic-root loop skipped them and the literal child paths disappeared even though their absolute paths need no parent proof. Keep the source records reachable and let the walker report the unproven ancestry as an interpretation gap. * test(javascript): bound the index-route CLI acceptance matrix The flag matrix runs sixteen CLI invocations in one test and passed at 20.7 s against a 30 s default timeout, so it timed out on a loaded runner. The same matrix is already asserted per case in javascriptRouteScan.test.ts, where each case is an in-process parse; this file keeps one resolved case and one unresolved case to prove both branches of the public CLI view. * fix(javascript): retain ownership of unproven route literals Keep source ownership separate from runtime value proof so rejected program-var route segments cannot reappear through the generic constant pass. Cover path, route, typed and alias forms, retain navigation controls and independent literals, and verify inventory and public CLI projections. Run the existing CLI index cases independently so repeated process startup does not consume one combined test deadline. Preserve both cases and all assertions. * fix(javascript): keep unisolated navigation data out of route literals An unisolated program-var slot holds data, not a route. Refusing its runtime identity released the object back to the independent route scan, which then reported a query payload as a nested route. Report the object as consumed navigation data and stop following it as a path. * fix(javascript): follow rejected navigation data without trusting it Claiming the whole object for a rejected value also claimed its sibling properties, so an independent route in the same object disappeared. Keep the property selection: ownership follows the slot, and only the route consumers refuse an unproven runtime identity.
678 lines
22 KiB
TypeScript
678 lines
22 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { analyzeJavaScriptStaticSource } from "./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([]);
|
|
},
|
|
);
|