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

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

AUTHOR, APPROACHABLE OPEN SOURCE

MAINTAINER, NODEJS.ORG

📣 DEPENDENCIES ARE GREAT!

CLICK ME

🌱 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

🌱 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

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

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

🥼 SAMPLE PROJECT

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:

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

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

🃏 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.

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”) }) })

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

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…

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”,

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/) }) })

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

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

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

  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.

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

👀 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.

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

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.

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”,

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

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

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

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

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.

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()

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

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

🎚️ 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.

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

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

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

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”,

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

🗺️ 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.

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

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

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 );

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

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

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

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))

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

❓ What’s left?

❓ What’s left? TypeScript.

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

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

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> }

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

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

We still get feedback in editor.

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

🤓 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.

SOME NOTES no type-stripping under node_modules

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

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

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

HOPE OF 2024…

…NOW CONFIRMED

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

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” } }

⚖️ COMPARISONS This ain’t your entire app…

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

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

TWO ORDERS OF MAGNITUDE LESS DEPENDENCIES.

TWO ORDERS OF MAGNITUDE LESS DEPENDENCIES.

DEPENDABOT WILL BE BORED.

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

COMPARISON COMPLEMENT

☀️ PACE LAYERS

“FAST LEARNS; SLOW REMEMBERS.” — Stewart Brand

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

🔥 CHURN IS PACE LAYERS IN MOTION.

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

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

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