What’s New in Node.js 20 — SitePoint
Version 20 of Node.js was released on April 18, 2023. It fixes some issues and critiques already “fixed” by Deno and Bun, including a new permissions model and a stable native test runner. This article explores the new options available to developers using the world’s most widely used JavaScript runtime.
Contents:
- The Node.js release schedule
- New permission model
- Native test runner
- Compile a single executable application
- Updated V8 JavaScript engine
- Various updates
The Node.js release schedule
Node.js has a six-month release schedule:
-
The even-numbered April releases (14, 16, 18, etc.) are stable and receive support updates (LTS) for three years.
-
October’s odd numbers (15, 17, 19, etc.) are more experimental and updates often end after a year.
In general, you should go for the even-numbered LTS version unless you need a specific feature in an experimental release and plan to upgrade later. That said, Node.js 20 is new and the website advises you to continue with version 18 while the development team fixes any final issues.
Node.js 20 has the following new features…
New permission model
Run node somescript.js
is not without risk. A script can do anything: delete essential files, send private data to a server, or run a cryptocurrency miner in an underlying process. It’s hard to guarantee that your own code won’t break anything: can you be sure that all modules and their dependencies are safe?
The new (experimental) Node.js permission model limits what script can do. To use it, add the --experimental-permission
mark you node
command line followed by:
-
--allow-fs-read
to grant read access to files. You can limit read access to:- specific folders:
--allow-fs-read=/tmp/
- specific files:
--allow-fs-read=/home/me/data.json
- or wildcard file patterns:
--allow-fs-read=/home/me/*.json
- specific folders:
-
--allow-fs-write
to grant write access to files with identical directory, file, or wildcard patterns. -
--allow-child-process
to enable underlying processes, such as running other scripts that may be written in other languages. -
--allow-worker
to allow worker threads, which execute Node.js code in parallel with the main processing thread.
In the following example somescript.js
can read files in the /home/me/data/
folder:
node --experimental-permission --allow-fs-read=/home/me/data/ somescript.js
Any attempt to write a file, run another process, or launch a web worker will result in a ERR_ACCESS_DENIED
wrong.
You can check the permissions within your application using the new process.permission
object. For example, you can check if the script can write files as follows:
process.permission.has('fs.write');
To check if the script can write to a specific file:
if ( !process.permission.has('fs.write', '/home/me/mydata.json') ) {
console.error('Cannot write to file');
}
JavaScript permission management was first introduced by Deno, which provides fine-grained control over access to files, environment variables, operating system information, timing, the network, dynamically loaded libraries, and underlying processes. Node.js is insecure by default unless you change it --experimental-permission
flag. This is less effective, but allows existing scripts to continue running without modification.
Native test runner
Historically, Node.js was a minimal runtime, allowing developers to choose which tools and modules they needed. Running code tests required a third-party module such as Mocha, AVA, or Jest. While this resulted in many choices, it can be difficult to make the right choice best decision, and switching tools may not be easy.
Other runtimes took an alternative view, offering built-in tools that were considered essential for development. Deno, Bun, Go, and Rust all offer built-in test runners. Developers have a default choice, but can choose an alternative if their project has specific requirements.
Node.js 18 introduced an experimental test runner which is now stable in version 20. You don’t need to install any third-party module and you can create test scripts:
- in your projects
/test/
folder - by naming the file
test.js
,test.mjs
ortest.cjs
- using
test-
at the beginning of the file name — such astest-mycode.js
- using
test
at the end of the file name with preceding dot (.
), hyphen (-
) or underscore (_
) – asmycode-test.js
,mycode_test.cjs
ormycode.test.mjs
You can then import node:test
And node:assert
and write test functions:
import { test, mock } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
test('my first test', (t) => {
assert.strictEqual(1, 1);
});
test('my second test', (t) => {
assert.strictEqual(1, 2);
});
mock.method(fs, 'readFile', async () => 'Node.js test');
test('my third test', async (t) => {
assert.strictEqual( await fs.readFile('anyfile'), 'Node.js test' );
});
Run the tests with node --test test.mjs
and view the output:
✔ my first test (0.9792ms)
✖ my second test (1.2304ms)
AssertionError: Expected values to be strictly equal:
1 !== 2
at TestContext.<anonymous> (test.mjs:10:10)
at Test.runInAsyncScope (node:async_hooks:203:9)
at Test.run (node:internal/test_runner/test:547:25)
at Test.processPendingSubtests (node:internal/test_runner/test:300:27)
at Test.postRun (node:internal/test_runner/test:637:19)
at Test.run (node:internal/test_runner/test:575:10)
at async startSubtest (node:internal/test_runner/harness:190:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 2,
operator: 'strictEqual'
}
✔ my third test (0.1882ms)
ℹ tests 3
ℹ pass 2
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 72.6767
You can add one --watch
flag to automatically rerun tests when the file changes:
node --test --watch test.mjs
You can also run any tests found in the project:
node --test
Native testing is a welcome addition to the Node.js runtime. There’s less of a need to learn different third-party APIs, and I no longer have an excuse when forget to add tests to smaller projects!
Compile a single executable application
Node.js projects need the runtime to run. This can be a hindrance when distributing applications to platforms or users who cannot easily install or maintain Node.js.
Version 20 provides an experimental feature that allows you to create a single executable application (SEA) that you can deploy without dependencies. The manual explains the process, although it’s a bit complicated:
-
You must have a project with a single script. It should use CommonJS instead of ES modules.
-
Create a JSON configuration file that will be used to build your script into a blob that will run within the runtime. For example,
sea-config.json
:{ "main": "myscript.js", "output": "sea-prep.blob" }
-
Generate the blob with
node --experimental-sea-config sea-config.json
. -
Then, according to your operating system, you need to install the
node
executable, unsign the binary, inject the blob into the binary, sign it again, and test the resulting application.
While it works, you’re limited to older CommonJS projects and can only target the same OS you’re using. It will surely improve as the superior Deno compiler can create an executable file for any platform with a single command from JavaScript or TypeScript source files.
You should also consider the file size of the resulting executable file. A console.log('Hello World');
generates an 85 MB file, because Node.js (and Deno) have to add the whole V8 JavaScript engine and standard libraries. Options to reduce the file size are being considered, but are unlikely to go below 25 MB.
Compilation isn’t practical for small command-line tools, but it’s a more viable option for larger projects, such as a full web server application.
Updated V8 JavaScript engine
Node.js 20 includes the latest version of the V8 engine, which includes the following JavaScript features:
Various updates
The following updates and improvements are also available:
Resume
Node.js 20 is a big step forward. It’s a more significant release and implements some of Deno’s better features.
However, this begs the question: should you use Deno instead?
Dennis is great. It is stable, supports standard TypeScript, shortens development times, requires fewer tools, and receives regular updates. On the other hand, it has been less time, has fewer modules, and is often shallower imitations of Node.js libraries.
Deno and Bun are worth considering for new projects, but there are thousands of existing Node.js applications. Deno and Bun make it easier to port code, but there won’t always be a clear benefit to moving away from Node.js.
The good news is that we have a thriving JavaScript ecosystem. The runtime teams learn from each other and rapid evolution benefits developers.