@sanity/workbench-cli 1.1.0 → 1.1.1
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/dev.d.ts +54 -98
- package/dist/_exports/dev.js +1 -8
- package/dist/_exports/dev.js.map +1 -1
- package/dist/actions/dev/appServerSupervisor.js +66 -0
- package/dist/actions/dev/appServerSupervisor.js.map +1 -0
- package/dist/actions/dev/deriveInterfaces.js +34 -0
- package/dist/actions/dev/deriveInterfaces.js.map +1 -0
- package/dist/actions/dev/interfaceSetId.js +51 -0
- package/dist/actions/dev/interfaceSetId.js.map +1 -0
- package/dist/actions/dev/startDevManifestWatcher.js +104 -0
- package/dist/actions/dev/startDevManifestWatcher.js.map +1 -0
- package/dist/actions/dev/startDevServerRegistration.js +78 -0
- package/dist/actions/dev/startDevServerRegistration.js.map +1 -0
- package/dist/actions/dev/startWorkbenchDev.js +152 -0
- package/dist/actions/dev/startWorkbenchDev.js.map +1 -0
- package/dist/actions/dev/startWorkbenchDevServer.js +289 -0
- package/dist/actions/dev/startWorkbenchDevServer.js.map +1 -0
- package/dist/actions/dev/writeWorkbenchRuntime.js +67 -0
- package/dist/actions/dev/writeWorkbenchRuntime.js.map +1 -0
- package/package.json +5 -4
package/dist/_exports/dev.d.ts
CHANGED
|
@@ -1,40 +1,23 @@
|
|
|
1
|
+
import { CliConfig } from "@sanity/cli-core";
|
|
2
|
+
import { Output } from "@sanity/cli-core";
|
|
3
|
+
import { ViteDevServer } from "vite";
|
|
1
4
|
import { z } from "zod/mini";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* The lock stores `{pid, host, port}` so other processes can find the
|
|
9
|
-
* running workbench. Call `updatePort` after the Vite server starts to
|
|
10
|
-
* write the actual port (Vite may pick a different one).
|
|
11
|
-
*
|
|
12
|
-
* @returns A {@link WorkbenchLock} if acquired, or `undefined` if another
|
|
13
|
-
* live process already holds it.
|
|
14
|
-
*/
|
|
15
|
-
export declare function acquireWorkbenchLock(
|
|
16
|
-
info: {
|
|
17
|
-
host: string;
|
|
18
|
-
port: number;
|
|
19
|
-
},
|
|
20
|
-
retries?: number,
|
|
21
|
-
): WorkbenchLock | undefined;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Resolve a directory to its canonical (long) form before handing it to
|
|
25
|
-
* `fs.watch`.
|
|
26
|
-
*
|
|
27
|
-
* On Windows, `fs.watch` aborts with a libuv assertion
|
|
28
|
-
* (`!_wcsnicmp(filename, dir, dirlen)` in `fs-event.c`) when the watched path
|
|
29
|
-
* is an 8.3 short name — e.g. temp dirs under `RUNNER~1` — because the OS
|
|
30
|
-
* reports long-form filenames that fail libuv's prefix check.
|
|
31
|
-
* `realpathSync.native` expands short names to their long form so the
|
|
32
|
-
* prefixes match.
|
|
33
|
-
*
|
|
34
|
-
* Falls back to the original path when it can't be resolved (e.g. it doesn't
|
|
35
|
-
* exist yet), which is no worse than watching it directly.
|
|
7
|
+
* Minimal shape the supervisor needs back from a started app/studio dev server.
|
|
8
|
+
* The not-started arm is reserved for expected early exits the server already
|
|
9
|
+
* reported (e.g. a missing organization id) — a failure to *boot* still throws.
|
|
36
10
|
*/
|
|
37
|
-
|
|
11
|
+
declare type AppServerResult =
|
|
12
|
+
| {
|
|
13
|
+
close: () => Promise<void>;
|
|
14
|
+
server: ViteDevServer;
|
|
15
|
+
started: true;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
reason: string;
|
|
19
|
+
started: false;
|
|
20
|
+
};
|
|
38
21
|
|
|
39
22
|
/**
|
|
40
23
|
* A manifest describing a running dev server process (studio or app).
|
|
@@ -43,7 +26,7 @@ export declare function canonicalizeWatchDir(dir: string): string;
|
|
|
43
26
|
* The workbench singleton is tracked separately via the lock file — see
|
|
44
27
|
* `acquireWorkbenchLock` and `readWorkbenchLock` below.
|
|
45
28
|
*/
|
|
46
|
-
|
|
29
|
+
declare type DevServerManifest = z.infer<typeof devServerManifestSchema>;
|
|
47
30
|
|
|
48
31
|
declare const devServerManifestSchema: z.ZodMiniObject<
|
|
49
32
|
{
|
|
@@ -93,74 +76,47 @@ declare const devServerManifestSchema: z.ZodMiniObject<
|
|
|
93
76
|
z.core.$strip
|
|
94
77
|
>;
|
|
95
78
|
|
|
96
|
-
declare interface DevServerRegistration {
|
|
97
|
-
/** Remove the registry entry. */
|
|
98
|
-
release: () => void;
|
|
99
|
-
/**
|
|
100
|
-
* Rewrite the registry entry with partial updates merged in. Also bumps the
|
|
101
|
-
* file's mtime, which fires `watchRegistry` in any workbench process and
|
|
102
|
-
* triggers a rebroadcast to connected clients.
|
|
103
|
-
*/
|
|
104
|
-
update: (
|
|
105
|
-
patch: Partial<Omit<DevServerManifest, "pid" | "startedAt" | "version">>,
|
|
106
|
-
) => void;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Read all manifest files from the registry, prune stale entries (dead PIDs),
|
|
111
|
-
* and return the live ones.
|
|
112
|
-
*/
|
|
113
|
-
export declare function getRegisteredServers(): DevServerManifest[];
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Read the workbench lock file and return its contents if the holding
|
|
117
|
-
* process is still alive. Prunes stale locks from crashed processes.
|
|
118
|
-
*/
|
|
119
|
-
export declare function readWorkbenchLock():
|
|
120
|
-
| z.infer<typeof workbenchLockSchema>
|
|
121
|
-
| undefined;
|
|
122
|
-
|
|
123
79
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* any signal handler could fire.
|
|
128
|
-
*/
|
|
129
|
-
export declare function registerDevServer(
|
|
130
|
-
manifest: Omit<DevServerManifest, "pid" | "startedAt" | "version">,
|
|
131
|
-
): DevServerRegistration;
|
|
132
|
-
|
|
133
|
-
declare interface RegistryWatcher {
|
|
134
|
-
close(): void;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Watch the registry directory for changes and invoke the callback with the
|
|
139
|
-
* current list of live servers whenever a change is detected.
|
|
80
|
+
* Orchestrate the dev servers a workbench project needs: a singleton workbench
|
|
81
|
+
* Vite server plus the app/studio dev server it renders, wired to the dev-server
|
|
82
|
+
* registry so view/service edits re-sync live.
|
|
140
83
|
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
84
|
+
* A running workbench claims the configured port, so the app server binds the
|
|
85
|
+
* next one. If the workbench can't start (package or port unavailable), the app
|
|
86
|
+
* server falls back to the configured port and announces its own URL, exactly as
|
|
87
|
+
* a plain `sanity dev` would.
|
|
143
88
|
*/
|
|
144
|
-
export declare function
|
|
145
|
-
|
|
146
|
-
):
|
|
89
|
+
export declare function startWorkbenchDev(
|
|
90
|
+
options: StartWorkbenchDevOptions,
|
|
91
|
+
): Promise<{
|
|
92
|
+
close: () => Promise<void>;
|
|
93
|
+
}>;
|
|
147
94
|
|
|
148
|
-
declare interface
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
|
|
95
|
+
export declare interface StartWorkbenchDevOptions {
|
|
96
|
+
/** Resolved app id for the registry entry (the CLI owns id resolution). */
|
|
97
|
+
appId: string | undefined;
|
|
98
|
+
/** Directory for the workbench Vite server's dependency cache. */
|
|
99
|
+
cacheDir: string;
|
|
100
|
+
/** CLI-domain `app.id`/`deployment.appId` deprecation check, run before registering. */
|
|
101
|
+
checkForDeprecatedAppId: () => void;
|
|
102
|
+
cliConfig: CliConfig;
|
|
103
|
+
/** Extract the project manifest to inline into the registry (studio-vs-app handled by the CLI). */
|
|
104
|
+
extractManifest: (params: {
|
|
105
|
+
configPath: string;
|
|
106
|
+
workDir: string;
|
|
107
|
+
}) => Promise<DevServerManifest["manifest"]>;
|
|
108
|
+
httpHost: string | undefined;
|
|
109
|
+
httpPort: number;
|
|
110
|
+
isApp: boolean;
|
|
111
|
+
output: Output;
|
|
112
|
+
reactStrictMode: boolean;
|
|
113
|
+
/** Start the app/studio dev server — the CLI owns the server, this orchestrates it. */
|
|
114
|
+
startAppServer: (params: {
|
|
115
|
+
announceUrl: boolean;
|
|
116
|
+
cliConfig: CliConfig;
|
|
117
|
+
httpPort: number;
|
|
118
|
+
}) => Promise<AppServerResult>;
|
|
119
|
+
workDir: string;
|
|
153
120
|
}
|
|
154
121
|
|
|
155
|
-
declare const workbenchLockSchema: z.ZodMiniObject<
|
|
156
|
-
{
|
|
157
|
-
host: z.ZodMiniString<string>;
|
|
158
|
-
pid: z.ZodMiniNumber<number>;
|
|
159
|
-
port: z.ZodMiniNumber<number>;
|
|
160
|
-
startedAt: z.ZodMiniString<string>;
|
|
161
|
-
version: z.ZodMiniLiteral<1>;
|
|
162
|
-
},
|
|
163
|
-
z.core.$strip
|
|
164
|
-
>;
|
|
165
|
-
|
|
166
122
|
export {};
|
package/dist/_exports/dev.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
// orchestration drives. It tracks running studio/app dev servers (single
|
|
3
|
-
// instance via a lock + PID-liveness), and the workbench host watches it to
|
|
4
|
-
// render local panels/services without a deploy. The CLI owns the orchestration
|
|
5
|
-
// (starting servers, extracting manifests); this package owns the registry it
|
|
6
|
-
// registers into and watches.
|
|
7
|
-
export { canonicalizeWatchDir } from '../actions/dev/canonicalizeWatchDir.js';
|
|
8
|
-
export { acquireWorkbenchLock, getRegisteredServers, readWorkbenchLock, registerDevServer, watchRegistry } from '../actions/dev/registry.js';
|
|
1
|
+
export { startWorkbenchDev } from '../actions/dev/startWorkbenchDev.js';
|
|
9
2
|
|
|
10
3
|
//# sourceMappingURL=dev.js.map
|
package/dist/_exports/dev.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/_exports/dev.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../src/_exports/dev.ts"],"sourcesContent":["export {startWorkbenchDev, type StartWorkbenchDevOptions} from '../actions/dev/startWorkbenchDev.js'\n"],"names":["startWorkbenchDev"],"mappings":"AAAA,SAAQA,iBAAiB,QAAsC,sCAAqC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getCliConfigUncached } from '@sanity/cli-core';
|
|
2
|
+
const noop = async ()=>{};
|
|
3
|
+
/**
|
|
4
|
+
* Own the app/studio dev server's lifecycle behind the dev-server registry seam.
|
|
5
|
+
*
|
|
6
|
+
* Adding or removing a view/service rebuilds the federation remote: its
|
|
7
|
+
* module-federation `exposes` map and codegen artifacts are computed once at
|
|
8
|
+
* server start, so a newly-declared interface has no expose until the server is
|
|
9
|
+
* recreated — `server.restart()` can't do it (it reuses the inline config).
|
|
10
|
+
* `rebuild` therefore tears the server down and starts a fresh one with a
|
|
11
|
+
* reloaded config; the registry watcher calls it when the interface set changes.
|
|
12
|
+
*
|
|
13
|
+
* Returns the not-started result verbatim when the initial boot is an expected
|
|
14
|
+
* early exit, so the caller can skip the rest of the orchestration.
|
|
15
|
+
*/ export async function startAppServerSupervisor(options) {
|
|
16
|
+
const { cliConfig, start, workDir } = options;
|
|
17
|
+
const initial = await start(cliConfig);
|
|
18
|
+
if (!initial.started) return {
|
|
19
|
+
reason: initial.reason,
|
|
20
|
+
started: false
|
|
21
|
+
};
|
|
22
|
+
// `closeCurrent` repoints at the replacement only once a rebuild succeeds, so a
|
|
23
|
+
// failed rebuild (old server already closed) leaves nothing for close() to re-close.
|
|
24
|
+
let server = initial.server;
|
|
25
|
+
let closeCurrent = initial.close;
|
|
26
|
+
let closed = false;
|
|
27
|
+
// close() waits on this so a rebuild racing teardown can't orphan the replacement.
|
|
28
|
+
// Rejections are the watcher's (warn + retry); the tracked copy is swallowed.
|
|
29
|
+
let rebuildInFlight = Promise.resolve();
|
|
30
|
+
const runRebuild = async ()=>{
|
|
31
|
+
// Refuse once shutting down — a config save in the teardown window must not
|
|
32
|
+
// boot a replacement nobody owns.
|
|
33
|
+
if (closed) throw new Error('Dev server is shutting down');
|
|
34
|
+
const freshConfig = await getCliConfigUncached(workDir);
|
|
35
|
+
await closeCurrent();
|
|
36
|
+
closeCurrent = noop;
|
|
37
|
+
const result = await start(freshConfig);
|
|
38
|
+
if (!result.started) {
|
|
39
|
+
// The server already reported why (e.g. organizationId was removed).
|
|
40
|
+
throw new Error('Dev server did not restart after the view/service change');
|
|
41
|
+
}
|
|
42
|
+
server = result.server;
|
|
43
|
+
closeCurrent = result.close;
|
|
44
|
+
return server;
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
started: true,
|
|
48
|
+
supervisor: {
|
|
49
|
+
async close () {
|
|
50
|
+
closed = true;
|
|
51
|
+
await rebuildInFlight;
|
|
52
|
+
await closeCurrent();
|
|
53
|
+
},
|
|
54
|
+
rebuild () {
|
|
55
|
+
const rebuild = runRebuild();
|
|
56
|
+
rebuildInFlight = rebuild.catch(()=>{});
|
|
57
|
+
return rebuild;
|
|
58
|
+
},
|
|
59
|
+
get server () {
|
|
60
|
+
return server;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=appServerSupervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/appServerSupervisor.ts"],"sourcesContent":["import {type CliConfig, getCliConfigUncached} from '@sanity/cli-core'\nimport {type ViteDevServer} from 'vite'\n\n/**\n * Minimal shape the supervisor needs back from a started app/studio dev server.\n * The not-started arm is reserved for expected early exits the server already\n * reported (e.g. a missing organization id) — a failure to *boot* still throws.\n */\nexport type AppServerResult =\n | {close: () => Promise<void>; server: ViteDevServer; started: true}\n | {reason: string; started: false}\n\n/** Start the app/studio dev server for a given config (port + URL intent baked in by the caller). */\nexport type StartAppServer = (cliConfig: CliConfig) => Promise<AppServerResult>\n\nconst noop = async () => {}\n\nexport interface AppServerSupervisor {\n /** Stop the server once, waiting out any rebuild already in flight. */\n close: () => Promise<void>\n /** Tear down the running server and bring it back up with a freshly-loaded config. */\n rebuild: () => Promise<ViteDevServer>\n /** The currently-live dev server; re-points after a rebuild. */\n readonly server: ViteDevServer\n}\n\n/**\n * Own the app/studio dev server's lifecycle behind the dev-server registry seam.\n *\n * Adding or removing a view/service rebuilds the federation remote: its\n * module-federation `exposes` map and codegen artifacts are computed once at\n * server start, so a newly-declared interface has no expose until the server is\n * recreated — `server.restart()` can't do it (it reuses the inline config).\n * `rebuild` therefore tears the server down and starts a fresh one with a\n * reloaded config; the registry watcher calls it when the interface set changes.\n *\n * Returns the not-started result verbatim when the initial boot is an expected\n * early exit, so the caller can skip the rest of the orchestration.\n */\nexport async function startAppServerSupervisor(options: {\n cliConfig: CliConfig\n start: StartAppServer\n workDir: string\n}): Promise<{reason: string; started: false} | {started: true; supervisor: AppServerSupervisor}> {\n const {cliConfig, start, workDir} = options\n\n const initial = await start(cliConfig)\n if (!initial.started) return {reason: initial.reason, started: false}\n\n // `closeCurrent` repoints at the replacement only once a rebuild succeeds, so a\n // failed rebuild (old server already closed) leaves nothing for close() to re-close.\n let server = initial.server\n let closeCurrent = initial.close\n let closed = false\n // close() waits on this so a rebuild racing teardown can't orphan the replacement.\n // Rejections are the watcher's (warn + retry); the tracked copy is swallowed.\n let rebuildInFlight: Promise<unknown> = Promise.resolve()\n\n const runRebuild = async (): Promise<ViteDevServer> => {\n // Refuse once shutting down — a config save in the teardown window must not\n // boot a replacement nobody owns.\n if (closed) throw new Error('Dev server is shutting down')\n const freshConfig = await getCliConfigUncached(workDir)\n await closeCurrent()\n closeCurrent = noop\n const result = await start(freshConfig)\n if (!result.started) {\n // The server already reported why (e.g. organizationId was removed).\n throw new Error('Dev server did not restart after the view/service change')\n }\n server = result.server\n closeCurrent = result.close\n return server\n }\n\n return {\n started: true,\n supervisor: {\n async close() {\n closed = true\n await rebuildInFlight\n await closeCurrent()\n },\n rebuild() {\n const rebuild = runRebuild()\n rebuildInFlight = rebuild.catch(() => {})\n return rebuild\n },\n get server() {\n return server\n },\n },\n }\n}\n"],"names":["getCliConfigUncached","noop","startAppServerSupervisor","options","cliConfig","start","workDir","initial","started","reason","server","closeCurrent","close","closed","rebuildInFlight","Promise","resolve","runRebuild","Error","freshConfig","result","supervisor","rebuild","catch"],"mappings":"AAAA,SAAwBA,oBAAoB,QAAO,mBAAkB;AAerE,MAAMC,OAAO,WAAa;AAW1B;;;;;;;;;;;;CAYC,GACD,OAAO,eAAeC,yBAAyBC,OAI9C;IACC,MAAM,EAACC,SAAS,EAAEC,KAAK,EAAEC,OAAO,EAAC,GAAGH;IAEpC,MAAMI,UAAU,MAAMF,MAAMD;IAC5B,IAAI,CAACG,QAAQC,OAAO,EAAE,OAAO;QAACC,QAAQF,QAAQE,MAAM;QAAED,SAAS;IAAK;IAEpE,gFAAgF;IAChF,qFAAqF;IACrF,IAAIE,SAASH,QAAQG,MAAM;IAC3B,IAAIC,eAAeJ,QAAQK,KAAK;IAChC,IAAIC,SAAS;IACb,mFAAmF;IACnF,8EAA8E;IAC9E,IAAIC,kBAAoCC,QAAQC,OAAO;IAEvD,MAAMC,aAAa;QACjB,4EAA4E;QAC5E,kCAAkC;QAClC,IAAIJ,QAAQ,MAAM,IAAIK,MAAM;QAC5B,MAAMC,cAAc,MAAMnB,qBAAqBM;QAC/C,MAAMK;QACNA,eAAeV;QACf,MAAMmB,SAAS,MAAMf,MAAMc;QAC3B,IAAI,CAACC,OAAOZ,OAAO,EAAE;YACnB,qEAAqE;YACrE,MAAM,IAAIU,MAAM;QAClB;QACAR,SAASU,OAAOV,MAAM;QACtBC,eAAeS,OAAOR,KAAK;QAC3B,OAAOF;IACT;IAEA,OAAO;QACLF,SAAS;QACTa,YAAY;YACV,MAAMT;gBACJC,SAAS;gBACT,MAAMC;gBACN,MAAMH;YACR;YACAW;gBACE,MAAMA,UAAUL;gBAChBH,kBAAkBQ,QAAQC,KAAK,CAAC,KAAO;gBACvC,OAAOD;YACT;YACA,IAAIZ,UAAS;gBACX,OAAOA;YACT;QACF;IACF;AACF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isWorkbenchApp } from '@sanity/cli-core';
|
|
2
|
+
/**
|
|
3
|
+
* Map an app's `unstable_defineApp` config to the interface records forwarded on
|
|
4
|
+
* its registry entry: `views` → panels, `services` → workers, `entry` → the
|
|
5
|
+
* navigable `app` view (`entry_point` is the raw `src`, not a resolved URL).
|
|
6
|
+
* `undefined` for a non-branded app; a studio that declares `entry` is rejected
|
|
7
|
+
* (studio app views are not implemented yet).
|
|
8
|
+
*/ export function deriveInterfaces(app, options) {
|
|
9
|
+
if (!isWorkbenchApp(app)) return undefined;
|
|
10
|
+
if (!options.isApp && app.entry !== undefined) {
|
|
11
|
+
throw new Error('App views for studios are not implemented yet');
|
|
12
|
+
}
|
|
13
|
+
return [
|
|
14
|
+
...app.views?.map((view)=>({
|
|
15
|
+
entry_point: view.src,
|
|
16
|
+
interface_type: view.type,
|
|
17
|
+
name: view.name
|
|
18
|
+
})) ?? [],
|
|
19
|
+
...app.services?.map((service)=>({
|
|
20
|
+
entry_point: service.src,
|
|
21
|
+
interface_type: service.type,
|
|
22
|
+
name: service.name
|
|
23
|
+
})) ?? [],
|
|
24
|
+
...app.entry === undefined ? [] : [
|
|
25
|
+
{
|
|
26
|
+
entry_point: app.entry,
|
|
27
|
+
interface_type: 'app',
|
|
28
|
+
name: app.name
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=deriveInterfaces.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/deriveInterfaces.ts"],"sourcesContent":["import {type CliConfig, isWorkbenchApp} from '@sanity/cli-core'\n\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/**\n * Map an app's `unstable_defineApp` config to the interface records forwarded on\n * its registry entry: `views` → panels, `services` → workers, `entry` → the\n * navigable `app` view (`entry_point` is the raw `src`, not a resolved URL).\n * `undefined` for a non-branded app; a studio that declares `entry` is rejected\n * (studio app views are not 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 return [\n ...(app.views?.map((view) => ({\n entry_point: view.src,\n interface_type: view.type,\n name: view.name,\n })) ?? []),\n ...(app.services?.map((service) => ({\n entry_point: service.src,\n interface_type: service.type,\n name: service.name,\n })) ?? []),\n ...(app.entry === undefined\n ? []\n : [{entry_point: app.entry, interface_type: 'app' as const, name: app.name}]),\n ]\n}\n"],"names":["isWorkbenchApp","deriveInterfaces","app","options","undefined","isApp","entry","Error","views","map","view","entry_point","src","interface_type","type","name","services","service"],"mappings":"AAAA,SAAwBA,cAAc,QAAO,mBAAkB;AAO/D;;;;;;CAMC,GACD,OAAO,SAASC,iBACdC,GAAqB,EACrBC,OAAyB;IAEzB,IAAI,CAACH,eAAeE,MAAM,OAAOE;IAEjC,IAAI,CAACD,QAAQE,KAAK,IAAIH,IAAII,KAAK,KAAKF,WAAW;QAC7C,MAAM,IAAIG,MAAM;IAClB;IAEA,OAAO;WACDL,IAAIM,KAAK,EAAEC,IAAI,CAACC,OAAU,CAAA;gBAC5BC,aAAaD,KAAKE,GAAG;gBACrBC,gBAAgBH,KAAKI,IAAI;gBACzBC,MAAML,KAAKK,IAAI;YACjB,CAAA,MAAO,EAAE;WACLb,IAAIc,QAAQ,EAAEP,IAAI,CAACQ,UAAa,CAAA;gBAClCN,aAAaM,QAAQL,GAAG;gBACxBC,gBAAgBI,QAAQH,IAAI;gBAC5BC,MAAME,QAAQF,IAAI;YACpB,CAAA,MAAO,EAAE;WACLb,IAAII,KAAK,KAAKF,YACd,EAAE,GACF;YAAC;gBAACO,aAAaT,IAAII,KAAK;gBAAEO,gBAAgB;gBAAgBE,MAAMb,IAAIa,IAAI;YAAA;SAAE;KAC/E;AACH"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Order-independent identity of an app's interface set. Reordering
|
|
3
|
+
* `views`/`services` keeps the same id (not a change); adding, removing,
|
|
4
|
+
* renaming, or repointing one changes it. `undefined` ids to the empty set.
|
|
5
|
+
*/ export function interfaceSetId(interfaces) {
|
|
6
|
+
if (!interfaces || interfaces.length === 0) return '';
|
|
7
|
+
return interfaces.map((iface)=>[
|
|
8
|
+
iface.interface_type,
|
|
9
|
+
iface.name,
|
|
10
|
+
iface.entry_point
|
|
11
|
+
].join('::')).toSorted().join('|');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Tracks one app's interface set across config reloads. `changed` and `commit`
|
|
15
|
+
* are split so the caller commits only after the rebuild that depends on the new
|
|
16
|
+
* set succeeds — a thrown rebuild leaves it uncommitted, so the next save retries
|
|
17
|
+
* instead of skipping. Seed with the initially registered set.
|
|
18
|
+
*/ export function trackInterfaceSet(initial) {
|
|
19
|
+
let lastId = interfaceSetId(initial);
|
|
20
|
+
return {
|
|
21
|
+
changed: (interfaces)=>interfaceSetId(interfaces) !== lastId,
|
|
22
|
+
commit: (interfaces)=>{
|
|
23
|
+
lastId = interfaceSetId(interfaces);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const serverKey = (server)=>`${server.id ?? ''}@${server.host ?? ''}:${server.port}`;
|
|
28
|
+
/**
|
|
29
|
+
* Tracks every registered app's interface set across registry snapshots (the
|
|
30
|
+
* multi-app counterpart to {@link trackInterfaceSet}). `hasChanged` is true when
|
|
31
|
+
* a *known* app's set changed since the last call — its remote was rebuilt with
|
|
32
|
+
* new exposes, so the workbench must full-reload to drop the stale remote-entry.
|
|
33
|
+
* A new/removed app or manifest-only edit isn't a rebuild.
|
|
34
|
+
*/ export function createInterfacesTracker() {
|
|
35
|
+
let known = new Map();
|
|
36
|
+
return {
|
|
37
|
+
hasChanged (servers) {
|
|
38
|
+
const rebuilt = servers.some((server)=>{
|
|
39
|
+
const key = serverKey(server);
|
|
40
|
+
return known.has(key) && known.get(key) !== interfaceSetId(server.interfaces);
|
|
41
|
+
});
|
|
42
|
+
known = new Map(servers.map((server)=>[
|
|
43
|
+
serverKey(server),
|
|
44
|
+
interfaceSetId(server.interfaces)
|
|
45
|
+
]));
|
|
46
|
+
return rebuilt;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=interfaceSetId.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/interfaceSetId.ts"],"sourcesContent":["import {type DevServerInterface} from './deriveInterfaces.js'\nimport {type DevServerManifest} from './registry.js'\n\n/**\n * Order-independent identity of an app's interface set. Reordering\n * `views`/`services` keeps the same id (not a change); adding, removing,\n * renaming, or repointing one changes it. `undefined` ids to the empty set.\n */\nexport function interfaceSetId(interfaces: readonly DevServerInterface[] | undefined): string {\n if (!interfaces || interfaces.length === 0) return ''\n return interfaces\n .map((iface) => [iface.interface_type, iface.name, iface.entry_point].join('::'))\n .toSorted()\n .join('|')\n}\n\n/**\n * Tracks one app's interface set across config reloads. `changed` and `commit`\n * are split so the caller commits only after the rebuild that depends on the new\n * set succeeds — a thrown rebuild leaves it uncommitted, so the next save retries\n * instead of skipping. Seed with the initially registered set.\n */\nexport function trackInterfaceSet(initial: readonly DevServerInterface[] | undefined): {\n changed: (interfaces: readonly DevServerInterface[] | undefined) => boolean\n commit: (interfaces: readonly DevServerInterface[] | undefined) => void\n} {\n let lastId = interfaceSetId(initial)\n return {\n changed: (interfaces) => interfaceSetId(interfaces) !== lastId,\n commit: (interfaces) => {\n lastId = interfaceSetId(interfaces)\n },\n }\n}\n\nconst serverKey = (server: DevServerManifest): string =>\n `${server.id ?? ''}@${server.host ?? ''}:${server.port}`\n\n/**\n * Tracks every registered app's interface set across registry snapshots (the\n * multi-app counterpart to {@link trackInterfaceSet}). `hasChanged` is true when\n * a *known* app's set changed since the last call — its remote was rebuilt with\n * new exposes, so the workbench must full-reload to drop the stale remote-entry.\n * A new/removed app or manifest-only edit isn't a rebuild.\n */\nexport function createInterfacesTracker(): {\n hasChanged: (servers: readonly DevServerManifest[]) => boolean\n} {\n let known = new Map<string, string>()\n return {\n hasChanged(servers) {\n const rebuilt = servers.some((server) => {\n const key = serverKey(server)\n return known.has(key) && known.get(key) !== interfaceSetId(server.interfaces)\n })\n known = new Map(\n servers.map((server) => [serverKey(server), interfaceSetId(server.interfaces)]),\n )\n return rebuilt\n },\n }\n}\n"],"names":["interfaceSetId","interfaces","length","map","iface","interface_type","name","entry_point","join","toSorted","trackInterfaceSet","initial","lastId","changed","commit","serverKey","server","id","host","port","createInterfacesTracker","known","Map","hasChanged","servers","rebuilt","some","key","has","get"],"mappings":"AAGA;;;;CAIC,GACD,OAAO,SAASA,eAAeC,UAAqD;IAClF,IAAI,CAACA,cAAcA,WAAWC,MAAM,KAAK,GAAG,OAAO;IACnD,OAAOD,WACJE,GAAG,CAAC,CAACC,QAAU;YAACA,MAAMC,cAAc;YAAED,MAAME,IAAI;YAAEF,MAAMG,WAAW;SAAC,CAACC,IAAI,CAAC,OAC1EC,QAAQ,GACRD,IAAI,CAAC;AACV;AAEA;;;;;CAKC,GACD,OAAO,SAASE,kBAAkBC,OAAkD;IAIlF,IAAIC,SAASZ,eAAeW;IAC5B,OAAO;QACLE,SAAS,CAACZ,aAAeD,eAAeC,gBAAgBW;QACxDE,QAAQ,CAACb;YACPW,SAASZ,eAAeC;QAC1B;IACF;AACF;AAEA,MAAMc,YAAY,CAACC,SACjB,GAAGA,OAAOC,EAAE,IAAI,GAAG,CAAC,EAAED,OAAOE,IAAI,IAAI,GAAG,CAAC,EAAEF,OAAOG,IAAI,EAAE;AAE1D;;;;;;CAMC,GACD,OAAO,SAASC;IAGd,IAAIC,QAAQ,IAAIC;IAChB,OAAO;QACLC,YAAWC,OAAO;YAChB,MAAMC,UAAUD,QAAQE,IAAI,CAAC,CAACV;gBAC5B,MAAMW,MAAMZ,UAAUC;gBACtB,OAAOK,MAAMO,GAAG,CAACD,QAAQN,MAAMQ,GAAG,CAACF,SAAS3B,eAAegB,OAAOf,UAAU;YAC9E;YACAoB,QAAQ,IAAIC,IACVE,QAAQrB,GAAG,CAAC,CAACa,SAAW;oBAACD,UAAUC;oBAAShB,eAAegB,OAAOf,UAAU;iBAAE;YAEhF,OAAOwB;QACT;IACF;AACF"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
import { basename, dirname } from 'node:path';
|
|
3
|
+
import { findProjectRoot, subdebug } from '@sanity/cli-core';
|
|
4
|
+
import { canonicalizeWatchDir } from './canonicalizeWatchDir.js';
|
|
5
|
+
const devDebug = subdebug('dev');
|
|
6
|
+
/**
|
|
7
|
+
* Debounce window between config file events and the next manifest
|
|
8
|
+
* regeneration. Coalesces rapid saves (e.g. editor auto-save) and
|
|
9
|
+
* atomic-rename bursts emitted by tools like VS Code.
|
|
10
|
+
*/ const DEBOUNCE_MS = 250;
|
|
11
|
+
/**
|
|
12
|
+
* Generate the project manifest once and then keep it in sync with the
|
|
13
|
+
* project's config file (`sanity.config.(ts|js)` for studios,
|
|
14
|
+
* `sanity.cli.(ts|js)` for core-apps) on disk. The initial generation runs
|
|
15
|
+
* fire-and-forget so it doesn't block dev-server startup; subsequent
|
|
16
|
+
* file-system events are coalesced behind it, so the extractor never has
|
|
17
|
+
* overlapping writes to its shared output directory. Each successful
|
|
18
|
+
* regeneration inlines the new manifest into the registry via the `update`
|
|
19
|
+
* callback, so any running workbench rebroadcasts to its clients.
|
|
20
|
+
*
|
|
21
|
+
* Errors during extraction are logged as warnings and do not crash the dev
|
|
22
|
+
* server — the previously extracted manifest (if any) stays in the
|
|
23
|
+
* registry.
|
|
24
|
+
*/ export async function startDevManifestWatcher({ extract, extraWatchFilenames, output, update, workDir }) {
|
|
25
|
+
const projectRoot = await findProjectRoot(workDir);
|
|
26
|
+
const configPath = projectRoot.path;
|
|
27
|
+
let running = false;
|
|
28
|
+
let pending = false;
|
|
29
|
+
let closed = false;
|
|
30
|
+
const regenerate = async ()=>{
|
|
31
|
+
if (closed) return;
|
|
32
|
+
if (running) {
|
|
33
|
+
pending = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
running = true;
|
|
37
|
+
try {
|
|
38
|
+
const { interfaces, manifest } = await extract({
|
|
39
|
+
configPath,
|
|
40
|
+
workDir
|
|
41
|
+
});
|
|
42
|
+
if (closed) return;
|
|
43
|
+
await update({
|
|
44
|
+
interfaces,
|
|
45
|
+
manifest,
|
|
46
|
+
manifestUpdatedAt: new Date().toISOString()
|
|
47
|
+
});
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// Extractors print their own spinner failure; log the reason here so
|
|
50
|
+
// the user sees what went wrong alongside the spinner indicator.
|
|
51
|
+
devDebug('Manifest regeneration failed: %O', err);
|
|
52
|
+
output.warn(`Could not extract manifest for workbench: ${err instanceof Error ? err.message : String(err)}`);
|
|
53
|
+
} finally{
|
|
54
|
+
running = false;
|
|
55
|
+
if (pending && !closed) {
|
|
56
|
+
pending = false;
|
|
57
|
+
void regenerate();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
// Route the initial extraction through `regenerate` too, so file-system
|
|
62
|
+
// events arriving before it finishes get coalesced rather than racing it
|
|
63
|
+
// for the shared output directory.
|
|
64
|
+
void regenerate();
|
|
65
|
+
// Watch the config file's parent directory and filter by filename.
|
|
66
|
+
// Watching the file itself is unreliable across editors that perform
|
|
67
|
+
// atomic-save (delete + rename) — the watcher loses its target once the
|
|
68
|
+
// inode changes. Directory watches survive those transitions.
|
|
69
|
+
// Canonicalize to the real long path so `fs.watch` doesn't abort on Windows
|
|
70
|
+
// short-path dirs. See `canonicalizeWatchDir`.
|
|
71
|
+
const configDir = canonicalizeWatchDir(dirname(configPath));
|
|
72
|
+
const watchFilenames = new Set([
|
|
73
|
+
basename(configPath),
|
|
74
|
+
...extraWatchFilenames ?? []
|
|
75
|
+
]);
|
|
76
|
+
let debounceTimer;
|
|
77
|
+
const onEvent = (_event, filename)=>{
|
|
78
|
+
if (!filename) return;
|
|
79
|
+
const name = typeof filename === 'string' ? filename : filename.toString('utf8');
|
|
80
|
+
if (!watchFilenames.has(name)) return;
|
|
81
|
+
clearTimeout(debounceTimer);
|
|
82
|
+
debounceTimer = setTimeout(()=>{
|
|
83
|
+
void regenerate();
|
|
84
|
+
}, DEBOUNCE_MS);
|
|
85
|
+
};
|
|
86
|
+
const watcher = watch(configDir, onEvent);
|
|
87
|
+
watcher.on('error', (err)=>{
|
|
88
|
+
devDebug('Config watcher error: %O', err);
|
|
89
|
+
output.warn(`Manifest watcher error: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
});
|
|
91
|
+
return {
|
|
92
|
+
// Idempotent — a repeat close (e.g. a signal handler racing an explicit
|
|
93
|
+
// close) is a no-op, so we never clear an already-cleared timer or
|
|
94
|
+
// double-close the underlying watcher.
|
|
95
|
+
close: async ()=>{
|
|
96
|
+
if (closed) return;
|
|
97
|
+
closed = true;
|
|
98
|
+
clearTimeout(debounceTimer);
|
|
99
|
+
watcher.close();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=startDevManifestWatcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/startDevManifestWatcher.ts"],"sourcesContent":["import {watch} from 'node:fs'\nimport {basename, dirname} from 'node:path'\n\nimport {findProjectRoot, type Output, subdebug} from '@sanity/cli-core'\n\nimport {canonicalizeWatchDir} from './canonicalizeWatchDir.js'\nimport {type DevServerInterface} from './deriveInterfaces.js'\n\nconst devDebug = subdebug('dev')\n\n/**\n * Debounce window between config file events and the next manifest\n * regeneration. Coalesces rapid saves (e.g. editor auto-save) and\n * atomic-rename bursts emitted by tools like VS Code.\n */\nconst DEBOUNCE_MS = 250\n\ninterface DevManifestWatcher {\n close: () => Promise<void>\n}\n\n/** Subset of registry fields the watcher is allowed to update. */\ninterface ManifestPatch<T> {\n manifest: T | undefined\n manifestUpdatedAt: string\n\n /**\n * Workbench interfaces (views/services/app view) re-derived from the config\n * on each change, so editing `views`/`services`/`entry` in `sanity.cli.ts`\n * re-syncs live like `title`/`icon`. `undefined` only for\n * non-branded configs — the registry patch is a shallow merge, so extractors\n * must re-derive rather than omit, or the registered set gets wiped.\n */\n interfaces?: DevServerInterface[] | undefined\n}\n\ninterface StartDevManifestWatcherOptions<T> {\n /**\n * Run the project-specific extraction and resolve to the inlined manifest\n * plus the workbench `interfaces[]` (when the project declares them).\n * Receives the resolved config path (e.g. `sanity.config.ts` for studios,\n * `sanity.cli.ts` for core-apps) and the working directory.\n */\n extract: (params: {\n configPath: string\n workDir: string\n }) => Promise<{interfaces?: DevServerInterface[] | undefined; manifest: T | undefined}>\n output: Output\n /**\n * Called after every successful extraction with the inlined manifest +\n * interfaces. Awaited, so an interface-set change can rebuild the federation\n * remote before the registry is patched (which is what reloads the workbench).\n */\n update: (patch: ManifestPatch<T>) => Promise<void> | void\n workDir: string\n\n /**\n * Extra config filenames (basenames in the project root directory) that also\n * trigger a regeneration. Studios resolve their project root via\n * `sanity.config.*` but declare workbench interfaces in `sanity.cli.*`, so\n * their watcher needs to react to both files.\n */\n extraWatchFilenames?: readonly string[]\n}\n\n/**\n * Generate the project manifest once and then keep it in sync with the\n * project's config file (`sanity.config.(ts|js)` for studios,\n * `sanity.cli.(ts|js)` for core-apps) on disk. The initial generation runs\n * fire-and-forget so it doesn't block dev-server startup; subsequent\n * file-system events are coalesced behind it, so the extractor never has\n * overlapping writes to its shared output directory. Each successful\n * regeneration inlines the new manifest into the registry via the `update`\n * callback, so any running workbench rebroadcasts to its clients.\n *\n * Errors during extraction are logged as warnings and do not crash the dev\n * server — the previously extracted manifest (if any) stays in the\n * registry.\n */\nexport async function startDevManifestWatcher<T>({\n extract,\n extraWatchFilenames,\n output,\n update,\n workDir,\n}: StartDevManifestWatcherOptions<T>): Promise<DevManifestWatcher> {\n const projectRoot = await findProjectRoot(workDir)\n const configPath = projectRoot.path\n\n let running = false\n let pending = false\n let closed = false\n\n const regenerate = async () => {\n if (closed) return\n if (running) {\n pending = true\n return\n }\n running = true\n try {\n const {interfaces, manifest} = await extract({configPath, workDir})\n if (closed) return\n await update({interfaces, manifest, manifestUpdatedAt: new Date().toISOString()})\n } catch (err) {\n // Extractors print their own spinner failure; log the reason here so\n // the user sees what went wrong alongside the spinner indicator.\n devDebug('Manifest regeneration failed: %O', err)\n output.warn(\n `Could not extract manifest for workbench: ${err instanceof Error ? err.message : String(err)}`,\n )\n } finally {\n running = false\n if (pending && !closed) {\n pending = false\n void regenerate()\n }\n }\n }\n\n // Route the initial extraction through `regenerate` too, so file-system\n // events arriving before it finishes get coalesced rather than racing it\n // for the shared output directory.\n void regenerate()\n\n // Watch the config file's parent directory and filter by filename.\n // Watching the file itself is unreliable across editors that perform\n // atomic-save (delete + rename) — the watcher loses its target once the\n // inode changes. Directory watches survive those transitions.\n // Canonicalize to the real long path so `fs.watch` doesn't abort on Windows\n // short-path dirs. See `canonicalizeWatchDir`.\n const configDir = canonicalizeWatchDir(dirname(configPath))\n const watchFilenames = new Set([basename(configPath), ...(extraWatchFilenames ?? [])])\n\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n const onEvent = (_event: string, filename: Buffer | string | null) => {\n if (!filename) return\n const name = typeof filename === 'string' ? filename : filename.toString('utf8')\n if (!watchFilenames.has(name)) return\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n void regenerate()\n }, DEBOUNCE_MS)\n }\n\n const watcher = watch(configDir, onEvent)\n\n watcher.on('error', (err) => {\n devDebug('Config watcher error: %O', err)\n output.warn(`Manifest watcher error: ${err instanceof Error ? err.message : String(err)}`)\n })\n\n return {\n // Idempotent — a repeat close (e.g. a signal handler racing an explicit\n // close) is a no-op, so we never clear an already-cleared timer or\n // double-close the underlying watcher.\n close: async () => {\n if (closed) return\n closed = true\n clearTimeout(debounceTimer)\n watcher.close()\n },\n }\n}\n"],"names":["watch","basename","dirname","findProjectRoot","subdebug","canonicalizeWatchDir","devDebug","DEBOUNCE_MS","startDevManifestWatcher","extract","extraWatchFilenames","output","update","workDir","projectRoot","configPath","path","running","pending","closed","regenerate","interfaces","manifest","manifestUpdatedAt","Date","toISOString","err","warn","Error","message","String","configDir","watchFilenames","Set","debounceTimer","onEvent","_event","filename","name","toString","has","clearTimeout","setTimeout","watcher","on","close"],"mappings":"AAAA,SAAQA,KAAK,QAAO,UAAS;AAC7B,SAAQC,QAAQ,EAAEC,OAAO,QAAO,YAAW;AAE3C,SAAQC,eAAe,EAAeC,QAAQ,QAAO,mBAAkB;AAEvE,SAAQC,oBAAoB,QAAO,4BAA2B;AAG9D,MAAMC,WAAWF,SAAS;AAE1B;;;;CAIC,GACD,MAAMG,cAAc;AAkDpB;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeC,wBAA2B,EAC/CC,OAAO,EACPC,mBAAmB,EACnBC,MAAM,EACNC,MAAM,EACNC,OAAO,EAC2B;IAClC,MAAMC,cAAc,MAAMX,gBAAgBU;IAC1C,MAAME,aAAaD,YAAYE,IAAI;IAEnC,IAAIC,UAAU;IACd,IAAIC,UAAU;IACd,IAAIC,SAAS;IAEb,MAAMC,aAAa;QACjB,IAAID,QAAQ;QACZ,IAAIF,SAAS;YACXC,UAAU;YACV;QACF;QACAD,UAAU;QACV,IAAI;YACF,MAAM,EAACI,UAAU,EAAEC,QAAQ,EAAC,GAAG,MAAMb,QAAQ;gBAACM;gBAAYF;YAAO;YACjE,IAAIM,QAAQ;YACZ,MAAMP,OAAO;gBAACS;gBAAYC;gBAAUC,mBAAmB,IAAIC,OAAOC,WAAW;YAAE;QACjF,EAAE,OAAOC,KAAK;YACZ,qEAAqE;YACrE,iEAAiE;YACjEpB,SAAS,oCAAoCoB;YAC7Cf,OAAOgB,IAAI,CACT,CAAC,0CAA0C,EAAED,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ,MAAM;QAEnG,SAAU;YACRT,UAAU;YACV,IAAIC,WAAW,CAACC,QAAQ;gBACtBD,UAAU;gBACV,KAAKE;YACP;QACF;IACF;IAEA,wEAAwE;IACxE,yEAAyE;IACzE,mCAAmC;IACnC,KAAKA;IAEL,mEAAmE;IACnE,qEAAqE;IACrE,wEAAwE;IACxE,8DAA8D;IAC9D,4EAA4E;IAC5E,+CAA+C;IAC/C,MAAMW,YAAY1B,qBAAqBH,QAAQa;IAC/C,MAAMiB,iBAAiB,IAAIC,IAAI;QAAChC,SAASc;WAAiBL,uBAAuB,EAAE;KAAE;IAErF,IAAIwB;IAEJ,MAAMC,UAAU,CAACC,QAAgBC;QAC/B,IAAI,CAACA,UAAU;QACf,MAAMC,OAAO,OAAOD,aAAa,WAAWA,WAAWA,SAASE,QAAQ,CAAC;QACzE,IAAI,CAACP,eAAeQ,GAAG,CAACF,OAAO;QAC/BG,aAAaP;QACbA,gBAAgBQ,WAAW;YACzB,KAAKtB;QACP,GAAGb;IACL;IAEA,MAAMoC,UAAU3C,MAAM+B,WAAWI;IAEjCQ,QAAQC,EAAE,CAAC,SAAS,CAAClB;QACnBpB,SAAS,4BAA4BoB;QACrCf,OAAOgB,IAAI,CAAC,CAAC,wBAAwB,EAAED,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ,MAAM;IAC3F;IAEA,OAAO;QACL,wEAAwE;QACxE,mEAAmE;QACnE,uCAAuC;QACvCmB,OAAO;YACL,IAAI1B,QAAQ;YACZA,SAAS;YACTsB,aAAaP;YACbS,QAAQE,KAAK;QACf;IACF;AACF"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getCliConfigUncached } from '@sanity/cli-core';
|
|
2
|
+
import { deriveInterfaces } from './deriveInterfaces.js';
|
|
3
|
+
import { trackInterfaceSet } from './interfaceSetId.js';
|
|
4
|
+
import { registerDevServer } from './registry.js';
|
|
5
|
+
import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
6
|
+
/** The address the server actually bound — the live socket, which can differ from the configured port under non-strict ports. */ function serverAddress(server) {
|
|
7
|
+
const resolvedHost = server.config.server.host;
|
|
8
|
+
const addr = server.httpServer?.address();
|
|
9
|
+
return {
|
|
10
|
+
host: typeof resolvedHost === 'string' ? resolvedHost : 'localhost',
|
|
11
|
+
port: typeof addr === 'object' && addr ? addr.port : server.config.server.port
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Register the dev server in the registry and watch its config for manifest +
|
|
16
|
+
* interface changes. The workbench reads the entry to locate and render the
|
|
17
|
+
* server; the watcher keeps it current as `sanity.cli.ts` is edited.
|
|
18
|
+
*/ export async function startDevServerRegistration(options) {
|
|
19
|
+
const { appId, cliConfig, extractManifest, isApp, onInterfaceSetChange, output, server, workDir } = options;
|
|
20
|
+
const { host: appHost, port: appPort } = serverAddress(server);
|
|
21
|
+
// Forwarded alongside (not inside) the manifest so the workbench renders local
|
|
22
|
+
// panels/workers without a deploy; the watcher re-derives them on each edit.
|
|
23
|
+
const interfaces = deriveInterfaces(cliConfig.app, {
|
|
24
|
+
isApp
|
|
25
|
+
});
|
|
26
|
+
const registration = registerDevServer({
|
|
27
|
+
host: appHost,
|
|
28
|
+
id: appId,
|
|
29
|
+
interfaces,
|
|
30
|
+
port: appPort,
|
|
31
|
+
projectId: cliConfig?.api?.projectId,
|
|
32
|
+
type: isApp ? 'coreApp' : 'studio',
|
|
33
|
+
workDir
|
|
34
|
+
});
|
|
35
|
+
const interfaceSet = trackInterfaceSet(interfaces);
|
|
36
|
+
const watcher = await startDevManifestWatcher({
|
|
37
|
+
// Re-derive interfaces every pass (don't omit): the registry patch is a
|
|
38
|
+
// shallow merge, so omitting would wipe the registered set.
|
|
39
|
+
extract: async (params)=>({
|
|
40
|
+
interfaces: deriveInterfaces((await getCliConfigUncached(params.workDir)).app, {
|
|
41
|
+
isApp
|
|
42
|
+
}),
|
|
43
|
+
manifest: await extractManifest(params)
|
|
44
|
+
}),
|
|
45
|
+
// A studio's root resolves to `sanity.config.*` but its interfaces live in
|
|
46
|
+
// `sanity.cli.*` — watch that too. Apps already root at `sanity.cli.*`.
|
|
47
|
+
extraWatchFilenames: isApp ? undefined : [
|
|
48
|
+
'sanity.cli.js',
|
|
49
|
+
'sanity.cli.ts'
|
|
50
|
+
],
|
|
51
|
+
output,
|
|
52
|
+
update: async (patch)=>{
|
|
53
|
+
if (!interfaceSet.changed(patch.interfaces)) {
|
|
54
|
+
registration.update(patch);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Rebuild the remote *before* patching the registry — the patch reloads the
|
|
58
|
+
// page, which must re-fetch a remote that already exposes the new interface.
|
|
59
|
+
const rebuiltServer = await onInterfaceSetChange?.();
|
|
60
|
+
// Commit only after a successful rebuild, so a thrown one retries next pass.
|
|
61
|
+
interfaceSet.commit(patch.interfaces);
|
|
62
|
+
// The recreated server can bind a different port (non-strict ports).
|
|
63
|
+
registration.update(rebuiltServer ? {
|
|
64
|
+
...patch,
|
|
65
|
+
...serverAddress(rebuiltServer)
|
|
66
|
+
} : patch);
|
|
67
|
+
},
|
|
68
|
+
workDir
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
close: async ()=>{
|
|
72
|
+
registration.release();
|
|
73
|
+
await watcher.close();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=startDevServerRegistration.js.map
|
|
@@ -0,0 +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 {deriveInterfaces} from './deriveInterfaces.js'\nimport {trackInterfaceSet} from './interfaceSetId.js'\nimport {type DevServerManifest, registerDevServer} from './registry.js'\nimport {startDevManifestWatcher} from './startDevManifestWatcher.js'\n\ninterface DevServerRegistrationOptions {\n /** Resolved app id for the registry entry; the caller owns id resolution and the deprecation check. */\n appId: string | undefined\n cliConfig: CliConfig\n /**\n * Extract the project manifest to inline into the registry. The caller owns the\n * studio-vs-app split (manifest formats are CLI-domain); registration re-derives\n * the interface set alongside it.\n */\n extractManifest: (params: {\n configPath: string\n workDir: string\n }) => Promise<DevServerManifest['manifest']>\n isApp: boolean\n output: Output\n server: ViteDevServer\n workDir: string\n\n /**\n * Rebuild the app's federation remote when its interface set changes, awaited\n * *before* the registry patch — the patch reloads the workbench page, which must\n * re-fetch a remote that already exposes the new interface. Resolves with the\n * recreated server so the entry gets its actual address (non-strict ports may\n * shift it); must reject if the restart produces no server, so the set stays\n * uncommitted and the next save retries instead of advertising a dead port.\n */\n onInterfaceSetChange?: () => Promise<ViteDevServer>\n}\n\ninterface DevServerRegistrationHandle {\n close: () => Promise<void>\n}\n\n/** The address the server actually bound — the live socket, which can differ from the configured port under non-strict ports. */\nfunction serverAddress(server: ViteDevServer) {\n const resolvedHost = server.config.server.host\n const addr = server.httpServer?.address()\n return {\n host: typeof resolvedHost === 'string' ? resolvedHost : 'localhost',\n port: typeof addr === 'object' && addr ? addr.port : server.config.server.port,\n }\n}\n\n/**\n * Register the dev server in the registry and watch its config for manifest +\n * interface changes. The workbench reads the entry to locate and render the\n * server; the watcher keeps it current as `sanity.cli.ts` is edited.\n */\nexport async function startDevServerRegistration(\n options: DevServerRegistrationOptions,\n): Promise<DevServerRegistrationHandle> {\n const {appId, cliConfig, extractManifest, isApp, onInterfaceSetChange, output, server, workDir} =\n options\n\n const {host: appHost, port: appPort} = serverAddress(server)\n\n // Forwarded alongside (not inside) the manifest so the workbench renders local\n // panels/workers without a deploy; the watcher re-derives them on each edit.\n const interfaces = deriveInterfaces(cliConfig.app, {isApp})\n\n const registration = registerDevServer({\n host: appHost,\n id: appId,\n interfaces,\n port: appPort,\n projectId: cliConfig?.api?.projectId,\n type: isApp ? 'coreApp' : 'studio',\n workDir,\n })\n\n const interfaceSet = trackInterfaceSet(interfaces)\n\n const watcher = await startDevManifestWatcher({\n // Re-derive interfaces every pass (don't omit): the registry patch is a\n // shallow merge, so omitting would wipe the registered set.\n extract: async (params) => ({\n interfaces: deriveInterfaces((await getCliConfigUncached(params.workDir)).app, {isApp}),\n manifest: await extractManifest(params),\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 (!interfaceSet.changed(patch.interfaces)) {\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 interfaceSet.commit(patch.interfaces)\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","deriveInterfaces","trackInterfaceSet","registerDevServer","startDevManifestWatcher","serverAddress","server","resolvedHost","config","host","addr","httpServer","address","port","startDevServerRegistration","options","appId","cliConfig","extractManifest","isApp","onInterfaceSetChange","output","workDir","appHost","appPort","interfaces","app","registration","id","projectId","api","type","interfaceSet","watcher","extract","params","manifest","extraWatchFilenames","undefined","update","patch","changed","rebuiltServer","commit","close","release"],"mappings":"AAAA,SAAwBA,oBAAoB,QAAoB,mBAAkB;AAGlF,SAAQC,gBAAgB,QAAO,wBAAuB;AACtD,SAAQC,iBAAiB,QAAO,sBAAqB;AACrD,SAAgCC,iBAAiB,QAAO,gBAAe;AACvE,SAAQC,uBAAuB,QAAO,+BAA8B;AAmCpE,+HAA+H,GAC/H,SAASC,cAAcC,MAAqB;IAC1C,MAAMC,eAAeD,OAAOE,MAAM,CAACF,MAAM,CAACG,IAAI;IAC9C,MAAMC,OAAOJ,OAAOK,UAAU,EAAEC;IAChC,OAAO;QACLH,MAAM,OAAOF,iBAAiB,WAAWA,eAAe;QACxDM,MAAM,OAAOH,SAAS,YAAYA,OAAOA,KAAKG,IAAI,GAAGP,OAAOE,MAAM,CAACF,MAAM,CAACO,IAAI;IAChF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeC,2BACpBC,OAAqC;IAErC,MAAM,EAACC,KAAK,EAAEC,SAAS,EAAEC,eAAe,EAAEC,KAAK,EAAEC,oBAAoB,EAAEC,MAAM,EAAEf,MAAM,EAAEgB,OAAO,EAAC,GAC7FP;IAEF,MAAM,EAACN,MAAMc,OAAO,EAAEV,MAAMW,OAAO,EAAC,GAAGnB,cAAcC;IAErD,+EAA+E;IAC/E,6EAA6E;IAC7E,MAAMmB,aAAaxB,iBAAiBgB,UAAUS,GAAG,EAAE;QAACP;IAAK;IAEzD,MAAMQ,eAAexB,kBAAkB;QACrCM,MAAMc;QACNK,IAAIZ;QACJS;QACAZ,MAAMW;QACNK,WAAWZ,WAAWa,KAAKD;QAC3BE,MAAMZ,QAAQ,YAAY;QAC1BG;IACF;IAEA,MAAMU,eAAe9B,kBAAkBuB;IAEvC,MAAMQ,UAAU,MAAM7B,wBAAwB;QAC5C,wEAAwE;QACxE,4DAA4D;QAC5D8B,SAAS,OAAOC,SAAY,CAAA;gBAC1BV,YAAYxB,iBAAiB,AAAC,CAAA,MAAMD,qBAAqBmC,OAAOb,OAAO,CAAA,EAAGI,GAAG,EAAE;oBAACP;gBAAK;gBACrFiB,UAAU,MAAMlB,gBAAgBiB;YAClC,CAAA;QACA,2EAA2E;QAC3E,wEAAwE;QACxEE,qBAAqBlB,QAAQmB,YAAY;YAAC;YAAiB;SAAgB;QAC3EjB;QACAkB,QAAQ,OAAOC;YACb,IAAI,CAACR,aAAaS,OAAO,CAACD,MAAMf,UAAU,GAAG;gBAC3CE,aAAaY,MAAM,CAACC;gBACpB;YACF;YACA,4EAA4E;YAC5E,6EAA6E;YAC7E,MAAME,gBAAgB,MAAMtB;YAC5B,6EAA6E;YAC7EY,aAAaW,MAAM,CAACH,MAAMf,UAAU;YACpC,qEAAqE;YACrEE,aAAaY,MAAM,CAACG,gBAAgB;gBAAC,GAAGF,KAAK;gBAAE,GAAGnC,cAAcqC,cAAc;YAAA,IAAIF;QACpF;QACAlB;IACF;IAEA,OAAO;QACLsB,OAAO;YACLjB,aAAakB,OAAO;YACpB,MAAMZ,QAAQW,KAAK;QACrB;IACF;AACF"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { startAppServerSupervisor } from './appServerSupervisor.js';
|
|
3
|
+
import { startDevServerRegistration } from './startDevServerRegistration.js';
|
|
4
|
+
import { startWorkbenchDevServer, startWorkbenchRemoteCoordinator } from './startWorkbenchDevServer.js';
|
|
5
|
+
/** How long teardown runs before re-raising the signal to force-exit — enough for
|
|
6
|
+
* Vite/watchers to close, short enough not to strand a backgrounded process. */ const SHUTDOWN_GRACE_MS = 5000;
|
|
7
|
+
// Bind-only addresses ('0.0.0.0', '::') aren't routable in every browser (notably
|
|
8
|
+
// Windows); the displayed URL falls back to localhost. The bind address is untouched.
|
|
9
|
+
function toDisplayHost(host) {
|
|
10
|
+
if (!host || host === '0.0.0.0' || host === '::' || host === '[::]') {
|
|
11
|
+
return 'localhost';
|
|
12
|
+
}
|
|
13
|
+
return host;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Orchestrate the dev servers a workbench project needs: a singleton workbench
|
|
17
|
+
* Vite server plus the app/studio dev server it renders, wired to the dev-server
|
|
18
|
+
* registry so view/service edits re-sync live.
|
|
19
|
+
*
|
|
20
|
+
* A running workbench claims the configured port, so the app server binds the
|
|
21
|
+
* next one. If the workbench can't start (package or port unavailable), the app
|
|
22
|
+
* server falls back to the configured port and announces its own URL, exactly as
|
|
23
|
+
* a plain `sanity dev` would.
|
|
24
|
+
*/ export async function startWorkbenchDev(options) {
|
|
25
|
+
const { appId, cacheDir, checkForDeprecatedAppId, cliConfig, extractManifest, httpHost, httpPort, isApp, output, reactStrictMode, startAppServer, workDir } = options;
|
|
26
|
+
// The remote can't render itself, so it runs as a plain app server (not the
|
|
27
|
+
// shell) that still claims the lock and bridges the registry, so app
|
|
28
|
+
// `sanity dev`s register into it.
|
|
29
|
+
if (process.env.SANITY_INTERNAL_IS_WORKBENCH_REMOTE === 'true') {
|
|
30
|
+
const remote = await startAppServer({
|
|
31
|
+
announceUrl: true,
|
|
32
|
+
cliConfig,
|
|
33
|
+
httpPort
|
|
34
|
+
});
|
|
35
|
+
if (!remote.started) return {
|
|
36
|
+
close: async ()=>{}
|
|
37
|
+
};
|
|
38
|
+
const addr = remote.server.httpServer?.address();
|
|
39
|
+
const port = (typeof addr === 'object' && addr ? addr.port : remote.server.config.server.port) ?? httpPort;
|
|
40
|
+
const coordinator = startWorkbenchRemoteCoordinator({
|
|
41
|
+
httpHost,
|
|
42
|
+
port,
|
|
43
|
+
server: remote.server
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
close: async ()=>{
|
|
47
|
+
await coordinator.close();
|
|
48
|
+
await remote.close();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Unwound in reverse on any failure or on close(): the watcher stops before
|
|
53
|
+
// the app server, and the supervisor waits out an in-flight rebuild.
|
|
54
|
+
const closers = [];
|
|
55
|
+
const disposeAll = async ()=>{
|
|
56
|
+
for (const close of closers.splice(0).toReversed()){
|
|
57
|
+
await close().catch(()=>{});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const workbench = await startWorkbenchDevServer({
|
|
61
|
+
cacheDir,
|
|
62
|
+
cliConfig,
|
|
63
|
+
httpHost,
|
|
64
|
+
httpPort,
|
|
65
|
+
output,
|
|
66
|
+
reactStrictMode,
|
|
67
|
+
workDir
|
|
68
|
+
});
|
|
69
|
+
closers.push(workbench.close);
|
|
70
|
+
// A running workbench owns the configured port; the app server takes the next.
|
|
71
|
+
// Without one it claims the configured port and announces its own URL.
|
|
72
|
+
const appPort = workbench.workbenchAvailable ? workbench.workbenchPort + 1 : httpPort;
|
|
73
|
+
const announceUrl = !workbench.workbenchAvailable;
|
|
74
|
+
let closing;
|
|
75
|
+
const close = ()=>{
|
|
76
|
+
closing ??= (async ()=>{
|
|
77
|
+
process.off('SIGINT', onSignal);
|
|
78
|
+
process.off('SIGTERM', onSignal);
|
|
79
|
+
await disposeAll();
|
|
80
|
+
})();
|
|
81
|
+
return closing;
|
|
82
|
+
};
|
|
83
|
+
const supervised = await startAppServerSupervisor({
|
|
84
|
+
cliConfig,
|
|
85
|
+
start: (config)=>startAppServer({
|
|
86
|
+
announceUrl,
|
|
87
|
+
cliConfig: config,
|
|
88
|
+
httpPort: appPort
|
|
89
|
+
}),
|
|
90
|
+
workDir
|
|
91
|
+
}).catch(async (err)=>{
|
|
92
|
+
await disposeAll();
|
|
93
|
+
throw err;
|
|
94
|
+
});
|
|
95
|
+
if (!supervised.started) {
|
|
96
|
+
// The app server already reported why (e.g. missing organization id). Hand
|
|
97
|
+
// back a close that releases the workbench lock; nothing else came up.
|
|
98
|
+
return {
|
|
99
|
+
close
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const { supervisor } = supervised;
|
|
103
|
+
closers.push(supervisor.close);
|
|
104
|
+
try {
|
|
105
|
+
// The deprecated-id check and manifest extractor are CLI-domain, injected here.
|
|
106
|
+
checkForDeprecatedAppId();
|
|
107
|
+
const registration = await startDevServerRegistration({
|
|
108
|
+
appId,
|
|
109
|
+
cliConfig,
|
|
110
|
+
extractManifest,
|
|
111
|
+
isApp,
|
|
112
|
+
onInterfaceSetChange: ()=>supervisor.rebuild(),
|
|
113
|
+
output,
|
|
114
|
+
server: supervisor.server,
|
|
115
|
+
workDir
|
|
116
|
+
});
|
|
117
|
+
closers.push(registration.close);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
// Registration runs after both servers are up; a failure here would leak the
|
|
120
|
+
// workbench lock and dev servers without this teardown.
|
|
121
|
+
await disposeAll();
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
if (workbench.workbenchAvailable) {
|
|
125
|
+
const workbenchUrl = `http://${toDisplayHost(workbench.httpHost)}:${workbench.workbenchPort}`;
|
|
126
|
+
const addr = supervisor.server.httpServer?.address();
|
|
127
|
+
const port = typeof addr === 'object' && addr ? addr.port : supervisor.server.config.server.port;
|
|
128
|
+
output.log(`Workbench dev server started at ${styleText([
|
|
129
|
+
'blue',
|
|
130
|
+
'underline'
|
|
131
|
+
], workbenchUrl)} (app on port ${port})`);
|
|
132
|
+
}
|
|
133
|
+
// Trapping the signal disables Node's default exit, and a finished teardown
|
|
134
|
+
// doesn't guarantee an empty event loop (keep-alive sockets, an extraction
|
|
135
|
+
// worker mid-run) — so re-raise after teardown to restore conventional signal
|
|
136
|
+
// exit semantics. A backstop timer force-exits if teardown wedges.
|
|
137
|
+
function onSignal(signal) {
|
|
138
|
+
const graceTimer = setTimeout(()=>process.kill(process.pid, signal), SHUTDOWN_GRACE_MS);
|
|
139
|
+
graceTimer.unref();
|
|
140
|
+
void close().finally(()=>{
|
|
141
|
+
clearTimeout(graceTimer);
|
|
142
|
+
process.kill(process.pid, signal);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
process.once('SIGINT', onSignal);
|
|
146
|
+
process.once('SIGTERM', onSignal);
|
|
147
|
+
return {
|
|
148
|
+
close
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
//# sourceMappingURL=startWorkbenchDev.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/startWorkbenchDev.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {type CliConfig, type Output} from '@sanity/cli-core'\n\nimport {type AppServerResult, startAppServerSupervisor} from './appServerSupervisor.js'\nimport {type DevServerManifest} from './registry.js'\nimport {startDevServerRegistration} from './startDevServerRegistration.js'\nimport {\n startWorkbenchDevServer,\n startWorkbenchRemoteCoordinator,\n} from './startWorkbenchDevServer.js'\n\n/** How long teardown runs before re-raising the signal to force-exit — enough for\n * Vite/watchers to close, short enough not to strand a backgrounded process. */\nconst SHUTDOWN_GRACE_MS = 5000\n\n// Bind-only addresses ('0.0.0.0', '::') aren't routable in every browser (notably\n// Windows); the displayed URL falls back to localhost. The bind address is untouched.\nfunction toDisplayHost(host: string | undefined): string {\n if (!host || host === '0.0.0.0' || host === '::' || host === '[::]') {\n return 'localhost'\n }\n return host\n}\n\nexport interface StartWorkbenchDevOptions {\n /** Resolved app id for the registry entry (the CLI owns id resolution). */\n appId: string | undefined\n /** Directory for the workbench Vite server's dependency cache. */\n cacheDir: string\n /** CLI-domain `app.id`/`deployment.appId` deprecation check, run before registering. */\n checkForDeprecatedAppId: () => void\n cliConfig: CliConfig\n /** Extract the project manifest to inline into the registry (studio-vs-app handled by the CLI). */\n extractManifest: (params: {\n configPath: string\n workDir: string\n }) => Promise<DevServerManifest['manifest']>\n httpHost: string | undefined\n httpPort: number\n isApp: boolean\n output: Output\n reactStrictMode: boolean\n /** Start the app/studio dev server — the CLI owns the server, this orchestrates it. */\n startAppServer: (params: {\n announceUrl: boolean\n cliConfig: CliConfig\n httpPort: number\n }) => Promise<AppServerResult>\n workDir: string\n}\n\n/**\n * Orchestrate the dev servers a workbench project needs: a singleton workbench\n * Vite server plus the app/studio dev server it renders, wired to the dev-server\n * registry so view/service edits re-sync live.\n *\n * A running workbench claims the configured port, so the app server binds the\n * next one. If the workbench can't start (package or port unavailable), the app\n * server falls back to the configured port and announces its own URL, exactly as\n * a plain `sanity dev` would.\n */\nexport async function startWorkbenchDev(\n options: StartWorkbenchDevOptions,\n): Promise<{close: () => Promise<void>}> {\n const {\n appId,\n cacheDir,\n checkForDeprecatedAppId,\n cliConfig,\n extractManifest,\n httpHost,\n httpPort,\n isApp,\n output,\n reactStrictMode,\n startAppServer,\n workDir,\n } = options\n\n // The remote can't render itself, so it runs as a plain app server (not the\n // shell) that still claims the lock and bridges the registry, so app\n // `sanity dev`s register into it.\n if (process.env.SANITY_INTERNAL_IS_WORKBENCH_REMOTE === 'true') {\n const remote = await startAppServer({announceUrl: true, cliConfig, httpPort})\n if (!remote.started) return {close: async () => {}}\n\n const addr = remote.server.httpServer?.address()\n const port =\n (typeof addr === 'object' && addr ? addr.port : remote.server.config.server.port) ?? httpPort\n const coordinator = startWorkbenchRemoteCoordinator({httpHost, port, server: remote.server})\n\n return {\n close: async () => {\n await coordinator.close()\n await remote.close()\n },\n }\n }\n\n // Unwound in reverse on any failure or on close(): the watcher stops before\n // the app server, and the supervisor waits out an in-flight rebuild.\n const closers: Array<() => Promise<void>> = []\n const disposeAll = async () => {\n for (const close of closers.splice(0).toReversed()) {\n await close().catch(() => {})\n }\n }\n\n const workbench = await startWorkbenchDevServer({\n cacheDir,\n cliConfig,\n httpHost,\n httpPort,\n output,\n reactStrictMode,\n workDir,\n })\n closers.push(workbench.close)\n\n // A running workbench owns the configured port; the app server takes the next.\n // Without one it claims the configured port and announces its own URL.\n const appPort = workbench.workbenchAvailable ? workbench.workbenchPort + 1 : httpPort\n const announceUrl = !workbench.workbenchAvailable\n\n let closing: Promise<void> | undefined\n const close = () => {\n closing ??= (async () => {\n process.off('SIGINT', onSignal)\n process.off('SIGTERM', onSignal)\n await disposeAll()\n })()\n return closing\n }\n\n const supervised = await startAppServerSupervisor({\n cliConfig,\n start: (config) => startAppServer({announceUrl, cliConfig: config, httpPort: appPort}),\n workDir,\n }).catch(async (err) => {\n await disposeAll()\n throw err\n })\n\n if (!supervised.started) {\n // The app server already reported why (e.g. missing organization id). Hand\n // back a close that releases the workbench lock; nothing else came up.\n return {close}\n }\n const {supervisor} = supervised\n closers.push(supervisor.close)\n\n try {\n // The deprecated-id check and manifest extractor are CLI-domain, injected here.\n checkForDeprecatedAppId()\n const registration = await startDevServerRegistration({\n appId,\n cliConfig,\n extractManifest,\n isApp,\n onInterfaceSetChange: () => supervisor.rebuild(),\n output,\n server: supervisor.server,\n workDir,\n })\n closers.push(registration.close)\n } catch (err) {\n // Registration runs after both servers are up; a failure here would leak the\n // workbench lock and dev servers without this teardown.\n await disposeAll()\n throw err\n }\n\n if (workbench.workbenchAvailable) {\n const workbenchUrl = `http://${toDisplayHost(workbench.httpHost)}:${workbench.workbenchPort}`\n const addr = supervisor.server.httpServer?.address()\n const port = typeof addr === 'object' && addr ? addr.port : supervisor.server.config.server.port\n output.log(\n `Workbench dev server started at ${styleText(['blue', 'underline'], workbenchUrl)} (app on port ${port})`,\n )\n }\n\n // Trapping the signal disables Node's default exit, and a finished teardown\n // doesn't guarantee an empty event loop (keep-alive sockets, an extraction\n // worker mid-run) — so re-raise after teardown to restore conventional signal\n // exit semantics. A backstop timer force-exits if teardown wedges.\n function onSignal(signal: NodeJS.Signals) {\n const graceTimer = setTimeout(() => process.kill(process.pid, signal), SHUTDOWN_GRACE_MS)\n graceTimer.unref()\n void close().finally(() => {\n clearTimeout(graceTimer)\n process.kill(process.pid, signal)\n })\n }\n process.once('SIGINT', onSignal)\n process.once('SIGTERM', onSignal)\n\n return {close}\n}\n"],"names":["styleText","startAppServerSupervisor","startDevServerRegistration","startWorkbenchDevServer","startWorkbenchRemoteCoordinator","SHUTDOWN_GRACE_MS","toDisplayHost","host","startWorkbenchDev","options","appId","cacheDir","checkForDeprecatedAppId","cliConfig","extractManifest","httpHost","httpPort","isApp","output","reactStrictMode","startAppServer","workDir","process","env","SANITY_INTERNAL_IS_WORKBENCH_REMOTE","remote","announceUrl","started","close","addr","server","httpServer","address","port","config","coordinator","closers","disposeAll","splice","toReversed","catch","workbench","push","appPort","workbenchAvailable","workbenchPort","closing","off","onSignal","supervised","start","err","supervisor","registration","onInterfaceSetChange","rebuild","workbenchUrl","log","signal","graceTimer","setTimeout","kill","pid","unref","finally","clearTimeout","once"],"mappings":"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"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { isWorkbenchApp, resolveLocalPackage, subdebug } from '@sanity/cli-core';
|
|
2
|
+
import viteReact from '@vitejs/plugin-react';
|
|
3
|
+
import { createServer } from 'vite';
|
|
4
|
+
import { z } from 'zod/mini';
|
|
5
|
+
import { createInterfacesTracker } from './interfaceSetId.js';
|
|
6
|
+
import { acquireWorkbenchLock, getRegisteredServers, readWorkbenchLock, watchRegistry } from './registry.js';
|
|
7
|
+
import { writeWorkbenchRuntime } from './writeWorkbenchRuntime.js';
|
|
8
|
+
const devDebug = subdebug('dev');
|
|
9
|
+
const noop = async ()=>{};
|
|
10
|
+
const toApplicationsPayload = (servers)=>({
|
|
11
|
+
applications: servers.map(({ host, id, interfaces, manifest, port, projectId, type })=>({
|
|
12
|
+
host,
|
|
13
|
+
id,
|
|
14
|
+
interfaces,
|
|
15
|
+
manifest,
|
|
16
|
+
port,
|
|
17
|
+
projectId,
|
|
18
|
+
type
|
|
19
|
+
}))
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Bridge the dev-server registry into a workbench Vite server's HMR channel so
|
|
23
|
+
* the page tracks apps as they come and go. A changed interface set means a
|
|
24
|
+
* rebuilt remote — full-reload to drop the stale remote-entry; otherwise
|
|
25
|
+
* rebroadcast for a soft reconcile. Returns a detach fn.
|
|
26
|
+
*/ function attachViteDevServerBridge(server) {
|
|
27
|
+
server.ws.on('sanity:workbench:get-local-applications', (_, client)=>{
|
|
28
|
+
client.send('sanity:workbench:local-applications', toApplicationsPayload(getRegisteredServers()));
|
|
29
|
+
});
|
|
30
|
+
const setTracker = createInterfacesTracker();
|
|
31
|
+
const registryWatcher = watchRegistry((servers)=>{
|
|
32
|
+
if (setTracker.hasChanged(servers)) {
|
|
33
|
+
server.ws.send({
|
|
34
|
+
type: 'full-reload'
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
server.ws.send('sanity:workbench:local-applications', toApplicationsPayload(servers));
|
|
39
|
+
});
|
|
40
|
+
return ()=>registryWatcher.close();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Make the workbench remote act as the machine's workbench: claim the singleton
|
|
44
|
+
* lock so app `sanity dev`s register into it instead of each starting their own,
|
|
45
|
+
* and bridge the registry so the remote shows the local apps. No-op lock if one
|
|
46
|
+
* is already held.
|
|
47
|
+
*/ export function startWorkbenchRemoteCoordinator(options) {
|
|
48
|
+
const { httpHost, port, server } = options;
|
|
49
|
+
const lock = acquireWorkbenchLock({
|
|
50
|
+
host: httpHost || 'localhost',
|
|
51
|
+
port
|
|
52
|
+
});
|
|
53
|
+
if (!lock) {
|
|
54
|
+
const existing = readWorkbenchLock();
|
|
55
|
+
devDebug('Workbench lock already held by pid %d on port %d; bridging the registry without claiming it', existing?.pid, existing?.port);
|
|
56
|
+
}
|
|
57
|
+
const detachBridge = attachViteDevServerBridge(server);
|
|
58
|
+
return {
|
|
59
|
+
close: async ()=>{
|
|
60
|
+
detachBridge();
|
|
61
|
+
lock?.release();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Start the workbench dev server when federation is enabled and the workbench
|
|
67
|
+
* package is available. If the desired port is already taken — by another
|
|
68
|
+
* workbench instance or an unrelated process — fall back to running without a
|
|
69
|
+
* workbench and let the app/studio dev server claim the configured port.
|
|
70
|
+
*/ export async function startWorkbenchDevServer(options) {
|
|
71
|
+
const { cacheDir, cliConfig, httpHost, httpPort: workbenchPort, output, reactStrictMode, workDir } = options;
|
|
72
|
+
// Workbench is opted into solely by calling `unstable_defineApp`.
|
|
73
|
+
if (!isWorkbenchApp(cliConfig?.app)) {
|
|
74
|
+
devDebug('Not a workbench app, skipping workbench dev server');
|
|
75
|
+
return {
|
|
76
|
+
close: noop,
|
|
77
|
+
httpHost,
|
|
78
|
+
workbenchAvailable: false,
|
|
79
|
+
workbenchPort
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
let workbenchAvailable = false;
|
|
83
|
+
try {
|
|
84
|
+
await resolveLocalPackage('sanity/workbench', workDir);
|
|
85
|
+
workbenchAvailable = true;
|
|
86
|
+
} catch {
|
|
87
|
+
devDebug('Workbench not available, skipping workbench dev server');
|
|
88
|
+
}
|
|
89
|
+
if (!workbenchAvailable) {
|
|
90
|
+
return {
|
|
91
|
+
close: noop,
|
|
92
|
+
httpHost,
|
|
93
|
+
workbenchAvailable,
|
|
94
|
+
workbenchPort
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Acquire an exclusive lock — only one workbench per machine.
|
|
98
|
+
// Uses O_EXCL which is atomic at the OS level, preventing races when
|
|
99
|
+
// multiple `sanity dev` processes start simultaneously (e.g. via turbo).
|
|
100
|
+
const workbenchLock = acquireWorkbenchLock({
|
|
101
|
+
host: httpHost || 'localhost',
|
|
102
|
+
port: workbenchPort
|
|
103
|
+
});
|
|
104
|
+
if (!workbenchLock) {
|
|
105
|
+
const existing = readWorkbenchLock();
|
|
106
|
+
devDebug('Workbench already running at pid %d on port %d, skipping', existing?.pid, existing?.port);
|
|
107
|
+
return {
|
|
108
|
+
close: noop,
|
|
109
|
+
httpHost: existing?.host ?? httpHost,
|
|
110
|
+
workbenchAvailable: true,
|
|
111
|
+
workbenchPort: existing?.port ?? workbenchPort
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// The lock is already held; an exception here (runtime-file write failure,
|
|
115
|
+
// invalid remote URL) would otherwise leak it until the next acquire prunes
|
|
116
|
+
// the stale PID.
|
|
117
|
+
let result;
|
|
118
|
+
try {
|
|
119
|
+
result = await createWorkbenchViteServer({
|
|
120
|
+
cacheDir,
|
|
121
|
+
cliConfig,
|
|
122
|
+
httpHost,
|
|
123
|
+
output,
|
|
124
|
+
reactStrictMode,
|
|
125
|
+
workbenchPort,
|
|
126
|
+
workDir
|
|
127
|
+
});
|
|
128
|
+
} catch (err) {
|
|
129
|
+
workbenchLock.release();
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
if (!result) {
|
|
133
|
+
workbenchLock.release();
|
|
134
|
+
return {
|
|
135
|
+
close: noop,
|
|
136
|
+
httpHost,
|
|
137
|
+
workbenchAvailable: false,
|
|
138
|
+
workbenchPort
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const { actualPort, close } = result;
|
|
142
|
+
workbenchLock.updatePort(actualPort);
|
|
143
|
+
return {
|
|
144
|
+
close: async ()=>{
|
|
145
|
+
workbenchLock.release();
|
|
146
|
+
await close();
|
|
147
|
+
},
|
|
148
|
+
httpHost,
|
|
149
|
+
workbenchAvailable,
|
|
150
|
+
workbenchPort: actualPort
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function createWorkbenchViteServer(options) {
|
|
154
|
+
const { cacheDir, cliConfig, httpHost, output, reactStrictMode, workbenchPort, workDir } = options;
|
|
155
|
+
const remoteUrl = parseRemoteUrl(process.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL);
|
|
156
|
+
const organizationId = resolveOrganizationId(cliConfig);
|
|
157
|
+
devDebug('Writing workbench runtime files');
|
|
158
|
+
const root = await writeWorkbenchRuntime({
|
|
159
|
+
cwd: workDir,
|
|
160
|
+
organizationId,
|
|
161
|
+
reactStrictMode,
|
|
162
|
+
remoteUrl
|
|
163
|
+
});
|
|
164
|
+
const viteConfig = {
|
|
165
|
+
// Custom cache directory so sanity's vite cache doesn't conflict with local vite projects
|
|
166
|
+
cacheDir,
|
|
167
|
+
configFile: false,
|
|
168
|
+
define: {
|
|
169
|
+
__SANITY_STAGING__: process.env.SANITY_INTERNAL_ENV === 'staging',
|
|
170
|
+
'import.meta.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL': JSON.stringify(remoteUrl)
|
|
171
|
+
},
|
|
172
|
+
logLevel: 'warn',
|
|
173
|
+
mode: 'development',
|
|
174
|
+
optimizeDeps: {
|
|
175
|
+
// Exclude sanity/workbench (and its transitive dep @sanity/workbench)
|
|
176
|
+
// from dep pre-bundling so that `import.meta.hot` is available at
|
|
177
|
+
// runtime — pre-bundled modules do not receive Vite's HMR client
|
|
178
|
+
// injection, which causes the custom HMR events for local application
|
|
179
|
+
// discovery to silently not fire.
|
|
180
|
+
exclude: [
|
|
181
|
+
'sanity',
|
|
182
|
+
'@sanity/workbench'
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
// viteReact looks inert here — it transforms none of the host's own modules —
|
|
186
|
+
// but it's load-bearing for the remotes. It serves the Fast Refresh runtime at
|
|
187
|
+
// /@react-refresh and injects the preamble that defines window.$RefreshReg$. The
|
|
188
|
+
// federated remotes loaded into this page are react-refresh transformed, so
|
|
189
|
+
// without the preamble they throw "can't detect preamble", and without the
|
|
190
|
+
// runtime their /@react-refresh import (wired by @module-federation/vite's
|
|
191
|
+
// remoteHmr) fails. Dropping it as dead code broke every panel; see #1262.
|
|
192
|
+
plugins: [
|
|
193
|
+
viteReact(),
|
|
194
|
+
...remoteUrl ? [
|
|
195
|
+
remoteManifestPreloadHeaderPlugin(remoteUrl)
|
|
196
|
+
] : []
|
|
197
|
+
],
|
|
198
|
+
resolve: {
|
|
199
|
+
dedupe: [
|
|
200
|
+
'react',
|
|
201
|
+
'react-dom'
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
root,
|
|
205
|
+
server: {
|
|
206
|
+
host: httpHost,
|
|
207
|
+
port: workbenchPort,
|
|
208
|
+
strictPort: false,
|
|
209
|
+
warmup: {
|
|
210
|
+
clientFiles: [
|
|
211
|
+
'./workbench.js'
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
devDebug('Creating workbench vite server');
|
|
217
|
+
const server = await createServer(viteConfig);
|
|
218
|
+
try {
|
|
219
|
+
await server.listen();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
await server.close();
|
|
222
|
+
output.warn(`Workbench dev server failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
// Vite may have picked a different port if the desired one was occupied
|
|
226
|
+
const addr = server.httpServer?.address();
|
|
227
|
+
const actualPort = typeof addr === 'object' && addr ? addr.port : workbenchPort;
|
|
228
|
+
// Fire-and-forget: warm the workbench remote's Vite transform pipeline so
|
|
229
|
+
// the first browser request hits a pre-populated module graph.
|
|
230
|
+
if (remoteUrl) {
|
|
231
|
+
fetch(remoteUrl).then((r)=>r.body?.cancel()).catch(()=>{});
|
|
232
|
+
devDebug('Warming workbench remote at %s', remoteUrl);
|
|
233
|
+
}
|
|
234
|
+
const detachBridge = attachViteDevServerBridge(server);
|
|
235
|
+
return {
|
|
236
|
+
actualPort,
|
|
237
|
+
close: async ()=>{
|
|
238
|
+
detachBridge();
|
|
239
|
+
await server.close();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// Workbench is opted into via `unstable_defineApp`, which carries the
|
|
244
|
+
// organization ID. Deliberately no fallback (e.g. resolving it from the
|
|
245
|
+
// configured project): the lookup would need an authenticated user and an
|
|
246
|
+
// API round-trip on every startup for something the opt-in already declares.
|
|
247
|
+
const resolveOrganizationId = (cliConfig)=>{
|
|
248
|
+
if (cliConfig.app?.organizationId) {
|
|
249
|
+
return cliConfig.app.organizationId;
|
|
250
|
+
}
|
|
251
|
+
throw new Error('Workbench requires an organization ID. Pass "organizationId" to unstable_defineApp() in sanity.cli.ts.');
|
|
252
|
+
};
|
|
253
|
+
// Restricts protocol to http(s) so the URL is safe to interpolate into HTML
|
|
254
|
+
// attributes and Link headers downstream.
|
|
255
|
+
const remoteUrlSchema = z.url({
|
|
256
|
+
normalize: true,
|
|
257
|
+
protocol: /^https?$/
|
|
258
|
+
});
|
|
259
|
+
function parseRemoteUrl(value) {
|
|
260
|
+
if (!value) return undefined;
|
|
261
|
+
const result = remoteUrlSchema.safeParse(value);
|
|
262
|
+
if (!result.success) {
|
|
263
|
+
throw new Error(`Invalid SANITY_INTERNAL_WORKBENCH_REMOTE_URL: ${value} (must be an http(s) URL)`);
|
|
264
|
+
}
|
|
265
|
+
return result.data;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Sets a `Link: <remoteUrl>; rel=preload; as=fetch; crossorigin` response header
|
|
269
|
+
* on the index document so the browser can start fetching the Module Federation
|
|
270
|
+
* manifest as soon as response headers arrive — before HTML parsing reaches the
|
|
271
|
+
* in-head preconnect hint. `as=fetch` matches how the federation runtime later
|
|
272
|
+
* retrieves the JSON manifest, allowing the preload entry to satisfy that fetch.
|
|
273
|
+
*/ function remoteManifestPreloadHeaderPlugin(remoteUrl) {
|
|
274
|
+
return {
|
|
275
|
+
apply: 'serve',
|
|
276
|
+
configureServer (server) {
|
|
277
|
+
server.middlewares.use((req, res, next)=>{
|
|
278
|
+
const pathname = (req.url || '/').split('?')[0];
|
|
279
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
280
|
+
res.setHeader('Link', `<${remoteUrl}>; rel=preload; as=fetch; crossorigin`);
|
|
281
|
+
}
|
|
282
|
+
next();
|
|
283
|
+
});
|
|
284
|
+
},
|
|
285
|
+
name: 'sanity:workbench-remote-preload-header'
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
//# sourceMappingURL=startWorkbenchDevServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/startWorkbenchDevServer.ts"],"sourcesContent":["import {\n type CliConfig,\n isWorkbenchApp,\n type Output,\n resolveLocalPackage,\n subdebug,\n} from '@sanity/cli-core'\nimport viteReact from '@vitejs/plugin-react'\nimport {createServer, type InlineConfig, type Plugin, type ViteDevServer} from 'vite'\nimport {z} from 'zod/mini'\n\nimport {createInterfacesTracker} from './interfaceSetId.js'\nimport {\n acquireWorkbenchLock,\n type DevServerManifest,\n getRegisteredServers,\n readWorkbenchLock,\n watchRegistry,\n} from './registry.js'\nimport {writeWorkbenchRuntime} from './writeWorkbenchRuntime.js'\n\nconst devDebug = subdebug('dev')\n\nconst noop = async () => {}\n\nconst toApplicationsPayload = (servers: DevServerManifest[]) => ({\n applications: servers.map(({host, id, interfaces, manifest, port, projectId, type}) => ({\n host,\n id,\n interfaces,\n manifest,\n port,\n projectId,\n type,\n })),\n})\n\n/**\n * Bridge the dev-server registry into a workbench Vite server's HMR channel so\n * the page tracks apps as they come and go. A changed interface set means a\n * rebuilt remote — full-reload to drop the stale remote-entry; otherwise\n * rebroadcast for a soft reconcile. Returns a detach fn.\n */\nfunction attachViteDevServerBridge(server: ViteDevServer): () => void {\n server.ws.on('sanity:workbench:get-local-applications', (_, client) => {\n client.send(\n 'sanity:workbench:local-applications',\n toApplicationsPayload(getRegisteredServers()),\n )\n })\n\n const setTracker = createInterfacesTracker()\n const registryWatcher = watchRegistry((servers) => {\n if (setTracker.hasChanged(servers)) {\n server.ws.send({type: 'full-reload'})\n return\n }\n server.ws.send('sanity:workbench:local-applications', toApplicationsPayload(servers))\n })\n\n return () => registryWatcher.close()\n}\n\n/**\n * Make the workbench remote act as the machine's workbench: claim the singleton\n * lock so app `sanity dev`s register into it instead of each starting their own,\n * and bridge the registry so the remote shows the local apps. No-op lock if one\n * is already held.\n */\nexport function startWorkbenchRemoteCoordinator(options: {\n httpHost: string | undefined\n port: number\n server: ViteDevServer\n}): {close: () => Promise<void>} {\n const {httpHost, port, server} = options\n\n const lock = acquireWorkbenchLock({host: httpHost || 'localhost', port})\n if (!lock) {\n const existing = readWorkbenchLock()\n devDebug(\n 'Workbench lock already held by pid %d on port %d; bridging the registry without claiming it',\n existing?.pid,\n existing?.port,\n )\n }\n\n const detachBridge = attachViteDevServerBridge(server)\n\n return {\n close: async () => {\n detachBridge()\n lock?.release()\n },\n }\n}\n\ninterface WorkbenchDevServerResult {\n close: () => Promise<void>\n httpHost: string | undefined\n workbenchAvailable: boolean\n workbenchPort: number\n}\n\nexport interface StartWorkbenchOptions {\n /** Dependency-cache dir for the workbench Vite server, kept apart from the user's own. */\n cacheDir: string\n cliConfig: CliConfig\n httpHost: string | undefined\n httpPort: number\n output: Output\n /** Wrap the workbench in React StrictMode; the CLI resolves it (unset collapses to `false`). */\n reactStrictMode: boolean\n workDir: string\n}\n\n/**\n * Start the workbench dev server when federation is enabled and the workbench\n * package is available. If the desired port is already taken — by another\n * workbench instance or an unrelated process — fall back to running without a\n * workbench and let the app/studio dev server claim the configured port.\n */\nexport async function startWorkbenchDevServer(\n options: StartWorkbenchOptions,\n): Promise<WorkbenchDevServerResult> {\n const {\n cacheDir,\n cliConfig,\n httpHost,\n httpPort: workbenchPort,\n output,\n reactStrictMode,\n workDir,\n } = options\n\n // Workbench is opted into solely by calling `unstable_defineApp`.\n if (!isWorkbenchApp(cliConfig?.app)) {\n devDebug('Not a workbench app, skipping workbench dev server')\n return {close: noop, httpHost, workbenchAvailable: false, workbenchPort}\n }\n\n let workbenchAvailable = false\n\n try {\n await resolveLocalPackage('sanity/workbench', workDir)\n workbenchAvailable = true\n } catch {\n devDebug('Workbench not available, skipping workbench dev server')\n }\n\n if (!workbenchAvailable) {\n return {close: noop, httpHost, workbenchAvailable, workbenchPort}\n }\n\n // Acquire an exclusive lock — only one workbench per machine.\n // Uses O_EXCL which is atomic at the OS level, preventing races when\n // multiple `sanity dev` processes start simultaneously (e.g. via turbo).\n const workbenchLock = acquireWorkbenchLock({host: httpHost || 'localhost', port: workbenchPort})\n if (!workbenchLock) {\n const existing = readWorkbenchLock()\n devDebug(\n 'Workbench already running at pid %d on port %d, skipping',\n existing?.pid,\n existing?.port,\n )\n return {\n close: noop,\n httpHost: existing?.host ?? httpHost,\n workbenchAvailable: true,\n workbenchPort: existing?.port ?? workbenchPort,\n }\n }\n\n // The lock is already held; an exception here (runtime-file write failure,\n // invalid remote URL) would otherwise leak it until the next acquire prunes\n // the stale PID.\n let result: Awaited<ReturnType<typeof createWorkbenchViteServer>>\n try {\n result = await createWorkbenchViteServer({\n cacheDir,\n cliConfig,\n httpHost,\n output,\n reactStrictMode,\n workbenchPort,\n workDir,\n })\n } catch (err) {\n workbenchLock.release()\n throw err\n }\n\n if (!result) {\n workbenchLock.release()\n return {close: noop, httpHost, workbenchAvailable: false, workbenchPort}\n }\n\n const {actualPort, close} = result\n workbenchLock.updatePort(actualPort)\n\n return {\n close: async () => {\n workbenchLock.release()\n await close()\n },\n httpHost,\n workbenchAvailable,\n workbenchPort: actualPort,\n }\n}\n\ninterface CreateWorkbenchViteServerOptions {\n cacheDir: string\n cliConfig: CliConfig\n httpHost: string | undefined\n output: Output\n reactStrictMode: boolean\n workbenchPort: number\n workDir: string\n}\n\ninterface CreateWorkbenchViteServerResult {\n actualPort: number\n close: () => Promise<void>\n}\n\nasync function createWorkbenchViteServer(\n options: CreateWorkbenchViteServerOptions,\n): Promise<CreateWorkbenchViteServerResult | undefined> {\n const {cacheDir, cliConfig, httpHost, output, reactStrictMode, workbenchPort, workDir} = options\n\n const remoteUrl = parseRemoteUrl(process.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL)\n\n const organizationId = resolveOrganizationId(cliConfig)\n\n devDebug('Writing workbench runtime files')\n const root = await writeWorkbenchRuntime({\n cwd: workDir,\n organizationId,\n reactStrictMode,\n remoteUrl,\n })\n\n const viteConfig: InlineConfig = {\n // Custom cache directory so sanity's vite cache doesn't conflict with local vite projects\n cacheDir,\n configFile: false,\n define: {\n __SANITY_STAGING__: process.env.SANITY_INTERNAL_ENV === 'staging',\n 'import.meta.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL': JSON.stringify(remoteUrl),\n },\n logLevel: 'warn',\n mode: 'development',\n optimizeDeps: {\n // Exclude sanity/workbench (and its transitive dep @sanity/workbench)\n // from dep pre-bundling so that `import.meta.hot` is available at\n // runtime — pre-bundled modules do not receive Vite's HMR client\n // injection, which causes the custom HMR events for local application\n // discovery to silently not fire.\n exclude: ['sanity', '@sanity/workbench'],\n },\n // viteReact looks inert here — it transforms none of the host's own modules —\n // but it's load-bearing for the remotes. It serves the Fast Refresh runtime at\n // /@react-refresh and injects the preamble that defines window.$RefreshReg$. The\n // federated remotes loaded into this page are react-refresh transformed, so\n // without the preamble they throw \"can't detect preamble\", and without the\n // runtime their /@react-refresh import (wired by @module-federation/vite's\n // remoteHmr) fails. Dropping it as dead code broke every panel; see #1262.\n plugins: [viteReact(), ...(remoteUrl ? [remoteManifestPreloadHeaderPlugin(remoteUrl)] : [])],\n resolve: {dedupe: ['react', 'react-dom']},\n root,\n server: {\n host: httpHost,\n port: workbenchPort,\n strictPort: false,\n warmup: {\n clientFiles: ['./workbench.js'],\n },\n },\n }\n\n devDebug('Creating workbench vite server')\n const server = await createServer(viteConfig)\n try {\n await server.listen()\n } catch (err) {\n await server.close()\n output.warn(\n `Workbench dev server failed to start: ${err instanceof Error ? err.message : String(err)}`,\n )\n return undefined\n }\n\n // Vite may have picked a different port if the desired one was occupied\n const addr = server.httpServer?.address()\n const actualPort = typeof addr === 'object' && addr ? addr.port : workbenchPort\n\n // Fire-and-forget: warm the workbench remote's Vite transform pipeline so\n // the first browser request hits a pre-populated module graph.\n if (remoteUrl) {\n fetch(remoteUrl)\n .then((r) => r.body?.cancel())\n .catch(() => {})\n devDebug('Warming workbench remote at %s', remoteUrl)\n }\n\n const detachBridge = attachViteDevServerBridge(server)\n\n return {\n actualPort,\n close: async () => {\n detachBridge()\n await server.close()\n },\n }\n}\n\n// Workbench is opted into via `unstable_defineApp`, which carries the\n// organization ID. Deliberately no fallback (e.g. resolving it from the\n// configured project): the lookup would need an authenticated user and an\n// API round-trip on every startup for something the opt-in already declares.\nconst resolveOrganizationId = (cliConfig: CliConfig): string => {\n if (cliConfig.app?.organizationId) {\n return cliConfig.app.organizationId\n }\n\n throw new Error(\n 'Workbench requires an organization ID. Pass \"organizationId\" to unstable_defineApp() in sanity.cli.ts.',\n )\n}\n\n// Restricts protocol to http(s) so the URL is safe to interpolate into HTML\n// attributes and Link headers downstream.\nconst remoteUrlSchema = z.url({normalize: true, protocol: /^https?$/})\n\nfunction parseRemoteUrl(value: string | undefined): string | undefined {\n if (!value) return undefined\n\n const result = remoteUrlSchema.safeParse(value)\n\n if (!result.success) {\n throw new Error(\n `Invalid SANITY_INTERNAL_WORKBENCH_REMOTE_URL: ${value} (must be an http(s) URL)`,\n )\n }\n\n return result.data\n}\n\n/**\n * Sets a `Link: <remoteUrl>; rel=preload; as=fetch; crossorigin` response header\n * on the index document so the browser can start fetching the Module Federation\n * manifest as soon as response headers arrive — before HTML parsing reaches the\n * in-head preconnect hint. `as=fetch` matches how the federation runtime later\n * retrieves the JSON manifest, allowing the preload entry to satisfy that fetch.\n */\nfunction remoteManifestPreloadHeaderPlugin(remoteUrl: string): Plugin {\n return {\n apply: 'serve',\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const pathname = (req.url || '/').split('?')[0]\n if (pathname === '/' || pathname === '/index.html') {\n res.setHeader('Link', `<${remoteUrl}>; rel=preload; as=fetch; crossorigin`)\n }\n next()\n })\n },\n name: 'sanity:workbench-remote-preload-header',\n }\n}\n"],"names":["isWorkbenchApp","resolveLocalPackage","subdebug","viteReact","createServer","z","createInterfacesTracker","acquireWorkbenchLock","getRegisteredServers","readWorkbenchLock","watchRegistry","writeWorkbenchRuntime","devDebug","noop","toApplicationsPayload","servers","applications","map","host","id","interfaces","manifest","port","projectId","type","attachViteDevServerBridge","server","ws","on","_","client","send","setTracker","registryWatcher","hasChanged","close","startWorkbenchRemoteCoordinator","options","httpHost","lock","existing","pid","detachBridge","release","startWorkbenchDevServer","cacheDir","cliConfig","httpPort","workbenchPort","output","reactStrictMode","workDir","app","workbenchAvailable","workbenchLock","result","createWorkbenchViteServer","err","actualPort","updatePort","remoteUrl","parseRemoteUrl","process","env","SANITY_INTERNAL_WORKBENCH_REMOTE_URL","organizationId","resolveOrganizationId","root","cwd","viteConfig","configFile","define","__SANITY_STAGING__","SANITY_INTERNAL_ENV","JSON","stringify","logLevel","mode","optimizeDeps","exclude","plugins","remoteManifestPreloadHeaderPlugin","resolve","dedupe","strictPort","warmup","clientFiles","listen","warn","Error","message","String","undefined","addr","httpServer","address","fetch","then","r","body","cancel","catch","remoteUrlSchema","url","normalize","protocol","value","safeParse","success","data","apply","configureServer","middlewares","use","req","res","next","pathname","split","setHeader","name"],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { subdebug } from '@sanity/cli-core';
|
|
4
|
+
const devDebug = subdebug('dev');
|
|
5
|
+
const workbenchJsTemplate = `\
|
|
6
|
+
// This file is auto-generated on 'sanity dev'
|
|
7
|
+
// Modifications to this file are automatically discarded
|
|
8
|
+
import {renderWorkbench} from "sanity/workbench"
|
|
9
|
+
|
|
10
|
+
renderWorkbench(
|
|
11
|
+
document.getElementById("workbench"),
|
|
12
|
+
{organizationId: %SANITY_WORKBENCH_ORGANIZATION_ID%},
|
|
13
|
+
{reactStrictMode: %SANITY_WORKBENCH_REACT_STRICT_MODE%}
|
|
14
|
+
)
|
|
15
|
+
`;
|
|
16
|
+
const indexHtmlTemplate = `\
|
|
17
|
+
<!DOCTYPE html>
|
|
18
|
+
<!-- This file is auto-generated on 'sanity dev' -->
|
|
19
|
+
<!-- Modifications to this file are automatically discarded -->
|
|
20
|
+
<html>
|
|
21
|
+
<head>
|
|
22
|
+
<meta charset="UTF-8" />
|
|
23
|
+
%SANITY_WORKBENCH_PREFETCH_HINTS%
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<div id="workbench"></div>
|
|
27
|
+
<script type="module" src="./workbench.js"></script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
`;
|
|
31
|
+
/**
|
|
32
|
+
* Generates the `.sanity/workbench` directory with static entry files for
|
|
33
|
+
* the workbench Vite dev server.
|
|
34
|
+
*
|
|
35
|
+
* @param cwd - Current working directory (Sanity root dir)
|
|
36
|
+
* @returns The absolute path to the written workbench runtime directory
|
|
37
|
+
* @internal
|
|
38
|
+
*/ export async function writeWorkbenchRuntime(options) {
|
|
39
|
+
const { cwd, organizationId, reactStrictMode, remoteUrl } = options;
|
|
40
|
+
const workbenchDir = path.join(cwd, '.sanity', 'workbench');
|
|
41
|
+
const workbenchJs = workbenchJsTemplate.replace(/%SANITY_WORKBENCH_ORGANIZATION_ID%/, organizationId === undefined ? 'undefined' : JSON.stringify(organizationId)).replace(/%SANITY_WORKBENCH_REACT_STRICT_MODE%/, JSON.stringify(reactStrictMode));
|
|
42
|
+
const prefetchHints = buildPrefetchHints(remoteUrl);
|
|
43
|
+
const indexHtml = indexHtmlTemplate.replace(/%SANITY_WORKBENCH_PREFETCH_HINTS%/, prefetchHints);
|
|
44
|
+
devDebug('Making workbench runtime directory');
|
|
45
|
+
await fs.mkdir(workbenchDir, {
|
|
46
|
+
recursive: true
|
|
47
|
+
});
|
|
48
|
+
devDebug('Writing workbench.js to workbench runtime directory');
|
|
49
|
+
await fs.writeFile(path.join(workbenchDir, 'workbench.js'), workbenchJs);
|
|
50
|
+
devDebug('Writing index.html to workbench runtime directory');
|
|
51
|
+
await fs.writeFile(path.join(workbenchDir, 'index.html'), indexHtml);
|
|
52
|
+
return workbenchDir;
|
|
53
|
+
}
|
|
54
|
+
function buildPrefetchHints(remoteUrl) {
|
|
55
|
+
if (!remoteUrl) return '';
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(remoteUrl);
|
|
58
|
+
return [
|
|
59
|
+
` <link rel="preconnect" href="${url.origin}" />`,
|
|
60
|
+
` <link rel="preload" as="fetch" href="${url.toString()}" crossorigin />`
|
|
61
|
+
].join('\n');
|
|
62
|
+
} catch {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=writeWorkbenchRuntime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/writeWorkbenchRuntime.ts"],"sourcesContent":["import fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport {subdebug} from '@sanity/cli-core'\n\nconst devDebug = subdebug('dev')\n\nconst workbenchJsTemplate = `\\\n// This file is auto-generated on 'sanity dev'\n// Modifications to this file are automatically discarded\nimport {renderWorkbench} from \"sanity/workbench\"\n\nrenderWorkbench(\n document.getElementById(\"workbench\"),\n {organizationId: %SANITY_WORKBENCH_ORGANIZATION_ID%},\n {reactStrictMode: %SANITY_WORKBENCH_REACT_STRICT_MODE%}\n)\n`\n\nconst indexHtmlTemplate = `\\\n<!DOCTYPE html>\n<!-- This file is auto-generated on 'sanity dev' -->\n<!-- Modifications to this file are automatically discarded -->\n<html>\n <head>\n <meta charset=\"UTF-8\" />\n%SANITY_WORKBENCH_PREFETCH_HINTS%\n </head>\n <body>\n <div id=\"workbench\"></div>\n <script type=\"module\" src=\"./workbench.js\"></script>\n </body>\n</html>\n`\n\n/**\n * Generates the `.sanity/workbench` directory with static entry files for\n * the workbench Vite dev server.\n *\n * @param cwd - Current working directory (Sanity root dir)\n * @returns The absolute path to the written workbench runtime directory\n * @internal\n */\nexport async function writeWorkbenchRuntime(options: {\n cwd: string\n organizationId?: string\n reactStrictMode: boolean\n remoteUrl?: string\n}): Promise<string> {\n const {cwd, organizationId, reactStrictMode, remoteUrl} = options\n const workbenchDir = path.join(cwd, '.sanity', 'workbench')\n\n const workbenchJs = workbenchJsTemplate\n .replace(\n /%SANITY_WORKBENCH_ORGANIZATION_ID%/,\n organizationId === undefined ? 'undefined' : JSON.stringify(organizationId),\n )\n .replace(/%SANITY_WORKBENCH_REACT_STRICT_MODE%/, JSON.stringify(reactStrictMode))\n\n const prefetchHints = buildPrefetchHints(remoteUrl)\n\n const indexHtml = indexHtmlTemplate.replace(/%SANITY_WORKBENCH_PREFETCH_HINTS%/, prefetchHints)\n\n devDebug('Making workbench runtime directory')\n await fs.mkdir(workbenchDir, {recursive: true})\n\n devDebug('Writing workbench.js to workbench runtime directory')\n await fs.writeFile(path.join(workbenchDir, 'workbench.js'), workbenchJs)\n\n devDebug('Writing index.html to workbench runtime directory')\n await fs.writeFile(path.join(workbenchDir, 'index.html'), indexHtml)\n\n return workbenchDir\n}\n\nfunction buildPrefetchHints(remoteUrl: string | undefined): string {\n if (!remoteUrl) return ''\n\n try {\n const url = new URL(remoteUrl)\n return [\n ` <link rel=\"preconnect\" href=\"${url.origin}\" />`,\n ` <link rel=\"preload\" as=\"fetch\" href=\"${url.toString()}\" crossorigin />`,\n ].join('\\n')\n } catch {\n return ''\n }\n}\n"],"names":["fs","path","subdebug","devDebug","workbenchJsTemplate","indexHtmlTemplate","writeWorkbenchRuntime","options","cwd","organizationId","reactStrictMode","remoteUrl","workbenchDir","join","workbenchJs","replace","undefined","JSON","stringify","prefetchHints","buildPrefetchHints","indexHtml","mkdir","recursive","writeFile","url","URL","origin","toString"],"mappings":"AAAA,OAAOA,QAAQ,mBAAkB;AACjC,OAAOC,UAAU,YAAW;AAE5B,SAAQC,QAAQ,QAAO,mBAAkB;AAEzC,MAAMC,WAAWD,SAAS;AAE1B,MAAME,sBAAsB,CAAC;;;;;;;;;;AAU7B,CAAC;AAED,MAAMC,oBAAoB,CAAC;;;;;;;;;;;;;;AAc3B,CAAC;AAED;;;;;;;CAOC,GACD,OAAO,eAAeC,sBAAsBC,OAK3C;IACC,MAAM,EAACC,GAAG,EAAEC,cAAc,EAAEC,eAAe,EAAEC,SAAS,EAAC,GAAGJ;IAC1D,MAAMK,eAAeX,KAAKY,IAAI,CAACL,KAAK,WAAW;IAE/C,MAAMM,cAAcV,oBACjBW,OAAO,CACN,sCACAN,mBAAmBO,YAAY,cAAcC,KAAKC,SAAS,CAACT,iBAE7DM,OAAO,CAAC,wCAAwCE,KAAKC,SAAS,CAACR;IAElE,MAAMS,gBAAgBC,mBAAmBT;IAEzC,MAAMU,YAAYhB,kBAAkBU,OAAO,CAAC,qCAAqCI;IAEjFhB,SAAS;IACT,MAAMH,GAAGsB,KAAK,CAACV,cAAc;QAACW,WAAW;IAAI;IAE7CpB,SAAS;IACT,MAAMH,GAAGwB,SAAS,CAACvB,KAAKY,IAAI,CAACD,cAAc,iBAAiBE;IAE5DX,SAAS;IACT,MAAMH,GAAGwB,SAAS,CAACvB,KAAKY,IAAI,CAACD,cAAc,eAAeS;IAE1D,OAAOT;AACT;AAEA,SAASQ,mBAAmBT,SAA6B;IACvD,IAAI,CAACA,WAAW,OAAO;IAEvB,IAAI;QACF,MAAMc,MAAM,IAAIC,IAAIf;QACpB,OAAO;YACL,CAAC,iCAAiC,EAAEc,IAAIE,MAAM,CAAC,IAAI,CAAC;YACpD,CAAC,yCAAyC,EAAEF,IAAIG,QAAQ,GAAG,gBAAgB,CAAC;SAC7E,CAACf,IAAI,CAAC;IACT,EAAE,OAAM;QACN,OAAO;IACT;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/workbench-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Internal implementation detail of the Sanity CLI's unstable workbench support. Not intended for direct use.",
|
|
5
5
|
"homepage": "https://github.com/sanity-io/cli",
|
|
6
6
|
"bugs": "https://github.com/sanity-io/cli/issues",
|
|
@@ -45,10 +45,11 @@
|
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@module-federation/vite": "1.16.
|
|
49
|
-
"
|
|
48
|
+
"@module-federation/vite": "1.16.11",
|
|
49
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
50
|
+
"vite": "^8.1.0",
|
|
50
51
|
"zod": "^4.4.3",
|
|
51
|
-
"@sanity/cli-core": "^2.1.
|
|
52
|
+
"@sanity/cli-core": "^2.1.1"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@eslint/compat": "^2.1.0",
|