From cf55481bab552e27fba316f6597ffd506defffdc Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 2 Jun 2026 10:47:18 +0200 Subject: [PATCH 01/15] feat(cli): build app view artifacts and register them on deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Threads the app's declared views from sanity.cli.ts through to the federation vite plugin (build + dev), so each view's src is built into a render artifact under .sanity/federation/views. On deploy, the views are validated into the application-service payload and logged — the service that stores them doesn't exist yet, so nothing is sent, but a malformed view declaration fails the deploy before the bundle ships. --- .../src/actions/build/getViteConfig.ts | 10 ++++- .../src/config/cli/types/cliConfig.ts | 15 +++++++ .../@sanity/cli/src/actions/build/buildApp.ts | 3 ++ .../cli/src/actions/build/buildStaticFiles.ts | 4 ++ .../cli/src/actions/deploy/deployApp.ts | 23 +++++++++++ .../cli/src/actions/deploy/viewDeployment.ts | 41 +++++++++++++++++++ .../cli/src/actions/dev/getDevServerConfig.ts | 1 + packages/@sanity/cli/src/server/devServer.ts | 4 ++ 8 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 packages/@sanity/cli/src/actions/deploy/viewDeployment.ts diff --git a/packages/@sanity/cli-build/src/actions/build/getViteConfig.ts b/packages/@sanity/cli-build/src/actions/build/getViteConfig.ts index 43a2a63ae..36d144b12 100644 --- a/packages/@sanity/cli-build/src/actions/build/getViteConfig.ts +++ b/packages/@sanity/cli-build/src/actions/build/getViteConfig.ts @@ -7,7 +7,7 @@ import { readPackageJson, type UserViteConfig, } from '@sanity/cli-core' -import {federation as viteFederation} from '@sanity/federation/vite' +import {type ViewArtifact, federation as viteFederation} from '@sanity/federation/vite' import viteReact from '@vitejs/plugin-react' import {type PluginOptions as ReactCompilerConfig} from 'babel-plugin-react-compiler' import debug from 'debug' @@ -95,6 +95,12 @@ interface ViteOptions extends Pick { * Whether or not to enable source maps */ sourceMap?: boolean + + /** + * Views the workbench app declares. Built into render-contract artifacts and + * exposed through module federation as `./views/`. + */ + views?: readonly ViewArtifact[] } /** @@ -120,6 +126,7 @@ export async function getViteConfig(options: ViteOptions): Promise server, // default to `true` when `mode=development` sourceMap = options.mode === 'development', + views, } = options const basePath = normalizeBasePath(rawBasePath) @@ -206,6 +213,7 @@ export async function getViteConfig(options: ViteOptions): Promise studioConfigPath: entries.relativeConfigLocation!, }), pkgJson: await readPackageJson(path.join(cwd, 'package.json')), + views, workDir: cwd, }), ] diff --git a/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts b/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts index 80001a582..4703a03d8 100644 --- a/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts +++ b/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts @@ -39,6 +39,21 @@ export interface CliConfig { priority?: number /** The title of the custom app, as it is seen in Dashboard UI */ title?: string + /** + * Views the app exposes (e.g. dock panels). Each is built into a render + * artifact on `sanity build` and persisted to the application service on + * `sanity deploy`. View types may carry extra attributes that are stored + * alongside `type`/`name`/`src`. + */ + views?: Array<{ + [attribute: string]: unknown + /** View name, unique within the app. */ + name: string + /** Path to the view src file (default-exports `unstable_defineView`). */ + src: string + /** View type — selects the render contract the build generates. */ + type: 'panel' + }> } /** @deprecated Use deployment.autoUpdates */ diff --git a/packages/@sanity/cli/src/actions/build/buildApp.ts b/packages/@sanity/cli/src/actions/build/buildApp.ts index ae3348276..b92012821 100644 --- a/packages/@sanity/cli/src/actions/build/buildApp.ts +++ b/packages/@sanity/cli/src/actions/build/buildApp.ts @@ -43,6 +43,7 @@ interface InternalBuildOptions { sourceMap: boolean stats: boolean unattendedMode: boolean + views: NonNullable['views'] vite: UserViteConfig | undefined workDir: string } @@ -71,6 +72,7 @@ export async function buildApp(options: BuildOptions): Promise { sourceMap: Boolean(flags['source-maps']), stats: flags.stats, unattendedMode: flags.yes, + views: cliConfig && 'app' in cliConfig ? cliConfig.app?.views : undefined, vite: cliConfig.vite, workDir, }) @@ -231,6 +233,7 @@ async function internalBuildApp(options: InternalBuildOptions): Promise { reactCompiler: options.reactCompiler, schemaExtraction: options.schemaExtraction, sourceMap: options.sourceMap, + views: options.views, vite: options.vite, }) diff --git a/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts b/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts index 0b8150d1e..de4a44caa 100644 --- a/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts +++ b/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts @@ -11,6 +11,7 @@ import { writeSanityRuntime, } from '@sanity/cli-build/_internal/build' import {type CliConfig, type UserViteConfig} from '@sanity/cli-core' +import {type ViewArtifact} from '@sanity/federation/vite' import {type PluginOptions as ReactCompilerConfig} from 'babel-plugin-react-compiler' import {build, createBuilder} from 'vite' @@ -47,6 +48,7 @@ interface StaticBuildOptions { reactCompiler?: ReactCompilerConfig schemaExtraction?: CliConfig['schemaExtraction'] sourceMap?: boolean + views?: readonly ViewArtifact[] vite?: UserViteConfig } @@ -72,6 +74,7 @@ export async function buildStaticFiles( reactCompiler, schemaExtraction, sourceMap = false, + views, vite: extendViteConfig, } = options @@ -98,6 +101,7 @@ export async function buildStaticFiles( outputDir, reactCompiler, sourceMap, + views, }) // Apply the user's Vite config so plugins like `@vanilla-extract/vite-plugin` diff --git a/packages/@sanity/cli/src/actions/deploy/deployApp.ts b/packages/@sanity/cli/src/actions/deploy/deployApp.ts index c556a034e..8c8c81a60 100644 --- a/packages/@sanity/cli/src/actions/deploy/deployApp.ts +++ b/packages/@sanity/cli/src/actions/deploy/deployApp.ts @@ -20,6 +20,7 @@ import {createUserApplicationForApp} from './createUserApplicationForApp.js' import {deployDebug} from './deployDebug.js' import {findUserApplicationForApp} from './findUserApplicationForApp.js' import {type DeployAppOptions} from './types.js' +import {buildViewDeploymentPayload} from './viewDeployment.js' /** * Deploy a Sanity application. @@ -137,6 +138,28 @@ export async function deployApp(options: DeployAppOptions) { } } + // Register the app's declared views with the application service. That + // service doesn't exist yet, so validate the payload and log it (no store); + // a malformed view declaration fails the deploy before we ship the bundle. + const declaredViews = cliConfig.app?.views ?? [] + if (declaredViews.length > 0) { + try { + const payload = buildViewDeploymentPayload({ + applicationId: userApplication.id, + views: declaredViews, + }) + output.log( + `Validated ${payload.views.length} view(s) for the application service (not yet persisted):`, + ) + output.log(JSON.stringify(payload, null, 2)) + deployDebug('View deployment payload', payload) + } catch (err) { + const message = getErrorMessage(err) + output.error(`Invalid view declaration: ${message}`, {exit: 1}) + return + } + } + spin = spinner('Deploying...').start() await createDeployment({ applicationId: userApplication.id, diff --git a/packages/@sanity/cli/src/actions/deploy/viewDeployment.ts b/packages/@sanity/cli/src/actions/deploy/viewDeployment.ts new file mode 100644 index 000000000..df51f8061 --- /dev/null +++ b/packages/@sanity/cli/src/actions/deploy/viewDeployment.ts @@ -0,0 +1,41 @@ +import {z} from 'zod' + +/** + * A view record as persisted to the application service: `type`, `name`, `src`, + * plus any view-type-specific attributes (passed through for storage). + */ +const viewRecordSchema = z + .object({ + name: z.string().regex(/^[a-zA-Z0-9_-]+$/, 'View `name` must match /^[a-zA-Z0-9_-]+$/'), + src: z.string(), + type: z.enum(['panel']), + }) + .passthrough() + +/** + * Payload registering an app's views with the application service on deploy. + * + * Phase 1 stub: the service that stores views does not exist yet, so the + * payload is validated and logged only — never sent. Builds the contract the + * application-service endpoint will accept. + */ +export const viewDeploymentPayloadSchema = z.object({ + applicationId: z.string(), + views: z.array(viewRecordSchema), +}) + +export type ViewDeploymentPayload = z.infer + +/** + * Validate an app's declared views into the application-service payload. + * Throws (via Zod) when a view declaration is malformed. + */ +export function buildViewDeploymentPayload(input: { + applicationId: string + views?: ReadonlyArray> +}): ViewDeploymentPayload { + return viewDeploymentPayloadSchema.parse({ + applicationId: input.applicationId, + views: input.views ?? [], + }) +} diff --git a/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts b/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts index 3e769d1f7..fe9d6025b 100644 --- a/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts +++ b/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts @@ -51,5 +51,6 @@ export function getDevServerConfig({ reactStrictMode, staticPath: path.join(workDir, 'static'), typegen: cliConfig?.typegen, + views: cliConfig?.app?.views, } } diff --git a/packages/@sanity/cli/src/server/devServer.ts b/packages/@sanity/cli/src/server/devServer.ts index c9b368bbb..250274dc1 100644 --- a/packages/@sanity/cli/src/server/devServer.ts +++ b/packages/@sanity/cli/src/server/devServer.ts @@ -4,6 +4,7 @@ import { writeSanityRuntime, } from '@sanity/cli-build/_internal/build' import {CliConfig, getCliTelemetry, type UserViteConfig} from '@sanity/cli-core' +import {type ViewArtifact} from '@sanity/federation/vite' import {type PluginOptions as ReactCompilerConfig} from 'babel-plugin-react-compiler' import {type FSWatcher} from 'chokidar' import {createServer, type InlineConfig, type ViteDevServer} from 'vite' @@ -35,6 +36,7 @@ export interface DevServerOptions { projectName?: string schemaExtraction?: CliConfig['schemaExtraction'] typegen?: CliConfig['typegen'] + views?: readonly ViewArtifact[] vite?: UserViteConfig } @@ -59,6 +61,7 @@ export async function startDevServer(options: DevServerOptions): Promise Date: Tue, 2 Jun 2026 08:53:51 +0000 Subject: [PATCH 02/15] chore: update auto-generated changeset for PR #1149 --- .changeset/pr-1149.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/pr-1149.md diff --git a/.changeset/pr-1149.md b/.changeset/pr-1149.md new file mode 100644 index 000000000..b1e3ed3a3 --- /dev/null +++ b/.changeset/pr-1149.md @@ -0,0 +1,7 @@ + +--- +'@sanity/cli-core': minor +'@sanity/cli': minor +--- + +feat(cli): build app view artifacts and register them on deploy \ No newline at end of file From 706af2101ef7906d42b1c26e3493bffd193999e5 Mon Sep 17 00:00:00 2001 From: "squiggler-app[bot]" <265501495+squiggler-app[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:55:03 +0000 Subject: [PATCH 03/15] chore: update auto-generated changeset for PR #1149 --- .changeset/pr-1149.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pr-1149.md b/.changeset/pr-1149.md index b1e3ed3a3..0990bf884 100644 --- a/.changeset/pr-1149.md +++ b/.changeset/pr-1149.md @@ -4,4 +4,4 @@ '@sanity/cli': minor --- -feat(cli): build app view artifacts and register them on deploy \ No newline at end of file +feat(workbench): build app view artifacts and register them on deploy \ No newline at end of file From 02346e5d969904e704d66187bedc36200c9ddd68 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 2 Jun 2026 11:09:19 +0200 Subject: [PATCH 04/15] fix(cli): read app views off the branded defineApp result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `views` is an `unstable_defineApp` field — its source of truth is `DefineAppInput` in @sanity/federation, not the legacy `app` config object on CliConfig. Drop it from the cli-core type and read it through the `isWorkbenchApp` narrowing at each call site (build, dev, deploy), so the field lives in one place. --- .../cli-core/src/config/cli/types/cliConfig.ts | 15 --------------- .../@sanity/cli/src/actions/build/buildApp.ts | 17 +++++++++++------ .../@sanity/cli/src/actions/deploy/deployApp.ts | 5 ++++- .../cli/src/actions/dev/getDevServerConfig.ts | 7 +++++-- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts b/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts index 4703a03d8..80001a582 100644 --- a/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts +++ b/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts @@ -39,21 +39,6 @@ export interface CliConfig { priority?: number /** The title of the custom app, as it is seen in Dashboard UI */ title?: string - /** - * Views the app exposes (e.g. dock panels). Each is built into a render - * artifact on `sanity build` and persisted to the application service on - * `sanity deploy`. View types may carry extra attributes that are stored - * alongside `type`/`name`/`src`. - */ - views?: Array<{ - [attribute: string]: unknown - /** View name, unique within the app. */ - name: string - /** Path to the view src file (default-exports `unstable_defineView`). */ - src: string - /** View type — selects the render contract the build generates. */ - type: 'panel' - }> } /** @deprecated Use deployment.autoUpdates */ diff --git a/packages/@sanity/cli/src/actions/build/buildApp.ts b/packages/@sanity/cli/src/actions/build/buildApp.ts index b92012821..d935241f2 100644 --- a/packages/@sanity/cli/src/actions/build/buildApp.ts +++ b/packages/@sanity/cli/src/actions/build/buildApp.ts @@ -13,7 +13,7 @@ import { UserViteConfig, } from '@sanity/cli-core' import {confirm, logSymbols, spinner, type SpinnerInstance} from '@sanity/cli-core/ux' -import {isWorkbenchApp} from '@sanity/federation' +import {type DefineAppInput, isWorkbenchApp} from '@sanity/federation' import {parse as semverParse} from 'semver' import {getAppId} from '../../util/appId.js' @@ -43,7 +43,7 @@ interface InternalBuildOptions { sourceMap: boolean stats: boolean unattendedMode: boolean - views: NonNullable['views'] + views: DefineAppInput['views'] vite: UserViteConfig | undefined workDir: string } @@ -56,14 +56,19 @@ interface InternalBuildOptions { export async function buildApp(options: BuildOptions): Promise { const {cliConfig, flags, outDir, output, workDir} = options + const app = cliConfig && 'app' in cliConfig ? cliConfig.app : undefined + // `views` lives on `unstable_defineApp`'s result, not the legacy `app` config + // object — read it off the branded app. + const workbenchApp = isWorkbenchApp(app) ? app : undefined + await internalBuildApp({ appId: getAppId(cliConfig), - appTitle: cliConfig && 'app' in cliConfig ? cliConfig.app?.title : undefined, + appTitle: app?.title, autoUpdatesEnabled: options.autoUpdatesEnabled, calledFromDeploy: options.calledFromDeploy, determineBasePath: () => determineBasePath(cliConfig, 'app', output), - entry: cliConfig && 'app' in cliConfig ? cliConfig.app?.entry : undefined, - isWorkbench: isWorkbenchApp(cliConfig && 'app' in cliConfig ? cliConfig.app : undefined), + entry: app?.entry, + isWorkbench: Boolean(workbenchApp), minify: flags.minify, outDir, output, @@ -72,7 +77,7 @@ export async function buildApp(options: BuildOptions): Promise { sourceMap: Boolean(flags['source-maps']), stats: flags.stats, unattendedMode: flags.yes, - views: cliConfig && 'app' in cliConfig ? cliConfig.app?.views : undefined, + views: workbenchApp?.views, vite: cliConfig.vite, workDir, }) diff --git a/packages/@sanity/cli/src/actions/deploy/deployApp.ts b/packages/@sanity/cli/src/actions/deploy/deployApp.ts index 8c8c81a60..fa3685ce4 100644 --- a/packages/@sanity/cli/src/actions/deploy/deployApp.ts +++ b/packages/@sanity/cli/src/actions/deploy/deployApp.ts @@ -5,6 +5,7 @@ import {createGzip} from 'node:zlib' import {CLIError} from '@oclif/core/errors' import {getLocalPackageVersion} from '@sanity/cli-core' import {spinner} from '@sanity/cli-core/ux' +import {isWorkbenchApp} from '@sanity/federation' import {pack} from 'tar-fs' import {createDeployment, updateUserApplication} from '../../services/userApplications.js' @@ -141,7 +142,9 @@ export async function deployApp(options: DeployAppOptions) { // Register the app's declared views with the application service. That // service doesn't exist yet, so validate the payload and log it (no store); // a malformed view declaration fails the deploy before we ship the bundle. - const declaredViews = cliConfig.app?.views ?? [] + // `views` lives on the branded `unstable_defineApp` result, not the legacy + // `app` config object. + const declaredViews = isWorkbenchApp(cliConfig.app) ? (cliConfig.app.views ?? []) : [] if (declaredViews.length > 0) { try { const payload = buildViewDeploymentPayload({ diff --git a/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts b/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts index fe9d6025b..7276c2074 100644 --- a/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts +++ b/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts @@ -36,6 +36,9 @@ export function getDevServerConfig({ const isApp = cliConfig ? determineIsApp(cliConfig) : false const reactStrictMode = resolveReactStrictMode(cliConfig) + // `views` is declared via `unstable_defineApp`, so read it off the branded + // app result rather than the legacy `app` config type. + const app = cliConfig?.app const envBasePath = getSanityEnvVar('BASEPATH', isApp ?? false) if (envBasePath && cliConfig?.project?.basePath) { @@ -46,11 +49,11 @@ export function getDevServerConfig({ return { ...baseConfig, - isWorkbench: isWorkbenchApp(cliConfig?.app), + isWorkbench: isWorkbenchApp(app), reactCompiler: cliConfig && 'reactCompiler' in cliConfig ? cliConfig.reactCompiler : undefined, reactStrictMode, staticPath: path.join(workDir, 'static'), typegen: cliConfig?.typegen, - views: cliConfig?.app?.views, + views: isWorkbenchApp(app) ? app.views : undefined, } } From bb994229555fda3c9307ac818fec9936fc19becf Mon Sep 17 00:00:00 2001 From: "squiggler-app[bot]" <265501495+squiggler-app[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 09:10:07 +0000 Subject: [PATCH 05/15] chore: update auto-generated changeset for PR #1149 --- .changeset/pr-1149.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/pr-1149.md b/.changeset/pr-1149.md index 0990bf884..b41869af1 100644 --- a/.changeset/pr-1149.md +++ b/.changeset/pr-1149.md @@ -1,6 +1,5 @@ --- -'@sanity/cli-core': minor '@sanity/cli': minor --- From 7a6a953f8461d99e93732c99bcde90e914bdc682 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 2 Jun 2026 11:33:42 +0200 Subject: [PATCH 06/15] feat(cli): build view artifacts for workbench studios too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Studio dev already threaded views (shared getDevServerConfig), but studio build dropped them — so a studio opting into unstable_defineApp with views emitted no artifacts. Read views off the branded app and thread them through buildStudio for parity with apps. Views apply to coreApp and studio alike, per each one's cli config. --- .../@sanity/cli/src/actions/build/buildStudio.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/@sanity/cli/src/actions/build/buildStudio.ts b/packages/@sanity/cli/src/actions/build/buildStudio.ts index abc4f0b2a..6def5323f 100644 --- a/packages/@sanity/cli/src/actions/build/buildStudio.ts +++ b/packages/@sanity/cli/src/actions/build/buildStudio.ts @@ -18,7 +18,7 @@ import { UserViteConfig, } from '@sanity/cli-core' import {confirm, logSymbols, select, spinner, type SpinnerInstance} from '@sanity/cli-core/ux' -import {isWorkbenchApp} from '@sanity/federation' +import {type DefineAppInput, isWorkbenchApp} from '@sanity/federation' import {parse as semverParse} from 'semver' import {getAppId} from '../../util/appId.js' @@ -53,6 +53,7 @@ interface InternalBuildOptions { stats: boolean unattendedMode: boolean upgradePackages(options: {packages: [name: string, version: string][]}): Promise + views: DefineAppInput['views'] vite: UserViteConfig | undefined workDir: string } @@ -65,6 +66,11 @@ interface InternalBuildOptions { export async function buildStudio(options: BuildOptions): Promise { const {calledFromDeploy, cliConfig, flags, outDir, output, workDir} = options + // `views` lives on the branded `unstable_defineApp` result — read it off the + // branded app so it's gated on the brand, like the app build. + const app = cliConfig?.app + const workbenchApp = isWorkbenchApp(app) ? app : undefined + const upgradePkgs = async (options: { packages: [name: string, version: string][] }): Promise => { @@ -83,7 +89,7 @@ export async function buildStudio(options: BuildOptions): Promise { calledFromDeploy, determineBasePath: () => determineBasePath(cliConfig, 'studio', output), isApp: determineIsApp(cliConfig), - isWorkbench: isWorkbenchApp(cliConfig?.app), + isWorkbench: Boolean(workbenchApp), minify: Boolean(flags.minify), outDir, output, @@ -94,6 +100,7 @@ export async function buildStudio(options: BuildOptions): Promise { stats: flags.stats, unattendedMode: Boolean(flags.yes), upgradePackages: upgradePkgs, + views: workbenchApp?.views, vite: cliConfig.vite, workDir, }) @@ -121,6 +128,7 @@ async function internalBuildStudio(options: InternalBuildOptions): Promise stats, unattendedMode, upgradePackages, + views, vite, workDir, } = options @@ -303,6 +311,7 @@ async function internalBuildStudio(options: InternalBuildOptions): Promise reactCompiler, schemaExtraction, sourceMap, + views, vite, }) From bf1939a5be2972abc2464a5b9f6c27a47257614c Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 2 Jun 2026 11:45:31 +0200 Subject: [PATCH 07/15] feat(cli): forward app views to the workbench over the dev websocket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So local panels render without a deploy: carry the branded app's views on the dev-server registry entry (alongside the manifest, not inside it — views live in the application service) and include them in the local-applications payload the workbench subscribes to. --- packages/@sanity/cli/src/actions/dev/devServerRegistry.ts | 7 +++++++ .../cli/src/actions/dev/startFederationRegistration.ts | 7 +++++++ .../@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts | 3 ++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts b/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts index 79f4670f9..521a9cb83 100644 --- a/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts +++ b/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts @@ -40,6 +40,13 @@ const devServerManifestSchema = z.extend(workbenchLockSchema, { manifestUpdatedAt: z.optional(z.string()), projectId: z.optional(z.string()), type: z.enum(['coreApp', 'studio']), + /** + * Views the app declares (e.g. dock panels). Carried separately from the + * manifest — views live in the application service, not the manifest — so the + * workbench can render local panels without a deploy. Lenient by design; + * `@sanity/federation` is the authority on the view shape. + */ + views: z.optional(z.array(z.object({name: z.string(), src: z.string(), type: z.string()}))), workDir: z.string(), }) /** diff --git a/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts b/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts index 5057f0202..b57d78dab 100644 --- a/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts +++ b/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts @@ -1,4 +1,5 @@ import {type CliConfig, type Output} from '@sanity/cli-core' +import {isWorkbenchApp} from '@sanity/federation' import {type ViteDevServer} from 'vite' import {checkForDeprecatedAppId, getAppId} from '../../util/appId.js' @@ -37,12 +38,18 @@ export async function startFederationRegistration( const addr = server.httpServer?.address() const appPort = typeof addr === 'object' && addr ? addr.port : server.config.server.port + // Views live on the branded `unstable_defineApp` result. Forward them on the + // registry entry (alongside, not inside, the manifest) so the workbench can + // render local panels without a deploy. + const views = isWorkbenchApp(cliConfig.app) ? cliConfig.app.views : undefined + const registration = registerDevServer({ host: appHost, id: getAppId(cliConfig), port: appPort, projectId: cliConfig?.api?.projectId, type: isApp ? 'coreApp' : 'studio', + views, workDir, }) diff --git a/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts b/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts index 5df86d1ad..e7d978842 100644 --- a/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts +++ b/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts @@ -21,13 +21,14 @@ import {writeWorkbenchRuntime} from './writeWorkbenchRuntime.js' const noop = async () => {} const toApplicationsPayload = (servers: DevServerManifest[]) => ({ - applications: servers.map(({host, id, manifest, port, projectId, type}) => ({ + applications: servers.map(({host, id, manifest, port, projectId, type, views}) => ({ host, id, manifest, port, projectId, type, + views, })), }) From 0d6d1e3ddc1a4aca2c68bb420714c8879f8d45c8 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 2 Jun 2026 12:33:13 +0200 Subject: [PATCH 08/15] fix(cli): resolve federation views API + drop unused view-deploy exports Pin @sanity/federation to the sanity-io/workbench#236 pkg.pr.new preview so the views/ViewArtifact surface resolves at typecheck time, and un-export viewDeploymentPayloadSchema / ViewDeploymentPayload (used only internally) so knip's depcheck passes. --- package.json | 3 ++- packages/@sanity/cli/src/actions/deploy/viewDeployment.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e5e9454d6..9b1c97174 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,8 @@ "node-pty" ], "overrides": { - "@sanity/cli": "workspace:*" + "@sanity/cli": "workspace:*", + "@sanity/federation": "https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091" } } } diff --git a/packages/@sanity/cli/src/actions/deploy/viewDeployment.ts b/packages/@sanity/cli/src/actions/deploy/viewDeployment.ts index df51f8061..7fbf11588 100644 --- a/packages/@sanity/cli/src/actions/deploy/viewDeployment.ts +++ b/packages/@sanity/cli/src/actions/deploy/viewDeployment.ts @@ -19,12 +19,12 @@ const viewRecordSchema = z * payload is validated and logged only — never sent. Builds the contract the * application-service endpoint will accept. */ -export const viewDeploymentPayloadSchema = z.object({ +const viewDeploymentPayloadSchema = z.object({ applicationId: z.string(), views: z.array(viewRecordSchema), }) -export type ViewDeploymentPayload = z.infer +type ViewDeploymentPayload = z.infer /** * Validate an app's declared views into the application-service payload. From 3a07d85fe1b07919a6e4a5de609fcb5ff3b0e7b1 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 2 Jun 2026 13:06:34 +0200 Subject: [PATCH 09/15] feat(cli): re-export unstable_defineView from @sanity/cli So view src files can import it alongside unstable_defineApp until the sanity runtime package surfaces it. Canonical impl stays in @sanity/federation; no prop types re-exported (inferred at the call site). --- packages/@sanity/cli/src/__tests__/exports.test.ts | 2 +- packages/@sanity/cli/src/exports/index.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/@sanity/cli/src/__tests__/exports.test.ts b/packages/@sanity/cli/src/__tests__/exports.test.ts index 0879a0a1a..110887b57 100644 --- a/packages/@sanity/cli/src/__tests__/exports.test.ts +++ b/packages/@sanity/cli/src/__tests__/exports.test.ts @@ -145,7 +145,7 @@ async function getSanityPackageTypeExports() { } // Value exports intentionally added in v6 that the v5 CLI didn't have. -const NEW_EXPORTS = new Set(['unstable_defineApp']) +const NEW_EXPORTS = new Set(['unstable_defineApp', 'unstable_defineView']) test('should match exports of the current cli package', async () => { const oldCliExports = await getSanityPackageExports() diff --git a/packages/@sanity/cli/src/exports/index.ts b/packages/@sanity/cli/src/exports/index.ts index adc1be882..414871596 100644 --- a/packages/@sanity/cli/src/exports/index.ts +++ b/packages/@sanity/cli/src/exports/index.ts @@ -9,4 +9,8 @@ export type {CliConfig, UserViteConfig} from '@sanity/cli-core' // Workbench application extension API. Canonical implementation in // `@sanity/federation`; re-exported here so `sanity/cli` can surface it to // app authors via `import {unstable_defineApp} from 'sanity/cli'`. -export {type DefineAppInput, unstable_defineApp} from '@sanity/federation' +// `unstable_defineView` is the runtime view-authoring helper; its long-term +// home is the `sanity` runtime package, but it's surfaced here for now so view +// src files can `import {unstable_defineView} from '@sanity/cli'`. Component +// props are inferred from the view type, so no prop types are re-exported. +export {type DefineAppInput, unstable_defineApp, unstable_defineView} from '@sanity/federation' From 576edea177d7b9b184b0fb03e71dffcab6175679 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 2 Jun 2026 15:39:16 +0200 Subject: [PATCH 10/15] feat(cli): browser-safe @sanity/cli/runtime entry for view helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Importing unstable_defineView from the main @sanity/cli entry dragged cli-core's Node worker loader into the browser federation bundle and broke view builds. Move the runtime helper to a dedicated @sanity/cli/runtime entry that re-exports only from @sanity/federation — no cli-core/Node imports — so view src files (and the future sanity runtime re-export) stay out of the CLI's dependency graph. defineApp stays config-time on the main entry. --- packages/@sanity/cli/package.json | 4 ++++ packages/@sanity/cli/src/__tests__/exports.test.ts | 2 +- packages/@sanity/cli/src/exports/index.ts | 13 ++++++------- packages/@sanity/cli/src/exports/runtime.ts | 11 +++++++++++ 4 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 packages/@sanity/cli/src/exports/runtime.ts diff --git a/packages/@sanity/cli/package.json b/packages/@sanity/cli/package.json index 5241f7c85..9f1b56388 100644 --- a/packages/@sanity/cli/package.json +++ b/packages/@sanity/cli/package.json @@ -45,6 +45,10 @@ "source": "./src/exports/_internal.ts", "default": "./dist/exports/_internal.js" }, + "./runtime": { + "source": "./src/exports/runtime.ts", + "default": "./dist/exports/runtime.js" + }, "./package.json": "./package.json" }, "publishConfig": { diff --git a/packages/@sanity/cli/src/__tests__/exports.test.ts b/packages/@sanity/cli/src/__tests__/exports.test.ts index 110887b57..0879a0a1a 100644 --- a/packages/@sanity/cli/src/__tests__/exports.test.ts +++ b/packages/@sanity/cli/src/__tests__/exports.test.ts @@ -145,7 +145,7 @@ async function getSanityPackageTypeExports() { } // Value exports intentionally added in v6 that the v5 CLI didn't have. -const NEW_EXPORTS = new Set(['unstable_defineApp', 'unstable_defineView']) +const NEW_EXPORTS = new Set(['unstable_defineApp']) test('should match exports of the current cli package', async () => { const oldCliExports = await getSanityPackageExports() diff --git a/packages/@sanity/cli/src/exports/index.ts b/packages/@sanity/cli/src/exports/index.ts index 414871596..b60c926d8 100644 --- a/packages/@sanity/cli/src/exports/index.ts +++ b/packages/@sanity/cli/src/exports/index.ts @@ -6,11 +6,10 @@ export {type CliClientOptions, getCliClient} from '../util/cliClient.js' export {loadEnv} from '../util/loadEnv.js' export type {CliConfig, UserViteConfig} from '@sanity/cli-core' -// Workbench application extension API. Canonical implementation in -// `@sanity/federation`; re-exported here so `sanity/cli` can surface it to +// Workbench application extension API (config-time). Canonical implementation +// in `@sanity/federation`; re-exported here so `sanity/cli` can surface it to // app authors via `import {unstable_defineApp} from 'sanity/cli'`. -// `unstable_defineView` is the runtime view-authoring helper; its long-term -// home is the `sanity` runtime package, but it's surfaced here for now so view -// src files can `import {unstable_defineView} from '@sanity/cli'`. Component -// props are inferred from the view type, so no prop types are re-exported. -export {type DefineAppInput, unstable_defineApp, unstable_defineView} from '@sanity/federation' +// The runtime helper `unstable_defineView` is NOT here — it bundles to the +// browser, so it lives on the browser-safe `@sanity/cli/runtime` entry to keep +// `cli-core` out of the frontend bundle. +export {type DefineAppInput, unstable_defineApp} from '@sanity/federation' diff --git a/packages/@sanity/cli/src/exports/runtime.ts b/packages/@sanity/cli/src/exports/runtime.ts new file mode 100644 index 000000000..38e001320 --- /dev/null +++ b/packages/@sanity/cli/src/exports/runtime.ts @@ -0,0 +1,11 @@ +// Browser-safe runtime authoring helpers for the workbench extension API. +// +// View/service src files bundle to the browser, so this entry re-exports ONLY +// from `@sanity/federation` (a pure, dependency-light package) — it must never +// import `@sanity/cli-core` or anything Node-only, or it would drag the CLI +// into the frontend bundle. The `sanity` runtime package re-exports from here, +// so the same constraint protects it. +// +// `unstable_defineApp` is config-time (Node) and stays on the main `@sanity/cli` +// entry; only the runtime helpers live here. +export {unstable_defineView} from '@sanity/federation' From d0019db88f2cd7353c3f65271c9a25a4d6c617f0 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Tue, 2 Jun 2026 18:21:33 +0200 Subject: [PATCH 11/15] feat(dev): forward local app interfaces over the dev websocket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workbench renamed the application-surface concept from views to interfaces (one per interface_type, the shape the application service returns). Map each declared view to that local interface shape on the dev-server registry entry — { interface_type, name, entry_point }, with the local dev server as the entry_point — so the workbench parses and renders local panels. Non-local (deployed) entry_points are deferred. --- .../cli/src/actions/dev/devServerRegistry.ts | 17 ++++++++++------- .../actions/dev/startFederationRegistration.ts | 15 ++++++++++++--- .../src/actions/dev/startWorkbenchDevServer.ts | 4 ++-- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts b/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts index 521a9cb83..84d3a44d7 100644 --- a/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts +++ b/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts @@ -30,6 +30,16 @@ const workbenchLockSchema = z.object({ const devServerManifestSchema = z.extend(workbenchLockSchema, { id: z.optional(z.string()), + /** + * Interfaces the app exposes (e.g. dock panels), mapped from the declared + * views with the local dev server as their `entry_point`. Carried separately + * from the manifest — interfaces live in the application service, not the + * manifest — so the workbench can render local panels without a deploy. + * Lenient by design; the workbench is the authority on the interface shape. + */ + interfaces: z.optional( + z.array(z.object({entry_point: z.string(), interface_type: z.string(), name: z.string()})), + ), /** Inlined manifest — either a {@link StudioManifest} or {@link CoreAppManifest}. */ manifest: z.optional(z.union([studioManifestSchema, coreAppManifestSchema])), /** @@ -40,13 +50,6 @@ const devServerManifestSchema = z.extend(workbenchLockSchema, { manifestUpdatedAt: z.optional(z.string()), projectId: z.optional(z.string()), type: z.enum(['coreApp', 'studio']), - /** - * Views the app declares (e.g. dock panels). Carried separately from the - * manifest — views live in the application service, not the manifest — so the - * workbench can render local panels without a deploy. Lenient by design; - * `@sanity/federation` is the authority on the view shape. - */ - views: z.optional(z.array(z.object({name: z.string(), src: z.string(), type: z.string()}))), workDir: z.string(), }) /** diff --git a/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts b/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts index b57d78dab..c256cc772 100644 --- a/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts +++ b/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts @@ -38,18 +38,27 @@ export async function startFederationRegistration( const addr = server.httpServer?.address() const appPort = typeof addr === 'object' && addr ? addr.port : server.config.server.port - // Views live on the branded `unstable_defineApp` result. Forward them on the + // Interfaces live on the branded `unstable_defineApp` result as declared + // views. Map them to the local interface shape — the dev server is the + // `entry_point` the workbench loads each from — and forward them on the // registry entry (alongside, not inside, the manifest) so the workbench can // render local panels without a deploy. - const views = isWorkbenchApp(cliConfig.app) ? cliConfig.app.views : undefined + const entryPoint = `http://${appHost}:${appPort}/mf-manifest.json` + const interfaces = isWorkbenchApp(cliConfig.app) + ? cliConfig.app.views?.map((view) => ({ + entry_point: entryPoint, + interface_type: view.type, + name: view.name, + })) + : undefined const registration = registerDevServer({ host: appHost, id: getAppId(cliConfig), + interfaces, port: appPort, projectId: cliConfig?.api?.projectId, type: isApp ? 'coreApp' : 'studio', - views, workDir, }) diff --git a/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts b/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts index e7d978842..2e2244d41 100644 --- a/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts +++ b/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts @@ -21,14 +21,14 @@ import {writeWorkbenchRuntime} from './writeWorkbenchRuntime.js' const noop = async () => {} const toApplicationsPayload = (servers: DevServerManifest[]) => ({ - applications: servers.map(({host, id, manifest, port, projectId, type, views}) => ({ + applications: servers.map(({host, id, interfaces, manifest, port, projectId, type}) => ({ host, id, + interfaces, manifest, port, projectId, type, - views, })), }) From 33233b6ae5b91d38f29f59158487408812eb1817 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Wed, 3 Jun 2026 13:03:47 +0200 Subject: [PATCH 12/15] chore: refresh lockfile after restacking onto rebased feat/workbench --- pnpm-lock.yaml | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 188fdbe0c..4b1191806 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,7 @@ catalogs: overrides: '@sanity/client': ^7.22.0 '@sanity/cli': workspace:* + '@sanity/federation': https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091 importers: @@ -265,8 +266,8 @@ importers: fixtures/federated-studio: dependencies: '@sanity/federation': - specifier: 0.1.0-alpha.9 - version: 0.1.0-alpha.9(typescript@5.9.3)(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + specifier: https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091 + version: https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091(typescript@5.9.3)(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) react: specifier: ^19.2.5 version: 19.2.5 @@ -537,8 +538,8 @@ importers: specifier: ^6.2.0 version: 6.2.0 '@sanity/federation': - specifier: 0.1.0-alpha.9 - version: 0.1.0-alpha.9(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.41)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + specifier: https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091 + version: https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.41)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) '@sanity/generate-help-url': specifier: ^4.0.0 version: 4.0.0 @@ -826,8 +827,8 @@ importers: specifier: workspace:^ version: link:../cli-core '@sanity/federation': - specifier: 0.1.0-alpha.9 - version: 0.1.0-alpha.9(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.41)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + specifier: https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091 + version: https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.41)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) '@sanity/generate-help-url': specifier: ^4.0.0 version: 4.0.0 @@ -962,8 +963,8 @@ importers: specifier: ^7.22.0 version: 7.22.0 '@sanity/federation': - specifier: 0.1.0-alpha.9 - version: 0.1.0-alpha.9(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.41)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + specifier: https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091 + version: https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.41)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) babel-plugin-react-compiler: specifier: ^1.0.0 version: 1.0.0 @@ -4924,8 +4925,9 @@ packages: engines: {node: '>=20.19 <22 || >=22.12'} hasBin: true - '@sanity/federation@0.1.0-alpha.9': - resolution: {integrity: sha512-mSsRVzqSrniPtg50+hhbO2WE8vzX/PGkI123ysS8xrUECV0m/v/9YH3ha159dcuUxG0JzgjAtj1+gVy+TmqANQ==} + '@sanity/federation@https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091': + resolution: {tarball: https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091} + version: 0.1.0-alpha.9 engines: {node: '>=20.19.1 <22 || >=22.12'} peerDependencies: vite: ^7.0.0 || ^8.0.0 @@ -14543,7 +14545,7 @@ snapshots: - react-native-b4a - supports-color - '@sanity/federation@0.1.0-alpha.9(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.41)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': + '@sanity/federation@https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.41)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': dependencies: '@module-federation/runtime': 2.5.0 '@module-federation/vite': 1.16.0(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.41)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -14556,7 +14558,7 @@ snapshots: - utf-8-validate - vue-tsc - '@sanity/federation@0.1.0-alpha.9(typescript@5.9.3)(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': + '@sanity/federation@https://pkg.pr.new/sanity-io/workbench/@sanity/federation@1a9ececc8238fb63e5a8f7bd21b091c2067c2091(typescript@5.9.3)(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': dependencies: '@module-federation/runtime': 2.5.0 '@module-federation/vite': 1.16.0(typescript@5.9.3)(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) From 71b323be608c4989ac2740ba441eacdc2cffbd35 Mon Sep 17 00:00:00 2001 From: "squiggler-app[bot]" <265501495+squiggler-app[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:04:26 +0000 Subject: [PATCH 13/15] chore: update auto-generated changeset for PR #1149 --- .changeset/pr-1149.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/pr-1149.md b/.changeset/pr-1149.md index b41869af1..9bcf2c9f2 100644 --- a/.changeset/pr-1149.md +++ b/.changeset/pr-1149.md @@ -1,5 +1,6 @@ --- +'@sanity/cli-build': minor '@sanity/cli': minor --- From bb4b8ea1e2ce2e0fc617358a0802209837763c39 Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Wed, 3 Jun 2026 14:12:03 +0200 Subject: [PATCH 14/15] refactor(dev): stop sending interface entry_point; the workbench derives it The workbench now builds each local interface's entry_point from the dev server's host/port (already on the registry entry), so forward interfaces as {interface_type, name} only. --- .../cli/src/actions/dev/devServerRegistry.ts | 13 ++++++------- .../src/actions/dev/startFederationRegistration.ts | 10 ++++------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts b/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts index 84d3a44d7..acb55895e 100644 --- a/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts +++ b/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts @@ -32,14 +32,13 @@ const devServerManifestSchema = z.extend(workbenchLockSchema, { id: z.optional(z.string()), /** * Interfaces the app exposes (e.g. dock panels), mapped from the declared - * views with the local dev server as their `entry_point`. Carried separately - * from the manifest — interfaces live in the application service, not the - * manifest — so the workbench can render local panels without a deploy. - * Lenient by design; the workbench is the authority on the interface shape. + * views. Carried separately from the manifest — interfaces live in the + * application service, not the manifest — so the workbench can render local + * panels without a deploy. The workbench derives each interface's + * `entry_point` from this entry's host/port. Lenient by design; the workbench + * is the authority on the interface shape. */ - interfaces: z.optional( - z.array(z.object({entry_point: z.string(), interface_type: z.string(), name: z.string()})), - ), + interfaces: z.optional(z.array(z.object({interface_type: z.string(), name: z.string()}))), /** Inlined manifest — either a {@link StudioManifest} or {@link CoreAppManifest}. */ manifest: z.optional(z.union([studioManifestSchema, coreAppManifestSchema])), /** diff --git a/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts b/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts index c256cc772..6e3814d43 100644 --- a/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts +++ b/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts @@ -39,14 +39,12 @@ export async function startFederationRegistration( const appPort = typeof addr === 'object' && addr ? addr.port : server.config.server.port // Interfaces live on the branded `unstable_defineApp` result as declared - // views. Map them to the local interface shape — the dev server is the - // `entry_point` the workbench loads each from — and forward them on the - // registry entry (alongside, not inside, the manifest) so the workbench can - // render local panels without a deploy. - const entryPoint = `http://${appHost}:${appPort}/mf-manifest.json` + // views. Forward each as `{interface_type, name}` on the registry entry + // (alongside, not inside, the manifest) so the workbench can render local + // panels without a deploy. The workbench derives each interface's + // `entry_point` from this entry's host/port. const interfaces = isWorkbenchApp(cliConfig.app) ? cliConfig.app.views?.map((view) => ({ - entry_point: entryPoint, interface_type: view.type, name: view.name, })) From 6d9b767bc22e0fc664101484e8eb07b284e06cbe Mon Sep 17 00:00:00 2001 From: Gustav Hansen Date: Wed, 3 Jun 2026 14:21:16 +0200 Subject: [PATCH 15/15] refactor(dev): send interface entry_point as the declared src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entry_point is the view's declared `src` — the raw value, not a resolved URL. The workbench carries it through as-is. --- .../@sanity/cli/src/actions/dev/devServerRegistry.ts | 9 +++++---- .../cli/src/actions/dev/startFederationRegistration.ts | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts b/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts index acb55895e..9bcc28bee 100644 --- a/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts +++ b/packages/@sanity/cli/src/actions/dev/devServerRegistry.ts @@ -34,11 +34,12 @@ const devServerManifestSchema = z.extend(workbenchLockSchema, { * Interfaces the app exposes (e.g. dock panels), mapped from the declared * views. Carried separately from the manifest — interfaces live in the * application service, not the manifest — so the workbench can render local - * panels without a deploy. The workbench derives each interface's - * `entry_point` from this entry's host/port. Lenient by design; the workbench - * is the authority on the interface shape. + * panels without a deploy. `entry_point` is the declared `src`. Lenient by + * design; the workbench is the authority on the interface shape. */ - interfaces: z.optional(z.array(z.object({interface_type: z.string(), name: z.string()}))), + interfaces: z.optional( + z.array(z.object({entry_point: z.string(), interface_type: z.string(), name: z.string()})), + ), /** Inlined manifest — either a {@link StudioManifest} or {@link CoreAppManifest}. */ manifest: z.optional(z.union([studioManifestSchema, coreAppManifestSchema])), /** diff --git a/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts b/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts index 6e3814d43..ba897fa4f 100644 --- a/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts +++ b/packages/@sanity/cli/src/actions/dev/startFederationRegistration.ts @@ -39,12 +39,12 @@ export async function startFederationRegistration( const appPort = typeof addr === 'object' && addr ? addr.port : server.config.server.port // Interfaces live on the branded `unstable_defineApp` result as declared - // views. Forward each as `{interface_type, name}` on the registry entry - // (alongside, not inside, the manifest) so the workbench can render local - // panels without a deploy. The workbench derives each interface's - // `entry_point` from this entry's host/port. + // views. Forward each on the registry entry (alongside, not inside, the + // manifest) so the workbench can render local panels without a deploy. The + // `entry_point` is the declared `src` — the raw value, not a resolved URL. const interfaces = isWorkbenchApp(cliConfig.app) ? cliConfig.app.views?.map((view) => ({ + entry_point: view.src, interface_type: view.type, name: view.name, }))