Skip to content

Commit a47410e

Browse files
committed
feat(builtin): introduce dynamic dependencies concept
This lets our generated nodejs_binary targets for npm packages express dynamic runtime-only dependencies This is needed for example so that rollup binary can require the rollup-plugin-* packages
1 parent ec6e0d1 commit a47410e

6 files changed

Lines changed: 81 additions & 10 deletions

File tree

WORKSPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ yarn_install(
219219

220220
yarn_install(
221221
name = "npm_install_test",
222+
# exercise the dynamic_deps feature, even though it doesn't make sense for a real jasmine binary to depend on zone.js
223+
# This will just inject an extra data[] dependency into the jasmine_bin generated target.
224+
dynamic_deps = {"jasmine": "zone.js"},
222225
manual_build_file_contents = """
223226
filegroup(
224227
name = "test_files",

internal/npm_install/generate_build_file.js

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const RULE_TYPE = args[1];
5858
const ERROR_ON_BAZEL_FILES = parseInt(args[2]);
5959
const LOCK_FILE_LABEL = args[3];
6060
const INCLUDED_FILES = args[4] ? args[4].split(',') : [];
61+
const DYNAMIC_DEPS = JSON.parse(args[5] || '{}');
6162

6263
if (require.main === module) {
6364
main();
@@ -103,6 +104,7 @@ function main() {
103104
module.exports = {
104105
main,
105106
printPackageBin,
107+
addDynamicDependencies,
106108
};
107109

108110
/**
@@ -436,6 +438,27 @@ function hasRootBuildFile(pkg, rootPath) {
436438
return false;
437439
}
438440

441+
function addDynamicDependencies(pkgs, dynamic_deps = DYNAMIC_DEPS) {
442+
pkgs.forEach(p => {
443+
function match(name) {
444+
// Automatically include dynamic dependency on plugins of the form pkg-plugin-foo
445+
if (name.startsWith(`${p._name}-plugin-`)) return true;
446+
447+
const value = dynamic_deps[p._name];
448+
if (name === value) return true;
449+
450+
// Support wildcard match
451+
if (value && value.includes('*') && name.startsWith(value.substring(0, value.indexOf('*')))) {
452+
return true;
453+
}
454+
455+
return false;
456+
}
457+
p._dynamicDependencies =
458+
pkgs.filter(x => !!x._name && match(x._name)).map(dyn => `//${dyn._dir}:${dyn._name}`);
459+
});
460+
}
461+
439462
/**
440463
* Finds and returns an array of all packages under a given path.
441464
*/
@@ -460,6 +483,8 @@ function findPackages(p = 'node_modules') {
460483
packages.forEach(
461484
f => pkgs.push(parsePackage(f), ...findPackages(path.posix.join(f, 'node_modules'))));
462485

486+
addDynamicDependencies(pkgs);
487+
463488
const scopes = listing.filter(f => f.startsWith('@'))
464489
.map(f => path.posix.join(p, f))
465490
.filter(f => isDirectory(f));
@@ -582,10 +607,10 @@ function cleanupEntryPointPath(p) {
582607
}
583608

584609
/**
585-
* Cleans up the given path
610+
* Cleans up the given path
586611
* Then tries to resolve the path into a file and warns if in DEBUG and the file dosen't exist
587-
* @param {any} pkg
588-
* @param {string} path
612+
* @param {any} pkg
613+
* @param {string} path
589614
* @returns {string | undefined}
590615
*/
591616
function findEntryFile(pkg, path) {
@@ -631,9 +656,9 @@ function resolveMainFile(pkg, mainFileName) {
631656
}
632657

633658
/**
634-
* Tries to resolve the mainFile from a given pkg
659+
* Tries to resolve the mainFile from a given pkg
635660
* This uses seveal mainFileNames in priority to find a correct usable file
636-
* @param {any} pkg
661+
* @param {any} pkg
637662
* @returns {string | undefined}
638663
*/
639664
function resolvePkgMainFile(pkg) {
@@ -953,6 +978,10 @@ function printPackageBin(pkg) {
953978
result = `load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
954979
955980
`;
981+
const data = [`//${pkg._dir}:${pkg._name}`];
982+
if (pkg._dynamicDependencies) {
983+
data.push(...pkg._dynamicDependencies);
984+
}
956985

957986
for (const [name, path] of executables.entries()) {
958987
// Handle additionalAttributes of format:
@@ -977,7 +1006,7 @@ nodejs_binary(
9771006
name = "${name}",
9781007
entry_point = "//:node_modules/${pkg._dir}/${path}",
9791008
install_source_map_support = False,
980-
data = ["//${pkg._dir}:${pkg._name}"],${additionalAttributes}
1009+
data = [${data.map(p => `"${p}"`).join(', ')}],${additionalAttributes}
9811010
)
9821011
9831012
`;

internal/npm_install/npm_install.bzl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ COMMON_ATTRIBUTES = dict(dict(), **{
6565
If symlink_node_modules is True, this attribute is ignored since
6666
the dependency manager will run in the package.json location.""",
6767
),
68+
"dynamic_deps": attr.string_dict(
69+
doc = """Declare implicit dependencies between npm packages.
70+
71+
In many cases, an npm package doesn't list a dependency on another package, yet still require()s it.
72+
One example is plugins, where a tool like rollup can require rollup-plugin-json if the user installed it.
73+
Another example is the tsc_wrapped binary in @bazel/typescript which can require tsickle if its installed.
74+
75+
Note, this may sound like "optionalDependencies" but that field in package.json actually means real dependencies
76+
which are installed, but failures on installation are ignored.
77+
78+
We must declare these dependencies so that Bazel includes the maybe-require()d package in the inputs to the program.""",
79+
default = {"@bazel/typescript": "tsickle"},
80+
),
6881
"exclude_packages": attr.string_list(
6982
doc = """DEPRECATED. This attribute is no longer used.""",
7083
),
@@ -141,6 +154,7 @@ def _create_build_files(repository_ctx, rule_type, node, lock_file):
141154
"1" if error_on_build_files else "0",
142155
str(lock_file),
143156
",".join(repository_ctx.attr.included_files),
157+
str(repository_ctx.attr.dynamic_deps),
144158
])
145159
if result.return_code:
146160
fail("generate_build_file.js failed: \nSTDOUT:\n%s\nSTDERR:\n%s" % (result.stdout, result.stderr))

internal/npm_install/test/generate_build_file.spec.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const {check, files} = require('./check');
2-
const {printPackageBin} = require('../generate_build_file');
2+
const {printPackageBin, addDynamicDependencies} = require('../generate_build_file');
33

44
describe('build file generator', () => {
55
describe('integration test', () => {
@@ -77,4 +77,27 @@ describe('build file generator', () => {
7777
.toContain('nodejs_binary(');
7878
});
7979
});
80+
81+
describe('dynamic dependencies', () => {
82+
it('should include requested dynamic dependencies in nodejs_binary data', () => {
83+
const pkgs = [{_name: 'foo', bin: 'foobin', _dir: 'some_dir'}, {_name: 'bar', _dir: 'bar'}];
84+
addDynamicDependencies(pkgs, {'foo': 'bar'});
85+
expect(pkgs[0]._dynamicDependencies).toEqual(['//bar:bar']);
86+
expect(printPackageBin(pkgs[0])).toContain('data = ["//some_dir:foo", "//bar:bar"]');
87+
});
88+
it('should support wildcard', () => {
89+
const pkgs = [{_name: 'foo', bin: 'foobin', _dir: 'some_dir'}, {_name: 'bar', _dir: 'bar'}];
90+
addDynamicDependencies(pkgs, {'foo': 'b*'});
91+
expect(pkgs[0]._dynamicDependencies).toEqual(['//bar:bar']);
92+
expect(printPackageBin(pkgs[0])).toContain('data = ["//some_dir:foo", "//bar:bar"]');
93+
});
94+
it('should automatically include plugins in nodejs_binary data', () => {
95+
const pkgs =
96+
[{_name: 'foo', bin: 'foobin', _dir: 'some_dir'}, {_name: 'foo-plugin-bar', _dir: 'bar'}];
97+
addDynamicDependencies(pkgs, {});
98+
expect(pkgs[0]._dynamicDependencies).toEqual(['//bar:foo-plugin-bar']);
99+
expect(printPackageBin(pkgs[0]))
100+
.toContain('data = ["//some_dir:foo", "//bar:foo-plugin-bar"]');
101+
});
102+
});
80103
});

internal/npm_install/test/golden/jasmine/bin/BUILD.bazel.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ nodejs_binary(
55
name = "jasmine",
66
entry_point = "//:node_modules/jasmine/bin/jasmine.js",
77
install_source_map_support = False,
8-
data = ["//jasmine:jasmine"],
8+
data = ["//jasmine:jasmine", "//zone.js:zone.js"],
99
)

packages/typescript/docs/install.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ managing npm dependencies with Bazel.
104104

105105
## Customizing the TypeScript compiler binary
106106

107-
An example is needing to increase the NodeJS heap size used for compilations.
107+
An example use case is needing to increase the NodeJS heap size used for compilations.
108108

109109
Similar to above, you declare your own binary for running tsc_wrapped, e.g.:
110110

@@ -127,7 +127,9 @@ nodejs_binary(
127127

128128
then refer to that target in the `compiler` attribute of your `ts_library` rule.
129129

130-
Another use case is to use [tsickle], which is an optional dependency of `tsc_wrapped`. In case it needs `"@npm//tsickle"` added to the `data` attribute above.
130+
Note that `nodejs_binary` targets generated by `npm_install`/`yarn_install` can include data dependencies
131+
on packages which aren't declared as dependencies. For example, if you use [tsickle] to generate Closure Compiler-compatible JS, then it needs to be a `data` dependency of `tsc_wrapped` so that it can be loaded at runtime.
132+
See the `dynamic_deps` attribute of `npm_install`/`yarn_install` to include more such runtime dependencies in the generated `nodejs_binary` targets, rather than needing to define a custom one.
131133
132134
[tsickle]: https://github.com/angular/tsickle
133135

0 commit comments

Comments
 (0)