Node.js - You Don’t Need a Dependency

A presentation at JSConf in October 2025 in Cambridge, MD 21613, USA by Brian Muenzenmeyer

Slide 1

Slide 1

YOU DON’T NEED A DEPENDENCY BRIAN MUENZENMEYER JSConf, October 2025

Slide 2

Slide 2

PRINCIPAL ENGINEER LEADER, OPEN SOURCE PROGRAM OFFICE @BRIANMUENZENMEYER.COM @BMUENZENMEYER @ME

Slide 3

Slide 3

AUTHOR, APPROACHABLE OPEN SOURCE

Slide 4

Slide 4

MAINTAINER, NODEJS.ORG

Slide 5

Slide 5

Slide 6

Slide 6

📣 DEPENDENCIES ARE GREAT!

Slide 7

Slide 7

Slide 8

Slide 8

CLICK ME

Slide 9

Slide 9

🌱 NEW(ISH) FEATURES Capability testing source code watching source code parsing arguments reading environment styling output glob files run typescript Introduced 16.17.0 16.19.0 18.3.0 20.6.0 20.12.0 22.0.0 22.6.0

Slide 10

Slide 10

🌱 NEW(ISH) FEATURES Capability testing source code watching source code parsing arguments reading environment styling output glob files run typescript Introduced 16.17.0 16.19.0 18.3.0 20.6.0 20.12.0 22.0.0 22.6.0

Slide 11

Slide 11

Jul 2025 Main Oct 2025 MAINTENANCE Node.js 22 ACTIVE Node.js 24 CURRENT Node.js 26 Apr 2026 Jul 2026 Oct 2026 Jan 2027 Apr 2027 UNSTABLE Node.js 20 Node.js 25 Jan 2026 MAINTENANCE ACTIVE MAINTENANCE CURRENT CURRENT ACTIVE

Slide 12

Slide 12

These releases potentially replace an external dependency in your project. Capability testing source code watching source code parsing arguments reading environment styling output glob files run typescript Dependency Replaced jest, ava, ts-jest nodemon commander, yargs dotenv colors, chalk glob, globby, fast-glob ts-node, tsx

Slide 13

Slide 13

🥼 SAMPLE PROJECT

Slide 14

Slide 14

Slide 15

Slide 15

Slide 16

Slide 16

BEFORE { “dependencies”: { “chalk”: “5.6.0”, “commander”: “14.0.0”, “dotenv”: “17.2.2”, “glob”: “11.0.3” }, “devDependencies”: { “@types/jest”: “30.0.0”, “jest”: “30.1.3”, “nodemon”: “3.1.10”, “ts-jest”: “29.4.1”, “typescript”: “5.9.2” }, “scripts”: { “build”: “tsc src/parser.ts —outDir src —target ES2022 -“dev”: “nodemon watch src/cli js watch src/parser ts Core business logic by GitHub + Claude:

Slide 17

Slide 17

🧪 TESTING SOURCE CODE 16.17.0 (August 2022) Tests of all kinds build confidence in release.

Slide 18

Slide 18

🃏 For many projects, I’d turn to jest to test my code.

Slide 19

Slide 19

🃏 For many projects, I’d turn to jest to test my code. It’s been the default for so long, is part of the OpenJS Foundation, and enjoys a large ecosystem of tools and attention, making it hard to argue against.

Slide 20

Slide 20

We can test our SBOMParser class with this test: import { SBOMParser } from “./parser.js” describe(“SBOMParser”, () => { test(“should throw error for invalid JSON”, () => { expect(() => { SBOMParser.parseSBOM(“invalid json”) }).toThrow(“Failed to parse SBOM JSON”) }) })

Slide 21

Slide 21

And then run it with: “test”: “node —experimental-vm-modules node_modules/jest/bin/j “test:watch”: “pnpm test — —watch”

Slide 22

Slide 22

And then run it with: “test”: “node —experimental-vm-modules node_modules/jest/bin/j “test:watch”: “pnpm test — —watch” Already there’s trouble brewing…

Slide 23

Slide 23

Node.js now includes a built-in test runner, node —test -“test”: “node -“test”: “pnpm +”test”: “node +”test:watch”: —experimental-vm-modules node_modules/jest/bin/ test:jest —watch”, —test”, “node —test —watch”,

Slide 24

Slide 24

Here’s a test diff: +import assert from “node:assert” +import { test, describe } from “node:test” import { SBOMParser } from “./parser.js” describe(“SBOMParser”, () => { test(“should throw error for invalid JSON”, () => { - expect(() => { + assert.throws(() => { SBOMParser.parseSBOM(“invalid json”) - }).toThrow(“Failed to parse SBOM JSON”) + }, /Failed to parse SBOM JSON/) }) })

Slide 25

Slide 25

🏌️ I’m not interested in the code golf here, but it is worth emphasizing two things:

Slide 26

Slide 26

  1. Jest’s support for ESM is still evolving (pinned issue since 2020), not yet with a polished developer experience.

Slide 27

Slide 27

  1. Jest’s support for ESM is still evolving (pinned issue since 2020), not yet with a polished developer experience.

Slide 28

Slide 28

  1. Jest’s support for ESM is still evolving (pinned issue since 2020), not yet with a polished developer experience. 2. Jest is slower, even with one suite. Benchmarking via time pnpm test against both showed the Node.js test runner to be 5 times faster.

Slide 29

Slide 29

👀 WATCHING SOURCE CODE 16.19.0 (December 2022) Tasks re-run when as your source code changes during development.

Slide 30

Slide 30

👀 WATCHING SOURCE CODE 16.19.0 (December 2022) Tasks re-run when as your source code changes during development. Stopping and restarting the server each time is a pain.

Slide 31

Slide 31

Our sample project contains this script in our package.json: “dev”: “nodemon —watch src src/cli.js”

Slide 32

Slide 32

Our sample project contains this script in our package.json: “dev”: “nodemon —watch src src/cli.js” Works great, and has for a long time.

Slide 33

Slide 33

But now, Node.js has built-in arguments —watch and —watch-path. We can replace this with: -“dev”: “nodemon —watch src src/cli.js”, +”dev”: “node —watch-path=src src/cli.js”,

Slide 34

Slide 34

🙈 Not much different at the surface. It works for this use case.

Slide 35

Slide 35

🙈 Not much different at the surface. It works for this use case. Critically, it survives parsing errors in the JavaScript.

Slide 36

Slide 36

💬 PARSING ARGUMENTS 18.13.0 (January 2023) Our CLI should support configurable, runtime usecases.

Slide 37

Slide 37

Slide 38

Slide 38

0️⃣ Node.js supports process.argv since 0.1.27 so what’s the fuss?

Slide 39

Slide 39

0️⃣ Node.js supports process.argv since 0.1.27 so what’s the fuss? Tools like yargs and commander have been the goto for a long time.

Slide 40

Slide 40

Our arguments start parsed with commander: import { Command } from “commander” const program = new Command() program .name(“sbom-parser”) .argument( “[sbom-file]”, “Path to the SBOM JSON file (optional if using —glob)” ) .option(“-f, —format <format>”, “Output format: stdout or js .action(async (sbomFilePath, options) => { … }) await program.parseAsync()

Slide 41

Slide 41

We can do this: import { parseArgs } from “node:util” const parsed = parseArgs({ options: { format: { type: “string”, short: “f”, }, }, allowPositionals: true, }) const { values: options, positionals } = parsed

Slide 42

Slide 42

🎚️ Lops off the two first arguments by default, synchronous by default, and enough for my needs.

Slide 43

Slide 43

🎚️ Lops off the two first arguments by default, synchronous by default, and enough for my needs. Node.js also throws an error for missing or extra params - which is nice - and again, perhaps enough.

Slide 44

Slide 44

🌲 READING ENVIRONMENT 20.6.0 (September 2023) Environment variables provide flexibility and portability to code.

Slide 45

Slide 45

The obvious choice for years was dotenv: import dotenv from “dotenv” dotenv.config() const format = options.format ? options.format : process.env.DEFAULT_FORMAT

Slide 46

Slide 46

But Node.js can do this too! Delete that dotenv import. -import dotenv from “dotenv” -dotenv.config() const format = options.format ? options.format : process.env.DEFAULT_FORMAT

Slide 47

Slide 47

We add this to our package.json dev script: -“dev”: “node —watch-path src src/server.js”, +”dev”: “node —env-file=.env —watch-path src src/server.js”,

Slide 48

Slide 48

🗺️ We get multiple file support with overrides and a familiar enough syntax to dotenv files.

Slide 49

Slide 49

🗺️ We get multiple file support with overrides and a familiar enough syntax to dotenv files. Node.js can also error or gracefully handle missing env files, the choice is yours.

Slide 50

Slide 50

🖌️ STYLING OUTPUT 20.12.0 (March 2024) Terminal output styling can improve UX.

Slide 51

Slide 51

The ubiquitous module chalk suffices: import chalk from ‘chalk’ … lines.push( Total packages: ${chalk.green.bold(summary.totalPackages)} )

Slide 52

Slide 52

But look at this native Node.js code: -import chalk from ‘chalk’ +import { styleText } from ‘node:util’ … lines.push( - Total packages: ${chalk.green.bold(summary.totalPackages)} +Total packages: ${styleText([“green”, “bold”], summary.total );

Slide 53

Slide 53

🎨 Rudimentary support for terminal color detection and environment variable overrides.

Slide 54

Slide 54

⭐️ GLOB FILES 22.0.0 (April 2024) Operating on groups of files is a common capability.

Slide 55

Slide 55

Plenty of ways to do this. import { glob } from “glob” … const files = await glob(options.glob) Lots of tools race to be fastest.

Slide 56

Slide 56

Another Node.js internal: - import { glob } from “glob” + import { glob } from “node:fs/promises” … - const files = await glob(options.glob) + const files = await Array.fromAsync(glob(options.glob))

Slide 57

Slide 57

🎮 CHECKPOINT Nice, we got through quite a bit. But it’s always smart to save before the big boss fight.

Slide 58

Slide 58

❓ What’s left?

Slide 59

Slide 59

❓ What’s left? TypeScript.

Slide 60

Slide 60

🏗️ TYPESCRIPT 22.6.0 (August 2024) Native TypeScript support was consistent community ask and wedge issue.

Slide 61

Slide 61

Here’s a tiny snippet of our parser class: static filterByLicense(summary, licenseFilter) { return summary.dependencies.filter((dep) => dep.license.toLowerCase().includes( licenseFilter.toLowerCase()) ); }

Slide 62

Slide 62

Let’s add some types to match the SBOM schema export interface SBOMDependency { name: string version: string license: string packageManager: string copyright?: string spdxId: string downloadLocation: string } export interface SBOMSummary { totalPackages: number dependencies: SBOMDependency[] licenses: Record<string, number> packageManagers: Record<string, number> }

Slide 63

Slide 63

static filterByLicense( summary: SBOMSummary, licenseFilter: string ): SBOMSummary { return summary.dependencies.filter((dep) => dep.license.toLowerCase().includes( licenseFilter.toLowerCase() ); }

Slide 64

Slide 64

As of v23.6.0 in January… THIS JUST RUNS!

Slide 65

Slide 65

We still get feedback in editor.

Slide 66

Slide 66

🤓 REPLY GUY: WELL ACTUALLY… you didn’t build the project at all, it only removed the typings, flow-style. Sad!

Slide 67

Slide 67

🤓 REPLY GUY: WELL ACTUALLY… you didn’t build the project at all, it only removed the typings, flow-style. Sad! But wait, I say, my editor gave me immediate feedback of the error, without needing a build process at all.

Slide 68

Slide 68

SOME NOTES no type-stripping under node_modules

Slide 69

Slide 69

SOME NOTES no type-stripping under node_modules (perhaps ever)

Slide 70

Slide 70

SOME NOTES no type-stripping under node_modules (perhaps ever) —experimental-transform-types

Slide 71

Slide 71

SOME NOTES no type-stripping under node_modules (perhaps ever) —experimental-transform-types —erasableSyntaxOnly for TS@5.8

Slide 72

Slide 72

HOPE OF 2024…

Slide 73

Slide 73

…NOW CONFIRMED

Slide 74

Slide 74

Slide 75

Slide 75

🛑 STOP Let’s not go deeper today. I’ll spare you the ESM + TypeScript + Jest headache. 👉

Slide 76

Slide 76

AFTER { “dependencies”: { }, “devDependencies”: { “@types/node”: “^24.5.1”, “typescript”: “^5.9.2” }, “scripts”: { “dev”: “node —env-file=.env —watch-path=src src/cli.js”, “start”: “node —env-file=.env src/cli.js”, “test”: “node —test”, “test:watch”: “node —test —watch” } }

Slide 77

Slide 77

Slide 78

Slide 78

⚖️ COMPARISONS This ain’t your entire app…

Slide 79

Slide 79

⚖️ COMPARISONS This ain’t your entire app… …and we can all caveat this with enough asterisks to call in Legal.

Slide 80

Slide 80

BUT NUMBERS ARE NUMBERS Metric Before After Delta # node_modules 393 4 1%, or 98 times smaller size node_modules 75 MB 26 MB 35%, or 2.5 times smaller

Slide 81

Slide 81

TWO ORDERS OF MAGNITUDE LESS DEPENDENCIES.

Slide 82

Slide 82

TWO ORDERS OF MAGNITUDE LESS DEPENDENCIES.

Slide 83

Slide 83

DEPENDABOT WILL BE BORED.

Slide 84

Slide 84

LESS IS MORE LESS And, these direct dependencies, over the past 12 months, have had 35 releases.

Slide 85

Slide 85

COMPARISON COMPLEMENT

Slide 86

Slide 86

☀️ PACE LAYERS

Slide 87

Slide 87

“FAST LEARNS; SLOW REMEMBERS.” — Stewart Brand

Slide 88

Slide 88

SYM • MATHESY : TOGETHER, LEARN An entity composed by transcontextual mutual learning through interaction — Nora Bateson

Slide 89

Slide 89

Slide 90

Slide 90

🔥 CHURN IS PACE LAYERS IN MOTION.

Slide 91

Slide 91

WE CAN CELEBRATE: innovation can be quick / creators have agency to explore competition puts pressure on established systems to improve maintainers craving momentum and stability have space and time to cultivate layers exist for anyone to contribute within their means

Slide 92

Slide 92

FIND YOUR LAYER. There’s room for all of us.

Slide 93

Slide 93

THANKS! GRAB A COPY NOW @BRIANMUENZENMEYER.COM @BMUENZENMEYER @ME