Typed CSS modules with Bazel
Tags: bazel, css, typescript
Published: 2019-06-23 13:58:09

If you are building TypeScript with [Bazel](https://bazel.build) 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](https://github.com/bazelbuild/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: 

```css
.someClass {
  color: #fff;
}

.someClass .someOtherClass {
  display: flex;
}
```

And we want to generate a `styles.css.d.ts` that looks like this:

```typescript
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):

```py
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:

```py
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:

```py
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.