Monorepo Layout¶
rules_typescript is designed for monorepos. The recommended layout:
my-monorepo/
├── MODULE.bazel
├── pnpm-lock.yaml # single lockfile for all packages
├── packages/
│ ├── ui/
│ │ ├── BUILD.bazel # ts_compile(name = "ui", ...)
│ │ └── index.ts
│ ├── utils/
│ │ ├── BUILD.bazel # ts_compile(name = "utils", ...)
│ │ └── index.ts
│ └── config/
│ ├── BUILD.bazel
│ └── index.ts
└── apps/
└── server/
├── BUILD.bazel # ts_compile that depends on //packages/ui, //packages/utils
└── main.ts
Package Boundaries¶
A directory should have its own ts_compile target when:
- It has an
index.tsthat forms a public API (Gazelle auto-detects this). - Other packages import from it — cross-package imports must go through the
ts_compiletarget. - It will be published as a separate npm package.
# packages/utils/BUILD.bazel
load("@rules_typescript//ts:defs.bzl", "ts_compile")
ts_compile(
name = "utils",
srcs = ["index.ts", "string.ts", "number.ts"],
visibility = ["//visibility:public"], # allow other packages to depend on this
)
# apps/server/BUILD.bazel
load("@rules_typescript//ts:defs.bzl", "ts_compile")
ts_compile(
name = "server",
srcs = ["main.ts"],
deps = [
"//packages/utils",
"//packages/ui",
"@npm//:express",
],
)
Cross-Package Dependencies¶
.d.ts files are the compilation boundary between packages:
# //lib/BUILD.bazel
ts_compile(
name = "lib",
srcs = ["math.ts"],
visibility = ["//visibility:public"],
)
# //app/BUILD.bazel
ts_compile(
name = "app",
srcs = ["main.ts"],
deps = ["//lib"],
)
If lib/math.ts changes but its exported types don't change, app is not recompiled. Bazel's content-based caching uses the .d.ts fingerprint as the dependency boundary.
Using Gazelle¶
Run Gazelle once to generate BUILD files for the entire monorepo:
Gazelle creates ts_compile targets for every directory with TypeScript files, resolves import paths to Bazel labels, and generates ts_test targets for test files. After adding new source files or packages, re-run Gazelle to update BUILD files.
Single pnpm Lockfile¶
Use a single pnpm-lock.yaml at the repo root covering all packages. The npm_translate_lock extension reads this one file and creates the @npm repository that all packages share. This is simpler than per-package lockfiles and avoids version conflicts.
Visibility¶
Set visibility = ["//visibility:public"] on packages that other workspaces depend on. Keep leaf-node packages at ["//visibility:private"] unless needed externally.