Files
reverse-engineering-anything/tests/boundary/filesystem/javascript-applications/viteOutputDependencies.test.ts
T
JG Tromp 7aa943c14b fix(javascript): resolve Vite build-output preload dependencies (#7745)
* fix(javascript): resolve Vite build-output preload dependencies

With an absolute base, Vite writes __vite__mapDeps entries relative to the
build output directory (for example assets/chunk.js), and its preload helper
prefixes the configured base at runtime. REA emitted these entries as
dynamic-import module specifiers, so a scheme-less path without a leading
dot resolved as a bare package and every entry was reported as an
unresolved "external" module, even when the build output directory was
selected.

The static analyzer now marks such entries with a vite-output: specifier,
as Webpack chunk loads are marked with chunk:. Entries starting with ./,
../ or /, and scheme-qualified entries, keep their previous handling. The
graph resolves a marked entry as a URL path under each ancestor directory
of the importing file; exactly one ancestor holding the file establishes
the build output directory. Otherwise the entry is not-found (or
unavailable when several ancestors match) with a limitation that names
the build output directory.

On a synthetic 7-file Vite build, the 12 preload entries that were
"external" now resolve when the build output directory is selected, and
are not-found with that guidance when only assets/ is selected.

* fix(javascript): keep unanalyzed Vite output candidates

resolveViteOutputDependency considered only resolved ancestors. A file
that exists under an ancestor but was not analyzed (for example
dist/assets/lazy.css for the entry assets/lazy.css) resolves there as
unavailable, and that result was replaced by the root's not-found with a
limitation saying that no ancestor holds the file.

Keep resolved and unavailable ancestor candidates together. One candidate
is returned with its own path and limitations; several are reported as
unavailable with every candidate path and limitation. Only when no
ancestor can hold the file does the root's not-found result stand.
2026-10-28 19:46:45 +08:00

100 lines
3.7 KiB
TypeScript

import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import {
reconstructJavaScriptFixture,
writeFixtureFiles,
} from "../../../support/javascriptApplicationFixture.js";
// Vite's absolute-base build output: the helper table names files under the
// output directory, and the real import is relative to the importing chunk.
const viteBuild = async (): Promise<string> => {
const root = await createTestTempDirectory("rea-vite-output-");
await writeFixtureFiles(root, {
"index.html":
'<!doctype html><script type="module" src="/assets/index.js"></script>',
"assets/index.js": `
const __vite__mapDeps = (i, m = __vite__mapDeps, d =
(m.f || (m.f = ["assets/lazy.js", "assets/missing.js"]))) => i.map((i) => d[i]);
const preload = (load, deps) => load();
export const route = () =>
preload(() => import("./lazy.js"), __vite__mapDeps([0, 1]));
`,
"assets/lazy.js": "export const lazy = true;",
});
return root;
};
const preloadEdges = (
graph: Awaited<ReturnType<typeof reconstructJavaScriptFixture>>["graph"],
) =>
graph.edges
.filter(
({ relation, properties }) =>
relation === "imports" &&
typeof properties.specifier === "string" &&
properties.specifier.startsWith("vite-output:"),
)
.map(({ properties }) => ({
specifier: properties.specifier,
resolution_status: properties.resolution_status,
resolved_path: properties.resolved_path,
}));
describe("Vite build-output preload dependencies", () => {
it("resolves a dependency under the selected build output directory", async () => {
const result = await reconstructJavaScriptFixture(await viteBuild());
expect(preloadEdges(result.graph)).toContainEqual({
specifier: "vite-output:assets/lazy.js",
resolution_status: "resolved",
resolved_path: "assets/lazy.js",
});
expect(JSON.stringify(result.graph)).toContain(
"no ancestor of the importing file holds it",
);
});
it("does not report a dependency as an external package when only the assets directory is selected", async () => {
const root = await viteBuild();
const result = await reconstructJavaScriptFixture(join(root, "assets"));
const serialized = JSON.stringify(result.graph);
expect(serialized).not.toContain('"resolution_status":"external"');
expect(serialized).toContain(
"A Vite preload dependency is relative to the build output directory",
);
});
it("keeps an inventoried but unanalyzed dependency under a nested build output directory", async () => {
const root = await createTestTempDirectory("rea-vite-output-css-");
await writeFixtureFiles(root, {
"dist/index.html":
'<!doctype html><script type="module" src="/assets/index.js"></script>',
"dist/assets/index.js": `
const __vite__mapDeps = (i, m = __vite__mapDeps, d =
(m.f || (m.f = ["assets/lazy.css"]))) => i.map((i) => d[i]);
export const route = () => __vite__mapDeps([0]);
`,
"dist/assets/lazy.css": ".lazy { color: green; }",
});
const result = await reconstructJavaScriptFixture(root);
const edge = result.graph.edges.find(
({ properties }) =>
properties.specifier === "vite-output:assets/lazy.css",
);
expect(edge?.properties).toMatchObject({
resolution_status: "unavailable",
candidate_path: "dist/assets/lazy.css",
});
const serialized = JSON.stringify(edge);
expect(serialized).toContain("dist/assets/lazy.css");
expect(serialized).not.toContain(
"no ancestor of the importing file holds it",
);
});
});