-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refs #41. See #41 (comment) for more info on the design of this ruleset. This includes rules for `css_library()` and `css_binaries()` along with supporting providers and rules. The core design here is that `css_library()` targets return a `CssInfo` provider which tracks direct and transitive sources. `css_binaries()` uses them to generate a bundled CSS file for every direct CSS source file of its direct `css_library()` dependencies. These bundled files have resolved all of their imports and inlined them. `css_binaries()` also provides a `CssImportMapInfo` which maps importable CSS file names to their actual file path on disk. This solves two problems: 1. Users shouldn't be writing artifact roots in their code, but the files actually being imported do have artifact roots and may exist in many configurations. 2. `css_binaries()` bundle source files from dependency `css_library()` targets which may exist in different packages. As a result, the generated bundle may not have the same root-relative path as the library file that the user wants to import. `css_group()` provides `filegroup()`-like semantics for merging multiple `css_binary()` targets into a single target with a cohesive `CssImportMapInfo` provider. I did my best to include tests for the major interactions between these rules, hopefully this should provide some reasonable confidence going forward.
- Loading branch information
Showing
25 changed files
with
694 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
load("@bazel_skylib//:bzl_library.bzl", "bzl_library") | ||
|
||
bzl_library( | ||
name = "css_binaries", | ||
srcs = ["css_binaries.bzl"], | ||
deps = [ | ||
":css_group", | ||
":css_map", | ||
":css_providers", | ||
"//packages/rules_prerender:postcss_import_plugin", | ||
], | ||
) | ||
|
||
bzl_library( | ||
name = "css_group", | ||
srcs = ["css_group.bzl"], | ||
deps = [":css_providers"], | ||
) | ||
|
||
bzl_library( | ||
name = "css_library", | ||
srcs = ["css_library.bzl"], | ||
deps = [":css_providers"], | ||
) | ||
|
||
bzl_library( | ||
name = "css_map", | ||
srcs = ["css_map.bzl"], | ||
deps = [":css_providers"], | ||
) | ||
|
||
bzl_library( | ||
name = "css_providers", | ||
srcs = ["css_providers.bzl"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
"""Defines `css_binaries()` and related rules.""" | ||
|
||
load("@npm//@bazel/postcss:index.bzl", "postcss_multi_binary") | ||
load( | ||
"//packages/rules_prerender:postcss_import_plugin.bzl", | ||
IMPORT_PLUGIN_CONFIG = "PLUGIN_CONFIG", | ||
) | ||
load(":css_group.bzl", "css_group") | ||
load(":css_map.bzl", "css_map") | ||
load(":css_providers.bzl", "CssInfo") | ||
|
||
def css_binaries( | ||
name, | ||
deps, | ||
sourcemap = True, | ||
testonly = None, | ||
visibility = None, | ||
tags = None, | ||
): | ||
"""Generates CSS "binaries" of direct `css_library()` dependencies. | ||
Each `css_library()` in `deps` gets each of its direct sources compiled into a | ||
"binary", a new CSS file which bundles all the `@import` dependencies of that source. | ||
Returns: | ||
`DefaultInfo`: Contains bundled CSS and sourcemaps. | ||
`CssImportMapInfo`: Maps importable paths to generated CSS binary files. | ||
Args: | ||
name: Name of this target. | ||
deps: Dependencies of this target which provide `CssInfo` (generally | ||
`css_library()`). | ||
sourcemap: Whether to generate sourcemaps (defaults to `True`). | ||
testonly: https://docs.bazel.build/versions/main/be/common-definitions.html#common-attributes | ||
visibility: https://docs.bazel.build/versions/main/be/common-definitions.html#common-attributes | ||
tags: https://docs.bazel.build/versions/main/be/common-definitions.html#common-attributes | ||
""" | ||
binaries = [] | ||
for (index, dep) in enumerate(deps): | ||
binary_name = "%s_binary_%s" % (name, index) | ||
_css_binary( | ||
name = binary_name, | ||
sourcemap = sourcemap, | ||
dep = dep, | ||
testonly = testonly, | ||
tags = tags, | ||
) | ||
binaries.append(binary_name) | ||
|
||
css_group( | ||
name = name, | ||
deps = [":%s" % binary for binary in binaries], | ||
testonly = testonly, | ||
visibility = visibility, | ||
tags = tags, | ||
) | ||
|
||
# It might look like this implementation should accept multiple `css_library()` targets, | ||
# but doing so would be a bad idea. This is because the library is separated into "direct | ||
# sources" and "transitive dependencies" which are fed into `postcss_multi_binary()`. If | ||
# this allowed multiple libraries and same split was done, it would be "all libraries' | ||
# direct sources" and "all libraries' transitive dependencies". This sounds reasonable, | ||
# but would break strict deps. If one library had `@import url("rules_prerender/foo");` | ||
# but forgot to add a dep on `//:foo` yet another library in this binary had a dep on | ||
# `//:foo`, the first would incorrectly compile successfully. As a result, we need a | ||
# separate `postcss_multi_binary()` for each compiled `css_library()`. | ||
def _css_binary( | ||
name, | ||
dep, | ||
sourcemap, | ||
testonly = None, | ||
visibility = None, | ||
tags = None, | ||
): | ||
"""Generates a CSS "binary" for a single `css_library()` dependency. | ||
The `css_library()` dependency gets each of its direct sources compiled into a | ||
"binary", a new CSS file which bundles all the `@import` dependencies of that source. | ||
Note that while this macro is called `_css_binary()` in the singular form, if the | ||
`css_library()` dependency has multiple direct sources, each one will be compiled into | ||
an independent "binary", meaning multiple binaries can be generated by this macro. | ||
Returns: | ||
`DefaultInfo`: Contains bundled CSS and sourcemaps. | ||
`CssImportMapInfo`: Maps importable paths to generated CSS binary files. | ||
Args: | ||
name: Name of this target. | ||
dep: Dependency of this target which provides `CssInfo` (generally | ||
`css_library()`). | ||
sourcemap: Whether to generate sourcemaps. | ||
testonly: https://docs.bazel.build/versions/main/be/common-definitions.html#common-attributes | ||
visibility: https://docs.bazel.build/versions/main/be/common-definitions.html#common-attributes | ||
tags: https://docs.bazel.build/versions/main/be/common-definitions.html#common-attributes | ||
""" | ||
# Collect all transitive CSS dependencies into a single `DefaultInfo` target. | ||
css_transitive_deps = "%s_transitive_deps" % name | ||
_css_deps( | ||
name = css_transitive_deps, | ||
dep = dep, | ||
testonly = testonly, | ||
tags = tags, | ||
) | ||
|
||
# Compile the CSS. This only compiles one library, but that library may have many | ||
# source files, each one should be compiled into its own output, which requires | ||
# `postcss_multi_binary()` even though `postcss_binary()` sounds like it would be | ||
# sufficient here. | ||
binary = "%s_postcss" % name | ||
postcss_multi_binary( | ||
name = binary, | ||
srcs = [dep], | ||
output_pattern = "{name}", | ||
sourcemap = sourcemap, | ||
plugins = { | ||
"//tools/internal:postcss_import_plugin": IMPORT_PLUGIN_CONFIG, | ||
}, | ||
testonly = testonly, | ||
tags = tags, | ||
deps = [":%s" % css_transitive_deps], | ||
) | ||
|
||
# Return the binary outputs with a map of import name -> file path. | ||
css_map( | ||
name = name, | ||
bin = ":%s" % binary, | ||
lib = dep, | ||
testonly = testonly, | ||
visibility = visibility, | ||
tags = tags, | ||
) | ||
|
||
def _css_deps_impl(ctx): | ||
return DefaultInfo( | ||
files = ctx.attr.dep[CssInfo].transitive_sources, | ||
) | ||
|
||
_css_deps = rule( | ||
implementation = _css_deps_impl, | ||
attrs = { | ||
"dep": attr.label( | ||
mandatory = True, | ||
providers = [CssInfo], | ||
), | ||
}, | ||
doc = "Returns transitive CSS sources in `DefaultInfo`.", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
"""Defines `css_group()` to group multiple `css_library()` targets like `filegroup()`.""" | ||
|
||
load(":css_providers.bzl", "CssImportMapInfo") | ||
|
||
def _css_group_impl(ctx): | ||
return [ | ||
DefaultInfo( | ||
files = depset([], transitive = [dep[DefaultInfo].files | ||
for dep in ctx.attr.deps]), | ||
), | ||
CssImportMapInfo( | ||
import_map = _merge_import_maps([dep[CssImportMapInfo] | ||
for dep in ctx.attr.deps | ||
if CssImportMapInfo in dep]), | ||
), | ||
] | ||
|
||
css_group = rule( | ||
implementation = _css_group_impl, | ||
attrs = { | ||
"deps": attr.label_list(mandatory = True), | ||
}, | ||
doc = "Like a `filegroup()`, but for `css_library()` targets.", | ||
) | ||
|
||
def _merge_import_maps(css_import_maps): | ||
"""Merges a list of `CssImportMapInfo` into a single `CssImportMapInfo`. | ||
Fails the build if the same import path appears as a key in two maps. | ||
""" | ||
import_map = dict() | ||
for css_import_map in css_import_maps: | ||
for (key, value) in css_import_map.import_map.items(): | ||
if key in import_map: | ||
fail(("Found duplicate CSS import path in `_css_group()`. %s maps to" + | ||
" both %s and %s.") % (key, import_map[key].path, value.path)) | ||
import_map[key] = value | ||
|
||
return import_map |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
"""Defines the `css_library()` rule.""" | ||
|
||
load(":css_providers.bzl", "CssInfo") | ||
|
||
def _css_library_impl(ctx): | ||
return [ | ||
DefaultInfo(files = depset(ctx.files.srcs)), | ||
CssInfo( | ||
direct_sources = ctx.files.srcs, | ||
transitive_sources = depset(ctx.files.srcs, | ||
transitive = [dep[CssInfo].transitive_sources | ||
for dep in ctx.attr.deps], | ||
), | ||
), | ||
] | ||
|
||
css_library = rule( | ||
implementation = _css_library_impl, | ||
attrs = { | ||
"srcs": attr.label_list( | ||
allow_files = [".css"], | ||
doc = "List of CSS source files in this library.", | ||
), | ||
"deps": attr.label_list( | ||
providers = [CssInfo], | ||
doc = """ | ||
List of other `css_library()` dependencies with sources imported by this target's sources. | ||
""".strip(), | ||
), | ||
}, | ||
doc = """ | ||
A library of CSS source files and their `@import` dependencies. | ||
This rule does not currently implement any form of strict dependencies, take care to | ||
manage your imports and dependencies. | ||
""".strip(), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
"""Defines the `css_map()` rule.""" | ||
|
||
load(":css_providers.bzl", "CssImportMapInfo", "CssInfo") | ||
|
||
def _css_map_impl(ctx): | ||
return [ | ||
ctx.attr.bin[DefaultInfo], | ||
CssImportMapInfo(import_map = _make_import_map(ctx)), | ||
] | ||
|
||
css_map = rule( | ||
implementation = _css_map_impl, | ||
attrs = { | ||
"bin": attr.label( | ||
mandatory = True, | ||
doc = """ | ||
The `postcss_multi_binary()` target which compiles the library. Files generated by this | ||
target are used as the actual files paths in the provided `CssImportMapInfo`. | ||
""".strip(), | ||
), | ||
"lib": attr.label( | ||
mandatory = True, | ||
providers = [CssInfo], | ||
doc = """ | ||
The `css_library()` target containing direct sources used in the `postcss_multi_binary()`. | ||
Files included here are used to define the user-authorable import path in the provided | ||
`CssImportMapInfo`. | ||
""".strip(), | ||
), | ||
}, | ||
doc = """ | ||
Provides a `CssImportMapInfo` which maps the user-authorable import path to the actual | ||
file path to resolve to. | ||
""".strip(), | ||
) | ||
|
||
def _make_import_map(ctx): | ||
lib_files = sorted(ctx.attr.lib[CssInfo].direct_sources) | ||
bin_files = sorted([file for file in ctx.files.bin if not file.basename.endswith(".map")]) | ||
|
||
# Validate that the `postcss_binary()` and the `css_library()` contain the same number | ||
# of files. | ||
if len(lib_files) != len(bin_files): | ||
fail(("Number of files from the CSS library (%s from %s) does not equal the" + | ||
" number of files from the CSS binary (%s from %s)") % ( | ||
len(lib_files), | ||
ctx.attr.lib.label, | ||
len(bin_files), | ||
ctx.attr.bin.label, | ||
)) | ||
|
||
import_map = dict() | ||
lib_wksp = (ctx.attr.lib.label.workspace_name | ||
if ctx.attr.lib.label.workspace_name | ||
else ctx.workspace_name) | ||
for index in range(len(lib_files)): | ||
lib_file = lib_files[index] | ||
bin_file = bin_files[index] | ||
|
||
# The library and binary file should have the same name, but might be in | ||
# different packages. | ||
if lib_file.basename != bin_file.basename: | ||
fail(("CSS library files did not match up with CSS binary files.\n" + | ||
"Lib files:\n%s\n\nBin files:\n%s") % ( | ||
"\n".join([file.path for file in lib_files]), | ||
"\n".join([file.path for file in bin_files]), | ||
)) | ||
|
||
# Verify that the importable path isn't already registered. | ||
key = "%s/%s" % (lib_wksp, lib_file.short_path) | ||
if key in import_map: | ||
fail("CSS library file (%s) mapped twice, once to %s and a second time to %s." % ( | ||
key, | ||
import_map[key].path, | ||
bin_file.path, | ||
)) | ||
|
||
import_map[key] = bin_file | ||
|
||
return import_map |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Provider of information related to CSS compilation. | ||
CssInfo = provider(fields = { | ||
"direct_sources": "Direct sources of the target.", | ||
"transitive_sources": "All direct *and* transitive sources of the target.", | ||
}) | ||
|
||
# Provider of a map which relates an importable path to the actual file path which it | ||
# references. | ||
CssImportMapInfo = provider(fields = { | ||
"import_map": """ | ||
A `dict[str, str]` where a key is a paths which can be used in a user-authored import | ||
statement (wksp/foo/bar/baz.css) mapped to a value which is the file it actually refers | ||
to (bazel-out/bin/foo/bar/baz.css). | ||
This abstracts away the artifact root from user-authored code and also decouples the | ||
import statement authored from the file actually being imported. The root-relative paths | ||
of both the import statement and the real file path *usually* align, but not always and | ||
should not be assumed to match. | ||
""".strip(), | ||
}) |
Oops, something went wrong.