Runtimes and package managers
For its first fourteen years, JavaScript lived almost entirely in the browser. In 2009 it got a second home, and a year later a way to share code between projects. Almost everything about how JavaScript is run and installed today follows from the decisions made in those two years, from the mistakes among them, and from the tools that were built to fix the mistakes.
This lecture has no session. Read it, run the examples on your own machine, and watch the videos marked as required at the end.
The examples need Node 22.18 or later. Every other tool is optional, and every example's output is shown.
JavaScript leaves the browser
1.1 An engine, and everything around it
In 2009, Ryan Dahl took V8, the JavaScript engine Google had just built for Chrome, and gave it a home outside the browser. He called it Node.
An engine runs the language. It parses JavaScript, compiles it and executes it, and provides everything the language specification defines, such as Array, Promise and Map. That is all it does. The language can't read a file, open a network connection or even wait: setTimeout and console aren't part of it. Everything else comes from the runtime around the engine:
- An event loop, which decides when callbacks, timers and promise reactions run.
- Input and output: files, the network, processes, the terminal.
- A module loader, which turns
import "x"into a file. - Globals, from
fetchtoprocess. - Often tools: a test runner, a formatter, a package manager.
The browser had always been a runtime, with the DOM (Document Object Model) and the rest of the web platform as its globals. Node was a second one, for servers: the same engine, with a different everything else. Three engines matter today, and each is inside more than one runtime:
| Engine | Made by | Used by |
|---|---|---|
| V8 | Chrome, Edge, Node | |
| JavaScriptCore | Apple | Safari, Bun |
| SpiderMonkey | Mozilla | Firefox |
1.2 What Node decided
A server needs a great deal that a browser page doesn't, and in 2009 none of it had a standard. Node made its own answers, and because Node succeeded, they became everybody's:
- A module system, CommonJS, with
require. The language wouldn't have modules of its own for another six years. - Callbacks for everything asynchronous. Early Node had promises, and took them out again in 2010.
- Its own APIs, application programming interfaces, for what servers do:
fsfor files,http,Bufferfor binary data,processfor the running program. - No limits. A Node program can do anything the user running it can do: read every file, open any connection, start any program.
- A folder called
node_modules, whererequirelooks for packages: next to the importing file, then in each parent folder up to the root.
The last two turned out to matter most, because of what was built on top of them.
1.3 npm, and the shape of node_modules
In 2010, Isaac Schlueter released npm: a registry where anybody could publish a package, and a command that installs packages into node_modules. A project lists what it needs in package.json, as ranges, and npm fetches those packages, then their dependencies, then theirs.
Every package has its own dependencies, so npm first nested them, each inside the package that needed it. A package used by twenty others was installed twenty times, and the paths grew so deep that Windows couldn't delete them. npm 3, in 2015, started hoisting: it moves as many packages as it can to the top of node_modules, side by side.
nested, before npm 3 hoisted, since npm 3
node_modules/ node_modules/
debug/ debug/
node_modules/ ms/
ms/
The paths got short, and something nobody had planned became possible. A project that depends on debug, and nothing else:
{
"type": "module",
"dependencies": {
"debug": "^4.4.3"
}
}
can import ms, which only debug uses, because hoisting put ms where every file can find it:
// main.js
import ms from "ms";
console.log(ms("2 days"));
npm install && node main.js # ?
Output
172800000
ms is a phantom dependency: the project depends on it, and nothing says so. The day debug stops using ms, or moves to a version with a different API, this code breaks, without a single change to its own package.json.
The other problem was time. A range allows any newer version, so two developers who installed a week apart could get different versions of the same package, and a bug that only one of them could reproduce. Yarn, a second package manager released by Facebook in 2016, answered that with a lockfile, yarn.lock: the exact version of every package in the tree, written once and installed the same everywhere. npm added its own, package-lock.json, in 2017.
Second thoughts
2.1 The regrets
In 2018, Dahl gave a talk at JSConf EU called "10 Things I Regret About Node.js". It is the first required video for this lecture, and it takes under half an hour. Among the regrets:
- Taking promises out, which made every asynchronous API in Node a callback, for years.
- Security. Any program, and any package it installs, can do anything.
- The build system for native add-ons, which every package with compiled code still depends on.
package.json, and the central registry it assumes.node_modules, and the complicated wayrequiresearches for it.require("./file")without the.js, which forces the loader to guess.
None of these could be changed any more. Millions of programs relied on every one of them. At the end of the same talk, Dahl showed what he would do instead.
2.2 Deno: nothing without permission
Deno, which reached 1.0 in 2020, answered the regrets one by one. It runs on V8 too, and is written in Rust. It runs TypeScript directly, uses ES modules and web APIs only, and imports packages by URL, without package.json or node_modules.
Its best-known answer is to the security regret. A script that reads a file:
// read.mjs
import { readFile } from "node:fs/promises";
console.log(await readFile("notes.txt", "utf8"));
node read.mjs # ?
deno run read.mjs # ?
Output
secret
┏ ⚠️ Deno requests read access to "notes.txt".
┠─ Requested by `node:fs.open` API.
┠─ To see a stack trace for this prompt, set the DENO_TRACE_PERMISSIONS environmental variable.
┠─ Learn more at: https://docs.deno.com/go/--allow-read
┠─ Run again with --allow-read to bypass this prompt.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >
Node runs it, because Node runs everything. Deno lets a program do nothing it wasn't allowed to. In a terminal it asks, and anywhere else it refuses. Flags grant access, as narrowly as you like: --allow-read=./data, --allow-net=api.example.com, --allow-env=API_KEY.
The clean break had a cost: Deno couldn't use the million packages on npm. Deno 2, in 2024, changed course. It reads package.json, installs into node_modules, and implements most of Node's built-in modules, which is why read.mjs, written with node:fs/promises, ran in Deno at all. It wasn't enough. In October 2026, Cloudflare bought the company behind Deno, and its team moved on to Cloudflare's own runtime. Deno gets bug fixes and security patches for about a year, and then its development ends.
Its ideas have done better than the runtime. Node took several of them.
2.3 pnpm: node_modules, done strictly
Deno threw node_modules away. pnpm, started in 2016 by Rico Sta. Cruz and maintained for most of its life by Zoltan Kochan, kept it, and fixed what hoisting had broken. Its node_modules contains only what package.json lists, as symbolic links, files that point to a folder somewhere else, and a hidden folder with everything else:
node_modules/
debug → .pnpm/debug@4.4.3/node_modules/debug
.pnpm/
debug@4.4.3/node_modules/
debug/ ← the package
ms → ../../ms@2.1.3/node_modules/ms
ms@2.1.3/node_modules/
ms/
debug finds ms, because ms is linked next to it. The project doesn't, because nothing links ms into the top. Each package sees exactly what it declared. The same project, in a fresh folder, installed with pnpm:
pnpm install && node main.js # ?
Output
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'ms' imported from …/main.js
The phantom dependency is now an error on the first run, not a surprise a year later.
The files themselves live in a content-addressable store: one folder per machine, where every file is kept once, under a name made from a hash of its contents. Projects get hard links into it, a second name for the same file on disk, so ten projects with the same version of a package store it once, and an install that finds a package already in the store copies nothing.
Yarn went further still. Its Plug'n'Play mode, the default since Yarn 2 in 2020, writes no node_modules at all, only a loader file that tells Node exactly where each package is on disk. Every tool has to understand it, which is why some projects switch it off.
2.4 Bun: everything in one
Bun, started by Jarred Sumner and released as 1.0 in 2023, took a different engine, JavaScriptCore from Safari, and put a whole toolchain around it: a runtime, a package manager, a bundler and a test runner, in one program, built to be fast. Unlike Deno, it set out to run Node's code from the start: node_modules, CommonJS, Node's built-in modules. Its package manager hoists like npm's, and bun install of the same debug project runs main.js without complaint. Bun was rewritten from Zig, a young systems programming language, to Rust for version 1.4, in 2026.
By then, the alternatives had changed Node as much as Node had shaped them.
Converging
3.1 One set of APIs
Server runtimes used to invent their own API for everything. Now they implement the browser's wherever one exists. Node added fetch in 2022, and today every major runtime has fetch with Request, Response and Headers, URL, AbortController, TextEncoder, structuredClone, web streams and crypto.subtle.
That list has a name. In 2022, Cloudflare, Deno and Node's core contributors started a community group to agree on it. In 2025 it became WinterTC, Ecma International's Technical Committee 55, and published the Minimum Common Web API as the standard ECMA-429: the web APIs, from the W3C (World Wide Web Consortium) and the WHATWG (Web Hypertext Application Technology Working Group), that every web-interoperable runtime should have.
The other half is Node's own modules. Bun implemented them from the start, and Deno from version 2. So code written against web APIs and node: modules runs everywhere. What doesn't carry over is everything named after one runtime, such as Bun.file or Bun.serve.
What is shared shows up in a small test. The same file, asking which globals exist:
// runtime.mjs
console.log(navigator.userAgent);
console.log({
window: typeof window,
process: typeof process,
Bun: typeof Bun,
fetch: typeof fetch,
require: typeof require,
});
node runtime.mjs # ?
bun runtime.mjs # ?
Output
Node.js/24
{
window: 'undefined',
process: 'object',
Bun: 'undefined',
fetch: 'function',
require: 'undefined'
}
Bun/1.4.3
{
window: "undefined",
process: "object",
Bun: "object",
fetch: "function",
require: "function",
}
Both have fetch, and both have process, though process began as Node's alone. Bun adds a global of its own, and even offers require in an ES module. Code that checks typeof process to detect Node, or typeof window to detect a browser, guesses wrong somewhere. Check for the feature you need, typeof fetch === "function", or for the runtime's own global.
3.2 Node learns back
Node, in turn, took what made the others attractive:
- A test runner,
node --test, and watch mode,node --watch, in 2022. - A permission model, in 2023, with network access added in Node 25.
- TypeScript without a build step, by default since 2025. Node strips the type annotations and runs what is left, so syntax that generates code, such as an enum, is refused. Bun transpiles instead, and runs enums too. Neither checks the types when it runs a file: that stays a separate step,
tsc, orbun checksince Bun 1.4.3.
Node's permission model is opt-in. --permission turns it on, and each --allow-* flag opens one door again:
node --permission read.mjs # ?
Output
Error: Access to this API has been restricted. Use --allow-fs-read to manage permissions.
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: 'notes.txt'
node --permission --allow-fs-read=. read.mjs
secret
On Node 24, --permission covers files, child processes, workers and native add-ons, but not the network: a fetch still succeeds. On Node 26, the same fetch fails with ERR_ACCESS_DENIED unless --allow-net is given.
Node's documentation says its permission model "does not protect against malicious code". It stops trusted code from touching what it shouldn't by mistake.
3.3 Install scripts, switched off
The security regret had a second half, on the package manager's side. A package can declare preinstall, install and postinstall scripts, which run when it is installed. They exist for good reasons, mostly to download or compile a native binary. They also run whatever code the package's author put there, or whoever stole the author's credentials, on every machine that installs it, before anybody has imported a line.
The Shai-Hulud worm of September 2025 spread exactly that way. Its install script searched each machine for npm tokens, and used them to publish infected versions of more packages. Bun never ran dependencies' install scripts by default. pnpm stopped in version 10, in 2025, and npm in version 12, in July 2026:
| Manager | Dependencies' install scripts | To allow one |
|---|---|---|
| npm 12 | not run, with a warning | npm install-scripts approve <pkg>, saved as allowScripts in package.json |
| pnpm 11+ | not run, and the install fails until you decide | pnpm approve-builds <pkg>, saved as allowBuilds in pnpm-workspace.yaml |
| Bun | not run, except for a built-in list of popular packages | bun pm trust <pkg>, saved as trustedDependencies in package.json |
The project's own scripts, such as build or test, run as before. Only the scripts of installed packages are affected.
Bun itself is an example. Its package on npm downloads the right binary for your system in a postinstall script, so under npm 12:
npx bun runtime.mjs
Error: Bun's postinstall script was not run.
This occurs when using --ignore-scripts during installation, or when using a
package manager like pnpm that does not run postinstall scripts by default.
npx --allow-scripts=bun bun runtime.mjs allows that one package, and it runs.
Since version 11, pnpm also waits one day before it installs a newly published version, which is often enough for a malicious release to be found and removed. minimumReleaseAge changes the delay.
3.4 What is left to choose
After years of borrowing in both directions, the runtimes differ less than their histories suggest:
| Node | Bun | |
|---|---|---|
| Engine | V8 | JavaScriptCore |
| Written in | C++ | Rust, since 1.4. Zig before |
| TypeScript | strips the types, then runs | transpiles, then runs |
| Checking types | no, that is tsc's job | bun check |
| Permissions | everything, unless --permission | everything |
| Built in | test runner, watch mode | package manager, bundler, test runner |
and so do the package managers:
| Manager | Comes with | Lockfile | Packages on disk |
|---|---|---|---|
| npm 12 | Node | package-lock.json | flat node_modules |
| pnpm 12 | separately | pnpm-lock.yaml | node_modules of links into a store |
| Yarn 4 | separately | yarn.lock | no node_modules by default, a loader file instead |
| Bun | Bun | bun.lock | flat node_modules |
The versions are from October 2026: Node 24 and 26, Bun 1.4, npm 12, pnpm 12, rewritten in Rust that year, and Yarn 4. Each claims to be fastest at something, and each benchmark measures one thing under one setup. Choose for what your code needs, and measure your own code if speed decides it.
One repository, many packages
4.1 Why repositories grow
A library rarely stays alone. async-kit has an app that uses it, and each lives in a folder of its own, with its own install. To try a change to the library in the app, you build the library, publish a version and install that version in the app. Every experiment costs a version number, so most changes skip the loop: they are published, and the app finds out later.
A monorepo keeps several packages in one repository instead. Vue, Vite, React and Babel all work this way. What it gives you:
- One change, one commit, across a library and the code that uses it, reviewed and tested together.
- No publishing to try a change. The app uses the library as it is on disk right now.
- One install, one lockfile, one set of tools.
What it costs: every command has to say which package it is for, builds and tests grow with the repository, and every tool has to understand the layout. The package manager's half of that understanding is called workspaces, and every package manager has them. Yarn introduced them in 2017, and npm followed in 2020. The idea is the same in all of them: the root lists which folders are packages, one install at the root installs them all, and a package that depends on a sibling gets a link to the sibling's folder instead of a copy from the registry.
4.2 Workspaces with npm
npm comes with Node, so it is the one every machine already has. The library and the app move under one root:
kit/
package.json
packages/
async-kit/ the library, 0.2.0
app/ uses async-kit
The root package.json is "private": true, because nobody publishes the repository itself, and lists the packages:
{
"name": "kit",
"private": true,
"workspaces": ["packages/*"]
}
The app depends on the library as it always did, with a range: "async-kit": "^0.2.0". One npm install at the root installs every package's dependencies, writes one package-lock.json, and links the library instead of downloading it:
npm ls async-kit
kit@ /…/kit
+-- app@ -> ./packages/app
| `-- async-kit@0.2.0 deduped -> ./packages/async-kit
`-- async-kit@0.2.0 -> ./packages/async-kit
-> is a link: the app's async-kit is the folder next to it. -w runs a script in one package, and --workspaces runs it in all of them:
npm run build -w async-kit
npm run build -w app
npm start -w app
[ 2, 4, 6 ]
42
npm run build --workspaces doesn't look at which package uses which. With a glob such as packages/*, the order is alphabetical, so on a fresh clone app is built before the library it imports:
main.ts(1,38): error TS2307: Cannot find module 'async-kit' or its corresponding type declarations.
npm then carries on and builds async-kit, so the second run passes, with the first run's dist still on disk. A clean build server fails every time. List the packages in the order they must be built, "workspaces": ["packages/async-kit", "packages/app"], or build the library on its own first.
The link has a condition: npm links a sibling only while its version fits the range. When it doesn't, npm goes to the registry instead, without a warning. Raise the library to 0.3.0 and leave the app at ^0.2.0, and npm installs the 0.2.0 you published earlier, so the app goes on running the old version. The registry may even have an unrelated package with the same name. There is one called async-kit on npm, so an app here that asks for ^2.0.0 gets somebody else's library:
kit@ /…/kit
+-- app@ -> ./packages/app
| `-- async-kit@2.2.4
`-- async-kit@0.2.0 -> ./packages/async-kit
4.3 pnpm's comfier ways
The same repository with pnpm. The list of packages moves into a file of its own, pnpm-workspace.yaml:
packages:
- "packages/*"
and the app names its sibling with the workspace protocol:
"dependencies": {
"async-kit": "workspace:^"
}
workspace: means "the package of that name in this repository, never one from the registry". workspace:^ follows the sibling's version, whatever it is. A range written out, such as workspace:^0.3.0, has to fit, and when it doesn't, the install stops instead of fetching something else:
No matching version found for async-kit@workspace:^0.3.0 inside the workspace.
Available versions: 0.2.0
When a package that depends on a sibling is published, pnpm replaces workspace:^ with a real range, ^0.2.0, because nobody outside the repository could resolve workspace:. Yarn and Bun understand the protocol too. npm doesn't:
npm error code EUNSUPPORTEDPROTOCOL
npm error Unsupported URL Type "workspace:": workspace:^
pnpm install at the root installs everything, and reports what it found:
Scope: all 3 workspace projects
The two packages, and the root. The app's node_modules then holds exactly what its package.json lists:
packages/app/node_modules/
async-kit → ../../async-kit
typescript → ../../../node_modules/.pnpm/typescript@7.0.2/node_modules/typescript
Every package in the repository sees only what it declared. In the npm workspace, async-kit's build tools were hoisted to the root, so the app could import vite without listing it, a phantom dependency inside your own repository. With pnpm, the same import fails, as the phantom ms did.
From the root, -r, recursive, runs a command in every package. In which order?
pnpm -r build # ?
Output
$ vite build
vite v8.3.5 building client environment for production...
…
✓ built in 1.21s
$ tsc
vite build is async-kit's build, and tsc is the app's. -r runs in dependency order: a package after everything it depends on, whatever the folders are called. --filter picks packages instead:
pnpm --filter app start # a script in one package
pnpm --filter async-kit add -D vitest # a dependency for one package
Last, both packages use TypeScript, and two copies of "typescript": "^7.0.2" drift apart the first time somebody updates one of them. A catalog names the version once, in pnpm-workspace.yaml:
catalog:
typescript: ^7.0.2
and every package refers to it as "typescript": "catalog:". Like workspace:, catalog: is replaced by the real range when a package is published.
4.4 Why this course uses pnpm
Any of these managers can run a workspace. This course uses pnpm from here on, for reasons that hold well beyond it:
- It is strict by default. Phantom dependencies fail on the first run, in the repository and in every package in it.
- Workspaces need nothing else: the
workspace:protocol, dependency order, filters and catalogs are all built in. - One store per machine. Each version of each file is on disk once, and an install copies nothing that is already there.
- It is what you will meet. Vue, Vite, Vitest, Nuxt, Svelte and Next.js are all developed in pnpm workspaces.
Install it once, with the npm you already have:
npm install --global --allow-scripts=pnpm pnpm@12
pnpm --version
pnpm's own package has an install script, which puts a native binary for your system in place, so the command allows that one package's script.
Older guides install pnpm with corepack enable instead. Corepack was a tool that came with Node and installed whichever package manager a project named in its package.json. Node stopped shipping it in version 25. It still exists as a package of its own, but npm install --global does the same job.
The commands map almost one to one. npm install becomes pnpm install, npm install zod becomes pnpm add zod, npm run build becomes pnpm build, and npx becomes pnpm exec for a tool the project installed, or pnpm dlx for one it didn't.
4.5 When workspaces aren't enough
With dozens of packages, two more kinds of tool appear:
- Task runners, Turborepo and Nx, run scripts across packages in parallel and cache the results, so a build that already ran with the same inputs isn't run again.
- Release tools version and publish each package on its own. Changesets has the author add a small file with each change, naming the packages it affects and whether it is a patch, minor or major change, and turns those files into version numbers and changelogs at release time.
For a library and an app, the package manager's workspaces are all you need.
Choosing, and writing it down
5.1 Which Node
Node has released a new major version every April and October:
- Even-numbered versions become LTS, long-term support, in October: about 30 months of fixes, first active, then maintenance with security fixes only.
- Odd-numbered versions live for about eight months, and few projects use them.
| Version | In October 2026 | End of life |
|---|---|---|
| 22 | maintenance | April 2027 |
| 24 | active LTS, maintenance from 20 October | April 2028 |
| 26 | current, active LTS from 28 October | April 2029 |
Node 26 is the last line on that schedule. From Node 27, in April 2027, there will be one major version a year. Every one becomes LTS, and there are no odd versions any more. An alpha channel, from October to March, takes over what odd versions were for: trying what is coming.
A version manager switches between installed versions: nvm or fnm, which read an .nvmrc file in the project with the version in it.
5.2 One project, one manager
Each manager reads only its own lockfile. A project where one person runs npm install and another pnpm install ends up with two lockfiles that disagree, and two developers with different node_modules, the very problem lockfiles were invented to solve. A project chooses one manager, and one range of Node versions, and says so, in devEngines in package.json:
"devEngines": {
"packageManager": { "name": "pnpm", "version": "^12.0.0", "onFail": "error" },
"runtime": { "name": "node", "version": ">=24", "onFail": "error" }
}
Both npm and pnpm read it. npm install in that project:
npm error code EBADDEVENGINES
npm error EBADDEVENGINES The developer of this package has specified the following through devEngines
npm error EBADDEVENGINES Invalid devEngines.packageManager
npm error EBADDEVENGINES Invalid name "pnpm" does not match "npm" for "packageManager"
and pnpm, with a Node older than the runtime entry allows:
Error: ERR_PNPM_BAD_RUNTIME_VERSION
× This project requires Node.js >=30. Your current Node.js is v24.21.0
You will also see an older field, "packageManager": "pnpm@12.14.0", which was Corepack's. pnpm still reads it, and refuses to work in a project that names another manager. npm 12 ignores it.
To move an existing project from npm to pnpm, pnpm import writes pnpm-lock.yaml from the package-lock.json, with the same versions. Delete package-lock.json in the same commit.
Take these 5 things from here
- A runtime is an engine plus everything else: event loop, input and output, modules, globals and tools. Node set the defaults, Deno and Bun answered its mistakes, and Node took back the answers that worked.
- Detect features, not runtimes.
typeof processandtypeof windowno longer tell you where your code runs. - Hoisting lets code import what it never declared. pnpm's linked layout gives each package exactly what it lists, and stores each file once per machine.
- Dependencies' install scripts no longer run by default. Allow each one on purpose, and know which file records the decision.
- Every package manager has workspaces. npm runs them in the order they are listed and falls back to the registry. pnpm adds the
workspace:protocol, dependency order and strictness, which is why this course uses it. One project, one manager, written down indevEngines.
You should now be able to
- Explain the difference between an engine and a runtime, and name the engine behind each major runtime.
- Explain which of Node's early decisions shaped the ecosystem, and which ones Deno, pnpm and Bun set out to fix.
- Write code that runs on Node and Bun, and say which APIs it may use.
- Explain why
typeof windowandtypeof processare unreliable ways to detect the runtime. - Restrict what a program may do with Node's
--permission, and explain what it doesn't protect against. - Explain how hoisting creates phantom dependencies, why they break later, and how pnpm prevents them.
- Explain why install scripts are dangerous, and allow one in npm, pnpm and Bun.
- Set up a workspace with npm and with pnpm, and explain what the
workspace:protocol,-rand catalogs add. - Explain why
npm run --workspacescan build a package before its dependency, and how to prevent it. - Say when a monorepo needs a task runner or a release tool.
- Read Node's release schedule, choose a Node version, and pin a project to one package manager and one Node version with
devEngines.
Reading
Required
- 📺 Ryan Dahl, "10 Things I Regret About Node.js" — JSConf EU 2018, about 27 minutes. Node's creator on its design mistakes, and the first look at Deno
- 📺 Zoltan Kochan, "pnpm – a Fast, Disk Space Efficient Package Manager for JavaScript" — DevOps.js Conf 2022, 31 minutes, by pnpm's lead maintainer
- 📺 Fireship, "Monorepos - How the Pros Scale Huge Software Projects" — 2021, short. What monorepos are for, and what Turborepo and Nx add
- pnpm: Motivation and Flat node_modules is not the only way
- npm: Workspaces
- pnpm: Workspace and Catalogs
Recommended
- 📺 Node.js: The Documentary — 2024, about an hour. How Node and npm came to be
- 📺 Bun 1.0 is here — 2023, Bun's launch video
- The New Stack: Cloudflare acquires Deno — October 2026, what happens to Deno and why
- Node.js: Running TypeScript natively
- Node.js: Permissions
- Node.js: Evolving the release schedule and the release table
- ECMA-429: Minimum common web API
- GitHub changelog: npm install-time security — what npm 12 changed about install scripts
- pnpm 11 and pnpm 12 release notes
- Bun: Lifecycle scripts
- Yarn: Install modes
- Turborepo, Nx and Changesets
Demos
- Runtimes —
runtime.mjs,read.mjsandnet.mjs, to run on Node and Bun, and with Node's--permission - Phantom dependency — the same project installed with npm and with pnpm
- npm workspace and pnpm workspace — async-kit and an app that uses it, in both