camox 0.51.0 → 0.52.2

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.
Files changed (91) hide show
  1. package/dist/core/createApp.d.ts +1 -0
  2. package/dist/core/createApp.js +1 -0
  3. package/dist/core/editing/contentEdits.js +78 -0
  4. package/dist/core/editing/createEditableBlock.js +277 -311
  5. package/dist/core/lib/contentType.js +2 -2
  6. package/dist/core/recordItem.js +3 -3
  7. package/dist/features/canvas/CamoxCanvas.js +15 -15
  8. package/dist/features/canvas/CanvasLoadingScreen.js +15 -11
  9. package/dist/features/canvas/CanvasPageFrame.js +81 -87
  10. package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
  11. package/dist/features/canvas/DerivedPagePathInput.js +12 -12
  12. package/dist/features/canvas/canvasOverlayGeometry.js +1 -1
  13. package/dist/features/canvas/canvasStore.js +2 -2
  14. package/dist/features/canvas/sidebar/CuratedPageInspector.js +1 -1
  15. package/dist/features/canvas/sidebar/LayoutPageBlocks.js +23 -31
  16. package/dist/features/canvas/useCanvasCamera.js +1 -1
  17. package/dist/features/canvas/useDiscoveredPreviewPaths.js +16 -9
  18. package/dist/features/page/CuratedPageContent.js +1 -1
  19. package/dist/features/preview/CamoxPreview.d.ts +8 -4
  20. package/dist/features/preview/CamoxPreview.js +257 -442
  21. package/dist/features/preview/EditablePageContent.js +244 -2
  22. package/dist/features/preview/EditablePageExperience.js +2 -4
  23. package/dist/features/preview/components/AddBlockSheet.js +55 -15
  24. package/dist/features/preview/components/BlockActionsPopover.js +12 -16
  25. package/dist/features/preview/components/BlockPeek.js +66 -0
  26. package/dist/features/preview/components/BlockThumbnail.js +134 -119
  27. package/dist/features/preview/components/CanvasPreview.js +1 -1
  28. package/dist/features/preview/components/CreatePageModal.js +150 -156
  29. package/dist/features/preview/components/Frame.js +27 -4
  30. package/dist/features/preview/components/MobilePreviewDrawer.js +1 -1
  31. package/dist/features/preview/components/PageInfo.js +161 -126
  32. package/dist/features/preview/components/PagePicker.js +2 -2
  33. package/dist/features/preview/components/PageTree.js +174 -190
  34. package/dist/features/preview/components/PreviewPanel.js +3 -3
  35. package/dist/features/preview/components/ReferenceFieldEditor.js +4 -4
  36. package/dist/features/preview/components/ReferenceListFieldEditor.js +4 -4
  37. package/dist/features/preview/components/RemovedBlockPlaceholder.js +37 -70
  38. package/dist/features/preview/components/RepeatableItemsList.js +1 -1
  39. package/dist/features/preview/components/SelectionEditor.js +248 -256
  40. package/dist/features/preview/components/StudioEditingFallback.js +22 -26
  41. package/dist/features/preview/components/useFieldWriter.js +19 -19
  42. package/dist/features/preview/components/useRecordView.js +105 -147
  43. package/dist/features/preview/components/useRepeatableItemActions.js +0 -2
  44. package/dist/features/preview/pageRecord.js +30 -24
  45. package/dist/features/preview/previewStore.js +2 -0
  46. package/dist/features/preview/studioNotice.js +20 -0
  47. package/dist/features/provider/AuthenticatedCamoxProvider.js +46 -30
  48. package/dist/features/renderer/RendererApp.js +542 -0
  49. package/dist/features/renderer/RendererFrames.js +450 -0
  50. package/dist/features/renderer/definitionAnswers.js +26 -0
  51. package/dist/features/renderer/protocol.js +35 -0
  52. package/dist/features/renderer/rendererHost.js +249 -0
  53. package/dist/features/renderer/studioConnection.js +186 -0
  54. package/dist/features/routes/pageRuntime.js +7 -2
  55. package/dist/features/runtime/RuntimeChrome.js +2 -5
  56. package/dist/features/runtime/assetPrefix.js +14 -0
  57. package/dist/features/runtime/breadcrumbs.js +1 -1
  58. package/dist/features/runtime/editingFallback.js +4 -8
  59. package/dist/features/runtime/livePages.js +1 -1
  60. package/dist/features/runtime/pageApp.js +57 -83
  61. package/dist/features/runtime/pageClient.js +1 -1
  62. package/dist/features/runtime/pageNavigation.js +3 -2
  63. package/dist/features/runtime/publicCache.js +15 -3
  64. package/dist/features/runtime/publishingDraft.js +23 -0
  65. package/dist/features/runtime/rendererClient.d.ts +8 -0
  66. package/dist/features/runtime/rendererClient.js +77 -0
  67. package/dist/features/runtime/runtime.d.ts +9 -0
  68. package/dist/features/runtime/runtime.js +36 -17
  69. package/dist/features/runtime/runtimeRoutes.d.ts +2 -0
  70. package/dist/features/runtime/runtimeRoutes.js +2 -0
  71. package/dist/features/runtime/worker.d.ts +10 -1
  72. package/dist/features/runtime/worker.js +20 -7
  73. package/dist/features/studio/FollowAgentProvider.js +10 -10
  74. package/dist/features/studio/components/ProjectMenu.js +1 -1
  75. package/dist/features/studio/components/ReviewDraftSheet.js +242 -138
  76. package/dist/features/studio/components/ThreadMessages.js +100 -65
  77. package/dist/features/studio/components/UserButton.js +1 -1
  78. package/dist/features/vite/definitionsSync.js +1 -1
  79. package/dist/features/vite/hostedChecks.js +127 -0
  80. package/dist/features/vite/runtimeDev.js +26 -0
  81. package/dist/features/vite/vite.js +33 -5
  82. package/dist/hooks/use-page-destinations.js +10 -10
  83. package/dist/lib/revisions.js +20 -0
  84. package/dist/lib/studio-definitions.js +160 -0
  85. package/dist/lib/studio-reads.js +65 -21
  86. package/dist/lib/use-project-room.js +16 -10
  87. package/dist/package.js +1 -1
  88. package/dist/studio.css +1 -1
  89. package/package.json +8 -4
  90. package/skills/camox/SKILL.md +4 -0
  91. package/skills/camox/references/artifacts.md +1 -1
@@ -9,12 +9,13 @@ import { MessageScroller, MessageScrollerButton, MessageScrollerContent, Message
9
9
  //#region src/features/studio/components/ThreadMessages.tsx
10
10
  /**
11
11
  * A thread's messages, and "Working…" while a run is going or a message is still `sending`: on
12
- * its way to the thread, which hasn't accepted it yet. A draft's thread is shared, so it shows
12
+ * its way to the thread, which hasn't accepted it yet. While the end of a turn builds the
13
+ * agent's code, or the agent fixes a build error, it says so instead. A draft's thread is shared, so it shows
13
14
  * senders' names too (`showSenders`). A fresh Live thread links to the draft the editor's
14
15
  * previous one continued in (`continuedIn`).
15
16
  */
16
17
  function ThreadMessages(t0) {
17
- const $ = c(25);
18
+ const $ = c(28);
18
19
  const { view, sending: t1, showSenders: t2, continuedIn } = t0;
19
20
  const sending = t1 === void 0 ? false : t1;
20
21
  const showSenders = t2 === void 0 ? false : t2;
@@ -53,9 +54,9 @@ function ThreadMessages(t0) {
53
54
  $[6] = t3;
54
55
  } else t3 = $[6];
55
56
  let t4;
56
- if ($[7] !== showSenders || $[8] !== view.messages || $[9] !== working) {
57
+ if ($[7] !== showSenders || $[8] !== view.messages || $[9] !== view.status || $[10] !== working) {
57
58
  let t5;
58
- if ($[11] !== showSenders || $[12] !== view.messages.length || $[13] !== working) {
59
+ if ($[12] !== showSenders || $[13] !== view.messages.length || $[14] !== view.status || $[15] !== working) {
59
60
  t5 = (message, index) => /* @__PURE__ */ jsxs(MessageScrollerItem, {
60
61
  messageId: message.id,
61
62
  scrollAnchor: message.from === "editor",
@@ -63,28 +64,31 @@ function ThreadMessages(t0) {
63
64
  children: [/* @__PURE__ */ jsx(ThreadMessageRow, {
64
65
  message,
65
66
  showSender: showSenders
66
- }), working && index === view.messages.length - 1 && /* @__PURE__ */ jsx(Working, {})]
67
+ }), working && index === view.messages.length - 1 && /* @__PURE__ */ jsx(Working, { status: view.status })]
67
68
  }, message.id);
68
- $[11] = showSenders;
69
- $[12] = view.messages.length;
70
- $[13] = working;
71
- $[14] = t5;
72
- } else t5 = $[14];
69
+ $[12] = showSenders;
70
+ $[13] = view.messages.length;
71
+ $[14] = view.status;
72
+ $[15] = working;
73
+ $[16] = t5;
74
+ } else t5 = $[16];
73
75
  t4 = view.messages.map(t5);
74
76
  $[7] = showSenders;
75
77
  $[8] = view.messages;
76
- $[9] = working;
77
- $[10] = t4;
78
- } else t4 = $[10];
78
+ $[9] = view.status;
79
+ $[10] = working;
80
+ $[11] = t4;
81
+ } else t4 = $[11];
79
82
  let t5;
80
- if ($[15] !== view.messages.length || $[16] !== working) {
81
- t5 = working && view.messages.length === 0 && /* @__PURE__ */ jsx(Working, {});
82
- $[15] = view.messages.length;
83
- $[16] = working;
84
- $[17] = t5;
85
- } else t5 = $[17];
83
+ if ($[17] !== view.messages.length || $[18] !== view.status || $[19] !== working) {
84
+ t5 = working && view.messages.length === 0 && /* @__PURE__ */ jsx(Working, { status: view.status });
85
+ $[17] = view.messages.length;
86
+ $[18] = view.status;
87
+ $[19] = working;
88
+ $[20] = t5;
89
+ } else t5 = $[20];
86
90
  let t6;
87
- if ($[18] !== t3 || $[19] !== t4 || $[20] !== t5) {
91
+ if ($[21] !== t3 || $[22] !== t4 || $[23] !== t5) {
88
92
  t6 = /* @__PURE__ */ jsx(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs(MessageScrollerContent, {
89
93
  className: "flex flex-col gap-4 px-2 py-3",
90
94
  children: [
@@ -93,46 +97,53 @@ function ThreadMessages(t0) {
93
97
  t5
94
98
  ]
95
99
  }) });
96
- $[18] = t3;
97
- $[19] = t4;
98
- $[20] = t5;
99
- $[21] = t6;
100
- } else t6 = $[21];
100
+ $[21] = t3;
101
+ $[22] = t4;
102
+ $[23] = t5;
103
+ $[24] = t6;
104
+ } else t6 = $[24];
101
105
  let t7;
102
- if ($[22] === Symbol.for("react.memo_cache_sentinel")) {
106
+ if ($[25] === Symbol.for("react.memo_cache_sentinel")) {
103
107
  t7 = /* @__PURE__ */ jsx(MessageScrollerButton, {});
104
- $[22] = t7;
105
- } else t7 = $[22];
108
+ $[25] = t7;
109
+ } else t7 = $[25];
106
110
  let t8;
107
- if ($[23] !== t6) {
111
+ if ($[26] !== t6) {
108
112
  t8 = /* @__PURE__ */ jsx(MessageScrollerProvider, { children: /* @__PURE__ */ jsxs(MessageScroller, {
109
113
  className: "min-h-0 flex-1",
110
114
  children: [t6, t7]
111
115
  }) });
112
- $[23] = t6;
113
- $[24] = t8;
114
- } else t8 = $[24];
116
+ $[26] = t6;
117
+ $[27] = t8;
118
+ } else t8 = $[27];
115
119
  return t8;
116
120
  }
117
121
  /**
118
122
  * Shown in the last message, not after it: the scroller only brings a message the editor just
119
123
  * sent into view when it's appended last, and it arrives while the agent is already working.
120
124
  */
121
- function Working() {
122
- const $ = c(1);
123
- let t0;
124
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
125
- t0 = /* @__PURE__ */ jsx("p", {
125
+ function Working(t0) {
126
+ const $ = c(2);
127
+ const { status } = t0;
128
+ const t1 = status ? STATUS_LINES[status] : "Working…";
129
+ let t2;
130
+ if ($[0] !== t1) {
131
+ t2 = /* @__PURE__ */ jsx("p", {
126
132
  role: "status",
127
133
  className: "shimmer text-muted-foreground text-sm",
128
- children: "Working…"
134
+ children: t1
129
135
  });
130
- $[0] = t0;
131
- } else t0 = $[0];
132
- return t0;
136
+ $[0] = t1;
137
+ $[1] = t2;
138
+ } else t2 = $[1];
139
+ return t2;
133
140
  }
141
+ const STATUS_LINES = {
142
+ building: "Building…",
143
+ fixing: "Fixing a build error…"
144
+ };
134
145
  function ThreadMessageRow(t0) {
135
- const $ = c(17);
146
+ const $ = c(20);
136
147
  const { message, showSender } = t0;
137
148
  if (message.from === "stopped" || message.from === "failed") {
138
149
  const t1 = message.from === "stopped" ? `Stopped by ${message.sender.name}` : "Camox couldn’t finish this. Send your message again to retry.";
@@ -156,24 +167,48 @@ function ThreadMessageRow(t0) {
156
167
  } else t1 = $[3];
157
168
  return t1;
158
169
  }
170
+ if (message.from === "camox") {
171
+ let t1;
172
+ if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
173
+ t1 = /* @__PURE__ */ jsx(MessageHeader, {
174
+ className: "text-muted-foreground text-xs",
175
+ children: "Camox"
176
+ });
177
+ $[4] = t1;
178
+ } else t1 = $[4];
179
+ let t2;
180
+ if ($[5] !== message.text) {
181
+ t2 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsxs(MessageContent, { children: [t1, /* @__PURE__ */ jsx(Bubble, {
182
+ variant: "muted",
183
+ className: "max-w-full",
184
+ children: /* @__PURE__ */ jsx(BubbleContent, {
185
+ className: "whitespace-pre-wrap",
186
+ children: message.text
187
+ })
188
+ })] }) });
189
+ $[5] = message.text;
190
+ $[6] = t2;
191
+ } else t2 = $[6];
192
+ return t2;
193
+ }
159
194
  let t1;
160
- if ($[4] !== message.sender) {
195
+ if ($[7] !== message.sender) {
161
196
  t1 = /* @__PURE__ */ jsx(MessageAvatar, { children: /* @__PURE__ */ jsx(SenderAvatar, { sender: message.sender }) });
162
- $[4] = message.sender;
163
- $[5] = t1;
164
- } else t1 = $[5];
197
+ $[7] = message.sender;
198
+ $[8] = t1;
199
+ } else t1 = $[8];
165
200
  let t2;
166
- if ($[6] !== message.sender || $[7] !== showSender) {
201
+ if ($[9] !== message.sender || $[10] !== showSender) {
167
202
  t2 = showSender && /* @__PURE__ */ jsx(MessageHeader, {
168
203
  className: "text-muted-foreground text-xs",
169
204
  children: message.sender.name
170
205
  });
171
- $[6] = message.sender;
172
- $[7] = showSender;
173
- $[8] = t2;
174
- } else t2 = $[8];
206
+ $[9] = message.sender;
207
+ $[10] = showSender;
208
+ $[11] = t2;
209
+ } else t2 = $[11];
175
210
  let t3;
176
- if ($[9] !== message.text) {
211
+ if ($[12] !== message.text) {
177
212
  t3 = /* @__PURE__ */ jsx(Bubble, {
178
213
  align: "end",
179
214
  variant: "muted",
@@ -183,26 +218,26 @@ function ThreadMessageRow(t0) {
183
218
  children: message.text
184
219
  })
185
220
  });
186
- $[9] = message.text;
187
- $[10] = t3;
188
- } else t3 = $[10];
221
+ $[12] = message.text;
222
+ $[13] = t3;
223
+ } else t3 = $[13];
189
224
  let t4;
190
- if ($[11] !== t2 || $[12] !== t3) {
225
+ if ($[14] !== t2 || $[15] !== t3) {
191
226
  t4 = /* @__PURE__ */ jsxs(MessageContent, { children: [t2, t3] });
192
- $[11] = t2;
193
- $[12] = t3;
194
- $[13] = t4;
195
- } else t4 = $[13];
227
+ $[14] = t2;
228
+ $[15] = t3;
229
+ $[16] = t4;
230
+ } else t4 = $[16];
196
231
  let t5;
197
- if ($[14] !== t1 || $[15] !== t4) {
232
+ if ($[17] !== t1 || $[18] !== t4) {
198
233
  t5 = /* @__PURE__ */ jsxs(Message, {
199
234
  align: "end",
200
235
  children: [t1, t4]
201
236
  });
202
- $[14] = t1;
203
- $[15] = t4;
204
- $[16] = t5;
205
- } else t5 = $[16];
237
+ $[17] = t1;
238
+ $[18] = t4;
239
+ $[19] = t5;
240
+ } else t5 = $[19];
206
241
  return t5;
207
242
  }
208
243
  function ContinuedIn(t0) {
@@ -2,9 +2,9 @@ import { useAuthContext, useAuthState } from "../../../lib/auth.js";
2
2
  import { useNavigate } from "../../navigation/navigation.js";
3
3
  import { useLocalePath, useLocalizedPath } from "../../../lib/view-locale.js";
4
4
  import { PlatformLabel } from "../../../components/PlatformLabel.js";
5
+ import { previewStore, selectIsCanvasShown } from "../../preview/previewStore.js";
5
6
  import { STUDIO_BASE_PATH } from "../routes.js";
6
7
  import { studioStore } from "../studioStore.js";
7
- import { previewStore, selectIsCanvasShown } from "../../preview/previewStore.js";
8
8
  import { useApplyTheme } from "../useTheme.js";
9
9
  import { c } from "react/compiler-runtime";
10
10
  import { useSelector } from "@xstate/store-react";
@@ -207,4 +207,4 @@ async function syncDefinitions(server, options) {
207
207
  }
208
208
 
209
209
  //#endregion
210
- export { VIRTUAL_CAMOX_APP, syncDefinitions, syncDefinitionsToApi };
210
+ export { VIRTUAL_CAMOX_APP, loadDefinitionSet, syncDefinitions, syncDefinitionsToApi };
@@ -0,0 +1,127 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Visitor, parseSync } from "vite-plus";
5
+
6
+ //#region src/features/vite/hostedChecks.ts
7
+ /**
8
+ * Set by Camox's revision and release sandboxes: a hosted project's build checks the code an
9
+ * agent may have written (ADR 0021). `apps/landing` and dev mode never set it.
10
+ */
11
+ const HOSTED_BUILD_ENV = "CAMOX_INTERNAL_HOSTED";
12
+ const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
13
+ /** `camox`'s public entry points, as `camox/<path>`: its exports, less its internal ones. */
14
+ function camoxEntryPoints() {
15
+ const { exports } = JSON.parse(readFileSync(join(sdkRoot, "package.json"), "utf8"));
16
+ return new Set(Object.keys(exports).filter((path) => !path.startsWith("./_internal/")).map((path) => `camox${path.slice(1)}`));
17
+ }
18
+ function readHostedProject(root) {
19
+ const { dependencies = {} } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
20
+ return {
21
+ root,
22
+ src: join(root, "src"),
23
+ dependencies: new Set(Object.keys(dependencies)),
24
+ camoxEntryPoints: camoxEntryPoints()
25
+ };
26
+ }
27
+ const withoutQuery = (path) => path.replace(/[?#].*$/, "");
28
+ const isInside = (dir, path) => path.startsWith(dir + sep);
29
+ const displayPath = (project, file) => relative(project.root, file).split(sep).join("/");
30
+ /** The project's own file an importer names, when it is one of `src/`'s: the code it checks. */
31
+ function sourceFile(project, importer) {
32
+ if (!importer || importer.startsWith("\0")) return;
33
+ const file = withoutQuery(importer);
34
+ return isAbsolute(file) && isInside(project.src, file) ? file : void 0;
35
+ }
36
+ function isAllowedImport(project, specifier, importer) {
37
+ if (specifier.startsWith("\0")) return true;
38
+ const path = withoutQuery(specifier);
39
+ let file;
40
+ if (path.startsWith("./") || path.startsWith("../")) file = resolve(dirname(importer), path);
41
+ else if (path.startsWith("@/")) file = resolve(project.src, path.slice(2));
42
+ else if (path.startsWith("/")) file = isInside(project.root, path) ? resolve(path) : resolve(project.root, `.${path}`);
43
+ if (file !== void 0) return isInside(project.src, file);
44
+ if (path === "camox" || path.startsWith("camox/")) return project.camoxEntryPoints.has(path);
45
+ const [scopeOrName, name] = path.split("/");
46
+ const packageName = scopeOrName.startsWith("@") ? `${scopeOrName}/${name}` : scopeOrName;
47
+ return project.dependencies.has(packageName);
48
+ }
49
+ /**
50
+ * Why a file of `src/` can't import `specifier`, or nothing when it may: files of `src/`,
51
+ * `camox`'s public entry points and `package.json`'s `dependencies`.
52
+ */
53
+ function forbiddenImport(project, specifier, importer) {
54
+ const file = sourceFile(project, importer);
55
+ if (!file || isAllowedImport(project, specifier, file)) return;
56
+ return `${displayPath(project, file)} imports "${specifier}", which a hosted project can't import. It can import files in src/, camox's public entry points and the packages package.json lists in dependencies.`;
57
+ }
58
+ /** The modules the registry loads blocks from, as its globs name them. */
59
+ const BLOCK_MODULE = /^src\/(?:camox\/)?blocks\/([^/]+)\.tsx?$/;
60
+ /** A string an expression spells out, without running it. */
61
+ function literalString(node) {
62
+ if (node?.type === "Literal" && typeof node.value === "string") return node.value;
63
+ if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value.cooked ?? void 0;
64
+ }
65
+ /**
66
+ * Whether `callee` calls `createBlock`: by its name, the names a module imports it as
67
+ * (`import { createBlock as defineBlock }`), or as a namespace's member.
68
+ */
69
+ function isCreateBlock(callee, names) {
70
+ if (callee.type === "Identifier") return names.has(callee.name);
71
+ return callee.type === "MemberExpression" && callee.property.type === "Identifier" && callee.property.name === "createBlock";
72
+ }
73
+ /** The local names a module calls `createBlock` by: its own name, and its imports' aliases. */
74
+ function createBlockNames(program) {
75
+ const names = /* @__PURE__ */ new Set(["createBlock"]);
76
+ for (const statement of program.body) {
77
+ if (statement.type !== "ImportDeclaration") continue;
78
+ for (const specifier of statement.specifiers) {
79
+ if (specifier.type !== "ImportSpecifier") continue;
80
+ const imported = specifier.imported;
81
+ if ((imported.type === "Identifier" ? imported.name : imported.value) === "createBlock") names.add(specifier.local.name);
82
+ }
83
+ }
84
+ return names;
85
+ }
86
+ /** The `id` a block definition's object literal spells out. */
87
+ function definitionId(definition) {
88
+ if (definition?.type !== "ObjectExpression") return;
89
+ for (const property of definition.properties) {
90
+ if (property.type !== "Property" || property.computed) continue;
91
+ const key = property.key;
92
+ if (key.type === "Identifier" && key.name === "id" || key.type === "Literal" && key.value === "id") return property.value;
93
+ }
94
+ }
95
+ /**
96
+ * Why a block's module breaks the registry, or nothing when it doesn't. The browser finds a block
97
+ * by its file name, so a block's file must be named after its id: the build reads the id from the
98
+ * `createBlock()` call, and needs it written as a string.
99
+ */
100
+ function misnamedBlock(project, moduleId, code) {
101
+ const file = sourceFile(project, moduleId);
102
+ if (!file) return;
103
+ const path = displayPath(project, file);
104
+ const fileName = BLOCK_MODULE.exec(path)?.[1];
105
+ if (!fileName) return;
106
+ const { program, errors } = parseSync(file, code);
107
+ if (errors.length > 0) return;
108
+ let problem;
109
+ const names = createBlockNames(program);
110
+ new Visitor({ CallExpression(call) {
111
+ if (problem || !isCreateBlock(call.callee, names)) return;
112
+ const id = literalString(definitionId(call.arguments[0]));
113
+ const at = definitionId(call.arguments[0])?.start ?? call.start;
114
+ if (id === void 0) problem = {
115
+ message: `${path} calls createBlock() without its id as a string literal, which the build needs to check that the file is named after it: write \`id: "${fileName}"\`.`,
116
+ at
117
+ };
118
+ else if (id !== fileName) problem = {
119
+ message: `${path} defines the block "${id}", but its file is named "${fileName}": a block's file is named after its id. Rename the file to ${id}${file.slice(file.lastIndexOf("."))} or the id to "${fileName}".`,
120
+ at
121
+ };
122
+ } }).visit(program);
123
+ return problem;
124
+ }
125
+
126
+ //#endregion
127
+ export { HOSTED_BUILD_ENV, forbiddenImport, misnamedBlock, readHostedProject };
@@ -22,6 +22,10 @@ const VIRTUAL_STUDIO_CLIENT = "virtual:camox/studio-client";
22
22
  const RESOLVED_VIRTUAL_STUDIO_CLIENT = "\0" + VIRTUAL_STUDIO_CLIENT;
23
23
  const VIRTUAL_STUDIO_CLIENT_URL = "virtual:camox/studio-client-url";
24
24
  const RESOLVED_VIRTUAL_STUDIO_CLIENT_URL = "\0" + VIRTUAL_STUDIO_CLIENT_URL;
25
+ const VIRTUAL_RENDERER_CLIENT = "virtual:camox/renderer-client";
26
+ const RESOLVED_VIRTUAL_RENDERER_CLIENT = "\0" + VIRTUAL_RENDERER_CLIENT;
27
+ const VIRTUAL_RENDERER_CLIENT_URL = "virtual:camox/renderer-client-url";
28
+ const RESOLVED_VIRTUAL_RENDERER_CLIENT_URL = "\0" + VIRTUAL_RENDERER_CLIENT_URL;
25
29
  const VIRTUAL_BLOCK_CHUNKS = "virtual:camox/block-chunks";
26
30
  const RESOLVED_VIRTUAL_BLOCK_CHUNKS = "\0" + VIRTUAL_BLOCK_CHUNKS;
27
31
  const VIRTUAL_APP_STYLESHEET_URL = "virtual:camox/app-stylesheet-url";
@@ -64,6 +68,7 @@ function resolveCamoxSourceImport(id) {
64
68
  "camox/_internal/pageServer": resolve(sdkRoot, "src/features/runtime/pageServer.tsx"),
65
69
  "camox/_internal/pageClient": resolve(sdkRoot, "src/features/runtime/pageClient.tsx"),
66
70
  "camox/_internal/studioClient": resolve(sdkRoot, "src/features/runtime/studioClient.tsx"),
71
+ "camox/_internal/rendererClient": resolve(sdkRoot, "src/features/runtime/rendererClient.tsx"),
67
72
  "camox/_internal/studioServer": resolve(sdkRoot, "src/features/runtime/studioServer.tsx"),
68
73
  "camox/_internal/runtime": resolve(sdkRoot, "src/features/runtime/runtime.ts"),
69
74
  "camox/worker": resolve(sdkRoot, "src/features/runtime/worker.ts")
@@ -167,6 +172,13 @@ import { camoxApp } from "virtual:camox/app";
167
172
  hydrateStudioWithApp(camoxApp);
168
173
  `;
169
174
  }
175
+ function generateVirtualRendererClient() {
176
+ return `import { startRendererWithApp } from "camox/_internal/rendererClient";
177
+ import { camoxApp } from "virtual:camox/app";
178
+
179
+ startRendererWithApp(camoxApp);
180
+ `;
181
+ }
170
182
  function wrapViteDevId(id) {
171
183
  return `/@id/${id.replace("\0", "__x00__")}`;
172
184
  }
@@ -176,6 +188,9 @@ function generateVirtualPageClientUrl() {
176
188
  function generateVirtualStudioClientUrl() {
177
189
  return `export default ${JSON.stringify(wrapViteDevId(RESOLVED_VIRTUAL_STUDIO_CLIENT))};`;
178
190
  }
191
+ function generateVirtualRendererClientUrl() {
192
+ return `export default ${JSON.stringify(wrapViteDevId(RESOLVED_VIRTUAL_RENDERER_CLIENT))};`;
193
+ }
179
194
  function generateVirtualAppStylesheetUrl() {
180
195
  return `import stylesheetUrl from "/src/styles.css?url";
181
196
 
@@ -194,6 +209,7 @@ import appStylesheetUrl from "virtual:camox/app-stylesheet-url";
194
209
  import studioStylesheetUrl from "virtual:camox-studio-css";
195
210
  import pageClientEntryUrl from "virtual:camox/page-client-url";
196
211
  import studioClientEntryUrl from "virtual:camox/studio-client-url";
212
+ import rendererClientEntryUrl from "virtual:camox/renderer-client-url";
197
213
  import { renderPage } from "virtual:camox/page-server";
198
214
  import { renderStudio } from "virtual:camox/studio-server";
199
215
  import devHtml from "virtual:camox/dev-html";
@@ -207,6 +223,7 @@ const options = {
207
223
  blockPreloads: { modulePaths: blockModulePaths, chunks: blockChunks },
208
224
  pageClientEntryUrl,
209
225
  studioClientEntryUrl,
226
+ rendererClientEntryUrl,
210
227
  developmentDraft: __CAMOX_DEVELOPMENT_DRAFT__,
211
228
  getCamoxApp: async () => camoxApp,
212
229
  getDocument: async () => camoxDocument,
@@ -231,6 +248,11 @@ export async function handleCamoxRequest(request) {
231
248
  export function isPublicRequest(request) {
232
249
  return isPublicRuntimeRequest(request, options);
233
250
  }
251
+
252
+ // JSON, as \`definitions.sync\` receives it: what isn't JSON never reaches the API.
253
+ export async function readDefinitionSet() {
254
+ return JSON.parse(JSON.stringify(await camoxApp.getDefinitionSet()));
255
+ }
234
256
  `;
235
257
  }
236
258
  function resolveRuntimeDevId(id, importer) {
@@ -247,6 +269,8 @@ function resolveRuntimeDevId(id, importer) {
247
269
  if (id === VIRTUAL_PAGE_CLIENT_URL) return RESOLVED_VIRTUAL_PAGE_CLIENT_URL;
248
270
  if (id === VIRTUAL_STUDIO_CLIENT) return RESOLVED_VIRTUAL_STUDIO_CLIENT;
249
271
  if (id === VIRTUAL_STUDIO_CLIENT_URL) return RESOLVED_VIRTUAL_STUDIO_CLIENT_URL;
272
+ if (id === VIRTUAL_RENDERER_CLIENT) return RESOLVED_VIRTUAL_RENDERER_CLIENT;
273
+ if (id === VIRTUAL_RENDERER_CLIENT_URL) return RESOLVED_VIRTUAL_RENDERER_CLIENT_URL;
250
274
  if (id === VIRTUAL_APP_STYLESHEET_URL) return RESOLVED_VIRTUAL_APP_STYLESHEET_URL;
251
275
  if (id === VIRTUAL_BLOCK_CHUNKS) return RESOLVED_VIRTUAL_BLOCK_CHUNKS;
252
276
  if (id === VIRTUAL_STUDIO_CSS) return RESOLVED_VIRTUAL_STUDIO_CSS;
@@ -264,6 +288,8 @@ function loadRuntimeDevModule(id, options = {}) {
264
288
  if (id === RESOLVED_VIRTUAL_PAGE_CLIENT_URL) return generateVirtualPageClientUrl();
265
289
  if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT) return generateVirtualStudioClient();
266
290
  if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT_URL) return generateVirtualStudioClientUrl();
291
+ if (id === RESOLVED_VIRTUAL_RENDERER_CLIENT) return generateVirtualRendererClient();
292
+ if (id === RESOLVED_VIRTUAL_RENDERER_CLIENT_URL) return generateVirtualRendererClientUrl();
267
293
  if (id === RESOLVED_VIRTUAL_APP_STYLESHEET_URL) return generateVirtualAppStylesheetUrl();
268
294
  if (id === RESOLVED_VIRTUAL_STUDIO_CSS) {
269
295
  const cssUrl = `/@fs/${resolve(sdkRoot, "dist/studio.css").replaceAll("\\", "/").replace(/^\/+/, "")}`;
@@ -1,4 +1,5 @@
1
1
  import { createServerApiClient } from "../../lib/api-client-server.js";
2
+ import { ASSET_PREFIX_GLOBAL } from "../runtime/assetPrefix.js";
2
3
  import { transformArtifactRuntime } from "./artifactRuntimeMinification.js";
3
4
  import { readAuthTokenForUrl } from "./auth.js";
4
5
  import { watchNewBlockFiles } from "./blockBoilerplate.js";
@@ -6,6 +7,7 @@ import { collectBlockChunks } from "./blockChunks.js";
6
7
  import { withoutExpectedCamoxLogs } from "./bundlerLogs.js";
7
8
  import { installDevAuthenticationMiddleware } from "./devAuthentication.js";
8
9
  import { installDevSignInLink } from "./devSignIn.js";
10
+ import { HOSTED_BUILD_ENV, forbiddenImport, misnamedBlock, readHostedProject } from "./hostedChecks.js";
9
11
  import { generateIconTypes } from "./iconGeneration.js";
10
12
  import { generateLayoutFiles, watchLayoutFiles } from "./layoutGeneration.js";
11
13
  import { publicFilesInHashedDirs, readOwnHeaders, staticAssetHeaders, unhashedEntries } from "./staticAssetHeaders.js";
@@ -18,7 +20,7 @@ import { projectIdSchema } from "@camox/api-contract/ids";
18
20
  import { DEVELOPMENT_DRAFT } from "@camox/api-contract/drafts";
19
21
  import { createHash, randomUUID } from "node:crypto";
20
22
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
21
- import { dirname, join, resolve } from "node:path";
23
+ import { dirname, join, posix, resolve } from "node:path";
22
24
  import { fileURLToPath } from "node:url";
23
25
  import { createServer } from "vite-plus";
24
26
 
@@ -34,7 +36,11 @@ const VIRTUAL_PAGE_CLIENT = "virtual:camox/page-client";
34
36
  const RESOLVED_VIRTUAL_PAGE_CLIENT_URL = "\0virtual:camox/page-client-url";
35
37
  const VIRTUAL_STUDIO_CLIENT = "virtual:camox/studio-client";
36
38
  const RESOLVED_VIRTUAL_STUDIO_CLIENT_URL = "\0virtual:camox/studio-client-url";
39
+ const VIRTUAL_RENDERER_CLIENT = "virtual:camox/renderer-client";
40
+ const RESOLVED_VIRTUAL_RENDERER_CLIENT_URL = "\0virtual:camox/renderer-client-url";
37
41
  const RESOLVED_VIRTUAL_BLOCK_CHUNKS = "\0virtual:camox/block-chunks";
42
+ /** A built file's URL in the server bundle: under the asset prefix, read when the bundle loads. */
43
+ const serverAssetUrl = (file) => `(globalThis.${ASSET_PREFIX_GLOBAL} ?? "/") + ${JSON.stringify(file)}`;
38
44
  const PRODUCTION_API_URL = "https://api.camox.ai";
39
45
  /** Authentication URL to use for Camox authentication (production Camox Dashboard) */
40
46
  const DEFAULT_AUTHENTICATION_URL = "https://app.camox.ai";
@@ -174,6 +180,8 @@ function camox(options) {
174
180
  let isRelease = false;
175
181
  /** A release's build, without syncing to Live: what the upgrade job checks a project with. */
176
182
  let isDryRun = false;
183
+ /** Set when the build checks a hosted project's code. */
184
+ let hostedProject;
177
185
  let resolvedConfig;
178
186
  let localAuth = null;
179
187
  let blockChunks = {};
@@ -182,6 +190,10 @@ function camox(options) {
182
190
  name: "camox",
183
191
  enforce: "pre",
184
192
  resolveId(id, importer) {
193
+ if (hostedProject) {
194
+ const forbidden = forbiddenImport(hostedProject, id, importer);
195
+ if (forbidden) this.error(forbidden);
196
+ }
185
197
  const runtimeId = resolveRuntimeDevId(id, importer);
186
198
  if (runtimeId) return runtimeId;
187
199
  if (id === VIRTUAL_STUDIO_CSS) return RESOLVED_VIRTUAL_STUDIO_CSS;
@@ -199,7 +211,8 @@ function camox(options) {
199
211
  fileName,
200
212
  source: css
201
213
  });
202
- return `export default ${JSON.stringify(`/${fileName}`)};`;
214
+ if (this.environment.config.consumer === "client") return `export default new URL(/* @vite-ignore */ ${JSON.stringify(`../${fileName}`)}, import.meta.url).href;`;
215
+ return `export default ${serverAssetUrl(fileName)};`;
203
216
  }
204
217
  const cssUrl = `/@fs/${cssPath.replaceAll("\\", "/").replace(/^\/+/, "")}`;
205
218
  return `export default ${JSON.stringify(cssUrl)};`;
@@ -209,8 +222,9 @@ function camox(options) {
209
222
  const css = readFileSync(cssPath, "utf-8");
210
223
  return `export default ${JSON.stringify(css)};`;
211
224
  }
212
- if (id === RESOLVED_VIRTUAL_PAGE_CLIENT_URL && isBuild) return "export default \"/_chunks/camox-page-client.mjs\";";
213
- if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT_URL && isBuild) return "export default \"/_chunks/camox-studio-client.mjs\";";
225
+ if (id === RESOLVED_VIRTUAL_PAGE_CLIENT_URL && isBuild) return `export default ${serverAssetUrl("_chunks/camox-page-client.mjs")};`;
226
+ if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT_URL && isBuild) return `export default ${serverAssetUrl("_chunks/camox-studio-client.mjs")};`;
227
+ if (id === RESOLVED_VIRTUAL_RENDERER_CLIENT_URL && isBuild) return `export default ${serverAssetUrl("_chunks/camox-renderer-client.mjs")};`;
214
228
  if (id === RESOLVED_VIRTUAL_BLOCK_CHUNKS && isBuild) return `export default ${JSON.stringify(blockChunks)};`;
215
229
  if (id === "\0virtual:camox/dev-html") {
216
230
  if (!devServer) return "export default null;";
@@ -222,6 +236,10 @@ function camox(options) {
222
236
  });
223
237
  },
224
238
  transform(code, id) {
239
+ if (hostedProject) {
240
+ const misnamed = misnamedBlock(hostedProject, id, code);
241
+ if (misnamed) this.error(misnamed.message, misnamed.at);
242
+ }
225
243
  return transformArtifactRuntime(code, id);
226
244
  },
227
245
  generateBundle(_outputOptions, bundle) {
@@ -249,6 +267,14 @@ function camox(options) {
249
267
  if (isBuild && !localAuth) throw new Error("Camox: not authenticated. Run `npx camox login` before building the site.");
250
268
  }
251
269
  return {
270
+ ...isBuild && {
271
+ base: "./",
272
+ experimental: { renderBuiltUrl(filename, { ssr, hostType }) {
273
+ if (hostType === "css") return posix.relative("assets", filename);
274
+ if (!ssr) return { relative: true };
275
+ return { runtime: serverAssetUrl(filename) };
276
+ } }
277
+ },
252
278
  build: { emitAssets: true },
253
279
  ..._config.builder?.buildApp ? {} : { builder: { buildApp: buildClientThenWorker } },
254
280
  define: {
@@ -266,7 +292,8 @@ function camox(options) {
266
292
  onLog: withoutExpectedCamoxLogs(findClientOnLog(_config)),
267
293
  input: {
268
294
  "camox-page-client": VIRTUAL_PAGE_CLIENT,
269
- "camox-studio-client": VIRTUAL_STUDIO_CLIENT
295
+ "camox-studio-client": VIRTUAL_STUDIO_CLIENT,
296
+ "camox-renderer-client": VIRTUAL_RENDERER_CLIENT
270
297
  },
271
298
  output: {
272
299
  assetFileNames: "assets/[name]-[hash][extname]",
@@ -298,6 +325,7 @@ function camox(options) {
298
325
  },
299
326
  configResolved(config) {
300
327
  resolvedConfig = config;
328
+ if (process.env["CAMOX_INTERNAL_HOSTED"] === "1") hostedProject = readHostedProject(config.root);
301
329
  if (!config.plugins.some((plugin) => plugin.name.startsWith("vite-plugin-cloudflare"))) throw new Error("Camox: Camox apps run on Cloudflare Workers. Add `cloudflare()` from `@cloudflare/vite-plugin` to your Vite plugins, and point `main` in `wrangler.jsonc` at `camox/worker`.");
302
330
  const routesDir = resolve(config.root, "src/routes");
303
331
  writeRuntimeSidecar(config.root, {
@@ -1,7 +1,7 @@
1
1
  import { useProjectUuid } from "../lib/auth.js";
2
- import { useCamoxApp } from "../features/provider/components/CamoxAppContext.js";
3
2
  import { reads, useReadQuery } from "../lib/studio-reads.js";
4
3
  import { linkablePages, useLinkablePages } from "../lib/link-pages.js";
4
+ import { useRoutableLayouts } from "../lib/studio-definitions.js";
5
5
  import { getPageDestinations } from "../core/pageDestinations.js";
6
6
  import { c } from "react/compiler-runtime";
7
7
 
@@ -9,7 +9,7 @@ import { c } from "react/compiler-runtime";
9
9
  /** The pages of the locale viewed, which the studio navigates to. */
10
10
  function usePageDestinations() {
11
11
  const $ = c(11);
12
- const app = useCamoxApp();
12
+ const layouts = useRoutableLayouts();
13
13
  const projectUuid = useProjectUuid();
14
14
  let t0;
15
15
  if ($[0] !== projectUuid) {
@@ -39,9 +39,9 @@ function usePageDestinations() {
39
39
  $[7] = t4;
40
40
  } else t4 = $[7];
41
41
  let t5;
42
- if ($[8] !== app || $[9] !== t4) {
43
- t5 = getPageDestinations(t4, app.getLayouts());
44
- $[8] = app;
42
+ if ($[8] !== layouts || $[9] !== t4) {
43
+ t5 = getPageDestinations(t4, layouts);
44
+ $[8] = layouts;
45
45
  $[9] = t4;
46
46
  $[10] = t5;
47
47
  } else t5 = $[10];
@@ -53,7 +53,7 @@ function usePageDestinations() {
53
53
  */
54
54
  function useLinkDestinations() {
55
55
  const $ = c(7);
56
- const app = useCamoxApp();
56
+ const layouts = useRoutableLayouts();
57
57
  let t0;
58
58
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
59
59
  t0 = { fetchProject: true };
@@ -61,16 +61,16 @@ function useLinkDestinations() {
61
61
  } else t0 = $[0];
62
62
  const { pages, fallbackPages } = useLinkablePages(useProjectUuid(), t0);
63
63
  let t1;
64
- if ($[1] !== app || $[2] !== fallbackPages || $[3] !== pages) {
64
+ if ($[1] !== fallbackPages || $[2] !== layouts || $[3] !== pages) {
65
65
  let t2;
66
66
  if ($[5] !== pages) {
67
67
  t2 = pages ?? [];
68
68
  $[5] = pages;
69
69
  $[6] = t2;
70
70
  } else t2 = $[6];
71
- t1 = getPageDestinations(linkablePages(t2, fallbackPages).map(_temp), app.getLayouts());
72
- $[1] = app;
73
- $[2] = fallbackPages;
71
+ t1 = getPageDestinations(linkablePages(t2, fallbackPages).map(_temp), layouts);
72
+ $[1] = fallbackPages;
73
+ $[2] = layouts;
74
74
  $[3] = pages;
75
75
  $[4] = t1;
76
76
  } else t1 = $[4];