Skip to content

Build Targets & TypeScript Configuration

dazeb edited this page Sep 17, 2026 · 2 revisions

Build Targets & TypeScript Configuration

This page describes the three bundles this repo produces, which module trees each typecheck project covers, and where path aliases must be registered. If you are adding a file, an alias, a dependency, or a new entrypoint, the invariants below are what will break your change.

What actually runs

There is no single "build" that produces one artifact. pnpm build runs build:cli and then electron-vite build, which emits three independent outputs driven by one config:

Target Config Output Consumed by
Electron main + preload electron.vite.config.ts (main, preload) out/main/index.js, out/preload/* Electron, via package.json "main"
Renderer electron.vite.config.ts (renderer) out/renderer Electron (loaded by the window) and the server edition (static serve)
Server vite.config.server.ts (build.ssr of src/server/index.ts) out/server/index.js node out/server/index.js with TERMSPRAWL_SERVER_ENTRY=1
Context CLI (out of scope here) scripts/build-context-cli.mjs, invoked by build:cli — Prerequisite of both dev and build
flowchart LR
  subgraph S["src/"]
    M["main/**"]
    P["preload/**"]
    R["renderer/src/**"]
    CO["core/**"]
    SH["shared/**"]
    SV["server/**"]
  end
  EB["electron-vite build<br/>electron.vite.config.ts"]
  VS["vite build --config<br/>vite.config.server.ts"]
  M --> EB
  P --> EB
  R --> EB
  CO --> EB
  SH --> EB
  CO --> VS
  SH --> VS
  SV --> VS
  EB --> OM["out/main · out/preload"]
  EB --> OR["out/renderer"]
  VS --> OS["out/server/index.js"]
  OM --> EL["Electron process"]
  OR --> EL
  OR --> SRV["node out/server/index.js"]
  OS --> SRV
Loading

Key nodes: the shared alias is duplicated across both bundlers, so src/shared and src/core land in the Electron bundles and the server bundle; out/renderer is a single artifact with two consumers, which is why build:server does not build it (its own header comment says the renderer comes from pnpm run build). Practical consequence: pnpm build && pnpm build:server is the correct order for a runnable server; running build:server alone yields a server with a stale or missing out/renderer.

Roles of the runtime outputs: out/main/index.js is the Electron entry and owns the window lifecycle, PTY spawning and file/git services; out/preload is the contextBridge surface; out/renderer is the React app that, in server mode, is driven through the browser shim instead of the preload bridge. Main/preload builds apply externalizeDepsPlugin(), so runtime dependencies (node-pty, electron-updater, ws, …) are not bundled — they must exist under node_modules at run time. That is why package.json's build.files ships out/** plus package.json, why node-pty is listed in asarUnpack (a native .node file cannot be loaded from inside asar), and why postinstall runs electron-rebuild -f -w node-pty.

The server build externalizes ws, node-pty and Node builtins (per its header comment). The config itself only sets build.ssr; it does not list rollupOptions.external, so externalization comes from Vite's SSR defaults rather than an explicit list — if you ever need a dependency inlined into the server bundle, you must add it explicitly. Note also that vite.config.server.ts aliases only @shared; if server code imports @renderer/* it fails at build time.

start:server sets TERMSPRAWL_SERVER_ENTRY=1. That env var is the contract that marks the non-Electron entry for shared code that branches on process role; its consuming site is not in the fragments used for this page.

The typecheck gates

pnpm typecheck is two sequential tsc --noEmit runs — tsconfig.node.json first, then tsconfig.web.json — so a Node-side error stops the pipeline before the renderer project is even evaluated.

  • tsconfig.node.json covers src/main/**, src/preload/**, src/shared/**, src/core/**, src/server/**, plus electron.vite.config.ts and vite.config.server.ts. It is the whole non-renderer world, and it is also the only place the two build configs get type-checked.
  • tsconfig.web.json covers src/renderer/src/**, all of src/shared/**, and an explicit whitelist of src/core modules (chat types/conversation/cost, links registry, a2a protocol and client, relay-term, space-sync, space-snapshots, and further entries truncated in the fragment used here). It adds DOM/DOM.Iterable libs and jsx: react-jsx; the node project sets neither.
  • tsconfig.json is not a solution file: it has no references, and its include is only electron.vite.config.ts. It exists so the root-level Vite config is typed in isolation; it is not the gate for src/**.
flowchart TB
  SH["src/shared/**"] --> NP["tsconfig.node.json program"]
  CORE["src/core/**"] --> NP
  NODE["src/main · src/preload · src/server<br/>+ both vite configs"] --> NP
  SH --> WP["tsconfig.web.json program"]
  WL["whitelisted src/core modules"] --> WP
  REN["src/renderer/src/**"] --> WP
  NP --> TC["pnpm typecheck"]
  WP --> TC
Loading

src/shared/** is the pivot of this graph: it is in both programs, so any file there must compile under both a Node-types context and a DOM-lib context. src/core/** is the asymmetric half — the node project sees all of it, the web project sees a curated subset, which documents which core modules are considered renderer-safe.

Two caveats that matter when you are deciding where new code lives:

  • include is intent, not a sandbox. TypeScript adds imported files to a program even if they are not matched by include, so a renderer import of a non-whitelisted core file still typechecks. The thing that actually breaks a Node-only import in shared code is the renderer bundle (Vite's browser shim for Node builtins) or the run time — not pnpm typecheck. Adding a core module to the web whitelist is therefore a statement that it is browser-safe and should be covered by the DOM-configured program, not a security fence.
  • tsconfig.node.json does not pin lib, and with target: ES2022 TypeScript's default library set still contains DOM. So DOM globals are not excluded from main/preload/server by typechecking alone; the real protection is that src/renderer/** is excluded from the node program and renderer code is the only place DOM APIs are used. If you want the gate to be stricter, pinning "lib": ["ES2022"] in the node project is the extension point.
  • composite: true is set in both project configs while noEmit: true and no references exist, and no script runs tsc -b. Treat the projects as two standalone -p invocations; the composite flags currently buy nothing but do constrain you if you later introduce build info.

Alias plumbing (the four-place rule)

@shared is declared in four places and @renderer in three, because tsconfig paths are consulted by tsc only, while each bundler/test runner resolves its own aliases:

Alias tsconfig.node.json tsconfig.web.json electron.vite.config.ts vite.config.server.ts vitest.config.ts
@shared/* yes yes yes (all three targets) yes yes
@renderer/* no yes yes (all three targets) no yes

The header comment in electron.vite.config.ts explains the split: type-only imports were erased before bundling so nothing broke, but value imports such as @shared/agents/config need the alias at bundle time. vitest.config.ts repeats the note and mirrors the paths because Vitest does not read tsconfig path resolution and no vite-tsconfig-paths plugin is present.

One concrete asymmetry to keep in mind: electron.vite.config.ts applies the same sharedAlias object to main and preload, so the bundler would resolve @renderer/* from main-process code — but tsconfig.node.json has no such path mapping, so tsc rejects it with an unresolved-module error. That is the intended enforcement: the bundler is permissive, typecheck is the gate. The server path is stricter on both ends.

Tests

vitest.config.ts defines the test surface by glob: src/**/*.test.ts, src/**/*.test.tsx, scripts/**/*.test.ts, and relay/**/*.test.mjs. Note the asymmetry — relay files are only picked up as .mjs; a .test.ts file added under relay/ would silently never run. No environment is configured, so tests run in the Node environment and there is no jsdom/happy-dom dependency; the comment that .test.tsx was added "for renderer component tests (react-dom/server render)" confirms the convention that renderer components are exercised by server-rendering to a string rather than by mounting into a DOM. If you add a component test that needs document or window, you must add both a DOM environment and its dependency.

Extension points and pitfalls

  • New shared/core module used by the renderer: add it to tsconfig.web.json's include, verify it compiles under the node project too, and confirm it needs no Node builtins.
  • New alias: expect to touch up to five files; forgetting vitest.config.ts produces tests that fail only at resolution time.
  • New server-side dependency that must not be externalized: extend vite.config.server.ts explicitly; the current config relies on SSR defaults.
  • Native modules: externalization, asarUnpack and electron-rebuild are three parts of one mechanism. postinstall rebuilds node-pty for Electron's ABI while the server target loads it under plain Node with the module left external — if the server edition fails to load node-pty, an ABI/NODE_MODULE_VERSION mismatch is the first thing to check.
  • src/server/index.ts is both the SSR entry and the runtime entry name (out/server/index.js); renaming it means editing vite.config.server.ts and package.json together.

Limits of this page

The tsconfig.web.json include list is truncated in the available fragment (src/core/w…), so the full whitelist of renderer-safe core modules is not enumerated here. scripts/build-context-cli.mjs, scripts/** and relay/** test contents, and the runtime consumers of TERMSPRAWL_SERVER_ENTRY were not available, so their behavior is described only as far as package.json and the Vite configs reveal it. The generated out/ layout is inferred from config values (main entry, outDir, emptyOutDir), not inspected.

Sources: electron.vite.config.ts, vite.config.server.ts, tsconfig.json, tsconfig.node.json, tsconfig.web.json, vitest.config.ts, package.json

termsprawl

App Shell & Platform Foundations

Canvas, Nodes & Renderer State

Terminals & Session Continuity

Persistence, Projects & Files

Agent Runtime & Tooling

Chat Nodes & Model Providers

Git & Source Control

Embedded Browser Nodes

Server Edition

Relay & Remote Access

Integrations & Secondary Surfaces

Settings, Updates & Maintenance

Clone this wiki locally