[GH-ISSUE #7606] [Bug] Exporting a captured plus-sign chunk breaks its relative import #4536

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

Originally created by @coygeek on GitHub (Oct 27, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7606

Before submitting

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

Area

Evidence, artifacts, or reconstruction

Observed behavior

Given a saved capture containing main.js and chunk+abc.js, node scripts/rea.mjs export-web-scripts "$capture" "$output" --json succeeds but places the chunk under an isolated filename. The exported main.js still imports ./chunk+abc.js. Running node scripts/rea.mjs analyze-javascript-application "$output/files" --json succeeds and reports that import as not-found.

Changing the captured chunk name to chunk-abc.js keeps both sources in their relative URL layout and resolves the import. I also created a local directory containing the original plus-sign filenames and the same source bytes: analysis resolved that import. The filesystem and analyzer on this host can represent this relationship; export changes its layout.

Reproduction

  1. Use the baseline checkout with its dependencies and compiled runtime already available, with Node.js v24.22.1 on PATH. Create isolated state:
export W=$(mktemp -d)
mkdir -p "$W/home" "$W/config" "$W/cache" "$W/data" "$W/state" "$W/tmp"
export HOME="$W/home" XDG_CONFIG_HOME="$W/config" XDG_CACHE_HOME="$W/cache"
export XDG_DATA_HOME="$W/data" XDG_STATE_HOME="$W/state" TMPDIR="$W/tmp"
  1. Run this driver from the checkout root. It constructs valid saved captures using the baseline's envelope helper, publishes each through the CLI, checks persisted source bytes, and analyzes the exported directory. It removes each fixture and export afterward.
node --import tsx --input-type=module <<'JS'
import { spawnSync } from 'node:child_process';
import { writeFileSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { scriptScenarioFixture } from './tests/fixtures/webScriptCapture.ts';
const run = args => {
  const r = spawnSync(process.execPath, ['scripts/rea.mjs', ...args], {
    encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, timeout: 60000,
  });
  if (r.status !== 0) throw new Error(r.stdout + r.stderr);
  return r.stdout;
};
for (const [name, filename] of [
  ['control', 'chunk-abc.js'],
  ['plus-1', 'chunk+abc.js'],
  ['plus-2', 'chunk+abc.js'],
]) {
  const capture = join(process.env.W, `${name}.json`);
  const output = join(process.env.W, `${name}-export`);
  const sources = [
    { url: 'https://verify.test/app/main.js',
      bytes: Buffer.from(`import './${filename}';\n`) },
    { url: `https://verify.test/app/${filename}`,
      bytes: Buffer.from('export const c = 1;\n') },
  ];
  try {
    writeFileSync(capture, JSON.stringify(scriptScenarioFixture(sources)));
    run(['export-web-scripts', capture, output, '--json']);
    const manifest = JSON.parse(readFileSync(join(output, 'manifest.json'), 'utf8'));
    const bytesMatch = manifest.scripts.every((s, i) =>
      readFileSync(join(output, 'files', s.content.relative_path)).equals(sources[i].bytes));
    const text = run(['analyze-javascript-application', join(output, 'files'), '--json']);
    const evidence = text.trim().split('\n').map(line => JSON.parse(line))
      .find(value => value.evidence_id);
    const imports = evidence.normalized_result.graph.edges
      .filter(e => e.relation === 'imports' && e.properties?.kind === 'static-import')
      .map(e => ({ specifier: e.properties.specifier, status: e.properties.resolution_status }));
    console.log(JSON.stringify({ name, bytesMatch,
      layouts: manifest.scripts.map(s => s.content.layout), imports,
      chunkReason: manifest.scripts[1].content.layout_reason }));
  } finally {
    rmSync(capture, { force: true });
    rmSync(output, { recursive: true, force: true });
  }
}
JS
  1. Compare the printed layouts and import results. The driver was executed twice. All four plus-sign exports reproduced the missing import; both hyphen controls resolved. All export and analysis commands exited 0. Remove the disposable state after inspection:
rm -rf "$W"

Expected behavior

Preserve the relative pathname of a short, representable chunk+abc.js so main.js can resolve its captured dependency. These captures contain no query or fragment, competing version, duplicate path, or prefix conflict.

The export guide promises per-origin relative layouts for ordinary unambiguous query-free JavaScript module paths and reserves isolation for ambiguous or unrepresentable inputs. It also describes ordinary captured relative-module resolution. I found no documented character allowlist excluding a literal plus sign. A separate local filesystem/analyzer control successfully used that exact filename and import. Exact contract and implementation locators appear below.

Impact and scope

P2: this bounded export-layout failure makes a retained dependency unavailable to the ordinary relative-import analysis of the exported artifact. Source bytes and URLs remain intact, and the missing relationship is explicitly reported as not-found. No capture corruption or source-byte loss was observed.

Verified only for these synthetic saved captures and a local literal-plus control on this macOS host. Other punctuation, other filesystems, live browser capture and MCP transport were not tested. Manually constructing the original relative layout from the same byte sequences allowed analysis to resolve the import; a repair that also maintains the export manifest was not verified.

REA version

Exact output of node scripts/rea.mjs --version:

6.8.0

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; Darwin arm64. Used the baseline's prebuilt CLI and installed tsx. No browser or native analysis provider ran. The destination filesystem type was not independently identified.

Target and artifact context

Schema-validated saved browser-scenario captures, each containing two retained JavaScript request/response transactions. The independently authored sources are one side-effect import and one constant export, as shown inline above.

Interface

CLI

Earliest failure stage

Artifact or evidence registration

Relevant output or evidence

The independently executed CLI runs produced these layouts and graph properties; this display omits unrelated manifest and graph fields:

control: bytes match; main=url-path; chunk=url-path
  ./chunk-abc.js -> resolved
plus-1: bytes match; main=url-path; chunk=isolated
  ./chunk+abc.js -> not-found
plus-2: bytes match; main=url-path; chunk=isolated
  ./chunk+abc.js -> not-found

Both plus exports give the chunk this exact reason:

URL path is not an ordinary portable JavaScript module path.

The additional local control wrote main.js containing import './chunk+abc.js'; and chunk+abc.js containing export const c = 1;, then ran analyze-javascript-application on their directory. It exited 0 and returned specifier: "./chunk+abc.js", resolution_status: "resolved". Native URL parsing retained /app/chunk+abc.js as its pathname.

At the baseline, docs/website-script-export.md:59–70 defines preserved relative layouts and isolation conditions; lines 81–86 describe analyzer resolution limits. src/browser/assets/PublishWebScripts.ts:28–32 repeats that contract in the manifest limitation. src/cli/webScriptCommands.ts:31–57 exposes the supported export workflow. src/domain/webScriptExportPlan.ts:11 defines the segment allowlist, and lines 73–107 reject the plus-sign segment before constructing its canonical path. The planner's current isolation tests in src/domain/webScriptExportPlan.test.ts:72–102 do not include a literal plus-sign case.

Analysis or suspected cause

Source-derived explanation: the planner accepts only letters, digits, underscore, tilde, dot and hyphen in URL path segments. A literal plus sign therefore receives an isolated layout even when its original filename is representable. The importer retains its exact bytes, so its relative specifier no longer names the exported dependency. No fix was implemented or tested.

Additional context

Open issue #7521 and open PR #7526 concern a different closure condition: isolating an oversized pathname so export succeeds. Both exports here already succeed, and isolating the plus-sign chunk is the source of the missing relationship.

Closed #1490 and #6242 concern destination-dependent case collisions and their documentation. This capture has no case-distinct pair. Other returned reports concerned analyzer URL decoding, manifest path admission, selected indices, and cancellation. Searches covered open and closed issues, comments, and open/merged/closed PRs; no matching plus-sign export report or fix was found. The introducing revision and last known good version are unknown.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7606 ### 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. ### Area Evidence, artifacts, or reconstruction ### Observed behavior Given a saved capture containing `main.js` and `chunk+abc.js`, `node scripts/rea.mjs export-web-scripts "$capture" "$output" --json` succeeds but places the chunk under an isolated filename. The exported `main.js` still imports `./chunk+abc.js`. Running `node scripts/rea.mjs analyze-javascript-application "$output/files" --json` succeeds and reports that import as `not-found`. Changing the captured chunk name to `chunk-abc.js` keeps both sources in their relative URL layout and resolves the import. I also created a local directory containing the original plus-sign filenames and the same source bytes: analysis resolved that import. The filesystem and analyzer on this host can represent this relationship; export changes its layout. ### Reproduction 1. Use the baseline checkout with its dependencies and compiled runtime already available, with Node.js v24.22.1 on PATH. Create isolated state: ```sh export W=$(mktemp -d) mkdir -p "$W/home" "$W/config" "$W/cache" "$W/data" "$W/state" "$W/tmp" export HOME="$W/home" XDG_CONFIG_HOME="$W/config" XDG_CACHE_HOME="$W/cache" export XDG_DATA_HOME="$W/data" XDG_STATE_HOME="$W/state" TMPDIR="$W/tmp" ``` 2. Run this driver from the checkout root. It constructs valid saved captures using the baseline's envelope helper, publishes each through the CLI, checks persisted source bytes, and analyzes the exported directory. It removes each fixture and export afterward. ```sh node --import tsx --input-type=module <<'JS' import { spawnSync } from 'node:child_process'; import { writeFileSync, readFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { scriptScenarioFixture } from './tests/fixtures/webScriptCapture.ts'; const run = args => { const r = spawnSync(process.execPath, ['scripts/rea.mjs', ...args], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, timeout: 60000, }); if (r.status !== 0) throw new Error(r.stdout + r.stderr); return r.stdout; }; for (const [name, filename] of [ ['control', 'chunk-abc.js'], ['plus-1', 'chunk+abc.js'], ['plus-2', 'chunk+abc.js'], ]) { const capture = join(process.env.W, `${name}.json`); const output = join(process.env.W, `${name}-export`); const sources = [ { url: 'https://verify.test/app/main.js', bytes: Buffer.from(`import './${filename}';\n`) }, { url: `https://verify.test/app/${filename}`, bytes: Buffer.from('export const c = 1;\n') }, ]; try { writeFileSync(capture, JSON.stringify(scriptScenarioFixture(sources))); run(['export-web-scripts', capture, output, '--json']); const manifest = JSON.parse(readFileSync(join(output, 'manifest.json'), 'utf8')); const bytesMatch = manifest.scripts.every((s, i) => readFileSync(join(output, 'files', s.content.relative_path)).equals(sources[i].bytes)); const text = run(['analyze-javascript-application', join(output, 'files'), '--json']); const evidence = text.trim().split('\n').map(line => JSON.parse(line)) .find(value => value.evidence_id); const imports = evidence.normalized_result.graph.edges .filter(e => e.relation === 'imports' && e.properties?.kind === 'static-import') .map(e => ({ specifier: e.properties.specifier, status: e.properties.resolution_status })); console.log(JSON.stringify({ name, bytesMatch, layouts: manifest.scripts.map(s => s.content.layout), imports, chunkReason: manifest.scripts[1].content.layout_reason })); } finally { rmSync(capture, { force: true }); rmSync(output, { recursive: true, force: true }); } } JS ``` 3. Compare the printed layouts and import results. The driver was executed twice. All four plus-sign exports reproduced the missing import; both hyphen controls resolved. All export and analysis commands exited 0. Remove the disposable state after inspection: ```sh rm -rf "$W" ``` ### Expected behavior Preserve the relative pathname of a short, representable `chunk+abc.js` so `main.js` can resolve its captured dependency. These captures contain no query or fragment, competing version, duplicate path, or prefix conflict. The export guide promises per-origin relative layouts for ordinary unambiguous query-free JavaScript module paths and reserves isolation for ambiguous or unrepresentable inputs. It also describes ordinary captured relative-module resolution. I found no documented character allowlist excluding a literal plus sign. A separate local filesystem/analyzer control successfully used that exact filename and import. Exact contract and implementation locators appear below. ### Impact and scope P2: this bounded export-layout failure makes a retained dependency unavailable to the ordinary relative-import analysis of the exported artifact. Source bytes and URLs remain intact, and the missing relationship is explicitly reported as `not-found`. No capture corruption or source-byte loss was observed. Verified only for these synthetic saved captures and a local literal-plus control on this macOS host. Other punctuation, other filesystems, live browser capture and MCP transport were not tested. Manually constructing the original relative layout from the same byte sequences allowed analysis to resolve the import; a repair that also maintains the export manifest was not verified. ### REA version Exact output of `node scripts/rea.mjs --version`: ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js `v24.22.1`; macOS `27.0.1`; Darwin arm64. Used the baseline's prebuilt CLI and installed `tsx`. No browser or native analysis provider ran. The destination filesystem type was not independently identified. ### Target and artifact context Schema-validated saved browser-scenario captures, each containing two retained JavaScript request/response transactions. The independently authored sources are one side-effect import and one constant export, as shown inline above. ### Interface CLI ### Earliest failure stage Artifact or evidence registration ### Relevant output or evidence The independently executed CLI runs produced these layouts and graph properties; this display omits unrelated manifest and graph fields: ```text control: bytes match; main=url-path; chunk=url-path ./chunk-abc.js -> resolved plus-1: bytes match; main=url-path; chunk=isolated ./chunk+abc.js -> not-found plus-2: bytes match; main=url-path; chunk=isolated ./chunk+abc.js -> not-found ``` Both plus exports give the chunk this exact reason: ```text URL path is not an ordinary portable JavaScript module path. ``` The additional local control wrote `main.js` containing `import './chunk+abc.js';` and `chunk+abc.js` containing `export const c = 1;`, then ran `analyze-javascript-application` on their directory. It exited 0 and returned `specifier: "./chunk+abc.js"`, `resolution_status: "resolved"`. Native URL parsing retained `/app/chunk+abc.js` as its pathname. At the baseline, `docs/website-script-export.md:59–70` defines preserved relative layouts and isolation conditions; lines 81–86 describe analyzer resolution limits. `src/browser/assets/PublishWebScripts.ts:28–32` repeats that contract in the manifest limitation. `src/cli/webScriptCommands.ts:31–57` exposes the supported export workflow. `src/domain/webScriptExportPlan.ts:11` defines the segment allowlist, and lines 73–107 reject the plus-sign segment before constructing its canonical path. The planner's current isolation tests in `src/domain/webScriptExportPlan.test.ts:72–102` do not include a literal plus-sign case. ### Analysis or suspected cause Source-derived explanation: the planner accepts only letters, digits, underscore, tilde, dot and hyphen in URL path segments. A literal plus sign therefore receives an isolated layout even when its original filename is representable. The importer retains its exact bytes, so its relative specifier no longer names the exported dependency. No fix was implemented or tested. ### Additional context Open issue [#7521](https://github.com/morluto/rea/issues/7521) and open PR [#7526](https://github.com/morluto/rea/pull/7526) concern a different closure condition: isolating an oversized pathname so export succeeds. Both exports here already succeed, and isolating the plus-sign chunk is the source of the missing relationship. Closed [#1490](https://github.com/morluto/rea/issues/1490) and [#6242](https://github.com/morluto/rea/issues/6242) concern destination-dependent case collisions and their documentation. This capture has no case-distinct pair. Other returned reports concerned analyzer URL decoding, manifest path admission, selected indices, and cancellation. Searches covered open and closed issues, comments, and open/merged/closed PRs; no matching plus-sign export report or fix was found. The introducing revision and last known good version are unknown.
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#4536
No description provided.