@sanity/workbench-cli 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/_exports/build.d.ts +36 -1
  2. package/dist/_exports/build.js +1 -0
  3. package/dist/_exports/build.js.map +1 -1
  4. package/dist/_exports/deploy.d.ts +5 -1
  5. package/dist/_exports/index.d.ts +1 -1
  6. package/dist/_exports/preview.d.ts +169 -0
  7. package/dist/_exports/preview.js +3 -0
  8. package/dist/_exports/preview.js.map +1 -0
  9. package/dist/_exports/undeploy.d.ts +3 -1
  10. package/dist/actions/build/vite/optimize-deps.js +84 -0
  11. package/dist/actions/build/vite/optimize-deps.js.map +1 -0
  12. package/dist/actions/deploy/checkBuiltOutput.js +16 -2
  13. package/dist/actions/deploy/checkBuiltOutput.js.map +1 -1
  14. package/dist/actions/deploy/deployWorkbenchApp.js +17 -3
  15. package/dist/actions/deploy/deployWorkbenchApp.js.map +1 -1
  16. package/dist/actions/dev/registry.js +69 -1
  17. package/dist/actions/dev/registry.js.map +1 -1
  18. package/dist/actions/dev/startWorkbenchDev.js +6 -41
  19. package/dist/actions/dev/startWorkbenchDev.js.map +1 -1
  20. package/dist/actions/dev/startWorkbenchDevServer.js +7 -3
  21. package/dist/actions/dev/startWorkbenchDevServer.js.map +1 -1
  22. package/dist/actions/preview/serveBuiltApplication.js +53 -0
  23. package/dist/actions/preview/serveBuiltApplication.js.map +1 -0
  24. package/dist/actions/preview/startWorkbenchPreview.js +91 -0
  25. package/dist/actions/preview/startWorkbenchPreview.js.map +1 -0
  26. package/dist/defineApp.js +2 -5
  27. package/dist/defineApp.js.map +1 -1
  28. package/dist/resolveWorkbenchApp.js +1 -0
  29. package/dist/resolveWorkbenchApp.js.map +1 -1
  30. package/dist/services/applications.js +14 -1
  31. package/dist/services/applications.js.map +1 -1
  32. package/dist/util/serverOrchestration.js +75 -0
  33. package/dist/util/serverOrchestration.js.map +1 -0
  34. package/package.json +7 -3
@@ -5,7 +5,25 @@ import { z } from 'zod/mini';
5
5
  import { AppInterfaceMetadataSchema } from '../../contract.js';
6
6
  import { canonicalizeWatchDir } from './canonicalizeWatchDir.js';
7
7
  import { getProcessStartTime, isOurProcess } from './processLiveness.js';
8
- const devDebug = subdebug('dev');
8
+ /**
9
+ * The dev-server registry: how a running `sanity dev` / `sanity start` process
10
+ * advertises itself so the workbench on this machine can find and load it.
11
+ *
12
+ * Two kinds of file under `~/.sanity/dev-servers/` do the coordinating:
13
+ *
14
+ * - `<pid>.json` — one per running app/studio server, holding where it's served
15
+ * plus its inlined manifest and interfaces. The workbench reads these to
16
+ * discover and render local apps. Written by `registerDevServer`, watched by
17
+ * `watchRegistry`.
18
+ * - `workbench.lock` — a single machine-wide lock, so only one workbench shell
19
+ * runs at a time and later `dev`s register into it instead of starting their
20
+ * own. Managed by `acquireWorkbenchLock` / `readWorkbenchLock`.
21
+ *
22
+ * Both files belong to the process that created them and must not outlive it.
23
+ * Three things keep that true: an explicit `release()` on clean shutdown, an
24
+ * `exit` backstop for abrupt exits (`unlinkOnProcessExit`), and a dead-pid prune
25
+ * on read (`isOurProcess`) that clears whatever a crashed process left behind.
26
+ */ const devDebug = subdebug('dev');
9
27
  /** Bump when the manifest/lock shape changes in a breaking way. */ const REGISTRY_VERSION = 1;
10
28
  /**
11
29
  * The current process's start time as reported by the OS, for the `startedAt`
@@ -100,6 +118,39 @@ const devServerManifestSchema = z.object({
100
118
  */ function getRegistryDir() {
101
119
  return join(getSanityDataDir(), 'dev-servers');
102
120
  }
121
+ // One shared `exit` listener drives every registered cleanup, so N locks/entries
122
+ // don't each add a listener and trip Node's MaxListeners warning.
123
+ const exitCleanups = new Set();
124
+ let exitListenerInstalled = false;
125
+ function runExitCleanups() {
126
+ for (const cleanup of exitCleanups)cleanup();
127
+ }
128
+ /** Exercise the exit backstop in tests without terminating the process; not part
129
+ * of the package's public surface. */ export const runRegistryExitCleanupForTesting = runExitCleanups;
130
+ /**
131
+ * Delete a registry file synchronously on process exit, as a backstop for abrupt
132
+ * termination. Vite installs its own SIGTERM handler that calls `process.exit()`,
133
+ * which can outrun the async server teardown and leave the lock or registry entry
134
+ * behind — a stray dev-server that lingers until the dead-pid prune clears it. The
135
+ * `exit` event only runs synchronous work, hence `unlinkSync`. `ownedByUs` guards
136
+ * the shared lock so a successor that reacquired it isn't wiped. Returns a
137
+ * detacher to call after a clean release.
138
+ */ function unlinkOnProcessExit(filePath, ownedByUs) {
139
+ const cleanup = ()=>{
140
+ if (!ownedByUs()) return;
141
+ try {
142
+ unlinkSync(filePath);
143
+ } catch {
144
+ // The file may already have been removed during shutdown.
145
+ }
146
+ };
147
+ exitCleanups.add(cleanup);
148
+ if (!exitListenerInstalled) {
149
+ exitListenerInstalled = true;
150
+ process.once('exit', runExitCleanups);
151
+ }
152
+ return ()=>exitCleanups.delete(cleanup);
153
+ }
103
154
  /**
104
155
  * Write a manifest file for the current process and return a handle with a
105
156
  * `release` function that removes it plus an `update` function for patching
@@ -122,9 +173,12 @@ const devServerManifestSchema = z.object({
122
173
  // manifest extraction) landing after `release()` has deleted the file —
123
174
  // without this, the update would re-create the registry entry and leak.
124
175
  let released = false;
176
+ // The file is pid-named, so it's always ours to remove on exit.
177
+ const detachExitCleanup = unlinkOnProcessExit(filePath, ()=>!released);
125
178
  return {
126
179
  release () {
127
180
  released = true;
181
+ detachExitCleanup();
128
182
  try {
129
183
  unlinkSync(filePath);
130
184
  } catch {
@@ -285,8 +339,22 @@ function pruneWorkbenchLock(lockPath) {
285
339
  flag: 'wx'
286
340
  });
287
341
  devDebug('Workbench lock acquired');
342
+ let released = false;
343
+ // Only wipe the lock on exit if it's still ours — a successor that reacquired
344
+ // it after our own release must not be clobbered.
345
+ const detachExitCleanup = unlinkOnProcessExit(lockPath, ()=>{
346
+ if (released) return false;
347
+ try {
348
+ const disk = parseLockContents(readFileSync(lockPath, 'utf8'));
349
+ return disk?.pid === process.pid && disk.startedAt === startedAt;
350
+ } catch {
351
+ return false;
352
+ }
353
+ });
288
354
  return {
289
355
  release () {
356
+ released = true;
357
+ detachExitCleanup();
290
358
  try {
291
359
  unlinkSync(lockPath);
292
360
  } catch {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/dev/registry.ts"],"sourcesContent":["import {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n unlinkSync,\n watch,\n writeFileSync,\n} from 'node:fs'\nimport {join} from 'node:path'\n\nimport {\n coreAppManifestSchema,\n getSanityDataDir,\n studioManifestSchema,\n subdebug,\n} from '@sanity/cli-core'\nimport {z} from 'zod/mini'\n\nimport {AppInterfaceMetadataSchema} from '../../contract.js'\nimport {canonicalizeWatchDir} from './canonicalizeWatchDir.js'\nimport {getProcessStartTime, isOurProcess} from './processLiveness.js'\n\nconst devDebug = subdebug('dev')\n\n/** Bump when the manifest/lock shape changes in a breaking way. */\nconst REGISTRY_VERSION = 1\n\n/**\n * The current process's start time as reported by the OS, for the `startedAt`\n * that `isOurProcess` checks on re-read. Falls back to now when the OS time is\n * unavailable — `new Date()` alone records the write time, which drifts from\n * process start by enough to look stale and get pruned right after writing.\n */\nfunction ownStartedAt(): string {\n return (getProcessStartTime(process.pid) ?? new Date()).toISOString()\n}\n\nconst interfaceBaseFields = {\n /** CLI-minted for a local interface; a deployed one gets its id from Brett. */\n id: z.string(),\n moduleId: z.string(),\n name: z.string(),\n /** Raw source vite serves; a deployed interface carries only the `moduleId`. */\n src: z.string(),\n title: z.string(),\n version: z.optional(z.string()),\n}\n\n/**\n * A forwarded interface, discriminated on `type`. Kept outside the manifest so\n * the workbench renders local panels and runs workers without a deploy.\n */\nconst devServerInterfaceSchema = z.discriminatedUnion('type', [\n z.object({\n ...interfaceBaseFields,\n metadata: z.nullable(AppInterfaceMetadataSchema),\n type: z.literal('app'),\n }),\n z.object({...interfaceBaseFields, metadata: z.null(), type: z.literal('panel')}),\n z.object({...interfaceBaseFields, metadata: z.null(), type: z.literal('worker')}),\n])\n\nconst devServerManifestSchema = z.object({\n /**\n * Field schema *values* load from the federation module; each field's `src`\n * rides along so a repoint bumps the exposes-set id and forces a rebuild.\n * Lenient — the workbench is the authority.\n */\n configs: z.optional(\n z.array(\n z.object({\n // Identifies the owning app when it has no app id (singletons).\n appType: z.optional(z.string()),\n fields: z.array(\n z.object({\n name: z.string(),\n public: z.optional(z.boolean()),\n src: z.string(),\n title: z.string(),\n }),\n ),\n // Content hash of the config — the workbench's change-detection key\n // (see deriveConfigs).\n id: z.string(),\n // The app's `unstable_defineApp` name — the module-federation alias the\n // workbench loads this config's live values from.\n moduleName: z.optional(z.string()),\n // Config contract version the generated module exports, so the\n // workbench knows what it can resolve before loading the module.\n version: z.number(),\n }),\n ),\n ),\n host: z.string(),\n id: z.optional(z.string()),\n interfaces: z.optional(z.array(devServerInterfaceSchema)),\n /**\n * Inlined manifest — either a {@link StudioManifest} or {@link CoreAppManifest},\n * validated against the shared cli-core schemas. The registry stores and\n * rebroadcasts it; the CLI is what extracts and writes it.\n */\n manifest: z.optional(z.union([studioManifestSchema, coreAppManifestSchema])),\n /**\n * ISO timestamp of the most recent successful manifest extraction. Bumped\n * on every regeneration so re-writing this registry entry triggers the\n * workbench `watchRegistry` watcher and forces a rebroadcast to clients.\n */\n manifestUpdatedAt: z.optional(z.string()),\n pid: z.number(),\n port: z.number(),\n projectId: z.optional(z.string()),\n startedAt: z.string(),\n type: z.enum(['coreApp', 'studio']),\n version: z.literal(REGISTRY_VERSION),\n workDir: z.string(),\n})\n/**\n * A manifest describing a running dev server process (studio or app).\n * Stored as `~/.sanity/dev-servers/<pid>.json`.\n *\n * The workbench singleton is tracked separately via the lock file — see\n * `acquireWorkbenchLock` and `readWorkbenchLock` below.\n */\nexport type DevServerManifest = z.infer<typeof devServerManifestSchema>\n\n/**\n * Path to the dev server registry directory. Lives under the shared Sanity\n * config directory to stay consistent with other CLI paths.\n */\nfunction getRegistryDir(): string {\n return join(getSanityDataDir(), 'dev-servers')\n}\n\ninterface DevServerRegistration {\n /** Remove the registry entry. */\n release: () => void\n /**\n * Rewrite the registry entry with partial updates merged in. Also bumps the\n * file's mtime, which fires `watchRegistry` in any workbench process and\n * triggers a rebroadcast to connected clients.\n */\n update: (patch: Partial<Omit<DevServerManifest, 'pid' | 'startedAt' | 'version'>>) => void\n}\n\n/**\n * Write a manifest file for the current process and return a handle with a\n * `release` function that removes it plus an `update` function for patching\n * fields post-registration. Uses synchronous I/O so the file exists before\n * any signal handler could fire.\n */\nexport function registerDevServer(\n manifest: Omit<DevServerManifest, 'pid' | 'startedAt' | 'version'>,\n): DevServerRegistration {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n let current: DevServerManifest = {\n ...manifest,\n pid: process.pid,\n startedAt: ownStartedAt(),\n version: REGISTRY_VERSION,\n }\n\n const filePath = join(registryDir, `${process.pid}.json`)\n writeFileSync(filePath, JSON.stringify(current, null, 2))\n\n // Guard against late updates from background tasks (e.g. the initial\n // manifest extraction) landing after `release()` has deleted the file —\n // without this, the update would re-create the registry entry and leak.\n let released = false\n\n return {\n release() {\n released = true\n try {\n unlinkSync(filePath)\n } catch {\n // ENOENT is fine — already cleaned up\n }\n },\n update(patch) {\n if (released) return\n current = {...current, ...patch}\n writeFileSync(filePath, JSON.stringify(current, null, 2))\n },\n }\n}\n\n/**\n * Read all manifest files from the registry, prune stale entries (dead PIDs),\n * and return the live ones.\n */\nexport function getRegisteredServers(): DevServerManifest[] {\n const registryDir = getRegistryDir()\n\n if (!existsSync(registryDir)) {\n return []\n }\n\n const files = readdirSync(registryDir).filter((f) => f.endsWith('.json'))\n const servers: DevServerManifest[] = []\n\n for (const file of files) {\n const filePath = join(registryDir, file)\n let raw: unknown\n try {\n raw = JSON.parse(readFileSync(filePath, 'utf8'))\n } catch {\n continue\n }\n\n const {data, success} = devServerManifestSchema.safeParse(raw)\n if (!success) continue\n\n if (isOurProcess(data.pid, data.startedAt)) {\n servers.push(data)\n } else {\n try {\n unlinkSync(filePath)\n } catch {\n // Ignore — another process may have already cleaned it up\n }\n }\n }\n\n return servers\n}\n\ninterface RegistryWatcher {\n close(): void\n}\n\n/**\n * Watch the registry directory for changes and invoke the callback with the\n * current list of live servers whenever a change is detected.\n *\n * Uses `fs.watch` with a debounce to coalesce rapid file changes (e.g. a\n * server starting and writing its manifest triggers multiple FS events).\n */\nexport function watchRegistry(callback: (servers: DevServerManifest[]) => void): RegistryWatcher {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n // Canonicalize to the real long path so `fs.watch` doesn't abort on Windows\n // short-path dirs. See `canonicalizeWatchDir`.\n const watchDir = canonicalizeWatchDir(registryDir)\n\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n const notify = () => {\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n callback(getRegisteredServers())\n }, 50)\n }\n\n const watcher = watch(watchDir, notify)\n\n return {\n close() {\n clearTimeout(debounceTimer)\n watcher.close()\n },\n }\n}\n\n// The workbench singleton lock — \"one workbench per machine\". Lives in the same\n// registry dir and shares the liveness/prune model: a stale lock left by a\n// crashed process is pruned on read so the next acquire isn't blocked forever.\n\nconst workbenchLockSchema = z.object({\n host: z.string(),\n pid: z.number(),\n port: z.number(),\n startedAt: z.string(),\n version: z.literal(REGISTRY_VERSION),\n})\n\n/**\n * Read the workbench lock file and return its contents if the holding\n * process is still alive. Prunes stale locks from crashed processes.\n */\nexport function readWorkbenchLock(): z.infer<typeof workbenchLockSchema> | undefined {\n const lockPath = join(getRegistryDir(), 'workbench.lock')\n\n let contents: string\n try {\n contents = readFileSync(lockPath, 'utf8')\n } catch {\n // File doesn't exist — nothing to prune, nothing to return\n return undefined\n }\n\n // Past this point the file exists. Anything that isn't a live, valid lock\n // (unparsable JSON, schema mismatch, dead/reused PID) is stale and must be\n // pruned — otherwise the next `acquireWorkbenchLock` call is blocked by\n // EEXIST forever and `sanity dev` silently no-ops the workbench server.\n const data = parseLockContents(contents)\n devDebug('Read workbench lock: %o', data)\n if (data && isOurProcess(data.pid, data.startedAt)) {\n devDebug('Workbench process is alive at pid %d on port %d', data.pid, data.port)\n return data\n }\n\n pruneWorkbenchLock(lockPath)\n return undefined\n}\n\nfunction parseLockContents(contents: string): z.infer<typeof workbenchLockSchema> | undefined {\n try {\n const {data, success} = workbenchLockSchema.safeParse(JSON.parse(contents))\n return success ? data : undefined\n } catch {\n return undefined\n }\n}\n\nfunction pruneWorkbenchLock(lockPath: string): void {\n try {\n devDebug('Removing stale workbench lock')\n unlinkSync(lockPath)\n devDebug('Stale workbench lock removed')\n } catch {\n // Another process may have already cleaned it up\n }\n}\n\ninterface WorkbenchLock {\n /** Release the lock file. */\n release: () => void\n /** Update the lock with the actual port after the server starts listening. */\n updatePort: (port: number) => void\n}\n\n/**\n * Attempt to acquire an exclusive lock for the workbench process.\n * Uses `O_EXCL` (the `wx` flag) which is atomic at the OS level — only one\n * process can create the file.\n *\n * The lock stores `{pid, host, port}` so other processes can find the\n * running workbench. Call `updatePort` after the Vite server starts to\n * write the actual port (Vite may pick a different one).\n *\n * @returns A {@link WorkbenchLock} if acquired, or `undefined` if another\n * live process already holds it.\n */\nexport function acquireWorkbenchLock(\n info: {host: string; port: number},\n retries = 1,\n): WorkbenchLock | undefined {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n const lockPath = join(registryDir, 'workbench.lock')\n const startedAt = ownStartedAt()\n const lockData = {\n host: info.host,\n pid: process.pid,\n port: info.port,\n startedAt,\n version: REGISTRY_VERSION,\n }\n\n devDebug('Acquiring workbench lock at %s', lockPath)\n\n try {\n writeFileSync(lockPath, JSON.stringify(lockData), {flag: 'wx'})\n devDebug('Workbench lock acquired')\n return {\n release() {\n try {\n unlinkSync(lockPath)\n } catch {\n // Already cleaned up\n }\n },\n updatePort(port: number) {\n writeFileSync(lockPath, JSON.stringify({...lockData, port}))\n },\n }\n } catch (err: unknown) {\n devDebug(\n 'Failed to acquire workbench lock: %s',\n err instanceof Error ? err.message : String(err),\n )\n if (!isNodeError(err) || err.code !== 'EEXIST') return undefined\n\n // Lock exists — check if the holder is still alive\n const existing = readWorkbenchLock()\n if (existing) return undefined\n\n // Stale lock was pruned by readWorkbenchLock — retry (with guard against infinite recursion)\n if (retries <= 0) return undefined\n return acquireWorkbenchLock(info, retries - 1)\n }\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && 'code' in err\n}\n"],"names":["existsSync","mkdirSync","readdirSync","readFileSync","unlinkSync","watch","writeFileSync","join","coreAppManifestSchema","getSanityDataDir","studioManifestSchema","subdebug","z","AppInterfaceMetadataSchema","canonicalizeWatchDir","getProcessStartTime","isOurProcess","devDebug","REGISTRY_VERSION","ownStartedAt","process","pid","Date","toISOString","interfaceBaseFields","id","string","moduleId","name","src","title","version","optional","devServerInterfaceSchema","discriminatedUnion","object","metadata","nullable","type","literal","null","devServerManifestSchema","configs","array","appType","fields","public","boolean","moduleName","number","host","interfaces","manifest","union","manifestUpdatedAt","port","projectId","startedAt","enum","workDir","getRegistryDir","registerDevServer","registryDir","recursive","current","filePath","JSON","stringify","released","release","update","patch","getRegisteredServers","files","filter","f","endsWith","servers","file","raw","parse","data","success","safeParse","push","watchRegistry","callback","watchDir","debounceTimer","notify","clearTimeout","setTimeout","watcher","close","workbenchLockSchema","readWorkbenchLock","lockPath","contents","undefined","parseLockContents","pruneWorkbenchLock","acquireWorkbenchLock","info","retries","lockData","flag","updatePort","err","Error","message","String","isNodeError","code","existing"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/actions/dev/registry.ts"],"sourcesContent":["import {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n unlinkSync,\n watch,\n writeFileSync,\n} from 'node:fs'\nimport {join} from 'node:path'\n\nimport {\n coreAppManifestSchema,\n getSanityDataDir,\n studioManifestSchema,\n subdebug,\n} from '@sanity/cli-core'\nimport {z} from 'zod/mini'\n\nimport {AppInterfaceMetadataSchema} from '../../contract.js'\nimport {canonicalizeWatchDir} from './canonicalizeWatchDir.js'\nimport {getProcessStartTime, isOurProcess} from './processLiveness.js'\n\n/**\n * The dev-server registry: how a running `sanity dev` / `sanity start` process\n * advertises itself so the workbench on this machine can find and load it.\n *\n * Two kinds of file under `~/.sanity/dev-servers/` do the coordinating:\n *\n * - `<pid>.json` — one per running app/studio server, holding where it's served\n * plus its inlined manifest and interfaces. The workbench reads these to\n * discover and render local apps. Written by `registerDevServer`, watched by\n * `watchRegistry`.\n * - `workbench.lock` — a single machine-wide lock, so only one workbench shell\n * runs at a time and later `dev`s register into it instead of starting their\n * own. Managed by `acquireWorkbenchLock` / `readWorkbenchLock`.\n *\n * Both files belong to the process that created them and must not outlive it.\n * Three things keep that true: an explicit `release()` on clean shutdown, an\n * `exit` backstop for abrupt exits (`unlinkOnProcessExit`), and a dead-pid prune\n * on read (`isOurProcess`) that clears whatever a crashed process left behind.\n */\n\nconst devDebug = subdebug('dev')\n\n/** Bump when the manifest/lock shape changes in a breaking way. */\nconst REGISTRY_VERSION = 1\n\n/**\n * The current process's start time as reported by the OS, for the `startedAt`\n * that `isOurProcess` checks on re-read. Falls back to now when the OS time is\n * unavailable — `new Date()` alone records the write time, which drifts from\n * process start by enough to look stale and get pruned right after writing.\n */\nfunction ownStartedAt(): string {\n return (getProcessStartTime(process.pid) ?? new Date()).toISOString()\n}\n\nconst interfaceBaseFields = {\n /** CLI-minted for a local interface; a deployed one gets its id from Brett. */\n id: z.string(),\n moduleId: z.string(),\n name: z.string(),\n /** Raw source vite serves; a deployed interface carries only the `moduleId`. */\n src: z.string(),\n title: z.string(),\n version: z.optional(z.string()),\n}\n\n/**\n * A forwarded interface, discriminated on `type`. Kept outside the manifest so\n * the workbench renders local panels and runs workers without a deploy.\n */\nconst devServerInterfaceSchema = z.discriminatedUnion('type', [\n z.object({\n ...interfaceBaseFields,\n metadata: z.nullable(AppInterfaceMetadataSchema),\n type: z.literal('app'),\n }),\n z.object({...interfaceBaseFields, metadata: z.null(), type: z.literal('panel')}),\n z.object({...interfaceBaseFields, metadata: z.null(), type: z.literal('worker')}),\n])\n\nconst devServerManifestSchema = z.object({\n /**\n * Field schema *values* load from the federation module; each field's `src`\n * rides along so a repoint bumps the exposes-set id and forces a rebuild.\n * Lenient — the workbench is the authority.\n */\n configs: z.optional(\n z.array(\n z.object({\n // Identifies the owning app when it has no app id (singletons).\n appType: z.optional(z.string()),\n fields: z.array(\n z.object({\n name: z.string(),\n public: z.optional(z.boolean()),\n src: z.string(),\n title: z.string(),\n }),\n ),\n // Content hash of the config — the workbench's change-detection key\n // (see deriveConfigs).\n id: z.string(),\n // The app's `unstable_defineApp` name — the module-federation alias the\n // workbench loads this config's live values from.\n moduleName: z.optional(z.string()),\n // Config contract version the generated module exports, so the\n // workbench knows what it can resolve before loading the module.\n version: z.number(),\n }),\n ),\n ),\n host: z.string(),\n id: z.optional(z.string()),\n interfaces: z.optional(z.array(devServerInterfaceSchema)),\n /**\n * Inlined manifest — either a {@link StudioManifest} or {@link CoreAppManifest},\n * validated against the shared cli-core schemas. The registry stores and\n * rebroadcasts it; the CLI is what extracts and writes it.\n */\n manifest: z.optional(z.union([studioManifestSchema, coreAppManifestSchema])),\n /**\n * ISO timestamp of the most recent successful manifest extraction. Bumped\n * on every regeneration so re-writing this registry entry triggers the\n * workbench `watchRegistry` watcher and forces a rebroadcast to clients.\n */\n manifestUpdatedAt: z.optional(z.string()),\n pid: z.number(),\n port: z.number(),\n projectId: z.optional(z.string()),\n startedAt: z.string(),\n type: z.enum(['coreApp', 'studio']),\n version: z.literal(REGISTRY_VERSION),\n workDir: z.string(),\n})\n/**\n * A manifest describing a running dev server process (studio or app).\n * Stored as `~/.sanity/dev-servers/<pid>.json`.\n *\n * The workbench singleton is tracked separately via the lock file — see\n * `acquireWorkbenchLock` and `readWorkbenchLock` below.\n */\nexport type DevServerManifest = z.infer<typeof devServerManifestSchema>\n\n/**\n * Path to the dev server registry directory. Lives under the shared Sanity\n * config directory to stay consistent with other CLI paths.\n */\nfunction getRegistryDir(): string {\n return join(getSanityDataDir(), 'dev-servers')\n}\n\n// One shared `exit` listener drives every registered cleanup, so N locks/entries\n// don't each add a listener and trip Node's MaxListeners warning.\nconst exitCleanups = new Set<() => void>()\nlet exitListenerInstalled = false\n\nfunction runExitCleanups(): void {\n for (const cleanup of exitCleanups) cleanup()\n}\n\n/** Exercise the exit backstop in tests without terminating the process; not part\n * of the package's public surface. */\nexport const runRegistryExitCleanupForTesting = runExitCleanups\n\n/**\n * Delete a registry file synchronously on process exit, as a backstop for abrupt\n * termination. Vite installs its own SIGTERM handler that calls `process.exit()`,\n * which can outrun the async server teardown and leave the lock or registry entry\n * behind — a stray dev-server that lingers until the dead-pid prune clears it. The\n * `exit` event only runs synchronous work, hence `unlinkSync`. `ownedByUs` guards\n * the shared lock so a successor that reacquired it isn't wiped. Returns a\n * detacher to call after a clean release.\n */\nfunction unlinkOnProcessExit(filePath: string, ownedByUs: () => boolean): () => void {\n const cleanup = () => {\n if (!ownedByUs()) return\n try {\n unlinkSync(filePath)\n } catch {\n // The file may already have been removed during shutdown.\n }\n }\n exitCleanups.add(cleanup)\n\n if (!exitListenerInstalled) {\n exitListenerInstalled = true\n process.once('exit', runExitCleanups)\n }\n\n return () => exitCleanups.delete(cleanup)\n}\n\ninterface DevServerRegistration {\n /** Remove the registry entry. */\n release: () => void\n /**\n * Rewrite the registry entry with partial updates merged in. Also bumps the\n * file's mtime, which fires `watchRegistry` in any workbench process and\n * triggers a rebroadcast to connected clients.\n */\n update: (patch: Partial<Omit<DevServerManifest, 'pid' | 'startedAt' | 'version'>>) => void\n}\n\n/**\n * Write a manifest file for the current process and return a handle with a\n * `release` function that removes it plus an `update` function for patching\n * fields post-registration. Uses synchronous I/O so the file exists before\n * any signal handler could fire.\n */\nexport function registerDevServer(\n manifest: Omit<DevServerManifest, 'pid' | 'startedAt' | 'version'>,\n): DevServerRegistration {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n let current: DevServerManifest = {\n ...manifest,\n pid: process.pid,\n startedAt: ownStartedAt(),\n version: REGISTRY_VERSION,\n }\n\n const filePath = join(registryDir, `${process.pid}.json`)\n writeFileSync(filePath, JSON.stringify(current, null, 2))\n\n // Guard against late updates from background tasks (e.g. the initial\n // manifest extraction) landing after `release()` has deleted the file —\n // without this, the update would re-create the registry entry and leak.\n let released = false\n\n // The file is pid-named, so it's always ours to remove on exit.\n const detachExitCleanup = unlinkOnProcessExit(filePath, () => !released)\n\n return {\n release() {\n released = true\n detachExitCleanup()\n try {\n unlinkSync(filePath)\n } catch {\n // ENOENT is fine — already cleaned up\n }\n },\n update(patch) {\n if (released) return\n current = {...current, ...patch}\n writeFileSync(filePath, JSON.stringify(current, null, 2))\n },\n }\n}\n\n/**\n * Read all manifest files from the registry, prune stale entries (dead PIDs),\n * and return the live ones.\n */\nexport function getRegisteredServers(): DevServerManifest[] {\n const registryDir = getRegistryDir()\n\n if (!existsSync(registryDir)) {\n return []\n }\n\n const files = readdirSync(registryDir).filter((f) => f.endsWith('.json'))\n const servers: DevServerManifest[] = []\n\n for (const file of files) {\n const filePath = join(registryDir, file)\n let raw: unknown\n try {\n raw = JSON.parse(readFileSync(filePath, 'utf8'))\n } catch {\n continue\n }\n\n const {data, success} = devServerManifestSchema.safeParse(raw)\n if (!success) continue\n\n if (isOurProcess(data.pid, data.startedAt)) {\n servers.push(data)\n } else {\n try {\n unlinkSync(filePath)\n } catch {\n // Ignore — another process may have already cleaned it up\n }\n }\n }\n\n return servers\n}\n\ninterface RegistryWatcher {\n close(): void\n}\n\n/**\n * Watch the registry directory for changes and invoke the callback with the\n * current list of live servers whenever a change is detected.\n *\n * Uses `fs.watch` with a debounce to coalesce rapid file changes (e.g. a\n * server starting and writing its manifest triggers multiple FS events).\n */\nexport function watchRegistry(callback: (servers: DevServerManifest[]) => void): RegistryWatcher {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n // Canonicalize to the real long path so `fs.watch` doesn't abort on Windows\n // short-path dirs. See `canonicalizeWatchDir`.\n const watchDir = canonicalizeWatchDir(registryDir)\n\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n const notify = () => {\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n callback(getRegisteredServers())\n }, 50)\n }\n\n const watcher = watch(watchDir, notify)\n\n return {\n close() {\n clearTimeout(debounceTimer)\n watcher.close()\n },\n }\n}\n\n// The workbench singleton lock — \"one workbench per machine\". Lives in the same\n// registry dir and shares the liveness/prune model: a stale lock left by a\n// crashed process is pruned on read so the next acquire isn't blocked forever.\n\nconst workbenchLockSchema = z.object({\n host: z.string(),\n pid: z.number(),\n port: z.number(),\n startedAt: z.string(),\n version: z.literal(REGISTRY_VERSION),\n})\n\n/**\n * Read the workbench lock file and return its contents if the holding\n * process is still alive. Prunes stale locks from crashed processes.\n */\nexport function readWorkbenchLock(): z.infer<typeof workbenchLockSchema> | undefined {\n const lockPath = join(getRegistryDir(), 'workbench.lock')\n\n let contents: string\n try {\n contents = readFileSync(lockPath, 'utf8')\n } catch {\n // File doesn't exist — nothing to prune, nothing to return\n return undefined\n }\n\n // Past this point the file exists. Anything that isn't a live, valid lock\n // (unparsable JSON, schema mismatch, dead/reused PID) is stale and must be\n // pruned — otherwise the next `acquireWorkbenchLock` call is blocked by\n // EEXIST forever and `sanity dev` silently no-ops the workbench server.\n const data = parseLockContents(contents)\n devDebug('Read workbench lock: %o', data)\n if (data && isOurProcess(data.pid, data.startedAt)) {\n devDebug('Workbench process is alive at pid %d on port %d', data.pid, data.port)\n return data\n }\n\n pruneWorkbenchLock(lockPath)\n return undefined\n}\n\nfunction parseLockContents(contents: string): z.infer<typeof workbenchLockSchema> | undefined {\n try {\n const {data, success} = workbenchLockSchema.safeParse(JSON.parse(contents))\n return success ? data : undefined\n } catch {\n return undefined\n }\n}\n\nfunction pruneWorkbenchLock(lockPath: string): void {\n try {\n devDebug('Removing stale workbench lock')\n unlinkSync(lockPath)\n devDebug('Stale workbench lock removed')\n } catch {\n // Another process may have already cleaned it up\n }\n}\n\ninterface WorkbenchLock {\n /** Release the lock file. */\n release: () => void\n /** Update the lock with the actual port after the server starts listening. */\n updatePort: (port: number) => void\n}\n\n/**\n * Attempt to acquire an exclusive lock for the workbench process.\n * Uses `O_EXCL` (the `wx` flag) which is atomic at the OS level — only one\n * process can create the file.\n *\n * The lock stores `{pid, host, port}` so other processes can find the\n * running workbench. Call `updatePort` after the Vite server starts to\n * write the actual port (Vite may pick a different one).\n *\n * @returns A {@link WorkbenchLock} if acquired, or `undefined` if another\n * live process already holds it.\n */\nexport function acquireWorkbenchLock(\n info: {host: string; port: number},\n retries = 1,\n): WorkbenchLock | undefined {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n const lockPath = join(registryDir, 'workbench.lock')\n const startedAt = ownStartedAt()\n const lockData = {\n host: info.host,\n pid: process.pid,\n port: info.port,\n startedAt,\n version: REGISTRY_VERSION,\n }\n\n devDebug('Acquiring workbench lock at %s', lockPath)\n\n try {\n writeFileSync(lockPath, JSON.stringify(lockData), {flag: 'wx'})\n devDebug('Workbench lock acquired')\n\n let released = false\n // Only wipe the lock on exit if it's still ours — a successor that reacquired\n // it after our own release must not be clobbered.\n const detachExitCleanup = unlinkOnProcessExit(lockPath, () => {\n if (released) return false\n try {\n const disk = parseLockContents(readFileSync(lockPath, 'utf8'))\n return disk?.pid === process.pid && disk.startedAt === startedAt\n } catch {\n return false\n }\n })\n\n return {\n release() {\n released = true\n detachExitCleanup()\n try {\n unlinkSync(lockPath)\n } catch {\n // Already cleaned up\n }\n },\n updatePort(port: number) {\n writeFileSync(lockPath, JSON.stringify({...lockData, port}))\n },\n }\n } catch (err: unknown) {\n devDebug(\n 'Failed to acquire workbench lock: %s',\n err instanceof Error ? err.message : String(err),\n )\n if (!isNodeError(err) || err.code !== 'EEXIST') return undefined\n\n // Lock exists — check if the holder is still alive\n const existing = readWorkbenchLock()\n if (existing) return undefined\n\n // Stale lock was pruned by readWorkbenchLock — retry (with guard against infinite recursion)\n if (retries <= 0) return undefined\n return acquireWorkbenchLock(info, retries - 1)\n }\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && 'code' in err\n}\n"],"names":["existsSync","mkdirSync","readdirSync","readFileSync","unlinkSync","watch","writeFileSync","join","coreAppManifestSchema","getSanityDataDir","studioManifestSchema","subdebug","z","AppInterfaceMetadataSchema","canonicalizeWatchDir","getProcessStartTime","isOurProcess","devDebug","REGISTRY_VERSION","ownStartedAt","process","pid","Date","toISOString","interfaceBaseFields","id","string","moduleId","name","src","title","version","optional","devServerInterfaceSchema","discriminatedUnion","object","metadata","nullable","type","literal","null","devServerManifestSchema","configs","array","appType","fields","public","boolean","moduleName","number","host","interfaces","manifest","union","manifestUpdatedAt","port","projectId","startedAt","enum","workDir","getRegistryDir","exitCleanups","Set","exitListenerInstalled","runExitCleanups","cleanup","runRegistryExitCleanupForTesting","unlinkOnProcessExit","filePath","ownedByUs","add","once","delete","registerDevServer","registryDir","recursive","current","JSON","stringify","released","detachExitCleanup","release","update","patch","getRegisteredServers","files","filter","f","endsWith","servers","file","raw","parse","data","success","safeParse","push","watchRegistry","callback","watchDir","debounceTimer","notify","clearTimeout","setTimeout","watcher","close","workbenchLockSchema","readWorkbenchLock","lockPath","contents","undefined","parseLockContents","pruneWorkbenchLock","acquireWorkbenchLock","info","retries","lockData","flag","disk","updatePort","err","Error","message","String","isNodeError","code","existing"],"mappings":"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"}
@@ -1,17 +1,8 @@
1
1
  import { styleText } from 'node:util';
2
+ import { createServerLifecycle, toDisplayHost } from '../../util/serverOrchestration.js';
2
3
  import { startAppServerSupervisor } from './appServerSupervisor.js';
3
4
  import { startDevServerRegistration } from './startDevServerRegistration.js';
4
5
  import { startWorkbenchDevServer, startWorkbenchRemoteCoordinator } from './startWorkbenchDevServer.js';
5
- /** How long teardown runs before re-raising the signal to force-exit — enough for
6
- * Vite/watchers to close, short enough not to strand a backgrounded process. */ const SHUTDOWN_GRACE_MS = 5000;
7
- // Bind-only addresses ('0.0.0.0', '::') aren't routable in every browser (notably
8
- // Windows); the displayed URL falls back to localhost. The bind address is untouched.
9
- function toDisplayHost(host) {
10
- if (!host || host === '0.0.0.0' || host === '::' || host === '[::]') {
11
- return 'localhost';
12
- }
13
- return host;
14
- }
15
6
  /**
16
7
  * Orchestrate the dev servers a workbench project needs: a singleton workbench
17
8
  * Vite server plus the app/studio dev server it renders, wired to the dev-server
@@ -51,17 +42,13 @@ function toDisplayHost(host) {
51
42
  }
52
43
  // Unwound in reverse on any failure or on close(): the watcher stops before
53
44
  // the app server, and the supervisor waits out an in-flight rebuild.
54
- const closers = [];
55
- const disposeAll = async ()=>{
56
- for (const close of closers.splice(0).toReversed()){
57
- await close().catch(()=>{});
58
- }
59
- };
45
+ const { close, closers, installSignalHandlers } = createServerLifecycle();
60
46
  const workbench = await startWorkbenchDevServer({
61
47
  cacheDir,
62
48
  cliConfig,
63
49
  httpHost,
64
50
  httpPort,
51
+ mode: 'development',
65
52
  output,
66
53
  reactStrictMode,
67
54
  workDir
@@ -71,15 +58,6 @@ function toDisplayHost(host) {
71
58
  // Without one it claims the configured port and announces its own URL.
72
59
  const appPort = workbench.workbenchAvailable ? workbench.workbenchPort + 1 : httpPort;
73
60
  const announceUrl = !workbench.workbenchAvailable;
74
- let closing;
75
- const close = ()=>{
76
- closing ??= (async ()=>{
77
- process.off('SIGINT', onSignal);
78
- process.off('SIGTERM', onSignal);
79
- await disposeAll();
80
- })();
81
- return closing;
82
- };
83
61
  const supervised = await startAppServerSupervisor({
84
62
  cliConfig,
85
63
  start: (config)=>startAppServer({
@@ -89,7 +67,7 @@ function toDisplayHost(host) {
89
67
  }),
90
68
  workDir
91
69
  }).catch(async (err)=>{
92
- await disposeAll();
70
+ await close();
93
71
  throw err;
94
72
  });
95
73
  if (!supervised.started) {
@@ -118,7 +96,7 @@ function toDisplayHost(host) {
118
96
  } catch (err) {
119
97
  // Registration runs after both servers are up; a failure here would leak the
120
98
  // workbench lock and dev servers without this teardown.
121
- await disposeAll();
99
+ await close();
122
100
  throw err;
123
101
  }
124
102
  if (workbench.workbenchAvailable) {
@@ -130,20 +108,7 @@ function toDisplayHost(host) {
130
108
  'underline'
131
109
  ], workbenchUrl)} (app on port ${port})`);
132
110
  }
133
- // Trapping the signal disables Node's default exit, and a finished teardown
134
- // doesn't guarantee an empty event loop (keep-alive sockets, an extraction
135
- // worker mid-run) — so re-raise after teardown to restore conventional signal
136
- // exit semantics. A backstop timer force-exits if teardown wedges.
137
- function onSignal(signal) {
138
- const graceTimer = setTimeout(()=>process.kill(process.pid, signal), SHUTDOWN_GRACE_MS);
139
- graceTimer.unref();
140
- void close().finally(()=>{
141
- clearTimeout(graceTimer);
142
- process.kill(process.pid, signal);
143
- });
144
- }
145
- process.once('SIGINT', onSignal);
146
- process.once('SIGTERM', onSignal);
111
+ installSignalHandlers();
147
112
  return {
148
113
  close
149
114
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/dev/startWorkbenchDev.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {type CliConfig, type Output} from '@sanity/cli-core'\n\nimport {type AppServerResult, startAppServerSupervisor} from './appServerSupervisor.js'\nimport {type DevServerManifest} from './registry.js'\nimport {startDevServerRegistration} from './startDevServerRegistration.js'\nimport {\n startWorkbenchDevServer,\n startWorkbenchRemoteCoordinator,\n} from './startWorkbenchDevServer.js'\n\n/** How long teardown runs before re-raising the signal to force-exit — enough for\n * Vite/watchers to close, short enough not to strand a backgrounded process. */\nconst SHUTDOWN_GRACE_MS = 5000\n\n// Bind-only addresses ('0.0.0.0', '::') aren't routable in every browser (notably\n// Windows); the displayed URL falls back to localhost. The bind address is untouched.\nfunction toDisplayHost(host: string | undefined): string {\n if (!host || host === '0.0.0.0' || host === '::' || host === '[::]') {\n return 'localhost'\n }\n return host\n}\n\nexport interface StartWorkbenchDevOptions {\n /** Resolved app id for the registry entry (the CLI owns id resolution). */\n appId: string | undefined\n /** Directory for the workbench Vite server's dependency cache. */\n cacheDir: string\n /** CLI-domain `app.id`/`deployment.appId` deprecation check, run before registering. */\n checkForDeprecatedAppId: () => void\n cliConfig: CliConfig\n /** Extract the project manifest to inline into the registry (studio-vs-app handled by the CLI). */\n extractManifest: (params: {\n configPath: string\n workDir: string\n }) => Promise<DevServerManifest['manifest']>\n httpHost: string | undefined\n httpPort: number\n isApp: boolean\n output: Output\n reactStrictMode: boolean\n /** Start the app/studio dev server — the CLI owns the server, this orchestrates it. */\n startAppServer: (params: {\n announceUrl: boolean\n cliConfig: CliConfig\n httpPort: number\n }) => Promise<AppServerResult>\n workDir: string\n}\n\n/**\n * Orchestrate the dev servers a workbench project needs: a singleton workbench\n * Vite server plus the app/studio dev server it renders, wired to the dev-server\n * registry so view/service edits re-sync live.\n *\n * A running workbench claims the configured port, so the app server binds the\n * next one. If the workbench can't start (package or port unavailable), the app\n * server falls back to the configured port and announces its own URL, exactly as\n * a plain `sanity dev` would.\n */\nexport async function startWorkbenchDev(\n options: StartWorkbenchDevOptions,\n): Promise<{close: () => Promise<void>}> {\n const {\n appId,\n cacheDir,\n checkForDeprecatedAppId,\n cliConfig,\n extractManifest,\n httpHost,\n httpPort,\n isApp,\n output,\n reactStrictMode,\n startAppServer,\n workDir,\n } = options\n\n // The remote can't render itself, so it runs as a plain app server (not the\n // shell) that still claims the lock and bridges the registry, so app\n // `sanity dev`s register into it.\n if (process.env.SANITY_INTERNAL_IS_WORKBENCH_REMOTE === 'true') {\n const remote = await startAppServer({announceUrl: true, cliConfig, httpPort})\n if (!remote.started) return {close: async () => {}}\n\n const addr = remote.server.httpServer?.address()\n const port =\n (typeof addr === 'object' && addr ? addr.port : remote.server.config.server.port) ?? httpPort\n const coordinator = startWorkbenchRemoteCoordinator({httpHost, port, server: remote.server})\n\n return {\n close: async () => {\n await coordinator.close()\n await remote.close()\n },\n }\n }\n\n // Unwound in reverse on any failure or on close(): the watcher stops before\n // the app server, and the supervisor waits out an in-flight rebuild.\n const closers: Array<() => Promise<void>> = []\n const disposeAll = async () => {\n for (const close of closers.splice(0).toReversed()) {\n await close().catch(() => {})\n }\n }\n\n const workbench = await startWorkbenchDevServer({\n cacheDir,\n cliConfig,\n httpHost,\n httpPort,\n output,\n reactStrictMode,\n workDir,\n })\n closers.push(workbench.close)\n\n // A running workbench owns the configured port; the app server takes the next.\n // Without one it claims the configured port and announces its own URL.\n const appPort = workbench.workbenchAvailable ? workbench.workbenchPort + 1 : httpPort\n const announceUrl = !workbench.workbenchAvailable\n\n let closing: Promise<void> | undefined\n const close = () => {\n closing ??= (async () => {\n process.off('SIGINT', onSignal)\n process.off('SIGTERM', onSignal)\n await disposeAll()\n })()\n return closing\n }\n\n const supervised = await startAppServerSupervisor({\n cliConfig,\n start: (config) => startAppServer({announceUrl, cliConfig: config, httpPort: appPort}),\n workDir,\n }).catch(async (err) => {\n await disposeAll()\n throw err\n })\n\n if (!supervised.started) {\n // The app server already reported why (e.g. missing organization id). Hand\n // back a close that releases the workbench lock; nothing else came up.\n return {close}\n }\n const {supervisor} = supervised\n closers.push(supervisor.close)\n\n try {\n // The deprecated-id check and manifest extractor are CLI-domain, injected here.\n checkForDeprecatedAppId()\n const registration = await startDevServerRegistration({\n appId,\n cliConfig,\n extractManifest,\n isApp,\n onInterfaceSetChange: () => supervisor.rebuild(),\n output,\n server: supervisor.server,\n workDir,\n })\n closers.push(registration.close)\n } catch (err) {\n // Registration runs after both servers are up; a failure here would leak the\n // workbench lock and dev servers without this teardown.\n await disposeAll()\n throw err\n }\n\n if (workbench.workbenchAvailable) {\n const workbenchUrl = `http://${toDisplayHost(workbench.httpHost)}:${workbench.workbenchPort}`\n const addr = supervisor.server.httpServer?.address()\n const port = typeof addr === 'object' && addr ? addr.port : supervisor.server.config.server.port\n output.log(\n `Workbench dev server started at ${styleText(['blue', 'underline'], workbenchUrl)} (app on port ${port})`,\n )\n }\n\n // Trapping the signal disables Node's default exit, and a finished teardown\n // doesn't guarantee an empty event loop (keep-alive sockets, an extraction\n // worker mid-run) — so re-raise after teardown to restore conventional signal\n // exit semantics. A backstop timer force-exits if teardown wedges.\n function onSignal(signal: NodeJS.Signals) {\n const graceTimer = setTimeout(() => process.kill(process.pid, signal), SHUTDOWN_GRACE_MS)\n graceTimer.unref()\n void close().finally(() => {\n clearTimeout(graceTimer)\n process.kill(process.pid, signal)\n })\n }\n process.once('SIGINT', onSignal)\n process.once('SIGTERM', onSignal)\n\n return {close}\n}\n"],"names":["styleText","startAppServerSupervisor","startDevServerRegistration","startWorkbenchDevServer","startWorkbenchRemoteCoordinator","SHUTDOWN_GRACE_MS","toDisplayHost","host","startWorkbenchDev","options","appId","cacheDir","checkForDeprecatedAppId","cliConfig","extractManifest","httpHost","httpPort","isApp","output","reactStrictMode","startAppServer","workDir","process","env","SANITY_INTERNAL_IS_WORKBENCH_REMOTE","remote","announceUrl","started","close","addr","server","httpServer","address","port","config","coordinator","closers","disposeAll","splice","toReversed","catch","workbench","push","appPort","workbenchAvailable","workbenchPort","closing","off","onSignal","supervised","start","err","supervisor","registration","onInterfaceSetChange","rebuild","workbenchUrl","log","signal","graceTimer","setTimeout","kill","pid","unref","finally","clearTimeout","once"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/actions/dev/startWorkbenchDev.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {type CliConfig, type Output} from '@sanity/cli-core'\n\nimport {createServerLifecycle, toDisplayHost} from '../../util/serverOrchestration.js'\nimport {type AppServerResult, startAppServerSupervisor} from './appServerSupervisor.js'\nimport {type DevServerManifest} from './registry.js'\nimport {startDevServerRegistration} from './startDevServerRegistration.js'\nimport {\n startWorkbenchDevServer,\n startWorkbenchRemoteCoordinator,\n} from './startWorkbenchDevServer.js'\n\nexport interface StartWorkbenchDevOptions {\n /** Resolved app id for the registry entry (the CLI owns id resolution). */\n appId: string | undefined\n /** Directory for the workbench Vite server's dependency cache. */\n cacheDir: string\n /** CLI-domain `app.id`/`deployment.appId` deprecation check, run before registering. */\n checkForDeprecatedAppId: () => void\n cliConfig: CliConfig\n /** Extract the project manifest to inline into the registry (studio-vs-app handled by the CLI). */\n extractManifest: (params: {\n configPath: string\n workDir: string\n }) => Promise<DevServerManifest['manifest']>\n httpHost: string | undefined\n httpPort: number\n isApp: boolean\n output: Output\n reactStrictMode: boolean\n /** Start the app/studio dev server — the CLI owns the server, this orchestrates it. */\n startAppServer: (params: {\n announceUrl: boolean\n cliConfig: CliConfig\n httpPort: number\n }) => Promise<AppServerResult>\n workDir: string\n}\n\n/**\n * Orchestrate the dev servers a workbench project needs: a singleton workbench\n * Vite server plus the app/studio dev server it renders, wired to the dev-server\n * registry so view/service edits re-sync live.\n *\n * A running workbench claims the configured port, so the app server binds the\n * next one. If the workbench can't start (package or port unavailable), the app\n * server falls back to the configured port and announces its own URL, exactly as\n * a plain `sanity dev` would.\n */\nexport async function startWorkbenchDev(\n options: StartWorkbenchDevOptions,\n): Promise<{close: () => Promise<void>}> {\n const {\n appId,\n cacheDir,\n checkForDeprecatedAppId,\n cliConfig,\n extractManifest,\n httpHost,\n httpPort,\n isApp,\n output,\n reactStrictMode,\n startAppServer,\n workDir,\n } = options\n\n // The remote can't render itself, so it runs as a plain app server (not the\n // shell) that still claims the lock and bridges the registry, so app\n // `sanity dev`s register into it.\n if (process.env.SANITY_INTERNAL_IS_WORKBENCH_REMOTE === 'true') {\n const remote = await startAppServer({announceUrl: true, cliConfig, httpPort})\n if (!remote.started) return {close: async () => {}}\n\n const addr = remote.server.httpServer?.address()\n const port =\n (typeof addr === 'object' && addr ? addr.port : remote.server.config.server.port) ?? httpPort\n const coordinator = startWorkbenchRemoteCoordinator({httpHost, port, server: remote.server})\n\n return {\n close: async () => {\n await coordinator.close()\n await remote.close()\n },\n }\n }\n\n // Unwound in reverse on any failure or on close(): the watcher stops before\n // the app server, and the supervisor waits out an in-flight rebuild.\n const {close, closers, installSignalHandlers} = createServerLifecycle()\n\n const workbench = await startWorkbenchDevServer({\n cacheDir,\n cliConfig,\n httpHost,\n httpPort,\n mode: 'development',\n output,\n reactStrictMode,\n workDir,\n })\n closers.push(workbench.close)\n\n // A running workbench owns the configured port; the app server takes the next.\n // Without one it claims the configured port and announces its own URL.\n const appPort = workbench.workbenchAvailable ? workbench.workbenchPort + 1 : httpPort\n const announceUrl = !workbench.workbenchAvailable\n\n const supervised = await startAppServerSupervisor({\n cliConfig,\n start: (config) => startAppServer({announceUrl, cliConfig: config, httpPort: appPort}),\n workDir,\n }).catch(async (err) => {\n await close()\n throw err\n })\n\n if (!supervised.started) {\n // The app server already reported why (e.g. missing organization id). Hand\n // back a close that releases the workbench lock; nothing else came up.\n return {close}\n }\n const {supervisor} = supervised\n closers.push(supervisor.close)\n\n try {\n // The deprecated-id check and manifest extractor are CLI-domain, injected here.\n checkForDeprecatedAppId()\n const registration = await startDevServerRegistration({\n appId,\n cliConfig,\n extractManifest,\n isApp,\n onInterfaceSetChange: () => supervisor.rebuild(),\n output,\n server: supervisor.server,\n workDir,\n })\n closers.push(registration.close)\n } catch (err) {\n // Registration runs after both servers are up; a failure here would leak the\n // workbench lock and dev servers without this teardown.\n await close()\n throw err\n }\n\n if (workbench.workbenchAvailable) {\n const workbenchUrl = `http://${toDisplayHost(workbench.httpHost)}:${workbench.workbenchPort}`\n const addr = supervisor.server.httpServer?.address()\n const port = typeof addr === 'object' && addr ? addr.port : supervisor.server.config.server.port\n output.log(\n `Workbench dev server started at ${styleText(['blue', 'underline'], workbenchUrl)} (app on port ${port})`,\n )\n }\n\n installSignalHandlers()\n\n return {close}\n}\n"],"names":["styleText","createServerLifecycle","toDisplayHost","startAppServerSupervisor","startDevServerRegistration","startWorkbenchDevServer","startWorkbenchRemoteCoordinator","startWorkbenchDev","options","appId","cacheDir","checkForDeprecatedAppId","cliConfig","extractManifest","httpHost","httpPort","isApp","output","reactStrictMode","startAppServer","workDir","process","env","SANITY_INTERNAL_IS_WORKBENCH_REMOTE","remote","announceUrl","started","close","addr","server","httpServer","address","port","config","coordinator","closers","installSignalHandlers","workbench","mode","push","appPort","workbenchAvailable","workbenchPort","supervised","start","catch","err","supervisor","registration","onInterfaceSetChange","rebuild","workbenchUrl","log"],"mappings":"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"}
@@ -86,7 +86,7 @@ const toApplicationsPayload = (servers)=>({
86
86
  * workbench instance or an unrelated process — fall back to running without a
87
87
  * workbench and let the app/studio dev server claim the configured port.
88
88
  */ export async function startWorkbenchDevServer(options) {
89
- const { cacheDir, cliConfig, httpHost, httpPort: workbenchPort, output, reactStrictMode, workDir } = options;
89
+ const { cacheDir, cliConfig, httpHost, httpPort: workbenchPort, mode, output, reactStrictMode, workDir } = options;
90
90
  // Workbench is opted into solely by calling `unstable_defineApp`.
91
91
  if (!isWorkbenchApp(cliConfig?.app)) {
92
92
  devDebug('Not a workbench app, skipping workbench dev server');
@@ -138,6 +138,7 @@ const toApplicationsPayload = (servers)=>({
138
138
  cacheDir,
139
139
  cliConfig,
140
140
  httpHost,
141
+ mode,
141
142
  output,
142
143
  reactStrictMode,
143
144
  workbenchPort,
@@ -169,8 +170,11 @@ const toApplicationsPayload = (servers)=>({
169
170
  };
170
171
  }
171
172
  async function createWorkbenchViteServer(options) {
172
- const { cacheDir, cliConfig, httpHost, output, reactStrictMode, workbenchPort, workDir } = options;
173
- const remoteUrl = parseRemoteUrl(process.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL);
173
+ const { cacheDir, cliConfig, httpHost, mode, output, reactStrictMode, workbenchPort, workDir } = options;
174
+ // `preview` loads `.env.development` (the env hook treats only `build`/`deploy`
175
+ // as production), which points the workbench UI at a local dev server that
176
+ // isn't running here. Ignore the override and load the deployed UI instead.
177
+ const remoteUrl = mode === 'preview' ? undefined : parseRemoteUrl(process.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL);
174
178
  const organizationId = resolveOrganizationId(cliConfig);
175
179
  devDebug('Writing workbench runtime files');
176
180
  const root = await writeWorkbenchRuntime({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/dev/startWorkbenchDevServer.ts"],"sourcesContent":["import {type CliConfig, type Output, resolveLocalPackage, subdebug} from '@sanity/cli-core'\nimport viteReact from '@vitejs/plugin-react'\nimport {createServer, type InlineConfig, type Plugin, type ViteDevServer} from 'vite'\nimport {z} from 'zod/mini'\n\nimport {isWorkbenchApp} from '../../defineApp.js'\nimport {createExposesTracker} from './exposesSetId.js'\nimport {\n acquireWorkbenchLock,\n type DevServerManifest,\n getRegisteredServers,\n readWorkbenchLock,\n watchRegistry,\n} from './registry.js'\nimport {writeWorkbenchRuntime} from './writeWorkbenchRuntime.js'\n\nconst devDebug = subdebug('dev')\n\nconst noop = async () => {}\n\n// Every server is a local app except a config-only one — an config\n// with no interfaces (the media library). A server with both lands in both channels.\nconst isLocalApp = (server: DevServerManifest): boolean => {\n const configOnly = Boolean(server.configs?.length) && !server.interfaces?.length\n return !configOnly\n}\n\nconst toApplicationsPayload = (servers: DevServerManifest[]) => ({\n applications: servers\n .filter((server) => isLocalApp(server))\n .map(({host, id, interfaces, manifest, port, projectId, type}) => ({\n host,\n id,\n interfaces,\n manifest,\n port,\n projectId,\n type,\n })),\n configs: servers.flatMap(({configs, host, port}) =>\n // The registry stores the config flat; the workbench wire shape nests the\n // type-specific payload (`fields` for a media library) under `config`, keyed\n // by the `appType` discriminator.\n (configs ?? []).map(({appType, id, moduleName, version, ...config}) => ({\n appType,\n config,\n id,\n moduleName,\n remoteURL: `http://${host}:${port}`,\n version,\n })),\n ),\n})\n\n/**\n * Bridge the dev-server registry into a workbench Vite server's HMR channel so\n * the page tracks apps as they come and go. A changed interface set means a\n * rebuilt remote — full-reload to drop the stale remote-entry; otherwise\n * rebroadcast for a soft reconcile. Returns a detach fn.\n */\nfunction attachViteDevServerBridge(server: ViteDevServer): () => void {\n server.ws.on('sanity:workbench:get-local-applications', (_, client) => {\n client.send(\n 'sanity:workbench:local-applications',\n toApplicationsPayload(getRegisteredServers()),\n )\n })\n\n const setTracker = createExposesTracker()\n const registryWatcher = watchRegistry((servers) => {\n if (setTracker.hasChanged(servers)) {\n server.ws.send({type: 'full-reload'})\n return\n }\n server.ws.send('sanity:workbench:local-applications', toApplicationsPayload(servers))\n })\n\n return () => registryWatcher.close()\n}\n\n/**\n * Make the workbench remote act as the machine's workbench: claim the singleton\n * lock so app `sanity dev`s register into it instead of each starting their own,\n * and bridge the registry so the remote shows the local apps. No-op lock if one\n * is already held.\n */\nexport function startWorkbenchRemoteCoordinator(options: {\n httpHost: string | undefined\n port: number\n server: ViteDevServer\n}): {close: () => Promise<void>} {\n const {httpHost, port, server} = options\n\n const lock = acquireWorkbenchLock({host: httpHost || 'localhost', port})\n if (!lock) {\n const existing = readWorkbenchLock()\n devDebug(\n 'Workbench lock already held by pid %d on port %d; bridging the registry without claiming it',\n existing?.pid,\n existing?.port,\n )\n }\n\n const detachBridge = attachViteDevServerBridge(server)\n\n return {\n close: async () => {\n detachBridge()\n lock?.release()\n },\n }\n}\n\ninterface WorkbenchDevServerResult {\n close: () => Promise<void>\n httpHost: string | undefined\n workbenchAvailable: boolean\n workbenchPort: number\n}\n\nexport interface StartWorkbenchOptions {\n /** Dependency-cache dir for the workbench Vite server, kept apart from the user's own. */\n cacheDir: string\n cliConfig: CliConfig\n httpHost: string | undefined\n httpPort: number\n output: Output\n /** Wrap the workbench in React StrictMode; the CLI resolves it (unset collapses to `false`). */\n reactStrictMode: boolean\n workDir: string\n}\n\n/**\n * Start the workbench dev server when federation is enabled and the workbench\n * package is available. If the desired port is already taken — by another\n * workbench instance or an unrelated process — fall back to running without a\n * workbench and let the app/studio dev server claim the configured port.\n */\nexport async function startWorkbenchDevServer(\n options: StartWorkbenchOptions,\n): Promise<WorkbenchDevServerResult> {\n const {\n cacheDir,\n cliConfig,\n httpHost,\n httpPort: workbenchPort,\n output,\n reactStrictMode,\n workDir,\n } = options\n\n // Workbench is opted into solely by calling `unstable_defineApp`.\n if (!isWorkbenchApp(cliConfig?.app)) {\n devDebug('Not a workbench app, skipping workbench dev server')\n return {close: noop, httpHost, workbenchAvailable: false, workbenchPort}\n }\n\n let workbenchAvailable = false\n\n try {\n await resolveLocalPackage('sanity/workbench', workDir)\n workbenchAvailable = true\n } catch {\n devDebug('Workbench not available, skipping workbench dev server')\n }\n\n if (!workbenchAvailable) {\n return {close: noop, httpHost, workbenchAvailable, workbenchPort}\n }\n\n // Acquire an exclusive lock — only one workbench per machine.\n // Uses O_EXCL which is atomic at the OS level, preventing races when\n // multiple `sanity dev` processes start simultaneously (e.g. via turbo).\n const workbenchLock = acquireWorkbenchLock({host: httpHost || 'localhost', port: workbenchPort})\n if (!workbenchLock) {\n const existing = readWorkbenchLock()\n devDebug(\n 'Workbench already running at pid %d on port %d, skipping',\n existing?.pid,\n existing?.port,\n )\n return {\n close: noop,\n httpHost: existing?.host ?? httpHost,\n workbenchAvailable: true,\n workbenchPort: existing?.port ?? workbenchPort,\n }\n }\n\n // The lock is already held; an exception here (runtime-file write failure,\n // invalid remote URL) would otherwise leak it until the next acquire prunes\n // the stale PID.\n let result: Awaited<ReturnType<typeof createWorkbenchViteServer>>\n try {\n result = await createWorkbenchViteServer({\n cacheDir,\n cliConfig,\n httpHost,\n output,\n reactStrictMode,\n workbenchPort,\n workDir,\n })\n } catch (err) {\n workbenchLock.release()\n throw err\n }\n\n if (!result) {\n workbenchLock.release()\n return {close: noop, httpHost, workbenchAvailable: false, workbenchPort}\n }\n\n const {actualPort, close} = result\n workbenchLock.updatePort(actualPort)\n\n return {\n close: async () => {\n workbenchLock.release()\n await close()\n },\n httpHost,\n workbenchAvailable,\n workbenchPort: actualPort,\n }\n}\n\ninterface CreateWorkbenchViteServerOptions {\n cacheDir: string\n cliConfig: CliConfig\n httpHost: string | undefined\n output: Output\n reactStrictMode: boolean\n workbenchPort: number\n workDir: string\n}\n\ninterface CreateWorkbenchViteServerResult {\n actualPort: number\n close: () => Promise<void>\n}\n\nasync function createWorkbenchViteServer(\n options: CreateWorkbenchViteServerOptions,\n): Promise<CreateWorkbenchViteServerResult | undefined> {\n const {cacheDir, cliConfig, httpHost, output, reactStrictMode, workbenchPort, workDir} = options\n\n const remoteUrl = parseRemoteUrl(process.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL)\n\n const organizationId = resolveOrganizationId(cliConfig)\n\n devDebug('Writing workbench runtime files')\n const root = await writeWorkbenchRuntime({\n cwd: workDir,\n organizationId,\n reactStrictMode,\n remoteUrl,\n })\n\n const viteConfig: InlineConfig = {\n // Custom cache directory so sanity's vite cache doesn't conflict with local vite projects\n cacheDir,\n configFile: false,\n define: {\n __SANITY_STAGING__: process.env.SANITY_INTERNAL_ENV === 'staging',\n 'import.meta.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL': JSON.stringify(remoteUrl),\n },\n logLevel: 'warn',\n mode: 'development',\n optimizeDeps: {\n // Exclude sanity/workbench (and its transitive dep @sanity/workbench)\n // from dep pre-bundling so that `import.meta.hot` is available at\n // runtime — pre-bundled modules do not receive Vite's HMR client\n // injection, which causes the custom HMR events for local application\n // discovery to silently not fire.\n exclude: ['sanity', '@sanity/workbench'],\n },\n // viteReact looks inert here — it transforms none of the host's own modules —\n // but it's load-bearing for the remotes. It serves the Fast Refresh runtime at\n // /@react-refresh and injects the preamble that defines window.$RefreshReg$. The\n // federated remotes loaded into this page are react-refresh transformed, so\n // without the preamble they throw \"can't detect preamble\", and without the\n // runtime their /@react-refresh import (wired by @module-federation/vite's\n // remoteHmr) fails. Dropping it as dead code broke every panel; see #1262.\n plugins: [viteReact(), ...(remoteUrl ? [remoteManifestPreloadHeaderPlugin(remoteUrl)] : [])],\n resolve: {dedupe: ['react', 'react-dom']},\n root,\n server: {\n host: httpHost,\n port: workbenchPort,\n strictPort: false,\n warmup: {\n clientFiles: ['./workbench.js'],\n },\n },\n }\n\n devDebug('Creating workbench vite server')\n const server = await createServer(viteConfig)\n try {\n await server.listen()\n } catch (err) {\n await server.close()\n output.warn(\n `Workbench dev server failed to start: ${err instanceof Error ? err.message : String(err)}`,\n )\n return undefined\n }\n\n // Vite may have picked a different port if the desired one was occupied\n const addr = server.httpServer?.address()\n const actualPort = typeof addr === 'object' && addr ? addr.port : workbenchPort\n\n // Fire-and-forget: warm the workbench remote's Vite transform pipeline so\n // the first browser request hits a pre-populated module graph.\n if (remoteUrl) {\n fetch(remoteUrl)\n .then((r) => r.body?.cancel())\n .catch(() => {})\n devDebug('Warming workbench remote at %s', remoteUrl)\n }\n\n const detachBridge = attachViteDevServerBridge(server)\n\n return {\n actualPort,\n close: async () => {\n detachBridge()\n await server.close()\n },\n }\n}\n\n// Workbench is opted into via `unstable_defineApp`, which carries the\n// organization ID. Deliberately no fallback (e.g. resolving it from the\n// configured project): the lookup would need an authenticated user and an\n// API round-trip on every startup for something the opt-in already declares.\nconst resolveOrganizationId = (cliConfig: CliConfig): string => {\n if (cliConfig.app?.organizationId) {\n return cliConfig.app.organizationId\n }\n\n throw new Error(\n 'Workbench requires an organization ID. Pass \"organizationId\" to unstable_defineApp() in sanity.cli.ts.',\n )\n}\n\n// Restricts protocol to http(s) so the URL is safe to interpolate into HTML\n// attributes and Link headers downstream.\nconst remoteUrlSchema = z.url({normalize: true, protocol: /^https?$/})\n\nfunction parseRemoteUrl(value: string | undefined): string | undefined {\n if (!value) return undefined\n\n const result = remoteUrlSchema.safeParse(value)\n\n if (!result.success) {\n throw new Error(\n `Invalid SANITY_INTERNAL_WORKBENCH_REMOTE_URL: ${value} (must be an http(s) URL)`,\n )\n }\n\n return result.data\n}\n\n/**\n * Sets a `Link: <remoteUrl>; rel=preload; as=fetch; crossorigin` response header\n * on the index document so the browser can start fetching the Module Federation\n * manifest as soon as response headers arrive — before HTML parsing reaches the\n * in-head preconnect hint. `as=fetch` matches how the federation runtime later\n * retrieves the JSON manifest, allowing the preload entry to satisfy that fetch.\n */\nfunction remoteManifestPreloadHeaderPlugin(remoteUrl: string): Plugin {\n return {\n apply: 'serve',\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const pathname = (req.url || '/').split('?')[0]\n if (pathname === '/' || pathname === '/index.html') {\n res.setHeader('Link', `<${remoteUrl}>; rel=preload; as=fetch; crossorigin`)\n }\n next()\n })\n },\n name: 'sanity:workbench-remote-preload-header',\n }\n}\n"],"names":["resolveLocalPackage","subdebug","viteReact","createServer","z","isWorkbenchApp","createExposesTracker","acquireWorkbenchLock","getRegisteredServers","readWorkbenchLock","watchRegistry","writeWorkbenchRuntime","devDebug","noop","isLocalApp","server","configOnly","Boolean","configs","length","interfaces","toApplicationsPayload","servers","applications","filter","map","host","id","manifest","port","projectId","type","flatMap","appType","moduleName","version","config","remoteURL","attachViteDevServerBridge","ws","on","_","client","send","setTracker","registryWatcher","hasChanged","close","startWorkbenchRemoteCoordinator","options","httpHost","lock","existing","pid","detachBridge","release","startWorkbenchDevServer","cacheDir","cliConfig","httpPort","workbenchPort","output","reactStrictMode","workDir","app","workbenchAvailable","workbenchLock","result","createWorkbenchViteServer","err","actualPort","updatePort","remoteUrl","parseRemoteUrl","process","env","SANITY_INTERNAL_WORKBENCH_REMOTE_URL","organizationId","resolveOrganizationId","root","cwd","viteConfig","configFile","define","__SANITY_STAGING__","SANITY_INTERNAL_ENV","JSON","stringify","logLevel","mode","optimizeDeps","exclude","plugins","remoteManifestPreloadHeaderPlugin","resolve","dedupe","strictPort","warmup","clientFiles","listen","warn","Error","message","String","undefined","addr","httpServer","address","fetch","then","r","body","cancel","catch","remoteUrlSchema","url","normalize","protocol","value","safeParse","success","data","apply","configureServer","middlewares","use","req","res","next","pathname","split","setHeader","name"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/actions/dev/startWorkbenchDevServer.ts"],"sourcesContent":["import {type CliConfig, type Output, resolveLocalPackage, subdebug} from '@sanity/cli-core'\nimport viteReact from '@vitejs/plugin-react'\nimport {createServer, type InlineConfig, type Plugin, type ViteDevServer} from 'vite'\nimport {z} from 'zod/mini'\n\nimport {isWorkbenchApp} from '../../defineApp.js'\nimport {createExposesTracker} from './exposesSetId.js'\nimport {\n acquireWorkbenchLock,\n type DevServerManifest,\n getRegisteredServers,\n readWorkbenchLock,\n watchRegistry,\n} from './registry.js'\nimport {writeWorkbenchRuntime} from './writeWorkbenchRuntime.js'\n\nconst devDebug = subdebug('dev')\n\nconst noop = async () => {}\n\n// Every server is a local app except a config-only one — an config\n// with no interfaces (the media library). A server with both lands in both channels.\nconst isLocalApp = (server: DevServerManifest): boolean => {\n const configOnly = Boolean(server.configs?.length) && !server.interfaces?.length\n return !configOnly\n}\n\nconst toApplicationsPayload = (servers: DevServerManifest[]) => ({\n applications: servers\n .filter((server) => isLocalApp(server))\n .map(({host, id, interfaces, manifest, port, projectId, type}) => ({\n host,\n id,\n interfaces,\n manifest,\n port,\n projectId,\n type,\n })),\n configs: servers.flatMap(({configs, host, port}) =>\n // The registry stores the config flat; the workbench wire shape nests the\n // type-specific payload (`fields` for a media library) under `config`, keyed\n // by the `appType` discriminator.\n (configs ?? []).map(({appType, id, moduleName, version, ...config}) => ({\n appType,\n config,\n id,\n moduleName,\n remoteURL: `http://${host}:${port}`,\n version,\n })),\n ),\n})\n\n/**\n * Bridge the dev-server registry into a workbench Vite server's HMR channel so\n * the page tracks apps as they come and go. A changed interface set means a\n * rebuilt remote — full-reload to drop the stale remote-entry; otherwise\n * rebroadcast for a soft reconcile. Returns a detach fn.\n */\nfunction attachViteDevServerBridge(server: ViteDevServer): () => void {\n server.ws.on('sanity:workbench:get-local-applications', (_, client) => {\n client.send(\n 'sanity:workbench:local-applications',\n toApplicationsPayload(getRegisteredServers()),\n )\n })\n\n const setTracker = createExposesTracker()\n const registryWatcher = watchRegistry((servers) => {\n if (setTracker.hasChanged(servers)) {\n server.ws.send({type: 'full-reload'})\n return\n }\n server.ws.send('sanity:workbench:local-applications', toApplicationsPayload(servers))\n })\n\n return () => registryWatcher.close()\n}\n\n/**\n * Make the workbench remote act as the machine's workbench: claim the singleton\n * lock so app `sanity dev`s register into it instead of each starting their own,\n * and bridge the registry so the remote shows the local apps. No-op lock if one\n * is already held.\n */\nexport function startWorkbenchRemoteCoordinator(options: {\n httpHost: string | undefined\n port: number\n server: ViteDevServer\n}): {close: () => Promise<void>} {\n const {httpHost, port, server} = options\n\n const lock = acquireWorkbenchLock({host: httpHost || 'localhost', port})\n if (!lock) {\n const existing = readWorkbenchLock()\n devDebug(\n 'Workbench lock already held by pid %d on port %d; bridging the registry without claiming it',\n existing?.pid,\n existing?.port,\n )\n }\n\n const detachBridge = attachViteDevServerBridge(server)\n\n return {\n close: async () => {\n detachBridge()\n lock?.release()\n },\n }\n}\n\ninterface WorkbenchDevServerResult {\n close: () => Promise<void>\n httpHost: string | undefined\n workbenchAvailable: boolean\n workbenchPort: number\n}\n\nexport interface StartWorkbenchOptions {\n /** Dependency-cache dir for the workbench Vite server, kept apart from the user's own. */\n cacheDir: string\n cliConfig: CliConfig\n httpHost: string | undefined\n httpPort: number\n /** `dev` renders a live app and honors a local workbench-UI override; `preview`\n * (`sanity start`) previews a build and loads the deployed workbench UI. */\n mode: 'development' | 'preview'\n output: Output\n /** Wrap the workbench in React StrictMode; the CLI resolves it (unset collapses to `false`). */\n reactStrictMode: boolean\n workDir: string\n}\n\n/**\n * Start the workbench dev server when federation is enabled and the workbench\n * package is available. If the desired port is already taken — by another\n * workbench instance or an unrelated process — fall back to running without a\n * workbench and let the app/studio dev server claim the configured port.\n */\nexport async function startWorkbenchDevServer(\n options: StartWorkbenchOptions,\n): Promise<WorkbenchDevServerResult> {\n const {\n cacheDir,\n cliConfig,\n httpHost,\n httpPort: workbenchPort,\n mode,\n output,\n reactStrictMode,\n workDir,\n } = options\n\n // Workbench is opted into solely by calling `unstable_defineApp`.\n if (!isWorkbenchApp(cliConfig?.app)) {\n devDebug('Not a workbench app, skipping workbench dev server')\n return {close: noop, httpHost, workbenchAvailable: false, workbenchPort}\n }\n\n let workbenchAvailable = false\n\n try {\n await resolveLocalPackage('sanity/workbench', workDir)\n workbenchAvailable = true\n } catch {\n devDebug('Workbench not available, skipping workbench dev server')\n }\n\n if (!workbenchAvailable) {\n return {close: noop, httpHost, workbenchAvailable, workbenchPort}\n }\n\n // Acquire an exclusive lock — only one workbench per machine.\n // Uses O_EXCL which is atomic at the OS level, preventing races when\n // multiple `sanity dev` processes start simultaneously (e.g. via turbo).\n const workbenchLock = acquireWorkbenchLock({host: httpHost || 'localhost', port: workbenchPort})\n if (!workbenchLock) {\n const existing = readWorkbenchLock()\n devDebug(\n 'Workbench already running at pid %d on port %d, skipping',\n existing?.pid,\n existing?.port,\n )\n return {\n close: noop,\n httpHost: existing?.host ?? httpHost,\n workbenchAvailable: true,\n workbenchPort: existing?.port ?? workbenchPort,\n }\n }\n\n // The lock is already held; an exception here (runtime-file write failure,\n // invalid remote URL) would otherwise leak it until the next acquire prunes\n // the stale PID.\n let result: Awaited<ReturnType<typeof createWorkbenchViteServer>>\n try {\n result = await createWorkbenchViteServer({\n cacheDir,\n cliConfig,\n httpHost,\n mode,\n output,\n reactStrictMode,\n workbenchPort,\n workDir,\n })\n } catch (err) {\n workbenchLock.release()\n throw err\n }\n\n if (!result) {\n workbenchLock.release()\n return {close: noop, httpHost, workbenchAvailable: false, workbenchPort}\n }\n\n const {actualPort, close} = result\n workbenchLock.updatePort(actualPort)\n\n return {\n close: async () => {\n workbenchLock.release()\n await close()\n },\n httpHost,\n workbenchAvailable,\n workbenchPort: actualPort,\n }\n}\n\ninterface CreateWorkbenchViteServerOptions {\n cacheDir: string\n cliConfig: CliConfig\n httpHost: string | undefined\n mode: 'development' | 'preview'\n output: Output\n reactStrictMode: boolean\n workbenchPort: number\n workDir: string\n}\n\ninterface CreateWorkbenchViteServerResult {\n actualPort: number\n close: () => Promise<void>\n}\n\nasync function createWorkbenchViteServer(\n options: CreateWorkbenchViteServerOptions,\n): Promise<CreateWorkbenchViteServerResult | undefined> {\n const {cacheDir, cliConfig, httpHost, mode, output, reactStrictMode, workbenchPort, workDir} =\n options\n\n // `preview` loads `.env.development` (the env hook treats only `build`/`deploy`\n // as production), which points the workbench UI at a local dev server that\n // isn't running here. Ignore the override and load the deployed UI instead.\n const remoteUrl =\n mode === 'preview'\n ? undefined\n : parseRemoteUrl(process.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL)\n\n const organizationId = resolveOrganizationId(cliConfig)\n\n devDebug('Writing workbench runtime files')\n const root = await writeWorkbenchRuntime({\n cwd: workDir,\n organizationId,\n reactStrictMode,\n remoteUrl,\n })\n\n const viteConfig: InlineConfig = {\n // Custom cache directory so sanity's vite cache doesn't conflict with local vite projects\n cacheDir,\n configFile: false,\n define: {\n __SANITY_STAGING__: process.env.SANITY_INTERNAL_ENV === 'staging',\n 'import.meta.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL': JSON.stringify(remoteUrl),\n },\n logLevel: 'warn',\n mode: 'development',\n optimizeDeps: {\n // Exclude sanity/workbench (and its transitive dep @sanity/workbench)\n // from dep pre-bundling so that `import.meta.hot` is available at\n // runtime — pre-bundled modules do not receive Vite's HMR client\n // injection, which causes the custom HMR events for local application\n // discovery to silently not fire.\n exclude: ['sanity', '@sanity/workbench'],\n },\n // viteReact looks inert here — it transforms none of the host's own modules —\n // but it's load-bearing for the remotes. It serves the Fast Refresh runtime at\n // /@react-refresh and injects the preamble that defines window.$RefreshReg$. The\n // federated remotes loaded into this page are react-refresh transformed, so\n // without the preamble they throw \"can't detect preamble\", and without the\n // runtime their /@react-refresh import (wired by @module-federation/vite's\n // remoteHmr) fails. Dropping it as dead code broke every panel; see #1262.\n plugins: [viteReact(), ...(remoteUrl ? [remoteManifestPreloadHeaderPlugin(remoteUrl)] : [])],\n resolve: {dedupe: ['react', 'react-dom']},\n root,\n server: {\n host: httpHost,\n port: workbenchPort,\n strictPort: false,\n warmup: {\n clientFiles: ['./workbench.js'],\n },\n },\n }\n\n devDebug('Creating workbench vite server')\n const server = await createServer(viteConfig)\n try {\n await server.listen()\n } catch (err) {\n await server.close()\n output.warn(\n `Workbench dev server failed to start: ${err instanceof Error ? err.message : String(err)}`,\n )\n return undefined\n }\n\n // Vite may have picked a different port if the desired one was occupied\n const addr = server.httpServer?.address()\n const actualPort = typeof addr === 'object' && addr ? addr.port : workbenchPort\n\n // Fire-and-forget: warm the workbench remote's Vite transform pipeline so\n // the first browser request hits a pre-populated module graph.\n if (remoteUrl) {\n fetch(remoteUrl)\n .then((r) => r.body?.cancel())\n .catch(() => {})\n devDebug('Warming workbench remote at %s', remoteUrl)\n }\n\n const detachBridge = attachViteDevServerBridge(server)\n\n return {\n actualPort,\n close: async () => {\n detachBridge()\n await server.close()\n },\n }\n}\n\n// Workbench is opted into via `unstable_defineApp`, which carries the\n// organization ID. Deliberately no fallback (e.g. resolving it from the\n// configured project): the lookup would need an authenticated user and an\n// API round-trip on every startup for something the opt-in already declares.\nconst resolveOrganizationId = (cliConfig: CliConfig): string => {\n if (cliConfig.app?.organizationId) {\n return cliConfig.app.organizationId\n }\n\n throw new Error(\n 'Workbench requires an organization ID. Pass \"organizationId\" to unstable_defineApp() in sanity.cli.ts.',\n )\n}\n\n// Restricts protocol to http(s) so the URL is safe to interpolate into HTML\n// attributes and Link headers downstream.\nconst remoteUrlSchema = z.url({normalize: true, protocol: /^https?$/})\n\nfunction parseRemoteUrl(value: string | undefined): string | undefined {\n if (!value) return undefined\n\n const result = remoteUrlSchema.safeParse(value)\n\n if (!result.success) {\n throw new Error(\n `Invalid SANITY_INTERNAL_WORKBENCH_REMOTE_URL: ${value} (must be an http(s) URL)`,\n )\n }\n\n return result.data\n}\n\n/**\n * Sets a `Link: <remoteUrl>; rel=preload; as=fetch; crossorigin` response header\n * on the index document so the browser can start fetching the Module Federation\n * manifest as soon as response headers arrive — before HTML parsing reaches the\n * in-head preconnect hint. `as=fetch` matches how the federation runtime later\n * retrieves the JSON manifest, allowing the preload entry to satisfy that fetch.\n */\nfunction remoteManifestPreloadHeaderPlugin(remoteUrl: string): Plugin {\n return {\n apply: 'serve',\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const pathname = (req.url || '/').split('?')[0]\n if (pathname === '/' || pathname === '/index.html') {\n res.setHeader('Link', `<${remoteUrl}>; rel=preload; as=fetch; crossorigin`)\n }\n next()\n })\n },\n name: 'sanity:workbench-remote-preload-header',\n }\n}\n"],"names":["resolveLocalPackage","subdebug","viteReact","createServer","z","isWorkbenchApp","createExposesTracker","acquireWorkbenchLock","getRegisteredServers","readWorkbenchLock","watchRegistry","writeWorkbenchRuntime","devDebug","noop","isLocalApp","server","configOnly","Boolean","configs","length","interfaces","toApplicationsPayload","servers","applications","filter","map","host","id","manifest","port","projectId","type","flatMap","appType","moduleName","version","config","remoteURL","attachViteDevServerBridge","ws","on","_","client","send","setTracker","registryWatcher","hasChanged","close","startWorkbenchRemoteCoordinator","options","httpHost","lock","existing","pid","detachBridge","release","startWorkbenchDevServer","cacheDir","cliConfig","httpPort","workbenchPort","mode","output","reactStrictMode","workDir","app","workbenchAvailable","workbenchLock","result","createWorkbenchViteServer","err","actualPort","updatePort","remoteUrl","undefined","parseRemoteUrl","process","env","SANITY_INTERNAL_WORKBENCH_REMOTE_URL","organizationId","resolveOrganizationId","root","cwd","viteConfig","configFile","define","__SANITY_STAGING__","SANITY_INTERNAL_ENV","JSON","stringify","logLevel","optimizeDeps","exclude","plugins","remoteManifestPreloadHeaderPlugin","resolve","dedupe","strictPort","warmup","clientFiles","listen","warn","Error","message","String","addr","httpServer","address","fetch","then","r","body","cancel","catch","remoteUrlSchema","url","normalize","protocol","value","safeParse","success","data","apply","configureServer","middlewares","use","req","res","next","pathname","split","setHeader","name"],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import { subdebug } from '@sanity/cli-core';
2
+ import { preview } from 'vite';
3
+ import { checkBuiltOutput } from '../deploy/checkBuiltOutput.js';
4
+ const previewDebug = subdebug('preview');
5
+ /**
6
+ * Serve a built workbench app's `dist` as static files over a Vite `preview`
7
+ * server, so its `mf-manifest.json` + `remote-entry.js` are reachable exactly as
8
+ * a live `sanity dev` remote would be — the workbench then federates it in.
9
+ *
10
+ * A missing federation build surfaces as `BUILD_NOT_FOUND` (from
11
+ * `checkBuiltOutput`) so the command shows the same "run sanity build" hint the
12
+ * studio preview path does; real I/O errors surface as themselves.
13
+ */ export async function serveBuiltApplication(options) {
14
+ const { cacheDir, httpHost, httpPort, outDir, workDir } = options;
15
+ await checkBuiltOutput(outDir);
16
+ const config = {
17
+ // Serve at the root so `mf-manifest.json` and its `publicPath: 'auto'` chunks
18
+ // resolve under the registered remote URL.
19
+ base: '/',
20
+ // Needed for vite to serve `outDir` itself rather than `outDir/dist`.
21
+ build: {
22
+ outDir
23
+ },
24
+ cacheDir,
25
+ configFile: false,
26
+ logLevel: 'warn',
27
+ mode: 'production',
28
+ preview: {
29
+ host: httpHost,
30
+ port: httpPort,
31
+ strictPort: false
32
+ },
33
+ root: workDir
34
+ };
35
+ previewDebug('Serving built remote from %s', outDir);
36
+ const server = await preview(config);
37
+ const addr = server.httpServer.address();
38
+ const port = typeof addr === 'object' && addr ? addr.port : httpPort;
39
+ return {
40
+ // Drop idle keep-alive sockets so shutdown doesn't stall on a browser that
41
+ // kept the connection open — otherwise `close` waits out the teardown grace.
42
+ // `closeAllConnections` isn't on Vite's `HttpServer` type, but is on the
43
+ // underlying node server (18.2+); the optional call tolerates its absence.
44
+ close: ()=>new Promise((resolve, reject)=>{
45
+ server.httpServer.close((err)=>err ? reject(err) : resolve());
46
+ server.httpServer.closeAllConnections?.();
47
+ }),
48
+ host: httpHost,
49
+ port
50
+ };
51
+ }
52
+
53
+ //# sourceMappingURL=serveBuiltApplication.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/preview/serveBuiltApplication.ts"],"sourcesContent":["import {subdebug} from '@sanity/cli-core'\nimport {type InlineConfig, preview} from 'vite'\n\nimport {checkBuiltOutput} from '../deploy/checkBuiltOutput.js'\n\nconst previewDebug = subdebug('preview')\n\nexport interface BuiltApplicationServer {\n close: () => Promise<void>\n /** Routable host the remote is reachable at, for the registry's remote URL. */\n host: string\n /** Port Vite bound — may differ from the requested one under non-strict ports. */\n port: number\n}\n\n/**\n * Serve a built workbench app's `dist` as static files over a Vite `preview`\n * server, so its `mf-manifest.json` + `remote-entry.js` are reachable exactly as\n * a live `sanity dev` remote would be — the workbench then federates it in.\n *\n * A missing federation build surfaces as `BUILD_NOT_FOUND` (from\n * `checkBuiltOutput`) so the command shows the same \"run sanity build\" hint the\n * studio preview path does; real I/O errors surface as themselves.\n */\nexport async function serveBuiltApplication(options: {\n cacheDir: string\n httpHost: string\n httpPort: number\n outDir: string\n workDir: string\n}): Promise<BuiltApplicationServer> {\n const {cacheDir, httpHost, httpPort, outDir, workDir} = options\n\n await checkBuiltOutput(outDir)\n\n const config: InlineConfig = {\n // Serve at the root so `mf-manifest.json` and its `publicPath: 'auto'` chunks\n // resolve under the registered remote URL.\n base: '/',\n // Needed for vite to serve `outDir` itself rather than `outDir/dist`.\n build: {outDir},\n cacheDir,\n configFile: false,\n logLevel: 'warn',\n mode: 'production',\n preview: {host: httpHost, port: httpPort, strictPort: false},\n root: workDir,\n }\n\n previewDebug('Serving built remote from %s', outDir)\n const server = await preview(config)\n\n const addr = server.httpServer.address()\n const port = typeof addr === 'object' && addr ? addr.port : httpPort\n\n return {\n // Drop idle keep-alive sockets so shutdown doesn't stall on a browser that\n // kept the connection open — otherwise `close` waits out the teardown grace.\n // `closeAllConnections` isn't on Vite's `HttpServer` type, but is on the\n // underlying node server (18.2+); the optional call tolerates its absence.\n close: () =>\n new Promise((resolve, reject) => {\n server.httpServer.close((err) => (err ? reject(err) : resolve()))\n ;(server.httpServer as {closeAllConnections?: () => void}).closeAllConnections?.()\n }),\n host: httpHost,\n port,\n }\n}\n"],"names":["subdebug","preview","checkBuiltOutput","previewDebug","serveBuiltApplication","options","cacheDir","httpHost","httpPort","outDir","workDir","config","base","build","configFile","logLevel","mode","host","port","strictPort","root","server","addr","httpServer","address","close","Promise","resolve","reject","err","closeAllConnections"],"mappings":"AAAA,SAAQA,QAAQ,QAAO,mBAAkB;AACzC,SAA2BC,OAAO,QAAO,OAAM;AAE/C,SAAQC,gBAAgB,QAAO,gCAA+B;AAE9D,MAAMC,eAAeH,SAAS;AAU9B;;;;;;;;CAQC,GACD,OAAO,eAAeI,sBAAsBC,OAM3C;IACC,MAAM,EAACC,QAAQ,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,OAAO,EAAC,GAAGL;IAExD,MAAMH,iBAAiBO;IAEvB,MAAME,SAAuB;QAC3B,8EAA8E;QAC9E,2CAA2C;QAC3CC,MAAM;QACN,sEAAsE;QACtEC,OAAO;YAACJ;QAAM;QACdH;QACAQ,YAAY;QACZC,UAAU;QACVC,MAAM;QACNf,SAAS;YAACgB,MAAMV;YAAUW,MAAMV;YAAUW,YAAY;QAAK;QAC3DC,MAAMV;IACR;IAEAP,aAAa,gCAAgCM;IAC7C,MAAMY,SAAS,MAAMpB,QAAQU;IAE7B,MAAMW,OAAOD,OAAOE,UAAU,CAACC,OAAO;IACtC,MAAMN,OAAO,OAAOI,SAAS,YAAYA,OAAOA,KAAKJ,IAAI,GAAGV;IAE5D,OAAO;QACL,2EAA2E;QAC3E,6EAA6E;QAC7E,yEAAyE;QACzE,2EAA2E;QAC3EiB,OAAO,IACL,IAAIC,QAAQ,CAACC,SAASC;gBACpBP,OAAOE,UAAU,CAACE,KAAK,CAAC,CAACI,MAASA,MAAMD,OAAOC,OAAOF;gBACpDN,OAAOE,UAAU,CAAwCO,mBAAmB;YAChF;QACFb,MAAMV;QACNW;IACF;AACF"}