Skip to main content

Build from Source

Clone the repository and build Daintree from source: prerequisites, the native-module split, the script surface, output directories, and packaging.

Reviewed

Daintree is an Electron app: a Vite-built React renderer, an ESBuild-bundled main process, and a handful of native modules. Building it is an ordinary npm install followed by npm run dev. The parts that trip people up are the pinned Node version and the native rebuild, so both get their own sections below.

Prerequisites

  • Git 2.30 or newer.
  • Node.js 22.13.0. The version is pinned identically in .nvmrc, .node-version, and the engines.node floor in package.json. A CI check (npm run check:node-version) fails the build if those three ever drift apart. Use nvm or fnm to match it.
  • npm. Ships with Node. The repo is an npm workspace, so npm is the supported package manager.
  • A C/C++ toolchain, to compile node-pty against Electron's ABI.

Daintree runs on Electron 42 (Chromium 148). You don't install Electron yourself; it comes down as a devDependency.

Platform build tools

Note
The Daintree repository doesn't declare toolchain prerequisites anywhere in its own docs, so the commands below are the conventional ones for building native Node addons on each platform rather than a quote from the project. If npm install succeeds without them, you don't need them.

Install the Xcode Command Line Tools:

xcode-select --install

Clone & Install

git clone https://github.com/daintreehq/daintree.git
cd daintree
npm install

Use npm install for day-to-day development and npm ci for clean or CI builds. Either is fine; the difference is whether the lockfile is treated as authoritative.

The install also builds the workspace packages under packages/ when you ask for them: @daintreehq/plugin-sdk, plugin-testing, plugin-vite, the daintree-plugin CLI, and create-daintree-plugin. Build them explicitly with npm run packages:build. That's why npm install at the repo root does more work than a single-package project would.

Native Modules

This is the one part of the build that isn't obvious, and it's the most common cause of a failed first install.

The postinstall script (also reachable as npm run rebuild) rebuilds exactly three modules against the Electron runtime:

  • node-pty: the pseudo-terminal binding every terminal panel sits on.
  • win-job-object: Windows process-tree teardown.
  • posix-pty-reaper: the macOS and Linux equivalent.

It then runs node-pty's own post-install step, which fetches the ConPTY assets on Windows and exits cleanly everywhere else. That step always runs, even if one of the rebuilds failed, because it's idempotent and skipping it would leave Windows without a working ConPTY.

better-sqlite3 is deliberately excluded. Since version 13 it is an N-API addon that loads a prebuilt binary shipped inside the package. N-API is ABI-stable, so the same prebuild works under both Node and Electron, and version 13's binding.gyp suppresses source compilation whenever a prebuild is present. Rebuilding it would emit gyp metadata and nothing else. If you're following older instructions that tell you to run an electron-rebuild step for better-sqlite3, delete that step; it no longer does anything.

Windows on arm64

Windows arm64 needs one extra pass, because the default rebuild targets the host architecture:

npm run rebuild:win-arm64

Development

npm run dev

This starts the Vite renderer dev server and the Electron main process together. Renderer changes hot-reload; main-process changes trigger an automatic restart. Two variants are worth knowing:

  • npm run dev:fresh: same thing with DAINTREE_RESET_DATA=1, so the app boots against an empty user-data directory. Useful for testing first-run flows.
  • npm run dev:perf: turns on internal performance capture and writes perf-metrics.ndjson.

The Script Surface

ScriptWhat it does
npm run checkThe full gate chain: typecheck, then every codegen and drift check, then lint ratchet and format check. This is what has to be clean before you push.
npm run fixRuns Prettier and ESLint with autofix.
npm run typecheckTypechecks the renderer, the main process, the preload, and every workspace package. Runs with a 4 GB heap because the project graph is large.
npm run lint / lint:fixESLint over the repo.
npm run format / format:checkPrettier.
npm test / test:watchVitest unit tests. test:integration runs the integration config.
npm run test:e2ePlaywright. Project-scoped variants exist (test:e2e:core, test:e2e:full, and the per-suite full-* projects).
npm run packages:buildBuilds the five workspace packages.
npm run perfThe performance harness.
npm run rebuildRe-runs the native-module rebuild described above.
Note
There is no npm start, no npm run preview, no npm run serve, and no standalone npm run electron. If a guide tells you to run one of those, it isn't describing this repository.

Production Build

npm run build

This runs tsc, builds the renderer with Vite, and bundles the main process. It writes four directories, which is worth knowing when you're cleaning up or wondering where something went:

DirectoryContents
dist/The Vite-built renderer.
dist-electron/The bundled main process and preload.
dist-typecheck/Output from the tsc -b electron/tsconfig.json leg. Build artifacts only; nothing ships from here.
release/electron-builder distributables. Override with -c.directories.output=<path>.

Run the Built App

npm run dev:electron

That's electron . with NODE_ENV=development set. Electron reads the "main" field in package.json, which points at dist-electron/electron/bootstrap.js, so you're launching whatever npm run build last produced. It's the quickest way to check a production build without packaging it.

Tip
electron is already a devDependency, so nothing extra is downloaded. Running npx electron . directly works too; it just skips the NODE_ENV that the script sets for you.

Package for Distribution

npm run package:mac

Writes DMG and ZIP artifacts to release/ for three architectures: arm64, x64, and universal. It isn't one universal binary; you get all three variants, each named with its architecture.

npm run package does the same thing and lets electron-builder pick the target for your current platform.

Local unsigned macOS builds

For a local build with no Developer ID signing and no notarization, build first and then package with signing disabled:

npm run build && CSC_IDENTITY_AUTO_DISCOVERY=false npx electron-builder --publish never \
  -c.mac.notarize=false -c.mac.forceCodeSigning=false

npm run build has to come first: electron-builder packages whatever is already in dist/ and dist-electron/, so skipping it silently ships a stale bundle. If CSC_LINK, CSC_NAME, or any APPLE_* variables are exported in your shell, unset them too. The notarization hook itself is gated separately by DAINTREE_SKIP_NOTARIZATION=true.

Release builds do notarize, and the hook submits and waits as two steps: notarytool submit, then notarytool wait --timeout 1800. The wait is bounded so an Apple-side delay can't block CI indefinitely, and a resumable .notarization-state.json in the output directory lets a re-run reattach to an in-flight submission instead of resubmitting.

Fast local install loop

If you want the built app installed rather than just packaged, there's a dedicated loop:

  • npm run package:local: a --dir package (no installer).
  • npm run package:local:dmg: the same, as a DMG.
  • npm run install:local and install:local:fast: package and install in one step.

These stamp a development version one minor above package.json (a repo at 0.32.0 produces 0.33.0-dev), so a local build is never confused with a release.

Note
A local --dir package ships without app-update.yml, so the auto-updater is deliberately inert in it. That's expected, not a bug. See When Auto-Update Is Disabled.

Troubleshooting

node-pty build failures

If npm install fails during the native rebuild:

  1. Confirm the platform build tools are installed (see Prerequisites).
  2. Rebuild by hand: npm run rebuild
  3. On macOS, make sure the Xcode CLI tools are current: xcode-select --install
  4. On Windows arm64, run npm run rebuild:win-arm64 as well.

If the failure names better-sqlite3, the problem is upstream of the rebuild (the package's prebuild wasn't downloaded) because Daintree never rebuilds it.

Wrong Node.js version

Daintree pins Node 22.13.0. If you see a version error, switch to the pinned version:

nvm install    # reads .nvmrc automatically
nvm use

npm run check:node-version verifies that .nvmrc, .node-version, and engines.node still agree.

Stale renderer after a dependency change

Delete node_modules/.vite to clear Vite's cache, then restart the dev server.