mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
feat(javascript): retain queryable static route inventories (#6019)
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
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!,
|
||||
});
|
||||
const build = (inputs: JavaScriptRouteInventoryInput[], bytes?: number) =>
|
||||
javaScriptRouteInventorySchema.parse(
|
||||
completeSemanticSteps(buildJavaScriptRouteInventorySteps(inputs, bytes)),
|
||||
);
|
||||
|
||||
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: 3,
|
||||
source_count: 2,
|
||||
});
|
||||
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("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("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,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user