@sanity/workbench-cli 2.0.2 → 2.2.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/README.md +1 -1
- package/dist/_exports/_internal_render.d.ts +12 -0
- package/dist/_exports/_internal_render.js +3 -0
- package/dist/_exports/_internal_render.js.map +1 -0
- package/dist/_exports/build.d.ts +4 -4
- package/dist/_exports/build.js +2 -1
- package/dist/_exports/build.js.map +1 -1
- package/dist/_exports/defineApp-DldjQwq6.d.ts +375 -0
- package/dist/_exports/deploy.d.ts +10 -9
- package/dist/_exports/deploy.js +1 -0
- package/dist/_exports/deploy.js.map +1 -1
- package/dist/_exports/dev.d.ts +1 -1
- package/dist/_exports/index.d.ts +23 -16
- package/dist/_exports/index.js +2 -1
- package/dist/_exports/index.js.map +1 -1
- package/dist/_exports/preview.d.ts +1 -1
- package/dist/_exports/{registry-DI7hnTof.d.ts → registry-HE_dEHFa.d.ts} +38 -15
- package/dist/_exports/resolveWorkbenchConfig-6iznHbED.d.ts +60 -0
- package/dist/_exports/{summarizeInterfaces-DBGiAwNT.d.ts → summarizeInterfaces-CjkeWwkY.d.ts} +20 -17
- package/dist/_exports/undeploy.d.ts +12 -9
- package/dist/actions/build/artifact.js +1 -1
- package/dist/actions/build/artifact.js.map +1 -1
- package/dist/actions/build/views/artifact.js +1 -1
- package/dist/actions/build/views/artifact.js.map +1 -1
- package/dist/actions/build/vite/optimize-deps.js +3 -3
- package/dist/actions/build/vite/optimize-deps.js.map +1 -1
- package/dist/actions/build/vite/plugins/plugin-module-federation.js +8 -17
- package/dist/actions/build/vite/plugins/plugin-module-federation.js.map +1 -1
- package/dist/actions/build/vite/plugins/plugin-sanity-federation-runtime.js +1 -1
- package/dist/actions/build/vite/plugins/plugin-sanity-federation-runtime.js.map +1 -1
- package/dist/actions/build/vite/workbench-vite-plugins.js +1 -1
- package/dist/actions/build/vite/workbench-vite-plugins.js.map +1 -1
- package/dist/actions/deploy/deployPayload.js +0 -3
- package/dist/actions/deploy/deployPayload.js.map +1 -1
- package/dist/actions/deploy/deployWorkbenchApp.js +46 -4
- package/dist/actions/deploy/deployWorkbenchApp.js.map +1 -1
- package/dist/actions/deploy/getWorkbench.js +4 -5
- package/dist/actions/deploy/getWorkbench.js.map +1 -1
- package/dist/actions/deploy/summarizeInterfaces.js +14 -10
- package/dist/actions/deploy/summarizeInterfaces.js.map +1 -1
- package/dist/actions/deploy/viewDeployment.js +17 -5
- package/dist/actions/deploy/viewDeployment.js.map +1 -1
- package/dist/actions/dev/deriveConfigs.js +9 -6
- package/dist/actions/dev/deriveConfigs.js.map +1 -1
- package/dist/actions/dev/exposesSetId.js +2 -2
- package/dist/actions/dev/exposesSetId.js.map +1 -1
- package/dist/actions/dev/registry.js +31 -25
- package/dist/actions/dev/registry.js.map +1 -1
- package/dist/actions/dev/startDevManifestWatcher.js.map +1 -1
- package/dist/actions/dev/startDevServerRegistration.js +35 -60
- package/dist/actions/dev/startDevServerRegistration.js.map +1 -1
- package/dist/actions/dev/startWorkbenchDevServer.js +15 -33
- package/dist/actions/dev/startWorkbenchDevServer.js.map +1 -1
- package/dist/actions/dev/writeWorkbenchRuntime.js +2 -2
- package/dist/actions/dev/writeWorkbenchRuntime.js.map +1 -1
- package/dist/actions/preview/serveBuiltApplication.js +1 -1
- package/dist/actions/preview/serveBuiltApplication.js.map +1 -1
- package/dist/actions/preview/startWorkbenchPreview.js +1 -1
- package/dist/actions/preview/startWorkbenchPreview.js.map +1 -1
- package/dist/actions/undeploy/workbenchUndeployAdapter.js +24 -33
- package/dist/actions/undeploy/workbenchUndeployAdapter.js.map +1 -1
- package/dist/appId.js +13 -9
- package/dist/appId.js.map +1 -1
- package/dist/applicationReference.js +12 -0
- package/dist/applicationReference.js.map +1 -0
- package/dist/contract.js +69 -31
- package/dist/contract.js.map +1 -1
- package/dist/defineApp.js +53 -63
- package/dist/defineApp.js.map +1 -1
- package/dist/defineView.js +3 -3
- package/dist/defineView.js.map +1 -1
- package/dist/deriveInterfaces.js +45 -19
- package/dist/deriveInterfaces.js.map +1 -1
- package/dist/renderDashboard.js +63 -0
- package/dist/renderDashboard.js.map +1 -0
- package/dist/resolveWorkbenchApp.js +13 -9
- package/dist/resolveWorkbenchApp.js.map +1 -1
- package/dist/resolveWorkbenchConfig.js +30 -0
- package/dist/resolveWorkbenchConfig.js.map +1 -0
- package/dist/services/applications.js +7 -1
- package/dist/services/applications.js.map +1 -1
- package/package.json +12 -6
- package/dist/_exports/contract-DyG11fQ7.d.ts +0 -45
- package/dist/_exports/defineApp-r_CmoZfS.d.ts +0 -169
- package/dist/_exports/resolveWorkbenchApp-DqFqET9E.d.ts +0 -41
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/actions/dev/startDevManifestWatcher.ts"],"sourcesContent":["import {watch} from 'node:fs'\nimport {basename, dirname} from 'node:path'\n\nimport {findProjectRoot, type Output, subdebug} from '@sanity/cli-core'\n\nimport {canonicalizeWatchDir} from './canonicalizeWatchDir.js'\nimport {type DevServerConfig, type DevServerInterface} from './deriveConfigs.js'\n\nconst devDebug = subdebug('dev')\n\n/**\n * Debounce window between config file events and the next manifest\n * regeneration. Coalesces rapid saves (e.g. editor auto-save) and\n * atomic-rename bursts emitted by tools like VS Code.\n */\nconst DEBOUNCE_MS = 250\n\ninterface DevManifestWatcher {\n close: () => Promise<void>\n}\n\n/** Subset of registry fields the watcher is allowed to update. */\ninterface ManifestPatch<T> {\n manifest: T | undefined\n manifestUpdatedAt: string\n\n /** Same re-derive-don't-omit contract as `interfaces`. */\n configs?: DevServerConfig[] | undefined\n /**\n * Workbench interfaces (views/
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/startDevManifestWatcher.ts"],"sourcesContent":["import {watch} from 'node:fs'\nimport {basename, dirname} from 'node:path'\n\nimport {findProjectRoot, type Output, subdebug} from '@sanity/cli-core'\n\nimport {canonicalizeWatchDir} from './canonicalizeWatchDir.js'\nimport {type DevServerConfig, type DevServerInterface} from './deriveConfigs.js'\n\nconst devDebug = subdebug('dev')\n\n/**\n * Debounce window between config file events and the next manifest\n * regeneration. Coalesces rapid saves (e.g. editor auto-save) and\n * atomic-rename bursts emitted by tools like VS Code.\n */\nconst DEBOUNCE_MS = 250\n\ninterface DevManifestWatcher {\n close: () => Promise<void>\n}\n\n/** Subset of registry fields the watcher is allowed to update. */\ninterface ManifestPatch<T> {\n manifest: T | undefined\n manifestUpdatedAt: string\n\n /** Same re-derive-don't-omit contract as `interfaces`. */\n configs?: DevServerConfig[] | undefined\n /**\n * Workbench interfaces (views/web workers/app view) re-derived from the config\n * on each change, so editing `views`/`webWorkers`/`entry` in `sanity.cli.ts`\n * re-syncs live like `title`/`icon`. `undefined` only for\n * non-branded configs — the registry patch is a shallow merge, so extractors\n * must re-derive rather than omit, or the registered set gets wiped.\n */\n interfaces?: DevServerInterface[] | undefined\n}\n\ninterface StartDevManifestWatcherOptions<T> {\n /**\n * Run the project-specific extraction and resolve to the manifest patch, which\n * the watcher stamps with `manifestUpdatedAt` before forwarding. Receives the\n * resolved config path (`sanity.config.ts` for studios, `sanity.cli.ts` for\n * core-apps) and the working directory.\n */\n extract: (params: {\n configPath: string\n workDir: string\n }) => Promise<Omit<ManifestPatch<T>, 'manifestUpdatedAt'>>\n output: Output\n /**\n * Called after every successful extraction with the inlined manifest +\n * interfaces. Awaited, so an interface-set change can rebuild the federation\n * remote before the registry is patched (which is what reloads the workbench).\n */\n update: (patch: ManifestPatch<T>) => Promise<void> | void\n workDir: string\n\n /**\n * Extra config filenames (basenames in the project root directory) that also\n * trigger a regeneration. Studios resolve their project root via\n * `sanity.config.*` but declare workbench interfaces in `sanity.cli.*`, so\n * their watcher needs to react to both files.\n */\n extraWatchFilenames?: readonly string[]\n}\n\n/**\n * Generate the project manifest once and then keep it in sync with the\n * project's config file (`sanity.config.(ts|js)` for studios,\n * `sanity.cli.(ts|js)` for core-apps) on disk. The initial generation runs\n * fire-and-forget so it doesn't block dev-server startup; subsequent\n * file-system events are coalesced behind it, so the extractor never has\n * overlapping writes to its shared output directory. Each successful\n * regeneration inlines the new manifest into the registry via the `update`\n * callback, so any running workbench rebroadcasts to its clients.\n *\n * Errors during extraction are logged as warnings and do not crash the dev\n * server — the previously extracted manifest (if any) stays in the\n * registry.\n */\nexport async function startDevManifestWatcher<T>({\n extract,\n extraWatchFilenames,\n output,\n update,\n workDir,\n}: StartDevManifestWatcherOptions<T>): Promise<DevManifestWatcher> {\n const projectRoot = await findProjectRoot(workDir)\n const configPath = projectRoot.path\n\n let running = false\n let pending = false\n let closed = false\n\n const regenerate = async () => {\n if (closed) return\n if (running) {\n pending = true\n return\n }\n running = true\n try {\n const {configs, interfaces, manifest} = await extract({configPath, workDir})\n if (closed) return\n await update({\n configs,\n interfaces,\n manifest,\n manifestUpdatedAt: new Date().toISOString(),\n })\n } catch (err) {\n // Extractors print their own spinner failure; log the reason here so\n // the user sees what went wrong alongside the spinner indicator.\n devDebug('Manifest regeneration failed: %O', err)\n output.warn(\n `Could not extract manifest for workbench: ${err instanceof Error ? err.message : String(err)}`,\n )\n } finally {\n running = false\n if (pending && !closed) {\n pending = false\n void regenerate()\n }\n }\n }\n\n // Route the initial extraction through `regenerate` too, so file-system\n // events arriving before it finishes get coalesced rather than racing it\n // for the shared output directory.\n void regenerate()\n\n // Watch the config file's parent directory and filter by filename.\n // Watching the file itself is unreliable across editors that perform\n // atomic-save (delete + rename) — the watcher loses its target once the\n // inode changes. Directory watches survive those transitions.\n // Canonicalize to the real long path so `fs.watch` doesn't abort on Windows\n // short-path dirs. See `canonicalizeWatchDir`.\n const configDir = canonicalizeWatchDir(dirname(configPath))\n const watchFilenames = new Set([basename(configPath), ...(extraWatchFilenames ?? [])])\n\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n\n const onEvent = (_event: string, filename: Buffer | string | null) => {\n if (!filename) return\n const name = typeof filename === 'string' ? filename : filename.toString('utf8')\n if (!watchFilenames.has(name)) return\n clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n void regenerate()\n }, DEBOUNCE_MS)\n }\n\n const watcher = watch(configDir, onEvent)\n\n watcher.on('error', (err) => {\n devDebug('Config watcher error: %O', err)\n output.warn(`Manifest watcher error: ${err instanceof Error ? err.message : String(err)}`)\n })\n\n return {\n // Idempotent — a repeat close (e.g. a signal handler racing an explicit\n // close) is a no-op, so we never clear an already-cleared timer or\n // double-close the underlying watcher.\n close: async () => {\n if (closed) return\n closed = true\n clearTimeout(debounceTimer)\n watcher.close()\n },\n }\n}\n"],"names":["watch","basename","dirname","findProjectRoot","subdebug","canonicalizeWatchDir","devDebug","DEBOUNCE_MS","startDevManifestWatcher","extract","extraWatchFilenames","output","update","workDir","projectRoot","configPath","path","running","pending","closed","regenerate","configs","interfaces","manifest","manifestUpdatedAt","Date","toISOString","err","warn","Error","message","String","configDir","watchFilenames","Set","debounceTimer","onEvent","_event","filename","name","toString","has","clearTimeout","setTimeout","watcher","on","close"],"mappings":"AAAA,SAAQA,KAAK,QAAO,UAAS;AAC7B,SAAQC,QAAQ,EAAEC,OAAO,QAAO,YAAW;AAE3C,SAAQC,eAAe,EAAeC,QAAQ,QAAO,mBAAkB;AAEvE,SAAQC,oBAAoB,QAAO,4BAA2B;AAG9D,MAAMC,WAAWF,SAAS;AAE1B;;;;CAIC,GACD,MAAMG,cAAc;AAoDpB;;;;;;;;;;;;;CAaC,GACD,OAAO,eAAeC,wBAA2B,EAC/CC,OAAO,EACPC,mBAAmB,EACnBC,MAAM,EACNC,MAAM,EACNC,OAAO,EAC2B;IAClC,MAAMC,cAAc,MAAMX,gBAAgBU;IAC1C,MAAME,aAAaD,YAAYE,IAAI;IAEnC,IAAIC,UAAU;IACd,IAAIC,UAAU;IACd,IAAIC,SAAS;IAEb,MAAMC,aAAa;QACjB,IAAID,QAAQ;QACZ,IAAIF,SAAS;YACXC,UAAU;YACV;QACF;QACAD,UAAU;QACV,IAAI;YACF,MAAM,EAACI,OAAO,EAAEC,UAAU,EAAEC,QAAQ,EAAC,GAAG,MAAMd,QAAQ;gBAACM;gBAAYF;YAAO;YAC1E,IAAIM,QAAQ;YACZ,MAAMP,OAAO;gBACXS;gBACAC;gBACAC;gBACAC,mBAAmB,IAAIC,OAAOC,WAAW;YAC3C;QACF,EAAE,OAAOC,KAAK;YACZ,qEAAqE;YACrE,iEAAiE;YACjErB,SAAS,oCAAoCqB;YAC7ChB,OAAOiB,IAAI,CACT,CAAC,0CAA0C,EAAED,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ,MAAM;QAEnG,SAAU;YACRV,UAAU;YACV,IAAIC,WAAW,CAACC,QAAQ;gBACtBD,UAAU;gBACV,KAAKE;YACP;QACF;IACF;IAEA,wEAAwE;IACxE,yEAAyE;IACzE,mCAAmC;IACnC,KAAKA;IAEL,mEAAmE;IACnE,qEAAqE;IACrE,wEAAwE;IACxE,8DAA8D;IAC9D,4EAA4E;IAC5E,+CAA+C;IAC/C,MAAMY,YAAY3B,qBAAqBH,QAAQa;IAC/C,MAAMkB,iBAAiB,IAAIC,IAAI;QAACjC,SAASc;WAAiBL,uBAAuB,EAAE;KAAE;IAErF,IAAIyB;IAEJ,MAAMC,UAAU,CAACC,QAAgBC;QAC/B,IAAI,CAACA,UAAU;QACf,MAAMC,OAAO,OAAOD,aAAa,WAAWA,WAAWA,SAASE,QAAQ,CAAC;QACzE,IAAI,CAACP,eAAeQ,GAAG,CAACF,OAAO;QAC/BG,aAAaP;QACbA,gBAAgBQ,WAAW;YACzB,KAAKvB;QACP,GAAGb;IACL;IAEA,MAAMqC,UAAU5C,MAAMgC,WAAWI;IAEjCQ,QAAQC,EAAE,CAAC,SAAS,CAAClB;QACnBrB,SAAS,4BAA4BqB;QACrChB,OAAOiB,IAAI,CAAC,CAAC,wBAAwB,EAAED,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ,MAAM;IAC3F;IAEA,OAAO;QACL,wEAAwE;QACxE,mEAAmE;QACnE,uCAAuC;QACvCmB,OAAO;YACL,IAAI3B,QAAQ;YACZA,SAAS;YACTuB,aAAaP;YACbS,QAAQE,KAAK;QACf;IACF;AACF"}
|
|
@@ -1,37 +1,24 @@
|
|
|
1
1
|
import { getCliConfigUncached } from '@sanity/cli-core';
|
|
2
|
-
import {
|
|
2
|
+
import { applicationReference } from '../../applicationReference.js';
|
|
3
|
+
import { isWorkbenchApp, isWorkbenchConfig } from '../../defineApp.js';
|
|
3
4
|
import { deriveInterfaces } from '../../deriveInterfaces.js';
|
|
5
|
+
import { resolveWorkbenchConfig } from '../../resolveWorkbenchConfig.js';
|
|
4
6
|
import { formatWorkbenchAppErrors, validateWorkbenchApp } from '../../validateWorkbenchApp.js';
|
|
5
7
|
import { deriveConfigs } from './deriveConfigs.js';
|
|
6
8
|
import { trackExposesSet } from './exposesSetId.js';
|
|
7
|
-
import { getRegisteredServers,
|
|
9
|
+
import { getRegisteredServers, registerDevServer } from './registry.js';
|
|
8
10
|
import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
9
11
|
/**
|
|
10
|
-
* Log any
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* Log any app validation errors without aborting. Unlike build and deploy, dev
|
|
13
|
+
* stays up on an invalid app so the author sees the errors and fixes them live on
|
|
14
|
+
* the next save. A config is not an app — it isn't validated by the app schema
|
|
15
|
+
* (which would spuriously demand a slug/title), so skip it here.
|
|
13
16
|
*/ function reportConfigErrors(app, output) {
|
|
17
|
+
if (isWorkbenchConfig(app)) return;
|
|
14
18
|
const errors = validateWorkbenchApp(app);
|
|
15
19
|
if (errors.length === 0) return;
|
|
16
20
|
output.warn(formatWorkbenchAppErrors(errors));
|
|
17
21
|
}
|
|
18
|
-
/**
|
|
19
|
-
* A live server — other than this process — already playing the given role for
|
|
20
|
-
* the slug. Only a *same-role* duplicate is a conflict: a config-only server
|
|
21
|
-
* (configs, no interfaces — e.g. a media-library config app) is never routed
|
|
22
|
-
* as an app, so it may share a slug with the app server it configures. The
|
|
23
|
-
* workbench renders the app and publishes both servers' configs, and can
|
|
24
|
-
* always tell them apart.
|
|
25
|
-
*/ function findSameRoleConflict(id, configOnly) {
|
|
26
|
-
return getRegisteredServers().find((server)=>server.pid !== process.pid && server.id === id && isConfigOnlyServer(server) === configOnly);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Remedy line for a same-role slug conflict, phrased for the role. Changing the
|
|
30
|
-
* slug is only real advice for an app — a config app's slug is fixed by the
|
|
31
|
-
* app it configures (e.g. `unstable_defineMediaLibrary` hard-codes it).
|
|
32
|
-
*/ function conflictRemedy(configOnly) {
|
|
33
|
-
return configOnly ? 'Stop that server first.' : 'Stop that server, or give this app its own `slug` in sanity.cli.ts.';
|
|
34
|
-
}
|
|
35
22
|
/** The address the server actually bound — the live socket, which can differ from the configured port under non-strict ports. */ function serverAddress(server) {
|
|
36
23
|
const resolvedHost = server.config.server.host;
|
|
37
24
|
const addr = server.httpServer?.address();
|
|
@@ -53,33 +40,42 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
|
53
40
|
const interfaces = deriveInterfaces(cliConfig.app, {
|
|
54
41
|
isApp
|
|
55
42
|
});
|
|
56
|
-
const configs = await deriveConfigs(cliConfig
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
43
|
+
const configs = await deriveConfigs(cliConfig);
|
|
44
|
+
const workbenchApp = isWorkbenchApp(cliConfig.app) ? cliConfig.app : undefined;
|
|
45
|
+
const config = resolveWorkbenchConfig(cliConfig);
|
|
46
|
+
// A config runs alongside the app it configures, so give it its own id
|
|
47
|
+
// namespace — sharing the app's slug would make the two indistinguishable here.
|
|
48
|
+
const id = config ? `config:${config.appType}` : workbenchApp?.slug;
|
|
49
|
+
// Identity is resolved and the reference composed here, so the workbench reads
|
|
50
|
+
// both off the registry entry instead of recomposing them. Name defaults to the
|
|
51
|
+
// slug, mirroring brett.
|
|
52
|
+
const name = workbenchApp?.name ?? workbenchApp?.slug;
|
|
53
|
+
const reference = workbenchApp && name ? // is a config, not an app — so its reference is always `<org>/<name>`.
|
|
54
|
+
applicationReference({
|
|
55
|
+
isSingleton: false,
|
|
56
|
+
name,
|
|
57
|
+
organizationId: workbenchApp.organizationId
|
|
58
|
+
}) : undefined;
|
|
59
|
+
// Separate namespaces mean a shared id is a genuine duplicate, so the gate is
|
|
60
|
+
// a plain id match — no config-vs-app role left to reconcile.
|
|
61
|
+
const devServer = id ? getRegisteredServers().find((server)=>server.id === id) : undefined;
|
|
63
62
|
if (id && devServer) {
|
|
64
|
-
|
|
65
|
-
output.error(`${subject} is already served by another dev server running on port ${devServer.port}, ` + "so the workbench can't tell them apart and this one stays out of it. " + conflictRemedy(configOnly), {
|
|
63
|
+
output.error(`"${id}" is already served by another dev server running on port ${devServer.port}, ` + "so the workbench can't tell them apart and this one stays out of it. Stop that server first.", {
|
|
66
64
|
exit: false
|
|
67
65
|
});
|
|
68
66
|
return {
|
|
69
67
|
close: async ()=>{}
|
|
70
68
|
};
|
|
71
69
|
}
|
|
72
|
-
// The role the registry currently advertises for this server; a config edit
|
|
73
|
-
// can flip it (see the re-check in `update`). Committed only after a
|
|
74
|
-
// successful registry patch, so a failed pass re-checks on the next save.
|
|
75
|
-
let registeredConfigOnly = configOnly;
|
|
76
70
|
const registration = registerDevServer({
|
|
77
71
|
configs,
|
|
78
72
|
host: appHost,
|
|
79
73
|
id,
|
|
80
74
|
interfaces,
|
|
75
|
+
name,
|
|
81
76
|
port: appPort,
|
|
82
77
|
projectId: cliConfig?.api?.projectId,
|
|
78
|
+
reference,
|
|
83
79
|
type: isApp ? 'coreApp' : 'studio',
|
|
84
80
|
workDir
|
|
85
81
|
});
|
|
@@ -91,11 +87,11 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
|
91
87
|
// Re-derive every pass (don't omit): the registry patch is a shallow merge,
|
|
92
88
|
// so omitting would wipe the registered set.
|
|
93
89
|
extract: async (params)=>{
|
|
94
|
-
const
|
|
95
|
-
reportConfigErrors(app, output);
|
|
90
|
+
const nextConfig = await getCliConfigUncached(params.workDir);
|
|
91
|
+
reportConfigErrors(nextConfig.app, output);
|
|
96
92
|
return {
|
|
97
|
-
configs: await deriveConfigs(
|
|
98
|
-
interfaces: deriveInterfaces(app, {
|
|
93
|
+
configs: await deriveConfigs(nextConfig),
|
|
94
|
+
interfaces: deriveInterfaces(nextConfig.app, {
|
|
99
95
|
isApp
|
|
100
96
|
}),
|
|
101
97
|
manifest: await extractManifest(params)
|
|
@@ -109,31 +105,11 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
|
109
105
|
],
|
|
110
106
|
output,
|
|
111
107
|
update: async (patch)=>{
|
|
112
|
-
// A save can flip the server's role — e.g. a config-only app gaining an
|
|
113
|
-
// `entry` becomes app-role — so re-run the same-role collision check the
|
|
114
|
-
// registration gate applied, or the flip would quietly reintroduce the
|
|
115
|
-
// ambiguity (two app-role servers on one slug). The patch is skipped, not
|
|
116
|
-
// fatal: the registry keeps the previous shape and the next save retries.
|
|
117
|
-
const nextConfigOnly = isConfigOnlyServer({
|
|
118
|
-
configs: patch.configs,
|
|
119
|
-
interfaces: patch.interfaces
|
|
120
|
-
});
|
|
121
|
-
if (id && nextConfigOnly !== registeredConfigOnly) {
|
|
122
|
-
const conflict = findSameRoleConflict(id, nextConfigOnly);
|
|
123
|
-
if (conflict) {
|
|
124
|
-
const subject = nextConfigOnly ? `a config for "${id}"` : `the app "${id}"`;
|
|
125
|
-
output.error(`This change makes this dev server serve ${subject} like the dev server running on ` + `port ${conflict.port} already does, so the workbench couldn't tell them apart — ` + `keeping the previous registration. ${conflictRemedy(nextConfigOnly)}`, {
|
|
126
|
-
exit: false
|
|
127
|
-
});
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
108
|
if (!exposesSet.changed({
|
|
132
109
|
configs: patch.configs,
|
|
133
110
|
interfaces: patch.interfaces
|
|
134
111
|
})) {
|
|
135
112
|
registration.update(patch);
|
|
136
|
-
registeredConfigOnly = nextConfigOnly;
|
|
137
113
|
return;
|
|
138
114
|
}
|
|
139
115
|
// Rebuild the remote *before* patching the registry — the patch reloads the
|
|
@@ -149,7 +125,6 @@ import { startDevManifestWatcher } from './startDevManifestWatcher.js';
|
|
|
149
125
|
...patch,
|
|
150
126
|
...serverAddress(rebuiltServer)
|
|
151
127
|
} : patch);
|
|
152
|
-
registeredConfigOnly = nextConfigOnly;
|
|
153
128
|
},
|
|
154
129
|
workDir
|
|
155
130
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/actions/dev/startDevServerRegistration.ts"],"sourcesContent":["import {type CliConfig, getCliConfigUncached, type Output} from '@sanity/cli-core'\nimport {type ViteDevServer} from 'vite'\n\nimport {isWorkbenchApp} from '../../defineApp.js'\nimport {deriveInterfaces} from '../../deriveInterfaces.js'\nimport {formatWorkbenchAppErrors, validateWorkbenchApp} from '../../validateWorkbenchApp.js'\nimport {deriveConfigs} from './deriveConfigs.js'\nimport {trackExposesSet} from './exposesSetId.js'\nimport {\n type DevServerManifest,\n getRegisteredServers,\n isConfigOnlyServer,\n registerDevServer,\n} from './registry.js'\nimport {startDevManifestWatcher} from './startDevManifestWatcher.js'\n\ninterface DevServerRegistrationOptions {\n cliConfig: CliConfig\n /**\n * Extract the project manifest to inline into the registry. The caller owns the\n * studio-vs-app split (manifest formats are CLI-domain); registration re-derives\n * the interface set alongside it.\n */\n extractManifest: (params: {\n configPath: string\n workDir: string\n }) => Promise<DevServerManifest['manifest']>\n isApp: boolean\n output: Output\n server: ViteDevServer\n workDir: string\n\n /**\n * Rebuild the app's federation remote when its interface set changes, awaited\n * *before* the registry patch — the patch reloads the workbench page, which must\n * re-fetch a remote that already exposes the new interface. Resolves with the\n * recreated server so the entry gets its actual address (non-strict ports may\n * shift it); must reject if the restart produces no server, so the set stays\n * uncommitted and the next save retries instead of advertising a dead port.\n */\n onInterfaceSetChange?: () => Promise<ViteDevServer>\n}\n\ninterface DevServerRegistrationHandle {\n close: () => Promise<void>\n}\n\n/**\n * Log any config validation errors without aborting. Unlike build and deploy,\n * dev stays up on an invalid config so the author sees the errors and fixes them\n * live on the next save.\n */\nfunction reportConfigErrors(app: CliConfig['app'], output: Output): void {\n const errors = validateWorkbenchApp(app)\n if (errors.length === 0) return\n output.warn(formatWorkbenchAppErrors(errors))\n}\n\n/**\n * A live server — other than this process — already playing the given role for\n * the slug. Only a *same-role* duplicate is a conflict: a config-only server\n * (configs, no interfaces — e.g. a media-library config app) is never routed\n * as an app, so it may share a slug with the app server it configures. The\n * workbench renders the app and publishes both servers' configs, and can\n * always tell them apart.\n */\nfunction findSameRoleConflict(id: string, configOnly: boolean): DevServerManifest | undefined {\n return getRegisteredServers().find(\n (server) =>\n server.pid !== process.pid && server.id === id && isConfigOnlyServer(server) === configOnly,\n )\n}\n\n/**\n * Remedy line for a same-role slug conflict, phrased for the role. Changing the\n * slug is only real advice for an app — a config app's slug is fixed by the\n * app it configures (e.g. `unstable_defineMediaLibrary` hard-codes it).\n */\nfunction conflictRemedy(configOnly: boolean): string {\n return configOnly\n ? 'Stop that server first.'\n : 'Stop that server, or give this app its own `slug` in sanity.cli.ts.'\n}\n\n/** The address the server actually bound — the live socket, which can differ from the configured port under non-strict ports. */\nfunction serverAddress(server: ViteDevServer) {\n const resolvedHost = server.config.server.host\n const addr = server.httpServer?.address()\n return {\n host: typeof resolvedHost === 'string' ? resolvedHost : 'localhost',\n port: typeof addr === 'object' && addr ? addr.port : server.config.server.port,\n }\n}\n\n/**\n * Register the dev server in the registry and watch its config for manifest +\n * interface changes. The workbench reads the entry to locate and render the\n * server; the watcher keeps it current as `sanity.cli.ts` is edited.\n */\nexport async function startDevServerRegistration(\n options: DevServerRegistrationOptions,\n): Promise<DevServerRegistrationHandle> {\n const {cliConfig, extractManifest, isApp, onInterfaceSetChange, output, server, workDir} = options\n\n const {host: appHost, port: appPort} = serverAddress(server)\n\n reportConfigErrors(cliConfig.app, output)\n\n // Forwarded alongside (not inside) the manifest so the workbench renders local\n // panels/workers and reads the configs without a deploy.\n const interfaces = deriveInterfaces(cliConfig.app, {isApp})\n const configs = await deriveConfigs(cliConfig.app)\n\n const id = isWorkbenchApp(cliConfig.app) ? cliConfig.app.slug : undefined\n\n const configOnly = isConfigOnlyServer({configs, interfaces})\n const devServer = id ? findSameRoleConflict(id, configOnly) : undefined\n\n if (id && devServer) {\n const subject = configOnly ? `A config for \"${id}\"` : `The app \"${id}\"`\n output.error(\n `${subject} is already served by another dev server running on port ${devServer.port}, ` +\n \"so the workbench can't tell them apart and this one stays out of it. \" +\n conflictRemedy(configOnly),\n {exit: false},\n )\n return {close: async () => {}}\n }\n\n // The role the registry currently advertises for this server; a config edit\n // can flip it (see the re-check in `update`). Committed only after a\n // successful registry patch, so a failed pass re-checks on the next save.\n let registeredConfigOnly = configOnly\n\n const registration = registerDevServer({\n configs,\n host: appHost,\n id,\n interfaces,\n port: appPort,\n projectId: cliConfig?.api?.projectId,\n type: isApp ? 'coreApp' : 'studio',\n workDir,\n })\n\n const exposesSet = trackExposesSet({configs, interfaces})\n\n const watcher = await startDevManifestWatcher({\n // Re-derive every pass (don't omit): the registry patch is a shallow merge,\n // so omitting would wipe the registered set.\n extract: async (params) => {\n const app = (await getCliConfigUncached(params.workDir)).app\n reportConfigErrors(app, output)\n return {\n configs: await deriveConfigs(app),\n interfaces: deriveInterfaces(app, {isApp}),\n manifest: await extractManifest(params),\n }\n },\n // A studio's root resolves to `sanity.config.*` but its interfaces live in\n // `sanity.cli.*` — watch that too. Apps already root at `sanity.cli.*`.\n extraWatchFilenames: isApp ? undefined : ['sanity.cli.js', 'sanity.cli.ts'],\n output,\n update: async (patch) => {\n // A save can flip the server's role — e.g. a config-only app gaining an\n // `entry` becomes app-role — so re-run the same-role collision check the\n // registration gate applied, or the flip would quietly reintroduce the\n // ambiguity (two app-role servers on one slug). The patch is skipped, not\n // fatal: the registry keeps the previous shape and the next save retries.\n const nextConfigOnly = isConfigOnlyServer({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n if (id && nextConfigOnly !== registeredConfigOnly) {\n const conflict = findSameRoleConflict(id, nextConfigOnly)\n if (conflict) {\n const subject = nextConfigOnly ? `a config for \"${id}\"` : `the app \"${id}\"`\n output.error(\n `This change makes this dev server serve ${subject} like the dev server running on ` +\n `port ${conflict.port} already does, so the workbench couldn't tell them apart — ` +\n `keeping the previous registration. ${conflictRemedy(nextConfigOnly)}`,\n {exit: false},\n )\n return\n }\n }\n\n if (\n !exposesSet.changed({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n ) {\n registration.update(patch)\n registeredConfigOnly = nextConfigOnly\n return\n }\n // Rebuild the remote *before* patching the registry — the patch reloads the\n // page, which must re-fetch a remote that already exposes the new interface.\n const rebuiltServer = await onInterfaceSetChange?.()\n // Commit only after a successful rebuild, so a thrown one retries next pass.\n exposesSet.commit({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n // The recreated server can bind a different port (non-strict ports).\n registration.update(rebuiltServer ? {...patch, ...serverAddress(rebuiltServer)} : patch)\n registeredConfigOnly = nextConfigOnly\n },\n workDir,\n })\n\n return {\n close: async () => {\n registration.release()\n await watcher.close()\n },\n }\n}\n"],"names":["getCliConfigUncached","isWorkbenchApp","deriveInterfaces","formatWorkbenchAppErrors","validateWorkbenchApp","deriveConfigs","trackExposesSet","getRegisteredServers","isConfigOnlyServer","registerDevServer","startDevManifestWatcher","reportConfigErrors","app","output","errors","length","warn","findSameRoleConflict","id","configOnly","find","server","pid","process","conflictRemedy","serverAddress","resolvedHost","config","host","addr","httpServer","address","port","startDevServerRegistration","options","cliConfig","extractManifest","isApp","onInterfaceSetChange","workDir","appHost","appPort","interfaces","configs","slug","undefined","devServer","subject","error","exit","close","registeredConfigOnly","registration","projectId","api","type","exposesSet","watcher","extract","params","manifest","extraWatchFilenames","update","patch","nextConfigOnly","conflict","changed","rebuiltServer","commit","release"],"mappings":"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"}
|
|
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 {applicationReference} from '../../applicationReference.js'\nimport {isWorkbenchApp, isWorkbenchConfig} from '../../defineApp.js'\nimport {deriveInterfaces} from '../../deriveInterfaces.js'\nimport {resolveWorkbenchConfig} from '../../resolveWorkbenchConfig.js'\nimport {formatWorkbenchAppErrors, validateWorkbenchApp} from '../../validateWorkbenchApp.js'\nimport {deriveConfigs} from './deriveConfigs.js'\nimport {trackExposesSet} from './exposesSetId.js'\nimport {type DevServerManifest, getRegisteredServers, registerDevServer} from './registry.js'\nimport {startDevManifestWatcher} from './startDevManifestWatcher.js'\n\ninterface DevServerRegistrationOptions {\n cliConfig: CliConfig\n /**\n * Extract the project manifest to inline into the registry. The caller owns the\n * studio-vs-app split (manifest formats are CLI-domain); registration re-derives\n * the interface set alongside it.\n */\n extractManifest: (params: {\n configPath: string\n workDir: string\n }) => Promise<DevServerManifest['manifest']>\n isApp: boolean\n output: Output\n server: ViteDevServer\n workDir: string\n\n /**\n * Rebuild the app's federation remote when its interface set changes, awaited\n * *before* the registry patch — the patch reloads the workbench page, which must\n * re-fetch a remote that already exposes the new interface. Resolves with the\n * recreated server so the entry gets its actual address (non-strict ports may\n * shift it); must reject if the restart produces no server, so the set stays\n * uncommitted and the next save retries instead of advertising a dead port.\n */\n onInterfaceSetChange?: () => Promise<ViteDevServer>\n}\n\ninterface DevServerRegistrationHandle {\n close: () => Promise<void>\n}\n\n/**\n * Log any app validation errors without aborting. Unlike build and deploy, dev\n * stays up on an invalid app so the author sees the errors and fixes them live on\n * the next save. A config is not an app — it isn't validated by the app schema\n * (which would spuriously demand a slug/title), so skip it here.\n */\nfunction reportConfigErrors(app: CliConfig['app'], output: Output): void {\n if (isWorkbenchConfig(app)) return\n const errors = validateWorkbenchApp(app)\n if (errors.length === 0) return\n output.warn(formatWorkbenchAppErrors(errors))\n}\n\n/** The address the server actually bound — the live socket, which can differ from the configured port under non-strict ports. */\nfunction serverAddress(server: ViteDevServer) {\n const resolvedHost = server.config.server.host\n const addr = server.httpServer?.address()\n return {\n host: typeof resolvedHost === 'string' ? resolvedHost : 'localhost',\n port: typeof addr === 'object' && addr ? addr.port : server.config.server.port,\n }\n}\n\n/**\n * Register the dev server in the registry and watch its config for manifest +\n * interface changes. The workbench reads the entry to locate and render the\n * server; the watcher keeps it current as `sanity.cli.ts` is edited.\n */\nexport async function startDevServerRegistration(\n options: DevServerRegistrationOptions,\n): Promise<DevServerRegistrationHandle> {\n const {cliConfig, extractManifest, isApp, onInterfaceSetChange, output, server, workDir} = options\n\n const {host: appHost, port: appPort} = serverAddress(server)\n\n reportConfigErrors(cliConfig.app, output)\n\n // Forwarded alongside (not inside) the manifest so the workbench renders local\n // panels/workers and reads the configs without a deploy.\n const interfaces = deriveInterfaces(cliConfig.app, {isApp})\n const configs = await deriveConfigs(cliConfig)\n\n const workbenchApp = isWorkbenchApp(cliConfig.app) ? cliConfig.app : undefined\n const config = resolveWorkbenchConfig(cliConfig)\n // A config runs alongside the app it configures, so give it its own id\n // namespace — sharing the app's slug would make the two indistinguishable here.\n const id = config ? `config:${config.appType}` : workbenchApp?.slug\n // Identity is resolved and the reference composed here, so the workbench reads\n // both off the registry entry instead of recomposing them. Name defaults to the\n // slug, mirroring brett.\n const name = workbenchApp?.name ?? workbenchApp?.slug\n const reference =\n workbenchApp && name\n ? // No workbench app is a singleton now that the only one (the Media Library)\n // is a config, not an app — so its reference is always `<org>/<name>`.\n applicationReference({\n isSingleton: false,\n name,\n organizationId: workbenchApp.organizationId,\n })\n : undefined\n\n // Separate namespaces mean a shared id is a genuine duplicate, so the gate is\n // a plain id match — no config-vs-app role left to reconcile.\n const devServer = id ? getRegisteredServers().find((server) => server.id === id) : undefined\n\n if (id && devServer) {\n output.error(\n `\"${id}\" is already served by another dev server running on port ${devServer.port}, ` +\n \"so the workbench can't tell them apart and this one stays out of it. Stop that server first.\",\n {exit: false},\n )\n return {close: async () => {}}\n }\n\n const registration = registerDevServer({\n configs,\n host: appHost,\n id,\n interfaces,\n name,\n port: appPort,\n projectId: cliConfig?.api?.projectId,\n reference,\n type: isApp ? 'coreApp' : 'studio',\n workDir,\n })\n\n const exposesSet = trackExposesSet({configs, interfaces})\n\n const watcher = await startDevManifestWatcher({\n // Re-derive every pass (don't omit): the registry patch is a shallow merge,\n // so omitting would wipe the registered set.\n extract: async (params) => {\n const nextConfig = await getCliConfigUncached(params.workDir)\n reportConfigErrors(nextConfig.app, output)\n return {\n configs: await deriveConfigs(nextConfig),\n interfaces: deriveInterfaces(nextConfig.app, {isApp}),\n manifest: await extractManifest(params),\n }\n },\n // A studio's root resolves to `sanity.config.*` but its interfaces live in\n // `sanity.cli.*` — watch that too. Apps already root at `sanity.cli.*`.\n extraWatchFilenames: isApp ? undefined : ['sanity.cli.js', 'sanity.cli.ts'],\n output,\n update: async (patch) => {\n if (\n !exposesSet.changed({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n ) {\n registration.update(patch)\n return\n }\n // Rebuild the remote *before* patching the registry — the patch reloads the\n // page, which must re-fetch a remote that already exposes the new interface.\n const rebuiltServer = await onInterfaceSetChange?.()\n // Commit only after a successful rebuild, so a thrown one retries next pass.\n exposesSet.commit({\n configs: patch.configs,\n interfaces: patch.interfaces,\n })\n // The recreated server can bind a different port (non-strict ports).\n registration.update(rebuiltServer ? {...patch, ...serverAddress(rebuiltServer)} : patch)\n },\n workDir,\n })\n\n return {\n close: async () => {\n registration.release()\n await watcher.close()\n },\n }\n}\n"],"names":["getCliConfigUncached","applicationReference","isWorkbenchApp","isWorkbenchConfig","deriveInterfaces","resolveWorkbenchConfig","formatWorkbenchAppErrors","validateWorkbenchApp","deriveConfigs","trackExposesSet","getRegisteredServers","registerDevServer","startDevManifestWatcher","reportConfigErrors","app","output","errors","length","warn","serverAddress","server","resolvedHost","config","host","addr","httpServer","address","port","startDevServerRegistration","options","cliConfig","extractManifest","isApp","onInterfaceSetChange","workDir","appHost","appPort","interfaces","configs","workbenchApp","undefined","id","appType","slug","name","reference","isSingleton","organizationId","devServer","find","error","exit","close","registration","projectId","api","type","exposesSet","watcher","extract","params","nextConfig","manifest","extraWatchFilenames","update","patch","changed","rebuiltServer","commit","release"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { subdebug } from '@sanity/cli-core';
|
|
2
3
|
import { isStaging } from '@sanity/cli-core/util';
|
|
3
4
|
import viteReact from '@vitejs/plugin-react';
|
|
4
5
|
import { createServer } from 'vite';
|
|
@@ -8,6 +9,8 @@ import { createExposesTracker } from './exposesSetId.js';
|
|
|
8
9
|
import { acquireWorkbenchLock, getRegisteredServers, isConfigOnlyServer, readWorkbenchLock, watchRegistry } from './registry.js';
|
|
9
10
|
import { writeWorkbenchRuntime } from './writeWorkbenchRuntime.js';
|
|
10
11
|
const devDebug = subdebug('dev');
|
|
12
|
+
const renderDashboardEntry = '@sanity/workbench-cli/_internal_render';
|
|
13
|
+
const renderDashboardPath = fileURLToPath(new URL('../../_exports/_internal_render.js', import.meta.url));
|
|
11
14
|
const noop = async ()=>{};
|
|
12
15
|
// Every server is a local app except a config-only one — a config
|
|
13
16
|
// with no interfaces (the media library). A server with both lands in both channels.
|
|
@@ -78,14 +81,9 @@ const toApplicationsPayload = (servers)=>({
|
|
|
78
81
|
}
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
|
-
|
|
82
|
-
* Start the workbench dev server when federation is enabled and the workbench
|
|
83
|
-
* package is available. If the desired port is already taken — by another
|
|
84
|
-
* workbench instance or an unrelated process — fall back to running without a
|
|
85
|
-
* workbench and let the app/studio dev server claim the configured port.
|
|
86
|
-
*/ export async function startWorkbenchDevServer(options) {
|
|
84
|
+
export async function startWorkbenchDevServer(options) {
|
|
87
85
|
const { cacheDir, cliConfig, httpHost, httpPort: workbenchPort, mode, output, reactStrictMode, workDir } = options;
|
|
88
|
-
// Workbench is opted into solely by calling `
|
|
86
|
+
// Workbench is opted into solely by calling `defineApplication`.
|
|
89
87
|
if (!isWorkbenchApp(cliConfig?.app)) {
|
|
90
88
|
devDebug('Not a workbench app, skipping workbench dev server');
|
|
91
89
|
return {
|
|
@@ -95,21 +93,6 @@ const toApplicationsPayload = (servers)=>({
|
|
|
95
93
|
workbenchPort
|
|
96
94
|
};
|
|
97
95
|
}
|
|
98
|
-
let workbenchAvailable = false;
|
|
99
|
-
try {
|
|
100
|
-
await resolveLocalPackage('sanity/workbench', workDir);
|
|
101
|
-
workbenchAvailable = true;
|
|
102
|
-
} catch {
|
|
103
|
-
devDebug('Workbench not available, skipping workbench dev server');
|
|
104
|
-
}
|
|
105
|
-
if (!workbenchAvailable) {
|
|
106
|
-
return {
|
|
107
|
-
close: noop,
|
|
108
|
-
httpHost,
|
|
109
|
-
workbenchAvailable,
|
|
110
|
-
workbenchPort
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
96
|
// Acquire an exclusive lock — only one workbench per machine.
|
|
114
97
|
// Uses O_EXCL which is atomic at the OS level, preventing races when
|
|
115
98
|
// multiple `sanity dev` processes start simultaneously (e.g. via turbo).
|
|
@@ -163,7 +146,7 @@ const toApplicationsPayload = (servers)=>({
|
|
|
163
146
|
await close();
|
|
164
147
|
},
|
|
165
148
|
httpHost,
|
|
166
|
-
workbenchAvailable,
|
|
149
|
+
workbenchAvailable: true,
|
|
167
150
|
workbenchPort: actualPort
|
|
168
151
|
};
|
|
169
152
|
}
|
|
@@ -192,14 +175,9 @@ async function createWorkbenchViteServer(options) {
|
|
|
192
175
|
logLevel: 'warn',
|
|
193
176
|
mode: 'development',
|
|
194
177
|
optimizeDeps: {
|
|
195
|
-
//
|
|
196
|
-
// from dep pre-bundling so that `import.meta.hot` is available at
|
|
197
|
-
// runtime — pre-bundled modules do not receive Vite's HMR client
|
|
198
|
-
// injection, which causes the custom HMR events for local application
|
|
199
|
-
// discovery to silently not fire.
|
|
178
|
+
// Keep this entry out of pre-bundling so Vite injects its HMR context.
|
|
200
179
|
exclude: [
|
|
201
|
-
|
|
202
|
-
'@sanity/workbench'
|
|
180
|
+
renderDashboardEntry
|
|
203
181
|
]
|
|
204
182
|
},
|
|
205
183
|
// viteReact looks inert here — it transforms none of the host's own modules —
|
|
@@ -216,6 +194,10 @@ async function createWorkbenchViteServer(options) {
|
|
|
216
194
|
] : []
|
|
217
195
|
],
|
|
218
196
|
resolve: {
|
|
197
|
+
// The generated Vite root cannot reliably resolve this package.
|
|
198
|
+
alias: {
|
|
199
|
+
[renderDashboardEntry]: renderDashboardPath
|
|
200
|
+
},
|
|
219
201
|
dedupe: [
|
|
220
202
|
'react',
|
|
221
203
|
'react-dom'
|
|
@@ -260,7 +242,7 @@ async function createWorkbenchViteServer(options) {
|
|
|
260
242
|
}
|
|
261
243
|
};
|
|
262
244
|
}
|
|
263
|
-
// Workbench is opted into via `
|
|
245
|
+
// Workbench is opted into via `defineApplication`, which carries the
|
|
264
246
|
// organization ID. Deliberately no fallback (e.g. resolving it from the
|
|
265
247
|
// configured project): the lookup would need an authenticated user and an
|
|
266
248
|
// API round-trip on every startup for something the opt-in already declares.
|
|
@@ -268,7 +250,7 @@ const resolveOrganizationId = (cliConfig)=>{
|
|
|
268
250
|
if (cliConfig.app?.organizationId) {
|
|
269
251
|
return cliConfig.app.organizationId;
|
|
270
252
|
}
|
|
271
|
-
throw new Error('Workbench requires an organization ID. Pass "organizationId" to
|
|
253
|
+
throw new Error('Workbench requires an organization ID. Pass "organizationId" to defineApplication() in sanity.cli.ts.');
|
|
272
254
|
};
|
|
273
255
|
// Restricts protocol to http(s) so the URL is safe to interpolate into HTML
|
|
274
256
|
// attributes and Link headers downstream.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/actions/dev/startWorkbenchDevServer.ts"],"sourcesContent":["import {type CliConfig, type Output, resolveLocalPackage, subdebug} from '@sanity/cli-core'\nimport {isStaging} from '@sanity/cli-core/util'\nimport viteReact from '@vitejs/plugin-react'\nimport {createServer, type InlineConfig, type Plugin, type ViteDevServer} from 'vite'\nimport {z} from 'zod/mini'\n\nimport {isWorkbenchApp} from '../../defineApp.js'\nimport {createExposesTracker} from './exposesSetId.js'\nimport {\n acquireWorkbenchLock,\n type DevServerManifest,\n getRegisteredServers,\n isConfigOnlyServer,\n readWorkbenchLock,\n watchRegistry,\n} from './registry.js'\nimport {writeWorkbenchRuntime} from './writeWorkbenchRuntime.js'\n\nconst devDebug = subdebug('dev')\n\nconst noop = async () => {}\n\n// Every server is a local app except a config-only one — a config\n// with no interfaces (the media library). A server with both lands in both channels.\nconst isLocalApp = (server: DevServerManifest): boolean => !isConfigOnlyServer(server)\n\nconst toApplicationsPayload = (servers: DevServerManifest[]) => ({\n applications: servers\n .filter((server) => isLocalApp(server))\n .map(({host, id, interfaces, manifest, port, projectId, type}) => ({\n host,\n id,\n interfaces,\n manifest,\n port,\n projectId,\n type,\n })),\n configs: servers.flatMap(({configs, host, port}) =>\n // The registry stores the config flat; the workbench wire shape nests the\n // type-specific payload (`fields` for a media library) under `config`, keyed\n // by the `appType` discriminator.\n (configs ?? []).map(({appType, id, moduleName, version, ...config}) => ({\n appType,\n config,\n id,\n moduleName,\n remoteURL: `http://${host}:${port}`,\n version,\n })),\n ),\n})\n\n/**\n * Bridge the dev-server registry into a workbench Vite server's HMR channel so\n * the page tracks apps as they come and go. A changed interface set means a\n * rebuilt remote — full-reload to drop the stale remote-entry; otherwise\n * rebroadcast for a soft reconcile. Returns a detach fn.\n */\nfunction attachViteDevServerBridge(server: ViteDevServer): () => void {\n server.ws.on('sanity:workbench:get-local-applications', (_, client) => {\n client.send(\n 'sanity:workbench:local-applications',\n toApplicationsPayload(getRegisteredServers()),\n )\n })\n\n const setTracker = createExposesTracker()\n const registryWatcher = watchRegistry((servers) => {\n if (setTracker.hasChanged(servers)) {\n server.ws.send({type: 'full-reload'})\n return\n }\n server.ws.send('sanity:workbench:local-applications', toApplicationsPayload(servers))\n })\n\n return () => registryWatcher.close()\n}\n\n/**\n * Make the workbench remote act as the machine's workbench: claim the singleton\n * lock so app `sanity dev`s register into it instead of each starting their own,\n * and bridge the registry so the remote shows the local apps. No-op lock if one\n * is already held.\n */\nexport function startWorkbenchRemoteCoordinator(options: {\n httpHost: string | undefined\n port: number\n server: ViteDevServer\n}): {close: () => Promise<void>} {\n const {httpHost, port, server} = options\n\n const lock = acquireWorkbenchLock({host: httpHost || 'localhost', port})\n if (!lock) {\n const existing = readWorkbenchLock()\n devDebug(\n 'Workbench lock already held by pid %d on port %d; bridging the registry without claiming it',\n existing?.pid,\n existing?.port,\n )\n }\n\n const detachBridge = attachViteDevServerBridge(server)\n\n return {\n close: async () => {\n detachBridge()\n lock?.release()\n },\n }\n}\n\ninterface WorkbenchDevServerResult {\n close: () => Promise<void>\n httpHost: string | undefined\n workbenchAvailable: boolean\n workbenchPort: number\n}\n\nexport interface StartWorkbenchOptions {\n /** Dependency-cache dir for the workbench Vite server, kept apart from the user's own. */\n cacheDir: string\n cliConfig: CliConfig\n httpHost: string | undefined\n httpPort: number\n /** `dev` renders a live app and honors a local workbench-UI override; `preview`\n * (`sanity start`) previews a build and loads the deployed workbench UI. */\n mode: 'development' | 'preview'\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 mode,\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 mode,\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 mode: 'development' | 'preview'\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, mode, output, reactStrictMode, workbenchPort, workDir} =\n options\n\n // `preview` loads `.env.development` (the env hook treats only `build`/`deploy`\n // as production), which points the workbench UI at a local dev server that\n // isn't running here. Ignore the override and load the deployed UI instead.\n const remoteUrl =\n mode === 'preview'\n ? undefined\n : 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__: isStaging(),\n 'import.meta.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL': JSON.stringify(remoteUrl),\n },\n logLevel: 'warn',\n mode: 'development',\n optimizeDeps: {\n // Exclude sanity/workbench (and its transitive dep @sanity/workbench)\n // from dep pre-bundling so that `import.meta.hot` is available at\n // runtime — pre-bundled modules do not receive Vite's HMR client\n // injection, which causes the custom HMR events for local application\n // discovery to silently not fire.\n exclude: ['sanity', '@sanity/workbench'],\n },\n // viteReact looks inert here — it transforms none of the host's own modules —\n // but it's load-bearing for the remotes. It serves the Fast Refresh runtime at\n // /@react-refresh and injects the preamble that defines window.$RefreshReg$. The\n // federated remotes loaded into this page are react-refresh transformed, so\n // without the preamble they throw \"can't detect preamble\", and without the\n // runtime their /@react-refresh import (wired by @module-federation/vite's\n // remoteHmr) fails. Dropping it as dead code broke every panel; see #1262.\n plugins: [viteReact(), ...(remoteUrl ? [remoteManifestPreloadHeaderPlugin(remoteUrl)] : [])],\n resolve: {dedupe: ['react', 'react-dom']},\n root,\n server: {\n host: httpHost,\n port: workbenchPort,\n strictPort: false,\n warmup: {\n clientFiles: ['./workbench.js'],\n },\n },\n }\n\n devDebug('Creating workbench vite server')\n const server = await createServer(viteConfig)\n try {\n await server.listen()\n } catch (err) {\n await server.close()\n output.warn(\n `Workbench dev server failed to start: ${err instanceof Error ? err.message : String(err)}`,\n )\n return undefined\n }\n\n // Vite may have picked a different port if the desired one was occupied\n const addr = server.httpServer?.address()\n const actualPort = typeof addr === 'object' && addr ? addr.port : workbenchPort\n\n // Fire-and-forget: warm the workbench remote's Vite transform pipeline so\n // the first browser request hits a pre-populated module graph.\n if (remoteUrl) {\n fetch(remoteUrl)\n .then((r) => r.body?.cancel())\n .catch(() => {})\n devDebug('Warming workbench remote at %s', remoteUrl)\n }\n\n const detachBridge = attachViteDevServerBridge(server)\n\n return {\n actualPort,\n close: async () => {\n detachBridge()\n await server.close()\n },\n }\n}\n\n// Workbench is opted into via `unstable_defineApp`, which carries the\n// organization ID. Deliberately no fallback (e.g. resolving it from the\n// configured project): the lookup would need an authenticated user and an\n// API round-trip on every startup for something the opt-in already declares.\nconst resolveOrganizationId = (cliConfig: CliConfig): string => {\n if (cliConfig.app?.organizationId) {\n return cliConfig.app.organizationId\n }\n\n throw new Error(\n 'Workbench requires an organization ID. Pass \"organizationId\" to unstable_defineApp() in sanity.cli.ts.',\n )\n}\n\n// Restricts protocol to http(s) so the URL is safe to interpolate into HTML\n// attributes and Link headers downstream.\nconst remoteUrlSchema = z.url({normalize: true, protocol: /^https?$/})\n\nfunction parseRemoteUrl(value: string | undefined): string | undefined {\n if (!value) return undefined\n\n const result = remoteUrlSchema.safeParse(value)\n\n if (!result.success) {\n throw new Error(\n `Invalid SANITY_INTERNAL_WORKBENCH_REMOTE_URL: ${value} (must be an http(s) URL)`,\n )\n }\n\n return result.data\n}\n\n/**\n * Sets a `Link: <remoteUrl>; rel=preload; as=fetch; crossorigin` response header\n * on the index document so the browser can start fetching the Module Federation\n * manifest as soon as response headers arrive — before HTML parsing reaches the\n * in-head preconnect hint. `as=fetch` matches how the federation runtime later\n * retrieves the JSON manifest, allowing the preload entry to satisfy that fetch.\n */\nfunction remoteManifestPreloadHeaderPlugin(remoteUrl: string): Plugin {\n return {\n apply: 'serve',\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const pathname = (req.url || '/').split('?')[0]\n if (pathname === '/' || pathname === '/index.html') {\n res.setHeader('Link', `<${remoteUrl}>; rel=preload; as=fetch; crossorigin`)\n }\n next()\n })\n },\n name: 'sanity:workbench-remote-preload-header',\n }\n}\n"],"names":["resolveLocalPackage","subdebug","isStaging","viteReact","createServer","z","isWorkbenchApp","createExposesTracker","acquireWorkbenchLock","getRegisteredServers","isConfigOnlyServer","readWorkbenchLock","watchRegistry","writeWorkbenchRuntime","devDebug","noop","isLocalApp","server","toApplicationsPayload","servers","applications","filter","map","host","id","interfaces","manifest","port","projectId","type","configs","flatMap","appType","moduleName","version","config","remoteURL","attachViteDevServerBridge","ws","on","_","client","send","setTracker","registryWatcher","hasChanged","close","startWorkbenchRemoteCoordinator","options","httpHost","lock","existing","pid","detachBridge","release","startWorkbenchDevServer","cacheDir","cliConfig","httpPort","workbenchPort","mode","output","reactStrictMode","workDir","app","workbenchAvailable","workbenchLock","result","createWorkbenchViteServer","err","actualPort","updatePort","remoteUrl","undefined","parseRemoteUrl","process","env","SANITY_INTERNAL_WORKBENCH_REMOTE_URL","organizationId","resolveOrganizationId","root","cwd","viteConfig","configFile","define","__SANITY_STAGING__","JSON","stringify","logLevel","optimizeDeps","exclude","plugins","remoteManifestPreloadHeaderPlugin","resolve","dedupe","strictPort","warmup","clientFiles","listen","warn","Error","message","String","addr","httpServer","address","fetch","then","r","body","cancel","catch","remoteUrlSchema","url","normalize","protocol","value","safeParse","success","data","apply","configureServer","middlewares","use","req","res","next","pathname","split","setHeader","name"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/startWorkbenchDevServer.ts"],"sourcesContent":["import {fileURLToPath} from 'node:url'\n\nimport {type CliConfig, type Output, subdebug} from '@sanity/cli-core'\nimport {isStaging} from '@sanity/cli-core/util'\nimport viteReact from '@vitejs/plugin-react'\nimport {createServer, type InlineConfig, type Plugin, type ViteDevServer} from 'vite'\nimport {z} from 'zod/mini'\n\nimport {isWorkbenchApp} from '../../defineApp.js'\nimport {createExposesTracker} from './exposesSetId.js'\nimport {\n acquireWorkbenchLock,\n type DevServerManifest,\n getRegisteredServers,\n isConfigOnlyServer,\n readWorkbenchLock,\n watchRegistry,\n} from './registry.js'\nimport {writeWorkbenchRuntime} from './writeWorkbenchRuntime.js'\n\nconst devDebug = subdebug('dev')\nconst renderDashboardEntry = '@sanity/workbench-cli/_internal_render'\nconst renderDashboardPath = fileURLToPath(\n new URL('../../_exports/_internal_render.js', import.meta.url),\n)\n\nconst noop = async () => {}\n\n// Every server is a local app except a config-only one — a config\n// with no interfaces (the media library). A server with both lands in both channels.\nconst isLocalApp = (server: DevServerManifest): boolean => !isConfigOnlyServer(server)\n\nconst toApplicationsPayload = (servers: DevServerManifest[]) => ({\n applications: servers\n .filter((server) => isLocalApp(server))\n .map(({host, id, interfaces, manifest, port, projectId, type}) => ({\n host,\n id,\n interfaces,\n manifest,\n port,\n projectId,\n type,\n })),\n configs: servers.flatMap(({configs, host, port}) =>\n // The registry stores the config flat; the workbench wire shape nests the\n // type-specific payload (`fields` for a media library) under `config`, keyed\n // by the `appType` discriminator.\n (configs ?? []).map(({appType, id, moduleName, version, ...config}) => ({\n appType,\n config,\n id,\n moduleName,\n remoteURL: `http://${host}:${port}`,\n version,\n })),\n ),\n})\n\n/**\n * Bridge the dev-server registry into a workbench Vite server's HMR channel so\n * the page tracks apps as they come and go. A changed interface set means a\n * rebuilt remote — full-reload to drop the stale remote-entry; otherwise\n * rebroadcast for a soft reconcile. Returns a detach fn.\n */\nfunction attachViteDevServerBridge(server: ViteDevServer): () => void {\n server.ws.on('sanity:workbench:get-local-applications', (_, client) => {\n client.send(\n 'sanity:workbench:local-applications',\n toApplicationsPayload(getRegisteredServers()),\n )\n })\n\n const setTracker = createExposesTracker()\n const registryWatcher = watchRegistry((servers) => {\n if (setTracker.hasChanged(servers)) {\n server.ws.send({type: 'full-reload'})\n return\n }\n server.ws.send('sanity:workbench:local-applications', toApplicationsPayload(servers))\n })\n\n return () => registryWatcher.close()\n}\n\n/**\n * Make the workbench remote act as the machine's workbench: claim the singleton\n * lock so app `sanity dev`s register into it instead of each starting their own,\n * and bridge the registry so the remote shows the local apps. No-op lock if one\n * is already held.\n */\nexport function startWorkbenchRemoteCoordinator(options: {\n httpHost: string | undefined\n port: number\n server: ViteDevServer\n}): {close: () => Promise<void>} {\n const {httpHost, port, server} = options\n\n const lock = acquireWorkbenchLock({host: httpHost || 'localhost', port})\n if (!lock) {\n const existing = readWorkbenchLock()\n devDebug(\n 'Workbench lock already held by pid %d on port %d; bridging the registry without claiming it',\n existing?.pid,\n existing?.port,\n )\n }\n\n const detachBridge = attachViteDevServerBridge(server)\n\n return {\n close: async () => {\n detachBridge()\n lock?.release()\n },\n }\n}\n\ninterface WorkbenchDevServerResult {\n close: () => Promise<void>\n httpHost: string | undefined\n workbenchAvailable: boolean\n workbenchPort: number\n}\n\nexport interface StartWorkbenchOptions {\n /** Dependency-cache dir for the workbench Vite server, kept apart from the user's own. */\n cacheDir: string\n cliConfig: CliConfig\n httpHost: string | undefined\n httpPort: number\n /** `dev` renders a live app and honors a local workbench-UI override; `preview`\n * (`sanity start`) previews a build and loads the deployed workbench UI. */\n mode: 'development' | 'preview'\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\nexport async function startWorkbenchDevServer(\n options: StartWorkbenchOptions,\n): Promise<WorkbenchDevServerResult> {\n const {\n cacheDir,\n cliConfig,\n httpHost,\n httpPort: workbenchPort,\n mode,\n output,\n reactStrictMode,\n workDir,\n } = options\n\n // Workbench is opted into solely by calling `defineApplication`.\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 // 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 mode,\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: true,\n workbenchPort: actualPort,\n }\n}\n\ninterface CreateWorkbenchViteServerOptions {\n cacheDir: string\n cliConfig: CliConfig\n httpHost: string | undefined\n mode: 'development' | 'preview'\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, mode, output, reactStrictMode, workbenchPort, workDir} =\n options\n\n // `preview` loads `.env.development` (the env hook treats only `build`/`deploy`\n // as production), which points the workbench UI at a local dev server that\n // isn't running here. Ignore the override and load the deployed UI instead.\n const remoteUrl =\n mode === 'preview'\n ? undefined\n : 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__: isStaging(),\n 'import.meta.env.SANITY_INTERNAL_WORKBENCH_REMOTE_URL': JSON.stringify(remoteUrl),\n },\n logLevel: 'warn',\n mode: 'development',\n optimizeDeps: {\n // Keep this entry out of pre-bundling so Vite injects its HMR context.\n exclude: [renderDashboardEntry],\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: {\n // The generated Vite root cannot reliably resolve this package.\n alias: {[renderDashboardEntry]: renderDashboardPath},\n dedupe: ['react', 'react-dom'],\n },\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 `defineApplication`, 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 defineApplication() 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":["fileURLToPath","subdebug","isStaging","viteReact","createServer","z","isWorkbenchApp","createExposesTracker","acquireWorkbenchLock","getRegisteredServers","isConfigOnlyServer","readWorkbenchLock","watchRegistry","writeWorkbenchRuntime","devDebug","renderDashboardEntry","renderDashboardPath","URL","url","noop","isLocalApp","server","toApplicationsPayload","servers","applications","filter","map","host","id","interfaces","manifest","port","projectId","type","configs","flatMap","appType","moduleName","version","config","remoteURL","attachViteDevServerBridge","ws","on","_","client","send","setTracker","registryWatcher","hasChanged","close","startWorkbenchRemoteCoordinator","options","httpHost","lock","existing","pid","detachBridge","release","startWorkbenchDevServer","cacheDir","cliConfig","httpPort","workbenchPort","mode","output","reactStrictMode","workDir","app","workbenchAvailable","workbenchLock","result","createWorkbenchViteServer","err","actualPort","updatePort","remoteUrl","undefined","parseRemoteUrl","process","env","SANITY_INTERNAL_WORKBENCH_REMOTE_URL","organizationId","resolveOrganizationId","root","cwd","viteConfig","configFile","define","__SANITY_STAGING__","JSON","stringify","logLevel","optimizeDeps","exclude","plugins","remoteManifestPreloadHeaderPlugin","resolve","alias","dedupe","strictPort","warmup","clientFiles","listen","warn","Error","message","String","addr","httpServer","address","fetch","then","r","body","cancel","catch","remoteUrlSchema","normalize","protocol","value","safeParse","success","data","apply","configureServer","middlewares","use","req","res","next","pathname","split","setHeader","name"],"mappings":"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"}
|
|
@@ -6,9 +6,9 @@ const devDebug = subdebug('dev');
|
|
|
6
6
|
const workbenchJsTemplate = `\
|
|
7
7
|
// This file is auto-generated on 'sanity dev'
|
|
8
8
|
// Modifications to this file are automatically discarded
|
|
9
|
-
import {
|
|
9
|
+
import {renderDashboard} from "@sanity/workbench-cli/_internal_render"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
renderDashboard(
|
|
12
12
|
document.getElementById("workbench"),
|
|
13
13
|
{organizationId: %SANITY_WORKBENCH_ORGANIZATION_ID%},
|
|
14
14
|
{reactStrictMode: %SANITY_WORKBENCH_REACT_STRICT_MODE%}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/actions/dev/writeWorkbenchRuntime.ts"],"sourcesContent":["import fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport {subdebug} from '@sanity/cli-core'\nimport {isStaging} from '@sanity/cli-core/util'\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 {
|
|
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'\nimport {isStaging} from '@sanity/cli-core/util'\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 {renderDashboard} from \"@sanity/workbench-cli/_internal_render\"\n\nrenderDashboard(\n document.getElementById(\"workbench\"),\n {organizationId: %SANITY_WORKBENCH_ORGANIZATION_ID%},\n {reactStrictMode: %SANITY_WORKBENCH_REACT_STRICT_MODE%}\n)\n`\n\nconst indexHtmlTemplate = `\\\n<!DOCTYPE html>\n<!-- This file is auto-generated on 'sanity dev' -->\n<!-- Modifications to this file are automatically discarded -->\n<html>\n <head>\n <meta charset=\"UTF-8\" />\n%SANITY_WORKBENCH_PREFETCH_HINTS%\n </head>\n <body>\n <div id=\"workbench\"></div>\n <script>globalThis.__SANITY_STAGING__ = %SANITY_WORKBENCH_STAGING%</script>\n <script type=\"module\" src=\"./workbench.js\"></script>\n </body>\n</html>\n`\n\n/**\n * Generates the `.sanity/workbench` directory with static entry files for\n * the workbench Vite dev server.\n *\n * @param cwd - Current working directory (Sanity root dir)\n * @returns The absolute path to the written workbench runtime directory\n * @internal\n */\nexport async function writeWorkbenchRuntime(options: {\n cwd: string\n organizationId?: string\n reactStrictMode: boolean\n remoteUrl?: string\n}): Promise<string> {\n const {cwd, organizationId, reactStrictMode, remoteUrl} = options\n const workbenchDir = path.join(cwd, '.sanity', 'workbench')\n\n const workbenchJs = workbenchJsTemplate\n .replace(\n /%SANITY_WORKBENCH_ORGANIZATION_ID%/,\n organizationId === undefined ? 'undefined' : JSON.stringify(organizationId),\n )\n .replace(/%SANITY_WORKBENCH_REACT_STRICT_MODE%/, JSON.stringify(reactStrictMode))\n\n const prefetchHints = buildPrefetchHints(remoteUrl)\n\n // The runtime flag builds get via decorateIndexWithStagingScript — a vite\n // `define` never reaches pre-bundled dependencies like the SDK.\n const indexHtml = indexHtmlTemplate\n .replace(/%SANITY_WORKBENCH_PREFETCH_HINTS%/, prefetchHints)\n .replace(/%SANITY_WORKBENCH_STAGING%/, JSON.stringify(isStaging()))\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","isStaging","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;AACzC,SAAQC,SAAS,QAAO,wBAAuB;AAE/C,MAAMC,WAAWF,SAAS;AAE1B,MAAMG,sBAAsB,CAAC;;;;;;;;;;AAU7B,CAAC;AAED,MAAMC,oBAAoB,CAAC;;;;;;;;;;;;;;;AAe3B,CAAC;AAED;;;;;;;CAOC,GACD,OAAO,eAAeC,sBAAsBC,OAK3C;IACC,MAAM,EAACC,GAAG,EAAEC,cAAc,EAAEC,eAAe,EAAEC,SAAS,EAAC,GAAGJ;IAC1D,MAAMK,eAAeZ,KAAKa,IAAI,CAACL,KAAK,WAAW;IAE/C,MAAMM,cAAcV,oBACjBW,OAAO,CACN,sCACAN,mBAAmBO,YAAY,cAAcC,KAAKC,SAAS,CAACT,iBAE7DM,OAAO,CAAC,wCAAwCE,KAAKC,SAAS,CAACR;IAElE,MAAMS,gBAAgBC,mBAAmBT;IAEzC,0EAA0E;IAC1E,gEAAgE;IAChE,MAAMU,YAAYhB,kBACfU,OAAO,CAAC,qCAAqCI,eAC7CJ,OAAO,CAAC,8BAA8BE,KAAKC,SAAS,CAAChB;IAExDC,SAAS;IACT,MAAMJ,GAAGuB,KAAK,CAACV,cAAc;QAACW,WAAW;IAAI;IAE7CpB,SAAS;IACT,MAAMJ,GAAGyB,SAAS,CAACxB,KAAKa,IAAI,CAACD,cAAc,iBAAiBE;IAE5DX,SAAS;IACT,MAAMJ,GAAGyB,SAAS,CAACxB,KAAKa,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"}
|
|
@@ -4,7 +4,7 @@ import { checkBuiltOutput } from '../deploy/checkBuiltOutput.js';
|
|
|
4
4
|
const previewDebug = subdebug('preview');
|
|
5
5
|
/**
|
|
6
6
|
* Serve a built workbench app's `dist` as static files over a Vite `preview`
|
|
7
|
-
* server, so its
|
|
7
|
+
* server, so its manifest and referenced assets are reachable exactly as
|
|
8
8
|
* a live `sanity dev` remote would be — the workbench then federates it in.
|
|
9
9
|
*
|
|
10
10
|
* A missing federation build surfaces as `BUILD_NOT_FOUND` (from
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/actions/preview/serveBuiltApplication.ts"],"sourcesContent":["import {subdebug} from '@sanity/cli-core'\nimport {type InlineConfig, preview} from 'vite'\n\nimport {checkBuiltOutput} from '../deploy/checkBuiltOutput.js'\n\nconst previewDebug = subdebug('preview')\n\nexport interface BuiltApplicationServer {\n close: () => Promise<void>\n /** Routable host the remote is reachable at, for the registry's remote URL. */\n host: string\n /** Port Vite bound — may differ from the requested one under non-strict ports. */\n port: number\n}\n\n/**\n * Serve a built workbench app's `dist` as static files over a Vite `preview`\n * server, so its
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/preview/serveBuiltApplication.ts"],"sourcesContent":["import {subdebug} from '@sanity/cli-core'\nimport {type InlineConfig, preview} from 'vite'\n\nimport {checkBuiltOutput} from '../deploy/checkBuiltOutput.js'\n\nconst previewDebug = subdebug('preview')\n\nexport interface BuiltApplicationServer {\n close: () => Promise<void>\n /** Routable host the remote is reachable at, for the registry's remote URL. */\n host: string\n /** Port Vite bound — may differ from the requested one under non-strict ports. */\n port: number\n}\n\n/**\n * Serve a built workbench app's `dist` as static files over a Vite `preview`\n * server, so its manifest and referenced assets are reachable exactly as\n * a live `sanity dev` remote would be — the workbench then federates it in.\n *\n * A missing federation build surfaces as `BUILD_NOT_FOUND` (from\n * `checkBuiltOutput`) so the command shows the same \"run sanity build\" hint the\n * studio preview path does; real I/O errors surface as themselves.\n */\nexport async function serveBuiltApplication(options: {\n cacheDir: string\n httpHost: string\n httpPort: number\n outDir: string\n workDir: string\n}): Promise<BuiltApplicationServer> {\n const {cacheDir, httpHost, httpPort, outDir, workDir} = options\n\n await checkBuiltOutput(outDir)\n\n const config: InlineConfig = {\n // Serve at the root so `mf-manifest.json` and its `publicPath: 'auto'` chunks\n // resolve under the registered remote URL.\n base: '/',\n // Needed for vite to serve `outDir` itself rather than `outDir/dist`.\n build: {outDir},\n cacheDir,\n configFile: false,\n logLevel: 'warn',\n mode: 'production',\n preview: {host: httpHost, port: httpPort, strictPort: false},\n root: workDir,\n }\n\n previewDebug('Serving built remote from %s', outDir)\n const server = await preview(config)\n\n const addr = server.httpServer.address()\n const port = typeof addr === 'object' && addr ? addr.port : httpPort\n\n return {\n // Drop idle keep-alive sockets so shutdown doesn't stall on a browser that\n // kept the connection open — otherwise `close` waits out the teardown grace.\n // `closeAllConnections` isn't on Vite's `HttpServer` type, but is on the\n // underlying node server (18.2+); the optional call tolerates its absence.\n close: () =>\n new Promise((resolve, reject) => {\n server.httpServer.close((err) => (err ? reject(err) : resolve()))\n ;(server.httpServer as {closeAllConnections?: () => void}).closeAllConnections?.()\n }),\n host: httpHost,\n port,\n }\n}\n"],"names":["subdebug","preview","checkBuiltOutput","previewDebug","serveBuiltApplication","options","cacheDir","httpHost","httpPort","outDir","workDir","config","base","build","configFile","logLevel","mode","host","port","strictPort","root","server","addr","httpServer","address","close","Promise","resolve","reject","err","closeAllConnections"],"mappings":"AAAA,SAAQA,QAAQ,QAAO,mBAAkB;AACzC,SAA2BC,OAAO,QAAO,OAAM;AAE/C,SAAQC,gBAAgB,QAAO,gCAA+B;AAE9D,MAAMC,eAAeH,SAAS;AAU9B;;;;;;;;CAQC,GACD,OAAO,eAAeI,sBAAsBC,OAM3C;IACC,MAAM,EAACC,QAAQ,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,OAAO,EAAC,GAAGL;IAExD,MAAMH,iBAAiBO;IAEvB,MAAME,SAAuB;QAC3B,8EAA8E;QAC9E,2CAA2C;QAC3CC,MAAM;QACN,sEAAsE;QACtEC,OAAO;YAACJ;QAAM;QACdH;QACAQ,YAAY;QACZC,UAAU;QACVC,MAAM;QACNf,SAAS;YAACgB,MAAMV;YAAUW,MAAMV;YAAUW,YAAY;QAAK;QAC3DC,MAAMV;IACR;IAEAP,aAAa,gCAAgCM;IAC7C,MAAMY,SAAS,MAAMpB,QAAQU;IAE7B,MAAMW,OAAOD,OAAOE,UAAU,CAACC,OAAO;IACtC,MAAMN,OAAO,OAAOI,SAAS,YAAYA,OAAOA,KAAKJ,IAAI,GAAGV;IAE5D,OAAO;QACL,2EAA2E;QAC3E,6EAA6E;QAC7E,yEAAyE;QACzE,2EAA2E;QAC3EiB,OAAO,IACL,IAAIC,QAAQ,CAACC,SAASC;gBACpBP,OAAOE,UAAU,CAACE,KAAK,CAAC,CAACI,MAASA,MAAMD,OAAOC,OAAOF;gBACpDN,OAAOE,UAAU,CAAwCO,mBAAmB;YAChF;QACFb,MAAMV;QACNW;IACF;AACF"}
|
|
@@ -54,7 +54,7 @@ import { serveBuiltApplication } from './serveBuiltApplication.js';
|
|
|
54
54
|
const workbench = resolveWorkbenchApp(cliConfig);
|
|
55
55
|
if (!workbench) throw new Error('`sanity start` was invoked in a non-workbench application');
|
|
56
56
|
const inlinedId = await readInlinedAppId(outDir);
|
|
57
|
-
const configs = await deriveConfigs(cliConfig
|
|
57
|
+
const configs = await deriveConfigs(cliConfig);
|
|
58
58
|
const id = inlinedId ?? await buildAppId(workbench);
|
|
59
59
|
const registration = registerDevServer({
|
|
60
60
|
configs,
|