跪拜 Guibai
← All articles
Frontend · Backend · Node.js

Scaffolding a Coding Agent Monorepo with npm Workspaces and Strict TypeScript

By 东方小月 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A monorepo with strict ESM boundaries and explicit build ordering prevents the import chaos and type pollution that derail larger TypeScript projects. The setup shown here — exact dependency versions, `verbatimModuleSyntax`, and a single Biome pass before type-checking — catches errors early and keeps package contracts clean as the agent grows.

Summary

The `di-code` monorepo splits a coding agent into four packages — `ai`, `agent`, `coding-agent`, and `tui` — each an ESM-only npm workspace with its own build config. npm links local packages through the `workspaces` field so that `@di-code/agent` can import `@di-code/ai` as a normal dependency, avoiding fragile relative-path imports.

TypeScript configuration enforces strict mode, `NodeNext` module resolution, and `verbatimModuleSyntax` across the whole repo. A shared `tsconfig.base.json` sets the baseline, while each package's `tsconfig.build.json` extends it and emits declarations, source maps, and inline sources into a local `dist/` directory. The root `tsconfig.json` runs type-checking only, with `noEmit` enabled.

Biome replaces both ESLint and Prettier with a single tool, configured to format and lint all TypeScript and JSON files. The build script respects dependency order, running `tui` and `ai` before `agent` and `coding-agent`. A `check` script gates TypeScript on a passing Biome run, and tests are dispatched to any workspace that defines a `test` script.

Takeaways
npm workspaces link local packages so that `@di-code/agent` imports `@di-code/ai` as a regular dependency, not a relative path.
Every package is ESM-only (`"type": "module"`) and targets Node.js ≥ 22.19.0.
`tsconfig.base.json` sets `strict`, `NodeNext` module resolution, `verbatimModuleSyntax`, and `noEmitOnError` for the whole repo.
Root `tsconfig.json` uses `noEmit` for type-checking only; each package's `tsconfig.build.json` emits declarations, source maps, and inline sources into a local `dist/`.
Biome handles both linting and formatting; the `check` script runs Biome first, then TypeScript.
The build script orders packages explicitly: `tui` and `ai` build before `agent` and `coding-agent`.
All devDependencies use exact versions (no `^` or `~`), and `private: true` prevents accidental publishing of the root or any sub-package.
`exports` in each package.json restricts the public API to the root entry, blocking deep imports into internal file paths.
Conclusions

Using `verbatimModuleSyntax` and `erasableSyntaxOnly` together signals a project that treats TypeScript as a type-checker on top of standard ESM, not a dialect — a stance that avoids whole categories of emit surprises.

Running Biome before TypeScript in the `check` script is a deliberate sequencing choice: formatting and linting failures are cheaper to fix and should block a full type-check, which is the more expensive operation.

Pinning all devDependencies to exact versions removes a source of non-determinism across machines; in a monorepo where multiple packages share the same toolchain, a single version drift can produce inconsistent builds.

Concepts & terms
npm workspaces
A feature of npm that lets you manage multiple packages within a single root repository. Local packages are symlinked into the root `node_modules`, so they can depend on each other as if they were published packages, without relative-path imports.
verbatimModuleSyntax
A TypeScript compiler option that requires import/export statements to match the final module format exactly. It forces `import type` for type-only imports and rejects syntax that would be erased during emit, ensuring the source is valid ESM.
NodeNext module resolution
TypeScript's module resolution mode that mimics Node.js's native ESM and CJS resolution algorithm, including support for `package.json` `"exports"` fields and file extensions in imports.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗