@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.
- package/README.md +24 -0
- package/dist/_chunks-es/index.js +39 -0
- package/dist/_chunks-es/index.js.map +1 -0
- package/dist/_chunks-es/module-federation.js +59 -0
- package/dist/_chunks-es/module-federation.js.map +1 -0
- package/dist/_chunks-es/studio.js +892 -0
- package/dist/_chunks-es/studio.js.map +1 -0
- package/dist/_internal.d.ts +16 -4
- package/dist/_internal.js +34 -27
- package/dist/_internal.js.map +1 -1
- package/dist/core.d.ts +2250 -0
- package/dist/core.js +74 -0
- package/dist/core.js.map +1 -0
- package/dist/system.d.ts +2135 -0
- package/dist/system.js +887 -0
- package/dist/system.js.map +1 -0
- package/package.json +34 -6
- package/src/_exports/core.ts +1 -0
- package/src/_exports/system.ts +1 -0
- package/src/_internal/index.ts +2 -1
- package/src/_internal/render.ts +72 -43
- package/src/core/applications/application-list.ts +104 -0
- package/src/core/applications/application.ts +177 -0
- package/src/core/applications/interface.ts +126 -0
- package/src/core/canvases.ts +92 -0
- package/src/core/config.ts +34 -0
- package/src/core/env.ts +43 -0
- package/src/core/index.ts +13 -0
- package/src/core/log/index.ts +125 -0
- package/src/core/media-libraries.ts +93 -0
- package/src/core/organizations.ts +115 -0
- package/src/core/projects.ts +114 -0
- package/src/core/shared/urls.ts +129 -0
- package/src/core/user-applications/core-app.ts +148 -0
- package/src/core/user-applications/studios/index.ts +3 -0
- package/src/core/user-applications/studios/schemas.ts +128 -0
- package/src/core/user-applications/studios/studio.ts +533 -0
- package/src/core/user-applications/studios/workspace.ts +156 -0
- package/src/core/user-applications/user-application.ts +222 -0
- package/src/system/auth.machine.ts +223 -0
- package/src/system/index.ts +22 -0
- package/src/system/inspect.ts +40 -0
- package/src/system/load-federated-module.ts +53 -0
- package/src/system/module-federation.ts +116 -0
- package/src/system/remote.machine.ts +219 -0
- package/src/system/remotes.machine.ts +92 -0
- package/src/system/root.machine.ts +224 -0
- package/src/system/service.machine.ts +207 -0
- package/src/system/services.machine.ts +120 -0
- package/src/system/system-preferences.machine.ts +215 -0
- package/src/system/telemetry.machine.ts +179 -0
- package/src/_internal/render.test.ts +0 -18
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// eslint-disable-next-line no-restricted-imports
|
|
2
|
+
import type { Resource as ProtocolResource } from "@sanity/message-protocol";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AbstractApplication,
|
|
7
|
+
type AbstractApplicationType,
|
|
8
|
+
type LocalInterface,
|
|
9
|
+
LocalInterfaceSchema,
|
|
10
|
+
} from "../applications/application";
|
|
11
|
+
import { getSanityDomain, getSanityEnv } from "../env";
|
|
12
|
+
import type { CoreAppUserApplicationManifest } from "./core-app";
|
|
13
|
+
import type { ClientManifest, ServerManifest } from "./studios/schemas";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* User application ID schema, branded for type safety.
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export const UserApplicationId = z
|
|
20
|
+
.string()
|
|
21
|
+
.nonempty()
|
|
22
|
+
.brand("UserApplicationId");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* User application ID type, branded for type safety.
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
export type UserApplicationId = z.output<typeof UserApplicationId>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validates and brands a string as a UserApplicationId.
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
export function brandUserApplicationId(id: string): UserApplicationId {
|
|
35
|
+
return UserApplicationId.parse(id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const LocalUserApplicationBase = z.object({
|
|
39
|
+
host: z.string(),
|
|
40
|
+
port: z.number(),
|
|
41
|
+
/**
|
|
42
|
+
* Interfaces the app exposes — dock panels (`interface_type: "panel"`) and
|
|
43
|
+
* background workers (`interface_type: "worker"`), forwarded from the dev
|
|
44
|
+
* server in one list.
|
|
45
|
+
*/
|
|
46
|
+
interfaces: z.array(LocalInterfaceSchema).optional(),
|
|
47
|
+
/** The `deployment.appId` from the application's `sanity.cli.ts`, when set. */
|
|
48
|
+
id: z.string().optional(),
|
|
49
|
+
/**
|
|
50
|
+
* The `api.projectId` from the application's `sanity.cli.ts`. Available
|
|
51
|
+
* synchronously at dev-server startup (no manifest extraction required), so
|
|
52
|
+
* the studio's primary project is resolvable from the very first local
|
|
53
|
+
* application event.
|
|
54
|
+
*/
|
|
55
|
+
projectId: z.string().optional(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Raw data for a local application discovered by the CLI dev server.
|
|
60
|
+
*
|
|
61
|
+
* The CLI forwards the full studio or app manifest so the workbench can
|
|
62
|
+
* derive icons, titles, workspaces and schema references the same way it
|
|
63
|
+
* does for deployed applications. The manifest shape is discriminated on
|
|
64
|
+
* `type`: studios receive a `ClientManifest`, core apps a `CoreAppUserApplicationManifest`.
|
|
65
|
+
*
|
|
66
|
+
* When set on a `UserApplication`, `isLocal` is `true` and `url`/`isFederated`
|
|
67
|
+
* honour the local dev-server instead of the deployed application's host.
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
export const LocalUserApplication = z.discriminatedUnion("type", [
|
|
72
|
+
LocalUserApplicationBase.extend({
|
|
73
|
+
type: z.literal("studio"),
|
|
74
|
+
manifest: z.custom<ClientManifest>().optional(),
|
|
75
|
+
}),
|
|
76
|
+
LocalUserApplicationBase.extend({
|
|
77
|
+
type: z.literal("coreApp"),
|
|
78
|
+
manifest: z.custom<CoreAppUserApplicationManifest>().optional(),
|
|
79
|
+
}),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
export type LocalUserApplication = z.output<typeof LocalUserApplication>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
export const UserApplicationBase = z.object({
|
|
91
|
+
id: UserApplicationId,
|
|
92
|
+
appHost: z.string(),
|
|
93
|
+
urlType: z.enum(["internal", "external"]),
|
|
94
|
+
createdAt: z.string(),
|
|
95
|
+
updatedAt: z.string(),
|
|
96
|
+
dashboardStatus: z.enum(["default", "disabled"]),
|
|
97
|
+
/**
|
|
98
|
+
* Interfaces the app exposes (dock panels and background workers). Not part
|
|
99
|
+
* of the manifest.
|
|
100
|
+
*/
|
|
101
|
+
interfaces: z.array(LocalInterfaceSchema).optional(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
export type UserApplicationBase = z.output<typeof UserApplicationBase>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
112
|
+
export const ActiveDeployment = z.object({
|
|
113
|
+
id: z.string(),
|
|
114
|
+
version: z.string(),
|
|
115
|
+
isActiveDeployment: z.boolean(),
|
|
116
|
+
userApplicationId: z.string(),
|
|
117
|
+
isAutoUpdating: z.boolean().nullable(),
|
|
118
|
+
size: z.number().nullable(),
|
|
119
|
+
deployedAt: z.string(),
|
|
120
|
+
deployedBy: z.string(),
|
|
121
|
+
createdAt: z.string(),
|
|
122
|
+
updatedAt: z.string(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @public
|
|
127
|
+
*/
|
|
128
|
+
export abstract class UserApplication<
|
|
129
|
+
TUserApplication extends UserApplicationBase,
|
|
130
|
+
TType extends Extract<AbstractApplicationType, "coreApp" | "studio">,
|
|
131
|
+
TProtocolResource extends ProtocolResource = Extract<
|
|
132
|
+
ProtocolResource,
|
|
133
|
+
{ type: TType }
|
|
134
|
+
>,
|
|
135
|
+
> extends AbstractApplication<TType, TProtocolResource> {
|
|
136
|
+
readonly application: TUserApplication;
|
|
137
|
+
|
|
138
|
+
readonly id: UserApplicationId;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* For local applications (`isLocal === true`), the deployed application
|
|
142
|
+
* that shares the same `id` — if one was passed at construction. `null`
|
|
143
|
+
* for deployed applications or when no remote twin was provided.
|
|
144
|
+
*/
|
|
145
|
+
readonly remoteApplication: this | null;
|
|
146
|
+
|
|
147
|
+
readonly #isLocal: boolean;
|
|
148
|
+
|
|
149
|
+
constructor(
|
|
150
|
+
application: TUserApplication,
|
|
151
|
+
type: TType,
|
|
152
|
+
options: {
|
|
153
|
+
isLocal?: boolean;
|
|
154
|
+
remoteApplication?: UserApplication<
|
|
155
|
+
TUserApplication,
|
|
156
|
+
TType,
|
|
157
|
+
TProtocolResource
|
|
158
|
+
> | null;
|
|
159
|
+
} = {},
|
|
160
|
+
) {
|
|
161
|
+
super(type);
|
|
162
|
+
this.application = application;
|
|
163
|
+
this.id = brandUserApplicationId(application.id);
|
|
164
|
+
this.#isLocal = options.isLocal ?? false;
|
|
165
|
+
this.remoteApplication =
|
|
166
|
+
(options.remoteApplication as this | undefined) ?? null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
abstract get subtitle(): string | undefined;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Local dev servers are rendered as federated remotes; deployed applications
|
|
173
|
+
* are rendered in an iframe.
|
|
174
|
+
*/
|
|
175
|
+
get isFederated(): boolean {
|
|
176
|
+
return this.isLocal;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
override get isLocal(): boolean {
|
|
180
|
+
return this.#isLocal;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
override get interfaces(): readonly LocalInterface[] {
|
|
184
|
+
return this.application.interfaces ?? [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @returns A fully resolved URL instance for the application.
|
|
189
|
+
* For internal applications, constructs a URL using the Sanity studio domain
|
|
190
|
+
* pattern resolved from the environment at the consuming app's build time.
|
|
191
|
+
* For external applications (including local dev servers), returns the
|
|
192
|
+
* provided app host as a URL.
|
|
193
|
+
*/
|
|
194
|
+
get url(): URL {
|
|
195
|
+
if (this.application.urlType === "internal") {
|
|
196
|
+
if (getSanityEnv() === "production") {
|
|
197
|
+
return new URL(`https://${this.application.appHost}.sanity.studio`);
|
|
198
|
+
}
|
|
199
|
+
return new URL(
|
|
200
|
+
`https://${this.application.appHost}.studio.${getSanityDomain()}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return new URL(this.application.appHost);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Unified manifest accessor for user applications. Resolves the most
|
|
209
|
+
* authoritative manifest available for the application, preferring the
|
|
210
|
+
* deployment manifest (`activeDeployment.manifest`) before falling back to
|
|
211
|
+
* any client-side manifest. The returned shape depends on the application
|
|
212
|
+
* type and source: studios may yield either a `ServerManifest` (deployment)
|
|
213
|
+
* or a `ClientManifest` (fallback); core apps yield a
|
|
214
|
+
* `CoreAppUserApplicationManifest`. Returns `null` when no manifest is
|
|
215
|
+
* available.
|
|
216
|
+
*/
|
|
217
|
+
abstract get manifest():
|
|
218
|
+
| ClientManifest
|
|
219
|
+
| ServerManifest
|
|
220
|
+
| CoreAppUserApplicationManifest
|
|
221
|
+
| null;
|
|
222
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AuthState,
|
|
3
|
+
AuthStateType,
|
|
4
|
+
type CurrentUser,
|
|
5
|
+
getAuthState,
|
|
6
|
+
type LoggedInAuthState,
|
|
7
|
+
logout,
|
|
8
|
+
type SanityInstance,
|
|
9
|
+
} from "@sanity/sdk";
|
|
10
|
+
import { assign, fromObservable, fromPromise, setup } from "xstate";
|
|
11
|
+
|
|
12
|
+
import type { OSBaseInput } from "./root.machine";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export interface AuthInput extends OSBaseInput {}
|
|
18
|
+
|
|
19
|
+
const authStateLogic = fromObservable<AuthState, AuthInput>(
|
|
20
|
+
({ input }) => getAuthState(input.instance).observable,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export interface LogoutInput extends OSBaseInput {}
|
|
27
|
+
|
|
28
|
+
const logoutActorLogic = fromPromise<void, LogoutInput>(async ({ input }) => {
|
|
29
|
+
await logout(input.instance);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
type AuthContext = {
|
|
33
|
+
instance: SanityInstance;
|
|
34
|
+
token: string | null;
|
|
35
|
+
currentUser: CurrentUser | null;
|
|
36
|
+
error: unknown;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type AuthEvent = { type: "auth.logout" };
|
|
40
|
+
|
|
41
|
+
export const authLogic = setup({
|
|
42
|
+
types: {
|
|
43
|
+
input: {} as AuthInput,
|
|
44
|
+
context: {} as AuthContext,
|
|
45
|
+
events: {} as AuthEvent,
|
|
46
|
+
tags: {} as "authenticating" | "authenticated" | "error",
|
|
47
|
+
},
|
|
48
|
+
actors: {
|
|
49
|
+
authState: authStateLogic,
|
|
50
|
+
logoutActor: logoutActorLogic,
|
|
51
|
+
},
|
|
52
|
+
delays: {
|
|
53
|
+
authTimeout: 30_000,
|
|
54
|
+
},
|
|
55
|
+
guards: {
|
|
56
|
+
isLoggedInComplete: (_, params: { state: AuthState | undefined }) =>
|
|
57
|
+
params.state?.type === AuthStateType.LOGGED_IN &&
|
|
58
|
+
Boolean((params.state as LoggedInAuthState).token) &&
|
|
59
|
+
(params.state as LoggedInAuthState).currentUser !== null,
|
|
60
|
+
isAuthState: (
|
|
61
|
+
_,
|
|
62
|
+
params: { state: AuthState | undefined; type: AuthStateType },
|
|
63
|
+
) => params.state?.type === params.type,
|
|
64
|
+
},
|
|
65
|
+
actions: {
|
|
66
|
+
setLoggedIn: assign({
|
|
67
|
+
token: (_, params: { token: string; currentUser: CurrentUser }) =>
|
|
68
|
+
params.token,
|
|
69
|
+
currentUser: (_, params: { token: string; currentUser: CurrentUser }) =>
|
|
70
|
+
params.currentUser,
|
|
71
|
+
error: () => null,
|
|
72
|
+
}),
|
|
73
|
+
clearAuth: assign({
|
|
74
|
+
token: () => null,
|
|
75
|
+
currentUser: () => null,
|
|
76
|
+
error: () => null,
|
|
77
|
+
}),
|
|
78
|
+
setError: assign({
|
|
79
|
+
token: () => null,
|
|
80
|
+
currentUser: () => null,
|
|
81
|
+
error: (_, params: { error: unknown }) => params.error,
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
}).createMachine({
|
|
85
|
+
id: "auth",
|
|
86
|
+
initial: "init",
|
|
87
|
+
context: ({ input }) => ({
|
|
88
|
+
instance: input.instance,
|
|
89
|
+
token: null,
|
|
90
|
+
currentUser: null,
|
|
91
|
+
error: null,
|
|
92
|
+
}),
|
|
93
|
+
invoke: {
|
|
94
|
+
src: "authState",
|
|
95
|
+
input: ({ context }) => ({ instance: context.instance }),
|
|
96
|
+
onSnapshot: [
|
|
97
|
+
{
|
|
98
|
+
guard: {
|
|
99
|
+
type: "isLoggedInComplete",
|
|
100
|
+
params: ({ event }) => ({
|
|
101
|
+
state: event.snapshot.context,
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
actions: [
|
|
105
|
+
{
|
|
106
|
+
type: "setLoggedIn",
|
|
107
|
+
params: ({ event }) => {
|
|
108
|
+
const state = event.snapshot.context as LoggedInAuthState;
|
|
109
|
+
return {
|
|
110
|
+
token: state.token,
|
|
111
|
+
currentUser: state.currentUser!,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
target: `.${AuthStateType.LOGGED_IN}`,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
guard: {
|
|
120
|
+
type: "isAuthState",
|
|
121
|
+
params: ({ event }) => ({
|
|
122
|
+
state: event.snapshot.context,
|
|
123
|
+
type: AuthStateType.LOGGING_IN,
|
|
124
|
+
}),
|
|
125
|
+
},
|
|
126
|
+
actions: [{ type: "clearAuth" }],
|
|
127
|
+
target: `.${AuthStateType.LOGGING_IN}`,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
guard: {
|
|
131
|
+
type: "isAuthState",
|
|
132
|
+
params: ({ event }) => ({
|
|
133
|
+
state: event.snapshot.context,
|
|
134
|
+
type: AuthStateType.ERROR,
|
|
135
|
+
}),
|
|
136
|
+
},
|
|
137
|
+
actions: [
|
|
138
|
+
{
|
|
139
|
+
type: "setError",
|
|
140
|
+
params: ({ event }) => ({
|
|
141
|
+
error:
|
|
142
|
+
event.snapshot.context?.type === AuthStateType.ERROR
|
|
143
|
+
? event.snapshot.context.error
|
|
144
|
+
: null,
|
|
145
|
+
}),
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
target: `.${AuthStateType.ERROR}`,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
guard: {
|
|
152
|
+
type: "isAuthState",
|
|
153
|
+
params: ({ event }) => ({
|
|
154
|
+
state: event.snapshot.context,
|
|
155
|
+
type: AuthStateType.LOGGED_OUT,
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
actions: [{ type: "clearAuth" }],
|
|
159
|
+
target: `.${AuthStateType.LOGGED_OUT}`,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
states: {
|
|
164
|
+
init: {
|
|
165
|
+
tags: ["authenticating"],
|
|
166
|
+
after: {
|
|
167
|
+
authTimeout: {
|
|
168
|
+
actions: [
|
|
169
|
+
{
|
|
170
|
+
type: "setError",
|
|
171
|
+
params: () => ({
|
|
172
|
+
error: new Error("Authentication timed out"),
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
target: AuthStateType.ERROR,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
[AuthStateType.LOGGING_IN]: {
|
|
181
|
+
tags: ["authenticating"],
|
|
182
|
+
after: {
|
|
183
|
+
authTimeout: {
|
|
184
|
+
actions: [
|
|
185
|
+
{
|
|
186
|
+
type: "setError",
|
|
187
|
+
params: () => ({
|
|
188
|
+
error: new Error("Authentication timed out"),
|
|
189
|
+
}),
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
target: AuthStateType.ERROR,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
[AuthStateType.LOGGED_IN]: {
|
|
197
|
+
tags: ["authenticated"],
|
|
198
|
+
on: {
|
|
199
|
+
"auth.logout": { target: "logging-out" },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
["logging-out"]: {
|
|
203
|
+
invoke: {
|
|
204
|
+
src: "logoutActor",
|
|
205
|
+
input: ({ context }) => ({ instance: context.instance }),
|
|
206
|
+
onDone: {
|
|
207
|
+
target: AuthStateType.LOGGED_OUT,
|
|
208
|
+
},
|
|
209
|
+
onError: {
|
|
210
|
+
actions: [
|
|
211
|
+
{
|
|
212
|
+
type: "setError",
|
|
213
|
+
params: ({ event }) => ({ error: event.error }),
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
target: AuthStateType.ERROR,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
[AuthStateType.LOGGED_OUT]: {},
|
|
221
|
+
[AuthStateType.ERROR]: { tags: ["error"] },
|
|
222
|
+
},
|
|
223
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type { AuthInput, LogoutInput } from "./auth.machine";
|
|
2
|
+
export { ModuleShapeError } from "./remote.machine";
|
|
3
|
+
export type {
|
|
4
|
+
LoadedRemoteModule,
|
|
5
|
+
RemoteError,
|
|
6
|
+
RemoteInput,
|
|
7
|
+
RemoteModuleByInterfaceType,
|
|
8
|
+
RemoteRenderOptions,
|
|
9
|
+
ServiceLoaderModule,
|
|
10
|
+
} from "./remote.machine";
|
|
11
|
+
export type {
|
|
12
|
+
SystemPreferencesAdapter,
|
|
13
|
+
SystemPreferencesContext,
|
|
14
|
+
SystemPreferencesEvent,
|
|
15
|
+
} from "./system-preferences.machine";
|
|
16
|
+
export { loadFederatedModule } from "./load-federated-module";
|
|
17
|
+
export type { FederatedModuleRef } from "./load-federated-module";
|
|
18
|
+
export { os, createOSOptions } from "./root.machine";
|
|
19
|
+
export type {
|
|
20
|
+
TelemetryInput,
|
|
21
|
+
WorkbenchUserProperties,
|
|
22
|
+
} from "./telemetry.machine";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ActorRefLike, type InspectionEvent } from "xstate";
|
|
2
|
+
|
|
3
|
+
import { logger } from "../core";
|
|
4
|
+
|
|
5
|
+
// Mirrors @statelyai/inspect's ActorRefLikeWithData — the inspect
|
|
6
|
+
// callback receives full Actor instances at runtime, but the type is
|
|
7
|
+
// narrowed for @xstate/store compat.
|
|
8
|
+
type ActorRefWithAncestry = ActorRefLike & {
|
|
9
|
+
id?: string;
|
|
10
|
+
_parent?: ActorRefWithAncestry;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const actorPath = (ref: ActorRefWithAncestry): string => {
|
|
14
|
+
const segments: string[] = [];
|
|
15
|
+
let current: ActorRefWithAncestry | undefined = ref;
|
|
16
|
+
while (current) {
|
|
17
|
+
segments.unshift(current.id ?? current.sessionId);
|
|
18
|
+
current = current._parent;
|
|
19
|
+
}
|
|
20
|
+
return segments.join(":");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** @internal exported for testing */
|
|
24
|
+
export const inspect = (event: InspectionEvent): void => {
|
|
25
|
+
const ref = event.actorRef as ActorRefWithAncestry;
|
|
26
|
+
const log = logger.child(actorPath(ref));
|
|
27
|
+
|
|
28
|
+
switch (event.type) {
|
|
29
|
+
case "@xstate.snapshot": {
|
|
30
|
+
log.debug("snapshot", event.snapshot);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "@xstate.event":
|
|
34
|
+
log.debug("event", event.event);
|
|
35
|
+
break;
|
|
36
|
+
case "@xstate.action":
|
|
37
|
+
log.debug("action", event.action);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { logger } from "../core/log";
|
|
2
|
+
import type { FederationInstance } from "./module-federation";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A module exposed by an application's federation remote.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export interface FederatedModuleRef {
|
|
10
|
+
/**
|
|
11
|
+
* Fully-qualified module id — `${appId}/${expose}`, e.g.
|
|
12
|
+
* `studio-1/views/feed/panel` (a view component) or `studio-1/App` (the
|
|
13
|
+
* full-page view). The remote's name is its first segment.
|
|
14
|
+
*/
|
|
15
|
+
moduleId: string;
|
|
16
|
+
/** The remote's `mf-manifest.json` URL (served from the app's origin). */
|
|
17
|
+
entry: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register an app's federation remote (idempotent) and load one of its exposes
|
|
22
|
+
* by its fully-qualified `moduleId`. Failures propagate, so the caller drives
|
|
23
|
+
* its own error handling (e.g. a machine's error state with the original
|
|
24
|
+
* cause). For the render-empty-on-failure policy, use {@link loadFederatedModule}.
|
|
25
|
+
*/
|
|
26
|
+
export async function loadRemoteModule<T>(
|
|
27
|
+
instance: FederationInstance,
|
|
28
|
+
{ moduleId, entry }: FederatedModuleRef,
|
|
29
|
+
): Promise<T | null> {
|
|
30
|
+
// The remote name is the first segment; the expose is the remainder.
|
|
31
|
+
const name = moduleId.slice(0, moduleId.indexOf("/"));
|
|
32
|
+
instance.registerRemotes([{ name, entry }]);
|
|
33
|
+
return (await instance.loadRemote<T | null>(moduleId)) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* {@link loadRemoteModule} with the render-empty-on-failure policy: any failure
|
|
38
|
+
* resolves to `null` (and logs), so every caller handles a missing interface
|
|
39
|
+
* the same way — a panel renders empty, a worker doesn't spawn.
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export async function loadFederatedModule<T>(
|
|
44
|
+
instance: FederationInstance,
|
|
45
|
+
ref: FederatedModuleRef,
|
|
46
|
+
): Promise<T | null> {
|
|
47
|
+
try {
|
|
48
|
+
return await loadRemoteModule<T>(instance, ref);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.error(`Failed to load federated module "${ref.moduleId}"`, error);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createInstance as createMFInstance } from "@module-federation/runtime";
|
|
2
|
+
import type {
|
|
3
|
+
ModuleFederationRuntimePlugin,
|
|
4
|
+
UserOptions,
|
|
5
|
+
} from "@module-federation/runtime/types";
|
|
6
|
+
|
|
7
|
+
/** Make some properties of a type optional. */
|
|
8
|
+
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export type FederationRemote = {
|
|
14
|
+
name: string;
|
|
15
|
+
entry: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
export type CreateInstanceOptions = MakeOptional<
|
|
22
|
+
Pick<UserOptions, "name" | "remotes" | "shared" | "plugins">,
|
|
23
|
+
"remotes" | "plugins"
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export interface FederationInstance {
|
|
30
|
+
registerRemotes(remotes: FederationRemote[]): void;
|
|
31
|
+
loadRemote<T>(id: string): Promise<T | null>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export function createInstance(
|
|
38
|
+
options: CreateInstanceOptions,
|
|
39
|
+
): FederationInstance {
|
|
40
|
+
const instance = createMFInstance({
|
|
41
|
+
remotes: [],
|
|
42
|
+
...options,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
registerRemotes: (remotes) =>
|
|
47
|
+
instance.registerRemotes(remotes, { force: false }),
|
|
48
|
+
loadRemote: async <T>(id: string) => {
|
|
49
|
+
let remoteModule: T | null;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
remoteModule = await instance.loadRemote<T>(id);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(`Failed to load remote module "${id}"`, {
|
|
55
|
+
cause: error,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return remoteModule;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Forward Module Federation lifecycle events to a provided logging function
|
|
66
|
+
* from workbench.
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
export function log(
|
|
71
|
+
onDebug: (message: string, context?: Record<string, unknown>) => void,
|
|
72
|
+
): ModuleFederationRuntimePlugin {
|
|
73
|
+
const logEvent = (eventName: string, args: unknown) => {
|
|
74
|
+
// `origin` logs the whole instance, which is noisy
|
|
75
|
+
const { origin: _origin, ...data } =
|
|
76
|
+
typeof args === "object" && args !== null
|
|
77
|
+
? (args as Record<string, unknown>)
|
|
78
|
+
: { args };
|
|
79
|
+
|
|
80
|
+
onDebug(`[Lifecycle] ${eventName}`, data);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
name: "sanity-logger",
|
|
85
|
+
beforeInit(args) {
|
|
86
|
+
logEvent("beforeInit", args);
|
|
87
|
+
return args;
|
|
88
|
+
},
|
|
89
|
+
beforeRegisterRemote(args) {
|
|
90
|
+
logEvent("beforeRegisterRemote", args);
|
|
91
|
+
return args;
|
|
92
|
+
},
|
|
93
|
+
beforePreloadRemote(args) {
|
|
94
|
+
logEvent("beforePreloadRemote", args);
|
|
95
|
+
},
|
|
96
|
+
beforeRequest(args) {
|
|
97
|
+
logEvent("beforeRequest", args);
|
|
98
|
+
return args;
|
|
99
|
+
},
|
|
100
|
+
afterResolve(args) {
|
|
101
|
+
logEvent("afterResolve", args);
|
|
102
|
+
return args;
|
|
103
|
+
},
|
|
104
|
+
onLoad(args) {
|
|
105
|
+
logEvent("onLoad", args);
|
|
106
|
+
return args;
|
|
107
|
+
},
|
|
108
|
+
loadShare(args) {
|
|
109
|
+
logEvent("loadShare", args);
|
|
110
|
+
},
|
|
111
|
+
beforeLoadShare(args) {
|
|
112
|
+
logEvent("beforeLoadShare", args);
|
|
113
|
+
return args;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|