@sanity/runtime-cli 17.13.1-next.g13a83b0 → 17.13.1-next.g4fa31d1

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.
@@ -0,0 +1,38 @@
1
+ import { type BuildStudioEventListener } from '@sanity/cli-build/_internal/build';
2
+ import type { Output } from '@sanity/cli-core';
3
+ import type { AssetPrepResult } from '../types.js';
4
+ import type { BlueprintApplicationResource, BlueprintInstallationConfigResource, BlueprintStudioResource } from './types.js';
5
+ export type BundleResource = BlueprintStudioResource | BlueprintApplicationResource | BlueprintInstallationConfigResource;
6
+ type BuildContext<R extends BundleResource> = {
7
+ resource: R;
8
+ appId: string | undefined;
9
+ workDir: string;
10
+ outDir: string;
11
+ output: Output;
12
+ eventListener: Partial<BuildStudioEventListener>;
13
+ };
14
+ type BuildResult = {
15
+ mismatchedPackages?: string[];
16
+ };
17
+ export declare const BUNDLE_SPECS: {
18
+ 'sanity.studio': {
19
+ requiresAppId: true;
20
+ build: typeof buildStudioBundle;
21
+ };
22
+ 'sanity.application': {
23
+ requiresAppId: true;
24
+ build: typeof buildApplicationBundle;
25
+ };
26
+ 'sanity.installation.config': {
27
+ requiresAppId: false;
28
+ build: typeof buildInstallationConfigBundle;
29
+ };
30
+ };
31
+ export declare function prepareBundleAsset<R extends BundleResource>({ blueprintFileDir, resource, }: {
32
+ blueprintFileDir: string;
33
+ resource: R;
34
+ }): Promise<AssetPrepResult>;
35
+ declare function buildStudioBundle({ resource, appId, workDir, outDir, output, eventListener, }: BuildContext<BlueprintStudioResource>): Promise<BuildResult>;
36
+ declare function buildApplicationBundle({ resource, appId, workDir, outDir, output, eventListener, }: BuildContext<BlueprintApplicationResource>): Promise<BuildResult>;
37
+ declare function buildInstallationConfigBundle({ resource, workDir, outDir, output, eventListener, }: BuildContext<BlueprintInstallationConfigResource>): Promise<BuildResult>;
38
+ export {};
@@ -0,0 +1,271 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path, { basename, dirname, join } from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { createGzip } from 'node:zlib';
7
+ import { buildApp, buildStudio, compareDependencyVersions, } from '@sanity/cli-build/_internal/build';
8
+ import { getLocalPackageVersion } from '@sanity/cli-core';
9
+ import { pack } from 'tar-fs';
10
+ export const BUNDLE_SPECS = {
11
+ 'sanity.studio': { requiresAppId: true, build: buildStudioBundle },
12
+ 'sanity.application': { requiresAppId: true, build: buildApplicationBundle },
13
+ 'sanity.installation.config': { requiresAppId: false, build: buildInstallationConfigBundle },
14
+ };
15
+ export async function prepareBundleAsset({ blueprintFileDir, resource, }) {
16
+ // The lookup can't prove it yields the spec matching this R, so it's asserted once here.
17
+ const spec = BUNDLE_SPECS[resource.type];
18
+ const appId = getAppId(resource);
19
+ if (spec.requiresAppId && !appId) {
20
+ return {
21
+ success: false,
22
+ async cleanup() { },
23
+ error: `Unable to determine appId for ${resource.type} resource '${resource.name}'`,
24
+ };
25
+ }
26
+ let cleanup = async () => { };
27
+ const assetPath = String(resource.root);
28
+ const workDir = path.resolve(blueprintFileDir, assetPath);
29
+ // Defence in depth alongside blueprints-node's own check: an absolute path or one
30
+ // that climbs above the blueprint dir would bundle files outside the project.
31
+ if (path.isAbsolute(assetPath) || path.relative(blueprintFileDir, workDir).startsWith('..')) {
32
+ return {
33
+ success: false,
34
+ cleanup,
35
+ error: `Property 'root' on resource with type '${resource.type}' must be under the blueprint directory`,
36
+ };
37
+ }
38
+ try {
39
+ const workDirStat = await stat(workDir);
40
+ if (!workDirStat.isDirectory()) {
41
+ return {
42
+ success: false,
43
+ cleanup,
44
+ error: `Property 'root' on resource with type '${resource.type}' must be a directory`,
45
+ };
46
+ }
47
+ }
48
+ catch (error) {
49
+ const msg = error instanceof Error ? error.message : String(error);
50
+ return {
51
+ success: false,
52
+ cleanup,
53
+ error: `Unable to read given 'root' on resource with type '${resource.type}': ${msg}`,
54
+ };
55
+ }
56
+ try {
57
+ const outDir = await mkdtemp(join(tmpdir(), `bundle-`));
58
+ const outFile = `${outDir}.tar.gz`;
59
+ cleanup = async () => {
60
+ await rm(outDir, { force: true, recursive: true });
61
+ await rm(outFile, { force: true });
62
+ };
63
+ let mismatchedPackages;
64
+ try {
65
+ ;
66
+ ({ mismatchedPackages } = await spec.build({
67
+ resource,
68
+ appId,
69
+ workDir,
70
+ outDir,
71
+ output: createSilentOutput(),
72
+ eventListener,
73
+ }));
74
+ }
75
+ catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ return {
78
+ success: false,
79
+ cleanup,
80
+ error: message,
81
+ };
82
+ }
83
+ await pipeline(pack(dirname(outDir), { entries: [basename(outDir)] }), createGzip(), createWriteStream(outFile));
84
+ const buffer = await readFile(outFile);
85
+ return {
86
+ success: true,
87
+ cleanup,
88
+ outputBuffer: buffer,
89
+ fileExtension: 'tar.gz',
90
+ assetParameter: 'root',
91
+ metadata: {
92
+ sanityVersion: await getInstalledSanityVersion(workDir),
93
+ mismatchedPackages,
94
+ assetBytes: buffer.length,
95
+ },
96
+ };
97
+ }
98
+ catch (error) {
99
+ return {
100
+ success: false,
101
+ cleanup,
102
+ error: error instanceof Error ? error.message : String(error),
103
+ };
104
+ }
105
+ }
106
+ async function buildStudioBundle({ resource, appId, workDir, outDir, output, eventListener, }) {
107
+ let mismatchedPackages;
108
+ await buildStudio({
109
+ appId,
110
+ autoUpdatesEnabled: resource.autoUpdates.enabled,
111
+ checkAppId() { },
112
+ async compareDependencyVersions(packages) {
113
+ const result = await compareDependencyVersions(packages, workDir, { appId });
114
+ mismatchedPackages = result.mismatched.map((m) => m.pkg);
115
+ return result;
116
+ },
117
+ determineBasePath() {
118
+ return resource.basePath ?? '/';
119
+ },
120
+ // a studio is a standalone app, not a workbench module-federation app
121
+ isApp: false,
122
+ isBlueprints: true,
123
+ isWorkbenchApp: false,
124
+ minify: resource.minify ?? true,
125
+ outDir,
126
+ output,
127
+ reactCompiler: resource.reactCompiler || false,
128
+ schemaExtraction: undefined,
129
+ sourceMap: resource.sourceMap ?? false,
130
+ stats: false,
131
+ unattendedMode: true,
132
+ // TODO pass in vite config from the studio resource
133
+ vite: undefined,
134
+ workDir,
135
+ eventListener,
136
+ });
137
+ return { mismatchedPackages };
138
+ }
139
+ async function buildApplicationBundle({ resource, appId, workDir, outDir, output, eventListener, }) {
140
+ const views = resource.views ?? [];
141
+ const [window, extraWindow] = views.filter((v) => v.type === 'app');
142
+ if (extraWindow) {
143
+ throw new Error(`Application resource '${resource.name}' declares more than one window view; at most one is allowed`);
144
+ }
145
+ await buildApp({
146
+ ...sharedBuildAppOptions({ appId, workDir, outDir, output, eventListener }),
147
+ appId,
148
+ appTitle: resource.title,
149
+ entry: window?.src,
150
+ workbenchAppId: appId,
151
+ exposes: {
152
+ views: views.map(toWorkbenchView),
153
+ webWorkers: resource.webWorkers,
154
+ },
155
+ });
156
+ // a workbench app never auto-updates, so no dependency mismatches are compared
157
+ return {};
158
+ }
159
+ async function buildInstallationConfigBundle({ resource, workDir, outDir, output, eventListener, }) {
160
+ await buildApp({
161
+ ...sharedBuildAppOptions({ appId: undefined, workDir, outDir, output, eventListener }),
162
+ appId: undefined,
163
+ appTitle: undefined,
164
+ entry: undefined,
165
+ workbenchAppId: undefined,
166
+ exposes: { config: { appType: resource.appType, fields: resource.fields } },
167
+ });
168
+ return {};
169
+ }
170
+ // Options a workbench build (application or installation config) always sets the
171
+ // same way; each caller adds only what differs (identity, entry, exposes).
172
+ function sharedBuildAppOptions({ appId, workDir, outDir, output, eventListener, }) {
173
+ return {
174
+ autoUpdatesEnabled: false,
175
+ checkAppId() { },
176
+ async compareDependencyVersions(packages) {
177
+ return compareDependencyVersions(packages, workDir, { appId });
178
+ },
179
+ determineBasePath() {
180
+ return '/';
181
+ },
182
+ isWorkbenchApp: true,
183
+ isBlueprints: true,
184
+ minify: true,
185
+ outDir,
186
+ output,
187
+ reactCompiler: false,
188
+ schemaExtraction: undefined,
189
+ sourceMap: false,
190
+ stats: false,
191
+ unattendedMode: true,
192
+ vite: undefined,
193
+ workDir,
194
+ eventListener,
195
+ };
196
+ }
197
+ // blueprints-node emits `type` (window views as `app`); workbench-cli discriminates on `surface`.
198
+ function toWorkbenchView(view) {
199
+ switch (view.type) {
200
+ case 'app': {
201
+ const { type: _type, ...rest } = view;
202
+ return { ...rest, surface: 'window' };
203
+ }
204
+ case 'panel': {
205
+ const { type, ...rest } = view;
206
+ return { ...rest, surface: type };
207
+ }
208
+ case 'asset_source': {
209
+ const { type, ...rest } = view;
210
+ return { ...rest, surface: type };
211
+ }
212
+ case 'tile': {
213
+ const { type, ...rest } = view;
214
+ return { ...rest, surface: type };
215
+ }
216
+ }
217
+ }
218
+ const eventListener = {
219
+ onBuildFail({ message }) {
220
+ throw new Error(message);
221
+ },
222
+ onInvalidStyledComponentsVersionRange({ message }) {
223
+ throw new Error(message);
224
+ },
225
+ onNoDeclaredStyledComponentsVersion({ message }) {
226
+ throw new Error(message);
227
+ },
228
+ onNoInstalledSanityVersion({ message }) {
229
+ throw new Error(message);
230
+ },
231
+ onNoInstalledStyledComponentsVersion({ message }) {
232
+ throw new Error(message);
233
+ },
234
+ onPreReleaseInNonInteractiveAutoUpdate({ message }) {
235
+ throw new Error(message);
236
+ },
237
+ };
238
+ function createSilentOutput() {
239
+ return {
240
+ error(input, options) {
241
+ if (typeof options?.exit === 'number') {
242
+ if (input instanceof Error) {
243
+ throw input;
244
+ }
245
+ throw new Error(input);
246
+ }
247
+ return undefined;
248
+ },
249
+ log() { },
250
+ warn(input) {
251
+ return input;
252
+ },
253
+ };
254
+ }
255
+ async function getInstalledSanityVersion(workDir) {
256
+ try {
257
+ return (await getLocalPackageVersion('sanity', workDir)) ?? undefined;
258
+ }
259
+ catch {
260
+ return undefined;
261
+ }
262
+ }
263
+ function getAppId(resource) {
264
+ if ('externalId' in resource && typeof resource.externalId === 'string' && resource.externalId) {
265
+ return resource.externalId;
266
+ }
267
+ else if (resource.lifecycle?.ownershipAction?.type === 'attach') {
268
+ return resource.lifecycle.ownershipAction.id;
269
+ }
270
+ return undefined;
271
+ }
@@ -0,0 +1,61 @@
1
+ import type { BlueprintProjectResourceLifecycle, BlueprintResource, BlueprintStudioResource as PublishedStudioResource } from '@sanity/blueprints';
2
+ /** Studios point at their directory with `root`, same as applications. */
3
+ export type BlueprintStudioResource = Omit<PublishedStudioResource, 'src'> & {
4
+ root: string;
5
+ };
6
+ type ApplicationDock = {
7
+ group?: 'system' | 'applications' | 'user';
8
+ order?: number;
9
+ };
10
+ type ChildBase = {
11
+ name: string;
12
+ title: string;
13
+ src: string;
14
+ };
15
+ type WindowView = ChildBase & {
16
+ type: 'app';
17
+ dock?: ApplicationDock;
18
+ };
19
+ type PanelView = ChildBase & {
20
+ type: 'panel';
21
+ dock?: ApplicationDock;
22
+ };
23
+ type AssetSourceView = ChildBase & {
24
+ type: 'asset_source';
25
+ };
26
+ type TileView = ChildBase & {
27
+ type: 'tile';
28
+ size: 'small' | 'large' | 'banner';
29
+ order?: number;
30
+ };
31
+ type WebWorker = ChildBase & {
32
+ type: 'worker';
33
+ };
34
+ export type ApplicationView = WindowView | PanelView | AssetSourceView | TileView;
35
+ export interface BlueprintApplicationResource extends BlueprintResource<BlueprintProjectResourceLifecycle> {
36
+ type: 'sanity.application';
37
+ slug: string;
38
+ title: string;
39
+ /** Application directory, relative to the blueprint file; contains package.json. */
40
+ root: string;
41
+ icon?: string;
42
+ visibility?: 'default' | 'unlisted' | 'disabled';
43
+ views?: ApplicationView[];
44
+ webWorkers?: WebWorker[];
45
+ }
46
+ type InstallationConfigField = {
47
+ name: string;
48
+ title: string;
49
+ src: string;
50
+ public?: boolean;
51
+ };
52
+ export interface BlueprintInstallationConfigResource extends BlueprintResource {
53
+ type: 'sanity.installation.config';
54
+ appType: 'media-library';
55
+ /** Build directory, relative to the blueprint file; contains package.json. Field `src` resolves against it. */
56
+ root: string;
57
+ fields: InstallationConfigField[];
58
+ /** Stamped by the CLI before pre-deploy; the authored shape from blueprints-node has none. */
59
+ version?: string;
60
+ }
61
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -4,7 +4,11 @@ import type { DeploymentPlan } from '../../actions/blueprints/stacks.js';
4
4
  import { type BlueprintResourceRecord, type Stack, type StackSummary } from '../types.js';
5
5
  export declare function formatTitle(title: string, name: string): string;
6
6
  export declare function formatDeployedResourceTree(resources: BlueprintResourceRecord[] | undefined, verbose?: boolean): string;
7
- export declare function formatStudioUrls(resources: BlueprintResourceRecord[] | undefined): string[];
7
+ /**
8
+ * Returns hosted Studio URLs, one line per Studio from resource-ish shapes
9
+ * Searches `parameters`, `resolvedParameters` and root for `studioHost`
10
+ */
11
+ export declare function formatStudioUrls(resources: (BlueprintResource | BlueprintResourceRecord)[] | undefined): string[];
8
12
  export declare function formatResourceTree(resources: BlueprintResource[] | undefined, verbose?: boolean): string;
9
13
  export declare function formatStackInfo(stack: Stack | StackSummary | Blueprint, isCurrentStack?: boolean): string;
10
14
  export declare function formatStacksListing(stacks: StackSummary[], currentStackId?: string): string;
@@ -1,6 +1,6 @@
1
1
  import { formatDateTime, formatElapsed } from '@sanity/cli-core/dates';
2
2
  import { treeify } from 'array-treeify';
3
- import { SANITY_ACCESS_ROBOT, SANITY_ACCESS_ROLE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_DURABLE, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_PUBSUB, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, SANITY_PROJECT, SANITY_PROJECT_CORS, SANITY_PROJECT_DATASET, SANITY_PROJECT_WEBHOOK, SANITY_STUDIO, } from '../../constants.js';
3
+ import { SANITY_ACCESS_ROBOT, SANITY_ACCESS_ROLE, SANITY_APPLICATION, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_DURABLE, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_PUBSUB, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, SANITY_INSTALLATION_CONFIG, SANITY_PROJECT, SANITY_PROJECT_CORS, SANITY_PROJECT_DATASET, SANITY_PROJECT_WEBHOOK, SANITY_STUDIO, } from '../../constants.js';
4
4
  import { styleText } from '../style-text.js';
5
5
  import { isCorsOriginResource, isDatasetResource, isRobotResource, isRoleResource, isStudioResource, isWebhookResource, } from '../types.js';
6
6
  import { capitalize, niceId, renderSection } from './presenters.js';
@@ -65,6 +65,14 @@ const RESOURCE_CATEGORIES = {
65
65
  return isStudioResource(res) ? arrayifyStudio(res) : [];
66
66
  },
67
67
  },
68
+ [SANITY_APPLICATION]: {
69
+ label: 'Applications',
70
+ displayNameAttribute: 'title',
71
+ },
72
+ [SANITY_INSTALLATION_CONFIG]: {
73
+ label: 'Installation configs',
74
+ displayNameAttribute: 'name',
75
+ },
68
76
  };
69
77
  const categoryByLabel = Object.values(RESOURCE_CATEGORIES).reduce((acc, curr) => {
70
78
  acc[curr.label] = curr;
@@ -165,6 +173,10 @@ export function formatDeployedResourceTree(resources, verbose = false) {
165
173
  (res) => ({ name: res.name, type: res.type, ...(res.resolvedParameters ?? res.parameters) }), verbose);
166
174
  return `${treeify(output)}\n`;
167
175
  }
176
+ /**
177
+ * Returns hosted Studio URLs, one line per Studio from resource-ish shapes
178
+ * Searches `parameters`, `resolvedParameters` and root for `studioHost`
179
+ */
168
180
  export function formatStudioUrls(resources) {
169
181
  if (!resources)
170
182
  return [];
@@ -172,7 +184,8 @@ export function formatStudioUrls(resources) {
172
184
  for (const res of resources) {
173
185
  if (res.type !== SANITY_STUDIO)
174
186
  continue;
175
- const host = studioHost(res.resolvedParameters ?? res.parameters);
187
+ const { resolvedParameters, parameters } = res;
188
+ const host = studioHost(resolvedParameters ?? parameters ?? res);
176
189
  if (host)
177
190
  lines.push(`Studio ${styleText(['bold', 'green'], res.name)} ${studioUrl(host)}`);
178
191
  }
@@ -0,0 +1,4 @@
1
+ export declare const REQUEST_ID_HEADER = "x-request-id";
2
+ /** Stack PUT/POST/DELETE success bodies also carry `requestId`; error bodies do not. */
3
+ export declare function getRequestId(response?: Response, body?: unknown): string | undefined;
4
+ export declare function withRequestId(message: string, requestId?: string): string;
@@ -0,0 +1,19 @@
1
+ export const REQUEST_ID_HEADER = 'x-request-id';
2
+ /** Stack PUT/POST/DELETE success bodies also carry `requestId`; error bodies do not. */
3
+ export function getRequestId(response, body) {
4
+ const fromHeader = response?.headers.get(REQUEST_ID_HEADER);
5
+ if (fromHeader)
6
+ return fromHeader;
7
+ if (body && typeof body === 'object' && 'requestId' in body) {
8
+ const fromBody = body.requestId;
9
+ if (typeof fromBody === 'string' && fromBody)
10
+ return fromBody;
11
+ }
12
+ return undefined;
13
+ }
14
+ export function withRequestId(message, requestId) {
15
+ if (!requestId)
16
+ return message;
17
+ const suffix = `(request ID ${requestId})`;
18
+ return message.endsWith('.') ? `${message.slice(0, -1)} ${suffix}.` : `${message} ${suffix}`;
19
+ }
@@ -65,6 +65,15 @@ interface StackBase {
65
65
  export interface Stack extends StackBase {
66
66
  resources: Array<BlueprintResourceRecord>;
67
67
  }
68
+ /**
69
+ * Shape returned by `PUT /stacks/{id}`
70
+ * The authored document is echoed by the API: resources without `parameters` or `resolvedParameters`.
71
+ */
72
+ export interface UpdatedStack extends StackBase {
73
+ resources: BlueprintResource[];
74
+ operationId: string;
75
+ changeCount?: number;
76
+ }
68
77
  export interface StackSummary extends StackBase {
69
78
  resourceCount: number;
70
79
  }