Skip to content

About this page

On this page

tsconfig ​

The tsconfig section generates every workspace's tsconfig.json from a workspace default tsconfig (tsconfig.default.json), layering path aliases and project references from a single root workspaces tsconfig (tsconfig.workspaces.json). It runs only at a monorepo root (project.type: 'monorepo-root'); a single project keeps its hand-written tsconfig untouched.

dbx.config.ts
ts
export default defineConfig({
    toolchain: {
        project: { type: 'monorepo-root', name: 'acme', scope: '@acme' },
        tsconfig: { aliases: true, refs: true, indent: 4 },
    },
});

dbx sync regenerates the managed tsconfig.json files and tracks them in .dbx/toolchain/manifest.json; dbx sync --check is the CI gate. See Configuration for the opt-in-by-presence shorthand (tsconfig: true enables the defaults).

Why so many tsconfig files? ​

TypeScript's language service (IntelliSense, diagnostics, autocomplete, "go to definition", project references, ...) always picks up the file literally named tsconfig.json. It offers no way to point the editor at a different file, and it only understands the options it can express in a single config.

That single-file constraint is painful in practice:

  • Builds need different options than the editor. tsc for a build wants declaration/composite/emitDeclarationOnly, a wider include and no noEmit, while the editor wants fast, no-emit checking of src. Cramming both into one tsconfig.json makes one of the two slower or wrong.
  • A monorepo has many projects with different options. Each workspace wants its own include/lib/types/jsx, but the editor only reads the nearest tsconfig.json.
  • The language service can get slow. For TypeScript (especially ≤ 7) the editor loads and type-checks whatever the tsconfig.json includes; a config that pulls in every workspace (or the whole build graph) can noticeably slow IntelliSense. Keeping the editor config small and correct matters.
  • Hand-managing the graph is tedious. Path aliases and project references have to stay in sync with the workspace list, and a stale reference silently degrades the editor.

The toolchain splits the concerns so each file has one job, and dbx sync keeps the generated tsconfig.json (the file the LSP actually reads) in sync with a shared workspaces tsconfig that declares the paths + references.

NOTE: the paths + references in tsconfig.workspaces.json automatically resolves relative paths in each workspace's generated tsconfig.

The files ​

The generator reads two hand-authored inputs (tsconfig.default.json, tsconfig.workspaces.json) and writes one managed output (tsconfig.json):

FileOwnerRole
tsconfig.base.jsonyouShared compiler options that every workspace config extends.
tsconfig.default.jsonyouA workspace's own options — the input the generator reads. Optional.
tsconfig.build.jsonyouBuild/emit options (tsc -p tsconfig.build.json). Optional, never rewritten.
tsconfig.workspaces.jsonyou (root)The root paths + references the generator expands. Optional.
tsconfig.jsongeneratedExtends tsconfig.default.json + the workspaces aliases/references.

Only tsconfig.json must keep its name — that is the file TypeScript's LSP reads. Everything else is optional and, if you prefer, renameable (see Renaming the files):

  • No tsconfig.default.json: there is nothing to expand (the task logs a message and, when workspaces are expected, warns — see Warnings).
  • No tsconfig.build.json: builds simply use another config; it is never required or rewritten.
  • No tsconfig.workspaces.json: each workspace still gets a tsconfig.json that extends its own default, but there are no shared paths/references to wire.

The token in the templates (default src/env.d.ts) is resolved when a project is scaffolded, so a generated config's include points at the project's env types file.

Development vs build configs ​

tsconfig.json is the editor/development config: it extends tsconfig.default.json and layers the workspace paths aliases on top, so imports such as @acme/core resolve directly to packages/core/src. That is what gives go-to-definition, inline diagnostics and fast editor feedback against the real workspace source.

tsconfig.build.json intentionally extends tsconfig.default.json, nottsconfig.json, so it does not get those path aliases. A build (tsc -p tsconfig.build.json, tsdown, ...) therefore treats sibling workspaces as packages (resolved through node_modules / project references) instead of treating their source as part of the current package. Keeping them as packages matters: aliasing them into the build graph would compile/bundle another workspace's source into the output and increase the package size.

You can opt into build-time aliases by changing the build config to

tsconfig.build.json
json
{
    "extends": "./tsconfig.json",
    "include": ["src"]
}

It works, but sibling workspace source now participates in the build graph, so only do it when that is what you want.

Options ​

OptionTypeDefaultDescription
enabledbooleanenabled when definedMaster switch (only meaningful with project.type: monorepo-root).
aliasesbooleantrueInject compilerOptions.paths from the workspaces config.
refsbooleanfalseWire project references between workspaces.
compositebooleanfalseEmit composite: true on the generated configs.
globstring | string[]**/tsconfig.default.jsonGlob(s) for the workspace default tsconfig files to expand.
excludestring | string[]common ignored pathsGlob(s) excluded from glob.
gitignorebooleantrueRespect .gitignore files (root + nested).
defaultFilestringtsconfig.default.jsonThe workspace default tsconfig file name.
buildFilestringtsconfig.build.jsonThe workspace build tsconfig file name.
indentnumber4JSON indentation of the generated files.
rootFilestringtsconfig.workspaces.jsonThe workspaces tsconfig file name.
baseFilestringtsconfig.base.jsonThe root base tsconfig file name (the overrides.base target).
extends{ base?, default?, build? }template defaultsPer-file extends overrides for scaffolded tsconfig templates.
overrides{ base?, default?, build?, root? }noneManaged deep-merge overrides applied to the user-owned tsconfigs.

aliases ​

When on (the default), each generated tsconfig.json gets a compilerOptions.paths block derived from the workspaces tsconfig's paths. The generator rewrites every alias relative to the workspace it is writing, and drops any alias that resolves to the workspace itself (or into its own subtree) so a package never maps its own import back to a self-reference.

refs ​

When on, the workspaces references are merged with each workspace's own references, deduplicated, rewritten relative to the workspace, and pruned whenever the target config is not composite (a reference to a non-composite project is invalid). This is what lets tsc -b build the graph in order.

composite ​

When on, the generated configs spread the workspaces tsconfig's compilerOptions (including composite: true, declaration, emitDeclarationOnly, ...) and merge its types. Leave it off unless you build the graph with tsc --build.

refsAuto ​

refsAuto derives the workspaces references from the project graph. It defaults to auto; refs must also be enabled and each referenced workspace must be composite. References are only followed by tsc --build (tsc -b); tsc -p --noEmit ignores them. If your typecheck runs tsc -p --noEmit (as this repo's does), keep refsAuto: false and maintain the references in tsconfig.workspaces.json by hand, or switch the check to tsc -b.

glob ​

The glob(s) selecting the workspace default tsconfig files to expand. A single string or an array:

ts
tsconfig: {
    glob: '**/tsconfig.default.json';
}
tsconfig: {
    glob: ['packages/*/tsconfig.default.json', 'apps/*/tsconfig.default.json'];
}

It defaults to **/<defaultFile>, so setting defaultFile alone is usually enough.

exclude / gitignore ​

exclude drops paths from glob; it adds to the common ignored paths the toolchain already skips (node_modules, .dbx, dist, build, coverage, .turbo, .git, .yarn, ...). gitignore (on by default) additionally honours the repo's .gitignore files — root and nested, each scoped to its own directory, like git — so an ignored workspace is never scanned:

ts
tsconfig: {
    glob: '**/tsconfig.default.json',
    exclude: ['**/legacy/**'],
    gitignore: true,
}

Set gitignore: false to scan ignored paths anyway. The base list is exported as DEFAULT_IGNORE_PATTERNS from @deltabox/toolchain and @deltabox/toolchain/shared, so you can compose your own excludes from it.

Renaming the files ​

defaultFile, buildFile, rootFile and baseFile rename the files the generator reads and the overrides patch:

ts
tsconfig: {
    defaultFile: 'tsconfig.src.json',          // expanded into tsconfig.json
    buildFile: 'tsconfig.dist.json',           // read, never rewritten by the generator
    rootFile: 'tsconfig.graph.json',           // the workspaces paths + references
    baseFile: 'config/tsconfig.shared.json',   // the shared base (`overrides.base`)
}

Only tsconfig.json is fixed (TypeScript's LSP requirement). rootFile and baseFile are paths relative to the repo root; defaultFile and buildFile are file names inside each workspace. The custom names are honoured everywhere the sync task looks: the generator, the overrides, the watched inputs (dbx watch) and the files it keeps. Project detection looks for tsconfig.workspaces.json / tsconfig.default.json by default, but honours rootFile / defaultFile when they are set in the config. Scaffolding (dbx project:init*) always writes the default names.

indent ​

The JSON indentation of every generated file (default 4).

extends ​

Overrides the extends field written into scaffolded tsconfig templates (dbx project:init*). This is separate from the tsconfig.workspaces.jsonextends chain used by the resolver. Set a value to add/replace the field, or false to remove it:

ts
tsconfig: {
    extends: {
        base: '@tsconfig/node22',       // tsconfig.base.json currently has no extends
        default: './tsconfig.base.json', // keep/customize the normal default chain
        build: './tsconfig.json',       // opt into path aliases during builds
    },
}

Existing projects are user-owned by default: edit their tsconfig.base.json, tsconfig.default.json and tsconfig.build.json directly. dbx sync only rewrites the generated tsconfig.json unless you opt into overrides.

overrides ​

Deep-merges managed options into the user-owned tsconfigs during dbx sync, so a config can define those files programmatically:

ts
tsconfig: {
    overrides: {
        base: { compilerOptions: { target: 'ES2022' } },
        root: { compilerOptions: { composite: true, types: ['node'] } },
        default: { compilerOptions: { strict: true } },
        build: { compilerOptions: { declaration: true } },
    },
}
KeyTarget
basethe root base tsconfig (baseFile, tsconfig.base.json)
rootthe workspaces tsconfig (rootFile, tsconfig.workspaces.json)
defaultevery workspace's default tsconfig (defaultFile)
buildevery workspace's build tsconfig (buildFile)

Overrides are deep-merged: objects merge key by key, while arrays and scalars replace the target value. Keys the override does not mention are left alone. The patched files stay yours: they are not added to the manifest's managed files, so a sync never deletes them and dbx reset never removes them.

Snapshots ​

The first time an override touches a file, its original content is saved in .dbx/toolchain/manifest.json under a top-level snapshots field. The tree mirrors the repository with relative paths (the manifest is the tracked file under .dbx, so it never holds an absolute path), and the applied override per file is recorded under tsconfigOverrides:

.dbx/toolchain/manifest.json
json
{
    "files": ["packages/core/tsconfig.json"],
    "snapshots": {
        "tsconfig.workspaces.json": { "compilerOptions": { "composite": false } },
        "packages": {
            "core": { "tsconfig.build.json": { "extends": "./tsconfig.default.json" } }
        }
    },
    "tsconfigOverrides": {
        "tsconfig.workspaces.json": {
            "kind": "root",
            "override": { "compilerOptions": { "composite": true, "types": ["node"] } }
        }
    }
}

Removing an override ​

Removing an override (or one of its keys) reverts it on the next sync. For every key the override no longer sets, the sync compares the target file with the snapshot:

  • the snapshot defines the key: the original value is restored (eg. dropping composite: true above brings back the original composite: false);
  • the snapshot does not define it: the key is removed, along with any parent object the override created that is now empty (eg. types);
  • the key holds another value than the override wrote: it was edited after the override applied it, so it is kept and a tsconfig.override-edited warning is logged.

Once no override targets a file anymore, its snapshot and tsconfigOverrides entry are dropped. A file is never deleted, even when nothing else is left in it: other tooling may depend on it.

Restoring ​

dbx tsconfig --restore removes the applied override content right away (with the same snapshot comparison), optionally for one kind only:

sh
dbx tsconfig --restore           # every kind
dbx tsconfig --restore default   # base | root | default | build

If the overrides are still configured, the command warns (tsconfig.restore-reapply): the next dbx sync applies them again, so remove them from toolchain.tsconfig.overrides to keep the restored files. dbx reset does the same restore before it removes the manifest, so resetting a project never strands override content without its snapshots.

Notes ​

  • A target that does not exist is not created: the sync warns (tsconfig.missing-file) and skips that file. A target outside the project root warns with tsconfig.outside-root.
  • The user-owned files are read as JSONC but written back as plain JSON, so comments are dropped from a file the first time an override changes it. A file whose content would not change is never rewritten.
  • dbx sync --check reports pending override changes (and reverts) as drift without writing them.

How generation works ​

For each workspace matched by glob, the generator:

  1. reads its default tsconfig and, when present, the workspaces tsconfig;
  2. writes a sibling tsconfig.json that extends: './<defaultFile>', layering the workspaces compilerOptions (aliases/types) and references on top;
  3. leaves tsconfig.build.json alone — it is a user-owned input, like the default tsconfig.

Because the output only ever extends the default and adds the workspaces wiring, your workspace-specific options stay in the default tsconfig (the file the generator reads, not the one it writes).

Warnings ​

IdWhen
tsconfig.missing-fileAn override targets a file that does not exist (it is skipped).
tsconfig.outside-rootAn override target resolves outside the project root.
tsconfig.override-editedA reverted key was edited after the override set it (it is kept).
tsconfig.restore-reapply--restore ran while the restored overrides are still configured.

Silence any of them with logger.suppress.

When a sync finds no workspace tsconfig files, it warns (in the CLI logs and as a VS Code notification) if either:

  • refs or composite is enabled, or
  • a tsconfig.workspaces.json is present,

because in those cases workspaces are clearly expected. Otherwise it logs a quiet "nothing to do" message.

no workspace tsconfig files found (looked for **/tsconfig.default.json), but project references + tsconfig.workspaces.json have nothing to wire.

The workspaces config ​

tsconfig.workspaces.json holds the paths and references the generator expands. Scaffold it with project:init:monorepo, or build it programmatically from a workspace list:

ts
import { buildMonorepoTsconfig, renderMonorepoTsconfig } from '@deltabox/toolchain/tsconfig';

const config = buildMonorepoTsconfig({
    scope: '@acme',
    workspaces: [
        { alias: 'core', dir: 'packages/core' },
        { alias: 'cli', dir: 'packages/cli' },
    ],
});
// -> { compilerOptions: { composite, paths: { '@acme/core': ['./packages/core/src/index.ts'], ... } },
//      references: [{ path: './packages/core/tsconfig.json' }, ...] }
renderMonorepoTsconfig({ scope: '@acme', workspaces: [{ alias: 'core', dir: 'packages/core' }] });

CLI ​

sh
dbx tsconfig                       # regenerate every workspace tsconfig.json
dbx tsconfig --no-aliases          # skip the workspaces path aliases
dbx tsconfig --refs                # wire project references
dbx tsconfig --composite           # emit composite: true
dbx tsconfig -i 2                  # JSON indentation
dbx tsconfig --glob "packages/*/tsconfig.default.json" "apps/*/tsconfig.default.json"
dbx tsconfig --exclude "**/legacy/**"     # exclude paths (on top of the defaults)
dbx tsconfig --no-gitignore               # do not respect .gitignore files
dbx tsconfig --default-file tsconfig.src.json
dbx tsconfig --build-file tsconfig.dist.json
dbx tsconfig --root-file tsconfig.workspaces.json
dbx tsconfig --restore [kind]             # remove applied overrides (base|root|default|build)

Every flag overrides the matching tsconfig option; with no flags the command uses the config defaults (including in-memory overrides, so the output matches dbx sync). dbx tsconfig --restore [kind] removes applied overrides instead (see Restoring). dbx sync runs the same generator through the tsconfig sync task.

Resolving a tsconfig ​

The same command can inspect a config instead of generating files. It follows extends per field, so a child can define paths while inheriting references (or the reverse):

sh
dbx tsconfig --json               # { alias, paths, references }
dbx tsconfig --alias              # Vite/Vitest resolve.alias entries
dbx tsconfig --paths              # resolved compilerOptions.paths
dbx tsconfig --references         # resolved project references
dbx tsconfig --tsconfig tsconfig.build.json
dbx tsconfig --root packages/app  # root used for detection + relative tsconfig
dbx tsconfig --no-extends         # only the selected file

The output is machine-clean JSON (absolute paths). Exact alias keys are strings; wildcards are anchored regular expressions (rendered as /.../ strings in the CLI JSON), so @acme/core never captures @acme/core-utils. Exact keys sort before wildcards.

Programmatic API ​

ts
import {
    generateTsconfigs,
    runTsconfigCli,
    resolveTsconfig,
    readTsconfig,
    renderTsconfigBase,
    renderTsconfigDefault,
    renderTsconfigBuild,
    renderTsconfigTemplates,
    buildMonorepoTsconfig,
    renderMonorepoTsconfig,
    tsconfigSyncTask,
    tsconfigFileNames,
    restoreTsconfigOverrides,
} from '@deltabox/toolchain/tsconfig';
ExportDescription
generateTsconfigs(options)Expand every default tsconfig (dry-run-able).
runTsconfigCli(argv?)The dbx tsconfig entry point.
resolveTsconfig(options?)Resolve { alias, paths, references } (extends-aware).
readTsconfig(file)Read a JSONC tsconfig (comments + trailing commas).
renderTsconfigBase / renderTsconfigDefault / renderTsconfigBuildRender a template with replacements.
renderTsconfigTemplates(replacements?)Render every template keyed by file name.
buildMonorepoTsconfig(options) / renderMonorepoTsconfig(options)The workspaces config (object / JSON text).
tsconfigSyncTask()The sync task wired into dbx sync.
tsconfigFileNames(config)The configured base/root/default/build file names.
restoreTsconfigOverrides(options)Remove applied overrides ({ cwd, kind?, dryRun? }).
applyOverride / mergeOverride / revertOverrideThe deep-merge + snapshot-aware revert helpers.
getSnapshot / setSnapshot / deleteSnapshotRead/write the manifest snapshot tree by relative path.

resolveTsconfig accepts { tsconfig, cwd, root, extends, overrides }: tsconfig is resolved from root (default cwd), and extends defaults to true. Paths are resolved against the defining config's baseUrl (or its directory), matching TypeScript.

generateTsconfigs returns one result per workspace (with newTsconfig, originalConfig, ...); pass write: false for a dry run. The task-level options are documented in the Programmatic API reference.

renderTsconfigBase / renderTsconfigDefault / renderTsconfigBuild accept an optional second extends argument (string | string[] | false), and renderTsconfigTemplates accepts { base, default, build } as its second argument.

Sync & CI ​

At a monorepo root with the section enabled, dbx sync runs the tsconfig task and reports the regenerated files:

tsconfig: regenerated 6 workspace tsconfig(s).

The task watches the workspaces tsconfig and every default tsconfig (custom rootFile / defaultFile names included), keeps the build tsconfigs, and only applies to monorepo-root projects. Run dbx sync --check in CI to fail when a generated tsconfig.json drifts from its inputs.

Refresh aliases after builds ​

Auto-wired aliases and references are derived from each workspace's package.json (exports, then types/main, then a source scan). If a build step can change a workspace manifest (a generated package.json, a rewritten export map, ...), run the tsconfig-only sync once after all workspace builds:

sh
dbx sync --only tsconfig

This regenerates the workspaces tsconfig and every managed tsconfig.json without running the other sync tasks, and it does not prune files owned by those tasks. Add it to the end of your root build script so the aliases always reflect the final package.json exports:

package.json
json
{
    "scripts": {
        "build": "yarn workspaces foreach -A run build && dbx sync --only tsconfig"
    }
}

The task is skipped when toolchain.tsconfig is omitted or false — including dbx tsconfig and dbx sync --only tsconfig — so a disabled section never generates files. A nested workspace that defines its own tsconfig section owns its generated file: the root still aliases/references it, but does not rewrite it.

Per-project configs ​

toolchain.tsconfig is repository-scoped: the root sync task expands every workspace tsconfig.json, so nested configs do not inherit it — inheriting it would make dbx tsconfig re-scan a workspace's own subtree. A nested config may still declare its own tsconfig section when it manages its own workspace tree (a nested monorepo); otherwise add the workspace's tsconfig.default.json and run dbx tsconfig / dbx sync from the root. Other sections still inherit as usual; see Configuration → Nested configs for the rules.

Scaffolding ​

dbx project:init:monorepo writes tsconfig.base.json + a tsconfig.workspaces.json; dbx project:init:workspace writes the workspace's tsconfig.default.json / tsconfig.build.json / tsconfig.json (and a dbx.config.ts). See the Monorepo Guide for the full flow.