From b4f322ad319c7ecc29275d94206b3e497a6da736 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Mon, 10 May 2021 04:57:27 -0700 Subject: [PATCH] feat(esbuild): add support for multiple entry points (#2663) --- packages/esbuild/esbuild.bzl | 38 +++++++++++++------- packages/esbuild/helpers.bzl | 22 ++++++++++++ packages/esbuild/test/entries/BUILD.bazel | 36 +++++++++++++++++++ packages/esbuild/test/entries/a.ts | 3 ++ packages/esbuild/test/entries/b.ts | 3 ++ packages/esbuild/test/entries/bundle.spec.js | 25 +++++++++++++ packages/esbuild/test/entries/index.ts | 1 + 7 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 packages/esbuild/test/entries/BUILD.bazel create mode 100644 packages/esbuild/test/entries/a.ts create mode 100644 packages/esbuild/test/entries/b.ts create mode 100644 packages/esbuild/test/entries/bundle.spec.js create mode 100644 packages/esbuild/test/entries/index.ts diff --git a/packages/esbuild/esbuild.bzl b/packages/esbuild/esbuild.bzl index f030559c6b..1a14ebf50a 100644 --- a/packages/esbuild/esbuild.bzl +++ b/packages/esbuild/esbuild.bzl @@ -5,7 +5,7 @@ esbuild rule load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary") load("@build_bazel_rules_nodejs//:providers.bzl", "JSEcmaScriptModuleInfo", "JSModuleInfo", "NpmPackageInfo", "node_modules_aspect", "run_node") load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "MODULE_MAPPINGS_ASPECT_RESULTS_NAME", "module_mappings_aspect") -load(":helpers.bzl", "filter_files", "generate_path_mapping", "resolve_entry_point", "write_jsconfig_file") +load(":helpers.bzl", "desugar_entry_point_names", "filter_files", "generate_path_mapping", "resolve_entry_point", "write_jsconfig_file") def _esbuild_impl(ctx): # For each dep, JSEcmaScriptModuleInfo is used if found, then JSModuleInfo and finally @@ -38,18 +38,21 @@ def _esbuild_impl(ctx): package_name = key.split(":")[0] path_alias_mappings.update(generate_path_mapping(package_name, value[1].replace(ctx.bin_dir.path + "/", ""))) + entry_points = desugar_entry_point_names(ctx.file.entry_point, ctx.files.entry_points) + deps_inputs = depset(transitive = deps_depsets).to_list() - inputs = filter_files(ctx.files.entry_point) + ctx.files.srcs + deps_inputs + inputs = filter_files(entry_points) + ctx.files.srcs + deps_inputs metafile = ctx.actions.declare_file("%s_metadata.json" % ctx.attr.name) outputs = [metafile] - entry_point = resolve_entry_point(ctx.file.entry_point, inputs, ctx.files.srcs) - args = ctx.actions.args() args.use_param_file(param_file_arg = "--esbuild_flags=%s", use_always = True) - args.add("--bundle", entry_point.path) + # the entry point files to bundle + for entry_point in entry_points: + args.add(resolve_entry_point(entry_point, inputs, ctx.files.srcs)) + args.add("--bundle") if len(ctx.attr.sourcemap) > 0: args.add_joined(["--sourcemap", ctx.attr.sourcemap], join_with = "=") @@ -126,7 +129,7 @@ def _esbuild_impl(ctx): inputs = depset(inputs), outputs = outputs, arguments = [launcher_args, args], - progress_message = "%s Javascript %s [esbuild]" % ("Bundling" if not ctx.attr.output_dir else "Splitting", entry_point.short_path), + progress_message = "%s Javascript %s [esbuild]" % ("Bundling" if not ctx.attr.output_dir else "Splitting", " ".join([entry_point.short_path for entry_point in entry_points])), execution_requirements = execution_requirements, mnemonic = "esbuild", env = env, @@ -168,9 +171,19 @@ See https://esbuild.github.io/api/#define for more details 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)", + doc = """The bundle's entry point (e.g. your main.js or app.js or index.js) + +This is a shortcut for the `entry_points` attribute with a single entry. +Specify either this attribute or `entry_point`, but not both. +""", + ), + "entry_points": attr.label_list( + allow_files = True, + doc = """The bundle's entry points (e.g. your main.js or app.js or index.js) + +Specify either this attribute or `entry_point`, but not both. +""", ), "external": attr.string_list( default = [], @@ -183,7 +196,7 @@ See https://esbuild.github.io/api/#external for more details 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. +and cjs when platform is node. If performing code splitting or multiple entry_points are specified, defaults to esm. See https://esbuild.github.io/api/#format for more details """, @@ -229,9 +242,9 @@ file is named 'foo.js', you should set this to 'foo.css'.""", ), "output_dir": attr.bool( default = False, - doc = """If true, esbuild produces an output directory containing all the output files from code splitting + doc = """If true, esbuild produces an output directory containing all the output files from code splitting for multiple entry points -See https://esbuild.github.io/api/#splitting for more details +See https://esbuild.github.io/api/#splitting and https://esbuild.github.io/api/#entry-points for more details """, ), "output_map": attr.output( @@ -308,7 +321,8 @@ def esbuild_macro(name, output_dir = False, **kwargs): entry_point = Label("@build_bazel_rules_nodejs//packages/esbuild:launcher.js"), ) - if output_dir == True: + entry_points = kwargs.get("entry_points", None) + if output_dir == True or entry_points: esbuild( name = name, output_dir = True, diff --git a/packages/esbuild/helpers.bzl b/packages/esbuild/helpers.bzl index 61cfc87894..f18e0f90e6 100644 --- a/packages/esbuild/helpers.bzl +++ b/packages/esbuild/helpers.bzl @@ -40,6 +40,28 @@ def resolve_entry_point(f, inputs, srcs): fail("Could not find corresponding entry point for %s. Add the %s.js to your deps or %s.ts to your srcs" % (f.path, no_ext, no_ext)) +def desugar_entry_point_names(entry_point, entry_points): + """Users can specify entry_point (sugar) or entry_points (long form). + + This function allows our code to treat it like they always used the long form. + + It also validates that exactly one of these attributes should be specified. + + Args: + entry_point: the simple argument for specifying a single entry + entry_points: the long form argument for specifing one or more entry points + + Returns: + the array of entry poitns + """ + if entry_point and entry_points: + fail("Cannot specify both entry_point and entry_points") + if not entry_point and not entry_points: + fail("One of entry_point or entry_points must be specified") + if entry_point: + return [entry_point] + return entry_points + def filter_files(input, endings = ALLOWED_EXTENSIONS): """Filters a list of files for specific endings diff --git a/packages/esbuild/test/entries/BUILD.bazel b/packages/esbuild/test/entries/BUILD.bazel new file mode 100644 index 0000000000..962d5ecce3 --- /dev/null +++ b/packages/esbuild/test/entries/BUILD.bazel @@ -0,0 +1,36 @@ +load("//:index.bzl", "nodejs_test") +load("//packages/esbuild/test:tests.bzl", "esbuild") +load("//packages/typescript:index.bzl", "ts_library") + +ts_library( + name = "index", + srcs = ["index.ts"], + module_name = "lib", +) + +ts_library( + name = "lib", + srcs = [ + "a.ts", + "b.ts", + ], + deps = [":index"], +) + +esbuild( + name = "bundle", + entry_points = [ + "a.ts", + "b.ts", + ], + deps = [":lib"], +) + +nodejs_test( + name = "bundle_test", + data = [ + "bundle.spec.js", + ":bundle", + ], + entry_point = ":bundle.spec.js", +) diff --git a/packages/esbuild/test/entries/a.ts b/packages/esbuild/test/entries/a.ts new file mode 100644 index 0000000000..4979aafe0b --- /dev/null +++ b/packages/esbuild/test/entries/a.ts @@ -0,0 +1,3 @@ +import {NAME} from 'lib'; + +console.log(`Hello ${NAME}`); \ No newline at end of file diff --git a/packages/esbuild/test/entries/b.ts b/packages/esbuild/test/entries/b.ts new file mode 100644 index 0000000000..44589f0fa8 --- /dev/null +++ b/packages/esbuild/test/entries/b.ts @@ -0,0 +1,3 @@ +import {NAME} from 'lib'; + +console.log(`Hello ${NAME} from b.ts`); \ No newline at end of file diff --git a/packages/esbuild/test/entries/bundle.spec.js b/packages/esbuild/test/entries/bundle.spec.js new file mode 100644 index 0000000000..86fe350865 --- /dev/null +++ b/packages/esbuild/test/entries/bundle.spec.js @@ -0,0 +1,25 @@ +const {join} = require('path'); +const {readFileSync, lstatSync} = require('fs'); + +const helper = require(process.env.BAZEL_NODE_RUNFILES_HELPER); +const location = helper.resolve('build_bazel_rules_nodejs/packages/esbuild/test/entries/bundle/'); + +const a = readFileSync(join(location, 'a.js'), {encoding: 'utf8'}); +const b = readFileSync(join(location, 'b.js'), {encoding: 'utf8'}); +const aHasImportOfChunk = a.match(/\/(chunk-[a-zA-Z0-9]+\.js)";/); +const bHasImportOfChunk = b.match(/\/(chunk-[a-zA-Z0-9]+\.js)";/); + +if (!aHasImportOfChunk || !bHasImportOfChunk) { + console.error(`Expected entry_points 'a.js' and 'b.js' to have an import of './chunk-[hash].js'`); +} + +if (aHasImportOfChunk[1] !== bHasImportOfChunk[1]) { + console.error(`Expected entry_points 'a.js' and 'b.js' to the same shared chunk`); +} + +// throws if file does not exist +lstatSync(join(location, aHasImportOfChunk && aHasImportOfChunk[1])); + +process.exit( + (aHasImportOfChunk && bHasImportOfChunk && aHasImportOfChunk[1] === bHasImportOfChunk[1]) ? 0 : + 1); diff --git a/packages/esbuild/test/entries/index.ts b/packages/esbuild/test/entries/index.ts new file mode 100644 index 0000000000..6d6d9837ea --- /dev/null +++ b/packages/esbuild/test/entries/index.ts @@ -0,0 +1 @@ +export const NAME = 'bazel';