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.

Updated
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. Use a maintained Git installation; this checkout does not declare a Git 2.30 build-time minimum.
  • Node.js 22.23.2. 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
Native builds require Python and the compiler toolchain used by node-gyp. The examples below are common setup paths; consult the installed node-gyp/README.md for the tool versions supported by your checkout.

Install the Xcode Command Line Tools:

xcode-select --install

Install the Visual Studio Build Tools with the Desktop development with C++ workload, either from the Visual Studio installer or with winget:

winget install Microsoft.VisualStudio.2022.BuildTools

Install Python as well. For Windows ARM64, include the ARM64 C++ compiler and ATL components; the package manager command alone does not select every required workload.

On Debian-family systems, start with Python, make and a C/C++ toolchain:

sudo apt install build-essential python3

Clone & Install

git clone https://github.com/daintreehq/daintree.git
cd daintree
nvm install
nvm use
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.

Warning
Never install with --ignore-scripts. It skips the postinstall native rebuild, and the app crashes on startup with a module-version error that looks nothing like the cause.

The repository also contains workspace packages under packages/: @daintreehq/plugin-sdk, plugin-testing, plugin-vite, the daintree-plugin CLI, and create-daintree-plugin. Build them explicitly with npm run packages:build. The root install links these workspaces; packages:build is the explicit compilation step.

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, which deletes the existing development user-data directory before booting. This resets development settings and sessions; use it deliberately for first-run testing, not as a routine start command.
  • 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 across the repository. Review the diff for changes outside your task.
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 against an E2E build (npm run build:e2e). Choose the spec or project you need; this is a separate, expensive check. 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. The build, typecheck, and packaging commands have different outputs:

DirectoryContents
dist/The Vite-built renderer.
dist-electron/The bundled main process and preload.
dist-typecheck/Output from the main-process project-reference leg of npm run typecheck. Build artifacts only; nothing ships from here.
release/electron-builder distributables, created by packaging rather than npm run build alone. 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:win

Builds both Windows targets: an .appx for the Microsoft Store (x64) and an NSIS .exe installer (x64 and arm64). There is no portable-executable target. To build just one, use npm run package:win:nsis or npm run package:win:appx.

npm run package:linux

Writes an AppImage and a .deb to release/.

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.

Source reference

Check package.json and .nvmrc for the audited scripts and runtime pin, postinstall.cjs for native modules, and electron-builder.config.cjs for packaging targets. The app's contributor guide covers the development workflow. Plugin authors can usually work in their own repository; start with Building & Distributing plugins.

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