DEV Community

Lewis Hemens
Lewis Hemens

Posted on

Typed CSS modules with Bazel

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.

Latest comments (0)