@sanity/workbench-cli 2.0.1 → 2.0.2
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 +13 -253
- package/dist/_exports/contract-DyG11fQ7.d.ts +45 -0
- package/dist/_exports/defineApp-r_CmoZfS.d.ts +169 -0
- package/dist/_exports/deploy.d.ts +94 -427
- package/dist/_exports/dev.d.ts +26 -196
- package/dist/_exports/index.d.ts +74 -407
- package/dist/_exports/init.d.ts +5 -9
- package/dist/_exports/preview.d.ts +20 -187
- package/dist/_exports/registry-DI7hnTof.d.ts +102 -0
- package/dist/_exports/resolveWorkbenchApp-DqFqET9E.d.ts +41 -0
- package/dist/_exports/summarizeInterfaces-DBGiAwNT.d.ts +62 -0
- package/dist/_exports/undeploy.d.ts +15 -296
- package/dist/actions/dev/registry.js +9 -0
- package/dist/actions/dev/registry.js.map +1 -1
- package/dist/actions/dev/startDevServerRegistration.js +50 -3
- package/dist/actions/dev/startDevServerRegistration.js.map +1 -1
- package/dist/actions/dev/startWorkbenchDevServer.js +3 -6
- package/dist/actions/dev/startWorkbenchDevServer.js.map +1 -1
- package/dist/services/applications.js +7 -7
- package/dist/services/applications.js.map +1 -1
- package/dist/services/installations.js +4 -4
- package/dist/services/installations.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { UndeployApplicationTarget } from "@sanity/cli-core/undeploy";
|
|
4
|
-
|
|
5
|
-
import { z } from "zod/mini";
|
|
6
|
-
|
|
7
|
-
/** The `asset_source` variant of an app's `views`. @public */
|
|
8
|
-
declare type AssetSourceView = Extract<
|
|
9
|
-
NonNullable<z.output<typeof DefineAppInputSchema>["views"]>[number],
|
|
10
|
-
{
|
|
11
|
-
type: "asset_source";
|
|
12
|
-
}
|
|
13
|
-
>;
|
|
14
|
-
|
|
15
|
-
declare interface ConfigSnapshot {
|
|
1
|
+
import { r as DeployableWorkbenchApp, t as DeployedInterface } from "./summarizeInterfaces-DBGiAwNT.js";
|
|
2
|
+
import "node:zlib";
|
|
3
|
+
import { UndeployAdapter, UndeployApplicationTarget, UndeployConfigTarget } from "@sanity/cli-core/undeploy";
|
|
4
|
+
interface ConfigSnapshot {
|
|
16
5
|
id: string;
|
|
17
6
|
createdAt?: string;
|
|
18
7
|
deployedBy?: string;
|
|
19
8
|
/** Whether this snapshot is the one being served; at most one per installation. */
|
|
20
9
|
isActive?: boolean;
|
|
21
10
|
}
|
|
22
|
-
|
|
11
|
+
/** The workbench extension of the shared target; serializes into `--json` as-is. */
|
|
12
|
+
type WorkbenchUndeployTarget = (UndeployApplicationTarget & {
|
|
13
|
+
services: DeployedInterface[];
|
|
14
|
+
views: DeployedInterface[];
|
|
15
|
+
}) | (UndeployConfigTarget & {
|
|
16
|
+
configs: ConfigSnapshot[];
|
|
17
|
+
});
|
|
23
18
|
/**
|
|
24
19
|
* The undeploy adapter for workbench apps, mirroring what a workbench deploy
|
|
25
20
|
* creates: apps that expose interfaces delete their Brett application (the
|
|
@@ -27,287 +22,11 @@ declare interface ConfigSnapshot {
|
|
|
27
22
|
* installations); a singleton without interfaces — the media library — deletes
|
|
28
23
|
* its installation's config snapshots instead.
|
|
29
24
|
*/
|
|
30
|
-
|
|
25
|
+
declare function createWorkbenchUndeployAdapter(options: {
|
|
31
26
|
appId: string | undefined;
|
|
32
27
|
organizationId: string | undefined;
|
|
33
|
-
type:
|
|
28
|
+
type: 'coreApp' | 'studio';
|
|
34
29
|
workbench: DeployableWorkbenchApp;
|
|
35
30
|
}): UndeployAdapter<WorkbenchUndeployTarget>;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* User-facing input for `unstable_defineApp`. Excludes the internal
|
|
39
|
-
* `applicationType`, `isSingleton`, and `config` — validated by the schema but
|
|
40
|
-
* not part of the public surface (Sanity-owned apps set them via
|
|
41
|
-
* `@ts-expect-error`). A union: an app declares an app `entry` (navigable) or
|
|
42
|
-
* panel `views`, never both — but `asset_source` and `tile` views are separate
|
|
43
|
-
* kinds and may accompany either.
|
|
44
|
-
* @public
|
|
45
|
-
*/
|
|
46
|
-
declare type DefineAppInput = Omit<
|
|
47
|
-
z.output<typeof DefineAppInputSchema>,
|
|
48
|
-
"applicationType" | "config" | "entry" | "isSingleton" | "views"
|
|
49
|
-
> &
|
|
50
|
-
(
|
|
51
|
-
| {
|
|
52
|
-
entry?: never;
|
|
53
|
-
views?: NonNullable<z.output<typeof DefineAppInputSchema>["views"]>;
|
|
54
|
-
}
|
|
55
|
-
| {
|
|
56
|
-
entry?: string;
|
|
57
|
-
views?: (AssetSourceView | TileView)[];
|
|
58
|
-
}
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Runtime-validation schema for `unstable_defineApp`.
|
|
63
|
-
* @internal
|
|
64
|
-
*/
|
|
65
|
-
declare const DefineAppInputSchema: z.ZodMiniObject<
|
|
66
|
-
{
|
|
67
|
-
applicationType: z.ZodMiniOptional<
|
|
68
|
-
z.ZodMiniEnum<{
|
|
69
|
-
"media-library": "media-library";
|
|
70
|
-
coreApp: "coreApp";
|
|
71
|
-
studio: "studio";
|
|
72
|
-
canvas: "canvas";
|
|
73
|
-
dashboard: "dashboard";
|
|
74
|
-
}>
|
|
75
|
-
>;
|
|
76
|
-
config: z.ZodMiniOptional<
|
|
77
|
-
z.ZodMiniDiscriminatedUnion<
|
|
78
|
-
[
|
|
79
|
-
z.ZodMiniObject<
|
|
80
|
-
{
|
|
81
|
-
appType: z.ZodMiniLiteral<"media-library">;
|
|
82
|
-
fields: z.ZodMiniArray<
|
|
83
|
-
z.ZodMiniObject<
|
|
84
|
-
{
|
|
85
|
-
public: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
86
|
-
title: z.ZodMiniString<string>;
|
|
87
|
-
name: z.ZodMiniString<string>;
|
|
88
|
-
src: z.ZodMiniString<string>;
|
|
89
|
-
},
|
|
90
|
-
z.core.$strip
|
|
91
|
-
>
|
|
92
|
-
>;
|
|
93
|
-
},
|
|
94
|
-
z.core.$strip
|
|
95
|
-
>,
|
|
96
|
-
],
|
|
97
|
-
"appType"
|
|
98
|
-
>
|
|
99
|
-
>;
|
|
100
|
-
entry: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
101
|
-
group: z.ZodMiniOptional<
|
|
102
|
-
z.ZodMiniEnum<{
|
|
103
|
-
"dock.system": "dock.system";
|
|
104
|
-
"dock.applications": "dock.applications";
|
|
105
|
-
"dock.user": "dock.user";
|
|
106
|
-
}>
|
|
107
|
-
>;
|
|
108
|
-
icon: z.ZodMiniOptional<z.ZodMiniString<string>>;
|
|
109
|
-
isSingleton: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
|
|
110
|
-
organizationId: z.ZodMiniString<string>;
|
|
111
|
-
priority: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
112
|
-
services: z.ZodMiniOptional<
|
|
113
|
-
z.ZodMiniArray<
|
|
114
|
-
z.ZodMiniDiscriminatedUnion<
|
|
115
|
-
[
|
|
116
|
-
z.ZodMiniObject<
|
|
117
|
-
{
|
|
118
|
-
title: z.ZodMiniString<string>;
|
|
119
|
-
name: z.ZodMiniString<string>;
|
|
120
|
-
src: z.ZodMiniString<string>;
|
|
121
|
-
type: z.ZodMiniLiteral<"worker">;
|
|
122
|
-
},
|
|
123
|
-
z.core.$strip
|
|
124
|
-
>,
|
|
125
|
-
],
|
|
126
|
-
"type"
|
|
127
|
-
>
|
|
128
|
-
>
|
|
129
|
-
>;
|
|
130
|
-
slug: z.ZodMiniString<string>;
|
|
131
|
-
title: z.ZodMiniString<string>;
|
|
132
|
-
views: z.ZodMiniOptional<
|
|
133
|
-
z.ZodMiniArray<
|
|
134
|
-
z.ZodMiniDiscriminatedUnion<
|
|
135
|
-
[
|
|
136
|
-
z.ZodMiniObject<
|
|
137
|
-
{
|
|
138
|
-
title: z.ZodMiniString<string>;
|
|
139
|
-
name: z.ZodMiniString<string>;
|
|
140
|
-
src: z.ZodMiniString<string>;
|
|
141
|
-
type: z.ZodMiniLiteral<"panel">;
|
|
142
|
-
},
|
|
143
|
-
z.core.$strip
|
|
144
|
-
>,
|
|
145
|
-
z.ZodMiniObject<
|
|
146
|
-
{
|
|
147
|
-
title: z.ZodMiniString<string>;
|
|
148
|
-
name: z.ZodMiniString<string>;
|
|
149
|
-
src: z.ZodMiniString<string>;
|
|
150
|
-
type: z.ZodMiniLiteral<"asset_source">;
|
|
151
|
-
},
|
|
152
|
-
z.core.$strip
|
|
153
|
-
>,
|
|
154
|
-
z.ZodMiniObject<
|
|
155
|
-
{
|
|
156
|
-
priority: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
|
|
157
|
-
size: z.ZodMiniEnum<{
|
|
158
|
-
small: "small";
|
|
159
|
-
large: "large";
|
|
160
|
-
banner: "banner";
|
|
161
|
-
}>;
|
|
162
|
-
title: z.ZodMiniString<string>;
|
|
163
|
-
name: z.ZodMiniString<string>;
|
|
164
|
-
src: z.ZodMiniString<string>;
|
|
165
|
-
type: z.ZodMiniLiteral<"tile">;
|
|
166
|
-
},
|
|
167
|
-
z.core.$strip
|
|
168
|
-
>,
|
|
169
|
-
],
|
|
170
|
-
"type"
|
|
171
|
-
>
|
|
172
|
-
>
|
|
173
|
-
>;
|
|
174
|
-
visibility: z.ZodMiniOptional<
|
|
175
|
-
z.ZodMiniEnum<{
|
|
176
|
-
default: "default";
|
|
177
|
-
unlisted: "unlisted";
|
|
178
|
-
disabled: "disabled";
|
|
179
|
-
}>
|
|
180
|
-
>;
|
|
181
|
-
},
|
|
182
|
-
z.core.$strip
|
|
183
|
-
>;
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* The branded result of `unstable_defineApp`. Carries the same fields as the
|
|
187
|
-
* input plus the internal brand — users only ever see `DefineAppInput`.
|
|
188
|
-
* @public
|
|
189
|
-
*/
|
|
190
|
-
declare type DefineAppResult = DefineAppInput & {
|
|
191
|
-
readonly [WORKBENCH_APP]: true;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
declare interface DeployableWorkbenchApp extends ResolvedWorkbenchApp {
|
|
195
|
-
/**
|
|
196
|
-
* Throws when the app exposes nothing (no entry, view, service, or config) —
|
|
197
|
-
* the remote would have nothing to load. Gated before any prompt or API call.
|
|
198
|
-
*/
|
|
199
|
-
assertDeployable(): void;
|
|
200
|
-
/**
|
|
201
|
-
* Validates the app's declared views into the application-service payload.
|
|
202
|
-
* Throws when a view declaration is malformed.
|
|
203
|
-
*/
|
|
204
|
-
buildViewDeploymentPayload(applicationId: string): ViewDeploymentPayload;
|
|
205
|
-
/**
|
|
206
|
-
* A singleton (the Media Library) that carries an config — deploy
|
|
207
|
-
* persists the config to the org's installation. Independent of the interfaces,
|
|
208
|
-
* which register regardless; non-singletons never carry a config.
|
|
209
|
-
*/
|
|
210
|
-
deploySingletonConfig: boolean;
|
|
211
|
-
/** Declares something to host as an application — an entry, view, or service. */
|
|
212
|
-
hasInterfaces: boolean;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** A view or service as the deploy report and `--json` output surface it. */
|
|
216
|
-
declare interface DeployedInterface {
|
|
217
|
-
name: string;
|
|
218
|
-
src: string;
|
|
219
|
-
title: string;
|
|
220
|
-
type: string;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** @public */
|
|
224
|
-
declare interface ResolvedWorkbenchApp {
|
|
225
|
-
/** Organization that owns the app — part of its build-id identity. */
|
|
226
|
-
readonly organizationId: string;
|
|
227
|
-
/** Background worker services the app declares. */
|
|
228
|
-
readonly services: NonNullable<DefineAppInput["services"]>;
|
|
229
|
-
readonly slug: string;
|
|
230
|
-
/** Dock panel views the app declares. */
|
|
231
|
-
readonly views: NonNullable<DefineAppInput["views"]>;
|
|
232
|
-
/** Resolved app kind — `studio` or one of the SDK app types. */
|
|
233
|
-
readonly applicationType?: string;
|
|
234
|
-
/** Deploys on its own path, separate from the interfaces. */
|
|
235
|
-
readonly config?: WorkbenchApp["config"];
|
|
236
|
-
/** SDK app-view entrypoint, when declared. */
|
|
237
|
-
readonly entry?: string;
|
|
238
|
-
/** Path to the app's icon SVG, resolved and shipped to Brett on deploy. */
|
|
239
|
-
readonly icon?: string;
|
|
240
|
-
/** Explicit singleton flag (a Sanity-owned app); `undefined` when the app doesn't set it. */
|
|
241
|
-
readonly isSingleton?: boolean;
|
|
242
|
-
/** Dashboard visibility declared by the app; `undefined` when unset. */
|
|
243
|
-
readonly visibility?: AppVisibility;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** The `tile` variant of an app's `views`. @public */
|
|
247
|
-
declare type TileView = Extract<
|
|
248
|
-
NonNullable<z.output<typeof DefineAppInputSchema>["views"]>[number],
|
|
249
|
-
{
|
|
250
|
-
type: "tile";
|
|
251
|
-
}
|
|
252
|
-
>;
|
|
253
|
-
|
|
254
|
-
declare type ViewDeploymentPayload = z.infer<
|
|
255
|
-
typeof viewDeploymentPayloadSchema
|
|
256
|
-
>;
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Payload registering an app's views with the application service on deploy.
|
|
260
|
-
*
|
|
261
|
-
* Phase 1 stub: the service that stores views does not exist yet, so the
|
|
262
|
-
* payload is validated and logged only — never sent. Builds the contract the
|
|
263
|
-
* application-service endpoint will accept.
|
|
264
|
-
*/
|
|
265
|
-
declare const viewDeploymentPayloadSchema: z.ZodMiniObject<
|
|
266
|
-
{
|
|
267
|
-
applicationId: z.ZodMiniString<string>;
|
|
268
|
-
views: z.ZodMiniArray<
|
|
269
|
-
z.ZodMiniObject<
|
|
270
|
-
{
|
|
271
|
-
name: z.ZodMiniString<string>;
|
|
272
|
-
src: z.ZodMiniString<string>;
|
|
273
|
-
type: z.ZodMiniEnum<{
|
|
274
|
-
asset_source: "asset_source";
|
|
275
|
-
panel: "panel";
|
|
276
|
-
tile: "tile";
|
|
277
|
-
}>;
|
|
278
|
-
},
|
|
279
|
-
z.core.$loose
|
|
280
|
-
>
|
|
281
|
-
>;
|
|
282
|
-
},
|
|
283
|
-
z.core.$strip
|
|
284
|
-
>;
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Nominal brand the CLI discriminates on to enable the workbench build/deploy
|
|
288
|
-
* codepath. Registered via `Symbol.for` so the marker survives module-realm
|
|
289
|
-
* boundaries — `@sanity/cli-core` re-derives the same global symbol with
|
|
290
|
-
* `Symbol.for` rather than importing it, so it stays internal to this module.
|
|
291
|
-
*/
|
|
292
|
-
declare const WORKBENCH_APP: unique symbol;
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* A branded app as the CLI reads it — the full schema shape, including the
|
|
296
|
-
* internal fields `DefineAppInput` omits. Schema-derived so the narrowing
|
|
297
|
-
* can't drift from what the schema validates.
|
|
298
|
-
* @public
|
|
299
|
-
*/
|
|
300
|
-
declare type WorkbenchApp = DefineAppResult &
|
|
301
|
-
z.output<typeof DefineAppInputSchema>;
|
|
302
|
-
|
|
303
|
-
/** The workbench extension of the shared target; serializes into `--json` as-is. */
|
|
304
|
-
declare type WorkbenchUndeployTarget =
|
|
305
|
-
| (UndeployApplicationTarget & {
|
|
306
|
-
services: DeployedInterface[];
|
|
307
|
-
views: DeployedInterface[];
|
|
308
|
-
})
|
|
309
|
-
| (UndeployConfigTarget & {
|
|
310
|
-
configs: ConfigSnapshot[];
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
export {};
|
|
31
|
+
export { createWorkbenchUndeployAdapter };
|
|
32
|
+
//# sourceMappingURL=undeploy.d.ts.map
|
|
@@ -122,6 +122,15 @@ const devServerManifestSchema = z.object({
|
|
|
122
122
|
version: z.literal(REGISTRY_VERSION),
|
|
123
123
|
workDir: z.string()
|
|
124
124
|
});
|
|
125
|
+
/**
|
|
126
|
+
* A config-only server carries configs but no interfaces — e.g. a
|
|
127
|
+
* media-library config app under development. The workbench never routes it
|
|
128
|
+
* as an app; only its configs are published. It therefore plays a different
|
|
129
|
+
* role than an app server, and the two may share a slug (a config app
|
|
130
|
+
* developed alongside the locally served singleton it configures).
|
|
131
|
+
*/ export function isConfigOnlyServer(server) {
|
|
132
|
+
return Boolean(server.configs?.length) && !server.interfaces?.length;
|
|
133
|
+
}
|
|
125
134
|
/**
|
|
126
135
|
* Path to the dev server registry directory. Lives under the shared Sanity
|
|
127
136
|
* config directory to stay consistent with other CLI paths.
|
|
@@ -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, TileInterfaceMetadataSchema} 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('asset_source')}),\n z.object({\n ...interfaceBaseFields,\n metadata: TileInterfaceMetadataSchema,\n type: z.literal('tile'),\n }),\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` slug — the module-federation alias the\n // workbench loads this config's live values from.\n moduleName: z.optional(z.string()),\n // The version the workbench federates this config's module under —\n // a string, like the one Brett returns on a deployed `activeConfig`.\n version: z.string(),\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","TileInterfaceMetadataSchema","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","host","interfaces","manifest","union","manifestUpdatedAt","number","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":"AAAA,SACEA,UAAU,EACVC,SAAS,EACTC,WAAW,EACXC,YAAY,EACZC,UAAU,EACVC,KAAK,EACLC,aAAa,QACR,UAAS;AAChB,SAAQC,IAAI,QAAO,YAAW;AAE9B,SACEC,qBAAqB,EACrBC,gBAAgB,EAChBC,oBAAoB,EACpBC,QAAQ,QACH,mBAAkB;AACzB,SAAQC,CAAC,QAAO,WAAU;AAE1B,SAAQC,0BAA0B,EAAEC,2BAA2B,QAAO,oBAAmB;AACzF,SAAQC,oBAAoB,QAAO,4BAA2B;AAC9D,SAAQC,mBAAmB,EAAEC,YAAY,QAAO,uBAAsB;AAEtE;;;;;;;;;;;;;;;;;;CAkBC,GAED,MAAMC,WAAWP,SAAS;AAE1B,iEAAiE,GACjE,MAAMQ,mBAAmB;AAEzB;;;;;CAKC,GACD,SAASC;IACP,OAAO,AAACJ,CAAAA,oBAAoBK,QAAQC,GAAG,KAAK,IAAIC,MAAK,EAAGC,WAAW;AACrE;AAEA,MAAMC,sBAAsB;IAC1B,6EAA6E,GAC7EC,IAAId,EAAEe,MAAM;IACZC,UAAUhB,EAAEe,MAAM;IAClBE,MAAMjB,EAAEe,MAAM;IACd,8EAA8E,GAC9EG,KAAKlB,EAAEe,MAAM;IACbI,OAAOnB,EAAEe,MAAM;IACfK,SAASpB,EAAEqB,QAAQ,CAACrB,EAAEe,MAAM;AAC9B;AAEA;;;CAGC,GACD,MAAMO,2BAA2BtB,EAAEuB,kBAAkB,CAAC,QAAQ;IAC5DvB,EAAEwB,MAAM,CAAC;QACP,GAAGX,mBAAmB;QACtBY,UAAUzB,EAAE0B,QAAQ,CAACzB;QACrB0B,MAAM3B,EAAE4B,OAAO,CAAC;IAClB;IACA5B,EAAEwB,MAAM,CAAC;QAAC,GAAGX,mBAAmB;QAAEY,UAAUzB,EAAE6B,IAAI;QAAIF,MAAM3B,EAAE4B,OAAO,CAAC;IAAQ;IAC9E5B,EAAEwB,MAAM,CAAC;QAAC,GAAGX,mBAAmB;QAAEY,UAAUzB,EAAE6B,IAAI;QAAIF,MAAM3B,EAAE4B,OAAO,CAAC;IAAe;IACrF5B,EAAEwB,MAAM,CAAC;QACP,GAAGX,mBAAmB;QACtBY,UAAUvB;QACVyB,MAAM3B,EAAE4B,OAAO,CAAC;IAClB;IACA5B,EAAEwB,MAAM,CAAC;QAAC,GAAGX,mBAAmB;QAAEY,UAAUzB,EAAE6B,IAAI;QAAIF,MAAM3B,EAAE4B,OAAO,CAAC;IAAS;CAChF;AAED,MAAME,0BAA0B9B,EAAEwB,MAAM,CAAC;IACvC;;;;GAIC,GACDO,SAAS/B,EAAEqB,QAAQ,CACjBrB,EAAEgC,KAAK,CACLhC,EAAEwB,MAAM,CAAC;QACP,gEAAgE;QAChES,SAASjC,EAAEqB,QAAQ,CAACrB,EAAEe,MAAM;QAC5BmB,QAAQlC,EAAEgC,KAAK,CACbhC,EAAEwB,MAAM,CAAC;YACPP,MAAMjB,EAAEe,MAAM;YACdoB,QAAQnC,EAAEqB,QAAQ,CAACrB,EAAEoC,OAAO;YAC5BlB,KAAKlB,EAAEe,MAAM;YACbI,OAAOnB,EAAEe,MAAM;QACjB;QAEF,oEAAoE;QACpE,uBAAuB;QACvBD,IAAId,EAAEe,MAAM;QACZ,wEAAwE;QACxE,kDAAkD;QAClDsB,YAAYrC,EAAEqB,QAAQ,CAACrB,EAAEe,MAAM;QAC/B,mEAAmE;QACnE,qEAAqE;QACrEK,SAASpB,EAAEe,MAAM;IACnB;IAGJuB,MAAMtC,EAAEe,MAAM;IACdD,IAAId,EAAEqB,QAAQ,CAACrB,EAAEe,MAAM;IACvBwB,YAAYvC,EAAEqB,QAAQ,CAACrB,EAAEgC,KAAK,CAACV;IAC/B;;;;GAIC,GACDkB,UAAUxC,EAAEqB,QAAQ,CAACrB,EAAEyC,KAAK,CAAC;QAAC3C;QAAsBF;KAAsB;IAC1E;;;;GAIC,GACD8C,mBAAmB1C,EAAEqB,QAAQ,CAACrB,EAAEe,MAAM;IACtCL,KAAKV,EAAE2C,MAAM;IACbC,MAAM5C,EAAE2C,MAAM;IACdE,WAAW7C,EAAEqB,QAAQ,CAACrB,EAAEe,MAAM;IAC9B+B,WAAW9C,EAAEe,MAAM;IACnBY,MAAM3B,EAAE+C,IAAI,CAAC;QAAC;QAAW;KAAS;IAClC3B,SAASpB,EAAE4B,OAAO,CAACrB;IACnByC,SAAShD,EAAEe,MAAM;AACnB;AAUA;;;CAGC,GACD,SAASkC;IACP,OAAOtD,KAAKE,oBAAoB;AAClC;AAEA,iFAAiF;AACjF,kEAAkE;AAClE,MAAMqD,eAAe,IAAIC;AACzB,IAAIC,wBAAwB;AAE5B,SAASC;IACP,KAAK,MAAMC,WAAWJ,aAAcI;AACtC;AAEA;oCACoC,GACpC,OAAO,MAAMC,mCAAmCF,gBAAe;AAE/D;;;;;;;;CAQC,GACD,SAASG,oBAAoBC,QAAgB,EAAEC,SAAwB;IACrE,MAAMJ,UAAU;QACd,IAAI,CAACI,aAAa;QAClB,IAAI;YACFlE,WAAWiE;QACb,EAAE,OAAM;QACN,0DAA0D;QAC5D;IACF;IACAP,aAAaS,GAAG,CAACL;IAEjB,IAAI,CAACF,uBAAuB;QAC1BA,wBAAwB;QACxB3C,QAAQmD,IAAI,CAAC,QAAQP;IACvB;IAEA,OAAO,IAAMH,aAAaW,MAAM,CAACP;AACnC;AAaA;;;;;CAKC,GACD,OAAO,SAASQ,kBACdtB,QAAkE;IAElE,MAAMuB,cAAcd;IACpB5D,UAAU0E,aAAa;QAACC,WAAW;IAAI;IAEvC,IAAIC,UAA6B;QAC/B,GAAGzB,QAAQ;QACX9B,KAAKD,QAAQC,GAAG;QAChBoC,WAAWtC;QACXY,SAASb;IACX;IAEA,MAAMkD,WAAW9D,KAAKoE,aAAa,GAAGtD,QAAQC,GAAG,CAAC,KAAK,CAAC;IACxDhB,cAAc+D,UAAUS,KAAKC,SAAS,CAACF,SAAS,MAAM;IAEtD,qEAAqE;IACrE,wEAAwE;IACxE,wEAAwE;IACxE,IAAIG,WAAW;IAEf,gEAAgE;IAChE,MAAMC,oBAAoBb,oBAAoBC,UAAU,IAAM,CAACW;IAE/D,OAAO;QACLE;YACEF,WAAW;YACXC;YACA,IAAI;gBACF7E,WAAWiE;YACb,EAAE,OAAM;YACN,sCAAsC;YACxC;QACF;QACAc,QAAOC,KAAK;YACV,IAAIJ,UAAU;YACdH,UAAU;gBAAC,GAAGA,OAAO;gBAAE,GAAGO,KAAK;YAAA;YAC/B9E,cAAc+D,UAAUS,KAAKC,SAAS,CAACF,SAAS,MAAM;QACxD;IACF;AACF;AAEA;;;CAGC,GACD,OAAO,SAASQ;IACd,MAAMV,cAAcd;IAEpB,IAAI,CAAC7D,WAAW2E,cAAc;QAC5B,OAAO,EAAE;IACX;IAEA,MAAMW,QAAQpF,YAAYyE,aAAaY,MAAM,CAAC,CAACC,IAAMA,EAAEC,QAAQ,CAAC;IAChE,MAAMC,UAA+B,EAAE;IAEvC,KAAK,MAAMC,QAAQL,MAAO;QACxB,MAAMjB,WAAW9D,KAAKoE,aAAagB;QACnC,IAAIC;QACJ,IAAI;YACFA,MAAMd,KAAKe,KAAK,CAAC1F,aAAakE,UAAU;QAC1C,EAAE,OAAM;YACN;QACF;QAEA,MAAM,EAACyB,IAAI,EAAEC,OAAO,EAAC,GAAGrD,wBAAwBsD,SAAS,CAACJ;QAC1D,IAAI,CAACG,SAAS;QAEd,IAAI9E,aAAa6E,KAAKxE,GAAG,EAAEwE,KAAKpC,SAAS,GAAG;YAC1CgC,QAAQO,IAAI,CAACH;QACf,OAAO;YACL,IAAI;gBACF1F,WAAWiE;YACb,EAAE,OAAM;YACN,0DAA0D;YAC5D;QACF;IACF;IAEA,OAAOqB;AACT;AAMA;;;;;;CAMC,GACD,OAAO,SAASQ,cAAcC,QAAgD;IAC5E,MAAMxB,cAAcd;IACpB5D,UAAU0E,aAAa;QAACC,WAAW;IAAI;IAEvC,4EAA4E;IAC5E,+CAA+C;IAC/C,MAAMwB,WAAWrF,qBAAqB4D;IAEtC,IAAI0B;IAEJ,MAAMC,SAAS;QACbC,aAAaF;QACbA,gBAAgBG,WAAW;YACzBL,SAASd;QACX,GAAG;IACL;IAEA,MAAMoB,UAAUpG,MAAM+F,UAAUE;IAEhC,OAAO;QACLI;YACEH,aAAaF;YACbI,QAAQC,KAAK;QACf;IACF;AACF;AAEA,gFAAgF;AAChF,2EAA2E;AAC3E,+EAA+E;AAE/E,MAAMC,sBAAsB/F,EAAEwB,MAAM,CAAC;IACnCc,MAAMtC,EAAEe,MAAM;IACdL,KAAKV,EAAE2C,MAAM;IACbC,MAAM5C,EAAE2C,MAAM;IACdG,WAAW9C,EAAEe,MAAM;IACnBK,SAASpB,EAAE4B,OAAO,CAACrB;AACrB;AAEA;;;CAGC,GACD,OAAO,SAASyF;IACd,MAAMC,WAAWtG,KAAKsD,kBAAkB;IAExC,IAAIiD;IACJ,IAAI;QACFA,WAAW3G,aAAa0G,UAAU;IACpC,EAAE,OAAM;QACN,2DAA2D;QAC3D,OAAOE;IACT;IAEA,0EAA0E;IAC1E,2EAA2E;IAC3E,wEAAwE;IACxE,wEAAwE;IACxE,MAAMjB,OAAOkB,kBAAkBF;IAC/B5F,SAAS,2BAA2B4E;IACpC,IAAIA,QAAQ7E,aAAa6E,KAAKxE,GAAG,EAAEwE,KAAKpC,SAAS,GAAG;QAClDxC,SAAS,mDAAmD4E,KAAKxE,GAAG,EAAEwE,KAAKtC,IAAI;QAC/E,OAAOsC;IACT;IAEAmB,mBAAmBJ;IACnB,OAAOE;AACT;AAEA,SAASC,kBAAkBF,QAAgB;IACzC,IAAI;QACF,MAAM,EAAChB,IAAI,EAAEC,OAAO,EAAC,GAAGY,oBAAoBX,SAAS,CAAClB,KAAKe,KAAK,CAACiB;QACjE,OAAOf,UAAUD,OAAOiB;IAC1B,EAAE,OAAM;QACN,OAAOA;IACT;AACF;AAEA,SAASE,mBAAmBJ,QAAgB;IAC1C,IAAI;QACF3F,SAAS;QACTd,WAAWyG;QACX3F,SAAS;IACX,EAAE,OAAM;IACN,iDAAiD;IACnD;AACF;AASA;;;;;;;;;;;CAWC,GACD,OAAO,SAASgG,qBACdC,IAAkC,EAClCC,UAAU,CAAC;IAEX,MAAMzC,cAAcd;IACpB5D,UAAU0E,aAAa;QAACC,WAAW;IAAI;IAEvC,MAAMiC,WAAWtG,KAAKoE,aAAa;IACnC,MAAMjB,YAAYtC;IAClB,MAAMiG,WAAW;QACfnE,MAAMiE,KAAKjE,IAAI;QACf5B,KAAKD,QAAQC,GAAG;QAChBkC,MAAM2D,KAAK3D,IAAI;QACfE;QACA1B,SAASb;IACX;IAEAD,SAAS,kCAAkC2F;IAE3C,IAAI;QACFvG,cAAcuG,UAAU/B,KAAKC,SAAS,CAACsC,WAAW;YAACC,MAAM;QAAI;QAC7DpG,SAAS;QAET,IAAI8D,WAAW;QACf,8EAA8E;QAC9E,kDAAkD;QAClD,MAAMC,oBAAoBb,oBAAoByC,UAAU;YACtD,IAAI7B,UAAU,OAAO;YACrB,IAAI;gBACF,MAAMuC,OAAOP,kBAAkB7G,aAAa0G,UAAU;gBACtD,OAAOU,MAAMjG,QAAQD,QAAQC,GAAG,IAAIiG,KAAK7D,SAAS,KAAKA;YACzD,EAAE,OAAM;gBACN,OAAO;YACT;QACF;QAEA,OAAO;YACLwB;gBACEF,WAAW;gBACXC;gBACA,IAAI;oBACF7E,WAAWyG;gBACb,EAAE,OAAM;gBACN,qBAAqB;gBACvB;YACF;YACAW,YAAWhE,IAAY;gBACrBlD,cAAcuG,UAAU/B,KAAKC,SAAS,CAAC;oBAAC,GAAGsC,QAAQ;oBAAE7D;gBAAI;YAC3D;QACF;IACF,EAAE,OAAOiE,KAAc;QACrBvG,SACE,wCACAuG,eAAeC,QAAQD,IAAIE,OAAO,GAAGC,OAAOH;QAE9C,IAAI,CAACI,YAAYJ,QAAQA,IAAIK,IAAI,KAAK,UAAU,OAAOf;QAEvD,mDAAmD;QACnD,MAAMgB,WAAWnB;QACjB,IAAImB,UAAU,OAAOhB;QAErB,6FAA6F;QAC7F,IAAIK,WAAW,GAAG,OAAOL;QACzB,OAAOG,qBAAqBC,MAAMC,UAAU;IAC9C;AACF;AAEA,SAASS,YAAYJ,GAAY;IAC/B,OAAOA,eAAeC,SAAS,UAAUD;AAC3C"}
|
|
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, TileInterfaceMetadataSchema} 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('asset_source')}),\n z.object({\n ...interfaceBaseFields,\n metadata: TileInterfaceMetadataSchema,\n type: z.literal('tile'),\n }),\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` slug — the module-federation alias the\n // workbench loads this config's live values from.\n moduleName: z.optional(z.string()),\n // The version the workbench federates this config's module under —\n // a string, like the one Brett returns on a deployed `activeConfig`.\n version: z.string(),\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 * A config-only server carries configs but no interfaces — e.g. a\n * media-library config app under development. The workbench never routes it\n * as an app; only its configs are published. It therefore plays a different\n * role than an app server, and the two may share a slug (a config app\n * developed alongside the locally served singleton it configures).\n */\nexport function isConfigOnlyServer(\n server: Pick<DevServerManifest, 'configs' | 'interfaces'>,\n): boolean {\n return Boolean(server.configs?.length) && !server.interfaces?.length\n}\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","TileInterfaceMetadataSchema","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","host","interfaces","manifest","union","manifestUpdatedAt","number","port","projectId","startedAt","enum","workDir","isConfigOnlyServer","server","Boolean","length","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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"}
|
|
@@ -4,7 +4,7 @@ import { deriveInterfaces } from '../../deriveInterfaces.js';
|
|
|
4
4
|
import { formatWorkbenchAppErrors, validateWorkbenchApp } from '../../validateWorkbenchApp.js';
|
|
5
5
|
import { deriveConfigs } from './deriveConfigs.js';
|
|
6
6
|
import { trackExposesSet } from './exposesSetId.js';
|
|
7
|
-
import { getRegisteredServers, registerDevServer } from './registry.js';
|
|
7
|
+
import { getRegisteredServers, isConfigOnlyServer, registerDevServer } from './registry.js';
|
|
8
8
|
import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
9
9
|
/**
|
|
10
10
|
* Log any config validation errors without aborting. Unlike build and deploy,
|
|
@@ -15,6 +15,23 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
|
15
15
|
if (errors.length === 0) return;
|
|
16
16
|
output.warn(formatWorkbenchAppErrors(errors));
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A live server — other than this process — already playing the given role for
|
|
20
|
+
* the slug. Only a *same-role* duplicate is a conflict: a config-only server
|
|
21
|
+
* (configs, no interfaces — e.g. a media-library config app) is never routed
|
|
22
|
+
* as an app, so it may share a slug with the app server it configures. The
|
|
23
|
+
* workbench renders the app and publishes both servers' configs, and can
|
|
24
|
+
* always tell them apart.
|
|
25
|
+
*/ function findSameRoleConflict(id, configOnly) {
|
|
26
|
+
return getRegisteredServers().find((server)=>server.pid !== process.pid && server.id === id && isConfigOnlyServer(server) === configOnly);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Remedy line for a same-role slug conflict, phrased for the role. Changing the
|
|
30
|
+
* slug is only real advice for an app — a config app's slug is fixed by the
|
|
31
|
+
* app it configures (e.g. `unstable_defineMediaLibrary` hard-codes it).
|
|
32
|
+
*/ function conflictRemedy(configOnly) {
|
|
33
|
+
return configOnly ? 'Stop that server first.' : 'Stop that server, or give this app its own `slug` in sanity.cli.ts.';
|
|
34
|
+
}
|
|
18
35
|
/** The address the server actually bound — the live socket, which can differ from the configured port under non-strict ports. */ function serverAddress(server) {
|
|
19
36
|
const resolvedHost = server.config.server.host;
|
|
20
37
|
const addr = server.httpServer?.address();
|
|
@@ -38,15 +55,24 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
|
38
55
|
});
|
|
39
56
|
const configs = await deriveConfigs(cliConfig.app);
|
|
40
57
|
const id = isWorkbenchApp(cliConfig.app) ? cliConfig.app.slug : undefined;
|
|
41
|
-
const
|
|
58
|
+
const configOnly = isConfigOnlyServer({
|
|
59
|
+
configs,
|
|
60
|
+
interfaces
|
|
61
|
+
});
|
|
62
|
+
const devServer = id ? findSameRoleConflict(id, configOnly) : undefined;
|
|
42
63
|
if (id && devServer) {
|
|
43
|
-
|
|
64
|
+
const subject = configOnly ? `A config for "${id}"` : `The app "${id}"`;
|
|
65
|
+
output.error(`${subject} is already served by another dev server running on port ${devServer.port}, ` + "so the workbench can't tell them apart and this one stays out of it. " + conflictRemedy(configOnly), {
|
|
44
66
|
exit: false
|
|
45
67
|
});
|
|
46
68
|
return {
|
|
47
69
|
close: async ()=>{}
|
|
48
70
|
};
|
|
49
71
|
}
|
|
72
|
+
// The role the registry currently advertises for this server; a config edit
|
|
73
|
+
// can flip it (see the re-check in `update`). Committed only after a
|
|
74
|
+
// successful registry patch, so a failed pass re-checks on the next save.
|
|
75
|
+
let registeredConfigOnly = configOnly;
|
|
50
76
|
const registration = registerDevServer({
|
|
51
77
|
configs,
|
|
52
78
|
host: appHost,
|
|
@@ -83,11 +109,31 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
|
83
109
|
],
|
|
84
110
|
output,
|
|
85
111
|
update: async (patch)=>{
|
|
112
|
+
// A save can flip the server's role — e.g. a config-only app gaining an
|
|
113
|
+
// `entry` becomes app-role — so re-run the same-role collision check the
|
|
114
|
+
// registration gate applied, or the flip would quietly reintroduce the
|
|
115
|
+
// ambiguity (two app-role servers on one slug). The patch is skipped, not
|
|
116
|
+
// fatal: the registry keeps the previous shape and the next save retries.
|
|
117
|
+
const nextConfigOnly = isConfigOnlyServer({
|
|
118
|
+
configs: patch.configs,
|
|
119
|
+
interfaces: patch.interfaces
|
|
120
|
+
});
|
|
121
|
+
if (id && nextConfigOnly !== registeredConfigOnly) {
|
|
122
|
+
const conflict = findSameRoleConflict(id, nextConfigOnly);
|
|
123
|
+
if (conflict) {
|
|
124
|
+
const subject = nextConfigOnly ? `a config for "${id}"` : `the app "${id}"`;
|
|
125
|
+
output.error(`This change makes this dev server serve ${subject} like the dev server running on ` + `port ${conflict.port} already does, so the workbench couldn't tell them apart — ` + `keeping the previous registration. ${conflictRemedy(nextConfigOnly)}`, {
|
|
126
|
+
exit: false
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
86
131
|
if (!exposesSet.changed({
|
|
87
132
|
configs: patch.configs,
|
|
88
133
|
interfaces: patch.interfaces
|
|
89
134
|
})) {
|
|
90
135
|
registration.update(patch);
|
|
136
|
+
registeredConfigOnly = nextConfigOnly;
|
|
91
137
|
return;
|
|
92
138
|
}
|
|
93
139
|
// Rebuild the remote *before* patching the registry — the patch reloads the
|
|
@@ -103,6 +149,7 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
|
103
149
|
...patch,
|
|
104
150
|
...serverAddress(rebuiltServer)
|
|
105
151
|
} : patch);
|
|
152
|
+
registeredConfigOnly = nextConfigOnly;
|
|
106
153
|
},
|
|
107
154
|
workDir
|
|
108
155
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/actions/dev/startDevServerRegistration.ts"],"sourcesContent":["import {type CliConfig, getCliConfigUncached, type Output} from '@sanity/cli-core'\nimport {type ViteDevServer} from 'vite'\n\nimport {isWorkbenchApp} from '../../defineApp.js'\nimport {deriveInterfaces} from '../../deriveInterfaces.js'\nimport {formatWorkbenchAppErrors, validateWorkbenchApp} from '../../validateWorkbenchApp.js'\nimport {deriveConfigs} from './deriveConfigs.js'\nimport {trackExposesSet} from './exposesSetId.js'\nimport {type DevServerManifest, getRegisteredServers, registerDevServer} from './registry.js'\nimport {startDevManifestWatcher} from './startDevManifestWatcher.js'\n\ninterface DevServerRegistrationOptions {\n cliConfig: CliConfig\n /**\n * Extract the project manifest to inline into the registry. The caller owns the\n * studio-vs-app split (manifest formats are CLI-domain); registration re-derives\n * the interface set alongside it.\n */\n extractManifest: (params: {\n configPath: string\n workDir: string\n }) => Promise<DevServerManifest['manifest']>\n isApp: boolean\n output: Output\n server: ViteDevServer\n workDir: string\n\n /**\n * Rebuild the app's federation remote when its interface set changes, awaited\n * *before* the registry patch — the patch reloads the workbench page, which must\n * re-fetch a remote that already exposes the new interface. Resolves with the\n * recreated server so the entry gets its actual address (non-strict ports may\n * shift it); must reject if the restart produces no server, so the set stays\n * uncommitted and the next save retries instead of advertising a dead port.\n */\n onInterfaceSetChange?: () => Promise<ViteDevServer>\n}\n\ninterface DevServerRegistrationHandle {\n close: () => Promise<void>\n}\n\n/**\n * Log any config validation errors without aborting. Unlike build and deploy,\n * dev stays up on an invalid config so the author sees the errors and fixes them\n * live on the next save.\n */\nfunction reportConfigErrors(app: CliConfig['app'], output: Output): void {\n const errors = validateWorkbenchApp(app)\n if (errors.length === 0) return\n output.warn(formatWorkbenchAppErrors(errors))\n}\n\n/** The address the server actually bound — the live socket, which can differ from the configured port under non-strict ports. */\nfunction serverAddress(server: ViteDevServer) {\n const resolvedHost = server.config.server.host\n const addr = server.httpServer?.address()\n return {\n host: typeof resolvedHost === 'string' ? resolvedHost : 'localhost',\n port: typeof addr === 'object' && addr ? addr.port : server.config.server.port,\n }\n}\n\n/**\n * Register the dev server in the registry and watch its config for manifest +\n * interface changes. The workbench reads the entry to locate and render the\n * server; the watcher keeps it current as `sanity.cli.ts` is edited.\n */\nexport async function startDevServerRegistration(\n options: DevServerRegistrationOptions,\n): Promise<DevServerRegistrationHandle> {\n const {cliConfig, extractManifest, isApp, onInterfaceSetChange, output, server, workDir} = options\n\n const {host: appHost, port: appPort} = serverAddress(server)\n\n reportConfigErrors(cliConfig.app, output)\n\n // Forwarded alongside (not inside) the manifest so the workbench renders local\n // panels/workers and reads the configs without a deploy.\n const interfaces = deriveInterfaces(cliConfig.app, {isApp})\n const configs = await deriveConfigs(cliConfig.app)\n\n const id = isWorkbenchApp(cliConfig.app) ? cliConfig.app.slug : undefined\n\n const devServer = id ? getRegisteredServers().find((server) => server.id === id) : undefined\n\n if (id && devServer) {\n output.error(\n `The app \"${id}\" is already served by another dev server running on port ${devServer.port}, ` +\n \"so the workbench can't tell them apart and this one stays out of it. \" +\n 'Stop that server, or give this app its own `slug` in sanity.cli.ts.',\n {exit: false},\n )\n return {close: async () => {}}\n }\n\n const registration = registerDevServer({\n configs,\n host: appHost,\n id,\n interfaces,\n port: appPort,\n projectId: cliConfig?.api?.projectId,\n type: isApp ? 'coreApp' : 'studio',\n workDir,\n })\n\n const exposesSet = trackExposesSet({configs, interfaces})\n\n const watcher = await startDevManifestWatcher({\n // Re-derive every pass (don't omit): the registry patch is a shallow merge,\n // so omitting would wipe the registered set.\n extract: async (params) => {\n const app = (await getCliConfigUncached(params.workDir)).app\n reportConfigErrors(app, output)\n return {\n configs: await deriveConfigs(app),\n interfaces: deriveInterfaces(app, {isApp}),\n manifest: await extractManifest(params),\n }\n },\n // A studio's root resolves to `sanity.config.*` but its interfaces live in\n // `sanity.cli.*` — watch that too. Apps already root at `sanity.cli.*`.\n extraWatchFilenames: isApp ? undefined : ['sanity.cli.js', 'sanity.cli.ts'],\n output,\n update: async (patch) => {\n if (\n !exposesSet.changed({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n ) {\n registration.update(patch)\n return\n }\n // Rebuild the remote *before* patching the registry — the patch reloads the\n // page, which must re-fetch a remote that already exposes the new interface.\n const rebuiltServer = await onInterfaceSetChange?.()\n // Commit only after a successful rebuild, so a thrown one retries next pass.\n exposesSet.commit({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n // The recreated server can bind a different port (non-strict ports).\n registration.update(rebuiltServer ? {...patch, ...serverAddress(rebuiltServer)} : patch)\n },\n workDir,\n })\n\n return {\n close: async () => {\n registration.release()\n await watcher.close()\n },\n }\n}\n"],"names":["getCliConfigUncached","isWorkbenchApp","deriveInterfaces","formatWorkbenchAppErrors","validateWorkbenchApp","deriveConfigs","trackExposesSet","getRegisteredServers","registerDevServer","startDevManifestWatcher","reportConfigErrors","app","output","errors","length","warn","serverAddress","server","resolvedHost","config","host","addr","httpServer","address","port","startDevServerRegistration","options","cliConfig","extractManifest","isApp","onInterfaceSetChange","workDir","appHost","appPort","interfaces","configs","id","slug","undefined","devServer","find","error","exit","close","registration","projectId","api","type","exposesSet","watcher","extract","params","manifest","extraWatchFilenames","update","patch","changed","rebuiltServer","commit","release"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/startDevServerRegistration.ts"],"sourcesContent":["import {type CliConfig, getCliConfigUncached, type Output} from '@sanity/cli-core'\nimport {type ViteDevServer} from 'vite'\n\nimport {isWorkbenchApp} from '../../defineApp.js'\nimport {deriveInterfaces} from '../../deriveInterfaces.js'\nimport {formatWorkbenchAppErrors, validateWorkbenchApp} from '../../validateWorkbenchApp.js'\nimport {deriveConfigs} from './deriveConfigs.js'\nimport {trackExposesSet} from './exposesSetId.js'\nimport {\n type DevServerManifest,\n getRegisteredServers,\n isConfigOnlyServer,\n registerDevServer,\n} from './registry.js'\nimport {startDevManifestWatcher} from './startDevManifestWatcher.js'\n\ninterface DevServerRegistrationOptions {\n cliConfig: CliConfig\n /**\n * Extract the project manifest to inline into the registry. The caller owns the\n * studio-vs-app split (manifest formats are CLI-domain); registration re-derives\n * the interface set alongside it.\n */\n extractManifest: (params: {\n configPath: string\n workDir: string\n }) => Promise<DevServerManifest['manifest']>\n isApp: boolean\n output: Output\n server: ViteDevServer\n workDir: string\n\n /**\n * Rebuild the app's federation remote when its interface set changes, awaited\n * *before* the registry patch — the patch reloads the workbench page, which must\n * re-fetch a remote that already exposes the new interface. Resolves with the\n * recreated server so the entry gets its actual address (non-strict ports may\n * shift it); must reject if the restart produces no server, so the set stays\n * uncommitted and the next save retries instead of advertising a dead port.\n */\n onInterfaceSetChange?: () => Promise<ViteDevServer>\n}\n\ninterface DevServerRegistrationHandle {\n close: () => Promise<void>\n}\n\n/**\n * Log any config validation errors without aborting. Unlike build and deploy,\n * dev stays up on an invalid config so the author sees the errors and fixes them\n * live on the next save.\n */\nfunction reportConfigErrors(app: CliConfig['app'], output: Output): void {\n const errors = validateWorkbenchApp(app)\n if (errors.length === 0) return\n output.warn(formatWorkbenchAppErrors(errors))\n}\n\n/**\n * A live server — other than this process — already playing the given role for\n * the slug. Only a *same-role* duplicate is a conflict: a config-only server\n * (configs, no interfaces — e.g. a media-library config app) is never routed\n * as an app, so it may share a slug with the app server it configures. The\n * workbench renders the app and publishes both servers' configs, and can\n * always tell them apart.\n */\nfunction findSameRoleConflict(id: string, configOnly: boolean): DevServerManifest | undefined {\n return getRegisteredServers().find(\n (server) =>\n server.pid !== process.pid && server.id === id && isConfigOnlyServer(server) === configOnly,\n )\n}\n\n/**\n * Remedy line for a same-role slug conflict, phrased for the role. Changing the\n * slug is only real advice for an app — a config app's slug is fixed by the\n * app it configures (e.g. `unstable_defineMediaLibrary` hard-codes it).\n */\nfunction conflictRemedy(configOnly: boolean): string {\n return configOnly\n ? 'Stop that server first.'\n : 'Stop that server, or give this app its own `slug` in sanity.cli.ts.'\n}\n\n/** The address the server actually bound — the live socket, which can differ from the configured port under non-strict ports. */\nfunction serverAddress(server: ViteDevServer) {\n const resolvedHost = server.config.server.host\n const addr = server.httpServer?.address()\n return {\n host: typeof resolvedHost === 'string' ? resolvedHost : 'localhost',\n port: typeof addr === 'object' && addr ? addr.port : server.config.server.port,\n }\n}\n\n/**\n * Register the dev server in the registry and watch its config for manifest +\n * interface changes. The workbench reads the entry to locate and render the\n * server; the watcher keeps it current as `sanity.cli.ts` is edited.\n */\nexport async function startDevServerRegistration(\n options: DevServerRegistrationOptions,\n): Promise<DevServerRegistrationHandle> {\n const {cliConfig, extractManifest, isApp, onInterfaceSetChange, output, server, workDir} = options\n\n const {host: appHost, port: appPort} = serverAddress(server)\n\n reportConfigErrors(cliConfig.app, output)\n\n // Forwarded alongside (not inside) the manifest so the workbench renders local\n // panels/workers and reads the configs without a deploy.\n const interfaces = deriveInterfaces(cliConfig.app, {isApp})\n const configs = await deriveConfigs(cliConfig.app)\n\n const id = isWorkbenchApp(cliConfig.app) ? cliConfig.app.slug : undefined\n\n const configOnly = isConfigOnlyServer({configs, interfaces})\n const devServer = id ? findSameRoleConflict(id, configOnly) : undefined\n\n if (id && devServer) {\n const subject = configOnly ? `A config for \"${id}\"` : `The app \"${id}\"`\n output.error(\n `${subject} is already served by another dev server running on port ${devServer.port}, ` +\n \"so the workbench can't tell them apart and this one stays out of it. \" +\n conflictRemedy(configOnly),\n {exit: false},\n )\n return {close: async () => {}}\n }\n\n // The role the registry currently advertises for this server; a config edit\n // can flip it (see the re-check in `update`). Committed only after a\n // successful registry patch, so a failed pass re-checks on the next save.\n let registeredConfigOnly = configOnly\n\n const registration = registerDevServer({\n configs,\n host: appHost,\n id,\n interfaces,\n port: appPort,\n projectId: cliConfig?.api?.projectId,\n type: isApp ? 'coreApp' : 'studio',\n workDir,\n })\n\n const exposesSet = trackExposesSet({configs, interfaces})\n\n const watcher = await startDevManifestWatcher({\n // Re-derive every pass (don't omit): the registry patch is a shallow merge,\n // so omitting would wipe the registered set.\n extract: async (params) => {\n const app = (await getCliConfigUncached(params.workDir)).app\n reportConfigErrors(app, output)\n return {\n configs: await deriveConfigs(app),\n interfaces: deriveInterfaces(app, {isApp}),\n manifest: await extractManifest(params),\n }\n },\n // A studio's root resolves to `sanity.config.*` but its interfaces live in\n // `sanity.cli.*` — watch that too. Apps already root at `sanity.cli.*`.\n extraWatchFilenames: isApp ? undefined : ['sanity.cli.js', 'sanity.cli.ts'],\n output,\n update: async (patch) => {\n // A save can flip the server's role — e.g. a config-only app gaining an\n // `entry` becomes app-role — so re-run the same-role collision check the\n // registration gate applied, or the flip would quietly reintroduce the\n // ambiguity (two app-role servers on one slug). The patch is skipped, not\n // fatal: the registry keeps the previous shape and the next save retries.\n const nextConfigOnly = isConfigOnlyServer({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n if (id && nextConfigOnly !== registeredConfigOnly) {\n const conflict = findSameRoleConflict(id, nextConfigOnly)\n if (conflict) {\n const subject = nextConfigOnly ? `a config for \"${id}\"` : `the app \"${id}\"`\n output.error(\n `This change makes this dev server serve ${subject} like the dev server running on ` +\n `port ${conflict.port} already does, so the workbench couldn't tell them apart — ` +\n `keeping the previous registration. ${conflictRemedy(nextConfigOnly)}`,\n {exit: false},\n )\n return\n }\n }\n\n if (\n !exposesSet.changed({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n ) {\n registration.update(patch)\n registeredConfigOnly = nextConfigOnly\n return\n }\n // Rebuild the remote *before* patching the registry — the patch reloads the\n // page, which must re-fetch a remote that already exposes the new interface.\n const rebuiltServer = await onInterfaceSetChange?.()\n // Commit only after a successful rebuild, so a thrown one retries next pass.\n exposesSet.commit({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n // The recreated server can bind a different port (non-strict ports).\n registration.update(rebuiltServer ? {...patch, ...serverAddress(rebuiltServer)} : patch)\n registeredConfigOnly = nextConfigOnly\n },\n workDir,\n })\n\n return {\n close: async () => {\n registration.release()\n await watcher.close()\n },\n }\n}\n"],"names":["getCliConfigUncached","isWorkbenchApp","deriveInterfaces","formatWorkbenchAppErrors","validateWorkbenchApp","deriveConfigs","trackExposesSet","getRegisteredServers","isConfigOnlyServer","registerDevServer","startDevManifestWatcher","reportConfigErrors","app","output","errors","length","warn","findSameRoleConflict","id","configOnly","find","server","pid","process","conflictRemedy","serverAddress","resolvedHost","config","host","addr","httpServer","address","port","startDevServerRegistration","options","cliConfig","extractManifest","isApp","onInterfaceSetChange","workDir","appHost","appPort","interfaces","configs","slug","undefined","devServer","subject","error","exit","close","registeredConfigOnly","registration","projectId","api","type","exposesSet","watcher","extract","params","manifest","extraWatchFilenames","update","patch","nextConfigOnly","conflict","changed","rebuiltServer","commit","release"],"mappings":"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"}
|
|
@@ -5,16 +5,13 @@ import { createServer } from 'vite';
|
|
|
5
5
|
import { z } from 'zod/mini';
|
|
6
6
|
import { isWorkbenchApp } from '../../defineApp.js';
|
|
7
7
|
import { createExposesTracker } from './exposesSetId.js';
|
|
8
|
-
import { acquireWorkbenchLock, getRegisteredServers, readWorkbenchLock, watchRegistry } from './registry.js';
|
|
8
|
+
import { acquireWorkbenchLock, getRegisteredServers, isConfigOnlyServer, readWorkbenchLock, watchRegistry } from './registry.js';
|
|
9
9
|
import { writeWorkbenchRuntime } from './writeWorkbenchRuntime.js';
|
|
10
10
|
const devDebug = subdebug('dev');
|
|
11
11
|
const noop = async ()=>{};
|
|
12
|
-
// Every server is a local app except a config-only one —
|
|
12
|
+
// Every server is a local app except a config-only one — a config
|
|
13
13
|
// with no interfaces (the media library). A server with both lands in both channels.
|
|
14
|
-
const isLocalApp = (server)
|
|
15
|
-
const configOnly = Boolean(server.configs?.length) && !server.interfaces?.length;
|
|
16
|
-
return !configOnly;
|
|
17
|
-
};
|
|
14
|
+
const isLocalApp = (server)=>!isConfigOnlyServer(server);
|
|
18
15
|
const toApplicationsPayload = (servers)=>({
|
|
19
16
|
applications: servers.filter((server)=>isLocalApp(server)).map(({ host, id, interfaces, manifest, port, projectId, type })=>({
|
|
20
17
|
host,
|