Source code for projspec.proj.node

import re

from projspec.proj.base import ProjectSpec, ParseFailed
from projspec.content.package import NodePackage
from projspec.artifact.process import Process
from projspec.utils import run_subprocess
from projspec.content.executable import Command
from projspec.utils import AttrDict


[docs] class Node(ProjectSpec): """Node.js project, managed by NPM This is a project that contains a package.json file. """ icon = "🟩" spec_doc = "https://docs.npmjs.com/cli/v11/configuring-npm/package-json" def match(self): return "package.json" in self.proj.basenames def parse0(self): from projspec.content.environment import Environment, Stack, Precision from projspec.content.metadata import DescriptiveMetadata from projspec.artifact.python_env import LockFile import json with self.proj.fs.open(f"{self.proj.url}/package.json", "rt") as f: pkg_json = json.load(f) self.meta = pkg_json # Metadata name = pkg_json.get("name") version = pkg_json.get("version") description = pkg_json.get("description") # Dependencies dependencies = pkg_json.get("dependencies") dev_dependencies = pkg_json.get("devDependencies") # Entry points for runtime execution: CLI scripts = pkg_json.get("scripts", {}) bin = pkg_json.get("bin") # Entry points for importable code: library main = pkg_json.get("main") module = pkg_json.get("module") # TBD: exports? # Package manager package_manager = pkg_json.get("packageManager", "npm@latest") if isinstance(package_manager, str): package_manager_name = package_manager.split("@")[0] else: package_manager_name = package_manager.get("name", "npm") # Commands bin_entry = {} if bin and isinstance(bin, str): bin_entry = {name: bin} elif bin and isinstance(bin, dict): bin_entry = bin # Contents conts = AttrDict( { "descriptive_metadata": DescriptiveMetadata( proj=self.proj, meta={ "name": name, "version": version, "description": description, "main": main, "module": module, }, ), }, ) cmd = AttrDict() for name, path in bin_entry.items(): cmd[name] = Command(proj=self.proj, cmd=["node", f"{self.proj.url}/{path}"]) # Artifacts arts = AttrDict() for script_name, script_cmd in scripts.items(): if script_name == "build": arts["build"] = Process( proj=self.proj, cmd=[package_manager_name, "run", script_name], ) else: cmd[script_name] = Command( proj=self.proj, cmd=[package_manager_name, "run", script_name], ) if "package-lock.json" in self.proj.basenames: arts["lock_file"] = LockFile( proj=self.proj, cmd=["npm", "install"], fn=self.proj.basenames["package-lock.json"], ) # TODO: load lockfile and make environment conts.setdefault("environment", {})["node"] = Environment( proj=self.proj, stack=Stack.NPM, packages=dependencies, precision=Precision.SPEC, ) conts.setdefault("environment", {})["node_dev"] = Environment( proj=self.proj, stack=Stack.NPM, packages=dev_dependencies, # + dependencies? precision=Precision.SPEC, ) conts["node_package"] = (NodePackage(name=name, proj=self.proj),) conts["command"] = cmd self._artifacts = arts self._contents = conts def parse(self): self.parse0()
# TODO: a vscode extension has key "contributes" in package.json and engine: vscode: {}, # and then you can build a .vsix with `vsce pack`.
[docs] class Yarn(Node): """A node project that uses `yarn` for building""" icon = "🧶" spec_doc = "https://yarnpkg.com/configuration/yarnrc" def match(self): return ".yarnrc.yml" in self.proj.basenames def parse(self, ignore=False): from projspec.content.environment import Environment, Stack, Precision from projspec.artifact.python_env import LockFile super().parse0() try: with self.proj.fs.open(f"{self.proj.url}/yarn.lock", "rt") as f: txt = f.read() except FileNotFoundError: if ignore: # only used by JLab - we know it complies with yarn even without lock-file. return raise ParseFailed hits = re.findall(r'resolution: "(.*?)"', txt, flags=re.MULTILINE) self.artifacts["lock_file"] = LockFile( proj=self.proj, cmd=["yarn", "install"], fn=self.proj.basenames["yarn.lock"], ) self.contents.setdefault("environment", {})["yarn_lock"] = Environment( proj=self.proj, stack=Stack.NPM, packages=hits, precision=Precision.LOCK, )
[docs] class JLabExtension(Yarn): """A node variant specific to Jupyter-Lab https://jupyterlab.readthedocs.io/en/latest/developer/contributing.html #installing-node-js-and-jlpm These projects have at least a front-end component, but build to python wheels for distribution. A running jupyter-lab process might be considered an output. Some jlab projects will also have python components (i.e., server extensions https://jupyter-server.readthedocs.io/en/latest/developers/extensions.html). """ icon = "🧩" # TODO: we may add a jupyter server extension python project type in the # future, defined by a JSON server config file. def match(self): return "package.json" in self.proj.basenames and bool(self.proj.pyproject) def parse(self): from projspec.artifact.python_env import LockFile super().parse(ignore=True) if not self.meta["scripts"].get("build", "").startswith("jlpm"): raise ParseFailed("JLab extensions build with jlpm") self.artifacts["lock_file"] = LockFile( proj=self.proj, cmd=["jlpm", "install"], fn=f"{self.proj.url}/yarn.lock", ) # create() with https://github.com/jupyterlab/extension-template @staticmethod def _create(path: str, name: str | None = None) -> None: # this is a highly opinionated template cmd = [ "copier", "copy", "--trust", "--defaults", "--data", "author_name=you", "--data", f"kind=frontend-and-server", "--data", "repository=https://github.com/github_username/my-extension", "https://github.com/jupyterlab/extension-template", path, ] run_subprocess(cmd, cwd=path, output=False)