@jxsuite/server 0.28.2 → 0.28.3

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.28.2",
3
+ "version": "0.28.3",
4
4
  "description": "Jx development server with live reload, server-side proxy, and studio integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,13 +27,13 @@
27
27
  "test:coverage": "bun test --isolate --coverage"
28
28
  },
29
29
  "dependencies": {
30
- "@anthropic-ai/claude-agent-sdk": "^0.3.170",
30
+ "@anthropic-ai/claude-agent-sdk": "^0.3.173",
31
31
  "@anthropic-ai/sdk": "^0.104.1",
32
- "@jxsuite/compiler": "^0.28.2",
33
- "@jxsuite/create": "^0.28.2",
34
- "@jxsuite/parser": "^0.28.2",
35
- "@jxsuite/runtime": "^0.28.2",
36
- "@jxsuite/schema": "^0.28.2",
32
+ "@jxsuite/compiler": "^0.28.3",
33
+ "@jxsuite/create": "^0.28.3",
34
+ "@jxsuite/parser": "^0.28.3",
35
+ "@jxsuite/runtime": "^0.28.3",
36
+ "@jxsuite/schema": "^0.28.3",
37
37
  "@modelcontextprotocol/sdk": "^1.29.0",
38
38
  "chokidar": "^5.0.0",
39
39
  "zod": "^4.4.3"
package/src/build.ts CHANGED
@@ -21,13 +21,18 @@ export async function buildAll(
21
21
  for (const entry of builds) {
22
22
  const { match: _match, label, ...opts } = entry;
23
23
  const result = await Bun.build({
24
- target: "browser",
25
24
  format: "esm",
26
25
  sourcemap: "linked",
26
+ target: "browser",
27
27
  ...opts,
28
28
  });
29
- if (!result.success) result.logs.forEach((l) => console.error(l));
30
- else console.log(`Built → ${entry.outdir}/${label ?? "bundle"}.js`);
29
+ if (!result.success) {
30
+ for (const l of result.logs) {
31
+ console.error(l);
32
+ }
33
+ } else {
34
+ console.log(`Built → ${entry.outdir}/${label ?? "bundle"}.js`);
35
+ }
31
36
  }
32
37
  }
33
38
 
@@ -55,26 +60,32 @@ export async function rebuild(
55
60
  const rebuilt = [];
56
61
  let ok = true;
57
62
  for (const entry of builds) {
58
- if (!entry.match) continue;
63
+ if (!entry.match) {
64
+ continue;
65
+ }
59
66
  const matches =
60
67
  typeof entry.match === "function"
61
68
  ? entry.match(changedFile)
62
69
  : entry.match instanceof RegExp
63
70
  ? entry.match.test(changedFile)
64
71
  : false;
65
- if (!matches) continue;
72
+ if (!matches) {
73
+ continue;
74
+ }
66
75
  const { match: _match, label, ...opts } = entry;
67
76
  const result = await Bun.build({
68
- target: "browser",
69
77
  format: "esm",
70
78
  sourcemap: "linked",
79
+ target: "browser",
71
80
  ...opts,
72
81
  });
73
82
  if (result.success) {
74
83
  rebuilt.push(label ?? entry.outdir);
75
84
  console.log(`Rebuilt → ${entry.outdir}/${label ?? "bundle"}.js (${changedFile} changed)`);
76
85
  } else {
77
- result.logs.forEach((l) => console.error(l));
86
+ for (const l of result.logs) {
87
+ console.error(l);
88
+ }
78
89
  ok = false;
79
90
  }
80
91
  }
@@ -8,8 +8,7 @@
8
8
 
9
9
  import { query } from "@anthropic-ai/claude-agent-sdk";
10
10
 
11
- import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
12
- import type { Options as AgentOptions } from "@anthropic-ai/claude-agent-sdk";
11
+ import type { Options as AgentOptions, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
13
12
 
14
13
  interface Session {
15
14
  id: string;
@@ -39,12 +38,12 @@ function buildSdkEnv() {
39
38
  return env;
40
39
  }
41
40
 
42
- const sessions: Map<string, Session> = new Map();
41
+ const sessions = new Map<string, Session>();
43
42
 
44
43
  let idCounter = 0;
45
44
 
46
45
  function genId() {
47
- return `ai_${Date.now().toString(36)}_${(++idCounter).toString(36)}`;
46
+ return `ai_${Date.now().toString(36)}_${(idCounter += 1).toString(36)}`;
48
47
  }
49
48
 
50
49
  /**
@@ -71,15 +70,17 @@ async function processStream(session: Session, stream: AsyncGenerator<SDKMessage
71
70
  session.status = "active";
72
71
  try {
73
72
  for await (const message of stream) {
74
- if ((session.status as string) === "stopped") break;
73
+ if ((session.status as string) === "stopped") {
74
+ break;
75
+ }
75
76
  if (message.session_id && !session.sessionId) {
76
77
  session.sessionId = message.session_id;
77
78
  }
78
79
  broadcast(session, message.type, message);
79
80
  }
80
- } catch (err) {
81
- if (err instanceof Error && err.name !== "AbortError") {
82
- broadcast(session, "error", { type: "error", error: String(err) });
81
+ } catch (error) {
82
+ if (error instanceof Error && error.name !== "AbortError") {
83
+ broadcast(session, "error", { error: String(error), type: "error" });
83
84
  }
84
85
  } finally {
85
86
  session.status = "idle";
@@ -106,34 +107,34 @@ export function createSession(
106
107
  const abortController = new AbortController();
107
108
 
108
109
  const session: Session = {
110
+ abortController,
109
111
  id,
110
- sessionId: null,
111
- projectRoot,
112
112
  messages: [],
113
+ projectRoot,
114
+ query: null,
115
+ sessionId: null,
113
116
  sseClients: new Set(),
114
117
  status: "active",
115
- abortController,
116
- query: null,
117
118
  };
118
119
 
119
120
  sessions.set(id, session);
120
121
 
121
122
  const queryOpts: AgentOptions = {
122
- cwd: projectRoot,
123
123
  abortController,
124
- permissionMode: "acceptEdits",
125
124
  allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
126
- maxTurns: 30,
125
+ cwd: projectRoot,
126
+ env: buildSdkEnv(),
127
127
  includePartialMessages: true,
128
+ maxTurns: 30,
129
+ permissionMode: "acceptEdits",
128
130
  persistSession: true,
129
- env: buildSdkEnv(),
130
131
  };
131
132
 
132
133
  if (opts.systemPrompt) {
133
134
  queryOpts.systemPrompt = opts.systemPrompt;
134
135
  }
135
136
 
136
- const stream = query({ prompt: message, options: queryOpts });
137
+ const stream = query({ options: queryOpts, prompt: message });
137
138
  session.query = stream;
138
139
  processStream(session, stream);
139
140
 
@@ -148,21 +149,25 @@ export function createSession(
148
149
  */
149
150
  export function sendMessage(id: string, message: string) {
150
151
  const session = sessions.get(id);
151
- if (!session) throw new Error(`Session not found: ${id}`);
152
- if (session.status === "active") throw new Error("Session is still processing");
152
+ if (!session) {
153
+ throw new Error(`Session not found: ${id}`);
154
+ }
155
+ if (session.status === "active") {
156
+ throw new Error("Session is still processing");
157
+ }
153
158
 
154
159
  const abortController = new AbortController();
155
160
  session.abortController = abortController;
156
161
 
157
162
  const queryOpts: AgentOptions = {
158
- cwd: session.projectRoot,
159
163
  abortController,
160
- permissionMode: "acceptEdits",
161
164
  allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
162
- maxTurns: 30,
163
- includePartialMessages: true,
164
165
  continue: true,
166
+ cwd: session.projectRoot,
165
167
  env: buildSdkEnv(),
168
+ includePartialMessages: true,
169
+ maxTurns: 30,
170
+ permissionMode: "acceptEdits",
166
171
  };
167
172
 
168
173
  if (session.sessionId) {
@@ -170,7 +175,7 @@ export function sendMessage(id: string, message: string) {
170
175
  delete queryOpts.continue;
171
176
  }
172
177
 
173
- const stream = query({ prompt: message, options: queryOpts });
178
+ const stream = query({ options: queryOpts, prompt: message });
174
179
  session.query = stream;
175
180
  processStream(session, stream);
176
181
  }
@@ -182,7 +187,9 @@ export function sendMessage(id: string, message: string) {
182
187
  */
183
188
  export function stopSession(id: string) {
184
189
  const session = sessions.get(id);
185
- if (!session) return;
190
+ if (!session) {
191
+ return;
192
+ }
186
193
  session.status = "stopped";
187
194
  if (session.abortController) {
188
195
  session.abortController.abort();
@@ -214,6 +221,9 @@ export function streamSession(id: string) {
214
221
  const encoder = new TextEncoder();
215
222
  let sendRef: (event: string, data: unknown) => void = () => {};
216
223
  const stream = new ReadableStream({
224
+ cancel() {
225
+ session.sseClients.delete(sendRef);
226
+ },
217
227
  start(controller) {
218
228
  /** @param {string} event @param {unknown} data */
219
229
  sendRef = (event: string, data: unknown) => {
@@ -237,18 +247,15 @@ export function streamSession(id: string) {
237
247
  clearInterval(heartbeat);
238
248
  session.sseClients.delete(sendRef);
239
249
  }
240
- }, 15000);
241
- },
242
- cancel() {
243
- session.sseClients.delete(sendRef);
250
+ }, 15_000);
244
251
  },
245
252
  });
246
253
 
247
254
  return new Response(stream, {
248
255
  headers: {
249
- "Content-Type": "text/event-stream",
250
256
  "Cache-Control": "no-cache",
251
257
  Connection: "keep-alive",
258
+ "Content-Type": "text/event-stream",
252
259
  },
253
260
  });
254
261
  }
@@ -261,16 +268,16 @@ export function streamSession(id: string) {
261
268
  export async function getAuthStatus() {
262
269
  try {
263
270
  const ctrl = new AbortController();
264
- const timeout = setTimeout(() => ctrl.abort(), 15000);
271
+ const timeout = setTimeout(() => ctrl.abort(), 15_000);
265
272
  const testStream = query({
266
- prompt: "Say OK",
267
273
  options: {
268
274
  abortController: ctrl,
275
+ env: buildSdkEnv(),
276
+ includePartialMessages: true,
269
277
  maxTurns: 1,
270
278
  persistSession: false,
271
- includePartialMessages: true,
272
- env: buildSdkEnv(),
273
279
  },
280
+ prompt: "Say OK",
274
281
  });
275
282
 
276
283
  for await (const msg of testStream) {
@@ -279,9 +286,9 @@ export async function getAuthStatus() {
279
286
  | { content?: unknown[] }
280
287
  | undefined;
281
288
  if (Array.isArray(content?.content)) {
282
- const text = content.content.find((b: any) => b.type === "text") as
283
- | { text?: string }
284
- | undefined;
289
+ const text = content.content.find(
290
+ (b: unknown) => (b as { type?: string }).type === "text",
291
+ ) as { text?: string } | undefined;
285
292
  if (text?.text?.toLowerCase().includes("error")) {
286
293
  clearTimeout(timeout);
287
294
  ctrl.abort();
@@ -308,8 +315,8 @@ export async function getAuthStatus() {
308
315
 
309
316
  clearTimeout(timeout);
310
317
  return { authenticated: true };
311
- } catch (err) {
312
- return { authenticated: false, error: String(err) };
318
+ } catch (error) {
319
+ return { authenticated: false, error: String(error) };
313
320
  }
314
321
  }
315
322
 
@@ -320,10 +327,12 @@ export async function getAuthStatus() {
320
327
  */
321
328
  export function getSession(id: string) {
322
329
  const session = sessions.get(id);
323
- if (!session) return null;
330
+ if (!session) {
331
+ return null;
332
+ }
324
333
  return {
325
334
  id: session.id,
326
- status: session.status,
327
335
  messageCount: session.messages.length,
336
+ status: session.status,
328
337
  };
329
338
  }
package/src/code-api.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { tmpdir } from "node:os";
9
+ import { errorMessage } from "@jxsuite/schema/parse";
9
10
  import { join, resolve } from "node:path";
10
11
  import { unlink } from "node:fs/promises";
11
12
  import { format } from "oxfmt";
@@ -42,8 +43,12 @@ function unwrapFormatted(formatted: string) {
42
43
  const lines = formatted.split("\n");
43
44
  // Remove first line (function header) and last non-empty line (closing brace)
44
45
  let end = lines.length - 1;
45
- while (end > 0 && lines[end].trim() === "") end--;
46
- if (lines[end].trim() === "}") end--;
46
+ while (end > 0 && lines[end].trim() === "") {
47
+ end -= 1;
48
+ }
49
+ if (lines[end].trim() === "}") {
50
+ end -= 1;
51
+ }
47
52
  const bodyLines = lines.slice(1, end + 1);
48
53
  // Dedent by one tab (oxfmt uses the project's indentStyle)
49
54
  return bodyLines.map((l) => (l.startsWith("\t") ? l.slice(1) : l)).join("\n");
@@ -59,17 +64,14 @@ function adjustDiagnostics(diagnostics: OxcDiagnostic[], headerLen: number) {
59
64
  const line = d.labels?.[0]?.span?.line;
60
65
  return line == null || line > 1;
61
66
  })
62
- .map((d) => ({
63
- ...d,
64
- labels: d.labels?.map((label) => ({
65
- ...label,
66
- span: {
67
- ...label.span,
68
- offset: label.span.offset - headerLen,
69
- line: label.span.line - 1,
70
- },
71
- })),
72
- }));
67
+ .map((d) => {
68
+ // Diagnostics are freshly parsed and serialized right after; mutate in place
69
+ for (const label of d.labels ?? []) {
70
+ label.span.line -= 1;
71
+ label.span.offset -= headerLen;
72
+ }
73
+ return d;
74
+ });
73
75
  }
74
76
 
75
77
  // ─── Reusable transpiler ─────────────────────────────────────────────────────
@@ -84,7 +86,9 @@ const minifier = new Bun.Transpiler({ minifyWhitespace: true });
84
86
  */
85
87
  export async function handleCodeApi(req: Request, url: URL) {
86
88
  const path = url.pathname;
87
- if (!path.startsWith("/__studio/code/") || req.method !== "POST") return null;
89
+ if (!path.startsWith("/__studio/code/") || req.method !== "POST") {
90
+ return null;
91
+ }
88
92
 
89
93
  let body;
90
94
  try {
@@ -99,7 +103,9 @@ export async function handleCodeApi(req: Request, url: URL) {
99
103
 
100
104
  if (action === "format") {
101
105
  const { code, args } = body;
102
- if (!code?.trim()) return Response.json({ code: "", errors: [] });
106
+ if (!code?.trim()) {
107
+ return Response.json({ code: "", errors: [] });
108
+ }
103
109
 
104
110
  try {
105
111
  const wrapped = wrapBody(code, args);
@@ -108,10 +114,10 @@ export async function handleCodeApi(req: Request, url: URL) {
108
114
  code: unwrapFormatted(result.code),
109
115
  errors: result.errors,
110
116
  });
111
- } catch (e) {
117
+ } catch (error) {
112
118
  return Response.json({
113
119
  code,
114
- errors: [{ message: (e as Error).message }],
120
+ errors: [{ message: errorMessage(error) }],
115
121
  });
116
122
  }
117
123
  }
@@ -120,13 +126,15 @@ export async function handleCodeApi(req: Request, url: URL) {
120
126
 
121
127
  if (action === "minify") {
122
128
  const { code } = body;
123
- if (!code?.trim()) return Response.json({ code: "" });
129
+ if (!code?.trim()) {
130
+ return Response.json({ code: "" });
131
+ }
124
132
 
125
133
  try {
126
134
  const minified = minifier.transformSync(code).trim();
127
135
  return Response.json({ code: minified });
128
- } catch (e) {
129
- return Response.json({ code, error: (e as Error).message });
136
+ } catch (error) {
137
+ return Response.json({ code, error: errorMessage(error) });
130
138
  }
131
139
  }
132
140
 
@@ -134,7 +142,9 @@ export async function handleCodeApi(req: Request, url: URL) {
134
142
 
135
143
  if (action === "lint") {
136
144
  const { code, args } = body;
137
- if (!code?.trim()) return Response.json({ diagnostics: [] });
145
+ if (!code?.trim()) {
146
+ return Response.json({ diagnostics: [] });
147
+ }
138
148
 
139
149
  const wrapped = wrapBody(code, args);
140
150
  const headerLen = wrapped.indexOf("\n") + 1;
@@ -146,8 +156,8 @@ export async function handleCodeApi(req: Request, url: URL) {
146
156
  try {
147
157
  await Bun.write(tmpFile, wrapped);
148
158
  const proc = Bun.spawn([OXLINT_BIN, "--format=json", "-A", "no-unused-vars", tmpFile], {
149
- stdout: "pipe",
150
159
  stderr: "pipe",
160
+ stdout: "pipe",
151
161
  });
152
162
  const output = await new Response(proc.stdout).text();
153
163
  await proc.exited;
@@ -155,10 +165,10 @@ export async function handleCodeApi(req: Request, url: URL) {
155
165
  const parsed = JSON.parse(output);
156
166
  const adjusted = adjustDiagnostics(parsed.diagnostics || [], headerLen);
157
167
  return Response.json({ diagnostics: adjusted });
158
- } catch (e) {
168
+ } catch (error) {
159
169
  return Response.json({
160
170
  diagnostics: [],
161
- error: (e as Error).message,
171
+ error: errorMessage(error),
162
172
  });
163
173
  } finally {
164
174
  try {
package/src/resolve.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  /** Resolve.js — Generic $src module proxy + timing: "server" function proxy */
2
2
 
3
- import { resolve, relative, dirname } from "node:path";
4
- import { readFileSync, existsSync } from "node:fs";
3
+ import { dirname, relative, resolve } from "node:path";
4
+ import { errorMessage, parseClassDef } from "@jxsuite/schema/parse";
5
+ import { existsSync, readFileSync } from "node:fs";
5
6
  import { createRequire } from "node:module";
6
7
  import { loadContentTypes } from "@jxsuite/compiler/content-loader";
7
8
  import type { DynamicClass } from "@jxsuite/runtime/types";
8
- import type { ClassJsonDef } from "./types.ts";
9
+ import type { JxClassDef } from "@jxsuite/schema/types";
9
10
 
10
11
  /**
11
12
  * Lazy-load project context (project.json + content types) for class instantiation.
@@ -19,7 +20,9 @@ import type { ClassJsonDef } from "./types.ts";
19
20
  */
20
21
  async function loadProjectContext(projectRoot: string) {
21
22
  const projectJsonPath = resolve(projectRoot, "project.json");
22
- if (!existsSync(projectJsonPath)) return null;
23
+ if (!existsSync(projectJsonPath)) {
24
+ return null;
25
+ }
23
26
  try {
24
27
  const config = JSON.parse(readFileSync(projectJsonPath, "utf8"));
25
28
  const contentTypes = config.contentTypes
@@ -51,7 +54,9 @@ export async function handleResolve(
51
54
  }
52
55
 
53
56
  const { $src, $prototype, $export: xport, $base, ...config } = body;
54
- if (!$src) return new Response("Missing $src", { status: 400 });
57
+ if (!$src) {
58
+ return new Response("Missing $src", { status: 400 });
59
+ }
55
60
 
56
61
  let moduleAbsPath;
57
62
  try {
@@ -60,12 +65,12 @@ export async function handleResolve(
60
65
  if ($base) {
61
66
  const docUrlPath = new URL($base).pathname;
62
67
  const docDir = docUrlPath.slice(0, docUrlPath.lastIndexOf("/") + 1);
63
- moduleAbsPath = resolve(resolve(root, "." + docDir), $src);
68
+ moduleAbsPath = resolve(resolve(root, `.${docDir}`), $src);
64
69
  } else {
65
70
  moduleAbsPath = resolve(activeProjectRoot || root, $src);
66
71
  }
67
72
  } else {
68
- // npm/bare specifier — use createRequire from project root, fall back to server package
73
+ // Npm/bare specifier — use createRequire from project root, fall back to server package
69
74
  const projectRoot = activeProjectRoot || root;
70
75
  const projRequire = createRequire(resolve(projectRoot, "package.json"));
71
76
  try {
@@ -75,18 +80,18 @@ export async function handleResolve(
75
80
  moduleAbsPath = serverRequire.resolve($src);
76
81
  }
77
82
  }
78
- } catch (e) {
79
- return new Response(`Cannot resolve $src "${$src}": ${(e as Error).message}`, { status: 400 });
83
+ } catch (error) {
84
+ return new Response(`Cannot resolve $src "${$src}": ${errorMessage(error)}`, { status: 400 });
80
85
  }
81
86
 
82
87
  // Rebase relative config paths from doc-relative to CWD-relative
83
88
  if ($base) {
84
89
  const docUrlPath = new URL($base).pathname;
85
90
  const docDir = docUrlPath.slice(0, docUrlPath.lastIndexOf("/") + 1);
86
- const docAbsDir = resolve(root, "." + docDir);
91
+ const docAbsDir = resolve(root, `.${docDir}`);
87
92
  for (const [k, v] of Object.entries(config)) {
88
93
  if (typeof v === "string" && (v.startsWith("./") || v.startsWith("../"))) {
89
- config[k] = "./" + relative(process.cwd(), resolve(docAbsDir, v)).split("\\").join("/");
94
+ config[k] = `./${relative(process.cwd(), resolve(docAbsDir, v)).split("\\").join("/")}`;
90
95
  }
91
96
  }
92
97
  }
@@ -95,7 +100,7 @@ export async function handleResolve(
95
100
  if (moduleAbsPath.endsWith(".class.json")) {
96
101
  try {
97
102
  const content = readFileSync(moduleAbsPath, "utf8");
98
- const classDef = JSON.parse(content);
103
+ const classDef = parseClassDef(content, moduleAbsPath);
99
104
 
100
105
  // Inject project context for classes that need it
101
106
  const projectRoot = activeProjectRoot || root;
@@ -139,8 +144,8 @@ export async function handleResolve(
139
144
  ? instance.value
140
145
  : instance;
141
146
  return Response.json(value);
142
- } catch (e) {
143
- return Response.json({ error: (e as Error).message }, { status: 500 });
147
+ } catch (error) {
148
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
144
149
  }
145
150
  }
146
151
 
@@ -168,19 +173,21 @@ export async function handleServerFunction(req: Request, root: string) {
168
173
  }
169
174
 
170
175
  const { $src, $export: xport, $base, arguments: args = {} } = body;
171
- if (!$src || !xport) return new Response("Missing $src or $export", { status: 400 });
176
+ if (!$src || !xport) {
177
+ return new Response("Missing $src or $export", { status: 400 });
178
+ }
172
179
 
173
180
  let moduleAbsPath;
174
181
  try {
175
182
  if ($base) {
176
183
  const docUrlPath = new URL($base).pathname;
177
184
  const docDir = docUrlPath.slice(0, docUrlPath.lastIndexOf("/") + 1);
178
- moduleAbsPath = resolve(resolve(root, "." + docDir), $src);
185
+ moduleAbsPath = resolve(resolve(root, `.${docDir}`), $src);
179
186
  } else {
180
187
  moduleAbsPath = resolve(root, $src);
181
188
  }
182
- } catch (e) {
183
- return new Response(`Cannot resolve $src: ${(e as Error).message}`, {
189
+ } catch (error) {
190
+ return new Response(`Cannot resolve $src: ${errorMessage(error)}`, {
184
191
  status: 400,
185
192
  });
186
193
  }
@@ -188,8 +195,8 @@ export async function handleServerFunction(req: Request, root: string) {
188
195
  let mod;
189
196
  try {
190
197
  mod = await import(moduleAbsPath);
191
- } catch (e) {
192
- return new Response(`Failed to import "${$src}": ${(e as Error).message}`, {
198
+ } catch (error) {
199
+ return new Response(`Failed to import "${$src}": ${errorMessage(error)}`, {
193
200
  status: 500,
194
201
  });
195
202
  }
@@ -204,8 +211,8 @@ export async function handleServerFunction(req: Request, root: string) {
204
211
  try {
205
212
  const result = await fn(args);
206
213
  return Response.json(result ?? null);
207
- } catch (e) {
208
- return Response.json({ error: (e as Error).message }, { status: 500 });
214
+ } catch (error) {
215
+ return Response.json({ error: errorMessage(error) }, { status: 500 });
209
216
  }
210
217
  }
211
218
 
@@ -213,23 +220,31 @@ export async function handleServerFunction(req: Request, root: string) {
213
220
  * Dynamically construct a class from a .class.json schema definition. Server-side variant — no
214
221
  * private field limitations.
215
222
  *
216
- * @param {ClassJsonDef} classDef
223
+ * @param {JxClassDef} classDef
217
224
  */
218
- function classFromSchema(classDef: ClassJsonDef) {
225
+ function classFromSchema(classDef: JxClassDef) {
219
226
  const fields = classDef.$defs?.fields ?? {};
220
- const ctor = classDef.$defs?.constructor;
227
+ // JSON objects inherit Object.prototype.constructor — only an own object value counts.
228
+ const rawCtor = classDef.$defs?.constructor;
229
+ const ctor = typeof rawCtor === "object" ? rawCtor : undefined;
221
230
  const methods = classDef.$defs?.methods ?? {};
222
231
 
232
+ // oxlint-disable-next-line typescript/no-extraneous-class -- methods are attached to the prototype dynamically below
223
233
  class DynClass {
224
234
  constructor(config = {}) {
225
235
  const self = this as Record<string, unknown>;
226
236
  const cfg = config as Record<string, unknown>;
227
237
  for (const [key, field] of Object.entries(fields)) {
228
238
  const id = field.identifier ?? key;
229
- if (cfg[id] !== undefined) self[id] = cfg[id];
230
- else if (field.initializer !== undefined) self[id] = field.initializer;
231
- else if (field.default !== undefined) self[id] = structuredClone(field.default);
232
- else self[id] = null;
239
+ if (cfg[id] !== undefined) {
240
+ self[id] = cfg[id];
241
+ } else if (field.initializer !== undefined) {
242
+ self[id] = field.initializer;
243
+ } else if (field.default !== undefined) {
244
+ self[id] = structuredClone(field.default);
245
+ } else {
246
+ self[id] = null;
247
+ }
233
248
  }
234
249
  if (ctor?.body) {
235
250
  const bodyStr = Array.isArray(ctor.body) ? ctor.body.join("\n") : ctor.body;
@@ -241,17 +256,22 @@ function classFromSchema(classDef: ClassJsonDef) {
241
256
  for (const [key, method] of Object.entries(methods)) {
242
257
  const name = method.identifier ?? key;
243
258
  const params = (method.parameters ?? []).map((p) => {
244
- if (p.$ref) return p.$ref.split("/").pop() ?? "arg";
245
- return p.identifier ?? p.name ?? "arg";
259
+ if (p.$ref) {
260
+ return p.$ref.split("/").pop() ?? "arg";
261
+ }
262
+ const n = p.identifier ?? p.name;
263
+ return typeof n === "string" ? n : "arg";
246
264
  });
247
265
  const bodyStr = Array.isArray(method.body) ? method.body.join("\n") : (method.body ?? "");
248
266
 
249
267
  if (method.role === "accessor") {
250
268
  const descriptor: PropertyDescriptor = {};
251
- if (method.getter) descriptor.get = new Function(method.getter.body) as () => unknown;
269
+ if (method.getter) {
270
+ descriptor.get = new Function(method.getter.body ?? "") as () => unknown;
271
+ }
252
272
  if (method.setter) {
253
273
  const sp = (method.setter.parameters ?? []).map((p) => p.$ref?.split("/").pop() ?? "v");
254
- descriptor.set = new Function(...sp, method.setter.body) as (v: unknown) => void;
274
+ descriptor.set = new Function(...sp, method.setter.body ?? "") as (v: unknown) => void;
255
275
  }
256
276
  Object.defineProperty(DynClass.prototype, name, {
257
277
  ...descriptor,
@@ -265,8 +285,8 @@ function classFromSchema(classDef: ClassJsonDef) {
265
285
  }
266
286
 
267
287
  Object.defineProperty(DynClass, "name", {
268
- value: classDef.title,
269
288
  configurable: true,
289
+ value: classDef.title,
270
290
  });
271
291
  return DynClass;
272
292
  }