@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,177 @@
|
|
|
1
|
+
import type { Resource as ProtocolResource } from "@sanity/message-protocol";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type App,
|
|
5
|
+
type DockGroup,
|
|
6
|
+
type Interface,
|
|
7
|
+
InterfaceSchema,
|
|
8
|
+
type LocalApp,
|
|
9
|
+
type LocalInterface,
|
|
10
|
+
LocalInterfaceSchema,
|
|
11
|
+
type LocalPanel,
|
|
12
|
+
type LocalService,
|
|
13
|
+
type Panel,
|
|
14
|
+
type PanelComponent,
|
|
15
|
+
type Service,
|
|
16
|
+
} from "./interface";
|
|
17
|
+
|
|
18
|
+
// The consumer/wire half of the interface model lives in `./interface`. The
|
|
19
|
+
// authoring half (`unstable_defineView` / `unstable_defineService`) lives with
|
|
20
|
+
// the CLI build helpers. Re-export the wire model so it stays part of the
|
|
21
|
+
// workbench app-model surface. `Panel`/`LocalPanel`, `Service`/`LocalService`,
|
|
22
|
+
// and `App`/`LocalApp` are the panel-, worker-, and app-typed subsets.
|
|
23
|
+
export {
|
|
24
|
+
type App,
|
|
25
|
+
type DockGroup,
|
|
26
|
+
type Interface,
|
|
27
|
+
InterfaceSchema,
|
|
28
|
+
type LocalApp,
|
|
29
|
+
type LocalInterface,
|
|
30
|
+
LocalInterfaceSchema,
|
|
31
|
+
type LocalPanel,
|
|
32
|
+
type LocalService,
|
|
33
|
+
type Panel,
|
|
34
|
+
type PanelComponent,
|
|
35
|
+
type Service,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
export type AbstractApplicationType =
|
|
42
|
+
| "studio"
|
|
43
|
+
| "coreApp"
|
|
44
|
+
| "canvas"
|
|
45
|
+
| "media-library"
|
|
46
|
+
| "workspace";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
export abstract class AbstractApplication<
|
|
52
|
+
TType extends AbstractApplicationType,
|
|
53
|
+
TProtocolResource extends ProtocolResource = Extract<
|
|
54
|
+
ProtocolResource,
|
|
55
|
+
{ type: TType }
|
|
56
|
+
>,
|
|
57
|
+
> {
|
|
58
|
+
readonly type: TType;
|
|
59
|
+
|
|
60
|
+
constructor(type: TType) {
|
|
61
|
+
this.type = type;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The in-app route this application navigates to, or `null` when it isn't
|
|
66
|
+
* navigable as a full-page app (US5, spec 002-workbench-extension-api — e.g. an SDK app with no `app` view).
|
|
67
|
+
* The dock renders a navigable item only for a non-null `href`; the app
|
|
68
|
+
* routes 404 a `null`-href app on direct visit.
|
|
69
|
+
*/
|
|
70
|
+
abstract get href(): string | null;
|
|
71
|
+
|
|
72
|
+
abstract get title(): string;
|
|
73
|
+
|
|
74
|
+
abstract get id(): string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the application is federated or not. This is used to determine
|
|
78
|
+
* if the application should be rendered in an iframe or not.
|
|
79
|
+
*/
|
|
80
|
+
abstract get isFederated(): boolean;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether the application is served by a local CLI dev server rather than a
|
|
84
|
+
* deployed remote. Defaults to `false`; user applications flip this when
|
|
85
|
+
* constructed from a `LocalUserApplication` payload.
|
|
86
|
+
*/
|
|
87
|
+
get isLocal(): boolean {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
abstract get url(): URL;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Interfaces the application exposes (e.g. dock panels). Defaults to none;
|
|
95
|
+
* user applications surface what they declared via `unstable_defineApp`.
|
|
96
|
+
*/
|
|
97
|
+
get interfaces(): readonly LocalInterface[] {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether the application has a navigable full-page `app` view (US5, spec 002-workbench-extension-api). Defaults
|
|
103
|
+
* to `true` — studios, Canvas, and Media Library are always navigable. An SDK
|
|
104
|
+
* app overrides this to derive it from whether it declares an `app` interface.
|
|
105
|
+
*/
|
|
106
|
+
get hasAppView(): boolean {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Federation module id of the app's full-page view (`${id}/App`), or `null`
|
|
112
|
+
* when it can't be federation-loaded: the app isn't federated (Canvas, Media
|
|
113
|
+
* Library, deployed apps shown in an iframe) or has no app view. Only a
|
|
114
|
+
* non-null `moduleId` is fetched and rendered by the remotes machine.
|
|
115
|
+
*/
|
|
116
|
+
get moduleId(): string | null {
|
|
117
|
+
return this.isFederated && this.hasAppView ? `${this.id}/App` : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Federation module id of one of this app's panel view components, or `null`
|
|
122
|
+
* when the app isn't federated (a non-federated app's panels can't be loaded
|
|
123
|
+
* as remotes).
|
|
124
|
+
*/
|
|
125
|
+
resolveViewModuleId(
|
|
126
|
+
view: LocalPanel,
|
|
127
|
+
component: PanelComponent,
|
|
128
|
+
): string | null {
|
|
129
|
+
return this.isFederated
|
|
130
|
+
? `${this.id}/views/${view.name}/${component}`
|
|
131
|
+
: null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get initials(): string {
|
|
135
|
+
const SYMBOLS = /[^\p{Alpha}\p{N}\p{White_Space}]/gu;
|
|
136
|
+
const WHITESPACE = /\p{White_Space}+/u;
|
|
137
|
+
const ALPHANUMERIC_SEGMENTS = /(\p{N}+|\p{Alpha}+)/gu;
|
|
138
|
+
const IS_NUMERIC = /^\p{N}+$/u;
|
|
139
|
+
|
|
140
|
+
if (!this.title) return "";
|
|
141
|
+
|
|
142
|
+
const namesArray = this.title
|
|
143
|
+
.replace(SYMBOLS, "")
|
|
144
|
+
.split(WHITESPACE)
|
|
145
|
+
.filter(Boolean);
|
|
146
|
+
|
|
147
|
+
if (namesArray.length === 0) return "";
|
|
148
|
+
|
|
149
|
+
if (namesArray.length === 1) {
|
|
150
|
+
const word = namesArray[0];
|
|
151
|
+
const segments = word.match(ALPHANUMERIC_SEGMENTS) || [];
|
|
152
|
+
|
|
153
|
+
if (segments.length === 0) return "";
|
|
154
|
+
if (segments.length === 1) {
|
|
155
|
+
if (word.length === 1) {
|
|
156
|
+
return word.toUpperCase();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (IS_NUMERIC.test(word)) {
|
|
160
|
+
return `${word.charAt(0)}${word.charAt(1)}`.toUpperCase();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return word.charAt(0).toUpperCase();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return `${segments[0]!.charAt(0)}${segments[1].charAt(0)}`.toUpperCase();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return `${namesArray[0].charAt(0)}${namesArray[namesArray.length - 1].charAt(0)}`.toUpperCase();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Converts the resource to a protocol resource that comlink expects
|
|
174
|
+
* for backwards compatibility with the old API format.
|
|
175
|
+
*/
|
|
176
|
+
abstract toProtocolResource(): TProtocolResource;
|
|
177
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The consumer/wire half of the workbench extension interface model — the
|
|
5
|
+
* records the application service returns and the workbench renders from.
|
|
6
|
+
*
|
|
7
|
+
* The authoring half (`unstable_defineView` / `unstable_defineService` and the
|
|
8
|
+
* `*DeclarationSchema`s) lives with the CLI build helpers, not here: this module
|
|
9
|
+
* only models the wire shape the workbench consumes. Field names are snake_case
|
|
10
|
+
* to match the application service's JSON.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fields every interface record carries. Per-type schemas add their
|
|
15
|
+
* `interface_type` discriminator on top.
|
|
16
|
+
*/
|
|
17
|
+
const interfaceBaseFields = {
|
|
18
|
+
id: z.string(),
|
|
19
|
+
deployment_id: z.string(),
|
|
20
|
+
name: z.string(),
|
|
21
|
+
entry_point: z.string(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The `panel` interface record — a view that renders in the dock.
|
|
26
|
+
*/
|
|
27
|
+
const PanelInterfaceSchema = z.object({
|
|
28
|
+
...interfaceBaseFields,
|
|
29
|
+
interface_type: z.literal("panel"),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The `worker` interface record — a background service the workbench runs.
|
|
34
|
+
*/
|
|
35
|
+
const WorkerInterfaceSchema = z.object({
|
|
36
|
+
...interfaceBaseFields,
|
|
37
|
+
interface_type: z.literal("worker"),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The `app` interface record — an application's navigable full-page view. Its
|
|
42
|
+
* presence is the sole signal a branded app is navigable (US5, spec
|
|
43
|
+
* 002-workbench-extension-api).
|
|
44
|
+
*/
|
|
45
|
+
const AppInterfaceSchema = z.object({
|
|
46
|
+
...interfaceBaseFields,
|
|
47
|
+
interface_type: z.literal("app"),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* An interface record discriminated on `interface_type` — a `panel` renders in
|
|
52
|
+
* the dock, a `worker` runs a background service, an `app` is the navigable
|
|
53
|
+
* full-page view.
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export const InterfaceSchema = z.discriminatedUnion("interface_type", [
|
|
57
|
+
PanelInterfaceSchema,
|
|
58
|
+
WorkerInterfaceSchema,
|
|
59
|
+
AppInterfaceSchema,
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/** @public */
|
|
63
|
+
export type Interface = z.output<typeof InterfaceSchema>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* An {@link Interface} without the service-assigned `id`/`deployment_id` a local
|
|
67
|
+
* dev server can't provide — the shape the workbench renders from.
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
export const LocalInterfaceSchema = z.discriminatedUnion("interface_type", [
|
|
71
|
+
PanelInterfaceSchema.omit({ id: true, deployment_id: true }),
|
|
72
|
+
WorkerInterfaceSchema.omit({ id: true, deployment_id: true }),
|
|
73
|
+
AppInterfaceSchema.omit({ id: true, deployment_id: true }),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/** @public */
|
|
77
|
+
export type LocalInterface = z.output<typeof LocalInterfaceSchema>;
|
|
78
|
+
|
|
79
|
+
/** The interface record for a given `interface_type`. @public */
|
|
80
|
+
export type InterfaceOfType<T extends Interface["interface_type"]> = Extract<
|
|
81
|
+
Interface,
|
|
82
|
+
{ interface_type: T }
|
|
83
|
+
>;
|
|
84
|
+
|
|
85
|
+
/** The {@link InterfaceOfType} counterpart without `id`/`deployment_id`. @public */
|
|
86
|
+
export type LocalInterfaceOfType<T extends LocalInterface["interface_type"]> =
|
|
87
|
+
Extract<LocalInterface, { interface_type: T }>;
|
|
88
|
+
|
|
89
|
+
/** @public */
|
|
90
|
+
export type Panel = InterfaceOfType<"panel">;
|
|
91
|
+
/** @public */
|
|
92
|
+
export type LocalPanel = LocalInterfaceOfType<"panel">;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A service an application runs — the worker-typed interface record. `Service`/
|
|
96
|
+
* `LocalService` are the worker-typed subset of the interface model.
|
|
97
|
+
* @public
|
|
98
|
+
*/
|
|
99
|
+
export type Service = InterfaceOfType<"worker">;
|
|
100
|
+
/** @public */
|
|
101
|
+
export type LocalService = LocalInterfaceOfType<"worker">;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The navigable full-page view — its presence is the sole signal a branded app
|
|
105
|
+
* is navigable (US5, spec 002-workbench-extension-api).
|
|
106
|
+
* @public
|
|
107
|
+
*/
|
|
108
|
+
export type App = InterfaceOfType<"app">;
|
|
109
|
+
/** @public */
|
|
110
|
+
export type LocalApp = LocalInterfaceOfType<"app">;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A panel's view-component slots, in render order — each its own federation
|
|
114
|
+
* island (`${id}/views/${name}/${component}`). The authoring counterpart that
|
|
115
|
+
* drives the build's per-slot codegen lives with the CLI define helpers.
|
|
116
|
+
* @public
|
|
117
|
+
*/
|
|
118
|
+
export type PanelComponent = "title" | "panel";
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Where an application docks. The manifest's `group` value the workbench reads
|
|
122
|
+
* to place an app in the dock; the authoring counterpart that validates it on
|
|
123
|
+
* `unstable_defineApp` lives with the CLI define helpers.
|
|
124
|
+
* @public
|
|
125
|
+
*/
|
|
126
|
+
export type DockGroup = "dock.system" | "dock.applications" | "dock.user";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { CanvasResource as ProtocolCanvasResource } from "@sanity/message-protocol";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { AbstractApplication } from "./applications/application";
|
|
5
|
+
import { getSanityDomain } from "./env";
|
|
6
|
+
import { OrganizationId } from "./organizations";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Canvas ID schema, branded for type safety.
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
const CanvasId = z.string().nonempty().brand("CanvasId");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Canvas ID type, branded for type safety.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export type CanvasId = z.output<typeof CanvasId>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates and brands a string as a CanvasId.
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export function brandCanvasId(id: string): CanvasId {
|
|
25
|
+
return CanvasId.parse(id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Canvas = z.object({
|
|
29
|
+
id: CanvasId,
|
|
30
|
+
organizationId: OrganizationId,
|
|
31
|
+
status: z.enum(["active", "provisioning"]),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Represents a Canvas resource as returned from the API.
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
export type Canvas = z.output<typeof Canvas>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validates and parses a raw API response into a branded
|
|
42
|
+
* Canvas.
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export function parseCanvas(data: unknown): Canvas {
|
|
46
|
+
return Canvas.parse(data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whilst the constructor takes an organization's canvas resource the existance
|
|
51
|
+
* therefore implies the organization has access to the canvas application which
|
|
52
|
+
* is what workbench most importantly wants to know.
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export class CanvasApplication extends AbstractApplication<"canvas"> {
|
|
56
|
+
readonly canvas: Canvas;
|
|
57
|
+
|
|
58
|
+
constructor(canvas: Canvas) {
|
|
59
|
+
super("canvas");
|
|
60
|
+
|
|
61
|
+
this.canvas = canvas;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get id(): CanvasId {
|
|
65
|
+
return this.canvas.id;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get href(): string {
|
|
69
|
+
return "canvas";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get title(): string {
|
|
73
|
+
return "Canvas";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get url(): URL {
|
|
77
|
+
return new URL(
|
|
78
|
+
`https://canvas.${getSanityDomain()}/o/${this.canvas.organizationId}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get isFederated(): boolean {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
toProtocolResource(): ProtocolCanvasResource {
|
|
87
|
+
return {
|
|
88
|
+
type: "canvas",
|
|
89
|
+
id: this.id,
|
|
90
|
+
} satisfies ProtocolCanvasResource;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { OrganizationId } from "./organizations";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workbench configuration.
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
interface Config {
|
|
9
|
+
/**
|
|
10
|
+
* The organization ID to use when rendering the workbench.
|
|
11
|
+
*/
|
|
12
|
+
organizationId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The resolved configuration after processing
|
|
17
|
+
* and validating the provided config.
|
|
18
|
+
*
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
type ResolvedConfig = Omit<Config, "organizationId"> & {
|
|
22
|
+
organizationId: OrganizationId;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for rendering a remote module, such as a user application.
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
interface RemoteModuleRenderOptions {
|
|
31
|
+
reactStrictMode?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type { Config, ResolvedConfig, RemoteModuleRenderOptions };
|
package/src/core/env.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
declare const __SANITY_STAGING__: boolean | undefined;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the Sanity domain based on the `__SANITY_STAGING__` runtime-time flag.
|
|
5
|
+
* If the flag is set to `true`, the staging domain is returned.
|
|
6
|
+
* Otherwise, the production domain is returned.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export function getSanityDomain(): string {
|
|
11
|
+
if (getSanityEnv() === "staging") {
|
|
12
|
+
return "sanity.work";
|
|
13
|
+
}
|
|
14
|
+
return "sanity.io";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns the API host based on the `__SANITY_STAGING__` runtime-time flag.
|
|
19
|
+
* If the flag is set to `true`, the staging API host is returned.
|
|
20
|
+
* Otherwise, the production API host is returned.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export function getApiHost(): string | undefined {
|
|
25
|
+
return `https://api.${getSanityDomain()}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns the current Sanity environment based on the `__SANITY_STAGING__` runtime-time flag.
|
|
30
|
+
* If the flag is set to `true`, "staging" is returned.
|
|
31
|
+
* Otherwise, "production" is returned.
|
|
32
|
+
*
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export function getSanityEnv(): "staging" | "production" {
|
|
36
|
+
if (
|
|
37
|
+
typeof __SANITY_STAGING__ !== "undefined" &&
|
|
38
|
+
__SANITY_STAGING__ === true
|
|
39
|
+
) {
|
|
40
|
+
return "staging";
|
|
41
|
+
}
|
|
42
|
+
return "production";
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./applications/application";
|
|
2
|
+
export * from "./applications/application-list";
|
|
3
|
+
export * from "./canvases";
|
|
4
|
+
export * from "./config";
|
|
5
|
+
export * from "./env";
|
|
6
|
+
export * from "./log";
|
|
7
|
+
export * from "./media-libraries";
|
|
8
|
+
export * from "./organizations";
|
|
9
|
+
export * from "./projects";
|
|
10
|
+
export { joinUrlPaths } from "./shared/urls";
|
|
11
|
+
export * from "./user-applications/core-app";
|
|
12
|
+
export * from "./user-applications/studios";
|
|
13
|
+
export * from "./user-applications/user-application";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log levels in order of verbosity (least to most)
|
|
3
|
+
* - none: Silent
|
|
4
|
+
* - error: Critical failures that prevent operation
|
|
5
|
+
* - warn: Issues that may cause problems but don't stop execution
|
|
6
|
+
* - info: High-level informational messages (default)
|
|
7
|
+
* - debug: Detailed debugging information (maintainer level)
|
|
8
|
+
* - trace: Very detailed tracing — sets `internal: true` on context
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export type LogLevel = "none" | "error" | "warn" | "info" | "debug";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Namespaces organize logs by functional domain.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export type LogNamespace = string;
|
|
18
|
+
|
|
19
|
+
type LogContext = { [key: string]: unknown };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export interface Logger {
|
|
25
|
+
error: (message: string, context?: LogContext) => void;
|
|
26
|
+
warn: (message: string, context?: LogContext) => void;
|
|
27
|
+
info: (message: string, context?: LogContext) => void;
|
|
28
|
+
debug: (message: string, context?: LogContext) => void;
|
|
29
|
+
child: (domain: string, context?: LogContext) => Logger;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const LEVELS: readonly LogLevel[] = ["none", "error", "warn", "info", "debug"];
|
|
33
|
+
|
|
34
|
+
interface LoggerOptions {
|
|
35
|
+
namespace?: LogNamespace;
|
|
36
|
+
context?: LogContext;
|
|
37
|
+
logLevel?: LogLevel;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a leveled logger with an optional namespace prefix and bound
|
|
42
|
+
* context. Calls below the configured `logLevel` are suppressed; `"none"`
|
|
43
|
+
* silences the logger entirely.
|
|
44
|
+
*
|
|
45
|
+
* Use {@link Logger.child} to derive a sub-logger with an extended namespace
|
|
46
|
+
* (e.g. `parent:domain`) that inherits the parent's level and merges its
|
|
47
|
+
* bound context.
|
|
48
|
+
*
|
|
49
|
+
* @param options - Logger configuration.
|
|
50
|
+
* @param options.namespace - Prepended to every message in `[brackets]`.
|
|
51
|
+
* @param options.context - Bound context merged with per-call context.
|
|
52
|
+
* Per-call keys win on conflict.
|
|
53
|
+
* @param options.logLevel - Maximum verbosity to emit. Default `"info"`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const log = createLogger({ namespace: "checkout", logLevel: "debug" });
|
|
58
|
+
* log.info("placed", { orderId: "abc" });
|
|
59
|
+
* // → [checkout] placed { orderId: "abc" }
|
|
60
|
+
*
|
|
61
|
+
* const auth = log.child("auth", { tenant: "acme" });
|
|
62
|
+
* auth.warn("token expiring");
|
|
63
|
+
* // → [checkout:auth] token expiring { tenant: "acme" }
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export function createLogger({
|
|
69
|
+
namespace,
|
|
70
|
+
context: baseContext,
|
|
71
|
+
logLevel = "info",
|
|
72
|
+
}: LoggerOptions = {}): Logger {
|
|
73
|
+
function isLevelEnabled(level: LogLevel): boolean {
|
|
74
|
+
return LEVELS.indexOf(level) <= LEVELS.indexOf(logLevel);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function logAtLevel(
|
|
78
|
+
level: LogLevel,
|
|
79
|
+
message: string,
|
|
80
|
+
context?: LogContext,
|
|
81
|
+
): void {
|
|
82
|
+
if (!isLevelEnabled(level)) return;
|
|
83
|
+
|
|
84
|
+
const merged =
|
|
85
|
+
(baseContext ?? context) ? { ...baseContext, ...context } : undefined;
|
|
86
|
+
const args: unknown[] = [
|
|
87
|
+
...(namespace ? [`[${namespace}]`] : []),
|
|
88
|
+
message,
|
|
89
|
+
...(merged ? [merged] : []),
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
if (level === "error") console.error(...args);
|
|
93
|
+
else if (level === "warn") console.warn(...args);
|
|
94
|
+
// oxlint-disable-next-line no-console
|
|
95
|
+
else if (level === "info") console.info(...args);
|
|
96
|
+
// oxlint-disable-next-line no-console
|
|
97
|
+
else console.debug(...args);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
error: (message, context) => logAtLevel("error", message, context),
|
|
102
|
+
warn: (message, context) => logAtLevel("warn", message, context),
|
|
103
|
+
info: (message, context) => logAtLevel("info", message, context),
|
|
104
|
+
debug: (message, context) => logAtLevel("debug", message, context),
|
|
105
|
+
child: (domain, context) =>
|
|
106
|
+
createLogger({
|
|
107
|
+
logLevel,
|
|
108
|
+
namespace: namespace ? `${namespace}:${domain}` : domain,
|
|
109
|
+
context:
|
|
110
|
+
baseContext || context ? { ...baseContext, ...context } : undefined,
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Shared workbench logger instance. Use this from both the workbench host
|
|
117
|
+
* and its remotes so lifecycle and diagnostic logs appear under a single
|
|
118
|
+
* namespace.
|
|
119
|
+
*
|
|
120
|
+
* @public
|
|
121
|
+
*/
|
|
122
|
+
export const logger: Logger = createLogger({
|
|
123
|
+
namespace: "sanity-workbench",
|
|
124
|
+
logLevel: "debug",
|
|
125
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { MediaResource as ProtocolMediaResource } from "@sanity/message-protocol";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { AbstractApplication } from "./applications/application";
|
|
5
|
+
import { getSanityDomain } from "./env";
|
|
6
|
+
import { OrganizationId } from "./organizations";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Canvas ID schema, branded for type safety.
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
const MediaLibraryId = z.string().nonempty().brand("MediaLibraryId");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* MediaLibrary ID type, branded for type safety.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export type MediaLibraryId = z.output<typeof MediaLibraryId>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates and brands a string as a MediaLibraryId.
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export function brandMediaLibraryId(id: string): MediaLibraryId {
|
|
25
|
+
return MediaLibraryId.parse(id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const MediaLibrary = z.object({
|
|
29
|
+
id: MediaLibraryId,
|
|
30
|
+
organizationId: OrganizationId,
|
|
31
|
+
status: z.enum(["active", "provisioning"]),
|
|
32
|
+
aclMode: z.enum(["private", "public"]),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Represents a MediaLibrary resource as returned from the API.
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export type MediaLibrary = z.output<typeof MediaLibrary>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates and parses a raw API response into a branded
|
|
43
|
+
* MediaLibrary.
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
export function parseMediaLibrary(data: unknown): MediaLibrary {
|
|
47
|
+
return MediaLibrary.parse(data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whilst the constructor takes an organization's media library resource the existance
|
|
52
|
+
* therefore implies the organization has access to the media library application which
|
|
53
|
+
* is what workbench most importantly wants to know.
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export class MediaLibraryApplication extends AbstractApplication<"media-library"> {
|
|
57
|
+
readonly library: MediaLibrary;
|
|
58
|
+
|
|
59
|
+
constructor(library: MediaLibrary) {
|
|
60
|
+
super("media-library");
|
|
61
|
+
|
|
62
|
+
this.library = library;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get id(): string {
|
|
66
|
+
return this.library.id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get href(): string {
|
|
70
|
+
return "media";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get title(): string {
|
|
74
|
+
return "Media Library";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get isFederated(): boolean {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get url(): URL {
|
|
82
|
+
return new URL(
|
|
83
|
+
`https://media.${getSanityDomain()}/${this.library.organizationId}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
toProtocolResource(): ProtocolMediaResource {
|
|
88
|
+
return {
|
|
89
|
+
type: "media-library",
|
|
90
|
+
id: this.id,
|
|
91
|
+
} satisfies ProtocolMediaResource;
|
|
92
|
+
}
|
|
93
|
+
}
|