@jxsuite/server 0.26.2 → 0.28.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 +8 -8
- package/src/claude-session.ts +9 -2
- package/src/resolve.ts +18 -5
- package/src/server.ts +3 -1
- package/src/studio-api.ts +177 -23
- package/src/watch.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
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.
|
|
31
|
-
"@anthropic-ai/sdk": "^0.
|
|
32
|
-
"@jxsuite/compiler": "^0.
|
|
33
|
-
"@jxsuite/create": "^0.
|
|
34
|
-
"@jxsuite/parser": "^0.
|
|
35
|
-
"@jxsuite/runtime": "^0.
|
|
36
|
-
"@jxsuite/schema": "^0.
|
|
30
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.170",
|
|
31
|
+
"@anthropic-ai/sdk": "^0.104.1",
|
|
32
|
+
"@jxsuite/compiler": "^0.28.0",
|
|
33
|
+
"@jxsuite/create": "^0.28.0",
|
|
34
|
+
"@jxsuite/parser": "^0.28.0",
|
|
35
|
+
"@jxsuite/runtime": "^0.28.0",
|
|
36
|
+
"@jxsuite/schema": "^0.28.0",
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
38
38
|
"chokidar": "^5.0.0",
|
|
39
39
|
"zod": "^4.4.3"
|
package/src/claude-session.ts
CHANGED
|
@@ -297,7 +297,10 @@ export async function getAuthStatus() {
|
|
|
297
297
|
clearTimeout(timeout);
|
|
298
298
|
ctrl.abort();
|
|
299
299
|
if (result.is_error) {
|
|
300
|
-
return {
|
|
300
|
+
return {
|
|
301
|
+
authenticated: false,
|
|
302
|
+
error: (result.result as string) || "API error",
|
|
303
|
+
};
|
|
301
304
|
}
|
|
302
305
|
return { authenticated: true };
|
|
303
306
|
}
|
|
@@ -318,5 +321,9 @@ export async function getAuthStatus() {
|
|
|
318
321
|
export function getSession(id: string) {
|
|
319
322
|
const session = sessions.get(id);
|
|
320
323
|
if (!session) return null;
|
|
321
|
-
return {
|
|
324
|
+
return {
|
|
325
|
+
id: session.id,
|
|
326
|
+
status: session.status,
|
|
327
|
+
messageCount: session.messages.length,
|
|
328
|
+
};
|
|
322
329
|
}
|
package/src/resolve.ts
CHANGED
|
@@ -128,7 +128,10 @@ export async function handleResolve(
|
|
|
128
128
|
|
|
129
129
|
// Self-contained: construct class from schema
|
|
130
130
|
const DynClass = classFromSchema(classDef);
|
|
131
|
-
const instance = new DynClass(config) as {
|
|
131
|
+
const instance = new DynClass(config) as {
|
|
132
|
+
resolve?: () => unknown;
|
|
133
|
+
value?: unknown;
|
|
134
|
+
};
|
|
132
135
|
const value =
|
|
133
136
|
typeof instance.resolve === "function"
|
|
134
137
|
? await instance.resolve()
|
|
@@ -186,12 +189,16 @@ export async function handleServerFunction(req: Request, root: string) {
|
|
|
186
189
|
try {
|
|
187
190
|
mod = await import(moduleAbsPath);
|
|
188
191
|
} catch (e) {
|
|
189
|
-
return new Response(`Failed to import "${$src}": ${(e as Error).message}`, {
|
|
192
|
+
return new Response(`Failed to import "${$src}": ${(e as Error).message}`, {
|
|
193
|
+
status: 500,
|
|
194
|
+
});
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
const fn = mod[xport] ?? mod.default?.[xport];
|
|
193
198
|
if (typeof fn !== "function") {
|
|
194
|
-
return new Response(`Export "${xport}" not found in "${$src}"`, {
|
|
199
|
+
return new Response(`Export "${xport}" not found in "${$src}"`, {
|
|
200
|
+
status: 500,
|
|
201
|
+
});
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
try {
|
|
@@ -246,7 +253,10 @@ function classFromSchema(classDef: ClassJsonDef) {
|
|
|
246
253
|
const sp = (method.setter.parameters ?? []).map((p) => p.$ref?.split("/").pop() ?? "v");
|
|
247
254
|
descriptor.set = new Function(...sp, method.setter.body) as (v: unknown) => void;
|
|
248
255
|
}
|
|
249
|
-
Object.defineProperty(DynClass.prototype, name, {
|
|
256
|
+
Object.defineProperty(DynClass.prototype, name, {
|
|
257
|
+
...descriptor,
|
|
258
|
+
configurable: true,
|
|
259
|
+
});
|
|
250
260
|
} else if (method.scope === "static") {
|
|
251
261
|
(DynClass as DynamicClass)[name] = new Function(...params, bodyStr);
|
|
252
262
|
} else {
|
|
@@ -254,6 +264,9 @@ function classFromSchema(classDef: ClassJsonDef) {
|
|
|
254
264
|
}
|
|
255
265
|
}
|
|
256
266
|
|
|
257
|
-
Object.defineProperty(DynClass, "name", {
|
|
267
|
+
Object.defineProperty(DynClass, "name", {
|
|
268
|
+
value: classDef.title,
|
|
269
|
+
configurable: true,
|
|
270
|
+
});
|
|
258
271
|
return DynClass;
|
|
259
272
|
}
|
package/src/server.ts
CHANGED
|
@@ -285,7 +285,9 @@ export async function createDevServer(options: {
|
|
|
285
285
|
const bundled = bundleCache.get(cacheKey);
|
|
286
286
|
if (bundled) {
|
|
287
287
|
return new Response(bundled, {
|
|
288
|
-
headers: {
|
|
288
|
+
headers: {
|
|
289
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
290
|
+
},
|
|
289
291
|
});
|
|
290
292
|
}
|
|
291
293
|
}
|
package/src/studio-api.ts
CHANGED
|
@@ -7,16 +7,45 @@
|
|
|
7
7
|
* All paths are relative to the project root. Directory traversal above root is rejected.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { resolve, relative, basename, dirname, isAbsolute } from "node:path";
|
|
10
|
+
import { resolve, relative, basename, dirname, extname, isAbsolute } from "node:path";
|
|
11
11
|
import { readdir, stat, readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
|
|
12
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
13
|
-
import {
|
|
12
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
13
|
+
import { buildProjectFormatRegistry } from "@jxsuite/compiler/format-host";
|
|
14
|
+
import type { FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
14
15
|
import * as claude from "./claude-session.ts";
|
|
15
16
|
import type { ClassJsonDef } from "./types.ts";
|
|
16
17
|
|
|
17
18
|
/** Normalise a path to forward slashes (Windows `path` module returns backslashes). */
|
|
18
19
|
const fwd = (p: string) => p.replaceAll("\\", "/");
|
|
19
20
|
|
|
21
|
+
// ─── Format registry (per project root, invalidated on project.json change) ──
|
|
22
|
+
|
|
23
|
+
const formatRegistryCache = new Map<string, { mtime: number; registry: FormatRegistry }>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build (or reuse) the format registry for a project root from its project.json imports map. An
|
|
27
|
+
* empty registry is returned when there is no project.json — only .json files are handled then.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} projectRoot
|
|
30
|
+
* @returns {Promise<FormatRegistry>}
|
|
31
|
+
*/
|
|
32
|
+
async function getFormatRegistry(projectRoot: string): Promise<FormatRegistry> {
|
|
33
|
+
const projectJsonPath = resolve(projectRoot, "project.json");
|
|
34
|
+
let mtime = 0;
|
|
35
|
+
let projectConfig;
|
|
36
|
+
try {
|
|
37
|
+
mtime = statSync(projectJsonPath).mtimeMs;
|
|
38
|
+
projectConfig = JSON.parse(readFileSync(projectJsonPath, "utf8"));
|
|
39
|
+
} catch {
|
|
40
|
+
projectConfig = undefined;
|
|
41
|
+
}
|
|
42
|
+
const cached = formatRegistryCache.get(projectRoot);
|
|
43
|
+
if (cached && cached.mtime === mtime) return cached.registry;
|
|
44
|
+
const registry = await buildProjectFormatRegistry(projectRoot, projectConfig);
|
|
45
|
+
formatRegistryCache.set(projectRoot, { mtime, registry });
|
|
46
|
+
return registry;
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
/**
|
|
21
50
|
* Check that a path is under either the server root OR the active project root. This allows file
|
|
22
51
|
* operations on external projects that have been explicitly activated via /__studio/activate.
|
|
@@ -90,7 +119,11 @@ export function parseGitStatus(out: string) {
|
|
|
90
119
|
filePath = parts.slice(8).join(" ");
|
|
91
120
|
}
|
|
92
121
|
if (stagedCode !== ".") {
|
|
93
|
-
files.push({
|
|
122
|
+
files.push({
|
|
123
|
+
path: filePath,
|
|
124
|
+
status: statusMap[stagedCode] || stagedCode,
|
|
125
|
+
staged: true,
|
|
126
|
+
});
|
|
94
127
|
}
|
|
95
128
|
if (unstagedCode !== ".") {
|
|
96
129
|
files.push({
|
|
@@ -176,7 +209,12 @@ export async function handleStudioApi(
|
|
|
176
209
|
}
|
|
177
210
|
} catch {}
|
|
178
211
|
|
|
179
|
-
return Response.json({
|
|
212
|
+
return Response.json({
|
|
213
|
+
isSiteProject,
|
|
214
|
+
projectConfig,
|
|
215
|
+
directories,
|
|
216
|
+
projectRoot,
|
|
217
|
+
});
|
|
180
218
|
} catch (e) {
|
|
181
219
|
return Response.json({ error: (e as Error).message }, { status: 500 });
|
|
182
220
|
}
|
|
@@ -278,7 +316,12 @@ export async function handleStudioApi(
|
|
|
278
316
|
assertAccessible(destPath, root, activeProjectRoot);
|
|
279
317
|
|
|
280
318
|
const { generateProject } = await import("@jxsuite/create/generate");
|
|
281
|
-
await generateProject(destPath, {
|
|
319
|
+
await generateProject(destPath, {
|
|
320
|
+
name,
|
|
321
|
+
description,
|
|
322
|
+
url: siteUrl,
|
|
323
|
+
adapter,
|
|
324
|
+
});
|
|
282
325
|
|
|
283
326
|
const config = JSON.parse(await readFile(resolve(destPath, "project.json"), "utf8"));
|
|
284
327
|
const projectRoot = fwd(relative(root, destPath));
|
|
@@ -359,7 +402,9 @@ export async function handleStudioApi(
|
|
|
359
402
|
return Response.json({ error: (e as Error).message }, { status: 400 });
|
|
360
403
|
}
|
|
361
404
|
try {
|
|
362
|
-
const
|
|
405
|
+
const registry = await getFormatRegistry(scanRoot);
|
|
406
|
+
const componentExts = registry.documentExtensions("component").map((e: string) => e.slice(1));
|
|
407
|
+
const glob = new Bun.Glob(`**/*.{${["json", ...componentExts].join(",")}}`);
|
|
363
408
|
const components = [];
|
|
364
409
|
for await (const match of glob.scan({ cwd: scanRoot, dot: false })) {
|
|
365
410
|
if (
|
|
@@ -371,14 +416,13 @@ export async function handleStudioApi(
|
|
|
371
416
|
const fp = resolve(scanRoot, match);
|
|
372
417
|
try {
|
|
373
418
|
let content;
|
|
374
|
-
if (match.endsWith(".
|
|
375
|
-
const source = await readFile(fp, "utf8");
|
|
376
|
-
const fmMatch = source.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
377
|
-
if (!fmMatch) continue;
|
|
378
|
-
if (!/^tagName:\s*.+-.+/m.test(fmMatch[1])) continue;
|
|
379
|
-
content = transpileJxMarkdown(source);
|
|
380
|
-
} else {
|
|
419
|
+
if (match.endsWith(".json")) {
|
|
381
420
|
content = JSON.parse(await readFile(fp, "utf8"));
|
|
421
|
+
} else {
|
|
422
|
+
const entry = registry.byExtension(extname(match), "parse");
|
|
423
|
+
if (!entry) continue;
|
|
424
|
+
const source = await readFile(fp, "utf8");
|
|
425
|
+
content = (await entry.call("parse", source)) as Record<string, unknown>;
|
|
382
426
|
}
|
|
383
427
|
if (content.tagName && content.tagName.includes("-")) {
|
|
384
428
|
components.push({
|
|
@@ -401,7 +445,12 @@ export async function handleStudioApi(
|
|
|
401
445
|
return { name, type: typeof d, default: d };
|
|
402
446
|
}
|
|
403
447
|
const obj = d as Record<string, unknown>;
|
|
404
|
-
return {
|
|
448
|
+
return {
|
|
449
|
+
name,
|
|
450
|
+
type: obj.type,
|
|
451
|
+
default: obj.default,
|
|
452
|
+
format: obj.format,
|
|
453
|
+
};
|
|
405
454
|
}),
|
|
406
455
|
hasElements: Array.isArray(content.$elements) && content.$elements.length > 0,
|
|
407
456
|
});
|
|
@@ -514,7 +563,7 @@ export async function handleStudioApi(
|
|
|
514
563
|
const depPkg = JSON.parse(await readFile(actualPath, "utf8"));
|
|
515
564
|
packages.push({
|
|
516
565
|
name,
|
|
517
|
-
version: /** @type {string} */
|
|
566
|
+
version: /** @type {string} */ version,
|
|
518
567
|
hasCem: !!depPkg.customElements,
|
|
519
568
|
customElementsPath: depPkg.customElements || null,
|
|
520
569
|
});
|
|
@@ -563,7 +612,11 @@ export async function handleStudioApi(
|
|
|
563
612
|
const cwd = dir ? (isAbsolute(dir) ? dir : resolve(root, dir)) : root;
|
|
564
613
|
const args = ["add", name];
|
|
565
614
|
if (body.dev) args.splice(1, 0, "-d");
|
|
566
|
-
const proc = Bun.spawn(["bun", ...args], {
|
|
615
|
+
const proc = Bun.spawn(["bun", ...args], {
|
|
616
|
+
cwd,
|
|
617
|
+
stdout: "pipe",
|
|
618
|
+
stderr: "pipe",
|
|
619
|
+
});
|
|
567
620
|
const exitCode = await proc.exited;
|
|
568
621
|
if (exitCode !== 0) {
|
|
569
622
|
const stderr = await new Response(proc.stderr).text();
|
|
@@ -587,7 +640,11 @@ export async function handleStudioApi(
|
|
|
587
640
|
return Response.json({ error: "Missing name" }, { status: 400 });
|
|
588
641
|
const dir = body.dir || activeProjectRoot;
|
|
589
642
|
const cwd = dir ? (isAbsolute(dir) ? dir : resolve(root, dir)) : root;
|
|
590
|
-
const proc = Bun.spawn(["bun", "remove", name], {
|
|
643
|
+
const proc = Bun.spawn(["bun", "remove", name], {
|
|
644
|
+
cwd,
|
|
645
|
+
stdout: "pipe",
|
|
646
|
+
stderr: "pipe",
|
|
647
|
+
});
|
|
591
648
|
const exitCode = await proc.exited;
|
|
592
649
|
if (exitCode !== 0) {
|
|
593
650
|
const stderr = await new Response(proc.stderr).text();
|
|
@@ -747,6 +804,71 @@ export async function handleStudioApi(
|
|
|
747
804
|
}
|
|
748
805
|
|
|
749
806
|
// Discover a plugin module's schema for studio form rendering
|
|
807
|
+
// Format capability proxy — studio fallback for capabilities whose timing excludes "client".
|
|
808
|
+
// Dispatches parse/serialize through the project's format registry by import name.
|
|
809
|
+
if (path === "/__studio/format" && req.method === "POST") {
|
|
810
|
+
try {
|
|
811
|
+
const body = await req.json();
|
|
812
|
+
const { format, action, source, doc, options } = body as {
|
|
813
|
+
format?: string;
|
|
814
|
+
action?: string;
|
|
815
|
+
source?: string;
|
|
816
|
+
doc?: Record<string, unknown>;
|
|
817
|
+
options?: Record<string, unknown>;
|
|
818
|
+
};
|
|
819
|
+
const dir = (body.dir as string | undefined) || activeProjectRoot || root;
|
|
820
|
+
const projectRoot = isAbsolute(dir) ? dir : resolve(root, dir);
|
|
821
|
+
assertAccessible(projectRoot, root, activeProjectRoot);
|
|
822
|
+
|
|
823
|
+
if (!format || !action) {
|
|
824
|
+
return Response.json({ error: "Missing format or action" }, { status: 400 });
|
|
825
|
+
}
|
|
826
|
+
if (action !== "parse" && action !== "serialize") {
|
|
827
|
+
return Response.json({ error: `Unsupported action "${action}"` }, { status: 400 });
|
|
828
|
+
}
|
|
829
|
+
const registry = await getFormatRegistry(projectRoot);
|
|
830
|
+
const entry = registry.byName(format);
|
|
831
|
+
if (!entry) {
|
|
832
|
+
return Response.json(
|
|
833
|
+
{ error: `Format "${format}" is not an imported format class` },
|
|
834
|
+
{ status: 404 },
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
const result =
|
|
838
|
+
action === "parse"
|
|
839
|
+
? await entry.call("parse", source ?? "", options)
|
|
840
|
+
: await entry.call("serialize", doc ?? {}, options);
|
|
841
|
+
return Response.json({ result });
|
|
842
|
+
} catch (e) {
|
|
843
|
+
return Response.json({ error: (e as Error).message }, { status: 500 });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Format registry listing — lets the studio introspect available formats without
|
|
848
|
+
// fetching each .class.json itself.
|
|
849
|
+
if (path === "/__studio/formats" && req.method === "GET") {
|
|
850
|
+
const dir = url.searchParams.get("dir") || activeProjectRoot || root;
|
|
851
|
+
const projectRoot = isAbsolute(dir) ? dir : resolve(root, dir);
|
|
852
|
+
try {
|
|
853
|
+
assertAccessible(projectRoot, root, activeProjectRoot);
|
|
854
|
+
const registry = await getFormatRegistry(projectRoot);
|
|
855
|
+
return Response.json({
|
|
856
|
+
formats: registry.entries.map((e) => ({
|
|
857
|
+
name: e.name,
|
|
858
|
+
extensions: e.extensions,
|
|
859
|
+
mediaType: e.mediaType,
|
|
860
|
+
documentKinds: e.documentKinds,
|
|
861
|
+
exportTarget: e.exportTarget,
|
|
862
|
+
remote: e.remote,
|
|
863
|
+
studio: e.studio,
|
|
864
|
+
capabilities: e.capabilities,
|
|
865
|
+
})),
|
|
866
|
+
});
|
|
867
|
+
} catch (e) {
|
|
868
|
+
return Response.json({ error: (e as Error).message }, { status: 400 });
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
750
872
|
if (path === "/__studio/plugin-schema" && req.method === "GET") {
|
|
751
873
|
const src = url.searchParams.get("src");
|
|
752
874
|
const prototype = url.searchParams.get("prototype");
|
|
@@ -791,7 +913,9 @@ export async function handleStudioApi(
|
|
|
791
913
|
try {
|
|
792
914
|
const content = readFileSync(moduleAbsPath, "utf8");
|
|
793
915
|
const classDef = JSON.parse(content);
|
|
794
|
-
return Response.json({
|
|
916
|
+
return Response.json({
|
|
917
|
+
schema: extractStudioSchema(classDef, moduleAbsPath),
|
|
918
|
+
});
|
|
795
919
|
} catch (e) {
|
|
796
920
|
return Response.json({
|
|
797
921
|
schema: null,
|
|
@@ -807,7 +931,9 @@ export async function handleStudioApi(
|
|
|
807
931
|
try {
|
|
808
932
|
const content = readFileSync(classJsonPath, "utf8");
|
|
809
933
|
const classDef = JSON.parse(content);
|
|
810
|
-
return Response.json({
|
|
934
|
+
return Response.json({
|
|
935
|
+
schema: extractStudioSchema(classDef, classJsonPath),
|
|
936
|
+
});
|
|
811
937
|
} catch {
|
|
812
938
|
// Fall through to JS module import
|
|
813
939
|
}
|
|
@@ -818,7 +944,10 @@ export async function handleStudioApi(
|
|
|
818
944
|
const mod = await import(moduleAbsPath);
|
|
819
945
|
const ExportedClass = mod[exportName] ?? mod.default?.[exportName];
|
|
820
946
|
if (typeof ExportedClass !== "function") {
|
|
821
|
-
return Response.json({
|
|
947
|
+
return Response.json({
|
|
948
|
+
schema: null,
|
|
949
|
+
error: `Export "${exportName}" not found`,
|
|
950
|
+
});
|
|
822
951
|
}
|
|
823
952
|
return Response.json({ schema: ExportedClass.schema ?? null });
|
|
824
953
|
} catch (e) {
|
|
@@ -836,7 +965,11 @@ export async function handleStudioApi(
|
|
|
836
965
|
const gitCmd = path.slice("/__studio/git/".length);
|
|
837
966
|
|
|
838
967
|
const runGit = async (args: string[]) => {
|
|
839
|
-
const proc = Bun.spawn(["git", ...args], {
|
|
968
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
969
|
+
cwd,
|
|
970
|
+
stdout: "pipe",
|
|
971
|
+
stderr: "pipe",
|
|
972
|
+
});
|
|
840
973
|
const exitCode = await proc.exited;
|
|
841
974
|
const stdout = await new Response(proc.stdout).text();
|
|
842
975
|
const stderr = await new Response(proc.stderr).text();
|
|
@@ -1004,7 +1137,7 @@ export async function handleStudioApi(
|
|
|
1004
1137
|
if (!fp) return Response.json({ error: "Missing path" }, { status: 400 });
|
|
1005
1138
|
if (fp.includes("..")) return Response.json({ error: "Invalid path" }, { status: 400 });
|
|
1006
1139
|
const content = await runGit(["show", `${ref}:${fp}`]);
|
|
1007
|
-
return Response.json({ content
|
|
1140
|
+
return Response.json({ content });
|
|
1008
1141
|
}
|
|
1009
1142
|
|
|
1010
1143
|
if (gitCmd === "discard" && req.method === "POST") {
|
|
@@ -1183,10 +1316,31 @@ function extractStudioSchema(classDef: ClassJsonDef, classJsonPath: string) {
|
|
|
1183
1316
|
|
|
1184
1317
|
const resolveMethod = classDef.$defs?.methods?.resolve;
|
|
1185
1318
|
|
|
1319
|
+
// Surface format-extension metadata: the format block, studio hints, and a
|
|
1320
|
+
// capability summary ({ parse: { timing }, serialize: { timing }, ... }).
|
|
1321
|
+
const def = classDef as Record<string, unknown>;
|
|
1322
|
+
const capabilityRoles = new Set(["parse", "serialize", "discover", "load"]);
|
|
1323
|
+
const capabilities: Record<string, { identifier: string; timing: string[] }> = {};
|
|
1324
|
+
const methods = (classDef.$defs?.methods ?? {}) as Record<
|
|
1325
|
+
string,
|
|
1326
|
+
{ role?: string; identifier?: string; timing?: string[] }
|
|
1327
|
+
>;
|
|
1328
|
+
for (const [key, method] of Object.entries(methods)) {
|
|
1329
|
+
if (method.role && capabilityRoles.has(method.role)) {
|
|
1330
|
+
capabilities[method.role] = {
|
|
1331
|
+
identifier: method.identifier ?? key,
|
|
1332
|
+
timing: method.timing ?? ["compiler", "server"],
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1186
1337
|
return {
|
|
1187
1338
|
description: classDef.description ?? classDef.title,
|
|
1188
1339
|
properties,
|
|
1189
1340
|
required: [...requiredSet],
|
|
1190
1341
|
...(resolveMethod?.returns ? { returns: resolveMethod.returns } : {}),
|
|
1342
|
+
...(def.format ? { format: def.format } : {}),
|
|
1343
|
+
...(def.$studio ? { $studio: def.$studio } : {}),
|
|
1344
|
+
...(Object.keys(capabilities).length > 0 ? { capabilities } : {}),
|
|
1191
1345
|
};
|
|
1192
1346
|
}
|
package/src/watch.ts
CHANGED
|
@@ -67,7 +67,11 @@ export function injectSSE(html: string) {
|
|
|
67
67
|
export function createWatcher(
|
|
68
68
|
root: string,
|
|
69
69
|
builds: BuildEntry[],
|
|
70
|
-
opts: {
|
|
70
|
+
opts: {
|
|
71
|
+
ignore?: string[];
|
|
72
|
+
debounce?: number;
|
|
73
|
+
reloadOnAnyChange?: boolean;
|
|
74
|
+
} = {},
|
|
71
75
|
) {
|
|
72
76
|
const ignore = opts.ignore ?? DEFAULT_IGNORE;
|
|
73
77
|
const debounceMs = opts.debounce ?? 50;
|