import { z } from "zod"; import { ServiceDeclarationSchema } from "../interfaces/services/service"; import { InterfaceDeclarationSchema } from "../interfaces/views/interface"; /** Allowed characters for an app `name`. */ const APP_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/; /** * Internal application discriminator. Sanity-owned singleton apps only; * validated by the schema but excluded from the public `DefineAppInput` type. */ const ApplicationType = z.enum([ "coreApp", "studio", "canvas", "dashboard", "media-library", "system", ]); /** Dock groups an app can place itself into. */ const DockGroup = z.enum(["dock.system", "dock.applications", "dock.user"]); /** * Dock group identifier. The API does not block a user app from declaring a * reserved group (e.g. `dock.system`); priority conventions keep Sanity-owned * apps ahead. * @public */ export type DockGroup = z.output; /** * Runtime-validation schema for `unstable_defineApp`. Validates the full shape * including the internal `applicationType`; the user-facing `DefineAppInput` * type below omits that field. Services land in a later step and extend this * object additively via `.extend(...)`. * @internal */ export const DefineAppInputSchema = z .object({ /** Unique app identifier — matches `/^[a-zA-Z0-9_-]+$/`. */ name: z .string() .regex(APP_NAME_PATTERN, "App `name` must match /^[a-zA-Z0-9_-]+$/"), /** User-facing app title. Wins over studio.config.ts title on merge. */ title: z.string(), /** Optional icon override (path to an SVG). Wins over manifest/studio icon. */ icon: z.string().optional(), /** Dock group to render in. Defaults to `dock.applications` when omitted. */ group: DockGroup.optional(), /** Sort position within the group, ascending. Defaults to `100` when omitted. */ priority: z.number().optional(), /** * App entrypoint module. Defaults to `./src/App.tsx` when omitted. The build * derives the app's navigable `app` view from it (US5). SDK apps only — * setting it on a studio is rejected (studio app views are not yet * implemented; see the refine below). */ entry: z.string().optional(), /** Organization that owns the app — required to run and deploy an SDK app. */ organizationId: z.string().optional(), /** * Views the app exposes (e.g. dock panels). Metadata only — built into * render artifacts and persisted to the application service on deploy, not * into the app manifest. View `name`s must be unique within the app. */ views: z .array(InterfaceDeclarationSchema) .refine( (views) => new Set(views.map((view) => view.name)).size === views.length, "View `name` must be unique within an app", ) .optional(), /** * Background services the app runs (e.g. a `worker` emitting dock badges). * Metadata only — built into worker artifacts and persisted to the * application service on deploy, not into the app manifest. Service `name`s * must be unique within the app. */ services: z .array(ServiceDeclarationSchema) .refine( (services) => new Set(services.map((service) => service.name)).size === services.length, "Service `name` must be unique within an app", ) .optional(), /** * Internal — Sanity-owned singleton apps only. Validated here but excluded * from the public `DefineAppInput` type. * @internal */ applicationType: ApplicationType.optional(), }) .refine( // US5 — studio app views are not implemented yet. A studio that declares // `entry` (the SDK app-view entrypoint) is rejected here rather than silently // generating one; studios keep navigating via their existing render path. The // background-generated studio app view returns in a later iteration (FR-026). (input) => !(input.applicationType === "studio" && input.entry !== undefined), { message: "App views for studios are not implemented yet", path: ["entry"], }, ); /** * 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`). * @public */ export type DefineAppInput = Omit< z.output, "applicationType" >;