@jxsuite/server 0.30.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/server",
3
- "version": "0.30.1",
3
+ "version": "0.31.0",
4
4
  "description": "Jx development server with live reload, server-side proxy, and studio integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -15,6 +15,7 @@
15
15
  "exports": {
16
16
  ".": "./src/server.ts",
17
17
  "./claude-session": "./src/claude-session.ts",
18
+ "./packages": "./src/packages.ts",
18
19
  "./refactor": "./src/refactor/index.ts",
19
20
  "./resolve": "./src/resolve.ts",
20
21
  "./types": "./src/types.ts"
@@ -28,13 +29,13 @@
28
29
  "test:coverage": "bun test --isolate --coverage"
29
30
  },
30
31
  "dependencies": {
31
- "@anthropic-ai/claude-agent-sdk": "^0.3.186",
32
+ "@anthropic-ai/claude-agent-sdk": "^0.3.190",
32
33
  "@anthropic-ai/sdk": "^0.105.0",
33
- "@jxsuite/compiler": "^0.30.1",
34
- "@jxsuite/create": "^0.30.1",
35
- "@jxsuite/parser": "^0.30.1",
36
- "@jxsuite/runtime": "^0.30.1",
37
- "@jxsuite/schema": "^0.30.1",
34
+ "@jxsuite/compiler": "^0.31.0",
35
+ "@jxsuite/create": "^0.31.0",
36
+ "@jxsuite/parser": "^0.31.0",
37
+ "@jxsuite/runtime": "^0.31.0",
38
+ "@jxsuite/schema": "^0.31.0",
38
39
  "@modelcontextprotocol/sdk": "^1.29.0",
39
40
  "chokidar": "^5.0.0",
40
41
  "zod": "^4.4.3"
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Bun-driven package operations, parameterized by a project root. Shared by the desktop app
3
+ * (`createPackageOps`) and the dev server's `/__studio/packages*` endpoints so both backends behave
4
+ * identically. Every function is pure with respect to its `root` argument and spawns `bun` in that
5
+ * directory.
6
+ */
7
+
8
+ import { existsSync } from "node:fs";
9
+ import { readFile, writeFile } from "node:fs/promises";
10
+ import { resolve } from "node:path";
11
+
12
+ export interface PackageInfo {
13
+ name: string;
14
+ version: string;
15
+ /** True when the dependency lives in `devDependencies` rather than `dependencies`. */
16
+ dev?: boolean;
17
+ }
18
+
19
+ export interface OutdatedInfo {
20
+ name: string;
21
+ /** The version range pinned in package.json (e.g. "^0.19.0"). */
22
+ current: string;
23
+ /** The newest published version. */
24
+ latest: string;
25
+ dev?: boolean;
26
+ }
27
+
28
+ /** Result of a package mutation that runs `bun install` (install / set-versions). */
29
+ export interface PackageOpResult {
30
+ ok: boolean;
31
+ /** Combined stdout/stderr from the bun invocation, surfaced to the user on failure. */
32
+ log?: string;
33
+ }
34
+
35
+ interface PackageJson {
36
+ dependencies?: Record<string, string>;
37
+ devDependencies?: Record<string, string>;
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ /** Minimal fetch signature (injectable for tests; the global `fetch` satisfies it). */
42
+ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
43
+
44
+ /** Strip a semver range prefix to its base version: "^0.19.0" → "0.19.0". */
45
+ export function stripRange(range: string): string {
46
+ const base = range.replace(/^[\^~>=<v\s]+/, "").trim();
47
+ return base.split(/\s+/)[0] ?? "";
48
+ }
49
+
50
+ /**
51
+ * Whether a version spec points at the npm registry (a plain semver range). Skips workspace:,
52
+ * file:, link:, npm:, git, and URL specs, plus wildcards like "*" or "latest".
53
+ */
54
+ export function isRegistryRange(spec: string): boolean {
55
+ if (spec.includes(":")) {
56
+ return false;
57
+ }
58
+ return /^\d/.test(stripRange(spec));
59
+ }
60
+
61
+ async function readPackageJson(root: string): Promise<PackageJson | null> {
62
+ const pkgPath = resolve(root, "package.json");
63
+ if (!existsSync(pkgPath)) {
64
+ return null;
65
+ }
66
+ try {
67
+ return JSON.parse(await readFile(pkgPath, "utf8")) as PackageJson;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /** List the project's dependencies and devDependencies (devDeps tagged with `dev: true`). */
74
+ export async function listPackages(root: string): Promise<PackageInfo[]> {
75
+ const pkg = await readPackageJson(root);
76
+ if (!pkg) {
77
+ return [];
78
+ }
79
+ const out: PackageInfo[] = [];
80
+ for (const [name, version] of Object.entries(pkg.dependencies ?? {})) {
81
+ out.push({ name, version });
82
+ }
83
+ for (const [name, version] of Object.entries(pkg.devDependencies ?? {})) {
84
+ out.push({ dev: true, name, version });
85
+ }
86
+ return out;
87
+ }
88
+
89
+ /** Whether the project has a package.json but no installed node_modules. */
90
+ export function dependenciesNeedInstall(root: string): boolean {
91
+ if (!existsSync(resolve(root, "package.json"))) {
92
+ return false;
93
+ }
94
+ return !existsSync(resolve(root, "node_modules"));
95
+ }
96
+
97
+ async function runBun(args: string[], cwd: string): Promise<PackageOpResult> {
98
+ const proc = Bun.spawn(["bun", ...args], { cwd, stderr: "pipe", stdout: "pipe" });
99
+ const [stdout, stderr, exitCode] = await Promise.all([
100
+ new Response(proc.stdout).text(),
101
+ new Response(proc.stderr).text(),
102
+ proc.exited,
103
+ ]);
104
+ const log = `${stdout}${stderr}`.trim();
105
+ return { ok: exitCode === 0, ...(log ? { log } : {}) };
106
+ }
107
+
108
+ /** Run `bun install` in the project root. */
109
+ export async function installDependencies(root: string): Promise<PackageOpResult> {
110
+ return runBun(["install"], root);
111
+ }
112
+
113
+ /** Run `bun add` (with `-d` for a dev dependency). */
114
+ export async function addPackage(
115
+ root: string,
116
+ name: string,
117
+ dev = false,
118
+ ): Promise<PackageOpResult> {
119
+ return runBun(dev ? ["add", "-d", name] : ["add", name], root);
120
+ }
121
+
122
+ /** Run `bun remove`. */
123
+ export async function removePackage(root: string, name: string): Promise<PackageOpResult> {
124
+ return runBun(["remove", name], root);
125
+ }
126
+
127
+ /** Fetch the newest published version for a package from the npm registry, or null on any failure. */
128
+ export async function fetchLatestVersion(
129
+ name: string,
130
+ fetchImpl: FetchLike = fetch,
131
+ ): Promise<string | null> {
132
+ try {
133
+ const res = await fetchImpl(`https://registry.npmjs.org/${name}/latest`, {
134
+ headers: { accept: "application/vnd.npm.install-v1+json" },
135
+ });
136
+ if (!res.ok) {
137
+ return null;
138
+ }
139
+ const data = (await res.json()) as { version?: string };
140
+ return data.version ?? null;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * List dependencies whose newest published version differs from the pinned base. Registry lookups
148
+ * run concurrently; non-registry specs (workspace:, file:, …) and lookup failures are skipped.
149
+ */
150
+ export async function outdatedPackages(
151
+ root: string,
152
+ fetchImpl: FetchLike = fetch,
153
+ ): Promise<OutdatedInfo[]> {
154
+ const pkgs = await listPackages(root);
155
+ const checks = await Promise.all(
156
+ pkgs.map(async (p): Promise<OutdatedInfo | null> => {
157
+ if (!isRegistryRange(p.version)) {
158
+ return null;
159
+ }
160
+ const latest = await fetchLatestVersion(p.name, fetchImpl);
161
+ if (!latest || latest === stripRange(p.version)) {
162
+ return null;
163
+ }
164
+ const entry: OutdatedInfo = { current: p.version, latest, name: p.name };
165
+ if (p.dev) {
166
+ entry.dev = true;
167
+ }
168
+ return entry;
169
+ }),
170
+ );
171
+ return checks.filter((entry): entry is OutdatedInfo => entry !== null);
172
+ }
173
+
174
+ /**
175
+ * Rewrite the version range of each named package in whichever section it already occupies
176
+ * (preserving dependencies/devDependencies placement; new names default to the caller's `dev`
177
+ * flag), then run a single `bun install`.
178
+ */
179
+ export async function setPackageVersions(
180
+ root: string,
181
+ updates: { name: string; version: string; dev?: boolean }[],
182
+ ): Promise<PackageOpResult> {
183
+ if (updates.length === 0) {
184
+ return { ok: true };
185
+ }
186
+ const pkgPath = resolve(root, "package.json");
187
+ if (!existsSync(pkgPath)) {
188
+ return { log: "No package.json in project root", ok: false };
189
+ }
190
+ let pkg: PackageJson;
191
+ try {
192
+ pkg = JSON.parse(await readFile(pkgPath, "utf8")) as PackageJson;
193
+ } catch (error) {
194
+ return { log: `Unparseable package.json: ${String(error)}`, ok: false };
195
+ }
196
+ for (const update of updates) {
197
+ if (pkg.devDependencies && update.name in pkg.devDependencies) {
198
+ pkg.devDependencies[update.name] = update.version;
199
+ } else if (pkg.dependencies && update.name in pkg.dependencies) {
200
+ pkg.dependencies[update.name] = update.version;
201
+ } else {
202
+ const bucket = update.dev ? "devDependencies" : "dependencies";
203
+ pkg[bucket] = { ...(pkg[bucket] as Record<string, string> | undefined) };
204
+ (pkg[bucket] as Record<string, string>)[update.name] = update.version;
205
+ }
206
+ }
207
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
208
+ return installDependencies(root);
209
+ }
package/src/studio-api.ts CHANGED
@@ -13,6 +13,12 @@ import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:
13
13
  import { existsSync, readFileSync, statSync } from "node:fs";
14
14
  import { buildProjectFormatRegistry } from "@jxsuite/compiler/format-host";
15
15
  import { applyRename } from "./refactor/apply.ts";
16
+ import {
17
+ dependenciesNeedInstall,
18
+ installDependencies,
19
+ outdatedPackages,
20
+ setPackageVersions,
21
+ } from "./packages.ts";
16
22
  import type { FormatRegistry } from "@jxsuite/schema/format-registry";
17
23
  import * as claude from "./claude-session.ts";
18
24
  import type { ClassJsonDef } from "./types.ts";
@@ -677,6 +683,7 @@ export async function handleStudioApi(
677
683
  const packages: {
678
684
  name: string;
679
685
  version: string;
686
+ dev: boolean;
680
687
  hasCem: boolean;
681
688
  customElementsPath: string | null;
682
689
  }[] = [];
@@ -695,6 +702,7 @@ export async function handleStudioApi(
695
702
  const depPkg = JSON.parse(await readFile(actualPath, "utf8")) as PackageJson;
696
703
  packages.push({
697
704
  customElementsPath: depPkg.customElements || null,
705
+ dev: pkg.dependencies?.[name] === undefined,
698
706
  hasCem: Boolean(depPkg.customElements),
699
707
  name,
700
708
  version,
@@ -803,6 +811,54 @@ export async function handleStudioApi(
803
811
  }
804
812
  }
805
813
 
814
+ // Install all dependencies (bun install)
815
+ if (path === "/__studio/packages/install" && req.method === "POST") {
816
+ try {
817
+ const body = (await req.json().catch(() => ({}))) as { dir?: string };
818
+ const dir = body.dir || activeProjectRoot || root;
819
+ const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
820
+ return Response.json(await installDependencies(scanRoot));
821
+ } catch (error) {
822
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
823
+ }
824
+ }
825
+
826
+ // Whether dependencies need installing (node_modules missing)
827
+ if (path === "/__studio/packages/needs-install" && req.method === "GET") {
828
+ const dir = url.searchParams.get("dir") || activeProjectRoot || root;
829
+ const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
830
+ return Response.json({ needsInstall: dependenciesNeedInstall(scanRoot) });
831
+ }
832
+
833
+ // Dependencies with a newer version available
834
+ if (path === "/__studio/packages/outdated" && req.method === "GET") {
835
+ try {
836
+ const dir = url.searchParams.get("dir") || activeProjectRoot || root;
837
+ const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
838
+ return Response.json(await outdatedPackages(scanRoot));
839
+ } catch (error) {
840
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
841
+ }
842
+ }
843
+
844
+ // Set version ranges for one or more packages, then reinstall
845
+ if (path === "/__studio/packages/set-versions" && req.method === "POST") {
846
+ try {
847
+ const body = (await req.json()) as {
848
+ dir?: string;
849
+ updates?: { name: string; version: string; dev?: boolean }[];
850
+ };
851
+ if (!Array.isArray(body.updates)) {
852
+ return Response.json({ error: "Missing updates" }, { status: 400 });
853
+ }
854
+ const dir = body.dir || activeProjectRoot || root;
855
+ const scanRoot = isAbsolute(dir) ? dir : resolve(root, dir);
856
+ return Response.json(await setPackageVersions(scanRoot, body.updates));
857
+ } catch (error) {
858
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
859
+ }
860
+ }
861
+
806
862
  // Read file
807
863
  if (path === "/__studio/file" && req.method === "GET") {
808
864
  const fp = url.searchParams.get("path");