blob: feb89697c00f908dbc20ea6c4074046ef258c7f9 [file]
"Rules for running Rollup under Bazel"
load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "module_mappings_aspect", "register_node_modules_linker")
_DOC = """Runs the Rollup.js CLI under Bazel.
See https://rollupjs.org/guide/en/#command-line-reference
Typical example:
```python
load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
rollup_bundle(
name = "bundle",
srcs = ["dependency.js"],
entry_point = "input.js",
config_file = "rollup.config.js",
)
```
Note that the command-line options set by Bazel override what appears in the rollup config file.
This means that typically a single `rollup.config.js` can contain settings for your whole repo,
and multiple `rollup_bundle` rules can share the configuration.
Thus, setting options that Bazel controls will have no effect, e.g.
```javascript
module.exports = {
output: { file: 'this_is_ignored.js' },
}
```
You must determine ahead of time whether Rollup needs to produce a directory output.
This is the case if you have dynamic imports which cause code-splitting, or if you
provide multiple entry points. Use the `output_dir` attribute to specify that you want a
directory output.
Rollup's CLI has the same behavior, forcing you to pick `--output.file` or `--output.dir`.
To get multiple output formats, wrap the rule with a macro or list comprehension, e.g.
```python
[
rollup_bundle(
name = "bundle.%s" % format,
entry_point = "foo.js",
format = format,
)
for format in [
"cjs",
"umd",
]
]
```
This will produce one output per requested format.
"""
_ROLLUP_ATTRS = {
"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,
),
"config_file": attr.label(
doc = """A rollup.config.js file
Passed to the --config
See https://rollupjs.org/guide/en/#configuration-files
If not set, a default basic Rollup config is used.
""",
allow_single_file = True,
default = "@npm_bazel_rollup//: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"
}
)
```
""",
allow_single_file = [".js"],
),
"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 = [".js"],
),
"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",
),
"globals": attr.string_dict(
doc = """Specifies id: variableName pairs necessary for external imports in umd/iife bundles.
Passed to the [`--globals` option](https://github.com/rollup/rollup/blob/master/docs/999-big-list-of-options.md#outputglobals) in Rollup.
Also, the keys from the map are passed to the [`--external` option](https://github.com/rollup/rollup/blob/master/docs/999-big-list-of-options.md#external).
""",
),
"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 = "host",
default = "@npm//rollup/bin:rollup",
),
"sourcemap": attr.bool(
doc = """Whether to produce a .js.map output
Passed to the [`--sourcemap` option](https://github.com/rollup/rollup/blob/master/docs/999-big-list-of-options.md#outputsourcemap") in Rollup
""",
default = True,
),
"deps": attr.label_list(
aspects = [module_mappings_aspect],
doc = """Other libraries that are required by the code, or by the rollup.config.js""",
),
}
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):
"""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 {entry_point.files.to_list()[0]: 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[f[0]] = name
return result
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:
result[out + "_map"] = "%s.map" % result[out]
return result
def _no_ext(f):
return f.short_path[:-len(f.extension) - 1]
def _rollup_bundle(ctx):
"Generate a rollup config file and run rollup"
inputs = ctx.files.entry_point + ctx.files.entry_points + ctx.files.srcs + ctx.files.deps
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()
# 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).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], _no_ext(entry_point[0])], join_with = "=")
args.add_all(["--output.dir", outputs[0].path])
else:
args.add(_no_ext(entry_points[0][0]))
args.add_all(["--output.file", outputs[0].path])
args.add_all(["--format", ctx.attr.format])
register_node_modules_linker(ctx, args, inputs)
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_stamp_file": "\"%s\"" % ctx.version_file.path if ctx.version_file else "undefined",
},
)
args.add_all(["--config", config.path])
inputs.append(config)
if ctx.version_file:
inputs.append(ctx.version_file)
# 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):
args.add("--sourcemap")
if ctx.attr.globals:
args.add("--external")
args.add_joined(ctx.attr.globals.keys(), join_with = ",")
args.add("--globals")
args.add_joined(["%s:%s" % g for g in ctx.attr.globals.items()], join_with = ",")
ctx.actions.run(
progress_message = "Bundling JavaScript %s [rollup]" % outputs[0].short_path,
executable = ctx.executable.rollup_bin,
inputs = inputs,
outputs = outputs,
arguments = [args],
)
return [
DefaultInfo(files = depset(outputs)),
]
rollup_bundle = rule(
doc = _DOC,
implementation = _rollup_bundle,
attrs = _ROLLUP_ATTRS,
outputs = _rollup_outs,
)