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 SDK, Node.js, and npm packages) is fetched hermetically by Bazel on the first build. This workspace and consumers build the Go tools from source with rules_go. Prebuilt tools require an explicit release configuration.
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 hit Bazel's content-addressed cache; do not
bazel clean.
Which Binary a Toolchain Resolved¶
//ts/toolchain provides the runnable inspection targets below. They run the binary selected by toolchain resolution, with the arguments after --. Action tools use the execution platform; the launcher and runtime use the target platform.
bazel run //ts/toolchain:oxc_resolved -- --help
bazel run //ts/toolchain:tsgo_resolved -- --version
bazel run //ts/toolchain:node_resolved -- --version
bazel run //ts/toolchain:tools_resolved
bazel run //ts/toolchain:launcher_resolved
The second prints Version 7.0.2 and the third v22.23.1: the typescript
release ts/private/tsgo/pnpm-lock.yaml pins and the Node.js version
MODULE.bazel pins. oxc_resolved builds oxc-bazel first; tools_resolved
prints tsaction's usage and launcher_resolved the launcher's config error,
both built from source by the default toolchains. //tests/integration/tsgo_lockfile checks the compiler version selected by a consumer's lockfile. //tests/integration/new_project uses the default public registration without source overrides. node_resolved is the node the tests/dev_server and tests/lsp
suites run, and the one a tests/integration workspace that makes no
node.toolchain() call runs (//tests/integration/node_version makes one);
//tests/toolchain pins which platform each toolchain's binary comes from.
Pre-Push Hook¶
.githooks/pre-push refuses a push whose working tree differs from HEAD,
tracked edits and untracked files alike; .gitignore covers what a working
checkout carries. A push sends commits, so a file edited but never committed is
not in it. The failure names the files and says 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.
Buildifier¶
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/:
the rule declarations in ts/private/rules/, one action per file in
ts/private/actions/ (rules_go's go/private/{rules,actions} layout)
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:
Updating Rust dependencies¶
Edit oj/Cargo.toml or oxc_cli/Cargo.toml and update its Cargo.lock with Cargo. Run bazel mod deps --lockfile_mode=update and verify the affected native tests. rules_rs resolves both manifests directly; no Cargo.Bazel.lock, vendoring script, or compatibility-floor rendering is maintained. Crate patches and platform-specific build inputs stay in MODULE.bazel annotations.
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, 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¶
A Gazelle run that deletes a test target still satisfies bazel build //...,
bazel test //... and a byte-identical Gazelle rerun. The script compares the
test sources on disk against the srcs of every test target, and again against
only the targets bazel test //... 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 the reason is written 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. git ls-files cannot see an unstaged new
file, so a local run reports green on a test not yet git added.
Retired Names¶
A rule attribute, Gazelle kind, provider, directive, export or file path this
ruleset has retired is named in changelog.d/ and nowhere else. The script
greps every tracked file outside changelog.d/, CHANGELOG.md and the two
history documents for the names it carries, in identifier form, and fails on a
hit. A file that asserts a retired name is absent or inert is listed in
ALLOWED inside the script with the reason; the list is exact in both
directions. It is the third step of the test job.
Coverage Report¶
The suite never runs bazel coverage, and a coverage run whose report is
empty passes. The script runs //tests/vitest/coverage:math_coverage_test
under it twice, with the default --instrumentation_filter and with one naming
//tests/vitest, and compares the combined report's SF: lines against the
files each filter selects. It is the step after the suite in the test job.
Determinism¶
//tests/smoke:hello and the four Go tools built from the empty output bases
DIR/a and DIR/b with no disk or remote cache, and every file of the built
configuration compared byte for byte; the two bases must be empty. It is the
determinism job (docs/CI_CD.md § Determinism Verification).
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¶
cd e2e/basic
bazel run //:pnpm -- install --frozen-lockfile
bazel run //:gazelle -- -mode=diff # prints nothing: the files are Gazelle's
bazel build //...
bazel test //...
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 |
| Retired names | tools/ci/check_retired_names.sh |
No tracked prose or code outside the changelog names a retired attribute, kind, provider, directive, export or path |
Pull Request Process¶
- Fork the repository and create your branch from
main. - Write tests for new behaviour, at unit, integration or e2e level.
- 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; the rest is the entry:
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 `TsInfo` 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.
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 reviews and may request changes. Respond to review comments within two weeks.
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 — rule changes
- gazelle — Gazelle extension
- oxc_cli — Rust CLI
- npm — npm/lockfile support
- toolchain — toolchain registration
- runtime — JS runtime support
- vite — the Vite dev server and plugin under vite/
Examples:
feat(gazelle): list the vitest configs in one tsgo run
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() is empty |
gazelle/program.go |
The tsgo listing of each tsconfig.json, and the foreign projects it does not list; the combined run over the vitest configs; the install check |
gazelle/owner.go |
The run's packages, owner(f), the test/library/declaration split, the unowned report |
gazelle/npm.go |
The lockfile gate, the importer-scoped label, the member table and the member view |
gazelle/manifest.go |
The nearest package.json: its name, its dependencies |
gazelle/generate.go |
One package's rules from the owner map; Empty where no program is |
gazelle/resolve.go |
deps from the listing's edges: one label per edge target |
gazelle/workers_pool.go |
The Workers pool's half: the wrangler config a vitest config names as a filegroup, and a pooled ts_test's wrangler_config, coverage_provider and istanbul dep |
gazelle/config.go |
The root-once lockfile load, the foreign-project mark, the ts_codegen bookkeeping |
gazelle/keep.go |
The managed-attribute reports: what a run drops and what it cannot merge |
gazelle/pnpm_lock.go |
The lockfile's names, importers, links and aliases |
ts/tools/explainfiles/ |
The --explainFiles grammar: one listing's files, roots, edges and types entries -- shared with the build actions |
ts/tools/tsconfig/ |
The tsconfig.json reader -- one file, or its extends chain flattened leaf-wins -- shared with the build actions |
ts/tools/jsonc/ |
JSONC parser, so a commented tsconfig.json still yields its paths |
docs/gazelle/overview.md is the reference for what a run reads and writes;
AGENTS.md carries the contributor rules.
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.