angular-cn/tools/ng_rollup_bundle/ng_rollup_bundle.bzl
Paul Gschwendtner c0143cb2ab build: ng_rollup_bundle internal rule should support ngcc (#36044)
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
2020-03-18 15:57:07 -07:00

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,
)