Skip to content

Commit

Permalink
Provide mechanism for autopopulating node.js process.env
Browse files Browse the repository at this point in the history
Autopopulates the process.env from bindings in local dev. A similar
PR will be needed internally to enable it there as it won't be
automatic.
  • Loading branch information
jasnell committed Jan 9, 2025
1 parent d189253 commit d08499e
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 60 deletions.
111 changes: 54 additions & 57 deletions src/node/internal/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/node/internal/util.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>[];
export function processExitImpl(code: number): void;
Expand Down
14 changes: 14 additions & 0 deletions src/workerd/api/node/tests/process-nodejs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
};
14 changes: 11 additions & 3 deletions src/workerd/api/node/tests/process-nodejs-test.wd-test
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
),
],
Expand Down
4 changes: 4 additions & 0 deletions src/workerd/api/node/util.c++
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/workerd/api/node/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/workerd/io/compatibility-date.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
7 changes: 7 additions & 0 deletions src/workerd/jsg/jsg.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/workerd/jsg/setup.h
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ class IsolateBase {
// object with 2 internal fields.
v8::Global<v8::FunctionTemplate> opaqueTemplate;

// Object that is used as the underlying target of process.env when nodejs-compat mode is used.
v8::Global<v8::Object> envObj;

// Polyfilled Symbol.asyncDispose.
v8::Global<v8::Symbol> symbolAsyncDispose;

Expand Down Expand Up @@ -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<v8::Object> env = obj();
jsgIsolate.envObj.Reset(v8Isolate, env);
}
return JsObject(jsgIsolate.envObj.Get(v8Isolate));
}

private:
Isolate& jsgIsolate;

Expand Down
6 changes: 6 additions & 0 deletions src/workerd/server/workerd-api.c++
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,9 @@ static v8::Local<v8::Value> createBindingValue(JsgWorkerdIsolate::Lock& lock,
KJ_CASE_ONEOF(json, Global::Json) {
v8::Local<v8::String> 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) {
Expand Down Expand Up @@ -711,6 +714,9 @@ static v8::Local<v8::Value> 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<byte>) {
Expand Down

0 comments on commit d08499e

Please sign in to comment.