Javascript

Build Typescript Project with Bazel Chapter 2: File Structure

Jia Li
6 min read
bazel

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 still be relevant, but please refer to official documentation 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.

About the author

Jia Li

Jia Li

Architect

Jia Li is a frontend developer with passion, he is an Angular Collaborator and the code owner of angular/zone.js. He loves Angular and now develops Angular enterprise application.

Keep reading

View all posts →

Build Typescript Project with Bazel Chapter 1: Bazel introduction

Bazel is a fast, scalable, universal build tool, especially for big mono repo project, in this blog, I would like to introduce the basic concept of Bazel and how to build a typescript project with it....

Jia Li8 mins
Bazel

This Dot AI Field Notes - Anatomy of a Coding Harness

A coding agent is not magic, it’s a loop. We call this a harness. The harness is a deterministic layer of code that wraps an LLM....

1 min
AI

AI Is Speeding Up Development. But Where Are the New Bottlenecks?

AI is accelerating development, but it’s also exposing everything else that’s broken. At the Leadership Exchange, leaders unpacked how AI is reshaping the SDLC and what organizations need to address beyond just coding to make adoption successful. Moderated by Rob Ocel, VP of Innovation at This Dot Labs, the panel featured Itai Gerchikov at Anthropic and Harald Kirschner, Principal Product Manager for GitHub Copilot & VS Code at Microsoft. Panelists explored the current state of AI adoption across the software development lifecycle and shared practical insights into how organizations can effectively integrate AI tools. Panelists discussed how companies are investing in AI tools, skills, and managed competency programs to support developers. While AI can dramatically accelerate coding, the panel emphasized that adoption affects every stage of the SDLC. Bottlenecks now appear in testing, DevOps, product delivery, and marketing as AI speeds up development. Organizations that address technical debt and process inefficiencies are better positioned to extract maximum value from AI tools. The conversation also focused on opportunities and risks. Security, governance, and workforce education were highlighted as critical factors for adoption. Panelists stressed that AI initiatives should be aligned with broader business goals rather than pursued in isolation. They noted that companies experimenting at the cutting edge need to consider organizational readiness just as carefully as technical capabilities. Panelists also explored how leading organizations are navigating the early stages of adoption. Those ahead of the curve are using structured experimentation, prioritizing process improvements, and continuously evaluating outcomes to refine their AI strategies. Learning from these early adopters allows other organizations to anticipate emerging trends and prepare for the next phase of AI adoption rather than simply replicating past approaches. Key Takeaways Investing in AI skills and tools should be done thoughtfully, with clear alignment to business objectives. Examining the full SDLC helps identify bottlenecks that AI may accelerate or expose. Organizations can gain a competitive advantage by learning from early adopters and planning for where AI adoption is heading. AI adoption is not just a technical initiative; it is a strategic transformation that requires attention to people, process, and technology. Organizations that balance innovation with operational discipline will be best positioned to capture the full potential of AI across the software lifecycle. Seeing similar challenges in your own SDLC? Let’s compare notes. Join us at an upcoming Leadership Exchange or reach out to continue the conversation. Tracy can be reached at tlee@thisdot.co....

Calypso Hernandez2 mins
AI AdoptionAILeadership ExchangeEngineering Leadership

Making AI Deliver: From Pilots to Measurable Business Impact

A lot of organizations have experimented with AI, but far fewer are seeing real business results. At the Leadership Exchange, this panel focused on what it actually takes to move beyond experimentation and turn AI into measurable ROI. Over the past few years, many organizations have experimented with AI, but the challenge today is translating experimentation into measurable business value. Moderated by Tracy Lee, CEO at This Dot Labs, panelists featured Dorren Schmitt, Vice President IT Strategy & Innovation at Allen Media Group, Greg Geodakyan, CTO at Client Command, and Elliott Fouts, CAIO & CTO at This Dot Labs. Panelists discussed how companies are moving from early AI experiments to initiatives that deliver real results. They began by examining how experimentation has evolved over the past year. While many organizations did not fully utilize AI experimentation budgets in 2025, 2026 is showing a shift toward more intentional investment. Structured budgets and clearly defined frameworks are enabling companies to explore AI strategically and identify initiatives with high potential impact. The conversation then turned to alignment and ROI. Panelists highlighted the importance of connecting AI projects to corporate strategy and leadership priorities. Ensuring that AI initiatives translate into operational efficiency, productivity gains, and measurable business impact is essential. Companies that successfully align AI efforts with organizational goals are better equipped to demonstrate tangible outcomes from their investments. Moving from pilots and proofs of concept to production was another major focus. Governance, prioritization, and workflow integration were cited as essential for scaling AI initiatives. One panelist shared that out of nine proofs of concept, eight successfully launched, resulting in improvements in quality and operational efficiency. Panelists also explored the future of AI within organizations, including the potential for agentic workflows and reduced human in the loop processes. New capabilities are emerging that extend beyond coding tasks, reshaping how teams collaborate and how work is structured across departments. Key Takeaways Structured experimentation and defined budgets allow organizations to explore AI strategically and safely. Alignment with business priorities is essential for translating AI capabilities into measurable outcomes. Governance and workflow integration are critical to moving AI initiatives from pilot stages to production deployment. Successfully leveraging AI requires a balance between experimentation, strategic alignment, and operational discipline. Organizations that approach AI as a structured, measurable initiative can capture meaningful results and unlock new opportunities for innovation. Curious how your organization can move from AI experimentation to real impact? Let’s talk. Reach out to continue the conversation or join us at an upcoming Leadership Exchange. Tracy can be reached at tlee@thisdot.co....

Calypso Hernandez2 mins
AI AdoptionAILeadership ExchangeEngineering Leadership