@jxsuite/server 2.2.6 → 3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/server",
3
- "version": "2.2.6",
3
+ "version": "3.0.0",
4
4
  "description": "Jx development server with live reload, server-side proxy, and studio integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,21 +38,21 @@
38
38
  "upgrade": "bunx npm-check-updates -u && bun install"
39
39
  },
40
40
  "dependencies": {
41
- "@jxsuite/collab": "^0.8.1",
42
- "@jxsuite/compiler": "^2.0.6",
43
- "@jxsuite/create": "^1.3.5",
44
- "@jxsuite/import": "^0.39.6",
45
- "@jxsuite/protocol": "^1.2.0",
46
- "@jxsuite/runtime": "^2.0.4",
47
- "@jxsuite/schema": "^1.8.0",
48
- "@jxsuite/starters": "^1.6.2",
41
+ "@jxsuite/collab": "^0.8.3",
42
+ "@jxsuite/compiler": "^2.0.8",
43
+ "@jxsuite/create": "^1.3.7",
44
+ "@jxsuite/import": "^0.39.8",
45
+ "@jxsuite/protocol": "^2.0.0",
46
+ "@jxsuite/runtime": "^2.1.0",
47
+ "@jxsuite/schema": "^1.9.0",
48
+ "@jxsuite/starters": "^1.6.4",
49
49
  "chokidar": "^5.0.0",
50
50
  "kysely": "^0.29.5",
51
51
  "zod": "^4.4.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@jxsuite/auth": "^0.5.3",
55
- "@jxsuite/connector": "^0.5.3",
56
- "@jxsuite/parser": "^1.5.4"
54
+ "@jxsuite/auth": "^0.5.5",
55
+ "@jxsuite/connector": "^0.5.5",
56
+ "@jxsuite/parser": "^1.5.6"
57
57
  }
58
58
  }
package/src/net-guard.ts CHANGED
@@ -307,14 +307,83 @@ export async function serveContained(absPath: string, root: string): Promise<Res
307
307
  return null;
308
308
  }
309
309
 
310
+ /**
311
+ * Extensions a BUILD publishes as static assets, so a page may reference one by site URL.
312
+ *
313
+ * Deliberately excludes `.json`, `.md` and every other document extension: `serveProjectFile`
314
+ * serves two URL spaces at once — the SITE's, and the project tree's, which is how the Studio
315
+ * canvas fetches a component `$ref` — and only the site's is what {@link BUILD_LANES} defines. A
316
+ * `/components/x-card.json` answered from the project root is Studio's own protocol working
317
+ * correctly; a `/hero.jpg` answered from the project root is a preview telling a lie.
318
+ */
319
+ const SITE_ASSET_EXTENSIONS = new Set([
320
+ ".apng",
321
+ ".avif",
322
+ ".bmp",
323
+ ".css",
324
+ ".gif",
325
+ ".ico",
326
+ ".jpeg",
327
+ ".jpg",
328
+ ".mp3",
329
+ ".mp4",
330
+ ".oga",
331
+ ".ogg",
332
+ ".ogv",
333
+ ".otf",
334
+ ".pdf",
335
+ ".png",
336
+ ".svg",
337
+ ".ttf",
338
+ ".txt",
339
+ ".wav",
340
+ ".webm",
341
+ ".webmanifest",
342
+ ".webp",
343
+ ".woff",
344
+ ".woff2",
345
+ ".xml",
346
+ ]);
347
+
348
+ /** Paths already warned about, so a page that loads one image fifty times says it once. */
349
+ const rootLaneWarned = new Set<string>();
350
+
351
+ /**
352
+ * Say once, per path, that a file resolves here and will not resolve on the built site.
353
+ *
354
+ * @param {string} decodedPath - The site URL that matched the compatibility lane
355
+ */
356
+ function warnRootLane(decodedPath: string): void {
357
+ const dot = decodedPath.lastIndexOf(".");
358
+ const ext = dot === -1 ? "" : decodedPath.slice(dot).toLowerCase();
359
+ if (!SITE_ASSET_EXTENSIONS.has(ext) || rootLaneWarned.has(decodedPath)) {
360
+ return;
361
+ }
362
+ rootLaneWarned.add(decodedPath);
363
+ console.warn(
364
+ `${decodedPath} resolves from the project root, which a BUILD does not do: it publishes ` +
365
+ `public/ at the site root and nothing else (site-architecture.md §9.3). This file loads in ` +
366
+ `the preview and 404s on the deployed site — move it into public/ to fix it. This ` +
367
+ `compatibility lane is going away.`,
368
+ );
369
+ }
370
+
310
371
  /**
311
372
  * Try to serve a project file at its natural URL: an extension asset mount, then
312
- * absolute-under-root, then root-relative, then public/. Each candidate goes through containedPath
313
- * (realpath). `decodedPath` is the once-decoded URL pathname (leading slash, forward slashes).
373
+ * absolute-under-root, then `public/`, then the project root. Each candidate goes through
374
+ * containedPath (realpath). `decodedPath` is the once-decoded URL pathname (leading slash, forward
375
+ * slashes).
314
376
  *
315
377
  * Mounts come first and are contained against their own directory rather than the project root:
316
378
  * they exist precisely to publish directories that may sit outside it (extensions.md §8.5).
317
379
  *
380
+ * **`public/` precedes the project root, which is the order a BUILD resolves in.** It did not: the
381
+ * root came first, so a file at `<root>/hero.jpg` loaded at `/hero.jpg` here and 404'd in
382
+ * production — a preview that lies in the one direction that matters, and one the spec never
383
+ * described (`site-architecture.md` §9.3 has always matched the compiler). The root lane survives
384
+ * as a COMPATIBILITY lane that warns, and is going away; it also still answers the project tree's
385
+ * own URL space, which is how the Studio canvas fetches a component `$ref`.
386
+ *
318
387
  * The project's BUILT OUTPUT is deliberately NOT here, nor anywhere else in an editing server's
319
388
  * chain: these paths mean the project's SOURCES, and a built page addresses the same paths meaning
320
389
  * its own output. The built site is served on its own origin instead — see
@@ -342,7 +411,8 @@ export async function serveProjectFile(
342
411
  ? decodedPath.slice(1)
343
412
  : decodedPath.replace(/^\/([A-Za-z]:)/, "$1");
344
413
 
345
- // 1. Absolute path that falls under the project root.
414
+ /* 1. Absolute path that falls under the project root. Not a site URL at all — it is how Studio
415
+ addresses a file it already holds an absolute path for, so it stays ahead of both lanes. */
346
416
  if (normalizeForCompare(fsPath).startsWith(normalizeForCompare(root))) {
347
417
  const res = await serveContained(fsPath, root);
348
418
  if (res) {
@@ -350,21 +420,27 @@ export async function serveProjectFile(
350
420
  }
351
421
  }
352
422
 
353
- // 2. Root-relative.
354
- const relRes = await serveContained(resolve(root, `.${decodedPath}`), root);
355
- if (relRes) {
356
- return relRes;
357
- }
358
-
359
- // 3. public/ subdirectory.
423
+ // 2. public/ — the site's own URL space, and the only one a build publishes.
360
424
  const pubRes = await serveContained(resolve(root, "public", `.${decodedPath}`), root);
361
425
  if (pubRes) {
362
426
  return pubRes;
363
427
  }
364
428
 
429
+ // 3. The project root — the project tree's URL space, and a compatibility lane for site URLs.
430
+ const relRes = await serveContained(resolve(root, `.${decodedPath}`), root);
431
+ if (relRes) {
432
+ warnRootLane(decodedPath);
433
+ return relRes;
434
+ }
435
+
365
436
  return null;
366
437
  }
367
438
 
439
+ /** Forget which paths have been warned about — for tests, which assert the warning fires. */
440
+ export function resetRootLaneWarnings(): void {
441
+ rootLaneWarned.clear();
442
+ }
443
+
368
444
  /** Result of decoding a URL pathname: the safe decoded/normalized path, or a rejection Response. */
369
445
  export type DecodedPath = { path: string; normPath: string } | { reject: Response };
370
446
 
package/src/studio-api.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
11
+ import { componentMetaFrom } from "@jxsuite/schema/component-meta";
11
12
  import { errorMessage } from "@jxsuite/schema/parse";
12
13
  import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
13
14
  import { existsSync, readFileSync, statSync, unlinkSync } from "node:fs";
@@ -93,38 +94,6 @@ interface ComponentDoc {
93
94
  [key: string]: unknown;
94
95
  }
95
96
 
96
- interface SlotDef {
97
- name: string;
98
- fallback?: unknown[];
99
- }
100
-
101
- /**
102
- * Collect slot definitions (name + fallback children) from a parsed component tree. Whitespace-only
103
- * names count as unnamed (""). Only static children arrays are walked.
104
- */
105
- function collectSlotDefs(node: unknown, out: SlotDef[] = []): SlotDef[] {
106
- if (!node || typeof node !== "object" || Array.isArray(node)) {
107
- return out;
108
- }
109
- const el = node as Record<string, unknown>;
110
- if (el.tagName === "slot") {
111
- const attrs = el.attributes as Record<string, unknown> | undefined;
112
- const rawName = attrs?.name;
113
- const name = typeof rawName === "string" ? rawName.trim() : "";
114
- const { children } = el;
115
- out.push({
116
- name,
117
- ...(Array.isArray(children) && children.length > 0 ? { fallback: children } : {}),
118
- });
119
- }
120
- if (Array.isArray(el.children)) {
121
- for (const c of el.children) {
122
- collectSlotDefs(c, out);
123
- }
124
- }
125
- return out;
126
- }
127
-
128
97
  // ─── Extension registry (per project root, invalidated on project.json change) ──
129
98
 
130
99
  const extensionRegistryCache = new Map<string, { mtime: number; registry: ExtensionRegistry }>();
@@ -792,42 +761,14 @@ export async function handleStudioApi(
792
761
  const source = await readFile(fp, "utf8");
793
762
  content = (await entry.call("parse", source)) as ComponentDoc;
794
763
  }
795
- if (content.tagName && content.tagName.includes("-")) {
796
- const slotDefs = collectSlotDefs(content);
797
- components.push({
798
- $id: content.$id || null,
799
- hasElements: Array.isArray(content.$elements) && content.$elements.length > 0,
800
- path: fwd(match),
801
- props: Object.entries(content.state || {})
802
- .filter(([, d]) => {
803
- if (d == null) {
804
- return false;
805
- }
806
- // Shorthand: "key": "value" or "key": 0 etc.
807
- if (typeof d !== "object") {
808
- return true;
809
- }
810
- // Full form: skip computed/handler/prototype entries
811
- const obj = d as Record<string, unknown>;
812
- return !obj.$prototype && !obj.$handler && !obj.$compute;
813
- })
814
- .map(([name, d]) => {
815
- if (typeof d !== "object" || d === null) {
816
- // Shorthand: infer type from value
817
- return { default: d, name, type: typeof d };
818
- }
819
- const obj = d as Record<string, unknown>;
820
- return {
821
- default: obj.default,
822
- format: obj.format,
823
- name,
824
- type: obj.type,
825
- };
826
- }),
827
- ...(slotDefs.length > 0 ? { slots: slotDefs } : {}),
828
- source: "jx",
829
- tagName: content.tagName,
830
- });
764
+ /* The metadata rules live in @jxsuite/schema/component-meta because THREE backends need
765
+ the same answer — this one, the desktop session, and now the cloud adapter, which can
766
+ discover JSON components precisely because deriving them executes nothing. Two copies
767
+ had already drifted apart in wording; a third would have made "which state entries are
768
+ props" a question with three answers. */
769
+ const meta = componentMetaFrom(content, fwd(match));
770
+ if (meta) {
771
+ components.push({ ...meta, source: "jx" });
831
772
  }
832
773
  } catch {} // Skip non-JSON or parse errors
833
774
  }