Skip to content

Leading whitespace is removed without updating source maps #1437

Description

@hyfdev

Expected Behavior

When sourceMap is enabled, the returned map should describe the exact string passed to obfuscate. In sources-content mode, sourcesContent[0] should equal that input string.

Formatting may omit leading blank lines from the generated output, but the source map should still map generated tokens to their positions in the unmodified input.

Current Behavior

HashbangOperatorTransformer calls .trim() on every input, even when the input has no hashbang. Parsing and source-map generation therefore use the trimmed string rather than the caller-provided string.

With the reproduction below, Error is at 2:10 in the input, but the returned map reports 1:10. sourcesContent[0] also omits the leading newline.

{
  "generated": { "line": 1, "column": 10 },
  "expectedOriginal": { "line": 2, "column": 10 },
  "actualOriginal": {
    "source": "sourceMap",
    "line": 1,
    "column": 10,
    "name": "Error"
  },
  "sourcesContentMatches": false
}

This behavior is present in JavaScript Obfuscator 5.1.0, 5.4.7, and the current master branch.

Steps to Reproduce

  1. Install javascript-obfuscator@5.4.7 and source-map@0.6.1.
  2. Save the minimal example below as repro.cjs.
  3. Run node repro.cjs.

JavaScript Obfuscator Edition

  • JavaScript Obfuscator Open Source

Your Environment

  • Obfuscator version used: 5.4.7
  • Node version used: v24.18.0

Stack trace

N/A

Minimal working example that will help to reproduce issue

const JavaScriptObfuscator = require('javascript-obfuscator');
const { SourceMapConsumer } = require('source-map');

const input = '\nthrow new Error("boom");\n';

const result = JavaScriptObfuscator.obfuscate(input, {
  compact: false,
  sourceMap: true,
  sourceMapMode: 'separate',
  sourceMapSourcesMode: 'sources-content',
  stringArray: false,
  seed: 1,
});

const output = result.getObfuscatedCode();
const map = JSON.parse(result.getSourceMap());
const consumer = new SourceMapConsumer(map);

function positionOf(code, token) {
  const before = code.slice(0, code.indexOf(token));
  const lines = before.split('\n');

  return {
    line: lines.length,
    column: lines.at(-1).length,
  };
}

const generated = positionOf(output, 'Error');
const expectedOriginal = positionOf(input, 'Error');

console.log({
  generated,
  expectedOriginal,
  actualOriginal: consumer.originalPositionFor(generated),
  sourcesContentMatches: map.sourcesContent[0] === input,
});

Real-world impact

This surfaced in rolldown/rolldown#7555. Rolldown had a separate bug that inserted an unintended leading separator into the exact renderChunk hook input. Rolldown's producer map accounted for that separator, but JavaScript Obfuscator removed it without representing the removal in its returned map, so downstream composition looked up the wrong coordinate.

rolldown/rolldown#10289 removed that separator and fixed the reported Rolldown reproduction. It does not fix the general JavaScript Obfuscator behavior for callers whose valid input contains leading whitespace.

Suggested direction

For inputs without a hashbang, HashbangOperatorTransformer should return the input unchanged.

More generally, any code transformer that mutates source text while source maps are enabled should expose a transformation map and have it composed with the map produced by the code generator. A map-aware string editor such as MagicString could represent those edits, but its map would still need to be composed with the escodegen map; replacing direct string operations with MagicString without map composition would not be sufficient. The current ICodeTransformer string -> string interface cannot represent such a transformation map.

A regression test should assert both that sourcesContent[0] === input and that the generated Error token maps back to input 2:10.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions