blob: 9ea51c325b5b1f30d323e31f0022db55087e5d45 [file]
"Utility functions for npm rules"
load("@aspect_bazel_lib//lib:paths.bzl", "relative_file")
load(":yaml.bzl", _parse_yaml = "parse")
def _bazel_name(name, version = None):
"Make a bazel friendly name from a package name and (optionally) a version that can be used in repository and target names"
escaped_name = name.replace("@", "at_").replace("/", "_").replace("#", "_")
if not version:
return escaped_name
version_segments = version.split("_")
escaped_version = version_segments[0].replace("@", "at_").replace("/", "_").replace("#", "_")
peer_version = "_".join(version_segments[1:])
if peer_version.startswith("@"):
peer_version = "at_" + peer_version[1:]
if peer_version:
escaped_version = "%s__%s" % (escaped_version, peer_version.replace("/", "_").replace("@", "_").replace("#", "_").replace("+", "_"))
return "%s__%s" % (escaped_name, escaped_version)
def _strip_peer_dep_version(version):
"Remove peer dependency syntax from version string"
# 21.1.0_rollup@2.70.2 becomes 21.1.0
index = version.find("_")
if index != -1:
return version[:index]
return version
def _pnpm_name(name, version):
"Make a name/version pnpm-style name for a package name and version"
return "%s/%s" % (name, version)
def _parse_pnpm_name(pnpmName):
# Parse a name/version or @scope/name/version string and return
# a (name, version) tuple
segments = pnpmName.rsplit("/", 1)
if len(segments) != 2:
fail("unexpected pnpm versioned name " + pnpmName)
return (segments[0], segments[1])
def _parse_pnpm_lock(lockfile_content):
"""Parse a pnpm lock file.
Args:
lockfile_content: yaml lockfile content
Returns:
dict containing parsed lockfile
"""
return _parse_yaml(lockfile_content)
def _assert_lockfile_version(version, testonly = False):
if type(version) != type(1.0):
fail("version should be passed as a float")
# Restrict the supported lock file versions to what this code has been tested with:
# 5.3 - pnpm v6.x.x
# 5.4 - pnpm v7.0.0 bumped the lockfile version to 5.4
min_lock_version = 5.3
max_lock_version = 5.4
msg = None
if version < min_lock_version:
msg = "npm_translate_lock requires lock_version at least {min}, but found {actual}. Please upgrade to pnpm v6 or greater.".format(
min = min_lock_version,
actual = version,
)
if version > max_lock_version:
msg = "npm_translate_lock currently supports a maximum lock_version of {max}, but found {actual}. Please file an issue on rules_js".format(
max = max_lock_version,
actual = version,
)
if msg and not testonly:
fail(msg)
return msg
def _friendly_name(name, version):
"Make a name@version developer-friendly name for a package name and version"
return "%s@%s" % (name, version)
def _virtual_store_name(name, version):
"Make a virtual store name for a given package and version"
if version.startswith("@"):
# Special case where the package name should _not_ be included in the virtual store name.
# See https://github.com/aspect-build/rules_js/issues/423 for more context.
return version.replace("/", "+")
else:
escaped_name = name.replace("/", "+")
escaped_version = version.replace("/", "+")
return "%s@%s" % (escaped_name, escaped_version)
def _make_symlink(ctx, symlink_path, target_file):
files = []
if ctx.attr.use_declare_symlink:
symlink = ctx.actions.declare_symlink(symlink_path)
ctx.actions.symlink(
output = symlink,
target_path = relative_file(target_file.path, symlink.path),
)
files.append(target_file)
else:
if _is_at_least_bazel_6() and target_file.is_directory:
# BREAKING CHANGE in Bazel 6 requires you to use declare_directory if your target_file
# in ctx.actions.symlink is a directory artifact
symlink = ctx.actions.declare_directory(symlink_path)
else:
symlink = ctx.actions.declare_file(symlink_path)
ctx.actions.symlink(
output = symlink,
target_file = target_file,
)
files.append(symlink)
return files
def _is_at_least_bazel_6():
# Hacky way to check if the we're using at least Bazel 6. Would be nice if there was a ctx.bazel_version instead.
# native.bazel_version only works in repository rules.
return "apple_binary" not in dir(native)
def _parse_package_name(package):
# Parse a @scope/name string and return a (scope, name) tuple
segments = package.split("/", 1)
if len(segments) == 2 and segments[0].startswith("@"):
return (segments[0], segments[1])
return ("", segments[0])
def _npm_registry_download_url(package, version, registries, default_registry):
"Make a registry download URL for a given package and version"
(package_scope, package_name_no_scope) = _parse_package_name(package)
registry = "https://{}".format(registries[package_scope]) if package_scope in registries else default_registry
return "{0}/{1}/-/{2}-{3}.tgz".format(
registry.removesuffix("/"),
package,
package_name_no_scope,
_strip_peer_dep_version(version),
)
utils = struct(
bazel_name = _bazel_name,
pnpm_name = _pnpm_name,
assert_lockfile_version = _assert_lockfile_version,
parse_pnpm_name = _parse_pnpm_name,
parse_pnpm_lock = _parse_pnpm_lock,
friendly_name = _friendly_name,
virtual_store_name = _virtual_store_name,
strip_peer_dep_version = _strip_peer_dep_version,
make_symlink = _make_symlink,
# Symlinked node_modules structure virtual store path under node_modules
virtual_store_root = ".aspect_rules_js",
# Suffix for npm_import links repository
links_repo_suffix = "__links",
# Output group name for the package directory of a linked package
package_directory_output_group = "package_directory",
npm_registry_download_url = _npm_registry_download_url,
parse_package_name = _parse_package_name,
)