Typed CSS modules with Bazel
Lewis Hemens
Posted on June 23, 2019
If you are building TypeScript with Bazel and using CSS, you probably want it to be typed. Tools such as Webpack can generate .d.ts
files for you, however Webpack plugins do this by adding the .d.ts
files to your source directory.
This is a no go in Bazel, where you generally can't / shouldn't mutate the source during a build step.
This can be accomplished fairly easily with a custom build rule.
Note: the following has been tested with Bazel 0.26.1 and rules_nodejs 0.31.1.
All the following code snippets are from an open-source project and you can see them working in their entirety here: https://github.com/dataform-co/dataform
The problem
Assume we have a CSS file called styles.css
containing the following:
.someClass {
color: #fff;
}
.someClass .someOtherClass {
display: flex;
}
And we want to generate a styles.css.d.ts
that looks like this:
export const someClass: string;
export const someOtherClass: string;
So that we can provide this as an input to any consuming rules such as ts_library
to make sure we aren't using any class names that don't exist and to keep the TS compiler happy.
Using typed-css-modules
We can do this using the typed-css-modules
package.
First we want to set up a nodejs_binary
that we can use to generate these typings.
Firstly add this to your root package.json
assuming you manage your projects dependencies with rules_nodejs
and npm_install/yarn_install
rules, then add the following to a BUILD
file somewhere in your repo (we keep this in the root BUILD
file):
nodejs_binary(
name = "tcm",
data = [
"@npm//typed-css-modules",
],
entry_point = "@npm//node_modules/typed-css-modules:lib/cli.js",
)
This allows us to run the TCM CLI from within Bazel rules.
Writing the Bazel rule
Now we need to define a Bazel rule that can actually run the TCM CLI and generate typings files. Add a new file called css_typings.bzl
somewhere in your repository with the following:
def _impl(ctx):
outs = []
for f in ctx.files.srcs:
# Only create outputs for css files.
if f.path[-4:] != ".css":
fail("Only .css file inputs are allowed.")
out = ctx.actions.declare_file(f.basename.replace(".css", ".css.d.ts"), sibling = f)
outs.append(out)
ctx.actions.run(
inputs = [f] + [ctx.executable._tool],
outputs = [out],
executable = ctx.executable._tool,
arguments = ["-o", out.root.path, "-p", f.path, "--silent"],
progress_message = "Generating CSS type definitions for %s" % f.path,
)
# Return a structure that is compatible with the deps[] of a ts_library.
return struct(
files = depset(outs),
typescript = struct(
declarations = depset(outs),
transitive_declarations = depset(outs),
type_blacklisted_declarations = depset(),
es5_sources = depset(),
es6_sources = depset(),
transitive_es5_sources = depset(),
transitive_es6_sources = depset(),
),
)
css_typings = rule(
implementation = _impl,
attrs = {
"srcs": attr.label_list(doc = "css files", allow_files = True),
"_tool": attr.label(
executable = True,
cfg = "host",
allow_files = True,
default = Label("//:tcm"),
),
},
)
This is a fairly simple Bazel build rule that generates CSS typings using the TCM CLI we installed previously.
- Iterates through each input CSS file
- Runs the TCM tool against the CSS file to generate typings
- Returns something that is compatible as an input to
ts_library
rules and can be added as a dependency
You will need to replace the Label("//:tcm")
line with the name of the nodejs_binary
rule you created in the previous step.
Putting it all together
Here is a simple example BUILD
file that uses this rule, assuming you put the css_typings.bzl
file in a tools
directory:
filegroup(
name = "css",
srcs = glob(["**/*.css"]),
)
load("//tools:css_typings.bzl", "css_typings")
css_typings(
name = "css_typings",
srcs = [":css"],
)
load("@npm_bazel_typescript//:index.bzl", "ts_library")
ts_library(
name = "components",
srcs = glob([
"**/*.ts",
"**/*.tsx",
]),
data = [
":css",
],
deps = [
":css_typings",
...
],
)
You probably still need to actually bundle the CSS files using the data
attribute above, assuming that this library is going to bundled for web by some downstream consumer.
And that should be all you need to generate CSS typings with Bazel Typescript projects!
If this was useful to you, please let me know and I'll put it into a repository so it can be imported and used without the copy-paste.
Posted on June 23, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.