Please with esbuild
These rules attempt to use esbuild to treat Javascript bundling more like a traditional compiler. By that I mean, there's some analogy to distinct compile and link stages. In doing so, we can fit javascript into the Please paradigm in a neat and incremental way. We can avoid using high level tools such as yarn, or webpack, and instead handle that with Please build definitions.
Compiling
Similar to go tool compile
, esbuild has a concept of resolution that happens before the load phase. This is similar
to how the go compiler resolves import paths to .a
files. I have written a special please
resolver. This takes in
a list of known modules, that we can derrive from the direct dependencies of the rule. This method looks as such:
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
// opts.Modules here are the list of known dependencies of this modules we're "compiling"
if path, ok := opts.Modules[args.Path]; ok {
return api.OnResolveResult{
Path: path,
Namespace: "please",
}, nil
}
// If we don't know about this path, return an empty result and esbuild will try to resolve it
// the normal way. This usually means that it's a internal require in the module itself but could
// also meen there's a missing dep on the build rule.
return api.OnResolveResult{}, nil
}
At this point, esbuild has tagged this require()
as being part of the please
namespace. This means that the
please plugin will handle this going forward. We've essentially resolved the require()
to a filepath based on the
opts.Module
mapping.
The node_module()
and js_library()
rules will use this to resolve thier require()
s to the correct paths.
One great thing about this is that we can resolve the same require differently for different js_library()
or
node_module()
rules. The modules must have a direct dependency on the modules they require. If two modules require
the same module at a different version, we can look at their direct dependencies to pick up the correct one.
Linking
Similar to go tool link
, esbuild has a load
phase. This will be used by the js_binary()
and js_test()
targets
to produce a single bundle.js
. At this point, we have injected some metadata into the require()
calls in the previous
"link" stage. We simply have to read this out and provide the correct data back to esbuild. The code looks like this:
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
// args.Path is set by the resolver above for us
path := filepath.Join(wd, args.Path)
data, err := ioutil.ReadFile(path)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load %v: %v\n", args.Path, err)
os.Exit(1)
}
contents := string(data)
return api.OnLoadResult{
Contents: &contents,
}, nil
}
Considerations
- Can node modules provide resources, or require runtime data?