Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/content/limitations/native-worker-threads.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
`node:worker_threads` runs workers as native threads.
Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`.
Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap.
Nested workers inherit their parent's environment snapshot.
Supported constructor options are `workerData`, `argv`, a `transferList` of MessagePorts, `name` (reported by `threadName` and `worker.threadName`), `stdout`, `stderr` and `stdin`, and `resourceLimits.stackSizeMb`, which sets the worker thread's stack size (at least 192 KiB; without it a worker's stack is 8 MiB, where Node's is 4 MiB).
Each worker initializes its own module state, event loop, and reference-counted heap.
A worker's `process.env` is a copy of its parent's, taken by the constructor.
With the `env` option it holds an object's own enumerable properties converted with `String()`, and with `env: SHARE_ENV` the worker and its parent use one environment, as in Node; `SHARE_ENV` is unsupported in a worker that has its own copy.
Child processes that a worker spawns inherit the worker's `process.env`.
Supported constructor options are `workerData`, `argv`, `env`, a `transferList` of MessagePorts, `name` (reported by `threadName` and `worker.threadName`), `stdout`, `stderr` and `stdin`, and `resourceLimits.stackSizeMb`, which sets the worker thread's stack size (at least 192 KiB; without it a worker's stack is 8 MiB, where Node's is 4 MiB).
The `resourceLimits` heap limits (`maxOldGenerationSizeMb`, `maxYoungGenerationSizeMb`, and `codeRangeSizeMb`), `worker.resourceLimits` while the worker runs, and `resourceLimits` read inside a worker are refused; in the main thread `resourceLimits` is `{}`, and after a worker exits `worker.resourceLimits` is `{}`, as in Node.
With the `stdout` or `stderr` option, the worker's writes to that stream reach `worker.stdout` or `worker.stderr`, a Readable that ends after the worker's `'exit'` event; its chunks arrive in order with the worker's other events.
Without the option a worker writes to the process's own output, and reading `worker.stdout` or `worker.stderr` is refused.
With the `stdin` option, `worker.stdin` is a Writable whose chunks the worker reads from `process.stdin`, in order with the worker's other events, and `process.stdin` ends when `worker.stdin` ends; without it `worker.stdin` is `null` and the worker's `process.stdin` ends without data, as in Node.
Runtime paths, `eval`, loaders, and shared environments are unsupported.
Runtime paths, `eval`, and loaders are unsupported.
`setEnvironmentData` and `getEnvironmentData` keep a map per thread with SameValueZero keys; each new Worker starts from a clone of its parent's entries taken by the constructor, which throws `DataCloneError` when an entry cannot be cloned.
Worker argument strings containing null bytes are also unsupported.

Expand Down
10 changes: 5 additions & 5 deletions docs/src/generated/node-v24-compatibility.json
Original file line number Diff line number Diff line change
Expand Up @@ -884,9 +884,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
"partial": 46,
"partial": 47,
"refused": 0,
"not-implemented": 20,
"not-implemented": 19,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
Expand Down Expand Up @@ -84162,9 +84162,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
Expand Down
23 changes: 14 additions & 9 deletions internal/compatibility/generated/node-v24-backlog.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"tierItemCount": 3454,
"tiers": {
"static": {
"audit-partial": 1197,
"verify-gap": 2130,
"audit-partial": 1198,
"verify-gap": 2129,
"replace-refusal": 40,
"classify": 2,
"implement": 85
Expand Down Expand Up @@ -84619,7 +84619,9 @@
"tests/corpus/native-worker-messages.ts",
"tests/corpus/native-worker-commonjs.cjs",
"tests/corpus/native-worker-environment.ts",
"tests/corpus/native-worker-roots/main.ts"
"tests/corpus/native-worker-roots/main.ts",
"tests/corpus/5998-worker-env-option.ts",
"tests/corpus/5999-worker-env-nested.cjs"
]
}
}
Expand Down Expand Up @@ -85607,12 +85609,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:worker_threads",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:worker_threads.SHARE_ENV",
"tests": [
"tests/corpus/5998-worker-env-option.ts",
"tests/corpus/5999-worker-env-nested.cjs"
]
}
}
},
Expand Down
18 changes: 13 additions & 5 deletions internal/compatibility/generated/node-v24-internal.json
Original file line number Diff line number Diff line change
Expand Up @@ -941,9 +941,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
"partial": 46,
"partial": 47,
"refused": 0,
"not-implemented": 20,
"not-implemented": 19,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
Expand Down Expand Up @@ -113569,6 +113569,8 @@
"tests/corpus/5879-worker-stdin.ts",
"tests/corpus/5990-worker-uncloneable.mjs",
"tests/corpus/5991-worker-async-dispose.mjs",
"tests/corpus/5998-worker-env-option.ts",
"tests/corpus/5999-worker-env-nested.cjs",
"tests/corpus/native-worker-clone-errors.ts",
"tests/corpus/native-worker-commonjs.cjs",
"tests/corpus/native-worker-delayed-messages.ts",
Expand Down Expand Up @@ -114212,7 +114214,9 @@
"tests/corpus/native-worker-messages.ts",
"tests/corpus/native-worker-commonjs.cjs",
"tests/corpus/native-worker-environment.ts",
"tests/corpus/native-worker-roots/main.ts"
"tests/corpus/native-worker-roots/main.ts",
"tests/corpus/5998-worker-env-option.ts",
"tests/corpus/5999-worker-env-nested.cjs"
]
},
"anchorSource": "exact"
Expand Down Expand Up @@ -115157,8 +115161,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:worker_threads"
"status": "partial",
"evidence": "compiler-feature:worker_threads.SHARE_ENV",
"tests": [
"tests/corpus/5998-worker-env-option.ts",
"tests/corpus/5999-worker-env-nested.cjs"
]
},
"anchorSource": "chapter"
},
Expand Down
3 changes: 2 additions & 1 deletion internal/compatibility/static-support.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@
{"chapter": "stream", "symbols": ["readable.map", "readable.filter", "readable.forEach", "readable.toArray", "readable.some", "readable.find", "readable.every", "readable.flatMap", "readable.drop", "readable.take", "readable.reduce"], "status": "partial", "evidence": ["tests/corpus/5335-stream-operators-values.ts", "tests/corpus/5336-stream-operators-promise-timing.ts", "tests/corpus/5337-stream-operators-concurrency.ts", "tests/corpus/5338-stream-operators-checks.cjs"]},
{"chapter": "stream", "symbols": ["stream.Readable.from"], "status": "partial", "evidence": ["tests/corpus/5340-readable-from-options.ts", "tests/corpus/5341-readable-from-dynamic.cjs", "tests/corpus/5420-readable-from-generators.ts", "tests/corpus/5421-readable-from-generators.cjs", "tests/corpus/5610-readable-from-sets-and-views.ts", "tests/corpus/5611-readable-from-class-iterables.ts", "tests/corpus/5612-readable-from-iterables.cjs"]},
{"chapter": "stream", "symbols": ["stream.compose"], "status": "partial", "evidence": ["tests/corpus/5342-stream-compose.ts", "tests/corpus/5343-stream-compose-edges.ts"]},
{"chapter": "worker_threads", "symbols": ["Worker"], "status": "partial", "evidence": ["tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-environment.ts", "tests/corpus/native-worker-roots/main.ts"]},
{"chapter": "worker_threads", "symbols": ["Worker"], "status": "partial", "evidence": ["tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-environment.ts", "tests/corpus/native-worker-roots/main.ts", "tests/corpus/5998-worker-env-option.ts", "tests/corpus/5999-worker-env-nested.cjs"]},
{"chapter": "worker_threads", "symbols": ["SHARE_ENV"], "status": "partial", "evidence": ["tests/corpus/5998-worker-env-option.ts", "tests/corpus/5999-worker-env-nested.cjs"]},
{"chapter": "worker_threads", "symbols": ["MessagePort", "parentPort", "worker_threads.workerData", "isMainThread", "threadId"], "status": "partial", "evidence": ["tests/corpus/native-worker-commonjs.cjs", "tests/corpus/native-worker-port-lifetime.ts", "tests/corpus/native-worker-references.ts"]},
{"chapter": "worker_threads", "symbols": ["worker.postMessage", "port.postMessage"], "status": "partial", "evidence": ["tests/corpus/native-worker-message-graphs.ts", "tests/corpus/native-worker-shared-views.ts", "tests/corpus/native-worker-clone-errors.ts", "tests/corpus/native-worker-delayed-messages.ts"]},
{"chapter": "worker_threads", "symbols": ["worker.terminate"], "status": "partial", "evidence": ["tests/corpus/native-worker-terminate-loop.ts", "tests/corpus/native-worker-terminate-wait.ts", "tests/corpus/native-worker-terminate-suspended.ts", "tests/corpus/native-worker-loop-cancellation.ts"]},
Expand Down
5 changes: 5 additions & 0 deletions packages/compiler/ambient/scriptc-node-fallback.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6340,7 +6340,12 @@ declare module "worker_threads" {
export const resourceLimits: ResourceLimits;
/* the Worker's name option, trimmed ("WorkerThread" by default); "" in the main thread */
export const threadName: string | null;
/* as options.env, the worker shares this thread's process.env */
export const SHARE_ENV: unique symbol;
export interface WorkerOptions {
/* the worker's process.env: an object's values as strings, or SHARE_ENV;
* a copy of this thread's process.env by default */
env?: { [name: string]: unknown } | typeof SHARE_ENV | null;
workerData?: unknown;
transferList?: readonly unknown[];
argv?: readonly unknown[];
Expand Down
9 changes: 9 additions & 0 deletions packages/compiler/src/frontend/lowering/builtins/workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DYN,
F64,
STRING,
SYMBOL_T,
UNDEFINED_T,
VOID,
isFfiCallbackParam,
Expand All @@ -32,6 +33,14 @@ export function lowerWorkerMetadata(member: string, loc: SrcLoc): IrExpr | null
return { kind: "libCall", fn: "worker.resourceLimits", args: [], type: DYN, loc };
case "threadName":
return { kind: "libCall", fn: "worker.threadName", args: [], type: DYN, loc };
case "SHARE_ENV":
return {
kind: "libCall",
fn: "sym.for",
args: [{ kind: "strLit", value: "nodejs.worker_threads.SHARE_ENV", type: STRING, loc }],
type: SYMBOL_T,
loc,
};
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ function moduleValue(lowerer: Lowerer, module: string, loc: SrcLoc): IrExpr {
add("win32", moduleValue(lowerer, "path/win32", loc));
}
if (module === "worker_threads") {
for (const member of ["isInternalThread", "parentPort", "workerData"])
for (const member of ["SHARE_ENV", "isInternalThread", "parentPort", "workerData"])
add(member, lowerWorkerMetadata(member, loc)!);
}
body.push({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5998-worker-env-option.ts"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5999-worker-env-nested.cjs"
],
"diags": []
}
37 changes: 32 additions & 5 deletions packages/runtime/src/scr_child.c
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ static void scr_child_error_sync_fields(ScrError *e, bool has_status, double sta
scr_error_set_prop(e, "stderr", 6, err);
}

/* The [k,v,...] environment of a child whose options give none: the
* calling worker's process.env when the worker has its own, as Node spawns
* with process.env, or NULL to inherit the process environment. +1. */
static ScrArr *scr_child_default_env(void) {
#ifdef SCR_WORKERS
return scr_context_env_pairs();
#else
return NULL;
#endif
}

#ifdef _WIN32
/* ── Windows arm: real children via CreateProcessW ────────────────────
* The POSIX arm below owns the corresponding implementation; this arm
Expand Down Expand Up @@ -670,7 +681,11 @@ static void scr_win_create_process(ScrStr *cmd, ScrArr *args, ScrWinSpawn *sp,
cwd_w[0] = L'\0';
if (need > 0) (void)GetCurrentDirectoryW(need, cwd_w);
}
WCHAR *env_block = sp->env_pairs != NULL ? scr_child_env_block(sp->env_pairs) : NULL;
ScrArr *inherited = sp->env_pairs != NULL ? NULL : scr_child_default_env();
WCHAR *env_block = sp->env_pairs != NULL ? scr_child_env_block(sp->env_pairs)
: inherited != NULL ? scr_child_env_block(inherited)
: NULL;
scr_arr_release(inherited);
WCHAR *path_w = scr_child_path(env_block);
WCHAR *file_w;
if (shell) {
Expand Down Expand Up @@ -3076,6 +3091,9 @@ static bool scr_cap_read(ScrCapBuf *b, int fd) {
return true;
}

static char **scr_exec_envp(ScrArr *pairs);
static void scr_exec_envp_free(char **envp);

/* ── spawnSync ───────────────────────────────────────────────────────── */

static ScrSpawnRes *scr_spawn_res_new(bool has_status, double status,
Expand Down Expand Up @@ -3138,10 +3156,14 @@ static ScrSpawnRes *scr_spawn_sync_core(ScrStr *cmd, ScrArr *args, double timeou
}

char **argv = scr_child_argv(cmd, args);
ScrArr *inherited = scr_child_default_env();
char **envp = inherited != NULL ? scr_exec_envp(inherited) : NULL;
scr_arr_release(inherited);
pid_t pid = -1;
int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, environ);
int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv, envp != NULL ? envp : environ);

@vercel vercel Bot Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On POSIX, a worker with a different env.PATH still resolves spawned executables against the main thread's PATH, causing ENOENT for worker-only commands or running the wrong executable.

Fix on Vercel

posix_spawn_file_actions_destroy(&fa);
free(argv);
scr_exec_envp_free(envp);
if (cap_out) close(outfd[1]);
if (cap_err) close(errfd[1]);

Expand Down Expand Up @@ -3354,8 +3376,9 @@ static char *scr_exec_display(ScrStr *cmd, ScrArr *args, bool shell) {
}

/* Builds a NULL-terminated envp from [k,v,...] pairs ("k=v" strings, each
* malloc'd). Free with scr_exec_envp_free. */
* malloc'd), or NULL for NULL pairs. Free with scr_exec_envp_free. */
static char **scr_exec_envp(ScrArr *pairs) {
if (!pairs) return NULL;
size_t n = (size_t)scr_arr_len(pairs) / 2;
char **envp = malloc((n + 1) * sizeof(char *));
if (!envp) scr_child_oom();
Expand Down Expand Up @@ -3476,7 +3499,9 @@ ScrStr *scr_exec_sync_core(ScrStr *cmd, ScrArr *args, const ScrExecOpts *o) {
#endif

char **argv = scr_child_argv(cmd, args);
char **envp = o->env_pairs != NULL ? scr_exec_envp(o->env_pairs) : NULL;
ScrArr *inherited = o->env_pairs != NULL ? NULL : scr_child_default_env();
char **envp = scr_exec_envp(o->env_pairs != NULL ? o->env_pairs : inherited);
scr_arr_release(inherited);
pid_t pid = -1;
int spawn_err = posix_spawnp(&pid, cmd->data, &fa, NULL, argv,
envp != NULL ? envp : environ);
Expand Down Expand Up @@ -5034,7 +5059,9 @@ ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode,
#endif
char *shell_argv[] = {(char *)"/bin/sh", (char *)"-c", cmd->data, NULL};
char **argv = shell ? shell_argv : scr_child_argv(cmd, args);
char **envp = has_env && env_pairs != NULL ? scr_exec_envp(env_pairs) : NULL;
ScrArr *inherited = has_env && env_pairs != NULL ? NULL : scr_child_default_env();
char **envp = scr_exec_envp(has_env && env_pairs != NULL ? env_pairs : inherited);
scr_arr_release(inherited);
pid_t pid = -1;
const char *file = shell ? "/bin/sh" : cmd->data;
int spawn_err = posix_spawnp(&pid, file, &fa, &at, argv,
Expand Down
25 changes: 21 additions & 4 deletions packages/runtime/src/scr_context.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ const char *scr_context_getenv(const char *name) {
return NULL;
}

bool scr_context_env_set(const char *name, const char *value) {
ScrContextEnv *environment = scr_context_environment;
if (!environment) return false;
static void scr_context_env_store(ScrContextEnv *environment, const char *name, const char *value) {
for (size_t i = 0; i < environment->count; i++) {
ScrContextEnvEntry *entry = &environment->entries[i];
if (strcmp(entry->name, name)) continue;
Expand All @@ -92,14 +90,33 @@ bool scr_context_env_set(const char *name, const char *value) {
free(entry->name);
memmove(entry, entry + 1, (--environment->count - i) * sizeof(*entry));
}
return true;
return;
}
if (value) {
ScrContextEnvEntry *entries = realloc(environment->entries, (environment->count + 1) * sizeof(*entries));
if (!entries) scr_trap("scriptc: out of memory\n");
environment->entries = entries;
entries[environment->count++] = (ScrContextEnvEntry){scr_context_text_copy(name), scr_context_text_copy(value)};
}
}

ScrContextEnv *scr_context_env_new(void) {
ScrContextEnv *environment = calloc(1, sizeof(*environment));
if (!environment) scr_trap("scriptc: out of memory\n");
environment->entries = calloc(1, sizeof(*environment->entries));
if (!environment->entries) scr_trap("scriptc: out of memory\n");
return environment;
}

void scr_context_env_put(ScrContextEnv *environment, const char *name, const char *value) {
scr_context_env_store(environment, name, value);
}

bool scr_context_env_private(void) { return scr_context_environment != NULL; }

bool scr_context_env_set(const char *name, const char *value) {
if (!scr_context_environment) return false;
scr_context_env_store(scr_context_environment, name, value);
return true;
}

Expand Down
6 changes: 6 additions & 0 deletions packages/runtime/src/scr_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,13 @@ extern SCR_TL void (*scr_context_report_error)(void);
extern SCR_TL bool scr_context_report_rejection;
typedef struct ScrContextEnv ScrContextEnv;
ScrContextEnv *scr_context_env_capture(void);
/* An empty environment, and a variable set in one (copies both strings). */
ScrContextEnv *scr_context_env_new(void);
void scr_context_env_put(ScrContextEnv *environment, const char *name, const char *value);
void scr_context_env_free(ScrContextEnv *environment);
/* True when the calling thread has its own environment; false when it uses
* the process environment (the main thread and SHARE_ENV workers). */
bool scr_context_env_private(void);
void scr_context_env_enter(ScrContextEnv *environment);
bool scr_context_env_set(const char *name, const char *value);
const char *scr_context_getenv(const char *name);
Expand Down
4 changes: 4 additions & 0 deletions packages/runtime/src/scr_dyn.h
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,10 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value);
bool scr_dyn_is_frozen(const ScrDyn *value);
ScrDyn *scr_dyn_new_bigint(ScrBigInt *value); /* borrows and retains value */
ScrDyn *scr_dyn_new_symbol(ScrSym *value); /* borrows and retains value */
/* Symbol.keyFor for units outside the optional symbol unit: the +1
* registry key of a symbol, or NULL. The symbol unit installs it when its
* registry first holds a symbol; until then no symbol is registered. */
extern SCR_TL ScrStr *(*scr_dyn_symbol_key_fn)(ScrSym *s);
ScrSym *scr_sym_well_known(ScrStr *name);
ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key);
ScrDyn *scr_dyn_symbol_key_get(const ScrDyn *value, const ScrDyn *key, bool optional);
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/src/scr_json.c
Original file line number Diff line number Diff line change
Expand Up @@ -4126,6 +4126,7 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) {
* references the fiber machinery (the unit-test subset links). */
SCR_TL void (*scr_dyn_promise_release_fn)(ScrPromise *p) = NULL;
SCR_TL bool (*scr_dyn_promise_identity_fn)(ScrPromise *a, ScrPromise *b) = NULL;
SCR_TL ScrStr *(*scr_dyn_symbol_key_fn)(ScrSym *s) = NULL;

ScrDyn *scr_dyn_alloc_promise(void (*release_fn)(ScrPromise *p)) {
scr_dyn_promise_release_fn = release_fn;
Expand Down
Loading
Loading