Files
dotfiles/system-config/modules/npm_packages.py
T

104 lines
3.2 KiB
Python

"""Declarative global npm command packages for Decman profiles."""
from __future__ import annotations
import json
import shutil
import decman
import decman.core.error as errors
import decman.core.module as module
import decman.core.output as output
import decman.core.store as store_module
from decman import Plugin
from decman.plugins import run_methods_with_attribute
def packages(fn):
"""Annotate a method that returns globally installed npm package names."""
fn.__npm_packages__ = True
return fn
class Npm(Plugin):
"""Install declared global npm packages without pruning unrelated packages."""
NAME = "npm"
def __init__(self) -> None:
self.packages: set[str] = set()
def process_modules(
self, store: store_module.Store, modules: list[module.Module]
) -> None:
store.ensure("npm_packages_for_module", {})
for mod in modules:
store["npm_packages_for_module"].setdefault(mod.name, set())
declared = set().union(
*run_methods_with_attribute(mod, "__npm_packages__")
)
if store["npm_packages_for_module"][mod.name] != declared:
mod._changed = True
output.print_debug(
f"Module '{mod.name}' changed due to modified global npm packages."
)
self.packages |= declared
store["npm_packages_for_module"][mod.name] = declared
def apply(
self,
store: store_module.Store,
dry_run: bool = False,
params: list[str] | None = None,
) -> bool:
if not self.packages:
return True
if shutil.which("npm") is None:
if dry_run:
output.print_list(
"Installing global npm packages after npm becomes available:",
sorted(self.packages),
)
return True
output.print_error("npm is unavailable after the pacman step.")
return False
try:
installed = self._installed_packages()
to_install = self.packages - installed
output.print_list("Installing global npm packages:", sorted(to_install))
if to_install and not dry_run:
decman.prg(
[
"npm",
"install",
"--global",
"--no-audit",
"--no-fund",
*sorted(to_install),
]
)
except (errors.CommandFailedError, ValueError) as error:
output.print_error(f"Managing global npm packages failed: {error}")
return False
return True
@staticmethod
def _installed_packages() -> set[str]:
raw = decman.prg(
["npm", "list", "--global", "--depth=0", "--json"], pty=False
)
try:
result = json.loads(raw)
except json.JSONDecodeError as error:
raise ValueError("npm returned invalid JSON") from error
dependencies = result.get("dependencies", {})
if not isinstance(dependencies, dict):
raise ValueError("npm dependencies must be an object")
return set(dependencies)