Skip to content

Build Typescript Project with Bazel Chapter 2: File Structure

Build Typescript Project with Bazel Chapter 2: File Structure

Build Typescript Project with Bazel - 1 Part Series

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

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 graph, we will discuss the details one by one.

Workspace

A "workspace" refers to the directories, which contain

  1. The source files of the project.
  2. 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

  1. 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.
  2. Install environment related packages, such as yarn/npm/bazel.
  3. Setup toolchains needed to build/test the project, such as typescript/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 screenshot 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 package should have their own BUILD.bazel file.
  • The BUILD.bazel can only reference the file inside the current package, and if the current package depends on other packages, we need to reference the Bazel build target from the other packages instead of the files directly.

Here is a Bazel Package directory structure in Angular repo. Angular

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.

  • Starlark is a subset of Python.
  • It is a very feature-limited language. A ton of Python features, such as class, import, while, yield, lambda, is, raise, are not supported.
  • Recursion is 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.

  1. the name of the workspace: @com_thisdot_bazel_demo.
  2. the name of the package: lib.
  3. 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. The package_scope can 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 Files or Rule.
  • Target has input and output. The input and output are known at build time.
  • Target will only be rebuilt when input changes.

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.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co