My Road to NPM Fame
"In JavaScript, node_modules are like assholes - everyone's got them, and they're all inherited from their parents' parents' parents' parents' parents'..." ~Gandhi (probably)
Node modules are…
...project directories containing code from external NPM packages that are installed into JavaScript projects. These packages encapsulate pre-built functionality, saving developers from having to write that code from scratch. They're the building blocks that make modern JavaScript development possible—and occasionally nightmarish.
While incredibly helpful, the modern JavaScript ecosystem can often be lovingly memed with:
But this article isn’t really…
…about node_modules or NPM packages as a concept.
This article is about my NPM package. The very bestest package of them all.
🤩 npx create-electron-foundation 🤩
So a few weeks ago I…
...started researching local-first development
...which took a detour into researching native applications
...which turned into researching Electron
I was also generally frustrated that I didn't have a good process to quickly scaffold an MVP in any language.
...which culminated in me taking inspiration from Theo's create-t3-app and deciding to figure out how I could build and distribute an NPM package that encapsulates a Command Line Interface (CLI) for quickly scaffolding Electron applications!
Well if its just…
...a few configurations, how hard could it be?
Lucky for me, create-t3-app existed as a perfect blueprint—I could focus on the Electron-specific bits instead of reinventing the CLI tooling.
The goal for V1 was to include the following configuration options:
Routing Options:
Tanstack Router
(default) |React Router
Styling Options:
Tailwind CSS
(default) |Vanilla CSS
Data Layer:
SQLite
(optional) |Drizzle ORM
(optional, but required if database selected)
While trying to remain flexible I did bake in some opinionated packages for functionality across configurations:
electron-rebuild
andelectron-log
for the Electron-specific stuffTanstack Query
&Tanstack Form
because Tanner writes great software and is an inspiring personDay.js
for dates (because RIPMoment
— my first love as a jr. dev)NanoID
because I just learned about it...
Is that a Symlink…
...in your pocket or are you just excited to locally test your NPM package? 😉
To help prevent premature registration, you can use a symbolic link to test your package locally. A symbolic link sounds fancy but it's just a file system reference that points from one location on your computer to another. This lets you test your package as if it were installed globally without actually publishing to the NPM registry.
npm link
For CLI packages, npm link
reads the bin
field from your package.json
and creates a global symlink to your executable file.
Pretty neat! 👍
At its core…
...there are only 4 main processes within the CLI:
Process: any CLI arguments and update the initial configuration so we can skip these prompts in step 2.
Prompt: the user for their remaining scaffold configuration.
Scaffold: the base Electron application with all files that are not directly affected by the users configuration options.
Inject: configuration specific code & dependencies into the scaffolded project.
Of course there are other steps surrounding these pillars but the core of this magical NPM package really is just four steps.
Simple! 👌
But of course…
…there were other demons to exorcise on this voyage. Publishing wasn't the real villain here, but the combinatorial chaos of supporting the different configuration options, combined with new quirks of Electron that I am learning as I go. 🧠🚀
To build a file builder or…
…just exponentially duplicate & customize conflicted files.
Here's the combinatorics challenge: with 2 router options, 2 styling options, and optional database/ORM combinations, you get 8 distinct templates:
Tanstack Router
|Tailwind
Tanstack Router
|Tailwind
|SQLite
|Drizzle
Tanstack Router
|Vanilla CSS
Tanstack Router
|Vanilla CSS
|SQLite
|Drizzle
React Router
|Tailwind
React Router
|Tailwind
|SQLite
|Drizzle
React Router
|Vanilla CSS
React Router
|Vanilla CSS
|SQLite
|Drizzle
Do I build a smart templating system that dynamically injects code? Or just create 8 starter templates and copy the right one?
The lazy developer whispered:
"...mkdir...mkdir…copy…paste...so simple...so fast…mkdir…mkdir…"
The perfectionist replied:
Ah, but consider this: one must architect a sophisticated, enterprise-grade … … … replete with comprehensive … … … immutable configuration schemas, and naturally … … … we should be implementing a robust abstraction layer … … … leveraging the latest design patterns and … … … the entire solution must be containerized, version-controlled with proper … … … CI/CD pipelines and blue-green … … … Anything less would be: pedestrian.
Guess who won? 😅
For structured files like package.json
, I inject content directly. For most of the other files? Copy-paste-and-customize. For now…
This will be one of the first areas I look to improve as I consider adding more customization options.
Hey Fellow Kids...
...have you heard of CSS?
Something I (re?)discovered way too late: I could solve a lot of my styling complexity by extracting shared styles into actual CSS classes — thus shrinking the styling complexity down to 2 CSS files: 1 for Tailwind and 1 for Vanilla CSS.
This exposed some serious Tailwind tunnel vision on my part. I've gotten into the habit of jamming everything inline and only "extracting" styles when I extract entire components. Simply deleting dozens of repeated Tailwind classes—replacing them with proper CSS class names—was genuinely eye-opening.
But here's the thing: I know these fundamentals. They're basically the first things I learned coding raw HTML/JS/CSS back in the day.
...am I old now?
Now what the f*ck…
…is a native binary?
Some NPM packages (like better-sqlite3
) aren't just JavaScript—they're C++ addons that get compiled into .node
files. These are called native modules and they're basically little binary executables that Node.js
can talk to.
The problem? Electron has a different Application Binary Interface (ABI)
from a given Node.js
binary (🖕) which is almost certainly different from whatever Node.js
version you have installed on your machine (🖕🖕).
These .node
files are extremely picky about matching the exact Node.js
version they were compiled against.
So when you npm install better-sqlite3
on your machine, it compiles against your Node.js
. Then when you try to run it in Electron, which uses its own Node.js
... boom. ABI
mismatch. Your app crashes with some cryptic error about modules being "compiled against a different Node.js version".
ME:
hey <insert AI model> can you help me with this issue <extensive context / details / documentation / notes>?AI:
ah you seem to be encountering a common issue when working with native modules in Electron...ME
: please create a plan for my approval outlining how you would update my code to fix this "common" issue given our contextAI:
<responds with numerous radically different solutions that each fail in new and interesting ways>ME:
WELL IF IT'S SO F*CKING COMMON WHY DON'T YOU PROVIDE ME THE WIDELY KNOWN SOLUTION FOR THIS MOTHER F*CKING WASTE OF BITS KNOWN AS NATIVE MODULESAI:
...we have contacted the authorities...
The solution is electron-rebuild
, which basically recompiles these native modules specifically for Electron's Node.js
version:
npx electron-rebuild -f -w better-sqlite3
But wait, there's more! When you package your Electron app for distribution, it usually gets bundled into an asar
archive (think of it like a zip file). The problem is that .node
files cannot be loaded from inside an asar
archive—they need to be actual files on the filesystem. To fix this, your packager configuration needs to "unpack" these native modules.
The moral of the story? Native modules in Electron are a special kind of hell. They work great once you understand the dance, but getting there involves a lot of head-banging-against-desk moments.
Lights… Camera…
…existential dread — it's time to publish — the cameras are rolling! Smile 🤪
Pack
npm pack
Reads the
package.json
files array and gathers all the files that would be included if you were to publish the package to the npm registry.Creates a tarbell
(.tgz)
file which is named<name>-<version>.tgz
based on thepackage.json
values. This is the exact file that would be uploaded to the NPM registry if you actually rannpm publish
Login
npm login
Authorize with NPM so you can plant your digital flag 🇺🇸 …further corrupting the NPM namespace…
Dry Run
npm publish —dry-run
A safety measure that allows you to see what would happen if you actually published your package, without making any changes to the live NPM registry.
Similar to
Pack
it gathers and lists all relevant files but also performs some pre-checks like verifying the package name and versions are valid and thepackage.json
is well formedPublish
npm publish —tag alpha | beta | latest
It’s go time! This will publish your package to the NPM Registry.
By default, the command uses the version from your
package.json
. Passing thealpha
orbeta
tag (unless it's your first publication) will not update thelatest
tag in the registry—useful for testing versions without affecting what people get by default.Use!
npx
is perfect for CLI tools like this—it downloads and runs the package temporarily without permanently installing it on your machine. For packages you want to install into an ongoing project, you'd use `npm install` instead.<npx | npm> <name>
While that’s cool and everything…
…people often choose to automatically tag and publish new versions at the end of their CI/CD pipelines. This is beyond the scope of this article but once you invest the time in setting them up, like most Infrastructure as Code, it helps prevent mistakes & manual bottlenecks.
And that’s how you can…
…vibe code your open sourced NPM package to founder mode fame… or just cultivate a profound awareness of the staggering amount of work that paved the way for your tiny little package and thus expose the true extent of your developer inadequacies. 😀
If your still here…
To play along…
…please review / contribute to the repo 😀
…if you try it and run into errors, or just want to say hello, please reach out 👋
I am still having issues with…
…my own confidence… in the electron-rebuild
flow addressing the native modules issues for better-sqlite3
& Drizzle
configurations. This project, as a scaffolding CLI, was inherently “top heavy” as in I never got “deep” into a project with this setup which is something I would need to do to truly feel comfortable with this given my struggle setting it up.
…my automated testing CI/CD sucks. I need to review my e2e.yml GitHub action that is confirming a successful build but not any configuration specifics of it being the right build given the input configuration.
From here I plan…
…to touch up issues / bugs that arise.
…to start looking into electricSQL and Convex and if I can being them in as configuration options.
…to further research the file builder implementation..
…to rewrite the whole thing in Tiger Style...
Cheers 🎉
~Mr.T