This post is a writeup of a POC I did with Bazel and Svelte. The Bazel rules are still considered experimental, but I have a working Bazel build with Svelte compilation and Rollup bundling.

Bazel

First of all, what is Bazel?

Bazel is the open source version of Google’s internal build system (Blaze). The core design principle behind Bazel is building applications at scale. The idea is to avoid a linear slowdown of builds as the codebase grows huge.

The key to Bazel’s performance is incremental builds. Incremental means Bazel will only rebuild the parts of the application that actually changed after a code change.

If the application has 1000s of modules, but a code change is isolated to a single module, only that module, plus any dependent modules are rebuilt.

How does Bazel keep track of the changes?

Bazel requires us to define a Build graph where all input and output relationships are declared. The build graph defines the granularity of the compilation units in the application. The smaller the unit, the smaller the penalty of a rebuild.

In my POC I have defined compilation units at the Svelte component level since this is the scope of the Svelte compiler.

Build Graph

The build graph dependencies are defined in special BUILD.bazel files.

It's important to note that this only deals with setting up the build of your application. It doesn't impact how you write your Svelte code. Think of this as the Bazel equivalent of defining webpack configs for a webpack build.

Creating the build graph manually is one extra step, but in a more mature setup, the BUILD files can potentially be auto created based on code analysis.

In any event, the burden of creating the BUILD files is IMO manageable.

I have created a new Bazel rule called svelte for defining the dependencies that make up the component.

Below is an example:

svelte( name = "Treeview", srcs = glob(["*.js"]), entry_point = "Treeview.svelte", )

The svelte rule wraps the Svelte compiler under the hood, so you can think of the rule as a way to pass data to the compiler.

The entry_point is the .svelte file for the component. I am also passing in any external .js files in the srcs array.

Svelte components may depend on other Svelte components. Below is an example where a top level component depends on three other svelte rules in the deps array. The other svelte rules are addressed using folder paths. The leading // is Bazel’s way of defining the root of the workspace.

svelte( name = "App", srcs = ["main.js"], entry_point = "App.svelte", deps = [ "//src/frontend/treeview/Treeview", ] )

Rollup

The svelte rule is all we need to compile the components to Javascript, but what about bundling?

To bundle the output from every component into a single file, I am using Rollup. Like before I am wrapping the underlying binary in a Bazel rule.

Below I have defined two bundle rules. One for dev and one for prod

bundle_dev( name = "bundle_dev", entry_point = "main.js", deps = [ ":App" ], ) bundle_prod( name = "bundle_prod", entry_point = "main.js", deps = [ ":App" ], )

In the deps array I specify the top level svelte rule (App) since it defines the full dep graph of Svelte components. The : prefix in front of “App” just means that it’s defined in the same BUILD file as bundle.

The output of the dev bundle rule is an es2015 non minified JavaScript bundle. The prod rule outputs a minified es5 bundle.

Language Agnostic

Fast incremental builds is not the only benefit of using Bazel.

Bazel is created as a language agnostic build system. This means it can be used to create a single build for any supported programming language. The Bazel ecosystem already has wide support for common languages like Java, C++, Typescript, ++.

To show a small example of this I have added a simple Node app to the project. The node app is written in Typescript, which is compiled down to Javascript using the ts_library rule.

I show an example of a full stack build with React + Java here.

Another benefit of building across language boundaries is the possibility of shared typing across different languages.

This is hard to do with Javascript since there are no types, but in the case of Java and Typescript, Bazel can generate shared types for both languages based on a schema.

An example where this is useful is in client – server communication. Synchronizing the types between the server and the client means we can catch mismatches at compile time rather than runtime.

IMO this is a strong argument in favor of Typescript over plain Javascript in web development.

Demo

In the demo I have added watching on file changes using iBazel. This regenerates the bundles for every code change. That said, we still get incrementality. The Svelte compiler will only be called for changed components.

This holds true even if you stop the build. The next time you start the build, it will complete almost instantly since there is nothing to build until the next code change.

I have pushed my POC to Github in case you want to try it out.

Just follow the instructions in the readme to get started.