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
- Install
javascript-obfuscator@5.4.7 and source-map@0.6.1.
- Save the minimal example below as
repro.cjs.
- 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.
Expected Behavior
When
sourceMapis enabled, the returned map should describe the exact string passed toobfuscate. Insources-contentmode,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
HashbangOperatorTransformercalls.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,
Erroris at2:10in the input, but the returned map reports1: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
masterbranch.Steps to Reproduce
javascript-obfuscator@5.4.7andsource-map@0.6.1.repro.cjs.node repro.cjs.JavaScript Obfuscator Edition
Your Environment
Stack trace
N/A
Minimal working example that will help to reproduce issue
Real-world impact
This surfaced in rolldown/rolldown#7555. Rolldown had a separate bug that inserted an unintended leading separator into the exact
renderChunkhook 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,
HashbangOperatorTransformershould 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
ICodeTransformerstring -> stringinterface cannot represent such a transformation map.A regression test should assert both that
sourcesContent[0] === inputand that the generatedErrortoken maps back to input2:10.