[GH-ISSUE #2821] [Bug] Short sourcesContent arrays discard valid source-map originals #1007

Closed
opened 2026-10-15 18:30:18 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @shoyann on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2821

Before submitting

  • Searched open and closed issues for a related report.
  • Removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction; CLI.

Observed behavior

A version-3 source map with two declared sources and only one embedded content entry loses both originals during analyze-javascript-application. The graph is marked partial with Source map sourcesContent must contain one string or null entry per source. Padding the same array with one null restores both source declarations and the embedded content.

Reproduction

From a source checkout at 9680da76c84a6833635dc7a5df607806d899f804, using the pinned Node/npm versions:

npm ci
npm run build:cached
node --input-type=module <<'JS'
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
const root = await mkdtemp(join(tmpdir(), 'rea-partial-content-'));
try {
  for (const [name, sourcesContent] of [
    ['short', ['export const answer = 42;']],
    ['padded', ['export const answer = 42;', null]],
  ]) {
    const directory = join(root, name);
    await mkdir(directory);
    await writeFile(join(directory, 'bundle.js.map'), JSON.stringify({
      version: 3, names: [], sources: ['a.ts', 'b.ts'],
      sourcesContent, mappings: 'AAAA;ACAA',
    }));
    const result = JSON.parse(execFileSync(process.execPath, [
      'scripts/rea.mjs', 'analyze-javascript-application', directory,
      '--format', 'json',
    ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })).normalized_result;
    console.log({name, coverage: result.graph.coverage.status,
      originals: result.graph.nodes.filter(node =>
        node.identity.strategy === 'source-map-original').length});
  }
} finally {
  await rm(root, {recursive: true, force: true});
}
JS

Observed output:

{ name: 'short', coverage: 'partial', originals: 0 }
{ name: 'padded', coverage: 'complete', originals: 2 }

Expected behavior

Both inputs retain two original source declarations. a.ts has embedded content; b.ts has no embedded content. Missing trailing content slots should not invalidate the map or shift source indices.

ECMA-426 DecodeSourceMapSources, step 6.5, reads a content entry only when that index is within sourcesContent. The decoded source starts with null content. The algorithm iterates the source inventory, so extra content entries also do not introduce extra sources.

Impact and scope

Local JavaScript application reconstruction loses useful source identities and retained text from otherwise valid maps. Source inspection also shows the browser collection profile uses the same cardinality check. This concerns read-only source-map interpretation; no provider engine or target execution is needed.

Environment

  • REA source revision: 9680da76c84a6833635dc7a5df607806d899f804
  • Runtime/platform: Node 24.18.0, npm 11.16.0, Ubuntu x86_64 under WSL
  • Target: self-authored version-3 JSON source map
  • Interface: CLI (analyze-javascript-application)
  • Earliest failure stage: local source-map parsing and original-source collection

src/domain/sourceMapContents.ts requires contents.length === sourceCount. Both local original collection and browser source-map validation use it, although their collection loops already read content by source ordinal and handle absent entries as unavailable.

This is separate from #2023/#2034, which addressed a null field and retained the earlier exact-cardinality assumption. This report corrects that assumption according to the decoding algorithm; scalar content fields and non-string/non-null entries should still be rejected.

Originally created by @shoyann on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2821 ## Before submitting - [x] Searched open and closed issues for a related report. - [x] Removed credentials, tokens, private paths, and sensitive target data. ## Area Evidence, artifacts, or reconstruction; CLI. ## Observed behavior A version-3 source map with two declared sources and only one embedded content entry loses both originals during `analyze-javascript-application`. The graph is marked partial with `Source map sourcesContent must contain one string or null entry per source.` Padding the same array with one `null` restores both source declarations and the embedded content. ## Reproduction From a source checkout at `9680da76c84a6833635dc7a5df607806d899f804`, using the pinned Node/npm versions: ```sh npm ci npm run build:cached node --input-type=module <<'JS' import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { execFileSync } from 'node:child_process'; const root = await mkdtemp(join(tmpdir(), 'rea-partial-content-')); try { for (const [name, sourcesContent] of [ ['short', ['export const answer = 42;']], ['padded', ['export const answer = 42;', null]], ]) { const directory = join(root, name); await mkdir(directory); await writeFile(join(directory, 'bundle.js.map'), JSON.stringify({ version: 3, names: [], sources: ['a.ts', 'b.ts'], sourcesContent, mappings: 'AAAA;ACAA', })); const result = JSON.parse(execFileSync(process.execPath, [ 'scripts/rea.mjs', 'analyze-javascript-application', directory, '--format', 'json', ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })).normalized_result; console.log({name, coverage: result.graph.coverage.status, originals: result.graph.nodes.filter(node => node.identity.strategy === 'source-map-original').length}); } } finally { await rm(root, {recursive: true, force: true}); } JS ``` Observed output: ```text { name: 'short', coverage: 'partial', originals: 0 } { name: 'padded', coverage: 'complete', originals: 2 } ``` ## Expected behavior Both inputs retain two original source declarations. `a.ts` has embedded content; `b.ts` has no embedded content. Missing trailing content slots should not invalidate the map or shift source indices. [ECMA-426 DecodeSourceMapSources](https://tc39.es/ecma426/#sec-decodesourcemapsources), step 6.5, reads a content entry only when that index is within `sourcesContent`. The decoded source starts with null content. The algorithm iterates the source inventory, so extra content entries also do not introduce extra sources. ## Impact and scope Local JavaScript application reconstruction loses useful source identities and retained text from otherwise valid maps. Source inspection also shows the browser collection profile uses the same cardinality check. This concerns read-only source-map interpretation; no provider engine or target execution is needed. ## Environment - REA source revision: `9680da76c84a6833635dc7a5df607806d899f804` - Runtime/platform: Node 24.18.0, npm 11.16.0, Ubuntu x86_64 under WSL - Target: self-authored version-3 JSON source map - Interface: CLI (`analyze-javascript-application`) - Earliest failure stage: local source-map parsing and original-source collection ## Source-derived cause and related work `src/domain/sourceMapContents.ts` requires `contents.length === sourceCount`. Both local original collection and browser source-map validation use it, although their collection loops already read content by source ordinal and handle absent entries as unavailable. This is separate from #2023/#2034, which addressed a null field and retained the earlier exact-cardinality assumption. This report corrects that assumption according to the decoding algorithm; scalar content fields and non-string/non-null entries should still be rejected.
bsakaguchi 2026-10-15 18:30:18 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@Jackzigen commented on GitHub (Oct 13, 2026):

Taking this one — fix is up in #2869: the shared validator now accepts sourcesContent shorter than sources (missing trailing slots decode as null content per ECMA-426), still rejecting extra entries and non-string/non-null values. Both consumers already read content by ordinal and needed no changes.

<!-- gh-comment-id:6148623469 --> @Jackzigen commented on GitHub (Oct 13, 2026): Taking this one — fix is up in #2869: the shared validator now accepts `sourcesContent` shorter than `sources` (missing trailing slots decode as null content per ECMA-426), still rejecting extra entries and non-string/non-null values. Both consumers already read content by ordinal and needed no changes.
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#1007
No description provided.