blob: 3cdbd565a37ad0da8c3cea57ce26201cb0dfe437 [file]
"Rules for running Rollup under Bazel"
load("@rules_nodejs//nodejs:providers.bzl", "JSModuleInfo", "STAMP_ATTR", "StampSettingInfo")
load("@build_bazel_rules_nodejs//:providers.bzl", "ExternalNpmPackageInfo", "JSEcmaScriptModuleInfo", "node_modules_aspect", "run_node")
load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "module_mappings_aspect")
_DOC = "Runs the rollup.js CLI under Bazel."
_ROLLUP_ATTRS = {
"args": attr.string_list(
doc = """Command line arguments to pass to Rollup. Can be used to override config file settings.
These argument passed on the command line before arguments that are added by the rule.
Run `bazel` with `--subcommands` to see what Rollup CLI command line was invoked.
See the <a href="https://rollupjs.org/guide/en/#command-line-flags">Rollup CLI docs</a> for a complete list of supported arguments.""",
default = [],
),
"config_file": attr.label(
doc = """A `rollup.config.js` file
Passed to the `--config` option, see [the config doc](https://rollupjs.org/guide/en/#configuration-files)
If not set, a default basic Rollup config is used.
""",
allow_single_file = True,
default = "//packages/rollup:rollup.config.js",
),
"deps": attr.label_list(
aspects = [module_mappings_aspect, node_modules_aspect],
doc = """Other libraries that are required by the code, or by the rollup.config.js""",
),
"entry_point": attr.label(
doc = """The bundle's entry point (e.g. your main.js or app.js or index.js).
This is just a shortcut for the `entry_points` attribute with a single output chunk named the same as the rule.
For example, these are equivalent:
```python
rollup_bundle(
name = "bundle",
entry_point = "index.js",
)
```
```python
rollup_bundle(
name = "bundle",
entry_points = {
"index.js": "bundle"
}
)
```
If `rollup_bundle` is used on a `ts_library`, the `rollup_bundle` rule handles selecting the correct outputs from `ts_library`.
In this case, `entry_point` can be specified as the `.ts` file and `rollup_bundle` will handle the mapping to the `.mjs` output file.
For example:
```python
ts_library(
name = "foo",
srcs = [
"foo.ts",
"index.ts",
],
)
rollup_bundle(
name = "bundle",
deps = [ "foo" ],
entry_point = "index.ts",
)
```
""",
allow_single_file = True,
),
"entry_points": attr.label_keyed_string_dict(
doc = """The bundle's entry points (e.g. your main.js or app.js or index.js).
Passed to the [`--input` option](https://github.com/rollup/rollup/blob/master/docs/999-big-list-of-options.md#input) in Rollup.
Keys in this dictionary are labels pointing to .js entry point files.
Values are the name to be given to the corresponding output chunk.
Either this attribute or `entry_point` must be specified, but not both.
""",
allow_files = True,
),
"format": attr.string(
doc = """Specifies the format of the generated bundle. One of the following:
- `amd`: Asynchronous Module Definition, used with module loaders like RequireJS
- `cjs`: CommonJS, suitable for Node and other bundlers
- `esm`: Keep the bundle as an ES module file, suitable for other bundlers and inclusion as a `<script type=module>` tag in modern browsers
- `iife`: A self-executing function, suitable for inclusion as a `<script>` tag. (If you want to create a bundle for your application, you probably want to use this.)
- `umd`: Universal Module Definition, works as amd, cjs and iife all in one
- `system`: Native format of the SystemJS loader
""",
values = ["amd", "cjs", "esm", "iife", "umd", "system"],
default = "esm",
),
"link_workspace_root": attr.bool(
doc = """Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'.
If source files need to be required then they can be copied to the bin_dir with copy_to_bin.""",
),
"output_dir": attr.bool(
doc = """Whether to produce a directory output.
We will use the [`--output.dir` option](https://github.com/rollup/rollup/blob/master/docs/999-big-list-of-options.md#outputdir) in rollup
rather than `--output.file`.
If the program produces multiple chunks, you must specify this attribute.
Otherwise, the outputs are assumed to be a single file.
""",
),
"rollup_bin": attr.label(
doc = "Target that executes the rollup binary",
executable = True,
cfg = "exec",
default = (
# BEGIN-INTERNAL
"@npm" +
# END-INTERNAL
"//rollup/bin:rollup"
),
),
"rollup_worker_bin": attr.label(
doc = "Internal use only",
executable = True,
cfg = "exec",
default = "//packages/rollup/bin:rollup-worker",
),
"silent": attr.bool(
doc = """Whether to execute the rollup binary with the --silent flag, defaults to False.
Using --silent can cause rollup to [ignore errors/warnings](https://github.com/rollup/rollup/blob/master/docs/999-big-list-of-options.md#onwarn)
which are only surfaced via logging. Since bazel expects printing nothing on success, setting silent to True
is a more Bazel-idiomatic experience, however could cause rollup to drop important warnings.
""",
),
"sourcemap": attr.string(
doc = """Whether to produce sourcemaps.
Passed to the [`--sourcemap` option](https://github.com/rollup/rollup/blob/master/docs/999-big-list-of-options.md#outputsourcemap") in Rollup
""",
default = "inline",
values = ["inline", "hidden", "true", "false"],
),
"srcs": attr.label_list(
doc = """Non-entry point JavaScript source files from the workspace.
You must not repeat file(s) passed to entry_point/entry_points.
""",
# Don't try to constrain the filenames, could be json, svg, whatever
allow_files = True,
),
"stamp": STAMP_ATTR,
"supports_workers": attr.bool(
doc = """Experimental! Use only with caution.
Allows you to enable the Bazel Worker strategy for this library.
When enabled, this rule invokes the "rollup_worker_bin"
worker aware binary rather than "rollup_bin".""",
default = False,
),
}
def _desugar_entry_point_names(name, entry_point, entry_points):
"""Users can specify entry_point (sugar) or entry_points (long form).
This function allows our code to treat it like they always used the long form.
It also performs validation:
- exactly one of these attributes should be specified
"""
if entry_point and entry_points:
fail("Cannot specify both entry_point and entry_points")
if not entry_point and not entry_points:
fail("One of entry_point or entry_points must be specified")
if entry_point:
return [name]
return entry_points.values()
def _desugar_entry_points(name, entry_point, entry_points, inputs):
"""Like above, but used by the implementation function, where the types differ.
It also performs validation:
- attr.label_keyed_string_dict doesn't accept allow_single_file
so we have to do validation now to be sure each key is a label resulting in one file
It converts from dict[target: string] to dict[file: string]
"""
names = _desugar_entry_point_names(name, entry_point.label if entry_point else None, entry_points)
if entry_point:
return {_resolve_js_input(entry_point.files.to_list()[0], inputs): names[0]}
result = {}
for ep in entry_points.items():
entry_point = ep[0]
name = ep[1]
f = entry_point.files.to_list()
if len(f) != 1:
fail("keys in rollup_bundle#entry_points must provide one file, but %s has %s" % (entry_point.label, len(f)))
result[_resolve_js_input(f[0], inputs)] = name
return result
def _resolve_js_input(f, inputs):
if f.extension == "js" or f.extension == "mjs":
return f
# look for corresponding js file in inputs
no_ext = _no_ext(f)
for i in inputs:
if i.extension == "js" or i.extension == "mjs":
if _no_ext(i) == no_ext:
return i
fail("Could not find corresponding javascript entry point for %s. Add the %s.js to your deps." % (f.path, no_ext))
def _rollup_outs(sourcemap, name, entry_point, entry_points, output_dir):
"""Supply some labelled outputs in the common case of a single entry point"""
result = {}
entry_point_outs = _desugar_entry_point_names(name, entry_point, entry_points)
if output_dir:
# We can't declare a directory output here, because RBE will be confused, like
# com.google.devtools.build.lib.remote.ExecutionStatusException:
# INTERNAL: failed to upload outputs: failed to construct CAS files:
# failed to calculate file hash:
# read /b/f/w/bazel-out/k8-fastbuild/bin/packages/rollup/test/multiple_entry_points/chunks: is a directory
#result["chunks"] = output_dir
return {}
else:
if len(entry_point_outs) > 1:
fail("Multiple entry points require that output_dir be set")
out = entry_point_outs[0]
result[out] = out + ".js"
if sourcemap == "true":
result[out + "_map"] = "%s.map" % result[out]
return result
def _no_ext(f):
return f.short_path[:-len(f.extension) - 1]
def _filter_js(files):
return [f for f in files if f.extension == "js" or f.extension == "mjs"]
def _rollup_bundle(ctx):
"Generate a rollup config file and run rollup"
# rollup_bundle supports deps with JS providers. For each dep,
# JSEcmaScriptModuleInfo is used if found, then JSModuleInfo and finally
# the DefaultInfo files are used if the former providers are not found.
deps_depsets = []
for dep in ctx.attr.deps:
if JSEcmaScriptModuleInfo in dep:
deps_depsets.append(dep[JSEcmaScriptModuleInfo].sources)
if JSModuleInfo in dep:
deps_depsets.append(dep[JSModuleInfo].sources)
elif hasattr(dep, "files"):
deps_depsets.append(dep.files)
# Also include files from npm deps as inputs.
# These deps are identified by the ExternalNpmPackageInfo provider.
if ExternalNpmPackageInfo in dep:
deps_depsets.append(dep[ExternalNpmPackageInfo].sources)
deps_inputs = depset(transitive = deps_depsets).to_list()
inputs = _filter_js(ctx.files.entry_point) + _filter_js(ctx.files.entry_points) + ctx.files.srcs + deps_inputs
outputs = [getattr(ctx.outputs, o) for o in dir(ctx.outputs)]
# See CLI documentation at https://rollupjs.org/guide/en/#command-line-reference
args = ctx.actions.args()
if ctx.attr.supports_workers:
# Set to use a multiline param-file for worker mode
args.use_param_file("@%s", use_always = True)
args.set_param_file_format("multiline")
# Add user specified arguments *before* rule supplied arguments
args.add_all(ctx.attr.args)
# List entry point argument first to save some argv space
# Rollup doc says
# When provided as the first options, it is equivalent to not prefix them with --input
entry_points = _desugar_entry_points(ctx.label.name, ctx.attr.entry_point, ctx.attr.entry_points, inputs).items()
# If user requests an output_dir, then use output.dir rather than output.file
if ctx.attr.output_dir:
outputs.append(ctx.actions.declare_directory(ctx.label.name))
for entry_point in entry_points:
args.add_joined([entry_point[1], entry_point[0]], join_with = "=")
args.add_all(["--output.dir", outputs[0].path])
else:
args.add(entry_points[0][0])
args.add_all(["--output.file", outputs[0].path])
args.add_all(["--format", ctx.attr.format])
if ctx.attr.silent:
# Run the rollup binary with the --silent flag
args.add("--silent")
stamp = ctx.attr.stamp[StampSettingInfo].value
config = ctx.actions.declare_file("_%s.rollup_config.js" % ctx.label.name)
ctx.actions.expand_template(
template = ctx.file.config_file,
output = config,
substitutions = {
"bazel_info_file": "\"%s\"" % ctx.info_file.path if stamp else "undefined",
"bazel_version_file": "\"%s\"" % ctx.version_file.path if stamp else "undefined",
},
)
args.add_all(["--config", config.path])
inputs.append(config)
# Prevent rollup's module resolver from hopping outside Bazel's sandbox
# When set to false, symbolic links are followed when resolving a file.
# When set to true, instead of being followed, symbolic links are treated as if the file is
# where the link is.
args.add("--preserveSymlinks")
if (ctx.attr.sourcemap and ctx.attr.sourcemap != "false"):
args.add_all(["--sourcemap", ctx.attr.sourcemap])
executable = "rollup_bin"
execution_requirements = {}
if ctx.attr.supports_workers:
executable = "rollup_worker_bin"
execution_requirements["supports-workers"] = str(int(ctx.attr.supports_workers))
run_node(
ctx,
progress_message = "Bundling JavaScript %s [rollup]" % outputs[0].short_path,
executable = executable,
inputs = inputs,
outputs = outputs,
arguments = [args],
mnemonic = "Rollup",
execution_requirements = execution_requirements,
env = {"COMPILATION_MODE": ctx.var["COMPILATION_MODE"]},
link_workspace_root = ctx.attr.link_workspace_root,
)
outputs_depset = depset(outputs)
return [
DefaultInfo(files = outputs_depset),
JSModuleInfo(
direct_sources = outputs_depset,
sources = outputs_depset,
),
]
rollup_bundle = rule(
doc = _DOC,
implementation = _rollup_bundle,
attrs = dict(_ROLLUP_ATTRS),
outputs = _rollup_outs,
)