Adding an npm Library
This is an Electron app with three separate build targets (main, preload, renderer) and a few non-obvious bundling rules. Adding a dependency the wrong way silently breaks CSS, native modules, or the renderer bundle. Follow this guide every time.
Golden Rule: install the latest, caret-pinned
Always install the newest published version. Never hand-edit package.json to set or downgrade a version.
npm install <pkg>@latest # runtime dependency
npm install -D <pkg>@latest # dev / build-only tool (types, bundlers, test libs)
- This records a caret range
^x.y.z — the repo convention (every dep in package.json uses ^). Leave it as a caret range.
package-lock.json is committed and gets updated by the install. Both package.json and package-lock.json are part of your change — stage both.
- Use npm only (the lockfile is
package-lock.json — not yarn, not pnpm).
- Node v24 is pinned in
.nvmrc; engines.node is >=18. Run nvm use first if needed.
Step 1 — Decide which build target consumes the package
This determines every gotcha that follows.
| Target |
Source dirs |
How it's built |
What's allowed |
| Renderer |
src/renderer/** |
Bundled by esbuild into one IIFE (build:renderer) |
Plain JS/TS deps only. No Node built-ins, no native modules. |
| Main |
src/main/**, src/shared/** |
tsc → CommonJS (dist/main/) |
Any Node dep, including native modules. Resolved via require at runtime against node_modules — not bundled. |
| Preload |
src/preload/**, src/shared/** |
tsc → CommonJS (dist/preload/) |
Runs in Node/Electron context; same rules as main. |
- A pure JS/TS library used in the UI (like
marked, dompurify, gridstack, @xterm/*) just gets imported in renderer code and esbuild bundles it. Nothing else to do (except CSS — see Step 2).
- A library that touches the filesystem, spawns processes, or has a
.node binary belongs in main/preload only.
Step 2 — CSS / static assets gotcha (esbuild has NO CSS loader)
esbuild only has the .ts loader configured. It will not bundle any CSS the package ships. If the library needs a stylesheet to work (the way gridstack and @xterm/xterm do):
- Add a copy step in
scripts/copy-assets.js — copy from node_modules/<pkg>/.../file.css to dist/renderer/vendor/<file>.css. Mirror the existing gridstack precedent in that file.
- Add a
<link rel="stylesheet" href="vendor/<file>.css"> to src/renderer/index.html (gridstack/xterm links are already there as examples).
If you skip this, the JS bundles fine but the component renders unstyled. (CLAUDE.md documents this: "esbuild has no CSS loader" — gridstack CSS is copied manually.)
Step 3 — Native module gotcha (.node binaries)
Examples already in the repo: better-sqlite3, node-pty.
- Must be a regular
dependency (never imported from the renderer — main/preload only).
npm install triggers the postinstall hook → electron-builder install-app-deps, which rebuilds the .node binary against the pinned Electron ABI. If the module fails to load at runtime ("NODE_MODULE_VERSION mismatch"), re-run npm install and check the postinstall output.
- If the binary can't load from inside the asar archive, add the package to electron-builder's
asarUnpack in package.json (precedent: **/node_modules/better-sqlite3/**).
- Native modules must compile on macOS, Linux, and Windows — CI builds all three. Flag the cross-platform risk to the user; see
src/main/platform.ts for the platform-detection helpers.
Step 4 — Types
- If the package ships its own type declarations, you're done.
- Otherwise add the community types as a devDependency:
npm install -D @types/<pkg>@latest (precedent: @types/better-sqlite3, @types/dompurify, @types/picomatch).
Step 5 — Verify (required, not optional)
There is no hot reload — every change needs a rebuild.
npm run build # must pass: tsc main + tsc preload + esbuild renderer + copy-assets
npm test # Vitest suite
Then confirm the dependency actually works end to end:
- Renderer / UI / CSS dep:
npm start, and visually confirm the feature renders and styles load.
- Native / main dep: launch the app and exercise the feature that uses it.
Step 6 — Packaging sanity check
electron-builder packages only dist/main/**, dist/preload/**, dist/renderer/**:
- Renderer deps are safe — they're bundled into
dist/renderer/index.js.
- Main/preload deps resolve from the packaged production
node_modules at runtime, so they must be in dependencies (not devDependencies). Keep build-only tooling (bundlers, types, test libs) in devDependencies.
Do / Don't
Do:
npm install <pkg>@latest, leave the caret range, commit package.json + package-lock.json.
- Pick the right target (renderer vs main/preload) before importing.
- Add the CSS copy step +
<link> for any styled renderer library.
- Run
npm run build and npm test before declaring done.
Don't:
- Pin an exact version or hand-edit a version in
package.json.
- Import Node built-ins or native modules from the renderer.
- Forget the
scripts/copy-assets.js step for CSS (esbuild won't bundle it).
- Put a native module in
devDependencies or assume it works without the postinstall rebuild.
$ARGUMENTS
1---2name: add-npm-dependency3description: This skill MUST be used whenever the task involves adding, installing, or upgrading an npm package/library/dependency in this project. Use when the user asks to "add a library", "install <package>", "use <package>", "add a dependency", "bump/upgrade a package", or any change to package.json dependencies. Covers the latest-version policy, esbuild renderer bundling, manual CSS copy, native module rebuilds, and the three build targets.4---56# Adding an npm Library78This is an Electron app with **three separate build targets** (main, preload, renderer) and a few non-obvious bundling rules. Adding a dependency the wrong way silently breaks CSS, native modules, or the renderer bundle. Follow this guide every time.910## Golden Rule: install the latest, caret-pinned1112**Always install the newest published version. Never hand-edit `package.json` to set or downgrade a version.**1314```bash15npm install <pkg>@latest # runtime dependency16npm install -D <pkg>@latest # dev / build-only tool (types, bundlers, test libs)17```1819- This records a caret range `^x.y.z` — the repo convention (every dep in `package.json` uses `^`). Leave it as a caret range.20- `package-lock.json` is committed and gets updated by the install. **Both `package.json` and `package-lock.json` are part of your change** — stage both.21- Use **npm only** (the lockfile is `package-lock.json` — not yarn, not pnpm).22- Node **v24** is pinned in `.nvmrc`; `engines.node` is `>=18`. Run `nvm use` first if needed.2324## Step 1 — Decide which build target consumes the package2526This determines every gotcha that follows.2728| Target | Source dirs | How it's built | What's allowed |29|--------|-------------|----------------|----------------|30| **Renderer** | `src/renderer/**` | Bundled by **esbuild** into one IIFE (`build:renderer`) | Plain JS/TS deps only. **No Node built-ins, no native modules.** |31| **Main** | `src/main/**`, `src/shared/**` | `tsc` → CommonJS (`dist/main/`) | Any Node dep, including native modules. Resolved via `require` at runtime against `node_modules` — **not bundled**. |32| **Preload** | `src/preload/**`, `src/shared/**` | `tsc` → CommonJS (`dist/preload/`) | Runs in Node/Electron context; same rules as main. |3334- A pure JS/TS library used in the UI (like `marked`, `dompurify`, `gridstack`, `@xterm/*`) just gets `import`ed in renderer code and esbuild bundles it. Nothing else to do (except CSS — see Step 2).35- A library that touches the filesystem, spawns processes, or has a `.node` binary belongs in **main/preload only**.3637## Step 2 — CSS / static assets gotcha (esbuild has NO CSS loader)3839esbuild only has the `.ts` loader configured. **It will not bundle any CSS the package ships.** If the library needs a stylesheet to work (the way `gridstack` and `@xterm/xterm` do):40411. Add a copy step in `scripts/copy-assets.js` — copy from `node_modules/<pkg>/.../file.css` to `dist/renderer/vendor/<file>.css`. Mirror the existing **gridstack** precedent in that file.422. Add a `<link rel="stylesheet" href="vendor/<file>.css">` to `src/renderer/index.html` (gridstack/xterm links are already there as examples).4344If you skip this, the JS bundles fine but the component renders unstyled. (CLAUDE.md documents this: "esbuild has no CSS loader" — gridstack CSS is copied manually.)4546## Step 3 — Native module gotcha (`.node` binaries)4748Examples already in the repo: `better-sqlite3`, `node-pty`.4950- Must be a regular `dependency` (never imported from the renderer — main/preload only).51- `npm install` triggers the `postinstall` hook → `electron-builder install-app-deps`, which rebuilds the `.node` binary against the **pinned Electron ABI**. If the module fails to load at runtime ("NODE_MODULE_VERSION mismatch"), re-run `npm install` and check the postinstall output.52- If the binary can't load from inside the asar archive, add the package to electron-builder's `asarUnpack` in `package.json` (precedent: `**/node_modules/better-sqlite3/**`).53- Native modules must compile on **macOS, Linux, and Windows** — CI builds all three. Flag the cross-platform risk to the user; see `src/main/platform.ts` for the platform-detection helpers.5455## Step 4 — Types5657- If the package ships its own type declarations, you're done.58- Otherwise add the community types as a devDependency: `npm install -D @types/<pkg>@latest` (precedent: `@types/better-sqlite3`, `@types/dompurify`, `@types/picomatch`).5960## Step 5 — Verify (required, not optional)6162There is **no hot reload** — every change needs a rebuild.6364```bash65npm run build # must pass: tsc main + tsc preload + esbuild renderer + copy-assets66npm test # Vitest suite67```6869Then confirm the dependency actually works end to end:70- **Renderer / UI / CSS dep:** `npm start`, and visually confirm the feature renders and styles load.71- **Native / main dep:** launch the app and exercise the feature that uses it.7273## Step 6 — Packaging sanity check7475electron-builder packages only `dist/main/**`, `dist/preload/**`, `dist/renderer/**`:76- Renderer deps are safe — they're bundled into `dist/renderer/index.js`.77- Main/preload deps resolve from the packaged production `node_modules` at runtime, so they must be in `dependencies` (not `devDependencies`). Keep build-only tooling (bundlers, types, test libs) in `devDependencies`.7879## Do / Don't8081**Do:**82- `npm install <pkg>@latest`, leave the caret range, commit `package.json` + `package-lock.json`.83- Pick the right target (renderer vs main/preload) before importing.84- Add the CSS copy step + `<link>` for any styled renderer library.85- Run `npm run build` and `npm test` before declaring done.8687**Don't:**88- Pin an exact version or hand-edit a version in `package.json`.89- Import Node built-ins or native modules from the renderer.90- Forget the `scripts/copy-assets.js` step for CSS (esbuild won't bundle it).91- Put a native module in `devDependencies` or assume it works without the postinstall rebuild.9293$ARGUMENTS