@joycostudio/studio 0.1.3 → 0.1.4
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 +187 -55
- package/dist/chunk-2RGQF5X5.js +470 -0
- package/dist/chunk-2RGQF5X5.js.map +1 -0
- package/dist/{chunk-OIMLF2CL.js → chunk-DEJYGBPJ.js} +6 -5
- package/dist/chunk-DEJYGBPJ.js.map +1 -0
- package/dist/chunk-HVGJXFXT.js +314 -0
- package/dist/chunk-HVGJXFXT.js.map +1 -0
- package/dist/cli.js +869 -52
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +5 -460
- package/dist/client.js.map +1 -1
- package/dist/command-menu.css +4 -1
- package/dist/controls.d.ts +1 -0
- package/dist/controls.js +3 -0
- package/dist/controls.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/next-config.d.ts +4 -0
- package/dist/next-config.js +135 -0
- package/dist/next-config.js.map +1 -0
- package/dist/next.d.ts +8 -0
- package/dist/next.js +15 -0
- package/dist/next.js.map +1 -0
- package/dist/{protocol-Dgj_7ibg.d.ts → protocol-DjrMADPm.d.ts} +40 -5
- package/dist/react.d.ts +14 -7
- package/dist/react.js +357 -244
- package/dist/react.js.map +1 -1
- package/dist/runtime.d.ts +32 -4
- package/dist/runtime.js +97 -83
- package/dist/runtime.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +27 -7
- package/dist/chunk-OIMLF2CL.js.map +0 -1
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import {
|
|
2
|
+
error,
|
|
3
|
+
hasConfig,
|
|
4
|
+
validateConfig
|
|
5
|
+
} from "./chunk-DEJYGBPJ.js";
|
|
6
|
+
|
|
7
|
+
// src/client/studio.ts
|
|
8
|
+
import {
|
|
9
|
+
button,
|
|
10
|
+
schemaValues
|
|
11
|
+
} from "@joycostudio/xyz/debug";
|
|
12
|
+
|
|
13
|
+
// src/core/diff.ts
|
|
14
|
+
function diff(before, after, path = []) {
|
|
15
|
+
const nested = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
if (nested(before) && nested(after)) {
|
|
17
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
18
|
+
return [...keys].flatMap((key) => diff(before[key], after[key], [...path, key]));
|
|
19
|
+
}
|
|
20
|
+
return JSON.stringify(before) === JSON.stringify(after) ? [] : [{ path, before, after }];
|
|
21
|
+
}
|
|
22
|
+
function changedPaths(before, after) {
|
|
23
|
+
return diff(before, after).map(({ path }) => path.join("."));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/client/bridge.ts
|
|
27
|
+
function connectBridge(url, handlers) {
|
|
28
|
+
let socket = null;
|
|
29
|
+
let nextId = 0;
|
|
30
|
+
let retry = 0;
|
|
31
|
+
let timer;
|
|
32
|
+
let closed = false;
|
|
33
|
+
const pending = /* @__PURE__ */ new Map();
|
|
34
|
+
const open = () => {
|
|
35
|
+
handlers.connection({ phase: "connecting" });
|
|
36
|
+
const ws = new WebSocket(url);
|
|
37
|
+
socket = ws;
|
|
38
|
+
ws.onopen = () => {
|
|
39
|
+
retry = 0;
|
|
40
|
+
handlers.connection({ phase: "connected" });
|
|
41
|
+
};
|
|
42
|
+
ws.onmessage = (event) => {
|
|
43
|
+
const message = JSON.parse(String(event.data));
|
|
44
|
+
if (message.type === "state") return handlers.state(message.state);
|
|
45
|
+
pending.get(message.id)?.(message.result);
|
|
46
|
+
pending.delete(message.id);
|
|
47
|
+
};
|
|
48
|
+
ws.onclose = () => {
|
|
49
|
+
socket = null;
|
|
50
|
+
for (const resolve of pending.values()) resolve(error("runtime_unavailable"));
|
|
51
|
+
pending.clear();
|
|
52
|
+
if (closed) return;
|
|
53
|
+
handlers.connection({ phase: "offline" });
|
|
54
|
+
timer = setTimeout(open, Math.min(500 * 2 ** retry++, 5e3));
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
open();
|
|
58
|
+
return {
|
|
59
|
+
request(op, params) {
|
|
60
|
+
if (socket?.readyState !== WebSocket.OPEN) return Promise.resolve(error("runtime_unavailable"));
|
|
61
|
+
const id = nextId++;
|
|
62
|
+
socket.send(JSON.stringify({ id, op, params }));
|
|
63
|
+
return new Promise((resolve) => pending.set(id, resolve));
|
|
64
|
+
},
|
|
65
|
+
close() {
|
|
66
|
+
closed = true;
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
socket?.close();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/client/browser-sync.ts
|
|
74
|
+
function connectBrowserSync(definition, runtime, handlers) {
|
|
75
|
+
if (typeof window === "undefined" || !window.location?.origin || !window.BroadcastChannel) return null;
|
|
76
|
+
let channel;
|
|
77
|
+
try {
|
|
78
|
+
channel = new window.BroadcastChannel(
|
|
79
|
+
JSON.stringify(["joyco-studio:config", runtime || null, definition.id, definition.file])
|
|
80
|
+
);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const sender = crypto.getRandomValues(new Uint32Array(4)).join("-");
|
|
85
|
+
let clock = 0;
|
|
86
|
+
let latest;
|
|
87
|
+
let closed = false;
|
|
88
|
+
channel.onmessage = ({ data }) => {
|
|
89
|
+
if (closed || !data || typeof data !== "object") return;
|
|
90
|
+
if (data.type === "request") {
|
|
91
|
+
if (latest) channel.postMessage(latest);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (data.type !== "config" || !Number.isSafeInteger(data.clock) || data.clock < 1 || typeof data.sender !== "string")
|
|
95
|
+
return;
|
|
96
|
+
clock = Math.max(clock, data.clock);
|
|
97
|
+
if (!handlers.enabled()) return;
|
|
98
|
+
if (latest && (data.clock < latest.clock || data.clock === latest.clock && data.sender <= latest.sender)) return;
|
|
99
|
+
const result = validateConfig(definition, data.value);
|
|
100
|
+
if (!result.ok) return;
|
|
101
|
+
latest = { type: "config", clock: data.clock, sender: data.sender, value: result.data };
|
|
102
|
+
handlers.receive(result.data);
|
|
103
|
+
};
|
|
104
|
+
channel.postMessage({ type: "request" });
|
|
105
|
+
return {
|
|
106
|
+
publish(value) {
|
|
107
|
+
if (closed) return;
|
|
108
|
+
latest = { type: "config", clock: ++clock, sender, value: structuredClone(value) };
|
|
109
|
+
channel.postMessage(latest);
|
|
110
|
+
},
|
|
111
|
+
close() {
|
|
112
|
+
closed = true;
|
|
113
|
+
channel.onmessage = null;
|
|
114
|
+
channel.close();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/client/history.ts
|
|
120
|
+
var LIMIT = 100;
|
|
121
|
+
var MERGE_MS = 500;
|
|
122
|
+
function createHistory() {
|
|
123
|
+
const past = [];
|
|
124
|
+
let future = [];
|
|
125
|
+
let open = null;
|
|
126
|
+
const step = (from, to, side) => {
|
|
127
|
+
const entry = from.pop();
|
|
128
|
+
if (!entry) return null;
|
|
129
|
+
to.push(entry);
|
|
130
|
+
open = null;
|
|
131
|
+
return entry.changes.map((change) => ({ path: change.path, value: change[side] }));
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
get canUndo() {
|
|
135
|
+
return past.length > 0;
|
|
136
|
+
},
|
|
137
|
+
get canRedo() {
|
|
138
|
+
return future.length > 0;
|
|
139
|
+
},
|
|
140
|
+
/** Record the edit between two snapshots. `merge: false` keeps it a step of its own (e.g. Discard). */
|
|
141
|
+
record(before, after, { merge = true } = {}) {
|
|
142
|
+
const changes = structuredClone(diff(before, after));
|
|
143
|
+
if (!changes.length) return;
|
|
144
|
+
future = [];
|
|
145
|
+
const key = JSON.stringify(changes.map(({ path }) => path));
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
if (merge && open && open.key === key && now - open.at < MERGE_MS) {
|
|
148
|
+
open.changes = open.changes.map((change, i) => ({ ...change, after: changes[i].after }));
|
|
149
|
+
open.at = now;
|
|
150
|
+
if (open.changes.every((change) => JSON.stringify(change.before) === JSON.stringify(change.after))) {
|
|
151
|
+
past.pop();
|
|
152
|
+
open = null;
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const entry = { changes, key, at: now };
|
|
157
|
+
past.push(entry);
|
|
158
|
+
if (past.length > LIMIT) past.shift();
|
|
159
|
+
open = merge ? entry : null;
|
|
160
|
+
},
|
|
161
|
+
/** The leaves to write back, or null when there is nothing to undo. */
|
|
162
|
+
undo: () => step(past, future, "before"),
|
|
163
|
+
redo: () => step(future, past, "after")
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function writePath(target, path, value) {
|
|
167
|
+
const key = path.at(-1);
|
|
168
|
+
let parent = target;
|
|
169
|
+
for (const segment of path.slice(0, -1)) {
|
|
170
|
+
const next = parent[segment];
|
|
171
|
+
parent = typeof next === "object" && next !== null ? next : parent[segment] = {};
|
|
172
|
+
}
|
|
173
|
+
if (value === void 0) delete parent[key];
|
|
174
|
+
else parent[key] = structuredClone(value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/client/live.ts
|
|
178
|
+
function live(target, onWrite) {
|
|
179
|
+
const proxies = /* @__PURE__ */ new WeakMap();
|
|
180
|
+
const wrap = (object) => {
|
|
181
|
+
let proxy = proxies.get(object);
|
|
182
|
+
if (!proxy) {
|
|
183
|
+
proxy = new Proxy(object, {
|
|
184
|
+
get(target2, key, receiver) {
|
|
185
|
+
const value = Reflect.get(target2, key, receiver);
|
|
186
|
+
return typeof value === "object" && value !== null ? wrap(value) : value;
|
|
187
|
+
},
|
|
188
|
+
set(target2, key, value, receiver) {
|
|
189
|
+
const changed = Reflect.get(target2, key, receiver) !== value;
|
|
190
|
+
const ok = Reflect.set(target2, key, value, receiver);
|
|
191
|
+
if (changed) onWrite();
|
|
192
|
+
return ok;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
proxies.set(object, proxy);
|
|
196
|
+
}
|
|
197
|
+
return proxy;
|
|
198
|
+
};
|
|
199
|
+
return wrap(target);
|
|
200
|
+
}
|
|
201
|
+
function assignDeep(target, source) {
|
|
202
|
+
for (const [key, value] of Object.entries(source)) {
|
|
203
|
+
const current = target[key];
|
|
204
|
+
const nested = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
205
|
+
if (nested(current) && nested(value))
|
|
206
|
+
assignDeep(current, value);
|
|
207
|
+
else target[key] = structuredClone(value);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/client/studio.ts
|
|
212
|
+
var COMPARE_MESSAGE = "joyco-studio:compare";
|
|
213
|
+
function postCompare(target, tool, showSaved) {
|
|
214
|
+
const message = { type: COMPARE_MESSAGE, tool, showSaved };
|
|
215
|
+
target?.postMessage(message, location.origin);
|
|
216
|
+
}
|
|
217
|
+
function liveSchema(schema) {
|
|
218
|
+
return Object.fromEntries(
|
|
219
|
+
Object.entries(schema).map(([key, field]) => {
|
|
220
|
+
if (field.kind === "button") return [key, field];
|
|
221
|
+
if (field.kind === "folder")
|
|
222
|
+
return [
|
|
223
|
+
key,
|
|
224
|
+
{ ...field, definition: { ...field.definition, schema: () => liveSchema(field.definition.schema()) } }
|
|
225
|
+
];
|
|
226
|
+
return [key, { ...field, live: true }];
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
function resetSlidersOnDoubleClick(handle) {
|
|
231
|
+
return (folder, { fields }) => {
|
|
232
|
+
const sliders = fields().filter(({ field }) => field.kind === "slider");
|
|
233
|
+
const onDoubleClick = (event) => {
|
|
234
|
+
if (handle.comparing || event.target.closest("input")) return;
|
|
235
|
+
const slider = sliders.find(({ blade }) => blade.element.contains(event.target));
|
|
236
|
+
if (slider?.field.kind !== "slider") return;
|
|
237
|
+
const saved = slider.path.reduce(
|
|
238
|
+
(values, key) => values?.[key],
|
|
239
|
+
schemaValues(handle.definition.controls(structuredClone(handle.saved)))
|
|
240
|
+
);
|
|
241
|
+
if (typeof saved === "number") slider.field.write(saved);
|
|
242
|
+
};
|
|
243
|
+
folder.element.addEventListener("dblclick", onDoubleClick);
|
|
244
|
+
return () => folder.element.removeEventListener("dblclick", onDoubleClick);
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function everyFrame(refresh) {
|
|
248
|
+
let frame = requestAnimationFrame(function tick() {
|
|
249
|
+
refresh();
|
|
250
|
+
frame = requestAnimationFrame(tick);
|
|
251
|
+
});
|
|
252
|
+
return () => cancelAnimationFrame(frame);
|
|
253
|
+
}
|
|
254
|
+
var instances = globalThis.__joycoStudio ??= /* @__PURE__ */ new Map();
|
|
255
|
+
function createStudio(config, options = {}) {
|
|
256
|
+
const { tools } = config;
|
|
257
|
+
const runtime = options.runtime ?? config.runtime;
|
|
258
|
+
const instanceKey = runtime || JSON.stringify(tools.map(({ id, file }) => [id, file]).sort());
|
|
259
|
+
let bridge = null;
|
|
260
|
+
let connection = { phase: runtime ? "connecting" : "local" };
|
|
261
|
+
let version = 0;
|
|
262
|
+
const studioListeners = /* @__PURE__ */ new Set();
|
|
263
|
+
const touch = () => {
|
|
264
|
+
version++;
|
|
265
|
+
for (const listener of studioListeners) listener();
|
|
266
|
+
};
|
|
267
|
+
const createHandle = (definition) => {
|
|
268
|
+
const raw = structuredClone(definition.initial);
|
|
269
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
270
|
+
let pushing = Promise.resolve();
|
|
271
|
+
let scheduled = false;
|
|
272
|
+
let stashed = null;
|
|
273
|
+
const history = createHistory();
|
|
274
|
+
let snapshot = structuredClone(raw);
|
|
275
|
+
const changed = () => {
|
|
276
|
+
snapshot = structuredClone(raw);
|
|
277
|
+
handle.version++;
|
|
278
|
+
for (const listener of listeners) listener(handle.config);
|
|
279
|
+
touch();
|
|
280
|
+
};
|
|
281
|
+
const adopt = (state) => {
|
|
282
|
+
handle.state = state;
|
|
283
|
+
touch();
|
|
284
|
+
};
|
|
285
|
+
const receive = (state) => {
|
|
286
|
+
if (stashed) stashed = structuredClone(state.draft.value);
|
|
287
|
+
else assignDeep(raw, state.draft.value);
|
|
288
|
+
adopt(state);
|
|
289
|
+
if (!stashed) changed();
|
|
290
|
+
browserSync?.publish(state.draft.value);
|
|
291
|
+
};
|
|
292
|
+
const resync = async () => {
|
|
293
|
+
const result = await bridge?.request("tool.get", { tool: definition.id });
|
|
294
|
+
if (result?.ok) receive(result.data);
|
|
295
|
+
};
|
|
296
|
+
const push = () => {
|
|
297
|
+
if (scheduled || !bridge) return;
|
|
298
|
+
scheduled = true;
|
|
299
|
+
pushing = Promise.resolve().then(async () => {
|
|
300
|
+
scheduled = false;
|
|
301
|
+
const result = await bridge?.request("tool.update", { tool: definition.id, value: raw });
|
|
302
|
+
if (!result) return;
|
|
303
|
+
if (result.ok) adopt(result.data);
|
|
304
|
+
else if (result.error !== "runtime_unavailable") {
|
|
305
|
+
console.warn(`[studio] ${definition.id}: ${result.message ?? result.error}`);
|
|
306
|
+
await resync();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const edited = () => {
|
|
311
|
+
changed();
|
|
312
|
+
if (!stashed) {
|
|
313
|
+
browserSync?.publish(raw);
|
|
314
|
+
push();
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const restore = (patch) => {
|
|
318
|
+
if (!patch) return false;
|
|
319
|
+
for (const { path, value } of patch) writePath(raw, path, value);
|
|
320
|
+
edited();
|
|
321
|
+
return true;
|
|
322
|
+
};
|
|
323
|
+
const handle = {
|
|
324
|
+
definition,
|
|
325
|
+
config: live(raw, () => {
|
|
326
|
+
if (!stashed) history.record(snapshot, raw);
|
|
327
|
+
edited();
|
|
328
|
+
}),
|
|
329
|
+
state: null,
|
|
330
|
+
version: 0,
|
|
331
|
+
get saved() {
|
|
332
|
+
return handle.state?.saved.value ?? definition.initial;
|
|
333
|
+
},
|
|
334
|
+
get changes() {
|
|
335
|
+
return changedPaths(handle.saved, stashed ?? raw);
|
|
336
|
+
},
|
|
337
|
+
get dirty() {
|
|
338
|
+
return handle.changes.length > 0;
|
|
339
|
+
},
|
|
340
|
+
comparing: false,
|
|
341
|
+
compare(showSaved) {
|
|
342
|
+
if (showSaved === handle.comparing) return;
|
|
343
|
+
if (showSaved) stashed = structuredClone(raw);
|
|
344
|
+
const next = showSaved ? handle.saved : stashed;
|
|
345
|
+
assignDeep(raw, next);
|
|
346
|
+
if (!showSaved) stashed = null;
|
|
347
|
+
handle.comparing = showSaved;
|
|
348
|
+
changed();
|
|
349
|
+
},
|
|
350
|
+
get canUndo() {
|
|
351
|
+
return history.canUndo;
|
|
352
|
+
},
|
|
353
|
+
get canRedo() {
|
|
354
|
+
return history.canRedo;
|
|
355
|
+
},
|
|
356
|
+
undo: () => !stashed && restore(history.undo()),
|
|
357
|
+
redo: () => !stashed && restore(history.redo()),
|
|
358
|
+
subscribe(listener) {
|
|
359
|
+
listeners.add(listener);
|
|
360
|
+
return () => void listeners.delete(listener);
|
|
361
|
+
},
|
|
362
|
+
controls: () => definition.controls(handle.config),
|
|
363
|
+
debug() {
|
|
364
|
+
const actions = runtime ? {
|
|
365
|
+
save: button("Save", () => handle.save().then(fail), "Saved \u2713"),
|
|
366
|
+
discard: button("Discard", () => handle.discard().then(fail), "Discarded")
|
|
367
|
+
} : {};
|
|
368
|
+
return {
|
|
369
|
+
title: definition.title ?? definition.id,
|
|
370
|
+
schema: () => ({ ...liveSchema(handle.controls()), ...actions }),
|
|
371
|
+
extend: resetSlidersOnDoubleClick(handle)
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
async save({ force = false } = {}) {
|
|
375
|
+
if (!bridge) return error("runtime_unavailable");
|
|
376
|
+
await pushing;
|
|
377
|
+
const revision = handle.state?.draft.revision;
|
|
378
|
+
if (revision === void 0) return error("runtime_unavailable");
|
|
379
|
+
const result = await bridge.request("tool.save", { tool: definition.id, revision, force });
|
|
380
|
+
if (result.ok) adopt(result.data);
|
|
381
|
+
return result;
|
|
382
|
+
},
|
|
383
|
+
async discard() {
|
|
384
|
+
if (!bridge) return error("runtime_unavailable");
|
|
385
|
+
await pushing;
|
|
386
|
+
const before = snapshot;
|
|
387
|
+
const result = await bridge.request("tool.discard", { tool: definition.id });
|
|
388
|
+
if (result.ok) {
|
|
389
|
+
receive(result.data);
|
|
390
|
+
if (!stashed) history.record(before, raw, { merge: false });
|
|
391
|
+
}
|
|
392
|
+
return result;
|
|
393
|
+
},
|
|
394
|
+
receive,
|
|
395
|
+
resync,
|
|
396
|
+
close: () => browserSync?.close()
|
|
397
|
+
};
|
|
398
|
+
const browserSync = connectBrowserSync(definition, runtime, {
|
|
399
|
+
enabled: () => connection.phase !== "connected",
|
|
400
|
+
receive(value) {
|
|
401
|
+
if (stashed) {
|
|
402
|
+
stashed = value;
|
|
403
|
+
touch();
|
|
404
|
+
} else {
|
|
405
|
+
assignDeep(raw, value);
|
|
406
|
+
changed();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
return handle;
|
|
411
|
+
};
|
|
412
|
+
const handles = new Map(tools.filter(hasConfig).map((definition) => [definition.id, createHandle(definition)]));
|
|
413
|
+
const onMessage = (event) => {
|
|
414
|
+
const message = event.data;
|
|
415
|
+
if (event.origin !== location.origin || message?.type !== COMPARE_MESSAGE) return;
|
|
416
|
+
handles.get(message.tool)?.compare(message.showSaved);
|
|
417
|
+
};
|
|
418
|
+
if (typeof window !== "undefined") window.addEventListener?.("message", onMessage);
|
|
419
|
+
if (runtime && typeof window !== "undefined") {
|
|
420
|
+
bridge = connectBridge(runtime, {
|
|
421
|
+
state: (state) => handles.get(state.tool)?.receive(state),
|
|
422
|
+
connection(next) {
|
|
423
|
+
connection = next;
|
|
424
|
+
touch();
|
|
425
|
+
if (next.phase === "connected") for (const handle of handles.values()) void handle.resync();
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
const studio = {
|
|
430
|
+
definitions: tools,
|
|
431
|
+
tools: [...handles.values()],
|
|
432
|
+
tool(definition) {
|
|
433
|
+
const handle = handles.get(definition.id);
|
|
434
|
+
if (!handle) throw new Error(`[studio] tool "${definition.id}" was not passed to createStudio`);
|
|
435
|
+
return handle;
|
|
436
|
+
},
|
|
437
|
+
get connection() {
|
|
438
|
+
return connection;
|
|
439
|
+
},
|
|
440
|
+
get version() {
|
|
441
|
+
return version;
|
|
442
|
+
},
|
|
443
|
+
subscribe(listener) {
|
|
444
|
+
studioListeners.add(listener);
|
|
445
|
+
return () => void studioListeners.delete(listener);
|
|
446
|
+
},
|
|
447
|
+
close() {
|
|
448
|
+
bridge?.close();
|
|
449
|
+
for (const handle of handles.values()) handle.close();
|
|
450
|
+
studioListeners.clear();
|
|
451
|
+
if (typeof window !== "undefined") window.removeEventListener?.("message", onMessage);
|
|
452
|
+
if (instances.get(instanceKey) === studio) instances.delete(instanceKey);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
if (typeof window !== "undefined") {
|
|
456
|
+
instances.get(instanceKey)?.close();
|
|
457
|
+
instances.set(instanceKey, studio);
|
|
458
|
+
}
|
|
459
|
+
return studio;
|
|
460
|
+
}
|
|
461
|
+
function fail(result) {
|
|
462
|
+
if (!result.ok) throw new Error(result.message ?? result.error);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export {
|
|
466
|
+
postCompare,
|
|
467
|
+
everyFrame,
|
|
468
|
+
createStudio
|
|
469
|
+
};
|
|
470
|
+
//# sourceMappingURL=chunk-2RGQF5X5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client/studio.ts","../src/core/diff.ts","../src/client/bridge.ts","../src/client/browser-sync.ts","../src/client/history.ts","../src/client/live.ts"],"sourcesContent":["import {\n button,\n type DebugDefinition,\n type DebugField,\n type DebugFolder,\n type DebugSchema,\n schemaValues,\n} from '@joycostudio/xyz/debug'\nimport { changedPaths } from '../core/diff'\nimport type { ToolState } from '../core/protocol'\nimport { error, type Result } from '../core/result'\nimport { hasConfig, type StudioTool, type ToolDefinition, type ToolReference } from '../core/tool'\nimport { connectBridge, type BridgeClient, type Connection } from './bridge'\nimport { connectBrowserSync } from './browser-sync'\nimport { createHistory, writePath } from './history'\nimport { assignDeep, live } from './live'\n\nexport type ToolHandle<TConfig extends object = any> = {\n readonly definition: ToolDefinition<TConfig>\n /** The live config. Writes sync to same-origin pages and, when connected, the runtime. */\n readonly config: TConfig\n /** Runtime-owned state; null until the runtime answers (always null without one). */\n readonly state: ToolState<TConfig> | null\n /** Bumps on every config change. Handy as a React snapshot. */\n readonly version: number\n /** Called after every config change, local or remote. */\n subscribe(listener: (config: TConfig) => void): () => void\n /** The tool's Xyz schema bound to the live config. */\n controls(): DebugSchema\n /** Xyz `Debuggable`: `debugManager.register(handle)` mounts this tool in the app's floating pane. */\n debug(): DebugFolder\n /** The config on disk: the runtime's saved value or, until one answers, the bundled `initial`. */\n readonly saved: TConfig\n /** Dotted paths where the draft differs from `saved`. Tracked locally, so it works without a runtime too. */\n readonly changes: string[]\n /** The draft differs from `saved`. */\n readonly dirty: boolean\n /** True while `compare(true)` shows the saved config in place of the draft. */\n readonly comparing: boolean\n /**\n * Temporarily show `saved` (true) or go back to the draft (false). Local only: nothing\n * reaches the runtime, and edits made while comparing are dropped when it ends.\n */\n compare(showSaved: boolean): void\n /** True when `undo()` has an edit made on this page to step back through. */\n readonly canUndo: boolean\n readonly canRedo: boolean\n /**\n * Step back through edits made on this page (a slider drag counts as one), then sync the result like any\n * other edit. Only the leaves each edit changed are restored. Returns false with nothing to undo or while comparing.\n */\n undo(): boolean\n redo(): boolean\n save(options?: { force?: boolean }): Promise<Result<ToolState<TConfig>>>\n discard(): Promise<Result<ToolState<TConfig>>>\n}\n\nexport type Studio = {\n /** Every registered tool in order, including preview-only ones. */\n readonly definitions: readonly StudioTool[]\n /** A handle per tool with a config. Preview-only tools have none. */\n readonly tools: readonly ToolHandle[]\n tool<TConfig extends object>(definition: ToolReference<TConfig>): ToolHandle<TConfig>\n readonly connection: Connection\n /** Bumps on connection or any tool state change. */\n readonly version: number\n subscribe(listener: () => void): () => void\n close(): void\n}\n\nexport type StudioOptions = {\n tools: readonly StudioTool[]\n /**\n * Runtime bridge URL (e.g. `ws://127.0.0.1:5179`). Omit or pass false for browser-only syncing, as in production.\n * Only browsers connect, so a module-scope `createStudio` is safe to evaluate during SSR.\n */\n runtime?: string | false\n}\n\nconst COMPARE_MESSAGE = 'joyco-studio:compare'\ntype CompareMessage = { type: typeof COMPARE_MESSAGE; tool: string; showSaved: boolean }\n\n/** Make the studio running in `target` (e.g. a preview iframe) compare `tool` as well. */\nexport function postCompare(target: Window | null | undefined, tool: string, showSaved: boolean) {\n const message: CompareMessage = { type: COMPARE_MESSAGE, tool, showSaved }\n target?.postMessage(message, location.origin)\n}\n\n/** Mark every value control live so a pane registered with `registerFrame` reflects remote edits. */\nfunction liveSchema(schema: DebugSchema): DebugSchema {\n return Object.fromEntries(\n Object.entries(schema).map(([key, field]): [string, DebugField] => {\n if (field.kind === 'button') return [key, field]\n if (field.kind === 'folder')\n return [\n key,\n { ...field, definition: { ...field.definition, schema: () => liveSchema(field.definition.schema()) } },\n ]\n return [key, { ...field, live: true }]\n }),\n )\n}\n\n/**\n * Xyz `extend` for a folder bound to `handle.controls()` (optionally followed by more fields, as in `debug()`):\n * double-clicking a slider resets it to its saved value.\n */\nexport function resetSlidersOnDoubleClick(handle: ToolHandle): NonNullable<DebugDefinition['extend']> {\n return (folder, { fields }) => {\n const sliders = fields().filter(({ field }) => field.kind === 'slider')\n const onDoubleClick = (event: MouseEvent) => {\n // Double-clicking the number input selects its text instead.\n if (handle.comparing || (event.target as Element).closest('input')) return\n const slider = sliders.find(({ blade }) => blade.element.contains(event.target as Node))\n if (slider?.field.kind !== 'slider') return\n const saved = slider.path.reduce<unknown>(\n (values, key) => (values as Record<string, unknown> | undefined)?.[key],\n schemaValues(handle.definition.controls(structuredClone(handle.saved))),\n )\n if (typeof saved === 'number') slider.field.write(saved)\n }\n folder.element.addEventListener('dblclick', onDoubleClick)\n return () => folder.element.removeEventListener('dblclick', onDoubleClick)\n }\n}\n\n/** A `registerFrame` for Xyz's `DebugManager` / `bindDebugSchema`: refreshes live controls every animation frame. */\nexport function everyFrame(refresh: () => void) {\n let frame = requestAnimationFrame(function tick() {\n refresh()\n frame = requestAnimationFrame(tick)\n })\n return () => cancelAnimationFrame(frame)\n}\n\n/** One studio per runtime URL (or local tool set) per page; replace HMR instances without leaking transports. */\nconst instances: Map<string, Studio> = ((globalThis as { __joycoStudio?: Map<string, Studio> }).__joycoStudio ??=\n new Map())\n\nexport function createStudio(config: StudioOptions, options: Pick<StudioOptions, 'runtime'> = {}): Studio {\n const { tools } = config\n const runtime = options.runtime ?? config.runtime\n const instanceKey = runtime || JSON.stringify(tools.map(({ id, file }) => [id, file]).sort())\n let bridge: BridgeClient | null = null\n let connection: Connection = { phase: runtime ? 'connecting' : 'local' }\n let version = 0\n const studioListeners = new Set<() => void>()\n const touch = () => {\n version++\n for (const listener of studioListeners) listener()\n }\n\n const createHandle = <TConfig extends object>(definition: ToolDefinition<TConfig>) => {\n const raw = structuredClone(definition.initial)\n const listeners = new Set<(config: TConfig) => void>()\n let pushing: Promise<unknown> = Promise.resolve()\n let scheduled = false\n /** The draft to restore while comparing; null when showing the draft. */\n let stashed: TConfig | null = null\n const history = createHistory()\n /** A copy of `raw` as of the last change, so the next local edit can be diffed against it. */\n let snapshot = structuredClone(raw)\n\n const changed = () => {\n snapshot = structuredClone(raw)\n handle.version++\n for (const listener of listeners) listener(handle.config)\n touch()\n }\n const adopt = (state: ToolState) => {\n handle.state = state as ToolState<TConfig>\n touch()\n }\n const receive = (state: ToolState) => {\n if (stashed) stashed = structuredClone(state.draft.value) as TConfig\n else assignDeep(raw as Record<string, unknown>, state.draft.value as Record<string, unknown>)\n adopt(state)\n if (!stashed) changed()\n browserSync?.publish(state.draft.value as TConfig)\n }\n const resync = async () => {\n const result = await bridge?.request('tool.get', { tool: definition.id })\n if (result?.ok) receive(result.data)\n }\n const push = () => {\n if (scheduled || !bridge) return\n scheduled = true\n pushing = Promise.resolve().then(async () => {\n scheduled = false\n const result = await bridge?.request('tool.update', { tool: definition.id, value: raw })\n if (!result) return\n if (result.ok) adopt(result.data)\n else if (result.error !== 'runtime_unavailable') {\n console.warn(`[studio] ${definition.id}: ${result.message ?? result.error}`)\n await resync()\n }\n })\n }\n\n const edited = () => {\n changed()\n if (!stashed) {\n browserSync?.publish(raw)\n push()\n }\n }\n const restore = (patch: ReturnType<typeof history.undo>) => {\n if (!patch) return false\n for (const { path, value } of patch) writePath(raw as Record<string, unknown>, path, value)\n edited()\n return true\n }\n\n const handle = {\n definition,\n config: live(raw, () => {\n if (!stashed) history.record(snapshot, raw)\n edited()\n }),\n state: null as ToolState<TConfig> | null,\n version: 0,\n get saved(): TConfig {\n return handle.state?.saved.value ?? definition.initial\n },\n get changes() {\n return changedPaths(handle.saved, stashed ?? raw)\n },\n get dirty() {\n return handle.changes.length > 0\n },\n comparing: false,\n compare(showSaved: boolean) {\n if (showSaved === handle.comparing) return\n if (showSaved) stashed = structuredClone(raw)\n const next = showSaved ? handle.saved : stashed!\n assignDeep(raw as Record<string, unknown>, next as Record<string, unknown>)\n if (!showSaved) stashed = null\n handle.comparing = showSaved\n changed()\n },\n get canUndo() {\n return history.canUndo\n },\n get canRedo() {\n return history.canRedo\n },\n undo: () => !stashed && restore(history.undo()),\n redo: () => !stashed && restore(history.redo()),\n subscribe(listener: (config: TConfig) => void) {\n listeners.add(listener)\n return () => void listeners.delete(listener)\n },\n controls: () => definition.controls(handle.config),\n debug(): DebugFolder {\n const actions: DebugSchema = runtime\n ? {\n save: button('Save', () => handle.save().then(fail), 'Saved ✓'),\n discard: button('Discard', () => handle.discard().then(fail), 'Discarded'),\n }\n : {}\n return {\n title: definition.title ?? definition.id,\n schema: () => ({ ...liveSchema(handle.controls()), ...actions }),\n extend: resetSlidersOnDoubleClick(handle),\n }\n },\n async save({ force = false } = {}) {\n if (!bridge) return error('runtime_unavailable')\n await pushing\n const revision = handle.state?.draft.revision\n if (revision === undefined) return error('runtime_unavailable')\n const result = await bridge.request('tool.save', { tool: definition.id, revision, force })\n if (result.ok) adopt(result.data)\n return result as Result<ToolState<TConfig>>\n },\n async discard() {\n if (!bridge) return error('runtime_unavailable')\n await pushing\n const before = snapshot\n const result = await bridge.request('tool.discard', { tool: definition.id })\n if (result.ok) {\n receive(result.data)\n // Discarding is undoable, as its own step.\n if (!stashed) history.record(before, raw, { merge: false })\n }\n return result as Result<ToolState<TConfig>>\n },\n receive,\n resync,\n close: () => browserSync?.close(),\n }\n const browserSync = connectBrowserSync(definition, runtime, {\n enabled: () => connection.phase !== 'connected',\n receive(value) {\n if (stashed) {\n // The shown saved config stays put, but `changes` and `dirty` read the new draft.\n stashed = value\n touch()\n } else {\n assignDeep(raw as Record<string, unknown>, value as Record<string, unknown>)\n changed()\n }\n },\n })\n return handle\n }\n\n const handles = new Map(tools.filter(hasConfig).map((definition) => [definition.id, createHandle(definition)]))\n\n // /studio asks its same-origin preview iframe to compare too (see `postCompare`).\n const onMessage = (event: MessageEvent) => {\n const message = event.data as CompareMessage | undefined\n if (event.origin !== location.origin || message?.type !== COMPARE_MESSAGE) return\n handles.get(message.tool)?.compare(message.showSaved)\n }\n if (typeof window !== 'undefined') window.addEventListener?.('message', onMessage)\n\n if (runtime && typeof window !== 'undefined') {\n bridge = connectBridge(runtime, {\n state: (state) => handles.get(state.tool)?.receive(state),\n connection(next) {\n connection = next\n touch()\n if (next.phase === 'connected') for (const handle of handles.values()) void handle.resync()\n },\n })\n }\n\n const studio: Studio = {\n definitions: tools,\n tools: [...handles.values()],\n tool<TConfig extends object>(definition: ToolDefinition<TConfig>) {\n const handle = handles.get(definition.id)\n if (!handle) throw new Error(`[studio] tool \"${definition.id}\" was not passed to createStudio`)\n return handle as unknown as ToolHandle<TConfig>\n },\n get connection() {\n return connection\n },\n get version() {\n return version\n },\n subscribe(listener) {\n studioListeners.add(listener)\n return () => void studioListeners.delete(listener)\n },\n close() {\n bridge?.close()\n for (const handle of handles.values()) handle.close()\n studioListeners.clear()\n if (typeof window !== 'undefined') window.removeEventListener?.('message', onMessage)\n if (instances.get(instanceKey) === studio) instances.delete(instanceKey)\n },\n }\n if (typeof window !== 'undefined') {\n instances.get(instanceKey)?.close()\n instances.set(instanceKey, studio)\n }\n return studio\n}\n\nfunction fail(result: Result<unknown>) {\n if (!result.ok) throw new Error(result.message ?? result.error)\n}\n","export type Change = { path: string[]; before: unknown; after: unknown }\n\n/** Every leaf that differs between two configs of the same shape. Arrays count as leaves. */\nexport function diff(before: unknown, after: unknown, path: string[] = []): Change[] {\n const nested = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n if (nested(before) && nested(after)) {\n const keys = new Set([...Object.keys(before), ...Object.keys(after)])\n return [...keys].flatMap((key) => diff(before[key], after[key], [...path, key]))\n }\n return JSON.stringify(before) === JSON.stringify(after) ? [] : [{ path, before, after }]\n}\n\n/** Dotted paths of every leaf that differs between two configs of the same shape. */\nexport function changedPaths(before: unknown, after: unknown): string[] {\n return diff(before, after).map(({ path }) => path.join('.'))\n}\n","import type { Op, Operations, ServerMessage, ToolState } from '../core/protocol'\nimport { error, type Result } from '../core/result'\n\nexport type Connection = { phase: 'local' } | { phase: 'connecting' } | { phase: 'connected' } | { phase: 'offline' }\n\ntype Pending = (result: Result<unknown>) => void\n\n/** A reconnecting WebSocket to the Studio runtime with request/response correlation. */\nexport function connectBridge(\n url: string,\n handlers: { state: (state: ToolState) => void; connection: (connection: Connection) => void },\n) {\n let socket: WebSocket | null = null\n let nextId = 0\n let retry = 0\n let timer: ReturnType<typeof setTimeout> | undefined\n let closed = false\n const pending = new Map<number, Pending>()\n\n const open = () => {\n handlers.connection({ phase: 'connecting' })\n const ws = new WebSocket(url)\n socket = ws\n ws.onopen = () => {\n retry = 0\n handlers.connection({ phase: 'connected' })\n }\n ws.onmessage = (event) => {\n const message = JSON.parse(String(event.data)) as ServerMessage\n if (message.type === 'state') return handlers.state(message.state)\n pending.get(message.id)?.(message.result)\n pending.delete(message.id)\n }\n ws.onclose = () => {\n socket = null\n for (const resolve of pending.values()) resolve(error('runtime_unavailable'))\n pending.clear()\n if (closed) return\n handlers.connection({ phase: 'offline' })\n timer = setTimeout(open, Math.min(500 * 2 ** retry++, 5000))\n }\n }\n open()\n\n return {\n request<TOp extends Op>(op: TOp, params: Operations[TOp]['params']): Promise<Result<Operations[TOp]['result']>> {\n if (socket?.readyState !== WebSocket.OPEN) return Promise.resolve(error('runtime_unavailable'))\n const id = nextId++\n socket.send(JSON.stringify({ id, op, params }))\n return new Promise((resolve) => pending.set(id, resolve as Pending))\n },\n close() {\n closed = true\n clearTimeout(timer)\n socket?.close()\n },\n }\n}\n\nexport type BridgeClient = ReturnType<typeof connectBridge>\n","import type { ToolDefinition } from '../core/tool'\nimport { validateConfig } from '../core/validate'\n\ntype Snapshot = { type: 'config'; clock: number; sender: string; value: unknown }\n\n/** Ephemeral config sharing between same-origin pages, including Studio's preview iframe. */\nexport function connectBrowserSync<T extends object>(\n definition: ToolDefinition<T>,\n runtime: string | false | undefined,\n handlers: { enabled(): boolean; receive(value: T): void },\n) {\n // Also exclude SSR and Node environments that expose their own BroadcastChannel implementation.\n if (typeof window === 'undefined' || !window.location?.origin || !window.BroadcastChannel) return null\n\n let channel: BroadcastChannel\n try {\n channel = new window.BroadcastChannel(\n JSON.stringify(['joyco-studio:config', runtime || null, definition.id, definition.file]),\n )\n } catch {\n return null\n }\n const sender = crypto.getRandomValues(new Uint32Array(4)).join('-')\n let clock = 0\n let latest: Snapshot | undefined\n let closed = false\n\n channel.onmessage = ({ data }) => {\n if (closed || !data || typeof data !== 'object') return\n if (data.type === 'request') {\n if (latest) channel.postMessage(latest)\n return\n }\n if (\n data.type !== 'config' ||\n !Number.isSafeInteger(data.clock) ||\n data.clock < 1 ||\n typeof data.sender !== 'string'\n )\n return\n clock = Math.max(clock, data.clock)\n if (!handlers.enabled()) return\n // A stable tie-break keeps simultaneous edits and multiple snapshot replies from diverging.\n if (latest && (data.clock < latest.clock || (data.clock === latest.clock && data.sender <= latest.sender))) return\n const result = validateConfig(definition, data.value)\n if (!result.ok) return\n latest = { type: 'config', clock: data.clock, sender: data.sender, value: result.data }\n handlers.receive(result.data)\n }\n // A newly mounted/reloaded preview asks existing peers for their current draft.\n channel.postMessage({ type: 'request' })\n\n return {\n publish(value: T) {\n if (closed) return\n latest = { type: 'config', clock: ++clock, sender, value: structuredClone(value) }\n channel.postMessage(latest)\n },\n close() {\n closed = true\n channel.onmessage = null\n channel.close()\n },\n }\n}\n","import { diff, type Change } from '../core/diff'\n\ntype Entry = { changes: Change[]; key: string; at: number }\n\nconst LIMIT = 100\n/** Edits to the same leaves this close together (a slider drag) undo as one step. */\nconst MERGE_MS = 500\n\n/**\n * Undo/redo for one page's edits. Entries hold only the leaves an edit changed, so stepping back\n * never reverts what another page, the runtime, or an agent changed in between.\n */\nexport function createHistory() {\n const past: Entry[] = []\n let future: Entry[] = []\n /** The newest entry while later edits to the same leaves may still extend it. */\n let open: Entry | null = null\n\n const step = (from: Entry[], to: Entry[], side: 'before' | 'after') => {\n const entry = from.pop()\n if (!entry) return null\n to.push(entry)\n open = null\n return entry.changes.map((change) => ({ path: change.path, value: change[side] }))\n }\n\n return {\n get canUndo() {\n return past.length > 0\n },\n get canRedo() {\n return future.length > 0\n },\n /** Record the edit between two snapshots. `merge: false` keeps it a step of its own (e.g. Discard). */\n record(before: unknown, after: unknown, { merge = true } = {}) {\n // Cloned so later in-place edits (e.g. to an array leaf) cannot rewrite history.\n const changes = structuredClone(diff(before, after))\n if (!changes.length) return\n future = []\n const key = JSON.stringify(changes.map(({ path }) => path))\n const now = Date.now()\n if (merge && open && open.key === key && now - open.at < MERGE_MS) {\n open.changes = open.changes.map((change, i) => ({ ...change, after: changes[i]!.after }))\n open.at = now\n // Dragged back to where it started: nothing left to undo.\n if (open.changes.every((change) => JSON.stringify(change.before) === JSON.stringify(change.after))) {\n past.pop()\n open = null\n }\n return\n }\n const entry = { changes, key, at: now }\n past.push(entry)\n if (past.length > LIMIT) past.shift()\n open = merge ? entry : null\n },\n /** The leaves to write back, or null when there is nothing to undo. */\n undo: () => step(past, future, 'before'),\n redo: () => step(future, past, 'after'),\n }\n}\n\n/** Write `value` at `path` in place; undefined removes the key (it did not exist before the edit). */\nexport function writePath(target: Record<string, unknown>, path: string[], value: unknown) {\n const key = path.at(-1)!\n let parent = target\n for (const segment of path.slice(0, -1)) {\n const next = parent[segment]\n parent = (typeof next === 'object' && next !== null ? next : (parent[segment] = {})) as Record<string, unknown>\n }\n if (value === undefined) delete parent[key]\n else parent[key] = structuredClone(value)\n}\n","/**\n * A deep proxy that reports every write, so plain `property(config, 'key')`\n * bindings from Xyz feed Studio without the tool author wiring callbacks.\n */\nexport function live<T extends object>(target: T, onWrite: () => void): T {\n const proxies = new WeakMap<object, object>()\n const wrap = <O extends object>(object: O): O => {\n let proxy = proxies.get(object)\n if (!proxy) {\n proxy = new Proxy(object, {\n get(target, key, receiver) {\n const value = Reflect.get(target, key, receiver)\n return typeof value === 'object' && value !== null ? wrap(value) : value\n },\n set(target, key, value, receiver) {\n const changed = Reflect.get(target, key, receiver) !== value\n const ok = Reflect.set(target, key, value, receiver)\n if (changed) onWrite()\n return ok\n },\n })\n proxies.set(object, proxy)\n }\n return proxy as O\n }\n return wrap(target)\n}\n\n/** Copy `source` into `target` in place, keeping nested object identities (and every binding to them) intact. */\nexport function assignDeep(target: Record<string, unknown>, source: Record<string, unknown>) {\n for (const [key, value] of Object.entries(source)) {\n const current = target[key]\n const nested = (v: unknown) => typeof v === 'object' && v !== null && !Array.isArray(v)\n if (nested(current) && nested(value))\n assignDeep(current as Record<string, unknown>, value as Record<string, unknown>)\n else target[key] = structuredClone(value)\n }\n}\n"],"mappings":"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","names":["target"]}
|
|
@@ -3,10 +3,11 @@ var hasConfig = (tool) => tool.controls !== void 0;
|
|
|
3
3
|
var ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
4
4
|
function defineTool(tool) {
|
|
5
5
|
if (!ID.test(tool.id)) throw new Error(`[studio] tool id "${tool.id}" must be kebab-case`);
|
|
6
|
-
const
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const value = tool;
|
|
7
|
+
if (value.file !== void 0 || value.initial !== void 0) {
|
|
8
|
+
if (value.file === void 0 || value.initial === void 0 || value.controls === void 0)
|
|
9
|
+
throw new Error(`[studio] tool "${tool.id}" needs all of file, initial, and controls, or none of them`);
|
|
10
|
+
}
|
|
10
11
|
return tool;
|
|
11
12
|
}
|
|
12
13
|
var DEFAULT_PORT = 5179;
|
|
@@ -97,4 +98,4 @@ export {
|
|
|
97
98
|
error,
|
|
98
99
|
validateConfig
|
|
99
100
|
};
|
|
100
|
-
//# sourceMappingURL=chunk-
|
|
101
|
+
//# sourceMappingURL=chunk-DEJYGBPJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/tool.ts","../src/core/result.ts","../src/core/validate.ts"],"sourcesContent":["import type { DebugSchema } from '@joycostudio/xyz/debug'\n\n/**\n * An SVG icon as data: `[tag, attributes]` children of a 24×24 stroke icon.\n * Lucide's core package (`import { Palette } from 'lucide'`) exports exactly this shape.\n */\nexport type ToolIcon = readonly (readonly [tag: string, attrs: Record<string, string | number | undefined>])[]\n\ntype ToolBase = {\n /** Unique, URL-safe identifier. */\n id: string\n title?: string\n /** Shown next to the tool in /studio. Data, not a component, so contracts stay loadable in Node. */\n icon?: ToolIcon\n /** App route that renders this tool's result, shown next to the controls in /studio. */\n preview?: string\n /** Use a bare iframe for tools that already own their canvas layout and headers. */\n previewLayout?: 'canvas' | 'custom'\n}\n\n/**\n * A tool's contract. Isomorphic on purpose: the runtime loads it in Node to\n * validate and persist, the app loads it in the browser to apply and debug.\n *\n * `TConfig` is the one type that is read, edited, applied, and persisted.\n */\nexport type ToolDefinition<TConfig extends object = object> = ToolBase & {\n /** Repo-relative JSON file the config persists to. Import that same file as `initial`. */\n file: string\n /** Config used before the runtime answers (and in production). */\n initial: TConfig\n /**\n * What can be tweaked, as an Xyz/Tweakpane schema bound to `config`.\n * Doubles as the runtime validator: every control's bounds are enforced on write.\n */\n controls: (config: TConfig) => DebugSchema\n}\n\n/** A tool with nothing to tune: a sidebar entry for its preview, without an inspector or config file. */\nexport type PreviewToolDefinition = ToolBase & {\n preview: string\n file?: never\n initial?: never\n controls?: never\n}\n\n/** A tool with a config. */\nexport type AnyTool = ToolDefinition<any>\n\n/** Anything `defineTool` returns: the kind follows from whether it declares a config. */\nexport type StudioTool = AnyTool | PreviewToolDefinition\n\nexport const hasConfig = (tool: StudioTool): tool is AnyTool => tool.controls !== undefined\n\n/** Filled by .studio/types.generated.ts in each consuming project. */\nexport interface DiscoveredToolConfigs {}\n\n/** A discovered declaration is resolved into a full definition by the generated registry. */\nexport type DiscoveredToolDefinition<TConfig extends object = object> = ToolBase & {\n /** JSON source relative to this tool file, or a tsconfig path alias. Defaults to ./config.json. */\n config?: string\n controls: (config: TConfig) => DebugSchema\n file?: never\n initial?: never\n}\nexport type ToolReference<TConfig extends object> = Pick<ToolDefinition<TConfig>, 'id' | 'controls'>\nexport type ConfigOf<TTool> = TTool extends ToolReference<infer TConfig> ? TConfig : never\n\nconst ID = /^[a-z0-9][a-z0-9-]*$/\n\n/** Discovered tools infer their config from the generated ID-to-JSON type map. */\nexport function defineTool<K extends keyof DiscoveredToolConfigs & string>(\n tool: Omit<DiscoveredToolDefinition<Extract<DiscoveredToolConfigs[K], object>>, 'id'> & { id: K },\n): DiscoveredToolDefinition<Extract<DiscoveredToolConfigs[K], object>>\n/** Existing explicitly registered tools remain supported. */\nexport function defineTool<TConfig extends object>(tool: ToolDefinition<TConfig>): ToolDefinition<TConfig>\nexport function defineTool(tool: PreviewToolDefinition): PreviewToolDefinition\n/** A discovered preview-only tool may infer its preview from the adjacent page. */\nexport function defineTool(\n tool: ToolBase & { controls?: never; config?: never; file?: never; initial?: never },\n): ToolBase\nexport function defineTool(tool: StudioTool | DiscoveredToolDefinition<any> | ToolBase) {\n if (!ID.test(tool.id)) throw new Error(`[studio] tool id \"${tool.id}\" must be kebab-case`)\n const value = tool as Partial<AnyTool>\n if (value.file !== undefined || value.initial !== undefined) {\n if (value.file === undefined || value.initial === undefined || value.controls === undefined)\n throw new Error(`[studio] tool \"${tool.id}\" needs all of file, initial, and controls, or none of them`)\n }\n return tool\n}\n\nexport type StudioConfig = {\n /** Explicit registry; omit to enable convention-based discovery. */\n tools?: readonly StudioTool[]\n /**\n * Directory to scan and watch for `studio.tool.ts` files, relative to\n * the project root.\n *\n * Defaults to the active Next.js app directory: `app/` or `src/app/`.\n * Set a narrower directory to reduce scanning and discovery triggers.\n * This does not change preview URLs.\n *\n * @example 'src/app/experiments'\n */\n toolsDir?: string\n /** Runtime WebSocket and HTTP MCP port. Defaults to 5179. */\n port?: number\n /** Browser origins allowed on the bridge and MCP endpoint. Defaults to localhost origins. */\n origins?: readonly string[]\n /** Running app URL (e.g. http://localhost:3000), used for MCP preview captures. */\n appUrl?: string\n}\n\nexport const DEFAULT_PORT = 5179\n\nexport function defineConfig<T extends StudioConfig>(config: T): T {\n return config\n}\n","export type Success<T> = { ok: true; data: T }\n\nexport type Failure = {\n ok: false\n /** snake_case wire code, e.g. `invalid_config`. */\n error: string\n message?: string\n}\n\nexport type Result<T> = Success<T> | Failure\n\nexport const success = <T>(data: T): Success<T> => ({ ok: true, data })\n\nexport const error = (code: string, message?: string): Failure => ({\n ok: false,\n error: code,\n ...(message === undefined ? {} : { message }),\n})\n","import type { DebugSchema } from '@joycostudio/xyz/debug'\nimport { error, success, type Result } from './result'\nimport type { ToolDefinition } from './tool'\n\n/**\n * Accept `candidate` as a config for `tool` only if it has the exact shape of\n * `tool.initial` and every control in `tool.controls` accepts its value.\n * Returns a detached copy, never the input.\n */\nexport function validateConfig<TConfig extends object>(\n tool: ToolDefinition<TConfig>,\n candidate: unknown,\n): Result<TConfig> {\n const mismatch = shapeMismatch(tool.initial, candidate, 'config')\n if (mismatch) return error('invalid_config', mismatch)\n const config = structuredClone(candidate) as TConfig\n const invalid = invalidControl(tool.controls(config))\n return invalid ? error('invalid_config', invalid) : success(config)\n}\n\nfunction kind(value: unknown) {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value\n}\n\nfunction shapeMismatch(reference: unknown, value: unknown, path: string): string | null {\n const expected = kind(reference)\n if (kind(value) !== expected) return `${path}: expected ${expected}, got ${kind(value)}`\n if (expected === 'number' && !Number.isFinite(value)) return `${path}: expected a finite number`\n if (expected === 'array') {\n const [first] = reference as unknown[]\n if (first === undefined) return null\n for (const [index, item] of (value as unknown[]).entries()) {\n const nested = shapeMismatch(first, item, `${path}[${index}]`)\n if (nested) return nested\n }\n }\n if (expected === 'object') {\n const ref = reference as Record<string, unknown>\n const obj = value as Record<string, unknown>\n for (const key of Object.keys(obj)) if (!(key in ref)) return `${path}.${key}: unknown key`\n for (const key of Object.keys(ref)) {\n if (!(key in obj)) return `${path}.${key}: missing`\n const nested = shapeMismatch(ref[key], obj[key], `${path}.${key}`)\n if (nested) return nested\n }\n }\n return null\n}\n\nconst isRgb = (value: unknown) =>\n typeof value === 'object' &&\n value !== null &&\n ['r', 'g', 'b'].every((channel) => Number.isFinite((value as Record<string, unknown>)[channel]))\n\nfunction invalidControl(schema: DebugSchema, path = ''): string | null {\n for (const [key, field] of Object.entries(schema)) {\n if (field.kind === 'button') continue\n const name = `${path}${field.label ?? key}`\n if (field.kind === 'folder') {\n const nested = invalidControl(field.definition.schema(), `${name}/`)\n if (nested) return nested\n continue\n }\n const value: unknown = field.read()\n if (field.kind === 'slider') {\n if (typeof value !== 'number' || value < field.min || value > field.max)\n return `${name}: expected a number in [${field.min}, ${field.max}], got ${JSON.stringify(value)}`\n } else if (field.kind === 'bool') {\n if (typeof value !== 'boolean') return `${name}: expected a boolean`\n } else if (field.kind === 'select') {\n const options = Object.values(field.options)\n if (!options.includes(value as string | number))\n return `${name}: expected one of ${JSON.stringify(options)}, got ${JSON.stringify(value)}`\n } else if (field.kind === 'color-string') {\n if (typeof value !== 'string') return `${name}: expected a color string`\n } else if (field.kind === 'color') {\n if (!isRgb(value)) return `${name}: expected an { r, g, b } color`\n }\n }\n return null\n}\n"],"mappings":"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","names":[]}
|