@sanity/workbench-cli 1.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/_exports/build.d.ts +129 -0
- package/dist/_exports/build.js +9 -0
- package/dist/_exports/build.js.map +1 -0
- package/dist/_exports/deploy.d.ts +115 -0
- package/dist/_exports/deploy.js +6 -0
- package/dist/_exports/deploy.js.map +1 -0
- package/dist/_exports/dev.d.ts +166 -0
- package/dist/_exports/dev.js +10 -0
- package/dist/_exports/dev.js.map +1 -0
- package/dist/_exports/index.d.ts +307 -0
- package/dist/_exports/index.js +15 -0
- package/dist/_exports/index.js.map +1 -0
- package/dist/_exports/init.d.ts +12 -0
- package/dist/_exports/init.js +5 -0
- package/dist/_exports/init.js.map +1 -0
- package/dist/actions/build/artifact.js +29 -0
- package/dist/actions/build/artifact.js.map +1 -0
- package/dist/actions/build/render-remote.js +69 -0
- package/dist/actions/build/render-remote.js.map +1 -0
- package/dist/actions/build/services/artifact.js +122 -0
- package/dist/actions/build/services/artifact.js.map +1 -0
- package/dist/actions/build/views/artifact.js +31 -0
- package/dist/actions/build/views/artifact.js.map +1 -0
- package/dist/actions/build/vite/constants.js +5 -0
- package/dist/actions/build/vite/constants.js.map +1 -0
- package/dist/actions/build/vite/plugin.js +74 -0
- package/dist/actions/build/vite/plugin.js.map +1 -0
- package/dist/actions/build/vite/plugins/plugin-module-federation.js +53 -0
- package/dist/actions/build/vite/plugins/plugin-module-federation.js.map +1 -0
- package/dist/actions/build/vite/plugins/plugin-sanity-environment.js +29 -0
- package/dist/actions/build/vite/plugins/plugin-sanity-environment.js.map +1 -0
- package/dist/actions/build/vite/plugins/plugin-sanity-extension-artifacts.js +33 -0
- package/dist/actions/build/vite/plugins/plugin-sanity-extension-artifacts.js.map +1 -0
- package/dist/actions/build/vite/plugins/plugin-sanity-federation-runtime.js +83 -0
- package/dist/actions/build/vite/plugins/plugin-sanity-federation-runtime.js.map +1 -0
- package/dist/actions/build/vite/workbench-vite-plugins.js +43 -0
- package/dist/actions/build/vite/workbench-vite-plugins.js.map +1 -0
- package/dist/actions/deploy/getWorkbench.js +40 -0
- package/dist/actions/deploy/getWorkbench.js.map +1 -0
- package/dist/actions/dev/canonicalizeWatchDir.js +23 -0
- package/dist/actions/dev/canonicalizeWatchDir.js.map +1 -0
- package/dist/actions/dev/processLiveness.js +109 -0
- package/dist/actions/dev/processLiveness.js.map +1 -0
- package/dist/actions/dev/registry.js +279 -0
- package/dist/actions/dev/registry.js.map +1 -0
- package/dist/actions/init/cliConfig.js +45 -0
- package/dist/actions/init/cliConfig.js.map +1 -0
- package/dist/contract.js +66 -0
- package/dist/contract.js.map +1 -0
- package/dist/defineApp.js +82 -0
- package/dist/defineApp.js.map +1 -0
- package/dist/defineService.js +19 -0
- package/dist/defineService.js.map +1 -0
- package/dist/defineView.js +19 -0
- package/dist/defineView.js.map +1 -0
- package/dist/resolveWorkbenchApp.js +21 -0
- package/dist/resolveWorkbenchApp.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/actions/build/vite/constants.ts"],"sourcesContent":["export const FEDERATION_FILE_NAME = 'remote-entry'\nexport const FEDERATION_DIR_NAME = 'federation'\nexport const RUNTIME_DIR = `.sanity/${FEDERATION_DIR_NAME}`\n"],"names":["FEDERATION_FILE_NAME","FEDERATION_DIR_NAME","RUNTIME_DIR"],"mappings":"AAAA,OAAO,MAAMA,uBAAuB,eAAc;AAClD,OAAO,MAAMC,sBAAsB,aAAY;AAC/C,OAAO,MAAMC,cAAc,CAAC,QAAQ,EAAED,qBAAqB,CAAA"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { artifactExposes, workbenchArtifacts } from '../artifact.js';
|
|
3
|
+
import { FEDERATION_FILE_NAME, RUNTIME_DIR } from './constants.js';
|
|
4
|
+
import { sanityModuleFederation } from './plugins/plugin-module-federation.js';
|
|
5
|
+
import { sanityEnvironmentPlugin } from './plugins/plugin-sanity-environment.js';
|
|
6
|
+
import { sanityExtensionArtifacts } from './plugins/plugin-sanity-extension-artifacts.js';
|
|
7
|
+
import { sanityFederationRuntime } from './plugins/plugin-sanity-federation-runtime.js';
|
|
8
|
+
/**
|
|
9
|
+
* @internal
|
|
10
|
+
*/ export const federation = (options)=>{
|
|
11
|
+
const { exposes: defaultExposes = {}, name: defaultName, pkgJson, services = [], views = [], workDir = process.cwd() } = options;
|
|
12
|
+
let name = defaultName;
|
|
13
|
+
if (!name) {
|
|
14
|
+
name = pkgJson?.name;
|
|
15
|
+
}
|
|
16
|
+
if (!name) {
|
|
17
|
+
throw new Error('"name" option is required but could not be inferred from package.json');
|
|
18
|
+
}
|
|
19
|
+
const generatedEntry = `./${RUNTIME_DIR}/${FEDERATION_FILE_NAME}.jsx`;
|
|
20
|
+
function resolveEntryPath(entry) {
|
|
21
|
+
const resolvedPath = path.resolve(workDir, entry);
|
|
22
|
+
if (!resolvedPath) {
|
|
23
|
+
throw new Error(`Could not resolve path for entry "${entry}". Please check that the file exists and the path is correct.`);
|
|
24
|
+
}
|
|
25
|
+
return resolvedPath;
|
|
26
|
+
}
|
|
27
|
+
const entryPath = resolveEntryPath(generatedEntry);
|
|
28
|
+
const resolvedExposes = {};
|
|
29
|
+
for (const [key, exposePath] of Object.entries(defaultExposes)){
|
|
30
|
+
resolvedExposes[key] = resolveEntryPath(exposePath) ?? exposePath;
|
|
31
|
+
}
|
|
32
|
+
// Each view component (`./views/<view>/<component>`) and each service loader
|
|
33
|
+
// (`./services/<name>`) is exposed straight to the host, pointing at the file
|
|
34
|
+
// the extension-artifacts plugin generates under RUNTIME_DIR. A service's
|
|
35
|
+
// worker bundle carries no expose — the host reaches it through its loader.
|
|
36
|
+
const artifacts = workbenchArtifacts({
|
|
37
|
+
services,
|
|
38
|
+
views
|
|
39
|
+
});
|
|
40
|
+
const interfaceExposes = artifactExposes(artifacts, (artifactPath)=>resolveEntryPath(`./${RUNTIME_DIR}/${artifactPath}`));
|
|
41
|
+
// A dock-only app (`isApp` with no `appEntry`) has no navigable full-page
|
|
42
|
+
// view, so it exposes no `./App` — only its views. Studios and apps with an
|
|
43
|
+
// entry expose `./App` (the generated render entry).
|
|
44
|
+
const exposesApp = !options.isApp || options.appEntry !== undefined;
|
|
45
|
+
const exposes = {
|
|
46
|
+
...exposesApp ? {
|
|
47
|
+
'./App': entryPath
|
|
48
|
+
} : {},
|
|
49
|
+
...resolvedExposes,
|
|
50
|
+
...interfaceExposes
|
|
51
|
+
};
|
|
52
|
+
const runtimeOptions = options.isApp ? {
|
|
53
|
+
appEntry: options.appEntry,
|
|
54
|
+
isApp: true
|
|
55
|
+
} : {
|
|
56
|
+
isApp: false,
|
|
57
|
+
studioConfigPath: options.studioConfigPath
|
|
58
|
+
};
|
|
59
|
+
return [
|
|
60
|
+
sanityEnvironmentPlugin({
|
|
61
|
+
input: entryPath
|
|
62
|
+
}),
|
|
63
|
+
sanityFederationRuntime(runtimeOptions),
|
|
64
|
+
sanityExtensionArtifacts({
|
|
65
|
+
artifacts
|
|
66
|
+
}),
|
|
67
|
+
sanityModuleFederation({
|
|
68
|
+
exposes,
|
|
69
|
+
name
|
|
70
|
+
})
|
|
71
|
+
];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/actions/build/vite/plugin.ts"],"sourcesContent":["import path from 'node:path'\n\nimport {type ModuleFederationOptions} from '@module-federation/vite'\nimport {type PackageJson} from '@sanity/cli-core'\nimport {type PluginOption} from 'vite'\n\nimport {type DefineAppInput} from '../../../defineApp.js'\nimport {artifactExposes, workbenchArtifacts} from '../artifact.js'\nimport {FEDERATION_FILE_NAME, RUNTIME_DIR} from './constants.js'\nimport {type FederationOptions, sanityModuleFederation} from './plugins/plugin-module-federation.js'\nimport {sanityEnvironmentPlugin} from './plugins/plugin-sanity-environment.js'\nimport {sanityExtensionArtifacts} from './plugins/plugin-sanity-extension-artifacts.js'\nimport {\n type FederationRuntimeOptions,\n sanityFederationRuntime,\n} from './plugins/plugin-sanity-federation-runtime.js'\n\ninterface FederationPluginOptionsBase extends Omit<Partial<FederationOptions>, 'exposes'> {\n exposes?: Record<string, string>\n pkgJson?: PackageJson\n /**\n * Background services the app declares. Each is built into a self-contained\n * worker bundle plus a loader module exposed as `./services/<name>`, so the\n * workbench host can fetch the worker URL and run it.\n */\n services?: DefineAppInput['services']\n /**\n * Views the app declares. Each component of a view is built into a\n * render-contract artifact and exposed as `./views/<view>/<component>` so the\n * workbench host can load each as its own island.\n */\n views?: DefineAppInput['views']\n /**\n * Current working directory to read package.json from, defaults to process.cwd()\n */\n workDir?: string\n}\n\ninterface AppFederationPluginOptions extends FederationPluginOptionsBase {\n isApp: true\n\n /**\n * Relative path to the App entry from the runtime directory, e.g.\n * `../../src/App.tsx`. Omit it for a dock-only app that declares no `entry`:\n * no `./App` is exposed and the remote serves only its views.\n */\n appEntry?: string\n studioConfigPath?: never\n}\n\ninterface StudioFederationPluginOptions extends FederationPluginOptionsBase {\n /** relative path to the Studio config file from the runtime directory (e.g. `../../sanity.config.ts`). */\n studioConfigPath: string\n\n appEntry?: never\n /** @defaultValue false */\n isApp?: false\n}\n\n/**\n * Plugin options for the federation vite plugin.\n *\n * Discriminated on `isApp`:\n * - `isApp: true` → requires `appEntry`\n * - `isApp: false` (default) → requires `studioConfigPath`\n *\n * @internal\n */\ntype FederationPluginOptions = AppFederationPluginOptions | StudioFederationPluginOptions\n\n/**\n * @internal\n */\nexport const federation = (options: FederationPluginOptions): PluginOption => {\n const {\n exposes: defaultExposes = {},\n name: defaultName,\n pkgJson,\n services = [],\n views = [],\n workDir = process.cwd(),\n } = options\n\n let name = defaultName\n\n if (!name) {\n name = pkgJson?.name\n }\n\n if (!name) {\n throw new Error('\"name\" option is required but could not be inferred from package.json')\n }\n\n const generatedEntry = `./${RUNTIME_DIR}/${FEDERATION_FILE_NAME}.jsx`\n\n function resolveEntryPath(entry: string) {\n const resolvedPath = path.resolve(workDir, entry)\n\n if (!resolvedPath) {\n throw new Error(\n `Could not resolve path for entry \"${entry}\". Please check that the file exists and the path is correct.`,\n )\n }\n\n return resolvedPath\n }\n\n const entryPath = resolveEntryPath(generatedEntry)\n\n const resolvedExposes: Record<string, string> = {}\n for (const [key, exposePath] of Object.entries(defaultExposes)) {\n resolvedExposes[key] = resolveEntryPath(exposePath) ?? exposePath\n }\n\n // Each view component (`./views/<view>/<component>`) and each service loader\n // (`./services/<name>`) is exposed straight to the host, pointing at the file\n // the extension-artifacts plugin generates under RUNTIME_DIR. A service's\n // worker bundle carries no expose — the host reaches it through its loader.\n const artifacts = workbenchArtifacts({services, views})\n const interfaceExposes = artifactExposes(artifacts, (artifactPath) =>\n resolveEntryPath(`./${RUNTIME_DIR}/${artifactPath}`),\n )\n\n // A dock-only app (`isApp` with no `appEntry`) has no navigable full-page\n // view, so it exposes no `./App` — only its views. Studios and apps with an\n // entry expose `./App` (the generated render entry).\n const exposesApp = !options.isApp || options.appEntry !== undefined\n\n const exposes: NonNullable<ModuleFederationOptions['exposes']> = {\n ...(exposesApp ? {'./App': entryPath} : {}),\n ...resolvedExposes,\n ...interfaceExposes,\n }\n\n const runtimeOptions: FederationRuntimeOptions = options.isApp\n ? {appEntry: options.appEntry, isApp: true}\n : {isApp: false, studioConfigPath: options.studioConfigPath}\n\n return [\n sanityEnvironmentPlugin({input: entryPath}),\n sanityFederationRuntime(runtimeOptions),\n sanityExtensionArtifacts({artifacts}),\n sanityModuleFederation({exposes, name}),\n ]\n}\n"],"names":["path","artifactExposes","workbenchArtifacts","FEDERATION_FILE_NAME","RUNTIME_DIR","sanityModuleFederation","sanityEnvironmentPlugin","sanityExtensionArtifacts","sanityFederationRuntime","federation","options","exposes","defaultExposes","name","defaultName","pkgJson","services","views","workDir","process","cwd","Error","generatedEntry","resolveEntryPath","entry","resolvedPath","resolve","entryPath","resolvedExposes","key","exposePath","Object","entries","artifacts","interfaceExposes","artifactPath","exposesApp","isApp","appEntry","undefined","runtimeOptions","studioConfigPath","input"],"mappings":"AAAA,OAAOA,UAAU,YAAW;AAO5B,SAAQC,eAAe,EAAEC,kBAAkB,QAAO,iBAAgB;AAClE,SAAQC,oBAAoB,EAAEC,WAAW,QAAO,iBAAgB;AAChE,SAAgCC,sBAAsB,QAAO,wCAAuC;AACpG,SAAQC,uBAAuB,QAAO,yCAAwC;AAC9E,SAAQC,wBAAwB,QAAO,iDAAgD;AACvF,SAEEC,uBAAuB,QAClB,gDAA+C;AAuDtD;;CAEC,GACD,OAAO,MAAMC,aAAa,CAACC;IACzB,MAAM,EACJC,SAASC,iBAAiB,CAAC,CAAC,EAC5BC,MAAMC,WAAW,EACjBC,OAAO,EACPC,WAAW,EAAE,EACbC,QAAQ,EAAE,EACVC,UAAUC,QAAQC,GAAG,EAAE,EACxB,GAAGV;IAEJ,IAAIG,OAAOC;IAEX,IAAI,CAACD,MAAM;QACTA,OAAOE,SAASF;IAClB;IAEA,IAAI,CAACA,MAAM;QACT,MAAM,IAAIQ,MAAM;IAClB;IAEA,MAAMC,iBAAiB,CAAC,EAAE,EAAElB,YAAY,CAAC,EAAED,qBAAqB,IAAI,CAAC;IAErE,SAASoB,iBAAiBC,KAAa;QACrC,MAAMC,eAAezB,KAAK0B,OAAO,CAACR,SAASM;QAE3C,IAAI,CAACC,cAAc;YACjB,MAAM,IAAIJ,MACR,CAAC,kCAAkC,EAAEG,MAAM,6DAA6D,CAAC;QAE7G;QAEA,OAAOC;IACT;IAEA,MAAME,YAAYJ,iBAAiBD;IAEnC,MAAMM,kBAA0C,CAAC;IACjD,KAAK,MAAM,CAACC,KAAKC,WAAW,IAAIC,OAAOC,OAAO,CAACpB,gBAAiB;QAC9DgB,eAAe,CAACC,IAAI,GAAGN,iBAAiBO,eAAeA;IACzD;IAEA,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,4EAA4E;IAC5E,MAAMG,YAAY/B,mBAAmB;QAACc;QAAUC;IAAK;IACrD,MAAMiB,mBAAmBjC,gBAAgBgC,WAAW,CAACE,eACnDZ,iBAAiB,CAAC,EAAE,EAAEnB,YAAY,CAAC,EAAE+B,cAAc;IAGrD,0EAA0E;IAC1E,4EAA4E;IAC5E,qDAAqD;IACrD,MAAMC,aAAa,CAAC1B,QAAQ2B,KAAK,IAAI3B,QAAQ4B,QAAQ,KAAKC;IAE1D,MAAM5B,UAA2D;QAC/D,GAAIyB,aAAa;YAAC,SAAST;QAAS,IAAI,CAAC,CAAC;QAC1C,GAAGC,eAAe;QAClB,GAAGM,gBAAgB;IACrB;IAEA,MAAMM,iBAA2C9B,QAAQ2B,KAAK,GAC1D;QAACC,UAAU5B,QAAQ4B,QAAQ;QAAED,OAAO;IAAI,IACxC;QAACA,OAAO;QAAOI,kBAAkB/B,QAAQ+B,gBAAgB;IAAA;IAE7D,OAAO;QACLnC,wBAAwB;YAACoC,OAAOf;QAAS;QACzCnB,wBAAwBgC;QACxBjC,yBAAyB;YAAC0B;QAAS;QACnC5B,uBAAuB;YAACM;YAASE;QAAI;KACtC;AACH,EAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { federation as moduleFederation } from '@module-federation/vite';
|
|
2
|
+
import { FEDERATION_DIR_NAME, FEDERATION_FILE_NAME } from '../constants.js';
|
|
3
|
+
export function sanityModuleFederation({ exposes, name }) {
|
|
4
|
+
const mfPlugins = moduleFederation({
|
|
5
|
+
dev: {
|
|
6
|
+
disableDynamicRemoteTypeHints: true,
|
|
7
|
+
remoteHmr: true
|
|
8
|
+
},
|
|
9
|
+
// Remote type generation stays off: it compiles the exposes with the
|
|
10
|
+
// project's tsconfig, and that breaks twice over on real projects.
|
|
11
|
+
// The exposes are generated .js/.jsx shims, which tsc refuses without
|
|
12
|
+
// allowJs (TYPE-001/TS6504) — no app template sets it. And with allowJs
|
|
13
|
+
// worked around, declaration emit pulls in the user's own modules, which
|
|
14
|
+
// are noEmit projects never written to be declaration-emittable: TS2742
|
|
15
|
+
// (non-portable inferred types, endemic under pnpm) and TS4082 (private
|
|
16
|
+
// names in default exports) then fail the compile just the same.
|
|
17
|
+
dts: {
|
|
18
|
+
generateTypes: false
|
|
19
|
+
},
|
|
20
|
+
exposes,
|
|
21
|
+
filename: `${FEDERATION_FILE_NAME}.js`,
|
|
22
|
+
manifest: true,
|
|
23
|
+
name,
|
|
24
|
+
// Resolves the remote entry path relative to the manifest rather than the
|
|
25
|
+
// host origin.
|
|
26
|
+
publicPath: 'auto',
|
|
27
|
+
// @module-federation/vite auto-shares every package.json dependency
|
|
28
|
+
// that exposes an `exports` field. That breaks for workspace packages with
|
|
29
|
+
// subpath-only exports (no `.` entry) like `@sanity/cli-build` and
|
|
30
|
+
// `@sanity/workbench`, because vite tries to resolve them as bare imports
|
|
31
|
+
// and fails. Workbench remotes manage runtime sharing through the host's
|
|
32
|
+
// federation runtime, so we opt out of auto-share entirely.
|
|
33
|
+
shared: {}
|
|
34
|
+
});
|
|
35
|
+
// module-federation delivers its dts plugin as a Promise resolving to an
|
|
36
|
+
// array of plugins; spreading a promise (or an array) yields a junk object,
|
|
37
|
+
// which silently drops the plugin. Recurse through the PluginOption shape so
|
|
38
|
+
// every actual plugin gets scoped.
|
|
39
|
+
const scopeToEnvironment = (option)=>{
|
|
40
|
+
if (!option) return option;
|
|
41
|
+
if (option instanceof Promise) return option.then((resolved)=>scopeToEnvironment(resolved));
|
|
42
|
+
if (Array.isArray(option)) return option.map((entry)=>scopeToEnvironment(entry));
|
|
43
|
+
return {
|
|
44
|
+
...option,
|
|
45
|
+
// In dev, MF must run on client — the dev server serves through it.
|
|
46
|
+
// In build, scope to the federation environment to keep the library build clean.
|
|
47
|
+
applyToEnvironment: (env)=>env.config.command === 'serve' || env.name === FEDERATION_DIR_NAME
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
return mfPlugins.map((plugin)=>scopeToEnvironment(plugin));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=plugin-module-federation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/actions/build/vite/plugins/plugin-module-federation.ts"],"sourcesContent":["import {federation as moduleFederation, type ModuleFederationOptions} from '@module-federation/vite'\nimport {type Plugin, type PluginOption} from 'vite'\n\nimport {FEDERATION_DIR_NAME, FEDERATION_FILE_NAME} from '../constants.js'\n\n/**\n * @internal\n */\nexport interface FederationOptions extends Pick<ModuleFederationOptions, 'exposes'> {\n /**\n * namespace of the federation build, used as the global variable name for the exposed modules\n * e.g `@acme/studio` would then allow you to import modules like `import (\"@acme/studio/Button\")`\n * defaults to your package.json name if not provided.\n */\n name: string\n}\n\nexport function sanityModuleFederation({exposes, name}: FederationOptions): PluginOption {\n const mfPlugins = moduleFederation({\n dev: {\n disableDynamicRemoteTypeHints: true,\n remoteHmr: true,\n },\n // Remote type generation stays off: it compiles the exposes with the\n // project's tsconfig, and that breaks twice over on real projects.\n // The exposes are generated .js/.jsx shims, which tsc refuses without\n // allowJs (TYPE-001/TS6504) — no app template sets it. And with allowJs\n // worked around, declaration emit pulls in the user's own modules, which\n // are noEmit projects never written to be declaration-emittable: TS2742\n // (non-portable inferred types, endemic under pnpm) and TS4082 (private\n // names in default exports) then fail the compile just the same.\n dts: {generateTypes: false},\n exposes,\n filename: `${FEDERATION_FILE_NAME}.js`,\n manifest: true,\n name,\n // Resolves the remote entry path relative to the manifest rather than the\n // host origin.\n publicPath: 'auto',\n // @module-federation/vite auto-shares every package.json dependency\n // that exposes an `exports` field. That breaks for workspace packages with\n // subpath-only exports (no `.` entry) like `@sanity/cli-build` and\n // `@sanity/workbench`, because vite tries to resolve them as bare imports\n // and fails. Workbench remotes manage runtime sharing through the host's\n // federation runtime, so we opt out of auto-share entirely.\n shared: {},\n })\n\n // module-federation delivers its dts plugin as a Promise resolving to an\n // array of plugins; spreading a promise (or an array) yields a junk object,\n // which silently drops the plugin. Recurse through the PluginOption shape so\n // every actual plugin gets scoped.\n const scopeToEnvironment = (option: PluginOption): PluginOption => {\n if (!option) return option\n if (option instanceof Promise) return option.then((resolved) => scopeToEnvironment(resolved))\n if (Array.isArray(option)) return option.map((entry) => scopeToEnvironment(entry))\n return {\n ...option,\n // In dev, MF must run on client — the dev server serves through it.\n // In build, scope to the federation environment to keep the library build clean.\n applyToEnvironment: (env) =>\n env.config.command === 'serve' || env.name === FEDERATION_DIR_NAME,\n } satisfies Plugin\n }\n\n return mfPlugins.map((plugin: PluginOption) => scopeToEnvironment(plugin))\n}\n"],"names":["federation","moduleFederation","FEDERATION_DIR_NAME","FEDERATION_FILE_NAME","sanityModuleFederation","exposes","name","mfPlugins","dev","disableDynamicRemoteTypeHints","remoteHmr","dts","generateTypes","filename","manifest","publicPath","shared","scopeToEnvironment","option","Promise","then","resolved","Array","isArray","map","entry","applyToEnvironment","env","config","command","plugin"],"mappings":"AAAA,SAAQA,cAAcC,gBAAgB,QAAqC,0BAAyB;AAGpG,SAAQC,mBAAmB,EAAEC,oBAAoB,QAAO,kBAAiB;AAczE,OAAO,SAASC,uBAAuB,EAACC,OAAO,EAAEC,IAAI,EAAoB;IACvE,MAAMC,YAAYN,iBAAiB;QACjCO,KAAK;YACHC,+BAA+B;YAC/BC,WAAW;QACb;QACA,qEAAqE;QACrE,mEAAmE;QACnE,sEAAsE;QACtE,wEAAwE;QACxE,yEAAyE;QACzE,wEAAwE;QACxE,wEAAwE;QACxE,iEAAiE;QACjEC,KAAK;YAACC,eAAe;QAAK;QAC1BP;QACAQ,UAAU,GAAGV,qBAAqB,GAAG,CAAC;QACtCW,UAAU;QACVR;QACA,0EAA0E;QAC1E,eAAe;QACfS,YAAY;QACZ,oEAAoE;QACpE,2EAA2E;QAC3E,mEAAmE;QACnE,0EAA0E;QAC1E,yEAAyE;QACzE,4DAA4D;QAC5DC,QAAQ,CAAC;IACX;IAEA,yEAAyE;IACzE,4EAA4E;IAC5E,6EAA6E;IAC7E,mCAAmC;IACnC,MAAMC,qBAAqB,CAACC;QAC1B,IAAI,CAACA,QAAQ,OAAOA;QACpB,IAAIA,kBAAkBC,SAAS,OAAOD,OAAOE,IAAI,CAAC,CAACC,WAAaJ,mBAAmBI;QACnF,IAAIC,MAAMC,OAAO,CAACL,SAAS,OAAOA,OAAOM,GAAG,CAAC,CAACC,QAAUR,mBAAmBQ;QAC3E,OAAO;YACL,GAAGP,MAAM;YACT,oEAAoE;YACpE,iFAAiF;YACjFQ,oBAAoB,CAACC,MACnBA,IAAIC,MAAM,CAACC,OAAO,KAAK,WAAWF,IAAIrB,IAAI,KAAKJ;QACnD;IACF;IAEA,OAAOK,UAAUiB,GAAG,CAAC,CAACM,SAAyBb,mBAAmBa;AACpE"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FEDERATION_DIR_NAME } from '../constants.js';
|
|
2
|
+
export function sanityEnvironmentPlugin(options) {
|
|
3
|
+
return {
|
|
4
|
+
config () {
|
|
5
|
+
return {
|
|
6
|
+
builder: {
|
|
7
|
+
async buildApp (builder) {
|
|
8
|
+
await builder.build(builder.environments[FEDERATION_DIR_NAME]);
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
environments: {
|
|
12
|
+
[FEDERATION_DIR_NAME]: {
|
|
13
|
+
build: {
|
|
14
|
+
copyPublicDir: false,
|
|
15
|
+
outDir: `dist`,
|
|
16
|
+
rollupOptions: {
|
|
17
|
+
input: options.input
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
consumer: 'client'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
name: 'sanity/environment'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=plugin-sanity-environment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/actions/build/vite/plugins/plugin-sanity-environment.ts"],"sourcesContent":["import {type Plugin} from 'vite'\n\nimport {FEDERATION_DIR_NAME} from '../constants.js'\n\ninterface EnvironmentOptions {\n input: string\n}\n\nexport function sanityEnvironmentPlugin(options: EnvironmentOptions): Plugin {\n return {\n config() {\n return {\n builder: {\n async buildApp(builder) {\n await builder.build(builder.environments[FEDERATION_DIR_NAME])\n },\n },\n environments: {\n [FEDERATION_DIR_NAME]: {\n build: {\n copyPublicDir: false,\n outDir: `dist`,\n rollupOptions: {input: options.input},\n },\n consumer: 'client',\n },\n },\n }\n },\n name: 'sanity/environment',\n }\n}\n"],"names":["FEDERATION_DIR_NAME","sanityEnvironmentPlugin","options","config","builder","buildApp","build","environments","copyPublicDir","outDir","rollupOptions","input","consumer","name"],"mappings":"AAEA,SAAQA,mBAAmB,QAAO,kBAAiB;AAMnD,OAAO,SAASC,wBAAwBC,OAA2B;IACjE,OAAO;QACLC;YACE,OAAO;gBACLC,SAAS;oBACP,MAAMC,UAASD,OAAO;wBACpB,MAAMA,QAAQE,KAAK,CAACF,QAAQG,YAAY,CAACP,oBAAoB;oBAC/D;gBACF;gBACAO,cAAc;oBACZ,CAACP,oBAAoB,EAAE;wBACrBM,OAAO;4BACLE,eAAe;4BACfC,QAAQ,CAAC,IAAI,CAAC;4BACdC,eAAe;gCAACC,OAAOT,QAAQS,KAAK;4BAAA;wBACtC;wBACAC,UAAU;oBACZ;gBACF;YACF;QACF;QACAC,MAAM;IACR;AACF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { RUNTIME_DIR } from '../constants.js';
|
|
4
|
+
function relativeImport(fromFile, toFile) {
|
|
5
|
+
const rel = path.relative(path.dirname(fromFile), toFile).split(path.sep).join('/');
|
|
6
|
+
return rel.startsWith('.') ? rel : `./${rel}`;
|
|
7
|
+
}
|
|
8
|
+
function writeArtifacts(root, artifacts) {
|
|
9
|
+
for (const artifact of artifacts){
|
|
10
|
+
const artifactPath = path.resolve(root, RUNTIME_DIR, artifact.path);
|
|
11
|
+
fs.mkdirSync(path.dirname(artifactPath), {
|
|
12
|
+
recursive: true
|
|
13
|
+
});
|
|
14
|
+
fs.writeFileSync(artifactPath, artifact.source({
|
|
15
|
+
resolveImport: (src)=>relativeImport(artifactPath, path.resolve(root, src))
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Writes the federation runtime artifacts (view render-contract modules, service
|
|
21
|
+
* worker bundles + loaders) into `RUNTIME_DIR`, resolving each artifact's import
|
|
22
|
+
* paths relative to where it lands on disk. The set is expanded once by
|
|
23
|
+
* `workbenchArtifacts` and handed in — this plugin only writes it.
|
|
24
|
+
*/ export function sanityExtensionArtifacts(options) {
|
|
25
|
+
return {
|
|
26
|
+
configResolved (config) {
|
|
27
|
+
writeArtifacts(config.root, options.artifacts);
|
|
28
|
+
},
|
|
29
|
+
name: 'sanity/extension-artifacts'
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=plugin-sanity-extension-artifacts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/actions/build/vite/plugins/plugin-sanity-extension-artifacts.ts"],"sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\n\nimport {type Plugin} from 'vite'\n\nimport {type GeneratedArtifact} from '../../artifact.js'\nimport {RUNTIME_DIR} from '../constants.js'\n\nfunction relativeImport(fromFile: string, toFile: string): string {\n const rel = path.relative(path.dirname(fromFile), toFile).split(path.sep).join('/')\n return rel.startsWith('.') ? rel : `./${rel}`\n}\n\nfunction writeArtifacts(root: string, artifacts: readonly GeneratedArtifact[]): void {\n for (const artifact of artifacts) {\n const artifactPath = path.resolve(root, RUNTIME_DIR, artifact.path)\n fs.mkdirSync(path.dirname(artifactPath), {recursive: true})\n fs.writeFileSync(\n artifactPath,\n artifact.source({\n resolveImport: (src) => relativeImport(artifactPath, path.resolve(root, src)),\n }),\n )\n }\n}\n\n/**\n * Writes the federation runtime artifacts (view render-contract modules, service\n * worker bundles + loaders) into `RUNTIME_DIR`, resolving each artifact's import\n * paths relative to where it lands on disk. The set is expanded once by\n * `workbenchArtifacts` and handed in — this plugin only writes it.\n */\nexport function sanityExtensionArtifacts(options: {\n artifacts: readonly GeneratedArtifact[]\n}): Plugin {\n return {\n configResolved(config) {\n writeArtifacts(config.root, options.artifacts)\n },\n name: 'sanity/extension-artifacts',\n }\n}\n"],"names":["fs","path","RUNTIME_DIR","relativeImport","fromFile","toFile","rel","relative","dirname","split","sep","join","startsWith","writeArtifacts","root","artifacts","artifact","artifactPath","resolve","mkdirSync","recursive","writeFileSync","source","resolveImport","src","sanityExtensionArtifacts","options","configResolved","config","name"],"mappings":"AAAA,OAAOA,QAAQ,UAAS;AACxB,OAAOC,UAAU,YAAW;AAK5B,SAAQC,WAAW,QAAO,kBAAiB;AAE3C,SAASC,eAAeC,QAAgB,EAAEC,MAAc;IACtD,MAAMC,MAAML,KAAKM,QAAQ,CAACN,KAAKO,OAAO,CAACJ,WAAWC,QAAQI,KAAK,CAACR,KAAKS,GAAG,EAAEC,IAAI,CAAC;IAC/E,OAAOL,IAAIM,UAAU,CAAC,OAAON,MAAM,CAAC,EAAE,EAAEA,KAAK;AAC/C;AAEA,SAASO,eAAeC,IAAY,EAAEC,SAAuC;IAC3E,KAAK,MAAMC,YAAYD,UAAW;QAChC,MAAME,eAAehB,KAAKiB,OAAO,CAACJ,MAAMZ,aAAac,SAASf,IAAI;QAClED,GAAGmB,SAAS,CAAClB,KAAKO,OAAO,CAACS,eAAe;YAACG,WAAW;QAAI;QACzDpB,GAAGqB,aAAa,CACdJ,cACAD,SAASM,MAAM,CAAC;YACdC,eAAe,CAACC,MAAQrB,eAAec,cAAchB,KAAKiB,OAAO,CAACJ,MAAMU;QAC1E;IAEJ;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASC,yBAAyBC,OAExC;IACC,OAAO;QACLC,gBAAeC,MAAM;YACnBf,eAAee,OAAOd,IAAI,EAAEY,QAAQX,SAAS;QAC/C;QACAc,MAAM;IACR;AACF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { renderRemote } from '../../render-remote.js';
|
|
4
|
+
import { FEDERATION_FILE_NAME, RUNTIME_DIR } from '../constants.js';
|
|
5
|
+
const REMOTE_ENTRY_FILE = `${FEDERATION_FILE_NAME}.jsx`;
|
|
6
|
+
// The studio wraps `Studio` with the user's config; HMR re-renders through the
|
|
7
|
+
// new module so a config edit takes effect.
|
|
8
|
+
const STUDIO_ENTRY = renderRemote({
|
|
9
|
+
app: `(props) => createElement(Studio, { config, ...props })`,
|
|
10
|
+
hmr: true,
|
|
11
|
+
preamble: `import { Studio } from 'sanity'
|
|
12
|
+
import config from %STUDIO_CONFIG%`
|
|
13
|
+
});
|
|
14
|
+
// An SDK app's default export is the component; it Fast-Refreshes through its
|
|
15
|
+
// own dev server, so the wrapper needs no HMR boundary.
|
|
16
|
+
const APP_ENTRY = renderRemote({
|
|
17
|
+
preamble: `import App from %APP_ENTRY%`
|
|
18
|
+
});
|
|
19
|
+
// A branded app that declares no `entry` (e.g. a dock-only panel/worker app)
|
|
20
|
+
// has no navigable full-page view, so there's no `App` to import. The runtime
|
|
21
|
+
// still needs a valid module for the federation build input, but it exposes no
|
|
22
|
+
// `./App` (see `plugin.ts`) — its `render` is unreachable and throws if reached.
|
|
23
|
+
const HEADLESS_APP_ENTRY = `\
|
|
24
|
+
// This file is auto-generated on 'sanity dev'
|
|
25
|
+
// Modifications to this file are automatically discarded
|
|
26
|
+
// This application declares no app view (no \`entry\`): it isn't navigable as a
|
|
27
|
+
// full-page app, only its panels/services are exposed.
|
|
28
|
+
export function render() {
|
|
29
|
+
throw new Error('This application has no app view: it declares no \`entry\`.')
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
export function sanityFederationRuntime(options) {
|
|
33
|
+
let content;
|
|
34
|
+
if (options.isApp) {
|
|
35
|
+
content = options.appEntry ? APP_ENTRY.replace(/%APP_ENTRY%/, JSON.stringify(options.appEntry)) : HEADLESS_APP_ENTRY;
|
|
36
|
+
} else {
|
|
37
|
+
content = STUDIO_ENTRY.replace(/%STUDIO_CONFIG%/, JSON.stringify(options.studioConfigPath));
|
|
38
|
+
}
|
|
39
|
+
let entryFileAbsPath = '';
|
|
40
|
+
return {
|
|
41
|
+
configResolved (config) {
|
|
42
|
+
const dir = path.resolve(config.root, RUNTIME_DIR);
|
|
43
|
+
entryFileAbsPath = path.join(dir, REMOTE_ENTRY_FILE);
|
|
44
|
+
fs.mkdirSync(dir, {
|
|
45
|
+
recursive: true
|
|
46
|
+
});
|
|
47
|
+
fs.writeFileSync(entryFileAbsPath, content);
|
|
48
|
+
},
|
|
49
|
+
hotUpdate ({ file, modules, timestamp }) {
|
|
50
|
+
if (options.isApp) return;
|
|
51
|
+
if (this.environment.name !== 'client') return;
|
|
52
|
+
const { moduleGraph } = this.environment;
|
|
53
|
+
const studioMods = moduleGraph.getModulesByFile(entryFileAbsPath);
|
|
54
|
+
if (!studioMods?.size) return;
|
|
55
|
+
// Is the changed file reachable from the studio entry?
|
|
56
|
+
const visited = new Set();
|
|
57
|
+
const queue = [
|
|
58
|
+
...studioMods
|
|
59
|
+
];
|
|
60
|
+
while(queue.length > 0){
|
|
61
|
+
const mod = queue.pop();
|
|
62
|
+
if (visited.has(mod)) continue;
|
|
63
|
+
visited.add(mod);
|
|
64
|
+
if (mod.file === file) {
|
|
65
|
+
// The walk from `file` up through importers dead-ends at federation
|
|
66
|
+
// gaps, so invalidate changed modules ourselves and route HMR to the
|
|
67
|
+
// self-accepting studio entry.
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (const m of modules){
|
|
70
|
+
moduleGraph.invalidateModule(m, seen, timestamp, true);
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
...studioMods
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
for (const dep of mod.importedModules)queue.push(dep);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
name: 'sanity/federation-runtime'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=plugin-sanity-federation-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/actions/build/vite/plugins/plugin-sanity-federation-runtime.ts"],"sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\n\nimport {type EnvironmentModuleNode, type Plugin} from 'vite'\n\nimport {renderRemote} from '../../render-remote.js'\nimport {FEDERATION_FILE_NAME, RUNTIME_DIR} from '../constants.js'\n\nconst REMOTE_ENTRY_FILE = `${FEDERATION_FILE_NAME}.jsx`\n\n// The studio wraps `Studio` with the user's config; HMR re-renders through the\n// new module so a config edit takes effect.\nconst STUDIO_ENTRY = renderRemote({\n app: `(props) => createElement(Studio, { config, ...props })`,\n hmr: true,\n preamble: `import { Studio } from 'sanity'\nimport config from %STUDIO_CONFIG%`,\n})\n\n// An SDK app's default export is the component; it Fast-Refreshes through its\n// own dev server, so the wrapper needs no HMR boundary.\nconst APP_ENTRY = renderRemote({preamble: `import App from %APP_ENTRY%`})\n\n// A branded app that declares no `entry` (e.g. a dock-only panel/worker app)\n// has no navigable full-page view, so there's no `App` to import. The runtime\n// still needs a valid module for the federation build input, but it exposes no\n// `./App` (see `plugin.ts`) — its `render` is unreachable and throws if reached.\nconst HEADLESS_APP_ENTRY = `\\\n// This file is auto-generated on 'sanity dev'\n// Modifications to this file are automatically discarded\n// This application declares no app view (no \\`entry\\`): it isn't navigable as a\n// full-page app, only its panels/services are exposed.\nexport function render() {\n throw new Error('This application has no app view: it declares no \\`entry\\`.')\n}\n`\n\nexport type FederationRuntimeOptions =\n | {appEntry?: string; isApp: true}\n | {isApp: false; studioConfigPath: string}\n\nexport function sanityFederationRuntime(options: FederationRuntimeOptions): Plugin {\n let content: string\n if (options.isApp) {\n content = options.appEntry\n ? APP_ENTRY.replace(/%APP_ENTRY%/, JSON.stringify(options.appEntry))\n : HEADLESS_APP_ENTRY\n } else {\n content = STUDIO_ENTRY.replace(/%STUDIO_CONFIG%/, JSON.stringify(options.studioConfigPath))\n }\n\n let entryFileAbsPath = ''\n\n return {\n configResolved(config) {\n const dir = path.resolve(config.root, RUNTIME_DIR)\n entryFileAbsPath = path.join(dir, REMOTE_ENTRY_FILE)\n\n fs.mkdirSync(dir, {recursive: true})\n fs.writeFileSync(entryFileAbsPath, content)\n },\n hotUpdate({file, modules, timestamp}) {\n if (options.isApp) return\n if (this.environment.name !== 'client') return\n\n const {moduleGraph} = this.environment\n const studioMods = moduleGraph.getModulesByFile(entryFileAbsPath)\n if (!studioMods?.size) return\n\n // Is the changed file reachable from the studio entry?\n const visited = new Set<EnvironmentModuleNode>()\n const queue: EnvironmentModuleNode[] = [...studioMods]\n while (queue.length > 0) {\n const mod = queue.pop()!\n if (visited.has(mod)) continue\n visited.add(mod)\n if (mod.file === file) {\n // The walk from `file` up through importers dead-ends at federation\n // gaps, so invalidate changed modules ourselves and route HMR to the\n // self-accepting studio entry.\n const seen = new Set<EnvironmentModuleNode>()\n for (const m of modules) {\n moduleGraph.invalidateModule(m, seen, timestamp, true)\n }\n return [...studioMods]\n }\n for (const dep of mod.importedModules) queue.push(dep)\n }\n },\n name: 'sanity/federation-runtime',\n }\n}\n"],"names":["fs","path","renderRemote","FEDERATION_FILE_NAME","RUNTIME_DIR","REMOTE_ENTRY_FILE","STUDIO_ENTRY","app","hmr","preamble","APP_ENTRY","HEADLESS_APP_ENTRY","sanityFederationRuntime","options","content","isApp","appEntry","replace","JSON","stringify","studioConfigPath","entryFileAbsPath","configResolved","config","dir","resolve","root","join","mkdirSync","recursive","writeFileSync","hotUpdate","file","modules","timestamp","environment","name","moduleGraph","studioMods","getModulesByFile","size","visited","Set","queue","length","mod","pop","has","add","seen","m","invalidateModule","dep","importedModules","push"],"mappings":"AAAA,OAAOA,QAAQ,UAAS;AACxB,OAAOC,UAAU,YAAW;AAI5B,SAAQC,YAAY,QAAO,yBAAwB;AACnD,SAAQC,oBAAoB,EAAEC,WAAW,QAAO,kBAAiB;AAEjE,MAAMC,oBAAoB,GAAGF,qBAAqB,IAAI,CAAC;AAEvD,+EAA+E;AAC/E,4CAA4C;AAC5C,MAAMG,eAAeJ,aAAa;IAChCK,KAAK,CAAC,sDAAsD,CAAC;IAC7DC,KAAK;IACLC,UAAU,CAAC;kCACqB,CAAC;AACnC;AAEA,8EAA8E;AAC9E,wDAAwD;AACxD,MAAMC,YAAYR,aAAa;IAACO,UAAU,CAAC,2BAA2B,CAAC;AAAA;AAEvE,6EAA6E;AAC7E,8EAA8E;AAC9E,+EAA+E;AAC/E,iFAAiF;AACjF,MAAME,qBAAqB,CAAC;;;;;;;;AAQ5B,CAAC;AAMD,OAAO,SAASC,wBAAwBC,OAAiC;IACvE,IAAIC;IACJ,IAAID,QAAQE,KAAK,EAAE;QACjBD,UAAUD,QAAQG,QAAQ,GACtBN,UAAUO,OAAO,CAAC,eAAeC,KAAKC,SAAS,CAACN,QAAQG,QAAQ,KAChEL;IACN,OAAO;QACLG,UAAUR,aAAaW,OAAO,CAAC,mBAAmBC,KAAKC,SAAS,CAACN,QAAQO,gBAAgB;IAC3F;IAEA,IAAIC,mBAAmB;IAEvB,OAAO;QACLC,gBAAeC,MAAM;YACnB,MAAMC,MAAMvB,KAAKwB,OAAO,CAACF,OAAOG,IAAI,EAAEtB;YACtCiB,mBAAmBpB,KAAK0B,IAAI,CAACH,KAAKnB;YAElCL,GAAG4B,SAAS,CAACJ,KAAK;gBAACK,WAAW;YAAI;YAClC7B,GAAG8B,aAAa,CAACT,kBAAkBP;QACrC;QACAiB,WAAU,EAACC,IAAI,EAAEC,OAAO,EAAEC,SAAS,EAAC;YAClC,IAAIrB,QAAQE,KAAK,EAAE;YACnB,IAAI,IAAI,CAACoB,WAAW,CAACC,IAAI,KAAK,UAAU;YAExC,MAAM,EAACC,WAAW,EAAC,GAAG,IAAI,CAACF,WAAW;YACtC,MAAMG,aAAaD,YAAYE,gBAAgB,CAAClB;YAChD,IAAI,CAACiB,YAAYE,MAAM;YAEvB,uDAAuD;YACvD,MAAMC,UAAU,IAAIC;YACpB,MAAMC,QAAiC;mBAAIL;aAAW;YACtD,MAAOK,MAAMC,MAAM,GAAG,EAAG;gBACvB,MAAMC,MAAMF,MAAMG,GAAG;gBACrB,IAAIL,QAAQM,GAAG,CAACF,MAAM;gBACtBJ,QAAQO,GAAG,CAACH;gBACZ,IAAIA,IAAIb,IAAI,KAAKA,MAAM;oBACrB,oEAAoE;oBACpE,qEAAqE;oBACrE,+BAA+B;oBAC/B,MAAMiB,OAAO,IAAIP;oBACjB,KAAK,MAAMQ,KAAKjB,QAAS;wBACvBI,YAAYc,gBAAgB,CAACD,GAAGD,MAAMf,WAAW;oBACnD;oBACA,OAAO;2BAAII;qBAAW;gBACxB;gBACA,KAAK,MAAMc,OAAOP,IAAIQ,eAAe,CAAEV,MAAMW,IAAI,CAACF;YACpD;QACF;QACAhB,MAAM;IACR;AACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The build-facing entry of the workbench federation stack: turn a workbench
|
|
2
|
+
// app's build inputs into the Vite plugins that produce its module-federation
|
|
3
|
+
// remote. `@sanity/cli-build`'s `getViteConfig` calls this instead of
|
|
4
|
+
// assembling `federation`'s options itself, so the discriminated app-vs-studio
|
|
5
|
+
// option shape, the no-app-view rule, and the studio-config requirement all
|
|
6
|
+
// live here next to `federation` — the build package just hands over its inputs.
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { readPackageJson } from '@sanity/cli-core';
|
|
9
|
+
import { federation } from './plugin.js';
|
|
10
|
+
/**
|
|
11
|
+
* A workbench studio renders from its `sanity.config.*`, so the build needs one.
|
|
12
|
+
* An explicit `applicationType: 'studio'` wins over detection, so a studio can
|
|
13
|
+
* reach here with no config file — fail with the fix rather than a cryptic build
|
|
14
|
+
* error downstream.
|
|
15
|
+
*/ function requireStudioConfigPath(relativeConfigLocation) {
|
|
16
|
+
if (relativeConfigLocation === null) {
|
|
17
|
+
throw new Error('Workbench studios need a sanity.config.js or sanity.config.ts file. ' + "Add one, or remove `applicationType: 'studio'` from `unstable_defineApp` " + 'to let the CLI infer the application type.');
|
|
18
|
+
}
|
|
19
|
+
return relativeConfigLocation;
|
|
20
|
+
}
|
|
21
|
+
/** Build the Vite plugins for a workbench app's module-federation remote. */ export async function workbenchVitePlugins(options) {
|
|
22
|
+
const { cwd, entries, isApp, services, views } = options;
|
|
23
|
+
const pkgJson = await readPackageJson(path.join(cwd, 'package.json'));
|
|
24
|
+
return federation({
|
|
25
|
+
...isApp ? {
|
|
26
|
+
// `null` relativeEntry (a branded app with no `entry`) → omit `appEntry`,
|
|
27
|
+
// so the remote exposes no `./App`, only its views.
|
|
28
|
+
...entries.relativeEntry ? {
|
|
29
|
+
appEntry: entries.relativeEntry
|
|
30
|
+
} : {},
|
|
31
|
+
isApp: true
|
|
32
|
+
} : {
|
|
33
|
+
isApp: false,
|
|
34
|
+
studioConfigPath: requireStudioConfigPath(entries.relativeConfigLocation)
|
|
35
|
+
},
|
|
36
|
+
pkgJson,
|
|
37
|
+
services,
|
|
38
|
+
views,
|
|
39
|
+
workDir: cwd
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=workbench-vite-plugins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/actions/build/vite/workbench-vite-plugins.ts"],"sourcesContent":["// The build-facing entry of the workbench federation stack: turn a workbench\n// app's build inputs into the Vite plugins that produce its module-federation\n// remote. `@sanity/cli-build`'s `getViteConfig` calls this instead of\n// assembling `federation`'s options itself, so the discriminated app-vs-studio\n// option shape, the no-app-view rule, and the studio-config requirement all\n// live here next to `federation` — the build package just hands over its inputs.\n\nimport path from 'node:path'\n\nimport {readPackageJson} from '@sanity/cli-core'\nimport {type PluginOption} from 'vite'\n\nimport {type DefineAppInput} from '../../../defineApp.js'\nimport {federation} from './plugin.js'\n\ninterface WorkbenchViteOptions {\n /** Project root — read for the federation remote name, and the plugin workDir. */\n cwd: string\n /**\n * Build entry paths relative to the federation runtime dir. `relativeEntry` is\n * the app's `entry` (null for a dock-only app with no app view);\n * `relativeConfigLocation` is the studio's `sanity.config.*` (null when absent).\n */\n entries: {relativeConfigLocation: string | null; relativeEntry: string | null}\n\n /** App (vs studio) build — selects the discriminated federation option shape. */\n isApp?: boolean\n /** Declared background services. */\n services?: DefineAppInput['services']\n /** Declared dock views. */\n views?: DefineAppInput['views']\n}\n\n/**\n * A workbench studio renders from its `sanity.config.*`, so the build needs one.\n * An explicit `applicationType: 'studio'` wins over detection, so a studio can\n * reach here with no config file — fail with the fix rather than a cryptic build\n * error downstream.\n */\nfunction requireStudioConfigPath(relativeConfigLocation: string | null): string {\n if (relativeConfigLocation === null) {\n throw new Error(\n 'Workbench studios need a sanity.config.js or sanity.config.ts file. ' +\n \"Add one, or remove `applicationType: 'studio'` from `unstable_defineApp` \" +\n 'to let the CLI infer the application type.',\n )\n }\n return relativeConfigLocation\n}\n\n/** Build the Vite plugins for a workbench app's module-federation remote. */\nexport async function workbenchVitePlugins(options: WorkbenchViteOptions): Promise<PluginOption> {\n const {cwd, entries, isApp, services, views} = options\n const pkgJson = await readPackageJson(path.join(cwd, 'package.json'))\n\n return federation({\n ...(isApp\n ? {\n // `null` relativeEntry (a branded app with no `entry`) → omit `appEntry`,\n // so the remote exposes no `./App`, only its views.\n ...(entries.relativeEntry ? {appEntry: entries.relativeEntry} : {}),\n isApp: true as const,\n }\n : {\n isApp: false as const,\n studioConfigPath: requireStudioConfigPath(entries.relativeConfigLocation),\n }),\n pkgJson,\n services,\n views,\n workDir: cwd,\n })\n}\n"],"names":["path","readPackageJson","federation","requireStudioConfigPath","relativeConfigLocation","Error","workbenchVitePlugins","options","cwd","entries","isApp","services","views","pkgJson","join","relativeEntry","appEntry","studioConfigPath","workDir"],"mappings":"AAAA,6EAA6E;AAC7E,8EAA8E;AAC9E,sEAAsE;AACtE,+EAA+E;AAC/E,4EAA4E;AAC5E,iFAAiF;AAEjF,OAAOA,UAAU,YAAW;AAE5B,SAAQC,eAAe,QAAO,mBAAkB;AAIhD,SAAQC,UAAU,QAAO,cAAa;AAoBtC;;;;;CAKC,GACD,SAASC,wBAAwBC,sBAAqC;IACpE,IAAIA,2BAA2B,MAAM;QACnC,MAAM,IAAIC,MACR,yEACE,8EACA;IAEN;IACA,OAAOD;AACT;AAEA,2EAA2E,GAC3E,OAAO,eAAeE,qBAAqBC,OAA6B;IACtE,MAAM,EAACC,GAAG,EAAEC,OAAO,EAAEC,KAAK,EAAEC,QAAQ,EAAEC,KAAK,EAAC,GAAGL;IAC/C,MAAMM,UAAU,MAAMZ,gBAAgBD,KAAKc,IAAI,CAACN,KAAK;IAErD,OAAON,WAAW;QAChB,GAAIQ,QACA;YACE,0EAA0E;YAC1E,oDAAoD;YACpD,GAAID,QAAQM,aAAa,GAAG;gBAACC,UAAUP,QAAQM,aAAa;YAAA,IAAI,CAAC,CAAC;YAClEL,OAAO;QACT,IACA;YACEA,OAAO;YACPO,kBAAkBd,wBAAwBM,QAAQL,sBAAsB;QAC1E,CAAC;QACLS;QACAF;QACAC;QACAM,SAASV;IACX;AACF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The deploy command's view of a workbench app: the resolved interfaces plus
|
|
2
|
+
// the two deploy-time guards. `sanity deploy` calls `getWorkbench(config)` once
|
|
3
|
+
// and either gets `null` (plain project — normal deploy) or an object it asks
|
|
4
|
+
// to validate the app and its build output before shipping.
|
|
5
|
+
//
|
|
6
|
+
// Node-only (the build-output guard touches the filesystem).
|
|
7
|
+
import { stat } from 'node:fs/promises';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { resolveWorkbenchApp } from '../../resolveWorkbenchApp.js';
|
|
10
|
+
export function getWorkbench(cliConfig) {
|
|
11
|
+
const app = resolveWorkbenchApp(cliConfig);
|
|
12
|
+
if (!app) return null;
|
|
13
|
+
const { entry, services, views } = app;
|
|
14
|
+
return {
|
|
15
|
+
...app,
|
|
16
|
+
assertDeployable () {
|
|
17
|
+
if (!entry && views.length === 0 && services.length === 0) {
|
|
18
|
+
throw new Error('Nothing to deploy: `unstable_defineApp` declares no entry, views or services. ' + 'Add at least one to the app config.');
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
async checkBuiltOutput (sourceDir) {
|
|
22
|
+
try {
|
|
23
|
+
const stats = await stat(sourceDir);
|
|
24
|
+
if (!stats.isDirectory()) {
|
|
25
|
+
throw new Error(`"${sourceDir}" is not a directory`);
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw err.code === 'ENOENT' ? new Error(`Directory "${sourceDir}" does not exist`) : err;
|
|
29
|
+
}
|
|
30
|
+
const manifestPath = join(sourceDir, 'mf-manifest.json');
|
|
31
|
+
try {
|
|
32
|
+
await stat(manifestPath);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
throw err.code === 'ENOENT' ? new Error(`"${manifestPath}" does not exist. ` + 'The deploy directory must contain a federation build created with "sanity build".') : err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=getWorkbench.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/deploy/getWorkbench.ts"],"sourcesContent":["// The deploy command's view of a workbench app: the resolved interfaces plus\n// the two deploy-time guards. `sanity deploy` calls `getWorkbench(config)` once\n// and either gets `null` (plain project — normal deploy) or an object it asks\n// to validate the app and its build output before shipping.\n//\n// Node-only (the build-output guard touches the filesystem).\n\nimport {stat} from 'node:fs/promises'\nimport {join} from 'node:path'\n\nimport {type CliConfig} from '@sanity/cli-core'\n\nimport {type ResolvedWorkbenchApp, resolveWorkbenchApp} from '../../resolveWorkbenchApp.js'\n\ninterface DeployableWorkbenchApp extends ResolvedWorkbenchApp {\n /**\n * Throws when the app declares nothing the build can expose — no entry, view\n * or service. A federated app with none would ship a remote with nothing to\n * load, so deploy gates on this before any prompts or API calls.\n */\n assertDeployable(): void\n /**\n * Throws unless `sourceDir` is a directory holding a federation build.\n * Workbench builds emit a module-federation remote instead of a static SPA,\n * so the usual `index.html` contract doesn't apply — `mf-manifest.json` is the\n * marker that `sanity build` produced a federation build.\n */\n checkBuiltOutput(sourceDir: string): Promise<void>\n}\n\nexport function getWorkbench(\n cliConfig: CliConfig | null | undefined,\n): DeployableWorkbenchApp | null {\n const app = resolveWorkbenchApp(cliConfig)\n if (!app) return null\n\n const {entry, services, views} = app\n\n return {\n ...app,\n\n assertDeployable() {\n if (!entry && views.length === 0 && services.length === 0) {\n throw new Error(\n 'Nothing to deploy: `unstable_defineApp` declares no entry, views or services. ' +\n 'Add at least one to the app config.',\n )\n }\n },\n\n async checkBuiltOutput(sourceDir) {\n try {\n const stats = await stat(sourceDir)\n if (!stats.isDirectory()) {\n throw new Error(`\"${sourceDir}\" is not a directory`)\n }\n } catch (err) {\n throw err.code === 'ENOENT' ? new Error(`Directory \"${sourceDir}\" does not exist`) : err\n }\n\n const manifestPath = join(sourceDir, 'mf-manifest.json')\n try {\n await stat(manifestPath)\n } catch (err) {\n throw err.code === 'ENOENT'\n ? new Error(\n `\"${manifestPath}\" does not exist. ` +\n 'The deploy directory must contain a federation build created with \"sanity build\".',\n )\n : err\n }\n },\n }\n}\n"],"names":["stat","join","resolveWorkbenchApp","getWorkbench","cliConfig","app","entry","services","views","assertDeployable","length","Error","checkBuiltOutput","sourceDir","stats","isDirectory","err","code","manifestPath"],"mappings":"AAAA,6EAA6E;AAC7E,gFAAgF;AAChF,8EAA8E;AAC9E,4DAA4D;AAC5D,EAAE;AACF,6DAA6D;AAE7D,SAAQA,IAAI,QAAO,mBAAkB;AACrC,SAAQC,IAAI,QAAO,YAAW;AAI9B,SAAmCC,mBAAmB,QAAO,+BAA8B;AAkB3F,OAAO,SAASC,aACdC,SAAuC;IAEvC,MAAMC,MAAMH,oBAAoBE;IAChC,IAAI,CAACC,KAAK,OAAO;IAEjB,MAAM,EAACC,KAAK,EAAEC,QAAQ,EAAEC,KAAK,EAAC,GAAGH;IAEjC,OAAO;QACL,GAAGA,GAAG;QAENI;YACE,IAAI,CAACH,SAASE,MAAME,MAAM,KAAK,KAAKH,SAASG,MAAM,KAAK,GAAG;gBACzD,MAAM,IAAIC,MACR,mFACE;YAEN;QACF;QAEA,MAAMC,kBAAiBC,SAAS;YAC9B,IAAI;gBACF,MAAMC,QAAQ,MAAMd,KAAKa;gBACzB,IAAI,CAACC,MAAMC,WAAW,IAAI;oBACxB,MAAM,IAAIJ,MAAM,CAAC,CAAC,EAAEE,UAAU,oBAAoB,CAAC;gBACrD;YACF,EAAE,OAAOG,KAAK;gBACZ,MAAMA,IAAIC,IAAI,KAAK,WAAW,IAAIN,MAAM,CAAC,WAAW,EAAEE,UAAU,gBAAgB,CAAC,IAAIG;YACvF;YAEA,MAAME,eAAejB,KAAKY,WAAW;YACrC,IAAI;gBACF,MAAMb,KAAKkB;YACb,EAAE,OAAOF,KAAK;gBACZ,MAAMA,IAAIC,IAAI,KAAK,WACf,IAAIN,MACF,CAAC,CAAC,EAAEO,aAAa,kBAAkB,CAAC,GAClC,uFAEJF;YACN;QACF;IACF;AACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a directory to its canonical (long) form before handing it to
|
|
4
|
+
* `fs.watch`.
|
|
5
|
+
*
|
|
6
|
+
* On Windows, `fs.watch` aborts with a libuv assertion
|
|
7
|
+
* (`!_wcsnicmp(filename, dir, dirlen)` in `fs-event.c`) when the watched path
|
|
8
|
+
* is an 8.3 short name — e.g. temp dirs under `RUNNER~1` — because the OS
|
|
9
|
+
* reports long-form filenames that fail libuv's prefix check.
|
|
10
|
+
* `realpathSync.native` expands short names to their long form so the
|
|
11
|
+
* prefixes match.
|
|
12
|
+
*
|
|
13
|
+
* Falls back to the original path when it can't be resolved (e.g. it doesn't
|
|
14
|
+
* exist yet), which is no worse than watching it directly.
|
|
15
|
+
*/ export function canonicalizeWatchDir(dir) {
|
|
16
|
+
try {
|
|
17
|
+
return realpathSync.native(dir);
|
|
18
|
+
} catch {
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=canonicalizeWatchDir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/canonicalizeWatchDir.ts"],"sourcesContent":["import {realpathSync} from 'node:fs'\n\n/**\n * Resolve a directory to its canonical (long) form before handing it to\n * `fs.watch`.\n *\n * On Windows, `fs.watch` aborts with a libuv assertion\n * (`!_wcsnicmp(filename, dir, dirlen)` in `fs-event.c`) when the watched path\n * is an 8.3 short name — e.g. temp dirs under `RUNNER~1` — because the OS\n * reports long-form filenames that fail libuv's prefix check.\n * `realpathSync.native` expands short names to their long form so the\n * prefixes match.\n *\n * Falls back to the original path when it can't be resolved (e.g. it doesn't\n * exist yet), which is no worse than watching it directly.\n */\nexport function canonicalizeWatchDir(dir: string): string {\n try {\n return realpathSync.native(dir)\n } catch {\n return dir\n }\n}\n"],"names":["realpathSync","canonicalizeWatchDir","dir","native"],"mappings":"AAAA,SAAQA,YAAY,QAAO,UAAS;AAEpC;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,qBAAqBC,GAAW;IAC9C,IAAI;QACF,OAAOF,aAAaG,MAAM,CAACD;IAC7B,EAAE,OAAM;QACN,OAAOA;IACT;AACF"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Check whether a process is still alive.
|
|
4
|
+
* Sends signal 0 which doesn't kill anything — just checks existence.
|
|
5
|
+
*/ function isProcessAlive(pid) {
|
|
6
|
+
try {
|
|
7
|
+
process.kill(pid, 0);
|
|
8
|
+
return true;
|
|
9
|
+
} catch (err) {
|
|
10
|
+
// EPERM means the process exists but we lack permission to signal it
|
|
11
|
+
if (err && typeof err === 'object' && 'code' in err && err.code === 'EPERM') {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Tolerance in ms when comparing stored vs OS-reported process start times. */ const START_TIME_TOLERANCE_MS = 2000;
|
|
18
|
+
/**
|
|
19
|
+
* Retrieve the OS-reported start time for a process.
|
|
20
|
+
*
|
|
21
|
+
* - Unix (macOS / Linux): `ps -o lstart=`
|
|
22
|
+
* - Windows: PowerShell `Get-CimInstance Win32_Process` (`CreationDate`).
|
|
23
|
+
* We prefer CIM over `Get-Process -Id <pid>.StartTime` because `StartTime`
|
|
24
|
+
* opens a process handle and can throw "Access is denied" when the target
|
|
25
|
+
* is owned by another user, while `Win32_Process.CreationDate` is readable
|
|
26
|
+
* for any process the user can enumerate.
|
|
27
|
+
*
|
|
28
|
+
* On Windows the result for {@link process.pid} is memoised because the
|
|
29
|
+
* start time of the current process never changes and PowerShell cold-start
|
|
30
|
+
* is expensive (1–3s). Without the cache, a single `sanity dev` startup
|
|
31
|
+
* spawns PowerShell 4–5 times for our own PID. Other PIDs are not cached —
|
|
32
|
+
* they may be reused over time.
|
|
33
|
+
*
|
|
34
|
+
* Returns `undefined` if the process doesn't exist or the command fails.
|
|
35
|
+
*/ let ownWindowsStartTime;
|
|
36
|
+
export function getProcessStartTime(pid) {
|
|
37
|
+
if (process.platform === 'win32') {
|
|
38
|
+
if (pid === process.pid && ownWindowsStartTime) return ownWindowsStartTime.date;
|
|
39
|
+
const result = readWindowsStartTime(pid);
|
|
40
|
+
if (pid === process.pid) ownWindowsStartTime = {
|
|
41
|
+
cached: true,
|
|
42
|
+
date: result
|
|
43
|
+
};
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
return readUnixStartTime(pid);
|
|
47
|
+
}
|
|
48
|
+
/** Test-only: clear the cached own-process start time. */ export function __resetStartTimeCacheForTesting() {
|
|
49
|
+
ownWindowsStartTime = undefined;
|
|
50
|
+
}
|
|
51
|
+
function readUnixStartTime(pid) {
|
|
52
|
+
try {
|
|
53
|
+
const output = execSync(`ps -o lstart= -p ${pid}`, {
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
stdio: [
|
|
56
|
+
'ignore',
|
|
57
|
+
'pipe',
|
|
58
|
+
'pipe'
|
|
59
|
+
],
|
|
60
|
+
timeout: 1000
|
|
61
|
+
}).trim();
|
|
62
|
+
if (!output) return undefined;
|
|
63
|
+
const date = new Date(output);
|
|
64
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function readWindowsStartTime(pid) {
|
|
70
|
+
try {
|
|
71
|
+
// `-NoProfile -NonInteractive` keeps PowerShell start-up minimal.
|
|
72
|
+
// (CIM-vs-Get-Process rationale documented on `getProcessStartTime`.)
|
|
73
|
+
const output = execSync(`powershell.exe -NoProfile -NonInteractive -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CreationDate.ToString('o')"`, {
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
stdio: [
|
|
76
|
+
'ignore',
|
|
77
|
+
'pipe',
|
|
78
|
+
'pipe'
|
|
79
|
+
],
|
|
80
|
+
timeout: 5000
|
|
81
|
+
}).trim();
|
|
82
|
+
if (!output) return undefined;
|
|
83
|
+
const date = new Date(output);
|
|
84
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check whether a process is alive **and** is the same process that wrote
|
|
91
|
+
* the manifest/lock (not a PID that was reused by the OS after a crash).
|
|
92
|
+
*
|
|
93
|
+
* Compares the stored `startedAt` timestamp against the OS-reported process
|
|
94
|
+
* start time. Falls back to a plain alive-check when the start time cannot
|
|
95
|
+
* be retrieved (permissions, missing tools, etc.) — in that mode PID-reuse
|
|
96
|
+
* goes undetected, but in practice {@link getProcessStartTime} succeeds on
|
|
97
|
+
* all supported platforms.
|
|
98
|
+
*/ export function isOurProcess(pid, startedAt) {
|
|
99
|
+
if (!isProcessAlive(pid)) return false;
|
|
100
|
+
const osStart = getProcessStartTime(pid);
|
|
101
|
+
if (!osStart) return true // can't verify — assume alive is good enough
|
|
102
|
+
;
|
|
103
|
+
const storedStart = new Date(startedAt);
|
|
104
|
+
if (Number.isNaN(storedStart.getTime())) return true // bad stored value — fall back
|
|
105
|
+
;
|
|
106
|
+
return Math.abs(osStart.getTime() - storedStart.getTime()) <= START_TIME_TOLERANCE_MS;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=processLiveness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/actions/dev/processLiveness.ts"],"sourcesContent":["import {execSync} from 'node:child_process'\n\n/**\n * Check whether a process is still alive.\n * Sends signal 0 which doesn't kill anything — just checks existence.\n */\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n } catch (err: unknown) {\n // EPERM means the process exists but we lack permission to signal it\n if (err && typeof err === 'object' && 'code' in err && err.code === 'EPERM') {\n return true\n }\n return false\n }\n}\n\n/** Tolerance in ms when comparing stored vs OS-reported process start times. */\nconst START_TIME_TOLERANCE_MS = 2000\n\n/**\n * Retrieve the OS-reported start time for a process.\n *\n * - Unix (macOS / Linux): `ps -o lstart=`\n * - Windows: PowerShell `Get-CimInstance Win32_Process` (`CreationDate`).\n * We prefer CIM over `Get-Process -Id <pid>.StartTime` because `StartTime`\n * opens a process handle and can throw \"Access is denied\" when the target\n * is owned by another user, while `Win32_Process.CreationDate` is readable\n * for any process the user can enumerate.\n *\n * On Windows the result for {@link process.pid} is memoised because the\n * start time of the current process never changes and PowerShell cold-start\n * is expensive (1–3s). Without the cache, a single `sanity dev` startup\n * spawns PowerShell 4–5 times for our own PID. Other PIDs are not cached —\n * they may be reused over time.\n *\n * Returns `undefined` if the process doesn't exist or the command fails.\n */\nlet ownWindowsStartTime: {cached: true; date: Date | undefined} | undefined\n\nexport function getProcessStartTime(pid: number): Date | undefined {\n if (process.platform === 'win32') {\n if (pid === process.pid && ownWindowsStartTime) return ownWindowsStartTime.date\n const result = readWindowsStartTime(pid)\n if (pid === process.pid) ownWindowsStartTime = {cached: true, date: result}\n return result\n }\n return readUnixStartTime(pid)\n}\n\n/** Test-only: clear the cached own-process start time. */\nexport function __resetStartTimeCacheForTesting(): void {\n ownWindowsStartTime = undefined\n}\n\nfunction readUnixStartTime(pid: number): Date | undefined {\n try {\n const output = execSync(`ps -o lstart= -p ${pid}`, {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: 1000,\n }).trim()\n if (!output) return undefined\n const date = new Date(output)\n return Number.isNaN(date.getTime()) ? undefined : date\n } catch {\n return undefined\n }\n}\n\nfunction readWindowsStartTime(pid: number): Date | undefined {\n try {\n // `-NoProfile -NonInteractive` keeps PowerShell start-up minimal.\n // (CIM-vs-Get-Process rationale documented on `getProcessStartTime`.)\n const output = execSync(\n `powershell.exe -NoProfile -NonInteractive -Command \"(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CreationDate.ToString('o')\"`,\n {encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000},\n ).trim()\n if (!output) return undefined\n const date = new Date(output)\n return Number.isNaN(date.getTime()) ? undefined : date\n } catch {\n return undefined\n }\n}\n\n/**\n * Check whether a process is alive **and** is the same process that wrote\n * the manifest/lock (not a PID that was reused by the OS after a crash).\n *\n * Compares the stored `startedAt` timestamp against the OS-reported process\n * start time. Falls back to a plain alive-check when the start time cannot\n * be retrieved (permissions, missing tools, etc.) — in that mode PID-reuse\n * goes undetected, but in practice {@link getProcessStartTime} succeeds on\n * all supported platforms.\n */\nexport function isOurProcess(pid: number, startedAt: string): boolean {\n if (!isProcessAlive(pid)) return false\n\n const osStart = getProcessStartTime(pid)\n if (!osStart) return true // can't verify — assume alive is good enough\n\n const storedStart = new Date(startedAt)\n if (Number.isNaN(storedStart.getTime())) return true // bad stored value — fall back\n\n return Math.abs(osStart.getTime() - storedStart.getTime()) <= START_TIME_TOLERANCE_MS\n}\n"],"names":["execSync","isProcessAlive","pid","process","kill","err","code","START_TIME_TOLERANCE_MS","ownWindowsStartTime","getProcessStartTime","platform","date","result","readWindowsStartTime","cached","readUnixStartTime","__resetStartTimeCacheForTesting","undefined","output","encoding","stdio","timeout","trim","Date","Number","isNaN","getTime","isOurProcess","startedAt","osStart","storedStart","Math","abs"],"mappings":"AAAA,SAAQA,QAAQ,QAAO,qBAAoB;AAE3C;;;CAGC,GACD,SAASC,eAAeC,GAAW;IACjC,IAAI;QACFC,QAAQC,IAAI,CAACF,KAAK;QAClB,OAAO;IACT,EAAE,OAAOG,KAAc;QACrB,qEAAqE;QACrE,IAAIA,OAAO,OAAOA,QAAQ,YAAY,UAAUA,OAAOA,IAAIC,IAAI,KAAK,SAAS;YAC3E,OAAO;QACT;QACA,OAAO;IACT;AACF;AAEA,8EAA8E,GAC9E,MAAMC,0BAA0B;AAEhC;;;;;;;;;;;;;;;;;CAiBC,GACD,IAAIC;AAEJ,OAAO,SAASC,oBAAoBP,GAAW;IAC7C,IAAIC,QAAQO,QAAQ,KAAK,SAAS;QAChC,IAAIR,QAAQC,QAAQD,GAAG,IAAIM,qBAAqB,OAAOA,oBAAoBG,IAAI;QAC/E,MAAMC,SAASC,qBAAqBX;QACpC,IAAIA,QAAQC,QAAQD,GAAG,EAAEM,sBAAsB;YAACM,QAAQ;YAAMH,MAAMC;QAAM;QAC1E,OAAOA;IACT;IACA,OAAOG,kBAAkBb;AAC3B;AAEA,wDAAwD,GACxD,OAAO,SAASc;IACdR,sBAAsBS;AACxB;AAEA,SAASF,kBAAkBb,GAAW;IACpC,IAAI;QACF,MAAMgB,SAASlB,SAAS,CAAC,iBAAiB,EAAEE,KAAK,EAAE;YACjDiB,UAAU;YACVC,OAAO;gBAAC;gBAAU;gBAAQ;aAAO;YACjCC,SAAS;QACX,GAAGC,IAAI;QACP,IAAI,CAACJ,QAAQ,OAAOD;QACpB,MAAMN,OAAO,IAAIY,KAAKL;QACtB,OAAOM,OAAOC,KAAK,CAACd,KAAKe,OAAO,MAAMT,YAAYN;IACpD,EAAE,OAAM;QACN,OAAOM;IACT;AACF;AAEA,SAASJ,qBAAqBX,GAAW;IACvC,IAAI;QACF,kEAAkE;QAClE,sEAAsE;QACtE,MAAMgB,SAASlB,SACb,CAAC,sGAAsG,EAAEE,IAAI,8BAA8B,CAAC,EAC5I;YAACiB,UAAU;YAAQC,OAAO;gBAAC;gBAAU;gBAAQ;aAAO;YAAEC,SAAS;QAAI,GACnEC,IAAI;QACN,IAAI,CAACJ,QAAQ,OAAOD;QACpB,MAAMN,OAAO,IAAIY,KAAKL;QACtB,OAAOM,OAAOC,KAAK,CAACd,KAAKe,OAAO,MAAMT,YAAYN;IACpD,EAAE,OAAM;QACN,OAAOM;IACT;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASU,aAAazB,GAAW,EAAE0B,SAAiB;IACzD,IAAI,CAAC3B,eAAeC,MAAM,OAAO;IAEjC,MAAM2B,UAAUpB,oBAAoBP;IACpC,IAAI,CAAC2B,SAAS,OAAO,KAAK,6CAA6C;;IAEvE,MAAMC,cAAc,IAAIP,KAAKK;IAC7B,IAAIJ,OAAOC,KAAK,CAACK,YAAYJ,OAAO,KAAK,OAAO,KAAK,+BAA+B;;IAEpF,OAAOK,KAAKC,GAAG,CAACH,QAAQH,OAAO,KAAKI,YAAYJ,OAAO,OAAOnB;AAChE"}
|