@sanity/workbench 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (52) hide show
  1. package/README.md +24 -0
  2. package/dist/_chunks-es/index.js +39 -0
  3. package/dist/_chunks-es/index.js.map +1 -0
  4. package/dist/_chunks-es/module-federation.js +59 -0
  5. package/dist/_chunks-es/module-federation.js.map +1 -0
  6. package/dist/_chunks-es/studio.js +892 -0
  7. package/dist/_chunks-es/studio.js.map +1 -0
  8. package/dist/_internal.d.ts +16 -4
  9. package/dist/_internal.js +34 -27
  10. package/dist/_internal.js.map +1 -1
  11. package/dist/core.d.ts +2250 -0
  12. package/dist/core.js +74 -0
  13. package/dist/core.js.map +1 -0
  14. package/dist/system.d.ts +2135 -0
  15. package/dist/system.js +887 -0
  16. package/dist/system.js.map +1 -0
  17. package/package.json +34 -6
  18. package/src/_exports/core.ts +1 -0
  19. package/src/_exports/system.ts +1 -0
  20. package/src/_internal/index.ts +2 -1
  21. package/src/_internal/render.ts +72 -43
  22. package/src/core/applications/application-list.ts +104 -0
  23. package/src/core/applications/application.ts +177 -0
  24. package/src/core/applications/interface.ts +126 -0
  25. package/src/core/canvases.ts +92 -0
  26. package/src/core/config.ts +34 -0
  27. package/src/core/env.ts +43 -0
  28. package/src/core/index.ts +13 -0
  29. package/src/core/log/index.ts +125 -0
  30. package/src/core/media-libraries.ts +93 -0
  31. package/src/core/organizations.ts +115 -0
  32. package/src/core/projects.ts +114 -0
  33. package/src/core/shared/urls.ts +129 -0
  34. package/src/core/user-applications/core-app.ts +148 -0
  35. package/src/core/user-applications/studios/index.ts +3 -0
  36. package/src/core/user-applications/studios/schemas.ts +128 -0
  37. package/src/core/user-applications/studios/studio.ts +533 -0
  38. package/src/core/user-applications/studios/workspace.ts +156 -0
  39. package/src/core/user-applications/user-application.ts +222 -0
  40. package/src/system/auth.machine.ts +223 -0
  41. package/src/system/index.ts +22 -0
  42. package/src/system/inspect.ts +40 -0
  43. package/src/system/load-federated-module.ts +53 -0
  44. package/src/system/module-federation.ts +116 -0
  45. package/src/system/remote.machine.ts +219 -0
  46. package/src/system/remotes.machine.ts +92 -0
  47. package/src/system/root.machine.ts +224 -0
  48. package/src/system/service.machine.ts +207 -0
  49. package/src/system/services.machine.ts +120 -0
  50. package/src/system/system-preferences.machine.ts +215 -0
  51. package/src/system/telemetry.machine.ts +179 -0
  52. package/src/_internal/render.test.ts +0 -18
@@ -0,0 +1,219 @@
1
+ import { assign, fromPromise, sendParent, setup } from "xstate";
2
+
3
+ import { loadRemoteModule } from "./load-federated-module";
4
+ import type { FederationInstance } from "./module-federation";
5
+
6
+ /**
7
+ * @public
8
+ */
9
+ export interface RemoteRenderOptions {
10
+ basePath?: string;
11
+ scheme?: "light" | "dark";
12
+ /**
13
+ * This is a temporary measure designed to authenticate federated
14
+ * studios & SDK apps before we write a working protocol.
15
+ */
16
+ unstable_temporaryToken?: string | null;
17
+ }
18
+
19
+ /**
20
+ * A loaded render-contract module. `TProps` is what its `render` accepts —
21
+ * `RemoteRenderOptions` for an application's full-page view, the view's props
22
+ * for a dock-panel component.
23
+ * @public
24
+ */
25
+ export interface LoadedRemoteModule<TProps = RemoteRenderOptions> {
26
+ render: (rootElement: HTMLElement, props?: TProps) => () => void;
27
+ }
28
+
29
+ /**
30
+ * A worker service's loader module — the `worker` interface's expose. Carries
31
+ * the worker bundle URL (root-relative to the app origin) for the host to
32
+ * bootstrap, not a render function; the services counterpart of
33
+ * {@link LoadedRemoteModule}.
34
+ * @public
35
+ */
36
+ export interface ServiceLoaderModule {
37
+ url: string;
38
+ type: string;
39
+ name: string;
40
+ version: number;
41
+ }
42
+
43
+ /**
44
+ * The federated-module shape each interface type exposes, keyed by
45
+ * `interface_type`. Every interface — `panel`, `app`, `worker` — loads through
46
+ * the same {@link remoteLogic} lifecycle; this maps what its loaded module looks
47
+ * like so each consumer narrows the machine's shape-agnostic `module` to the
48
+ * right type: a render contract for `panel`/`app` (rendered by an island), a
49
+ * worker loader for `worker` (run as a Web Worker, never rendered).
50
+ * @public
51
+ */
52
+ export interface RemoteModuleByInterfaceType {
53
+ panel: LoadedRemoteModule;
54
+ app: LoadedRemoteModule;
55
+ worker: ServiceLoaderModule;
56
+ }
57
+
58
+ /**
59
+ * @public
60
+ */
61
+ export interface RemoteError {
62
+ message: string;
63
+ cause: unknown;
64
+ }
65
+
66
+ /**
67
+ * Thrown by a render consumer (e.g. the island) when a loaded module doesn't
68
+ * expose a `render` function. Render-specific — `remoteLogic` itself is
69
+ * shape-agnostic, so this lives with the consumer that needs the contract, not
70
+ * the loader. Surfaced via `RemoteError.cause` for consumers that discriminate.
71
+ * @public
72
+ */
73
+ export class ModuleShapeError extends Error {
74
+ constructor(remoteId: string) {
75
+ super(`Remote "${remoteId}" did not expose a render function`);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Thrown by the load actor when a remote's expose resolves empty — the module
81
+ * isn't published, or `loadRemote` returned nothing. Generic across interface
82
+ * types; the shape-specific check (a render function, a worker URL) is the
83
+ * consumer's, since `remoteLogic` loads every interface the same way.
84
+ * @internal
85
+ */
86
+ export class ModuleLoadError extends Error {
87
+ constructor(remoteId: string) {
88
+ super(`Remote module "${remoteId}" failed to load`);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * @internal
94
+ */
95
+ export interface RemoteInput {
96
+ /** Fully-qualified module id — `${appId}/${expose}`. One child per moduleId. */
97
+ moduleId: string;
98
+ entry: string;
99
+ instance: FederationInstance;
100
+ }
101
+
102
+ type RemoteContext = RemoteInput & {
103
+ /** The loaded module, shape-agnostic — each consumer narrows it. @see {@link RemoteModuleByInterfaceType} */
104
+ module: unknown;
105
+ error: RemoteError | null;
106
+ };
107
+
108
+ const loadLogic = fromPromise<unknown, RemoteInput>(async ({ input }) => {
109
+ const remoteModule = await loadRemoteModule<unknown>(input.instance, {
110
+ moduleId: input.moduleId,
111
+ entry: input.entry,
112
+ });
113
+ // The loader is interface-agnostic: it only guarantees a module came back.
114
+ // Render-vs-worker shape validation is the consumer's (a panel/app island
115
+ // checks `render`; a service checks `url`).
116
+ if (remoteModule == null) {
117
+ throw new ModuleLoadError(input.moduleId);
118
+ }
119
+ return remoteModule;
120
+ });
121
+
122
+ /**
123
+ * Lifecycle machine for a single federated interface module — an application's
124
+ * `App` entry, one view component, or a worker service's loader. Interface-
125
+ * agnostic: it loads and tracks any module, leaving the shape contract
126
+ * (render vs. worker URL) to the consumer.
127
+ *
128
+ * Spawned by the {@link remotesLogic} supervisor on demand, one per remote id
129
+ * (`${appId}/${moduleId}`). Each instance owns one module's load lifecycle:
130
+ * `loading` → `loaded | error`. Tags (`loading` / `ready` / `failed`) drive the
131
+ * UI contract — state names are internal.
132
+ *
133
+ * The machine sends a `remote.settled` event to its parent on entry to
134
+ * either terminal state, reserved for future supervision/retry.
135
+ *
136
+ * @internal
137
+ */
138
+ export const remoteLogic = setup({
139
+ types: {
140
+ input: {} as RemoteInput,
141
+ context: {} as RemoteContext,
142
+ tags: {} as "loading" | "ready" | "failed",
143
+ },
144
+ actors: {
145
+ load: loadLogic,
146
+ },
147
+ actions: {
148
+ setModule: assign({
149
+ module: (_, params: { module: unknown }) => params.module,
150
+ error: () => null,
151
+ }),
152
+ setError: assign({
153
+ module: () => null,
154
+ error: (_, params: { error: unknown }) => normaliseError(params.error),
155
+ }),
156
+ },
157
+ }).createMachine({
158
+ id: "remote",
159
+ initial: "loading",
160
+ context: ({ input }) => ({
161
+ ...input,
162
+ module: null,
163
+ error: null,
164
+ }),
165
+ states: {
166
+ loading: {
167
+ tags: ["loading"],
168
+ invoke: {
169
+ src: "load",
170
+ input: ({ context }) => ({
171
+ moduleId: context.moduleId,
172
+ entry: context.entry,
173
+ instance: context.instance,
174
+ }),
175
+ onDone: {
176
+ target: "loaded",
177
+ actions: [
178
+ {
179
+ type: "setModule",
180
+ params: ({ event }) => ({ module: event.output }),
181
+ },
182
+ ],
183
+ },
184
+ onError: {
185
+ target: "error",
186
+ actions: [
187
+ {
188
+ type: "setError",
189
+ params: ({ event }) => ({ error: event.error }),
190
+ },
191
+ ],
192
+ },
193
+ },
194
+ },
195
+ loaded: {
196
+ tags: ["ready"],
197
+ entry: sendParent(({ context }) => ({
198
+ type: "remote.settled" as const,
199
+ moduleId: context.moduleId,
200
+ status: "loaded" as const,
201
+ })),
202
+ },
203
+ error: {
204
+ tags: ["failed"],
205
+ entry: sendParent(({ context }) => ({
206
+ type: "remote.settled" as const,
207
+ moduleId: context.moduleId,
208
+ status: "error" as const,
209
+ })),
210
+ },
211
+ },
212
+ });
213
+
214
+ function normaliseError(error: unknown): RemoteError {
215
+ if (error instanceof Error) {
216
+ return { message: error.message, cause: error };
217
+ }
218
+ return { message: String(error), cause: error };
219
+ }
@@ -0,0 +1,92 @@
1
+ import { type ActorRefFrom, assign, setup } from "xstate";
2
+
3
+ import { type FederationInstance } from "./module-federation";
4
+ import { remoteLogic } from "./remote.machine";
5
+
6
+ /** The shared federation instance, supplied by the root machine. */
7
+ type RemotesInput = {
8
+ instance: FederationInstance;
9
+ };
10
+
11
+ type RemotesContext = {
12
+ instance: FederationInstance;
13
+ children: Map<string, ActorRefFrom<typeof remoteLogic>>;
14
+ };
15
+
16
+ type RemotesEvent =
17
+ | { type: "remote.load.request"; moduleId: string; entry: string }
18
+ | { type: "remote.settled"; moduleId: string; status: "loaded" | "error" };
19
+
20
+ /**
21
+ * Supervisor machine for federated modules.
22
+ *
23
+ * Uses the shared {@link FederationInstance} (the `workbench-applications`
24
+ * instance, supplied by the root machine and shared with `services`) and spawns
25
+ * one {@link remoteLogic} child per remote id (`${appId}/${moduleId}`) — so an
26
+ * application's `App` entry and each of its dock-panel view components load (and
27
+ * are tracked) independently. The
28
+ * supervisor is invoked at the root of the OS machine for inspector visibility
29
+ * (`[sanity-workbench:os:remotes]`) — boot does not gate on it.
30
+ *
31
+ * @internal
32
+ */
33
+ export const remotesLogic = setup({
34
+ types: {
35
+ input: {} as RemotesInput,
36
+ context: {} as RemotesContext,
37
+ events: {} as RemotesEvent,
38
+ },
39
+ actors: {
40
+ remote: remoteLogic,
41
+ },
42
+ guards: {
43
+ remoteNotKnown: ({ context }, params: { moduleId: string }) =>
44
+ !context.children.has(params.moduleId),
45
+ },
46
+ actions: {
47
+ spawnRemote: assign({
48
+ children: (
49
+ { context, spawn },
50
+ params: { moduleId: string; entry: string },
51
+ ) => {
52
+ const ref = spawn("remote", {
53
+ id: params.moduleId,
54
+ systemId: `remotes.${params.moduleId}`,
55
+ input: {
56
+ moduleId: params.moduleId,
57
+ entry: params.entry,
58
+ instance: context.instance,
59
+ },
60
+ });
61
+ return new Map(context.children).set(params.moduleId, ref);
62
+ },
63
+ }),
64
+ },
65
+ }).createMachine({
66
+ id: "remotes",
67
+ context: ({ input }) => ({
68
+ instance: input.instance,
69
+ children: new Map(),
70
+ }),
71
+ on: {
72
+ "remote.load.request": {
73
+ guard: {
74
+ type: "remoteNotKnown",
75
+ params: ({ event }) => ({ moduleId: event.moduleId }),
76
+ },
77
+ actions: [
78
+ {
79
+ type: "spawnRemote",
80
+ params: ({ event }) => ({
81
+ moduleId: event.moduleId,
82
+ entry: event.entry,
83
+ }),
84
+ },
85
+ ],
86
+ },
87
+ // Reserved for future supervision/retry. Forwarded by per-remote
88
+ // children on entry to `loaded` or `error`; currently a no-op so the
89
+ // event is part of the typed surface rather than a silent unknown.
90
+ "remote.settled": {},
91
+ },
92
+ });
@@ -0,0 +1,224 @@
1
+ import { createSanityInstance, type SanityInstance } from "@sanity/sdk";
2
+ import { raise, sendTo, setup, type ActorOptions } from "xstate";
3
+
4
+ import { logger } from "../core/log";
5
+ import { authLogic } from "./auth.machine";
6
+ import { inspect } from "./inspect";
7
+ import {
8
+ createInstance,
9
+ type FederationInstance,
10
+ log,
11
+ } from "./module-federation";
12
+ import { remotesLogic } from "./remotes.machine";
13
+ import { servicesLogic } from "./services.machine";
14
+ import {
15
+ type SystemPreferencesAdapter,
16
+ systemPreferencesLogic,
17
+ } from "./system-preferences.machine";
18
+ import {
19
+ telemetryLogic,
20
+ type WorkbenchUserProperties,
21
+ } from "./telemetry.machine";
22
+
23
+ type OSInput = WorkbenchUserProperties & {
24
+ systemPreferencesAdapter?: SystemPreferencesAdapter;
25
+ };
26
+
27
+ type OSContext = {
28
+ instance: SanityInstance;
29
+ /**
30
+ * The single module-federation instance every interface loads through —
31
+ * shared by the `remotes` (views) and `services` (workers) supervisors so an
32
+ * app's remote is registered once for all its interfaces.
33
+ */
34
+ federationInstance: FederationInstance;
35
+ userProperties: WorkbenchUserProperties;
36
+ systemPreferencesAdapter: SystemPreferencesAdapter | undefined;
37
+ };
38
+
39
+ /**
40
+ * The base inputs for the OS machine.
41
+ * @internal
42
+ */
43
+ export interface OSBaseInput extends Pick<OSContext, "instance"> {}
44
+
45
+ /**
46
+ * The sanity OS machine, responsible for managing the global state of the OS.
47
+ * @public
48
+ * @example
49
+ * ```ts
50
+ * import { os, createOSOptions } from "@sanity/workbench/system";
51
+ * import { useActor } from "@xstate/react";
52
+ *
53
+ * const [state, send] = useActor(os, createOSOptions({
54
+ * version: "1.0.0",
55
+ * organizationId: "...",
56
+ * environment: "production",
57
+ * userAgent: navigator.userAgent,
58
+ * }));
59
+ * ```
60
+ */
61
+ export const os = setup({
62
+ types: {
63
+ input: {} as OSInput,
64
+ context: {} as OSContext,
65
+ events: {} as
66
+ | { type: "boot.auth.ready" }
67
+ | { type: "boot.auth.failed" }
68
+ | { type: "boot.telemetry.ready" },
69
+ // https://github.com/statelyai/xstate/issues/5515
70
+ children: {} as {
71
+ auth: "auth";
72
+ telemetry: "telemetry";
73
+ "system-preferences": "systemPreferences";
74
+ remotes: "remotes";
75
+ services: "services";
76
+ },
77
+ },
78
+ actors: {
79
+ auth: authLogic,
80
+ telemetry: telemetryLogic,
81
+ systemPreferences: systemPreferencesLogic,
82
+ remotes: remotesLogic,
83
+ services: servicesLogic,
84
+ },
85
+ guards: {
86
+ hasTag: (_, params: { hasTag: boolean }) => params.hasTag,
87
+ },
88
+ actions: {
89
+ raiseAuthReady: raise({ type: "boot.auth.ready" }),
90
+ raiseAuthFailed: raise({ type: "boot.auth.failed" }),
91
+ raiseTelemetryReady: raise({
92
+ type: "boot.telemetry.ready",
93
+ }),
94
+ startTelemetry: sendTo("telemetry", {
95
+ type: "telemetry.start",
96
+ }),
97
+ },
98
+ }).createMachine({
99
+ id: "os",
100
+ context: ({ input }) => ({
101
+ instance: createSanityInstance(),
102
+ federationInstance: createInstance({
103
+ name: "workbench-applications",
104
+ plugins: [log(logger.debug)],
105
+ }),
106
+ userProperties: {
107
+ version: input.version,
108
+ organizationId: input.organizationId,
109
+ environment: input.environment,
110
+ userAgent: input.userAgent,
111
+ },
112
+ systemPreferencesAdapter: input.systemPreferencesAdapter,
113
+ }),
114
+ initial: "booting",
115
+ invoke: [
116
+ {
117
+ id: "auth",
118
+ systemId: "auth",
119
+ src: "auth",
120
+ input: ({ context }) => ({ instance: context.instance }),
121
+ onSnapshot: [
122
+ {
123
+ guard: {
124
+ type: "hasTag",
125
+ params: ({ event }) => ({
126
+ hasTag: event.snapshot.hasTag("authenticated"),
127
+ }),
128
+ },
129
+ actions: [{ type: "raiseAuthReady" }],
130
+ },
131
+ {
132
+ guard: {
133
+ type: "hasTag",
134
+ params: ({ event }) => ({
135
+ hasTag: event.snapshot.hasTag("error"),
136
+ }),
137
+ },
138
+ actions: [{ type: "raiseAuthFailed" }],
139
+ },
140
+ ],
141
+ },
142
+ {
143
+ id: "telemetry",
144
+ systemId: "telemetry",
145
+ src: "telemetry",
146
+ input: ({ context }) => ({
147
+ instance: context.instance,
148
+ ...context.userProperties,
149
+ }),
150
+ onSnapshot: [
151
+ {
152
+ guard: {
153
+ type: "hasTag",
154
+ params: ({ event }) => ({
155
+ hasTag: event.snapshot.hasTag("telemetry-resolved"),
156
+ }),
157
+ },
158
+ actions: [{ type: "raiseTelemetryReady" }],
159
+ },
160
+ ],
161
+ },
162
+ {
163
+ id: "system-preferences",
164
+ systemId: "system-preferences",
165
+ src: "systemPreferences",
166
+ input: ({ context }) => ({ adapter: context.systemPreferencesAdapter }),
167
+ },
168
+ {
169
+ id: "remotes",
170
+ systemId: "remotes",
171
+ src: "remotes",
172
+ input: ({ context }) => ({ instance: context.federationInstance }),
173
+ },
174
+ {
175
+ id: "services",
176
+ systemId: "services",
177
+ src: "services",
178
+ input: ({ context }) => ({ instance: context.federationInstance }),
179
+ },
180
+ ],
181
+ states: {
182
+ booting: {
183
+ initial: "auth",
184
+ states: {
185
+ auth: {
186
+ on: {
187
+ "boot.auth.ready": {
188
+ target: "telemetry",
189
+ actions: [{ type: "startTelemetry" }],
190
+ },
191
+ "boot.auth.failed": { target: "error" },
192
+ },
193
+ },
194
+ telemetry: {
195
+ on: {
196
+ "boot.telemetry.ready": { target: "done" },
197
+ },
198
+ },
199
+ error: {},
200
+ done: { type: "final" },
201
+ },
202
+ onDone: { target: "running" },
203
+ },
204
+ running: {
205
+ type: "parallel",
206
+ },
207
+ },
208
+ });
209
+
210
+ /**
211
+ * Creates a set of default options for the OS machine. Forwards the workbench
212
+ * user properties to the machine's input and wires the structured-logging
213
+ * `inspect` callback. Browser-specific concerns (color-scheme seeding,
214
+ * persistence) live in the {@link SystemPreferencesAdapter} the host passes
215
+ * via `systemPreferencesAdapter`.
216
+ * @public
217
+ */
218
+ export function createOSOptions(input: OSInput) {
219
+ return {
220
+ id: "os",
221
+ input,
222
+ inspect,
223
+ } satisfies ActorOptions<typeof os>;
224
+ }