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

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 (43) hide show
  1. package/dist/{log.js → _chunks-es/index.js} +1 -1
  2. package/dist/_chunks-es/index.js.map +1 -0
  3. package/dist/_internal.d.ts +13 -6
  4. package/dist/_internal.js +19 -3
  5. package/dist/_internal.js.map +1 -1
  6. package/dist/core.d.ts +906 -0
  7. package/dist/core.js +642 -0
  8. package/dist/core.js.map +1 -0
  9. package/package.json +10 -4
  10. package/src/_exports/core.ts +1 -0
  11. package/src/_internal/index.ts +2 -1
  12. package/src/_internal/render.test.ts +56 -1
  13. package/src/_internal/render.ts +51 -24
  14. package/src/core/__tests__/__fixtures__.ts +248 -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 +52 -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/core/media-libraries.test.ts +38 -0
  25. package/src/core/media-libraries.ts +83 -0
  26. package/src/core/organizations.test.ts +134 -0
  27. package/src/core/organizations.ts +115 -0
  28. package/src/core/projects.test.ts +248 -0
  29. package/src/core/projects.ts +114 -0
  30. package/src/core/shared/urls.test.ts +182 -0
  31. package/src/core/shared/urls.ts +128 -0
  32. package/src/core/user-applications/core-app.test.ts +151 -0
  33. package/src/core/user-applications/core-app.ts +57 -0
  34. package/src/core/user-applications/studio.test.ts +928 -0
  35. package/src/core/user-applications/studio.ts +656 -0
  36. package/src/core/user-applications/user-application.test.ts +126 -0
  37. package/src/core/user-applications/user-application.ts +76 -0
  38. package/src/vite-env.d.ts +8 -0
  39. package/dist/log.d.ts +0 -48
  40. package/dist/log.js.map +0 -1
  41. package/src/_exports/log.ts +0 -1
  42. /package/src/{log → core/log}/index.test.ts +0 -0
  43. /package/src/{log → core/log}/index.ts +0 -0
@@ -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/studio";
12
+ export * from "./user-applications/user-application";
@@ -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
+ private 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
+ });
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Organization ID schema, branded for type safety.
5
+ * @public
6
+ */
7
+ export const OrganizationId = z.string().nonempty().brand("OrganizationId");
8
+
9
+ /**
10
+ * Organization ID type, branded for type safety.
11
+ * @public
12
+ */
13
+ export type OrganizationId = z.output<typeof OrganizationId>;
14
+
15
+ /**
16
+ * Validates and brands a string as an OrganizationId.
17
+ * @public
18
+ */
19
+ export function brandOrganizationId(id: string): OrganizationId {
20
+ return OrganizationId.parse(id);
21
+ }
22
+
23
+ const OrganizationMember = z.object({
24
+ sanityUserId: z.string(),
25
+ isCurrentUser: z.boolean(),
26
+ user: z.object({
27
+ id: z.string(),
28
+ displayName: z.string(),
29
+ familyName: z.string(),
30
+ givenName: z.string(),
31
+ middleName: z.string().nullable(),
32
+ imageUrl: z.string().nullable(),
33
+ email: z.string(),
34
+ loginProvider: z.string(),
35
+ }),
36
+ roles: z.array(
37
+ z.object({
38
+ name: z.string(),
39
+ title: z.string(),
40
+ description: z.string().optional(),
41
+ }),
42
+ ),
43
+ });
44
+
45
+ /**
46
+ * @public
47
+ */
48
+ export type OrganizationMember = z.output<typeof OrganizationMember>;
49
+
50
+ /**
51
+ * Organization schema — validates and brands API responses
52
+ * from the `/organizations/:id` endpoint.
53
+ * @public
54
+ */
55
+ export const Organization = z.object({
56
+ id: OrganizationId,
57
+ name: z.string(),
58
+ slug: z.string().nullable(),
59
+ createdAt: z.string(),
60
+ updatedAt: z.string(),
61
+ dashboardStatus: z.enum(["enabled", "disabled"]),
62
+ aiFeaturesStatus: z.enum(["enabled", "disabled"]),
63
+ defaultRoleName: z.string(),
64
+ });
65
+
66
+ /**
67
+ * Represents an organization with optional members and
68
+ * features arrays depending on the generic parameters.
69
+ * - `Organization` — base fields only (default)
70
+ * - `Organization<true>` — includes `members`
71
+ * - `Organization<true, true>` — includes both
72
+ * @public
73
+ */
74
+ export type Organization<
75
+ IncludeMembers extends boolean = true,
76
+ IncludeFeatures extends boolean = true,
77
+ > = z.output<typeof Organization> &
78
+ (IncludeMembers extends true ? { members: OrganizationMember[] } : unknown) &
79
+ (IncludeFeatures extends true ? { features: string[] } : unknown);
80
+
81
+ /**
82
+ * Validates and parses a raw API response into a branded
83
+ * Organization. The options control which schema is used —
84
+ * matching what the API returns based on query params.
85
+ * @public
86
+ */
87
+ export function parseOrganization<
88
+ IncludeMembers extends boolean = true,
89
+ IncludeFeatures extends boolean = true,
90
+ >(
91
+ data: unknown,
92
+ options?: {
93
+ includeMembers?: IncludeMembers;
94
+ includeFeatures?: IncludeFeatures;
95
+ },
96
+ ): Organization<IncludeMembers, IncludeFeatures> {
97
+ const includeMembers = options?.includeMembers ?? true;
98
+ const includeFeatures = options?.includeFeatures ?? true;
99
+
100
+ const extensions = {
101
+ ...(includeMembers && {
102
+ members: z.array(OrganizationMember),
103
+ }),
104
+ ...(includeFeatures && {
105
+ features: z.array(z.string()),
106
+ }),
107
+ };
108
+
109
+ const schema =
110
+ Object.keys(extensions).length > 0
111
+ ? Organization.extend(extensions)
112
+ : Organization;
113
+
114
+ return schema.parse(data) as Organization<IncludeMembers, IncludeFeatures>;
115
+ }
@@ -0,0 +1,248 @@
1
+ import { describe, expect, expectTypeOf, it } from "vitest";
2
+
3
+ import { brandOrganizationId } from "./organizations";
4
+ import type { OrganizationId } from "./organizations";
5
+ import type { Project, ProjectId, ProjectMember } from "./projects";
6
+ import { brandProjectId, parseProject } from "./projects";
7
+
8
+ describe("Project type", () => {
9
+ it("includes members and features by default", () => {
10
+ expectTypeOf<Project>().toHaveProperty("members");
11
+ expectTypeOf<Project>().toHaveProperty("features");
12
+ });
13
+
14
+ it("excludes members when IncludeMembers is false", () => {
15
+ expectTypeOf<Project<false>>().toHaveProperty("features");
16
+ expectTypeOf<
17
+ "members" extends keyof Project<false> ? true : false
18
+ >().toEqualTypeOf<false>();
19
+ });
20
+
21
+ it("excludes features when IncludeFeatures is false", () => {
22
+ expectTypeOf<Project<true, false>>().toHaveProperty("members");
23
+ expectTypeOf<
24
+ "features" extends keyof Project<true, false> ? true : false
25
+ >().toEqualTypeOf<false>();
26
+ });
27
+
28
+ it("excludes both when both are false", () => {
29
+ expectTypeOf<
30
+ "members" extends keyof Project<false, false> ? true : false
31
+ >().toEqualTypeOf<false>();
32
+ expectTypeOf<
33
+ "features" extends keyof Project<false, false> ? true : false
34
+ >().toEqualTypeOf<false>();
35
+ });
36
+
37
+ it("always includes base properties", () => {
38
+ expectTypeOf<Project>().toHaveProperty("id");
39
+ expectTypeOf<Project>().toHaveProperty("displayName");
40
+ expectTypeOf<Project>().toHaveProperty("organizationId");
41
+
42
+ expectTypeOf<Project<false, false>>().toHaveProperty("id");
43
+ expectTypeOf<Project<false, false>>().toHaveProperty("displayName");
44
+ expectTypeOf<Project<false, false>>().toHaveProperty("organizationId");
45
+ });
46
+
47
+ it("has branded id and organizationId types", () => {
48
+ expectTypeOf<Project["id"]>().toEqualTypeOf<ProjectId>();
49
+ expectTypeOf<Project["organizationId"]>().toEqualTypeOf<OrganizationId>();
50
+ });
51
+ });
52
+
53
+ describe("brandProjectId", () => {
54
+ it("brands a valid project id", () => {
55
+ const id = brandProjectId("abc123");
56
+ expect(id).toBe("abc123");
57
+ expectTypeOf(id).toEqualTypeOf<ProjectId>();
58
+ });
59
+
60
+ it("rejects an empty string", () => {
61
+ expect(() => brandProjectId("")).toThrow();
62
+ });
63
+
64
+ it("is not assignable from a plain string", () => {
65
+ expectTypeOf<ProjectId>().branded.toEqualTypeOf<ProjectId>();
66
+ });
67
+
68
+ it("is not assignable from OrganizationId", () => {
69
+ expectTypeOf<
70
+ OrganizationId extends ProjectId ? true : false
71
+ >().toEqualTypeOf<false>();
72
+ });
73
+ });
74
+
75
+ describe("parseProject", () => {
76
+ const VALID_PROJECT = {
77
+ id: "proj123",
78
+ displayName: "My Project",
79
+ studioHost: "my-project",
80
+ organizationId: brandOrganizationId("org123"),
81
+ metadata: {
82
+ integration: "cli",
83
+ color: "#ff0000",
84
+ initialTemplate: "blog",
85
+ cliInitializedAt: "2024-01-01T00:00:00Z",
86
+ },
87
+ isBlocked: false,
88
+ isDisabled: false,
89
+ isDisabledByUser: false,
90
+ activityFeedEnabled: true,
91
+ createdAt: "2024-01-01T00:00:00Z",
92
+ updatedAt: "2024-01-01T00:00:00Z",
93
+ };
94
+
95
+ const VALID_MEMBER = {
96
+ id: "member1",
97
+ createdAt: "2024-01-01T00:00:00Z",
98
+ updatedAt: "2024-01-01T00:00:00Z",
99
+ isCurrentUser: true,
100
+ isRobot: false,
101
+ roles: [{ name: "admin", title: "Admin", description: "Full access" }],
102
+ };
103
+
104
+ const VALID_PROJECT_FULL = {
105
+ ...VALID_PROJECT,
106
+ members: [VALID_MEMBER],
107
+ features: ["customDomain"],
108
+ };
109
+
110
+ it("parses a valid project with members and features by default", () => {
111
+ const project = parseProject(VALID_PROJECT_FULL);
112
+ expect(project.displayName).toBe("My Project");
113
+ expect(project.members).toHaveLength(1);
114
+ expect(project.features).toEqual(["customDomain"]);
115
+ expectTypeOf(project).toEqualTypeOf<Project>();
116
+ expectTypeOf(project.id).toEqualTypeOf<ProjectId>();
117
+ expectTypeOf(project.organizationId).toEqualTypeOf<OrganizationId>();
118
+ });
119
+
120
+ it("rejects missing required fields", () => {
121
+ expect(() => parseProject({ id: "proj123" })).toThrow();
122
+ });
123
+
124
+ it("rejects missing members by default", () => {
125
+ expect(() => parseProject(VALID_PROJECT)).toThrow();
126
+ });
127
+
128
+ it("strips unknown properties", () => {
129
+ const project = parseProject({
130
+ ...VALID_PROJECT_FULL,
131
+ unknownField: "should be stripped",
132
+ });
133
+ expect(project).not.toHaveProperty("unknownField");
134
+ });
135
+
136
+ it("parses without members when excluded", () => {
137
+ const project = parseProject(
138
+ { ...VALID_PROJECT, features: ["customDomain"] },
139
+ { includeMembers: false },
140
+ );
141
+ expect(project).not.toHaveProperty("members");
142
+ expect(project.features).toEqual(["customDomain"]);
143
+ expectTypeOf(project).toEqualTypeOf<Project<false>>();
144
+ });
145
+
146
+ it("parses without features when excluded", () => {
147
+ const project = parseProject(
148
+ { ...VALID_PROJECT, members: [VALID_MEMBER] },
149
+ { includeFeatures: false },
150
+ );
151
+ expect(project).not.toHaveProperty("features");
152
+ expect(project.members).toHaveLength(1);
153
+ expectTypeOf(project).toEqualTypeOf<Project<true, false>>();
154
+ expectTypeOf(project.members).toEqualTypeOf<ProjectMember[]>();
155
+ });
156
+
157
+ it("parses base only when both excluded", () => {
158
+ const project = parseProject(VALID_PROJECT, {
159
+ includeMembers: false,
160
+ includeFeatures: false,
161
+ });
162
+ expect(project).not.toHaveProperty("members");
163
+ expect(project).not.toHaveProperty("features");
164
+ expectTypeOf(project).toEqualTypeOf<Project<false, false>>();
165
+ });
166
+
167
+ it("parses with both members and features", () => {
168
+ const project = parseProject(VALID_PROJECT_FULL, {
169
+ includeMembers: true,
170
+ includeFeatures: true,
171
+ });
172
+ expect(project.members).toHaveLength(1);
173
+ expect(project.features).toEqual(["customDomain"]);
174
+ expectTypeOf(project).toEqualTypeOf<Project<true, true>>();
175
+ });
176
+
177
+ describe("metadata fields are optional", () => {
178
+ const BASE = {
179
+ ...VALID_PROJECT,
180
+ members: [VALID_MEMBER],
181
+ features: ["customDomain"],
182
+ };
183
+
184
+ it("parses with only the integration field", () => {
185
+ const project = parseProject({
186
+ ...BASE,
187
+ metadata: { integration: "cli" },
188
+ });
189
+ expect(project.metadata).toEqual({ integration: "cli" });
190
+ });
191
+
192
+ describe("metadata.integration", () => {
193
+ it('accepts "cli"', () => {
194
+ const project = parseProject({
195
+ ...BASE,
196
+ metadata: { integration: "cli" },
197
+ });
198
+ expect(project.metadata.integration).toBe("cli");
199
+ });
200
+
201
+ it('accepts "manage"', () => {
202
+ const project = parseProject({
203
+ ...BASE,
204
+ metadata: { integration: "manage" },
205
+ });
206
+ expect(project.metadata.integration).toBe("manage");
207
+ });
208
+
209
+ it("rejects an unknown value", () => {
210
+ expect(() =>
211
+ parseProject({
212
+ ...BASE,
213
+ metadata: { integration: "unknown" },
214
+ }),
215
+ ).toThrow();
216
+ });
217
+
218
+ it("rejects a missing integration field", () => {
219
+ expect(() =>
220
+ parseProject({ ...BASE, metadata: { color: "#ff0000" } }),
221
+ ).toThrow();
222
+ });
223
+ });
224
+
225
+ it("parses without metadata.color", () => {
226
+ const { color: _color, ...rest } = BASE.metadata;
227
+ const project = parseProject({ ...BASE, metadata: rest });
228
+ expect(project.metadata).not.toHaveProperty("color");
229
+ });
230
+
231
+ it("parses without metadata.externalStudioHost", () => {
232
+ const project = parseProject({ ...BASE, metadata: BASE.metadata });
233
+ expect(project.metadata).not.toHaveProperty("externalStudioHost");
234
+ });
235
+
236
+ it("parses without metadata.initialTemplate", () => {
237
+ const { initialTemplate: _initialTemplate, ...rest } = BASE.metadata;
238
+ const project = parseProject({ ...BASE, metadata: rest });
239
+ expect(project.metadata).not.toHaveProperty("initialTemplate");
240
+ });
241
+
242
+ it("parses without metadata.cliInitializedAt", () => {
243
+ const { cliInitializedAt: _cliInitializedAt, ...rest } = BASE.metadata;
244
+ const project = parseProject({ ...BASE, metadata: rest });
245
+ expect(project.metadata).not.toHaveProperty("cliInitializedAt");
246
+ });
247
+ });
248
+ });