@sanity/workbench 0.1.0-alpha.6 → 0.1.0-alpha.8

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 (47) hide show
  1. package/dist/{log.js → _chunks-es/index.js} +7 -2
  2. package/dist/_chunks-es/index.js.map +1 -0
  3. package/dist/_internal.d.ts +13 -6
  4. package/dist/_internal.js +20 -9
  5. package/dist/_internal.js.map +1 -1
  6. package/dist/core.d.ts +1464 -0
  7. package/dist/core.js +744 -0
  8. package/dist/core.js.map +1 -0
  9. package/package.json +12 -4
  10. package/src/_exports/core.ts +1 -0
  11. package/src/_internal/index.ts +2 -1
  12. package/src/_internal/render.test.ts +91 -4
  13. package/src/_internal/render.ts +53 -33
  14. package/src/core/__tests__/__fixtures__.ts +245 -0
  15. package/src/core/applications/application-list.test.ts +222 -0
  16. package/src/core/applications/application-list.ts +103 -0
  17. package/src/core/applications/application.ts +78 -0
  18. package/src/core/applications/local-application.test.ts +93 -0
  19. package/src/core/applications/local-application.ts +56 -0
  20. package/src/core/canvases.test.ts +38 -0
  21. package/src/core/canvases.ts +81 -0
  22. package/src/core/config.ts +34 -0
  23. package/src/core/index.ts +12 -0
  24. package/src/{log → core/log}/index.ts +12 -0
  25. package/src/core/media-libraries.test.ts +38 -0
  26. package/src/core/media-libraries.ts +83 -0
  27. package/src/core/organizations.test.ts +134 -0
  28. package/src/core/organizations.ts +115 -0
  29. package/src/core/projects.test.ts +248 -0
  30. package/src/core/projects.ts +114 -0
  31. package/src/core/shared/urls.test.ts +182 -0
  32. package/src/core/shared/urls.ts +128 -0
  33. package/src/core/user-applications/core-app.test.ts +236 -0
  34. package/src/core/user-applications/core-app.ts +113 -0
  35. package/src/core/user-applications/studios/index.ts +3 -0
  36. package/src/core/user-applications/studios/schemas.test.ts +113 -0
  37. package/src/core/user-applications/studios/schemas.ts +106 -0
  38. package/src/core/user-applications/studios/studio.test.ts +997 -0
  39. package/src/core/user-applications/studios/studio.ts +498 -0
  40. package/src/core/user-applications/studios/workspace.ts +143 -0
  41. package/src/core/user-applications/user-application.test.ts +125 -0
  42. package/src/core/user-applications/user-application.ts +107 -0
  43. package/src/vite-env.d.ts +8 -0
  44. package/dist/log.d.ts +0 -48
  45. package/dist/log.js.map +0 -1
  46. package/src/_exports/log.ts +0 -1
  47. /package/src/{log → core/log}/index.test.ts +0 -0
@@ -0,0 +1,93 @@
1
+ import { describe, expect, expectTypeOf, it } from "vitest";
2
+
3
+ import { AbstractApplication } from "./application";
4
+ import type { LocalApplicationData } from "./local-application";
5
+ import { LocalApplication } from "./local-application";
6
+
7
+ const STUDIO_APP: LocalApplicationData = {
8
+ host: "localhost",
9
+ port: 3333,
10
+ type: "studio",
11
+ };
12
+
13
+ const CORE_APP: LocalApplicationData = {
14
+ host: "127.0.0.1",
15
+ port: 4444,
16
+ type: "coreApp",
17
+ };
18
+
19
+ describe("LocalApplication", () => {
20
+ it("is an instance of AbstractApplication", () => {
21
+ expect(new LocalApplication(STUDIO_APP)).toBeInstanceOf(
22
+ AbstractApplication,
23
+ );
24
+ });
25
+
26
+ describe("type", () => {
27
+ it("inherits the type from LocalApplicationData", () => {
28
+ expect(new LocalApplication(STUDIO_APP).type).toBe("studio");
29
+ expect(new LocalApplication(CORE_APP).type).toBe("coreApp");
30
+ });
31
+
32
+ it("has the correct TypeScript type", () => {
33
+ const app = new LocalApplication(STUDIO_APP);
34
+ expectTypeOf(app.type).toEqualTypeOf<"studio" | "coreApp">();
35
+ });
36
+ });
37
+
38
+ describe("id", () => {
39
+ it("generates a deterministic id from host and port", () => {
40
+ expect(new LocalApplication(STUDIO_APP).id).toBe("localhost-3333");
41
+ });
42
+
43
+ it("is stable across instances with the same data", () => {
44
+ expect(new LocalApplication(STUDIO_APP).id).toBe(
45
+ new LocalApplication(STUDIO_APP).id,
46
+ );
47
+ });
48
+
49
+ it("differs when host or port differs", () => {
50
+ const a = new LocalApplication(STUDIO_APP);
51
+ const b = new LocalApplication(CORE_APP);
52
+ expect(a.id).not.toBe(b.id);
53
+ });
54
+ });
55
+
56
+ describe("title", () => {
57
+ it("formats as host:port", () => {
58
+ expect(new LocalApplication(STUDIO_APP).title).toBe("localhost:3333");
59
+ expect(new LocalApplication(CORE_APP).title).toBe("127.0.0.1:4444");
60
+ });
61
+ });
62
+
63
+ describe("href", () => {
64
+ it("generates a /local/ route", () => {
65
+ expect(new LocalApplication(STUDIO_APP).href).toBe(
66
+ "/local/localhost-3333",
67
+ );
68
+ expect(new LocalApplication(CORE_APP).href).toBe("/local/127.0.0.1-4444");
69
+ });
70
+ });
71
+
72
+ describe("initials", () => {
73
+ it("derives initials from the title", () => {
74
+ // "localhost:3333" → strip symbols → "localhost3333"
75
+ // → alpha segment "localhost" + numeric segment "3333" → "L" + "3" = "L3"
76
+ expect(new LocalApplication(STUDIO_APP).initials).toBe("L3");
77
+ });
78
+
79
+ it("handles numeric-only titles (e.g. IP address)", () => {
80
+ // "127.0.0.1:4444" → strip symbols → "1270014444"
81
+ // → single numeric segment → first two chars → "12"
82
+ expect(new LocalApplication(CORE_APP).initials).toBe("12");
83
+ });
84
+ });
85
+
86
+ describe("toProtocolResource", () => {
87
+ it("throws because local applications have no protocol representation", () => {
88
+ expect(() =>
89
+ new LocalApplication(STUDIO_APP).toProtocolResource(),
90
+ ).toThrow();
91
+ });
92
+ });
93
+ });
@@ -0,0 +1,56 @@
1
+ import { AbstractApplication } from "./application";
2
+
3
+ /**
4
+ * Raw data for a local application discovered by the CLI dev server.
5
+ *
6
+ * @experimental
7
+ * @public
8
+ */
9
+ export interface LocalApplicationData {
10
+ host: string;
11
+ port: number;
12
+ type: "studio" | "coreApp";
13
+ }
14
+
15
+ /**
16
+ * An Application wrapping a locally-discovered CLI application.
17
+ * Provides a deterministic id, title and href suitable for use in navigation.
18
+ *
19
+ * @internal
20
+ */
21
+ export class LocalApplication extends AbstractApplication<
22
+ "studio" | "coreApp",
23
+ never
24
+ > {
25
+ readonly localApplication: LocalApplicationData;
26
+
27
+ constructor(localApplication: LocalApplicationData) {
28
+ super(localApplication.type);
29
+ this.localApplication = localApplication;
30
+ }
31
+
32
+ get id(): string {
33
+ const { host, port } = this.localApplication;
34
+ return `${host}-${port}`;
35
+ }
36
+
37
+ get title(): string {
38
+ const { host, port } = this.localApplication;
39
+ return `${host}:${port}`;
40
+ }
41
+
42
+ get href(): string {
43
+ return `/local/${this.id}`;
44
+ }
45
+
46
+ get url(): URL {
47
+ const { host, port } = this.localApplication;
48
+ return new URL(`http://${host}:${port}`);
49
+ }
50
+
51
+ toProtocolResource(): never {
52
+ throw new Error(
53
+ "LocalApplication cannot be serialized to a protocol resource",
54
+ );
55
+ }
56
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { CANVAS_DATA } from "./__tests__/__fixtures__";
4
+ import { CanvasApplication } from "./canvases";
5
+
6
+ const setup = () => new CanvasApplication(CANVAS_DATA);
7
+
8
+ describe("CanvasResource", () => {
9
+ it("should have a type of canvas", () => {
10
+ const resource = setup();
11
+ expect(resource.type).toBe("canvas");
12
+ });
13
+
14
+ it("should have a to of /canvas", () => {
15
+ const resource = setup();
16
+ expect(resource.href).toBe("canvas");
17
+ });
18
+
19
+ it("should have a name of Canvas", () => {
20
+ const resource = setup();
21
+ expect(resource.title).toBe("Canvas");
22
+ });
23
+
24
+ it("should have an id", () => {
25
+ const resource = setup();
26
+ expect(resource.id).toBe("cac1Na6lwtEI");
27
+ });
28
+
29
+ it("should implement the toProtocolResource method", () => {
30
+ const resource = setup();
31
+ expect(resource.toProtocolResource()).toMatchInlineSnapshot(`
32
+ {
33
+ "id": "cac1Na6lwtEI",
34
+ "type": "canvas",
35
+ }
36
+ `);
37
+ });
38
+ });
@@ -0,0 +1,81 @@
1
+ import type { CanvasResource as ProtocolCanvasResource } from "@sanity/message-protocol";
2
+ import { z } from "zod";
3
+
4
+ import { AbstractApplication } from "./applications/application";
5
+ import { OrganizationId } from "./organizations";
6
+
7
+ /**
8
+ * Canvas ID schema, branded for type safety.
9
+ * @public
10
+ */
11
+ const CanvasId = z.string().nonempty().brand("CanvasId");
12
+
13
+ /**
14
+ * Canvas ID type, branded for type safety.
15
+ * @public
16
+ */
17
+ export type CanvasId = z.output<typeof CanvasId>;
18
+
19
+ /**
20
+ * Validates and brands a string as a CanvasId.
21
+ * @public
22
+ */
23
+ export function brandCanvasId(id: string): CanvasId {
24
+ return CanvasId.parse(id);
25
+ }
26
+
27
+ const Canvas = z.object({
28
+ id: CanvasId,
29
+ organizationId: OrganizationId,
30
+ status: z.enum(["active", "provisioning"]),
31
+ });
32
+
33
+ /**
34
+ * Represents a Canvas resource as returned from the API.
35
+ * @public
36
+ */
37
+ export type Canvas = z.output<typeof Canvas>;
38
+
39
+ /**
40
+ * Validates and parses a raw API response into a branded
41
+ * Canvas.
42
+ * @public
43
+ */
44
+ export function parseCanvas(data: unknown): Canvas {
45
+ return Canvas.parse(data);
46
+ }
47
+
48
+ /**
49
+ * Whilst the constructor takes an organization's canvas resource the existance
50
+ * therefore implies the organization has access to the canvas application which
51
+ * is what workbench most importantly wants to know.
52
+ * @public
53
+ */
54
+ export class CanvasApplication extends AbstractApplication<"canvas"> {
55
+ readonly canvas: Canvas;
56
+
57
+ constructor(canvas: Canvas) {
58
+ super("canvas");
59
+
60
+ this.canvas = canvas;
61
+ }
62
+
63
+ get id(): CanvasId {
64
+ return this.canvas.id;
65
+ }
66
+
67
+ get href(): string {
68
+ return "canvas";
69
+ }
70
+
71
+ get title(): string {
72
+ return "Canvas";
73
+ }
74
+
75
+ toProtocolResource(): ProtocolCanvasResource {
76
+ return {
77
+ type: "canvas",
78
+ id: this.id,
79
+ } satisfies ProtocolCanvasResource;
80
+ }
81
+ }
@@ -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 };
@@ -0,0 +1,12 @@
1
+ export * from "./applications/application";
2
+ export * from "./applications/application-list";
3
+ export * from "./applications/local-application";
4
+ export * from "./canvases";
5
+ export * from "./config";
6
+ export * from "./log";
7
+ export * from "./media-libraries";
8
+ export * from "./organizations";
9
+ export * from "./projects";
10
+ export * from "./user-applications/core-app";
11
+ export * from "./user-applications/studios";
12
+ export * from "./user-applications/user-application";
@@ -78,3 +78,15 @@ export function createLogger({
78
78
  debug: (message, context) => logAtLevel("debug", message, context),
79
79
  };
80
80
  }
81
+
82
+ /**
83
+ * Shared workbench logger instance. Use this from both the workbench host
84
+ * and its remotes so lifecycle and diagnostic logs appear under a single
85
+ * namespace.
86
+ *
87
+ * @public
88
+ */
89
+ export const logger: Logger = createLogger({
90
+ namespace: "sanity-workbench",
91
+ logLevel: "debug",
92
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { MEDIA_LIBRARY_DATA } from "./__tests__/__fixtures__";
4
+ import { MediaLibraryApplication } from "./media-libraries";
5
+
6
+ const setup = () => new MediaLibraryApplication(MEDIA_LIBRARY_DATA);
7
+
8
+ describe("MediaLibraryApplication", () => {
9
+ it("should have a type of media-library", () => {
10
+ const resource = setup();
11
+ expect(resource.type).toBe("media-library");
12
+ });
13
+
14
+ it("should have a to of /media", () => {
15
+ const resource = setup();
16
+ expect(resource.href).toBe("media");
17
+ });
18
+
19
+ it("should have a name of Media Library", () => {
20
+ const resource = setup();
21
+ expect(resource.title).toBe("Media Library");
22
+ });
23
+
24
+ it("should have an id", () => {
25
+ const resource = setup();
26
+ expect(resource.id).toBe("al34RQcBfuD7");
27
+ });
28
+
29
+ it("should implement the toProtocolResource method", () => {
30
+ const resource = setup();
31
+ expect(resource.toProtocolResource()).toMatchInlineSnapshot(`
32
+ {
33
+ "id": "al34RQcBfuD7",
34
+ "type": "media-library",
35
+ }
36
+ `);
37
+ });
38
+ });
@@ -0,0 +1,83 @@
1
+ import type { MediaResource as ProtocolMediaResource } from "@sanity/message-protocol";
2
+ import { z } from "zod";
3
+
4
+ import { AbstractApplication } from "./applications/application";
5
+ import { OrganizationId } from "./organizations";
6
+
7
+ /**
8
+ * Canvas ID schema, branded for type safety.
9
+ * @public
10
+ */
11
+ const MediaLibraryId = z.string().nonempty().brand("MediaLibraryId");
12
+
13
+ /**
14
+ * MediaLibrary ID type, branded for type safety.
15
+ * @public
16
+ */
17
+ export type MediaLibraryId = z.output<typeof MediaLibraryId>;
18
+
19
+ /**
20
+ * Validates and brands a string as a MediaLibraryId.
21
+ * @public
22
+ */
23
+ export function brandMediaLibraryId(id: string): MediaLibraryId {
24
+ return MediaLibraryId.parse(id);
25
+ }
26
+
27
+ const MediaLibrary = z.object({
28
+ id: MediaLibraryId,
29
+ organizationId: OrganizationId,
30
+ status: z.enum(["active", "provisioning"]),
31
+ aclMode: z.enum(["private", "public"]),
32
+ });
33
+
34
+ /**
35
+ * Represents a MediaLibrary resource as returned from the API.
36
+ * @public
37
+ */
38
+ export type MediaLibrary = z.output<typeof MediaLibrary>;
39
+
40
+ /**
41
+ * Validates and parses a raw API response into a branded
42
+ * MediaLibrary.
43
+ * @public
44
+ */
45
+ export function parseMediaLibrary(data: unknown): MediaLibrary {
46
+ return MediaLibrary.parse(data);
47
+ }
48
+
49
+ /**
50
+ * Whilst the constructor takes an organization's media library resource the existance
51
+ * therefore implies the organization has access to the media library application which
52
+ * is what workbench most importantly wants to know.
53
+ * @public
54
+ */
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
+ toProtocolResource(): ProtocolMediaResource {
78
+ return {
79
+ type: "media-library",
80
+ id: this.id,
81
+ } satisfies ProtocolMediaResource;
82
+ }
83
+ }
@@ -0,0 +1,134 @@
1
+ import { describe, expect, expectTypeOf, it } from "vitest";
2
+
3
+ import type {
4
+ Organization,
5
+ OrganizationId,
6
+ OrganizationMember,
7
+ } from "./organizations";
8
+ import { brandOrganizationId, parseOrganization } from "./organizations";
9
+
10
+ describe("brandOrganizationId", () => {
11
+ it("brands a valid organization id", () => {
12
+ const id = brandOrganizationId("oABC123");
13
+ expect(id).toBe("oABC123");
14
+ expectTypeOf(id).toEqualTypeOf<OrganizationId>();
15
+ });
16
+
17
+ it("rejects an empty string", () => {
18
+ expect(() => brandOrganizationId("")).toThrow();
19
+ });
20
+
21
+ it("is not assignable from a plain string", () => {
22
+ expectTypeOf<OrganizationId>().branded.toEqualTypeOf<OrganizationId>();
23
+ });
24
+ });
25
+
26
+ describe("parseOrganization", () => {
27
+ const VALID_ORG = {
28
+ id: "org123",
29
+ name: "Sanity",
30
+ slug: "sanity",
31
+ createdAt: "2024-01-01T00:00:00Z",
32
+ updatedAt: "2024-01-01T00:00:00Z",
33
+ dashboardStatus: "enabled",
34
+ aiFeaturesStatus: "enabled",
35
+ defaultRoleName: "member",
36
+ };
37
+
38
+ const VALID_MEMBER = {
39
+ sanityUserId: "user1",
40
+ isCurrentUser: true,
41
+ user: {
42
+ id: "u1",
43
+ displayName: "Josh",
44
+ familyName: "Ellis",
45
+ givenName: "Josh",
46
+ middleName: null,
47
+ imageUrl: null,
48
+ email: "josh@sanity.io",
49
+ loginProvider: "google",
50
+ },
51
+ roles: [{ name: "admin", title: "Admin" }],
52
+ };
53
+
54
+ const VALID_ORG_FULL = {
55
+ ...VALID_ORG,
56
+ members: [VALID_MEMBER],
57
+ features: ["customLogo"],
58
+ };
59
+
60
+ it("parses a valid organization with members and features by default", () => {
61
+ const org = parseOrganization(VALID_ORG_FULL);
62
+ expect(org.name).toBe("Sanity");
63
+ expect(org.members).toHaveLength(1);
64
+ expect(org.features).toEqual(["customLogo"]);
65
+ expectTypeOf(org).toEqualTypeOf<Organization>();
66
+ expectTypeOf(org.id).toEqualTypeOf<OrganizationId>();
67
+ });
68
+
69
+ it("rejects missing required fields", () => {
70
+ expect(() => parseOrganization({ id: "org123" })).toThrow();
71
+ });
72
+
73
+ it("rejects missing members by default", () => {
74
+ expect(() => parseOrganization(VALID_ORG)).toThrow();
75
+ });
76
+
77
+ it("rejects invalid enum values", () => {
78
+ expect(() =>
79
+ parseOrganization({
80
+ ...VALID_ORG_FULL,
81
+ dashboardStatus: "invalid",
82
+ }),
83
+ ).toThrow();
84
+ });
85
+
86
+ it("strips unknown properties", () => {
87
+ const org = parseOrganization({
88
+ ...VALID_ORG_FULL,
89
+ unknownField: "should be stripped",
90
+ });
91
+ expect(org).not.toHaveProperty("unknownField");
92
+ });
93
+
94
+ it("parses without members when excluded", () => {
95
+ const org = parseOrganization(
96
+ { ...VALID_ORG, features: ["customLogo"] },
97
+ { includeMembers: false },
98
+ );
99
+ expect(org).not.toHaveProperty("members");
100
+ expect(org.features).toEqual(["customLogo"]);
101
+ expectTypeOf(org).toEqualTypeOf<Organization<false>>();
102
+ });
103
+
104
+ it("parses without features when excluded", () => {
105
+ const org = parseOrganization(
106
+ { ...VALID_ORG, members: [VALID_MEMBER] },
107
+ { includeFeatures: false },
108
+ );
109
+ expect(org).not.toHaveProperty("features");
110
+ expect(org.members).toHaveLength(1);
111
+ expectTypeOf(org).toEqualTypeOf<Organization<true, false>>();
112
+ expectTypeOf(org.members).toEqualTypeOf<OrganizationMember[]>();
113
+ });
114
+
115
+ it("parses base only when both excluded", () => {
116
+ const org = parseOrganization(VALID_ORG, {
117
+ includeMembers: false,
118
+ includeFeatures: false,
119
+ });
120
+ expect(org).not.toHaveProperty("members");
121
+ expect(org).not.toHaveProperty("features");
122
+ expectTypeOf(org).toEqualTypeOf<Organization<false, false>>();
123
+ });
124
+
125
+ it("parses with both members and features", () => {
126
+ const org = parseOrganization(VALID_ORG_FULL, {
127
+ includeMembers: true,
128
+ includeFeatures: true,
129
+ });
130
+ expect(org.members).toHaveLength(1);
131
+ expect(org.features).toEqual(["customLogo"]);
132
+ expectTypeOf(org).toEqualTypeOf<Organization<true, true>>();
133
+ });
134
+ });