@jxsuite/server 2.2.7 → 4.0.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/README.md CHANGED
@@ -26,19 +26,19 @@ await createDevServer({
26
26
 
27
27
  ## Endpoints
28
28
 
29
- ### Live reload — `GET /__reload`
29
+ ### Live reload: `GET /__reload`
30
30
 
31
31
  SSE endpoint. `chokidar` watches the project root and pushes change events to connected browsers, triggering automatic page reload.
32
32
 
33
- ### `$prototype`/`$src` proxy — `POST /__jx_resolve__`
33
+ ### `$prototype`/`$src` proxy: `POST /__jx_resolve__`
34
34
 
35
35
  Resolves external `$prototype` data sources server-side. Supports `.js` modules and `.class.json` schemas. Avoids browser CORS issues and enables Node.js-only dependencies (`fs`, `glob`, etc.).
36
36
 
37
- ### Server functions — `POST /__jx_server__`
37
+ ### Server functions: `POST /__jx_server__`
38
38
 
39
39
  Executes `timing: "server"` function entries during development. The runtime sends `{ $src, $export, arguments }` and receives the return value as JSON.
40
40
 
41
- ### Studio filesystem API — `/__studio/*`
41
+ ### Studio filesystem API: `/__studio/*`
42
42
 
43
43
  REST API for the Studio visual builder:
44
44
 
@@ -53,7 +53,7 @@ REST API for the Studio visual builder:
53
53
 
54
54
  All file operations are constrained to the project root (path traversal is rejected).
55
55
 
56
- ### Code services — `/__studio/code/*`
56
+ ### Code services: `/__studio/code/*`
57
57
 
58
58
  OXC-powered tools for the Studio function body editor:
59
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/server",
3
- "version": "2.2.7",
3
+ "version": "4.0.0",
4
4
  "description": "Jx development server with live reload, server-side proxy, and studio integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,6 +21,7 @@
21
21
  "./packages": "./src/packages.ts",
22
22
  "./project-server": "./src/project-server.ts",
23
23
  "./oauth-loopback": "./src/oauth-loopback.ts",
24
+ "./live-preview": "./src/live-preview.ts",
24
25
  "./site-preview": "./src/site-preview.ts",
25
26
  "./refactor": "./src/refactor/index.ts",
26
27
  "./resolve": "./src/resolve.ts",
@@ -35,24 +36,25 @@
35
36
  "lint:errors": "bun run scripts/check-error-shapes.ts",
36
37
  "test": "bun run lint:errors && bun test --isolate",
37
38
  "test:coverage": "bun run lint:errors && bun test --isolate --coverage",
38
- "upgrade": "bunx npm-check-updates -u && bun install"
39
+ "upgrade": "bun update --latest && bun install"
39
40
  },
40
41
  "dependencies": {
41
- "@jxsuite/collab": "^0.8.2",
42
- "@jxsuite/compiler": "^2.0.7",
43
- "@jxsuite/create": "^1.3.6",
44
- "@jxsuite/import": "^0.39.7",
45
- "@jxsuite/protocol": "^1.2.1",
46
- "@jxsuite/runtime": "^2.0.5",
47
- "@jxsuite/schema": "^1.8.1",
48
- "@jxsuite/starters": "^1.6.3",
42
+ "@jxsuite/collab": "^0.8.4",
43
+ "@jxsuite/compiler": "^3.0.0",
44
+ "@jxsuite/create": "^1.3.8",
45
+ "@jxsuite/import": "^0.40.0",
46
+ "@jxsuite/protocol": "^2.1.0",
47
+ "@jxsuite/runtime": "^3.0.0",
48
+ "@jxsuite/schema": "^2.0.0",
49
+ "@jxsuite/site": "^1.0.0",
50
+ "@jxsuite/starters": "^1.6.5",
49
51
  "chokidar": "^5.0.0",
50
52
  "kysely": "^0.29.5",
51
53
  "zod": "^4.4.3"
52
54
  },
53
55
  "devDependencies": {
54
- "@jxsuite/auth": "^0.5.4",
55
- "@jxsuite/connector": "^0.5.4",
56
- "@jxsuite/parser": "^1.5.5"
56
+ "@jxsuite/auth": "^0.5.6",
57
+ "@jxsuite/connector": "^0.5.6",
58
+ "@jxsuite/parser": "^1.6.0"
57
59
  }
58
60
  }
package/src/import-api.ts CHANGED
@@ -8,10 +8,16 @@
8
8
  *
9
9
  * Stream protocol (one JSON object per line):
10
10
  * {"type":"progress","phase":"...","message":"...","current":n,"total":n}
11
+ * {"type":"ready","root":"..."} — the destination exists and is openable
11
12
  * {"type":"heartbeat"} — keep-alives during silent phases
12
- * {"type":"done","root":"...","config":{…}} — terminal success
13
+ * {"type":"done","root":"...","config":{…},"result":{…}} — terminal success
13
14
  * {"type":"error","error":"..."} — terminal failure
14
15
  *
16
+ * `ready` arrives seconds in, before the browser launches; `done` arrives minutes later. They are
17
+ * two lines rather than one because they answer different questions — "where is it" and "what did it
18
+ * find" — and a caller that waits for the second to learn the first spends the whole crawl with
19
+ * nothing to show.
20
+ *
15
21
  * LLM key flow (matches ai-api.ts): X-Api-Key / Authorization: Bearer header, falling back to the
16
22
  * OPENAI_API_KEY env var; base URL from X-Api-Base-URL / OPENAI_BASE_URL. When AI component naming
17
23
  * is requested but no key resolves, the import proceeds without the AI pass and emits a warning
@@ -23,6 +29,8 @@
23
29
  import { readFile } from "node:fs/promises";
24
30
  import { resolve } from "node:path";
25
31
  import { problem } from "./problem.ts";
32
+ import type { BreakpointPolicy, BreakpointRounding } from "@jxsuite/import/breakpoint-plan";
33
+ import type { ImportBreakpointPolicy } from "@jxsuite/protocol";
26
34
 
27
35
  export interface ImportApiOptions {
28
36
  /**
@@ -44,6 +52,10 @@ interface ImportRequestBody {
44
52
  maxNodesPerPage?: number;
45
53
  aiComponents?: boolean;
46
54
  aiModel?: string;
55
+ verify?: boolean;
56
+ verifyThreshold?: number;
57
+ verifyMinFidelity?: number;
58
+ breakpoints?: ImportBreakpointPolicy;
47
59
  }
48
60
 
49
61
  /** Seconds between keep-alive lines while a phase is silent (browser launch is the longest gap). */
@@ -52,10 +64,60 @@ const HEARTBEAT_INTERVAL_MS = 15_000;
52
64
  const MAX_DEPTH = 5;
53
65
  const MAX_PAGES = 100;
54
66
 
67
+ /** Pixelmatch colour-tolerance bounds for the optional verify pass. */
68
+ const MIN_THRESHOLD = 0.01;
69
+ const MAX_THRESHOLD = 1;
70
+
71
+ /**
72
+ * The fidelity bar's bounds, as a percentage.
73
+ *
74
+ * `0` is a legal answer and means "report only", which is what this transport did before the bar
75
+ * existed — so the floor here is 0 rather than the CLI's default of 25.
76
+ */
77
+ const MIN_FIDELITY_FLOOR = 0;
78
+ const MIN_FIDELITY_CEILING = 100;
79
+
55
80
  function clamp(value: number, min: number, max: number): number {
56
81
  return Math.min(max, Math.max(min, Math.trunc(value)));
57
82
  }
58
83
 
84
+ /**
85
+ * The breakpoint policy this request asks for, or `undefined` for "whatever the pipeline defaults
86
+ * to".
87
+ *
88
+ * Shape only. The bounds — how many breakpoints a project may keep, which widths are plausible —
89
+ * belong to `@jxsuite/import`'s planner, which clamps them and is tested doing so; restating them
90
+ * here would be a second set of numbers to keep in step with the first. What this does is refuse to
91
+ * hand the pipeline JSON it would have to interpret: a `widths` that is not an array of numbers, a
92
+ * `mode` that is not one of the three.
93
+ */
94
+ function readBreakpointPolicy(
95
+ policy: ImportBreakpointPolicy | undefined,
96
+ ): BreakpointPolicy | undefined {
97
+ if (!policy) {
98
+ return undefined;
99
+ }
100
+ const rounding: BreakpointRounding =
101
+ policy.rounding === "down" || policy.rounding === "up" ? policy.rounding : "nearest";
102
+
103
+ if (policy.mode === "all") {
104
+ return { mode: "all" };
105
+ }
106
+ if (policy.mode === "explicit") {
107
+ const widths = (Array.isArray(policy.widths) ? policy.widths : [])
108
+ .map((width) => Math.trunc(Number(width)))
109
+ .filter((width) => Number.isFinite(width));
110
+ /* An override with no usable width in it is an empty override, not an instruction to keep
111
+ nothing. Falling through to the default beats both a 400 and an empty $media. */
112
+ return widths.length > 0 ? { mode: "explicit", rounding, widths } : undefined;
113
+ }
114
+ if (policy.mode === "limit") {
115
+ const count = Math.trunc(Number(policy.count));
116
+ return { count: Number.isFinite(count) && count > 0 ? count : 3, mode: "limit", rounding };
117
+ }
118
+ return undefined;
119
+ }
120
+
59
121
  function getAiConfig(req: Request): { apiKey: string | null; baseUrl: string | undefined } {
60
122
  const authHeader = req.headers.get("Authorization") || "";
61
123
  const apiKeyHeader = req.headers.get("X-Api-Key") || "";
@@ -102,8 +164,33 @@ async function handleImportSite(req: Request, opts: ImportApiOptions): Promise<R
102
164
  }
103
165
 
104
166
  const depth = clamp(body.depth ?? 1, 0, MAX_DEPTH);
167
+ /*
168
+ * `verify` builds the emitted project and screenshot-diffs every page, so it roughly doubles the
169
+ * run — opt-in, and never a default. It is worth the option because it produces the one number
170
+ * the caller can act on: every other finding is a count of things that were skipped, and "the
171
+ * pricing page renders at 61%" is a question worth putting to a person.
172
+ */
173
+ const verify: false | { threshold: number; minFidelity: number } = body.verify
174
+ ? {
175
+ /*
176
+ * Two different numbers, and conflating them is what let a run scoring 8% report success.
177
+ * `threshold` is pixelmatch's per-pixel COLOUR tolerance and only moves the score;
178
+ * `minFidelity` is the bar the run's `passed` is measured against.
179
+ */
180
+ threshold: Math.min(
181
+ MAX_THRESHOLD,
182
+ Math.max(MIN_THRESHOLD, Number(body.verifyThreshold ?? 0.15) || 0.15),
183
+ ),
184
+ minFidelity: clamp(
185
+ Number(body.verifyMinFidelity ?? 0) || 0,
186
+ MIN_FIDELITY_FLOOR,
187
+ MIN_FIDELITY_CEILING,
188
+ ),
189
+ }
190
+ : false;
105
191
  const maxPages = clamp(body.maxPages ?? 20, 1, MAX_PAGES);
106
192
  const maxNodesPerPage = clamp(body.maxNodesPerPage ?? 5000, 100, 50_000);
193
+ const breakpoints = readBreakpointPolicy(body.breakpoints);
107
194
  const { apiKey, baseUrl } = getAiConfig(req);
108
195
  const toRoot = opts.toRoot ?? ((p: string) => p);
109
196
 
@@ -151,16 +238,44 @@ async function handleImportSite(req: Request, opts: ImportApiOptions): Promise<R
151
238
  maxPages,
152
239
  maxNodesPerPage,
153
240
  ai,
241
+ verify,
154
242
  signal: req.signal,
243
+ ...(breakpoints === undefined ? {} : { breakpoints }),
155
244
  ...(opts.chromePath === undefined ? {} : { chromePath: opts.chromePath }),
156
245
  },
157
- (e) => writeLine({ type: "progress", ...e }),
246
+ (e) => {
247
+ /* The seed event carries the root, and it is the ONE thing a caller cannot wait for the
248
+ terminal line to learn: it arrives seconds in, and `done` arrives minutes later. It
249
+ is re-emitted as its own line rather than left inside a progress message, because a
250
+ root parsed out of prose is a root that breaks when the prose is reworded. */
251
+ if (e.phase === "seed" && e.root) {
252
+ writeLine({ type: "ready", root: toRoot(e.root) });
253
+ }
254
+ const { root: _root, ...progress } = e;
255
+ writeLine({ type: "progress", ...progress });
256
+ },
158
257
  );
159
258
 
160
259
  const config = JSON.parse(
161
260
  await readFile(resolve(result.outDir, "project.json"), "utf8"),
162
261
  ) as Record<string, unknown>;
163
- writeLine({ type: "done", root: toRoot(result.outDir), config });
262
+ /*
263
+ * The result travels with the `done` line. It used to be discarded here: the pipeline had
264
+ * computed the page list, the file count, the warnings and (with verify) a per-page
265
+ * fidelity score, and the caller learned only that an import had happened. A caller that
266
+ * cannot see what a run found cannot report it, and cannot ask about it either.
267
+ */
268
+ writeLine({
269
+ type: "done",
270
+ root: toRoot(result.outDir),
271
+ config,
272
+ result: {
273
+ pages: result.pages,
274
+ fileCount: result.fileCount,
275
+ warnings: result.warnings,
276
+ ...(result.verify ? { verify: result.verify } : {}),
277
+ },
278
+ });
164
279
  } catch (error) {
165
280
  const message = error instanceof Error ? error.message : String(error);
166
281
  writeLine({ type: "error", error: message });