@tailorkit/react 0.1.0-beta.13 → 0.1.0-beta.14

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.
@@ -2,7 +2,7 @@ import type { StandardJSONSchemaV1 } from "@standard-schema/spec";
2
2
  import type { ViewDefinition } from "@tailorkit/core/schema";
3
3
  import type { RegisteredViews } from "../tailor-kit";
4
4
  export type ViewName<TViews extends Record<string, ViewDefinition>> = keyof TViews & string;
5
- export type ViewContext<TView> = TView extends ViewDefinition<infer TContext> ? TContext extends StandardJSONSchemaV1 ? StandardJSONSchemaV1.InferOutput<TContext> : Record<string, never> : never;
5
+ export type ViewContext<TView> = TView extends StandardJSONSchemaV1 ? StandardJSONSchemaV1.InferOutput<TView> : Record<string, never>;
6
6
  interface ReadyViewOptions<TViews extends Record<string, ViewDefinition>, TView extends ViewName<TViews>> {
7
7
  context: ViewContext<TViews[TView]>;
8
8
  view: TView;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/app-view.tsx
2
- import { useEffect as useEffect2, useId as useId2, useMemo as useMemo3, useSyncExternalStore as useSyncExternalStore2 } from "react";
2
+ import { useCallback as useCallback2, useEffect as useEffect2, useId as useId2, useMemo as useMemo3, useRef as useRef3, useSyncExternalStore as useSyncExternalStore2 } from "react";
3
3
  import { isViewAncestor as isViewAncestor2 } from "@tailorkit/core/views";
4
4
 
5
5
  // src/view-registry.ts
@@ -63,8 +63,234 @@ function createViewRegistry() {
63
63
  };
64
64
  }
65
65
 
66
+ // src/preview-manager.ts
67
+ import {
68
+ createPreviewWebSocketClient,
69
+ previewMetadataSchema
70
+ } from "@tailorkit/client-platform/preview";
71
+ var empty = { revision: 0, source: null };
72
+ function viewerTokenExpiresAt(metadata) {
73
+ const expiresAt = Number(metadata.token.split(".", 1)[0]);
74
+ return Number.isSafeInteger(expiresAt) ? expiresAt : 0;
75
+ }
76
+ function bytesFromBase64(value) {
77
+ const raw = atob(value);
78
+ const bytes = new Uint8Array(raw.length);
79
+ for (let index = 0; index < raw.length; index++) {
80
+ bytes[index] = raw.codePointAt(index) ?? 0;
81
+ }
82
+ return bytes;
83
+ }
84
+ async function verify(candidate) {
85
+ let source = null;
86
+ for (const [fileIndex, file] of candidate.manifest.files.entries()) {
87
+ const parts = candidate.files[fileIndex];
88
+ if (!parts || parts.length !== file.chunks || parts.some((part) => part === null)) {
89
+ return null;
90
+ }
91
+ const bytes = new Uint8Array(file.size);
92
+ let offset = 0;
93
+ for (const part of parts) {
94
+ if (!part) {
95
+ return null;
96
+ }
97
+ bytes.set(part, offset);
98
+ offset += part.length;
99
+ }
100
+ if (offset !== file.size) {
101
+ return null;
102
+ }
103
+ const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
104
+ if (digest !== file.sha256) {
105
+ return null;
106
+ }
107
+ if (file.path === "client.js") {
108
+ source = new TextDecoder().decode(bytes);
109
+ }
110
+ }
111
+ return source;
112
+ }
113
+ function createPreviewManager(baseUrl, onEnded) {
114
+ const entries = /* @__PURE__ */ new Map();
115
+ const notify = (entry) => {
116
+ for (const listener of entry.listeners) {
117
+ listener();
118
+ }
119
+ };
120
+ const close = (entry) => {
121
+ if (entry.reconnect) {
122
+ clearTimeout(entry.reconnect);
123
+ }
124
+ entry.reconnect = null;
125
+ const socket = entry.socket;
126
+ entry.socket = null;
127
+ socket?.close();
128
+ entry.connecting = false;
129
+ entry.candidate = null;
130
+ };
131
+ const end = (entry) => {
132
+ close(entry);
133
+ entry.metadata = null;
134
+ entry.snapshot = empty;
135
+ notify(entry);
136
+ onEnded();
137
+ };
138
+ const handleEvent = async (entry, event) => {
139
+ if (event.type === "ended") {
140
+ end(entry);
141
+ return;
142
+ }
143
+ if (event.revision <= entry.snapshot.revision) {
144
+ return;
145
+ }
146
+ if (event.type === "begin") {
147
+ entry.candidate = {
148
+ revision: event.revision,
149
+ manifest: event.manifest,
150
+ files: event.manifest.files.map((file) => Array.from({ length: file.chunks }, () => null))
151
+ };
152
+ return;
153
+ }
154
+ const candidate = entry.candidate;
155
+ if (!candidate || candidate.revision !== event.revision) {
156
+ return;
157
+ }
158
+ if (event.type === "chunk") {
159
+ const file = candidate.manifest.files[event.fileIndex];
160
+ const slots = candidate.files[event.fileIndex];
161
+ if (!file || !slots || event.chunkIndex < 0 || event.chunkIndex >= slots.length || slots[event.chunkIndex]) {
162
+ entry.candidate = null;
163
+ return;
164
+ }
165
+ const bytes = bytesFromBase64(event.base64);
166
+ if (bytes.length !== Math.min(256 * 1024, file.size - event.chunkIndex * 256 * 1024)) {
167
+ entry.candidate = null;
168
+ return;
169
+ }
170
+ slots[event.chunkIndex] = bytes;
171
+ return;
172
+ }
173
+ if (event.type === "complete") {
174
+ const source = await verify(candidate);
175
+ if (source !== null && entry.candidate === candidate && candidate.revision > entry.snapshot.revision) {
176
+ entry.snapshot = { revision: candidate.revision, source };
177
+ notify(entry);
178
+ }
179
+ if (entry.candidate === candidate) {
180
+ entry.candidate = null;
181
+ }
182
+ }
183
+ };
184
+ const connect = async (entry) => {
185
+ if (!entry.listeners.size || !entry.app.preview || entry.socket || entry.connecting) {
186
+ return;
187
+ }
188
+ entry.connecting = true;
189
+ const appMetadata = entry.app.preview;
190
+ let metadata = entry.metadata && viewerTokenExpiresAt(entry.metadata) > Math.max(Date.now(), viewerTokenExpiresAt(appMetadata)) ? entry.metadata : appMetadata;
191
+ try {
192
+ const refresh = new URL("preview/metadata", baseUrl);
193
+ refresh.searchParams.set("sessionId", metadata.sessionId);
194
+ const response = await fetch(refresh, { credentials: "same-origin" });
195
+ if (response.ok) {
196
+ metadata = previewMetadataSchema.parse(await response.json());
197
+ entry.metadata = metadata;
198
+ } else if (response.status === 404) {
199
+ end(entry);
200
+ return;
201
+ }
202
+ } catch {
203
+ }
204
+ if (!entry.listeners.size) {
205
+ entry.connecting = false;
206
+ return;
207
+ }
208
+ const socket = new WebSocket(metadata.websocketUrl, metadata.token);
209
+ entry.socket = socket;
210
+ entry.connecting = false;
211
+ socket.addEventListener("open", () => {
212
+ entry.delay = 1e3;
213
+ const client = createPreviewWebSocketClient(socket);
214
+ void (async () => {
215
+ try {
216
+ const stream = await client.subscribe();
217
+ for await (const event of stream) {
218
+ await handleEvent(entry, event);
219
+ }
220
+ } catch {
221
+ } finally {
222
+ socket.close();
223
+ }
224
+ })();
225
+ });
226
+ socket.addEventListener("error", () => socket.close());
227
+ socket.addEventListener("close", () => {
228
+ if (entry.socket !== socket) {
229
+ return;
230
+ }
231
+ entry.socket = null;
232
+ entry.candidate = null;
233
+ if (entry.listeners.size) {
234
+ entry.reconnect = setTimeout(() => {
235
+ entry.reconnect = null;
236
+ void connect(entry);
237
+ }, entry.delay);
238
+ entry.delay = Math.min(entry.delay * 2, 3e4);
239
+ }
240
+ });
241
+ };
242
+ return {
243
+ getSnapshot: (sessionId) => entries.get(sessionId)?.snapshot ?? empty,
244
+ updateApp: (app) => {
245
+ const sessionId = app.preview?.sessionId;
246
+ const entry = sessionId && entries.get(sessionId);
247
+ if (entry) {
248
+ entry.app = app;
249
+ }
250
+ },
251
+ subscribe: (app, listener) => {
252
+ const sessionId = app.preview?.sessionId;
253
+ if (!sessionId) {
254
+ return () => {
255
+ };
256
+ }
257
+ let entry = entries.get(sessionId);
258
+ if (!entry) {
259
+ entry = {
260
+ app,
261
+ metadata: null,
262
+ candidate: null,
263
+ listeners: /* @__PURE__ */ new Set(),
264
+ snapshot: empty,
265
+ socket: null,
266
+ connecting: false,
267
+ reconnect: null,
268
+ delay: 1e3
269
+ };
270
+ entries.set(sessionId, entry);
271
+ }
272
+ entry.app = app;
273
+ entry.listeners.add(listener);
274
+ void connect(entry);
275
+ return () => {
276
+ entry?.listeners.delete(listener);
277
+ if (entry && !entry.listeners.size) {
278
+ close(entry);
279
+ entries.delete(sessionId);
280
+ }
281
+ };
282
+ },
283
+ dispose: () => {
284
+ for (const entry of entries.values()) {
285
+ close(entry);
286
+ }
287
+ entries.clear();
288
+ }
289
+ };
290
+ }
291
+
66
292
  // src/store.ts
67
- function createTailorKitStore(baseUrlInput, initialApps, previewSessionId) {
293
+ function createTailorKitStore(baseUrlInput, initialApps) {
68
294
  const baseUrl = toBaseUrl(baseUrlInput);
69
295
  const listeners = /* @__PURE__ */ new Set();
70
296
  let providedApps = initialApps;
@@ -118,9 +344,6 @@ function createTailorKitStore(baseUrlInput, initialApps, previewSessionId) {
118
344
  emit();
119
345
  const requestId = ++fetchAppsRequestId;
120
346
  const appsUrl = new URL("apps", baseUrl);
121
- if (previewSessionId) {
122
- appsUrl.searchParams.set("previewSessionId", previewSessionId);
123
- }
124
347
  fetchAppsPromise = fetch(appsUrl).then(async (response) => {
125
348
  if (!response.ok) {
126
349
  throw new Error(`Unable to fetch TailorKit apps from ${baseUrl.toString()}.`);
@@ -185,7 +408,12 @@ function createTailorKitStore(baseUrlInput, initialApps, previewSessionId) {
185
408
  };
186
409
  }
187
410
  };
188
- return store;
411
+ return {
412
+ ...store,
413
+ previews: createPreviewManager(baseUrl, () => {
414
+ void store.fetchApps({ force: true });
415
+ })
416
+ };
189
417
  }
190
418
  function toBaseUrl(value) {
191
419
  const url = value instanceof URL ? new URL(value) : new URL(value);
@@ -562,6 +790,7 @@ var RemoteErrorBoundary = class extends Component {
562
790
  };
563
791
  function RemoteViewHost({
564
792
  appUrl,
793
+ sourceText,
565
794
  components: components2,
566
795
  createIframe,
567
796
  props
@@ -609,6 +838,7 @@ function RemoteViewHost({
609
838
  useEffect(() => {
610
839
  const host = createIframeUiHost(appUrl, {
611
840
  createIframe,
841
+ sourceText,
612
842
  onError: (error2) => {
613
843
  console.error("TailorKit remote app failed", error2);
614
844
  setError(error2);
@@ -634,7 +864,7 @@ function RemoteViewHost({
634
864
  hostRef.current = null;
635
865
  host.destroy();
636
866
  };
637
- }, [appUrl, createIframe, store]);
867
+ }, [appUrl, createIframe, sourceText, store]);
638
868
  if (status === "error" && error) {
639
869
  return createElement("div", null, formatError(error));
640
870
  }
@@ -734,6 +964,29 @@ var AppView = ({
734
964
  return currentView === null ? void 0 : { slot, ...currentView };
735
965
  }, [context, currentView, view, status, slot]);
736
966
  const meta = useSyncExternalStore2(store.subscribe, store.getMetaSnapshot, store.getMetaSnapshot);
967
+ const previewSessionId = app.preview?.sessionId ?? "";
968
+ const appRef = useRef3(app);
969
+ appRef.current = app;
970
+ const subscribePreview = useCallback2(
971
+ (listener) => {
972
+ const currentApp = appRef.current;
973
+ return currentApp.preview?.sessionId === previewSessionId ? store.previews.subscribe(currentApp, listener) : () => {
974
+ };
975
+ },
976
+ [store, previewSessionId]
977
+ );
978
+ const getPreviewSnapshot = useCallback2(
979
+ () => store.previews.getSnapshot(previewSessionId),
980
+ [store, previewSessionId]
981
+ );
982
+ const previewSnapshot = useSyncExternalStore2(
983
+ subscribePreview,
984
+ getPreviewSnapshot,
985
+ getPreviewSnapshot
986
+ );
987
+ useEffect2(() => {
988
+ store.previews.updateApp(app);
989
+ }, [store, app]);
737
990
  const runtimeProps = useMemo3(
738
991
  () => props === void 0 ? void 0 : {
739
992
  ...props,
@@ -750,7 +1003,7 @@ var AppView = ({
750
1003
  useEffect2(() => {
751
1004
  void store.fetchMeta();
752
1005
  }, [store]);
753
- if (props === void 0 || appUrl === null || meta.schema === null) {
1006
+ if (props === void 0 || appUrl === null && previewSnapshot.source === null || meta.schema === null) {
754
1007
  return fallback;
755
1008
  }
756
1009
  const viewId = `tailorkit-view-${reactId.replaceAll(":", "")}`;
@@ -759,11 +1012,13 @@ var AppView = ({
759
1012
  /* @__PURE__ */ jsx3(
760
1013
  RemoteViewHost,
761
1014
  {
762
- appUrl: appUrl.toString(),
1015
+ appUrl: (appUrl ?? new URL(`preview/${previewSessionId}/client.js`, store.baseUrl)).toString(),
1016
+ sourceText: previewSnapshot.source ?? void 0,
763
1017
  components: wrappedComponents,
764
1018
  createIframe,
765
1019
  props: runtimeProps
766
- }
1020
+ },
1021
+ previewSnapshot.revision || appUrl?.toString()
767
1022
  )
768
1023
  ] }) });
769
1024
  };
@@ -810,7 +1065,6 @@ function createReactTailorKitClient(options) {
810
1065
  return {
811
1066
  baseUrl: options.baseUrl,
812
1067
  components: wrappedComponents,
813
- previewSessionId: options.previewSessionId,
814
1068
  theme
815
1069
  };
816
1070
  }
@@ -895,17 +1149,16 @@ function isEventHandler(key) {
895
1149
  import { jsx as jsx4 } from "react/jsx-runtime";
896
1150
  function Root({ apps: appsProp, children, render, client, ...props }) {
897
1151
  const baseUrl = toBaseUrl(client.baseUrl).toString();
898
- const [previousStore, setStore] = useState2(
899
- () => createTailorKitStore(baseUrl, appsProp, client.previewSessionId)
900
- );
1152
+ const [previousStore, setStore] = useState2(() => createTailorKitStore(baseUrl, appsProp));
901
1153
  let store = previousStore;
902
1154
  if (previousStore.baseUrl.toString() !== baseUrl) {
903
- store = createTailorKitStore(baseUrl, appsProp, client.previewSessionId);
1155
+ store = createTailorKitStore(baseUrl, appsProp);
904
1156
  setStore(store);
905
1157
  }
906
1158
  useEffect3(() => {
907
1159
  store.setProvidedApps(appsProp);
908
1160
  }, [store, appsProp]);
1161
+ useEffect3(() => () => store.previews.dispose(), [store]);
909
1162
  const context = useMemo5(
910
1163
  () => ({
911
1164
  store,
@@ -922,7 +1175,7 @@ function Root({ apps: appsProp, children, render, client, ...props }) {
922
1175
  }
923
1176
 
924
1177
  // src/hooks/use-apps.ts
925
- import { useCallback as useCallback2, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3 } from "react";
1178
+ import { useCallback as useCallback3, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3 } from "react";
926
1179
  function useApps() {
927
1180
  return useAppsStore(useTailorRootContext("useApps").store);
928
1181
  }
@@ -935,7 +1188,7 @@ function useAppsStore(store) {
935
1188
  useEffect4(() => {
936
1189
  void store.fetchApps();
937
1190
  }, [store]);
938
- const refetch = useCallback2(() => store.fetchApps({ force: true }), [store]);
1191
+ const refetch = useCallback3(() => store.fetchApps({ force: true }), [store]);
939
1192
  return toUseAppsResult(snapshot, refetch);
940
1193
  }
941
1194
  function toUseAppsResult(snapshot, refetch) {
@@ -0,0 +1,11 @@
1
+ import type { TailorKitApp } from "./tailor-kit";
2
+ export interface PreviewSnapshot {
3
+ revision: number;
4
+ source: string | null;
5
+ }
6
+ export declare function createPreviewManager(baseUrl: URL, onEnded: () => void): {
7
+ getSnapshot: (sessionId: string) => PreviewSnapshot;
8
+ updateApp: (app: TailorKitApp) => void;
9
+ subscribe: (app: TailorKitApp, listener: () => void) => (() => void);
10
+ dispose: () => void;
11
+ };
@@ -1,11 +1,12 @@
1
1
  import type { ReactNode } from "react";
2
2
  interface RemoteViewHostProps {
3
3
  appUrl: string | URL;
4
+ sourceText?: string;
4
5
  components: Record<string, unknown>;
5
6
  createIframe?: () => HTMLIFrameElement;
6
7
  props?: Record<string, unknown>;
7
8
  }
8
- export declare function RemoteViewHost({ appUrl, components, createIframe, props, }: RemoteViewHostProps): ReactNode;
9
+ export declare function RemoteViewHost({ appUrl, sourceText, components, createIframe, props, }: RemoteViewHostProps): ReactNode;
9
10
  interface RemoteViewProps {
10
11
  nodeId: string;
11
12
  }
package/dist/store.d.ts CHANGED
@@ -12,7 +12,7 @@ interface TailorKitMetaSnapshot {
12
12
  status: "error" | "idle" | "loading" | "ready";
13
13
  }
14
14
  export type TailorKitStore = ReturnType<typeof createTailorKitStore>;
15
- export declare function createTailorKitStore(baseUrlInput: string | URL, initialApps?: TailorKitApp[], previewSessionId?: string): {
15
+ export declare function createTailorKitStore(baseUrlInput: string | URL, initialApps?: TailorKitApp[]): {
16
16
  baseUrl: URL;
17
17
  views: {
18
18
  getSnapshot: () => import("@tailorkit/core/views").ActiveView | null;
@@ -28,6 +28,12 @@ export declare function createTailorKitStore(baseUrlInput: string | URL, initial
28
28
  getAppsSnapshot: () => TailorKitAppsSnapshot;
29
29
  getMetaSnapshot: () => TailorKitMetaSnapshot;
30
30
  subscribe: (listener: () => void) => (() => void);
31
+ previews: {
32
+ getSnapshot: (sessionId: string) => import("./preview-manager").PreviewSnapshot;
33
+ updateApp: (app: TailorKitApp) => void;
34
+ subscribe: (app: TailorKitApp, listener: () => void) => (() => void);
35
+ dispose: () => void;
36
+ };
31
37
  };
32
38
  export declare function toBaseUrl(value: string | URL): URL;
33
39
  export {};
@@ -30,7 +30,9 @@ export interface TailorKitApp {
30
30
  name?: string;
31
31
  preview?: {
32
32
  sessionId: string;
33
- status: "connected" | "offline";
33
+ expiresAt: string;
34
+ websocketUrl: string;
35
+ token: string;
34
36
  };
35
37
  }
36
38
  interface AppViewBaseProps {
@@ -70,11 +72,6 @@ export interface TailorKitInstance<TViews extends Record<string, ViewDefinition>
70
72
  readonly $slots?: TSlots;
71
73
  readonly $views?: TViews;
72
74
  readonly baseUrl: string | URL;
73
- /**
74
- * A host-selected, scope-authorized local-development preview session.
75
- * The host handler resolves this server-side before returning app URLs.
76
- */
77
- readonly previewSessionId?: string;
78
75
  readonly components: Record<string, unknown>;
79
76
  readonly theme: TailorKitTheme;
80
77
  }
@@ -87,7 +84,7 @@ interface TailorKitServerShape {
87
84
  $internal: {
88
85
  schema: {
89
86
  components: Record<string, unknown>;
90
- views: Record<string, unknown>;
87
+ contexts: Record<string, unknown>;
91
88
  };
92
89
  };
93
90
  }
@@ -95,13 +92,12 @@ type ServerComponentMap<TTailor extends TailorKitServerShape> = TTailor["$intern
95
92
  type ServerComponents<TTailor extends TailorKitServerShape> = {
96
93
  [TName in keyof ServerComponentMap<TTailor>]: ServerComponentMap<TTailor>[TName] extends AnyComponentDefinition ? ServerComponentMap<TTailor>[TName] : never;
97
94
  };
98
- type ServerViewMap<TTailor extends TailorKitServerShape> = TTailor["$internal"]["schema"]["views"];
95
+ type ServerViewMap<TTailor extends TailorKitServerShape> = TTailor["$internal"]["schema"]["contexts"];
99
96
  type ServerViews<TTailor extends TailorKitServerShape> = {
100
97
  [TName in keyof ServerViewMap<TTailor>]: ServerViewMap<TTailor>[TName] extends ViewDefinition ? ServerViewMap<TTailor>[TName] : never;
101
98
  };
102
99
  export declare function createTailorKitClient<TTailor extends TailorKitServerShape>(options: {
103
100
  baseUrl: string | URL;
104
- previewSessionId?: string;
105
101
  components?: CompleteComponentRenderers<ServerComponents<TTailor>>;
106
102
  theme?: TailorKitTheme;
107
103
  }): TailorKitInstance<ServerViews<TTailor>, TTailor extends {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailorkit/react",
3
- "version": "0.1.0-beta.13",
3
+ "version": "0.1.0-beta.14",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,8 +23,9 @@
23
23
  "dependencies": {
24
24
  "@floating-ui/react-dom": "^2.1.9",
25
25
  "@standard-schema/spec": "^1.1.0",
26
- "@tailorkit/core": "0.1.0-beta.13",
27
- "@tailorkit/sandbox": "0.1.0-beta.13"
26
+ "@tailorkit/client-platform": "0.1.0-beta.14",
27
+ "@tailorkit/core": "0.1.0-beta.14",
28
+ "@tailorkit/sandbox": "0.1.0-beta.14"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@tailorkit/config": "0.0.0",