mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 20:06:14 +00:00
fix(javascript): bind Next page admission to source context
This commit is contained in:
1 parent
c20b707680
commit
5ee5e49cb4
13 files changed
+532
-53
No files matched your search
@@ -42,6 +42,7 @@ import {
|
||||
type JavaScriptArtifactReconstructionInput,
|
||||
} from "./JavaScriptArtifactReconstructionInput.js";
|
||||
import { scanCanonicalArtifactInventoryInScope } from "../../artifacts/inventory/scanCanonical.js";
|
||||
import { createJavaScriptNextAppContext } from "./JavaScriptNextAppContext.js";
|
||||
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
|
||||
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
|
||||
import type { ProgressReporter } from "../ProgressReporter.js";
|
||||
@@ -285,6 +286,16 @@ export const reconstructJavaScriptArtifact = async (
|
||||
const semanticGraph =
|
||||
await completeJavaScriptAnalysisSteps(semanticGraphSteps);
|
||||
await checkpointCompletedAnalysis();
|
||||
const directoryPaths = new Set(
|
||||
snapshot.occurrences
|
||||
.filter(({ entry_kind }) => entry_kind === "directory")
|
||||
.map(({ logical_path }) => logical_path),
|
||||
);
|
||||
const nextAppContext = createJavaScriptNextAppContext(
|
||||
files.files,
|
||||
analysis.jsonDocuments,
|
||||
directoryPaths,
|
||||
);
|
||||
const completed = {
|
||||
route_inventory: await completeJavaScriptAnalysisSteps(
|
||||
buildJavaScriptRouteInventorySteps([
|
||||
@@ -292,6 +303,7 @@ export const reconstructJavaScriptArtifact = async (
|
||||
.filter(({ file }) => file.kind === "javascript")
|
||||
.map(({ file, javascript, analysis_failure }) => ({
|
||||
key: file.path,
|
||||
next_app_context: nextAppContext(file),
|
||||
path: file.path,
|
||||
url: null,
|
||||
sha256: file.sha256,
|
||||
@@ -318,6 +330,7 @@ export const reconstructJavaScriptArtifact = async (
|
||||
.filter((file) => file.kind === "javascript")
|
||||
.map((file) => ({
|
||||
key: file.path,
|
||||
next_app_context: nextAppContext(file),
|
||||
path: file.path,
|
||||
url: null,
|
||||
sha256: file.sha256,
|
||||
@@ -339,14 +352,7 @@ export const reconstructJavaScriptArtifact = async (
|
||||
: { source_selection: files.source_selection }),
|
||||
graph,
|
||||
semantic_graph: semanticGraph,
|
||||
electron_summary: summarizeElectronBoundaries(
|
||||
analysis,
|
||||
new Set(
|
||||
snapshot.occurrences
|
||||
.filter(({ entry_kind }) => entry_kind === "directory")
|
||||
.map(({ logical_path }) => logical_path),
|
||||
),
|
||||
),
|
||||
electron_summary: summarizeElectronBoundaries(analysis, directoryPaths),
|
||||
statistics: {
|
||||
relevant_files:
|
||||
files.files.length + files.unavailable_relevant_files.length,
|
||||
|
||||
@@ -15,6 +15,7 @@ interface JavaScriptJsonDocumentFactBase {
|
||||
export type JavaScriptJsonDocumentFacts =
|
||||
| (JavaScriptJsonDocumentFactBase & {
|
||||
readonly status: "included";
|
||||
readonly nextDependency: "declared" | "unproven";
|
||||
readonly packageFields: Readonly<
|
||||
Partial<Record<(typeof PACKAGE_PROPERTIES)[number], unknown>>
|
||||
> | null;
|
||||
@@ -127,6 +128,7 @@ const parseDocument = (
|
||||
return Object.freeze({
|
||||
source: snapshot.source,
|
||||
status: "included",
|
||||
nextDependency: "unproven",
|
||||
packageFields: null,
|
||||
topLevelKeys: Object.freeze([]),
|
||||
});
|
||||
@@ -145,9 +147,24 @@ const parseDocument = (
|
||||
packageFields[key] = Reflect.get(record, key);
|
||||
Object.freeze(packageFields);
|
||||
}
|
||||
const nextDeclared =
|
||||
snapshot.kind === "package-json" &&
|
||||
["dependencies", "devDependencies", "peerDependencies"].some((key) => {
|
||||
const dependencies: unknown = record[key];
|
||||
if (
|
||||
typeof dependencies !== "object" ||
|
||||
dependencies === null ||
|
||||
Array.isArray(dependencies) ||
|
||||
!Object.hasOwn(dependencies, "next")
|
||||
)
|
||||
return false;
|
||||
const version: unknown = Reflect.get(dependencies, "next");
|
||||
return typeof version === "string" && version.length > 0;
|
||||
});
|
||||
return Object.freeze({
|
||||
source: snapshot.source,
|
||||
status: "included",
|
||||
nextDependency: nextDeclared ? "declared" : "unproven",
|
||||
packageFields,
|
||||
topLevelKeys,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
import { createJavaScriptNextAppContext } from "./JavaScriptNextAppContext.js";
|
||||
|
||||
const file = (path: string, text = ""): JavaScriptArtifactFile => ({
|
||||
path,
|
||||
container_sha256: "a".repeat(64),
|
||||
sha256: "b".repeat(64),
|
||||
bytes: Buffer.byteLength(text),
|
||||
inventory_artifact_id: `art_${"b".repeat(64)}`,
|
||||
kind: path.endsWith("package.json") ? "package-json" : "javascript",
|
||||
unpacked: false,
|
||||
text: { included: true, value: text },
|
||||
});
|
||||
|
||||
it("uses the nearest declared Next package and root directory precedence", () => {
|
||||
const files = [
|
||||
file("package.json", '{"dependencies":{"next":"16.4.2"}}'),
|
||||
file("app/page.js"),
|
||||
file("src/app/control/page.js"),
|
||||
file("pages/app/page.js"),
|
||||
file("components/app/page.js"),
|
||||
file("node_modules/ordinary/package.json", '{"name":"ordinary"}'),
|
||||
file("node_modules/ordinary/app/page.js"),
|
||||
];
|
||||
const context = createJavaScriptNextAppContext(
|
||||
files,
|
||||
new JavaScriptJsonDocuments(),
|
||||
new Set([
|
||||
"app",
|
||||
"src/app",
|
||||
"pages",
|
||||
"components/app",
|
||||
"node_modules/ordinary/app",
|
||||
]),
|
||||
);
|
||||
expect(context(files[1]!)).toMatchObject({
|
||||
kind: "app-directory",
|
||||
directory: "app",
|
||||
package: { path: "package.json" },
|
||||
});
|
||||
for (const source of files.slice(2, 5))
|
||||
expect(context(source).kind).toBe("outside-app-directory");
|
||||
expect(context(files[6]!)).toMatchObject({
|
||||
kind: "unknown",
|
||||
package: { path: "node_modules/ordinary/package.json" },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps default compiled output under its declared package and unrelated chunk-like paths unknown", () => {
|
||||
const manifest = file("package.json", '{"dependencies":{"next":"16.4.2"}}');
|
||||
const chunk = file(".next/static/chunks/app/build/page-abc.js");
|
||||
const context = createJavaScriptNextAppContext(
|
||||
[manifest, chunk],
|
||||
new JavaScriptJsonDocuments(),
|
||||
new Set(),
|
||||
);
|
||||
expect(context(chunk)).toMatchObject({
|
||||
kind: "compiled-directory",
|
||||
directory: ".next/static/chunks/app",
|
||||
});
|
||||
expect(
|
||||
createJavaScriptNextAppContext(
|
||||
[chunk],
|
||||
new JavaScriptJsonDocuments(),
|
||||
new Set(),
|
||||
)(chunk),
|
||||
).toMatchObject({ kind: "unknown" });
|
||||
});
|
||||
|
||||
it("supports src/app only when app is not inventoried and retains missing framework identity", () => {
|
||||
const manifest = file(
|
||||
"package.json",
|
||||
'{"devDependencies":{"next":"16.4.2"}}',
|
||||
);
|
||||
const page = file("src/app/control/page.js");
|
||||
expect(
|
||||
createJavaScriptNextAppContext(
|
||||
[manifest, page],
|
||||
new JavaScriptJsonDocuments(),
|
||||
new Set(["src/app"]),
|
||||
)(page),
|
||||
).toMatchObject({ kind: "app-directory", directory: "src/app" });
|
||||
expect(
|
||||
createJavaScriptNextAppContext(
|
||||
[page],
|
||||
new JavaScriptJsonDocuments(),
|
||||
new Set(["src/app"]),
|
||||
)(page),
|
||||
).toMatchObject({ kind: "unknown", package: null });
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { posix } from "node:path";
|
||||
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import type { NextAppSourceContext } from "../../domain/javascript/javascriptRoutePaths.js";
|
||||
import type { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
/** Package declarations and inventoried directories own source-router applicability. */
|
||||
export const createJavaScriptNextAppContext = (
|
||||
files: readonly JavaScriptArtifactFile[],
|
||||
documents: JavaScriptJsonDocuments,
|
||||
directories: ReadonlySet<string>,
|
||||
): ((
|
||||
file: Pick<JavaScriptArtifactFile, "path" | "container_sha256">,
|
||||
) => NextAppSourceContext) => {
|
||||
const packages = new Map(
|
||||
files
|
||||
.filter(({ kind }) => kind === "package-json")
|
||||
.map((file) => [posix.dirname(file.path), file]),
|
||||
);
|
||||
const roots = new Map<JavaScriptArtifactFile, string | null>();
|
||||
return (file) => {
|
||||
for (
|
||||
let directory = posix.dirname(file.path);
|
||||
;
|
||||
directory = posix.dirname(directory)
|
||||
) {
|
||||
const manifest = packages.get(directory);
|
||||
if (
|
||||
manifest !== undefined &&
|
||||
manifest.container_sha256 === file.container_sha256
|
||||
) {
|
||||
const identity = {
|
||||
path: manifest.path,
|
||||
sha256: manifest.sha256,
|
||||
container_sha256: manifest.container_sha256,
|
||||
};
|
||||
const facts = documents.read(manifest);
|
||||
if (facts.status !== "included" || facts.nextDependency !== "declared")
|
||||
return {
|
||||
kind: "unknown",
|
||||
package: identity,
|
||||
reason:
|
||||
"The owning package does not establish a Next dependency for source routing.",
|
||||
};
|
||||
const prefix = directory === "." ? "" : `${directory}/`;
|
||||
const compiled = `${prefix}.next/static/chunks/app`;
|
||||
if (file.path.startsWith(`${compiled}/`))
|
||||
return {
|
||||
kind: "compiled-directory",
|
||||
directory: compiled,
|
||||
package: identity,
|
||||
};
|
||||
let root = roots.get(manifest);
|
||||
if (root === undefined) {
|
||||
root =
|
||||
["app", "src/app"]
|
||||
.map((candidate) => `${prefix}${candidate}`)
|
||||
.find((candidate) => directories.has(candidate)) ?? null;
|
||||
roots.set(manifest, root);
|
||||
}
|
||||
return root !== null && file.path.startsWith(`${root}/`)
|
||||
? { kind: "app-directory", directory: root, package: identity }
|
||||
: { kind: "outside-app-directory", package: identity };
|
||||
}
|
||||
if (directory === "." || directory === "/") break;
|
||||
}
|
||||
return {
|
||||
kind: "unknown",
|
||||
package: null,
|
||||
reason:
|
||||
"No owning package declaration establishes a Next App Router source root.",
|
||||
};
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user