The monorepo
An Nx workspace (package-based, a project.json per project) over Yarn 4 workspaces, cut along one line: what a module needs from where it runs. Code that needs nothing but JavaScript is neutral and goes everywhere; code that needs Node runs in a host or the indexer worker; code that needs the DOM is the UI; only an editor app touches an editor's API.
Projects
| Project | Package | Tags | Holds |
|---|---|---|---|
packages/protocol |
@orbit-code/protocol |
lib, neutral | Every message that crosses a boundary; workspace ids; MCP tool names |
packages/graph |
@orbit-code/graph |
lib, neutral | File kinds, the columnar graph, the directory tree, layout extension, node radius |
packages/common |
@orbit-code/common |
lib, neutral | Emitter, Event, Disposable, Logger, TickBatcher |
packages/indexer |
@orbit-code/indexer |
lib, node, publish:npm | The indexer worker and the orbit-index CLI; listFiles for hosts |
packages/agent |
@orbit-code/agent |
lib, node | Claude Code conversations, the CLI backend, stream-json, permissions, the catalog, history |
packages/core |
@orbit-code/core |
lib, node | The editor-agnostic host: GraphService, Store, the indexer client, ChangeBatcher, SessionProjector, the controller, settings |
packages/webview |
@orbit-code/webview |
lib, browser | The UI, built to dist/webview.js |
apps/vscode |
orbit-code |
app, vscode, publish:vscode | The VS Code extension |
apps/desktop |
@orbit-code/desktop |
app, electron, publish:desktop | The Electron desktop app |
tools/harness |
@orbit-code/harness |
tool, browser | The simulated host and the headless Chrome harness |
neutral protocol ◄──── graph common
▲ ▲ ▲
node indexer ─────────┘ (+ protocol) │
agent ── protocol ─────────────────┤
core ─── agent, graph, protocol, common, indexer/listFiles
browser webview ─ graph, protocol harness ─ graph, protocol, common
vscode apps/vscode ─ core, agent, graph, protocol, common, indexer/listFiles; ships the indexer and webview builds
electron apps/desktop ─ the same
Where code goes
Ask, in order:
- Does it call an editor's API? That editor's app.
- Does it need Node? If it extracts imports or needs dependency-cruiser or TypeScript:
packages/indexer, from which onlylistFilesis imported outside. If it is about Claude sessions and knows nothing of graphs:packages/agent. If it joins graphs, sessions and files on behalf of a host:packages/core. - Does it need the DOM?
packages/webview, ortools/harnessfor test pages. - Otherwise it is neutral. Part of a message's shape:
packages/protocol. Computing on graphs, paths or layouts:packages/graph. Plumbing any service needs:packages/common.
A service in core or agent takes editor facts as options and never asks an editor. A neutral package doesn't gain a Node or DOM import to save a copy: if two runtimes need the same logic, the logic is neutral and each runtime wraps it. A new package is worth its manifest only when it holds a different runtime or a different reason to change.
The boundary check
yarn boundaries (tools/scripts/check-boundaries.mjs, also in CI) reads each project's src/ imports and package.json and fails on:
- Tags. Anything but exactly one
type:(lib, app, tool) and oneruntime:(neutral, browser, node, vscode, electron) per project. - Type. A lib depending on anything but libs; an app or tool depending on an app or tool.
- Runtime. Neutral imports neutral; browser imports neutral and browser; node imports neutral and node; vscode and electron import neutral and node too. Only
runtime:vscodeimportsvscode. Node builtins are refused in neutral and browser code. - Declared. An import of a package the project's
package.jsondoesn't list, or a relative import leaving the project'ssrc/. - Indexer. Any
@orbit-code/indexerimport butlistFilesoutside the indexer, so dependency-cruiser and TypeScript stay in the worker.
It can't see what a bundle pulls in transitively (the extension's wiring check follows the extension's imports into every package for that) or globals: a DOM or Node global in the wrong package fails that package's own typecheck instead, because its tsconfig doesn't declare it.
Packages
- A workspace package exports its sources module by module:
"./*": "./src/*.ts". Consumers import@orbit-code/graph/languages, and typecheck and bundle each other's sources directly; there is no build step between libraries and no barrel. dependenciesare what the code imports (workspace packages asworkspace:*, npm packages pinned exactly, the same version everywhere).devDependenciesare types and the builds a project only ships (@orbit-code/webviewfor the extension and the harness). The desktop app lists every workspace package as a devDependency, because electron-builder copies an app'sdependenciesinto the package and everything is bundled already.- Workspace-wide tools (
nx,@nx/js,typescript,esbuild,@types/node) are root devDependencies. .yarnrc.ymlkeeps anode_modulestree, because the build scripts and tools run under plain Node, which can't resolve Plug'n'Play packages. Workspace packages are symlinked intonode_modules/@orbit-code/.- Names are
@orbit-code/<dir>; the extension keepsorbit-code, its Marketplace identity. Manifests areprivate: truewith a one-sentence description andlicense: MIT.
TypeScript per runtime
tsconfig.base.json holds the compiler options (noEmit, moduleResolution: bundler, strict). Each project's tsconfig.json extends it and adds only its runtime:
| Runtime | Adds |
|---|---|
| neutral | ES2023 and tools/typescript/neutral-globals.d.ts (timers and console, nothing else) |
| node | "types": ["node"] |
| browser | the DOM libs |
| vscode | "types": ["node", "vscode"] |
| electron | "types": ["node"] and the DOM (the preload sees the page's window); Electron's types come with its package |
A project's typecheck also checks the workspace sources it imports, under its own settings. esbuild doesn't typecheck, so yarn typecheck is a separate step. The build.mjs files, scripts/, test/ and tools/**/*.mjs are plain JavaScript and aren't typechecked; node --check catches syntax errors in them.
Targets
| Target | Projects | Does |
|---|---|---|
build |
indexer, webview, vscode, desktop, harness | node build.mjs [--production] in the project, writing its dist/. Depends on ^build, so -c production reaches the dependencies. The webview's runs its constraint check first |
typecheck |
every project | tsc -p tsconfig.json, from the target defaults |
watch |
the same as build | build.mjs --watch, continuous; vscode:watch starts the indexer and webview watches and copies their builds in as they change |
package |
vscode, desktop | A production build, then vsce or electron-builder. Uncached for the desktop, whose output is per platform |
smoke, e2e, start |
vscode, desktop, harness | Uncached, after a build |
nx-release-publish |
indexer, vscode, desktop | npm, the Marketplace and Open VSX, or GitHub release assets |
Each build writes nothing outside its own dist/; an app copies what it ships from its dependencies' dist/. Nx caches build and typecheck in .nx/cache from each project's files, tsconfig.base.json, tools/typescript and the sources of what it depends on, but not the environment: after upgrading Node or changing a build script's environment, yarn nx reset.
Nx passes a configuration along ^build but not along a dependsOn on the same project, which is why vscode:package runs nx run vscode:build:production itself, and why package.mjs refuses a dist/ still holding source maps.
yarn nx show projects # every project
yarn nx show project vscode --json # its targets
yarn nx graph # the project graph in a browser
yarn nx run-many -t typecheck build -p webview indexer
yarn nx affected -t typecheck build --base=main
yarn nx reset # clear the cache and stop the daemon
Arguments after a root script reach the script Nx runs: yarn harness --graph g.json runs the harness on that graph after Nx built what it needs.
Adding a library
- Create
packages/<name>with apackage.json(source exports,private: true), aproject.json(name,projectType,sourceRoot, tags,"typecheck": {}), atsconfig.jsonfor its runtime andsrc/<name>.ts. yarn install, which links it intonode_modules/@orbit-code/.- Add
"@orbit-code/<name>": "workspace:*"to thedependenciesof each project that imports it, thenyarn installagain: the lockfile records workspace dependencies, and CI installs with--immutable. yarn nx show project <name>,yarn typecheck,yarn boundaries.- Add its row to the table above and to the root README's layout table.
Moving code between packages
git mvthe file, so history follows it.- Imports inside the new package become relative; imports of it from elsewhere become
@orbit-code/<pkg>/<module>. - Update the
dependenciesof the package that lost it, the one that gained it, and every importer;yarn install. yarn typecheck && yarn boundaries && yarn build.- Grep the old path: the smoke suite names graph files, the harness names sources, and
ci.ymlchecks thatpackages/webview/src/main.tsis in the indexed graph.
Gotchas
node_modules/@orbit-code/*are relative symlinks. A copy of the workspace whosenode_modulesis a symlink to this one builds this workspace's sources; runyarn installin the copy.- A new project shows up in Nx from its
project.json, but nothing resolves@orbit-code/<name>untilyarn installhas linked it. A target that "doesn't exist" right after adding it:yarn nx reset. - Neutral means neutral:
TextEncoder,URL,structuredCloneandperformancearen't ECMAScript, so neutral packages don't have them unlessneutral-globals.d.tsdeclares them. - The test runners write into the workspace: the smoke creates
packages/graph/src/__orbitSmoke*.tsfor a few seconds, and the harness writes.harness/at the root.