Skip to content

Commit

Permalink
feat: add alias targets for unversioned reference to direct deps
Browse files Browse the repository at this point in the history
Also add starlark unit testing for translate_package_lock as it's going to get more complex.
  • Loading branch information
alexeagle committed Nov 8, 2021
1 parent 4fafe85 commit b34d373
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 15 deletions.
4 changes: 4 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ load("//js:repositories.bzl", "rules_js_dependencies")

rules_js_dependencies()

load("@bazel_skylib//lib:unittest.bzl", "register_unittest_toolchains")

register_unittest_toolchains()

############################################
# Gazelle, for generating bzl_library targets
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
Expand Down
63 changes: 50 additions & 13 deletions js/private/translate_package_lock.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,17 @@ def _repo_name(package_name, version):
"Make an external repository name from a package name and a version"
return "npm_%s-%s" % (_escape(package_name), version)

def _import_dependencies(repository_ctx, bzl_out, packages):
for (name, dep) in packages["dependencies"].items():
def _import_dependencies(lockfile, bzl_out = None):
# To allow recursion, accept an optional accumulator
# If it's not present, we need to start with the header of the file
bzl_out = bzl_out or ["""# @generated by package_lock.bzl from {package_lock}
load("@aspect_rules_js//js:npm_import.bzl", "npm_import")
def npm_repositories():
"Define external repositories to fetch each tarball individually from npm on-demand."
"""]
for (name, dep) in lockfile["dependencies"].items():
if "resolved" not in dep.keys():
continue
deps = []
Expand All @@ -72,29 +81,57 @@ def _import_dependencies(repository_ctx, bzl_out, packages):
integrity = dep["integrity"],
deps = deps,
)])
return bzl_out

def _define_aliases(repository_ctx, lockfile, do_writes = True):
# Return value if do_writes = False
aliases = {}

# The lockfile format refers to the context as the package with empty name.
# This gives us a way to know which deps the user declared in their package.json
# (the direct dependencies).
direct = lockfile["packages"][""]
direct_names = []
direct_names.extend(direct.get("devDependencies", {}).keys())
direct_names.extend(direct.get("dependencies", {}).keys())

for (direct_name, direct_dep) in lockfile["dependencies"].items():
if not direct_name in direct_names:
continue
dep_build_content = """# @generated by package_lock.bzl
alias(name = "{package}", actual = "{actual}", visibility = ["//visibility:public"])
""".format(
package = direct_name.split("/")[-1],
actual = "@" + _repo_name(direct_name, direct_dep["version"]),
)
if do_writes:
repository_ctx.file(direct_name + "/BUILD.bazel", dep_build_content)
else:
# In a starlark unit test, there is no global mutable data structure
# that allows us to capture the writes using a mock repository_ctx
# so we need a different return type that lets us assert on what's written.
# We could do this for production use as well, but then we'd waste memory
# in large package-lock.json files holding references to values
# we no longer need.
aliases[direct_name + "/BUILD.bazel"] = dep_build_content
return None if do_writes else aliases

def _translate_package_lock(repository_ctx):
bzl_content = ["""# @generated by package_lock.bzl from {package_lock}
load("@aspect_rules_js//js:npm_import.bzl", "npm_import")
def npm_repositories():
"Define external repositories to fetch each tarball individually from npm on-demand."
"""]

lock_content = json.decode(repository_ctx.read(repository_ctx.attr.package_lock))
lock_version = lock_content["lockfileVersion"]
if lock_version < 2:
fail("translate_package_lock only works with npm 7 lockfiles (lockfileVersion >= 2), found %s" % lock_version)

_import_dependencies(repository_ctx, bzl_content, lock_content)

repository_ctx.file("repositories.bzl", "\n".join(bzl_content))
repository_ctx.file("repositories.bzl", "\n".join(_import_dependencies(lock_content)))
repository_ctx.file("BUILD.bazel", "")
_define_aliases(repository_ctx, lock_content)

translate_package_lock = struct(
doc = _DOC,
implementation = _translate_package_lock,
attrs = _ATTRS,
repository_name = _repo_name,
testonly_import_dependencies = _import_dependencies,
testonly_define_aliases = _define_aliases,
)
3 changes: 3 additions & 0 deletions js/test/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
load(":translate_package_lock_tests.bzl", "translate_package_lock_tests")

translate_package_lock_tests(name = "test_translate_package_lock")
108 changes: 108 additions & 0 deletions js/test/translate_package_lock_tests.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Unit tests for starlark helpers
See https://docs.bazel.build/versions/main/skylark/testing.html#for-testing-starlark-utilities
"""

load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
load("//js/private:translate_package_lock.bzl", "translate_package_lock")

_lockfile = json.decode("""
{
"name": "test",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@gregmagolan/test-b": "^0.0.2"
}
}
},
"dependencies": {
"@gregmagolan/test-a": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@gregmagolan/test-a/-/test-a-0.0.1.tgz",
"integrity": "sha512-a==",
"dev": true
},
"@gregmagolan/test-b": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@gregmagolan/test-b/-/test-b-0.0.2.tgz",
"integrity": "sha512-b==",
"dev": true,
"requires": {
"@gregmagolan/test-a": "0.0.1"
}
}
}
}
""")

def _naming_test_impl(ctx):
env = unittest.begin(ctx)
asserts.equals(env, "npm_acorn-10.2.3", translate_package_lock.repository_name("acorn", "10.2.3"))
asserts.equals(env, "npm__types_node-11.2.3", translate_package_lock.repository_name("@types/node", "11.2.3"))
return unittest.end(env)

def _repository_bzl_test_impl(ctx):
env = unittest.begin(ctx)

expected_header = """\
# @generated by package_lock.bzl from {package_lock}
load("@aspect_rules_js//js:npm_import.bzl", "npm_import")
def npm_repositories():
"Define external repositories to fetch each tarball individually from npm on-demand."
"""
expected_a = """\
# @generated from [package-lock.json snippet here]
npm_import(
name = "npm__gregmagolan_test-a-0.0.1",
integrity = "sha512-a==",
package = "@gregmagolan/test-a",
version = "0.0.1",
deps = [],
)
"""
expected_b = """\
# @generated from [package-lock.json snippet here]
npm_import(
name = "npm__gregmagolan_test-b-0.0.2",
integrity = "sha512-b==",
package = "@gregmagolan/test-b",
version = "0.0.2",
deps = ["@npm__gregmagolan_test-a-0.0.1"],
)
"""
actual = translate_package_lock.testonly_import_dependencies(_lockfile)
asserts.equals(env, 3, len(actual))
asserts.equals(env, expected_header, actual[0])
asserts.equals(env, expected_a, actual[1])
asserts.equals(env, expected_b, actual[2])
return unittest.end(env)

def _aliases_test_impl(ctx):
env = unittest.begin(ctx)
mock_repository_ctx = struct()
actual = translate_package_lock.testonly_define_aliases(mock_repository_ctx, _lockfile, False)
asserts.equals(env, 1, len(actual.items()))

# Note: @gregmagolan/test-a should not appear here since it's only a transitive dependency
expected = {
"@gregmagolan/test-b/BUILD.bazel": """# @generated by package_lock.bzl
alias(name = "test-b", actual = "@npm__gregmagolan_test-b-0.0.2", visibility = ["//visibility:public"])
""",
}
asserts.equals(env, expected, actual)
return unittest.end(env)

# The unittest library requires that we export the test cases as named test rules,
# but their names are arbitrary and don't appear anywhere.
t0_test = unittest.make(_naming_test_impl)
t1_test = unittest.make(_repository_bzl_test_impl)
t2_test = unittest.make(_aliases_test_impl)

def translate_package_lock_tests(name):
unittest.suite(name, t0_test, t1_test, t2_test)
4 changes: 2 additions & 2 deletions test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ diff_test(

nodejs_test(
name = "test_test",
data = ["@npm__types_node-15.14.9"],
data = ["@npm_deps//@types/node"],
entry_point = "test.js",
)

Expand Down Expand Up @@ -206,7 +206,7 @@ write_file(

nodejs_binary(
name = "bin7",
data = ["@npm__gregmagolan_test-b-0.0.2"],
data = ["@npm_deps//@gregmagolan/test-b"],
entry_point = "case7.js",
)

Expand Down

0 comments on commit b34d373

Please sign in to comment.