@sanity/workbench-cli 1.2.0 → 1.3.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 (42) hide show
  1. package/dist/_exports/build.d.ts +22 -17
  2. package/dist/_exports/deploy.d.ts +156 -20
  3. package/dist/_exports/deploy.js +3 -1
  4. package/dist/_exports/deploy.js.map +1 -1
  5. package/dist/_exports/dev.d.ts +6 -3
  6. package/dist/_exports/index.d.ts +16 -13
  7. package/dist/actions/build/artifact.js +2 -2
  8. package/dist/actions/build/artifact.js.map +1 -1
  9. package/dist/actions/build/configs/artifact.js +5 -5
  10. package/dist/actions/build/configs/artifact.js.map +1 -1
  11. package/dist/actions/deploy/apiVersion.js +5 -0
  12. package/dist/actions/deploy/apiVersion.js.map +1 -0
  13. package/dist/actions/deploy/buildExposes.js +56 -0
  14. package/dist/actions/deploy/buildExposes.js.map +1 -0
  15. package/dist/actions/deploy/{deployInstallationConfig.js → deployConfig.js} +10 -11
  16. package/dist/actions/deploy/deployConfig.js.map +1 -0
  17. package/dist/actions/deploy/deployWorkbenchApp.js +185 -0
  18. package/dist/actions/deploy/deployWorkbenchApp.js.map +1 -0
  19. package/dist/actions/deploy/getWorkbench.js +4 -4
  20. package/dist/actions/deploy/getWorkbench.js.map +1 -1
  21. package/dist/actions/dev/deriveInterfaces.js +33 -19
  22. package/dist/actions/dev/deriveInterfaces.js.map +1 -1
  23. package/dist/actions/dev/exposesSetId.js +6 -6
  24. package/dist/actions/dev/exposesSetId.js.map +1 -1
  25. package/dist/actions/dev/registry.js +14 -5
  26. package/dist/actions/dev/registry.js.map +1 -1
  27. package/dist/actions/dev/startDevManifestWatcher.js +2 -2
  28. package/dist/actions/dev/startDevManifestWatcher.js.map +1 -1
  29. package/dist/actions/dev/startDevServerRegistration.js +7 -7
  30. package/dist/actions/dev/startDevServerRegistration.js.map +1 -1
  31. package/dist/actions/dev/startWorkbenchDevServer.js +9 -6
  32. package/dist/actions/dev/startWorkbenchDevServer.js.map +1 -1
  33. package/dist/actions/dev/writeWorkbenchRuntime.js +4 -1
  34. package/dist/actions/dev/writeWorkbenchRuntime.js.map +1 -1
  35. package/dist/contract.js +16 -8
  36. package/dist/contract.js.map +1 -1
  37. package/dist/defineApp.js +25 -20
  38. package/dist/defineApp.js.map +1 -1
  39. package/dist/resolveWorkbenchApp.js +4 -3
  40. package/dist/resolveWorkbenchApp.js.map +1 -1
  41. package/package.json +3 -3
  42. package/dist/actions/deploy/deployInstallationConfig.js.map +0 -1
@@ -15,13 +15,11 @@ const devDebug = subdebug('dev');
15
15
  return (getProcessStartTime(process.pid) ?? new Date()).toISOString();
16
16
  }
17
17
  const devServerManifestSchema = z.object({
18
- host: z.string(),
19
- id: z.optional(z.string()),
20
18
  /**
21
19
  * Field schema *values* load from the federation module; each field's `src`
22
20
  * rides along so a repoint bumps the exposes-set id and forces a rebuild.
23
21
  * Lenient — the workbench is the authority.
24
- */ installationConfigs: z.optional(z.array(z.object({
22
+ */ configs: z.optional(z.array(z.object({
25
23
  // Identifies the owning app when it has no app id (singletons).
26
24
  appType: z.optional(z.string()),
27
25
  fields: z.array(z.object({
@@ -30,10 +28,18 @@ const devServerManifestSchema = z.object({
30
28
  src: z.string(),
31
29
  title: z.string()
32
30
  })),
31
+ // Content hash of the config — the workbench's change-detection key
32
+ // (see deriveConfigs).
33
+ id: z.string(),
33
34
  // The app's `unstable_defineApp` name — the module-federation alias the
34
35
  // workbench loads this config's live values from.
35
- moduleName: z.optional(z.string())
36
+ moduleName: z.optional(z.string()),
37
+ // Config contract version the generated module exports, so the
38
+ // workbench knows what it can resolve before loading the module.
39
+ version: z.number()
36
40
  }))),
41
+ host: z.string(),
42
+ id: z.optional(z.string()),
37
43
  /**
38
44
  * Interfaces the app exposes, mapped from the declared `views` (dock panels,
39
45
  * `interface_type: "panel"`) and `services` (background workers,
@@ -46,7 +52,10 @@ const devServerManifestSchema = z.object({
46
52
  */ interfaces: z.optional(z.array(z.object({
47
53
  entry_point: z.string(),
48
54
  interface_type: z.string(),
49
- name: z.string()
55
+ name: z.string(),
56
+ // Contract version the interface's generated module exports; the app
57
+ // view has no versioned contract and carries none.
58
+ version: z.optional(z.number())
50
59
  }))),
51
60
  /**
52
61
  * Inlined manifest — either a {@link StudioManifest} or {@link CoreAppManifest},
@@ -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 {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 devServerManifestSchema = z.object({\n host: z.string(),\n id: z.optional(z.string()),\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 installationConfigs: 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 // 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 }),\n ),\n ),\n /**\n * Interfaces the app exposes, mapped from the declared `views` (dock panels,\n * `interface_type: \"panel\"`) and `services` (background workers,\n * `interface_type: \"worker\"`). A service is just an interface, so both live\n * in this one list. Carried separately from the manifest — interfaces live in\n * the application service, not the manifest — so the workbench can render\n * local panels and run local workers without a deploy. `entry_point` is the\n * declared `src`. Lenient by design; the workbench is the authority on the\n * interface shape.\n */\n interfaces: z.optional(\n z.array(z.object({entry_point: z.string(), interface_type: z.string(), name: z.string()})),\n ),\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","canonicalizeWatchDir","getProcessStartTime","isOurProcess","devDebug","REGISTRY_VERSION","ownStartedAt","process","pid","Date","toISOString","devServerManifestSchema","object","host","string","id","optional","installationConfigs","array","appType","fields","name","public","boolean","src","title","moduleName","interfaces","entry_point","interface_type","manifest","union","manifestUpdatedAt","number","port","projectId","startedAt","type","enum","version","literal","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 {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 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 /**\n * Interfaces the app exposes, mapped from the declared `views` (dock panels,\n * `interface_type: \"panel\"`) and `services` (background workers,\n * `interface_type: \"worker\"`). A service is just an interface, so both live\n * in this one list. Carried separately from the manifest — interfaces live in\n * the application service, not the manifest — so the workbench can render\n * local panels and run local workers without a deploy. `entry_point` is the\n * declared `src`. Lenient by design; the workbench is the authority on the\n * interface shape.\n */\n interfaces: z.optional(\n z.array(\n z.object({\n entry_point: z.string(),\n interface_type: z.string(),\n name: z.string(),\n // Contract version the interface's generated module exports; the app\n // view has no versioned contract and carries none.\n version: z.optional(z.number()),\n }),\n ),\n ),\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","canonicalizeWatchDir","getProcessStartTime","isOurProcess","devDebug","REGISTRY_VERSION","ownStartedAt","process","pid","Date","toISOString","devServerManifestSchema","object","configs","optional","array","appType","string","fields","name","public","boolean","src","title","id","moduleName","version","number","host","interfaces","entry_point","interface_type","manifest","union","manifestUpdatedAt","port","projectId","startedAt","type","enum","literal","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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"}
@@ -35,13 +35,13 @@ const devDebug = subdebug('dev');
35
35
  }
36
36
  running = true;
37
37
  try {
38
- const { installationConfigs, interfaces, manifest } = await extract({
38
+ const { configs, interfaces, manifest } = await extract({
39
39
  configPath,
40
40
  workDir
41
41
  });
42
42
  if (closed) return;
43
43
  await update({
44
- installationConfigs,
44
+ configs,
45
45
  interfaces,
46
46
  manifest,
47
47
  manifestUpdatedAt: new Date().toISOString()
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/dev/startDevManifestWatcher.ts"],"sourcesContent":["import {watch} from 'node:fs'\nimport {basename, dirname} from 'node:path'\n\nimport {findProjectRoot, type Output, subdebug} from '@sanity/cli-core'\n\nimport {canonicalizeWatchDir} from './canonicalizeWatchDir.js'\nimport {type DevServerConfig, type DevServerInterface} from './deriveInterfaces.js'\n\nconst devDebug = subdebug('dev')\n\n/**\n * Debounce window between config file events and the next manifest\n * regeneration. Coalesces rapid saves (e.g. editor auto-save) and\n * atomic-rename bursts emitted by tools like VS Code.\n */\nconst DEBOUNCE_MS = 250\n\ninterface DevManifestWatcher {\n close: () => Promise<void>\n}\n\n/** Subset of registry fields the watcher is allowed to update. */\ninterface ManifestPatch<T> {\n manifest: T | undefined\n manifestUpdatedAt: string\n\n /** Same re-derive-don't-omit contract as `interfaces`. */\n installationConfigs?: DevServerConfig[] | undefined\n /**\n * Workbench interfaces (views/services/app view) re-derived from the config\n * on each change, so editing `views`/`services`/`entry` in `sanity.cli.ts`\n * re-syncs live like `title`/`icon`. `undefined` only for\n * non-branded configs — the registry patch is a shallow merge, so extractors\n * must re-derive rather than omit, or the registered set gets wiped.\n */\n interfaces?: DevServerInterface[] | undefined\n}\n\ninterface StartDevManifestWatcherOptions<T> {\n /**\n * Run the project-specific extraction and resolve to the manifest patch, which\n * the watcher stamps with `manifestUpdatedAt` before forwarding. Receives the\n * resolved config path (`sanity.config.ts` for studios, `sanity.cli.ts` for\n * core-apps) and the working directory.\n */\n extract: (params: {\n configPath: string\n workDir: string\n }) => Promise<Omit<ManifestPatch<T>, 'manifestUpdatedAt'>>\n output: Output\n /**\n * Called after every successful extraction with the inlined manifest +\n * interfaces. Awaited, so an interface-set change can rebuild the federation\n * remote before the registry is patched (which is what reloads the workbench).\n */\n update: (patch: ManifestPatch<T>) => Promise<void> | void\n workDir: string\n\n /**\n * Extra config filenames (basenames in the project root directory) that also\n * trigger a regeneration. Studios resolve their project root via\n * `sanity.config.*` but declare workbench interfaces in `sanity.cli.*`, so\n * their watcher needs to react to both files.\n */\n extraWatchFilenames?: readonly string[]\n}\n\n/**\n * Generate the project manifest once and then keep it in sync with the\n * project's config file (`sanity.config.(ts|js)` for studios,\n * `sanity.cli.(ts|js)` for core-apps) on disk. The initial generation runs\n * fire-and-forget so it doesn't block dev-server startup; subsequent\n * file-system events are coalesced behind it, so the extractor never has\n * overlapping writes to its shared output directory. Each successful\n * regeneration inlines the new manifest into the registry via the `update`\n * callback, so any running workbench rebroadcasts to its clients.\n *\n * Errors during extraction are logged as warnings and do not crash the dev\n * server — the previously extracted manifest (if any) stays in the\n * registry.\n */\nexport async function startDevManifestWatcher<T>({\n extract,\n extraWatchFilenames,\n output,\n update,\n workDir,\n}: StartDevManifestWatcherOptions<T>): Promise<DevManifestWatcher> {\n const projectRoot = await findProjectRoot(workDir)\n const configPath = projectRoot.path\n\n let running = false\n let pending = false\n let closed = false\n\n const regenerate = async () => {\n if (closed) return\n if (running) {\n pending = true\n return\n }\n running = true\n try {\n const {installationConfigs, interfaces, manifest} = await extract({configPath, workDir})\n if (closed) return\n await update({\n installationConfigs,\n interfaces,\n manifest,\n manifestUpdatedAt: new Date().toISOString(),\n })\n } catch (err) {\n // Extractors print their own spinner failure; log the reason here so\n // the user sees what went wrong alongside the spinner indicator.\n devDebug('Manifest regeneration failed: %O', err)\n output.warn(\n `Could not extract manifest for workbench: ${err instanceof Error ? err.message : String(err)}`,\n )\n } finally {\n running = false\n if (pending && !closed) {\n pending = false\n void regenerate()\n }\n }\n }\n\n // Route the initial extraction through `regenerate` too, so file-system\n // events arriving before it finishes get coalesced rather than racing it\n // for the shared output directory.\n void regenerate()\n\n // Watch the config file's parent directory and filter by filename.\n // Watching the file itself is unreliable across editors that perform\n // atomic-save (delete + rename) — the watcher loses its target once the\n // inode changes. Directory watches survive those transitions.\n // Canonicalize to the real long path so `fs.watch` doesn't abort on Windows\n // short-path dirs. See `canonicalizeWatchDir`.\n const configDir = canonicalizeWatchDir(dirname(configPath))\n const watchFilenames = new Set([basename(configPath), ...(extraWatchFilenames ?? [])])\n\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n const onEvent = (_event: string, filename: Buffer | string | null) => {\n if (!filename) return\n const name = typeof filename === 'string' ? filename : filename.toString('utf8')\n if (!watchFilenames.has(name)) return\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n void regenerate()\n }, DEBOUNCE_MS)\n }\n\n const watcher = watch(configDir, onEvent)\n\n watcher.on('error', (err) => {\n devDebug('Config watcher error: %O', err)\n output.warn(`Manifest watcher error: ${err instanceof Error ? err.message : String(err)}`)\n })\n\n return {\n // Idempotent — a repeat close (e.g. a signal handler racing an explicit\n // close) is a no-op, so we never clear an already-cleared timer or\n // double-close the underlying watcher.\n close: async () => {\n if (closed) return\n closed = true\n clearTimeout(debounceTimer)\n watcher.close()\n },\n }\n}\n"],"names":["watch","basename","dirname","findProjectRoot","subdebug","canonicalizeWatchDir","devDebug","DEBOUNCE_MS","startDevManifestWatcher","extract","extraWatchFilenames","output","update","workDir","projectRoot","configPath","path","running","pending","closed","regenerate","installationConfigs","interfaces","manifest","manifestUpdatedAt","Date","toISOString","err","warn","Error","message","String","configDir","watchFilenames","Set","debounceTimer","onEvent","_event","filename","name","toString","has","clearTimeout","setTimeout","watcher","on","close"],"mappings":"AAAA,SAAQA,KAAK,QAAO,UAAS;AAC7B,SAAQC,QAAQ,EAAEC,OAAO,QAAO,YAAW;AAE3C,SAAQC,eAAe,EAAeC,QAAQ,QAAO,mBAAkB;AAEvE,SAAQC,oBAAoB,QAAO,4BAA2B;AAG9D,MAAMC,WAAWF,SAAS;AAE1B;;;;CAIC,GACD,MAAMG,cAAc;AAoDpB;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeC,wBAA2B,EAC/CC,OAAO,EACPC,mBAAmB,EACnBC,MAAM,EACNC,MAAM,EACNC,OAAO,EAC2B;IAClC,MAAMC,cAAc,MAAMX,gBAAgBU;IAC1C,MAAME,aAAaD,YAAYE,IAAI;IAEnC,IAAIC,UAAU;IACd,IAAIC,UAAU;IACd,IAAIC,SAAS;IAEb,MAAMC,aAAa;QACjB,IAAID,QAAQ;QACZ,IAAIF,SAAS;YACXC,UAAU;YACV;QACF;QACAD,UAAU;QACV,IAAI;YACF,MAAM,EAACI,mBAAmB,EAAEC,UAAU,EAAEC,QAAQ,EAAC,GAAG,MAAMd,QAAQ;gBAACM;gBAAYF;YAAO;YACtF,IAAIM,QAAQ;YACZ,MAAMP,OAAO;gBACXS;gBACAC;gBACAC;gBACAC,mBAAmB,IAAIC,OAAOC,WAAW;YAC3C;QACF,EAAE,OAAOC,KAAK;YACZ,qEAAqE;YACrE,iEAAiE;YACjErB,SAAS,oCAAoCqB;YAC7ChB,OAAOiB,IAAI,CACT,CAAC,0CAA0C,EAAED,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ,MAAM;QAEnG,SAAU;YACRV,UAAU;YACV,IAAIC,WAAW,CAACC,QAAQ;gBACtBD,UAAU;gBACV,KAAKE;YACP;QACF;IACF;IAEA,wEAAwE;IACxE,yEAAyE;IACzE,mCAAmC;IACnC,KAAKA;IAEL,mEAAmE;IACnE,qEAAqE;IACrE,wEAAwE;IACxE,8DAA8D;IAC9D,4EAA4E;IAC5E,+CAA+C;IAC/C,MAAMY,YAAY3B,qBAAqBH,QAAQa;IAC/C,MAAMkB,iBAAiB,IAAIC,IAAI;QAACjC,SAASc;WAAiBL,uBAAuB,EAAE;KAAE;IAErF,IAAIyB;IAEJ,MAAMC,UAAU,CAACC,QAAgBC;QAC/B,IAAI,CAACA,UAAU;QACf,MAAMC,OAAO,OAAOD,aAAa,WAAWA,WAAWA,SAASE,QAAQ,CAAC;QACzE,IAAI,CAACP,eAAeQ,GAAG,CAACF,OAAO;QAC/BG,aAAaP;QACbA,gBAAgBQ,WAAW;YACzB,KAAKvB;QACP,GAAGb;IACL;IAEA,MAAMqC,UAAU5C,MAAMgC,WAAWI;IAEjCQ,QAAQC,EAAE,CAAC,SAAS,CAAClB;QACnBrB,SAAS,4BAA4BqB;QACrChB,OAAOiB,IAAI,CAAC,CAAC,wBAAwB,EAAED,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ,MAAM;IAC3F;IAEA,OAAO;QACL,wEAAwE;QACxE,mEAAmE;QACnE,uCAAuC;QACvCmB,OAAO;YACL,IAAI3B,QAAQ;YACZA,SAAS;YACTuB,aAAaP;YACbS,QAAQE,KAAK;QACf;IACF;AACF"}
1
+ {"version":3,"sources":["../../../src/actions/dev/startDevManifestWatcher.ts"],"sourcesContent":["import {watch} from 'node:fs'\nimport {basename, dirname} from 'node:path'\n\nimport {findProjectRoot, type Output, subdebug} from '@sanity/cli-core'\n\nimport {canonicalizeWatchDir} from './canonicalizeWatchDir.js'\nimport {type DevServerConfig, type DevServerInterface} from './deriveInterfaces.js'\n\nconst devDebug = subdebug('dev')\n\n/**\n * Debounce window between config file events and the next manifest\n * regeneration. Coalesces rapid saves (e.g. editor auto-save) and\n * atomic-rename bursts emitted by tools like VS Code.\n */\nconst DEBOUNCE_MS = 250\n\ninterface DevManifestWatcher {\n close: () => Promise<void>\n}\n\n/** Subset of registry fields the watcher is allowed to update. */\ninterface ManifestPatch<T> {\n manifest: T | undefined\n manifestUpdatedAt: string\n\n /** Same re-derive-don't-omit contract as `interfaces`. */\n configs?: DevServerConfig[] | undefined\n /**\n * Workbench interfaces (views/services/app view) re-derived from the config\n * on each change, so editing `views`/`services`/`entry` in `sanity.cli.ts`\n * re-syncs live like `title`/`icon`. `undefined` only for\n * non-branded configs — the registry patch is a shallow merge, so extractors\n * must re-derive rather than omit, or the registered set gets wiped.\n */\n interfaces?: DevServerInterface[] | undefined\n}\n\ninterface StartDevManifestWatcherOptions<T> {\n /**\n * Run the project-specific extraction and resolve to the manifest patch, which\n * the watcher stamps with `manifestUpdatedAt` before forwarding. Receives the\n * resolved config path (`sanity.config.ts` for studios, `sanity.cli.ts` for\n * core-apps) and the working directory.\n */\n extract: (params: {\n configPath: string\n workDir: string\n }) => Promise<Omit<ManifestPatch<T>, 'manifestUpdatedAt'>>\n output: Output\n /**\n * Called after every successful extraction with the inlined manifest +\n * interfaces. Awaited, so an interface-set change can rebuild the federation\n * remote before the registry is patched (which is what reloads the workbench).\n */\n update: (patch: ManifestPatch<T>) => Promise<void> | void\n workDir: string\n\n /**\n * Extra config filenames (basenames in the project root directory) that also\n * trigger a regeneration. Studios resolve their project root via\n * `sanity.config.*` but declare workbench interfaces in `sanity.cli.*`, so\n * their watcher needs to react to both files.\n */\n extraWatchFilenames?: readonly string[]\n}\n\n/**\n * Generate the project manifest once and then keep it in sync with the\n * project's config file (`sanity.config.(ts|js)` for studios,\n * `sanity.cli.(ts|js)` for core-apps) on disk. The initial generation runs\n * fire-and-forget so it doesn't block dev-server startup; subsequent\n * file-system events are coalesced behind it, so the extractor never has\n * overlapping writes to its shared output directory. Each successful\n * regeneration inlines the new manifest into the registry via the `update`\n * callback, so any running workbench rebroadcasts to its clients.\n *\n * Errors during extraction are logged as warnings and do not crash the dev\n * server — the previously extracted manifest (if any) stays in the\n * registry.\n */\nexport async function startDevManifestWatcher<T>({\n extract,\n extraWatchFilenames,\n output,\n update,\n workDir,\n}: StartDevManifestWatcherOptions<T>): Promise<DevManifestWatcher> {\n const projectRoot = await findProjectRoot(workDir)\n const configPath = projectRoot.path\n\n let running = false\n let pending = false\n let closed = false\n\n const regenerate = async () => {\n if (closed) return\n if (running) {\n pending = true\n return\n }\n running = true\n try {\n const {configs, interfaces, manifest} = await extract({configPath, workDir})\n if (closed) return\n await update({\n configs,\n interfaces,\n manifest,\n manifestUpdatedAt: new Date().toISOString(),\n })\n } catch (err) {\n // Extractors print their own spinner failure; log the reason here so\n // the user sees what went wrong alongside the spinner indicator.\n devDebug('Manifest regeneration failed: %O', err)\n output.warn(\n `Could not extract manifest for workbench: ${err instanceof Error ? err.message : String(err)}`,\n )\n } finally {\n running = false\n if (pending && !closed) {\n pending = false\n void regenerate()\n }\n }\n }\n\n // Route the initial extraction through `regenerate` too, so file-system\n // events arriving before it finishes get coalesced rather than racing it\n // for the shared output directory.\n void regenerate()\n\n // Watch the config file's parent directory and filter by filename.\n // Watching the file itself is unreliable across editors that perform\n // atomic-save (delete + rename) — the watcher loses its target once the\n // inode changes. Directory watches survive those transitions.\n // Canonicalize to the real long path so `fs.watch` doesn't abort on Windows\n // short-path dirs. See `canonicalizeWatchDir`.\n const configDir = canonicalizeWatchDir(dirname(configPath))\n const watchFilenames = new Set([basename(configPath), ...(extraWatchFilenames ?? [])])\n\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n const onEvent = (_event: string, filename: Buffer | string | null) => {\n if (!filename) return\n const name = typeof filename === 'string' ? filename : filename.toString('utf8')\n if (!watchFilenames.has(name)) return\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n void regenerate()\n }, DEBOUNCE_MS)\n }\n\n const watcher = watch(configDir, onEvent)\n\n watcher.on('error', (err) => {\n devDebug('Config watcher error: %O', err)\n output.warn(`Manifest watcher error: ${err instanceof Error ? err.message : String(err)}`)\n })\n\n return {\n // Idempotent — a repeat close (e.g. a signal handler racing an explicit\n // close) is a no-op, so we never clear an already-cleared timer or\n // double-close the underlying watcher.\n close: async () => {\n if (closed) return\n closed = true\n clearTimeout(debounceTimer)\n watcher.close()\n },\n }\n}\n"],"names":["watch","basename","dirname","findProjectRoot","subdebug","canonicalizeWatchDir","devDebug","DEBOUNCE_MS","startDevManifestWatcher","extract","extraWatchFilenames","output","update","workDir","projectRoot","configPath","path","running","pending","closed","regenerate","configs","interfaces","manifest","manifestUpdatedAt","Date","toISOString","err","warn","Error","message","String","configDir","watchFilenames","Set","debounceTimer","onEvent","_event","filename","name","toString","has","clearTimeout","setTimeout","watcher","on","close"],"mappings":"AAAA,SAAQA,KAAK,QAAO,UAAS;AAC7B,SAAQC,QAAQ,EAAEC,OAAO,QAAO,YAAW;AAE3C,SAAQC,eAAe,EAAeC,QAAQ,QAAO,mBAAkB;AAEvE,SAAQC,oBAAoB,QAAO,4BAA2B;AAG9D,MAAMC,WAAWF,SAAS;AAE1B;;;;CAIC,GACD,MAAMG,cAAc;AAoDpB;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeC,wBAA2B,EAC/CC,OAAO,EACPC,mBAAmB,EACnBC,MAAM,EACNC,MAAM,EACNC,OAAO,EAC2B;IAClC,MAAMC,cAAc,MAAMX,gBAAgBU;IAC1C,MAAME,aAAaD,YAAYE,IAAI;IAEnC,IAAIC,UAAU;IACd,IAAIC,UAAU;IACd,IAAIC,SAAS;IAEb,MAAMC,aAAa;QACjB,IAAID,QAAQ;QACZ,IAAIF,SAAS;YACXC,UAAU;YACV;QACF;QACAD,UAAU;QACV,IAAI;YACF,MAAM,EAACI,OAAO,EAAEC,UAAU,EAAEC,QAAQ,EAAC,GAAG,MAAMd,QAAQ;gBAACM;gBAAYF;YAAO;YAC1E,IAAIM,QAAQ;YACZ,MAAMP,OAAO;gBACXS;gBACAC;gBACAC;gBACAC,mBAAmB,IAAIC,OAAOC,WAAW;YAC3C;QACF,EAAE,OAAOC,KAAK;YACZ,qEAAqE;YACrE,iEAAiE;YACjErB,SAAS,oCAAoCqB;YAC7ChB,OAAOiB,IAAI,CACT,CAAC,0CAA0C,EAAED,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ,MAAM;QAEnG,SAAU;YACRV,UAAU;YACV,IAAIC,WAAW,CAACC,QAAQ;gBACtBD,UAAU;gBACV,KAAKE;YACP;QACF;IACF;IAEA,wEAAwE;IACxE,yEAAyE;IACzE,mCAAmC;IACnC,KAAKA;IAEL,mEAAmE;IACnE,qEAAqE;IACrE,wEAAwE;IACxE,8DAA8D;IAC9D,4EAA4E;IAC5E,+CAA+C;IAC/C,MAAMY,YAAY3B,qBAAqBH,QAAQa;IAC/C,MAAMkB,iBAAiB,IAAIC,IAAI;QAACjC,SAASc;WAAiBL,uBAAuB,EAAE;KAAE;IAErF,IAAIyB;IAEJ,MAAMC,UAAU,CAACC,QAAgBC;QAC/B,IAAI,CAACA,UAAU;QACf,MAAMC,OAAO,OAAOD,aAAa,WAAWA,WAAWA,SAASE,QAAQ,CAAC;QACzE,IAAI,CAACP,eAAeQ,GAAG,CAACF,OAAO;QAC/BG,aAAaP;QACbA,gBAAgBQ,WAAW;YACzB,KAAKvB;QACP,GAAGb;IACL;IAEA,MAAMqC,UAAU5C,MAAMgC,WAAWI;IAEjCQ,QAAQC,EAAE,CAAC,SAAS,CAAClB;QACnBrB,SAAS,4BAA4BqB;QACrChB,OAAOiB,IAAI,CAAC,CAAC,wBAAwB,EAAED,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ,MAAM;IAC3F;IAEA,OAAO;QACL,wEAAwE;QACxE,mEAAmE;QACnE,uCAAuC;QACvCmB,OAAO;YACL,IAAI3B,QAAQ;YACZA,SAAS;YACTuB,aAAaP;YACbS,QAAQE,KAAK;QACf;IACF;AACF"}
@@ -1,5 +1,5 @@
1
1
  import { getCliConfigUncached } from '@sanity/cli-core';
2
- import { deriveInstallationConfigs, deriveInterfaces } from './deriveInterfaces.js';
2
+ import { deriveConfigs, deriveInterfaces } from './deriveInterfaces.js';
3
3
  import { trackExposesSet } from './exposesSetId.js';
4
4
  import { registerDevServer } from './registry.js';
5
5
  import { startDevManifestWatcher } from './startDevManifestWatcher.js';
@@ -23,11 +23,11 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
23
23
  const interfaces = deriveInterfaces(cliConfig.app, {
24
24
  isApp
25
25
  });
26
- const installationConfigs = deriveInstallationConfigs(cliConfig.app);
26
+ const configs = deriveConfigs(cliConfig.app);
27
27
  const registration = registerDevServer({
28
+ configs,
28
29
  host: appHost,
29
30
  id: appId,
30
- installationConfigs,
31
31
  interfaces,
32
32
  port: appPort,
33
33
  projectId: cliConfig?.api?.projectId,
@@ -35,7 +35,7 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
35
35
  workDir
36
36
  });
37
37
  const exposesSet = trackExposesSet({
38
- installationConfigs,
38
+ configs,
39
39
  interfaces
40
40
  });
41
41
  const watcher = await startDevManifestWatcher({
@@ -44,7 +44,7 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
44
44
  extract: async (params)=>{
45
45
  const app = (await getCliConfigUncached(params.workDir)).app;
46
46
  return {
47
- installationConfigs: deriveInstallationConfigs(app),
47
+ configs: deriveConfigs(app),
48
48
  interfaces: deriveInterfaces(app, {
49
49
  isApp
50
50
  }),
@@ -60,7 +60,7 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
60
60
  output,
61
61
  update: async (patch)=>{
62
62
  if (!exposesSet.changed({
63
- installationConfigs: patch.installationConfigs,
63
+ configs: patch.configs,
64
64
  interfaces: patch.interfaces
65
65
  })) {
66
66
  registration.update(patch);
@@ -71,7 +71,7 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
71
71
  const rebuiltServer = await onInterfaceSetChange?.();
72
72
  // Commit only after a successful rebuild, so a thrown one retries next pass.
73
73
  exposesSet.commit({
74
- installationConfigs: patch.installationConfigs,
74
+ configs: patch.configs,
75
75
  interfaces: patch.interfaces
76
76
  });
77
77
  // The recreated server can bind a different port (non-strict ports).
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/dev/startDevServerRegistration.ts"],"sourcesContent":["import {type CliConfig, getCliConfigUncached, type Output} from '@sanity/cli-core'\nimport {type ViteDevServer} from 'vite'\n\nimport {deriveInstallationConfigs, deriveInterfaces} from './deriveInterfaces.js'\nimport {trackExposesSet} from './exposesSetId.js'\nimport {type DevServerManifest, registerDevServer} from './registry.js'\nimport {startDevManifestWatcher} from './startDevManifestWatcher.js'\n\ninterface DevServerRegistrationOptions {\n /** Resolved app id for the registry entry; the caller owns id resolution and the deprecation check. */\n appId: string | undefined\n cliConfig: CliConfig\n /**\n * Extract the project manifest to inline into the registry. The caller owns the\n * studio-vs-app split (manifest formats are CLI-domain); registration re-derives\n * the interface set alongside it.\n */\n extractManifest: (params: {\n configPath: string\n workDir: string\n }) => Promise<DevServerManifest['manifest']>\n isApp: boolean\n output: Output\n server: ViteDevServer\n workDir: string\n\n /**\n * Rebuild the app's federation remote when its interface set changes, awaited\n * *before* the registry patch — the patch reloads the workbench page, which must\n * re-fetch a remote that already exposes the new interface. Resolves with the\n * recreated server so the entry gets its actual address (non-strict ports may\n * shift it); must reject if the restart produces no server, so the set stays\n * uncommitted and the next save retries instead of advertising a dead port.\n */\n onInterfaceSetChange?: () => Promise<ViteDevServer>\n}\n\ninterface DevServerRegistrationHandle {\n close: () => Promise<void>\n}\n\n/** The address the server actually bound — the live socket, which can differ from the configured port under non-strict ports. */\nfunction serverAddress(server: ViteDevServer) {\n const resolvedHost = server.config.server.host\n const addr = server.httpServer?.address()\n return {\n host: typeof resolvedHost === 'string' ? resolvedHost : 'localhost',\n port: typeof addr === 'object' && addr ? addr.port : server.config.server.port,\n }\n}\n\n/**\n * Register the dev server in the registry and watch its config for manifest +\n * interface changes. The workbench reads the entry to locate and render the\n * server; the watcher keeps it current as `sanity.cli.ts` is edited.\n */\nexport async function startDevServerRegistration(\n options: DevServerRegistrationOptions,\n): Promise<DevServerRegistrationHandle> {\n const {appId, cliConfig, extractManifest, isApp, onInterfaceSetChange, output, server, workDir} =\n options\n\n const {host: appHost, port: appPort} = serverAddress(server)\n\n // Forwarded alongside (not inside) the manifest so the workbench renders local\n // panels/workers and reads the configs without a deploy.\n const interfaces = deriveInterfaces(cliConfig.app, {isApp})\n const installationConfigs = deriveInstallationConfigs(cliConfig.app)\n\n const registration = registerDevServer({\n host: appHost,\n id: appId,\n installationConfigs,\n interfaces,\n port: appPort,\n projectId: cliConfig?.api?.projectId,\n type: isApp ? 'coreApp' : 'studio',\n workDir,\n })\n\n const exposesSet = trackExposesSet({installationConfigs, interfaces})\n\n const watcher = await startDevManifestWatcher({\n // Re-derive every pass (don't omit): the registry patch is a shallow merge,\n // so omitting would wipe the registered set.\n extract: async (params) => {\n const app = (await getCliConfigUncached(params.workDir)).app\n return {\n installationConfigs: deriveInstallationConfigs(app),\n interfaces: deriveInterfaces(app, {isApp}),\n manifest: await extractManifest(params),\n }\n },\n // A studio's root resolves to `sanity.config.*` but its interfaces live in\n // `sanity.cli.*` — watch that too. Apps already root at `sanity.cli.*`.\n extraWatchFilenames: isApp ? undefined : ['sanity.cli.js', 'sanity.cli.ts'],\n output,\n update: async (patch) => {\n if (\n !exposesSet.changed({\n installationConfigs: patch.installationConfigs,\n interfaces: patch.interfaces,\n })\n ) {\n registration.update(patch)\n return\n }\n // Rebuild the remote *before* patching the registry — the patch reloads the\n // page, which must re-fetch a remote that already exposes the new interface.\n const rebuiltServer = await onInterfaceSetChange?.()\n // Commit only after a successful rebuild, so a thrown one retries next pass.\n exposesSet.commit({\n installationConfigs: patch.installationConfigs,\n interfaces: patch.interfaces,\n })\n // The recreated server can bind a different port (non-strict ports).\n registration.update(rebuiltServer ? {...patch, ...serverAddress(rebuiltServer)} : patch)\n },\n workDir,\n })\n\n return {\n close: async () => {\n registration.release()\n await watcher.close()\n },\n }\n}\n"],"names":["getCliConfigUncached","deriveInstallationConfigs","deriveInterfaces","trackExposesSet","registerDevServer","startDevManifestWatcher","serverAddress","server","resolvedHost","config","host","addr","httpServer","address","port","startDevServerRegistration","options","appId","cliConfig","extractManifest","isApp","onInterfaceSetChange","output","workDir","appHost","appPort","interfaces","app","installationConfigs","registration","id","projectId","api","type","exposesSet","watcher","extract","params","manifest","extraWatchFilenames","undefined","update","patch","changed","rebuiltServer","commit","close","release"],"mappings":"AAAA,SAAwBA,oBAAoB,QAAoB,mBAAkB;AAGlF,SAAQC,yBAAyB,EAAEC,gBAAgB,QAAO,wBAAuB;AACjF,SAAQC,eAAe,QAAO,oBAAmB;AACjD,SAAgCC,iBAAiB,QAAO,gBAAe;AACvE,SAAQC,uBAAuB,QAAO,+BAA8B;AAmCpE,+HAA+H,GAC/H,SAASC,cAAcC,MAAqB;IAC1C,MAAMC,eAAeD,OAAOE,MAAM,CAACF,MAAM,CAACG,IAAI;IAC9C,MAAMC,OAAOJ,OAAOK,UAAU,EAAEC;IAChC,OAAO;QACLH,MAAM,OAAOF,iBAAiB,WAAWA,eAAe;QACxDM,MAAM,OAAOH,SAAS,YAAYA,OAAOA,KAAKG,IAAI,GAAGP,OAAOE,MAAM,CAACF,MAAM,CAACO,IAAI;IAChF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeC,2BACpBC,OAAqC;IAErC,MAAM,EAACC,KAAK,EAAEC,SAAS,EAAEC,eAAe,EAAEC,KAAK,EAAEC,oBAAoB,EAAEC,MAAM,EAAEf,MAAM,EAAEgB,OAAO,EAAC,GAC7FP;IAEF,MAAM,EAACN,MAAMc,OAAO,EAAEV,MAAMW,OAAO,EAAC,GAAGnB,cAAcC;IAErD,+EAA+E;IAC/E,yDAAyD;IACzD,MAAMmB,aAAaxB,iBAAiBgB,UAAUS,GAAG,EAAE;QAACP;IAAK;IACzD,MAAMQ,sBAAsB3B,0BAA0BiB,UAAUS,GAAG;IAEnE,MAAME,eAAezB,kBAAkB;QACrCM,MAAMc;QACNM,IAAIb;QACJW;QACAF;QACAZ,MAAMW;QACNM,WAAWb,WAAWc,KAAKD;QAC3BE,MAAMb,QAAQ,YAAY;QAC1BG;IACF;IAEA,MAAMW,aAAa/B,gBAAgB;QAACyB;QAAqBF;IAAU;IAEnE,MAAMS,UAAU,MAAM9B,wBAAwB;QAC5C,4EAA4E;QAC5E,6CAA6C;QAC7C+B,SAAS,OAAOC;YACd,MAAMV,MAAM,AAAC,CAAA,MAAM3B,qBAAqBqC,OAAOd,OAAO,CAAA,EAAGI,GAAG;YAC5D,OAAO;gBACLC,qBAAqB3B,0BAA0B0B;gBAC/CD,YAAYxB,iBAAiByB,KAAK;oBAACP;gBAAK;gBACxCkB,UAAU,MAAMnB,gBAAgBkB;YAClC;QACF;QACA,2EAA2E;QAC3E,wEAAwE;QACxEE,qBAAqBnB,QAAQoB,YAAY;YAAC;YAAiB;SAAgB;QAC3ElB;QACAmB,QAAQ,OAAOC;YACb,IACE,CAACR,WAAWS,OAAO,CAAC;gBAClBf,qBAAqBc,MAAMd,mBAAmB;gBAC9CF,YAAYgB,MAAMhB,UAAU;YAC9B,IACA;gBACAG,aAAaY,MAAM,CAACC;gBACpB;YACF;YACA,4EAA4E;YAC5E,6EAA6E;YAC7E,MAAME,gBAAgB,MAAMvB;YAC5B,6EAA6E;YAC7Ea,WAAWW,MAAM,CAAC;gBAChBjB,qBAAqBc,MAAMd,mBAAmB;gBAC9CF,YAAYgB,MAAMhB,UAAU;YAC9B;YACA,qEAAqE;YACrEG,aAAaY,MAAM,CAACG,gBAAgB;gBAAC,GAAGF,KAAK;gBAAE,GAAGpC,cAAcsC,cAAc;YAAA,IAAIF;QACpF;QACAnB;IACF;IAEA,OAAO;QACLuB,OAAO;YACLjB,aAAakB,OAAO;YACpB,MAAMZ,QAAQW,KAAK;QACrB;IACF;AACF"}
1
+ {"version":3,"sources":["../../../src/actions/dev/startDevServerRegistration.ts"],"sourcesContent":["import {type CliConfig, getCliConfigUncached, type Output} from '@sanity/cli-core'\nimport {type ViteDevServer} from 'vite'\n\nimport {deriveConfigs, deriveInterfaces} from './deriveInterfaces.js'\nimport {trackExposesSet} from './exposesSetId.js'\nimport {type DevServerManifest, registerDevServer} from './registry.js'\nimport {startDevManifestWatcher} from './startDevManifestWatcher.js'\n\ninterface DevServerRegistrationOptions {\n /** Resolved app id for the registry entry; the caller owns id resolution and the deprecation check. */\n appId: string | undefined\n cliConfig: CliConfig\n /**\n * Extract the project manifest to inline into the registry. The caller owns the\n * studio-vs-app split (manifest formats are CLI-domain); registration re-derives\n * the interface set alongside it.\n */\n extractManifest: (params: {\n configPath: string\n workDir: string\n }) => Promise<DevServerManifest['manifest']>\n isApp: boolean\n output: Output\n server: ViteDevServer\n workDir: string\n\n /**\n * Rebuild the app's federation remote when its interface set changes, awaited\n * *before* the registry patch — the patch reloads the workbench page, which must\n * re-fetch a remote that already exposes the new interface. Resolves with the\n * recreated server so the entry gets its actual address (non-strict ports may\n * shift it); must reject if the restart produces no server, so the set stays\n * uncommitted and the next save retries instead of advertising a dead port.\n */\n onInterfaceSetChange?: () => Promise<ViteDevServer>\n}\n\ninterface DevServerRegistrationHandle {\n close: () => Promise<void>\n}\n\n/** The address the server actually bound — the live socket, which can differ from the configured port under non-strict ports. */\nfunction serverAddress(server: ViteDevServer) {\n const resolvedHost = server.config.server.host\n const addr = server.httpServer?.address()\n return {\n host: typeof resolvedHost === 'string' ? resolvedHost : 'localhost',\n port: typeof addr === 'object' && addr ? addr.port : server.config.server.port,\n }\n}\n\n/**\n * Register the dev server in the registry and watch its config for manifest +\n * interface changes. The workbench reads the entry to locate and render the\n * server; the watcher keeps it current as `sanity.cli.ts` is edited.\n */\nexport async function startDevServerRegistration(\n options: DevServerRegistrationOptions,\n): Promise<DevServerRegistrationHandle> {\n const {appId, cliConfig, extractManifest, isApp, onInterfaceSetChange, output, server, workDir} =\n options\n\n const {host: appHost, port: appPort} = serverAddress(server)\n\n // Forwarded alongside (not inside) the manifest so the workbench renders local\n // panels/workers and reads the configs without a deploy.\n const interfaces = deriveInterfaces(cliConfig.app, {isApp})\n const configs = deriveConfigs(cliConfig.app)\n\n const registration = registerDevServer({\n configs,\n host: appHost,\n id: appId,\n interfaces,\n port: appPort,\n projectId: cliConfig?.api?.projectId,\n type: isApp ? 'coreApp' : 'studio',\n workDir,\n })\n\n const exposesSet = trackExposesSet({configs, interfaces})\n\n const watcher = await startDevManifestWatcher({\n // Re-derive every pass (don't omit): the registry patch is a shallow merge,\n // so omitting would wipe the registered set.\n extract: async (params) => {\n const app = (await getCliConfigUncached(params.workDir)).app\n return {\n configs: deriveConfigs(app),\n interfaces: deriveInterfaces(app, {isApp}),\n manifest: await extractManifest(params),\n }\n },\n // A studio's root resolves to `sanity.config.*` but its interfaces live in\n // `sanity.cli.*` — watch that too. Apps already root at `sanity.cli.*`.\n extraWatchFilenames: isApp ? undefined : ['sanity.cli.js', 'sanity.cli.ts'],\n output,\n update: async (patch) => {\n if (\n !exposesSet.changed({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n ) {\n registration.update(patch)\n return\n }\n // Rebuild the remote *before* patching the registry — the patch reloads the\n // page, which must re-fetch a remote that already exposes the new interface.\n const rebuiltServer = await onInterfaceSetChange?.()\n // Commit only after a successful rebuild, so a thrown one retries next pass.\n exposesSet.commit({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n // The recreated server can bind a different port (non-strict ports).\n registration.update(rebuiltServer ? {...patch, ...serverAddress(rebuiltServer)} : patch)\n },\n workDir,\n })\n\n return {\n close: async () => {\n registration.release()\n await watcher.close()\n },\n }\n}\n"],"names":["getCliConfigUncached","deriveConfigs","deriveInterfaces","trackExposesSet","registerDevServer","startDevManifestWatcher","serverAddress","server","resolvedHost","config","host","addr","httpServer","address","port","startDevServerRegistration","options","appId","cliConfig","extractManifest","isApp","onInterfaceSetChange","output","workDir","appHost","appPort","interfaces","app","configs","registration","id","projectId","api","type","exposesSet","watcher","extract","params","manifest","extraWatchFilenames","undefined","update","patch","changed","rebuiltServer","commit","close","release"],"mappings":"AAAA,SAAwBA,oBAAoB,QAAoB,mBAAkB;AAGlF,SAAQC,aAAa,EAAEC,gBAAgB,QAAO,wBAAuB;AACrE,SAAQC,eAAe,QAAO,oBAAmB;AACjD,SAAgCC,iBAAiB,QAAO,gBAAe;AACvE,SAAQC,uBAAuB,QAAO,+BAA8B;AAmCpE,+HAA+H,GAC/H,SAASC,cAAcC,MAAqB;IAC1C,MAAMC,eAAeD,OAAOE,MAAM,CAACF,MAAM,CAACG,IAAI;IAC9C,MAAMC,OAAOJ,OAAOK,UAAU,EAAEC;IAChC,OAAO;QACLH,MAAM,OAAOF,iBAAiB,WAAWA,eAAe;QACxDM,MAAM,OAAOH,SAAS,YAAYA,OAAOA,KAAKG,IAAI,GAAGP,OAAOE,MAAM,CAACF,MAAM,CAACO,IAAI;IAChF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeC,2BACpBC,OAAqC;IAErC,MAAM,EAACC,KAAK,EAAEC,SAAS,EAAEC,eAAe,EAAEC,KAAK,EAAEC,oBAAoB,EAAEC,MAAM,EAAEf,MAAM,EAAEgB,OAAO,EAAC,GAC7FP;IAEF,MAAM,EAACN,MAAMc,OAAO,EAAEV,MAAMW,OAAO,EAAC,GAAGnB,cAAcC;IAErD,+EAA+E;IAC/E,yDAAyD;IACzD,MAAMmB,aAAaxB,iBAAiBgB,UAAUS,GAAG,EAAE;QAACP;IAAK;IACzD,MAAMQ,UAAU3B,cAAciB,UAAUS,GAAG;IAE3C,MAAME,eAAezB,kBAAkB;QACrCwB;QACAlB,MAAMc;QACNM,IAAIb;QACJS;QACAZ,MAAMW;QACNM,WAAWb,WAAWc,KAAKD;QAC3BE,MAAMb,QAAQ,YAAY;QAC1BG;IACF;IAEA,MAAMW,aAAa/B,gBAAgB;QAACyB;QAASF;IAAU;IAEvD,MAAMS,UAAU,MAAM9B,wBAAwB;QAC5C,4EAA4E;QAC5E,6CAA6C;QAC7C+B,SAAS,OAAOC;YACd,MAAMV,MAAM,AAAC,CAAA,MAAM3B,qBAAqBqC,OAAOd,OAAO,CAAA,EAAGI,GAAG;YAC5D,OAAO;gBACLC,SAAS3B,cAAc0B;gBACvBD,YAAYxB,iBAAiByB,KAAK;oBAACP;gBAAK;gBACxCkB,UAAU,MAAMnB,gBAAgBkB;YAClC;QACF;QACA,2EAA2E;QAC3E,wEAAwE;QACxEE,qBAAqBnB,QAAQoB,YAAY;YAAC;YAAiB;SAAgB;QAC3ElB;QACAmB,QAAQ,OAAOC;YACb,IACE,CAACR,WAAWS,OAAO,CAAC;gBAClBf,SAASc,MAAMd,OAAO;gBACtBF,YAAYgB,MAAMhB,UAAU;YAC9B,IACA;gBACAG,aAAaY,MAAM,CAACC;gBACpB;YACF;YACA,4EAA4E;YAC5E,6EAA6E;YAC7E,MAAME,gBAAgB,MAAMvB;YAC5B,6EAA6E;YAC7Ea,WAAWW,MAAM,CAAC;gBAChBjB,SAASc,MAAMd,OAAO;gBACtBF,YAAYgB,MAAMhB,UAAU;YAC9B;YACA,qEAAqE;YACrEG,aAAaY,MAAM,CAACG,gBAAgB;gBAAC,GAAGF,KAAK;gBAAE,GAAGpC,cAAcsC,cAAc;YAAA,IAAIF;QACpF;QACAnB;IACF;IAEA,OAAO;QACLuB,OAAO;YACLjB,aAAakB,OAAO;YACpB,MAAMZ,QAAQW,KAAK;QACrB;IACF;AACF"}
@@ -8,10 +8,10 @@ import { acquireWorkbenchLock, getRegisteredServers, readWorkbenchLock, watchReg
8
8
  import { writeWorkbenchRuntime } from './writeWorkbenchRuntime.js';
9
9
  const devDebug = subdebug('dev');
10
10
  const noop = async ()=>{};
11
- // Every server is a local app except a config-only one — an installation config
11
+ // Every server is a local app except a config-only one — an config
12
12
  // with no interfaces (the media library). A server with both lands in both channels.
13
13
  const isLocalApp = (server)=>{
14
- const configOnly = Boolean(server.installationConfigs?.length) && !server.interfaces?.length;
14
+ const configOnly = Boolean(server.configs?.length) && !server.interfaces?.length;
15
15
  return !configOnly;
16
16
  };
17
17
  const toApplicationsPayload = (servers)=>({
@@ -24,13 +24,16 @@ const toApplicationsPayload = (servers)=>({
24
24
  projectId,
25
25
  type
26
26
  })),
27
- installationConfigs: servers.flatMap(({ host, installationConfigs, port })=>// Pass through the app-type-specific payload (`fields` for a media library)
28
- // once the discriminator and transport coordinates are peeled off.
29
- (installationConfigs ?? []).map(({ appType, moduleName, ...config })=>({
27
+ configs: servers.flatMap(({ configs, host, port })=>// The registry stores the config flat; the workbench wire shape nests the
28
+ // type-specific payload (`fields` for a media library) under `config`, keyed
29
+ // by the `appType` discriminator.
30
+ (configs ?? []).map(({ appType, id, moduleName, version, ...config })=>({
31
+ appType,
30
32
  config,
33
+ id,
31
34
  moduleName,
32
35
  remoteURL: `http://${host}:${port}`,
33
- type: appType
36
+ version
34
37
  })))
35
38
  });
36
39
  /**
@@ -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 installation 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.installationConfigs?.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 installationConfigs: servers.flatMap(({host, installationConfigs, port}) =>\n // Pass through the app-type-specific payload (`fields` for a media library)\n // once the discriminator and transport coordinates are peeled off.\n (installationConfigs ?? []).map(({appType, moduleName, ...config}) => ({\n config,\n moduleName,\n remoteURL: `http://${host}:${port}`,\n type: appType,\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","installationConfigs","length","interfaces","toApplicationsPayload","servers","applications","filter","map","host","id","manifest","port","projectId","type","flatMap","appType","moduleName","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 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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"}
@@ -24,6 +24,7 @@ const indexHtmlTemplate = `\
24
24
  </head>
25
25
  <body>
26
26
  <div id="workbench"></div>
27
+ <script>globalThis.__SANITY_STAGING__ = %SANITY_WORKBENCH_STAGING%</script>
27
28
  <script type="module" src="./workbench.js"></script>
28
29
  </body>
29
30
  </html>
@@ -40,7 +41,9 @@ const indexHtmlTemplate = `\
40
41
  const workbenchDir = path.join(cwd, '.sanity', 'workbench');
41
42
  const workbenchJs = workbenchJsTemplate.replace(/%SANITY_WORKBENCH_ORGANIZATION_ID%/, organizationId === undefined ? 'undefined' : JSON.stringify(organizationId)).replace(/%SANITY_WORKBENCH_REACT_STRICT_MODE%/, JSON.stringify(reactStrictMode));
42
43
  const prefetchHints = buildPrefetchHints(remoteUrl);
43
- const indexHtml = indexHtmlTemplate.replace(/%SANITY_WORKBENCH_PREFETCH_HINTS%/, prefetchHints);
44
+ // The runtime flag builds get via decorateIndexWithStagingScript — a vite
45
+ // `define` never reaches pre-bundled dependencies like the SDK.
46
+ const indexHtml = indexHtmlTemplate.replace(/%SANITY_WORKBENCH_PREFETCH_HINTS%/, prefetchHints).replace(/%SANITY_WORKBENCH_STAGING%/, JSON.stringify(process.env.SANITY_INTERNAL_ENV === 'staging'));
44
47
  devDebug('Making workbench runtime directory');
45
48
  await fs.mkdir(workbenchDir, {
46
49
  recursive: true
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/dev/writeWorkbenchRuntime.ts"],"sourcesContent":["import fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport {subdebug} from '@sanity/cli-core'\n\nconst devDebug = subdebug('dev')\n\nconst workbenchJsTemplate = `\\\n// This file is auto-generated on 'sanity dev'\n// Modifications to this file are automatically discarded\nimport {renderWorkbench} from \"sanity/workbench\"\n\nrenderWorkbench(\n document.getElementById(\"workbench\"),\n {organizationId: %SANITY_WORKBENCH_ORGANIZATION_ID%},\n {reactStrictMode: %SANITY_WORKBENCH_REACT_STRICT_MODE%}\n)\n`\n\nconst indexHtmlTemplate = `\\\n<!DOCTYPE html>\n<!-- This file is auto-generated on 'sanity dev' -->\n<!-- Modifications to this file are automatically discarded -->\n<html>\n <head>\n <meta charset=\"UTF-8\" />\n%SANITY_WORKBENCH_PREFETCH_HINTS%\n </head>\n <body>\n <div id=\"workbench\"></div>\n <script type=\"module\" src=\"./workbench.js\"></script>\n </body>\n</html>\n`\n\n/**\n * Generates the `.sanity/workbench` directory with static entry files for\n * the workbench Vite dev server.\n *\n * @param cwd - Current working directory (Sanity root dir)\n * @returns The absolute path to the written workbench runtime directory\n * @internal\n */\nexport async function writeWorkbenchRuntime(options: {\n cwd: string\n organizationId?: string\n reactStrictMode: boolean\n remoteUrl?: string\n}): Promise<string> {\n const {cwd, organizationId, reactStrictMode, remoteUrl} = options\n const workbenchDir = path.join(cwd, '.sanity', 'workbench')\n\n const workbenchJs = workbenchJsTemplate\n .replace(\n /%SANITY_WORKBENCH_ORGANIZATION_ID%/,\n organizationId === undefined ? 'undefined' : JSON.stringify(organizationId),\n )\n .replace(/%SANITY_WORKBENCH_REACT_STRICT_MODE%/, JSON.stringify(reactStrictMode))\n\n const prefetchHints = buildPrefetchHints(remoteUrl)\n\n const indexHtml = indexHtmlTemplate.replace(/%SANITY_WORKBENCH_PREFETCH_HINTS%/, prefetchHints)\n\n devDebug('Making workbench runtime directory')\n await fs.mkdir(workbenchDir, {recursive: true})\n\n devDebug('Writing workbench.js to workbench runtime directory')\n await fs.writeFile(path.join(workbenchDir, 'workbench.js'), workbenchJs)\n\n devDebug('Writing index.html to workbench runtime directory')\n await fs.writeFile(path.join(workbenchDir, 'index.html'), indexHtml)\n\n return workbenchDir\n}\n\nfunction buildPrefetchHints(remoteUrl: string | undefined): string {\n if (!remoteUrl) return ''\n\n try {\n const url = new URL(remoteUrl)\n return [\n ` <link rel=\"preconnect\" href=\"${url.origin}\" />`,\n ` <link rel=\"preload\" as=\"fetch\" href=\"${url.toString()}\" crossorigin />`,\n ].join('\\n')\n } catch {\n return ''\n }\n}\n"],"names":["fs","path","subdebug","devDebug","workbenchJsTemplate","indexHtmlTemplate","writeWorkbenchRuntime","options","cwd","organizationId","reactStrictMode","remoteUrl","workbenchDir","join","workbenchJs","replace","undefined","JSON","stringify","prefetchHints","buildPrefetchHints","indexHtml","mkdir","recursive","writeFile","url","URL","origin","toString"],"mappings":"AAAA,OAAOA,QAAQ,mBAAkB;AACjC,OAAOC,UAAU,YAAW;AAE5B,SAAQC,QAAQ,QAAO,mBAAkB;AAEzC,MAAMC,WAAWD,SAAS;AAE1B,MAAME,sBAAsB,CAAC;;;;;;;;;;AAU7B,CAAC;AAED,MAAMC,oBAAoB,CAAC;;;;;;;;;;;;;;AAc3B,CAAC;AAED;;;;;;;CAOC,GACD,OAAO,eAAeC,sBAAsBC,OAK3C;IACC,MAAM,EAACC,GAAG,EAAEC,cAAc,EAAEC,eAAe,EAAEC,SAAS,EAAC,GAAGJ;IAC1D,MAAMK,eAAeX,KAAKY,IAAI,CAACL,KAAK,WAAW;IAE/C,MAAMM,cAAcV,oBACjBW,OAAO,CACN,sCACAN,mBAAmBO,YAAY,cAAcC,KAAKC,SAAS,CAACT,iBAE7DM,OAAO,CAAC,wCAAwCE,KAAKC,SAAS,CAACR;IAElE,MAAMS,gBAAgBC,mBAAmBT;IAEzC,MAAMU,YAAYhB,kBAAkBU,OAAO,CAAC,qCAAqCI;IAEjFhB,SAAS;IACT,MAAMH,GAAGsB,KAAK,CAACV,cAAc;QAACW,WAAW;IAAI;IAE7CpB,SAAS;IACT,MAAMH,GAAGwB,SAAS,CAACvB,KAAKY,IAAI,CAACD,cAAc,iBAAiBE;IAE5DX,SAAS;IACT,MAAMH,GAAGwB,SAAS,CAACvB,KAAKY,IAAI,CAACD,cAAc,eAAeS;IAE1D,OAAOT;AACT;AAEA,SAASQ,mBAAmBT,SAA6B;IACvD,IAAI,CAACA,WAAW,OAAO;IAEvB,IAAI;QACF,MAAMc,MAAM,IAAIC,IAAIf;QACpB,OAAO;YACL,CAAC,iCAAiC,EAAEc,IAAIE,MAAM,CAAC,IAAI,CAAC;YACpD,CAAC,yCAAyC,EAAEF,IAAIG,QAAQ,GAAG,gBAAgB,CAAC;SAC7E,CAACf,IAAI,CAAC;IACT,EAAE,OAAM;QACN,OAAO;IACT;AACF"}
1
+ {"version":3,"sources":["../../../src/actions/dev/writeWorkbenchRuntime.ts"],"sourcesContent":["import fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport {subdebug} from '@sanity/cli-core'\n\nconst devDebug = subdebug('dev')\n\nconst workbenchJsTemplate = `\\\n// This file is auto-generated on 'sanity dev'\n// Modifications to this file are automatically discarded\nimport {renderWorkbench} from \"sanity/workbench\"\n\nrenderWorkbench(\n document.getElementById(\"workbench\"),\n {organizationId: %SANITY_WORKBENCH_ORGANIZATION_ID%},\n {reactStrictMode: %SANITY_WORKBENCH_REACT_STRICT_MODE%}\n)\n`\n\nconst indexHtmlTemplate = `\\\n<!DOCTYPE html>\n<!-- This file is auto-generated on 'sanity dev' -->\n<!-- Modifications to this file are automatically discarded -->\n<html>\n <head>\n <meta charset=\"UTF-8\" />\n%SANITY_WORKBENCH_PREFETCH_HINTS%\n </head>\n <body>\n <div id=\"workbench\"></div>\n <script>globalThis.__SANITY_STAGING__ = %SANITY_WORKBENCH_STAGING%</script>\n <script type=\"module\" src=\"./workbench.js\"></script>\n </body>\n</html>\n`\n\n/**\n * Generates the `.sanity/workbench` directory with static entry files for\n * the workbench Vite dev server.\n *\n * @param cwd - Current working directory (Sanity root dir)\n * @returns The absolute path to the written workbench runtime directory\n * @internal\n */\nexport async function writeWorkbenchRuntime(options: {\n cwd: string\n organizationId?: string\n reactStrictMode: boolean\n remoteUrl?: string\n}): Promise<string> {\n const {cwd, organizationId, reactStrictMode, remoteUrl} = options\n const workbenchDir = path.join(cwd, '.sanity', 'workbench')\n\n const workbenchJs = workbenchJsTemplate\n .replace(\n /%SANITY_WORKBENCH_ORGANIZATION_ID%/,\n organizationId === undefined ? 'undefined' : JSON.stringify(organizationId),\n )\n .replace(/%SANITY_WORKBENCH_REACT_STRICT_MODE%/, JSON.stringify(reactStrictMode))\n\n const prefetchHints = buildPrefetchHints(remoteUrl)\n\n // The runtime flag builds get via decorateIndexWithStagingScript — a vite\n // `define` never reaches pre-bundled dependencies like the SDK.\n const indexHtml = indexHtmlTemplate\n .replace(/%SANITY_WORKBENCH_PREFETCH_HINTS%/, prefetchHints)\n .replace(\n /%SANITY_WORKBENCH_STAGING%/,\n JSON.stringify(process.env.SANITY_INTERNAL_ENV === 'staging'),\n )\n\n devDebug('Making workbench runtime directory')\n await fs.mkdir(workbenchDir, {recursive: true})\n\n devDebug('Writing workbench.js to workbench runtime directory')\n await fs.writeFile(path.join(workbenchDir, 'workbench.js'), workbenchJs)\n\n devDebug('Writing index.html to workbench runtime directory')\n await fs.writeFile(path.join(workbenchDir, 'index.html'), indexHtml)\n\n return workbenchDir\n}\n\nfunction buildPrefetchHints(remoteUrl: string | undefined): string {\n if (!remoteUrl) return ''\n\n try {\n const url = new URL(remoteUrl)\n return [\n ` <link rel=\"preconnect\" href=\"${url.origin}\" />`,\n ` <link rel=\"preload\" as=\"fetch\" href=\"${url.toString()}\" crossorigin />`,\n ].join('\\n')\n } catch {\n return ''\n }\n}\n"],"names":["fs","path","subdebug","devDebug","workbenchJsTemplate","indexHtmlTemplate","writeWorkbenchRuntime","options","cwd","organizationId","reactStrictMode","remoteUrl","workbenchDir","join","workbenchJs","replace","undefined","JSON","stringify","prefetchHints","buildPrefetchHints","indexHtml","process","env","SANITY_INTERNAL_ENV","mkdir","recursive","writeFile","url","URL","origin","toString"],"mappings":"AAAA,OAAOA,QAAQ,mBAAkB;AACjC,OAAOC,UAAU,YAAW;AAE5B,SAAQC,QAAQ,QAAO,mBAAkB;AAEzC,MAAMC,WAAWD,SAAS;AAE1B,MAAME,sBAAsB,CAAC;;;;;;;;;;AAU7B,CAAC;AAED,MAAMC,oBAAoB,CAAC;;;;;;;;;;;;;;;AAe3B,CAAC;AAED;;;;;;;CAOC,GACD,OAAO,eAAeC,sBAAsBC,OAK3C;IACC,MAAM,EAACC,GAAG,EAAEC,cAAc,EAAEC,eAAe,EAAEC,SAAS,EAAC,GAAGJ;IAC1D,MAAMK,eAAeX,KAAKY,IAAI,CAACL,KAAK,WAAW;IAE/C,MAAMM,cAAcV,oBACjBW,OAAO,CACN,sCACAN,mBAAmBO,YAAY,cAAcC,KAAKC,SAAS,CAACT,iBAE7DM,OAAO,CAAC,wCAAwCE,KAAKC,SAAS,CAACR;IAElE,MAAMS,gBAAgBC,mBAAmBT;IAEzC,0EAA0E;IAC1E,gEAAgE;IAChE,MAAMU,YAAYhB,kBACfU,OAAO,CAAC,qCAAqCI,eAC7CJ,OAAO,CACN,8BACAE,KAAKC,SAAS,CAACI,QAAQC,GAAG,CAACC,mBAAmB,KAAK;IAGvDrB,SAAS;IACT,MAAMH,GAAGyB,KAAK,CAACb,cAAc;QAACc,WAAW;IAAI;IAE7CvB,SAAS;IACT,MAAMH,GAAG2B,SAAS,CAAC1B,KAAKY,IAAI,CAACD,cAAc,iBAAiBE;IAE5DX,SAAS;IACT,MAAMH,GAAG2B,SAAS,CAAC1B,KAAKY,IAAI,CAACD,cAAc,eAAeS;IAE1D,OAAOT;AACT;AAEA,SAASQ,mBAAmBT,SAA6B;IACvD,IAAI,CAACA,WAAW,OAAO;IAEvB,IAAI;QACF,MAAMiB,MAAM,IAAIC,IAAIlB;QACpB,OAAO;YACL,CAAC,iCAAiC,EAAEiB,IAAIE,MAAM,CAAC,IAAI,CAAC;YACpD,CAAC,yCAAyC,EAAEF,IAAIG,QAAQ,GAAG,gBAAgB,CAAC;SAC7E,CAAClB,IAAI,CAAC;IACT,EAAE,OAAM;QACN,OAAO;IACT;AACF"}
package/dist/contract.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { z } from 'zod/mini';
2
2
  // Shared module-federation extension contract: interface (view/service) and
3
- // installation-config declaration schemas, plus the versions the build stamps.
3
+ // config declaration schemas, plus the versions the build stamps.
4
4
  // `zod/mini` keeps the bundle small.
5
5
  /** @internal */ export const VIEW_CONTRACT_VERSION = 1;
6
6
  /** @internal */ export const SERVICE_CONTRACT_VERSION = 1;
7
- /** @internal */ export const MEDIA_LIBRARY_INSTALLATION_CONFIG_CONTRACT_VERSION = 1;
7
+ /** @internal */ export const MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION = 1;
8
8
  /**
9
9
  * Component slots each interface type exposes, in render order. Source of truth
10
10
  * for {@link InterfaceType} and the build; add a type by registering it here.
@@ -23,16 +23,24 @@ function extensionDeclarationFields(kind) {
23
23
  src: z.string()
24
24
  };
25
25
  }
26
+ // Every interface (view, service) shares `name` + `src` + an optional display
27
+ // `title` that defaults to `name` on deploy.
28
+ function interfaceDeclarationFields(kind) {
29
+ return {
30
+ ...extensionDeclarationFields(kind),
31
+ title: z.optional(z.string())
32
+ };
33
+ }
26
34
  const PanelViewSchema = z.object({
27
35
  type: z.literal('panel'),
28
- ...extensionDeclarationFields('View')
36
+ ...interfaceDeclarationFields('View')
29
37
  });
30
38
  /** @internal */ export const InterfaceDeclarationSchema = z.discriminatedUnion('type', [
31
39
  PanelViewSchema
32
40
  ]);
33
41
  const WorkerServiceSchema = z.object({
34
42
  type: z.literal('worker'),
35
- ...extensionDeclarationFields('Service')
43
+ ...interfaceDeclarationFields('Service')
36
44
  });
37
45
  /** @internal */ export const ServiceDeclarationSchema = z.discriminatedUnion('type', [
38
46
  WorkerServiceSchema
@@ -47,15 +55,15 @@ const MediaLibraryFieldSchema = z.object({
47
55
  * @internal
48
56
  */ export const INSTALLATION_CONFIG_TYPE = 'installation_config';
49
57
  // `appType` is stamped by `unstable_defineMediaLibrary`, never authored.
50
- const MediaLibraryInstallationConfigSchema = z.object({
58
+ const MediaLibraryConfigSchema = z.object({
51
59
  appType: z.literal('media-library'),
52
60
  fields: z.array(MediaLibraryFieldSchema).check(z.refine((fields)=>new Set(fields.map((field)=>field.name)).size === fields.length, 'Field `name` must be unique within a media library'))
53
61
  });
54
62
  /**
55
- * An app's optional installation config, keyed by `appType`; deploys as a versioned snapshot, not an interface.
63
+ * An app's optional config, keyed by `appType`; deploys as a versioned snapshot, not an interface.
56
64
  * @internal
57
- */ export const InstallationConfigSchema = z.discriminatedUnion('appType', [
58
- MediaLibraryInstallationConfigSchema
65
+ */ export const ConfigSchema = z.discriminatedUnion('appType', [
66
+ MediaLibraryConfigSchema
59
67
  ]);
60
68
 
61
69
  //# sourceMappingURL=contract.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/contract.ts"],"sourcesContent":["import {z} from 'zod/mini'\n\n// Shared module-federation extension contract: interface (view/service) and\n// installation-config declaration schemas, plus the versions the build stamps.\n// `zod/mini` keeps the bundle small.\n\n/** @internal */\nexport const VIEW_CONTRACT_VERSION = 1\n\n/** @internal */\nexport const SERVICE_CONTRACT_VERSION = 1\n\n/** @internal */\nexport const MEDIA_LIBRARY_INSTALLATION_CONFIG_CONTRACT_VERSION = 1\n\n/**\n * A view component. The return is opaque so the runtime helpers carry no React\n * dependency — the generated artifact renders it with the app's own React.\n * @public\n */\nexport type ViewComponent<TProps> = (props: TProps) => unknown\n\n/** @public */\nexport interface ViewComponentBaseProps<TView> {\n view: TView\n}\n\n/**\n * Component slots each interface type exposes, in render order. Source of truth\n * for {@link InterfaceType} and the build; add a type by registering it here.\n * @internal\n */\nexport const VIEW_COMPONENTS = {\n panel: ['title', 'panel'],\n} as const satisfies Record<string, readonly string[]>\n\n/** @public */\nexport type InterfaceType = keyof typeof VIEW_COMPONENTS\n\n/** @public */\nexport type ServiceType = 'worker'\n\n// Shared `name` + `src`; `kind` only tailors the validation message.\nfunction extensionDeclarationFields(kind: 'Field' | 'Service' | 'View') {\n const pattern = /^[a-zA-Z0-9_-]+$/\n return {\n name: z.string().check(z.regex(pattern, `${kind} \\`name\\` must match ${pattern}`)),\n src: z.string(),\n }\n}\n\nconst PanelViewSchema = z.object({\n type: z.literal('panel'),\n ...extensionDeclarationFields('View'),\n})\n\n/** @internal */\nexport const InterfaceDeclarationSchema = z.discriminatedUnion('type', [PanelViewSchema])\n\nconst WorkerServiceSchema = z.object({\n type: z.literal('worker'),\n ...extensionDeclarationFields('Service'),\n})\n\n/** @internal */\nexport const ServiceDeclarationSchema = z.discriminatedUnion('type', [WorkerServiceSchema])\n\nconst MediaLibraryFieldSchema = z.object({\n ...extensionDeclarationFields('Field'),\n public: z.optional(z.boolean()),\n title: z.string(),\n})\n\n/**\n * Stamped where the config crosses a boundary so the authoring model doesn't carry a constant discriminator.\n * @internal\n */\nexport const INSTALLATION_CONFIG_TYPE = 'installation_config'\n\n// `appType` is stamped by `unstable_defineMediaLibrary`, never authored.\nconst MediaLibraryInstallationConfigSchema = z.object({\n appType: z.literal('media-library'),\n fields: z\n .array(MediaLibraryFieldSchema)\n .check(\n z.refine(\n (fields) => new Set(fields.map((field) => field.name)).size === fields.length,\n 'Field `name` must be unique within a media library',\n ),\n ),\n})\n\n/**\n * An app's optional installation config, keyed by `appType`; deploys as a versioned snapshot, not an interface.\n * @internal\n */\nexport const InstallationConfigSchema = z.discriminatedUnion('appType', [\n MediaLibraryInstallationConfigSchema,\n])\n"],"names":["z","VIEW_CONTRACT_VERSION","SERVICE_CONTRACT_VERSION","MEDIA_LIBRARY_INSTALLATION_CONFIG_CONTRACT_VERSION","VIEW_COMPONENTS","panel","extensionDeclarationFields","kind","pattern","name","string","check","regex","src","PanelViewSchema","object","type","literal","InterfaceDeclarationSchema","discriminatedUnion","WorkerServiceSchema","ServiceDeclarationSchema","MediaLibraryFieldSchema","public","optional","boolean","title","INSTALLATION_CONFIG_TYPE","MediaLibraryInstallationConfigSchema","appType","fields","array","refine","Set","map","field","size","length","InstallationConfigSchema"],"mappings":"AAAA,SAAQA,CAAC,QAAO,WAAU;AAE1B,4EAA4E;AAC5E,+EAA+E;AAC/E,qCAAqC;AAErC,cAAc,GACd,OAAO,MAAMC,wBAAwB,EAAC;AAEtC,cAAc,GACd,OAAO,MAAMC,2BAA2B,EAAC;AAEzC,cAAc,GACd,OAAO,MAAMC,qDAAqD,EAAC;AAcnE;;;;CAIC,GACD,OAAO,MAAMC,kBAAkB;IAC7BC,OAAO;QAAC;QAAS;KAAQ;AAC3B,EAAsD;AAQtD,qEAAqE;AACrE,SAASC,2BAA2BC,IAAkC;IACpE,MAAMC,UAAU;IAChB,OAAO;QACLC,MAAMT,EAAEU,MAAM,GAAGC,KAAK,CAACX,EAAEY,KAAK,CAACJ,SAAS,GAAGD,KAAK,qBAAqB,EAAEC,SAAS;QAChFK,KAAKb,EAAEU,MAAM;IACf;AACF;AAEA,MAAMI,kBAAkBd,EAAEe,MAAM,CAAC;IAC/BC,MAAMhB,EAAEiB,OAAO,CAAC;IAChB,GAAGX,2BAA2B,OAAO;AACvC;AAEA,cAAc,GACd,OAAO,MAAMY,6BAA6BlB,EAAEmB,kBAAkB,CAAC,QAAQ;IAACL;CAAgB,EAAC;AAEzF,MAAMM,sBAAsBpB,EAAEe,MAAM,CAAC;IACnCC,MAAMhB,EAAEiB,OAAO,CAAC;IAChB,GAAGX,2BAA2B,UAAU;AAC1C;AAEA,cAAc,GACd,OAAO,MAAMe,2BAA2BrB,EAAEmB,kBAAkB,CAAC,QAAQ;IAACC;CAAoB,EAAC;AAE3F,MAAME,0BAA0BtB,EAAEe,MAAM,CAAC;IACvC,GAAGT,2BAA2B,QAAQ;IACtCiB,QAAQvB,EAAEwB,QAAQ,CAACxB,EAAEyB,OAAO;IAC5BC,OAAO1B,EAAEU,MAAM;AACjB;AAEA;;;CAGC,GACD,OAAO,MAAMiB,2BAA2B,sBAAqB;AAE7D,yEAAyE;AACzE,MAAMC,uCAAuC5B,EAAEe,MAAM,CAAC;IACpDc,SAAS7B,EAAEiB,OAAO,CAAC;IACnBa,QAAQ9B,EACL+B,KAAK,CAACT,yBACNX,KAAK,CACJX,EAAEgC,MAAM,CACN,CAACF,SAAW,IAAIG,IAAIH,OAAOI,GAAG,CAAC,CAACC,QAAUA,MAAM1B,IAAI,GAAG2B,IAAI,KAAKN,OAAOO,MAAM,EAC7E;AAGR;AAEA;;;CAGC,GACD,OAAO,MAAMC,2BAA2BtC,EAAEmB,kBAAkB,CAAC,WAAW;IACtES;CACD,EAAC"}
1
+ {"version":3,"sources":["../src/contract.ts"],"sourcesContent":["import {z} from 'zod/mini'\n\n// Shared module-federation extension contract: interface (view/service) and\n// config declaration schemas, plus the versions the build stamps.\n// `zod/mini` keeps the bundle small.\n\n/** @internal */\nexport const VIEW_CONTRACT_VERSION = 1\n\n/** @internal */\nexport const SERVICE_CONTRACT_VERSION = 1\n\n/** @internal */\nexport const MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION = 1\n\n/**\n * A view component. The return is opaque so the runtime helpers carry no React\n * dependency — the generated artifact renders it with the app's own React.\n * @public\n */\nexport type ViewComponent<TProps> = (props: TProps) => unknown\n\n/** @public */\nexport interface ViewComponentBaseProps<TView> {\n view: TView\n}\n\n/**\n * Component slots each interface type exposes, in render order. Source of truth\n * for {@link InterfaceType} and the build; add a type by registering it here.\n * @internal\n */\nexport const VIEW_COMPONENTS = {\n panel: ['title', 'panel'],\n} as const satisfies Record<string, readonly string[]>\n\n/** @public */\nexport type InterfaceType = keyof typeof VIEW_COMPONENTS\n\n/** @public */\nexport type ServiceType = 'worker'\n\n// Shared `name` + `src`; `kind` only tailors the validation message.\nfunction extensionDeclarationFields(kind: 'Field' | 'Service' | 'View') {\n const pattern = /^[a-zA-Z0-9_-]+$/\n return {\n name: z.string().check(z.regex(pattern, `${kind} \\`name\\` must match ${pattern}`)),\n src: z.string(),\n }\n}\n\n// Every interface (view, service) shares `name` + `src` + an optional display\n// `title` that defaults to `name` on deploy.\nfunction interfaceDeclarationFields(kind: 'Service' | 'View') {\n return {...extensionDeclarationFields(kind), title: z.optional(z.string())}\n}\n\nconst PanelViewSchema = z.object({\n type: z.literal('panel'),\n ...interfaceDeclarationFields('View'),\n})\n\n/** @internal */\nexport const InterfaceDeclarationSchema = z.discriminatedUnion('type', [PanelViewSchema])\n\nconst WorkerServiceSchema = z.object({\n type: z.literal('worker'),\n ...interfaceDeclarationFields('Service'),\n})\n\n/** @internal */\nexport const ServiceDeclarationSchema = z.discriminatedUnion('type', [WorkerServiceSchema])\n\nconst MediaLibraryFieldSchema = z.object({\n ...extensionDeclarationFields('Field'),\n public: z.optional(z.boolean()),\n title: z.string(),\n})\n\n/**\n * Stamped where the config crosses a boundary so the authoring model doesn't carry a constant discriminator.\n * @internal\n */\nexport const INSTALLATION_CONFIG_TYPE = 'installation_config'\n\n// `appType` is stamped by `unstable_defineMediaLibrary`, never authored.\nconst MediaLibraryConfigSchema = z.object({\n appType: z.literal('media-library'),\n fields: z\n .array(MediaLibraryFieldSchema)\n .check(\n z.refine(\n (fields) => new Set(fields.map((field) => field.name)).size === fields.length,\n 'Field `name` must be unique within a media library',\n ),\n ),\n})\n\n/**\n * An app's optional config, keyed by `appType`; deploys as a versioned snapshot, not an interface.\n * @internal\n */\nexport const ConfigSchema = z.discriminatedUnion('appType', [MediaLibraryConfigSchema])\n"],"names":["z","VIEW_CONTRACT_VERSION","SERVICE_CONTRACT_VERSION","MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION","VIEW_COMPONENTS","panel","extensionDeclarationFields","kind","pattern","name","string","check","regex","src","interfaceDeclarationFields","title","optional","PanelViewSchema","object","type","literal","InterfaceDeclarationSchema","discriminatedUnion","WorkerServiceSchema","ServiceDeclarationSchema","MediaLibraryFieldSchema","public","boolean","INSTALLATION_CONFIG_TYPE","MediaLibraryConfigSchema","appType","fields","array","refine","Set","map","field","size","length","ConfigSchema"],"mappings":"AAAA,SAAQA,CAAC,QAAO,WAAU;AAE1B,4EAA4E;AAC5E,kEAAkE;AAClE,qCAAqC;AAErC,cAAc,GACd,OAAO,MAAMC,wBAAwB,EAAC;AAEtC,cAAc,GACd,OAAO,MAAMC,2BAA2B,EAAC;AAEzC,cAAc,GACd,OAAO,MAAMC,wCAAwC,EAAC;AActD;;;;CAIC,GACD,OAAO,MAAMC,kBAAkB;IAC7BC,OAAO;QAAC;QAAS;KAAQ;AAC3B,EAAsD;AAQtD,qEAAqE;AACrE,SAASC,2BAA2BC,IAAkC;IACpE,MAAMC,UAAU;IAChB,OAAO;QACLC,MAAMT,EAAEU,MAAM,GAAGC,KAAK,CAACX,EAAEY,KAAK,CAACJ,SAAS,GAAGD,KAAK,qBAAqB,EAAEC,SAAS;QAChFK,KAAKb,EAAEU,MAAM;IACf;AACF;AAEA,8EAA8E;AAC9E,6CAA6C;AAC7C,SAASI,2BAA2BP,IAAwB;IAC1D,OAAO;QAAC,GAAGD,2BAA2BC,KAAK;QAAEQ,OAAOf,EAAEgB,QAAQ,CAAChB,EAAEU,MAAM;IAAG;AAC5E;AAEA,MAAMO,kBAAkBjB,EAAEkB,MAAM,CAAC;IAC/BC,MAAMnB,EAAEoB,OAAO,CAAC;IAChB,GAAGN,2BAA2B,OAAO;AACvC;AAEA,cAAc,GACd,OAAO,MAAMO,6BAA6BrB,EAAEsB,kBAAkB,CAAC,QAAQ;IAACL;CAAgB,EAAC;AAEzF,MAAMM,sBAAsBvB,EAAEkB,MAAM,CAAC;IACnCC,MAAMnB,EAAEoB,OAAO,CAAC;IAChB,GAAGN,2BAA2B,UAAU;AAC1C;AAEA,cAAc,GACd,OAAO,MAAMU,2BAA2BxB,EAAEsB,kBAAkB,CAAC,QAAQ;IAACC;CAAoB,EAAC;AAE3F,MAAME,0BAA0BzB,EAAEkB,MAAM,CAAC;IACvC,GAAGZ,2BAA2B,QAAQ;IACtCoB,QAAQ1B,EAAEgB,QAAQ,CAAChB,EAAE2B,OAAO;IAC5BZ,OAAOf,EAAEU,MAAM;AACjB;AAEA;;;CAGC,GACD,OAAO,MAAMkB,2BAA2B,sBAAqB;AAE7D,yEAAyE;AACzE,MAAMC,2BAA2B7B,EAAEkB,MAAM,CAAC;IACxCY,SAAS9B,EAAEoB,OAAO,CAAC;IACnBW,QAAQ/B,EACLgC,KAAK,CAACP,yBACNd,KAAK,CACJX,EAAEiC,MAAM,CACN,CAACF,SAAW,IAAIG,IAAIH,OAAOI,GAAG,CAAC,CAACC,QAAUA,MAAM3B,IAAI,GAAG4B,IAAI,KAAKN,OAAOO,MAAM,EAC7E;AAGR;AAEA;;;CAGC,GACD,OAAO,MAAMC,eAAevC,EAAEsB,kBAAkB,CAAC,WAAW;IAACO;CAAyB,EAAC"}