Repository: denoland/deno
Pull request: fix(ext/node): fix 'test-worker-process-env.js' tests

Done with @claude 

Changed files:
filename: ext/node/polyfills/worker_threads.ts
patch: @@ -49,16 +49,16 @@ const {
   encodeURIComponent,
   Error,
   FunctionPrototypeCall,
-  JSONParse,
-  JSONStringify,
   NumberIsFinite,
   ObjectHasOwn,
+  ObjectKeys,
   ObjectPrototypeIsPrototypeOf,
   PromiseReject,
   PromiseResolve,
   SafeMap,
   SafeSet,
   SafeWeakMap,
+  String,
   StringPrototypeStartsWith,
   StringPrototypeTrim,
   Symbol,
@@ -189,18 +189,55 @@ class NodeWorker extends EventEmitter {
     }
     this.#name = name;
 
-    // One of the most common usages will be to pass `process.env` here,
-    // but because `process.env` is a Proxy in Deno, we need to get a plain
-    // object out of it - otherwise we'll run in `DataCloneError`s.
+    // Handle the `env` option following Node.js semantics:
+    // - undefined/null: snapshot current process.env (isolated copy)
+    // - SHARE_ENV: worker shares the parent's OS environment
+    // - object: use that object, coercing values to strings
+    // - anything else: throw ERR_INVALID_ARG_TYPE
     // See https://github.com/denoland/deno/issues/23522.
     let env_ = undefined;
-    if (options?.env) {
-      env_ = JSONParse(JSONStringify(options?.env));
+    const envOpt = options?.env;
+    if (envOpt != null && envOpt !== SHARE_ENV) {
+      if (typeof envOpt !== "object") {
+        throw new ERR_INVALID_ARG_TYPE(
+          "options.env",
+          ["object", "undefined", "null", "worker_threads.SHARE_ENV"],
+          envOpt,
+        );
+      }
+      // Snapshot the provided env, coercing values to strings like Node.js.
+      // This also handles passing `process.env` (a Proxy in Deno) by
+      // producing a plain object that can be structured-cloned.
+      const envObj = {};
+      const keys = ObjectKeys(envOpt);
+      for (let i = 0; i < keys.length; i++) {
+        envObj[keys[i]] = String(envOpt[keys[i]]);
+      }
+      env_ = envObj;
+    } else if (envOpt !== SHARE_ENV) {
+      // Default: snapshot current process.env so the worker gets an
+      // isolated copy, not a live reference to the OS environment.
+      // Wrap in try/catch because accessing process.env requires
+      // --allow-env permission in Deno. If unavailable, fall back to
+      // shared OS env (env_ stays undefined).
+      try {
+        const envObj = {};
+        const keys = ObjectKeys(process.env);
+        for (let i = 0; i < keys.length; i++) {
+          envObj[keys[i]] = process.env[keys[i]];
+        }
+        env_ = envObj;
+      } catch {
+        // No env permission - worker will share the OS environment.
+      }
     }
+    // When envOpt === SHARE_ENV, env_ stays undefined and the worker
+    // will use the default process.env backed by Deno.env (shared OS env).
     const serializedWorkerMetadata = serializeJsMessageData({
       workerData: options?.workerData,
       environmentData: environmentData,
       env: env_,
+      isEval: !!options?.eval,
       isWorkerThread: true,
     }, options?.transferList ?? []);
     const id = op_create_worker(
@@ -529,6 +566,28 @@ internals.__initWorkerThreads = (
       if (env) {
         process.env = env;
       }
+
+      // Set process.argv for worker threads.
+      // In Node.js, worker process.argv is [execPath, scriptPath, ...argv].
+      if (isWorkerThread) {
+        let scriptPath;
+        if (metadata.isEval) {
+          scriptPath = "[worker eval]";
+        } else if (
+          moduleSpecifier &&
+          StringPrototypeStartsWith(moduleSpecifier, "file:")
+        ) {
+          scriptPath = new URL(moduleSpecifier).pathname;
+        } else {
+          scriptPath = moduleSpecifier ?? "";
+        }
+        process.argv = [process.execPath, scriptPath];
+        if (metadata.argv) {
+          for (let i = 0; i < metadata.argv.length; i++) {
+            process.argv[i + 2] = metadata.argv[i];
+          }
+        }
+      }
     }
     defaultExport.workerData = workerData;
     defaultExport.parentPort = parentPort;
@@ -564,12 +623,13 @@ internals.__initWorkerThreads = (
     parentPort.once = function (this: ParentPort, name, listener) {
       // deno-lint-ignore no-explicit-any
       const _listener = (ev: any) => {
+        listeners.delete(listener);
         const message = ev.data;
         patchMessagePortIfFound(message);
         return listener(message);
       };
       listeners.set(listener, _listener);
-      this.addEventListener(name, _listener);
+      this.addEventListener(name, _listener, { once: true });
       return this;
     };
 

filename: tests/node_compat/config.jsonc
patch: @@ -1562,6 +1562,10 @@
     // "parallel/test-worker-name.js": {},
     "parallel/test-worker-on-process-exit.js": {},
     "parallel/test-worker-onmessage.js": {},
+    // "parallel/test-worker-process-argv.js": {},
+    "parallel/test-worker-process-env.js": {},
+    // TODO(bartlomieju): this test just hangs
+    // "parallel/test-worker-process-env-shared.js": {},
     "parallel/test-worker-ref-onexit.js": {},
     "parallel/test-worker-relative-path-double-dot.js": {},
     "parallel/test-worker-relative-path.js": {},