-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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
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.
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.jsoncoverssrc/main/**,src/preload/**,src/shared/**,src/core/**,src/server/**, pluselectron.vite.config.tsandvite.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.jsoncoverssrc/renderer/src/**, all ofsrc/shared/**, and an explicit whitelist ofsrc/coremodules (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 addsDOM/DOM.Iterablelibs andjsx: react-jsx; the node project sets neither. -
tsconfig.jsonis not a solution file: it has noreferences, and itsincludeis onlyelectron.vite.config.ts. It exists so the root-level Vite config is typed in isolation; it is not the gate forsrc/**.
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
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:
-
includeis intent, not a sandbox. TypeScript adds imported files to a program even if they are not matched byinclude, 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 — notpnpm 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.jsondoes not pinlib, and withtarget: ES2022TypeScript's default library set still contains DOM. So DOM globals are not excluded from main/preload/server by typechecking alone; the real protection is thatsrc/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: trueis set in both project configs whilenoEmit: trueand noreferencesexist, and no script runstsc -b. Treat the projects as two standalone-pinvocations; the composite flags currently buy nothing but do constrain you if you later introduce build info.
@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.
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.
-
New shared/core module used by the renderer: add it to
tsconfig.web.json'sinclude, 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.tsproduces tests that fail only at resolution time. -
New server-side dependency that must not be externalized: extend
vite.config.server.tsexplicitly; the current config relies on SSR defaults. -
Native modules: externalization,
asarUnpackandelectron-rebuildare three parts of one mechanism.postinstallrebuildsnode-ptyfor Electron's ABI while the server target loads it under plain Node with the module left external — if the server edition fails to loadnode-pty, an ABI/NODE_MODULE_VERSIONmismatch is the first thing to check. -
src/server/index.tsis both the SSR entry and the runtime entry name (out/server/index.js); renaming it means editingvite.config.server.tsandpackage.jsontogether.
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
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance