Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pnpm): allow latest as version to get the latest version #1933

Merged
merged 2 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 12 additions & 38 deletions npm/extensions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ load("//npm/private:npm_translate_lock.bzl", "npm_translate_lock_lib", "npm_tran
load("//npm/private:npm_translate_lock_helpers.bzl", npm_translate_lock_helpers = "helpers")
load("//npm/private:npm_translate_lock_macro_helpers.bzl", macro_helpers = "helpers")
load("//npm/private:npm_translate_lock_state.bzl", "npm_translate_lock_state")
load("//npm/private:pnpm_extension.bzl", "DEFAULT_PNPM_REPO_NAME", "resolve_pnpm_repositories")
load("//npm/private:npmrc.bzl", "parse_npmrc")
load("//npm/private:tar.bzl", "detect_system_tar")
load("//npm/private:transitive_closure.bzl", "translate_to_transitive_closure")

DEFAULT_PNPM_VERSION = _DEFAULT_PNPM_VERSION
LATEST_PNPM_VERSION = _LATEST_PNPM_VERSION

_DEFAULT_PNPM_REPO_NAME = "pnpm"

def _npm_extension_impl(module_ctx):
if not bazel_lib_utils.is_bazel_6_or_greater():
# ctx.actions.declare_symlink was added in Bazel 6
Expand Down Expand Up @@ -240,45 +239,17 @@ npm = module_extension(
},
)

# copied from https://github.com/bazelbuild/bazel-skylib/blob/b459822483e05da514b539578f81eeb8a705d600/lib/versions.bzl#L60
# to avoid taking a dependency on skylib here
def _parse_version(version):
return tuple([int(n) for n in version.split(".")])

def _pnpm_extension_impl(module_ctx):
registrations = {}
integrity = {}
for mod in module_ctx.modules:
for attr in mod.tags.pnpm:
if attr.name != _DEFAULT_PNPM_REPO_NAME and not mod.is_root:
fail("""\
Only the root module may override the default name for the pnpm repository.
This prevents conflicting registrations in the global namespace of external repos.
""")
if attr.name not in registrations.keys():
registrations[attr.name] = []
registrations[attr.name].append(attr.pnpm_version)
if attr.pnpm_version_integrity:
integrity[attr.pnpm_version] = attr.pnpm_version_integrity
for name, versions in registrations.items():
# Use "Minimal Version Selection" like bzlmod does for resolving module conflicts
# Note, the 'sorted(list)' function in starlark doesn't allow us to provide a custom comparator
if len(versions) > 1:
selected = versions[0]
selected_tuple = _parse_version(selected)
for idx in range(1, len(versions)):
if _parse_version(versions[idx]) > selected_tuple:
selected = versions[idx]
selected_tuple = _parse_version(selected)
resolved = resolve_pnpm_repositories(module_ctx.modules)

# buildifier: disable=print
print("NOTE: repo '{}' has multiple versions {}; selected {}".format(name, versions, selected))
else:
selected = versions[0]
for note in resolved.notes:
# buildifier: disable=print
print(note)

for name, pnpm_version in resolved.repositories.items():
pnpm_repository(
name = name,
pnpm_version = (selected, integrity[selected]) if selected in integrity.keys() else selected,
pnpm_version = pnpm_version,
)

pnpm = module_extension(
Expand All @@ -289,9 +260,12 @@ pnpm = module_extension(
"name": attr.string(
doc = """Name of the generated repository, allowing more than one pnpm version to be registered.
Overriding the default is only permitted in the root module.""",
default = _DEFAULT_PNPM_REPO_NAME,
default = DEFAULT_PNPM_REPO_NAME,
),
"pnpm_version": attr.string(
doc = "pnpm version to use. The string `latest` will be resolved to LATEST_PNPM_VERSION.",
default = DEFAULT_PNPM_VERSION,
),
"pnpm_version": attr.string(default = DEFAULT_PNPM_VERSION),
"pnpm_version_integrity": attr.string(),
},
),
Expand Down
66 changes: 66 additions & 0 deletions npm/private/pnpm_extension.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""pnpm extension logic (the extension itself is in npm/extensions.bzl)."""

load(":pnpm_repository.bzl", "LATEST_PNPM_VERSION")

DEFAULT_PNPM_REPO_NAME = "pnpm"

# copied from https://github.com/bazelbuild/bazel-skylib/blob/b459822483e05da514b539578f81eeb8a705d600/lib/versions.bzl#L60
# to avoid taking a dependency on skylib here
def _parse_version(version):
return tuple([int(n) for n in version.split(".")])

def resolve_pnpm_repositories(modules):
"""Resolves pnpm tags in all `modules`

Args:
modules: module_ctx.modules

Returns:
A struct with the following fields:
- `repositories`: dict (name -> pnpm_version) to invoke `pnpm_repository` with.
- `notes`: list of notes to print to the user.
"""

registrations = {}
integrity = {}

result = struct(
notes = [],
repositories = {},
)

for mod in modules:
for attr in mod.tags.pnpm:
if attr.name != DEFAULT_PNPM_REPO_NAME and not mod.is_root:
fail("""\
Only the root module may override the default name for the pnpm repository.
This prevents conflicting registrations in the global namespace of external repos.
""")
if attr.name not in registrations.keys():
registrations[attr.name] = []

v = attr.pnpm_version
if v == "latest":
v = LATEST_PNPM_VERSION

registrations[attr.name].append(v)
if attr.pnpm_version_integrity:
integrity[attr.pnpm_version] = attr.pnpm_version_integrity
for name, versions in registrations.items():
# Use "Minimal Version Selection" like bzlmod does for resolving module conflicts
# Note, the 'sorted(list)' function in starlark doesn't allow us to provide a custom comparator
if len(versions) > 1:
selected = versions[0]
selected_tuple = _parse_version(selected)
for idx in range(1, len(versions)):
if _parse_version(versions[idx]) > selected_tuple:
selected = versions[idx]
selected_tuple = _parse_version(selected)

result.notes.append("NOTE: repo '{}' has multiple versions {}; selected {}".format(name, versions, selected))
else:
selected = versions[0]

result.repositories[name] = (selected, integrity[selected]) if selected in integrity.keys() else selected

return result
3 changes: 3 additions & 0 deletions npm/private/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ load("@npm//:defs.bzl", "npm_link_all_packages")
load(":generated_pkg_json_test.bzl", "generated_pkg_json_test")
load(":npm_auth_test.bzl", "npm_auth_test_suite")
load(":npmrc_test.bzl", "npmrc_tests")
load(":pnpm_test.bzl", "pnpm_tests")
load(":parse_pnpm_lock_tests.bzl", "parse_pnpm_lock_tests")
load(":transitive_closure_tests.bzl", "transitive_closure_tests")
load(":translate_lock_helpers_tests.bzl", "translate_lock_helpers_tests")
Expand All @@ -18,6 +19,8 @@ utils_tests(name = "test_utils")

npmrc_tests(name = "test_npmrc")

pnpm_tests(name = "test_pnpm")

transitive_closure_tests(name = "test_transitive_closure")

translate_lock_helpers_tests(name = "test_translate_lock")
Expand Down
148 changes: 148 additions & 0 deletions npm/private/test/pnpm_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Test for pnpm extension version resolution."""

load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
load("//npm/private:pnpm_extension.bzl", "DEFAULT_PNPM_REPO_NAME", "resolve_pnpm_repositories")
load("//npm/private:pnpm_repository.bzl", "LATEST_PNPM_VERSION")

def _fake_pnpm_tag(version, name = DEFAULT_PNPM_REPO_NAME, integrity = None):
return struct(
name = name,
pnpm_version = version,
pnpm_version_integrity = integrity,
)

def _fake_mod(is_root, *pnpm_tags):
return struct(
is_root = is_root,
tags = struct(pnpm = pnpm_tags),
)

def _resolve_test(ctx, repositories = [], notes = [], modules = []):
env = unittest.begin(ctx)

expected = struct(
repositories = repositories,
notes = notes,
)

result = resolve_pnpm_repositories(modules)

asserts.equals(env, expected, result)
return unittest.end(env)

def _basic(ctx):
# Essentially what happens without any user configuration.
# - Root module doesn't have any pnpm tag.
# - rules_js sets a default.
return _resolve_test(
ctx,
repositories = {"pnpm": ("8.6.7", "8.6.7-integrity")},
modules = [
_fake_mod(True),
_fake_mod(
False,
_fake_pnpm_tag(version = "8.6.7", integrity = "8.6.7-integrity"),
),
],
)

def _override(ctx):
# What happens when the root overrides the pnpm version.
return _resolve_test(
ctx,
repositories = {"pnpm": "9.1.0"},
notes = [
"""NOTE: repo 'pnpm' has multiple versions ["9.1.0", "8.6.7"]; selected 9.1.0""",
],
modules = [
_fake_mod(
True,
_fake_pnpm_tag(version = "9.1.0"),
),
_fake_mod(
False,
_fake_pnpm_tag(version = "8.6.7", integrity = "8.6.7-integrity"),
),
],
)

def _latest(ctx):
# Test the "latest" magic version,
#
# The test case is not entirely realistic: In reality, we'd have at least two tags:
# - The one of the root module (present in the test)
# - The one from rules_js (omitted in the test).
#
# We do this, to avoid `notes` that are dependent on `LATEST_PNPM_VERSION`.
# Otherwise we'd have to either:
# - Use regexes to check notes.
# - Accept a brittle test.
return _resolve_test(
ctx,
repositories = {"pnpm": LATEST_PNPM_VERSION},
modules = [
_fake_mod(True, _fake_pnpm_tag(version = "latest")),
],
)

def _custom_name(ctx):
return _resolve_test(
ctx,
repositories = {
"my-pnpm": "9.1.0",
"pnpm": ("8.6.7", "8.6.7-integrity"),
},
modules = [
_fake_mod(
True,
_fake_pnpm_tag(name = "my-pnpm", version = "9.1.0"),
),
_fake_mod(
False,
_fake_pnpm_tag(version = "8.6.7", integrity = "8.6.7-integrity"),
),
],
)

def _integrity_conflict(ctx):
# What happens if two modules define the same version with conflicting integrity parameters.
# @gzm0, 2024-10-04: The behavior here is probably not intended and merely an implementation artifact.
# I've added a test anyways to capture the existing behavior.

return _resolve_test(
ctx,
repositories = {
"pnpm": ("8.6.7", "dep-integrity"),
},
notes = [
"""NOTE: repo 'pnpm' has multiple versions ["8.6.7", "8.6.7"]; selected 8.6.7""",
],
# Modules are *BFS* from root:
# https://bazel.build/rules/lib/builtins/module_ctx#modules
modules = [
_fake_mod(
True,
_fake_pnpm_tag(version = "8.6.7", integrity = "root-integrity"),
),
_fake_mod(
False,
_fake_pnpm_tag(version = "8.6.7", integrity = "dep-integrity"),
),
],
)

basic_test = unittest.make(_basic)
override_test = unittest.make(_override)
latest_test = unittest.make(_latest)
custom_name_test = unittest.make(_custom_name)
integrity_conflict_test = unittest.make(_integrity_conflict)

def pnpm_tests(name):
unittest.suite(
name,
basic_test,
override_test,
latest_test,
custom_name_test,
integrity_conflict_test,
)
Loading