@tailorkit/react 0.1.0-beta.3 → 0.1.0-beta.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.
Files changed (41) hide show
  1. package/dist/components/context.d.ts +0 -1
  2. package/dist/components/index.d.ts +0 -1
  3. package/dist/components/render.d.ts +0 -1
  4. package/dist/components/root.d.ts +0 -1
  5. package/dist/hooks/use-apps.d.ts +0 -1
  6. package/dist/hooks/use-current-screen.d.ts +0 -1
  7. package/dist/index.d.ts +0 -1
  8. package/dist/node-store.d.ts +0 -1
  9. package/dist/primitives.d.ts +0 -1
  10. package/dist/remote-context.d.ts +0 -1
  11. package/dist/remote-view.d.ts +0 -1
  12. package/dist/tailor-kit.d.ts +0 -1
  13. package/package.json +4 -15
  14. package/dist/components/context.d.ts.map +0 -1
  15. package/dist/components/index.d.ts.map +0 -1
  16. package/dist/components/render.d.ts.map +0 -1
  17. package/dist/components/root.d.ts.map +0 -1
  18. package/dist/hooks/use-apps.d.ts.map +0 -1
  19. package/dist/hooks/use-current-screen.d.ts.map +0 -1
  20. package/dist/index.d.ts.map +0 -1
  21. package/dist/node-store.d.ts.map +0 -1
  22. package/dist/primitives.d.ts.map +0 -1
  23. package/dist/remote-context.d.ts.map +0 -1
  24. package/dist/remote-view.d.ts.map +0 -1
  25. package/dist/tailor-kit.d.ts.map +0 -1
  26. package/src/__tests__/node-store.test.ts +0 -206
  27. package/src/__tests__/primitives.test.tsx +0 -85
  28. package/src/__tests__/tailor-kit.test.tsx +0 -424
  29. package/src/__tests__/tailor-kit.types.tsx +0 -151
  30. package/src/components/context.ts +0 -21
  31. package/src/components/index.ts +0 -1
  32. package/src/components/render.tsx +0 -116
  33. package/src/components/root.tsx +0 -37
  34. package/src/hooks/use-apps.ts +0 -47
  35. package/src/hooks/use-current-screen.ts +0 -81
  36. package/src/index.ts +0 -8
  37. package/src/node-store.ts +0 -131
  38. package/src/primitives.tsx +0 -261
  39. package/src/remote-context.ts +0 -11
  40. package/src/remote-view.tsx +0 -232
  41. package/src/tailor-kit.tsx +0 -501
@@ -1,424 +0,0 @@
1
- import { act, cleanup, render, screen as testingScreen, waitFor } from "@testing-library/react";
2
- import { createElement } from "react";
3
- import type { ReactNode } from "react";
4
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
- import { createTailorKitServer } from "@tailorkit/core/server";
6
- import type { IframeUiHost } from "@tailorkit/sandbox/host";
7
- import type { HostToWorkerPayload, RemoteNode } from "@tailorkit/sandbox/protocol";
8
- import { createTailorKitClient } from "../tailor-kit";
9
- import type { TailorKitApp } from "../tailor-kit";
10
-
11
- const hostRecords: { appUrl: string; props: Record<string, unknown> | undefined }[] = [];
12
-
13
- vi.mock("@tailorkit/sandbox/host", () => ({
14
- createIframeUiHost: (
15
- appUrl: string | URL,
16
- options: { props?: Record<string, unknown> } = {},
17
- ): IframeUiHost => {
18
- hostRecords.push({ appUrl: appUrl.toString(), props: options.props });
19
-
20
- const tree: RemoteNode = {
21
- children: [{ id: `text-${hostRecords.length}`, kind: "text", text: appUrl.toString() }],
22
- id: `root-${hostRecords.length}`,
23
- kind: "element",
24
- props: {},
25
- type: appUrl.toString().includes("missing-component") ? "MissingComponent" : "Button",
26
- };
27
- let listener: (() => void) | null = null;
28
-
29
- return {
30
- destroy: () => {},
31
- dispatch: (_payload: HostToWorkerPayload) => {},
32
- getSnapshot: () => tree,
33
- iframe: document.createElement("iframe"),
34
- mount: () => {
35
- listener?.();
36
- },
37
- subscribe: (nextListener: () => void) => {
38
- listener = nextListener;
39
- return () => {
40
- listener = null;
41
- };
42
- },
43
- } as unknown as IframeUiHost;
44
- },
45
- }));
46
-
47
- const emptySchema = {
48
- "~standard": {
49
- jsonSchema: {
50
- input: () => ({}),
51
- output: () => ({}),
52
- },
53
- validate: (value: unknown) => ({ value }),
54
- vendor: "test",
55
- version: 1,
56
- },
57
- } as const;
58
-
59
- const server = createTailorKitServer({
60
- components: {
61
- Button: {},
62
- },
63
- screens: {
64
- "/": { context: emptySchema },
65
- "/home": { context: emptySchema },
66
- "/home/detail": { context: emptySchema },
67
- "/user": { context: emptySchema },
68
- },
69
- });
70
-
71
- const components = {
72
- Button: ({ slots }: { slots: { default?: ReactNode } }) =>
73
- createElement("button", null, slots.default),
74
- };
75
-
76
- const schema = server.$internal.schema;
77
-
78
- function CurrentScreenRoute({
79
- nested,
80
- tailor,
81
- }: {
82
- nested: boolean;
83
- tailor: ReturnType<typeof createTailorKitClient<typeof server>>;
84
- }) {
85
- tailor.useCurrentScreen(
86
- nested
87
- ? {
88
- context: { detail: "profile", page: "home" },
89
- screen: "/home/detail",
90
- }
91
- : {
92
- context: { page: "home" },
93
- screen: "/home",
94
- },
95
- );
96
-
97
- return <tailor.AppView app={{ clientPath: "/apps/todo.js", id: "todo" }} />;
98
- }
99
-
100
- function CurrentScreenHost({
101
- nested,
102
- tailor,
103
- }: {
104
- nested: boolean;
105
- tailor: ReturnType<typeof createTailorKitClient<typeof server>>;
106
- }) {
107
- return (
108
- <tailor.Root apps={[{ clientPath: "/apps/todo.js", id: "todo" }]}>
109
- <CurrentScreenRoute nested={nested} tailor={tailor} />
110
- </tailor.Root>
111
- );
112
- }
113
-
114
- function HomeAppView({
115
- app,
116
- tailor,
117
- }: {
118
- app: TailorKitApp;
119
- tailor: ReturnType<typeof createTailorKitClient<typeof server>>;
120
- }) {
121
- tailor.useCurrentScreen({ context: { page: "home" }, screen: "/home" });
122
- return <tailor.AppView app={app} />;
123
- }
124
-
125
- describe("tailorKitClient React adapter", () => {
126
- beforeEach(() => {
127
- hostRecords.length = 0;
128
- vi.restoreAllMocks();
129
- vi.spyOn(globalThis, "fetch").mockResolvedValue(
130
- Response.json({
131
- assetsBaseUrl: "http://assets.test/",
132
- schema: schema.serialize(),
133
- }),
134
- );
135
- });
136
-
137
- afterEach(() => {
138
- cleanup();
139
- });
140
-
141
- it("fetches and caches apps", async () => {
142
- vi.spyOn(globalThis, "fetch").mockResolvedValue(Response.json([{ id: "todo", name: "Todo" }]));
143
- const tailor = createTailorKitClient<typeof server>({
144
- baseUrl: "http://runtime.test/api/tailorkit",
145
- components,
146
- });
147
-
148
- function AppList() {
149
- const { data, status } = tailor.useApps();
150
- return createElement("p", null, `${status}:${(data ?? []).map((app) => app.id).join(",")}`);
151
- }
152
-
153
- render(createElement("div", null, createElement(AppList), createElement(AppList)));
154
-
155
- await waitFor(() => {
156
- expect(testingScreen.getAllByText("ready:todo")).toHaveLength(2);
157
- });
158
- expect(globalThis.fetch).toHaveBeenCalledTimes(1);
159
- expect(globalThis.fetch).toHaveBeenCalledWith(
160
- new URL("apps", "http://runtime.test/api/tailorkit/"),
161
- );
162
- expect(tailor.getApps()).toEqual([{ id: "todo", name: "Todo" }]);
163
- expect(tailor.getApp("todo")).toEqual({ id: "todo", name: "Todo" });
164
- });
165
-
166
- it("derives the hierarchy from the current screen and reuses its extended context", async () => {
167
- const tailor = createTailorKitClient<typeof server>({
168
- baseUrl: "http://runtime.test",
169
- components,
170
- });
171
-
172
- const view = render(<CurrentScreenHost nested tailor={tailor} />);
173
-
174
- await waitFor(() => {
175
- expect(hostRecords.at(-1)?.props).toMatchObject({
176
- screen: {
177
- context: { detail: "profile", page: "home" },
178
- path: "/home/detail",
179
- status: "ready",
180
- },
181
- });
182
- });
183
-
184
- await act(() => {
185
- view.rerender(<CurrentScreenHost nested={false} tailor={tailor} />);
186
- });
187
-
188
- await waitFor(() => {
189
- expect(hostRecords.at(-1)?.props).toMatchObject({
190
- screen: {
191
- context: { page: "home" },
192
- path: "/home",
193
- status: "ready",
194
- },
195
- });
196
- });
197
- });
198
-
199
- it("publishes loading and error states without stale context", async () => {
200
- const tailor = createTailorKitClient<typeof server>({
201
- baseUrl: "http://runtime.test",
202
- components,
203
- });
204
-
205
- function Route({ status }: { status: "error" | "loading" }) {
206
- tailor.useCurrentScreen({ screen: "/home/detail", status });
207
- return <tailor.AppView app={{ clientPath: "/apps/todo.js", id: "todo" }} />;
208
- }
209
-
210
- const view = render(
211
- <tailor.Root apps={[{ clientPath: "/apps/todo.js", id: "todo" }]}>
212
- <Route status="loading" />
213
- </tailor.Root>,
214
- );
215
-
216
- await waitFor(() => {
217
- expect(hostRecords.at(-1)?.props).toEqual({
218
- screen: { context: undefined, path: "/home/detail", status: "loading" },
219
- });
220
- });
221
-
222
- view.rerender(
223
- <tailor.Root apps={[{ clientPath: "/apps/todo.js", id: "todo" }]}>
224
- <Route status="error" />
225
- </tailor.Root>,
226
- );
227
-
228
- await waitFor(() => {
229
- expect(hostRecords.at(-1)?.props).toEqual({
230
- screen: { context: undefined, path: "/home/detail", status: "error" },
231
- });
232
- });
233
- });
234
-
235
- it("renders the current match for multiple direct app props", async () => {
236
- hostRecords.length = 0;
237
- const tailor = createTailorKitClient<typeof server>({
238
- baseUrl: "http://runtime.test",
239
- components,
240
- });
241
-
242
- function Route() {
243
- tailor.useCurrentScreen({ context: { page: "home" }, screen: "/home" });
244
- return (
245
- <>
246
- <tailor.AppView app={{ clientPath: "/apps/b.js", id: "b" }} />
247
- <tailor.AppView
248
- app={{
249
- currentDeployment: { id: "deployment_1" },
250
- id: "a",
251
- projectId: "project_1",
252
- }}
253
- />
254
- </>
255
- );
256
- }
257
-
258
- render(
259
- <tailor.Root
260
- apps={[
261
- { clientPath: "/apps/b.js", id: "b" },
262
- {
263
- currentDeployment: { id: "deployment_1" },
264
- id: "a",
265
- projectId: "project_1",
266
- },
267
- ]}
268
- >
269
- <Route />
270
- </tailor.Root>,
271
- );
272
-
273
- await waitFor(() => {
274
- expect(hostRecords).toHaveLength(2);
275
- });
276
- expect(hostRecords.map((record) => record.appUrl)).toEqual([
277
- "http://runtime.test/apps/b.js",
278
- "http://assets.test/projects/project_1/apps/a/deployments/deployment_1/files/client.js",
279
- ]);
280
- expect(hostRecords.map((record) => record.props?.screen)).toEqual([
281
- { context: { page: "home" }, path: "/home", status: "ready" },
282
- { context: { page: "home" }, path: "/home", status: "ready" },
283
- ]);
284
- });
285
-
286
- it("renders an explicit screen override without a registered current screen", async () => {
287
- const tailor = createTailorKitClient<typeof server>({
288
- baseUrl: "http://runtime.test",
289
- components,
290
- });
291
-
292
- render(
293
- <tailor.AppView
294
- app={{ clientPath: "/apps/todo.js", id: "todo" }}
295
- context={{ userId: "user_1" }}
296
- screen="/user"
297
- />,
298
- );
299
-
300
- await waitFor(() => {
301
- expect(hostRecords.at(-1)?.props).toMatchObject({
302
- screen: {
303
- context: { userId: "user_1" },
304
- path: "/user",
305
- status: "ready",
306
- },
307
- });
308
- });
309
- });
310
-
311
- it("warns when multiple hooks register screens at the same hierarchy depth", async () => {
312
- const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});
313
- const tailor = createTailorKitClient<typeof server>({
314
- baseUrl: "http://runtime.test",
315
- components,
316
- });
317
-
318
- function HomeRoute() {
319
- tailor.useCurrentScreen({ context: { page: "home" }, screen: "/home" });
320
- return null;
321
- }
322
-
323
- function UserRoute() {
324
- tailor.useCurrentScreen({ context: { userId: "user_1" }, screen: "/user" });
325
- return <tailor.AppView app={{ clientPath: "/apps/todo.js", id: "todo" }} />;
326
- }
327
-
328
- render(
329
- <tailor.Root apps={[{ clientPath: "/apps/todo.js", id: "todo" }]}>
330
- <HomeRoute />
331
- <UserRoute />
332
- </tailor.Root>,
333
- );
334
-
335
- await waitFor(() => {
336
- expect(consoleWarn).toHaveBeenCalledWith(expect.stringContaining('"/home", "/user"'));
337
- });
338
- });
339
-
340
- it("passes primitive theme tokens into mounted screens", async () => {
341
- const tailor = createTailorKitClient<typeof server>({
342
- baseUrl: "http://runtime.test",
343
- components,
344
- theme: {
345
- tokens: {
346
- background: {
347
- surface: "var(--card)",
348
- },
349
- },
350
- },
351
- });
352
-
353
- render(
354
- <tailor.Root apps={[{ clientPath: "/apps/todo.js", id: "todo" }]}>
355
- <HomeAppView app={{ clientPath: "/apps/todo.js", id: "todo" }} tailor={tailor} />
356
- </tailor.Root>,
357
- );
358
-
359
- await waitFor(() => {
360
- expect(hostRecords).toHaveLength(1);
361
- });
362
- expect(document.querySelector("[data-tailorkit-theme-style]")?.textContent).toContain(
363
- "--tailorkit-background-surface: var(--card);",
364
- );
365
- });
366
-
367
- it("renders missing component errors inside the app container", async () => {
368
- const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
369
- const tailor = createTailorKitClient<typeof server>({
370
- baseUrl: "http://runtime.test",
371
- });
372
-
373
- render(
374
- <tailor.Root apps={[{ clientPath: "/apps/todo.js", id: "todo" }]}>
375
- <HomeAppView app={{ clientPath: "/apps/todo.js", id: "todo" }} tailor={tailor} />
376
- </tailor.Root>,
377
- );
378
-
379
- await waitFor(() => {
380
- expect(
381
- testingScreen.getByText('TailorKit component "Button" is not registered.'),
382
- ).toBeTruthy();
383
- });
384
- expect(consoleError).toHaveBeenCalled();
385
- });
386
-
387
- it("clears a missing component error when switching apps", async () => {
388
- vi.spyOn(console, "error").mockImplementation(() => {});
389
- const tailor = createTailorKitClient<typeof server>({
390
- baseUrl: "http://runtime.test",
391
- components,
392
- });
393
-
394
- const view = render(
395
- <tailor.Root apps={[{ clientPath: "/apps/missing-component.js", id: "bad" }]}>
396
- <HomeAppView
397
- app={{ clientPath: "/apps/missing-component.js", id: "bad" }}
398
- tailor={tailor}
399
- />
400
- </tailor.Root>,
401
- );
402
-
403
- await waitFor(() => {
404
- expect(
405
- testingScreen.getByText('TailorKit component "MissingComponent" is not registered.'),
406
- ).toBeTruthy();
407
- });
408
-
409
- await act(() => {
410
- view.rerender(
411
- <tailor.Root apps={[{ clientPath: "/apps/email.js", id: "email" }]}>
412
- <HomeAppView app={{ clientPath: "/apps/email.js", id: "email" }} tailor={tailor} />
413
- </tailor.Root>,
414
- );
415
- });
416
-
417
- await waitFor(() => {
418
- expect(
419
- testingScreen.queryByText('TailorKit component "MissingComponent" is not registered.'),
420
- ).toBeNull();
421
- expect(testingScreen.getByRole("button").textContent).toContain("/apps/email.js");
422
- });
423
- });
424
- });
@@ -1,151 +0,0 @@
1
- import { createTailorKitServer } from "@tailorkit/core/server";
2
- import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec";
3
- import type { ReactNode } from "react";
4
- import { components, createTailorKitClient } from "../tailor-kit";
5
-
6
- const typedSchema = <TValue,>(): StandardSchemaV1<unknown, TValue> &
7
- StandardJSONSchemaV1<unknown, TValue> =>
8
- ({
9
- "~standard": {
10
- jsonSchema: {
11
- input: () => ({}),
12
- output: () => ({}),
13
- },
14
- validate: (value: unknown) => ({ value: value as TValue }),
15
- vendor: "test",
16
- version: 1,
17
- },
18
- }) as const satisfies StandardSchemaV1<unknown, TValue> & StandardJSONSchemaV1<unknown, TValue>;
19
-
20
- const server = createTailorKitServer({
21
- components: {
22
- Button: {},
23
- },
24
- screens: {
25
- "/": { context: typedSchema<{ user: { id: string } }>() },
26
- "/home": { context: typedSchema<{ page: { title: string }; user: { id: string } }>() },
27
- "/home/detail": {
28
- context: typedSchema<{
29
- detail: { id: string };
30
- page: { title: string };
31
- user: { id: string };
32
- }>(),
33
- },
34
- "/user": { context: typedSchema<{ user: { id: string }; userId: string }>() },
35
- },
36
- });
37
-
38
- const tailor = createTailorKitClient<typeof server>({ baseUrl: "http://runtime.test" });
39
- const app = { clientPath: "/apps/todo.js", id: "todo" };
40
-
41
- const slotsServer = createTailorKitServer({
42
- components: {
43
- Button: {
44
- slots: ["default"] as const,
45
- },
46
- },
47
- });
48
-
49
- const slotsSchema = slotsServer.$internal.schema;
50
-
51
- createTailorKitClient<typeof slotsServer>({
52
- baseUrl: "http://runtime.test",
53
- components: {
54
- Button: ({ slots }) => {
55
- const slot: ReactNode = slots.default;
56
- return slot;
57
- },
58
- },
59
- });
60
-
61
- const requiredComponentsServer = createTailorKitServer({
62
- components: {
63
- Button: {},
64
- Input: {},
65
- },
66
- });
67
-
68
- createTailorKitClient<typeof requiredComponentsServer>({
69
- baseUrl: "http://runtime.test",
70
- // @ts-expect-error all server components must have client renderers when components are provided
71
- components: {
72
- Button: () => null,
73
- },
74
- });
75
-
76
- components(slotsSchema, {
77
- Button: ({ props, slots }) => {
78
- const typedProps = props satisfies Record<string, never>;
79
- const typedSlot: ReactNode = slots.default;
80
- void typedProps;
81
- void typedSlot;
82
- return null;
83
- },
84
- });
85
-
86
- const callbackServer = createTailorKitServer({
87
- components: {
88
- Button: {
89
- fields: typedSchema<{ variant?: "default" | "secondary" }>(),
90
- callbacks: {
91
- onClick: {},
92
- },
93
- slots: ["default"] as const,
94
- },
95
- },
96
- });
97
-
98
- components(callbackServer.$internal.schema, {
99
- Button: ({ props }) => {
100
- const variant: "default" | "secondary" | undefined = props.variant;
101
- const onClick: (() => void) | undefined = props.onClick;
102
- void variant;
103
- void onClick;
104
- return null;
105
- },
106
- });
107
-
108
- tailor.useCurrentScreen({
109
- screen: "/home",
110
- context: { page: { title: "Home" }, user: { id: "user_1" } },
111
- });
112
-
113
- tailor.useCurrentScreen({ screen: "/user", status: "loading" });
114
-
115
- tailor.useCurrentScreen({ screen: "/user", status: "error" });
116
-
117
- // @ts-expect-error invalid screen name
118
- tailor.useCurrentScreen({ screen: "missing", context: {} });
119
-
120
- // @ts-expect-error invalid context shape for selected screen
121
- tailor.useCurrentScreen({ screen: "/user", context: { page: { title: "Home" } } });
122
-
123
- // @ts-expect-error ready matches require context
124
- tailor.useCurrentScreen({ screen: "/home" });
125
-
126
- // @ts-expect-error loading screens cannot expose partial context
127
- tailor.useCurrentScreen({ screen: "/user", status: "loading", context: { userId: "user_1" } });
128
-
129
- <tailor.AppView app={app} />;
130
-
131
- <tailor.AppView
132
- app={app}
133
- screen="/home"
134
- context={{ page: { title: "Home" }, user: { id: "user_1" } }}
135
- />;
136
-
137
- <tailor.AppView app={app} screen="/user" status="loading" />;
138
-
139
- <tailor.AppView app={app} screen="/user" status="error" />;
140
-
141
- // @ts-expect-error invalid screen name
142
- <tailor.AppView app={app} screen="missing" context={{}} />;
143
-
144
- // @ts-expect-error invalid context shape for selected screen
145
- <tailor.AppView app={app} screen="/user" context={{ page: { title: "Home" } }} />;
146
-
147
- // @ts-expect-error ready app views require context when screen is provided
148
- <tailor.AppView app={app} screen="/home" />;
149
-
150
- // @ts-expect-error loading app views cannot expose context
151
- <tailor.AppView app={app} screen="/user" status="loading" context={{ userId: "user_1" }} />;
@@ -1,21 +0,0 @@
1
- import { createContext, useContext } from "react";
2
- import type { UseAppsResult } from "../hooks/use-apps";
3
- import type { TailorKitApp } from "../tailor-kit";
4
-
5
- export interface TailorRootContextValue {
6
- apps: TailorKitApp[];
7
- }
8
-
9
- export interface RootTailor {
10
- useApps: () => UseAppsResult;
11
- }
12
-
13
- export const TailorRootContext = createContext<TailorRootContextValue | null>(null);
14
-
15
- export function useTailorRootContext(component: string): TailorRootContextValue {
16
- const context = useContext(TailorRootContext);
17
- if (!context) {
18
- throw new Error(`${component} must be rendered inside tailor.Root.`);
19
- }
20
- return context;
21
- }
@@ -1 +0,0 @@
1
- export { Root } from "./root";