Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/workbench-app-singleton.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sanity/workbench-cli': patch
'@sanity/cli-core': patch
---

Thread an internal `isSingleton` flag through `unstable_defineApp` to the deploy command. Hidden from the public `DefineAppInput` type like `applicationType` — Sanity-owned apps set it, user apps never see it.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ describe('parseWorkbenchCliConfig', () => {
expect((config.app as {applicationType?: string}).applicationType).toBe('media-library')
})

test('carries the internal isSingleton flag onto the resolved app', () => {
const app = brandedApp({isSingleton: true, name: 'media', title: 'Media'})

const config = parseWorkbenchCliConfig({app}, APP_DIR)

expect((config.app as {isSingleton?: boolean}).isSingleton).toBe(true)
})

test('rejects an unknown applicationType', () => {
const app = brandedApp({applicationType: 'Studio', name: 'typo', title: 'Typo'})

Expand Down
9 changes: 5 additions & 4 deletions packages/@sanity/cli-core/src/config/cli/workbenchApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ const WORKBENCH_APP_BRAND = Symbol.for('sanity.workbench.defineApp')
* workbench opt-in. Narrows to the shared `app` config plus the workbench-only
* fields a branded result carries: its `name`, the resolved `applicationType`
* (settled by `parseWorkbenchCliConfig` on load, so callers read it instead of
* re-deriving studio-vs-app), dock panel `views`, and background worker
* `services`. The `type` literals match the `DefineAppInput` schema so
* `views`/`services` stay assignable to `DefineAppInput['views' | 'services']`
* downstream.
* re-deriving studio-vs-app), the internal `isSingleton` flag, dock panel
* `views`, and background worker `services`. The `type` literals match the
* `DefineAppInput` schema so `views`/`services` stay assignable to
* `DefineAppInput['views' | 'services']` downstream.
*/
export function isWorkbenchApp(app: CliConfig['app']): app is NonNullable<CliConfig['app']> & {
applicationType?: ApplicationType
isSingleton?: boolean
name: string
services?: {name: string; src: string; type: 'worker'}[]
views?: {name: string; src: string; type: 'panel'}[]
Expand Down
22 changes: 22 additions & 0 deletions packages/@sanity/workbench-cli/src/__tests__/defineApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ describe('DefineAppInputSchema (build-time validation)', () => {
).toBe(false)
})

test('validates the internal isSingleton when present', () => {
const parsed = DefineAppInputSchema.parse({
isSingleton: true,
name: 'media',
organizationId: 'org-1',
title: 'Media',
})
expect(parsed.isSingleton).toBe(true)
expect(
DefineAppInputSchema.safeParse({
isSingleton: 'yes',
name: 'media',
organizationId: 'org-1',
title: 'Media',
}).success,
).toBe(false)
})

test('accepts group and priority, rejecting an unknown group', () => {
const parsed = DefineAppInputSchema.parse({
group: 'dock.system',
Expand Down Expand Up @@ -224,4 +242,8 @@ describe('type surface', () => {
test('does not expose the internal applicationType', () => {
expectTypeOf<DefineAppResult>().not.toHaveProperty('applicationType')
})

test('does not expose the internal isSingleton', () => {
expectTypeOf<DefineAppResult>().not.toHaveProperty('isSingleton')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ describe('getWorkbench', () => {
expect(resolved.views).toHaveLength(1)
expect(resolved.services).toHaveLength(1)
})

test('threads the internal isSingleton flag through to the deploy command', () => {
const app = unstable_defineApp({
entry: './src/App.tsx',
// Sanity-owned apps set the hidden `isSingleton` via the same escape hatch as `applicationType`.
// @ts-expect-error - isSingleton is excluded from the public DefineAppInput
isSingleton: true,
name: 'test-app',
organizationId: 'org-id',
title: 'Test App',
})
expect(getWorkbench({app} as CliConfig)?.isSingleton).toBe(true)
})

test('leaves isSingleton undefined when the app does not set it', () => {
expect(workbench({entry: './src/App.tsx'}).isSingleton).toBeUndefined()
})
})

describe('assertDeployable', () => {
Expand Down
12 changes: 9 additions & 3 deletions packages/@sanity/workbench-cli/src/defineApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export const DefineAppInputSchema = z
group: z.optional(DockGroupSchema),
/** Optional icon override (path to an SVG). Wins over manifest/studio icon. */
icon: z.optional(z.string()),
/** Internal — Sanity-owned singleton apps only; excluded from the public `DefineAppInput`. @internal */
isSingleton: z.optional(z.boolean()),
/** Unique app identifier — must match `APP_NAME_PATTERN`. */
name: z.string().check(z.regex(APP_NAME_PATTERN, 'App `name` must match /^[a-zA-Z0-9_-]+$/')),
/** Organization that owns the app — the workbench runs and deploys against it. */
Expand Down Expand Up @@ -100,11 +102,15 @@ export const DefineAppInputSchema = z

/**
* User-facing input for `unstable_defineApp`. Excludes the internal
* `applicationType` — that field is validated by the schema but is not part of
* the public surface (Sanity-owned apps set it via `@ts-expect-error`).
* `applicationType` and `isSingleton` — both are validated by the schema but
* are not part of the public surface (Sanity-owned apps set them via
* `@ts-expect-error`).
* @public
*/
export type DefineAppInput = Omit<z.output<typeof DefineAppInputSchema>, 'applicationType'>
export type DefineAppInput = Omit<
z.output<typeof DefineAppInputSchema>,
'applicationType' | 'isSingleton'
>

/**
* Nominal brand the CLI discriminates on to enable the workbench build/deploy
Expand Down
3 changes: 3 additions & 0 deletions packages/@sanity/workbench-cli/src/resolveWorkbenchApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface ResolvedWorkbenchApp {
readonly applicationType?: string
/** SDK app-view entrypoint, when declared. */
readonly entry?: string
/** Internal — marks a Sanity-owned singleton app. */
readonly isSingleton?: boolean
}

/**
Expand All @@ -33,6 +35,7 @@ export function resolveWorkbenchApp(
return {
applicationType: app.applicationType,
entry: app.entry,
isSingleton: app.isSingleton,
services: app.services ?? [],
views: app.views ?? [],
}
Expand Down
Loading