@tailorkit/react 0.1.0-beta.18 → 0.1.0-beta.19

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/index.js CHANGED
@@ -1,522 +1,170 @@
1
- // src/components/app-view.tsx
2
- import { createSessionProvider } from "@tailorkit/app/client";
3
- import { useCallback as useCallback2, useEffect as useEffect2, useId as useId2, useMemo as useMemo3, useRef as useRef3, useSyncExternalStore as useSyncExternalStore2 } from "react";
4
- import { isViewAncestor as isViewAncestor2 } from "@tailorkit/core/views";
1
+ // src/components/slot.tsx
2
+ import { createSessionProvider as createSessionProvider2 } from "@tailorkit/app/client";
3
+ import { useCallback as useCallback3, useEffect as useEffect3, useId as useId2, useMemo as useMemo4, useRef as useRef4, useSyncExternalStore as useSyncExternalStore3 } from "react";
5
4
 
6
- // src/view-registry.ts
7
- import { getViewDepth, isViewAncestor } from "@tailorkit/core/views";
8
- function createViewRegistry() {
9
- const entries = /* @__PURE__ */ new Map();
10
- const listeners = /* @__PURE__ */ new Set();
11
- let snapshot = null;
12
- let nextOrder = 0;
13
- let scheduled = false;
14
- const publish = () => {
15
- if (scheduled) {
16
- return;
17
- }
18
- scheduled = true;
19
- queueMicrotask(() => {
20
- scheduled = false;
21
- const ordered = [...entries.values()].toSorted(
22
- (a, b) => getViewDepth(b.view) - getViewDepth(a.view) || b.order - a.order
23
- );
24
- const selected = ordered[0];
25
- if (selected) {
26
- const peers = ordered.filter(
27
- (entry) => getViewDepth(entry.view) === getViewDepth(selected.view)
28
- );
29
- if (peers.length > 1) {
30
- console.warn(
31
- `TailorKit found multiple active views at the same hierarchy depth: ${peers.toReversed().map((entry) => `"${entry.view}"`).join(
32
- ", "
33
- )}. TailorKit selected "${selected.view}" by mount order. Only one route at a hierarchy depth should call useView.`
34
- );
35
- }
36
- snapshot = {
37
- view: selected.view,
38
- layers: ordered.filter((entry) => isViewAncestor(entry.view, selected.view)).toReversed().map((entry) => ({ path: entry.view, context: entry.context, status: entry.status }))
39
- };
40
- } else {
41
- snapshot = null;
42
- }
43
- for (const listener of listeners) {
44
- listener();
45
- }
46
- });
47
- };
5
+ // src/hooks/use-stable-context.ts
6
+ import { useRef } from "react";
7
+ function useStableContext(value) {
8
+ const key = JSON.stringify(value);
9
+ const ref = useRef({ key, value });
10
+ if (ref.current.key !== key) {
11
+ ref.current = { key, value };
12
+ }
13
+ return ref.current.value;
14
+ }
15
+
16
+ // src/hooks/use-slot-instances.ts
17
+ import { createClient, createSessionProvider, reference } from "@tailorkit/app/client";
18
+
19
+ // src/slot-view.ts
20
+ import { composeViewContext, getViewHierarchy } from "@tailorkit/core/views";
21
+ function selectSlotView(views, slot, activePath, schema) {
22
+ const supported = schema.slots[slot]?.views ?? [];
23
+ const path = getViewHierarchy(activePath).find(
24
+ (path2) => supported.includes(path2) && views.some((view2) => view2.slot === slot && view2.path === path2)
25
+ );
26
+ const view = views.find((view2) => view2.slot === slot && view2.path === path);
27
+ return !view || view.disabled ? null : view;
28
+ }
29
+ function resolveSlotView(views, slot, activeView, schema) {
30
+ const view = selectSlotView(views, slot, activeView.view, schema);
31
+ if (!view) return null;
48
32
  return {
49
- getSnapshot: () => snapshot,
50
- subscribe(listener) {
51
- listeners.add(listener);
52
- return () => {
53
- listeners.delete(listener);
54
- };
55
- },
56
- register(entry) {
57
- entries.set(entry.id, { ...entry, order: entries.get(entry.id)?.order ?? nextOrder++ });
58
- publish();
59
- },
60
- unregister(id) {
61
- entries.delete(id);
62
- publish();
63
- }
33
+ ...composeViewContext(view.path, {
34
+ layers: activeView.layers,
35
+ declaredViews: Object.keys(schema.views)
36
+ }),
37
+ instances: Boolean(view.instances)
64
38
  };
65
39
  }
66
40
 
67
- // src/preview-manager.ts
68
- import {
69
- createPreviewWebSocketClient,
70
- previewMetadataSchema
71
- } from "@tailorkit/client-platform/preview";
72
- var empty = { revision: 0, source: null };
73
- function viewerTokenExpiresAt(metadata) {
74
- const expiresAt = Number(metadata.token.split(".", 1)[0]);
75
- return Number.isSafeInteger(expiresAt) ? expiresAt : 0;
76
- }
77
- function bytesFromBase64(value) {
78
- const raw = atob(value);
79
- const bytes = new Uint8Array(raw.length);
80
- for (let index = 0; index < raw.length; index++) {
81
- bytes[index] = raw.codePointAt(index) ?? 0;
41
+ // src/hooks/use-slot-instances.ts
42
+ import { useCallback, useEffect, useMemo, useRef as useRef2, useState, useSyncExternalStore } from "react";
43
+
44
+ // src/components/context.ts
45
+ import { createContext, useContext } from "react";
46
+ var TailorRootContext = createContext(null);
47
+ function useTailorRootContext(component, expectedClient) {
48
+ const context = useContext(TailorRootContext);
49
+ if (!context) {
50
+ throw new Error(`${component} must be rendered inside Root.`);
82
51
  }
83
- return bytes;
84
- }
85
- async function verify(candidate) {
86
- let source = null;
87
- for (const [fileIndex, file] of candidate.manifest.files.entries()) {
88
- const parts = candidate.files[fileIndex];
89
- if (!parts || parts.length !== file.chunks || parts.some((part) => part === null)) {
90
- return null;
91
- }
92
- const bytes = new Uint8Array(file.size);
93
- let offset = 0;
94
- for (const part of parts) {
95
- if (!part) {
96
- return null;
97
- }
98
- bytes.set(part, offset);
99
- offset += part.length;
100
- }
101
- if (offset !== file.size) {
102
- return null;
103
- }
104
- const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
105
- if (digest !== file.sha256) {
106
- return null;
107
- }
108
- if (file.path === "client.js") {
109
- source = new TextDecoder().decode(bytes);
110
- }
52
+ if (expectedClient && context.client !== expectedClient) {
53
+ throw new Error(
54
+ `${component} was created for a different TailorKit client than the one passed to Root.`
55
+ );
111
56
  }
112
- return source;
57
+ return context;
113
58
  }
114
- function createPreviewManager(baseUrl, onEnded) {
115
- const entries = /* @__PURE__ */ new Map();
116
- const notify = (entry) => {
117
- for (const listener of entry.listeners) {
118
- listener();
119
- }
120
- };
121
- const close = (entry) => {
122
- if (entry.reconnect) {
123
- clearTimeout(entry.reconnect);
124
- }
125
- entry.reconnect = null;
126
- const socket = entry.socket;
127
- entry.socket = null;
128
- socket?.close();
129
- entry.connecting = false;
130
- entry.candidate = null;
131
- };
132
- const end = (entry) => {
133
- close(entry);
134
- entry.metadata = null;
135
- entry.snapshot = empty;
136
- notify(entry);
137
- onEnded();
138
- };
139
- const handleEvent = async (entry, event) => {
140
- if (event.type === "ended") {
141
- end(entry);
142
- return;
143
- }
144
- if (event.revision <= entry.snapshot.revision) {
145
- return;
146
- }
147
- if (event.type === "begin") {
148
- entry.candidate = {
149
- revision: event.revision,
150
- manifest: event.manifest,
151
- files: event.manifest.files.map((file) => Array.from({ length: file.chunks }, () => null))
152
- };
153
- return;
154
- }
155
- const candidate = entry.candidate;
156
- if (!candidate || candidate.revision !== event.revision) {
157
- return;
158
- }
159
- if (event.type === "chunk") {
160
- const file = candidate.manifest.files[event.fileIndex];
161
- const slots = candidate.files[event.fileIndex];
162
- if (!file || !slots || event.chunkIndex < 0 || event.chunkIndex >= slots.length || slots[event.chunkIndex]) {
163
- entry.candidate = null;
164
- return;
165
- }
166
- const bytes = bytesFromBase64(event.base64);
167
- if (bytes.length !== Math.min(256 * 1024, file.size - event.chunkIndex * 256 * 1024)) {
168
- entry.candidate = null;
169
- return;
59
+
60
+ // src/hooks/use-slot-instances.ts
61
+ var resolveInstances = reference("_tailorkit.instances.resolve", "action");
62
+ function useSlotInstances({ app, slot }) {
63
+ const { store } = useTailorRootContext("useSlotInstances");
64
+ const activeView = useSyncExternalStore(
65
+ store.views.subscribe,
66
+ store.views.getSnapshot,
67
+ store.views.getSnapshot
68
+ );
69
+ const request = useStableContext({
70
+ baseUrl: store.baseUrl.toString(),
71
+ appId: app.id,
72
+ deploymentId: app.currentDeployment?.id,
73
+ views: app.views ?? [],
74
+ slot,
75
+ activeView
76
+ });
77
+ const getSession = useMemo(
78
+ () => createSessionProvider({ baseUrl: store.baseUrl, appId: app.id }),
79
+ [store, app.id, app.currentDeployment?.id]
80
+ );
81
+ const [snapshot, setSnapshot] = useState({
82
+ request: null,
83
+ data: void 0,
84
+ error: null,
85
+ status: "idle"
86
+ });
87
+ const generation = useRef2(0);
88
+ const backend = useRef2(null);
89
+ const refetch = useCallback(async () => {
90
+ const id = ++generation.current;
91
+ backend.current?.close();
92
+ backend.current = null;
93
+ const publish = (status2, data, error = null) => {
94
+ if (id === generation.current) {
95
+ setSnapshot({
96
+ request,
97
+ status: status2,
98
+ data,
99
+ error
100
+ });
170
101
  }
171
- slots[event.chunkIndex] = bytes;
102
+ };
103
+ if (!request.activeView) {
104
+ publish("idle");
172
105
  return;
173
106
  }
174
- if (event.type === "complete") {
175
- const source = await verify(candidate);
176
- if (source !== null && entry.candidate === candidate && candidate.revision > entry.snapshot.revision) {
177
- entry.snapshot = { revision: candidate.revision, source };
178
- notify(entry);
179
- }
180
- if (entry.candidate === candidate) {
181
- entry.candidate = null;
182
- }
183
- }
184
- };
185
- const connect = async (entry) => {
186
- if (!entry.listeners.size || !entry.app.preview || entry.socket || entry.connecting) {
107
+ if (!request.views.some((view) => view.slot === slot && view.instances && !view.disabled)) {
108
+ publish("ready", []);
187
109
  return;
188
110
  }
189
- entry.connecting = true;
190
- const appMetadata = entry.app.preview;
191
- let metadata = entry.metadata && viewerTokenExpiresAt(entry.metadata) > Math.max(Date.now(), viewerTokenExpiresAt(appMetadata)) ? entry.metadata : appMetadata;
111
+ publish("loading");
112
+ let client;
192
113
  try {
193
- const refresh = new URL("preview/metadata", baseUrl);
194
- refresh.searchParams.set("sessionId", metadata.sessionId);
195
- const response = await fetch(refresh, { credentials: "same-origin" });
196
- if (response.ok) {
197
- metadata = previewMetadataSchema.parse(await response.json());
198
- entry.metadata = metadata;
199
- } else if (response.status === 404) {
200
- end(entry);
114
+ await store.fetchMeta({ force: store.getMetaSnapshot().status === "error" });
115
+ if (id !== generation.current) return;
116
+ const meta = store.getMetaSnapshot();
117
+ if (meta.error) throw meta.error;
118
+ if (!meta.schema) throw new Error("TailorKit metadata is unavailable.");
119
+ const resolved = resolveSlotView(request.views, slot, request.activeView, meta.schema);
120
+ if (!resolved?.instances) {
121
+ publish("ready", []);
201
122
  return;
202
123
  }
203
- } catch {
204
- }
205
- if (!entry.listeners.size) {
206
- entry.connecting = false;
207
- return;
208
- }
209
- const socket = new WebSocket(metadata.websocketUrl, metadata.token);
210
- entry.socket = socket;
211
- entry.connecting = false;
212
- socket.addEventListener("open", () => {
213
- entry.delay = 1e3;
214
- const client = createPreviewWebSocketClient(socket);
215
- void (async () => {
216
- try {
217
- const stream = await client.subscribe();
218
- for await (const event of stream) {
219
- await handleEvent(entry, event);
220
- }
221
- } catch {
222
- } finally {
223
- socket.close();
224
- }
225
- })();
226
- });
227
- socket.addEventListener("error", () => socket.close());
228
- socket.addEventListener("close", () => {
229
- if (entry.socket !== socket) {
124
+ if (resolved.status !== "ready") {
125
+ if (resolved.status === "error")
126
+ throw new Error(`Context for view "${resolved.view}" is unavailable.`);
127
+ publish("loading");
230
128
  return;
231
129
  }
232
- entry.socket = null;
233
- entry.candidate = null;
234
- if (entry.listeners.size) {
235
- entry.reconnect = setTimeout(() => {
236
- entry.reconnect = null;
237
- void connect(entry);
238
- }, entry.delay);
239
- entry.delay = Math.min(entry.delay * 2, 3e4);
240
- }
241
- });
242
- };
243
- return {
244
- getSnapshot: (sessionId) => entries.get(sessionId)?.snapshot ?? empty,
245
- updateApp: (app) => {
246
- const sessionId = app.preview?.sessionId;
247
- const entry = sessionId && entries.get(sessionId);
248
- if (entry) {
249
- entry.app = app;
250
- }
251
- },
252
- subscribe: (app, listener) => {
253
- const sessionId = app.preview?.sessionId;
254
- if (!sessionId) {
255
- return () => {
256
- };
257
- }
258
- let entry = entries.get(sessionId);
259
- if (!entry) {
260
- entry = {
261
- app,
262
- metadata: null,
263
- candidate: null,
264
- listeners: /* @__PURE__ */ new Set(),
265
- snapshot: empty,
266
- socket: null,
267
- connecting: false,
268
- reconnect: null,
269
- delay: 1e3
270
- };
271
- entries.set(sessionId, entry);
272
- }
273
- entry.app = app;
274
- entry.listeners.add(listener);
275
- void connect(entry);
276
- return () => {
277
- entry?.listeners.delete(listener);
278
- if (entry && !entry.listeners.size) {
279
- close(entry);
280
- entries.delete(sessionId);
281
- }
282
- };
283
- },
284
- dispose: () => {
285
- for (const entry of entries.values()) {
286
- close(entry);
287
- }
288
- entries.clear();
289
- }
290
- };
291
- }
292
-
293
- // src/scope-query.ts
294
- function appendScopeSelection(url, scopes) {
295
- if (scopes === void 0) {
296
- return;
297
- }
298
- if (scopes.length === 0) {
299
- url.searchParams.append("scopes", "");
300
- return;
301
- }
302
- for (const scope of scopes) {
303
- url.searchParams.append("scopes", scope);
304
- }
305
- }
306
- function normalizeScopeSelection(scopes) {
307
- if (scopes === void 0) {
308
- return { key: "*" };
309
- }
310
- const names = [...new Set(scopes)].sort();
311
- return { key: JSON.stringify(names), scopes: names };
312
- }
313
-
314
- // src/store.ts
315
- function createTailorKitStore(baseUrlInput, initialApps) {
316
- const baseUrl = toBaseUrl(baseUrlInput);
317
- const listeners = /* @__PURE__ */ new Set();
318
- let providedApps = initialApps;
319
- let unrequestedSnapshot = {
320
- apps: initialApps ?? [],
321
- error: null,
322
- status: initialApps === void 0 ? "idle" : "ready"
323
- };
324
- const appsEntries = /* @__PURE__ */ new Map();
325
- let metaSnapshot = {
326
- assetsBaseUrl: null,
327
- error: null,
328
- schema: null,
329
- status: "idle"
330
- };
331
- let fetchMetaPromise = null;
332
- const getAppsEntry = (scopes) => {
333
- const selection = normalizeScopeSelection(scopes);
334
- let entry = appsEntries.get(selection.key);
335
- if (!entry) {
336
- entry = {
337
- scopes: selection.scopes,
338
- snapshot: unrequestedSnapshot,
339
- promise: null,
340
- requestId: 0,
341
- requested: false,
342
- subscribers: 0
343
- };
344
- appsEntries.set(selection.key, entry);
345
- }
346
- return entry;
347
- };
348
- const emit = () => {
349
- for (const listener of listeners) {
350
- listener();
351
- }
352
- };
353
- const store = {
354
- baseUrl,
355
- views: createViewRegistry(),
356
- setProvidedApps(apps) {
357
- if (providedApps === apps) {
358
- return;
359
- }
360
- providedApps = apps;
361
- unrequestedSnapshot = {
362
- apps: apps ?? [],
363
- error: null,
364
- status: apps === void 0 ? "idle" : "ready"
365
- };
366
- for (const entry of appsEntries.values()) {
367
- entry.requestId += 1;
368
- entry.promise = null;
369
- entry.snapshot = unrequestedSnapshot;
370
- }
371
- emit();
372
- if (apps === void 0) {
373
- for (const entry of appsEntries.values()) {
374
- if (entry.requested && entry.subscribers > 0) {
375
- void store.fetchApps({ scopes: entry.scopes });
376
- }
377
- }
378
- }
379
- },
380
- fetchApps: (options = {}) => {
381
- const entry = getAppsEntry(options.scopes);
382
- entry.requested = true;
383
- if (providedApps !== void 0) {
384
- return Promise.resolve();
385
- }
386
- if (entry.promise && !options.force) {
387
- return entry.promise;
388
- }
389
- entry.snapshot = { ...entry.snapshot, status: "loading" };
390
- emit();
391
- const requestId = ++entry.requestId;
392
- const appsUrl = new URL("apps", baseUrl);
393
- appendScopeSelection(appsUrl, entry.scopes);
394
- entry.promise = fetch(appsUrl).then(async (response) => {
395
- if (!response.ok) {
396
- throw new Error(`Unable to fetch TailorKit apps from ${baseUrl.toString()}.`);
397
- }
398
- const apps = await response.json();
399
- if (requestId !== entry.requestId) {
400
- return;
401
- }
402
- entry.snapshot = {
403
- apps,
404
- error: null,
405
- status: "ready"
406
- };
407
- emit();
408
- }).catch((error) => {
409
- if (requestId !== entry.requestId) {
410
- return;
411
- }
412
- entry.snapshot = {
413
- ...entry.snapshot,
414
- error: error instanceof Error ? error : new Error(String(error)),
415
- status: "error"
416
- };
417
- emit();
418
- });
419
- return entry.promise;
420
- },
421
- fetchMeta: () => {
422
- if (fetchMetaPromise) {
423
- return fetchMetaPromise;
424
- }
425
- metaSnapshot = { ...metaSnapshot, status: "loading" };
426
- emit();
427
- fetchMetaPromise = fetch(new URL("meta", baseUrl)).then(async (response) => {
428
- if (!response.ok) {
429
- throw new Error(`Unable to fetch TailorKit metadata from ${baseUrl.toString()}.`);
430
- }
431
- const meta = await response.json();
432
- metaSnapshot = {
433
- assetsBaseUrl: meta.assetsBaseUrl ?? null,
434
- error: null,
435
- schema: meta.schema,
436
- status: "ready"
437
- };
438
- emit();
439
- }).catch((error) => {
440
- metaSnapshot = {
441
- ...metaSnapshot,
442
- error: error instanceof Error ? error : new Error(String(error)),
443
- status: "error"
444
- };
445
- emit();
130
+ client = createClient({ getSession });
131
+ backend.current = client;
132
+ const instances = await client.action(resolveInstances, {
133
+ slot,
134
+ path: resolved.view,
135
+ context: resolved.context
446
136
  });
447
- return fetchMetaPromise;
448
- },
449
- getAppsSnapshot: (scopes) => appsEntries.get(normalizeScopeSelection(scopes).key)?.snapshot ?? unrequestedSnapshot,
450
- getMetaSnapshot: () => metaSnapshot,
451
- subscribe: (listener) => {
452
- listeners.add(listener);
453
- return () => {
454
- listeners.delete(listener);
455
- };
456
- },
457
- subscribeApps: (scopes, listener) => {
458
- const selection = normalizeScopeSelection(scopes);
459
- const entry = getAppsEntry(selection.scopes);
460
- entry.subscribers += 1;
461
- listeners.add(listener);
462
- return () => {
463
- listeners.delete(listener);
464
- entry.subscribers -= 1;
465
- if (entry.subscribers === 0) {
466
- entry.requestId += 1;
467
- appsEntries.delete(selection.key);
468
- }
469
- };
137
+ publish("ready", instances);
138
+ } catch (error) {
139
+ publish("error", void 0, error instanceof Error ? error : new Error(String(error)));
140
+ } finally {
141
+ client?.close();
142
+ if (backend.current === client) backend.current = null;
470
143
  }
471
- };
144
+ }, [request, store, slot, getSession]);
145
+ useEffect(() => {
146
+ void refetch();
147
+ return () => {
148
+ generation.current += 1;
149
+ backend.current?.close();
150
+ backend.current = null;
151
+ };
152
+ }, [refetch]);
153
+ const status = snapshot.request === request ? snapshot.status : activeView ? "loading" : "idle";
472
154
  return {
473
- ...store,
474
- previews: createPreviewManager(baseUrl, () => {
475
- for (const entry of appsEntries.values()) {
476
- if (entry.requested && entry.subscribers > 0) {
477
- void store.fetchApps({ force: true, scopes: entry.scopes });
478
- }
479
- }
480
- })
155
+ data: snapshot.request === request ? snapshot.data : void 0,
156
+ error: snapshot.request === request ? snapshot.error : null,
157
+ status,
158
+ isPending: status === "idle" || status === "loading",
159
+ isLoading: status === "loading",
160
+ isSuccess: status === "ready",
161
+ isError: status === "error",
162
+ refetch
481
163
  };
482
164
  }
483
- function toBaseUrl(value) {
484
- const url = value instanceof URL ? new URL(value) : new URL(value);
485
- if (!url.pathname.endsWith("/")) {
486
- url.pathname = `${url.pathname}/`;
487
- }
488
- return url;
489
- }
490
-
491
- // src/hooks/use-stable-context.ts
492
- import { useRef } from "react";
493
- function useStableContext(value) {
494
- const key = JSON.stringify(value);
495
- const ref = useRef({ key, value });
496
- if (ref.current.key !== key) {
497
- ref.current = { key, value };
498
- }
499
- return ref.current.value;
500
- }
501
-
502
- // src/components/context.ts
503
- import { createContext, useContext } from "react";
504
- var TailorRootContext = createContext(null);
505
- function useTailorRootContext(component, expectedClient) {
506
- const context = useContext(TailorRootContext);
507
- if (!context) {
508
- throw new Error(`${component} must be rendered inside Root.`);
509
- }
510
- if (expectedClient && context.client !== expectedClient) {
511
- throw new Error(
512
- `${component} was created for a different TailorKit client than the one passed to Root.`
513
- );
514
- }
515
- return context;
516
- }
517
165
 
518
166
  // src/primitives.tsx
519
- import { createContext as createContext2, useContext as useContext2, useId, useMemo } from "react";
167
+ import { createContext as createContext2, useContext as useContext2, useId, useMemo as useMemo2 } from "react";
520
168
  import { resolveTheme } from "@tailorkit/core/primitives/theme";
521
169
  import { jsx, jsxs } from "react/jsx-runtime";
522
170
  var PrimitiveThemeContext = createContext2(null);
@@ -683,7 +331,7 @@ var buildThemeCss = (viewId, theme) => {
683
331
  };
684
332
  var usePrimitiveNodeId = () => {
685
333
  const id = useId();
686
- return useMemo(() => `tailorkit-${id.replaceAll(":", "")}`, [id]);
334
+ return useMemo2(() => `tailorkit-${id.replaceAll(":", "")}`, [id]);
687
335
  };
688
336
  var Primitive = ({
689
337
  children,
@@ -719,13 +367,13 @@ import {
719
367
  Fragment,
720
368
  createElement,
721
369
  memo,
722
- useCallback,
370
+ useCallback as useCallback2,
723
371
  useContext as useContext3,
724
- useEffect,
725
- useMemo as useMemo2,
726
- useRef as useRef2,
727
- useState,
728
- useSyncExternalStore
372
+ useEffect as useEffect2,
373
+ useMemo as useMemo3,
374
+ useRef as useRef3,
375
+ useState as useState2,
376
+ useSyncExternalStore as useSyncExternalStore2
729
377
  } from "react";
730
378
  import { createIframeUiHost } from "@tailorkit/sandbox/host";
731
379
 
@@ -855,195 +503,687 @@ var RemoteErrorBoundary = class extends Component {
855
503
  if (this.state.error) {
856
504
  return createElement("div", null, formatError(this.state.error));
857
505
  }
858
- return this.props.children;
859
- }
860
- };
861
- function RemoteViewHost({
862
- appUrl,
863
- sourceText,
864
- getBackendSession,
865
- components: components2,
866
- createIframe,
867
- props
868
- }) {
869
- const appKey = appUrl.toString();
870
- const storeRef = useRef2(null);
871
- if (storeRef.current === null) {
872
- storeRef.current = new NodeStore();
873
- }
874
- const store = storeRef.current;
875
- const appKeyRef = useRef2(appKey);
876
- if (appKeyRef.current !== appKey) {
877
- appKeyRef.current = appKey;
878
- store.clear();
879
- }
880
- const hostRef = useRef2(null);
881
- const propsRef = useRef2(props);
882
- useEffect(() => {
883
- propsRef.current = props;
884
- hostRef.current?.setProps(props);
885
- }, [props]);
886
- const dispatchRef = useRef2(null);
887
- const [error, setError] = useState(null);
888
- const [status, setStatus] = useState("starting");
889
- const ctxRef = useRef2({
890
- components: components2,
891
- dispatch: (payload) => dispatchRef.current?.(payload),
892
- store
893
- });
894
- useEffect(() => {
895
- ctxRef.current.components = components2;
896
- }, [components2]);
897
- const ui = useMemo2(() => {
898
- const ctx = ctxRef.current;
899
- return createElement(
900
- RemoteUIContext.Provider,
901
- { value: ctx },
902
- createElement(
903
- RemoteErrorBoundary,
904
- { key: appKey },
905
- createElement(RemoteRoot, { store: ctx.store })
906
- )
907
- );
908
- }, [appKey]);
909
- useEffect(() => {
910
- const host = createIframeUiHost(appUrl, {
911
- createIframe,
912
- sourceText,
913
- getBackendSession,
914
- onError: (error2) => {
915
- console.error("TailorKit remote app failed", error2);
916
- setError(error2);
917
- setStatus("error");
506
+ return this.props.children;
507
+ }
508
+ };
509
+ function RemoteViewHost({
510
+ appUrl,
511
+ sourceText,
512
+ getBackendSession,
513
+ components: components2,
514
+ createIframe,
515
+ props
516
+ }) {
517
+ const appKey = appUrl.toString();
518
+ const storeRef = useRef3(null);
519
+ if (storeRef.current === null) {
520
+ storeRef.current = new NodeStore();
521
+ }
522
+ const store = storeRef.current;
523
+ const appKeyRef = useRef3(appKey);
524
+ if (appKeyRef.current !== appKey) {
525
+ appKeyRef.current = appKey;
526
+ store.clear();
527
+ }
528
+ const hostRef = useRef3(null);
529
+ const propsRef = useRef3(props);
530
+ useEffect2(() => {
531
+ propsRef.current = props;
532
+ hostRef.current?.setProps(props);
533
+ }, [props]);
534
+ const dispatchRef = useRef3(null);
535
+ const [error, setError] = useState2(null);
536
+ const [status, setStatus] = useState2("starting");
537
+ const ctxRef = useRef3({
538
+ components: components2,
539
+ dispatch: (payload) => dispatchRef.current?.(payload),
540
+ store
541
+ });
542
+ useEffect2(() => {
543
+ ctxRef.current.components = components2;
544
+ }, [components2]);
545
+ const ui = useMemo3(() => {
546
+ const ctx = ctxRef.current;
547
+ return createElement(
548
+ RemoteUIContext.Provider,
549
+ { value: ctx },
550
+ createElement(
551
+ RemoteErrorBoundary,
552
+ { key: appKey },
553
+ createElement(RemoteRoot, { store: ctx.store })
554
+ )
555
+ );
556
+ }, [appKey]);
557
+ useEffect2(() => {
558
+ const host = createIframeUiHost(appUrl, {
559
+ createIframe,
560
+ sourceText,
561
+ getBackendSession,
562
+ onError: (error2) => {
563
+ console.error("TailorKit remote app failed", error2);
564
+ setError(error2);
565
+ setStatus("error");
566
+ },
567
+ props: propsRef.current
568
+ });
569
+ hostRef.current = host;
570
+ dispatchRef.current = (payload) => host.dispatch(payload);
571
+ const unsubscribe = host.subscribe(() => {
572
+ const tree = host.getSnapshot();
573
+ if (tree !== null) {
574
+ store.setSnapshot(tree);
575
+ setStatus("ready");
576
+ }
577
+ });
578
+ setStatus("starting");
579
+ setError(null);
580
+ host.mount();
581
+ return () => {
582
+ unsubscribe();
583
+ dispatchRef.current = null;
584
+ hostRef.current = null;
585
+ host.destroy();
586
+ };
587
+ }, [appUrl, createIframe, sourceText, getBackendSession, store]);
588
+ if (status === "error" && error) {
589
+ return createElement("div", null, formatError(error));
590
+ }
591
+ return ui;
592
+ }
593
+ function RemoteRoot({ store }) {
594
+ const subscribe = useCallback2((listener) => store.subscribeRoot(listener), [store]);
595
+ const getSnapshot = useCallback2(() => store.getRootId(), [store]);
596
+ const rootId = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
597
+ if (rootId === null) {
598
+ return null;
599
+ }
600
+ return /* @__PURE__ */ jsx2(RemoteView, { nodeId: rootId });
601
+ }
602
+ var RemoteElementView = memo(({ ctx, node }) => {
603
+ const { dispatch, components: components2 } = ctx;
604
+ const children = node.children.map((child) => /* @__PURE__ */ jsx2(RemoteView, { nodeId: child.id }, child.id));
605
+ const component = components2[node.type];
606
+ if (component === void 0) {
607
+ throw new Error(`TailorKit component "${node.type}" is not registered.`);
608
+ }
609
+ const props = { ...node.props };
610
+ for (const binding of node.callbacks ?? []) {
611
+ props[binding.callback] = (...args) => {
612
+ dispatch({
613
+ data: {
614
+ args: args.slice(0, binding.inputCount),
615
+ event: binding.event,
616
+ nodeId: node.id
617
+ },
618
+ type: "dispatchCallback"
619
+ });
620
+ };
621
+ }
622
+ return createElement(component, props, ...children);
623
+ });
624
+ function RemoteViewComponent({ nodeId }) {
625
+ const ctx = useContext3(RemoteUIContext);
626
+ if (ctx === null) {
627
+ return null;
628
+ }
629
+ const { store } = ctx;
630
+ const subscribe = useCallback2(
631
+ (listener) => store.subscribe(nodeId, listener),
632
+ [store, nodeId]
633
+ );
634
+ const getSnapshot = useCallback2(() => store.getNode(nodeId), [store, nodeId]);
635
+ const node = useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
636
+ if (node === null) {
637
+ return null;
638
+ }
639
+ if (node.kind === "text") {
640
+ return node.text;
641
+ }
642
+ const childElements = node.children.map((child) => /* @__PURE__ */ jsx2(RemoteView, { nodeId: child.id }, child.id));
643
+ if (node.kind === "fragment") {
644
+ return createElement(Fragment, { key: node.id }, ...childElements);
645
+ }
646
+ return /* @__PURE__ */ jsx2(RemoteElementView, { node, ctx });
647
+ }
648
+ var RemoteView = memo(RemoteViewComponent);
649
+
650
+ // src/view-registry.ts
651
+ import { getViewDepth, isViewAncestor } from "@tailorkit/core/views";
652
+ function createViewRegistry() {
653
+ const entries = /* @__PURE__ */ new Map();
654
+ const listeners = /* @__PURE__ */ new Set();
655
+ let snapshot = null;
656
+ let nextOrder = 0;
657
+ let scheduled = false;
658
+ const publish = () => {
659
+ if (scheduled) {
660
+ return;
661
+ }
662
+ scheduled = true;
663
+ queueMicrotask(() => {
664
+ scheduled = false;
665
+ const ordered = [...entries.values()].toSorted(
666
+ (a, b) => getViewDepth(b.view) - getViewDepth(a.view) || b.order - a.order
667
+ );
668
+ const selected = ordered[0];
669
+ if (selected) {
670
+ const peers = ordered.filter(
671
+ (entry) => getViewDepth(entry.view) === getViewDepth(selected.view)
672
+ );
673
+ if (peers.length > 1) {
674
+ console.warn(
675
+ `TailorKit found multiple active views at the same hierarchy depth: ${peers.toReversed().map((entry) => `"${entry.view}"`).join(
676
+ ", "
677
+ )}. TailorKit selected "${selected.view}" by mount order. Only one route at a hierarchy depth should call useRegisterView.`
678
+ );
679
+ }
680
+ snapshot = {
681
+ view: selected.view,
682
+ layers: ordered.filter((entry) => isViewAncestor(entry.view, selected.view)).toReversed().map((entry) => ({ path: entry.view, context: entry.context, status: entry.status }))
683
+ };
684
+ } else {
685
+ snapshot = null;
686
+ }
687
+ for (const listener of listeners) {
688
+ listener();
689
+ }
690
+ });
691
+ };
692
+ return {
693
+ getSnapshot: () => snapshot,
694
+ subscribe(listener) {
695
+ listeners.add(listener);
696
+ return () => {
697
+ listeners.delete(listener);
698
+ };
699
+ },
700
+ register(entry) {
701
+ entries.set(entry.id, { ...entry, order: entries.get(entry.id)?.order ?? nextOrder++ });
702
+ publish();
703
+ },
704
+ unregister(id) {
705
+ entries.delete(id);
706
+ publish();
707
+ }
708
+ };
709
+ }
710
+
711
+ // src/preview-manager.ts
712
+ import {
713
+ createPreviewWebSocketClient,
714
+ previewMetadataSchema
715
+ } from "@tailorkit/client-platform/preview";
716
+ var empty = { revision: 0, source: null };
717
+ function viewerTokenExpiresAt(metadata) {
718
+ const expiresAt = Number(metadata.token.split(".", 1)[0]);
719
+ return Number.isSafeInteger(expiresAt) ? expiresAt : 0;
720
+ }
721
+ function bytesFromBase64(value) {
722
+ const raw = atob(value);
723
+ const bytes = new Uint8Array(raw.length);
724
+ for (let index = 0; index < raw.length; index++) {
725
+ bytes[index] = raw.codePointAt(index) ?? 0;
726
+ }
727
+ return bytes;
728
+ }
729
+ async function verify(candidate) {
730
+ let source = null;
731
+ for (const [fileIndex, file] of candidate.manifest.files.entries()) {
732
+ const parts = candidate.files[fileIndex];
733
+ if (!parts || parts.length !== file.chunks || parts.some((part) => part === null)) {
734
+ return null;
735
+ }
736
+ const bytes = new Uint8Array(file.size);
737
+ let offset = 0;
738
+ for (const part of parts) {
739
+ if (!part) {
740
+ return null;
741
+ }
742
+ bytes.set(part, offset);
743
+ offset += part.length;
744
+ }
745
+ if (offset !== file.size) {
746
+ return null;
747
+ }
748
+ const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
749
+ if (digest !== file.sha256) {
750
+ return null;
751
+ }
752
+ if (file.path === "client.js") {
753
+ source = new TextDecoder().decode(bytes);
754
+ }
755
+ }
756
+ return source;
757
+ }
758
+ function createPreviewManager(baseUrl, onEnded, onViews) {
759
+ const entries = /* @__PURE__ */ new Map();
760
+ const notify = (entry) => {
761
+ for (const listener of entry.listeners) {
762
+ listener();
763
+ }
764
+ };
765
+ const close = (entry) => {
766
+ if (entry.reconnect) {
767
+ clearTimeout(entry.reconnect);
768
+ }
769
+ entry.reconnect = null;
770
+ const socket = entry.socket;
771
+ entry.socket = null;
772
+ socket?.close();
773
+ entry.connecting = false;
774
+ entry.candidate = null;
775
+ };
776
+ const end = (entry) => {
777
+ close(entry);
778
+ entry.metadata = null;
779
+ entry.snapshot = empty;
780
+ notify(entry);
781
+ onEnded();
782
+ };
783
+ const handleEvent = async (entry, event) => {
784
+ if (event.type === "ended") {
785
+ end(entry);
786
+ return;
787
+ }
788
+ if (event.revision <= entry.snapshot.revision) {
789
+ return;
790
+ }
791
+ if (event.type === "begin") {
792
+ entry.candidate = {
793
+ revision: event.revision,
794
+ manifest: event.manifest,
795
+ files: event.manifest.files.map((file) => Array.from({ length: file.chunks }, () => null))
796
+ };
797
+ return;
798
+ }
799
+ const candidate = entry.candidate;
800
+ if (!candidate || candidate.revision !== event.revision) {
801
+ return;
802
+ }
803
+ if (event.type === "chunk") {
804
+ const file = candidate.manifest.files[event.fileIndex];
805
+ const slots = candidate.files[event.fileIndex];
806
+ if (!file || !slots || event.chunkIndex < 0 || event.chunkIndex >= slots.length || slots[event.chunkIndex]) {
807
+ entry.candidate = null;
808
+ return;
809
+ }
810
+ const bytes = bytesFromBase64(event.base64);
811
+ if (bytes.length !== Math.min(256 * 1024, file.size - event.chunkIndex * 256 * 1024)) {
812
+ entry.candidate = null;
813
+ return;
814
+ }
815
+ slots[event.chunkIndex] = bytes;
816
+ return;
817
+ }
818
+ if (event.type === "complete") {
819
+ const source = await verify(candidate);
820
+ if (source !== null && entry.candidate === candidate && candidate.revision > entry.snapshot.revision) {
821
+ entry.snapshot = { revision: candidate.revision, source };
822
+ if (candidate.manifest.views !== void 0)
823
+ onViews?.(entry.app.id, candidate.manifest.views);
824
+ notify(entry);
825
+ }
826
+ if (entry.candidate === candidate) {
827
+ entry.candidate = null;
828
+ }
829
+ }
830
+ };
831
+ const connect = async (entry) => {
832
+ if (!entry.listeners.size || !entry.app.preview || entry.socket || entry.connecting) {
833
+ return;
834
+ }
835
+ entry.connecting = true;
836
+ const appMetadata = entry.app.preview;
837
+ let metadata = entry.metadata && viewerTokenExpiresAt(entry.metadata) > Math.max(Date.now(), viewerTokenExpiresAt(appMetadata)) ? entry.metadata : appMetadata;
838
+ try {
839
+ const refresh = new URL("preview/metadata", baseUrl);
840
+ refresh.searchParams.set("sessionId", metadata.sessionId);
841
+ const response = await fetch(refresh, { credentials: "same-origin" });
842
+ if (response.ok) {
843
+ metadata = previewMetadataSchema.parse(await response.json());
844
+ entry.metadata = metadata;
845
+ } else if (response.status === 404) {
846
+ end(entry);
847
+ return;
848
+ }
849
+ } catch {
850
+ }
851
+ if (!entry.listeners.size) {
852
+ entry.connecting = false;
853
+ return;
854
+ }
855
+ const socket = new WebSocket(metadata.websocketUrl, metadata.token);
856
+ entry.socket = socket;
857
+ entry.connecting = false;
858
+ socket.addEventListener("open", () => {
859
+ entry.delay = 1e3;
860
+ const client = createPreviewWebSocketClient(socket);
861
+ void (async () => {
862
+ try {
863
+ const stream = await client.subscribe();
864
+ for await (const event of stream) {
865
+ await handleEvent(entry, event);
866
+ }
867
+ } catch {
868
+ } finally {
869
+ socket.close();
870
+ }
871
+ })();
872
+ });
873
+ socket.addEventListener("error", () => socket.close());
874
+ socket.addEventListener("close", () => {
875
+ if (entry.socket !== socket) {
876
+ return;
877
+ }
878
+ entry.socket = null;
879
+ entry.candidate = null;
880
+ if (entry.listeners.size) {
881
+ entry.reconnect = setTimeout(() => {
882
+ entry.reconnect = null;
883
+ void connect(entry);
884
+ }, entry.delay);
885
+ entry.delay = Math.min(entry.delay * 2, 3e4);
886
+ }
887
+ });
888
+ };
889
+ return {
890
+ getSnapshot: (sessionId) => entries.get(sessionId)?.snapshot ?? empty,
891
+ updateApp: (app) => {
892
+ const sessionId = app.preview?.sessionId;
893
+ const entry = sessionId && entries.get(sessionId);
894
+ if (entry) {
895
+ entry.app = app;
896
+ }
897
+ },
898
+ subscribe: (app, listener) => {
899
+ const sessionId = app.preview?.sessionId;
900
+ if (!sessionId) {
901
+ return () => {
902
+ };
903
+ }
904
+ let entry = entries.get(sessionId);
905
+ if (!entry) {
906
+ entry = {
907
+ app,
908
+ metadata: null,
909
+ candidate: null,
910
+ listeners: /* @__PURE__ */ new Set(),
911
+ snapshot: empty,
912
+ socket: null,
913
+ connecting: false,
914
+ reconnect: null,
915
+ delay: 1e3
916
+ };
917
+ entries.set(sessionId, entry);
918
+ }
919
+ entry.app = app;
920
+ entry.listeners.add(listener);
921
+ void connect(entry);
922
+ return () => {
923
+ entry?.listeners.delete(listener);
924
+ if (entry && !entry.listeners.size) {
925
+ close(entry);
926
+ entries.delete(sessionId);
927
+ }
928
+ };
929
+ },
930
+ dispose: () => {
931
+ for (const entry of entries.values()) {
932
+ close(entry);
933
+ }
934
+ entries.clear();
935
+ }
936
+ };
937
+ }
938
+
939
+ // src/store.ts
940
+ function createTailorKitStore(baseUrlInput, initialApps) {
941
+ const baseUrl = toBaseUrl(baseUrlInput);
942
+ const listeners = /* @__PURE__ */ new Set();
943
+ let providedApps = initialApps;
944
+ let appsSnapshot = {
945
+ apps: initialApps ?? [],
946
+ views: listViews(initialApps ?? []),
947
+ error: null,
948
+ status: initialApps === void 0 ? "idle" : "ready"
949
+ };
950
+ let appsPromise = null;
951
+ let appsRequestId = 0;
952
+ let appsRequested = false;
953
+ let appsSubscribers = 0;
954
+ let metaSnapshot = {
955
+ assetsBaseUrl: null,
956
+ error: null,
957
+ schema: null,
958
+ status: "idle"
959
+ };
960
+ let fetchMetaPromise = null;
961
+ let metaRequestId = 0;
962
+ const emit = () => {
963
+ for (const listener of listeners) {
964
+ listener();
965
+ }
966
+ };
967
+ const store = {
968
+ baseUrl,
969
+ views: createViewRegistry(),
970
+ setProvidedApps(apps) {
971
+ if (providedApps === apps) {
972
+ return;
973
+ }
974
+ providedApps = apps;
975
+ appsRequestId += 1;
976
+ appsPromise = null;
977
+ appsSnapshot = {
978
+ apps: apps ?? [],
979
+ views: listViews(apps ?? []),
980
+ error: null,
981
+ status: apps === void 0 ? "idle" : "ready"
982
+ };
983
+ emit();
984
+ if (apps === void 0 && appsRequested && appsSubscribers > 0) {
985
+ void store.fetchApps();
986
+ }
987
+ },
988
+ fetchApps: (options = {}) => {
989
+ appsRequested = true;
990
+ if (providedApps !== void 0) {
991
+ return Promise.resolve();
992
+ }
993
+ if (appsPromise && !options.force) {
994
+ return appsPromise;
995
+ }
996
+ appsSnapshot = { ...appsSnapshot, error: null, status: "loading" };
997
+ emit();
998
+ const requestId = ++appsRequestId;
999
+ appsPromise = fetch(new URL("apps", baseUrl)).then(async (response) => {
1000
+ if (!response.ok) {
1001
+ throw new Error(`Unable to fetch TailorKit apps from ${baseUrl.toString()}.`);
1002
+ }
1003
+ const apps = await response.json();
1004
+ if (requestId !== appsRequestId) return;
1005
+ appsSnapshot = { apps, views: listViews(apps), error: null, status: "ready" };
1006
+ emit();
1007
+ }).catch((error) => {
1008
+ if (requestId !== appsRequestId) return;
1009
+ appsSnapshot = {
1010
+ ...appsSnapshot,
1011
+ error: error instanceof Error ? error : new Error(String(error)),
1012
+ status: "error"
1013
+ };
1014
+ emit();
1015
+ });
1016
+ return appsPromise;
1017
+ },
1018
+ fetchMeta: (options = {}) => {
1019
+ if (fetchMetaPromise && !options.force) {
1020
+ return fetchMetaPromise;
1021
+ }
1022
+ metaSnapshot = { ...metaSnapshot, error: null, status: "loading" };
1023
+ emit();
1024
+ const requestId = ++metaRequestId;
1025
+ fetchMetaPromise = fetch(new URL("meta", baseUrl)).then(async (response) => {
1026
+ if (!response.ok) {
1027
+ throw new Error(`Unable to fetch TailorKit metadata from ${baseUrl.toString()}.`);
1028
+ }
1029
+ const meta = await response.json();
1030
+ if (requestId !== metaRequestId) return;
1031
+ metaSnapshot = {
1032
+ assetsBaseUrl: meta.assetsBaseUrl ?? null,
1033
+ error: null,
1034
+ schema: meta.schema,
1035
+ status: "ready"
1036
+ };
1037
+ emit();
1038
+ }).catch((error) => {
1039
+ if (requestId !== metaRequestId) return;
1040
+ metaSnapshot = {
1041
+ ...metaSnapshot,
1042
+ error: error instanceof Error ? error : new Error(String(error)),
1043
+ status: "error"
1044
+ };
1045
+ emit();
1046
+ });
1047
+ return fetchMetaPromise;
1048
+ },
1049
+ getAppsSnapshot: () => appsSnapshot,
1050
+ getMetaSnapshot: () => metaSnapshot,
1051
+ subscribe: (listener) => {
1052
+ listeners.add(listener);
1053
+ return () => {
1054
+ listeners.delete(listener);
1055
+ };
1056
+ },
1057
+ subscribeApps: (listener) => {
1058
+ appsSubscribers += 1;
1059
+ listeners.add(listener);
1060
+ return () => {
1061
+ listeners.delete(listener);
1062
+ appsSubscribers -= 1;
1063
+ };
1064
+ }
1065
+ };
1066
+ return {
1067
+ ...store,
1068
+ previews: createPreviewManager(
1069
+ baseUrl,
1070
+ () => {
1071
+ if (appsRequested) void store.fetchApps({ force: true });
918
1072
  },
919
- props: propsRef.current
920
- });
921
- hostRef.current = host;
922
- dispatchRef.current = (payload) => host.dispatch(payload);
923
- const unsubscribe = host.subscribe(() => {
924
- const tree = host.getSnapshot();
925
- if (tree !== null) {
926
- store.setSnapshot(tree);
927
- setStatus("ready");
1073
+ (appId, views) => {
1074
+ const apps = appsSnapshot.apps.map((app) => app.id === appId ? { ...app, views } : app);
1075
+ appsSnapshot = { ...appsSnapshot, apps, views: listViews(apps) };
1076
+ emit();
928
1077
  }
929
- });
930
- setStatus("starting");
931
- setError(null);
932
- host.mount();
933
- return () => {
934
- unsubscribe();
935
- dispatchRef.current = null;
936
- hostRef.current = null;
937
- host.destroy();
938
- };
939
- }, [appUrl, createIframe, sourceText, getBackendSession, store]);
940
- if (status === "error" && error) {
941
- return createElement("div", null, formatError(error));
942
- }
943
- return ui;
1078
+ )
1079
+ };
944
1080
  }
945
- function RemoteRoot({ store }) {
946
- const subscribe = useCallback((listener) => store.subscribeRoot(listener), [store]);
947
- const getSnapshot = useCallback(() => store.getRootId(), [store]);
948
- const rootId = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
949
- if (rootId === null) {
950
- return null;
1081
+ function toBaseUrl(value) {
1082
+ const url = value instanceof URL ? new URL(value) : new URL(value);
1083
+ if (!url.pathname.endsWith("/")) {
1084
+ url.pathname = `${url.pathname}/`;
951
1085
  }
952
- return /* @__PURE__ */ jsx2(RemoteView, { nodeId: rootId });
1086
+ return url;
953
1087
  }
954
- var RemoteElementView = memo(({ ctx, node }) => {
955
- const { dispatch, components: components2 } = ctx;
956
- const children = node.children.map((child) => /* @__PURE__ */ jsx2(RemoteView, { nodeId: child.id }, child.id));
957
- const component = components2[node.type];
958
- if (component === void 0) {
959
- throw new Error(`TailorKit component "${node.type}" is not registered.`);
960
- }
961
- const props = { ...node.props };
962
- for (const binding of node.callbacks ?? []) {
963
- props[binding.callback] = (...args) => {
964
- dispatch({
965
- data: {
966
- args: args.slice(0, binding.inputCount),
967
- event: binding.event,
968
- nodeId: node.id
969
- },
970
- type: "dispatchCallback"
971
- });
972
- };
973
- }
974
- return createElement(component, props, ...children);
975
- });
976
- function RemoteViewComponent({ nodeId }) {
977
- const ctx = useContext3(RemoteUIContext);
978
- if (ctx === null) {
979
- return null;
980
- }
981
- const { store } = ctx;
982
- const subscribe = useCallback(
983
- (listener) => store.subscribe(nodeId, listener),
984
- [store, nodeId]
1088
+ function listViews(apps) {
1089
+ return apps.flatMap(
1090
+ (app) => (app.views ?? []).filter((view) => !view.disabled).map(({ slot, path, instances }) => ({
1091
+ id: JSON.stringify([app.id, slot, path]),
1092
+ ...instances ? { instances } : {},
1093
+ app,
1094
+ slot,
1095
+ path
1096
+ }))
985
1097
  );
986
- const getSnapshot = useCallback(() => store.getNode(nodeId), [store, nodeId]);
987
- const node = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
988
- if (node === null) {
989
- return null;
990
- }
991
- if (node.kind === "text") {
992
- return node.text;
993
- }
994
- const childElements = node.children.map((child) => /* @__PURE__ */ jsx2(RemoteView, { nodeId: child.id }, child.id));
995
- if (node.kind === "fragment") {
996
- return createElement(Fragment, { key: node.id }, ...childElements);
997
- }
998
- return /* @__PURE__ */ jsx2(RemoteElementView, { node, ctx });
999
1098
  }
1000
- var RemoteView = memo(RemoteViewComponent);
1001
1099
 
1002
- // src/components/app-view.tsx
1100
+ // src/components/slot.tsx
1003
1101
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1004
- var AppView = ({
1005
- app,
1006
- slot,
1007
- createIframe,
1008
- fallback = null,
1009
- ...viewProps
1010
- }) => {
1011
- const { store, client } = useTailorRootContext("AppView");
1012
- const { theme, components: wrappedComponents } = client;
1013
- const reactId = useId2();
1014
- const getBackendSession = useMemo3(
1015
- () => createSessionProvider({ baseUrl: store.baseUrl, appId: app.id }),
1016
- [store.baseUrl, app.id]
1017
- );
1018
- const currentView = useSyncExternalStore2(
1102
+ function ManagedSlot({ app, name, instanceKey }) {
1103
+ const { store } = useTailorRootContext("Slot");
1104
+ const state = useSyncExternalStore3(
1019
1105
  store.views.subscribe,
1020
1106
  store.views.getSnapshot,
1021
1107
  store.views.getSnapshot
1022
1108
  );
1023
- const view = viewProps.view;
1024
- const suppliedContext = viewProps.context;
1025
- const context = useStableContext(suppliedContext);
1026
- const status = viewProps.status ?? "ready";
1027
- const props = useMemo3(() => {
1028
- if (view !== void 0) {
1029
- return {
1030
- slot,
1031
- view,
1032
- layers: [
1033
- ...(currentView?.layers ?? []).filter(
1034
- (layer) => layer.path !== view && isViewAncestor2(layer.path, view)
1035
- ),
1036
- { path: view, context, status }
1037
- ]
1038
- };
1039
- }
1040
- return currentView === null ? void 0 : { slot, ...currentView };
1041
- }, [context, currentView, view, status, slot]);
1042
- const meta = useSyncExternalStore2(store.subscribe, store.getMetaSnapshot, store.getMetaSnapshot);
1109
+ if (state === null) return null;
1110
+ return instanceKey === void 0 ? /* @__PURE__ */ jsx3(SlotRenderer, { app, name, state }) : /* @__PURE__ */ jsx3(InstanceSlot, { app, name, instanceKey, state });
1111
+ }
1112
+ function InstanceSlot({
1113
+ app,
1114
+ name,
1115
+ instanceKey,
1116
+ state
1117
+ }) {
1118
+ const { store } = useTailorRootContext("Slot");
1119
+ const instances = useSlotInstances({ app, slot: name });
1120
+ const meta = useSyncExternalStore3(store.subscribe, store.getMetaSnapshot, store.getMetaSnapshot);
1121
+ const instance = instances.data?.find((instance2) => instance2.key === instanceKey);
1122
+ const resolved = instances.isSuccess && instance && meta.schema ? resolveSlotView(app.views ?? [], name, state, meta.schema) : null;
1123
+ const view = resolved?.status === "ready" ? resolved.view : void 0;
1124
+ const stableContext = useStableContext(resolved?.context);
1125
+ const stableInstance = useStableContext(instance);
1126
+ const readyState = useMemo4(
1127
+ () => view === void 0 || !stableInstance ? null : {
1128
+ controlled: true,
1129
+ view,
1130
+ context: stableContext,
1131
+ status: "ready",
1132
+ instance: stableInstance
1133
+ },
1134
+ [view, stableContext, stableInstance]
1135
+ );
1136
+ if (instances.isPending) return /* @__PURE__ */ jsx3("div", { role: "status", children: "Loading view\u2026" });
1137
+ if (instances.error) return /* @__PURE__ */ jsx3("div", { role: "alert", children: instances.error.message });
1138
+ if (!instance) return /* @__PURE__ */ jsxs2("div", { role: "alert", children: [
1139
+ 'View instance "',
1140
+ instanceKey,
1141
+ '" is unavailable.'
1142
+ ] });
1143
+ if (!readyState) return null;
1144
+ return /* @__PURE__ */ jsx3(SlotRenderer, { app, name, state: readyState });
1145
+ }
1146
+ function ControlledSlot({
1147
+ app,
1148
+ name,
1149
+ view,
1150
+ context,
1151
+ status,
1152
+ instance
1153
+ }) {
1154
+ useTailorRootContext("Slot.Controlled");
1155
+ const stableContext = useStableContext(context);
1156
+ const stableInstance = useStableContext(instance);
1157
+ const state = useMemo4(
1158
+ () => ({
1159
+ controlled: true,
1160
+ view,
1161
+ context: stableContext,
1162
+ status,
1163
+ ...status === "ready" && stableInstance ? { instance: stableInstance } : {}
1164
+ }),
1165
+ [view, stableContext, status, stableInstance]
1166
+ );
1167
+ if (status === "ready" && !instance && app.views?.some(
1168
+ (entry) => entry.slot === name && entry.path === view && entry.instances && !entry.disabled
1169
+ )) {
1170
+ return /* @__PURE__ */ jsx3("div", { role: "alert", children: "A view instance is required. Pass instance to Slot.Controlled." });
1171
+ }
1172
+ return /* @__PURE__ */ jsx3(SlotRenderer, { app, name, state });
1173
+ }
1174
+ var Slot = Object.assign(ManagedSlot, { Controlled: ControlledSlot });
1175
+ function SlotRenderer({ app, name, state }) {
1176
+ const { store, client } = useTailorRootContext("Slot");
1177
+ const reactId = useId2();
1178
+ const getBackendSession = useMemo4(
1179
+ () => createSessionProvider2({ baseUrl: store.baseUrl, appId: app.id }),
1180
+ [store.baseUrl, app.id, app.currentDeployment?.id]
1181
+ );
1182
+ const meta = useSyncExternalStore3(store.subscribe, store.getMetaSnapshot, store.getMetaSnapshot);
1043
1183
  const previewSessionId = app.preview?.sessionId ?? "";
1044
- const appRef = useRef3(app);
1184
+ const appRef = useRef4(app);
1045
1185
  appRef.current = app;
1046
- const subscribePreview = useCallback2(
1186
+ const subscribePreview = useCallback3(
1047
1187
  (listener) => {
1048
1188
  const currentApp = appRef.current;
1049
1189
  return currentApp.preview?.sessionId === previewSessionId ? store.previews.subscribe(currentApp, listener) : () => {
@@ -1051,85 +1191,75 @@ var AppView = ({
1051
1191
  },
1052
1192
  [store, previewSessionId]
1053
1193
  );
1054
- const getPreviewSnapshot = useCallback2(
1194
+ const getPreviewSnapshot = useCallback3(
1055
1195
  () => store.previews.getSnapshot(previewSessionId),
1056
1196
  [store, previewSessionId]
1057
1197
  );
1058
- const previewSnapshot = useSyncExternalStore2(
1059
- subscribePreview,
1060
- getPreviewSnapshot,
1061
- getPreviewSnapshot
1062
- );
1063
- useEffect2(() => {
1064
- store.previews.updateApp(app);
1065
- }, [store, app]);
1066
- const runtimeProps = useMemo3(
1067
- () => props === void 0 ? void 0 : {
1068
- ...props,
1198
+ const preview = useSyncExternalStore3(subscribePreview, getPreviewSnapshot, getPreviewSnapshot);
1199
+ const props = useMemo4(
1200
+ () => ({
1201
+ ...state,
1202
+ slot: name,
1069
1203
  declaredViews: Object.keys(meta.schema?.views ?? {}),
1070
- supportedViews: meta.schema?.slots[slot]?.views ?? []
1071
- },
1072
- [props, meta.schema, slot]
1073
- );
1074
- const assetsBaseUrl = app.clientPath ? null : meta.assetsBaseUrl;
1075
- const appUrl = useMemo3(
1076
- () => resolveAppUrl(app, store.baseUrl, assetsBaseUrl),
1077
- [app, assetsBaseUrl, store.baseUrl]
1204
+ supportedViews: meta.schema?.slots[name]?.views ?? []
1205
+ }),
1206
+ [state, name, meta.schema]
1078
1207
  );
1079
- useEffect2(() => {
1208
+ const appUrl = useMemo4(() => {
1209
+ if (app.clientPath) return new URL(app.clientPath, store.baseUrl);
1210
+ if (!meta.assetsBaseUrl || !app.projectId || !app.currentDeployment?.id) return null;
1211
+ return new URL(
1212
+ `projects/${app.projectId}/apps/${app.id}/deployments/${app.currentDeployment.id}/client/client.js`,
1213
+ toBaseUrl(meta.assetsBaseUrl)
1214
+ );
1215
+ }, [app, store.baseUrl, meta.assetsBaseUrl]);
1216
+ useEffect3(() => {
1080
1217
  void store.fetchMeta();
1081
1218
  }, [store]);
1082
- if (props === void 0 || appUrl === null && previewSnapshot.source === null || meta.schema === null) {
1083
- return fallback;
1219
+ useEffect3(() => {
1220
+ store.previews.updateApp(app);
1221
+ }, [store, app]);
1222
+ if (meta.schema === null || appUrl === null && preview.source === null) return null;
1223
+ if (!("controlled" in state)) {
1224
+ const selected = selectSlotView(app.views ?? [], name, state.view, meta.schema);
1225
+ if (selected?.instances)
1226
+ return /* @__PURE__ */ jsx3("div", { role: "alert", children: "A view instance key is required. Pass instanceKey to Slot." });
1084
1227
  }
1085
1228
  const viewId = `tailorkit-view-${reactId.replaceAll(":", "")}`;
1086
- return /* @__PURE__ */ jsx3(PrimitiveThemeContext.Provider, { value: { viewId, theme }, children: /* @__PURE__ */ jsxs2("div", { "data-tailorkit-view": viewId, children: [
1087
- /* @__PURE__ */ jsx3("style", { "data-tailorkit-theme-style": viewId, children: buildThemeCss(viewId, theme) }),
1229
+ return /* @__PURE__ */ jsx3(PrimitiveThemeContext.Provider, { value: { viewId, theme: client.theme }, children: /* @__PURE__ */ jsxs2("div", { "data-tailorkit-view": viewId, children: [
1230
+ /* @__PURE__ */ jsx3("style", { "data-tailorkit-theme-style": viewId, children: buildThemeCss(viewId, client.theme) }),
1088
1231
  /* @__PURE__ */ jsx3(
1089
1232
  RemoteViewHost,
1090
1233
  {
1091
1234
  appUrl: (appUrl ?? new URL(`preview/${previewSessionId}/client.js`, store.baseUrl)).toString(),
1092
- sourceText: previewSnapshot.source ?? void 0,
1235
+ sourceText: preview.source ?? void 0,
1093
1236
  getBackendSession,
1094
- components: wrappedComponents,
1095
- createIframe,
1096
- props: runtimeProps
1237
+ components: client.components,
1238
+ props
1097
1239
  },
1098
- previewSnapshot.revision || appUrl?.toString()
1240
+ preview.revision || appUrl?.toString()
1099
1241
  )
1100
1242
  ] }) });
1101
- };
1102
- function resolveAppUrl(app, baseUrl, assetsBaseUrl) {
1103
- if (app.clientPath) {
1104
- return new URL(app.clientPath, baseUrl);
1105
- }
1106
- if (!assetsBaseUrl || !app.projectId || !app.currentDeployment?.id) {
1107
- return null;
1108
- }
1109
- return new URL(
1110
- `projects/${app.projectId}/apps/${app.id}/deployments/${app.currentDeployment.id}/client/client.js`,
1111
- toBaseUrl(assetsBaseUrl)
1112
- );
1113
1243
  }
1114
1244
 
1115
1245
  // src/tailorkit.tsx
1116
1246
  import { createElement as createElement2 } from "react";
1117
1247
 
1118
- // src/hooks/use-view.ts
1119
- import { useEffect as useEffect3, useMemo as useMemo4 } from "react";
1120
- function useView(view, options) {
1121
- const { store } = useTailorRootContext("useView");
1122
- const id = useMemo4(() => /* @__PURE__ */ Symbol("tailorkit-current-view"), []);
1248
+ // src/hooks/use-register-view.ts
1249
+ import { useEffect as useEffect4, useMemo as useMemo5 } from "react";
1250
+ function useRegisterView(view, options) {
1251
+ const { store } = useTailorRootContext("useRegisterView");
1252
+ const id = useMemo5(() => /* @__PURE__ */ Symbol("tailorkit-current-view"), []);
1123
1253
  const status = options.status ?? "ready";
1124
1254
  const context = "context" in options ? options.context : void 0;
1125
1255
  const contextSnapshot = useStableContext(context);
1126
- useEffect3(
1256
+ useEffect4(
1127
1257
  () => () => {
1128
1258
  store.views.unregister(id);
1129
1259
  },
1130
1260
  [id, store]
1131
1261
  );
1132
- useEffect3(() => {
1262
+ useEffect4(() => {
1133
1263
  store.views.register({
1134
1264
  context: contextSnapshot,
1135
1265
  id,
@@ -1140,29 +1270,45 @@ function useView(view, options) {
1140
1270
  }
1141
1271
 
1142
1272
  // src/hooks/use-apps.ts
1143
- import { useCallback as useCallback3, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3 } from "react";
1273
+ import { useCallback as useCallback4, useEffect as useEffect5, useMemo as useMemo6, useSyncExternalStore as useSyncExternalStore4 } from "react";
1274
+
1275
+ // src/scope-query.ts
1276
+ function normalizeScopeSelection(scopes) {
1277
+ if (scopes === void 0) {
1278
+ return { key: "*" };
1279
+ }
1280
+ const names = [...new Set(scopes)].sort();
1281
+ return { key: JSON.stringify(names), scopes: names };
1282
+ }
1283
+
1284
+ // src/hooks/use-apps.ts
1144
1285
  function useApps(options = {}) {
1145
1286
  return useAppsStore(useTailorRootContext("useApps").store, options);
1146
1287
  }
1147
1288
  function useAppsStore(store, options = {}) {
1148
- const selection = normalizeScopeSelection(options.scopes);
1149
- const subscribe = useCallback3(
1150
- (listener) => store.subscribeApps(selection.scopes, listener),
1151
- [store, selection.key]
1152
- );
1153
- const snapshot = useSyncExternalStore3(
1154
- subscribe,
1155
- () => store.getAppsSnapshot(selection.scopes),
1156
- () => store.getAppsSnapshot(selection.scopes)
1289
+ const snapshot = useAppsSnapshot(store);
1290
+ const scopes = normalizeScopeSelection(options.scopes);
1291
+ const appIds = normalizeScopeSelection(options.appIds);
1292
+ const data = useMemo6(
1293
+ () => snapshot.status === "ready" ? snapshot.apps.filter((app) => matchesApp(app, scopes.scopes, appIds.scopes)) : void 0,
1294
+ [snapshot, scopes.key, appIds.key]
1157
1295
  );
1158
- useEffect4(() => {
1159
- void store.fetchApps({ scopes: selection.scopes });
1160
- }, [store, selection.key]);
1161
- const refetch = useCallback3(
1162
- () => store.fetchApps({ force: true, scopes: selection.scopes }),
1163
- [store, selection.key]
1296
+ const refetch = useCallback4(() => store.fetchApps({ force: true }), [store]);
1297
+ return { ...toUseAppsResult(snapshot, refetch), data };
1298
+ }
1299
+ function matchesApp(app, scopes, appIds) {
1300
+ return (scopes === void 0 || app.scope !== void 0 && scopes.includes(app.scope.name)) && (appIds === void 0 || appIds.includes(app.id));
1301
+ }
1302
+ function useAppsSnapshot(store) {
1303
+ const snapshot = useSyncExternalStore4(
1304
+ store.subscribeApps,
1305
+ store.getAppsSnapshot,
1306
+ store.getAppsSnapshot
1164
1307
  );
1165
- return toUseAppsResult(snapshot, refetch);
1308
+ useEffect5(() => {
1309
+ void store.fetchApps();
1310
+ }, [store]);
1311
+ return snapshot;
1166
1312
  }
1167
1313
  function toUseAppsResult(snapshot, refetch) {
1168
1314
  return {
@@ -1177,6 +1323,23 @@ function toUseAppsResult(snapshot, refetch) {
1177
1323
  };
1178
1324
  }
1179
1325
 
1326
+ // src/hooks/use-views.ts
1327
+ import { useCallback as useCallback5, useMemo as useMemo7 } from "react";
1328
+ function useViews(options = {}) {
1329
+ const { store } = useTailorRootContext("useViews");
1330
+ const snapshot = useAppsSnapshot(store);
1331
+ const scopes = normalizeScopeSelection(options.scopes);
1332
+ const appIds = normalizeScopeSelection(options.appIds);
1333
+ const data = useMemo7(
1334
+ () => snapshot.status === "ready" ? snapshot.views.filter(
1335
+ (view) => (options.slot === void 0 || view.slot === options.slot) && matchesApp(view.app, scopes.scopes, appIds.scopes)
1336
+ ) : void 0,
1337
+ [snapshot, scopes.key, appIds.key, options.slot]
1338
+ );
1339
+ const refetch = useCallback5(() => store.fetchApps({ force: true }), [store]);
1340
+ return { ...toUseAppsResult(snapshot, refetch), data };
1341
+ }
1342
+
1180
1343
  // src/tailorkit.tsx
1181
1344
  var componentTagPrefix = "tailorkit-";
1182
1345
  var toComponentTagName = (name) => `${componentTagPrefix}${name.replaceAll(/([a-z0-9])([A-Z])/gu, "$1-$2").replaceAll(/[\s_]+/gu, "-").toLowerCase()}`;
@@ -1211,37 +1374,51 @@ function createReactTailorKitClient(options) {
1211
1374
  };
1212
1375
  const client = {
1213
1376
  ...clientConfig,
1214
- AppView: function ClientAppView(props) {
1215
- useTailorRootContext("AppView", client);
1216
- const TypedReactAppView = AppView;
1217
- return createElement2(
1218
- TypedReactAppView,
1219
- props
1220
- );
1221
- },
1377
+ Slot: Object.assign(
1378
+ function ClientSlot(props) {
1379
+ useTailorRootContext("Slot", client);
1380
+ const TypedReactSlot = Slot;
1381
+ return createElement2(TypedReactSlot, props);
1382
+ },
1383
+ {
1384
+ Controlled: function ClientControlledSlot(props) {
1385
+ useTailorRootContext("Slot.Controlled", client);
1386
+ const TypedControlledSlot = Slot.Controlled;
1387
+ return createElement2(TypedControlledSlot, props);
1388
+ }
1389
+ }
1390
+ ),
1222
1391
  useApps: function useClientApps(options2) {
1223
1392
  useTailorRootContext("useApps", client);
1224
1393
  return useApps(options2);
1225
1394
  },
1226
- useView: function useClientView(view, options2) {
1227
- useTailorRootContext("useView", client);
1228
- useView(view, options2);
1395
+ useViews: function useClientViews(options2) {
1396
+ useTailorRootContext("useViews", client);
1397
+ return useViews(options2);
1398
+ },
1399
+ useSlotInstances: function useClientSlotInstances(options2) {
1400
+ useTailorRootContext("useSlotInstances", client);
1401
+ return useSlotInstances(options2);
1402
+ },
1403
+ useRegisterView: function useClientRegisterView(view, options2) {
1404
+ useTailorRootContext("useRegisterView", client);
1405
+ useRegisterView(view, options2);
1229
1406
  }
1230
1407
  };
1231
1408
  return client;
1232
1409
  }
1233
1410
 
1234
1411
  // src/components/root.tsx
1235
- import { useEffect as useEffect5, useMemo as useMemo6, useState as useState2 } from "react";
1412
+ import { useEffect as useEffect6, useMemo as useMemo9, useState as useState3 } from "react";
1236
1413
 
1237
1414
  // src/components/render.tsx
1238
- import { cloneElement, createElement as createElement3, isValidElement, useMemo as useMemo5 } from "react";
1415
+ import { cloneElement, createElement as createElement3, isValidElement, useMemo as useMemo8 } from "react";
1239
1416
  function useRender({
1240
1417
  defaultTagName,
1241
1418
  props,
1242
1419
  render
1243
1420
  }) {
1244
- return useMemo5(() => {
1421
+ return useMemo8(() => {
1245
1422
  if (render) {
1246
1423
  if (!isValidElement(render)) {
1247
1424
  throw new Error("The render prop must be a valid React element.");
@@ -1311,17 +1488,17 @@ function isEventHandler(key) {
1311
1488
  import { jsx as jsx4 } from "react/jsx-runtime";
1312
1489
  function Root({ apps: appsProp, children, render, client, ...props }) {
1313
1490
  const baseUrl = toBaseUrl(client.baseUrl).toString();
1314
- const [previousStore, setStore] = useState2(() => createTailorKitStore(baseUrl, appsProp));
1491
+ const [previousStore, setStore] = useState3(() => createTailorKitStore(baseUrl, appsProp));
1315
1492
  let store = previousStore;
1316
1493
  if (previousStore.baseUrl.toString() !== baseUrl) {
1317
1494
  store = createTailorKitStore(baseUrl, appsProp);
1318
1495
  setStore(store);
1319
1496
  }
1320
- useEffect5(() => {
1497
+ useEffect6(() => {
1321
1498
  store.setProvidedApps(appsProp);
1322
1499
  }, [store, appsProp]);
1323
- useEffect5(() => () => store.previews.dispose(), [store]);
1324
- const context = useMemo6(
1500
+ useEffect6(() => () => store.previews.dispose(), [store]);
1501
+ const context = useMemo9(
1325
1502
  () => ({
1326
1503
  store,
1327
1504
  client
@@ -1336,8 +1513,8 @@ function Root({ apps: appsProp, children, render, client, ...props }) {
1336
1513
  return /* @__PURE__ */ jsx4(TailorRootContext.Provider, { value: context, children: element });
1337
1514
  }
1338
1515
  export {
1339
- AppView,
1340
1516
  Root,
1517
+ Slot,
1341
1518
  components,
1342
1519
  createTailorKitClient,
1343
1520
  primitives