Feature Request: Granular Assignment Protection for Individual --define Mappings (e.g. --define-readonly)
This feature request proposes an enhancement to esbuild's --define flag: granular assignment protection for individual define mappings.
Problem
Currently, using --define:VAR1=window.location will replace all occurrences of VAR1 with window.location. However, assignments to VAR1 (e.g. VAR1 = 3;) are also replaced, resulting in window.location = 3;. This can unintentionally overwrite or mutate important globals, which is often undesirable.
Example:
// source code
VAR1 = 3;
console.log(VAR1);
Build command:
esbuild ./index.ts --define:VAR1=window.location
Current output:
window.location = 3;
console.log(window.location);
Feature Proposal
Introduce a way to mark individual --define mappings as assignment-protected, so assignments to the variable being replaced do not modify the replacement target.
Expected output with assignment protection:
If assignment protection (e.g., with --define-readonly:VAR1=window.location) were implemented, the output would be:
VAR1 = 3;
console.log(window.location);
This ensures assignments to VAR1 do not affect window.location, guarding sensitive globals from reassignment.
Possible Implementation
- Dedicated CLI Option (Cross-Tool Compatible):
- Add a new CLI option:
--define-readonly:VAR1=window.location - In config files, support an object/array structure:
{ "define": { "VAR1": { "value": "window.location", "readonly": true } } } - This makes assignment protection explicit and compatible with other build tools.
- Add a new CLI option:
Alternatives Considered
- Using a global flag (
--no-assign-override), which is insufficiently granular. - Manually avoiding assignment to sensitive defines, which is error-prone and hard to enforce.
What's the actual use case you have for this in your source code? This seems like something that should be handled by a linter or type checker instead of by esbuild. For example:
// source code
declare const VAR1: number;
VAR1 = 3;
console.log(VAR1);
This will prevent you from accidentally writing code that assigns to VAR1.
@evanw
I think the issue is not VAR1 being declared as a const
VAR1 might still be mutable.
let say I define my VAR1 with a default value
config.js
VAR1 = "default in code"
then when I build, I may want to optionally override that specific variable
now because VAR1 is being substituted with window.location for assignment as well as reads.
we end up with the unintentional outcome of window.location being mutated
- VAR1 = "default in code"
+ window.location = "default in code"
Why not declaring your VAR1 as a local variable instead of a global one?
// config.js
var VAR1_ = VAR1 // [--define:VAR1=window.location] => var VAR1_ = window.location
export {VAR1_ as VAR1}
// if you want to mutate that later
export const setVAR1 = v => { VAR1_ = v }
// some-other-file.js
import {VAR1} from './config.js'
console.log(VAR1)
@hyrious thanks for the suggestion, but that adds a layers of indirection and I am not sure it directly solves the case, with the default value in code
as shown in the message above if we try
VAR1 = "default in code" // missing from your example
var VAR1_ = VAR1
...
this have few issues
- if we
--defineVAR1as a string it will show warnings for the substation in line 1 beside the warning in that case, values like string number etc. work.
"str" = "default in code" // this will not transform to this, but we will get a warning
var VAR1_ = "str" // this is fine
...
however
- if we
--defineVAR1 as an expression we are back to the original problem
my.expression = "default in code"
var VAR1_ = my.expression // this is find but the code
...