Source code for projspec.proj.jsframeworks

"""JavaScript/Node.js framework specs: Next.js, Nuxt.js, SvelteKit, Vite, Deno, Bun, pnpm."""

import os

from projspec.proj.base import ParseFailed, ProjectSpec
from projspec.proj.node import Node
from projspec.utils import AttrDict, run_subprocess


[docs] class NextJS(Node): """Next.js React framework project.""" icon = "⚛️" spec_doc = "https://nextjs.org/docs/app/api-reference/config/next-config-js" _CONFIG_NAMES = { "next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs", } def match(self) -> bool: return bool(self._CONFIG_NAMES.intersection(self.proj.basenames)) def parse(self) -> None: from projspec.artifact.base import FileArtifact from projspec.artifact.process import Server super().parse0() pkg_mgr = self._pkg_manager() # Development server self._artifacts["dev"] = Server( proj=self.proj, cmd=[pkg_mgr, "run", "dev"], ) # Production build self._artifacts["build"] = FileArtifact( proj=self.proj, cmd=[pkg_mgr, "run", "build"], fn=f"{self.proj.url}/.next/BUILD_ID", ) # Production start self._artifacts["start"] = Server( proj=self.proj, cmd=[pkg_mgr, "run", "start"], ) def _pkg_manager(self) -> str: if "yarn.lock" in self.proj.basenames: return "yarn" if "pnpm-lock.yaml" in self.proj.basenames: return "pnpm" if "bun.lock" in self.proj.basenames or "bun.lockb" in self.proj.basenames: return "bun" return "npm" @staticmethod def _create(path: str) -> None: run_subprocess( ["npx", "create-next-app@latest", path, "--yes"], cwd=os.path.dirname(path) or ".", output=False, )
[docs] class NuxtJS(Node): """Nuxt.js Vue framework project.""" icon = "💚" spec_doc = "https://nuxt.com/docs/api/nuxt-config" _CONFIG_NAMES = {"nuxt.config.js", "nuxt.config.ts", "nuxt.config.mjs"} def match(self) -> bool: return bool(self._CONFIG_NAMES.intersection(self.proj.basenames)) def parse(self) -> None: from projspec.artifact.base import FileArtifact from projspec.artifact.process import Server super().parse0() pkg_mgr = self._pkg_manager() self._artifacts["dev"] = Server( proj=self.proj, cmd=[pkg_mgr, "run", "dev"], ) self._artifacts["build"] = FileArtifact( proj=self.proj, cmd=[pkg_mgr, "run", "build"], fn=f"{self.proj.url}/.nuxt/tsconfig.json", ) self._artifacts["generate"] = FileArtifact( proj=self.proj, cmd=[pkg_mgr, "run", "generate"], fn=f"{self.proj.url}/.output/public/index.html", ) def _pkg_manager(self) -> str: if "yarn.lock" in self.proj.basenames: return "yarn" if "pnpm-lock.yaml" in self.proj.basenames: return "pnpm" return "npm" @staticmethod def _create(path: str) -> None: run_subprocess( ["npx", "nuxi@latest", "init", path], cwd=os.path.dirname(path) or ".", output=False, )
[docs] class SvelteKit(Node): """SvelteKit project.""" icon = "🔥" spec_doc = "https://svelte.dev/docs/kit/configuration" _CONFIG_NAMES = {"svelte.config.js", "svelte.config.ts"} def match(self) -> bool: return bool(self._CONFIG_NAMES.intersection(self.proj.basenames)) def parse(self) -> None: from projspec.artifact.base import FileArtifact from projspec.artifact.process import Server super().parse0() pkg_mgr = self._pkg_manager() self._artifacts["dev"] = Server( proj=self.proj, cmd=[pkg_mgr, "run", "dev"], ) self._artifacts["build"] = FileArtifact( proj=self.proj, cmd=[pkg_mgr, "run", "build"], fn=f"{self.proj.url}/.svelte-kit/output/client/index.html", ) self._artifacts["preview"] = Server( proj=self.proj, cmd=[pkg_mgr, "run", "preview"], ) def _pkg_manager(self) -> str: if "yarn.lock" in self.proj.basenames: return "yarn" if "pnpm-lock.yaml" in self.proj.basenames: return "pnpm" if "bun.lock" in self.proj.basenames or "bun.lockb" in self.proj.basenames: return "bun" return "npm" @staticmethod def _create(path: str) -> None: run_subprocess( ["npm", "create", "svelte@latest", path], cwd=os.path.dirname(path) or ".", output=False, )
[docs] class Vite(Node): """Vite-based project (any frontend framework using Vite as the build tool). Note: SvelteKit also has a svelte.config, so SvelteKit takes priority via its more-specific match. """ icon = "⚡" spec_doc = "https://vitejs.dev/config/" _CONFIG_NAMES = { "vite.config.js", "vite.config.ts", "vite.config.mjs", "vite.config.cjs", "vite.config.mts", } def match(self) -> bool: return bool(self._CONFIG_NAMES.intersection(self.proj.basenames)) def parse(self) -> None: from projspec.artifact.infra import StaticSite from projspec.artifact.process import Server super().parse0() pkg_mgr = self._pkg_manager() self._artifacts["dev"] = Server( proj=self.proj, cmd=[pkg_mgr, "run", "dev"], ) self._artifacts["build"] = StaticSite( proj=self.proj, cmd=[pkg_mgr, "run", "build"], fn=f"{self.proj.url}/dist/index.html", ) self._artifacts["preview"] = Server( proj=self.proj, cmd=[pkg_mgr, "run", "preview"], ) def _pkg_manager(self) -> str: if "yarn.lock" in self.proj.basenames: return "yarn" if "pnpm-lock.yaml" in self.proj.basenames: return "pnpm" if "bun.lock" in self.proj.basenames or "bun.lockb" in self.proj.basenames: return "bun" return "npm" @staticmethod def _create(path: str) -> None: run_subprocess( [ "npm", "create", "vite@latest", path, "--", "--template", "vanilla", ], cwd=os.path.dirname(path) or ".", output=False, )
[docs] class Pnpm(Node): """Node project managed with pnpm.""" icon = "📦" spec_doc = "https://pnpm.io/package_json" def match(self) -> bool: return "pnpm-lock.yaml" in self.proj.basenames def parse(self) -> None: 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}/pnpm-lock.yaml", "rt") as f: import yaml as _yaml lock = _yaml.safe_load(f) except Exception: lock = {} self._artifacts["lock_file"] = LockFile( proj=self.proj, cmd=["pnpm", "install"], fn=self.proj.basenames["pnpm-lock.yaml"], ) if isinstance(lock, dict): pkgs = list(lock.get("packages", {}).keys()) if pkgs: self._contents.setdefault("environment", AttrDict())[ "pnpm_lock" ] = Environment( proj=self.proj, stack=Stack.NPM, packages=pkgs, precision=Precision.LOCK, ) @staticmethod def _create(path: str) -> None: run_subprocess(["pnpm", "init"], cwd=path, output=False)
[docs] class Bun(Node): """Node project managed with Bun.""" icon = "🍞" spec_doc = "https://bun.sh/docs/install/lockfile" def match(self) -> bool: return bool({"bun.lock", "bun.lockb"}.intersection(self.proj.basenames)) def parse(self) -> None: from projspec.artifact.python_env import LockFile super().parse0() lock_name = "bun.lock" if "bun.lock" in self.proj.basenames else "bun.lockb" self._artifacts["lock_file"] = LockFile( proj=self.proj, cmd=["bun", "install"], fn=self.proj.basenames[lock_name], ) @staticmethod def _create(path: str) -> None: run_subprocess(["bun", "init", "-y"], cwd=path, output=False)
[docs] class Deno(ProjectSpec): """Deno project. Note: this is a separate runtime, not a Node project. """ icon = "🖧" spec_doc = "https://docs.deno.com/runtime/fundamentals/configuration/" _CONFIG_NAMES = {"deno.json", "deno.jsonc"} def match(self) -> bool: return bool(self._CONFIG_NAMES.intersection(self.proj.basenames)) def parse(self) -> None: import json from projspec.artifact.base import FileArtifact from projspec.artifact.process import Process, Server from projspec.artifact.python_env import LockFile from projspec.content.executable import Command from projspec.content.metadata import DescriptiveMetadata fname = next(n for n in self._CONFIG_NAMES if n in self.proj.basenames) try: with self.proj.get_file(fname) as f: cfg = json.loads(f.read()) except Exception as exc: raise ParseFailed(f"Could not read {fname}: {exc}") from exc if not isinstance(cfg, dict): raise ParseFailed(f"{fname} did not parse to a mapping") meta: dict[str, str] = {} for key in ("name", "version", "description"): if val := cfg.get(key): meta[key] = str(val) conts = AttrDict() if meta: conts["descriptive_metadata"] = DescriptiveMetadata( proj=self.proj, meta=meta ) arts = AttrDict() tasks = cfg.get("tasks", {}) cmds = AttrDict() for task_name, task_cmd in tasks.items(): cmd_list = ["deno", "task", task_name] cmds[task_name] = Command(proj=self.proj, cmd=cmd_list) arts[task_name] = Process(proj=self.proj, cmd=cmd_list) if cmds: conts["command"] = cmds # Lock file if "deno.lock" in self.proj.basenames: arts["lock_file"] = LockFile( proj=self.proj, cmd=["deno", "cache", "--reload", "mod.ts"], fn=self.proj.basenames["deno.lock"], ) # Main entry point main = cfg.get("main") or cfg.get("exports") if main and isinstance(main, str): arts["run"] = Process( proj=self.proj, cmd=["deno", "run", "--allow-all", main], ) self._contents = conts self._artifacts = arts @staticmethod def _create(path: str) -> None: run_subprocess( ["deno", "init", path], cwd=os.path.dirname(path) or ".", output=False, )