diff --git a/src/node/internal/process.ts b/src/node/internal/process.ts index 2cab92469ea..cdb7d28a8e4 100644 --- a/src/node/internal/process.ts +++ b/src/node/internal/process.ts @@ -26,63 +26,60 @@ export function nextTick(cb: Function, ...args: unknown[]) { // for the worker are accessible from the env argument passed into the fetch // handler and have no impact here. -export const env = new Proxy( - {}, - { - // Per Node.js rules. process.env values must be coerced to strings. - // When defined using defineProperty, the property descriptor must be writable, - // configurable, and enumerable using just a falsy check. Getters and setters - // are not permitted. - set(obj: object, prop: PropertyKey, value: any) { - return Reflect.set(obj, prop, `${value}`); - }, - defineProperty( - obj: object, - prop: PropertyKey, - descriptor: PropertyDescriptor - ) { - validateObject(descriptor, 'descriptor', {}); - if (Reflect.has(descriptor, 'get') || Reflect.has(descriptor, 'set')) { - throw new ERR_INVALID_ARG_VALUE( - 'descriptor', - descriptor, - 'process.env value must not have getter/setter' - ); - } - if (!descriptor.configurable) { - throw new ERR_INVALID_ARG_VALUE( - 'descriptor.configurable', - descriptor, - 'process.env value must be configurable' - ); - } - if (!descriptor.enumerable) { - throw new ERR_INVALID_ARG_VALUE( - 'descriptor.enumerable', - descriptor, - 'process.env value must be enumerable' - ); - } - if (!descriptor.writable) { - throw new ERR_INVALID_ARG_VALUE( - 'descriptor.writable', - descriptor, - 'process.env value must be writable' - ); - } - if (Reflect.has(descriptor, 'value')) { - Reflect.set(descriptor, 'value', `${descriptor.value}`); - } else { - throw new ERR_INVALID_ARG_VALUE( - 'descriptor.value', - descriptor, - 'process.env value must be specified explicitly' - ); - } - return Reflect.defineProperty(obj, prop, descriptor); - }, - } -); +export const env = new Proxy(utilImpl.getEnvObject(), { + // Per Node.js rules. process.env values must be coerced to strings. + // When defined using defineProperty, the property descriptor must be writable, + // configurable, and enumerable using just a falsy check. Getters and setters + // are not permitted. + set(obj: object, prop: PropertyKey, value: any) { + return Reflect.set(obj, prop, `${value}`); + }, + defineProperty( + obj: object, + prop: PropertyKey, + descriptor: PropertyDescriptor + ) { + validateObject(descriptor, 'descriptor', {}); + if (Reflect.has(descriptor, 'get') || Reflect.has(descriptor, 'set')) { + throw new ERR_INVALID_ARG_VALUE( + 'descriptor', + descriptor, + 'process.env value must not have getter/setter' + ); + } + if (!descriptor.configurable) { + throw new ERR_INVALID_ARG_VALUE( + 'descriptor.configurable', + descriptor, + 'process.env value must be configurable' + ); + } + if (!descriptor.enumerable) { + throw new ERR_INVALID_ARG_VALUE( + 'descriptor.enumerable', + descriptor, + 'process.env value must be enumerable' + ); + } + if (!descriptor.writable) { + throw new ERR_INVALID_ARG_VALUE( + 'descriptor.writable', + descriptor, + 'process.env value must be writable' + ); + } + if (Reflect.has(descriptor, 'value')) { + Reflect.set(descriptor, 'value', `${descriptor.value}`); + } else { + throw new ERR_INVALID_ARG_VALUE( + 'descriptor.value', + descriptor, + 'process.env value must be specified explicitly' + ); + } + return Reflect.defineProperty(obj, prop, descriptor); + }, +}); export function getBuiltinModule(id: string): any { return utilImpl.getBuiltinModule(id); diff --git a/src/node/internal/util.d.ts b/src/node/internal/util.d.ts index 7b6e357c470..556e39fb3ea 100644 --- a/src/node/internal/util.d.ts +++ b/src/node/internal/util.d.ts @@ -120,6 +120,7 @@ export function isBoxedPrimitive( value: unknown ): value is number | string | boolean | bigint | symbol; +export function getEnvObject(): object; export function getBuiltinModule(id: string): any; export function getCallSite(frames: number): Record[]; export function processExitImpl(code: number): void; diff --git a/src/workerd/api/node/tests/process-nodejs-test.js b/src/workerd/api/node/tests/process-nodejs-test.js index eea2dd40a42..21f89c11b99 100644 --- a/src/workerd/api/node/tests/process-nodejs-test.js +++ b/src/workerd/api/node/tests/process-nodejs-test.js @@ -6,3 +6,17 @@ export const processPlatform = { assert.ok(['darwin', 'win32', 'linux'].includes(process.platform)); }, }; + +process.env.BAZ = 1; +const env = { ...process.env }; + +export const processEnv = { + async test() { + assert.strictEqual(env.FOO, 'BAR'); + assert.strictEqual(env.BAR, '{}'); + assert.strictEqual(env.BAZ, '1'); + + const { FOO } = await import('mod'); + assert.strictEqual(FOO, 'BAR'); + }, +}; diff --git a/src/workerd/api/node/tests/process-nodejs-test.wd-test b/src/workerd/api/node/tests/process-nodejs-test.wd-test index 77af308a27f..51efc414624 100644 --- a/src/workerd/api/node/tests/process-nodejs-test.wd-test +++ b/src/workerd/api/node/tests/process-nodejs-test.wd-test @@ -5,10 +5,18 @@ const unitTests :Workerd.Config = ( ( name = "nodejs-process-test", worker = ( modules = [ - (name = "worker", esModule = embed "process-nodejs-test.js") + (name = "worker", esModule = embed "process-nodejs-test.js"), + (name = "mod", esModule = "export const { FOO } = process.env;") + ], + compatibilityDate = "2024-12-28", + compatibilityFlags = [ + "nodejs_compat", + "nodejs_compat_populate_process_env" + ], + bindings = [ + (name = "FOO", text = "BAR"), + (name = "BAR", json = "{}"), ], - compatibilityDate = "2024-10-11", - compatibilityFlags = ["nodejs_compat"], ) ), ], diff --git a/src/workerd/api/node/util.c++ b/src/workerd/api/node/util.c++ index 0b526cb2bc4..b62422266fa 100644 --- a/src/workerd/api/node/util.c++ +++ b/src/workerd/api/node/util.c++ @@ -234,6 +234,10 @@ jsg::JsValue UtilModule::getBuiltinModule(jsg::Lock& js, kj::String specifier) { return js.undefined(); } +jsg::JsObject UtilModule::getEnvObject(jsg::Lock& js) { + return js.getEnv(true); +} + namespace { [[noreturn]] void handleProcessExit(jsg::Lock& js, int code) { // There are a few things happening here. First, we abort the current IoContext diff --git a/src/workerd/api/node/util.h b/src/workerd/api/node/util.h index b5ed95d849d..35b1a026150 100644 --- a/src/workerd/api/node/util.h +++ b/src/workerd/api/node/util.h @@ -239,6 +239,8 @@ class UtilModule final: public jsg::Object { return processPlatform; } + jsg::JsObject getEnvObject(jsg::Lock& js); + JSG_RESOURCE_TYPE(UtilModule) { JSG_NESTED_TYPE(MIMEType); JSG_NESTED_TYPE(MIMEParams); @@ -259,6 +261,8 @@ class UtilModule final: public jsg::Object { JSG_METHOD(getConstructorName); JSG_METHOD(getCallSite); + JSG_METHOD(getEnvObject); + #define V(Type) JSG_METHOD(is##Type); JS_UTIL_IS_TYPES(V) #undef V diff --git a/src/workerd/io/compatibility-date.capnp b/src/workerd/io/compatibility-date.capnp index 9c7aac02604..5ee4710efc9 100644 --- a/src/workerd/io/compatibility-date.capnp +++ b/src/workerd/io/compatibility-date.capnp @@ -679,4 +679,10 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef { $compatDisableFlag("cache_no_cache_disabled") $experimental; # Enables the use of cache: no-cache in the fetch api. + + populateProcessEnv @71 :Bool + $compatEnableFlag("nodejs_compat_populate_process_env") + $compatDisableFlag("nodejs_compat_dot_not_populate_process_env"); + # Automatically populate process.env from text and json bindings + # when nodejs_compat is being used. } diff --git a/src/workerd/jsg/jsg.h b/src/workerd/jsg/jsg.h index fdd5c402368..27a3c0794e1 100644 --- a/src/workerd/jsg/jsg.h +++ b/src/workerd/jsg/jsg.h @@ -2675,6 +2675,13 @@ class Lock { // the inspector (if attached), or to KJ_LOG(Info). virtual void reportError(const JsValue& value) = 0; + // Sets an env value that will be expressed on the process.env + // if/when nodejs-compat mode is used. + virtual void setEnvField(const JsValue& name, const JsValue& value) = 0; + + // Returns the env base object. + virtual JsObject getEnv(bool release = false) = 0; + private: // Mark the jsg::Lock as being disallowed from being passed as a parameter into // a kj promise coroutine. Note that this only blocks directly passing the Lock diff --git a/src/workerd/jsg/setup.h b/src/workerd/jsg/setup.h index e0f535a436e..267f17e3a1e 100644 --- a/src/workerd/jsg/setup.h +++ b/src/workerd/jsg/setup.h @@ -261,6 +261,9 @@ class IsolateBase { // object with 2 internal fields. v8::Global opaqueTemplate; + // Object that is used as the underlying target of process.env when nodejs-compat mode is used. + v8::Global envObj; + // Polyfilled Symbol.asyncDispose. v8::Global symbolAsyncDispose; @@ -665,6 +668,24 @@ class Isolate: public IsolateBase { } } + // Sets an env value that will be expressed on the process.env + // if/when nodejs-compat mode is used. + void setEnvField(const JsValue& name, const JsValue& value) override { + getEnv().set(*this, name, value); + } + + // Returns the env base object.s + JsObject getEnv(bool release = false) override { + KJ_DEFER({ + if (release) jsgIsolate.envObj.Reset(); + }); + if (jsgIsolate.envObj.IsEmpty()) { + v8::Local env = obj(); + jsgIsolate.envObj.Reset(v8Isolate, env); + } + return JsObject(jsgIsolate.envObj.Get(v8Isolate)); + } + private: Isolate& jsgIsolate; diff --git a/src/workerd/server/workerd-api.c++ b/src/workerd/server/workerd-api.c++ index dc930e91105..890d8d68ba4 100644 --- a/src/workerd/server/workerd-api.c++ +++ b/src/workerd/server/workerd-api.c++ @@ -626,6 +626,9 @@ static v8::Local createBindingValue(JsgWorkerdIsolate::Lock& lock, KJ_CASE_ONEOF(json, Global::Json) { v8::Local string = lock.wrap(context, kj::mv(json.text)); value = jsg::check(v8::JSON::Parse(context, string)); + if (featureFlags.getPopulateProcessEnv() && featureFlags.getNodeJsCompat()) { + lock.setEnvField(lock.str(global.name), jsg::JsValue(string)); + } } KJ_CASE_ONEOF(pipeline, Global::Fetcher) { @@ -711,6 +714,9 @@ static v8::Local createBindingValue(JsgWorkerdIsolate::Lock& lock, KJ_CASE_ONEOF(text, kj::String) { value = lock.wrap(context, kj::mv(text)); + if (featureFlags.getPopulateProcessEnv() && featureFlags.getNodeJsCompat()) { + lock.setEnvField(lock.str(global.name), jsg::JsValue(value)); + } } KJ_CASE_ONEOF(data, kj::Array) {