Contributing to rules_typescript¶
Table of Contents¶
- Development Environment
- Code Style
- Running Tests
- Pull Request Process
- Commit Message Format
- Reporting Security Issues
- Contributor License Agreement
- Gazelle Extension Architecture
Development Environment¶
The only prerequisite is Bazelisk (or Bazel 9+). Every other dependency — the Rust toolchain, Go toolchain, Node.js, and npm packages — is fetched hermetically by Bazel on the first build.
Install Bazelisk¶
# macOS (Homebrew)
brew install bazelisk
# Linux / macOS (manual)
curl -Lo ~/.local/bin/bazel \
https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64
chmod +x ~/.local/bin/bazel
# Windows (Scoop)
scoop install bazelisk
Clone and Verify¶
git clone https://github.com/mikn/rules_typescript
cd rules_typescript
# Build everything
bazel build //...
# Build with type checking
bazel build //... --output_groups=+_validation
# Run all tests
bazel test //...
The first build fetches a Rust toolchain, a Go SDK, Node.js and tsgo, then
compiles oxc-bazel and its crate graph from source. The Rust compile takes
minutes. Subsequent builds are fast via Bazel's content-addressed cache, so do
not bazel clean afterwards.
Enable the pre-push hook¶
.githooks/pre-push refuses a push whose working tree differs from HEAD —
tracked edits and untracked files alike, since .gitignore already covers what
a working checkout really does carry. A push sends commits, so a file you
edited but never committed is not in it: three PRs in one day were pushed
missing a file that only existed in the author's worktree, and one of them
turned main red for hours. The failure names the files and tells you to
commit or stash; git push --no-verify pushes anyway.
It is opt-in per clone, because core.hooksPath is repository config and
repository config is not checked in. A linked worktree inherits it from the
clone it was created from.
Optional: buildifier for Starlark formatting¶
See Starlark under Code Style.
Code Style¶
One formatter per language. The lint CI job checks Starlark and Go; Rust and
TypeScript formatting are local conventions.
Starlark (BUILD files and .bzl files)¶
Use buildifier:
# buildifier is not a bazel_dep — there is no @buildifier repo to run. Use the
# released binary, which is what CI checks with.
curl -fsSL -o /usr/local/bin/buildifier \
https://github.com/bazelbuild/buildtools/releases/download/v8.2.1/buildifier-linux-amd64
chmod +x /usr/local/bin/buildifier
buildifier -r . -exclude_patterns='bazel-*,.*' # format
buildifier --mode=check -r . # what CI runs
Key conventions (see also AGENTS.md):
- ctx.actions.run over ctx.actions.run_shell wherever possible
- depset(order = "postorder") for transitive file sets
- args.add_all() for file lists — never materialize depsets at analysis time
- Private attrs prefixed with _
- Public rules exposed from defs.bzl; raw implementations in ts/private/
Go (Gazelle extension)¶
Use gofmt:
The Gazelle extension lives in gazelle/. Run its tests with:
Rust (oxc_cli)¶
Use rustfmt:
The Rust CLI lives in oxc_cli/. Build with:
Repinning Crate Dependencies¶
Two crate_universe hubs are pinned by checked-in lockfiles. rules_rust
requires a lockfile for a hub declared by a non-root module, and rules_typescript
is a dependency in a consumer's build, where repinning across the module boundary
is not possible:
| Hub | Rendering | Cargo resolution |
|---|---|---|
@crates |
oxc_cli/Cargo.Bazel.lock |
oxc_cli/Cargo.lock |
@oj_crates |
oj/Cargo.Bazel.lock |
oj/Cargo.lock |
After editing oxc_cli/Cargo.toml or the crate.spec for oj in
MODULE.bazel, regenerate all four:
A rules_rust bump also invalidates the renderings: the digest covers the cargo
and rustc versions rules_rust pins, so repin in the same commit as the bump.
Without it, every build fails with "The current lockfile is out of date".
TypeScript (test fixtures and e2e workspaces)¶
Use prettier (if you have it locally). The TypeScript files in tests/ and
e2e/ are fixtures — keep them minimal and readable, illustrating the feature
under test. CI does not lint them.
Running Tests¶
Unit tests and type checking (main repo)¶
# Run all tests
bazel test //...
# Run tests and type-check all targets
bazel build //... --output_groups=+_validation
# Run a specific test suite
bazel test //tests/vitest:math_test
bazel test //gazelle/...
Test Source Coverage¶
bazel test //... passing does not mean your test ran. A Gazelle run that
deletes a test target satisfies bazel build //..., bazel test //... and a
byte-identical Gazelle rerun alike. This script compares the test sources on disk
against the srcs of every test target, and again against only the targets
bazel test //... actually runs, so a target tagged manual does not count as
coverage.
If a file's only target is manual, add the file to MANUAL_ONLY inside the
script with the reason it cannot run. The list is exact in both directions:
tagging a test manual fails CI until someone writes the reason down, and
untagging it fails until the entry is removed.
The script is read-only: a loading-phase query and git ls-files. It is the
first step of the test job in CI. One local caveat — git ls-files cannot see
an unstaged new file, so a local run reports green on a test you have not
git added yet.
Integration Tests¶
Integration tests spin up an isolated Bazel workspace each to verify end-to-end
user journeys. They are part of bazel test //..., need no environment variable,
and carry the tags nested-bazel and cpu:2, so Bazel runs as many at once as
the machine has cores for:
bazel test //tests/integration/...
bazel test //tests/integration:new_project_test --test_output=all
bazel test //tests/integration:existing_project_test --test_output=all
bazel test //tests/integration:npm_deps_test --test_output=all
bazel test //tests/integration:gazelle_roundtrip_test --test_output=all
They are slow (each spawns a nested Bazel). To iterate on everything else, use
--config=fast, whose --test_tag_filters=-nested-bazel drops them:
End-To-End Workspace Tests¶
Test Matrix Summary¶
| Suite | Command | What it covers |
|---|---|---|
| Smoke | bazel test //tests/smoke/... |
Single-file .ts and .tsx compilation |
| Multi-package | bazel test //tests/multi/... |
Cross-package deps, .d.ts boundary |
| Vitest | bazel test //tests/vitest/... |
ts_test + vitest runner |
| Bundle | bazel test //tests/bundle/... |
ts_binary bundling |
| npm | bazel test //tests/npm/... |
npm package targets from pnpm-lock.yaml |
| Integration | bazel test //tests/integration/... |
Full user-journey tests, each in a nested Bazel workspace (tagged nested-bazel) |
| LSP | bazel test //tests/lsp/... |
The tsserver resolution hook against a real tsserver |
| Gazelle | bazel test //gazelle/... |
Gazelle extension unit tests |
| E2E | cd e2e/basic && bazel build //... |
Real consumer workspace |
| Test-source coverage | tools/ci/check_test_sources.sh |
Every tracked test source is claimed by a target that runs |
Pull Request Process¶
- Fork the repository and create your branch from
main. - Write tests for new behaviour. The project has coverage at unit, integration and e2e level — add tests at the appropriate one.
- Run the full test suite before opening a PR:
- Add a changelog entry — a new file in
changelog.d/, not an edit toCHANGELOG.md. Its first line is the###section the entry belongs under, and the rest is the entry itself, in as much prose as the change needs:
cat > changelog.d/ts-binary-js-entry.md <<'EOF'
### Added
- **`ts_binary` takes a plain JavaScript file as its `entry_point`.** The
attr is polymorphic: a target providing `JsInfo` behaves exactly as before.
EOF
bazel run //tools/changelog # prints the section as it will read
changelog.d/README.md lists the sections and the rules. A release folds the
fragments into CHANGELOG.md; editing CHANGELOG.md directly is what put
nine PRs in one day into a rebase over the same few added lines.
5. Update documentation — a public-API change (rule attributes, providers,
directives) lands with its page under docs/ in the same PR, plus README.md
and AGENTS.md where they say the same thing. mkdocs build --strict runs in
the lint job, so a nav entry without a page, or a link to a page that does
not exist, fails CI.
6. Open the PR against main with the provided pull request template filled in.
7. A maintainer will review and may request changes. Please respond to review comments within a reasonable time (two weeks is a good guideline).
8. Once approved, a maintainer will squash-merge your PR.
What Makes a Good PR¶
- One logical change per PR. Stacked changes are welcome as separate PRs with clear dependency notes.
- Every breaking change carries a
changelog.d/entry under a### Breaking — <area>heading, stating the edit a consumer has to make. Pre-1.0 there is no deprecation window and no compatibility shim (see COMPATIBILITY.md). - No
bazel cleanin scripts or documentation. Trust the cache. - Never reference
bazel-out/directly in Starlark. Usectx.bin_dir.path,File.path,File.dirname.
Commit Message Format¶
Use the Conventional Commits format:
Types:
- feat — a new feature
- fix — a bug fix
- docs — documentation only
- refactor — code change that neither fixes a bug nor adds a feature
- test — adding or correcting tests
- chore — maintenance (dependency updates, build scripts, toolchain bumps)
Scopes (optional, use when helpful):
- ts_compile, ts_test, ts_binary, ts_bundle — rule changes
- gazelle — Gazelle extension
- oxc_cli — Rust CLI
- npm — npm/lockfile support
- toolchain — toolchain registration
- runtime — JS runtime support
- vite — Vite bundler integration
Examples:
feat(gazelle): add ts_path_alias directive support
fix(ts_compile): pass rootDirs to tsgo for bin_dir resolution
docs: update COMPATIBILITY.md for Bazel 9.x support
chore(toolchain): bump oxc to 0.120.0
The subject line is 72 characters or fewer. The body carries the reason for the change.
Reporting Security Issues¶
Do not open a public GitHub issue for security vulnerabilities.
Email security issues to the maintainers directly. Include: - A description of the vulnerability - Steps to reproduce or a proof-of-concept - The affected version(s)
We will acknowledge your report within 72 hours and work with you on a coordinated disclosure timeline.
Contributor License Agreement¶
Contributions to this project are made under the MIT License (the same license as the project itself). By submitting a pull request, you agree that your contribution is licensed under the MIT License and that you have the right to grant that license.
There is no separate CLA to sign.
Gazelle Extension Architecture¶
The Gazelle extension lives in gazelle/ and is a standard Gazelle language
extension written in Go.
| File | Role |
|---|---|
gazelle/language.go |
Entry point — registers the language, Kinds(), Loads(), KnownDirectives() |
gazelle/config.go |
Directive parsing (# gazelle:ts_*), gazelle_ts.json, framework and codegen detection |
gazelle/generate.go |
Rule generation — produces ts_compile, ts_test and the rest |
gazelle/resolve.go |
Import resolution — maps import specifiers to Bazel labels |
gazelle/imports.go |
Import extraction from TypeScript sources |
gazelle/jsonc.go |
JSONC parser, so a commented tsconfig.json still yields its paths |
gazelle/framework_bundle.go |
Vite-based framework bundle targets — TanStack Start, Remix, SvelteKit (Next.js has its own, framework_next.go) |
gazelle/codegen.go |
Auto-detected codegen targets |
AGENTS.md is the architectural reference for contributors: package boundary
heuristics, import resolution strategy, the directive reference, and
gazelle_ts.json migration notes.
The extension is compiled into two gazelle_binary targets. //gazelle:gazelle_ts
is the one this repo runs, through the gazelle runner beside it:
It also carries the Go and proto languages, because this repo generates BUILD
files for its own .go sources. //gazelle:gazelle_typescript is the exported
one and carries TypeScript alone, so it never rewrites a consumer's Go BUILD
files. A consumer workspace declares its own gazelle target pointing at
@rules_typescript//gazelle:gazelle_typescript — e2e/basic/BUILD.bazel is the
worked example — and runs bazel run //:gazelle.