| "Implementation details for js_run_devserver rule" |
| |
| load(":js_binary.bzl", "js_binary_lib") |
| load(":js_helpers.bzl", _gather_files_from_js_infos = "gather_files_from_js_infos") |
| |
| _attrs = js_binary_lib.attrs | { |
| "tool_exec_cfg": attr.label( |
| executable = True, |
| cfg = "exec", |
| ), |
| "tool_target_cfg": attr.label( |
| executable = True, |
| cfg = "target", |
| ), |
| "use_execroot_entry_point": attr.bool( |
| default = True, |
| ), |
| "grant_sandbox_write_permissions": attr.bool(), |
| "allow_execroot_entry_point_with_no_copy_data_to_bin": attr.bool(), |
| "command": attr.string(), |
| } |
| |
| def _file_to_entry_json(f): |
| if "/.aspect_rules_js/" in f.short_path: |
| # Special handling for package store deps; we only include 1st party deps since copying |
| # all 3rd party node_modules over is expensive for typical graphs |
| path_segments = f.path.split("/") |
| package_name_segment = path_segments.index(".aspect_rules_js") + 1 |
| |
| # TODO: @0.0.0 is by default the version of all 1p linked packages, however, it can be overridden by users |
| # if they are manually linking a 1p package and not using workspace. A more robust solution would be to |
| # split handling of 1p and 3p package in the JsInfo provider itself. Other optimizations in the rule set |
| # could also be made if that was the case. |
| if len(path_segments) <= package_name_segment or "@0.0.0" not in path_segments[package_name_segment]: |
| return None |
| |
| return json.encode([f.short_path, 1 if f.is_directory else 0]) |
| |
| def _js_run_devserver_impl(ctx): |
| config_file = ctx.actions.declare_file("{}_config.json".format(ctx.label.name)) |
| entries_json_file = ctx.actions.declare_file("{}_entries.json".format(ctx.label.name)) |
| |
| launcher = js_binary_lib.create_launcher( |
| ctx, |
| log_prefix_rule_set = "aspect_rules_js", |
| log_prefix_rule = "js_run_devserver", |
| fixed_args = [config_file.short_path, entries_json_file.short_path], |
| ) |
| |
| use_tool = ctx.attr.tool_target_cfg or ctx.attr.tool_exec_cfg |
| if use_tool and (not ctx.attr.tool_exec_cfg or not ctx.attr.tool_target_cfg): |
| fail("Internal error") |
| |
| if not use_tool and not ctx.attr.command: |
| fail("Either tool or command must be specified") |
| if use_tool and ctx.attr.command: |
| fail("Only one of tool or command may be specified") |
| |
| transitive_runfiles = [_gather_files_from_js_infos( |
| targets = ctx.attr.data, |
| include_sources = True, |
| include_types = ctx.attr.include_types, |
| include_transitive_sources = ctx.attr.include_transitive_sources, |
| include_transitive_types = ctx.attr.include_types, |
| include_npm_sources = ctx.attr.include_npm_sources, |
| )] |
| |
| default_data_runfiles = [target[DefaultInfo].default_runfiles.files for target in ctx.attr.data] |
| |
| # Build the list of data files to copy to the custom sandbox using ctx.actions.args() |
| entries = ctx.actions.args() |
| entries.set_param_file_format("multiline") |
| entries.add("[") |
| entries.add_joined( |
| depset(transitive = transitive_runfiles + [dep.files for dep in ctx.attr.data] + default_data_runfiles), |
| expand_directories = False, |
| map_each = _file_to_entry_json, |
| join_with = ",", |
| ) |
| entries.add("]") |
| ctx.actions.write(entries_json_file, content = entries) |
| |
| config = {} |
| |
| runfiles_merge_targets = ctx.attr.data[:] |
| |
| if use_tool: |
| if ctx.attr.use_execroot_entry_point: |
| config["tool"] = ctx.executable.tool_target_cfg.short_path |
| config["use_execroot_entry_point"] = "1" |
| config["bazel_bindir"] = ctx.bin_dir.path |
| if ctx.attr.allow_execroot_entry_point_with_no_copy_data_to_bin: |
| config["allow_execroot_entry_point_with_no_copy_data_to_bin"] = "1" |
| runfiles_merge_targets.append(ctx.attr.tool_target_cfg) |
| else: |
| config["tool"] = ctx.executable.tool_exec_cfg.short_path |
| runfiles_merge_targets.append(ctx.attr.tool_exec_cfg) |
| if ctx.attr.command: |
| config["command"] = ctx.attr.command |
| if ctx.attr.grant_sandbox_write_permissions: |
| config["grant_sandbox_write_permissions"] = "1" |
| |
| ctx.actions.write(config_file, json.encode(config)) |
| |
| runfiles = ctx.runfiles( |
| files = ctx.files.data + [config_file, entries_json_file], |
| transitive_files = depset(transitive = transitive_runfiles), |
| ).merge(launcher.runfiles).merge_all([ |
| target[DefaultInfo].default_runfiles |
| for target in runfiles_merge_targets |
| ]) |
| |
| return [ |
| DefaultInfo( |
| executable = launcher.executable, |
| runfiles = runfiles, |
| ), |
| ] |
| |
| js_run_devserver_lib = struct( |
| attrs = _attrs, |
| implementation = _js_run_devserver_impl, |
| toolchains = js_binary_lib.toolchains, |
| ) |
| |
| _js_run_devserver = rule( |
| attrs = js_run_devserver_lib.attrs, |
| implementation = js_run_devserver_lib.implementation, |
| toolchains = js_run_devserver_lib.toolchains, |
| executable = True, |
| ) |
| |
| def js_run_devserver( |
| name, |
| tool = None, |
| command = None, |
| grant_sandbox_write_permissions = False, |
| use_execroot_entry_point = True, |
| allow_execroot_entry_point_with_no_copy_data_to_bin = False, |
| **kwargs): |
| """Runs a devserver via binary target or command. |
| |
| A simple http-server, for example, can be setup as follows, |
| |
| ``` |
| load("@aspect_rules_js//js:defs.bzl", "js_run_devserver") |
| load("@npm//:http-server/package_json.bzl", http_server_bin = "bin") |
| |
| http_server_bin.http_server_binary( |
| name = "http_server", |
| ) |
| |
| js_run_devserver( |
| name = "serve", |
| args = ["."], |
| data = ["index.html"], |
| tool = ":http_server", |
| ) |
| ``` |
| |
| A Next.js devserver can be setup as follows, |
| |
| ``` |
| js_run_devserver( |
| name = "dev", |
| args = ["dev"], |
| command = "./node_modules/.bin/next", |
| data = [ |
| "next.config.js", |
| "package.json", |
| ":node_modules/next", |
| ":node_modules/react", |
| ":node_modules/react-dom", |
| ":node_modules/typescript", |
| "//pages", |
| "//public", |
| "//styles", |
| ], |
| ) |
| ``` |
| |
| where the `./node_modules/.bin/next` bin entry of Next.js is configured in |
| `npm_translate_lock` as such, |
| |
| ``` |
| npm_translate_lock( |
| name = "npm", |
| bins = { |
| # derived from "bin" attribute in node_modules/next/package.json |
| "next": { |
| "next": "./dist/bin/next", |
| }, |
| }, |
| pnpm_lock = "//:pnpm-lock.yaml", |
| ) |
| ``` |
| |
| and run in watch mode using [ibazel](https://github.com/bazelbuild/bazel-watcher) with |
| `ibazel run //:dev`. |
| |
| The devserver specified by either `tool` or `command` is run in a custom sandbox that is more |
| compatible with devserver watch modes in Node.js tools such as Webpack and Next.js. |
| |
| The custom sandbox is populated with the default outputs of all targets in `data` |
| as well as transitive sources & npm links. |
| |
| As an optimization, package store files are explicitly excluded from the sandbox since the npm |
| links will point to the package store in the execroot and Node.js will follow those links as it |
| does within the execroot. As a result, rules_js npm package link targets such as |
| `//:node_modules/next` are handled efficiently. Since these targets are symlinks in the output |
| tree, they are recreated as symlinks in the custom sandbox and do not incur a full copy of the |
| underlying npm packages. |
| |
| Supports running with [ibazel](https://github.com/bazelbuild/bazel-watcher). |
| Only `data` files that change on incremental builds are synchronized when running with ibazel. |
| |
| Note that the use of `alias` targets is not supported by ibazel: https://github.com/bazelbuild/bazel-watcher/issues/100 |
| |
| Args: |
| name: A unique name for this target. |
| |
| tool: The devserver binary target to run. |
| |
| Only one of `command` or `tool` may be specified. |
| |
| command: The devserver command to run. |
| |
| For example, this could be the bin entry of an npm package that is included |
| in data such as `./node_modules/.bin/next`. |
| |
| Using the bin entry of next, for example, resolves issues with Next.js and React |
| being found in multiple node_modules trees when next is run as an encapsulated |
| `js_binary` tool. |
| |
| Only one of `command` or `tool` may be specified. |
| |
| grant_sandbox_write_permissions: If set, write permissions is set on all files copied to the custom sandbox. |
| |
| This can be useful to support some devservers such as Next.js which may, under some |
| circumstances, try to modify files when running. |
| |
| See https://github.com/aspect-build/rules_js/issues/935 for more context. |
| |
| use_execroot_entry_point: Use the `entry_point` script of the `js_binary` `tool` that is in the execroot output tree |
| instead of the copy that is in runfiles. |
| |
| Using the entry point script that is in the execroot output tree means that there will be no conflicting |
| runfiles `node_modules` in the node_modules resolution path which can confuse npm packages such as next and |
| react that don't like being resolved in multiple node_modules trees. This more closely emulates the |
| environment that tools such as Next.js see when they are run outside of Bazel. |
| |
| When True, the `js_binary` tool must have `copy_data_to_bin` set to True (the default) so that all data files |
| needed by the binary are available in the execroot output tree. This requirement can be turned off with by |
| setting `allow_execroot_entry_point_with_no_copy_data_to_bin` to True. |
| |
| allow_execroot_entry_point_with_no_copy_data_to_bin: Turn off validation that the `js_binary` tool |
| has `copy_data_to_bin` set to True when `use_execroot_entry_point` is set to True. |
| |
| See `use_execroot_entry_point` doc for more info. |
| |
| **kwargs: All other args from `js_binary` except for `entry_point` which is set implicitly. |
| |
| `entry_point` is set implicitly by `js_run_devserver` and cannot be overridden. |
| |
| See https://docs.aspect.build/rules/aspect_rules_js/docs/js_binary |
| """ |
| if kwargs.get("entry_point", None): |
| fail("`entry_point` is set implicitly by `js_run_devserver` and cannot be overridden.") |
| |
| # Allow the js_run_devserver rule to execute to be overridden for tests |
| rule_to_execute = kwargs.pop("rule_to_execute", _js_run_devserver) |
| |
| rule_to_execute( |
| name = name, |
| enable_runfiles = select({ |
| Label("@aspect_bazel_lib//lib:enable_runfiles"): True, |
| "//conditions:default": False, |
| }), |
| entry_point = Label("@aspect_rules_js//js/private/devserver:js_devserver_entrypoint"), |
| # This rule speaks the ibazel protocol, supports live reload, supports incremental-build-protocol |
| tags = kwargs.pop("tags", []) + [ |
| "ibazel_live_reload", |
| "ibazel_notify_changes", |
| "supports_incremental_build_protocol", |
| ], |
| tool_exec_cfg = tool, |
| tool_target_cfg = tool, |
| command = command, |
| grant_sandbox_write_permissions = grant_sandbox_write_permissions, |
| use_execroot_entry_point = use_execroot_entry_point, |
| allow_execroot_entry_point_with_no_copy_data_to_bin = allow_execroot_entry_point_with_no_copy_data_to_bin, |
| **kwargs |
| ) |