Skip to content

@wtf/platform-node

Documentation / @wtf/platform-node

Run a program on @wtf/platform in Node: nodePlatform({ fetch, storage }) builds Node’s platform from Node’s own objects, and runMain runs your program with it, stopping on SIGINT or SIGTERM. Like Effect’s NodeContext, it uses Node’s objects where a web standard defines the member, and Node’s versions of the other members otherwise, spawn among them. It is the only package in the repo that imports node: modules.

SubpathWhat you get
.NodePlatformOptions, NodeConsoleOptions, NodeTerminalOptions, OutputStream
./platformnodePlatform({ fetch, storage, ...members }): every member; any you pass replaces Node’s
./locksnodeLocks(): locks from node:worker_threads, releasing the lock when a callback throws
./workerNodeWorker: a node:worker_threads thread that behaves like HTML’s Worker; parentScope(), a script’s self
./consolenodeConsole({ stdout, stderr, format, level, color }): the Console Standard over stdout and stderr
./envnodeEnv({ snapshot }): process.env, live or a snapshot
./pathnodePath({ flavour }): node:path, with file URLs from node:url
./dirsnodeDirs({ env }), nodeFacts(): from node:os and node:process, cwd read when asked
./terminalnodeTerminal({ stdin, stdout, env }): reads lines from stdin with node:readline
./opennodeOpen({ env }), spawnOpen({ spawn, platform, browser }), openerOf: BROWSER, else the platform’s app
./processnodeSpawn({ env }): spawn from node:child_process, its pipes Web Streams by Readable.toWeb
./runtimerunMain(program, { fetch, storage }), processInterrupts({ signals }); processExitCode

The standard members are Node’s own: setTimeout and clearTimeout from node:timers, performance from node:perf_hooks, webcrypto from node:crypto, the console from node:console, and BroadcastChannel and MessageChannel from node:worker_threads. Node has no navigator.storage, so your entry point passes one, such as nodeStorage() from @wtf/platform-fs-node/storage, whose root is /; this package depends on no file system. Node has fetch only as a global, which nothing reads, so your entry point passes that too. dirs.cwd is process.cwd(), read each time, so a chdir shows. Where Node differs from the standard, the package adapts it: nodeLocks releases the lock when a callback throws, which Node 24 keeps held, and NodeWorker makes a thread behave like a Worker, an EventTarget whose script is a URL.

open starts the program named by BROWSER in the platform’s env, with the URL as its one argument, as CLIs do. BROWSER=none opens nothing. Otherwise it starts the platform’s own app: open on macOS, rundll32 on Windows, or xdg-open. It reads BROWSER from the env it is given each time it opens a URL, so a test sets it there: nodePlatform({ fetch, storage, env: { BROWSER: 'none' } }).

spawn starts a process with spawn from node:child_process, with no shell, and turns its pipes into Web Streams with Readable.toWeb and Writable.toWeb. A process gets the platform’s env, read as it starts, unless you give it an env of its own, and an aborted signal ends it with SIGTERM. A command that is not there rejects its status with a SystemError of NotFound.

import type { Platform } from '@wtf/platform'
import { exec } from '@wtf/platform/process'
import { runMain } from '@wtf/platform-node/runtime'
import { nodeStorage } from '@wtf/platform-fs-node/storage'
declare const serve: (platform: Platform, options: { signal: AbortSignal }) => Promise<void>
await runMain(
async ({ platform, signal }) => {
const { stdout } = await exec(platform.spawn, 'git', ['rev-parse', '--short', 'HEAD'])
platform.console.log('Serving %s at %s', stdout.trim(), platform.env['PORT'] ?? '3000')
await serve(platform, { signal })
},
{ fetch, storage: nodeStorage() }
)

runMain runs your program with nodePlatform({ fetch, storage }), or with the platform you give it. It aborts the program’s signal at the first SIGINT or SIGTERM; a second one ends the process, as Node does by default. It sets process.exitCode from how the program ended: 0, or the exitCode of what it threw, or 1.

It uses Node’s own modules and runs only in Node. The file system is for your entry point to pass in, from @wtf/platform-fs-node.

Modules