diff --git a/internal/linker/link_node_modules.bzl b/internal/linker/link_node_modules.bzl index 0f9db7ae71..4710b1bb14 100644 --- a/internal/linker/link_node_modules.bzl +++ b/internal/linker/link_node_modules.bzl @@ -56,16 +56,18 @@ def _link_mapping(label, mappings, k, v): else: return True -def write_node_modules_manifest(ctx, extra_data = [], mnemonic = None): +def write_node_modules_manifest(ctx, extra_data = [], mnemonic = None, link_workspace_root = False): """Writes a manifest file read by the linker, containing info about resolving runtime dependencies Args: ctx: starlark rule execution context extra_data: labels to search for npm packages that need to be linked (ctx.attr.deps and ctx.attr.data will always be searched) mnemonic: optional action mnemonic, used to differentiate module mapping files from the same rule context + link_workspace_root: Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'. + If source files need to be required then they can be copied to the bin_dir with copy_to_bin. """ - mappings = {} + mappings = {ctx.workspace_name: ["execroot", ctx.bin_dir.path]} if link_workspace_root else {} node_modules_root = "" # Look through data/deps attributes to find... diff --git a/internal/linker/test/workspace_link/BUILD.bazel b/internal/linker/test/workspace_link/BUILD.bazel new file mode 100644 index 0000000000..3dd30cd4e5 --- /dev/null +++ b/internal/linker/test/workspace_link/BUILD.bazel @@ -0,0 +1,12 @@ +load("//packages/jasmine:index.bzl", "jasmine_node_test") + +jasmine_node_test( + name = "test", + srcs = ["test.js"], + link_workspace_root = True, + templated_args = ["--nobazel_patch_module_resolver"], + deps = [ + "//internal/linker/test/workspace_link/bar", + "//internal/linker/test/workspace_link/foo", + ], +) diff --git a/internal/linker/test/workspace_link/bar/BUILD.bazel b/internal/linker/test/workspace_link/bar/BUILD.bazel new file mode 100644 index 0000000000..14a1576186 --- /dev/null +++ b/internal/linker/test/workspace_link/bar/BUILD.bazel @@ -0,0 +1,10 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin") + +copy_to_bin( + name = "bar", + srcs = [ + "main.js", + "package.json", + ], + visibility = ["//internal/linker/test/workspace_link:__pkg__"], +) diff --git a/internal/linker/test/workspace_link/bar/main.js b/internal/linker/test/workspace_link/bar/main.js new file mode 100644 index 0000000000..282afcefd5 --- /dev/null +++ b/internal/linker/test/workspace_link/bar/main.js @@ -0,0 +1,3 @@ +module.exports = { + bar: 'bar', +} diff --git a/internal/linker/test/workspace_link/bar/package.json b/internal/linker/test/workspace_link/bar/package.json new file mode 100644 index 0000000000..fdc36a9edb --- /dev/null +++ b/internal/linker/test/workspace_link/bar/package.json @@ -0,0 +1,5 @@ +{ + "name": "bar", + "main": "main.js", + "typings": "main.d.ts" +} diff --git a/internal/linker/test/workspace_link/foo/BUILD.bazel b/internal/linker/test/workspace_link/foo/BUILD.bazel new file mode 100644 index 0000000000..27ecf978f1 --- /dev/null +++ b/internal/linker/test/workspace_link/foo/BUILD.bazel @@ -0,0 +1,35 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin") +load("@npm//typescript:index.bzl", "tsc") + +tsc( + name = "foo_lib", + outs = [ + "main.d.ts", + "main.js", + ], + args = [ + "-p", + "$(execpath tsconfig.json)", + "--outDir", + # $(RULEDIR) is a shorthand for the dist/bin directory where Bazel requires we write outputs + "$(RULEDIR)", + ], + data = [ + "main.ts", + "tsconfig.json", + ], +) + +copy_to_bin( + name = "foo_files", + srcs = ["package.json"], +) + +filegroup( + name = "foo", + srcs = [ + ":foo_files", + ":foo_lib", + ], + visibility = ["//internal/linker/test/workspace_link:__pkg__"], +) diff --git a/internal/linker/test/workspace_link/foo/main.ts b/internal/linker/test/workspace_link/foo/main.ts new file mode 100644 index 0000000000..e6f163ae0f --- /dev/null +++ b/internal/linker/test/workspace_link/foo/main.ts @@ -0,0 +1 @@ +export const foo: string = 'foo'; diff --git a/internal/linker/test/workspace_link/foo/package.json b/internal/linker/test/workspace_link/foo/package.json new file mode 100644 index 0000000000..9f06a3c4a7 --- /dev/null +++ b/internal/linker/test/workspace_link/foo/package.json @@ -0,0 +1,5 @@ +{ + "name": "foo", + "main": "main.js", + "typings": "main.d.ts" +} diff --git a/internal/linker/test/workspace_link/foo/tsconfig.json b/internal/linker/test/workspace_link/foo/tsconfig.json new file mode 100644 index 0000000000..67dcdc9d6a --- /dev/null +++ b/internal/linker/test/workspace_link/foo/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "declaration": true, + "types": [] + } +} \ No newline at end of file diff --git a/internal/linker/test/workspace_link/test.js b/internal/linker/test/workspace_link/test.js new file mode 100644 index 0000000000..9a44d4668d --- /dev/null +++ b/internal/linker/test/workspace_link/test.js @@ -0,0 +1,8 @@ +describe('linker', () => { + it('should be able to require by absolute path when link_workspace_root is True', () => { + const foo = require('build_bazel_rules_nodejs/internal/linker/test/workspace_link/foo'); + expect(foo.foo).toBe('foo'); + const bar = require('build_bazel_rules_nodejs/internal/linker/test/workspace_link/bar'); + expect(bar.bar).toBe('bar'); + }); +}); diff --git a/internal/node/node.bzl b/internal/node/node.bzl index 9a8f326555..e4f0e917a2 100644 --- a/internal/node/node.bzl +++ b/internal/node/node.bzl @@ -151,7 +151,7 @@ def _to_execroot_path(ctx, file): return file.path def _nodejs_binary_impl(ctx): - node_modules_manifest = write_node_modules_manifest(ctx) + node_modules_manifest = write_node_modules_manifest(ctx, link_workspace_root = ctx.attr.link_workspace_root) node_modules_depsets = [] node_modules_depsets.append(depset(ctx.files.node_modules)) if NpmPackageInfo in ctx.attr.node_modules: @@ -422,6 +422,10 @@ nodejs_binary( mandatory = True, allow_single_file = True, ), + "link_workspace_root": attr.bool( + doc = """Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'. +If source files need to be required then they can be copied to the bin_dir with copy_to_bin.""", + ), "node_modules": attr.label( doc = """The npm packages which should be available to `require()` during execution. diff --git a/internal/node/npm_package_bin.bzl b/internal/node/npm_package_bin.bzl index 961ec7559d..1ed16b768e 100644 --- a/internal/node/npm_package_bin.bzl +++ b/internal/node/npm_package_bin.bzl @@ -11,6 +11,7 @@ _ATTRS = { "configuration_env_vars": attr.string_list(default = []), "data": attr.label_list(allow_files = True, aspects = [module_mappings_aspect, node_modules_aspect]), "exit_code_out": attr.output(), + "link_workspace_root": attr.bool(), "output_dir": attr.bool(), "outs": attr.output_list(), "stderr": attr.output(), @@ -78,6 +79,7 @@ def _impl(ctx): stdout = ctx.outputs.stdout, stderr = ctx.outputs.stderr, exit_code_out = ctx.outputs.exit_code_out, + link_workspace_root = ctx.attr.link_workspace_root, ) return [DefaultInfo(files = depset(outputs + tool_outputs))] @@ -87,7 +89,7 @@ _npm_package_bin = rule( attrs = _ATTRS, ) -def npm_package_bin(tool = None, package = None, package_bin = None, data = [], outs = [], args = [], output_dir = False, **kwargs): +def npm_package_bin(tool = None, package = None, package_bin = None, data = [], outs = [], args = [], output_dir = False, link_workspace_root = False, **kwargs): """Run an arbitrary npm package binary (e.g. a program under node_modules/.bin/*) under Bazel. It must produce outputs. If you just want to run a program with `bazel run`, use the nodejs_binary rule. @@ -162,6 +164,8 @@ def npm_package_bin(tool = None, package = None, package_bin = None, data = [], package_bin: the "bin" entry from `package` that should be run. By default package_bin is the same string as `package` tool: a label for a binary to run, like `@npm//terser/bin:terser`. This is the longer form of package/package_bin. Note that you can also refer to a binary in your local workspace. + link_workspace_root: Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'. + If source files need to be required then they can be copied to the bin_dir with copy_to_bin. """ if not tool: if not package: @@ -175,5 +179,6 @@ def npm_package_bin(tool = None, package = None, package_bin = None, data = [], args = args, output_dir = output_dir, tool = tool, + link_workspace_root = link_workspace_root, **kwargs ) diff --git a/internal/providers/node_runtime_deps_info.bzl b/internal/providers/node_runtime_deps_info.bzl index 4c94d9b525..e08fef2bb8 100644 --- a/internal/providers/node_runtime_deps_info.bzl +++ b/internal/providers/node_runtime_deps_info.bzl @@ -66,6 +66,9 @@ def run_node(ctx, inputs, arguments, executable, **kwargs): inputs: list or depset of inputs to the action arguments: list or ctx.actions.Args object containing arguments to pass to the executable executable: stringy representation of the executable this action will run, eg eg. "my_executable" rather than ctx.executable.my_executable + mnemonic: optional action mnemonic, used to differentiate module mapping files from the same rule context + link_workspace_root: Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'. + If source files need to be required then they can be copied to the bin_dir with copy_to_bin. kwargs: all other args accepted by ctx.actions.run """ if (type(executable) != "string"): @@ -82,8 +85,15 @@ def run_node(ctx, inputs, arguments, executable, **kwargs): extra_inputs = exec_attr[NodeRuntimeDepsInfo].deps link_data = exec_attr[NodeRuntimeDepsInfo].pkgs + # NB: mnemonic is also passed to ctx.actions.run below mnemonic = kwargs.get("mnemonic") - modules_manifest = write_node_modules_manifest(ctx, link_data, mnemonic) + link_workspace_root = kwargs.pop("link_workspace_root", False) + modules_manifest = write_node_modules_manifest( + ctx, + extra_data = link_data, + mnemonic = mnemonic, + link_workspace_root = link_workspace_root, + ) add_arg(arguments, "--bazel_node_modules_manifest=%s" % modules_manifest.path) stdout_file = kwargs.pop("stdout", None) diff --git a/packages/rollup/rollup_bundle.bzl b/packages/rollup/rollup_bundle.bzl index a0eda3da32..e5d6ce077a 100644 --- a/packages/rollup/rollup_bundle.bzl +++ b/packages/rollup/rollup_bundle.bzl @@ -105,6 +105,10 @@ Either this attribute or `entry_point` must be specified, but not both. values = ["amd", "cjs", "esm", "iife", "umd", "system"], default = "esm", ), + "link_workspace_root": attr.bool( + doc = """Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'. +If source files need to be required then they can be copied to the bin_dir with copy_to_bin.""", + ), "output_dir": attr.bool( doc = """Whether to produce a directory output. @@ -349,6 +353,7 @@ def _rollup_bundle(ctx): mnemonic = "Rollup", execution_requirements = execution_requirements, env = {"COMPILATION_MODE": ctx.var["COMPILATION_MODE"]}, + link_workspace_root = ctx.attr.link_workspace_root, ) outputs_depset = depset(outputs) diff --git a/packages/rollup/test/workspace_link/BUILD.bazel b/packages/rollup/test/workspace_link/BUILD.bazel new file mode 100644 index 0000000000..9abc8d3100 --- /dev/null +++ b/packages/rollup/test/workspace_link/BUILD.bazel @@ -0,0 +1,30 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin") +load("//packages/jasmine:index.bzl", "jasmine_node_test") +load("//packages/rollup:index.bzl", "rollup_bundle") + +copy_to_bin( + name = "foo", + srcs = ["foo.js"], +) + +rollup_bundle( + name = "bundle", + srcs = [ + "bar.js", + "main.js", + ":foo", + ], + config_file = "rollup.config.js", + entry_point = "main.js", + link_workspace_root = True, + deps = [ + "@npm//@rollup/plugin-commonjs", + "@npm//@rollup/plugin-node-resolve", + ], +) + +jasmine_node_test( + name = "test", + srcs = ["spec.js"], + deps = ["bundle"], +) diff --git a/packages/rollup/test/workspace_link/bar.js b/packages/rollup/test/workspace_link/bar.js new file mode 100644 index 0000000000..bab638710c --- /dev/null +++ b/packages/rollup/test/workspace_link/bar.js @@ -0,0 +1 @@ +export const bar = 'bar'; \ No newline at end of file diff --git a/packages/rollup/test/workspace_link/foo.js b/packages/rollup/test/workspace_link/foo.js new file mode 100644 index 0000000000..f4596d5406 --- /dev/null +++ b/packages/rollup/test/workspace_link/foo.js @@ -0,0 +1 @@ +export const foo = 'foo'; \ No newline at end of file diff --git a/packages/rollup/test/workspace_link/main.js b/packages/rollup/test/workspace_link/main.js new file mode 100644 index 0000000000..eb8337298f --- /dev/null +++ b/packages/rollup/test/workspace_link/main.js @@ -0,0 +1,5 @@ +import * as foo from 'build_bazel_rules_nodejs/packages/rollup/test/workspace_link/foo'; +import * as bar from './bar'; + +console.log(foo); +console.log(bar); diff --git a/packages/rollup/test/workspace_link/rollup.config.js b/packages/rollup/test/workspace_link/rollup.config.js new file mode 100644 index 0000000000..da6ceea4d0 --- /dev/null +++ b/packages/rollup/test/workspace_link/rollup.config.js @@ -0,0 +1,15 @@ +import commonjs from '@rollup/plugin-commonjs'; +import nodeResolve from '@rollup/plugin-node-resolve'; + +module.exports = { + onwarn: (warning) => { + // Always fail on warnings, assuming we don't know which are harmless. + // We can add exclusions here based on warning.code, if we discover some + // types of warning should always be ignored under bazel. + throw new Error(warning.message); + }, + plugins: [ + nodeResolve(), + commonjs(), + ], +}; diff --git a/packages/rollup/test/workspace_link/spec.js b/packages/rollup/test/workspace_link/spec.js new file mode 100644 index 0000000000..af26734cd0 --- /dev/null +++ b/packages/rollup/test/workspace_link/spec.js @@ -0,0 +1,11 @@ +const fs = require('fs'); +const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']); + +describe('rollup', () => { + it('should bundle absolute & relative imports', async () => { + const file = runfiles.resolvePackageRelative('bundle.js'); + const bundle = fs.readFileSync(file, 'utf-8'); + expect(bundle).toContain(`const foo = 'foo';`); + expect(bundle).toContain(`const bar = 'bar';`); + }); +}); diff --git a/packages/terser/terser_minified.bzl b/packages/terser/terser_minified.bzl index efe4c258c4..2b4fed0878 100644 --- a/packages/terser/terser_minified.bzl +++ b/packages/terser/terser_minified.bzl @@ -82,6 +82,10 @@ Instead of setting this attribute, consider using debugging compilation mode ins bazel build --compilation_mode=dbg //my/terser:target so that it only affects the current build. """, + ), + "link_workspace_root": attr.bool( + doc = """Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'. +If source files need to be required then they can be copied to the bin_dir with copy_to_bin.""", ), "sourcemap": attr.bool( doc = "Whether to produce a .js.map output", @@ -183,6 +187,7 @@ def _terser(ctx): arguments = [args], env = {"COMPILATION_MODE": ctx.var["COMPILATION_MODE"]}, progress_message = "Minifying JavaScript %s [terser]" % (outputs[0].short_path), + link_workspace_root = ctx.attr.link_workspace_root, ) return [ diff --git a/packages/typescript/internal/build_defs.bzl b/packages/typescript/internal/build_defs.bzl index b4f202ee27..4be4a83182 100644 --- a/packages/typescript/internal/build_defs.bzl +++ b/packages/typescript/internal/build_defs.bzl @@ -191,6 +191,7 @@ def _compile_action(ctx, inputs, outputs, tsconfig_file, node_opts, description arguments = arguments, executable = "compiler", env = {"COMPILATION_MODE": ctx.var["COMPILATION_MODE"]}, + link_workspace_root = ctx.attr.link_workspace_root, ) # Enable the replay_params in case an aspect needs to re-build this library. @@ -384,6 +385,10 @@ This value will override the `target` option in the user supplied tsconfig.""", default = _DEVMODE_TARGET_DEFAULT, ), "internal_testing_type_check_dependencies": attr.bool(default = False, doc = "Testing only, whether to type check inputs that aren't srcs."), + "link_workspace_root": attr.bool( + doc = """Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'. + If source files need to be required then they can be copied to the bin_dir with copy_to_bin.""", + ), "node_modules": attr.label( doc = """The npm packages which should be available during the compile. diff --git a/packages/typescript/internal/ts_project.bzl b/packages/typescript/internal/ts_project.bzl index 7768d13417..636365e8de 100644 --- a/packages/typescript/internal/ts_project.bzl +++ b/packages/typescript/internal/ts_project.bzl @@ -16,6 +16,7 @@ _ATTRS = { "declaration_dir": attr.string(), "deps": attr.label_list(providers = [DeclarationInfo], aspects = [module_mappings_aspect]), "extends": attr.label_list(allow_files = [".json"]), + "link_workspace_root": attr.bool(), "out_dir": attr.string(), "root_dir": attr.string(), # NB: no restriction on extensions here, because tsc sometimes adds type-check support @@ -153,6 +154,7 @@ def _ts_project_impl(ctx): ctx.label, ctx.file.tsconfig.short_path, ), + link_workspace_root = ctx.attr.link_workspace_root, ) providers = [ @@ -271,6 +273,7 @@ def ts_project_macro( declaration_dir = None, out_dir = None, root_dir = None, + link_workspace_root = False, **kwargs): """Compiles one TypeScript project using `tsc --project` @@ -465,6 +468,9 @@ def ts_project_macro( ts_build_info_file: the user-specified value of `tsBuildInfoFile` from the tsconfig. Helps Bazel to predict the path where the .tsbuildinfo output is written. + link_workspace_root: Link the workspace root to the bin_dir to support absolute requires like 'my_wksp/path/to/file'. + If source files need to be required then they can be copied to the bin_dir with copy_to_bin. + **kwargs: passed through to underlying rule, allows eg. visibility, tags """ @@ -550,5 +556,6 @@ def ts_project_macro( typing_maps_outs = _out_paths(srcs, typings_out_dir, root_dir, ".d.ts.map") if declaration_map else [], buildinfo_out = tsbuildinfo_path if composite or incremental else None, tsc = tsc, + link_workspace_root = link_workspace_root, **kwargs )