The `ng_rollup_bundle` rule currently is only consumed in the Angular framework repository. This means that Angular packages are built from source, and ngcc is never needed to build rollup bundles using Ivy. Though, this rule is planned to be shared with other repositories to support common benchmark code. This means that ngcc needs to be handled as these other repositories cannot build Angular from source, but instead consume Angular through NPM (with ngcc enabling Ivy). The `ng_rollup_bundle` rule needs to dynamically prioritize `ngcc` generated main resolution fields if `--define=angular_ivy_enabled=True` is set (or with the alias: `--config=ivy`). ds PR Close #36044
472 lines
19 KiB
Python
472 lines
19 KiB
Python
# Copyright Google Inc. All Rights Reserved.
|
|
#
|
|
# Use of this source code is governed by an MIT-style license that can be
|
|
# found in the LICENSE file at https://angular.io/license
|
|
|
|
"""Rollup with Build Optimizer
|
|
|
|
This provides a variant of the [rollup_bundle] rule that works better for Angular apps.
|
|
|
|
It registers `@angular-devkit/build-optimizer` as a rollup plugin, to get
|
|
better optimization. It also uses ESM5 format inputs, as this is what
|
|
build-optimizer is hard-coded to look for and transform.
|
|
|
|
[rollup_bundle]: https://bazelbuild.github.io/rules_nodejs/rollup/rollup_bundle.html
|
|
"""
|
|
|
|
load("@build_bazel_rules_nodejs//:index.bzl", "npm_package_bin")
|
|
load("@build_bazel_rules_nodejs//:providers.bzl", "JSEcmaScriptModuleInfo", "NpmPackageInfo", "node_modules_aspect")
|
|
load("//packages/bazel/src:esm5.bzl", "esm5_outputs_aspect", "esm5_root_dir", "flatten_esm5")
|
|
load("@npm_bazel_terser//:index.bzl", "terser_minified")
|
|
|
|
_NG_ROLLUP_BUNDLE_OUTPUTS = {
|
|
"bundle": "%{name}.js",
|
|
"sourcemap": "%{name}.js.map",
|
|
}
|
|
|
|
_NG_ROLLUP_MODULE_MAPPINGS_ATTR = "ng_rollup_module_mappings"
|
|
|
|
def _ng_rollup_module_mappings_aspect_impl(target, ctx):
|
|
mappings = dict()
|
|
for dep in ctx.rule.attr.deps:
|
|
if hasattr(dep, _NG_ROLLUP_MODULE_MAPPINGS_ATTR):
|
|
for k, v in getattr(dep, _NG_ROLLUP_MODULE_MAPPINGS_ATTR).items():
|
|
if k in mappings and mappings[k] != v:
|
|
fail(("duplicate module mapping at %s: %s maps to both %s and %s" %
|
|
(target.label, k, mappings[k], v)), "deps")
|
|
mappings[k] = v
|
|
if ((hasattr(ctx.rule.attr, "module_name") and ctx.rule.attr.module_name) or
|
|
(hasattr(ctx.rule.attr, "module_root") and ctx.rule.attr.module_root)):
|
|
mn = ctx.rule.attr.module_name
|
|
if not mn:
|
|
mn = target.label.name
|
|
mr = target.label.package
|
|
if target.label.workspace_root:
|
|
mr = "%s/%s" % (target.label.workspace_root, mr)
|
|
if ctx.rule.attr.module_root and ctx.rule.attr.module_root != ".":
|
|
if ctx.rule.attr.module_root.endswith(".ts"):
|
|
# This is the type-checking module mapping. Strip the trailing .d.ts
|
|
# as it doesn't belong in TypeScript's path mapping.
|
|
mr = "%s/%s" % (mr, ctx.rule.attr.module_root.replace(".d.ts", ""))
|
|
else:
|
|
mr = "%s/%s" % (mr, ctx.rule.attr.module_root)
|
|
if mn in mappings and mappings[mn] != mr:
|
|
fail(("duplicate module mapping at %s: %s maps to both %s and %s" %
|
|
(target.label, mn, mappings[mn], mr)), "deps")
|
|
mappings[mn] = mr
|
|
return struct(ng_rollup_module_mappings = mappings)
|
|
|
|
ng_rollup_module_mappings_aspect = aspect(
|
|
_ng_rollup_module_mappings_aspect_impl,
|
|
attr_aspects = ["deps"],
|
|
)
|
|
|
|
_NG_ROLLUP_BUNDLE_DEPS_ASPECTS = [esm5_outputs_aspect, ng_rollup_module_mappings_aspect, node_modules_aspect]
|
|
|
|
_NG_ROLLUP_BUNDLE_ATTRS = {
|
|
"build_optimizer": attr.bool(
|
|
doc = """Use build optimizer plugin
|
|
|
|
Only used if sources are esm5 which depends on value of esm5_sources.""",
|
|
default = True,
|
|
),
|
|
"esm5_sources": attr.bool(
|
|
doc = """Use esm5 input sources""",
|
|
default = True,
|
|
),
|
|
"srcs": attr.label_list(
|
|
doc = """JavaScript source files from the workspace.
|
|
These can use ES2015 syntax and ES Modules (import/export)""",
|
|
allow_files = True,
|
|
),
|
|
"entry_point": attr.label(
|
|
doc = """The starting point of the application, passed as the `--input` flag to rollup.
|
|
|
|
If the entry JavaScript file belongs to the same package (as the BUILD file),
|
|
you can simply reference it by its relative name to the package directory:
|
|
|
|
```
|
|
ng_rollup_bundle(
|
|
name = "bundle",
|
|
entry_point = ":main.js",
|
|
)
|
|
```
|
|
|
|
You can specify the entry point as a typescript file so long as you also include
|
|
the ts_library target in deps:
|
|
|
|
```
|
|
ts_library(
|
|
name = "main",
|
|
srcs = ["main.ts"],
|
|
)
|
|
|
|
ng_rollup_bundle(
|
|
name = "bundle",
|
|
deps = [":main"]
|
|
entry_point = ":main.ts",
|
|
)
|
|
```
|
|
|
|
The rule will use the corresponding `.js` output of the ts_library rule as the entry point.
|
|
|
|
If the entry point target is a rule, it should produce a single JavaScript entry file that will be passed to the nodejs_binary rule.
|
|
For example:
|
|
|
|
```
|
|
filegroup(
|
|
name = "entry_file",
|
|
srcs = ["main.js"],
|
|
)
|
|
|
|
ng_rollup_bundle(
|
|
name = "bundle",
|
|
entry_point = ":entry_file",
|
|
)
|
|
```
|
|
""",
|
|
mandatory = True,
|
|
allow_single_file = True,
|
|
),
|
|
"deps": attr.label_list(
|
|
doc = """Other targets that provide JavaScript files.
|
|
Typically this will be `ts_library` or `ng_module` targets.""",
|
|
aspects = _NG_ROLLUP_BUNDLE_DEPS_ASPECTS,
|
|
),
|
|
"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",
|
|
),
|
|
"global_name": attr.string(
|
|
doc = """A name given to this package when referenced as a global variable.
|
|
This name appears in the bundle module incantation at the beginning of the file,
|
|
and governs the global symbol added to the global context (e.g. `window`) as a side-
|
|
effect of loading the UMD/IIFE JS bundle.
|
|
|
|
Rollup doc: "The variable name, representing your iife/umd bundle, by which other scripts on the same page can access it."
|
|
|
|
This is passed to the `output.name` setting in Rollup.""",
|
|
),
|
|
"globals": attr.string_dict(
|
|
doc = """A dict of symbols that reference external scripts.
|
|
The keys are variable names that appear in the program,
|
|
and the values are the symbol to reference at runtime in a global context (UMD bundles).
|
|
For example, a program referencing @angular/core should use ng.core
|
|
as the global reference, so Angular users should include the mapping
|
|
`"@angular/core":"ng.core"` in the globals.""",
|
|
default = {},
|
|
),
|
|
"license_banner": attr.label(
|
|
doc = """A .txt file passed to the `banner` config option of rollup.
|
|
The contents of the file will be copied to the top of the resulting bundles.
|
|
Note that you can replace a version placeholder in the license file, by using
|
|
the special version `0.0.0-PLACEHOLDER`. See the section on stamping in the README.""",
|
|
allow_single_file = [".txt"],
|
|
),
|
|
"_rollup": attr.label(
|
|
executable = True,
|
|
cfg = "host",
|
|
default = Label("//tools/ng_rollup_bundle:rollup_with_build_optimizer"),
|
|
),
|
|
"_rollup_config_tmpl": attr.label(
|
|
default = Label("//tools/ng_rollup_bundle:rollup.config.js"),
|
|
allow_single_file = True,
|
|
),
|
|
}
|
|
|
|
def _compute_node_modules_root(ctx):
|
|
"""Computes the node_modules root from the node_modules and deps attributes.
|
|
|
|
Args:
|
|
ctx: the skylark execution context
|
|
|
|
Returns:
|
|
The node_modules root as a string
|
|
"""
|
|
node_modules_root = None
|
|
for d in ctx.attr.deps:
|
|
if NpmPackageInfo in d:
|
|
possible_root = "/".join(["external", d[NpmPackageInfo].workspace, "node_modules"])
|
|
if not node_modules_root:
|
|
node_modules_root = possible_root
|
|
elif node_modules_root != possible_root:
|
|
fail("All npm dependencies need to come from a single workspace. Found '%s' and '%s'." % (node_modules_root, possible_root))
|
|
if not node_modules_root:
|
|
# there are no fine grained deps but we still need a node_modules_root even if its empty
|
|
node_modules_root = "external/npm/node_modules"
|
|
return node_modules_root
|
|
|
|
# Avoid using non-normalized paths (workspace/../other_workspace/path)
|
|
def _to_manifest_path(ctx, file):
|
|
if file.short_path.startswith("../"):
|
|
return file.short_path[3:]
|
|
else:
|
|
return ctx.workspace_name + "/" + file.short_path
|
|
|
|
# Expand entry_point into runfiles and strip the file extension
|
|
def _esm5_entry_point_path(ctx):
|
|
return _to_manifest_path(ctx, ctx.file.entry_point)[:-(len(ctx.file.entry_point.extension) + 1)]
|
|
|
|
def _no_ext(f):
|
|
return f.short_path[:-len(f.extension) - 1]
|
|
|
|
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 _write_rollup_config(ctx, root_dir, build_optimizer, filename = "_%s.rollup.conf.js"):
|
|
"""Generate a rollup config file.
|
|
|
|
Args:
|
|
ctx: Bazel rule execution context
|
|
root_dir: root directory for module resolution
|
|
build_optimizer: whether to enable Build Optimizer plugin
|
|
filename: output filename pattern (defaults to `_%s.rollup.conf.js`)
|
|
|
|
Returns:
|
|
The rollup config file. See https://rollupjs.org/guide/en#configuration-files
|
|
"""
|
|
config = ctx.actions.declare_file(filename % ctx.label.name)
|
|
|
|
mappings = dict()
|
|
all_deps = ctx.attr.deps + ctx.attr.srcs
|
|
for dep in all_deps:
|
|
if hasattr(dep, _NG_ROLLUP_MODULE_MAPPINGS_ATTR):
|
|
for k, v in getattr(dep, _NG_ROLLUP_MODULE_MAPPINGS_ATTR).items():
|
|
if k in mappings and mappings[k] != v:
|
|
fail(("duplicate module mapping at %s: %s maps to both %s and %s" %
|
|
(dep.label, k, mappings[k], v)), "deps")
|
|
mappings[k] = v
|
|
|
|
globals = {}
|
|
external = []
|
|
if ctx.attr.globals:
|
|
globals = ctx.attr.globals.items()
|
|
external = ctx.attr.globals.keys()
|
|
|
|
ctx.actions.expand_template(
|
|
output = config,
|
|
template = ctx.file._rollup_config_tmpl,
|
|
substitutions = {
|
|
"TMPL_banner_file": "\"%s\"" % ctx.file.license_banner.path if ctx.file.license_banner else "undefined",
|
|
"TMPL_build_optimizer": "true" if build_optimizer else "false",
|
|
"TMPL_module_mappings": str(mappings),
|
|
"TMPL_node_modules_root": _compute_node_modules_root(ctx),
|
|
"TMPL_root_dir": root_dir,
|
|
"TMPL_stamp_data": "\"%s\"" % ctx.version_file.path if ctx.version_file else "undefined",
|
|
"TMPL_workspace_name": ctx.workspace_name,
|
|
"TMPL_external": ", ".join(["'%s'" % e for e in external]),
|
|
"TMPL_globals": ", ".join(["'%s': '%s'" % g for g in globals]),
|
|
"TMPL_ivy_enabled": "true" if ctx.var.get("angular_ivy_enabled", None) == "True" else "false",
|
|
},
|
|
)
|
|
|
|
return config
|
|
|
|
def _filter_js_inputs(all_inputs):
|
|
all_inputs_list = all_inputs.to_list() if type(all_inputs) == type(depset()) else all_inputs
|
|
return [
|
|
f
|
|
for f in all_inputs_list
|
|
if f.path.endswith(".js") or f.path.endswith(".mjs") or f.path.endswith(".json")
|
|
]
|
|
|
|
def _run_rollup(ctx, entry_point_path, sources, config):
|
|
args = ctx.actions.args()
|
|
args.add("--config", config.path)
|
|
args.add("--input", entry_point_path)
|
|
args.add("--output.file", ctx.outputs.bundle)
|
|
args.add("--output.name", ctx.attr.global_name if ctx.attr.global_name else ctx.label.name)
|
|
args.add("--output.format", ctx.attr.format)
|
|
args.add("--output.sourcemap")
|
|
args.add("--output.sourcemapFile", ctx.outputs.sourcemap)
|
|
|
|
# We will produce errors as needed. Anything else is spammy: a well-behaved
|
|
# bazel rule prints nothing on success.
|
|
args.add("--silent")
|
|
|
|
args.add("--preserveSymlinks")
|
|
|
|
direct_inputs = [config]
|
|
|
|
# Also include files from npm fine grained deps as inputs.
|
|
# These deps are identified by the NpmPackageInfo provider.
|
|
for d in ctx.attr.deps:
|
|
if NpmPackageInfo in d:
|
|
# Note: we can't avoid calling .to_list() on sources
|
|
direct_inputs.extend(_filter_js_inputs(d[NpmPackageInfo].sources.to_list()))
|
|
|
|
if ctx.file.license_banner:
|
|
direct_inputs.append(ctx.file.license_banner)
|
|
if ctx.version_file:
|
|
direct_inputs.append(ctx.version_file)
|
|
|
|
ctx.actions.run(
|
|
progress_message = "Bundling JavaScript %s [rollup]" % ctx.outputs.bundle.short_path,
|
|
executable = ctx.executable._rollup,
|
|
inputs = depset(direct_inputs, transitive = [sources]),
|
|
outputs = [ctx.outputs.bundle, ctx.outputs.sourcemap],
|
|
arguments = [args],
|
|
)
|
|
|
|
def _ng_rollup_bundle_impl(ctx):
|
|
if ctx.attr.esm5_sources:
|
|
# Use esm5 sources and build optimzier if ctx.attr.build_optimizer is set
|
|
rollup_config = _write_rollup_config(ctx, build_optimizer = ctx.attr.build_optimizer, root_dir = "/".join([ctx.bin_dir.path, ctx.label.package, esm5_root_dir(ctx)]))
|
|
_run_rollup(ctx, _esm5_entry_point_path(ctx), flatten_esm5(ctx), rollup_config)
|
|
else:
|
|
# Use esm2015 sources and no build optimzier
|
|
rollup_config = _write_rollup_config(ctx, build_optimizer = False, root_dir = ctx.bin_dir.path)
|
|
esm2015_files_depsets = []
|
|
for dep in ctx.attr.deps:
|
|
if JSEcmaScriptModuleInfo in dep:
|
|
esm2015_files_depsets.append(dep[JSEcmaScriptModuleInfo].sources)
|
|
esm2015_files = depset(transitive = esm2015_files_depsets)
|
|
entry_point_path = _to_manifest_path(ctx, _resolve_js_input(ctx.file.entry_point, esm2015_files.to_list()))
|
|
_run_rollup(ctx, entry_point_path, esm2015_files, rollup_config)
|
|
|
|
return DefaultInfo(files = depset([ctx.outputs.bundle, ctx.outputs.sourcemap]))
|
|
|
|
_ng_rollup_bundle = rule(
|
|
implementation = _ng_rollup_bundle_impl,
|
|
attrs = _NG_ROLLUP_BUNDLE_ATTRS,
|
|
outputs = _NG_ROLLUP_BUNDLE_OUTPUTS,
|
|
)
|
|
"""
|
|
Run [Rollup] with the [Build Optimizer] plugin and use esm5 inputs.
|
|
|
|
[Rollup]: https://rollupjs.org/
|
|
[Build Optimizer]: https://www.npmjs.com/package/@angular-devkit/build-optimizer
|
|
"""
|
|
|
|
def ng_rollup_bundle(name, **kwargs):
|
|
"""Rollup with Build Optimizer on esm5 inputs.
|
|
|
|
This provides a variant of the [legacy rollup_bundle] rule that works better for Angular apps.
|
|
|
|
Runs [rollup], [terser_minified] and [brotli] to produce a number of output bundles.
|
|
|
|
es5 : "%{name}.js"
|
|
es5 minified : "%{name}.min.js"
|
|
es5 minified (compressed) : "%{name}.min.js.br",
|
|
es5 minified (debug) : "%{name}.min_debug.js"
|
|
es2015 : "%{name}.es2015.js"
|
|
es2015 minified : "%{name}.min.es2015.js"
|
|
es2015 minified (compressed) : "%{name}.min.js.es2015.br",
|
|
es2015 minified (debug) : "%{name}.min_debug.es2015.js"
|
|
|
|
It registers `@angular-devkit/build-optimizer` as a rollup plugin, to get
|
|
better optimization. It also uses ESM5 format inputs, as this is what
|
|
build-optimizer is hard-coded to look for and transform.
|
|
|
|
[legacy rollup_bundle]: https://github.com/bazelbuild/rules_nodejs/blob/0.38.3/internal/rollup/rollup_bundle.bzl
|
|
[rollup]: https://rollupjs.org/guide/en/
|
|
[terser_minified]: https://bazelbuild.github.io/rules_nodejs/Terser.html
|
|
[brotli]: https://brotli.org/
|
|
"""
|
|
format = kwargs.pop("format", "iife")
|
|
build_optimizer = kwargs.pop("build_optimizer", True)
|
|
visibility = kwargs.pop("visibility", None)
|
|
|
|
# Common arguments for all terser_minified targets
|
|
common_terser_args = {
|
|
# As of terser 4.3.4 license comments are preserved by default. See
|
|
# https://github.com/terser/terser/blob/master/CHANGELOG.md. We want to
|
|
# maintain the comments off behavior. We pass the --comments flag with
|
|
# a regex that always evaluates to false to do this.
|
|
"args": ["--comments", "/bogus_string_to_suppress_all_comments^/"],
|
|
"config_file": "//tools/ng_rollup_bundle:terser_config.json",
|
|
"sourcemap": False,
|
|
}
|
|
|
|
# TODO(gregmagolan): reduce this macro to just use the new @bazel/rollup rollup_bundle
|
|
# once esm5 inputs are no longer needed. _ng_rollup_bundle is just here for esm5 support
|
|
# and once that requirement is removed for Angular 10 then there is nothing that rule is doing
|
|
# that the new @bazel/rollup rollup_bundle rule can't do.
|
|
_ng_rollup_bundle(
|
|
name = name,
|
|
build_optimizer = build_optimizer,
|
|
format = format,
|
|
visibility = visibility,
|
|
**kwargs
|
|
)
|
|
terser_minified(name = name + ".min", src = name, visibility = visibility, **common_terser_args)
|
|
native.filegroup(name = name + ".min.js", srcs = [name + ".min"], visibility = visibility)
|
|
terser_minified(name = name + ".min_debug", src = name, debug = True, visibility = visibility, **common_terser_args)
|
|
native.filegroup(name = name + ".min_debug.js", srcs = [name + ".min_debug"], visibility = visibility)
|
|
npm_package_bin(
|
|
name = "_%s_brotli" % name,
|
|
tool = "//tools/brotli-cli",
|
|
data = [name + ".min.js"],
|
|
outs = [name + ".min.js.br"],
|
|
args = [
|
|
"--output=$(execpath %s.min.js.br)" % name,
|
|
"$(execpath %s.min.js)" % name,
|
|
],
|
|
visibility = visibility,
|
|
)
|
|
|
|
_ng_rollup_bundle(
|
|
name = name + ".es2015",
|
|
esm5_sources = False,
|
|
format = format,
|
|
visibility = visibility,
|
|
**kwargs
|
|
)
|
|
terser_minified(name = name + ".min.es2015", src = name + ".es2015", visibility = visibility, **common_terser_args)
|
|
native.filegroup(name = name + ".min.es2015.js", srcs = [name + ".min.es2015"], visibility = visibility)
|
|
terser_minified(name = name + ".min_debug.es2015", src = name + ".es2015", debug = True, visibility = visibility, **common_terser_args)
|
|
native.filegroup(name = name + ".min_debug.es2015.js", srcs = [name + ".min_debug.es2015"], visibility = visibility)
|
|
npm_package_bin(
|
|
name = "_%s_es2015_brotli" % name,
|
|
tool = "//tools/brotli-cli",
|
|
data = [name + ".min.es2015.js"],
|
|
outs = [name + ".min.es2015.js.br"],
|
|
args = [
|
|
"--output=$(execpath %s.min.es2015.js.br)" % name,
|
|
"$(execpath %s.min.es2015.js)" % name,
|
|
],
|
|
visibility = visibility,
|
|
)
|
|
|
|
def ls_rollup_bundle(name, **kwargs):
|
|
"""A variant of ng_rollup_bundle for the language-service bundle
|
|
|
|
ls_rollup_bundle uses esm5 inputs, outputs AMD and does not use the build optimizer.
|
|
"""
|
|
visibility = kwargs.pop("visibility", None)
|
|
|
|
# Note: the output file is called "umd.js" because of historical reasons.
|
|
# The format is actually AMD and not UMD, but we are afraid to rename
|
|
# the file because that would likely break the IDE and other integrations that
|
|
# have the path hardcoded in them.
|
|
ng_rollup_bundle(
|
|
name = name + ".umd",
|
|
build_optimizer = False,
|
|
format = "amd",
|
|
visibility = visibility,
|
|
**kwargs
|
|
)
|
|
native.alias(
|
|
name = name,
|
|
actual = name + ".umd",
|
|
visibility = visibility,
|
|
)
|