@sanity/workbench-cli 1.5.0 → 1.7.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 (48) hide show
  1. package/dist/_exports/build.d.ts +47 -3
  2. package/dist/_exports/build.js +2 -0
  3. package/dist/_exports/build.js.map +1 -1
  4. package/dist/_exports/deploy.d.ts +60 -37
  5. package/dist/_exports/deploy.js +1 -1
  6. package/dist/_exports/deploy.js.map +1 -1
  7. package/dist/_exports/dev.d.ts +0 -2
  8. package/dist/_exports/index.d.ts +39 -1
  9. package/dist/_exports/index.js +1 -0
  10. package/dist/_exports/index.js.map +1 -1
  11. package/dist/_exports/init.d.ts +2 -2
  12. package/dist/_exports/preview.d.ts +169 -0
  13. package/dist/_exports/preview.js +3 -0
  14. package/dist/_exports/preview.js.map +1 -0
  15. package/dist/_exports/undeploy.d.ts +7 -3
  16. package/dist/actions/build/vite/optimize-deps.js +84 -0
  17. package/dist/actions/build/vite/optimize-deps.js.map +1 -0
  18. package/dist/actions/build/vite/plugins/plugin-sanity-app-id.js +15 -1
  19. package/dist/actions/build/vite/plugins/plugin-sanity-app-id.js.map +1 -1
  20. package/dist/actions/deploy/checkBuiltOutput.js +16 -2
  21. package/dist/actions/deploy/checkBuiltOutput.js.map +1 -1
  22. package/dist/actions/deploy/deployWorkbenchApp.js +55 -73
  23. package/dist/actions/deploy/deployWorkbenchApp.js.map +1 -1
  24. package/dist/actions/dev/registry.js +69 -1
  25. package/dist/actions/dev/registry.js.map +1 -1
  26. package/dist/actions/dev/startDevServerRegistration.js +9 -2
  27. package/dist/actions/dev/startDevServerRegistration.js.map +1 -1
  28. package/dist/actions/dev/startWorkbenchDev.js +7 -43
  29. package/dist/actions/dev/startWorkbenchDev.js.map +1 -1
  30. package/dist/actions/dev/startWorkbenchDevServer.js +7 -3
  31. package/dist/actions/dev/startWorkbenchDevServer.js.map +1 -1
  32. package/dist/actions/init/cliConfig.js +2 -0
  33. package/dist/actions/init/cliConfig.js.map +1 -1
  34. package/dist/actions/preview/serveBuiltApplication.js +53 -0
  35. package/dist/actions/preview/serveBuiltApplication.js.map +1 -0
  36. package/dist/actions/preview/startWorkbenchPreview.js +107 -0
  37. package/dist/actions/preview/startWorkbenchPreview.js.map +1 -0
  38. package/dist/appId.js +52 -0
  39. package/dist/appId.js.map +1 -0
  40. package/dist/defineApp.js +2 -5
  41. package/dist/defineApp.js.map +1 -1
  42. package/dist/resolveWorkbenchApp.js +2 -0
  43. package/dist/resolveWorkbenchApp.js.map +1 -1
  44. package/dist/services/applications.js +37 -20
  45. package/dist/services/applications.js.map +1 -1
  46. package/dist/util/serverOrchestration.js +75 -0
  47. package/dist/util/serverOrchestration.js.map +1 -0
  48. package/package.json +7 -3
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * React APIs only the generated render-contract modules import — the SPA
5
+ * bootstrap (`getEntryModule`) and every render artifact (`renderRemote`)
6
+ * `createRoot` from `react-dom/client` and `createElement` from `react`. No
7
+ * app/interface source imports `react-dom/client` itself, so scanning `entries`
8
+ * never surfaces it; every workbench app depends on react/react-dom, so both
9
+ * always resolve.
10
+ */ const RENDER_CONTRACT_DEPS = [
11
+ 'react',
12
+ 'react-dom/client'
13
+ ];
14
+ /** Extensions an import-style source path may omit, in resolution order. */ const SOURCE_EXTENSIONS = [
15
+ '.tsx',
16
+ '.ts',
17
+ '.jsx',
18
+ '.js',
19
+ '.mjs',
20
+ '.cjs'
21
+ ];
22
+ /**
23
+ * Resolve an import-style source path to the file on disk. The app `entry`
24
+ * defaults to the extensionless `./src/App` (and a user may omit the extension
25
+ * anywhere), which the runtime imports fine through Vite's resolver — but
26
+ * `optimizeDeps.entries` is matched against the filesystem, so an extensionless
27
+ * path never matches and its deps go unscanned. Try the path as-is, then the
28
+ * common source extensions; fall back to the input so an already-resolved path
29
+ * (or a genuinely missing one) passes through unchanged.
30
+ */ function resolveSourceFile(absPath) {
31
+ if (fs.existsSync(absPath) && fs.statSync(absPath).isFile()) return absPath;
32
+ for (const extension of SOURCE_EXTENSIONS){
33
+ if (fs.existsSync(`${absPath}${extension}`)) return `${absPath}${extension}`;
34
+ }
35
+ return absPath;
36
+ }
37
+ /**
38
+ * Dep pre-bundling inputs for a workbench app's dev server.
39
+ *
40
+ * Vite's dep scanner crawls from the app's HTML entry, which reaches only the
41
+ * app's `entry` (or a studio's config) — never the federation `exposes` (dock
42
+ * views, worker services, media-library config fields), which the host loads
43
+ * dynamically at runtime. So a dep imported only by an exposed module (e.g.
44
+ * `sanity/workbench` in a service or view) escapes the startup scan; Vite
45
+ * discovers it on first request, re-optimizes, and full-page reloads — which
46
+ * flakes Playwright in e2e.
47
+ *
48
+ * Point `entries` at the source of every interface: the app `entry` (or, for a
49
+ * studio, its config), plus each view, service, and installation-config source.
50
+ * `entries` follows real import statements, so it covers whatever those sources
51
+ * transitively use — correct subpaths and all — with no hand-maintained dep
52
+ * list, and never crashes on an unresolvable name the way a bad `include` entry
53
+ * does. Each source is resolved to its on-disk file so an extensionless path
54
+ * (like the default `./src/App`) still matches `entries`' filesystem glob.
55
+ *
56
+ * @param cwd - Project root; `entries` are returned relative to it.
57
+ * @param appSources - Absolute paths to the app's `entry` and/or studio config.
58
+ * @param exposes - The app's declared views/services/config.
59
+ * @internal
60
+ */ export function workbenchOptimizeDeps(options) {
61
+ const { appSources, cwd, exposes } = options;
62
+ const interfaceSources = [
63
+ ...(exposes?.views ?? []).map((view)=>view.src),
64
+ ...(exposes?.services ?? []).map((service)=>service.src),
65
+ ...(exposes?.config?.fields ?? []).map((field)=>field.src)
66
+ ].map((src)=>path.resolve(cwd, src));
67
+ const entries = [
68
+ ...appSources,
69
+ ...interfaceSources
70
+ ].map((absPath)=>{
71
+ const resolved = resolveSourceFile(absPath);
72
+ return path.relative(cwd, resolved).split(path.sep).join('/');
73
+ });
74
+ return {
75
+ entries: [
76
+ ...new Set(entries)
77
+ ],
78
+ include: [
79
+ ...RENDER_CONTRACT_DEPS
80
+ ]
81
+ };
82
+ }
83
+
84
+ //# sourceMappingURL=optimize-deps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/actions/build/vite/optimize-deps.ts"],"sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\n\nimport {type WorkbenchExposes} from '../../../resolveWorkbenchApp.js'\n\n/**\n * React APIs only the generated render-contract modules import — the SPA\n * bootstrap (`getEntryModule`) and every render artifact (`renderRemote`)\n * `createRoot` from `react-dom/client` and `createElement` from `react`. No\n * app/interface source imports `react-dom/client` itself, so scanning `entries`\n * never surfaces it; every workbench app depends on react/react-dom, so both\n * always resolve.\n */\nconst RENDER_CONTRACT_DEPS = ['react', 'react-dom/client']\n\n/** Extensions an import-style source path may omit, in resolution order. */\nconst SOURCE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs']\n\n/**\n * Resolve an import-style source path to the file on disk. The app `entry`\n * defaults to the extensionless `./src/App` (and a user may omit the extension\n * anywhere), which the runtime imports fine through Vite's resolver — but\n * `optimizeDeps.entries` is matched against the filesystem, so an extensionless\n * path never matches and its deps go unscanned. Try the path as-is, then the\n * common source extensions; fall back to the input so an already-resolved path\n * (or a genuinely missing one) passes through unchanged.\n */\nfunction resolveSourceFile(absPath: string): string {\n if (fs.existsSync(absPath) && fs.statSync(absPath).isFile()) return absPath\n for (const extension of SOURCE_EXTENSIONS) {\n if (fs.existsSync(`${absPath}${extension}`)) return `${absPath}${extension}`\n }\n return absPath\n}\n\n/**\n * Dep pre-bundling inputs for a workbench app's dev server.\n *\n * Vite's dep scanner crawls from the app's HTML entry, which reaches only the\n * app's `entry` (or a studio's config) — never the federation `exposes` (dock\n * views, worker services, media-library config fields), which the host loads\n * dynamically at runtime. So a dep imported only by an exposed module (e.g.\n * `sanity/workbench` in a service or view) escapes the startup scan; Vite\n * discovers it on first request, re-optimizes, and full-page reloads — which\n * flakes Playwright in e2e.\n *\n * Point `entries` at the source of every interface: the app `entry` (or, for a\n * studio, its config), plus each view, service, and installation-config source.\n * `entries` follows real import statements, so it covers whatever those sources\n * transitively use — correct subpaths and all — with no hand-maintained dep\n * list, and never crashes on an unresolvable name the way a bad `include` entry\n * does. Each source is resolved to its on-disk file so an extensionless path\n * (like the default `./src/App`) still matches `entries`' filesystem glob.\n *\n * @param cwd - Project root; `entries` are returned relative to it.\n * @param appSources - Absolute paths to the app's `entry` and/or studio config.\n * @param exposes - The app's declared views/services/config.\n * @internal\n */\nexport function workbenchOptimizeDeps(options: {\n appSources: readonly string[]\n cwd: string\n exposes?: WorkbenchExposes\n}): {entries: string[]; include: string[]} {\n const {appSources, cwd, exposes} = options\n\n const interfaceSources = [\n ...(exposes?.views ?? []).map((view) => view.src),\n ...(exposes?.services ?? []).map((service) => service.src),\n ...(exposes?.config?.fields ?? []).map((field) => field.src),\n ].map((src) => path.resolve(cwd, src))\n\n const entries = [...appSources, ...interfaceSources].map((absPath) => {\n const resolved = resolveSourceFile(absPath)\n return path.relative(cwd, resolved).split(path.sep).join('/')\n })\n\n return {\n entries: [...new Set(entries)],\n include: [...RENDER_CONTRACT_DEPS],\n }\n}\n"],"names":["fs","path","RENDER_CONTRACT_DEPS","SOURCE_EXTENSIONS","resolveSourceFile","absPath","existsSync","statSync","isFile","extension","workbenchOptimizeDeps","options","appSources","cwd","exposes","interfaceSources","views","map","view","src","services","service","config","fields","field","resolve","entries","resolved","relative","split","sep","join","Set","include"],"mappings":"AAAA,OAAOA,QAAQ,UAAS;AACxB,OAAOC,UAAU,YAAW;AAI5B;;;;;;;CAOC,GACD,MAAMC,uBAAuB;IAAC;IAAS;CAAmB;AAE1D,0EAA0E,GAC1E,MAAMC,oBAAoB;IAAC;IAAQ;IAAO;IAAQ;IAAO;IAAQ;CAAO;AAExE;;;;;;;;CAQC,GACD,SAASC,kBAAkBC,OAAe;IACxC,IAAIL,GAAGM,UAAU,CAACD,YAAYL,GAAGO,QAAQ,CAACF,SAASG,MAAM,IAAI,OAAOH;IACpE,KAAK,MAAMI,aAAaN,kBAAmB;QACzC,IAAIH,GAAGM,UAAU,CAAC,GAAGD,UAAUI,WAAW,GAAG,OAAO,GAAGJ,UAAUI,WAAW;IAC9E;IACA,OAAOJ;AACT;AAEA;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GACD,OAAO,SAASK,sBAAsBC,OAIrC;IACC,MAAM,EAACC,UAAU,EAAEC,GAAG,EAAEC,OAAO,EAAC,GAAGH;IAEnC,MAAMI,mBAAmB;WACpB,AAACD,CAAAA,SAASE,SAAS,EAAE,AAAD,EAAGC,GAAG,CAAC,CAACC,OAASA,KAAKC,GAAG;WAC7C,AAACL,CAAAA,SAASM,YAAY,EAAE,AAAD,EAAGH,GAAG,CAAC,CAACI,UAAYA,QAAQF,GAAG;WACtD,AAACL,CAAAA,SAASQ,QAAQC,UAAU,EAAE,AAAD,EAAGN,GAAG,CAAC,CAACO,QAAUA,MAAML,GAAG;KAC5D,CAACF,GAAG,CAAC,CAACE,MAAQlB,KAAKwB,OAAO,CAACZ,KAAKM;IAEjC,MAAMO,UAAU;WAAId;WAAeG;KAAiB,CAACE,GAAG,CAAC,CAACZ;QACxD,MAAMsB,WAAWvB,kBAAkBC;QACnC,OAAOJ,KAAK2B,QAAQ,CAACf,KAAKc,UAAUE,KAAK,CAAC5B,KAAK6B,GAAG,EAAEC,IAAI,CAAC;IAC3D;IAEA,OAAO;QACLL,SAAS;eAAI,IAAIM,IAAIN;SAAS;QAC9BO,SAAS;eAAI/B;SAAqB;IACpC;AACF"}
@@ -1,13 +1,18 @@
1
+ import { SANITY_APP_ID_FILE } from '../../../../appId.js';
1
2
  /**
2
- * Bake the app's bus identity into its bundle: `@sanity/runtime` reads
3
+ * Inline the app's bus identity into its bundle: `@sanity/runtime` reads
3
4
  * `__SANITY_APP_ID__` where it connects. `define` covers everything the
4
5
  * pipeline transforms (all of a production build, and dev-served source); the
5
6
  * rolldown define covers dev's pre-bundled dependencies, which skip Vite's
6
7
  * define transform.
8
+ *
9
+ * A build also writes the id to disk so `sanity start`, which serves the build
10
+ * without recompiling, can advertise the exact id the bundle uses.
7
11
  */ export function sanityAppId(appId) {
8
12
  const define = {
9
13
  __SANITY_APP_ID__: JSON.stringify(appId)
10
14
  };
15
+ let emitted = false;
11
16
  return {
12
17
  config: ()=>({
13
18
  define,
@@ -19,6 +24,15 @@
19
24
  }
20
25
  }
21
26
  }),
27
+ generateBundle () {
28
+ if (emitted) return;
29
+ emitted = true;
30
+ this.emitFile({
31
+ fileName: SANITY_APP_ID_FILE,
32
+ source: appId,
33
+ type: 'asset'
34
+ });
35
+ },
22
36
  name: 'sanity/workbench/app-id'
23
37
  };
24
38
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../src/actions/build/vite/plugins/plugin-sanity-app-id.ts"],"sourcesContent":["import {type Plugin} from 'vite'\n\n/**\n * Bake the app's bus identity into its bundle: `@sanity/runtime` reads\n * `__SANITY_APP_ID__` where it connects. `define` covers everything the\n * pipeline transforms (all of a production build, and dev-served source); the\n * rolldown define covers dev's pre-bundled dependencies, which skip Vite's\n * define transform.\n */\nexport function sanityAppId(appId: string): Plugin {\n const define = {__SANITY_APP_ID__: JSON.stringify(appId)}\n return {\n config: () => ({define, optimizeDeps: {rolldownOptions: {transform: {define}}}}),\n name: 'sanity/workbench/app-id',\n }\n}\n"],"names":["sanityAppId","appId","define","__SANITY_APP_ID__","JSON","stringify","config","optimizeDeps","rolldownOptions","transform","name"],"mappings":"AAEA;;;;;;CAMC,GACD,OAAO,SAASA,YAAYC,KAAa;IACvC,MAAMC,SAAS;QAACC,mBAAmBC,KAAKC,SAAS,CAACJ;IAAM;IACxD,OAAO;QACLK,QAAQ,IAAO,CAAA;gBAACJ;gBAAQK,cAAc;oBAACC,iBAAiB;wBAACC,WAAW;4BAACP;wBAAM;oBAAC;gBAAC;YAAC,CAAA;QAC9EQ,MAAM;IACR;AACF"}
1
+ {"version":3,"sources":["../../../../../src/actions/build/vite/plugins/plugin-sanity-app-id.ts"],"sourcesContent":["import {type Plugin} from 'vite'\n\nimport {SANITY_APP_ID_FILE} from '../../../../appId.js'\n\n/**\n * Inline the app's bus identity into its bundle: `@sanity/runtime` reads\n * `__SANITY_APP_ID__` where it connects. `define` covers everything the\n * pipeline transforms (all of a production build, and dev-served source); the\n * rolldown define covers dev's pre-bundled dependencies, which skip Vite's\n * define transform.\n *\n * A build also writes the id to disk so `sanity start`, which serves the build\n * without recompiling, can advertise the exact id the bundle uses.\n */\nexport function sanityAppId(appId: string): Plugin {\n const define = {__SANITY_APP_ID__: JSON.stringify(appId)}\n let emitted = false\n return {\n config: () => ({define, optimizeDeps: {rolldownOptions: {transform: {define}}}}),\n generateBundle() {\n if (emitted) return\n emitted = true\n this.emitFile({fileName: SANITY_APP_ID_FILE, source: appId, type: 'asset'})\n },\n name: 'sanity/workbench/app-id',\n }\n}\n"],"names":["SANITY_APP_ID_FILE","sanityAppId","appId","define","__SANITY_APP_ID__","JSON","stringify","emitted","config","optimizeDeps","rolldownOptions","transform","generateBundle","emitFile","fileName","source","type","name"],"mappings":"AAEA,SAAQA,kBAAkB,QAAO,uBAAsB;AAEvD;;;;;;;;;CASC,GACD,OAAO,SAASC,YAAYC,KAAa;IACvC,MAAMC,SAAS;QAACC,mBAAmBC,KAAKC,SAAS,CAACJ;IAAM;IACxD,IAAIK,UAAU;IACd,OAAO;QACLC,QAAQ,IAAO,CAAA;gBAACL;gBAAQM,cAAc;oBAACC,iBAAiB;wBAACC,WAAW;4BAACR;wBAAM;oBAAC;gBAAC;YAAC,CAAA;QAC9ES;YACE,IAAIL,SAAS;YACbA,UAAU;YACV,IAAI,CAACM,QAAQ,CAAC;gBAACC,UAAUd;gBAAoBe,QAAQb;gBAAOc,MAAM;YAAO;QAC3E;QACAC,MAAM;IACR;AACF"}
@@ -1,5 +1,15 @@
1
1
  import { stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ /**
4
+ * A genuinely-missing-build error, named so callers can offer the "run sanity
5
+ * build" hint (the `preview` command keys off this name, matching how the studio
6
+ * path keys off a missing `index.html`). Only the missing cases get it — real
7
+ * I/O failures keep their own name so they surface as themselves.
8
+ */ function buildNotFound(message) {
9
+ const error = new Error(message);
10
+ error.name = 'BUILD_NOT_FOUND';
11
+ return error;
12
+ }
3
13
  /**
4
14
  * Throws unless `sourceDir` is a directory holding a federation build.
5
15
  * A workbench build always emits a module-federation remote, and may
@@ -13,13 +23,17 @@ import { join } from 'node:path';
13
23
  throw new Error(`"${sourceDir}" is not a directory`);
14
24
  }
15
25
  } catch (err) {
16
- throw err.code === 'ENOENT' ? new Error(`Directory "${sourceDir}" does not exist`) : err;
26
+ if (err.code === 'ENOENT') throw buildNotFound(`Directory "${sourceDir}" does not exist`);
27
+ throw err;
17
28
  }
18
29
  const manifestPath = join(sourceDir, 'mf-manifest.json');
19
30
  try {
20
31
  await stat(manifestPath);
21
32
  } catch (err) {
22
- throw err.code === 'ENOENT' ? new Error(`"${manifestPath}" does not exist. ` + 'The deploy directory must contain a federation build created with "sanity build".') : err;
33
+ if (err.code === 'ENOENT') {
34
+ throw buildNotFound(`"${manifestPath}" does not exist. ` + 'The deploy directory must contain a federation build created with "sanity build".');
35
+ }
36
+ throw err;
23
37
  }
24
38
  }
25
39
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/deploy/checkBuiltOutput.ts"],"sourcesContent":["import {stat} from 'node:fs/promises'\nimport {join} from 'node:path'\n\n/**\n * Throws unless `sourceDir` is a directory holding a federation build.\n * A workbench build always emits a module-federation remote, and may\n * additionally emit a standalone `index.html` SPA (workbench remotes). Either\n * way `mf-manifest.json` is the reliable marker that `sanity build` produced a\n * federation build, so that — not `index.html` — is what we check for.\n */\nexport async function checkBuiltOutput(sourceDir: string): Promise<void> {\n try {\n const stats = await stat(sourceDir)\n if (!stats.isDirectory()) {\n throw new Error(`\"${sourceDir}\" is not a directory`)\n }\n } catch (err) {\n throw err.code === 'ENOENT' ? new Error(`Directory \"${sourceDir}\" does not exist`) : err\n }\n\n const manifestPath = join(sourceDir, 'mf-manifest.json')\n try {\n await stat(manifestPath)\n } catch (err) {\n throw err.code === 'ENOENT'\n ? new Error(\n `\"${manifestPath}\" does not exist. ` +\n 'The deploy directory must contain a federation build created with \"sanity build\".',\n )\n : err\n }\n}\n"],"names":["stat","join","checkBuiltOutput","sourceDir","stats","isDirectory","Error","err","code","manifestPath"],"mappings":"AAAA,SAAQA,IAAI,QAAO,mBAAkB;AACrC,SAAQC,IAAI,QAAO,YAAW;AAE9B;;;;;;CAMC,GACD,OAAO,eAAeC,iBAAiBC,SAAiB;IACtD,IAAI;QACF,MAAMC,QAAQ,MAAMJ,KAAKG;QACzB,IAAI,CAACC,MAAMC,WAAW,IAAI;YACxB,MAAM,IAAIC,MAAM,CAAC,CAAC,EAAEH,UAAU,oBAAoB,CAAC;QACrD;IACF,EAAE,OAAOI,KAAK;QACZ,MAAMA,IAAIC,IAAI,KAAK,WAAW,IAAIF,MAAM,CAAC,WAAW,EAAEH,UAAU,gBAAgB,CAAC,IAAII;IACvF;IAEA,MAAME,eAAeR,KAAKE,WAAW;IACrC,IAAI;QACF,MAAMH,KAAKS;IACb,EAAE,OAAOF,KAAK;QACZ,MAAMA,IAAIC,IAAI,KAAK,WACf,IAAIF,MACF,CAAC,CAAC,EAAEG,aAAa,kBAAkB,CAAC,GAClC,uFAEJF;IACN;AACF"}
1
+ {"version":3,"sources":["../../../src/actions/deploy/checkBuiltOutput.ts"],"sourcesContent":["import {stat} from 'node:fs/promises'\nimport {join} from 'node:path'\n\n/**\n * A genuinely-missing-build error, named so callers can offer the \"run sanity\n * build\" hint (the `preview` command keys off this name, matching how the studio\n * path keys off a missing `index.html`). Only the missing cases get it — real\n * I/O failures keep their own name so they surface as themselves.\n */\nfunction buildNotFound(message: string): Error {\n const error = new Error(message)\n error.name = 'BUILD_NOT_FOUND'\n return error\n}\n\n/**\n * Throws unless `sourceDir` is a directory holding a federation build.\n * A workbench build always emits a module-federation remote, and may\n * additionally emit a standalone `index.html` SPA (workbench remotes). Either\n * way `mf-manifest.json` is the reliable marker that `sanity build` produced a\n * federation build, so that — not `index.html` — is what we check for.\n */\nexport async function checkBuiltOutput(sourceDir: string): Promise<void> {\n try {\n const stats = await stat(sourceDir)\n if (!stats.isDirectory()) {\n throw new Error(`\"${sourceDir}\" is not a directory`)\n }\n } catch (err) {\n if (err.code === 'ENOENT') throw buildNotFound(`Directory \"${sourceDir}\" does not exist`)\n throw err\n }\n\n const manifestPath = join(sourceDir, 'mf-manifest.json')\n try {\n await stat(manifestPath)\n } catch (err) {\n if (err.code === 'ENOENT') {\n throw buildNotFound(\n `\"${manifestPath}\" does not exist. ` +\n 'The deploy directory must contain a federation build created with \"sanity build\".',\n )\n }\n throw err\n }\n}\n"],"names":["stat","join","buildNotFound","message","error","Error","name","checkBuiltOutput","sourceDir","stats","isDirectory","err","code","manifestPath"],"mappings":"AAAA,SAAQA,IAAI,QAAO,mBAAkB;AACrC,SAAQC,IAAI,QAAO,YAAW;AAE9B;;;;;CAKC,GACD,SAASC,cAAcC,OAAe;IACpC,MAAMC,QAAQ,IAAIC,MAAMF;IACxBC,MAAME,IAAI,GAAG;IACb,OAAOF;AACT;AAEA;;;;;;CAMC,GACD,OAAO,eAAeG,iBAAiBC,SAAiB;IACtD,IAAI;QACF,MAAMC,QAAQ,MAAMT,KAAKQ;QACzB,IAAI,CAACC,MAAMC,WAAW,IAAI;YACxB,MAAM,IAAIL,MAAM,CAAC,CAAC,EAAEG,UAAU,oBAAoB,CAAC;QACrD;IACF,EAAE,OAAOG,KAAK;QACZ,IAAIA,IAAIC,IAAI,KAAK,UAAU,MAAMV,cAAc,CAAC,WAAW,EAAEM,UAAU,gBAAgB,CAAC;QACxF,MAAMG;IACR;IAEA,MAAME,eAAeZ,KAAKO,WAAW;IACrC,IAAI;QACF,MAAMR,KAAKa;IACb,EAAE,OAAOF,KAAK;QACZ,IAAIA,IAAIC,IAAI,KAAK,UAAU;YACzB,MAAMV,cACJ,CAAC,CAAC,EAAEW,aAAa,kBAAkB,CAAC,GAClC;QAEN;QACA,MAAMF;IACR;AACF"}
@@ -1,107 +1,89 @@
1
1
  import { basename, dirname } from 'node:path';
2
2
  import { createGzip } from 'node:zlib';
3
- import { exitCodes } from '@sanity/cli-core';
4
3
  import { spinner } from '@sanity/cli-core/ux';
5
4
  import { pack } from 'tar-fs';
6
- import { createApplication, createDeployment } from '../../services/applications.js';
5
+ import { createApplication, createDeployment, deleteApplication, updateApplication } from '../../services/applications.js';
7
6
  /**
8
- * Deploy a workbench coreApp through Brett: redeploy when `appId` is set,
9
- * otherwise create the application at `slug`. Returns the application id for the
10
- * shell to report.
7
+ * Create a coreApp record (no deployment), so the CLI can build with its id
8
+ * before shipping the first deployment. First deploy only.
11
9
  * @internal
12
- */ export async function deployCoreApp(options) {
13
- const { appId, interfaces, isAutoUpdating, isSingleton, organizationId, slug, sourceDir, title, version, visibility } = options;
14
- const tarball = pack(dirname(sourceDir), {
15
- entries: [
16
- basename(sourceDir)
17
- ]
18
- }).pipe(createGzip());
19
- const spin = spinner('Deploying...').start();
10
+ */ export async function createCoreApp(options) {
11
+ const spin = spinner('Creating application...').start();
20
12
  try {
21
- if (appId) {
22
- await createDeployment({
23
- applicationId: appId,
24
- interfaces,
25
- isAutoUpdating,
26
- tarball,
27
- version
28
- });
29
- spin.succeed();
30
- return {
31
- applicationId: appId
32
- };
33
- }
34
13
  const { id } = await createApplication({
35
- interfaces,
36
- isSingleton,
37
- organizationId,
38
- slug,
39
- tarball,
40
- title,
41
- type: 'coreApp',
42
- version,
43
- visibility
14
+ ...options,
15
+ type: 'coreApp'
44
16
  });
45
17
  spin.succeed();
46
18
  return {
47
- applicationId: id
19
+ applicationId: id,
20
+ rollback: ()=>deleteApplication(id)
48
21
  };
49
22
  } catch (error) {
50
- spin.clear();
23
+ spin.fail();
51
24
  throw error;
52
25
  }
53
26
  }
54
27
  /**
55
- * Deploy a workbench studio through Brett: redeploy when `appId` is set,
56
- * otherwise create the studio at `studioHost`. Returns the application id for
57
- * the shell to report; a missing `studioHost` on create is a usage error.
28
+ * Create a studio record (no deployment).
58
29
  * @internal
59
- */ export async function deployStudio(options) {
60
- const { appId, interfaces, isAutoUpdating, organizationId, output, projectId, sourceDir, studioHost, title, version, workspaces } = options;
30
+ */ export async function createStudio(options) {
31
+ const spin = spinner('Creating studio...').start();
32
+ try {
33
+ const { id } = await createApplication({
34
+ ...options,
35
+ type: 'studio'
36
+ });
37
+ spin.succeed();
38
+ return {
39
+ applicationId: id,
40
+ rollback: ()=>deleteApplication(id)
41
+ };
42
+ } catch (error) {
43
+ spin.fail();
44
+ throw error;
45
+ }
46
+ }
47
+ /**
48
+ * Ship a deployment to an already-created (or `deployment.appId`) application,
49
+ * then sync its mutable metadata (`title`, and `icon`/`visibility` when set)
50
+ * from config. The deploy endpoint ignores these, so a redeploy patches them
51
+ * here alongside the new deployment.
52
+ *
53
+ * `onDeployed` fires the instant the deployment is live, before the metadata
54
+ * sync — so a caller can disarm a create-time rollback that must not delete an
55
+ * application once it has an active deployment.
56
+ * @internal
57
+ */ export async function deployWorkbenchApp(options) {
58
+ const { applicationId, icon, interfaces, isAutoUpdating, label = 'Deploying...', onDeployed, sourceDir, title, version, visibility, workspaces } = options;
61
59
  const tarball = pack(dirname(sourceDir), {
62
60
  entries: [
63
61
  basename(sourceDir)
64
62
  ]
65
63
  }).pipe(createGzip());
66
- const spin = spinner('Deploying to sanity.studio').start();
64
+ const spin = spinner(label).start();
67
65
  try {
68
- if (appId) {
69
- await createDeployment({
70
- applicationId: appId,
71
- interfaces,
72
- isAutoUpdating,
73
- tarball,
74
- version,
75
- workspaces
76
- });
77
- spin.succeed();
78
- return {
79
- applicationId: appId
80
- };
81
- }
82
- if (!studioHost) {
83
- spin.fail();
84
- return output.error('No studio hostname configured. Set `studioHost` in sanity.cli.ts to create a studio.', {
85
- exit: exitCodes.USAGE_ERROR
86
- });
87
- }
88
- const application = await createApplication({
66
+ await createDeployment({
67
+ applicationId,
89
68
  interfaces,
90
- organizationId,
91
- projectId,
92
- slug: studioHost,
69
+ isAutoUpdating,
93
70
  tarball,
94
- title,
95
- type: 'studio',
96
71
  version,
97
72
  workspaces
98
73
  });
74
+ onDeployed?.();
75
+ await updateApplication(applicationId, {
76
+ title,
77
+ ...icon ? {
78
+ icon
79
+ } : {},
80
+ ...visibility ? {
81
+ visibility
82
+ } : {}
83
+ });
99
84
  spin.succeed();
100
- return {
101
- applicationId: application.id
102
- };
103
85
  } catch (error) {
104
- spin.fail();
86
+ spin.clear();
105
87
  throw error;
106
88
  }
107
89
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/deploy/deployWorkbenchApp.ts"],"sourcesContent":["import {basename, dirname} from 'node:path'\nimport {createGzip} from 'node:zlib'\n\nimport {type AppVisibility, exitCodes, type Output} from '@sanity/cli-core'\nimport {spinner} from '@sanity/cli-core/ux'\nimport {pack} from 'tar-fs'\n\nimport {\n type BrettInterface,\n type BrettWorkspace,\n createApplication,\n createDeployment,\n} from '../../services/applications.js'\n\n/**\n * Deploy a workbench coreApp through Brett: redeploy when `appId` is set,\n * otherwise create the application at `slug`. Returns the application id for the\n * shell to report.\n * @internal\n */\nexport async function deployCoreApp(options: {\n appId: string | undefined\n interfaces: readonly BrettInterface[]\n isAutoUpdating: boolean\n isSingleton?: boolean\n organizationId: string\n slug: string\n sourceDir: string\n title: string\n version: string\n visibility?: AppVisibility\n}): Promise<{applicationId: string}> {\n const {\n appId,\n interfaces,\n isAutoUpdating,\n isSingleton,\n organizationId,\n slug,\n sourceDir,\n title,\n version,\n visibility,\n } = options\n const tarball = pack(dirname(sourceDir), {entries: [basename(sourceDir)]}).pipe(createGzip())\n\n const spin = spinner('Deploying...').start()\n try {\n if (appId) {\n await createDeployment({applicationId: appId, interfaces, isAutoUpdating, tarball, version})\n spin.succeed()\n return {applicationId: appId}\n }\n\n const {id} = await createApplication({\n interfaces,\n isSingleton,\n organizationId,\n slug,\n tarball,\n title,\n type: 'coreApp',\n version,\n visibility,\n })\n spin.succeed()\n return {applicationId: id}\n } catch (error) {\n spin.clear()\n throw error\n }\n}\n\n/**\n * Deploy a workbench studio through Brett: redeploy when `appId` is set,\n * otherwise create the studio at `studioHost`. Returns the application id for\n * the shell to report; a missing `studioHost` on create is a usage error.\n * @internal\n */\nexport async function deployStudio(options: {\n appId: string | undefined\n interfaces: readonly BrettInterface[]\n isAutoUpdating: boolean\n organizationId: string\n output: Output\n projectId: string | undefined\n sourceDir: string\n studioHost: string | undefined\n title: string\n version: string\n workspaces: readonly BrettWorkspace[]\n}): Promise<{applicationId: string}> {\n const {\n appId,\n interfaces,\n isAutoUpdating,\n organizationId,\n output,\n projectId,\n sourceDir,\n studioHost,\n title,\n version,\n workspaces,\n } = options\n const tarball = pack(dirname(sourceDir), {entries: [basename(sourceDir)]}).pipe(createGzip())\n\n const spin = spinner('Deploying to sanity.studio').start()\n try {\n if (appId) {\n await createDeployment({\n applicationId: appId,\n interfaces,\n isAutoUpdating,\n tarball,\n version,\n workspaces,\n })\n spin.succeed()\n return {applicationId: appId}\n }\n\n if (!studioHost) {\n spin.fail()\n return output.error(\n 'No studio hostname configured. Set `studioHost` in sanity.cli.ts to create a studio.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const application = await createApplication({\n interfaces,\n organizationId,\n projectId,\n slug: studioHost,\n tarball,\n title,\n type: 'studio',\n version,\n workspaces,\n })\n spin.succeed()\n return {applicationId: application.id}\n } catch (error) {\n spin.fail()\n throw error\n }\n}\n"],"names":["basename","dirname","createGzip","exitCodes","spinner","pack","createApplication","createDeployment","deployCoreApp","options","appId","interfaces","isAutoUpdating","isSingleton","organizationId","slug","sourceDir","title","version","visibility","tarball","entries","pipe","spin","start","applicationId","succeed","id","type","error","clear","deployStudio","output","projectId","studioHost","workspaces","fail","exit","USAGE_ERROR","application"],"mappings":"AAAA,SAAQA,QAAQ,EAAEC,OAAO,QAAO,YAAW;AAC3C,SAAQC,UAAU,QAAO,YAAW;AAEpC,SAA4BC,SAAS,QAAoB,mBAAkB;AAC3E,SAAQC,OAAO,QAAO,sBAAqB;AAC3C,SAAQC,IAAI,QAAO,SAAQ;AAE3B,SAGEC,iBAAiB,EACjBC,gBAAgB,QACX,iCAAgC;AAEvC;;;;;CAKC,GACD,OAAO,eAAeC,cAAcC,OAWnC;IACC,MAAM,EACJC,KAAK,EACLC,UAAU,EACVC,cAAc,EACdC,WAAW,EACXC,cAAc,EACdC,IAAI,EACJC,SAAS,EACTC,KAAK,EACLC,OAAO,EACPC,UAAU,EACX,GAAGV;IACJ,MAAMW,UAAUf,KAAKJ,QAAQe,YAAY;QAACK,SAAS;YAACrB,SAASgB;SAAW;IAAA,GAAGM,IAAI,CAACpB;IAEhF,MAAMqB,OAAOnB,QAAQ,gBAAgBoB,KAAK;IAC1C,IAAI;QACF,IAAId,OAAO;YACT,MAAMH,iBAAiB;gBAACkB,eAAef;gBAAOC;gBAAYC;gBAAgBQ;gBAASF;YAAO;YAC1FK,KAAKG,OAAO;YACZ,OAAO;gBAACD,eAAef;YAAK;QAC9B;QAEA,MAAM,EAACiB,EAAE,EAAC,GAAG,MAAMrB,kBAAkB;YACnCK;YACAE;YACAC;YACAC;YACAK;YACAH;YACAW,MAAM;YACNV;YACAC;QACF;QACAI,KAAKG,OAAO;QACZ,OAAO;YAACD,eAAeE;QAAE;IAC3B,EAAE,OAAOE,OAAO;QACdN,KAAKO,KAAK;QACV,MAAMD;IACR;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeE,aAAatB,OAYlC;IACC,MAAM,EACJC,KAAK,EACLC,UAAU,EACVC,cAAc,EACdE,cAAc,EACdkB,MAAM,EACNC,SAAS,EACTjB,SAAS,EACTkB,UAAU,EACVjB,KAAK,EACLC,OAAO,EACPiB,UAAU,EACX,GAAG1B;IACJ,MAAMW,UAAUf,KAAKJ,QAAQe,YAAY;QAACK,SAAS;YAACrB,SAASgB;SAAW;IAAA,GAAGM,IAAI,CAACpB;IAEhF,MAAMqB,OAAOnB,QAAQ,8BAA8BoB,KAAK;IACxD,IAAI;QACF,IAAId,OAAO;YACT,MAAMH,iBAAiB;gBACrBkB,eAAef;gBACfC;gBACAC;gBACAQ;gBACAF;gBACAiB;YACF;YACAZ,KAAKG,OAAO;YACZ,OAAO;gBAACD,eAAef;YAAK;QAC9B;QAEA,IAAI,CAACwB,YAAY;YACfX,KAAKa,IAAI;YACT,OAAOJ,OAAOH,KAAK,CACjB,wFACA;gBAACQ,MAAMlC,UAAUmC,WAAW;YAAA;QAEhC;QAEA,MAAMC,cAAc,MAAMjC,kBAAkB;YAC1CK;YACAG;YACAmB;YACAlB,MAAMmB;YACNd;YACAH;YACAW,MAAM;YACNV;YACAiB;QACF;QACAZ,KAAKG,OAAO;QACZ,OAAO;YAACD,eAAec,YAAYZ,EAAE;QAAA;IACvC,EAAE,OAAOE,OAAO;QACdN,KAAKa,IAAI;QACT,MAAMP;IACR;AACF"}
1
+ {"version":3,"sources":["../../../src/actions/deploy/deployWorkbenchApp.ts"],"sourcesContent":["import {basename, dirname} from 'node:path'\nimport {createGzip} from 'node:zlib'\n\nimport {type AppVisibility} from '@sanity/cli-core'\nimport {spinner} from '@sanity/cli-core/ux'\nimport {pack} from 'tar-fs'\n\nimport {\n type BrettInterface,\n type BrettWorkspace,\n createApplication,\n createDeployment,\n deleteApplication,\n updateApplication,\n} from '../../services/applications.js'\n\n/**\n * A freshly created application record: its id, plus a way to undo the creation.\n * The caller builds and deploys with the id, and calls `rollback` if a later\n * step fails so the record isn't stranded at its slug.\n * @internal\n */\nexport interface CreatedApplication {\n applicationId: string\n rollback: () => Promise<void>\n}\n\n/**\n * Create a coreApp record (no deployment), so the CLI can build with its id\n * before shipping the first deployment. First deploy only.\n * @internal\n */\nexport async function createCoreApp(options: {\n isSingleton?: boolean\n organizationId: string\n slug: string\n title: string\n visibility?: AppVisibility\n}): Promise<CreatedApplication> {\n const spin = spinner('Creating application...').start()\n try {\n const {id} = await createApplication({...options, type: 'coreApp'})\n spin.succeed()\n return {applicationId: id, rollback: () => deleteApplication(id)}\n } catch (error) {\n spin.fail()\n throw error\n }\n}\n\n/**\n * Create a studio record (no deployment).\n * @internal\n */\nexport async function createStudio(options: {\n organizationId: string\n projectId: string | undefined\n slug: string\n title: string\n}): Promise<CreatedApplication> {\n const spin = spinner('Creating studio...').start()\n try {\n const {id} = await createApplication({...options, type: 'studio'})\n spin.succeed()\n return {applicationId: id, rollback: () => deleteApplication(id)}\n } catch (error) {\n spin.fail()\n throw error\n }\n}\n\n/**\n * Ship a deployment to an already-created (or `deployment.appId`) application,\n * then sync its mutable metadata (`title`, and `icon`/`visibility` when set)\n * from config. The deploy endpoint ignores these, so a redeploy patches them\n * here alongside the new deployment.\n *\n * `onDeployed` fires the instant the deployment is live, before the metadata\n * sync — so a caller can disarm a create-time rollback that must not delete an\n * application once it has an active deployment.\n * @internal\n */\nexport async function deployWorkbenchApp(options: {\n applicationId: string\n icon?: string\n interfaces: readonly BrettInterface[]\n isAutoUpdating: boolean\n label?: string\n onDeployed?: () => void\n sourceDir: string\n title: string\n version: string\n visibility?: AppVisibility\n workspaces?: readonly BrettWorkspace[]\n}): Promise<void> {\n const {\n applicationId,\n icon,\n interfaces,\n isAutoUpdating,\n label = 'Deploying...',\n onDeployed,\n sourceDir,\n title,\n version,\n visibility,\n workspaces,\n } = options\n const tarball = pack(dirname(sourceDir), {entries: [basename(sourceDir)]}).pipe(createGzip())\n\n const spin = spinner(label).start()\n try {\n await createDeployment({\n applicationId,\n interfaces,\n isAutoUpdating,\n tarball,\n version,\n workspaces,\n })\n onDeployed?.()\n await updateApplication(applicationId, {\n title,\n ...(icon ? {icon} : {}),\n ...(visibility ? {visibility} : {}),\n })\n spin.succeed()\n } catch (error) {\n spin.clear()\n throw error\n }\n}\n"],"names":["basename","dirname","createGzip","spinner","pack","createApplication","createDeployment","deleteApplication","updateApplication","createCoreApp","options","spin","start","id","type","succeed","applicationId","rollback","error","fail","createStudio","deployWorkbenchApp","icon","interfaces","isAutoUpdating","label","onDeployed","sourceDir","title","version","visibility","workspaces","tarball","entries","pipe","clear"],"mappings":"AAAA,SAAQA,QAAQ,EAAEC,OAAO,QAAO,YAAW;AAC3C,SAAQC,UAAU,QAAO,YAAW;AAGpC,SAAQC,OAAO,QAAO,sBAAqB;AAC3C,SAAQC,IAAI,QAAO,SAAQ;AAE3B,SAGEC,iBAAiB,EACjBC,gBAAgB,EAChBC,iBAAiB,EACjBC,iBAAiB,QACZ,iCAAgC;AAavC;;;;CAIC,GACD,OAAO,eAAeC,cAAcC,OAMnC;IACC,MAAMC,OAAOR,QAAQ,2BAA2BS,KAAK;IACrD,IAAI;QACF,MAAM,EAACC,EAAE,EAAC,GAAG,MAAMR,kBAAkB;YAAC,GAAGK,OAAO;YAAEI,MAAM;QAAS;QACjEH,KAAKI,OAAO;QACZ,OAAO;YAACC,eAAeH;YAAII,UAAU,IAAMV,kBAAkBM;QAAG;IAClE,EAAE,OAAOK,OAAO;QACdP,KAAKQ,IAAI;QACT,MAAMD;IACR;AACF;AAEA;;;CAGC,GACD,OAAO,eAAeE,aAAaV,OAKlC;IACC,MAAMC,OAAOR,QAAQ,sBAAsBS,KAAK;IAChD,IAAI;QACF,MAAM,EAACC,EAAE,EAAC,GAAG,MAAMR,kBAAkB;YAAC,GAAGK,OAAO;YAAEI,MAAM;QAAQ;QAChEH,KAAKI,OAAO;QACZ,OAAO;YAACC,eAAeH;YAAII,UAAU,IAAMV,kBAAkBM;QAAG;IAClE,EAAE,OAAOK,OAAO;QACdP,KAAKQ,IAAI;QACT,MAAMD;IACR;AACF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,eAAeG,mBAAmBX,OAYxC;IACC,MAAM,EACJM,aAAa,EACbM,IAAI,EACJC,UAAU,EACVC,cAAc,EACdC,QAAQ,cAAc,EACtBC,UAAU,EACVC,SAAS,EACTC,KAAK,EACLC,OAAO,EACPC,UAAU,EACVC,UAAU,EACX,GAAGrB;IACJ,MAAMsB,UAAU5B,KAAKH,QAAQ0B,YAAY;QAACM,SAAS;YAACjC,SAAS2B;SAAW;IAAA,GAAGO,IAAI,CAAChC;IAEhF,MAAMS,OAAOR,QAAQsB,OAAOb,KAAK;IACjC,IAAI;QACF,MAAMN,iBAAiB;YACrBU;YACAO;YACAC;YACAQ;YACAH;YACAE;QACF;QACAL;QACA,MAAMlB,kBAAkBQ,eAAe;YACrCY;YACA,GAAIN,OAAO;gBAACA;YAAI,IAAI,CAAC,CAAC;YACtB,GAAIQ,aAAa;gBAACA;YAAU,IAAI,CAAC,CAAC;QACpC;QACAnB,KAAKI,OAAO;IACd,EAAE,OAAOG,OAAO;QACdP,KAAKwB,KAAK;QACV,MAAMjB;IACR;AACF"}
@@ -5,7 +5,25 @@ import { z } from 'zod/mini';
5
5
  import { AppInterfaceMetadataSchema } from '../../contract.js';
6
6
  import { canonicalizeWatchDir } from './canonicalizeWatchDir.js';
7
7
  import { getProcessStartTime, isOurProcess } from './processLiveness.js';
8
- const devDebug = subdebug('dev');
8
+ /**
9
+ * The dev-server registry: how a running `sanity dev` / `sanity start` process
10
+ * advertises itself so the workbench on this machine can find and load it.
11
+ *
12
+ * Two kinds of file under `~/.sanity/dev-servers/` do the coordinating:
13
+ *
14
+ * - `<pid>.json` — one per running app/studio server, holding where it's served
15
+ * plus its inlined manifest and interfaces. The workbench reads these to
16
+ * discover and render local apps. Written by `registerDevServer`, watched by
17
+ * `watchRegistry`.
18
+ * - `workbench.lock` — a single machine-wide lock, so only one workbench shell
19
+ * runs at a time and later `dev`s register into it instead of starting their
20
+ * own. Managed by `acquireWorkbenchLock` / `readWorkbenchLock`.
21
+ *
22
+ * Both files belong to the process that created them and must not outlive it.
23
+ * Three things keep that true: an explicit `release()` on clean shutdown, an
24
+ * `exit` backstop for abrupt exits (`unlinkOnProcessExit`), and a dead-pid prune
25
+ * on read (`isOurProcess`) that clears whatever a crashed process left behind.
26
+ */ const devDebug = subdebug('dev');
9
27
  /** Bump when the manifest/lock shape changes in a breaking way. */ const REGISTRY_VERSION = 1;
10
28
  /**
11
29
  * The current process's start time as reported by the OS, for the `startedAt`
@@ -100,6 +118,39 @@ const devServerManifestSchema = z.object({
100
118
  */ function getRegistryDir() {
101
119
  return join(getSanityDataDir(), 'dev-servers');
102
120
  }
121
+ // One shared `exit` listener drives every registered cleanup, so N locks/entries
122
+ // don't each add a listener and trip Node's MaxListeners warning.
123
+ const exitCleanups = new Set();
124
+ let exitListenerInstalled = false;
125
+ function runExitCleanups() {
126
+ for (const cleanup of exitCleanups)cleanup();
127
+ }
128
+ /** Exercise the exit backstop in tests without terminating the process; not part
129
+ * of the package's public surface. */ export const runRegistryExitCleanupForTesting = runExitCleanups;
130
+ /**
131
+ * Delete a registry file synchronously on process exit, as a backstop for abrupt
132
+ * termination. Vite installs its own SIGTERM handler that calls `process.exit()`,
133
+ * which can outrun the async server teardown and leave the lock or registry entry
134
+ * behind — a stray dev-server that lingers until the dead-pid prune clears it. The
135
+ * `exit` event only runs synchronous work, hence `unlinkSync`. `ownedByUs` guards
136
+ * the shared lock so a successor that reacquired it isn't wiped. Returns a
137
+ * detacher to call after a clean release.
138
+ */ function unlinkOnProcessExit(filePath, ownedByUs) {
139
+ const cleanup = ()=>{
140
+ if (!ownedByUs()) return;
141
+ try {
142
+ unlinkSync(filePath);
143
+ } catch {
144
+ // The file may already have been removed during shutdown.
145
+ }
146
+ };
147
+ exitCleanups.add(cleanup);
148
+ if (!exitListenerInstalled) {
149
+ exitListenerInstalled = true;
150
+ process.once('exit', runExitCleanups);
151
+ }
152
+ return ()=>exitCleanups.delete(cleanup);
153
+ }
103
154
  /**
104
155
  * Write a manifest file for the current process and return a handle with a
105
156
  * `release` function that removes it plus an `update` function for patching
@@ -122,9 +173,12 @@ const devServerManifestSchema = z.object({
122
173
  // manifest extraction) landing after `release()` has deleted the file —
123
174
  // without this, the update would re-create the registry entry and leak.
124
175
  let released = false;
176
+ // The file is pid-named, so it's always ours to remove on exit.
177
+ const detachExitCleanup = unlinkOnProcessExit(filePath, ()=>!released);
125
178
  return {
126
179
  release () {
127
180
  released = true;
181
+ detachExitCleanup();
128
182
  try {
129
183
  unlinkSync(filePath);
130
184
  } catch {
@@ -285,8 +339,22 @@ function pruneWorkbenchLock(lockPath) {
285
339
  flag: 'wx'
286
340
  });
287
341
  devDebug('Workbench lock acquired');
342
+ let released = false;
343
+ // Only wipe the lock on exit if it's still ours — a successor that reacquired
344
+ // it after our own release must not be clobbered.
345
+ const detachExitCleanup = unlinkOnProcessExit(lockPath, ()=>{
346
+ if (released) return false;
347
+ try {
348
+ const disk = parseLockContents(readFileSync(lockPath, 'utf8'));
349
+ return disk?.pid === process.pid && disk.startedAt === startedAt;
350
+ } catch {
351
+ return false;
352
+ }
353
+ });
288
354
  return {
289
355
  release () {
356
+ released = true;
357
+ detachExitCleanup();
290
358
  try {
291
359
  unlinkSync(lockPath);
292
360
  } catch {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/dev/registry.ts"],"sourcesContent":["import {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n unlinkSync,\n watch,\n writeFileSync,\n} from 'node:fs'\nimport {join} from 'node:path'\n\nimport {\n coreAppManifestSchema,\n getSanityDataDir,\n studioManifestSchema,\n subdebug,\n} from '@sanity/cli-core'\nimport {z} from 'zod/mini'\n\nimport {AppInterfaceMetadataSchema} from '../../contract.js'\nimport {canonicalizeWatchDir} from './canonicalizeWatchDir.js'\nimport {getProcessStartTime, isOurProcess} from './processLiveness.js'\n\nconst devDebug = subdebug('dev')\n\n/** Bump when the manifest/lock shape changes in a breaking way. */\nconst REGISTRY_VERSION = 1\n\n/**\n * The current process's start time as reported by the OS, for the `startedAt`\n * that `isOurProcess` checks on re-read. Falls back to now when the OS time is\n * unavailable — `new Date()` alone records the write time, which drifts from\n * process start by enough to look stale and get pruned right after writing.\n */\nfunction ownStartedAt(): string {\n return (getProcessStartTime(process.pid) ?? new Date()).toISOString()\n}\n\nconst interfaceBaseFields = {\n /** CLI-minted for a local interface; a deployed one gets its id from Brett. */\n id: z.string(),\n moduleId: z.string(),\n name: z.string(),\n /** Raw source vite serves; a deployed interface carries only the `moduleId`. */\n src: z.string(),\n title: z.string(),\n version: z.optional(z.string()),\n}\n\n/**\n * A forwarded interface, discriminated on `type`. Kept outside the manifest so\n * the workbench renders local panels and runs workers without a deploy.\n */\nconst devServerInterfaceSchema = z.discriminatedUnion('type', [\n z.object({\n ...interfaceBaseFields,\n metadata: z.nullable(AppInterfaceMetadataSchema),\n type: z.literal('app'),\n }),\n z.object({...interfaceBaseFields, metadata: z.null(), type: z.literal('panel')}),\n z.object({...interfaceBaseFields, metadata: z.null(), type: z.literal('worker')}),\n])\n\nconst devServerManifestSchema = z.object({\n /**\n * Field schema *values* load from the federation module; each field's `src`\n * rides along so a repoint bumps the exposes-set id and forces a rebuild.\n * Lenient — the workbench is the authority.\n */\n configs: z.optional(\n z.array(\n z.object({\n // Identifies the owning app when it has no app id (singletons).\n appType: z.optional(z.string()),\n fields: z.array(\n z.object({\n name: z.string(),\n public: z.optional(z.boolean()),\n src: z.string(),\n title: z.string(),\n }),\n ),\n // Content hash of the config — the workbench's change-detection key\n // (see deriveConfigs).\n id: z.string(),\n // The app's `unstable_defineApp` name — the module-federation alias the\n // workbench loads this config's live values from.\n moduleName: z.optional(z.string()),\n // Config contract version the generated module exports, so the\n // workbench knows what it can resolve before loading the module.\n version: z.number(),\n }),\n ),\n ),\n host: z.string(),\n id: z.optional(z.string()),\n interfaces: z.optional(z.array(devServerInterfaceSchema)),\n /**\n * Inlined manifest — either a {@link StudioManifest} or {@link CoreAppManifest},\n * validated against the shared cli-core schemas. The registry stores and\n * rebroadcasts it; the CLI is what extracts and writes it.\n */\n manifest: z.optional(z.union([studioManifestSchema, coreAppManifestSchema])),\n /**\n * ISO timestamp of the most recent successful manifest extraction. Bumped\n * on every regeneration so re-writing this registry entry triggers the\n * workbench `watchRegistry` watcher and forces a rebroadcast to clients.\n */\n manifestUpdatedAt: z.optional(z.string()),\n pid: z.number(),\n port: z.number(),\n projectId: z.optional(z.string()),\n startedAt: z.string(),\n type: z.enum(['coreApp', 'studio']),\n version: z.literal(REGISTRY_VERSION),\n workDir: z.string(),\n})\n/**\n * A manifest describing a running dev server process (studio or app).\n * Stored as `~/.sanity/dev-servers/<pid>.json`.\n *\n * The workbench singleton is tracked separately via the lock file — see\n * `acquireWorkbenchLock` and `readWorkbenchLock` below.\n */\nexport type DevServerManifest = z.infer<typeof devServerManifestSchema>\n\n/**\n * Path to the dev server registry directory. Lives under the shared Sanity\n * config directory to stay consistent with other CLI paths.\n */\nfunction getRegistryDir(): string {\n return join(getSanityDataDir(), 'dev-servers')\n}\n\ninterface DevServerRegistration {\n /** Remove the registry entry. */\n release: () => void\n /**\n * Rewrite the registry entry with partial updates merged in. Also bumps the\n * file's mtime, which fires `watchRegistry` in any workbench process and\n * triggers a rebroadcast to connected clients.\n */\n update: (patch: Partial<Omit<DevServerManifest, 'pid' | 'startedAt' | 'version'>>) => void\n}\n\n/**\n * Write a manifest file for the current process and return a handle with a\n * `release` function that removes it plus an `update` function for patching\n * fields post-registration. Uses synchronous I/O so the file exists before\n * any signal handler could fire.\n */\nexport function registerDevServer(\n manifest: Omit<DevServerManifest, 'pid' | 'startedAt' | 'version'>,\n): DevServerRegistration {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n let current: DevServerManifest = {\n ...manifest,\n pid: process.pid,\n startedAt: ownStartedAt(),\n version: REGISTRY_VERSION,\n }\n\n const filePath = join(registryDir, `${process.pid}.json`)\n writeFileSync(filePath, JSON.stringify(current, null, 2))\n\n // Guard against late updates from background tasks (e.g. the initial\n // manifest extraction) landing after `release()` has deleted the file —\n // without this, the update would re-create the registry entry and leak.\n let released = false\n\n return {\n release() {\n released = true\n try {\n unlinkSync(filePath)\n } catch {\n // ENOENT is fine — already cleaned up\n }\n },\n update(patch) {\n if (released) return\n current = {...current, ...patch}\n writeFileSync(filePath, JSON.stringify(current, null, 2))\n },\n }\n}\n\n/**\n * Read all manifest files from the registry, prune stale entries (dead PIDs),\n * and return the live ones.\n */\nexport function getRegisteredServers(): DevServerManifest[] {\n const registryDir = getRegistryDir()\n\n if (!existsSync(registryDir)) {\n return []\n }\n\n const files = readdirSync(registryDir).filter((f) => f.endsWith('.json'))\n const servers: DevServerManifest[] = []\n\n for (const file of files) {\n const filePath = join(registryDir, file)\n let raw: unknown\n try {\n raw = JSON.parse(readFileSync(filePath, 'utf8'))\n } catch {\n continue\n }\n\n const {data, success} = devServerManifestSchema.safeParse(raw)\n if (!success) continue\n\n if (isOurProcess(data.pid, data.startedAt)) {\n servers.push(data)\n } else {\n try {\n unlinkSync(filePath)\n } catch {\n // Ignore — another process may have already cleaned it up\n }\n }\n }\n\n return servers\n}\n\ninterface RegistryWatcher {\n close(): void\n}\n\n/**\n * Watch the registry directory for changes and invoke the callback with the\n * current list of live servers whenever a change is detected.\n *\n * Uses `fs.watch` with a debounce to coalesce rapid file changes (e.g. a\n * server starting and writing its manifest triggers multiple FS events).\n */\nexport function watchRegistry(callback: (servers: DevServerManifest[]) => void): RegistryWatcher {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n // Canonicalize to the real long path so `fs.watch` doesn't abort on Windows\n // short-path dirs. See `canonicalizeWatchDir`.\n const watchDir = canonicalizeWatchDir(registryDir)\n\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n const notify = () => {\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n callback(getRegisteredServers())\n }, 50)\n }\n\n const watcher = watch(watchDir, notify)\n\n return {\n close() {\n clearTimeout(debounceTimer)\n watcher.close()\n },\n }\n}\n\n// The workbench singleton lock — \"one workbench per machine\". Lives in the same\n// registry dir and shares the liveness/prune model: a stale lock left by a\n// crashed process is pruned on read so the next acquire isn't blocked forever.\n\nconst workbenchLockSchema = z.object({\n host: z.string(),\n pid: z.number(),\n port: z.number(),\n startedAt: z.string(),\n version: z.literal(REGISTRY_VERSION),\n})\n\n/**\n * Read the workbench lock file and return its contents if the holding\n * process is still alive. Prunes stale locks from crashed processes.\n */\nexport function readWorkbenchLock(): z.infer<typeof workbenchLockSchema> | undefined {\n const lockPath = join(getRegistryDir(), 'workbench.lock')\n\n let contents: string\n try {\n contents = readFileSync(lockPath, 'utf8')\n } catch {\n // File doesn't exist — nothing to prune, nothing to return\n return undefined\n }\n\n // Past this point the file exists. Anything that isn't a live, valid lock\n // (unparsable JSON, schema mismatch, dead/reused PID) is stale and must be\n // pruned — otherwise the next `acquireWorkbenchLock` call is blocked by\n // EEXIST forever and `sanity dev` silently no-ops the workbench server.\n const data = parseLockContents(contents)\n devDebug('Read workbench lock: %o', data)\n if (data && isOurProcess(data.pid, data.startedAt)) {\n devDebug('Workbench process is alive at pid %d on port %d', data.pid, data.port)\n return data\n }\n\n pruneWorkbenchLock(lockPath)\n return undefined\n}\n\nfunction parseLockContents(contents: string): z.infer<typeof workbenchLockSchema> | undefined {\n try {\n const {data, success} = workbenchLockSchema.safeParse(JSON.parse(contents))\n return success ? data : undefined\n } catch {\n return undefined\n }\n}\n\nfunction pruneWorkbenchLock(lockPath: string): void {\n try {\n devDebug('Removing stale workbench lock')\n unlinkSync(lockPath)\n devDebug('Stale workbench lock removed')\n } catch {\n // Another process may have already cleaned it up\n }\n}\n\ninterface WorkbenchLock {\n /** Release the lock file. */\n release: () => void\n /** Update the lock with the actual port after the server starts listening. */\n updatePort: (port: number) => void\n}\n\n/**\n * Attempt to acquire an exclusive lock for the workbench process.\n * Uses `O_EXCL` (the `wx` flag) which is atomic at the OS level — only one\n * process can create the file.\n *\n * The lock stores `{pid, host, port}` so other processes can find the\n * running workbench. Call `updatePort` after the Vite server starts to\n * write the actual port (Vite may pick a different one).\n *\n * @returns A {@link WorkbenchLock} if acquired, or `undefined` if another\n * live process already holds it.\n */\nexport function acquireWorkbenchLock(\n info: {host: string; port: number},\n retries = 1,\n): WorkbenchLock | undefined {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n const lockPath = join(registryDir, 'workbench.lock')\n const startedAt = ownStartedAt()\n const lockData = {\n host: info.host,\n pid: process.pid,\n port: info.port,\n startedAt,\n version: REGISTRY_VERSION,\n }\n\n devDebug('Acquiring workbench lock at %s', lockPath)\n\n try {\n writeFileSync(lockPath, JSON.stringify(lockData), {flag: 'wx'})\n devDebug('Workbench lock acquired')\n return {\n release() {\n try {\n unlinkSync(lockPath)\n } catch {\n // Already cleaned up\n }\n },\n updatePort(port: number) {\n writeFileSync(lockPath, JSON.stringify({...lockData, port}))\n },\n }\n } catch (err: unknown) {\n devDebug(\n 'Failed to acquire workbench lock: %s',\n err instanceof Error ? err.message : String(err),\n )\n if (!isNodeError(err) || err.code !== 'EEXIST') return undefined\n\n // Lock exists — check if the holder is still alive\n const existing = readWorkbenchLock()\n if (existing) return undefined\n\n // Stale lock was pruned by readWorkbenchLock — retry (with guard against infinite recursion)\n if (retries <= 0) return undefined\n return acquireWorkbenchLock(info, retries - 1)\n }\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && 'code' in err\n}\n"],"names":["existsSync","mkdirSync","readdirSync","readFileSync","unlinkSync","watch","writeFileSync","join","coreAppManifestSchema","getSanityDataDir","studioManifestSchema","subdebug","z","AppInterfaceMetadataSchema","canonicalizeWatchDir","getProcessStartTime","isOurProcess","devDebug","REGISTRY_VERSION","ownStartedAt","process","pid","Date","toISOString","interfaceBaseFields","id","string","moduleId","name","src","title","version","optional","devServerInterfaceSchema","discriminatedUnion","object","metadata","nullable","type","literal","null","devServerManifestSchema","configs","array","appType","fields","public","boolean","moduleName","number","host","interfaces","manifest","union","manifestUpdatedAt","port","projectId","startedAt","enum","workDir","getRegistryDir","registerDevServer","registryDir","recursive","current","filePath","JSON","stringify","released","release","update","patch","getRegisteredServers","files","filter","f","endsWith","servers","file","raw","parse","data","success","safeParse","push","watchRegistry","callback","watchDir","debounceTimer","notify","clearTimeout","setTimeout","watcher","close","workbenchLockSchema","readWorkbenchLock","lockPath","contents","undefined","parseLockContents","pruneWorkbenchLock","acquireWorkbenchLock","info","retries","lockData","flag","updatePort","err","Error","message","String","isNodeError","code","existing"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/actions/dev/registry.ts"],"sourcesContent":["import {\n existsSync,\n mkdirSync,\n readdirSync,\n readFileSync,\n unlinkSync,\n watch,\n writeFileSync,\n} from 'node:fs'\nimport {join} from 'node:path'\n\nimport {\n coreAppManifestSchema,\n getSanityDataDir,\n studioManifestSchema,\n subdebug,\n} from '@sanity/cli-core'\nimport {z} from 'zod/mini'\n\nimport {AppInterfaceMetadataSchema} from '../../contract.js'\nimport {canonicalizeWatchDir} from './canonicalizeWatchDir.js'\nimport {getProcessStartTime, isOurProcess} from './processLiveness.js'\n\n/**\n * The dev-server registry: how a running `sanity dev` / `sanity start` process\n * advertises itself so the workbench on this machine can find and load it.\n *\n * Two kinds of file under `~/.sanity/dev-servers/` do the coordinating:\n *\n * - `<pid>.json` — one per running app/studio server, holding where it's served\n * plus its inlined manifest and interfaces. The workbench reads these to\n * discover and render local apps. Written by `registerDevServer`, watched by\n * `watchRegistry`.\n * - `workbench.lock` — a single machine-wide lock, so only one workbench shell\n * runs at a time and later `dev`s register into it instead of starting their\n * own. Managed by `acquireWorkbenchLock` / `readWorkbenchLock`.\n *\n * Both files belong to the process that created them and must not outlive it.\n * Three things keep that true: an explicit `release()` on clean shutdown, an\n * `exit` backstop for abrupt exits (`unlinkOnProcessExit`), and a dead-pid prune\n * on read (`isOurProcess`) that clears whatever a crashed process left behind.\n */\n\nconst devDebug = subdebug('dev')\n\n/** Bump when the manifest/lock shape changes in a breaking way. */\nconst REGISTRY_VERSION = 1\n\n/**\n * The current process's start time as reported by the OS, for the `startedAt`\n * that `isOurProcess` checks on re-read. Falls back to now when the OS time is\n * unavailable — `new Date()` alone records the write time, which drifts from\n * process start by enough to look stale and get pruned right after writing.\n */\nfunction ownStartedAt(): string {\n return (getProcessStartTime(process.pid) ?? new Date()).toISOString()\n}\n\nconst interfaceBaseFields = {\n /** CLI-minted for a local interface; a deployed one gets its id from Brett. */\n id: z.string(),\n moduleId: z.string(),\n name: z.string(),\n /** Raw source vite serves; a deployed interface carries only the `moduleId`. */\n src: z.string(),\n title: z.string(),\n version: z.optional(z.string()),\n}\n\n/**\n * A forwarded interface, discriminated on `type`. Kept outside the manifest so\n * the workbench renders local panels and runs workers without a deploy.\n */\nconst devServerInterfaceSchema = z.discriminatedUnion('type', [\n z.object({\n ...interfaceBaseFields,\n metadata: z.nullable(AppInterfaceMetadataSchema),\n type: z.literal('app'),\n }),\n z.object({...interfaceBaseFields, metadata: z.null(), type: z.literal('panel')}),\n z.object({...interfaceBaseFields, metadata: z.null(), type: z.literal('worker')}),\n])\n\nconst devServerManifestSchema = z.object({\n /**\n * Field schema *values* load from the federation module; each field's `src`\n * rides along so a repoint bumps the exposes-set id and forces a rebuild.\n * Lenient — the workbench is the authority.\n */\n configs: z.optional(\n z.array(\n z.object({\n // Identifies the owning app when it has no app id (singletons).\n appType: z.optional(z.string()),\n fields: z.array(\n z.object({\n name: z.string(),\n public: z.optional(z.boolean()),\n src: z.string(),\n title: z.string(),\n }),\n ),\n // Content hash of the config — the workbench's change-detection key\n // (see deriveConfigs).\n id: z.string(),\n // The app's `unstable_defineApp` name — the module-federation alias the\n // workbench loads this config's live values from.\n moduleName: z.optional(z.string()),\n // Config contract version the generated module exports, so the\n // workbench knows what it can resolve before loading the module.\n version: z.number(),\n }),\n ),\n ),\n host: z.string(),\n id: z.optional(z.string()),\n interfaces: z.optional(z.array(devServerInterfaceSchema)),\n /**\n * Inlined manifest — either a {@link StudioManifest} or {@link CoreAppManifest},\n * validated against the shared cli-core schemas. The registry stores and\n * rebroadcasts it; the CLI is what extracts and writes it.\n */\n manifest: z.optional(z.union([studioManifestSchema, coreAppManifestSchema])),\n /**\n * ISO timestamp of the most recent successful manifest extraction. Bumped\n * on every regeneration so re-writing this registry entry triggers the\n * workbench `watchRegistry` watcher and forces a rebroadcast to clients.\n */\n manifestUpdatedAt: z.optional(z.string()),\n pid: z.number(),\n port: z.number(),\n projectId: z.optional(z.string()),\n startedAt: z.string(),\n type: z.enum(['coreApp', 'studio']),\n version: z.literal(REGISTRY_VERSION),\n workDir: z.string(),\n})\n/**\n * A manifest describing a running dev server process (studio or app).\n * Stored as `~/.sanity/dev-servers/<pid>.json`.\n *\n * The workbench singleton is tracked separately via the lock file — see\n * `acquireWorkbenchLock` and `readWorkbenchLock` below.\n */\nexport type DevServerManifest = z.infer<typeof devServerManifestSchema>\n\n/**\n * Path to the dev server registry directory. Lives under the shared Sanity\n * config directory to stay consistent with other CLI paths.\n */\nfunction getRegistryDir(): string {\n return join(getSanityDataDir(), 'dev-servers')\n}\n\n// One shared `exit` listener drives every registered cleanup, so N locks/entries\n// don't each add a listener and trip Node's MaxListeners warning.\nconst exitCleanups = new Set<() => void>()\nlet exitListenerInstalled = false\n\nfunction runExitCleanups(): void {\n for (const cleanup of exitCleanups) cleanup()\n}\n\n/** Exercise the exit backstop in tests without terminating the process; not part\n * of the package's public surface. */\nexport const runRegistryExitCleanupForTesting = runExitCleanups\n\n/**\n * Delete a registry file synchronously on process exit, as a backstop for abrupt\n * termination. Vite installs its own SIGTERM handler that calls `process.exit()`,\n * which can outrun the async server teardown and leave the lock or registry entry\n * behind — a stray dev-server that lingers until the dead-pid prune clears it. The\n * `exit` event only runs synchronous work, hence `unlinkSync`. `ownedByUs` guards\n * the shared lock so a successor that reacquired it isn't wiped. Returns a\n * detacher to call after a clean release.\n */\nfunction unlinkOnProcessExit(filePath: string, ownedByUs: () => boolean): () => void {\n const cleanup = () => {\n if (!ownedByUs()) return\n try {\n unlinkSync(filePath)\n } catch {\n // The file may already have been removed during shutdown.\n }\n }\n exitCleanups.add(cleanup)\n\n if (!exitListenerInstalled) {\n exitListenerInstalled = true\n process.once('exit', runExitCleanups)\n }\n\n return () => exitCleanups.delete(cleanup)\n}\n\ninterface DevServerRegistration {\n /** Remove the registry entry. */\n release: () => void\n /**\n * Rewrite the registry entry with partial updates merged in. Also bumps the\n * file's mtime, which fires `watchRegistry` in any workbench process and\n * triggers a rebroadcast to connected clients.\n */\n update: (patch: Partial<Omit<DevServerManifest, 'pid' | 'startedAt' | 'version'>>) => void\n}\n\n/**\n * Write a manifest file for the current process and return a handle with a\n * `release` function that removes it plus an `update` function for patching\n * fields post-registration. Uses synchronous I/O so the file exists before\n * any signal handler could fire.\n */\nexport function registerDevServer(\n manifest: Omit<DevServerManifest, 'pid' | 'startedAt' | 'version'>,\n): DevServerRegistration {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n let current: DevServerManifest = {\n ...manifest,\n pid: process.pid,\n startedAt: ownStartedAt(),\n version: REGISTRY_VERSION,\n }\n\n const filePath = join(registryDir, `${process.pid}.json`)\n writeFileSync(filePath, JSON.stringify(current, null, 2))\n\n // Guard against late updates from background tasks (e.g. the initial\n // manifest extraction) landing after `release()` has deleted the file —\n // without this, the update would re-create the registry entry and leak.\n let released = false\n\n // The file is pid-named, so it's always ours to remove on exit.\n const detachExitCleanup = unlinkOnProcessExit(filePath, () => !released)\n\n return {\n release() {\n released = true\n detachExitCleanup()\n try {\n unlinkSync(filePath)\n } catch {\n // ENOENT is fine — already cleaned up\n }\n },\n update(patch) {\n if (released) return\n current = {...current, ...patch}\n writeFileSync(filePath, JSON.stringify(current, null, 2))\n },\n }\n}\n\n/**\n * Read all manifest files from the registry, prune stale entries (dead PIDs),\n * and return the live ones.\n */\nexport function getRegisteredServers(): DevServerManifest[] {\n const registryDir = getRegistryDir()\n\n if (!existsSync(registryDir)) {\n return []\n }\n\n const files = readdirSync(registryDir).filter((f) => f.endsWith('.json'))\n const servers: DevServerManifest[] = []\n\n for (const file of files) {\n const filePath = join(registryDir, file)\n let raw: unknown\n try {\n raw = JSON.parse(readFileSync(filePath, 'utf8'))\n } catch {\n continue\n }\n\n const {data, success} = devServerManifestSchema.safeParse(raw)\n if (!success) continue\n\n if (isOurProcess(data.pid, data.startedAt)) {\n servers.push(data)\n } else {\n try {\n unlinkSync(filePath)\n } catch {\n // Ignore — another process may have already cleaned it up\n }\n }\n }\n\n return servers\n}\n\ninterface RegistryWatcher {\n close(): void\n}\n\n/**\n * Watch the registry directory for changes and invoke the callback with the\n * current list of live servers whenever a change is detected.\n *\n * Uses `fs.watch` with a debounce to coalesce rapid file changes (e.g. a\n * server starting and writing its manifest triggers multiple FS events).\n */\nexport function watchRegistry(callback: (servers: DevServerManifest[]) => void): RegistryWatcher {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n // Canonicalize to the real long path so `fs.watch` doesn't abort on Windows\n // short-path dirs. See `canonicalizeWatchDir`.\n const watchDir = canonicalizeWatchDir(registryDir)\n\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n const notify = () => {\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n callback(getRegisteredServers())\n }, 50)\n }\n\n const watcher = watch(watchDir, notify)\n\n return {\n close() {\n clearTimeout(debounceTimer)\n watcher.close()\n },\n }\n}\n\n// The workbench singleton lock — \"one workbench per machine\". Lives in the same\n// registry dir and shares the liveness/prune model: a stale lock left by a\n// crashed process is pruned on read so the next acquire isn't blocked forever.\n\nconst workbenchLockSchema = z.object({\n host: z.string(),\n pid: z.number(),\n port: z.number(),\n startedAt: z.string(),\n version: z.literal(REGISTRY_VERSION),\n})\n\n/**\n * Read the workbench lock file and return its contents if the holding\n * process is still alive. Prunes stale locks from crashed processes.\n */\nexport function readWorkbenchLock(): z.infer<typeof workbenchLockSchema> | undefined {\n const lockPath = join(getRegistryDir(), 'workbench.lock')\n\n let contents: string\n try {\n contents = readFileSync(lockPath, 'utf8')\n } catch {\n // File doesn't exist — nothing to prune, nothing to return\n return undefined\n }\n\n // Past this point the file exists. Anything that isn't a live, valid lock\n // (unparsable JSON, schema mismatch, dead/reused PID) is stale and must be\n // pruned — otherwise the next `acquireWorkbenchLock` call is blocked by\n // EEXIST forever and `sanity dev` silently no-ops the workbench server.\n const data = parseLockContents(contents)\n devDebug('Read workbench lock: %o', data)\n if (data && isOurProcess(data.pid, data.startedAt)) {\n devDebug('Workbench process is alive at pid %d on port %d', data.pid, data.port)\n return data\n }\n\n pruneWorkbenchLock(lockPath)\n return undefined\n}\n\nfunction parseLockContents(contents: string): z.infer<typeof workbenchLockSchema> | undefined {\n try {\n const {data, success} = workbenchLockSchema.safeParse(JSON.parse(contents))\n return success ? data : undefined\n } catch {\n return undefined\n }\n}\n\nfunction pruneWorkbenchLock(lockPath: string): void {\n try {\n devDebug('Removing stale workbench lock')\n unlinkSync(lockPath)\n devDebug('Stale workbench lock removed')\n } catch {\n // Another process may have already cleaned it up\n }\n}\n\ninterface WorkbenchLock {\n /** Release the lock file. */\n release: () => void\n /** Update the lock with the actual port after the server starts listening. */\n updatePort: (port: number) => void\n}\n\n/**\n * Attempt to acquire an exclusive lock for the workbench process.\n * Uses `O_EXCL` (the `wx` flag) which is atomic at the OS level — only one\n * process can create the file.\n *\n * The lock stores `{pid, host, port}` so other processes can find the\n * running workbench. Call `updatePort` after the Vite server starts to\n * write the actual port (Vite may pick a different one).\n *\n * @returns A {@link WorkbenchLock} if acquired, or `undefined` if another\n * live process already holds it.\n */\nexport function acquireWorkbenchLock(\n info: {host: string; port: number},\n retries = 1,\n): WorkbenchLock | undefined {\n const registryDir = getRegistryDir()\n mkdirSync(registryDir, {recursive: true})\n\n const lockPath = join(registryDir, 'workbench.lock')\n const startedAt = ownStartedAt()\n const lockData = {\n host: info.host,\n pid: process.pid,\n port: info.port,\n startedAt,\n version: REGISTRY_VERSION,\n }\n\n devDebug('Acquiring workbench lock at %s', lockPath)\n\n try {\n writeFileSync(lockPath, JSON.stringify(lockData), {flag: 'wx'})\n devDebug('Workbench lock acquired')\n\n let released = false\n // Only wipe the lock on exit if it's still ours — a successor that reacquired\n // it after our own release must not be clobbered.\n const detachExitCleanup = unlinkOnProcessExit(lockPath, () => {\n if (released) return false\n try {\n const disk = parseLockContents(readFileSync(lockPath, 'utf8'))\n return disk?.pid === process.pid && disk.startedAt === startedAt\n } catch {\n return false\n }\n })\n\n return {\n release() {\n released = true\n detachExitCleanup()\n try {\n unlinkSync(lockPath)\n } catch {\n // Already cleaned up\n }\n },\n updatePort(port: number) {\n writeFileSync(lockPath, JSON.stringify({...lockData, port}))\n },\n }\n } catch (err: unknown) {\n devDebug(\n 'Failed to acquire workbench lock: %s',\n err instanceof Error ? err.message : String(err),\n )\n if (!isNodeError(err) || err.code !== 'EEXIST') return undefined\n\n // Lock exists — check if the holder is still alive\n const existing = readWorkbenchLock()\n if (existing) return undefined\n\n // Stale lock was pruned by readWorkbenchLock — retry (with guard against infinite recursion)\n if (retries <= 0) return undefined\n return acquireWorkbenchLock(info, retries - 1)\n }\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && 'code' in err\n}\n"],"names":["existsSync","mkdirSync","readdirSync","readFileSync","unlinkSync","watch","writeFileSync","join","coreAppManifestSchema","getSanityDataDir","studioManifestSchema","subdebug","z","AppInterfaceMetadataSchema","canonicalizeWatchDir","getProcessStartTime","isOurProcess","devDebug","REGISTRY_VERSION","ownStartedAt","process","pid","Date","toISOString","interfaceBaseFields","id","string","moduleId","name","src","title","version","optional","devServerInterfaceSchema","discriminatedUnion","object","metadata","nullable","type","literal","null","devServerManifestSchema","configs","array","appType","fields","public","boolean","moduleName","number","host","interfaces","manifest","union","manifestUpdatedAt","port","projectId","startedAt","enum","workDir","getRegistryDir","exitCleanups","Set","exitListenerInstalled","runExitCleanups","cleanup","runRegistryExitCleanupForTesting","unlinkOnProcessExit","filePath","ownedByUs","add","once","delete","registerDevServer","registryDir","recursive","current","JSON","stringify","released","detachExitCleanup","release","update","patch","getRegisteredServers","files","filter","f","endsWith","servers","file","raw","parse","data","success","safeParse","push","watchRegistry","callback","watchDir","debounceTimer","notify","clearTimeout","setTimeout","watcher","close","workbenchLockSchema","readWorkbenchLock","lockPath","contents","undefined","parseLockContents","pruneWorkbenchLock","acquireWorkbenchLock","info","retries","lockData","flag","disk","updatePort","err","Error","message","String","isNodeError","code","existing"],"mappings":"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"}