@sanity/workbench-cli 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_exports/build.d.ts +46 -1
- package/dist/_exports/build.js +1 -0
- package/dist/_exports/build.js.map +1 -1
- package/dist/_exports/deploy.d.ts +49 -6
- package/dist/_exports/dev.d.ts +52 -9
- package/dist/_exports/index.d.ts +8 -1
- package/dist/_exports/preview.d.ts +169 -0
- package/dist/_exports/preview.js +3 -0
- package/dist/_exports/preview.js.map +1 -0
- package/dist/_exports/undeploy.d.ts +13 -1
- package/dist/actions/build/vite/optimize-deps.js +84 -0
- package/dist/actions/build/vite/optimize-deps.js.map +1 -0
- package/dist/actions/deploy/buildExposes.js +23 -10
- package/dist/actions/deploy/buildExposes.js.map +1 -1
- package/dist/actions/deploy/checkBuiltOutput.js +16 -2
- package/dist/actions/deploy/checkBuiltOutput.js.map +1 -1
- package/dist/actions/deploy/deployWorkbenchApp.js +19 -4
- package/dist/actions/deploy/deployWorkbenchApp.js.map +1 -1
- package/dist/actions/dev/deriveInterfaces.js +41 -25
- package/dist/actions/dev/deriveInterfaces.js.map +1 -1
- package/dist/actions/dev/registry.js +99 -19
- package/dist/actions/dev/registry.js.map +1 -1
- package/dist/actions/dev/startWorkbenchDev.js +6 -41
- package/dist/actions/dev/startWorkbenchDev.js.map +1 -1
- package/dist/actions/dev/startWorkbenchDevServer.js +7 -3
- package/dist/actions/dev/startWorkbenchDevServer.js.map +1 -1
- package/dist/actions/preview/serveBuiltApplication.js +53 -0
- package/dist/actions/preview/serveBuiltApplication.js.map +1 -0
- package/dist/actions/preview/startWorkbenchPreview.js +91 -0
- package/dist/actions/preview/startWorkbenchPreview.js.map +1 -0
- package/dist/contract.js +32 -0
- package/dist/contract.js.map +1 -1
- package/dist/defineApp.js +13 -6
- package/dist/defineApp.js.map +1 -1
- package/dist/resolveWorkbenchApp.js +3 -1
- package/dist/resolveWorkbenchApp.js.map +1 -1
- package/dist/services/applications.js +15 -1
- package/dist/services/applications.js.map +1 -1
- package/dist/util/serverOrchestration.js +75 -0
- package/dist/util/serverOrchestration.js.map +1 -0
- package/package.json +16 -12
|
@@ -1,27 +1,40 @@
|
|
|
1
|
+
import { interfaceModuleId } from '../../contract.js';
|
|
1
2
|
/**
|
|
2
3
|
* The interface records deploy sends: the app view (only when `exposesAppView`),
|
|
3
4
|
* every view, and every service.
|
|
4
5
|
* @internal
|
|
5
6
|
*/ export function buildExposes(exposes, { appName, appTitle, exposesAppView, version }) {
|
|
6
|
-
const toRecord = (prefix, decl)=>({
|
|
7
|
-
moduleId: `${prefix}/${decl.name}`,
|
|
8
|
-
name: decl.name,
|
|
9
|
-
title: decl.title ?? decl.name,
|
|
10
|
-
type: decl.type,
|
|
11
|
-
version
|
|
12
|
-
});
|
|
13
7
|
const records = [];
|
|
14
8
|
if (exposesAppView) {
|
|
15
9
|
records.push({
|
|
16
|
-
|
|
10
|
+
metadata: null,
|
|
11
|
+
moduleId: interfaceModuleId('app', appName),
|
|
17
12
|
name: appName,
|
|
18
13
|
title: appTitle,
|
|
19
14
|
type: 'app',
|
|
20
15
|
version
|
|
21
16
|
});
|
|
22
17
|
}
|
|
23
|
-
for (const view of exposes.views ?? [])
|
|
24
|
-
|
|
18
|
+
for (const view of exposes.views ?? []){
|
|
19
|
+
records.push({
|
|
20
|
+
metadata: null,
|
|
21
|
+
moduleId: interfaceModuleId('panel', view.name),
|
|
22
|
+
name: view.name,
|
|
23
|
+
title: view.title ?? view.name,
|
|
24
|
+
type: 'panel',
|
|
25
|
+
version
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
for (const service of exposes.services ?? []){
|
|
29
|
+
records.push({
|
|
30
|
+
metadata: null,
|
|
31
|
+
moduleId: interfaceModuleId('worker', service.name),
|
|
32
|
+
name: service.name,
|
|
33
|
+
title: service.title ?? service.name,
|
|
34
|
+
type: 'worker',
|
|
35
|
+
version
|
|
36
|
+
});
|
|
37
|
+
}
|
|
25
38
|
return records;
|
|
26
39
|
}
|
|
27
40
|
const label = (item)=>item.title === item.name ? item.name : `${item.title} (${item.name})`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/actions/deploy/buildExposes.ts"],"sourcesContent":["import {type WorkbenchExposes} from '../../resolveWorkbenchApp.js'\nimport {type BrettInterface} from '../../services/applications.js'\n\ninterface BuildExposesContext {\n appName: string\n appTitle: string\n /** Whether the build exposes the app view (`./App`) — apps with an `entry`, and every studio. */\n exposesAppView: boolean\n version: string\n}\n\n/**\n * The interface records deploy sends: the app view (only when `exposesAppView`),\n * every view, and every service.\n * @internal\n */\nexport function buildExposes(\n exposes: WorkbenchExposes,\n {appName, appTitle, exposesAppView, version}: BuildExposesContext,\n): BrettInterface[] {\n const
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/deploy/buildExposes.ts"],"sourcesContent":["import {interfaceModuleId} from '../../contract.js'\nimport {type WorkbenchExposes} from '../../resolveWorkbenchApp.js'\nimport {type BrettInterface} from '../../services/applications.js'\n\ninterface BuildExposesContext {\n appName: string\n appTitle: string\n /** Whether the build exposes the app view (`./App`) — apps with an `entry`, and every studio. */\n exposesAppView: boolean\n version: string\n}\n\n/**\n * The interface records deploy sends: the app view (only when `exposesAppView`),\n * every view, and every service.\n * @internal\n */\nexport function buildExposes(\n exposes: WorkbenchExposes,\n {appName, appTitle, exposesAppView, version}: BuildExposesContext,\n): BrettInterface[] {\n const records: BrettInterface[] = []\n if (exposesAppView) {\n records.push({\n metadata: null,\n moduleId: interfaceModuleId('app', appName),\n name: appName,\n title: appTitle,\n type: 'app',\n version,\n })\n }\n for (const view of exposes.views ?? []) {\n records.push({\n metadata: null,\n moduleId: interfaceModuleId('panel', view.name),\n name: view.name,\n title: view.title ?? view.name,\n type: 'panel',\n version,\n })\n }\n for (const service of exposes.services ?? []) {\n records.push({\n metadata: null,\n moduleId: interfaceModuleId('worker', service.name),\n name: service.name,\n title: service.title ?? service.name,\n type: 'worker',\n version,\n })\n }\n return records\n}\n\n/** A view or service as the deploy report and `--json` output surface it. */\nexport interface DeployedExpose {\n name: string\n src: string\n title: string\n type: string\n}\n\nconst label = (item: {name: string; title: string}) =>\n item.title === item.name ? item.name : `${item.title} (${item.name})`\n\n/**\n * One `Title (name): src` report line per declared entry point.\n * @internal\n */\nexport function summarizeExposeGroup(\n heading: string,\n items: readonly {name: string; src: string; title: string}[],\n): string {\n return `${heading}:\\n${items.map((item) => ` ${label(item)}: ${item.src}`).join('\\n')}`\n}\n\n/**\n * The deploy summary of an app's exposes: the structured records (for `--json`)\n * and one report line per non-empty group (for the human report).\n * @internal\n */\nexport function summarizeExposes({services, views}: WorkbenchExposes): {\n exposes: DeployedExpose[]\n lines: string[]\n} {\n const toExpose = (decl: {\n name: string\n src: string\n title?: string\n type: string\n }): DeployedExpose => ({\n name: decl.name,\n src: decl.src,\n title: decl.title ?? decl.name,\n type: decl.type,\n })\n const viewExposes = (views ?? []).map((view) => toExpose(view))\n const serviceExposes = (services ?? []).map((service) => toExpose(service))\n\n const lines: string[] = []\n if (viewExposes.length > 0) lines.push(summarizeExposeGroup('Views', viewExposes))\n if (serviceExposes.length > 0) lines.push(summarizeExposeGroup('Services', serviceExposes))\n return {exposes: [...viewExposes, ...serviceExposes], lines}\n}\n"],"names":["interfaceModuleId","buildExposes","exposes","appName","appTitle","exposesAppView","version","records","push","metadata","moduleId","name","title","type","view","views","service","services","label","item","summarizeExposeGroup","heading","items","map","src","join","summarizeExposes","toExpose","decl","viewExposes","serviceExposes","lines","length"],"mappings":"AAAA,SAAQA,iBAAiB,QAAO,oBAAmB;AAYnD;;;;CAIC,GACD,OAAO,SAASC,aACdC,OAAyB,EACzB,EAACC,OAAO,EAAEC,QAAQ,EAAEC,cAAc,EAAEC,OAAO,EAAsB;IAEjE,MAAMC,UAA4B,EAAE;IACpC,IAAIF,gBAAgB;QAClBE,QAAQC,IAAI,CAAC;YACXC,UAAU;YACVC,UAAUV,kBAAkB,OAAOG;YACnCQ,MAAMR;YACNS,OAAOR;YACPS,MAAM;YACNP;QACF;IACF;IACA,KAAK,MAAMQ,QAAQZ,QAAQa,KAAK,IAAI,EAAE,CAAE;QACtCR,QAAQC,IAAI,CAAC;YACXC,UAAU;YACVC,UAAUV,kBAAkB,SAASc,KAAKH,IAAI;YAC9CA,MAAMG,KAAKH,IAAI;YACfC,OAAOE,KAAKF,KAAK,IAAIE,KAAKH,IAAI;YAC9BE,MAAM;YACNP;QACF;IACF;IACA,KAAK,MAAMU,WAAWd,QAAQe,QAAQ,IAAI,EAAE,CAAE;QAC5CV,QAAQC,IAAI,CAAC;YACXC,UAAU;YACVC,UAAUV,kBAAkB,UAAUgB,QAAQL,IAAI;YAClDA,MAAMK,QAAQL,IAAI;YAClBC,OAAOI,QAAQJ,KAAK,IAAII,QAAQL,IAAI;YACpCE,MAAM;YACNP;QACF;IACF;IACA,OAAOC;AACT;AAUA,MAAMW,QAAQ,CAACC,OACbA,KAAKP,KAAK,KAAKO,KAAKR,IAAI,GAAGQ,KAAKR,IAAI,GAAG,GAAGQ,KAAKP,KAAK,CAAC,EAAE,EAAEO,KAAKR,IAAI,CAAC,CAAC,CAAC;AAEvE;;;CAGC,GACD,OAAO,SAASS,qBACdC,OAAe,EACfC,KAA4D;IAE5D,OAAO,GAAGD,QAAQ,GAAG,EAAEC,MAAMC,GAAG,CAAC,CAACJ,OAAS,CAAC,EAAE,EAAED,MAAMC,MAAM,EAAE,EAAEA,KAAKK,GAAG,EAAE,EAAEC,IAAI,CAAC,OAAO;AAC1F;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBAAiB,EAACT,QAAQ,EAAEF,KAAK,EAAmB;IAIlE,MAAMY,WAAW,CAACC,OAKK,CAAA;YACrBjB,MAAMiB,KAAKjB,IAAI;YACfa,KAAKI,KAAKJ,GAAG;YACbZ,OAAOgB,KAAKhB,KAAK,IAAIgB,KAAKjB,IAAI;YAC9BE,MAAMe,KAAKf,IAAI;QACjB,CAAA;IACA,MAAMgB,cAAc,AAACd,CAAAA,SAAS,EAAE,AAAD,EAAGQ,GAAG,CAAC,CAACT,OAASa,SAASb;IACzD,MAAMgB,iBAAiB,AAACb,CAAAA,YAAY,EAAE,AAAD,EAAGM,GAAG,CAAC,CAACP,UAAYW,SAASX;IAElE,MAAMe,QAAkB,EAAE;IAC1B,IAAIF,YAAYG,MAAM,GAAG,GAAGD,MAAMvB,IAAI,CAACY,qBAAqB,SAASS;IACrE,IAAIC,eAAeE,MAAM,GAAG,GAAGD,MAAMvB,IAAI,CAACY,qBAAqB,YAAYU;IAC3E,OAAO;QAAC5B,SAAS;eAAI2B;eAAgBC;SAAe;QAAEC;IAAK;AAC7D"}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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"}
|
|
@@ -3,14 +3,14 @@ import { createGzip } from 'node:zlib';
|
|
|
3
3
|
import { exitCodes } from '@sanity/cli-core';
|
|
4
4
|
import { spinner } from '@sanity/cli-core/ux';
|
|
5
5
|
import { pack } from 'tar-fs';
|
|
6
|
-
import { createApplication, createDeployment } from '../../services/applications.js';
|
|
6
|
+
import { createApplication, createDeployment, updateApplication } from '../../services/applications.js';
|
|
7
7
|
/**
|
|
8
8
|
* Deploy a workbench coreApp through Brett: redeploy when `appId` is set,
|
|
9
9
|
* otherwise create the application at `slug`. Returns the application id for the
|
|
10
10
|
* shell to report.
|
|
11
11
|
* @internal
|
|
12
12
|
*/ export async function deployCoreApp(options) {
|
|
13
|
-
const { appId, interfaces, isAutoUpdating, isSingleton, organizationId, slug, sourceDir, title, version } = options;
|
|
13
|
+
const { appId, icon, interfaces, isAutoUpdating, isSingleton, organizationId, slug, sourceDir, title, version, visibility } = options;
|
|
14
14
|
const tarball = pack(dirname(sourceDir), {
|
|
15
15
|
entries: [
|
|
16
16
|
basename(sourceDir)
|
|
@@ -26,12 +26,19 @@ import { createApplication, createDeployment } from '../../services/applications
|
|
|
26
26
|
tarball,
|
|
27
27
|
version
|
|
28
28
|
});
|
|
29
|
+
await updateApplication(appId, {
|
|
30
|
+
title,
|
|
31
|
+
...icon ? {
|
|
32
|
+
icon
|
|
33
|
+
} : {}
|
|
34
|
+
});
|
|
29
35
|
spin.succeed();
|
|
30
36
|
return {
|
|
31
37
|
applicationId: appId
|
|
32
38
|
};
|
|
33
39
|
}
|
|
34
40
|
const { id } = await createApplication({
|
|
41
|
+
icon,
|
|
35
42
|
interfaces,
|
|
36
43
|
isSingleton,
|
|
37
44
|
organizationId,
|
|
@@ -39,7 +46,8 @@ import { createApplication, createDeployment } from '../../services/applications
|
|
|
39
46
|
tarball,
|
|
40
47
|
title,
|
|
41
48
|
type: 'coreApp',
|
|
42
|
-
version
|
|
49
|
+
version,
|
|
50
|
+
visibility
|
|
43
51
|
});
|
|
44
52
|
spin.succeed();
|
|
45
53
|
return {
|
|
@@ -56,7 +64,7 @@ import { createApplication, createDeployment } from '../../services/applications
|
|
|
56
64
|
* the shell to report; a missing `studioHost` on create is a usage error.
|
|
57
65
|
* @internal
|
|
58
66
|
*/ export async function deployStudio(options) {
|
|
59
|
-
const { appId, interfaces, isAutoUpdating, organizationId, output, projectId, sourceDir, studioHost, title, version, workspaces } = options;
|
|
67
|
+
const { appId, icon, interfaces, isAutoUpdating, organizationId, output, projectId, sourceDir, studioHost, title, version, workspaces } = options;
|
|
60
68
|
const tarball = pack(dirname(sourceDir), {
|
|
61
69
|
entries: [
|
|
62
70
|
basename(sourceDir)
|
|
@@ -73,6 +81,12 @@ import { createApplication, createDeployment } from '../../services/applications
|
|
|
73
81
|
version,
|
|
74
82
|
workspaces
|
|
75
83
|
});
|
|
84
|
+
await updateApplication(appId, {
|
|
85
|
+
title,
|
|
86
|
+
...icon ? {
|
|
87
|
+
icon
|
|
88
|
+
} : {}
|
|
89
|
+
});
|
|
76
90
|
spin.succeed();
|
|
77
91
|
return {
|
|
78
92
|
applicationId: appId
|
|
@@ -85,6 +99,7 @@ import { createApplication, createDeployment } from '../../services/applications
|
|
|
85
99
|
});
|
|
86
100
|
}
|
|
87
101
|
const application = await createApplication({
|
|
102
|
+
icon,
|
|
88
103
|
interfaces,
|
|
89
104
|
organizationId,
|
|
90
105
|
projectId,
|
|
@@ -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 {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}): Promise<{applicationId: string}> {\n const {\n appId,\n interfaces,\n isAutoUpdating,\n isSingleton,\n organizationId,\n slug,\n sourceDir,\n title,\n version,\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 })\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","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,
|
|
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 updateApplication,\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 icon?: string\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 icon,\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 await updateApplication(appId, {title, ...(icon ? {icon} : {})})\n spin.succeed()\n return {applicationId: appId}\n }\n\n const {id} = await createApplication({\n icon,\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 icon?: string\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 icon,\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 await updateApplication(appId, {title, ...(icon ? {icon} : {})})\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 icon,\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","updateApplication","deployCoreApp","options","appId","icon","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,EAChBC,iBAAiB,QACZ,iCAAgC;AAEvC;;;;;CAKC,GACD,OAAO,eAAeC,cAAcC,OAYnC;IACC,MAAM,EACJC,KAAK,EACLC,IAAI,EACJC,UAAU,EACVC,cAAc,EACdC,WAAW,EACXC,cAAc,EACdC,IAAI,EACJC,SAAS,EACTC,KAAK,EACLC,OAAO,EACPC,UAAU,EACX,GAAGX;IACJ,MAAMY,UAAUjB,KAAKJ,QAAQiB,YAAY;QAACK,SAAS;YAACvB,SAASkB;SAAW;IAAA,GAAGM,IAAI,CAACtB;IAEhF,MAAMuB,OAAOrB,QAAQ,gBAAgBsB,KAAK;IAC1C,IAAI;QACF,IAAIf,OAAO;YACT,MAAMJ,iBAAiB;gBAACoB,eAAehB;gBAAOE;gBAAYC;gBAAgBQ;gBAASF;YAAO;YAC1F,MAAMZ,kBAAkBG,OAAO;gBAACQ;gBAAO,GAAIP,OAAO;oBAACA;gBAAI,IAAI,CAAC,CAAC;YAAC;YAC9Da,KAAKG,OAAO;YACZ,OAAO;gBAACD,eAAehB;YAAK;QAC9B;QAEA,MAAM,EAACkB,EAAE,EAAC,GAAG,MAAMvB,kBAAkB;YACnCM;YACAC;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,aAAavB,OAalC;IACC,MAAM,EACJC,KAAK,EACLC,IAAI,EACJC,UAAU,EACVC,cAAc,EACdE,cAAc,EACdkB,MAAM,EACNC,SAAS,EACTjB,SAAS,EACTkB,UAAU,EACVjB,KAAK,EACLC,OAAO,EACPiB,UAAU,EACX,GAAG3B;IACJ,MAAMY,UAAUjB,KAAKJ,QAAQiB,YAAY;QAACK,SAAS;YAACvB,SAASkB;SAAW;IAAA,GAAGM,IAAI,CAACtB;IAEhF,MAAMuB,OAAOrB,QAAQ,8BAA8BsB,KAAK;IACxD,IAAI;QACF,IAAIf,OAAO;YACT,MAAMJ,iBAAiB;gBACrBoB,eAAehB;gBACfE;gBACAC;gBACAQ;gBACAF;gBACAiB;YACF;YACA,MAAM7B,kBAAkBG,OAAO;gBAACQ;gBAAO,GAAIP,OAAO;oBAACA;gBAAI,IAAI,CAAC,CAAC;YAAC;YAC9Da,KAAKG,OAAO;YACZ,OAAO;gBAACD,eAAehB;YAAK;QAC9B;QAEA,IAAI,CAACyB,YAAY;YACfX,KAAKa,IAAI;YACT,OAAOJ,OAAOH,KAAK,CACjB,wFACA;gBAACQ,MAAMpC,UAAUqC,WAAW;YAAA;QAEhC;QAEA,MAAMC,cAAc,MAAMnC,kBAAkB;YAC1CM;YACAC;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,37 +1,53 @@
|
|
|
1
1
|
import { hash } from 'node:crypto';
|
|
2
|
-
import { MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION, SERVICE_CONTRACT_VERSION, VIEW_CONTRACT_VERSION } from '../../contract.js';
|
|
2
|
+
import { interfaceModuleId, MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION, SERVICE_CONTRACT_VERSION, VIEW_CONTRACT_VERSION } from '../../contract.js';
|
|
3
3
|
import { isWorkbenchApp, readConfig } from '../../defineApp.js';
|
|
4
4
|
/**
|
|
5
|
-
* Map a workbench app's declarations to
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* for a non-branded app; a studio that declares `entry` is rejected
|
|
9
|
-
* views
|
|
10
|
-
* {@link deriveConfigs}. `version` is the module's contract version; the app
|
|
11
|
-
* view has no versioned contract, so it carries none.
|
|
5
|
+
* Map a workbench app's declarations to its registry interface records:
|
|
6
|
+
* `views` → panels, `services` → workers, `entry` → the `app` view. Each mirrors
|
|
7
|
+
* a deployed record so the workbench loads a local interface like a deployed one.
|
|
8
|
+
* `undefined` for a non-branded app; a studio that declares `entry` is rejected
|
|
9
|
+
* (studio app views aren't implemented yet).
|
|
12
10
|
*/ export function deriveInterfaces(app, options) {
|
|
13
11
|
if (!isWorkbenchApp(app)) return undefined;
|
|
14
12
|
if (!options.isApp && app.entry !== undefined) {
|
|
15
13
|
throw new Error('App views for studios are not implemented yet');
|
|
16
14
|
}
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
const interfaceId = (type, name)=>`${app.name}-${type}-${name}`;
|
|
16
|
+
const views = (app.views ?? []).map((view)=>({
|
|
17
|
+
id: interfaceId('panel', view.name),
|
|
18
|
+
metadata: null,
|
|
19
|
+
moduleId: interfaceModuleId('panel', view.name),
|
|
20
|
+
name: view.name,
|
|
21
|
+
src: view.src,
|
|
22
|
+
title: view.title ?? view.name,
|
|
23
|
+
type: 'panel',
|
|
24
|
+
version: String(VIEW_CONTRACT_VERSION)
|
|
25
|
+
}));
|
|
26
|
+
const services = (app.services ?? []).map((service)=>({
|
|
27
|
+
id: interfaceId('worker', service.name),
|
|
28
|
+
metadata: null,
|
|
29
|
+
moduleId: interfaceModuleId('worker', service.name),
|
|
30
|
+
name: service.name,
|
|
31
|
+
src: service.src,
|
|
32
|
+
title: service.title ?? service.name,
|
|
33
|
+
type: 'worker',
|
|
34
|
+
version: String(SERVICE_CONTRACT_VERSION)
|
|
35
|
+
}));
|
|
36
|
+
const appView = app.entry === undefined ? [] : [
|
|
37
|
+
{
|
|
38
|
+
id: interfaceId('app', app.name),
|
|
39
|
+
metadata: null,
|
|
40
|
+
moduleId: interfaceModuleId('app', app.name),
|
|
41
|
+
name: app.name,
|
|
42
|
+
src: app.entry,
|
|
43
|
+
title: app.title,
|
|
44
|
+
type: 'app'
|
|
45
|
+
}
|
|
46
|
+
];
|
|
24
47
|
return [
|
|
25
|
-
...
|
|
26
|
-
...
|
|
27
|
-
...
|
|
28
|
-
{
|
|
29
|
-
name: app.name,
|
|
30
|
-
src: app.entry,
|
|
31
|
-
title: app.title,
|
|
32
|
-
type: 'app'
|
|
33
|
-
}
|
|
34
|
-
]
|
|
48
|
+
...views,
|
|
49
|
+
...services,
|
|
50
|
+
...appView
|
|
35
51
|
];
|
|
36
52
|
}
|
|
37
53
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/actions/dev/deriveInterfaces.ts"],"sourcesContent":["import {hash} from 'node:crypto'\n\nimport {type CliConfig} from '@sanity/cli-core'\n\nimport {\n MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION,\n SERVICE_CONTRACT_VERSION,\n VIEW_CONTRACT_VERSION,\n} from '../../contract.js'\nimport {isWorkbenchApp, readConfig} from '../../defineApp.js'\nimport {type DevServerManifest} from './registry.js'\n\n/** One forwarded interface record on the dev-server registry entry. */\nexport type DevServerInterface = NonNullable<DevServerManifest['interfaces']>[number]\n\n/** One forwarded config on the dev-server registry entry. */\nexport type DevServerConfig = NonNullable<DevServerManifest['configs']>[number]\n\n/**\n * Map a workbench app's declarations to
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/deriveInterfaces.ts"],"sourcesContent":["import {hash} from 'node:crypto'\n\nimport {type CliConfig} from '@sanity/cli-core'\n\nimport {\n interfaceModuleId,\n MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION,\n SERVICE_CONTRACT_VERSION,\n VIEW_CONTRACT_VERSION,\n} from '../../contract.js'\nimport {isWorkbenchApp, readConfig} from '../../defineApp.js'\nimport {type DevServerManifest} from './registry.js'\n\n/** One forwarded interface record on the dev-server registry entry. */\nexport type DevServerInterface = NonNullable<DevServerManifest['interfaces']>[number]\n\n/** One forwarded config on the dev-server registry entry. */\nexport type DevServerConfig = NonNullable<DevServerManifest['configs']>[number]\n\n/**\n * Map a workbench app's declarations to its registry interface records:\n * `views` → panels, `services` → workers, `entry` → the `app` view. Each mirrors\n * a deployed record so the workbench loads a local interface like a deployed one.\n * `undefined` for a non-branded app; a studio that declares `entry` is rejected\n * (studio app views aren't implemented yet).\n */\nexport function deriveInterfaces(\n app: CliConfig['app'],\n options: {isApp: boolean},\n): DevServerInterface[] | undefined {\n if (!isWorkbenchApp(app)) return undefined\n\n if (!options.isApp && app.entry !== undefined) {\n throw new Error('App views for studios are not implemented yet')\n }\n\n const interfaceId = (type: string, name: string): string => `${app.name}-${type}-${name}`\n\n const views = (app.views ?? []).map(\n (view): DevServerInterface => ({\n id: interfaceId('panel', view.name),\n metadata: null,\n moduleId: interfaceModuleId('panel', view.name),\n name: view.name,\n src: view.src,\n title: view.title ?? view.name,\n type: 'panel',\n version: String(VIEW_CONTRACT_VERSION),\n }),\n )\n\n const services = (app.services ?? []).map(\n (service): DevServerInterface => ({\n id: interfaceId('worker', service.name),\n metadata: null,\n moduleId: interfaceModuleId('worker', service.name),\n name: service.name,\n src: service.src,\n title: service.title ?? service.name,\n type: 'worker',\n version: String(SERVICE_CONTRACT_VERSION),\n }),\n )\n\n const appView: DevServerInterface[] =\n app.entry === undefined\n ? []\n : [\n {\n id: interfaceId('app', app.name),\n metadata: null,\n moduleId: interfaceModuleId('app', app.name),\n name: app.name,\n src: app.entry,\n title: app.title,\n type: 'app',\n },\n ]\n\n return [...views, ...services, ...appView]\n}\n\n/**\n * The named source files a config's generated module is built from, dispatched\n * per app type — the projection the exposes-set id keys on, so the generic HMR\n * tracker owns none of the per-type shape. Throws on an app type it can't\n * handle, so a new config family has to register its shape here.\n */\nexport function deriveConfigEntries(config: DevServerConfig): {name: string; src: string}[] {\n switch (config.appType) {\n case 'media-library': {\n return config.fields.map((field) => ({name: field.name, src: field.src}))\n }\n default: {\n throw new Error(`Cannot derive entries for unknown config appType: ${config.appType}`)\n }\n }\n}\n\n/**\n * The fields' schema *values* can't serialize — the workbench loads them from\n * the federation module. `src` stays on so the exposes-set id keys on it and a\n * repoint rebuilds. `appType` routes the config to the singleton (no app id to\n * key on). `id` is a content hash of the entry — it fills the\n * installation-config id slot deployed apps get from the applications API,\n * and the workbench keys change detection on it. `version` is the config\n * contract version the generated module exports, known before the module runs.\n */\nexport function deriveConfigs(app: CliConfig['app']): DevServerConfig[] {\n if (!isWorkbenchApp(app)) return []\n const config = readConfig(app)\n if (!config) return []\n const entry = {\n appType: config.appType,\n fields: config.fields.map((field) => ({\n name: field.name,\n public: field.public,\n src: field.src,\n title: field.title,\n })),\n moduleName: app.name,\n version: MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION,\n }\n return [{...entry, id: hash('sha1', JSON.stringify(entry))}]\n}\n"],"names":["hash","interfaceModuleId","MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION","SERVICE_CONTRACT_VERSION","VIEW_CONTRACT_VERSION","isWorkbenchApp","readConfig","deriveInterfaces","app","options","undefined","isApp","entry","Error","interfaceId","type","name","views","map","view","id","metadata","moduleId","src","title","version","String","services","service","appView","deriveConfigEntries","config","appType","fields","field","deriveConfigs","public","moduleName","JSON","stringify"],"mappings":"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"}
|
|
@@ -2,9 +2,28 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, watch, wr
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { coreAppManifestSchema, getSanityDataDir, studioManifestSchema, subdebug } from '@sanity/cli-core';
|
|
4
4
|
import { z } from 'zod/mini';
|
|
5
|
+
import { AppInterfaceMetadataSchema } from '../../contract.js';
|
|
5
6
|
import { canonicalizeWatchDir } from './canonicalizeWatchDir.js';
|
|
6
7
|
import { getProcessStartTime, isOurProcess } from './processLiveness.js';
|
|
7
|
-
|
|
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');
|
|
8
27
|
/** Bump when the manifest/lock shape changes in a breaking way. */ const REGISTRY_VERSION = 1;
|
|
9
28
|
/**
|
|
10
29
|
* The current process's start time as reported by the OS, for the `startedAt`
|
|
@@ -14,6 +33,34 @@ const devDebug = subdebug('dev');
|
|
|
14
33
|
*/ function ownStartedAt() {
|
|
15
34
|
return (getProcessStartTime(process.pid) ?? new Date()).toISOString();
|
|
16
35
|
}
|
|
36
|
+
const interfaceBaseFields = {
|
|
37
|
+
/** CLI-minted for a local interface; a deployed one gets its id from Brett. */ id: z.string(),
|
|
38
|
+
moduleId: z.string(),
|
|
39
|
+
name: z.string(),
|
|
40
|
+
/** Raw source vite serves; a deployed interface carries only the `moduleId`. */ src: z.string(),
|
|
41
|
+
title: z.string(),
|
|
42
|
+
version: z.optional(z.string())
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A forwarded interface, discriminated on `type`. Kept outside the manifest so
|
|
46
|
+
* the workbench renders local panels and runs workers without a deploy.
|
|
47
|
+
*/ const devServerInterfaceSchema = z.discriminatedUnion('type', [
|
|
48
|
+
z.object({
|
|
49
|
+
...interfaceBaseFields,
|
|
50
|
+
metadata: z.nullable(AppInterfaceMetadataSchema),
|
|
51
|
+
type: z.literal('app')
|
|
52
|
+
}),
|
|
53
|
+
z.object({
|
|
54
|
+
...interfaceBaseFields,
|
|
55
|
+
metadata: z.null(),
|
|
56
|
+
type: z.literal('panel')
|
|
57
|
+
}),
|
|
58
|
+
z.object({
|
|
59
|
+
...interfaceBaseFields,
|
|
60
|
+
metadata: z.null(),
|
|
61
|
+
type: z.literal('worker')
|
|
62
|
+
})
|
|
63
|
+
]);
|
|
17
64
|
const devServerManifestSchema = z.object({
|
|
18
65
|
/**
|
|
19
66
|
* Field schema *values* load from the federation module; each field's `src`
|
|
@@ -40,24 +87,7 @@ const devServerManifestSchema = z.object({
|
|
|
40
87
|
}))),
|
|
41
88
|
host: z.string(),
|
|
42
89
|
id: z.optional(z.string()),
|
|
43
|
-
|
|
44
|
-
* Interfaces the app exposes, mapped from the declared `views` (dock panels,
|
|
45
|
-
* `type: "panel"`) and `services` (background workers,
|
|
46
|
-
* `type: "worker"`). A service is just an interface, so both live
|
|
47
|
-
* in this one list. Carried separately from the manifest — interfaces live in
|
|
48
|
-
* the application service, not the manifest — so the workbench can render
|
|
49
|
-
* local panels and run local workers without a deploy. `src` is the
|
|
50
|
-
* declared source file; `title` defaults to `name`. Lenient by design; the
|
|
51
|
-
* workbench is the authority on the interface shape.
|
|
52
|
-
*/ interfaces: z.optional(z.array(z.object({
|
|
53
|
-
name: z.string(),
|
|
54
|
-
src: z.string(),
|
|
55
|
-
title: z.string(),
|
|
56
|
-
type: z.string(),
|
|
57
|
-
// Contract version the interface's generated module exports; the app
|
|
58
|
-
// view has no versioned contract and carries none.
|
|
59
|
-
version: z.optional(z.number())
|
|
60
|
-
}))),
|
|
90
|
+
interfaces: z.optional(z.array(devServerInterfaceSchema)),
|
|
61
91
|
/**
|
|
62
92
|
* Inlined manifest — either a {@link StudioManifest} or {@link CoreAppManifest},
|
|
63
93
|
* validated against the shared cli-core schemas. The registry stores and
|
|
@@ -88,6 +118,39 @@ const devServerManifestSchema = z.object({
|
|
|
88
118
|
*/ function getRegistryDir() {
|
|
89
119
|
return join(getSanityDataDir(), 'dev-servers');
|
|
90
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
|
+
}
|
|
91
154
|
/**
|
|
92
155
|
* Write a manifest file for the current process and return a handle with a
|
|
93
156
|
* `release` function that removes it plus an `update` function for patching
|
|
@@ -110,9 +173,12 @@ const devServerManifestSchema = z.object({
|
|
|
110
173
|
// manifest extraction) landing after `release()` has deleted the file —
|
|
111
174
|
// without this, the update would re-create the registry entry and leak.
|
|
112
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);
|
|
113
178
|
return {
|
|
114
179
|
release () {
|
|
115
180
|
released = true;
|
|
181
|
+
detachExitCleanup();
|
|
116
182
|
try {
|
|
117
183
|
unlinkSync(filePath);
|
|
118
184
|
} catch {
|
|
@@ -273,8 +339,22 @@ function pruneWorkbenchLock(lockPath) {
|
|
|
273
339
|
flag: 'wx'
|
|
274
340
|
});
|
|
275
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
|
+
});
|
|
276
354
|
return {
|
|
277
355
|
release () {
|
|
356
|
+
released = true;
|
|
357
|
+
detachExitCleanup();
|
|
278
358
|
try {
|
|
279
359
|
unlinkSync(lockPath);
|
|
280
360
|
} catch {
|