"""Resolve editable npm source from a consumer's Git package lock, without npm hooks.""" from pathlib import Path from urllib.parse import unquote, urlsplit, urlunsplit from .common import WorkspaceError, read_json, safe_path from .manifest import SHA, validate_repo def repository_identity(value): value = value.removeprefix("git+") parsed = urlsplit(value) if parsed.scheme == "file": if parsed.netloc not in ("", "localhost"): raise WorkspaceError("npm Git origin must be local or approved") return str(Path(unquote(parsed.path)).resolve()) validate_repo(value) return value class Npm: def __init__(self, workspace): self.workspace = workspace def resolve(self, component): package = component.get("package_name") if not package: return None selected = [] consumers = sorted(self.workspace.manifest.select(), key=lambda entry: ("frontend" not in entry["id"].lower(), entry["id"])) for consumer in consumers: if consumer["role"] != "application": continue root = safe_path(self.workspace.root, consumer["path"]) path = safe_path(root, "package-lock.json") if not path.exists(): continue lock = read_json(path) if not isinstance(lock, dict) or lock.get("lockfileVersion") not in (2, 3) or not isinstance(lock.get("packages"), dict): raise WorkspaceError(f"unsupported npm lock for {consumer['id']}") for location, entry in lock["packages"].items(): if location != "node_modules/" + package and not location.endswith("/node_modules/" + package): continue if not isinstance(entry, dict) or entry.get("link"): raise WorkspaceError(f"local npm link cannot establish version baseline: {package}") resolved = entry.get("resolved", "") if not isinstance(resolved, str) or not resolved.startswith("git+"): raise WorkspaceError(f"npm source needs a fixed approved Git origin: {package}") parsed = urlsplit(resolved) if not SHA.fullmatch(parsed.fragment): raise WorkspaceError(f"npm Git source must pin a full commit: {package}") origin = urlunsplit((parsed.scheme, parsed.netloc, parsed.path, parsed.query, "")) if repository_identity(origin) != repository_identity(component["repo"]): raise WorkspaceError(f"npm Git origin mismatch: {package}") selected.append((parsed.fragment, {"kind": "npm-lock", "package": package, "version": entry.get("version"), "resolved": resolved, "consumer": consumer["id"], "lock_path": str(path.relative_to(self.workspace.root))})) if not selected: return None if len({entry[0] for entry in selected}) != 1: raise WorkspaceError(f"consumers select conflicting npm Git commits for {package}; owner must choose compatible versions") commit, source = selected[0] return commit, dict(source, selections=[entry[1] for entry in selected])