@jxsuite/server 0.28.2 → 0.28.4
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 +7 -7
- package/src/build.ts +18 -7
- package/src/claude-session.ts +50 -41
- package/src/code-api.ts +34 -24
- package/src/resolve.ts +54 -34
- package/src/server.ts +59 -31
- package/src/studio-api.ts +353 -221
- package/src/watch.ts +26 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/server",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.4",
|
|
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.
|
|
30
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.173",
|
|
31
31
|
"@anthropic-ai/sdk": "^0.104.1",
|
|
32
|
-
"@jxsuite/compiler": "^0.28.
|
|
33
|
-
"@jxsuite/create": "^0.28.
|
|
34
|
-
"@jxsuite/parser": "^0.28.
|
|
35
|
-
"@jxsuite/runtime": "^0.28.
|
|
36
|
-
"@jxsuite/schema": "^0.28.
|
|
32
|
+
"@jxsuite/compiler": "^0.28.4",
|
|
33
|
+
"@jxsuite/create": "^0.28.4",
|
|
34
|
+
"@jxsuite/parser": "^0.28.4",
|
|
35
|
+
"@jxsuite/runtime": "^0.28.4",
|
|
36
|
+
"@jxsuite/schema": "^0.28.4",
|
|
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)
|
|
30
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
86
|
+
for (const l of result.logs) {
|
|
87
|
+
console.error(l);
|
|
88
|
+
}
|
|
78
89
|
ok = false;
|
|
79
90
|
}
|
|
80
91
|
}
|
package/src/claude-session.ts
CHANGED
|
@@ -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
|
|
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)}_${(
|
|
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")
|
|
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 (
|
|
81
|
-
if (
|
|
82
|
-
broadcast(session, "error", {
|
|
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
|
-
|
|
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({
|
|
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)
|
|
152
|
-
|
|
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({
|
|
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)
|
|
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
|
-
},
|
|
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(),
|
|
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(
|
|
283
|
-
|
|
284
|
-
|
|
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 (
|
|
312
|
-
return { authenticated: false, error: String(
|
|
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)
|
|
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() === "")
|
|
46
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
span
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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")
|
|
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())
|
|
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 (
|
|
117
|
+
} catch (error) {
|
|
112
118
|
return Response.json({
|
|
113
119
|
code,
|
|
114
|
-
errors: [{ 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())
|
|
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 (
|
|
129
|
-
return Response.json({ code, error: (
|
|
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())
|
|
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 (
|
|
168
|
+
} catch (error) {
|
|
159
169
|
return Response.json({
|
|
160
170
|
diagnostics: [],
|
|
161
|
-
error: (
|
|
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 {
|
|
4
|
-
import {
|
|
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 {
|
|
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))
|
|
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)
|
|
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,
|
|
68
|
+
moduleAbsPath = resolve(resolve(root, `.${docDir}`), $src);
|
|
64
69
|
} else {
|
|
65
70
|
moduleAbsPath = resolve(activeProjectRoot || root, $src);
|
|
66
71
|
}
|
|
67
72
|
} else {
|
|
68
|
-
//
|
|
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 (
|
|
79
|
-
return new Response(`Cannot resolve $src "${$src}": ${(
|
|
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,
|
|
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] =
|
|
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 =
|
|
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 (
|
|
143
|
-
return Response.json({ error: (
|
|
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)
|
|
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,
|
|
185
|
+
moduleAbsPath = resolve(resolve(root, `.${docDir}`), $src);
|
|
179
186
|
} else {
|
|
180
187
|
moduleAbsPath = resolve(root, $src);
|
|
181
188
|
}
|
|
182
|
-
} catch (
|
|
183
|
-
return new Response(`Cannot resolve $src: ${(
|
|
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 (
|
|
192
|
-
return new Response(`Failed to import "${$src}": ${(
|
|
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 (
|
|
208
|
-
return Response.json({ error: (
|
|
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 {
|
|
223
|
+
* @param {JxClassDef} classDef
|
|
217
224
|
*/
|
|
218
|
-
function classFromSchema(classDef:
|
|
225
|
+
function classFromSchema(classDef: JxClassDef) {
|
|
219
226
|
const fields = classDef.$defs?.fields ?? {};
|
|
220
|
-
|
|
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)
|
|
230
|
-
|
|
231
|
-
else if (field.
|
|
232
|
-
|
|
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)
|
|
245
|
-
|
|
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)
|
|
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
|
}
|