Skip to content

Commit 678008b

Browse files
authored
Merge pull request #6 from RRosio/node_project
Initial node project work
2 parents 272b68c + 825791e commit 678008b

File tree

5 files changed

+171
-3
lines changed

5 files changed

+171
-3
lines changed

src/projspec/content/environment.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class Stack(Enum):
1010

1111
PIP = auto()
1212
CONDA = auto()
13+
NPM = auto()
1314

1415

1516
class Precision(Enum):

src/projspec/content/package.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,17 @@ class PythonPackage(BaseContent):
88
"""Importable python directory, i.e., containing an __init__.py file."""
99

1010
package_name: str
11+
12+
13+
@dataclass
14+
class RustModule(BaseContent):
15+
"""Usually a directory with a Cargo.toml file"""
16+
17+
name: str
18+
19+
20+
@dataclass
21+
class NodePackage(BaseContent):
22+
"""Buildable nodeJS source"""
23+
24+
name: str

src/projspec/proj/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from projspec.proj.documentation import RTD, MDBook
55
from projspec.proj.git import GitRepo
66
from projspec.proj.ide import JetbrainsIDE, NvidiaAIWorkbench, VSCode
7+
from projspec.proj.node import Node
78
from projspec.proj.pixi import Pixi
89
from projspec.proj.poetry import Poetry
910
from projspec.proj.pyscript import PyScript
@@ -21,6 +22,7 @@
2122
"JetbrainsIDE",
2223
"MDBook",
2324
"NvidiaAIWorkbench",
25+
"Node",
2426
"Poetry",
2527
"RattlerRecipe",
2628
"Pixi",

src/projspec/proj/ide.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class JetbrainsIDE(ProjectSpec):
1919
def match(self) -> bool:
2020
return self.proj.fs.exists(f"{self.proj.url}/.idea")
2121

22+
def parse(self) -> None:
23+
...
24+
2225

2326
class VSCode(ProjectSpec):
2427
spec_doc = (

src/projspec/proj/node.py

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,164 @@
1+
import re
2+
13
from projspec.proj.base import ProjectSpec
4+
from projspec.content.package import NodePackage
5+
from projspec.artifact.process import Process
6+
from projspec.content.executable import Command
7+
from projspec.utils import AttrDict
28

39

410
class Node(ProjectSpec):
5-
"""Node.js project"""
11+
"""Node.js project, managed by NPM
12+
13+
This is a project that contains a package.json file.
14+
"""
615

716
spec_doc = "https://docs.npmjs.com/cli/v11/configuring-npm/package-json"
817

918
def match(self):
1019
return "package.json" in self.proj.basenames
1120

12-
def parse(self):
21+
def parse0(self):
22+
from projspec.content.environment import Environment, Stack, Precision
23+
from projspec.artifact.python_env import LockFile
24+
1325
import json
1426

1527
with self.proj.fs.open(f"{self.proj.url}/package.json", "rt") as f:
16-
json.load(f)
28+
pkg_json = json.load(f)
29+
self.meta = pkg_json
30+
31+
# Metadata
32+
name = pkg_json.get("name")
33+
version = pkg_json.get("version")
34+
description = pkg_json.get("description")
35+
# Dependencies
36+
dependencies = pkg_json.get("dependencies")
37+
dev_dependencies = pkg_json.get("devDependencies")
38+
# Entry points for runtime execution: CLI
39+
scripts = pkg_json.get("scripts", {})
40+
bin = pkg_json.get("bin")
41+
# Entry points for importable code: library
42+
main = pkg_json.get("main")
43+
module = pkg_json.get("module")
44+
# TBD: exports?
45+
# Package manager
46+
package_manager = pkg_json.get("packageManager", "npm@latest")
47+
if isinstance(package_manager, str):
48+
package_manager_name = package_manager.split("@")[0]
49+
else:
50+
package_manager_name = package_manager.get("name", "npm")
51+
52+
# Commands
53+
bin_entry = {}
54+
if bin and isinstance(bin, str):
55+
bin_entry = {name: bin}
56+
elif bin and isinstance(bin, dict):
57+
bin_entry = bin
58+
59+
# Contents
60+
conts = AttrDict()
61+
cmd = AttrDict()
62+
for name, path in bin_entry.items():
63+
cmd[name] = Command(
64+
proj=self.proj, cmd=["node", f"{self.proj.url}/{path}"], artifacts=set()
65+
)
66+
67+
# Artifacts
68+
arts = AttrDict()
69+
for script_name, script_cmd in scripts.items():
70+
if script_name == "build":
71+
arts["build"] = Process(
72+
proj=self.proj, cmd=[package_manager_name, "run", script_name]
73+
)
74+
else:
75+
cmd[script_name] = Command(
76+
proj=self.proj,
77+
cmd=[package_manager_name, "run", script_name],
78+
artifacts=set(),
79+
)
80+
81+
if "package-lock.json" in self.proj.basenames:
82+
arts["lock_file"] = LockFile(
83+
proj=self.proj,
84+
artifacts={},
85+
cmd=["npm", "install"],
86+
fn=self.proj.basenames["package-lock.json"],
87+
)
88+
# TODO: load lockfile and make environment
89+
conts.setdefault("environment", {})["node"] = Environment(
90+
proj=self.proj,
91+
artifacts=set(),
92+
stack=Stack.NPM,
93+
packages=dependencies,
94+
precision=Precision.SPEC,
95+
)
96+
conts.setdefault("environment", {})["node_dev"] = Environment(
97+
proj=self.proj,
98+
artifacts=set(),
99+
stack=Stack.NPM,
100+
packages=dev_dependencies, # + dependencies?
101+
precision=Precision.SPEC,
102+
)
103+
104+
conts["node_package"] = node_package = (
105+
NodePackage(name=name, proj=self.proj, artifacts=set()),
106+
)
107+
conts["command"] = (cmd,)
108+
self._artifacts = arts
109+
self._contents = conts
110+
111+
112+
class Yarn(Node):
113+
"""A node project that uses `yarn` for building"""
114+
115+
spec_doc = "https://yarnpkg.com/configuration/yarnrc"
116+
117+
def match(self):
118+
return ".yarnrc.yml" in self.proj.basenames
119+
120+
def parse(self):
121+
from projspec.content.environment import Environment, Stack, Precision
122+
from projspec.artifact.python_env import LockFile
123+
124+
super().parse0()
125+
126+
with self.proj.fs.open(f"{self.proj.url}/yarn.lock", "rt") as f:
127+
txt = f.read()
128+
hits = re.findall(r'resolution: "(.*?)"', txt, flags=re.MULTILINE)
129+
130+
self.artifacts["lock_file"] = LockFile(
131+
proj=self.proj,
132+
cmd=["yarn", "install"],
133+
fn=self.proj.basenames["yarn.lock"],
134+
)
135+
self.contents.setdefault("environment", {})["yarn_lock"] = Environment(
136+
proj=self.proj,
137+
artifacts=set(),
138+
stack=Stack.NPM,
139+
packages=hits,
140+
precision=Precision.LOCK,
141+
)
142+
143+
144+
class JLabExtension(Yarn):
145+
"""A node variant specific to JLab
146+
147+
https://jupyterlab.readthedocs.io/en/latest/developer/contributing.html
148+
#installing-node-js-and-jlpm
149+
"""
150+
151+
# TODO: this should match even if yarn.lock is missing, so long as package.json
152+
# does exist, and uses jlpm to build
153+
154+
def parse(self):
155+
from projspec.artifact.python_env import LockFile
156+
157+
super().parse()
158+
if not self.meta["scripts"]["build"].startswith("jlpm"):
159+
raise ValueError
160+
self.artifacts["lock_file"] = LockFile(
161+
proj=self.proj,
162+
cmd=["jlpm", "install"],
163+
fn=self.proj.basenames["yarn.lock"],
164+
)

0 commit comments

Comments
 (0)