peridot/rules_resf/internal/resf_bundle/resf_bundle.bzl

463 lines
17 KiB
Python

load("@build_bazel_rules_nodejs//:providers.bzl", "JSEcmaScriptModuleInfo", "JSModuleInfo", "JSNamedModuleInfo", "NpmPackageInfo", "node_modules_aspect", "run_node")
load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "module_mappings_aspect")
load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
def _trim_package_node_modules(package_name):
# trim a package name down to its path prior to a node_modules
# segment. 'foo/node_modules/bar' would become 'foo' and
# 'node_modules/bar' would become ''
segments = []
for n in package_name.split("/"):
if n == "node_modules":
break
segments += [n]
return "/".join(segments)
# This function is similar but slightly different than _compute_node_modules_root
# in /internal/node/node.bzl. TODO(gregmagolan): consolidate these functions
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
if ctx.files.node_modules:
# ctx.files.node_modules is not an empty list
node_modules_root = "/".join([f for f in [
ctx.attr.node_modules.label.workspace_root,
_trim_package_node_modules(ctx.attr.node_modules.label.package),
"node_modules",
] if f])
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 and the node_modules attribute is an empty filegroup
# but we still need a node_modules_root even if its empty
node_modules_root = "@npm//:node_modules"
return node_modules_root
def collect_ts_sources(ctx):
non_rerooted_files = [d for d in ctx.files.deps]
if hasattr(ctx.attr, "srcs"):
non_rerooted_files += ctx.files.srcs
rerooted_files = []
for file in non_rerooted_files:
if file.is_directory:
rerooted_files += [file]
continue
path = file.short_path
if (path.startswith("../")):
path = "external/" + path[3:]
rerooted_file = ctx.actions.declare_file(
"%s" % (
path.replace(".closure.js", ".ts").replace(ctx.label.package + "/", ""),
),
)
# Cheap way to create an action that copies a file
# TODO(alexeagle): discuss with Bazel team how we can do something like
# runfiles to create a re-rooted tree. This has performance implications.
ctx.actions.expand_template(
output = rerooted_file,
template = file,
substitutions = {},
)
rerooted_files += [rerooted_file]
#TODO(mrmeku): we should include the files and closure_js_library contents too
return depset(direct = rerooted_files)
def _filter_js_inputs(all_inputs):
# Note: make sure that "all_inputs" is not a depset.
# Iterating over a depset is deprecated!
return [
f
for f in all_inputs
# We also need to include ".map" files as these can be read by
# the "rollup-plugin-sourcemaps" plugin.
if f.path.endswith(".js") or f.path.endswith(".jsx") or f.path.endswith(".json") or f.path.endswith(".map") or f.path.endswith(".ts") or f.path.endswith(".tsx") or f.path.endswith(".css")
]
def get_inputs(ctx, config, dep_files_attr, dep_attr, index_html = None, extra = []):
direct_inputs = [] + extra + dep_files_attr
if config:
direct_inputs.append(config)
if index_html:
direct_inputs.append(index_html)
if ctx.files.node_modules:
direct_inputs += _filter_js_inputs(ctx.files.node_modules)
# Also include files from npm fine grained deps as inputs.
# These deps are identified by the NpmPackageInfo provider.
for d in dep_attr:
if NpmPackageInfo in d:
# Note: we can't avoid calling .to_list() on sources
direct_inputs += _filter_js_inputs(d[NpmPackageInfo].sources.to_list())
else:
direct_inputs += _filter_js_inputs(d.files.to_list())
if ctx.file.license_banner:
direct_inputs += [ctx.file.license_banner]
return direct_inputs
def run_webpack(ctx, sources, config, output, map_output = None, direct_inputs = None, index_html = None):
args = ctx.actions.args()
args.add_all(["--config", config.path])
args.add_all(["--output-path", output.path])
# args.add_all(["--silent"])
outputs = [output]
ctx.actions.run(
progress_message = "Bundling TypeScript %s [webpack]" % ctx.attr.name,
executable = ctx.executable._webpack,
inputs = depset(direct_inputs, transitive = [sources, depset(ctx.files.srcs)]),
outputs = outputs,
arguments = [args],
env = {
"NODE_ENV": "production",
"BABEL_ENV": "production",
"TAILWIND_DISABLE_TOUCH": "true",
},
)
def write_index_html(ctx, filename = "index.hbs", output = None, index_html = None):
html = ctx.actions.declare_file(filename) if not output else output
ctx.actions.expand_template(
output = html,
template = index_html,
substitutions = {
"TMPL_name": ctx.attr.title if ctx.attr.title else "Peridot",
"TMPL_bundle": ctx.label.name,
"TMPL_prefix": ctx.attr.prefix,
},
)
return html
def write_webpack_config(ctx, plugins = [], root_dir = None, filename = "_%s.webpack.config.js", output_format = "iife", additional_entrypoints = [], index_html = None):
"""Generate a rollup config file.
This is also used by the ng_rollup_bundle and ng_package rules in @angular/bazel.
Args:
ctx: Bazel rule execution context
plugins: extra plugins (defaults to [])
See the ng_rollup_bundle in @angular/bazel for example of usage.
root_dir: root directory for module resolution (defaults to None)
filename: output filename pattern (defaults to `_%s.rollup.conf.js`)
output_format: passed to rollup output.format option, e.g. "umd"
additional_entrypoints: additional entry points for code splitting
Returns:
The rollup config file. See https://rollupjs.org/guide/en#configuration-files
"""
config = ctx.actions.declare_file(filename % ctx.label.name)
# build_file_path includes the BUILD.bazel file, transform here to only include the dirname
build_file_dirname = "/".join(ctx.build_file_path.split("/")[:-1])
entrypoints = [ctx.attr.entrypoint] + additional_entrypoints
mappings = dict()
all_deps = ctx.attr.deps
for dep in all_deps:
if hasattr(dep, "module_name"):
mappings[dep.module_name] = dep.label.package
if not root_dir:
# This must be .es6 to match collect_es6_sources.bzl
root_dir = "/".join([ctx.bin_dir.path, build_file_dirname, ctx.label.name + ".es6"])
node_modules_root = _compute_node_modules_root(ctx)
is_default_node_modules = False
if node_modules_root == "node_modules" and ctx.attr.node_modules.label.package == "" and ctx.attr.node_modules.label.name == "node_modules_none":
is_default_node_modules = True
direct_inputs = get_inputs(ctx, config, ctx.files.deps, ctx.attr.deps, index_html = index_html, extra = [ctx.file._tsconfig, ctx.file._babel_config, ctx.file.tailwind_config, ctx.file._base_tailwind_config])
input_paths = []
for input in direct_inputs:
path = input.path
if not "node_modules" in path:
input_paths.append(path)
ctx.actions.expand_template(
output = config,
template = ctx.file._webpack_config_tmpl,
substitutions = {
"TMPL_additional_plugins": ",\n".join(plugins),
"TMPL_banner_file": "\"%s\"" % ctx.file.license_banner.path if ctx.file.license_banner else "undefined",
"TMPL_global_name": ctx.attr.global_name if ctx.attr.global_name else ctx.label.name,
"TMPL_no_suffix_frontend": "true" if ctx.attr.no_suffix_frontend else "false",
"TMPL_inputs": ",".join(["\"%s\"" % e for e in entrypoints]),
"TMPL_module_mappings": str(mappings),
"TMPL_output_format": output_format,
"TMPL_indexHtml": index_html.short_path if index_html != None else "null",
"TMPL_target": str(ctx.label),
"TMPL_title": ctx.attr.title if ctx.attr.title else "Peridot",
"TMPL_body_script": ctx.attr.script,
"TMPL_head_style": ctx.attr.style,
"TMPL_typekit": ctx.attr.typekit,
"TMPL_api_url": ctx.var.API_URL if "API_URL" in ctx.var else "",
"TMPL_api_key": ctx.var.API_KEY if "API_KEY" in ctx.var else "",
"TMPL_tailwind_config": ctx.file.tailwind_config.path,
},
)
return dict({
"webpack": config,
"inputs": direct_inputs,
})
def packserver(ctx, webpack_config, webpack_inputs):
name = "{}.server".format(ctx.attr.name)
direct_inputs = get_inputs(ctx, None, ctx.files.server_deps, ctx.attr.server_deps)
direct_inputs_srcs = get_inputs(ctx, webpack_config, ctx.files.deps, ctx.attr.deps)
"""args = ctx.actions.args()
args.add(ctx.file.server_entrypoint.short_path)
args.add(webpack_config.path)
run_node(
ctx,
arguments = [args],
progress_message = "Packing frontend server %s" % ctx.attr.name,
executable = "_run_child",
inputs = direct_inputs + webpack_inputs + ctx.files.server_srcs + [webpack_config],
outputs = [server],
)"""
node_modules_root = _compute_node_modules_root(ctx)
out_file = ctx.actions.declare_file(name + ".bash")
ctx.actions.expand_template(
template = ctx.file._packserver,
output = out_file,
substitutions = {
"TMPL_run_child": ctx.file._run_child_script.short_path,
"TMPL_node": ctx.executable._node.short_path,
"TMPL_entrypoint": ctx.file.server_entrypoint.short_path,
"TMPL_webpack_config_path": webpack_config.short_path,
},
is_executable = True,
)
runfiles = ctx.runfiles().merge(ctx.attr._node_bash_runfiles[DefaultInfo].default_runfiles)
runfiles = runfiles.merge(ctx.attr._node[DefaultInfo].default_runfiles)
runfiles = runfiles.merge(ctx.runfiles(
transitive_files = ctx.attr._node.files,
files = direct_inputs + direct_inputs_srcs + ctx.files.srcs + webpack_inputs + ctx.files._webpack_data + ctx.files.server_srcs + collect_ts_sources(ctx).to_list() + [webpack_config, ctx.file._run_child_script] + ctx.files._node + ctx.files._node_files,
))
return [DefaultInfo(
files = depset([out_file]),
runfiles = runfiles,
executable = out_file,
)]
def _resf_bundle(ctx):
index_html = ctx.file.index_html
config = write_webpack_config(ctx, index_html = index_html)
webpack_config = config["webpack"]
direct_inputs = config["inputs"]
# Generate the bundles
if ctx.attr.build:
ui = ctx.actions.declare_directory("{}.ui".format(ctx.attr.name))
run_webpack(ctx, collect_ts_sources(ctx), webpack_config, ui, direct_inputs = direct_inputs, index_html = index_html)
files = [ui]
output_group = OutputGroupInfo(
ui = depset(files),
)
runfiles = ctx.runfiles(files)
return [
DefaultInfo(files = depset(files), runfiles = runfiles),
output_group,
]
else:
return packserver(ctx, webpack_config, direct_inputs)
WEBPACK_DATA = [
"//rules_resf/internal/resf_bundle:babel.config.js",
"//rules_resf/internal/resf_bundle:tailwind.config.js",
"//rules_resf/internal/resf_bundle:index.hbs",
"//rules_resf/internal/resf_bundle:tsconfig.json",
"@npm//@babel/plugin-transform-modules-commonjs",
"@npm//@babel/preset-env",
"@npm//@babel/preset-react",
"@npm//@babel/preset-typescript",
"@npm//@tailwindcss/forms",
"@npm//autoprefixer",
"@npm//babel-loader",
"@npm//babel-plugin-import",
"@npm//compression-webpack-plugin",
"@npm//css-loader",
"@npm//error-stack-parser",
"@npm//file-loader",
"@npm//glob",
"@npm//html-webpack-plugin",
"@npm//fs-extra",
"@npm//mini-css-extract-plugin",
"@npm//native-url",
"@npm//optimize-css-assets-webpack-plugin",
"@npm//postcss",
"@npm//postcss-loader",
"@npm//purgecss-webpack-plugin",
"@npm//stackframe",
"@npm//strip-ansi",
"@npm//style-loader",
"@npm//tailwindcss",
"@npm//terser-webpack-plugin",
"@npm//@pmmmwh/react-refresh-webpack-plugin",
"@npm//react-refresh",
"@npm//type-fest",
"@npm//webpack",
"@npm//webpack-cli",
"@npm//webpack-mild-compile",
"@npm//ansi-html-community",
"@npm//core-js-pure",
]
resf_bundle_ATTRS = {
"title": attr.string(),
"script": attr.string(
default = "",
),
"style": attr.string(
default = "",
),
"typekit": attr.string(
#default = "https://use.typekit.net/fjm0njo.css",
default = "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap",
),
"index_html": attr.label(
default = Label("//rules_resf/internal/resf_bundle:index.hbs"),
allow_single_file = True,
),
"prefix": attr.string(
default = "/",
),
"srcs": attr.label_list(
allow_files = True,
default = [],
),
"deps": attr.label_list(
aspects = [module_mappings_aspect, node_modules_aspect],
allow_files = True,
),
"node_modules": attr.label_list(
allow_files = True,
),
"license_banner": attr.label(
allow_single_file = True,
),
"global_name": attr.string(),
"no_suffix_frontend": attr.bool(
default = False,
),
"build": attr.bool(
default = True,
),
"server_entrypoint": attr.label(
allow_single_file = True,
mandatory = True,
),
"server_srcs": attr.label_list(
allow_files = True,
mandatory = True,
),
"server_deps": attr.label_list(
aspects = [module_mappings_aspect, node_modules_aspect],
),
"_webpack_config_tmpl": attr.label(
default = Label("//rules_resf/internal/resf_bundle:webpack.config.js"),
allow_single_file = True,
),
"_webpack": attr.label(
default = Label("//rules_resf/internal/resf_bundle:webpack"),
executable = True,
cfg = "host",
allow_files = True,
),
"_webpack_data": attr.label_list(
default = WEBPACK_DATA,
allow_files = True,
),
"_node": attr.label(
default = Label("@nodejs//:node_bin"),
allow_single_file = True,
executable = True,
cfg = "host",
),
"_node_files": attr.label_list(
default = [Label("@nodejs//:node_files")],
allow_files = True,
),
"_run_child": attr.label(
default = Label("//rules_resf/internal/resf_bundle:run_child"),
executable = True,
cfg = "host",
allow_files = True,
),
"_run_child_script": attr.label(
default = Label("//rules_resf/internal/resf_bundle:run_child.mjs"),
allow_single_file = True,
),
"_packserver": attr.label(
default = Label("//rules_resf/internal/resf_bundle:packserver.bash"),
allow_single_file = True,
),
"_tsconfig": attr.label(
default = Label("//rules_resf/internal/resf_bundle:tsconfig.json"),
allow_single_file = True,
),
"entrypoint": attr.string(
default = "src/entrypoint.tsx",
),
"entry_point": attr.string(
mandatory = False,
),
"_babel_config": attr.label(
default = Label("//rules_resf/internal/resf_bundle:babel.config.js"),
allow_single_file = True,
),
"tailwind_config": attr.label(
default = Label("//rules_resf/internal/resf_bundle:tailwind.config.js"),
allow_single_file = True,
),
"_base_tailwind_config": attr.label(
default = Label("//rules_resf/internal/resf_bundle:tailwind.config.js"),
allow_single_file = True,
),
"_bash_runfiles": attr.label(
default = Label("@bazel_tools//tools/bash/runfiles"),
),
"_node_bash_runfiles": attr.label(
default = Label("@build_bazel_rules_nodejs//third_party/github.com/bazelbuild/bazel/tools/bash/runfiles"),
),
}
resf_bundle = rule(
implementation = _resf_bundle,
attrs = resf_bundle_ATTRS,
)
resf_bundle_run = rule(
implementation = _resf_bundle,
attrs = resf_bundle_ATTRS,
executable = True,
)