Object.defineProperty accessor traps on process.env throw ERR_INVALID_OBJECT_DEFINE_PROPERTY; reassigning process.env to a Proxy with a set trap + Error().stack pinpoints mid-build NODE_ENV mutations.
When a build behaves differently across phases (e.g. Vite/SvelteKit evaluating the same config file multiple times with different NODE_ENV values), you need to find WHO mutates process.env.NODE_ENV and WHEN.
The obvious approach fails: Object.defineProperty(process.env, 'NODE_ENV', { get, set }) throws ERR_INVALID_OBJECT_DEFINE_PROPERTY — "'process.env' does not accept an accessor(getter/setter) descriptor". process.env is an exotic object backed by real environment variables and rejects accessor descriptors (verified on Node 24).
What works: replace process.env wholesale with a Proxy. Node allows reassigning process.env:
// Drop at the top of the config file that gets re-evaluated (guard against double-install):
const g = globalThis;
if (!g.__env_trap) {
g.__env_trap = true;
process.env = new Proxy(process.env, {
set(t, k, v) {
if (k === 'NODE_ENV' && t[k] !== v) {
console.error(`NODE_ENV ${t[k]} -> ${v}\n`, new Error().stack);
}
t[k] = v;
return true;
}
});
}The logged stack names the exact frame (in my case vite/dist/node/chunks/... resolveConfig — Vite's VITE_USER_NODE_ENV branch reacting to NODE_ENV=development in a .env file). Pair it with a tiny configResolved probe plugin that logs config.build.ssr plus the active plugin names per build to correlate the mutation with which nested build lost its plugins.
Caveats: the Proxy loses process.env's string-coercion semantics for values set after installation (fine for a one-off diagnostic); remove the trap after diagnosis; install it in the module that is evaluated earliest and guard with a global flag because bundler config files can be re-evaluated several times per process.