TypeError: redeclaration of var X when two required files share a top-level const/let binding name
Symptom
Given a multi-file project like:
// lib/log.js
exports.append = function (line) { return line; };
// lib/a.js
const log = require("./log.js");
exports.tag = "a";
// lib/b.js
const log = require("./log.js");
exports.tag = "b";
// main.js
const a = require("./lib/a.js");
const b = require("./lib/b.js");
Running main.js crashes the instant the second file loads:
org.mozilla.javascript.EcmaError: TypeError: redeclaration of var log. (file:///android_asset/modules/jvm-npm.js#67)
at org.mozilla.javascript.ScriptRuntime.constructError(...)
at org.mozilla.javascript.ScriptableObject.redefineProperty(...)
at org.mozilla.javascript.ScriptRuntime.initScript(...)
at org.mozilla.javascript.Interpreter$CallFrame.initializeArgs(...)
Reproducible on-device (confirmed on a Pixel 7a — full incident: djbclark/stayturgid#34), independent of const/let/var, and independent of which file is the entry script. Because any real multi-file project routinely shares common dependency names (log, config, notify, etc.) across files, this affects every file that reuses a common local binding name, not just one.
What I believe is the root cause
app/src/main/assets/modules/jvm-npm.js in this repo has been substantially rewritten from upstream nodyn/jvm-npm (confirmed identical between SuperMonster003/AutoJs6 and this fork's copy — this isn't fork-specific). The rewrite changed how Module._load actually loads a required file's code:
Upstream nodyn/jvm-npm (Module._load):
var body = readFile(module.filename, module.core);
var func = new Function('exports', 'module', 'require', '__filename', '__dirname', body);
func.apply(module, [module.exports, module, module.require, module.filename, dir]);
Each required file's source becomes the body of a freshly-constructed Function, invoked with exports/module/require/etc. as its formal parameters. This gives genuine, JS-spec-guaranteed, function-local variable scoping — two different modules' top-level const log = ... can never collide, because each is scoped to a separate function call, full stop. (See also upstream issue nodyn/jvm-npm#37, which documents this exact mechanism and a portability caveat with older Rhino — relevant context, though not the same bug.)
This repo's rewritten jvm-npm.js (_.Module._load):
_load(file) {
return NativeRequire.require(file);
},
The Function-wrapper mechanism has been removed entirely. Loading now delegates to NativeRequire.require(file) — i.e., whatever require implementation the host had installed before jvm-npm.js overwrote the global require binding (in this codebase, Rhino's own official org.mozilla.javascript.commonjs.module.Require, installed via RhinoJavaScriptEngine.initRequireBuilder() → RequireBuilder().setSandboxed(true).createRequire(...)). That class uses a fundamentally different isolation mechanism — Scriptable scope objects / prototype chains — not per-call function-local scoping.
I could not fully pin down (via several hours of controlled testing against this exact bundled org-mozilla-rhino-2_0_0-SNAPSHOT.jar, both via the bare Rhino shell and by mimicking initRequireBuilder's setup) the precise internal condition under which that scope-object-based isolation fails for const/let specifically — several plausible configurations I constructed did NOT reproduce the crash standalone, suggesting the failure also depends on some detail of this codebase's exact Kotlin-level wiring (AssetAndUrlModuleSourceProvider's dual asset+filesystem resolution, and/or the specific order of initRequireBuilder()/initPrologue()/initEpilogue()) that I wasn't able to fully replicate outside the real app. Full writeup of what I tried: https://github.com/djbclark/autojs6-typescript/blob/main/examples/broken/01-redeclaration/README.md
Regardless of that remaining internal detail, the actionable finding is clear: removing jvm-npm's original Function-wrapper isolation removed a structural guarantee (collision-proof scoping) and replaced it with a mechanism that doesn't appear to provide the same guarantee for ES6 const/let — syntax that didn't exist when either jvm-npm or (likely) this Rhino fork's commonjs module support were originally designed, and that any TypeScript project compiles to by default.
Suggested fix
Either restore genuine per-module Function-wrapper isolation in Module._load (matching upstream jvm-npm's original design), or otherwise verify/ensure the Scriptable-based delegation path actually provides equivalent guaranteed isolation for const/let-declared top-level bindings across sibling required files.
Related, not duplicate
Reference
TypeError: redeclaration of var Xwhen two required files share a top-levelconst/letbinding nameSymptom
Given a multi-file project like:
Running
main.jscrashes the instant the second file loads:Reproducible on-device (confirmed on a Pixel 7a — full incident: djbclark/stayturgid#34), independent of
const/let/var, and independent of which file is the entry script. Because any real multi-file project routinely shares common dependency names (log,config,notify, etc.) across files, this affects every file that reuses a common local binding name, not just one.What I believe is the root cause
app/src/main/assets/modules/jvm-npm.jsin this repo has been substantially rewritten from upstreamnodyn/jvm-npm(confirmed identical betweenSuperMonster003/AutoJs6and this fork's copy — this isn't fork-specific). The rewrite changed howModule._loadactually loads a required file's code:Upstream
nodyn/jvm-npm(Module._load):Each required file's source becomes the body of a freshly-constructed
Function, invoked withexports/module/require/etc. as its formal parameters. This gives genuine, JS-spec-guaranteed, function-local variable scoping — two different modules' top-levelconst log = ...can never collide, because each is scoped to a separate function call, full stop. (See also upstream issue nodyn/jvm-npm#37, which documents this exact mechanism and a portability caveat with older Rhino — relevant context, though not the same bug.)This repo's rewritten
jvm-npm.js(_.Module._load):The
Function-wrapper mechanism has been removed entirely. Loading now delegates toNativeRequire.require(file)— i.e., whateverrequireimplementation the host had installed before jvm-npm.js overwrote the globalrequirebinding (in this codebase, Rhino's own officialorg.mozilla.javascript.commonjs.module.Require, installed viaRhinoJavaScriptEngine.initRequireBuilder()→RequireBuilder().setSandboxed(true).createRequire(...)). That class uses a fundamentally different isolation mechanism — Scriptable scope objects / prototype chains — not per-call function-local scoping.I could not fully pin down (via several hours of controlled testing against this exact bundled
org-mozilla-rhino-2_0_0-SNAPSHOT.jar, both via the bare Rhino shell and by mimickinginitRequireBuilder's setup) the precise internal condition under which that scope-object-based isolation fails forconst/letspecifically — several plausible configurations I constructed did NOT reproduce the crash standalone, suggesting the failure also depends on some detail of this codebase's exact Kotlin-level wiring (AssetAndUrlModuleSourceProvider's dual asset+filesystem resolution, and/or the specific order ofinitRequireBuilder()/initPrologue()/initEpilogue()) that I wasn't able to fully replicate outside the real app. Full writeup of what I tried: https://github.com/djbclark/autojs6-typescript/blob/main/examples/broken/01-redeclaration/README.mdRegardless of that remaining internal detail, the actionable finding is clear: removing jvm-npm's original Function-wrapper isolation removed a structural guarantee (collision-proof scoping) and replaced it with a mechanism that doesn't appear to provide the same guarantee for ES6
const/let— syntax that didn't exist when either jvm-npm or (likely) this Rhino fork's commonjs module support were originally designed, and that any TypeScript project compiles to by default.Suggested fix
Either restore genuine per-module Function-wrapper isolation in
Module._load(matching upstream jvm-npm's original design), or otherwise verify/ensure the Scriptable-based delegation path actually provides equivalent guaranteed isolation forconst/let-declared top-level bindings across sibling required files.Related, not duplicate
Reference
djbclark/autojs6-typescript,examples/broken/01-redeclaration/