In short: everything builds with Bazel 9.1.0 and the Aspect JS/TS rules. npm run build is
bazel build //:extension, npm run package builds //:vsix with
vsce, and npm test is bazel test //.... Two in-repo macros wrap
the rules: ts_library compiles a package and drops its test files, and
jasmine_test compiles the tests and runs Jasmine with three helpers. One helper makes
require('vscode') return an empty object, which each test fills with fakes from
src/testing/ by calling installVscode. Another puts a pinned
jj 0.45.1 binary on PATH. Bazel deps and visibility
decide which packages may import Node or npm code. CI runs lint, a license-header check, a lockfile
check and the full test suite.
1. Files
| File | What it holds |
|---|---|
| MODULE.bazel | Bazel modules, the npm lockfile import, and pinned downloads of addlicense and jj. |
| BUILD.bazel | The root targets: extension, vsix, addlicense, tsconfig, jasmine_config, pnpm. |
| .bazelrc, .bazelversion | Test output on errors only, colour, timeout warnings. Bazel 9.1.0. |
| tsconfig.json | Strict CommonJS/ES2020 output with source maps, composite projects, path aliases for Lit and vscode-elements. |
| tools/bazel/ | The ts_library, jasmine_test and addlicense macros, and the jj binary target. |
| spec/support/jasmine.json | Jasmine's config: every *_test.js under the working directory. |
| src/testing/ | The fake VS Code API, the three Jasmine helpers, and newRepo for tests that need a real jj repository. |
| .github/workflows/ | Five workflows: ci, test-postsubmit, Restrict PR Merger and the two reusable ones they call. |
| scripts/populate_vscode_enum.py | Regenerates third_party/vscode/vscode_enums.ts from @types/vscode. |
| .vscodeignore | What vsce leaves out of the .vsix: sources, config, and everything under src/ except compiled JS, source maps, CSS and fonts. |
2. MODULE.bazel
| Field | Type | Meaning | Source |
|---|---|---|---|
module | name, version | jj-dojo 0.0.1, the same version as package.json. | 16–19 |
bazel_dep | 7 modules | aspect_rules_js 3.1.1, aspect_rules_ts 3.8.9, aspect_rules_esbuild 0.26.0, platforms 1.0.0, rules_nodejs 6.7.4, aspect_bazel_lib 2.22.5, rules_shell 0.6.1. | 21–27 |
npm.npm_translate_lock | repo @npm | Imports pnpm-lock.yaml so every npm package is a Bazel target under //:node_modules/…. | 32–40 |
pnpm.pnpm | version from package.json | The pnpm used by //:pnpm to check the lockfile, read from the packageManager field (pnpm@9.1.0). | 43–45 |
rules_ts_ext.deps | repo @npm_typescript | The TypeScript compiler for ts_project. | 47–49 |
addlicense_* | 6 http_archives | addlicense v1.2.0 for Linux, macOS and Windows on x86-64 and arm64, each pinned by SRI hash. | 53–93 |
JJ_VERSION, JJ_PLATFORMS | 5 http_archives | The jj release binary, v0.45.1, for macOS and Linux (musl) on both CPUs and Windows x86-64, each pinned by hash. A comment gives the steps to bump it. | 95–143 |
3. Root targets
| Target | Rule | Meaning | Source |
|---|---|---|---|
//:extension | js_library | What VS Code loads: package.json, the compiled //src, the webview bundle, its styles and the codicons. npm run build builds this. | 41–50 |
//:vsix | vsce binary | Runs vsce package --no-dependencies on the same inputs plus README.md, LICENSE and .vscodeignore, writing jj-dojo.vsix. npm run package builds this. | 57–84 |
//:addlicense.check, .fix | addlicense macro | Checks for, or adds, the license header from license_header.txt. npm run addlicense runs .fix; CI runs .check. | 23–25 |
//:tsconfig | ts_config | The shared tsconfig.json every TypeScript target compiles with. | 29–33 |
//:jasmine_config | js_library | Makes spec/support/jasmine.json available to test runs. | 35–39 |
//:node_modules | npm_link_all_packages | One target per npm package from the lockfile. | 27 |
//:pnpm | alias | The pinned pnpm, so bazel run //:pnpm works without a global install. The dep:add, dep:update and dep:install scripts run it (package.json). | 52–55 |
There is no root //:test target. Each package declares its own :tests, and
npm test runs bazel test //.... Ten packages
have one: //src, //src/logging, //src/utils,
//src/client, //src/file_system_watcher,
//src/ui/icon_theme_service, three under //src/ui/commit_graph
(algorithms, components, utils) and
//third_party/vscode.
4. Restricting dependencies
Inside Google the extension runs on Cider, a VS Code Web fork that can't import packages like
node. The design notes ask for code to be split into interface files and implementation files,
with only implementations allowed to import external-only packages, and use Bazel's deps to
enforce it (docs/intro.md). A ts_project only sees
the type declarations of what is in its deps, so a package without
//:node_modules/@types/node can't compile an import of node:fs.
| Where | How it's applied | Source |
|---|---|---|
//src/file_system_watcher | The clearest example. :file_system_watcher is the interface and depends only on @types/vscode. :local_file_system_watcher is the implementation and adds @parcel/watcher and @types/node. | BUILD.bazel |
//src/ui/commit_graph/utils | Visible only to //src/utils and the commit graph, which is why src/utils re-exports two of its files (see Shared utilities). | BUILD.bazel |
//third_party/vscode:vscode_enums | Visible only to //src/testing, so production code keeps using the real enums from vscode. | BUILD.bazel |
//src | Still one target over every non-test file under src/ that isn't in its own package, visible only to the root package. | BUILD.bazel |
planned A TODO in //src says it should become two
targets: one meant to be imported into Google that depends on little more than vscode, and one
for the rest of the extension that may depend on node
(source).
5. In-repo Bazel rules
ts_library
tools/bazel/typescript/defs.bzl
| Field | Type | Meaning | Source |
|---|---|---|---|
name | string | Target name. | 21 |
srcs | list of paths | Filtered before compiling: a path is dropped if it starts with test_, ends with _test.ts or contains _test_. | 32–36 |
**kwargs | any | Passed to ts_project, which is always called with composite, source_map, the tsc transpiler and //:tsconfig. | 37–45 |
jasmine_test
| Field | Type | Meaning | Source |
|---|---|---|---|
name | string | The test target. A <name>_ts_project is created next to it to compile the sources. | 35–46 |
srcs | list of paths | The *_test.ts files. Compiled with the same options as ts_library, plus @types/jasmine. | 36–46 |
deps | list of labels | What the tests import. | 43 |
args | fixed | --config pointing at spec/support/jasmine.json, and three --helpers: install_global_vscode, install_global_jj and source_map_stack_trace. Paths are made relative by counting the package's depth. | 48–60 |
node_options | fixed | --enable-source-maps, so stack traces point at .ts lines. | 61–63 |
chdir, size, data | fixed | Runs from the package directory as a small test, with the Jasmine config, all npm packages, //tools/bazel/jj and //src/testing available. | 52–72 |
addlicense
tools/bazel/private/addlicense/addlicense.bzl
| Field | Type | Meaning | Source |
|---|---|---|---|
name | string | Prefix for the two sh_binary targets, <name>.fix and <name>.check. | 41–73 |
copyright_holder | string | Defaults to Google LLC. | 34 |
license_type | string | Defaults to apache. | 34 |
extra_ignores | list of globs | Added to DEFAULT_IGNORES: VCS and build directories, lockfiles, .vscode/ and every *.json. | 20–32 |
Both binaries run runner.sh, which finds the
platform's addlicense through Bazel runfiles (picked by a select in
private/addlicense/BUILD.bazel), refuses to run outside
bazel run or without its template, changes to the workspace root and runs it over
. with -f pointing at
license_header.txt,
adding -check for the check target. Both targets carry the template in their runfiles
and pass its runfiles path as the second argument (source). The
template starts with an @license line, then the copyright holder and the Apache text. The public
tools/bazel/addlicense/defs.bzl only
re-exports the macro.
//tools/bazel/jj
copy_jj picks the downloaded jj for the
host's OS and CPU with a select over five config_settings and copies it into
tools/bazel/jj/jj as an executable. Windows on arm64 has no entry here, unlike
addlicense.
6. Tests and the fake VS Code API
Jasmine helpers
jasmine.json sets spec_dir to . and
spec_files to **/*_test.js. Each jasmine_test loads three helpers
before any spec.
| Helper | What it does | Source |
|---|---|---|
install_global_vscode | Patches Module.prototype.require so require('vscode') returns one shared empty object, marked __esModule so import * as vscode works. Without it, importing vscode outside the editor fails. | 28–49 |
install_global_jj | Prepends $TEST_SRCDIR/_main/tools/bazel/jj to PATH, so a test that runs jj gets the pinned binary. It also prepends /usr/local/git/current/bin and /opt/homebrew/bin: jj git clone, fetch and push need Git 2.41 or later, and Bazel's sanitized PATH would otherwise find macOS's older Xcode Git. The src/client tests rely on this. | 23–44 |
source_map_stack_trace | Wraps Error.prepareStackTrace: after Node's source-map handler runs, it swaps the sandbox …runfiles/_main/ prefix for the real workspace path. It finds that path by following the execroot's BUILD.bazel symlink. Paths in failures then open in an editor. | 42–93 |
installVscode
Tests call installVscode in a beforeEach
(for example extension_test.ts). It fills the shared
vscode object in three passes:
- Every enum from
third_party/vscode/vscode_enums.ts, 64 of them, generated from@types/vscode. - Classes:
CancellationTokenSource,CodeLens,Disposable,Position,Range,Selection,ThemeColor,EventEmitter,LogOutputChannel, andUriasvscode-uri'sURIwith itsUtilsmerged in. - Fresh namespace objects:
workspace,languages,commands,window,extensions.
Because the namespaces are new instances each time, every test starts clean. It returns the object typed
as the fakes, so tests can reach helpers the real API doesn't have.
fakeVscode() returns the same thing later, and
logs() returns the fake JJ Extension output
channel.
The fakes
fakes.ts re-exports 23 classes from two directories,
plus fakeCommit, which builds a
MutableCommit with empty defaults and overrides whatever fields the test passes. The
commit-topology tests use it.
| Directory | Classes | Meaning | Source |
|---|---|---|---|
custom_fakes/ | FakeCancellationTokenSource, FakeCodeLens, FakeCommands, FakeDisposable, FakeEventEmitter, FakeExtensions, FakeLanguages, FakeLogOutputChannel, FakePosition, FakeRange, FakeSelection, FakeThemeColor, FakeWebview, FakeWindow, FakeWorkspace | Written for this repository. Methods the code under test doesn't need call unimplemented, which throws and suggests writing the fake. | custom_fakes/ |
google_fakes/ | FakeEnvironmentVariableCollection, FakeExtension, FakeExtensionContext, FakeLanguageModelAccessInformation, FakeMemento, FakeSecretStorage, FakeTextDocument, FakeTextEditor | Brought over from Google's internal tree. Their comments still point at the real implementations by google3/… paths. | google_fakes/ |
//src/testing is a ts_library over all of these
(BUILD.bazel). Six test targets depend on it:
//src, //src/client, //src/logging,
//src/file_system_watcher, //src/ui/icon_theme_service and //src/utils. The others test
plain logic and don't need the fake API.
newRepo
newRepo() is called inside a
describe. Before each test it makes a fresh temporary directory, resolves it with
realpath and runs jj git init there, or, given {remote}, runs
jj git clone from another newRepo's root (which must be declared first, or it throws); after each test it deletes the directory.
It returns the repository plus helpers that run jj in it, write or remove a file by relative
path, and read the working-copy commit's change ID (source).
Callers destructure the result when the describe runs, before any beforeEach,
so the root path lives on a TestRepo object rather than in a string that would be captured
stale (source). It runs jj through the
client's subprocess helper, which is why //src/testing now depends on
//src/client:subprocess_util. The SubprocessClient tests are its only users.
7. CI
| Workflow | Trigger | Meaning | Source |
|---|---|---|---|
ci | pull request, merge queue | Calls _lint_run and _test_run, then all-checks-passed fails unless both succeeded. A newer push to the same PR cancels the running one. | ci.yaml |
test-postsubmit | push to main | Runs _test_run again, without cancelling runs in progress. | test-postsubmit.yaml |
_test_run | called | Sets up Bazel with caches keyed on the lockfile, MODULE.bazel, .bazelrc and tsconfig.json. It checks the pnpm lockfile is in sync (--frozen-lockfile --lockfile-only), then runs bazel test //.... 15-minute limit. | _test_run.yaml |
_lint_run | called | Uses abcxyz/actions to find lint targets, then splits them. JS/TS targets run pnpm install --frozen-lockfile and pnpm run lint (gts lint) on Node 22 and the pnpm named by packageManager. Other targets go to abcxyz's lint-all. A separate job runs bazel run //:addlicense.check. | _lint_run.yaml |
Restrict PR Merger | PR opened, synchronized, reopened | Fails whenever the user who triggered the event isn't the PR's author, so a push to someone else's PR leaves a red check. Its error message says only the author may trigger actions or merge. | restrict-merge.yaml |
Third-party actions are pinned to commit hashes. The Node version is pinned to 22 by hand, and a comment says to keep it in step with the Node that Bazel resolves.
8. Generated VS Code enums
Tests can't import the real enums, because the real vscode module only exists inside the
editor. populate_vscode_enum.py generates
third_party/vscode/vscode_enums.ts instead. It refuses to run unless npm run lint
is clean. Then it reads node_modules/@types/vscode/index.d.ts, pulls out every
export enum with its doc comment using a regex that tolerates one level of braces, writes
them after the original license header and a "do not edit" line, runs npm run fix to format
the file, and copies @types/vscode's LICENSE next to it.
9. Loose ends at this pin
- The
.vsixships no npm packages.vsceruns with--no-dependenciesand the compiled host code is not bundled, so a runtimerequireof an npm package from the extension host would fail in the packaged extension. A TODO says this will matter once@parcel/watcheris used (source). At this pin nothing that reachesactivateimports it. - Stale pointer in a helper.
install_global_vscode.tstells the reader to seevscode_test.tsfor the setup and teardown helpers. No such file exists; the helpers are ininstall_vscode.ts(source). - Prefix match on paths.
ts_library'stest_check usesstartswithon the whole path, sosub/test_x.tsfrom a recursive glob would not be dropped. No such file exists today (source). - No Windows arm64
jj.addlicenseis fetched for six platforms butjjfor five, so tests that needjjcan't build there. - Hand-synced Node version. pnpm is now read from
package.jsonby both Bazel and CI, but Node 22 is still written in the lint workflow and kept in step with the Bazel lockfile only by a comment (_lint_run.yaml). - One
//srctarget. The split between Google-importable code and the rest is still a TODO, so thedepsscheme isn't yet enforced for most ofsrc/.