What is the Problem Being Solved?
There is no explicit declaration of what engines (specifically, node) any given package is expected to support (except for promise-kit, eslint-plugin, and import-bundle). The root workspace claims support for Node.js v16+, which is most assuredly a lie.
We have dropped any version of Node.js older than 22.x from our build matrix, so we implicitly only support Node.js v22.x and newer. More precisely—as of this writing—implicit support is for Node.js v22.22.2+.
Description of the Design
Add engines.node to all package.json files and update those which need updating.
I do not recommend claiming support for an engine if we are not testing against it in CI.
We need to pick a minimum version. I recommend Node.js v22.5.1 as the absolute minimum version we support due to the major regression fixed in this version. However, there are certainly features which would be useful to us in newer versions; see the list below.
TypeScript Support
| Version |
Feature |
| 22.6.0 |
--experimental-strip-types — strips type annotations from .ts files; initial TypeScript execution support |
| 22.7.0 |
--experimental-transform-types — transforms TypeScript-only syntax (enum, namespace) to JS |
| 22.14.0 |
TypeScript support in STDIN eval (node --input-type=typescript) |
| 22.14.0 |
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX error code |
| 22.14.0 |
Worker eval TypeScript input |
| 22.18.0 |
Type stripping enabled by default — node file.ts works without flags (experimental, disable with --no-experimental-strip-types) |
Module System (ESM / CJS)
| Version |
Feature |
| 22.7.0 |
Module syntax detection (--experimental-detect-module) enabled by default |
| 22.10.0 |
New "module-sync" exports condition for package.json — enables dual-package ESM/CJS without the hazard |
| 22.12.0 |
require(esm) enabled by default — no longer behind a flag |
| 22.13.0 |
module.stripTypeScriptTypes() API |
| 22.13.0 |
require(esm) experimental warning only emitted under --trace-require-module |
| 22.14.0 |
module.findPackageJSON() utility |
| 22.15.0 |
module.registerHooks() — synchronous customization hooks; used for import(cjs) preparsing |
| 22.16.0 |
import.meta.filename and import.meta.dirname graduated to stable |
| 22.16.0 |
Top-level Wasm import without needing a "type" field in package.json |
| 22.18.0 |
import.meta.main — true if the current module is the entry point |
| 22.19.0 |
--experimental-wasm-modules unflagged — WebAssembly modules now stable |
| 22.22.1 |
--heapsnapshot-near-heap-limit marked stable |
CLI Flags
| Version |
Feature |
| 22.8.0 |
Coverage thresholds: --test-coverage-branches, --test-coverage-functions, --test-coverage-lines |
| 22.10.0 |
node --run stabilized — runs package.json scripts directly |
| 22.13.0 |
--trace-env and `--trace-env-[js |
| 22.14.0 |
--disable-sigusr1 — prevents creating the signal I/O thread (stabilized in v22.20.0) |
| 22.15.0 |
--cpu-prof* flags now allowed in NODE_OPTIONS |
| 22.18.0 |
--watch-kill-signal — configure the signal used to kill watched processes |
| 22.19.0 |
NODE_USE_SYSTEM_CA=1 environment variable |
| 22.19.0 |
${pid} placeholder in --cpu-prof-name |
| 22.16.0 |
node.config.json default config file support — Node.js reads this file at startup |
| 22.21.0 |
--use-env-proxy — enable proxy from environment variables (HTTP_PROXY, etc.) |
| 22.21.0 |
Percentage support in --max-old-space-size (e.g., --max-old-space-size=75%) |
node:sqlite
| Version |
Feature |
| 22.13.0 |
node:sqlite unflagged — the module is no longer experimental by default |
| 22.13.0 |
StatementSync.prototype.iterate() method |
| 22.13.0 |
SQLite constants aggregated under a single property |
| 22.12.0 |
SQLite Session Extension support (createSession(), applyChangeset()) |
| 22.14.0 |
StatementSync accepts TypedArray and DataView as bind parameters |
| 22.15.0 |
User-defined functions can return ArrayBufferViews |
| 22.16.0 |
StatementSync.prototype.columns() — returns column metadata |
| 22.18.0 |
readBigInts option at the DB connection level |
node:test (test runner)
| Version |
Feature |
| 22.6.0 |
test_runner.run() gains watch support and globPatterns option |
| 22.6.0 |
context.filePath property |
| 22.6.0 |
Snapshot test file determined from context |
| 22.8.0 |
Support running tests in-process |
| 22.8.0 |
Defer inheriting hooks until run() |
| 22.10.0 |
Support custom argv in run() |
| 22.10.0 |
'test:summary' event |
| 22.10.0 |
Code coverage via run() API |
| 22.14.0 |
TestContext.prototype.waitFor() — wait for a condition to become true |
| 22.14.0 |
t.assert.fileSnapshot() — file-based snapshot assertions |
| 22.14.0 |
assert.register() — register custom assertion methods |
| 22.20.0 |
Object property mocking (mock.getter(), mock.setter()) |
HTTP / HTTPS
| Version |
Feature |
| 22.6.0 |
Diagnostics channel http.client.request.error |
| 22.12.0 |
Diagnostics channel http.client.request.created |
| 22.19.0 |
server.keepAliveTimeoutBuffer option |
| 22.20.0 |
Agent.agentKeepAliveTimeoutBuffer option |
| 22.21.0 |
shouldUpgradeCallback — servers can control HTTP upgrade behavior |
| 22.21.0 |
Built-in proxy support in http.request(), https.request(), and Agent — picks up HTTP_PROXY/HTTPS_PROXY when --use-env-proxy or NODE_USE_ENV_PROXY is set |
| 22.21.0 |
Fetch respects proxy under NODE_USE_ENV_PROXY |
HTTP/2
| Version |
Feature |
| 22.10.0 |
Expose nghttp2_option_set_stream_reset_rate_limit as a session option |
| 22.17.0 |
Diagnostics channel http2.server.stream.finish |
| 22.20.0 |
Raw header arrays in h2Stream.respond() |
Crypto / TLS
| Version |
Feature |
| 22.9.0 |
tls.createSecureContext({ allowPartialTrustChain: true }) — treat intermediate certs as trusted |
| 22.10.0 |
KeyObject.prototype.toCryptoKey() |
| 22.10.0 |
x509.validFrom / x509.validTo as Date objects |
| 22.13.0 |
WebCryptoAPI Ed25519 and X25519 algorithms graduated to stable |
| 22.15.0 |
--use-system-ca support on macOS and Windows |
| 22.15.0 |
tls.getCACertificates() |
| 22.19.0 |
tls.setDefaultCACertificates() |
| 22.20.0 |
OpenSSL updated to 3.5.2 (previously 3.0.x) |
Streams
| Version |
Feature |
| 22.6.0 |
stream.DuplexPair API exposed |
| 22.16.0 |
stream.finished() preserves AsyncLocalStorage context |
| 22.20.0 |
Brotli support in CompressionStream / DecompressionStream |
Workers
| Version |
Feature |
| 22.10.0 |
worker.markAsUncloneable() — mark objects to throw on structured clone |
| 22.14.0 |
worker.isInternalWorker() — detect internal Node.js workers |
| 22.16.0 |
worker.getHeapStatistics() |
| 22.18.0 |
Worker is now async disposable (Symbol.asyncDispose) |
| 22.20.0 |
CPU profiling APIs for workers |
Process
| Version |
Feature |
| 22.10.0 |
process.features.require_module |
| 22.10.0 |
process.features.typescript |
| 22.14.0 |
process.ref() and process.unref() — control whether the process stays alive |
| 22.15.0 |
process.execve() — replace process image (Unix only) |
| 22.19.0 |
process.threadCpuUsage() — CPU usage for the current thread |
File System
| Version |
Feature |
| 22.14.0 |
fs.glob() exclude option accepts glob patterns |
| 22.17.0 |
fs.FileHandle.readableWebStream({ autoClose }) option |
| 22.17.0 |
fs.Dir supports explicit resource management (Symbol.asyncDispose) |
| 22.18.0 |
fs.watch() async iterator correctly handles event bursts |
| 22.20.0 |
path.matchesGlob() graduated to stable |
Networking
| Version |
Feature |
| 22.13.0 |
net.BlockList support in net.connect() and net.Server |
| 22.13.0 |
SocketAddress.parse() |
| 22.13.0 |
net.BlockList.isBlockList(value) |
| 22.13.0 |
dgram blocklist support (UDP) |
| 22.12.0 |
UV_TCP_REUSEPORT for TCP sockets |
| 22.12.0 |
UV_UDP_REUSEPORT for UDP sockets |
| 22.19.0 |
net.BlockList can now save/load to/from files |
| 22.19.0 |
DNS max timeout support |
| 22.15.0 |
DNS: TLSA record query and parsing |
Compile Cache
| Version |
Feature |
| 22.8.0 |
module.enableCompileCache() — JS API to enable on-disk code caching |
| 22.10.0 |
module.flushCompileCache() — flush cache to disk |
Utilities
| Version |
Feature |
| 22.9.0 |
util.getCallSite() — retrieve current execution stack trace |
| 22.13.0 |
util.getCallSites() gains source map support |
| 22.15.0 |
util.diff() — expose the Myers diff function used internally by assert |
| 22.16.0 |
util.types.isFloat16Array() |
| 22.17.0 |
util.styleText() gains 'none' style for removing terminal styling |
node:assert
| Version |
Feature |
| 22.12.0 |
AssertionError uses Myers diff algorithm for nicer diffs |
| 22.13.0 |
assert.partialDeepStrictEqual() (experimental) |
| 22.15.0 |
Improvements to partialDeepStrictEqual error comparison |
| 22.17.0 |
assert.partialDeepStrictEqual() graduated to stable |
node:v8 / node:vm
| Version |
Feature |
| 22.8.0 |
vm.constants.DONT_CONTEXTIFY — create a context with a freezable globalThis |
| 22.15.0 |
v8.getCppHeapStatistics() |
SEA (Single Executable Applications)
| Version |
Feature |
| 22.20.0 |
execArgv support in SEA config |
| 22.20.0 |
execArgvExtension |
Inspector / DevTools
| Version |
Feature |
| 22.6.0 |
Experimental network inspection (--experimental-network-inspection) |
| 22.7.0 |
Inspector Network.loadingFailed event |
Compression (node:zlib)
| Version |
Feature |
| 22.15.0 |
Zstd compression support (zlib.createZstd*, etc.) |
| 22.19.0 |
Dictionary support in zstdCompress / zstdDecompress |
Permission Model
| Version |
Feature |
| 22.13.0 |
Permission Model graduated to stable |
| 22.18.0 |
Permission model flags propagated when spawning child processes |
| 22.18.0 |
permission.has('addon') support |
| 22.17.0 |
Implicit --allow-fs-read to entrypoint file |
Other Stabilizations (v22.17.0 batch)
APIs graduated to stable in a single batch in v22.17.0:
dirent.parentPath
filehandle.readableWebStream()
fs.glob()
fs.openAsBlob()
node:readline/promises
port.hasRef()
readable.compose()
readable.iterator()
readable.readableAborted
readable.readableDidRead
Duplex.fromWeb()
Duplex.toWeb()
Readable.fromWeb()
Readable.isDisturbed()
Readable.toWeb()
stream.isErrored()
stream.isReadable()
URL.createObjectURL()
URL.revokeObjectURL()
v8.setHeapSnapshotNearHeapLimit()
Writable.fromWeb()
Writable.toWeb()
writable.writableAborted
- Startup Snapshot API
ERR_INPUT_TYPE_NOT_ALLOWED
ERR_UNKNOWN_FILE_EXTENSION
ERR_UNKNOWN_MODULE_FORMAT
ERR_USE_AFTER_CLOSE
Security Considerations
n/a
Scaling Considerations
n/a
Test Plan
n/a
Compatibility Considerations
We should document the minimum supported engine(s) in the README.md files.
Upgrade Considerations
I think this is a breaking change for certain package manager/version pairings. Others will warn.
What is the Problem Being Solved?
There is no explicit declaration of what engines (specifically,
node) any given package is expected to support (except forpromise-kit,eslint-plugin, andimport-bundle). The root workspace claims support for Node.js v16+, which is most assuredly a lie.We have dropped any version of Node.js older than
22.xfrom our build matrix, so we implicitly only support Node.js v22.x and newer. More precisely—as of this writing—implicit support is for Node.js v22.22.2+.Description of the Design
Add
engines.nodeto allpackage.jsonfiles and update those which need updating.I do not recommend claiming support for an engine if we are not testing against it in CI.
We need to pick a minimum version. I recommend Node.js v22.5.1 as the absolute minimum version we support due to the major regression fixed in this version. However, there are certainly features which would be useful to us in newer versions; see the list below.
TypeScript Support
--experimental-strip-types— strips type annotations from.tsfiles; initial TypeScript execution support--experimental-transform-types— transforms TypeScript-only syntax (enum,namespace) to JSnode --input-type=typescript)ERR_UNSUPPORTED_TYPESCRIPT_SYNTAXerror codeevalTypeScript inputnode file.tsworks without flags (experimental, disable with--no-experimental-strip-types)Module System (ESM / CJS)
--experimental-detect-module) enabled by default"module-sync"exports condition forpackage.json— enables dual-package ESM/CJS without the hazardrequire(esm)enabled by default — no longer behind a flagmodule.stripTypeScriptTypes()APIrequire(esm)experimental warning only emitted under--trace-require-modulemodule.findPackageJSON()utilitymodule.registerHooks()— synchronous customization hooks; used forimport(cjs)preparsingimport.meta.filenameandimport.meta.dirnamegraduated to stable"type"field inpackage.jsonimport.meta.main—trueif the current module is the entry point--experimental-wasm-modulesunflagged — WebAssembly modules now stable--heapsnapshot-near-heap-limitmarked stableCLI Flags
--test-coverage-branches,--test-coverage-functions,--test-coverage-linesnode --runstabilized — runspackage.jsonscripts directly--trace-envand `--trace-env-[js--disable-sigusr1— prevents creating the signal I/O thread (stabilized in v22.20.0)--cpu-prof*flags now allowed inNODE_OPTIONS--watch-kill-signal— configure the signal used to kill watched processesNODE_USE_SYSTEM_CA=1environment variable${pid}placeholder in--cpu-prof-namenode.config.jsondefault config file support — Node.js reads this file at startup--use-env-proxy— enable proxy from environment variables (HTTP_PROXY, etc.)--max-old-space-size(e.g.,--max-old-space-size=75%)node:sqlitenode:sqliteunflagged — the module is no longer experimental by defaultStatementSync.prototype.iterate()methodcreateSession(),applyChangeset())StatementSyncacceptsTypedArrayandDataViewas bind parametersArrayBufferViewsStatementSync.prototype.columns()— returns column metadatareadBigIntsoption at the DB connection levelnode:test(test runner)test_runner.run()gains watch support andglobPatternsoptioncontext.filePathpropertyrun()argvinrun()'test:summary'eventrun()APITestContext.prototype.waitFor()— wait for a condition to become truet.assert.fileSnapshot()— file-based snapshot assertionsassert.register()— register custom assertion methodsmock.getter(),mock.setter())HTTP / HTTPS
http.client.request.errorhttp.client.request.createdserver.keepAliveTimeoutBufferoptionAgent.agentKeepAliveTimeoutBufferoptionshouldUpgradeCallback— servers can control HTTP upgrade behaviorhttp.request(),https.request(), andAgent— picks upHTTP_PROXY/HTTPS_PROXYwhen--use-env-proxyorNODE_USE_ENV_PROXYis setNODE_USE_ENV_PROXYHTTP/2
nghttp2_option_set_stream_reset_rate_limitas a session optionhttp2.server.stream.finishh2Stream.respond()Crypto / TLS
tls.createSecureContext({ allowPartialTrustChain: true })— treat intermediate certs as trustedKeyObject.prototype.toCryptoKey()x509.validFrom/x509.validToasDateobjectsEd25519andX25519algorithms graduated to stable--use-system-casupport on macOS and Windowstls.getCACertificates()tls.setDefaultCACertificates()Streams
stream.DuplexPairAPI exposedstream.finished()preservesAsyncLocalStoragecontextCompressionStream/DecompressionStreamWorkers
worker.markAsUncloneable()— mark objects to throw on structured cloneworker.isInternalWorker()— detect internal Node.js workersworker.getHeapStatistics()Workeris now async disposable (Symbol.asyncDispose)Process
process.features.require_moduleprocess.features.typescriptprocess.ref()andprocess.unref()— control whether the process stays aliveprocess.execve()— replace process image (Unix only)process.threadCpuUsage()— CPU usage for the current threadFile System
fs.glob()excludeoption accepts glob patternsfs.FileHandle.readableWebStream({ autoClose })optionfs.Dirsupports explicit resource management (Symbol.asyncDispose)fs.watch()async iterator correctly handles event burstspath.matchesGlob()graduated to stableNetworking
net.BlockListsupport innet.connect()andnet.ServerSocketAddress.parse()net.BlockList.isBlockList(value)dgramblocklist support (UDP)UV_TCP_REUSEPORTfor TCP socketsUV_UDP_REUSEPORTfor UDP socketsnet.BlockListcan now save/load to/from filesCompile Cache
module.enableCompileCache()— JS API to enable on-disk code cachingmodule.flushCompileCache()— flush cache to diskUtilities
util.getCallSite()— retrieve current execution stack traceutil.getCallSites()gains source map supportutil.diff()— expose the Myers diff function used internally byassertutil.types.isFloat16Array()util.styleText()gains'none'style for removing terminal stylingnode:assertAssertionErroruses Myers diff algorithm for nicer diffsassert.partialDeepStrictEqual()(experimental)partialDeepStrictEqualerror comparisonassert.partialDeepStrictEqual()graduated to stablenode:v8/node:vmvm.constants.DONT_CONTEXTIFY— create a context with a freezableglobalThisv8.getCppHeapStatistics()SEA (Single Executable Applications)
execArgvsupport in SEA configexecArgvExtensionInspector / DevTools
--experimental-network-inspection)Network.loadingFailedeventCompression (
node:zlib)zlib.createZstd*, etc.)zstdCompress/zstdDecompressPermission Model
permission.has('addon')support--allow-fs-readto entrypoint fileOther Stabilizations (v22.17.0 batch)
APIs graduated to stable in a single batch in v22.17.0:
dirent.parentPathfilehandle.readableWebStream()fs.glob()fs.openAsBlob()node:readline/promisesport.hasRef()readable.compose()readable.iterator()readable.readableAbortedreadable.readableDidReadDuplex.fromWeb()Duplex.toWeb()Readable.fromWeb()Readable.isDisturbed()Readable.toWeb()stream.isErrored()stream.isReadable()URL.createObjectURL()URL.revokeObjectURL()v8.setHeapSnapshotNearHeapLimit()Writable.fromWeb()Writable.toWeb()writable.writableAbortedERR_INPUT_TYPE_NOT_ALLOWEDERR_UNKNOWN_FILE_EXTENSIONERR_UNKNOWN_MODULE_FORMATERR_USE_AFTER_CLOSESecurity Considerations
n/a
Scaling Considerations
n/a
Test Plan
n/a
Compatibility Considerations
We should document the minimum supported engine(s) in the
README.mdfiles.Upgrade Considerations
I think this is a breaking change for certain package manager/version pairings. Others will warn.