DEV Community

ke jia
ke jia

Posted on

One File, Zero Dependencies: How the Scaffolder Stays Small

Most scaffolders are not small. A typical "create-app" CLI has a dependency tree of 50-200 packages: a prompt library, a template engine, a download manager, a progress bar, a logger, a config parser, and three transitive copies of each.

I built scaffoldx-cli the other way: one file, zero runtime dependencies. Not "a few" — zero. The entire tool, including all nine templates, is a single Node script you can read in one sitting.

This post is about why that constraint is worth imposing on yourself, and what it costs.

Why zero dependencies isn't just aesthetic

1. The attack surface is the dependency tree. Every package you depend on is a package that can ship a malicious update, a yanked version, or a transitive vulnerability you didn't audit. A scaffolder with zero runtime dependencies has a supply-chain surface of exactly one file — the one you can read. For a tool whose job is to be the first thing that touches a new project, that's the property that matters most.

2. The install is the failure point. npx <tool> with 150 transitive dependencies means 150 packages that can fail to resolve, conflict with the host's Node version, or time out on a slow network. A single-file tool with no dependencies downloads in one request and runs on any Node that can run CommonJS. The "it just works on the train" property is a direct consequence of the zero-dependency constraint.

3. The version is stable by construction. A multi-dependency tool's behavior is a function of 150 package versions, all of which can shift under you. A zero-dependency tool's behavior is a function of one file's content and the Node version. "What changed between v1.0.0 and v1.0.1?" has a one-file answer, and the diff is reviewable in minutes.

4. The user can fork the whole tool. If the scaffolder is one file, a user who needs a template tweak can copy the file, edit it, and run their local copy — no build system, no package.json surgery. That's a maintenance property, not a feature: the tool's users become its contributors without a PR process.

What the constraint costs

Honest accounting, because zero dependencies isn't free:

1. You write the boring parts yourself. There's no prompts library for the interactive menu — it's readline and a loop. There's no ora spinner — it's a process.stdout.write with a frame. There's no template engine — it's string replacement. Each of these is 20-50 lines I wrote instead of imported. The total "hand-rolled" code is maybe 300 lines. That's the price.

2. You give up polish. The interactive menu is functional, not beautiful. There's no fancy progress bar, no colored error hierarchy, no graceful handling of every edge case a mature library handles. For a scaffolder — a tool you run for ten seconds and never see again — that's an acceptable trade. For a tool users live in, it wouldn't be.

3. Some things are genuinely harder. A download step (fetching a template from a remote) is trivial with a library and fiddly with raw https. I sidestepped it by making the templates inline — they're strings in the file, not remote resources. That's a design decision the zero-dependency constraint forced, and it's a good one (offline-capable, no remote to go stale), but I made it because of the constraint, not despite it.

The actual shape of the tool

For the curious, the file is structured as:

  1. Template definitions — nine objects, each a name, a description, and a map of filename → content. The contents are the templates themselves, with {{name}} placeholders.
  2. The menu — readline-based, numbered list, one prompt for the choice, one for the project name.
  3. The writer — expand placeholders, mkdir -p the directories, write the files.
  4. The git step — git init, write the .gitignore (template-appropriate), first commit.
  5. The summary — print what was created and the next command.

The whole thing is under 500 lines including the templates. The "engine" is maybe 100 lines. The rest is content.

The general lesson

The zero-dependency constraint is a design forcing function. It doesn't make the tool better in every dimension — the multi-dependency tool has better polish, more features, a larger community. But it makes the tool's trust properties trivially verifiable: one file, readable, no supply chain, stable, forkable, offline.

For a tool that runs on your machine before anything else in the project exists, those properties are the product. Everything else is features you can add later — if you decide the tool needs them.

The constraint cost me 300 lines of hand-rolled code and some polish. It bought me a tool I can defend in one sentence: "it's one file you can read, and it has nothing to install." That sentence is worth more than any feature list.

npx scaffoldx-cli
Enter fullscreen mode Exit fullscreen mode

More Tools

Tool What it does Command
scaffoldx-cli Production-ready project templates in seconds npx scaffoldx-cli
dotguard Scan .env files for exposed secrets npx @wuchunjie/dotguard
gitpulse Git repo analytics in your terminal npx @wuchunjie/gitpulse
snippetx Terminal code snippet manager npx @wuchunjie/snippetx

If these save you time, consider buying me a coffee. All tools are MIT-licensed, zero-dependency, and run fully offline.

Top comments (0)