diff --git a/.changeset/pr-1143.md b/.changeset/pr-1143.md new file mode 100644 index 000000000..75890513e --- /dev/null +++ b/.changeset/pr-1143.md @@ -0,0 +1,6 @@ +--- +'@sanity/cli-core': minor +'@sanity/cli': minor +--- + +Add `unstable_defineApp`, exported from `sanity/cli`, as the opt-in for workbench apps. Calling it in `sanity.cli.ts` enables workbench for a studio or SDK app — the previous experimental `federation: { enabled }` config flag is removed. diff --git a/fixtures/federated-studio/package.json b/fixtures/federated-studio/package.json index d3d18cb18..58c615976 100644 --- a/fixtures/federated-studio/package.json +++ b/fixtures/federated-studio/package.json @@ -12,6 +12,7 @@ "build:fixture": "sanity build" }, "dependencies": { + "@sanity/federation": "0.1.0-alpha.9", "react": "^19.2.5", "react-dom": "^19.2.5", "sanity": "catalog:", diff --git a/fixtures/federated-studio/sanity.cli.ts b/fixtures/federated-studio/sanity.cli.ts index 2ab1cd728..d9bccb303 100644 --- a/fixtures/federated-studio/sanity.cli.ts +++ b/fixtures/federated-studio/sanity.cli.ts @@ -1,3 +1,4 @@ +import {unstable_defineApp} from '@sanity/federation' import {defineCliConfig} from 'sanity/cli' export default defineCliConfig({ @@ -5,10 +6,12 @@ export default defineCliConfig({ dataset: 'test', projectId: 'ppsg7ml5', }, + // Calling `unstable_defineApp` opts this studio into workbench (a + // `sanity.config.ts` is present, so it resolves to `applicationType: 'studio'`). + app: unstable_defineApp({ + name: 'federated-studio', + }), deployment: { autoUpdates: true, }, - federation: { - enabled: true, - }, }) diff --git a/packages/@sanity/cli-core/package.json b/packages/@sanity/cli-core/package.json index 91d2fdca1..dea754b58 100644 --- a/packages/@sanity/cli-core/package.json +++ b/packages/@sanity/cli-core/package.json @@ -66,6 +66,7 @@ "@inquirer/prompts": "^8.3.0", "@oclif/core": "catalog:", "@sanity/client": "catalog:", + "@sanity/federation": "0.1.0-alpha.9", "babel-plugin-react-compiler": "^1.0.0", "boxen": "^8.0.1", "debug": "catalog:", diff --git a/packages/@sanity/cli-core/src/config/__tests__/getCliConfig.test.ts b/packages/@sanity/cli-core/src/config/__tests__/getCliConfig.test.ts index 0dcb347cb..a757dd906 100644 --- a/packages/@sanity/cli-core/src/config/__tests__/getCliConfig.test.ts +++ b/packages/@sanity/cli-core/src/config/__tests__/getCliConfig.test.ts @@ -12,6 +12,12 @@ vi.mock('../util/findConfigsPaths.js', () => ({ })) const ROOT = '/mock/project' +const BRAND = Symbol.for('sanity.workbench.defineApp') + +/** Mimics what `unstable_defineApp` returns: the input plus the brand. */ +function brandedApp(input: Record) { + return Object.defineProperty({...input}, BRAND, {enumerable: false, value: true}) +} function setupSingleConfig(configPath = `${ROOT}/sanity.cli.ts`) { mockFindPathForFiles.mockResolvedValue([ @@ -70,6 +76,21 @@ describe('getCliConfig', () => { await expect(getCliConfig(ROOT)).rejects.toThrow('CLI config cannot be loaded') }) + test('routes a branded app through the workbench loader', async () => { + const getCliConfig = await freshImport() + setupSingleConfig() + const app = brandedApp({name: 'drop-desk', title: 'Drop Desk'}) + mockImportModule.mockResolvedValue({api: {projectId: 'abc'}, app}) + + const config = await getCliConfig(ROOT) + + // `/mock/project` has no `sanity.config.*`, so it resolves to a core app — + // proving the branch ran parseWorkbenchCliConfig and kept the brand. + expect((config.app as {applicationType?: string}).applicationType).toBe('coreApp') + expect(BRAND in (config.app as object)).toBe(true) + expect('applicationType' in app).toBe(false) + }) + test('throws on schema validation failure', async () => { const getCliConfig = await freshImport() setupSingleConfig() diff --git a/packages/@sanity/cli-core/src/config/__tests__/getCliConfigSync.test.ts b/packages/@sanity/cli-core/src/config/__tests__/getCliConfigSync.test.ts index 97115c6e8..29840bc63 100644 --- a/packages/@sanity/cli-core/src/config/__tests__/getCliConfigSync.test.ts +++ b/packages/@sanity/cli-core/src/config/__tests__/getCliConfigSync.test.ts @@ -1,3 +1,7 @@ +import {mkdtempSync, rmSync, writeFileSync} from 'node:fs' +import {tmpdir} from 'node:os' +import {join} from 'node:path' + import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' import {getCliConfigSync} from '../cli/getCliConfigSync' @@ -37,4 +41,35 @@ describe('getCliConfigSync', () => { expect(() => getCliConfigSync(mockRootPath)).toThrow('Multiple CLI config files found') }) + + test('routes a branded app through the workbench loader', async () => { + const {existsSync} = await import('node:fs') + const realFs = await vi.importActual('node:fs') + vi.mocked(existsSync).mockImplementation((path) => realFs.existsSync(path)) + + // A self-contained config: brand the app via the global `Symbol.for` the + // same way `unstable_defineApp` does, so it needs no external import. + const dir = mkdtempSync(join(tmpdir(), 'cli-sync-cfg-')) + writeFileSync( + join(dir, 'sanity.cli.ts'), + [ + `const app = {name: 'drop-desk', title: 'Drop Desk'}`, + `Object.defineProperty(app, Symbol.for('sanity.workbench.defineApp'), {`, + ` enumerable: false, value: true,`, + `})`, + `export default {api: {projectId: 'abc'}, app}`, + ].join('\n'), + ) + + try { + const config = getCliConfigSync(dir) + + // No `sanity.config.*` in the temp dir, so it resolves to a core app — + // proving the branch ran parseWorkbenchCliConfig and kept the brand. + expect((config.app as {applicationType?: string}).applicationType).toBe('coreApp') + expect(Symbol.for('sanity.workbench.defineApp') in (config.app as object)).toBe(true) + } finally { + rmSync(dir, {force: true, recursive: true}) + } + }) }) diff --git a/packages/@sanity/cli-core/src/config/cli/__tests__/workbenchApp.test.ts b/packages/@sanity/cli-core/src/config/cli/__tests__/workbenchApp.test.ts new file mode 100644 index 000000000..8b0decc88 --- /dev/null +++ b/packages/@sanity/cli-core/src/config/cli/__tests__/workbenchApp.test.ts @@ -0,0 +1,64 @@ +import {tmpdir} from 'node:os' +import {join} from 'node:path' + +import {describe, expect, test} from 'vitest' + +import {parseWorkbenchCliConfig} from '../workbenchApp' + +const BRAND = Symbol.for('sanity.workbench.defineApp') +// A dir with no `sanity.config.*`, so detection resolves to a core app. +const APP_DIR = tmpdir() + +/** Mimics what `unstable_defineApp` returns: the input plus the brand. */ +function brandedApp(input: Record) { + return Object.defineProperty({...input}, BRAND, {enumerable: false, value: true}) +} + +describe('parseWorkbenchCliConfig', () => { + test('keeps the identity fields and the brand on the resolved app', () => { + const app = brandedApp({ + entry: './src/App.tsx', + name: 'drop-desk', + organizationId: 'o1', + title: 'Drop Desk', + }) + + const config = parseWorkbenchCliConfig({app, server: {port: 3333}}, APP_DIR) + + expect((config.app as {name?: string}).name).toBe('drop-desk') + expect(BRAND in (config.app as object)).toBe(true) + }) + + test('resolves applicationType onto a clone without mutating the caller', () => { + const app = brandedApp({name: 'drop-desk', title: 'Drop Desk'}) + + const config = parseWorkbenchCliConfig({app}, APP_DIR) + + // Caller's object is untouched; the resolved value lives on the returned clone. + expect('applicationType' in app).toBe(false) + expect(config.app).not.toBe(app) + expect((config.app as {applicationType?: string}).applicationType).toBe('coreApp') + }) + + test('keeps an explicit applicationType (no detection)', () => { + const app = brandedApp({applicationType: 'media-library', name: 'media', title: 'Media'}) + + const config = parseWorkbenchCliConfig({app}, join(APP_DIR, 'nope')) + + expect((config.app as {applicationType?: string}).applicationType).toBe('media-library') + }) + + test('rejects an unknown applicationType', () => { + const app = brandedApp({applicationType: 'Studio', name: 'typo', title: 'Typo'}) + + expect(() => parseWorkbenchCliConfig({app}, APP_DIR)).toThrow(/Invalid `applicationType`/) + }) + + test('still validates the non-app fields', () => { + const app = brandedApp({name: 'drop-desk', title: 'Drop Desk'}) + + expect(() => parseWorkbenchCliConfig({app, server: {port: 'nope'}}, APP_DIR)).toThrow( + /Invalid CLI config/, + ) + }) +}) diff --git a/packages/@sanity/cli-core/src/config/cli/getCliConfig.ts b/packages/@sanity/cli-core/src/config/cli/getCliConfig.ts index 40339da6e..75e4cc64a 100644 --- a/packages/@sanity/cli-core/src/config/cli/getCliConfig.ts +++ b/packages/@sanity/cli-core/src/config/cli/getCliConfig.ts @@ -1,4 +1,7 @@ import {createRequire} from 'node:module' +import {dirname} from 'node:path' + +import {isWorkbenchApp} from '@sanity/federation' import {debug} from '../../debug.js' import {NotFoundError} from '../../errors/NotFoundError.js' @@ -6,6 +9,7 @@ import {importModule} from '../../util/importModule.js' import {findPathForFiles} from '../util/findConfigsPaths.js' import {cliConfigSchema} from './schemas.js' import {type CliConfig} from './types/cliConfig.js' +import {parseWorkbenchCliConfig} from './workbenchApp.js' const cache = new Map>() @@ -97,6 +101,12 @@ export async function getCliConfigUncached(rootPath: string): Promise throw new Error('CLI config cannot be loaded', {cause: err}) } + // Branch as early as possible: a branded `unstable_defineApp(...)` opts into + // workbench behavior, so its `app` skips the legacy `app` schema entirely. + if (isWorkbenchApp(cliConfig?.app)) { + return parseWorkbenchCliConfig(cliConfig, dirname(configPath)) + } + const {data, error, success} = cliConfigSchema.safeParse(cliConfig) if (!success) { debug(`Invalid CLI config: ${error.message}`) diff --git a/packages/@sanity/cli-core/src/config/cli/getCliConfigSync.ts b/packages/@sanity/cli-core/src/config/cli/getCliConfigSync.ts index d5f02e1a5..967c12f31 100644 --- a/packages/@sanity/cli-core/src/config/cli/getCliConfigSync.ts +++ b/packages/@sanity/cli-core/src/config/cli/getCliConfigSync.ts @@ -2,12 +2,14 @@ import {existsSync} from 'node:fs' import {createRequire} from 'node:module' import {join} from 'node:path' +import {isWorkbenchApp} from '@sanity/federation' import {register} from 'tsx/esm/api' import {NotFoundError} from '../../errors/NotFoundError.js' import {tryGetDefaultExport} from '../../util/tryGetDefaultExport.js' import {cliConfigSchema} from './schemas.js' import {type CliConfig} from './types/cliConfig.js' +import {parseWorkbenchCliConfig} from './workbenchApp.js' /** * Get the CLI config for a project synchronously, given the root path. @@ -47,6 +49,12 @@ export function getCliConfigSync(rootPath: string): CliConfig { unregister() } + // Branch as early as possible: a branded `unstable_defineApp(...)` opts into + // workbench behavior, so its `app` skips the legacy `app` schema entirely. + if (isWorkbenchApp(cliConfig?.app)) { + return parseWorkbenchCliConfig(cliConfig, rootPath) + } + const {data, error, success} = cliConfigSchema.safeParse(cliConfig) if (!success) { throw new Error(`Invalid CLI config: ${error.message}`) diff --git a/packages/@sanity/cli-core/src/config/cli/schemas.ts b/packages/@sanity/cli-core/src/config/cli/schemas.ts index bf2b9d43e..f9bfb031a 100644 --- a/packages/@sanity/cli-core/src/config/cli/schemas.ts +++ b/packages/@sanity/cli-core/src/config/cli/schemas.ts @@ -34,12 +34,6 @@ export const cliConfigSchema = z.object({ }), ), - federation: z.optional( - z.object({ - enabled: z.boolean(), - }), - ), - graphql: z.optional( z.array( z.object({ 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 3a76c4389..b823c078e 100644 --- a/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts +++ b/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts @@ -59,14 +59,6 @@ export interface CliConfig { autoUpdates?: boolean } - /** - * Enable federated builds & dev environments for your studio or application. - * @experimental - */ - federation?: { - enabled: boolean - } - /** Define the GraphQL APIs that the CLI can deploy and interact with */ graphql?: Array<{ filterSuffix?: string diff --git a/packages/@sanity/cli-core/src/config/cli/workbenchApp.ts b/packages/@sanity/cli-core/src/config/cli/workbenchApp.ts new file mode 100644 index 000000000..29e26f41d --- /dev/null +++ b/packages/@sanity/cli-core/src/config/cli/workbenchApp.ts @@ -0,0 +1,83 @@ +import {existsSync} from 'node:fs' +import {join} from 'node:path' + +import {cliConfigSchema} from './schemas.js' +import {type CliConfig} from './types/cliConfig.js' + +const STUDIO_CONFIG_FILES = [ + 'sanity.config.ts', + 'sanity.config.tsx', + 'sanity.config.js', + 'sanity.config.jsx', + 'sanity.config.mjs', + 'sanity.config.cjs', +] + +// Mirrors `@sanity/federation`'s `ApplicationType` enum. `unstable_defineApp` +// is a pure identity wrapper that doesn't validate its input, so the loader is +// the first place an explicit `applicationType` can be checked. +const APPLICATION_TYPES = [ + 'coreApp', + 'studio', + 'canvas', + 'dashboard', + 'media-library', + 'system', +] as const +type ApplicationType = (typeof APPLICATION_TYPES)[number] + +function isApplicationType(value: unknown): value is ApplicationType { + return typeof value === 'string' && (APPLICATION_TYPES as readonly string[]).includes(value) +} + +/** + * Infer the application type for a workbench app when `unstable_defineApp` + * didn't set one: a project with a `sanity.config.*` is a studio, otherwise a + * core (SDK) app. An explicit `applicationType` always wins. + */ +function detectApplicationType(projectDir: string): ApplicationType { + return STUDIO_CONFIG_FILES.some((file) => existsSync(join(projectDir, file))) + ? 'studio' + : 'coreApp' +} + +/** + * Parse a config whose `app` is a branded `unstable_defineApp(...)` result. + * The branded `app` bypasses the legacy `app` object schema (which would strip + * its identity fields and the brand symbol); every other field is still + * validated. The brand is preserved so downstream code relies on the + * `isWorkbenchApp` identity (from `@sanity/federation`) instead of a flag. + * + * Resolves `applicationType` here — as early as possible — so studio-vs-app + * classification is settled once and read off the app everywhere else. The + * resolved value lands on a clone, never the caller's object, so re-parsing the + * same `app` for a different directory can't inherit a stale inference. + */ +export function parseWorkbenchCliConfig(cliConfig: unknown, projectDir: string): CliConfig { + const {app, ...rest} = cliConfig as Record & { + app: Record & {applicationType?: string} + } + const {data, error, success} = cliConfigSchema.safeParse(rest) + if (!success) { + throw new Error(`Invalid CLI config: ${error.message}`, {cause: error}) + } + + const explicit = app.applicationType + if (explicit !== undefined && !isApplicationType(explicit)) { + throw new Error( + `Invalid \`applicationType\` "${explicit}" in \`unstable_defineApp\` — expected one of: ${APPLICATION_TYPES.join(', ')}`, + ) + } + const applicationType = explicit ?? detectApplicationType(projectDir) + + // Clone the branded app rather than mutating the caller's object. Copying own + // property descriptors carries over the non-enumerable `unstable_defineApp` + // brand, which a spread would drop. + const resolvedApp = Object.defineProperties({}, Object.getOwnPropertyDescriptors(app)) as Record< + string, + unknown + > + resolvedApp.applicationType = applicationType + + return {...data, app: resolvedApp} as CliConfig +} diff --git a/packages/@sanity/cli-e2e/__tests__/init/init.app.test.ts b/packages/@sanity/cli-e2e/__tests__/init/init.app.test.ts index 601e8de78..15c24928f 100644 --- a/packages/@sanity/cli-e2e/__tests__/init/init.app.test.ts +++ b/packages/@sanity/cli-e2e/__tests__/init/init.app.test.ts @@ -100,7 +100,6 @@ describe('sanity init - app', {timeout: 120_000}, () => { tmp.path, '--no-git', '--no-mcp', - '--no-federation', ], interactive: true, }) diff --git a/packages/@sanity/cli-e2e/__tests__/init/init.studio-interactive.test.ts b/packages/@sanity/cli-e2e/__tests__/init/init.studio-interactive.test.ts index c8999164a..a8425574b 100644 --- a/packages/@sanity/cli-e2e/__tests__/init/init.studio-interactive.test.ts +++ b/packages/@sanity/cli-e2e/__tests__/init/init.studio-interactive.test.ts @@ -60,7 +60,6 @@ describe('sanity init - studio (interactive)', {timeout: 120_000}, () => { 'pnpm', '--no-mcp', '--no-git', - '--no-federation', ], interactive: true, }) @@ -91,7 +90,6 @@ describe('sanity init - studio (interactive)', {timeout: 120_000}, () => { tmp.path, '--no-mcp', '--no-git', - '--no-federation', ], interactive: true, }) @@ -131,7 +129,6 @@ describe('sanity init - studio (interactive)', {timeout: 120_000}, () => { '--typescript', '--no-mcp', '--no-git', - '--no-federation', ], interactive: true, }) @@ -162,7 +159,6 @@ describe('sanity init - studio (interactive)', {timeout: 120_000}, () => { '--package-manager', 'pnpm', '--no-git', - '--no-federation', ], interactive: true, }) @@ -195,7 +191,6 @@ describe('sanity init - studio (interactive)', {timeout: 120_000}, () => { '--package-manager', 'pnpm', '--no-git', - '--no-federation', ], interactive: true, }) diff --git a/packages/@sanity/cli/package.json b/packages/@sanity/cli/package.json index 5846350c3..268ef7254 100644 --- a/packages/@sanity/cli/package.json +++ b/packages/@sanity/cli/package.json @@ -80,7 +80,7 @@ "@sanity/codegen": "catalog:", "@sanity/descriptors": "^1.3.0", "@sanity/export": "^6.1.0", - "@sanity/federation": "0.1.0-alpha.8", + "@sanity/federation": "0.1.0-alpha.9", "@sanity/generate-help-url": "^4.0.0", "@sanity/id-utils": "^1.0.0", "@sanity/import": "^6.0.1", diff --git a/packages/@sanity/cli/src/__tests__/exports.test.ts b/packages/@sanity/cli/src/__tests__/exports.test.ts index b02f3a377..0879a0a1a 100644 --- a/packages/@sanity/cli/src/__tests__/exports.test.ts +++ b/packages/@sanity/cli/src/__tests__/exports.test.ts @@ -144,12 +144,17 @@ async function getSanityPackageTypeExports() { return exportedTypes } +// Value exports intentionally added in v6 that the v5 CLI didn't have. +const NEW_EXPORTS = new Set(['unstable_defineApp']) + test('should match exports of the current cli package', async () => { const oldCliExports = await getSanityPackageExports() - expect(Object.keys(newExports).toSorted()).toStrictEqual( - Object.keys(oldCliExports.default).toSorted(), - ) + expect( + Object.keys(newExports) + .filter((name) => !NEW_EXPORTS.has(name)) + .toSorted(), + ).toStrictEqual(Object.keys(oldCliExports.default).toSorted()) }) test('should include type exports of the old (v5) cli package', async () => { diff --git a/packages/@sanity/cli/src/actions/build/__tests__/buildStaticFiles.test.ts b/packages/@sanity/cli/src/actions/build/__tests__/buildStaticFiles.test.ts index b1c3e1a50..cd659c5da 100644 --- a/packages/@sanity/cli/src/actions/build/__tests__/buildStaticFiles.test.ts +++ b/packages/@sanity/cli/src/actions/build/__tests__/buildStaticFiles.test.ts @@ -72,7 +72,7 @@ describe('buildStaticFiles', () => { await buildStaticFiles({ basePath: '/', cwd, - federation: {enabled: true}, + isWorkbench: true, outputDir, vite: userVite, }) @@ -99,7 +99,7 @@ describe('buildStaticFiles', () => { await buildStaticFiles({ basePath: '/', cwd, - federation: {enabled: true}, + isWorkbench: true, outputDir, }) @@ -111,7 +111,7 @@ describe('buildStaticFiles', () => { await buildStaticFiles({ basePath: '/', cwd, - federation: {enabled: true}, + isWorkbench: true, outputDir, }) diff --git a/packages/@sanity/cli/src/actions/build/__tests__/getViteConfig.test.ts b/packages/@sanity/cli/src/actions/build/__tests__/getViteConfig.test.ts index 8eb31a53e..eaea529e0 100644 --- a/packages/@sanity/cli/src/actions/build/__tests__/getViteConfig.test.ts +++ b/packages/@sanity/cli/src/actions/build/__tests__/getViteConfig.test.ts @@ -486,7 +486,7 @@ describe('#getViteConfig', () => { const options = { cwd: mockTestCwd, entries: {relativeConfigLocation: '../../sanity.config.ts', relativeEntry: '../../src/App'}, - federation: {enabled: true}, + isWorkbench: true, mode: 'development' as const, reactCompiler: undefined, } @@ -510,7 +510,7 @@ describe('#getViteConfig', () => { const options = { cwd: mockTestCwd, entries: mockEntries, - federation: {enabled: false}, + isWorkbench: false, mode: 'development' as const, reactCompiler: undefined, } diff --git a/packages/@sanity/cli/src/actions/build/buildApp.ts b/packages/@sanity/cli/src/actions/build/buildApp.ts index 09ef92495..11d721db1 100644 --- a/packages/@sanity/cli/src/actions/build/buildApp.ts +++ b/packages/@sanity/cli/src/actions/build/buildApp.ts @@ -13,6 +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 {parse as semverParse} from 'semver' import {AppBuildTrace} from '../../telemetry/build.telemetry.js' @@ -35,7 +36,7 @@ interface InternalBuildOptions { calledFromDeploy: boolean | undefined determineBasePath: () => string entry: string | undefined - federation: CliConfig['federation'] + isWorkbench: boolean minify: boolean outDir: string | undefined output: Output @@ -63,7 +64,7 @@ export async function buildApp(options: BuildOptions): Promise { calledFromDeploy: options.calledFromDeploy, determineBasePath: () => determineBasePath(cliConfig, 'app', output), entry: cliConfig && 'app' in cliConfig ? cliConfig.app?.entry : undefined, - federation: cliConfig.federation, + isWorkbench: isWorkbenchApp(cliConfig && 'app' in cliConfig ? cliConfig.app : undefined), minify: flags.minify, outDir, output, @@ -206,7 +207,7 @@ async function internalBuildApp(options: InternalBuildOptions): Promise { let importMap: {imports?: Record} | undefined - if (autoUpdatesEnabled && !options.federation?.enabled) { + if (autoUpdatesEnabled && !options.isWorkbench) { importMap = { imports: { ...(await buildVendorDependencies({basePath, cwd: workDir, isApp: true, outputDir})), @@ -224,9 +225,9 @@ async function internalBuildApp(options: InternalBuildOptions): Promise { basePath, cwd: workDir, entry: options.entry, - federation: options.federation, importMap, isApp: true, + isWorkbench: options.isWorkbench, minify: options.minify, outputDir, reactCompiler: options.reactCompiler, diff --git a/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts b/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts index e6ac88c8d..9c247c491 100644 --- a/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts +++ b/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts @@ -19,7 +19,7 @@ export interface ChunkStats { name: string } -interface StaticBuildOptions extends Pick { +interface StaticBuildOptions { basePath: string cwd: string outputDir: string @@ -29,6 +29,8 @@ interface StaticBuildOptions extends Pick { entry?: string importMap?: {imports?: Record} isApp?: boolean + /** Workbench app (opted in via `unstable_defineApp`) — drives the federation build. */ + isWorkbench?: boolean minify?: boolean profile?: boolean reactCompiler?: ReactCompilerConfig @@ -51,9 +53,9 @@ export async function buildStaticFiles( basePath, cwd, entry, - federation, importMap, isApp, + isWorkbench, minify = true, outputDir, reactCompiler, @@ -68,7 +70,7 @@ export async function buildStaticFiles( * (remote-entry, mf-manifest) — skip client-specific steps like * runtime generation, static file copies, and favicons. */ - if (federation?.enabled) { + if (isWorkbench) { buildDebug('Resolving entries for federation build') const entries = await resolveEntries({cwd, entry, isApp}) @@ -77,8 +79,8 @@ export async function buildStaticFiles( basePath, cwd, entries, - federation, isApp, + isWorkbench, minify, mode, outputDir, @@ -123,9 +125,9 @@ export async function buildStaticFiles( basePath, cwd, entries, - federation, importMap, isApp, + isWorkbench, minify, mode, outputDir, diff --git a/packages/@sanity/cli/src/actions/build/buildStudio.ts b/packages/@sanity/cli/src/actions/build/buildStudio.ts index 66a665dfe..8369dae7e 100644 --- a/packages/@sanity/cli/src/actions/build/buildStudio.ts +++ b/packages/@sanity/cli/src/actions/build/buildStudio.ts @@ -13,6 +13,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 {parse as semverParse} from 'semver' import {StudioBuildTrace} from '../../telemetry/build.telemetry.js' @@ -37,8 +38,8 @@ interface InternalBuildOptions { autoUpdatesEnabled: boolean calledFromDeploy: boolean | undefined determineBasePath: () => string - federation: CliConfig['federation'] isApp: boolean + isWorkbench: boolean minify: boolean outDir: string | undefined output: Output @@ -78,8 +79,8 @@ export async function buildStudio(options: BuildOptions): Promise { autoUpdatesEnabled: options.autoUpdatesEnabled, calledFromDeploy, determineBasePath: () => determineBasePath(cliConfig, 'studio', output), - federation: cliConfig.federation, isApp: determineIsApp(cliConfig), + isWorkbench: isWorkbenchApp(cliConfig?.app), minify: Boolean(flags.minify), outDir, output, @@ -276,7 +277,7 @@ async function internalBuildStudio(options: InternalBuildOptions): Promise let importMap - if (autoUpdatesEnabled && !options.federation?.enabled) { + if (autoUpdatesEnabled && !options.isWorkbench) { importMap = { imports: { ...(await buildVendorDependencies({basePath, cwd: workDir, isApp: false, outputDir})), @@ -292,8 +293,8 @@ async function internalBuildStudio(options: InternalBuildOptions): Promise autoUpdatesCssUrls: autoUpdatesCssUrls.length > 0 ? autoUpdatesCssUrls : undefined, basePath, cwd: workDir, - federation: options.federation, importMap, + isWorkbench: options.isWorkbench, minify, outputDir, reactCompiler, diff --git a/packages/@sanity/cli/src/actions/build/getViteConfig.ts b/packages/@sanity/cli/src/actions/build/getViteConfig.ts index cc505aabf..75c515ac0 100644 --- a/packages/@sanity/cli/src/actions/build/getViteConfig.ts +++ b/packages/@sanity/cli/src/actions/build/getViteConfig.ts @@ -34,7 +34,7 @@ import {sanityBuildEntries} from './vite/plugin-sanity-build-entries.js' import {sanityFaviconsPlugin} from './vite/plugin-sanity-favicons.js' import {sanityRuntimeRewritePlugin} from './vite/plugin-sanity-runtime-rewrite.js' -interface ViteOptions extends Pick { +interface ViteOptions extends Pick { /** * Root path of the studio/sanity app */ @@ -72,6 +72,12 @@ interface ViteOptions extends Pick basePath: rawBasePath = '/', cwd, entries, - federation, importMap, isApp, + isWorkbench, minify, mode, outputDir, @@ -197,7 +203,7 @@ export async function getViteConfig(options: ViteOptions): Promise plugins: [ // Federation builds only need the federation plugin — skip client-specific // plugins (react, favicons, runtime rewrite, build entries, schema, typegen) - ...(federation?.enabled + ...(isWorkbench ? [ ...sharedPlugins, viteFederation({ @@ -251,7 +257,7 @@ export async function getViteConfig(options: ViteOptions): Promise // Federation builds don't produce a client bundle — the federation // plugin configures its own environment and build entry point. - if (mode === 'production' && !federation?.enabled) { + if (mode === 'production' && !isWorkbench) { viteConfig.build = { ...viteConfig.build, assetsDir: 'static', diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/devAction.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/devAction.test.ts index e9fbcec5f..e925990e9 100644 --- a/packages/@sanity/cli/src/actions/dev/__tests__/devAction.test.ts +++ b/packages/@sanity/cli/src/actions/dev/__tests__/devAction.test.ts @@ -1,7 +1,7 @@ import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' import {devAction} from '../devAction.js' -import {createBaseDevOptions, createMockOutput} from './testHelpers.js' +import {createBaseDevOptions, createMockOutput, workbenchCliConfig} from './testHelpers.js' const mockStartWorkbenchDevServer = vi.hoisted(() => vi.fn()) const mockStartAppDevServer = vi.hoisted(() => vi.fn()) @@ -136,7 +136,7 @@ describe('devAction', () => { test('starts federation registration when federation is enabled', async () => { mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3334})) - await devAction(createBaseDevOptions({cliConfig: {federation: {enabled: true}}})) + await devAction(createBaseDevOptions({cliConfig: workbenchCliConfig()})) expect(mockStartFederationRegistration).toHaveBeenCalledWith( expect.objectContaining({ @@ -157,7 +157,7 @@ describe('devAction', () => { test('passes isApp: true for app mode', async () => { mockStartAppDevServer.mockResolvedValue(mockServer({port: 3334})) - await devAction(createBaseDevOptions({cliConfig: {federation: {enabled: true}}, isApp: true})) + await devAction(createBaseDevOptions({cliConfig: workbenchCliConfig(), isApp: true})) expect(mockStartFederationRegistration).toHaveBeenCalledWith( expect.objectContaining({isApp: true}), @@ -168,7 +168,7 @@ describe('devAction', () => { const server = mockServer({port: 3334}) mockStartStudioDevServer.mockResolvedValue(server) - await devAction(createBaseDevOptions({cliConfig: {federation: {enabled: true}}})) + await devAction(createBaseDevOptions({cliConfig: workbenchCliConfig()})) expect(mockStartFederationRegistration).toHaveBeenCalledWith( expect.objectContaining({server: server.server}), @@ -180,9 +180,7 @@ describe('devAction', () => { mockStartFederationRegistration.mockResolvedValue({close: mockFederationClose}) mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3334})) - const result = await devAction( - createBaseDevOptions({cliConfig: {federation: {enabled: true}}}), - ) + const result = await devAction(createBaseDevOptions({cliConfig: workbenchCliConfig()})) await result.close() expect(mockFederationClose).toHaveBeenCalled() diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/startFederationRegistration.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/startFederationRegistration.test.ts index b174b3fc0..13036ade0 100644 --- a/packages/@sanity/cli/src/actions/dev/__tests__/startFederationRegistration.test.ts +++ b/packages/@sanity/cli/src/actions/dev/__tests__/startFederationRegistration.test.ts @@ -1,7 +1,7 @@ import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' import {startFederationRegistration} from '../startFederationRegistration.js' -import {createMockOutput} from './testHelpers.js' +import {createMockOutput, workbenchApp, workbenchCliConfig} from './testHelpers.js' const mockRegisterDevServer = vi.hoisted(() => vi.fn()) const mockStartDevManifestWatcher = vi.hoisted(() => vi.fn()) @@ -48,7 +48,7 @@ describe('startFederationRegistration', () => { test('registers studio in registry', async () => { await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: false, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -68,7 +68,7 @@ describe('startFederationRegistration', () => { mockGetAppId.mockReturnValue('app-abc') await startFederationRegistration({ - cliConfig: {deployment: {appId: 'app-abc'}, federation: {enabled: true}}, + cliConfig: {app: workbenchApp(), deployment: {appId: 'app-abc'}}, isApp: false, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -80,7 +80,7 @@ describe('startFederationRegistration', () => { test('forwards api.projectId to registerDevServer', async () => { await startFederationRegistration({ - cliConfig: {api: {projectId: 'x1g7jygt'}, federation: {enabled: true}}, + cliConfig: {api: {projectId: 'x1g7jygt'}, app: workbenchApp()}, isApp: false, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -94,7 +94,7 @@ describe('startFederationRegistration', () => { test('omits projectId when api.projectId is not configured', async () => { await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: false, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -109,7 +109,7 @@ describe('startFederationRegistration', () => { const output = createMockOutput() await startFederationRegistration({ - cliConfig: {app: {id: 'legacy-app'}, federation: {enabled: true}}, + cliConfig: {app: workbenchApp({id: 'legacy-app'})}, isApp: false, output, server: mockServer({port: 3334}) as any, @@ -121,7 +121,7 @@ describe('startFederationRegistration', () => { test('registers without icon/title — they are derived from the inlined manifest', async () => { await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: false, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -135,7 +135,7 @@ describe('startFederationRegistration', () => { test('registers app under the host applied by the vite dev server', async () => { await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: false, output: createMockOutput(), server: mockServer({host: 'mydev.local', port: 3334}) as any, @@ -149,7 +149,7 @@ describe('startFederationRegistration', () => { test('falls back to localhost when the vite server host is not a string', async () => { await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: false, output: createMockOutput(), server: mockServer({host: true, port: 3334}) as any, @@ -161,7 +161,7 @@ describe('startFederationRegistration', () => { test('registers app type when isApp is true', async () => { await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: true, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -173,7 +173,7 @@ describe('startFederationRegistration', () => { test('starts the manifest watcher for studios', async () => { await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: false, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -187,7 +187,7 @@ describe('startFederationRegistration', () => { test('starts the manifest watcher for core apps', async () => { await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: true, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -204,7 +204,7 @@ describe('startFederationRegistration', () => { mockExtractCoreAppManifest.mockResolvedValue(appManifest) await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: true, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -220,7 +220,7 @@ describe('startFederationRegistration', () => { test('wires extractStudioManifest into the studio watcher', async () => { await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: false, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -236,7 +236,7 @@ describe('startFederationRegistration', () => { mockRegisterDevServer.mockReturnValue({release: mockCleanup, update: vi.fn()}) const result = await startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: false, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -255,7 +255,7 @@ describe('startFederationRegistration', () => { await expect( startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: false, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -270,7 +270,7 @@ describe('startFederationRegistration', () => { await expect( startFederationRegistration({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), isApp: false, output: createMockOutput(), server: mockServer({port: 3334}) as any, @@ -288,9 +288,8 @@ describe('startFederationRegistration', () => { await startFederationRegistration({ cliConfig: { - app: {id: 'legacy-app'}, + app: workbenchApp({id: 'legacy-app'}), deployment: {appId: 'new-app'}, - federation: {enabled: true}, }, isApp: false, output, diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/startWorkbenchDevServer.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/startWorkbenchDevServer.test.ts index 5f60fd2d1..71d6d1e28 100644 --- a/packages/@sanity/cli/src/actions/dev/__tests__/startWorkbenchDevServer.test.ts +++ b/packages/@sanity/cli/src/actions/dev/__tests__/startWorkbenchDevServer.test.ts @@ -1,7 +1,13 @@ import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' import {startWorkbenchDevServer} from '../startWorkbenchDevServer.js' -import {createDevOptions, createMockOutput} from './testHelpers.js' +import { + createDevOptions, + createMockOutput, + studioWorkbenchApp, + workbenchApp, + workbenchCliConfig, +} from './testHelpers.js' const mockResolveLocalPackage = vi.hoisted(() => vi.fn()) const mockCreateServer = vi.hoisted(() => vi.fn()) @@ -69,9 +75,7 @@ describe('startWorkbenchDevServer', () => { }) test('skips workbench when federation is explicitly disabled', async () => { - const result = await startWorkbenchDevServer( - createDevOptions({cliConfig: {federation: {enabled: false}}}), - ) + const result = await startWorkbenchDevServer(createDevOptions({cliConfig: {}})) expect(result.workbenchAvailable).toBe(false) expect(result.close).toBeTypeOf('function') @@ -93,7 +97,7 @@ describe('startWorkbenchDevServer', () => { mockResolveLocalPackage.mockRejectedValue(new Error('Cannot find package')) const result = await startWorkbenchDevServer( - createDevOptions({cliConfig: {federation: {enabled: true}}}), + createDevOptions({cliConfig: workbenchCliConfig()}), ) expect(result.workbenchAvailable).toBe(false) @@ -106,7 +110,7 @@ describe('startWorkbenchDevServer', () => { const result = await startWorkbenchDevServer( createDevOptions({ - cliConfig: {federation: {enabled: true}}, + cliConfig: workbenchCliConfig(), httpHost: '0.0.0.0', httpPort: 4000, }), @@ -119,8 +123,7 @@ describe('startWorkbenchDevServer', () => { describe('successful startup', () => { const federationConfig = { - app: {organizationId: 'org-test'}, - federation: {enabled: true}, + app: workbenchApp({organizationId: 'org-test'}), } as const test('returns workbenchAvailable: true and close when server starts', async () => { @@ -179,7 +182,7 @@ describe('startWorkbenchDevServer', () => { await startWorkbenchDevServer( createDevOptions({ - cliConfig: {app: {organizationId: 'org-123'}, federation: {enabled: true}}, + cliConfig: {app: workbenchApp({organizationId: 'org-123'})}, }), ) @@ -195,7 +198,10 @@ describe('startWorkbenchDevServer', () => { await startWorkbenchDevServer( createDevOptions({ - cliConfig: {api: {projectId: 'proj-123'}, federation: {enabled: true}}, + cliConfig: { + api: {projectId: 'proj-123'}, + app: studioWorkbenchApp({organizationId: undefined}), + }, }), ) @@ -213,8 +219,7 @@ describe('startWorkbenchDevServer', () => { createDevOptions({ cliConfig: { api: {projectId: 'proj-123'}, - app: {organizationId: 'org-explicit'}, - federation: {enabled: true}, + app: workbenchApp({organizationId: 'org-explicit'}), }, }), ) @@ -230,7 +235,9 @@ describe('startWorkbenchDevServer', () => { mockCreateServer.mockResolvedValue(createMockServer()) await expect( - startWorkbenchDevServer(createDevOptions({cliConfig: {federation: {enabled: true}}})), + startWorkbenchDevServer( + createDevOptions({cliConfig: {app: workbenchApp({organizationId: undefined})}}), + ), ).rejects.toThrow(/Unable to determine organization ID/) }) @@ -242,7 +249,10 @@ describe('startWorkbenchDevServer', () => { await expect( startWorkbenchDevServer( createDevOptions({ - cliConfig: {api: {projectId: 'proj-123'}, federation: {enabled: true}}, + cliConfig: { + api: {projectId: 'proj-123'}, + app: studioWorkbenchApp({organizationId: undefined}), + }, }), ), ).rejects.toThrow(/Unable to determine organization ID/) @@ -268,8 +278,7 @@ describe('startWorkbenchDevServer', () => { describe('remote-preload Link header', () => { const federationConfig = { - app: {organizationId: 'org-test'}, - federation: {enabled: true}, + app: workbenchApp({organizationId: 'org-test'}), } as const function getMiddleware(): (req: {url?: string}, res: ResLike, next: () => void) => void { @@ -422,8 +431,7 @@ describe('startWorkbenchDevServer', () => { await startWorkbenchDevServer( createDevOptions({ cliConfig: { - app: {organizationId: 'org-test'}, - federation: {enabled: true}, + app: workbenchApp({organizationId: 'org-test'}), reactStrictMode: false, }, }), @@ -442,8 +450,7 @@ describe('startWorkbenchDevServer', () => { await startWorkbenchDevServer( createDevOptions({ cliConfig: { - app: {organizationId: 'org-test'}, - federation: {enabled: true}, + app: workbenchApp({organizationId: 'org-test'}), reactStrictMode: true, }, }), @@ -461,8 +468,7 @@ describe('startWorkbenchDevServer', () => { await startWorkbenchDevServer( createDevOptions({ cliConfig: { - app: {organizationId: 'org-test'}, - federation: {enabled: true}, + app: workbenchApp({organizationId: 'org-test'}), reactStrictMode: true, }, }), @@ -476,8 +482,7 @@ describe('startWorkbenchDevServer', () => { describe('server startup failure', () => { const federationConfig = { - app: {organizationId: 'org-test'}, - federation: {enabled: true}, + app: workbenchApp({organizationId: 'org-test'}), } as const test('warns and returns without close when listen() throws', async () => { @@ -510,8 +515,7 @@ describe('startWorkbenchDevServer', () => { describe('singleton detection', () => { const federationConfig = { - app: {organizationId: 'org-test'}, - federation: {enabled: true}, + app: workbenchApp({organizationId: 'org-test'}), } as const test('skips starting server when lock is held by another process', async () => { @@ -544,8 +548,7 @@ describe('startWorkbenchDevServer', () => { describe('registry integration', () => { const federationConfig = { - app: {organizationId: 'org-test'}, - federation: {enabled: true}, + app: workbenchApp({organizationId: 'org-test'}), } as const test('updates lock with actual port after successful startup', async () => { diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/testHelpers.ts b/packages/@sanity/cli/src/actions/dev/__tests__/testHelpers.ts index dcdcba694..e454ffa42 100644 --- a/packages/@sanity/cli/src/actions/dev/__tests__/testHelpers.ts +++ b/packages/@sanity/cli/src/actions/dev/__tests__/testHelpers.ts @@ -1,4 +1,5 @@ import {type CliConfig, type Output} from '@sanity/cli-core' +import {unstable_defineApp} from '@sanity/federation' // eslint-disable-next-line import-x/no-extraneous-dependencies import {vi} from 'vitest' @@ -7,6 +8,30 @@ import {type DevActionOptions} from '../types.js' /** Shared test helpers for dev-action test suites. */ +/** + * A CliConfig whose `app` is a branded `unstable_defineApp(...)` result — the + * workbench opt-in. Replaces the old `federation: {enabled: true}` test signal. + */ +export function workbenchApp(overrides: Record = {}): CliConfig['app'] { + return unstable_defineApp({ + name: 'test-app', + organizationId: 'org-123', + title: 'Test App', + ...overrides, + }) as unknown as CliConfig['app'] +} + +/** Branded workbench app explicitly typed as a studio. */ +export function studioWorkbenchApp(overrides: Record = {}): CliConfig['app'] { + const app = workbenchApp(overrides) + ;(app as {applicationType?: string}).applicationType = 'studio' + return app +} + +export function workbenchCliConfig(overrides: Partial = {}): CliConfig { + return {app: workbenchApp(), ...overrides} as CliConfig +} + export function createMockOutput(): Output { return { error: vi.fn(), diff --git a/packages/@sanity/cli/src/actions/dev/devAction.ts b/packages/@sanity/cli/src/actions/dev/devAction.ts index 427f644c1..2d5ac533d 100644 --- a/packages/@sanity/cli/src/actions/dev/devAction.ts +++ b/packages/@sanity/cli/src/actions/dev/devAction.ts @@ -1,5 +1,7 @@ import {styleText} from 'node:util' +import {isWorkbenchApp} from '@sanity/federation' + import {getSharedServerConfig} from '../../util/getSharedServerConfig.js' import {startAppDevServer} from './startAppDevServer.js' import {startFederationRegistration} from './startFederationRegistration.js' @@ -58,7 +60,9 @@ export async function devAction(options: DevActionOptions): Promise<{close: () = return {close: closeWorkbenchServer} } - const closeFederation = cliConfig?.federation?.enabled + // Workbench is opted into solely by calling `unstable_defineApp` — its + // branded identity is the only signal. + const closeFederation = isWorkbenchApp(cliConfig?.app) ? await startFederationRegistration({ cliConfig, isApp: options.isApp, diff --git a/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts b/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts index e4c0a09e0..3e769d1f7 100644 --- a/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts +++ b/packages/@sanity/cli/src/actions/dev/getDevServerConfig.ts @@ -2,8 +2,10 @@ import path from 'node:path' import {type CliConfig, getSanityEnvVar, type Output} from '@sanity/cli-core' import {spinner} from '@sanity/cli-core/ux' +import {isWorkbenchApp} from '@sanity/federation' import {type DevServerOptions} from '../../server/devServer.js' +import {determineIsApp} from '../../util/determineIsApp.js' import {getSharedServerConfig} from '../../util/getSharedServerConfig.js' import {resolveReactStrictMode} from '../../util/resolveReactStrictMode.js' import {type DevFlags} from './types.js' @@ -32,7 +34,7 @@ export function getDevServerConfig({ configSpinner.succeed() - const isApp = cliConfig && 'app' in cliConfig + const isApp = cliConfig ? determineIsApp(cliConfig) : false const reactStrictMode = resolveReactStrictMode(cliConfig) const envBasePath = getSanityEnvVar('BASEPATH', isApp ?? false) @@ -44,7 +46,7 @@ export function getDevServerConfig({ return { ...baseConfig, - federation: cliConfig?.federation, + isWorkbench: isWorkbenchApp(cliConfig?.app), reactCompiler: cliConfig && 'reactCompiler' in cliConfig ? cliConfig.reactCompiler : undefined, reactStrictMode, staticPath: path.join(workDir, 'static'), diff --git a/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts b/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts index 92047859a..381de12e7 100644 --- a/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts +++ b/packages/@sanity/cli/src/actions/dev/startWorkbenchDevServer.ts @@ -1,4 +1,5 @@ import {resolveLocalPackage} from '@sanity/cli-core' +import {isWorkbenchApp} from '@sanity/federation' import viteReact from '@vitejs/plugin-react' import {createServer, type InlineConfig, type Plugin} from 'vite' import {z} from 'zod/mini' @@ -53,8 +54,9 @@ export async function startWorkbenchDevServer( ): Promise { const {cliConfig, httpHost, httpPort: workbenchPort, output, workDir} = options - if (!cliConfig?.federation?.enabled) { - devDebug('Federation not enabled, skipping workbench dev server') + // Workbench is opted into solely by calling `unstable_defineApp`. + if (!isWorkbenchApp(cliConfig?.app)) { + devDebug('Not a workbench app, skipping workbench dev server') return {close: noop, httpHost, workbenchAvailable: false, workbenchPort} } diff --git a/packages/@sanity/cli/src/actions/init/__tests__/bootstrapLocalTemplate.test.ts b/packages/@sanity/cli/src/actions/init/__tests__/bootstrapLocalTemplate.test.ts index 24a1021df..bb1ba9530 100644 --- a/packages/@sanity/cli/src/actions/init/__tests__/bootstrapLocalTemplate.test.ts +++ b/packages/@sanity/cli/src/actions/init/__tests__/bootstrapLocalTemplate.test.ts @@ -5,7 +5,6 @@ import path from 'node:path' import {type Output} from '@sanity/cli-core' import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' -import {resolveLatestVersions} from '../../../util/resolveLatestVersions.js' import {bootstrapLocalTemplate} from '../bootstrapLocalTemplate.js' vi.mock('../../../util/resolveLatestVersions.js', () => ({ @@ -53,7 +52,6 @@ describe('bootstrapLocalTemplate (app templates)', () => { variables: { autoUpdates: false, dataset: 'production', - federation: false, organizationId: 'org1', projectId: 'abc123', projectName: 'my-app', @@ -77,7 +75,6 @@ describe('bootstrapLocalTemplate (app templates)', () => { variables: { autoUpdates: false, dataset: '', - federation: false, organizationId: 'org1', projectId: '', projectName: 'my-app', @@ -91,83 +88,3 @@ describe('bootstrapLocalTemplate (app templates)', () => { expect(appTsx).not.toContain('%dataset%') }) }) - -describe('bootstrapLocalTemplate (federation)', () => { - let tmp: string - beforeEach(async () => { - tmp = await mkdtemp(path.join(tmpdir(), 'cli-bootstrap-')) - }) - afterEach(async () => { - await rm(tmp, {force: true, recursive: true}) - vi.clearAllMocks() - }) - - test('overrides the `sanity` dependency with the `workbench` dist-tag when federation is enabled', async () => { - await bootstrapLocalTemplate({ - output: makeOutput(), - outputPath: tmp, - packageName: 'my-studio', - templateName: 'clean', - useTypeScript: true, - variables: { - autoUpdates: false, - dataset: 'production', - federation: true, - organizationId: 'org1', - projectId: 'abc123', - projectName: 'my-studio', - }, - }) - - expect(resolveLatestVersions).toHaveBeenCalledOnce() - const resolvedDeps = vi.mocked(resolveLatestVersions).mock.calls[0][0] - expect(resolvedDeps.sanity).toBe('workbench') - - const pkgJson = JSON.parse(await readFile(path.join(tmp, 'package.json'), 'utf8')) - expect(pkgJson.dependencies.sanity).toBe('1.0.0') - }) - - test('keeps the `sanity` dependency on the `latest` dist-tag when federation is disabled', async () => { - await bootstrapLocalTemplate({ - output: makeOutput(), - outputPath: tmp, - packageName: 'my-studio', - templateName: 'clean', - useTypeScript: true, - variables: { - autoUpdates: false, - dataset: 'production', - federation: false, - organizationId: 'org1', - projectId: 'abc123', - projectName: 'my-studio', - }, - }) - - expect(resolveLatestVersions).toHaveBeenCalledOnce() - const resolvedDeps = vi.mocked(resolveLatestVersions).mock.calls[0][0] - expect(resolvedDeps.sanity).toBe('latest') - }) - - test('overrides the `sanity` devDependency for app templates when federation is enabled', async () => { - await bootstrapLocalTemplate({ - output: makeOutput(), - outputPath: tmp, - packageName: 'my-app', - templateName: 'app-quickstart', - useTypeScript: true, - variables: { - autoUpdates: false, - dataset: 'production', - federation: true, - organizationId: 'org1', - projectId: 'abc123', - projectName: 'my-app', - }, - }) - - expect(resolveLatestVersions).toHaveBeenCalledOnce() - const resolvedDeps = vi.mocked(resolveLatestVersions).mock.calls[0][0] - expect(resolvedDeps.sanity).toBe('workbench') - }) -}) diff --git a/packages/@sanity/cli/src/actions/init/__tests__/bootstrapRemoteTemplate.test.ts b/packages/@sanity/cli/src/actions/init/__tests__/bootstrapRemoteTemplate.test.ts index 974b0df4d..ec40817bd 100644 --- a/packages/@sanity/cli/src/actions/init/__tests__/bootstrapRemoteTemplate.test.ts +++ b/packages/@sanity/cli/src/actions/init/__tests__/bootstrapRemoteTemplate.test.ts @@ -76,7 +76,6 @@ const baseOpts = { variables: { autoUpdates: false, dataset: 'production', - federation: true, projectId: 'test-project-id', }, } diff --git a/packages/@sanity/cli/src/actions/init/bootstrapLocalTemplate.ts b/packages/@sanity/cli/src/actions/init/bootstrapLocalTemplate.ts index 773d39829..79caf3863 100644 --- a/packages/@sanity/cli/src/actions/init/bootstrapLocalTemplate.ts +++ b/packages/@sanity/cli/src/actions/init/bootstrapLocalTemplate.ts @@ -89,7 +89,6 @@ export async function bootstrapLocalTemplate( ...(isAppTemplate ? sdkAppDependencies.devDependencies : studioDependencies.devDependencies), ...template.dependencies, ...template.devDependencies, - ...(variables.federation && {sanity: 'workbench'}), }) spin.succeed() @@ -142,13 +141,11 @@ export async function bootstrapLocalTemplate( const cliConfig = isAppTemplate ? createAppCliConfig({ entry: template.entry!, - federation: variables.federation, organizationId: variables.organizationId, }) : createCliConfig({ autoUpdates: variables.autoUpdates, dataset: variables.dataset, - federation: variables.federation, projectId: variables.projectId, }) diff --git a/packages/@sanity/cli/src/actions/init/bootstrapTemplate.ts b/packages/@sanity/cli/src/actions/init/bootstrapTemplate.ts index 73d37a347..a356535ea 100644 --- a/packages/@sanity/cli/src/actions/init/bootstrapTemplate.ts +++ b/packages/@sanity/cli/src/actions/init/bootstrapTemplate.ts @@ -9,7 +9,6 @@ interface BootstrapTemplateOptions { autoUpdates: boolean bearerToken: string | undefined dataset: string - federation: boolean organizationId: string | undefined output: Output outputPath: string @@ -28,7 +27,6 @@ export async function bootstrapTemplate({ autoUpdates, bearerToken, dataset, - federation, organizationId, output, outputPath, @@ -43,7 +41,6 @@ export async function bootstrapTemplate({ const bootstrapVariables: GenerateConfigOptions['variables'] = { autoUpdates, dataset, - federation, organizationId, projectId, projectName, diff --git a/packages/@sanity/cli/src/actions/init/createAppCliConfig.ts b/packages/@sanity/cli/src/actions/init/createAppCliConfig.ts index 185b3b9d8..7c8feebf0 100644 --- a/packages/@sanity/cli/src/actions/init/createAppCliConfig.ts +++ b/packages/@sanity/cli/src/actions/init/createAppCliConfig.ts @@ -8,15 +8,11 @@ export default defineCliConfig({ organizationId: '%organizationId%', entry: '%entry%', }, - federation: { - enabled: __BOOL__federation__, - }, }) ` interface GenerateCliConfigOptions { entry: string - federation: boolean organizationId?: string } diff --git a/packages/@sanity/cli/src/actions/init/createCliConfig.ts b/packages/@sanity/cli/src/actions/init/createCliConfig.ts index 167aff579..8ee5bc701 100644 --- a/packages/@sanity/cli/src/actions/init/createCliConfig.ts +++ b/packages/@sanity/cli/src/actions/init/createCliConfig.ts @@ -15,16 +15,12 @@ export default defineCliConfig({ */ autoUpdates: __BOOL__autoUpdates__, }, - federation: { - enabled: __BOOL__federation__, - }, }) ` interface GenerateCliConfigOptions { autoUpdates: boolean dataset: string - federation: boolean projectId: string } diff --git a/packages/@sanity/cli/src/actions/init/createStudioConfig.ts b/packages/@sanity/cli/src/actions/init/createStudioConfig.ts index 242656a9d..67a191466 100644 --- a/packages/@sanity/cli/src/actions/init/createStudioConfig.ts +++ b/packages/@sanity/cli/src/actions/init/createStudioConfig.ts @@ -31,7 +31,6 @@ export interface GenerateConfigOptions { variables: { autoUpdates: boolean dataset: string - federation: boolean organizationId?: string projectId: string projectName?: string diff --git a/packages/@sanity/cli/src/actions/init/initAction.ts b/packages/@sanity/cli/src/actions/init/initAction.ts index f3b3ea176..a1fd41b74 100644 --- a/packages/@sanity/cli/src/actions/init/initAction.ts +++ b/packages/@sanity/cli/src/actions/init/initAction.ts @@ -6,7 +6,6 @@ import {type TelemetryTrace} from '@sanity/telemetry' import {type Framework, frameworks} from '@vercel/frameworks' import deburr from 'lodash-es/deburr.js' -import {promptForFederation} from '../../prompts/init/federation.js' import {promptForConfigFiles} from '../../prompts/init/nextjs.js' import {getCliUser} from '../../services/user.js' import {CLIInitStepCompleted, type InitStepResult} from '../../telemetry/init.telemetry.js' @@ -251,14 +250,8 @@ export async function initAction(options: InitOptions, context: InitContext): Pr return } - let federation = flagOrDefault(options.federation, true) - if (shouldPrompt(options.unattended, options.federation)) { - federation = await promptForFederation() - } - const sharedParams = { defaults, - federation, mcpConfigured, options, organizationId, diff --git a/packages/@sanity/cli/src/actions/init/initApp.ts b/packages/@sanity/cli/src/actions/init/initApp.ts index 188e3cb14..db4f2dd1c 100644 --- a/packages/@sanity/cli/src/actions/init/initApp.ts +++ b/packages/@sanity/cli/src/actions/init/initApp.ts @@ -15,7 +15,6 @@ import {type InitOptions} from './types.js' export async function initApp({ datasetName, defaults, - federation, mcpConfigured, options, organizationId, @@ -29,7 +28,6 @@ export async function initApp({ }: { datasetName: string defaults: {projectName: string} - federation: boolean mcpConfigured: EditorName[] options: InitOptions organizationId: string | undefined @@ -59,7 +57,6 @@ export async function initApp({ datasetName, defaults, displayName: '', - federation, options, organizationId, output, diff --git a/packages/@sanity/cli/src/actions/init/initStudio.ts b/packages/@sanity/cli/src/actions/init/initStudio.ts index ecbcee64d..a10d0c1a3 100644 --- a/packages/@sanity/cli/src/actions/init/initStudio.ts +++ b/packages/@sanity/cli/src/actions/init/initStudio.ts @@ -27,7 +27,6 @@ export async function initStudio({ datasetName, defaults, displayName, - federation, isFirstProject, mcpConfigured, options, @@ -43,7 +42,6 @@ export async function initStudio({ datasetName: string defaults: {projectName: string} displayName: string - federation: boolean isFirstProject: boolean mcpConfigured: EditorName[] options: InitOptions @@ -93,7 +91,6 @@ export async function initStudio({ datasetName, defaults, displayName, - federation, options, organizationId, output, diff --git a/packages/@sanity/cli/src/actions/init/scaffoldTemplate.ts b/packages/@sanity/cli/src/actions/init/scaffoldTemplate.ts index be880735b..343e65fc5 100644 --- a/packages/@sanity/cli/src/actions/init/scaffoldTemplate.ts +++ b/packages/@sanity/cli/src/actions/init/scaffoldTemplate.ts @@ -98,7 +98,6 @@ export async function scaffoldAndInstall({ datasetName, defaults, displayName, - federation, options, organizationId, output, @@ -114,7 +113,6 @@ export async function scaffoldAndInstall({ datasetName: string defaults: {projectName: string} displayName: string - federation: boolean options: InitOptions organizationId: string | undefined output: Output @@ -134,7 +132,6 @@ export async function scaffoldAndInstall({ autoUpdates, bearerToken: templateToken, dataset: datasetName, - federation, organizationId, output, outputPath, diff --git a/packages/@sanity/cli/src/actions/init/types.ts b/packages/@sanity/cli/src/actions/init/types.ts index 72cd0d68c..5a92e8ed9 100644 --- a/packages/@sanity/cli/src/actions/init/types.ts +++ b/packages/@sanity/cli/src/actions/init/types.ts @@ -31,7 +31,6 @@ export interface InitOptions { coupon?: string dataset?: string env?: string - federation?: boolean git?: boolean | string importDataset?: boolean nextjsAddConfigFiles?: boolean @@ -70,7 +69,6 @@ interface InitCommandFlags { 'create-project'?: string dataset?: string env?: string - federation?: boolean git?: string 'import-dataset'?: boolean 'nextjs-add-config-files'?: boolean @@ -123,7 +121,6 @@ export function flagsToInitOptions( dataset: flags.dataset, datasetDefault: flags['dataset-default'], env: flags.env, - federation: flags.federation, fromCreate: flags['from-create'], git: flags['no-git'] ? false : flags.git, importDataset: flags['import-dataset'], diff --git a/packages/@sanity/cli/src/commands/__tests__/build.studio.test.ts b/packages/@sanity/cli/src/commands/__tests__/build.studio.test.ts index a9d601d57..c8093a2dd 100644 --- a/packages/@sanity/cli/src/commands/__tests__/build.studio.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/build.studio.test.ts @@ -85,45 +85,53 @@ describe('#build studio', {timeout: (platform() === 'win32' ? 120 : 60) * 1000}, expect(files).not.toContain('mf-manifest.json') }) - test('should build the "federated-studio" with only federation artifacts', async () => { - const cwd = await testFixture('federated-studio') - process.chdir(cwd) - - // `@module-federation/vite` short-circuits to an empty plugin array when - // it detects vitest/jest in the env, which leaves the federation env without - // its plugins and skips emitting `remote-entry.js` / `mf-manifest.json`. - // Opt out of that guard for this in-process build. - vi.stubEnv('MFE_VITE_NO_TEST_ENV_CHECK', 'true') - - const {error, stderr} = await testCommand(BuildCommand, ['--yes'], { - config: {root: cwd}, - }) - - // 1. Build succeeds - if (error) throw error - expect(stderr).toContain('✔ Build Sanity Studio') - - const distFiles = await readdir(join(cwd, 'dist')) - - // 2. No client artifacts - expect(distFiles).not.toContain('index.html') - expect(distFiles).not.toContain('static') - expect(distFiles).not.toContain('vendor') - - // 3. Stable remote entry (unhashed) - expect(distFiles).toContain('remote-entry.js') - - // 4. Federation manifest (valid JSON) - expect(distFiles).toContain('mf-manifest.json') - const manifest = JSON.parse(await readFile(join(cwd, 'dist', 'mf-manifest.json'), 'utf8')) - expect(manifest).toHaveProperty('id') - expect(manifest).toHaveProperty('name') - - // 5. Hashed federation chunks - expect(distFiles).toContain('assets') - const assetFiles = await readdir(join(cwd, 'dist', 'assets')) - expect(assetFiles.some((f) => /^remote-entry-.+\.js$/.test(f))).toBe(true) - }) + // Skipped on Windows: forcing `@module-federation/vite`'s plugins to run + // in-process (via MFE_VITE_NO_TEST_ENV_CHECK) crashes the vitest worker there — + // esbuild's service pipe dies with "Unexpected end of JSON input [plugin onEnd]". + // It's a test-harness limitation (real `sanity build` runs as its own process); + // the federation-artifact shape is platform-independent, so Linux coverage suffices. + test.skipIf(platform() === 'win32')( + 'should build the "federated-studio" with only federation artifacts', + async () => { + const cwd = await testFixture('federated-studio') + process.chdir(cwd) + + // `@module-federation/vite` short-circuits to an empty plugin array when + // it detects vitest/jest in the env, which leaves the federation env without + // its plugins and skips emitting `remote-entry.js` / `mf-manifest.json`. + // Opt out of that guard for this in-process build. + vi.stubEnv('MFE_VITE_NO_TEST_ENV_CHECK', 'true') + + const {error, stderr} = await testCommand(BuildCommand, ['--yes'], { + config: {root: cwd}, + }) + + // 1. Build succeeds + if (error) throw error + expect(stderr).toContain('✔ Build Sanity Studio') + + const distFiles = await readdir(join(cwd, 'dist')) + + // 2. No client artifacts + expect(distFiles).not.toContain('index.html') + expect(distFiles).not.toContain('static') + expect(distFiles).not.toContain('vendor') + + // 3. Stable remote entry (unhashed) + expect(distFiles).toContain('remote-entry.js') + + // 4. Federation manifest (valid JSON) + expect(distFiles).toContain('mf-manifest.json') + const manifest = JSON.parse(await readFile(join(cwd, 'dist', 'mf-manifest.json'), 'utf8')) + expect(manifest).toHaveProperty('id') + expect(manifest).toHaveProperty('name') + + // 5. Hashed federation chunks + expect(distFiles).toContain('assets') + const assetFiles = await readdir(join(cwd, 'dist', 'assets')) + expect(assetFiles.some((f) => /^remote-entry-.+\.js$/.test(f))).toBe(true) + }, + ) test("should build the 'worst-case-studio' example", async () => { const cwd = await testFixture('worst-case-studio') diff --git a/packages/@sanity/cli/src/commands/__tests__/init/init.authentication.test.ts b/packages/@sanity/cli/src/commands/__tests__/init/init.authentication.test.ts index 0829ddcbc..f0e6c6803 100644 --- a/packages/@sanity/cli/src/commands/__tests__/init/init.authentication.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/init/init.authentication.test.ts @@ -170,7 +170,6 @@ describe('#init: authentication', () => { '--output-path=/test/output', '--no-overwrite-files', '--template=clean', - '--federation', ], { mocks: { @@ -215,7 +214,6 @@ describe('#init: authentication', () => { '--output-path=/test/output', '--no-overwrite-files', '--template=clean', - '--federation', ], { mocks: { diff --git a/packages/@sanity/cli/src/commands/__tests__/init/init.bootstrap-app.test.ts b/packages/@sanity/cli/src/commands/__tests__/init/init.bootstrap-app.test.ts index 76ab47c04..3f0a49e3f 100644 --- a/packages/@sanity/cli/src/commands/__tests__/init/init.bootstrap-app.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/init/init.bootstrap-app.test.ts @@ -169,7 +169,6 @@ describe('#init: bootstrap-app-initialization', () => { '--dataset=test', '--package-manager=npm', '--typescript', - '--federation', ], { mocks: { @@ -183,7 +182,6 @@ describe('#init: bootstrap-app-initialization', () => { autoUpdates: true, bearerToken: undefined, dataset: 'test', - federation: true, organizationId: undefined, output: expect.any(Object), outputPath: convertToSystemPath('/test/output'), @@ -258,7 +256,6 @@ describe('#init: bootstrap-app-initialization', () => { '--output-path=/test/output', '--package-manager=npm', '--typescript', - '--federation', ], { mocks: { @@ -272,7 +269,6 @@ describe('#init: bootstrap-app-initialization', () => { autoUpdates: true, bearerToken: undefined, dataset: '', - federation: true, organizationId: 'org-1', output: expect.any(Object), outputPath: convertToSystemPath('/test/output'), @@ -335,7 +331,6 @@ describe('#init: bootstrap-app-initialization', () => { autoUpdates: true, bearerToken: undefined, dataset: '', - federation: true, organizationId: 'org-1', output: expect.any(Object), outputPath: convertToSystemPath('/test/output'), @@ -406,7 +401,6 @@ describe('#init: bootstrap-app-initialization', () => { autoUpdates: true, bearerToken: undefined, dataset: '', - federation: true, organizationId: 'org-1', output: expect.any(Object), outputPath: convertToSystemPath('/test/output'), diff --git a/packages/@sanity/cli/src/commands/__tests__/init/init.create-new-project.test.ts b/packages/@sanity/cli/src/commands/__tests__/init/init.create-new-project.test.ts index 25c419fdc..7aae59741 100644 --- a/packages/@sanity/cli/src/commands/__tests__/init/init.create-new-project.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/init/init.create-new-project.test.ts @@ -513,7 +513,6 @@ describe('#init: create new project', () => { '--no-overwrite-files', '--template=moviedb', '--no-import-dataset', - '--federation', ], {mocks: {...defaultMocks, isInteractive: true}}, ) diff --git a/packages/@sanity/cli/src/commands/__tests__/init/init.staging-env.test.ts b/packages/@sanity/cli/src/commands/__tests__/init/init.staging-env.test.ts index 2d50818f9..0b3c0752a 100644 --- a/packages/@sanity/cli/src/commands/__tests__/init/init.staging-env.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/init/init.staging-env.test.ts @@ -223,7 +223,6 @@ describe('#init: staging env propagation', () => { '--dataset=test', '--package-manager=npm', '--typescript', - '--federation', ], { mocks: { @@ -261,7 +260,6 @@ describe('#init: staging env propagation', () => { '--dataset=test', '--package-manager=npm', '--typescript', - '--federation', ], { mocks: { @@ -291,7 +289,6 @@ describe('#init: staging env propagation', () => { '--dataset=test', '--package-manager=npm', '--typescript', - '--federation', ], { mocks: { diff --git a/packages/@sanity/cli/src/commands/init.ts b/packages/@sanity/cli/src/commands/init.ts index ebc7213f1..ca539ef9f 100644 --- a/packages/@sanity/cli/src/commands/init.ts +++ b/packages/@sanity/cli/src/commands/init.ts @@ -78,11 +78,6 @@ export class InitCommand extends SanityCommand { return input }, }), - federation: Flags.boolean({ - allowNo: true, - default: undefined, - description: 'Enable federation for this project', - }), 'from-create': Flags.boolean({ description: 'Internal flag to indicate that the command is run from create-sanity', hidden: true, diff --git a/packages/@sanity/cli/src/exports/index.ts b/packages/@sanity/cli/src/exports/index.ts index dc374dbf1..adc1be882 100644 --- a/packages/@sanity/cli/src/exports/index.ts +++ b/packages/@sanity/cli/src/exports/index.ts @@ -5,3 +5,8 @@ export type {CliApiConfig} from '../types.js' 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 +// app authors via `import {unstable_defineApp} from 'sanity/cli'`. +export {type DefineAppInput, unstable_defineApp} from '@sanity/federation' diff --git a/packages/@sanity/cli/src/prompts/init/federation.ts b/packages/@sanity/cli/src/prompts/init/federation.ts deleted file mode 100644 index 6f5f0e644..000000000 --- a/packages/@sanity/cli/src/prompts/init/federation.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {confirm} from '@sanity/cli-core/ux' - -export function promptForFederation(): Promise { - return confirm({ - default: true, - message: 'Would you like to enable federation for this project?', - }) -} diff --git a/packages/@sanity/cli/src/server/devServer.ts b/packages/@sanity/cli/src/server/devServer.ts index c0a0f261f..8e112e705 100644 --- a/packages/@sanity/cli/src/server/devServer.ts +++ b/packages/@sanity/cli/src/server/devServer.ts @@ -22,9 +22,9 @@ export interface DevServerOptions { appTitle?: string entry?: string - federation?: CliConfig['federation'] httpHost?: string isApp?: boolean + isWorkbench?: boolean projectName?: string schemaExtraction?: CliConfig['schemaExtraction'] typegen?: CliConfig['typegen'] @@ -44,10 +44,10 @@ export async function startDevServer(options: DevServerOptions): Promise=20.19 <22 || >=22.12'} hasBin: true - '@sanity/federation@0.1.0-alpha.8': - resolution: {integrity: sha512-FMVXDu9XM8+I6XO1jXE3AIfL399CP9aVDotL3KWipPppTuq7iexo0wuD6d7mcYOwrhMukET7Pooy2+Z3WwQ3Iw==} + '@sanity/federation@0.1.0-alpha.9': + resolution: {integrity: sha512-mSsRVzqSrniPtg50+hhbO2WE8vzX/PGkI123ysS8xrUECV0m/v/9YH3ha159dcuUxG0JzgjAtj1+gVy+TmqANQ==} engines: {node: '>=20.19.1 <22 || >=22.12'} peerDependencies: vite: ^7.0.0 || ^8.0.0 @@ -10277,18 +10283,6 @@ packages: utf-8-validate: optional: true - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.20.1: resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} @@ -13320,6 +13314,22 @@ snapshots: - utf-8-validate - vue-tsc + '@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))': + dependencies: + '@module-federation/dts-plugin': 2.5.0(typescript@5.9.3) + '@module-federation/runtime': 2.5.0 + '@module-federation/sdk': 2.5.0 + es-module-lexer: 2.0.0 + estree-walker: 3.0.3 + pathe: 2.0.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) + transitivePeerDependencies: + - bufferutil + - node-fetch + - typescript + - utf-8-validate + - vue-tsc + '@mswjs/interceptors@0.41.2': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -14486,11 +14496,25 @@ snapshots: - react-native-b4a - supports-color - '@sanity/federation@0.1.0-alpha.8(typescript@5.9.3)(vite@7.3.2(@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@0.1.0-alpha.9(typescript@5.9.3)(vite@7.3.2(@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.2(@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)) vite: 7.3.2(@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) + zod: 4.3.6 + transitivePeerDependencies: + - bufferutil + - node-fetch + - typescript + - 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))': + 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)) + 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) + zod: 4.3.6 transitivePeerDependencies: - bufferutil - node-fetch @@ -18205,7 +18229,7 @@ snapshots: webidl-conversions: 8.0.1 whatwg-mimetype: 4.0.0 whatwg-url: 15.1.0 - ws: 8.19.0 + ws: 8.20.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' @@ -20935,8 +20959,6 @@ snapshots: ws@8.18.0: {} - ws@8.19.0: {} - ws@8.20.1: {} wsl-utils@0.3.1: diff --git a/turbo.json b/turbo.json index 00473fd74..ee7d61a7f 100644 --- a/turbo.json +++ b/turbo.json @@ -15,6 +15,11 @@ "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", "dist/**", ".sanity/**", "lib/**", "fixtures/**"] }, + "@sanity/cli-test#build": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/fixtures/**", "$TURBO_ROOT$/pnpm-workspace.yaml"], + "outputs": ["dist/**", "fixtures/**"] + }, "build:types": { "dependsOn": ["^build", "^build:types", "build"], "outputs": ["dist/**/*.d.ts"]