Why: Preserve a single Dev entry and independent repository ownership while allowing active feature branches to migrate without losing source or Git state. Evidence: SELF 29 real Git/Go workspace tests and ordinary component evidence/lock tests passed; detailed RED/GREEN runs are recorded by make evidence in the task card. Impact: Add fixed cloning, selected-version baselines, development overlays, npm source resolution, and independently preserved legacy snapshots. No other active worktree is modified. Refs: Versioned dependencies U2/U5; R15; feature/dependency-workspace
63 lines
3.2 KiB
Python
63 lines
3.2 KiB
Python
"""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])
|