[GH-ISSUE #7737] [Bug] Vite preload dependencies such as assets/x.js are reported as unresolved "external" packages #4628

Closed
opened 2026-10-31 00:51:34 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @jgt87 on GitHub (Oct 28, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7737

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Reproduced on rea-agents 6.8.0 (released) and on main 6a226bd09.

Same fixture (7 files), imports edges 6.8.0 (npm, Node 26.12.1) main 6a226bd09 (source, Node 24.22.1)
Build output directory selected 12 external, 18 + 6 resolved, 12 unresolved-reference nodes 12 external, 18 + 6 resolved, 12 unresolved-reference nodes
Only assets/ selected 12 external, 18 + 6 resolved, 12 unresolved-reference nodes 12 external, 18 + 6 resolved, 12 unresolved-reference nodes

Area

JavaScript application analysis, module relationship resolution (Vite).

Observed behavior

With an absolute base (the default /), Vite writes the __vite__mapDeps
table relative to the build output directory:

const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/chunk01.js","assets/chunk07.js"])))=>i.map(i=>d[i]);
export const pre0=()=>api.preload(()=>import("./chunk07.js"),__vite__mapDeps([0,1]));

REA recovers the table entries (indexVitePreloadDependencyTable) and emits
each one as a dynamic-import reference with the literal specifier. The
resolver then reads assets/chunk01.js as a bare package name and reports:

{"kind":"dynamic-import","specifier":"assets/chunk05-6ekos5.js","resolution":"unresolved","resolution_status":"external"}

This happens whether the selected root is the build output directory (the
folder with index.html and assets/) or the assets/ folder itself. On a
synthetic 7-file build: 12 preload entries, all external, and 12
unresolved-reference nodes, in both cases. The real import("./x.js")
calls resolve.

Analysis

Vite's preload helper prefixes the configured base to these entries at run
time, so they are paths under the build output directory, not module
specifiers and not relative to the importing chunk. With a relative base
(./), Vite writes importer-relative ./x.js entries instead, which REA
already resolves (tests/boundary/filesystem/javascript-applications/javascriptBundlerAdapters.test.ts
covers that form only).

vitePreloadDependencySpecifiers in src/domain/javascript/javascriptStaticAnalysisCalls.ts
returns the table value unchanged; resolveReference in
src/application/javascript/JavaScriptArtifactGraphFindings.ts resolves it as a
module-specifier, and bareModuleCandidate finds no node_modules package.

Reproduction

Synthetic fixture only (generator gen-vite-like.mjs):

node gen-vite-like.mjs ./fx/small 6 40
rea analyze-javascript-application ./fx/small --artifact-format directory --format json > root.json
rea analyze-javascript-application ./fx/small/assets --artifact-format directory --format json > assets.json

In both outputs, the imports edges are:
dynamic-import/resolved: 18, dynamic-import/external: 12, static-import/resolved: 6.

Expected behavior

  • With the build output directory selected, assets/x.js resolves to that file.
  • With only assets/ selected, the entry cannot be placed, but it is not an
    external package: it should be not-found with a limitation that names the
    build output directory.

Impact and scope

Every Vite build with the default base. In the original (non-synthetic) case,
1,248 import relationships, 832 resolved; the unresolved ones were these
preload entries. The real dynamic imports carry the same targets, so the
module graph is not missing edges, but the unresolved entries look like
missing packages and add noise to unknowns.

REA version

rea-agents 6.8.0 (npm, Node 26.12.1) and upstream main at 6a226bd09 (source, Node 24.22.1). First observed on 6.4.0 (MCP).

Runtime and platform

Windows 11 Pro (10.0.26300). 6.8.0: global npm install on Node.js 26.12.1. main: source checkout on Node.js 24.22.1.

  • PR #1840 (open): preserves Vite dependency values and indexes in the same producer. It does not change how base-relative entries resolve, but it edits the same code, so a fix may need a rebase on it.
  • #3907 / PR #4023 (closed/merged): Vite dependency slots after a spread.

Typed outcome or recovery expected

A fix is proposed in a PR (fix/vite-preload-output-paths): mark base-relative
entries with a vite-output: specifier (as Webpack chunk loads use chunk:)
and resolve them under the one ancestor directory of the importer that holds
the file.

gen-vite-like.mjs (the synthetic fixture generator)
// Generates a synthetic Vite-like build output. No real application code.
// Usage: node gen-vite-like.mjs <outDir> [chunkCount] [bigChunkKB] [first|last]
// Writes <outDir>/index.html and <outDir>/assets/*.js. Each chunk has an
// absolute-base __vite__mapDeps table, real dynamic imports, localStorage
// get/set calls and api.rpc("proc_N_M") calls. Deterministic output.
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
import { join } from "node:path";

const outDir = process.argv[2];
const chunkCount = Number(process.argv[3] ?? 87);
const bigKB = Number(process.argv[4] ?? 456);
if (!outDir) throw new Error("outDir required");
rmSync(outDir, { recursive: true, force: true });
const assets = join(outDir, "assets");
mkdirSync(assets, { recursive: true });

let seed = 7;
const rand = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648);
const names = Array.from({ length: chunkCount }, (_, i) => `chunk${String(i).padStart(2, "0")}-${(i * 2654435761 >>> 0).toString(36).slice(0, 6)}.js`);
const entry = "index-a1b2c3.js";

function body(i, targetBytes) {
  const parts = [];
  let n = 0;
  let size = 0;
  while (size < targetBytes) {
    const f = `f${i}_${n}`;
    const key = `demo.pref.${i}.${n % 5}`;
    const fn = `export function ${f}(a,b){const c=a*${n}+b;` +
      `if(c>${Math.floor(rand() * 1000)}){localStorage.setItem("${key}",String(c));}` +
      `const d=localStorage.getItem("${key}");` +
      `return api.rpc("proc_${i}_${n % 7}",{v:c,d}).then(r=>r.data);}\n`;
    parts.push(fn);
    size += fn.length;
    n++;
  }
  return parts.join("");
}

const total = [];
const deps = names.map((n) => `assets/${n}`);
for (let i = 0; i < chunkCount; i++) {
  const bigIndex = process.argv[5] === "last" ? chunkCount - 1 : 0;
  const target = i === bigIndex ? bigKB * 1024 : 20 * 1024 + Math.floor(rand() * 12 * 1024);
  const next = (i + 1) % chunkCount;
  const other = (i + 7) % chunkCount;
  const head =
    `import{api}from"./${entry}";\n` +
    `const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=${JSON.stringify([deps[next], deps[other]])})))=>i.map(i=>d[i]);\n` +
    `export const load${i}=()=>import("./${names[next]}");\n` +
    `export const pre${i}=()=>api.preload(()=>import("./${names[other]}"),__vite__mapDeps([0,1]));\n`;
  const text = head + body(i, target);
  writeFileSync(join(assets, names[i]), text);
  total.push(text.length);
}
const entryText =
  `const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=${JSON.stringify(deps)})))=>i.map(i=>d[i]);\n` +
  `export const api={rpc(name,args){return fetch("/rpc/"+name,{method:"POST",body:JSON.stringify(args)}).then(r=>r.json());},preload(f,d){return f();}};\n` +
  names.map((n, i) => `export const route${i}=()=>api.preload(()=>import("./${n}"),__vite__mapDeps([${i}]));\n`).join("") +
  `const saved=localStorage.getItem("demo.session.token");if(saved){route0();}\n`;
writeFileSync(join(assets, entry), entryText);
writeFileSync(join(outDir, "index.html"), `<!doctype html><script type="module" src="/assets/${entry}"></script>`);
const sum = total.reduce((a, b) => a + b, 0) + entryText.length;
console.log(JSON.stringify({ files: chunkCount + 1, bytes: sum, biggest: Math.max(...total) }));
Originally created by @jgt87 on GitHub (Oct 28, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7737 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. Reproduced on rea-agents 6.8.0 (released) and on main 6a226bd09. | Same fixture (7 files), `imports` edges | 6.8.0 (npm, Node 26.12.1) | main 6a226bd09 (source, Node 24.22.1) | | --- | --- | --- | | Build output directory selected | 12 `external`, 18 + 6 resolved, 12 unresolved-reference nodes | 12 `external`, 18 + 6 resolved, 12 unresolved-reference nodes | | Only `assets/` selected | 12 `external`, 18 + 6 resolved, 12 unresolved-reference nodes | 12 `external`, 18 + 6 resolved, 12 unresolved-reference nodes | ## Area JavaScript application analysis, module relationship resolution (Vite). ## Observed behavior With an absolute `base` (the default `/`), Vite writes the `__vite__mapDeps` table relative to the build output directory: ```js const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/chunk01.js","assets/chunk07.js"])))=>i.map(i=>d[i]); export const pre0=()=>api.preload(()=>import("./chunk07.js"),__vite__mapDeps([0,1])); ``` REA recovers the table entries (`indexVitePreloadDependencyTable`) and emits each one as a `dynamic-import` reference with the literal specifier. The resolver then reads `assets/chunk01.js` as a bare package name and reports: ```json {"kind":"dynamic-import","specifier":"assets/chunk05-6ekos5.js","resolution":"unresolved","resolution_status":"external"} ``` This happens whether the selected root is the build output directory (the folder with `index.html` and `assets/`) or the `assets/` folder itself. On a synthetic 7-file build: 12 preload entries, all `external`, and 12 `unresolved-reference` nodes, in both cases. The real `import("./x.js")` calls resolve. ## Analysis Vite's preload helper prefixes the configured base to these entries at run time, so they are paths under the build output directory, not module specifiers and not relative to the importing chunk. With a relative base (`./`), Vite writes importer-relative `./x.js` entries instead, which REA already resolves (`tests/boundary/filesystem/javascript-applications/javascriptBundlerAdapters.test.ts` covers that form only). `vitePreloadDependencySpecifiers` in `src/domain/javascript/javascriptStaticAnalysisCalls.ts` returns the table value unchanged; `resolveReference` in `src/application/javascript/JavaScriptArtifactGraphFindings.ts` resolves it as a `module-specifier`, and `bareModuleCandidate` finds no `node_modules` package. ## Reproduction Synthetic fixture only (generator `gen-vite-like.mjs`): ```sh node gen-vite-like.mjs ./fx/small 6 40 rea analyze-javascript-application ./fx/small --artifact-format directory --format json > root.json rea analyze-javascript-application ./fx/small/assets --artifact-format directory --format json > assets.json ``` In both outputs, the `imports` edges are: `dynamic-import/resolved: 18, dynamic-import/external: 12, static-import/resolved: 6`. ## Expected behavior - With the build output directory selected, `assets/x.js` resolves to that file. - With only `assets/` selected, the entry cannot be placed, but it is not an external package: it should be `not-found` with a limitation that names the build output directory. ## Impact and scope Every Vite build with the default base. In the original (non-synthetic) case, 1,248 import relationships, 832 resolved; the unresolved ones were these preload entries. The real dynamic imports carry the same targets, so the module graph is not missing edges, but the unresolved entries look like missing packages and add noise to unknowns. ## REA version rea-agents 6.8.0 (npm, Node 26.12.1) and upstream `main` at 6a226bd09 (source, Node 24.22.1). First observed on 6.4.0 (MCP). ## Runtime and platform Windows 11 Pro (10.0.26300). 6.8.0: global npm install on Node.js 26.12.1. main: source checkout on Node.js 24.22.1. ## Related - PR #1840 (open): preserves Vite dependency values and indexes in the same producer. It does not change how base-relative entries resolve, but it edits the same code, so a fix may need a rebase on it. - #3907 / PR #4023 (closed/merged): Vite dependency slots after a spread. ## Typed outcome or recovery expected A fix is proposed in a PR (fix/vite-preload-output-paths): mark base-relative entries with a `vite-output:` specifier (as Webpack chunk loads use `chunk:`) and resolve them under the one ancestor directory of the importer that holds the file. <details><summary><code>gen-vite-like.mjs</code> (the synthetic fixture generator)</summary> ```js // Generates a synthetic Vite-like build output. No real application code. // Usage: node gen-vite-like.mjs <outDir> [chunkCount] [bigChunkKB] [first|last] // Writes <outDir>/index.html and <outDir>/assets/*.js. Each chunk has an // absolute-base __vite__mapDeps table, real dynamic imports, localStorage // get/set calls and api.rpc("proc_N_M") calls. Deterministic output. import { mkdirSync, writeFileSync, rmSync } from "node:fs"; import { join } from "node:path"; const outDir = process.argv[2]; const chunkCount = Number(process.argv[3] ?? 87); const bigKB = Number(process.argv[4] ?? 456); if (!outDir) throw new Error("outDir required"); rmSync(outDir, { recursive: true, force: true }); const assets = join(outDir, "assets"); mkdirSync(assets, { recursive: true }); let seed = 7; const rand = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648); const names = Array.from({ length: chunkCount }, (_, i) => `chunk${String(i).padStart(2, "0")}-${(i * 2654435761 >>> 0).toString(36).slice(0, 6)}.js`); const entry = "index-a1b2c3.js"; function body(i, targetBytes) { const parts = []; let n = 0; let size = 0; while (size < targetBytes) { const f = `f${i}_${n}`; const key = `demo.pref.${i}.${n % 5}`; const fn = `export function ${f}(a,b){const c=a*${n}+b;` + `if(c>${Math.floor(rand() * 1000)}){localStorage.setItem("${key}",String(c));}` + `const d=localStorage.getItem("${key}");` + `return api.rpc("proc_${i}_${n % 7}",{v:c,d}).then(r=>r.data);}\n`; parts.push(fn); size += fn.length; n++; } return parts.join(""); } const total = []; const deps = names.map((n) => `assets/${n}`); for (let i = 0; i < chunkCount; i++) { const bigIndex = process.argv[5] === "last" ? chunkCount - 1 : 0; const target = i === bigIndex ? bigKB * 1024 : 20 * 1024 + Math.floor(rand() * 12 * 1024); const next = (i + 1) % chunkCount; const other = (i + 7) % chunkCount; const head = `import{api}from"./${entry}";\n` + `const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=${JSON.stringify([deps[next], deps[other]])})))=>i.map(i=>d[i]);\n` + `export const load${i}=()=>import("./${names[next]}");\n` + `export const pre${i}=()=>api.preload(()=>import("./${names[other]}"),__vite__mapDeps([0,1]));\n`; const text = head + body(i, target); writeFileSync(join(assets, names[i]), text); total.push(text.length); } const entryText = `const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=${JSON.stringify(deps)})))=>i.map(i=>d[i]);\n` + `export const api={rpc(name,args){return fetch("/rpc/"+name,{method:"POST",body:JSON.stringify(args)}).then(r=>r.json());},preload(f,d){return f();}};\n` + names.map((n, i) => `export const route${i}=()=>api.preload(()=>import("./${n}"),__vite__mapDeps([${i}]));\n`).join("") + `const saved=localStorage.getItem("demo.session.token");if(saved){route0();}\n`; writeFileSync(join(assets, entry), entryText); writeFileSync(join(outDir, "index.html"), `<!doctype html><script type="module" src="/assets/${entry}"></script>`); const sum = total.reduce((a, b) => a + b, 0) + entryText.length; console.log(JSON.stringify({ files: chunkCount + 1, bytes: sum, biggest: Math.max(...total) })); ``` </details>
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4628
No description provided.