This document details the steps to migrate a package to build with Bazel. These steps are easiest to understand with a working example, so this doc references tfjs-core
's setup as much as possible. Since this migration is still in the early phases, the steps and processes listed here may change as we improve on the process, add features to each package's build, and create tfjs-specific build functions.
Migrating a package to Bazel involves adding Bazel targets that build the package, run its tests, and pack the package for publishing to npm. To ensure a seamless transition to Bazel, we're maintaining the original build system and building the Bazel build system in parallel with it. When transitioning a package to build with Bazel, it's important not to break the current build system used for that package.
- Bazel will only make a dependency or file available to the build if you explicitly declare it as a dependency / input to the rule you're using.
- All Bazel builds use the root package.json, so you may have to add packages to it. As long as you use
@npm//dependency-name
in BUILD files to add dependencies, you won't need to worry about the build accidentally seeing the package'snode_modules
directory instead of the rootnode_modules
. Bazel will only make the rootnode_modules
directory visible to the build.- Even though the build doesn't use the package's
node_modules
, you may have to runyarn
within the package to get code completion to work correctly. I'm looking into why this is the case.
- Even though the build doesn't use the package's
- There may be issues with depending on explicitly pinned versions of
@tensorflow
scoped packages, which might affect some demos if they're migrated to use Bazel. We might just want to leave demos out of Bazel so they're easier to understand.
These steps are general guidelines for how to build a package with Bazel. They should work for most packages, but there may be some exceptions (e.g. wasm, react native).
A package's dependencies must be migrated before it can be migrated.
Bazel looks for targets to run in BUILD
and BUILD.bazel
files. Use the .bazel
extension since blaze uses BUILD
. You may want to install an extension for your editor to get syntax highlighting. Here's the vscode extension.
ts_project compiles the package. It runs tsc --project
on typescript source files.
Here's an example of how tfjs-core
uses ts_project
to build.
load("@npm//@bazel/typescript:index.bzl", "ts_project")
TEST_SRCS = ["src/setup_test.ts"]
ts_project(
name = "tfjs-core_lib", # Other targets reference this target using this name
srcs = glob( # .ts sources to compile
["src/**/*.ts"],
exclude = TEST_SRCS,
),
declaration = True, # From tsconfig.json
extends = "//:tsconfig.json", # The package's tsconfig extends the root tsconfig
incremental = True, # From tsconfig.json
out_dir = "dist", # From tsconfig.json
root_dir = "src", # From tsconfig.json
source_map = True, # From tsconfig.json
tsconfig = "tsconfig.json", # Use the package's tsconfig
deps = [ # Other targets this library depends on, including npm packages
"@npm//@types", # and other tfjs packages. See tfjs-backend-cpu/BUILD.bazel for
"@npm//jasmine-core", # an example of including other tfjs packages.
"@npm//seedrandom",
],
)
This step involves bundling the compiled files from the compilation step into a single file. TFJS generates several bundles for each package in order to support different execution environments. At the moment, the instructions here do not create es5 compatible bundles. These instructions will change in the future, and it's likely that we will have a single tfjs_bundle
function that can be imported to create all the necessary bundles instead of having to write them all out separately. At the moment, however, you'll need to use the esbuild rule for each bundle listed in package.json
.
load("//:esbuild.bzl", "esbuild")
esbuild(
name = "tf-core.min", # This should match package.json. The output file has '.js' appended automatically.
entry_point = "dist/index.js", # This should match the 'input' field in the package's rollup file
# except the path will be to the compiled .js file instead of the .ts file.
external = [ # These are external to the bundle. They can be copied from the rollup file.
"node-fetch",
"util",
],
minify = True,
sources_content = True, # Tell esbuild to include source file content in the generated sourcemaps instead
# of referencing files. Sourcemaps are broken without this. This does not increase
# bundle size since it only affects the sourcemaps.
deps = [
":tfjs-core_lib",
],
)
In order to make import {something} from '@tensorflow/tfjs-core'
work when we use tfjs-core
in other packages, we need to declare the package using js_library
. This is not very well documented at the moment, and may eventually be replaced with pkg_npm
. See this article, this section of documentation, and this issue for more info.
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
js_library(
name = "tfjs-core",
package_name = "@tensorflow/tfjs-core", # The name that can be imported in other .ts files
srcs = [
"package.json",
],
deps = [
":tf-core.min", # Include all bundles.
":tf-core.node",
":tfjs-core_lib", # ...and include the compiled files.
],
)
Since (in most cases) the tests are not published, we don't have to worry about where their files end up when compiled. We can use ts_library to compile them.
load("@npm//@bazel/typescript:index.bzl", "ts_library")
ts_library(
name = "tfjs-core_test_lib",
testonly = True, # This rule can only be used by tests.
srcs = [ # .ts files to compile. In most cases, this would be 'TEST_SRCS' as defined
"setup_test_bazel.ts", # above, but tfjs-core is a special case and couldn't use the current
], # 'setup_test.ts' file. We include a separate 'setup_test_bazel.ts' file instead.
tsconfig = "//:tsconfig_ts_library.json", # Use a different tsconfig because ts_library doesn't support 'incremental'.
deps = [
":tfjs-core",
"@npm//@types",
"@npm//jasmine-core",
"@npm//seedrandom",
],
)
jasmine_node_test can run Jasmine tests that run in node. There aren't actually any node tests added to the repo yet (so if you add one, please update this section with an example :).
Like the section on bundling, this section will likely be changed to use a single custom tfjs_web_test
rule instead of several different rules to set up tests. Right now, we use esbuild
to bundle the tests into a single file and karma_web_test to serve them to a browser to be run.
esbuild(
name = "tfjs-core_test_bundle",
testonly = True,
entry_point = "setup_test_bazel.ts",
external = [
# webworker tests call 'require('@tensorflow/tfjs')', which
# is external to the test bundle.
# Note: This is not a bazel target. It's just a string.
"@tensorflow/tfjs",
],
sources_content = True,
deps = [
":tfjs-core_lib",
":tfjs-core_test_lib",
"//tfjs-backend-cpu",
],
)
karma_web_test(
name = "tfjs-core_test",
srcs = [
":tfjs-core_test_bundle",
],
static_files = [
# Listed here so sourcemaps are served
":tfjs-core_test_bundle",
],
tags = ["native"],
)
To run tests in browserstack, we use another karma_web_test
target:
karma_web_test(
name = "browserstack_tfjs-core_test",
srcs = [
":tfjs-core_test_bundle",
],
config_file = "//:karma.conf.js", # This config file launches tests in BrowserStack
peer_deps = [ # These deps were automatically added in the prior
"@npm//karma", # rule, but since we're adding `karma-browserstack-launcher`,
"@npm//karma-jasmine", # we have to specify all of them manually.
"@npm//karma-requirejs",
"@npm//karma-sourcemap-loader",
"@npm//requirejs",
"@npm//karma-browserstack-launcher",
],
static_files = [
# Listed here so sourcemaps are served
":tfjs-core_test_bundle",
],
tags = ["ci"],
)