Build Typescript Project with Bazel Chapter 2: File Structure
In the last chapter, we introduced the basic concept of Bazel. In this blog, I would like to talk about the file structure of Bazel.
Concept and Terminology
Before we introduce the file structure, we need to understand several key concepts and terminology in Bazel.
- Workspace
- Package
- Target
- Rule
These concepts, and terminology, are composed to Build File, which Bazel will analyze, and execute.
The basic relationship among these concepts looks like this
, we will discuss the details one by one.
Workspace
A "workspace" refers to the directories, which contain
- The source files of the project.
- Symbolic links contain the build output.
And the Bazel definition is in a file named WORKSPACE, or WORKSPACE.bazel at the root of the project directory. NOTE, one project can only have one WORKSPACE definition file.
Here is an example of the WORKSPACE file.
workspace(
name = "com_thisdot_bazel_demo",
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# Fetch rules_nodejs so we can install our npm dependencies
http_archive(
name = "build_bazel_rules_nodejs",
sha256 = "ad4be2c6f40f5af70c7edf294955f9d9a0222c8e2756109731b25f79ea2ccea0",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.38.3/rules_nodejs-0.38.3.tar.gz"],
)
load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories", "yarn_install")
node_repositories()
yarn_install(
name = "npm",
package_json = "//:package.json",
yarn_lock = "//:yarn.lock",
)
# Install all Bazel dependencies of the @npm npm packages
load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies")
install_bazel_dependencies()
# Setup the rules_typescript toolchain
load("@npm_bazel_typescript//:index.bzl", "ts_setup_workspace")
ts_setup_workspace()
In a WORKSPACE file, we should
- Define the name of the workspace. The name should be unique globally, or at least unique in your organization. You could use the reverse dns name, such as
com_thisdot_bazel_demo, or the name of the project on GitHub. - Install environment related packages, such as
yarn/npm/bazel. - Setup toolchains needed to
build/testthe project, such astypescript/karma.
Once WORKSPACE is ready, application developers don't really need to touch this file.
Package
- The primary unit of code organization (something like module) in a repository
- Collection of related files and a specification of the dependencies among them
- Directory containing a file named BUILD or BUILD.bazel, residing beneath the top-level directory in the workspace
- A package includes all files in its directory, plus all subdirectories beneath it, except those which, themselves, contain a BUILD file
It is important to know how to split a project into package. It should be easy for the users to develop/test/share the unit of a package. If the unit is too big, the package has to be rebuilt on every package file change. If the unit is too small, it will be very hard to maintain and share. So, this is not an issue of Bazel. It is a general problem of project management.
In Bazel, every package will have a BUILD.bazel file, containing all of the build/test/bundle target definitions.
For example, here is a
of the Angular structure. Every directory under packages directory is a package of code organization, and also the build organization of Bazel.
Let's take a look at the file structure of gulpjs in Angular, so we can have a better understanding about the difference between Bazel and gulpjs.
gulp.task('build-animations', () => {});;
gulp.task('build-core', () => {});
gulp.task('build-core-schematics', () => {});
In most cases,
- a gulpjs file doesn't have 1:1 relationship to the package directory.
- a gulpjs file can reference any files inside the project.
But for Bazel,
- Each
packageshould have their ownBUILD.bazelfile. - The
BUILD.bazelcan only reference the file inside the currentpackage, and if the current package depends on other packages, we need to reference the Bazel buildtargetfrom the other packages instead of the files directly.
Here is a Bazel Package directory structure in Angular repo.

Build File
Before we talk about target, let's take a look at the content of a BUILD.bazel file.
package(default_visibility = ["//visibility:private"])
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "lib",
srcs = [":lib.ts"],
visibility = ["//visibility:public"],
)
The language of the BUILD.bazel file is Starlark.
Starlarkis a subset ofPython.- It is a very feature-limited language. A ton of
Pythonfeatures, such asclass,import,while,yield,lambda,is,raise, are not supported. Recursionis not allowed.- Most of Python's builtin methods are not supported.
So Starlark is a very very simple language, and only supports very limited Python syntax.
Target
The BUILD.bazel file contains build targets. Those targets are the definitions of the build, test, and bundle work we want to achieve.
The build target can represent:
- Files
- Rules
The target can also depend on other targets
- Circular dependencies are not allowed
- Two targets, generating the same output, will cause a problem
- Target dependency must be declared explicitly.
Let's see the previous sample,
package(default_visibility = ["//visibility:private"])
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "lib",
srcs = [":lib.ts"],
visibility = ["//visibility:public"],
)
Here, ts_library is a rule imported from @npm_bazel_typescript workspace, and ts_library(name = "lib") is a target. The name is lib, and this target defines the metadata for compiling the lib.ts with ts_library rule.
Label
Every target has a unique name called label. For example, if the BUILD.bazel file above is under /lib directory, then the label of the target is
@com_thisdot_bazel_demo//lib:lib
The label is composed of several parts.
- the name of the workspace:
@com_thisdot_bazel_demo. - the name of the package:
lib. - the name of the target:
lib.
So, the composition is <workspace name>//<package name>:<target name>.
Most of the times, the name of the workspace can be omitted, so the label above can also be expressed as //lib:lib.
Additionally, if the name of the target is the same as the package's name, the name of the target can also be omitted. Therefore, the label above can also be expressed as //lib.
NOTE: The label for the target needs to be unique in the workspace.
Visibility
We can also define the visibility to define whether the rule inside this package can be used by other packages.
package(default_visibility = ["//visibility:private"])
The visibility can be:
private: the rules can be only used inside the current package.public: the rules can be used everywhere.//some_package:package_scope: the rules can only be used in the specified scope under//some_package. Thepackage_scopecan be:__pkg__/__subpackages__/package group.
And if the rules in one package can be accessed from the other package, we can use load to import them. For example:
load("@npm_bazel_typescript//:index.bzl", "ts_library")
Here, we import the ts_library rule from the Bazel typescript package.
Target
- Target can be
FilesorRule. - Target has
inputandoutput. Theinputandoutputare known at build time. - Target will only be rebuilt when
inputchanges.
Let's take a look at Rule first.
Rule
The rule is just like a function or macro. It can accept named parameters as options. Just like in the previous post, calling a rule will not execute an action. It is just metadata. Bazel will decide what to do.
ts_library(
name = "lib",
srcs = [":lib.ts"],
visibility = ["//visibility:public"],
)
So here, we use the ts_library rule to define a target, and the name is lib. The srcs is lib.ts in the same directory. The visibility is public, so this target can be accessed from the other packages.
Rule Naming
It is very important to follow the naming convention when you want to create your own rule.
*_binary: executable programs in a given language (nodejs_binary)*_test: special _binary rule for testing*_library: compiled module for a given language (ts_library)
Rule common attributes
Several common attributes exist in almost all rules. For example:
ts_library(
name = "lib",
srcs = [":index.ts"],
tags = ["build-target"],
visibility = ["//visibility:public"],
deps = [
":date",
":user",
],
)
- name: unique name within this package
- srcs: inputs of the target, typically files
- deps: compile-time dependencies
- data: runtime dependencies
- testonly: target which should be executed only when running Bazel test
- visibility: specifies who can make a dependency on the given target
Let's see another example:
http_server(
name = "prodserver",
data = [
"index.html",
":bundle",
"styles.css",
],
)
Here, we use the data attribute. The data will only be used at runtime.
It will not be analyzed by Bazel at build time.
So, in this blog, we introduced the basic Bazel structure concepts. In the next blog, we will introduce how to query Bazel targets.

