Repository navigation
Description
Expected Behavior
A class constructor written with a string-literal key is valid JavaScript and is a real constructor:
class A {
"constructor"(x) { this.x = x }
}The obfuscated output should keep it valid. The key should stay the plain literal 'constructor', as it does with reservedStrings: ['^constructor$']. No computed or concatenated form keeps it a constructor.
Current Behavior
With splitStrings or stringArray enabled, the constructor key is rewritten in place and the output fails to parse.
splitStrings turns it into a concatenation:
class A{'co'+'ns'+'tr'+'uc'+'to'+'r'(_0x17215d){this['x']=_0x17215d;}}
// SyntaxError: Unexpected token '+'stringArray turns it into a call:
class A{_0xd809b7(0x1dc)(_0x17215d){this['x']=_0x17215d;}}
// SyntaxError: Unexpected numberoptionsPreset: 'medium-obfuscation' fails the same way because it enables splitStrings.
Other quoted keys are handled correctly. class A { "foo"() {} } becomes ['fo'+'o'](){}, which is valid. constructor is rightly kept out of that conversion to a computed key, but the string transform is still applied to it.
This occurs in real dependencies. @standard-server/peer, which the oRPC message-port adapter uses, ships "constructor"(callback) { ... }. In our Vite/Rolldown build, the invalid output produced an empty bundle without any error.
Steps to Reproduce
npm install javascript-obfuscator- Save the minimal example below as
repro.cjs - Run
node repro.cjs
Output:
javascript-obfuscator 5.9.0, Node v24.21.0
input:
class A {
"constructor"(x) { this.x = x }
}
console.log(new A(1).x)
--- without workaround (expected to fail) ---
options: {"splitStrings":true,"splitStringsChunkLength":2}
output: class A{'co'+'ns'+'tr'+'uc'+'to'+'r'(_0x17215d){this['x']=_0x17215d;}}
result: SyntaxError: Unexpected token '+'
options: {"stringArray":true,"stringArrayThreshold":1}
output: class A{_0xd809b7(0x1dc)(_0x17215d){this['x']=_0x17215d;}}
result: SyntaxError: Unexpected number
options: {"optionsPreset":"medium-obfuscation","disableConsoleOutput":false}
output: class A{'constructo'+'r'(_0x4298d2){this['x']=_0x4298d2;}}
result: SyntaxError: Unexpected token '+'
--- with workaround reservedStrings: ['^constructor$'] ---
options: {"splitStrings":true,"splitStringsChunkLength":2,"reservedStrings":["^constructor$"]}
output: class A{'constructor'(_0x17215d){this['x']=_0x17215d;}}
result: OK (prints 1)
options: {"stringArray":true,"stringArrayThreshold":1,"reservedStrings":["^constructor$"]}
output: class A{'constructor'(_0x17215d){this['x']=_0x17215d;}}
result: OK (prints 1)
options: {"optionsPreset":"medium-obfuscation","disableConsoleOutput":false,"reservedStrings":["^constructor$"]}
output: class A{'constructor'(_0x4298d2){this['x']=_0x4298d2;}}
result: OK (prints 1)
bug reproduced: true
workaround works: true
JavaScript Obfuscator Edition
- JavaScript Obfuscator Open Source
Your Environment
- Obfuscator version used: 5.9.0 (also reproduced on 5.5.0)
- Node version used: 24.21.0 (macOS)
Stack trace
Running the splitStrings output directly with node out.js:
out.js:1
...}(_0x358f,0xbde4a));class A{'co'+'ns'+'tr'+'uc'+'to'+'r'(_0x17215d){this['x']=_0x17215d;}}console['lo'+'g'](new A(0x1)['x']);...
^
SyntaxError: Unexpected token '+'
at wrapSafe (node:internal/modules/cjs/loader:1861:18)
at Module._compile (node:internal/modules/cjs/loader:1903:20)
at Object..js (node:internal/modules/cjs/loader:2060:10)
Minimal working example that will help to reproduce issue
// repro.cjs
// Quoted class constructor `"constructor"() {}` produces invalid code
// with `splitStrings` or `stringArray`.
const vm = require('node:vm')
const JavaScriptObfuscator = require('javascript-obfuscator')
const { version } = require('javascript-obfuscator/package.json')
const source = `class A {
"constructor"(x) { this.x = x }
}
console.log(new A(1).x)`
const cases = [
{ splitStrings: true, splitStringsChunkLength: 2 },
{ stringArray: true, stringArrayThreshold: 1 },
{ optionsPreset: 'medium-obfuscation', disableConsoleOutput: false },
]
// Workaround: exclude the string from transformation
const workaround = { reservedStrings: ['^constructor$'] }
function run(options) {
const code = JavaScriptObfuscator.obfuscate(source, {
...options,
seed: 1,
}).getObfuscatedCode()
const logs = []
let result
try {
vm.runInNewContext(code, {
console: { log: (...args) => logs.push(args.join(' ')) },
})
result =
logs.join('\n') === '1'
? 'OK (prints 1)'
: `WRONG OUTPUT: ${JSON.stringify(logs)}`
} catch (error) {
result = `${error.name}: ${error.message}`
}
const classPart = code.match(/class A\{.*?\}\}/)?.[0] ?? '(class not found)'
console.log(`options: ${JSON.stringify(options)}`)
console.log(`output: ${classPart}`)
console.log(`result: ${result}\n`)
return result.startsWith('OK')
}
console.log(`javascript-obfuscator ${version}, Node ${process.version}\n`)
console.log(`input:\n${source}\n`)
console.log('--- without workaround (expected to fail) ---\n')
const failing = cases.map((options) => run(options))
console.log("--- with workaround reservedStrings: ['^constructor$'] ---\n")
const fixed = cases.map((options) => run({ ...options, ...workaround }))
const bugReproduced = failing.every((ok) => !ok)
const workaroundWorks = fixed.every((ok) => ok)
console.log(`bug reproduced: ${bugReproduced}`)
console.log(`workaround works: ${workaroundWorks}`)
process.exitCode = bugReproduced && workaroundWorks ? 0 : 1
Activity
sanex3339 commentedon Nov 2, 2026
Released as 5.9.1