Skip to content

Commit

Permalink
feat: add esbuild package
Browse files Browse the repository at this point in the history
  • Loading branch information
mattem committed Oct 31, 2020
1 parent 12571ee commit 1fff5c6
Show file tree
Hide file tree
Showing 16 changed files with 600 additions and 0 deletions.
5 changes: 5 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ browser_repositories(
firefox = True,
)

# Setup esbuild dependencies
load("//packages/esbuild:index.bzl", "esbuild_repository")

esbuild_repository(name = "esbuild")

#
# Dependencies to run stardoc & generating documentation
#
Expand Down
1 change: 1 addition & 0 deletions docs/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ rules_nodejs_docs(
"Rollup": "//packages/rollup:README.md",
"Terser": "//packages/terser:README.md",
"TypeScript": "//packages/typescript:README.md",
"esbuild": "//packages/esbuild:README.md",
},
tags = [
"fix-windows",
Expand Down
81 changes: 81 additions & 0 deletions packages/esbuild/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright 2020 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load("@build_bazel_rules_nodejs//:tools/defaults.bzl", "codeowners", "pkg_npm")
load("@build_bazel_rules_nodejs//tools/stardoc:index.bzl", "stardoc")
load("//third_party/github.com/bazelbuild/bazel-skylib:rules/copy_file.bzl", "copy_file")

package(default_visibility = ["//visibility:public"])

codeowners(
teams = ["@mattem"],
)

bzl_library(
name = "bzl",
srcs = glob(["**/*.bzl"]),
deps = [
"@build_bazel_rules_nodejs//:bzl",
"@build_bazel_rules_nodejs//internal/common:bzl",
"@build_bazel_rules_nodejs//internal/node:bzl",
],
)

stardoc(
name = "docs",
testonly = True,
out = "index.md",
input = "index.bzl",
tags = ["fix-windows"],
deps = [":bzl"],
)

genrule(
name = "generate_README",
srcs = [
"_README.md",
"index.md",
],
outs = ["README.md"],
cmd = """cat $(execpath _README.md) $(execpath index.md) | sed 's/^##/\\\n##/' > $@""",
tags = ["fix-windows"],
visibility = ["//docs:__pkg__"],
)

copy_file(
name = "npm_version_check",
src = "//internal:npm_version_check.js",
out = ":npm_version_check.js",
)

pkg_npm(
name = "npm_package",
srcs = [
"esbuild.bzl",
"esbuild_repo.bzl",
"index.bzl",
"package.json",
],
build_file_content = " ",
deps = [
":npm_version_check",
] + select({
# FIXME: fix stardoc on Windows; //packages/karma:index.md generation fails with:
# ERROR: D:/b/62unjjin/external/npm_bazel_karma/BUILD.bazel:65:1: Couldn't build file
# external/npm_bazel_karma/docs.raw: Generating proto for Starlark doc for docs failed (Exit 1)
"@bazel_tools//src/conditions:windows": [],
"//conditions:default": [":generate_README"],
}),
)
60 changes: 60 additions & 0 deletions packages/esbuild/_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# esbuild rules for Bazel

The esbuild rules runs the [esbuild](https://github.com/evanw/esbuild) bundler tool with Bazel.

## Installation

Add the `@bazel/esbuild` npm packages to your `devDependencies` in `package.json`.

```
npm install --save-dev @bazel/esbuild
```
or using yarn
```
yarn add -D @bazel/esbuild
```

Add the esbuild repository to the `WORKSPACE` file

```python
load("@npm//@bazel/esbuild:index.bzl", "esbuild_repository")

esbuild_repository(name = "esbuild")
```

The version of `esbuild` can be specified by passing the `version` and `platform_sha` attributes

```python
load("@npm//@bazel/esbuild:index.bzl", "esbuild_repository")

esbuild_repository(
name = "esbuild",
version = "0.7.19",
platform_sha = {
"darwin_64": "deadf43c0868430983234f90781e1b542975a2bc3549b2353303fac236816149",
"linux_64": "2d25ad82dba8f565e8766b838acd3b966f9a2417499105ec10afb01611594ef1",
"windows_64": "135b2ff549d4b1cfa4f8e2226f85ee97641b468aaced7585112ebe8c0af2d766",
}
)
```

## Example use of esbuild

The `esbuild` rule can take a JS or TS dependency tree and bundle it to a single file, or split across multiple files, outputting a directory.

```python
ts_library(
name = "lib",
srcs = ["a.ts"],
)

esbuild(
name = "bundle",
entry_point = "a.ts",
deps = [":lib"],
)
```

The above will create three output files, `bundle.js`, `bundle.js.map` and `bundle_metadata.json` which contains the bundle metadata to aid in debugging and resoloution tracing.

Further options for minifying and splitting are avialable.
207 changes: 207 additions & 0 deletions packages/esbuild/esbuild.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"""
esbuild rule
"""

load("@build_bazel_rules_nodejs//:providers.bzl", "JSEcmaScriptModuleInfo", "JSModuleInfo", "NpmPackageInfo", "node_modules_aspect")
load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "module_mappings_aspect")

def _strip_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

no_ext = _strip_ext(f)
for i in inputs:
if i.extension == "js" or i.extension == "mjs":
if _strip_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 _filter_js(files):
return [f for f in files if f.extension == "js" or f.extension == "mjs"]

def _esbuild_impl(ctx):
# 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)
elif JSModuleInfo in dep:
deps_depsets.append(dep[JSModuleInfo].sources)
elif hasattr(dep, "files"):
deps_depsets.append(dep.files)

if NpmPackageInfo in dep:
deps_depsets.append(dep[NpmPackageInfo].sources)

deps_inputs = depset(transitive = deps_depsets).to_list()
inputs = _filter_js(ctx.files.entry_point) + ctx.files.srcs + deps_inputs

metafile = ctx.actions.declare_file("%s_metadata.json" % ctx.attr.name)
outputs = [metafile]

entry_point = _resolve_js_input(ctx.file.entry_point, inputs)

args = ctx.actions.args()
args.add("--bundle", entry_point.path)
args.add("--sourcemap")
args.add_joined(["--platform", ctx.attr.platform], join_with = "=")
args.add_joined(["--target", ctx.attr.target], join_with = "=")
args.add_joined(["--log-level", "info"], join_with = "=")
args.add_joined(["--metafile", metafile.path], join_with = "=")
args.add_joined(["--define:process.env.NODE_ENV", '"production"'], join_with = "=")

# disable the error limit and show all errors
args.add_joined(["--error-limit", "0"], join_with = "=")

if ctx.attr.splitting:
js_out = ctx.actions.declare_directory("%s" % ctx.attr.name)
outputs.append(js_out)

args.add("--splitting")
args.add_joined(["--format", "esm"], join_with = "=")
args.add_joined(["--outdir", js_out.path], join_with = "=")
else:
js_out = ctx.outputs.output
js_out_map = ctx.outputs.output_map
outputs.extend([js_out, js_out_map])

if ctx.attr.format:
args.add_joined(["--format", ctx.attr.format], join_with = "=")

args.add_joined(["--outfile", js_out.path], join_with = "=")

if len(ctx.attr.module_mappings):
rel_path_to_root = "/".join([".." for seg in ctx.build_file_path.split("/")[:-1]])

# generate a tsconfig.json file with module mappings
# we might be able to figure these out from the DeclarationInfo on the deps?
tsconfig = """{"compilerOptions":{"baseUrl":"%s","rootDirs":["."],"paths":%s}}""" % (rel_path_to_root, ctx.attr.module_mappings)

tsconfig_file = ctx.actions.declare_file("%s_tsconfig.json" % ctx.attr.name)
inputs.append(tsconfig_file)
ctx.actions.write(tsconfig_file, tsconfig)

args.add_joined(["--tsconfig", tsconfig_file.path], join_with = "=")

for ext in ctx.attr.external:
args.add("--external:%s" % ext)

if ctx.attr.minify:
args.add("--minify")

ctx.actions.run(
inputs = inputs,
outputs = outputs,
executable = ctx.executable.esbuild,
arguments = [args],
progress_message = "%s Javascript %s [esbuild]" % ("Bundling" if not ctx.attr.splitting else "Splitting", entry_point.short_path),
execution_requirements = {
"no-remote-exec": "1",
},
)

return [
DefaultInfo(files = depset(outputs)),
]

esbuild_bundle = rule(
attrs = {
"deps": attr.label_list(
aspects = [module_mappings_aspect, node_modules_aspect],
doc = "A list of direct dependencies that are required to build the bundle",
),
"entry_point": attr.label(
mandatory = True,
allow_single_file = True,
doc = "The bundle's entry point (e.g. your main.js or app.js or index.js)",
),
"esbuild": attr.label(
allow_single_file = True,
default = "@esbuild//:bin/esbuild",
executable = True,
cfg = "exec",
doc = "An executable for the esbuild binary, can be overriden if a custom esbuild binary is needed",
),
"external": attr.string_list(
default = [],
doc = "A list of module names that are treated as external and not included in the resulting bundle",
),
"format": attr.string(
values = ["iife", "cjs", "esm", ""],
mandatory = False,
doc = """The output format of the bundle, defaults to iife when platform is browser
and cjs when platform is node. If performing code splitting, defaults to esm""",
),
"minify": attr.bool(
default = False,
doc = "If true, produce a minified output",
),
"module_mappings": attr.string_list_dict(
doc = """A tsconfig style mapping of module names to paths for module import resolution.
To import from 'lib', set the module mapping to the path to the files:
```python
module_mappings = {
"lib": ["path/to/module"],
},
```""",
),
"output": attr.output(
mandatory = False,
doc = "Name of the output file when bundling",
),
"output_map": attr.output(
mandatory = False,
doc = "Name of the output source map when bundling",
),
"platform": attr.string(
default = "browser",
values = ["node", "browser", ""],
doc = "The platform to bundle for",
),
"splitting": attr.bool(
default = False,
doc = """If true, esbuild produces an output directory containing all the output files from code splitting
""",
),
"srcs": attr.label_list(
allow_files = True,
default = [],
doc = """Non-entry point JavaScript source files from the workspace.
You must not repeat file(s) passed to entry_point""",
),
"target": attr.string(
default = "es2015",
doc = "Language target for esbuild",
),
},
implementation = _esbuild_impl,
)

def esbuild(name, splitting = False, **kwargs):
"""esbuild helper macro around the `esbuild_bundle` rule
Args:
name: The name used for this rule and output files
splitting: If `True`, produce a code split bundle in an output directory
**kwargs: All other args from `esbuild_bundle`
"""

if splitting == True:
esbuild_bundle(
name = name,
splitting = True,
**kwargs
)
else:
esbuild_bundle(
name = name,
output = "%s.js" % name,
output_map = "%s.js.map" % name,
**kwargs
)
Loading

0 comments on commit 1fff5c6

Please sign in to comment.