camox 0.59.0 → 0.61.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/dist/core/createApp.d.ts +8 -8
- package/dist/core/createBlock.d.ts +1 -2
- package/dist/core/createLayout.d.ts +10 -2
- package/dist/core/createLayout.js +1 -3
- package/dist/core/editing/BlockEditingRuntime.d.ts +1 -0
- package/dist/core/editing/CompleteBlockEditingRuntime.js +12 -9
- package/dist/core/editing/createEditableBlock.d.ts +4 -4
- package/dist/core/editing/createEditableBlock.js +20 -26
- package/dist/core/lib/artifactData.js +2 -2
- package/dist/core/lib/contentType.js +5 -4
- package/dist/features/preview/components/SelectionEditor.js +1 -1
- package/dist/features/renderer/RendererApp.js +187 -103
- package/dist/features/renderer/rendererHost.js +23 -16
- package/dist/features/renderer/studioConnection.js +8 -4
- package/dist/features/runtime/loopbackFetch.d.ts +8 -0
- package/dist/features/runtime/publicCache.d.ts +21 -0
- package/dist/features/runtime/publishingDraft.d.ts +4 -0
- package/dist/features/runtime/runtime.js +54 -3
- package/dist/features/runtime/worker.js +1 -4
- package/dist/features/runtime/workerRequest.d.ts +8 -0
- package/dist/features/runtime/workerRuntime.d.ts +5 -0
- package/dist/features/runtime/workerRuntime.js +6 -0
- package/dist/features/sdkBundle/buildSdkBundle.d.ts +40 -0
- package/dist/features/sdkBundle/buildSdkBundle.js +323 -0
- package/dist/features/sdkBundle/localSdkBundle.d.ts +44 -0
- package/dist/features/sdkBundle/localSdkBundle.js +162 -0
- package/dist/features/sdkBundle/sdkBundle.d.ts +3 -0
- package/dist/features/sdkBundle/sdkBundle.js +4 -0
- package/dist/features/sdkBundle/sdkBundleName.d.ts +59 -0
- package/dist/features/sdkBundle/sdkBundleName.js +164 -0
- package/dist/features/sdkBundle/specifiers.d.ts +34 -0
- package/dist/features/sdkBundle/specifiers.js +146 -0
- package/dist/features/settings/SocialProfilesSettings.js +284 -151
- package/dist/features/settings/useProjectMutation.js +6 -5
- package/dist/features/studio/FollowAgentProvider.js +71 -12
- package/dist/features/studio/components/ChatWidget.js +85 -43
- package/dist/features/studio/components/Patrol.js +311 -208
- package/dist/features/studio/components/Squareling.js +164 -66
- package/dist/features/studio/components/ThreadComposer.js +139 -69
- package/dist/features/studio/components/ThreadMessages.js +115 -45
- package/dist/features/studio/editorWhereContext.js +11 -0
- package/dist/features/studio/useEditorSelection.js +135 -0
- package/dist/features/vite/hostedChecks.js +1 -1
- package/dist/features/vite/runtimeDev.js +2 -1
- package/dist/features/vite/sdkBundleBuild.js +74 -0
- package/dist/features/vite/staticAssetHeaders.js +7 -4
- package/dist/features/vite/vite.d.ts +0 -2
- package/dist/features/vite/vite.js +33 -3
- package/dist/lib/auth.js +24 -58
- package/dist/lib/server-auth-cookie.js +12 -2
- package/dist/lib/stored-auth-cookies.js +76 -0
- package/dist/lib/use-thread.js +9 -5
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +17 -4
|
@@ -14,9 +14,9 @@ import { MessageScroller, MessageScrollerButton, MessageScrollerContent, Message
|
|
|
14
14
|
* A thread's messages, and the agent's squareling, which always lives in it. An empty thread has
|
|
15
15
|
* it greet the editor; when the first message arrives, it moves to the end of the thread, and
|
|
16
16
|
* stays there after each message. While a run is going or a message is still `sending` (on its
|
|
17
|
-
* way to the thread, which hasn't accepted it yet), its face
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* way to the thread, which hasn't accepted it yet), its face and a line beside it say what the
|
|
18
|
+
* agent does, never how: thinking, looking around, making changes, building, fixing the build,
|
|
19
|
+
* or starting a draft. Each editor's message shows its
|
|
20
20
|
* sender's avatar, without their name. A Live turn handed off to a draft ends on the draft's
|
|
21
21
|
* card, which `onOpenDraft` opens.
|
|
22
22
|
*/
|
|
@@ -154,73 +154,143 @@ function useSquarelingMorph(empty) {
|
|
|
154
154
|
return node;
|
|
155
155
|
}
|
|
156
156
|
/**
|
|
157
|
-
* The squareling at the end of the thread: idle between turns, at work during one
|
|
158
|
-
* last message, not after it: the scroller only brings a message the
|
|
159
|
-
* when it's appended last, and it arrives while the agent is already
|
|
157
|
+
* The squareling at the end of the thread: idle between turns, at work during one, saying what
|
|
158
|
+
* the agent does. Shown in the last message, not after it: the scroller only brings a message the
|
|
159
|
+
* editor just sent into view when it's appended last, and it arrives while the agent is already
|
|
160
|
+
* working.
|
|
160
161
|
*/
|
|
161
162
|
function AgentPresence(t0) {
|
|
162
|
-
const $ = c(
|
|
163
|
+
const $ = c(8);
|
|
163
164
|
const { view: t1, working, squareling } = t0;
|
|
164
|
-
const {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
t2 = working ? squarelingStatus(status, usingTools) : "idle";
|
|
168
|
-
$[0] = status;
|
|
169
|
-
$[1] = usingTools;
|
|
170
|
-
$[2] = working;
|
|
171
|
-
$[3] = t2;
|
|
172
|
-
} else t2 = $[3];
|
|
165
|
+
const { activity } = t1;
|
|
166
|
+
const shown = useSteadyActivity(working ? activity ?? "thinking" : void 0);
|
|
167
|
+
const t2 = shown ? SQUARELING_STATUSES[shown] : "idle";
|
|
173
168
|
let t3;
|
|
174
|
-
if ($[
|
|
169
|
+
if ($[0] !== squareling || $[1] !== t2) {
|
|
175
170
|
t3 = /* @__PURE__ */ jsx(Squareling, {
|
|
176
171
|
ref: squareling,
|
|
177
172
|
status: t2,
|
|
178
173
|
className: "size-6"
|
|
179
174
|
});
|
|
180
|
-
$[
|
|
181
|
-
$[
|
|
182
|
-
$[
|
|
183
|
-
} else t3 = $[
|
|
175
|
+
$[0] = squareling;
|
|
176
|
+
$[1] = t2;
|
|
177
|
+
$[2] = t3;
|
|
178
|
+
} else t3 = $[2];
|
|
184
179
|
let t4;
|
|
185
|
-
if ($[
|
|
186
|
-
t4 =
|
|
180
|
+
if ($[3] !== shown) {
|
|
181
|
+
t4 = shown && /* @__PURE__ */ jsx("span", {
|
|
187
182
|
className: "shimmer",
|
|
188
|
-
children:
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
$[7] = status;
|
|
194
|
-
$[8] = working;
|
|
195
|
-
$[9] = t4;
|
|
196
|
-
} else t4 = $[9];
|
|
183
|
+
children: ACTIVITY_LINES[shown]
|
|
184
|
+
});
|
|
185
|
+
$[3] = shown;
|
|
186
|
+
$[4] = t4;
|
|
187
|
+
} else t4 = $[4];
|
|
197
188
|
let t5;
|
|
198
|
-
if ($[
|
|
189
|
+
if ($[5] !== t3 || $[6] !== t4) {
|
|
199
190
|
t5 = /* @__PURE__ */ jsxs("div", {
|
|
200
191
|
role: "status",
|
|
201
192
|
className: "text-muted-foreground flex items-center gap-2 text-sm",
|
|
202
193
|
children: [t3, t4]
|
|
203
194
|
});
|
|
204
|
-
$[
|
|
205
|
-
$[
|
|
206
|
-
$[
|
|
207
|
-
} else t5 = $[
|
|
195
|
+
$[5] = t3;
|
|
196
|
+
$[6] = t4;
|
|
197
|
+
$[7] = t5;
|
|
198
|
+
} else t5 = $[7];
|
|
208
199
|
return t5;
|
|
209
200
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
201
|
+
/**
|
|
202
|
+
* How long a line saying what the agent does stays at least, in ms, so editors have time to read
|
|
203
|
+
* it: a round of tool calls can take far less.
|
|
204
|
+
*/
|
|
205
|
+
const ACTIVITY_MIN_DURATION = 2e3;
|
|
206
|
+
/**
|
|
207
|
+
* How long the agent must keep doing something before a line says so, in ms: thinking briefly
|
|
208
|
+
* between two rounds of tool calls never shows.
|
|
209
|
+
*/
|
|
210
|
+
const ACTIVITY_SETTLE_DURATION = 800;
|
|
211
|
+
/**
|
|
212
|
+
* `activity` as a line shows it, steadied: a new activity shows once it lasted
|
|
213
|
+
* `ACTIVITY_SETTLE_DURATION` and the line before held for `ACTIVITY_MIN_DURATION`, so a brief
|
|
214
|
+
* one never shows. Only the line lags: a turn that starts or ends shows at once.
|
|
215
|
+
*/
|
|
216
|
+
function useSteadyActivity(activity) {
|
|
217
|
+
const $ = c(10);
|
|
218
|
+
let t0;
|
|
219
|
+
if ($[0] !== activity) {
|
|
220
|
+
t0 = { activity };
|
|
221
|
+
$[0] = activity;
|
|
222
|
+
$[1] = t0;
|
|
223
|
+
} else t0 = $[1];
|
|
224
|
+
const [latest, setLatest] = React.useState(t0);
|
|
225
|
+
const [lasted, setLasted] = React.useState();
|
|
226
|
+
let t1;
|
|
227
|
+
if ($[2] !== activity) {
|
|
228
|
+
t1 = { activity };
|
|
229
|
+
$[2] = activity;
|
|
230
|
+
$[3] = t1;
|
|
231
|
+
} else t1 = $[3];
|
|
232
|
+
const [held, setHeld] = React.useState(t1);
|
|
233
|
+
const [settled, setSettled] = React.useState();
|
|
234
|
+
let t2;
|
|
235
|
+
let t3;
|
|
236
|
+
if ($[4] !== latest) {
|
|
237
|
+
t2 = () => {
|
|
238
|
+
const timer = setTimeout(() => setLasted(latest), 800);
|
|
239
|
+
return () => clearTimeout(timer);
|
|
240
|
+
};
|
|
241
|
+
t3 = [latest];
|
|
242
|
+
$[4] = latest;
|
|
243
|
+
$[5] = t2;
|
|
244
|
+
$[6] = t3;
|
|
245
|
+
} else {
|
|
246
|
+
t2 = $[5];
|
|
247
|
+
t3 = $[6];
|
|
248
|
+
}
|
|
249
|
+
React.useEffect(t2, t3);
|
|
250
|
+
let t4;
|
|
251
|
+
let t5;
|
|
252
|
+
if ($[7] !== held) {
|
|
253
|
+
t4 = () => {
|
|
254
|
+
const timer_0 = setTimeout(() => setSettled(held), ACTIVITY_MIN_DURATION);
|
|
255
|
+
return () => clearTimeout(timer_0);
|
|
256
|
+
};
|
|
257
|
+
t5 = [held];
|
|
258
|
+
$[7] = held;
|
|
259
|
+
$[8] = t4;
|
|
260
|
+
$[9] = t5;
|
|
261
|
+
} else {
|
|
262
|
+
t4 = $[8];
|
|
263
|
+
t5 = $[9];
|
|
264
|
+
}
|
|
265
|
+
React.useEffect(t4, t5);
|
|
266
|
+
if (activity !== latest.activity) setLatest({ activity });
|
|
267
|
+
else if (activity !== held.activity) {
|
|
268
|
+
if (!activity || !held.activity || lasted === latest && settled === held) setHeld({ activity });
|
|
269
|
+
}
|
|
270
|
+
return activity && (held.activity ?? activity);
|
|
213
271
|
}
|
|
272
|
+
const SQUARELING_STATUSES = {
|
|
273
|
+
thinking: "thinking",
|
|
274
|
+
exploring: "exploring",
|
|
275
|
+
changing: "changing",
|
|
276
|
+
building: "building",
|
|
277
|
+
fixing: "fixing",
|
|
278
|
+
startingDraft: "changing"
|
|
279
|
+
};
|
|
280
|
+
const ACTIVITY_LINES = {
|
|
281
|
+
thinking: "Thinking…",
|
|
282
|
+
exploring: "Looking around…",
|
|
283
|
+
changing: "Making changes…",
|
|
284
|
+
building: "Building…",
|
|
285
|
+
fixing: "Fixing the build…",
|
|
286
|
+
startingDraft: "Starting a draft…"
|
|
287
|
+
};
|
|
214
288
|
/** How a member's pushed code built, as the thread says it. */
|
|
215
289
|
const PUSH_OUTCOMES = {
|
|
216
290
|
built: "Built, and in the preview.",
|
|
217
291
|
typeErrors: "Built, with type errors that block publishing.",
|
|
218
292
|
notPreviewed: "Built, but the preview can’t show it."
|
|
219
293
|
};
|
|
220
|
-
const STATUS_LINES = {
|
|
221
|
-
building: "Building…",
|
|
222
|
-
fixing: "Fixing a build error…"
|
|
223
|
-
};
|
|
224
294
|
function ThreadMessageRow(t0) {
|
|
225
295
|
const $ = c(36);
|
|
226
296
|
const { message, onOpenDraft } = t0;
|
|
@@ -506,4 +576,4 @@ function SenderAvatar(t0) {
|
|
|
506
576
|
}
|
|
507
577
|
|
|
508
578
|
//#endregion
|
|
509
|
-
export { ThreadMessages };
|
|
579
|
+
export { ACTIVITY_MIN_DURATION, ThreadMessages };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/features/studio/editorWhereContext.ts
|
|
4
|
+
/**
|
|
5
|
+
* Where the editor is in the Website Section: the Page they look at, or the Canvas with no page
|
|
6
|
+
* selected. Undefined elsewhere in the Studio. Their messages to the agent carry it as a hint.
|
|
7
|
+
*/
|
|
8
|
+
const EditorWhereContext = React.createContext(void 0);
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
export { EditorWhereContext };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { placedRecords } from "../../lib/normalized-data.js";
|
|
2
|
+
import { reads, useReadQuery } from "../../lib/studio-reads.js";
|
|
3
|
+
import { useBlockDefinition, useCollectionDefinition } from "../../lib/studio-definitions.js";
|
|
4
|
+
import { isBlockPlacement, previewStore, selectionBlockId, selectionForOwner, selectionItemId } from "../preview/previewStore.js";
|
|
5
|
+
import { entityName } from "../preview/entityName.js";
|
|
6
|
+
import { contentFieldSchema } from "../preview/components/contentFieldSchema.js";
|
|
7
|
+
import { formatFieldName } from "../preview/components/ItemFieldsEditor.js";
|
|
8
|
+
import { getSchemaForItem } from "../preview/components/SelectionEditor.js";
|
|
9
|
+
import { c } from "react/compiler-runtime";
|
|
10
|
+
import { useSelector } from "@xstate/store-react";
|
|
11
|
+
|
|
12
|
+
//#region src/features/studio/useEditorSelection.ts
|
|
13
|
+
/** The owner the page the editor looks at edits, as the editing frame names it. */
|
|
14
|
+
function ownerOf(where) {
|
|
15
|
+
if (where?.on !== "page") return null;
|
|
16
|
+
if (where.layoutId != null) return {
|
|
17
|
+
kind: "layout",
|
|
18
|
+
layoutId: where.layoutId
|
|
19
|
+
};
|
|
20
|
+
if (where.pageId != null) return {
|
|
21
|
+
kind: "page",
|
|
22
|
+
pageId: where.pageId
|
|
23
|
+
};
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/** The selection as the agent reads it, from the parts the studio loaded. */
|
|
27
|
+
function editorSelectionOf({ selection, block, items, schema, records, recordSchema }) {
|
|
28
|
+
const fieldLabel = (fieldSchema, fieldName) => contentFieldSchema(fieldSchema, fieldName)?.title ?? formatFieldName(fieldName);
|
|
29
|
+
const path = [];
|
|
30
|
+
const blockId = selectionBlockId(selection) ?? void 0;
|
|
31
|
+
if (blockId) path.push(block?.title ?? "Block");
|
|
32
|
+
for (const item of items) path.push(entityName(item, "Item"));
|
|
33
|
+
const recordSelected = selection.type === "record" || selection.type === "record-field";
|
|
34
|
+
if (recordSelected) path.push(...records.length > 0 ? records.map((record) => record.title || "Record") : ["Record"]);
|
|
35
|
+
let fieldName;
|
|
36
|
+
if (selection.type === "block-field" || selection.type === "item-field") {
|
|
37
|
+
fieldName = selection.fieldName;
|
|
38
|
+
path.push(fieldLabel(schema, fieldName));
|
|
39
|
+
} else if (selection.type === "record-field") {
|
|
40
|
+
fieldName = selection.recordFieldName;
|
|
41
|
+
path.push(fieldLabel(recordSchema, fieldName));
|
|
42
|
+
}
|
|
43
|
+
const itemId = selectionItemId(selection) ?? void 0;
|
|
44
|
+
const recordId = recordSelected ? selection.nested?.recordId ?? selection.recordId : void 0;
|
|
45
|
+
return {
|
|
46
|
+
path,
|
|
47
|
+
...blockId ? { blockId } : {},
|
|
48
|
+
...itemId ? { itemId } : {},
|
|
49
|
+
...recordId ? { recordId } : {},
|
|
50
|
+
...fieldName ? { fieldName } : {}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What the editor has selected on the page they look at, as the agent reads it, or undefined
|
|
55
|
+
* with nothing selected there.
|
|
56
|
+
*/
|
|
57
|
+
function useEditorSelection(where) {
|
|
58
|
+
const $ = c(12);
|
|
59
|
+
let t0;
|
|
60
|
+
if ($[0] !== where) {
|
|
61
|
+
t0 = ownerOf(where);
|
|
62
|
+
$[0] = where;
|
|
63
|
+
$[1] = t0;
|
|
64
|
+
} else t0 = $[1];
|
|
65
|
+
const owner = t0;
|
|
66
|
+
let t1;
|
|
67
|
+
if ($[2] !== owner) {
|
|
68
|
+
t1 = (state) => selectionForOwner(state.context, owner);
|
|
69
|
+
$[2] = owner;
|
|
70
|
+
$[3] = t1;
|
|
71
|
+
} else t1 = $[3];
|
|
72
|
+
const selection = useSelector(previewStore, t1);
|
|
73
|
+
const blockId = selectionBlockId(selection);
|
|
74
|
+
const itemId = selectionItemId(selection);
|
|
75
|
+
const t2 = blockId != null;
|
|
76
|
+
let t3;
|
|
77
|
+
if ($[4] !== t2) {
|
|
78
|
+
t3 = { enabled: t2 };
|
|
79
|
+
$[4] = t2;
|
|
80
|
+
$[5] = t3;
|
|
81
|
+
} else t3 = $[5];
|
|
82
|
+
const { data: bundle } = useReadQuery(reads.blockBundle(blockId), t3);
|
|
83
|
+
const blockDef = useBlockDefinition(bundle?.block.type);
|
|
84
|
+
bundle?.repeatableItems;
|
|
85
|
+
let t4;
|
|
86
|
+
if ($[6] !== bundle?.repeatableItems) {
|
|
87
|
+
t4 = bundle?.repeatableItems ?? [];
|
|
88
|
+
$[6] = bundle?.repeatableItems;
|
|
89
|
+
$[7] = t4;
|
|
90
|
+
} else t4 = $[7];
|
|
91
|
+
let t5;
|
|
92
|
+
if ($[8] !== t4) {
|
|
93
|
+
t5 = new Map(t4.map(_temp));
|
|
94
|
+
$[8] = t4;
|
|
95
|
+
$[9] = t5;
|
|
96
|
+
} else t5 = $[9];
|
|
97
|
+
const itemsMap = t5;
|
|
98
|
+
const items = [];
|
|
99
|
+
for (let item_0 = itemId ? itemsMap.get(itemId) : void 0; item_0;) {
|
|
100
|
+
items.unshift(item_0);
|
|
101
|
+
item_0 = item_0.parentItemId ? itemsMap.get(item_0.parentItemId) : void 0;
|
|
102
|
+
}
|
|
103
|
+
const schema = blockDef && itemId ? getSchemaForItem(blockDef.contentSchema, itemId, itemsMap) : blockDef?.contentSchema;
|
|
104
|
+
const records = [];
|
|
105
|
+
if ((selection?.type === "record" || selection?.type === "record-field") && isBlockPlacement(selection)) {
|
|
106
|
+
const placer = itemId ? items.at(-1) : bundle?.block;
|
|
107
|
+
let t6;
|
|
108
|
+
if ($[10] !== selection.recordId) {
|
|
109
|
+
t6 = (record) => record.id === selection.recordId;
|
|
110
|
+
$[10] = selection.recordId;
|
|
111
|
+
$[11] = t6;
|
|
112
|
+
} else t6 = $[11];
|
|
113
|
+
const outer = placedRecords(placer?.references, selection.fieldName).find(t6);
|
|
114
|
+
if (outer) records.push(outer);
|
|
115
|
+
const { nested } = selection;
|
|
116
|
+
const linked = nested && placedRecords(outer?.references, nested.fieldName).find((record_0) => record_0.id === nested.recordId);
|
|
117
|
+
if (linked) records.push(linked);
|
|
118
|
+
}
|
|
119
|
+
const recordSchema = useCollectionDefinition(records.at(-1)?.collectionId)?.contentSchema;
|
|
120
|
+
if (!selection) return;
|
|
121
|
+
return editorSelectionOf({
|
|
122
|
+
selection,
|
|
123
|
+
block: blockDef,
|
|
124
|
+
items,
|
|
125
|
+
schema,
|
|
126
|
+
records,
|
|
127
|
+
recordSchema
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function _temp(item) {
|
|
131
|
+
return [item.id, item];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
export { editorSelectionOf, useEditorSelection };
|
|
@@ -6,7 +6,7 @@ import { Visitor, parseSync } from "vite-plus";
|
|
|
6
6
|
//#region src/features/vite/hostedChecks.ts
|
|
7
7
|
/**
|
|
8
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).
|
|
9
|
+
* agent may have written (ADR 0021). Dev mode never sets it.
|
|
10
10
|
*/
|
|
11
11
|
const HOSTED_BUILD_ENV = "CAMOX_INTERNAL_HOSTED";
|
|
12
12
|
const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
@@ -73,7 +73,8 @@ function resolveCamoxSourceImport(id) {
|
|
|
73
73
|
"camox/_internal/rendererClient": resolve(sdkRoot, "src/features/runtime/rendererClient.tsx"),
|
|
74
74
|
"camox/_internal/studioServer": resolve(sdkRoot, "src/features/runtime/studioServer.tsx"),
|
|
75
75
|
"camox/_internal/runtime": resolve(sdkRoot, "src/features/runtime/runtime.ts"),
|
|
76
|
-
"camox/worker": resolve(sdkRoot, "src/features/runtime/worker.ts")
|
|
76
|
+
"camox/worker": resolve(sdkRoot, "src/features/runtime/worker.ts"),
|
|
77
|
+
"camox/_internal/workerRuntime": resolve(sdkRoot, "src/features/runtime/workerRuntime.ts")
|
|
77
78
|
}[id];
|
|
78
79
|
if (!sourceImport || !existsSync(sourceImport)) return;
|
|
79
80
|
return sourceImport;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { sdkBundleEntryFile } from "../sdkBundle/specifiers.js";
|
|
2
|
+
import { SDK_BUNDLE_BUILD_MANIFEST, sdkBundlePath } from "../sdkBundle/sdkBundleName.js";
|
|
3
|
+
import { SDK_BUNDLE_MANIFEST } from "../sdkBundle/buildSdkBundle.js";
|
|
4
|
+
import { buildsAgainstSdkBundle, ensureLocalSdkBundle } from "../sdkBundle/localSdkBundle.js";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
7
|
+
import { dirname, join, posix, resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
//#region src/features/vite/sdkBundleBuild.ts
|
|
10
|
+
/**
|
|
11
|
+
* The SDK Bundle a build builds against, from the local cache, or undefined when the switch is
|
|
12
|
+
* off and the build is whole-app.
|
|
13
|
+
*/
|
|
14
|
+
async function sdkBundleBuildState({ root, settings, mayBuild }) {
|
|
15
|
+
if (!buildsAgainstSdkBundle()) return;
|
|
16
|
+
let built = false;
|
|
17
|
+
const bundle = await ensureLocalSdkBundle({
|
|
18
|
+
root,
|
|
19
|
+
settings,
|
|
20
|
+
mayBuild,
|
|
21
|
+
onBuild: () => {
|
|
22
|
+
built = true;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const names = {
|
|
26
|
+
browser: bundle.browser.name,
|
|
27
|
+
workerd: bundle.workerd.name
|
|
28
|
+
};
|
|
29
|
+
const stylesheet = readdirSync(bundle.browser.dir).find((file) => /^camox-studio-.*\.css$/.test(file));
|
|
30
|
+
if (!stylesheet) throw new Error(`Camox: the SDK Bundle ${names.browser} has no Studio stylesheet.`);
|
|
31
|
+
const websiteRequire = createRequire(join(root, "package.json"));
|
|
32
|
+
/** What each target holds: its entry files by specifier, and by the file a path specifier names. */
|
|
33
|
+
const held = (dir) => {
|
|
34
|
+
const { entries } = JSON.parse(readFileSync(join(dir, SDK_BUNDLE_MANIFEST), "utf8"));
|
|
35
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const specifier of Object.keys(entries)) if (/\.[cm]?js$/.test(specifier)) byFile.set(websiteRequire.resolve(specifier), sdkBundleEntryFile(specifier));
|
|
37
|
+
return {
|
|
38
|
+
entries,
|
|
39
|
+
byFile
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const holds = {
|
|
43
|
+
browser: held(bundle.browser.dir),
|
|
44
|
+
workerd: held(bundle.workerd.dir)
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
names,
|
|
48
|
+
built,
|
|
49
|
+
studioStylesheetUrl: sdkBundlePath(names.browser) + stylesheet,
|
|
50
|
+
manifest: {
|
|
51
|
+
fileName: SDK_BUNDLE_BUILD_MANIFEST,
|
|
52
|
+
source: `${JSON.stringify(names)}\n`
|
|
53
|
+
},
|
|
54
|
+
resolve(id, importer, consumer) {
|
|
55
|
+
const target = consumer === "client" ? "browser" : "workerd";
|
|
56
|
+
const { entries, byFile } = holds[target];
|
|
57
|
+
const file = Object.hasOwn(entries, id) ? entries[id] : id.startsWith(".") && importer ? byFile.get(resolve(dirname(importer), id)) : void 0;
|
|
58
|
+
if (!file) return;
|
|
59
|
+
return {
|
|
60
|
+
id: sdkBundlePath(names[target]) + file,
|
|
61
|
+
external: "absolute"
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
relativizeWorkerImports(code, fileName) {
|
|
65
|
+
const prefix = sdkBundlePath(names.workerd);
|
|
66
|
+
if (!code.includes(prefix)) return;
|
|
67
|
+
const toRoot = posix.relative(posix.dirname(fileName), "") || ".";
|
|
68
|
+
return code.replaceAll(new RegExp(`(["'\`])${prefix.replaceAll("/", "\\/")}`, "g"), `$1${toRoot}${prefix}`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { sdkBundleBuildState };
|
|
@@ -5,17 +5,20 @@ import { join, relative } from "node:path";
|
|
|
5
5
|
/** Where the client build puts the files it names by their content's hash. */
|
|
6
6
|
const HASHED_DIRS = ["assets", "_chunks"];
|
|
7
7
|
const IMMUTABLE = "public, max-age=31536000, immutable";
|
|
8
|
+
/** Where a build against an SDK Bundle serves its files, under the bundle's name (ADR 0029). */
|
|
9
|
+
const SDK_BUNDLE_DIR = "_camox/sdk";
|
|
8
10
|
/** Workers Static Assets reads at most this many rules. */
|
|
9
11
|
const MAX_RULES = 100;
|
|
10
12
|
/**
|
|
11
13
|
* The `_headers` file Workers Static Assets serves the client build with. Files under
|
|
12
14
|
* `/assets/` and `/_chunks/` are named by their content's hash, so browsers keep them for good;
|
|
13
15
|
* the files there named otherwise, the entry chunks pages load by a fixed name and whatever the
|
|
14
|
-
* app's public directory puts there, keep the default, which revalidates.
|
|
15
|
-
*
|
|
16
|
+
* app's public directory puts there, keep the default, which revalidates. A build against an
|
|
17
|
+
* SDK Bundle has its files under `/_camox/sdk/<name>/`, which never change under a name either.
|
|
18
|
+
* The app's own `_headers` comes first, as it wrote it.
|
|
16
19
|
*/
|
|
17
|
-
function staticAssetHeaders({ ownHeaders, unhashedFiles }) {
|
|
18
|
-
const rules = [...HASHED_DIRS.map((dir) => `/${dir}/*\n Cache-Control: ${IMMUTABLE}`), ...[...unhashedFiles].sort().map((file) => `/${file}\n ! Cache-Control`)];
|
|
20
|
+
function staticAssetHeaders({ ownHeaders, unhashedFiles, sdkBundle = false }) {
|
|
21
|
+
const rules = [...(sdkBundle ? [...HASHED_DIRS, SDK_BUNDLE_DIR] : HASHED_DIRS).map((dir) => `/${dir}/*\n Cache-Control: ${IMMUTABLE}`), ...[...unhashedFiles].sort().map((file) => `/${file}\n ! Cache-Control`)];
|
|
19
22
|
const own = ownHeaders?.trimEnd();
|
|
20
23
|
const ownRules = own ? own.split("\n").filter((line) => /^\S/.test(line) && !line.startsWith("#")) : [];
|
|
21
24
|
if (ownRules.length + rules.length > MAX_RULES) throw new Error(`Camox: _headers would hold ${ownRules.length + rules.length} rules, past Workers Static Assets' ${MAX_RULES}. Move the public directory's files out of /assets/ and /_chunks/, or drop rules from public/_headers.`);
|
|
@@ -29,8 +29,6 @@ export interface CamoxPluginOptions {
|
|
|
29
29
|
apiUrl?: string;
|
|
30
30
|
/** URL of the Camox authentication backend (default: https://app.camox.ai) */
|
|
31
31
|
authenticationUrl?: string;
|
|
32
|
-
/** Show Tanstack query devtools (default: false) */
|
|
33
|
-
enableTanstackDevtools?: boolean;
|
|
34
32
|
/** Enable experimental features (default: false). */
|
|
35
33
|
enableExperimentalFeatures?: boolean;
|
|
36
34
|
/** Disable automatic code generation (route and layout files) (default: false) */
|
|
@@ -10,6 +10,7 @@ import { installDevSignInLink } from "./devSignIn.js";
|
|
|
10
10
|
import { HOSTED_BUILD_ENV, forbiddenImport, misnamedBlock, readHostedProject } from "./hostedChecks.js";
|
|
11
11
|
import { generateIconTypes } from "./iconGeneration.js";
|
|
12
12
|
import { generateLayoutFiles, watchLayoutFiles } from "./layoutGeneration.js";
|
|
13
|
+
import { sdkBundleBuildState } from "./sdkBundleBuild.js";
|
|
13
14
|
import { publicFilesInHashedDirs, readOwnHeaders, staticAssetHeaders, unhashedEntries } from "./staticAssetHeaders.js";
|
|
14
15
|
import { buildClientThenWorker } from "./workerBuild.js";
|
|
15
16
|
import { VIRTUAL_CAMOX_APP, syncDefinitions, syncDefinitionsToApi } from "./definitionsSync.js";
|
|
@@ -185,7 +186,6 @@ function camox(options) {
|
|
|
185
186
|
const projectUuid = projectIdOf(options);
|
|
186
187
|
const apiUrl = options._internal?.apiUrl ?? PRODUCTION_API_URL;
|
|
187
188
|
const authenticationUrl = options._internal?.authenticationUrl ?? DEFAULT_AUTHENTICATION_URL;
|
|
188
|
-
const enableTanstackDevtools = options._internal?.enableTanstackDevtools ?? false;
|
|
189
189
|
const disableCodeGen = options._internal?.disableCodeGen ?? false;
|
|
190
190
|
const runtimeBasePath = options._internal?.runtimeBasePath;
|
|
191
191
|
let isBuild = false;
|
|
@@ -200,6 +200,8 @@ function camox(options) {
|
|
|
200
200
|
let localDeployToken;
|
|
201
201
|
let blockChunks = {};
|
|
202
202
|
let devServer;
|
|
203
|
+
/** Set when the build builds against an SDK Bundle. */
|
|
204
|
+
let sdkBundle;
|
|
203
205
|
return {
|
|
204
206
|
name: "camox",
|
|
205
207
|
enforce: "pre",
|
|
@@ -209,6 +211,10 @@ function camox(options) {
|
|
|
209
211
|
const forbidden = forbiddenImport(hostedProject, id, importer);
|
|
210
212
|
if (forbidden) this.error(forbidden);
|
|
211
213
|
}
|
|
214
|
+
if (sdkBundle) {
|
|
215
|
+
const external = sdkBundle.resolve(id, importer, this.environment.config.consumer);
|
|
216
|
+
if (external) return external;
|
|
217
|
+
}
|
|
212
218
|
const runtimeId = resolveRuntimeDevId(id, importer);
|
|
213
219
|
if (runtimeId) return runtimeId;
|
|
214
220
|
if (id === VIRTUAL_STUDIO_CSS) return RESOLVED_VIRTUAL_STUDIO_CSS;
|
|
@@ -216,6 +222,7 @@ function camox(options) {
|
|
|
216
222
|
},
|
|
217
223
|
async load(id, loadOptions) {
|
|
218
224
|
const stylesDir = resolve(sdkRoot, !isBuild && isSdkSource ? ".dev" : "dist");
|
|
225
|
+
if (id === RESOLVED_VIRTUAL_STUDIO_CSS && sdkBundle) return `export default ${JSON.stringify(sdkBundle.studioStylesheetUrl)};`;
|
|
219
226
|
if (id === RESOLVED_VIRTUAL_STUDIO_CSS) {
|
|
220
227
|
const cssPath = resolve(stylesDir, "studio.css");
|
|
221
228
|
if (isBuild) {
|
|
@@ -257,7 +264,19 @@ function camox(options) {
|
|
|
257
264
|
}
|
|
258
265
|
return transformArtifactRuntime(code, id);
|
|
259
266
|
},
|
|
267
|
+
renderChunk(code, chunk) {
|
|
268
|
+
if (!sdkBundle || this.environment.config.consumer !== "server") return;
|
|
269
|
+
const relative = sdkBundle.relativizeWorkerImports(code, chunk.fileName);
|
|
270
|
+
return relative === void 0 ? void 0 : {
|
|
271
|
+
code: relative,
|
|
272
|
+
map: null
|
|
273
|
+
};
|
|
274
|
+
},
|
|
260
275
|
generateBundle(_outputOptions, bundle) {
|
|
276
|
+
if (sdkBundle && this.environment.config.consumer === "server") this.emitFile({
|
|
277
|
+
type: "asset",
|
|
278
|
+
...sdkBundle.manifest
|
|
279
|
+
});
|
|
261
280
|
if (this.environment.name !== "client") return;
|
|
262
281
|
blockChunks = collectBlockChunks(bundle, resolvedConfig.root);
|
|
263
282
|
if (!isBuild) return;
|
|
@@ -267,7 +286,8 @@ function camox(options) {
|
|
|
267
286
|
fileName: "_headers",
|
|
268
287
|
source: staticAssetHeaders({
|
|
269
288
|
ownHeaders: readOwnHeaders(publicDir),
|
|
270
|
-
unhashedFiles: [...unhashedEntries(bundle), ...publicFilesInHashedDirs(publicDir)]
|
|
289
|
+
unhashedFiles: [...unhashedEntries(bundle), ...publicFilesInHashedDirs(publicDir)],
|
|
290
|
+
sdkBundle: sdkBundle !== void 0
|
|
271
291
|
})
|
|
272
292
|
});
|
|
273
293
|
},
|
|
@@ -279,6 +299,16 @@ function camox(options) {
|
|
|
279
299
|
isDryRun = isRelease && release === "dry-run";
|
|
280
300
|
if (env.command === "serve" && !env.isPreview) localDeployToken = localDeployTokenOf(apiUrl, options);
|
|
281
301
|
if (!isBuild) localAuth = readAuthTokenForUrl(authenticationUrl);
|
|
302
|
+
if (isBuild) sdkBundle = await sdkBundleBuildState({
|
|
303
|
+
root: resolve(_config.root ?? process.cwd()),
|
|
304
|
+
settings: {
|
|
305
|
+
iconIds,
|
|
306
|
+
telemetryDisabled: !!options.disableTelemetry,
|
|
307
|
+
experimentalFeatures: options._internal?.enableExperimentalFeatures ?? false,
|
|
308
|
+
apiUrl
|
|
309
|
+
},
|
|
310
|
+
mayBuild: !(isRelease && process.env["CAMOX_INTERNAL_HOSTED"] === "1")
|
|
311
|
+
});
|
|
282
312
|
return {
|
|
283
313
|
...isBuild && {
|
|
284
314
|
base: "./",
|
|
@@ -296,7 +326,6 @@ function camox(options) {
|
|
|
296
326
|
define: {
|
|
297
327
|
__CAMOX_ICON_IDS__: JSON.stringify(iconIds),
|
|
298
328
|
__CAMOX_TELEMETRY_DISABLED__: JSON.stringify(!!options.disableTelemetry),
|
|
299
|
-
__ENABLE_TANSTACK_DEVTOOLS__: JSON.stringify(enableTanstackDevtools),
|
|
300
329
|
__CAMOX_ENABLE_EXPERIMENTAL_FEATURES__: JSON.stringify(options._internal?.enableExperimentalFeatures ?? false),
|
|
301
330
|
__CAMOX_API_URL__: JSON.stringify(apiUrl),
|
|
302
331
|
__CAMOX_AUTHENTICATION_URL__: JSON.stringify(authenticationUrl),
|
|
@@ -361,6 +390,7 @@ function camox(options) {
|
|
|
361
390
|
else if (localDeployToken) target = "local Live";
|
|
362
391
|
else if (!isBuild && !localAuth) target = "authentication required";
|
|
363
392
|
config.logger.info(`${mode} Camox app (${target})`, { timestamp: true });
|
|
393
|
+
if (sdkBundle?.built) config.logger.info(`Built the SDK Bundle ${sdkBundle.names.browser} (browser) and ${sdkBundle.names.workerd} (workerd) into its cache`, { timestamp: true });
|
|
364
394
|
},
|
|
365
395
|
configureServer(server) {
|
|
366
396
|
devServer = server;
|