@sanity/runtime-cli 17.13.1 → 17.14.0-next.g792820f

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 { BlueprintApplicationResource, BlueprintInstallationConfigResource, BlueprintStudioResource } from '@sanity/blueprints';
2
+ import { type BuildStudioEventListener } from '@sanity/cli-build/_internal/build';
3
+ import type { Output } from '@sanity/cli-core';
4
+ import type { AssetPrepResult } 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,5 @@
1
+ import type { BlueprintResource } from '@sanity/blueprints';
2
+ /**
3
+ * Stamps the installed `sanity` version onto each media-library config resource and studio resource.
4
+ */
5
+ export declare const stampConfigVersion: (resource: BlueprintResource, blueprintFileDir: string) => Promise<void>;
@@ -0,0 +1,45 @@
1
+ import { resolve } from 'node:path';
2
+ import { getLocalPackageVersion } from '@sanity/cli-core/package-manager';
3
+ import { SANITY_INSTALLATION_CONFIG, SANITY_STUDIO } from '../../constants.js';
4
+ /**
5
+ * Stamps the installed `sanity` version onto each media-library config resource and studio resource.
6
+ */
7
+ export const stampConfigVersion = async (resource, blueprintFileDir) => {
8
+ let update;
9
+ let root;
10
+ if (resource.type === SANITY_INSTALLATION_CONFIG) {
11
+ const config = resource;
12
+ if (config.appType !== 'media-library')
13
+ return;
14
+ // Leave values included in the blueprint.
15
+ if (config.version !== undefined)
16
+ return;
17
+ root = config.root;
18
+ update = (version) => {
19
+ config.version = version;
20
+ };
21
+ }
22
+ else if (resource.type === SANITY_STUDIO) {
23
+ const config = resource;
24
+ // Leave values included in the blueprint.
25
+ if (config.autoUpdates?.version !== undefined)
26
+ return;
27
+ root = config.root;
28
+ update = (version) => {
29
+ if (config.autoUpdates) {
30
+ config.autoUpdates.version = version;
31
+ }
32
+ else {
33
+ config.autoUpdates = { enabled: true, version };
34
+ }
35
+ };
36
+ }
37
+ if (update && root) {
38
+ const workDir = resolve(blueprintFileDir, root);
39
+ const version = await getLocalPackageVersion('sanity', workDir);
40
+ if (!version) {
41
+ throw new Error(`Unable to determine the installed 'sanity' version for resource '${resource.name}'; is 'sanity' installed in '${root}'?`);
42
+ }
43
+ update(version);
44
+ }
45
+ };
@@ -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
  }
@@ -2,7 +2,7 @@ import { type BlueprintResource } from '@sanity/blueprints';
2
2
  /**
3
3
  * Extracts the blueprint config from the function source code and mutates it to the resource object.
4
4
  * This is used for durable functions that may not have a config defined in the blueprint.
5
- * @param resources
5
+ * @param resource
6
6
  * @param blueprintFileDir
7
7
  */
8
- export declare const extractDurableConfig: (resources: BlueprintResource[], blueprintFileDir: string) => Promise<void>;
8
+ export declare const extractDurableConfig: (resource: BlueprintResource, blueprintFileDir: string) => Promise<void>;
@@ -0,0 +1,58 @@
1
+ import { resolve } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { validateDurableFunction, } from '@sanity/blueprints';
4
+ import { SANITY_FUNCTION_DURABLE } from '../../constants.js';
5
+ import { isLocalFunctionResource } from '../types.js';
6
+ import { findFunctionEntryPoint } from './find-entry-point.js';
7
+ import { transpileFunction } from './transpile/function.js';
8
+ const DURABLE_CONFIG_KEYS = ['event', 'concurrency', 'debounce', 'debounceKey'];
9
+ /**
10
+ * Extracts the blueprint config from the function source code and mutates it to the resource object.
11
+ * This is used for durable functions that may not have a config defined in the blueprint.
12
+ * @param resource
13
+ * @param blueprintFileDir
14
+ */
15
+ export const extractDurableConfig = async (resource, blueprintFileDir) => {
16
+ if (!isLocalFunctionResource(resource))
17
+ return;
18
+ // skip if not a durable function
19
+ if (resource.type !== SANITY_FUNCTION_DURABLE)
20
+ return;
21
+ // Check if source needs to be transpiled before attempting to get config
22
+ const resourceSrc = resolve(blueprintFileDir, resource.src);
23
+ const entryPoint = await findFunctionEntryPoint(resourceSrc, resource.name);
24
+ let importPath = entryPoint;
25
+ let cleanup = async () => { };
26
+ // need to transpile and grab the config from the built output if the entry point is a TypeScript file
27
+ if (entryPoint.endsWith('.ts')) {
28
+ const built = await transpileFunction({ resource, functionPath: resourceSrc, entryPoint });
29
+ importPath = await findFunctionEntryPoint(built.outputDir, resource.name);
30
+ cleanup = built.cleanup;
31
+ }
32
+ let config = {};
33
+ try {
34
+ const mod = await import(pathToFileURL(importPath).href);
35
+ config = (mod.handler || mod.default)?.config;
36
+ }
37
+ finally {
38
+ await cleanup();
39
+ }
40
+ // Update values not included in the blueprint.
41
+ if (resource.event === undefined && config?.event !== undefined) {
42
+ resource.event = config.event;
43
+ }
44
+ if (resource.concurrency === undefined && config?.concurrency !== undefined) {
45
+ resource.concurrency = config.concurrency;
46
+ }
47
+ if (resource.debounce === undefined && config?.debounce !== undefined) {
48
+ resource.debounce = config.debounce;
49
+ }
50
+ if (resource.debounceKey === undefined && config?.debounceKey !== undefined) {
51
+ resource.debounceKey = config.debounceKey;
52
+ }
53
+ // Validate the resource after merging the config values
54
+ const errors = validateDurableFunction(resource);
55
+ if (errors.length > 0) {
56
+ throw new Error(`Invalid durable resource "${resource.name}": ${errors.map((error) => error.message).join('; ')}`);
57
+ }
58
+ };
@@ -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
  }
@@ -329,11 +338,17 @@ export type PrepAssetFn<R extends BlueprintResource> = (options: {
329
338
  blueprintFileDir: string;
330
339
  installer?: InstallerType;
331
340
  }) => Promise<AssetPrepResult>;
341
+ export type PrepResourceFn<R extends BlueprintResource> = (options: {
342
+ resource: R;
343
+ blueprintFileDir: string;
344
+ }) => Promise<void>;
332
345
  export type PreDeployConfig = {
333
346
  /** The resource type this config applies to or a function that returns true if this config applies to the given type. */
334
347
  resourceType: string | ((type: string) => boolean);
335
348
  /** Function that takes a resource and returns a buffer to be sent to blueprints. */
336
349
  prepareAsset?: PrepAssetFn<BlueprintResource>;
350
+ /** Function that mutates the resource before the pre-deploy is executed. */
351
+ prepareResource?: PrepResourceFn<BlueprintResource>;
337
352
  };
338
353
  export type AssetResource<R extends BlueprintResource> = {
339
354
  resource: R;
@@ -3148,5 +3148,5 @@
3148
3148
  ]
3149
3149
  }
3150
3150
  },
3151
- "version": "17.13.1"
3151
+ "version": "17.14.0-next.g792820f"
3152
3152
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanity/runtime-cli",
3
3
  "description": "Sanity's Runtime CLI for Blueprints and Functions",
4
- "version": "17.13.1",
4
+ "version": "17.14.0-next.g792820f",
5
5
  "author": "Sanity Runtime Team",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -68,7 +68,7 @@
68
68
  "@oclif/core": "^4.14.0",
69
69
  "@oclif/plugin-help": "^6.3.0",
70
70
  "@sanity-labs/oclif-plugin-skills-flag": "^0.2.1",
71
- "@sanity/blueprints": "^0.24.0",
71
+ "@sanity/blueprints": "^0.27.0",
72
72
  "@sanity/blueprints-parser": "^0.5.0",
73
73
  "@sanity/cli-build": "^6.3.0",
74
74
  "@sanity/cli-core": "^3.7.0",
@@ -1,60 +0,0 @@
1
- import { resolve } from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
- import { validateDurableFunction, } from '@sanity/blueprints';
4
- import { SANITY_FUNCTION_DURABLE } from '../../constants.js';
5
- import { findFunctionEntryPoint } from '../functions/find-entry-point.js';
6
- import { transpileFunction } from '../functions/transpile/function.js';
7
- import { isLocalFunctionResource } from '../types.js';
8
- const DURABLE_CONFIG_KEYS = ['event', 'concurrency', 'debounce', 'debounceKey'];
9
- /**
10
- * Extracts the blueprint config from the function source code and mutates it to the resource object.
11
- * This is used for durable functions that may not have a config defined in the blueprint.
12
- * @param resources
13
- * @param blueprintFileDir
14
- */
15
- export const extractDurableConfig = async (resources, blueprintFileDir) => {
16
- for (const resource of resources) {
17
- if (!isLocalFunctionResource(resource))
18
- continue;
19
- // skip if not a durable function
20
- if (resource.type !== SANITY_FUNCTION_DURABLE)
21
- continue;
22
- // Check if source needs to be transpiled before attempting to get config
23
- const resourceSrc = resolve(blueprintFileDir, resource.src);
24
- const entryPoint = await findFunctionEntryPoint(resourceSrc, resource.name);
25
- let importPath = entryPoint;
26
- let cleanup = async () => { };
27
- // need to transpile and grab the config from the built output if the entry point is a TypeScript file
28
- if (entryPoint.endsWith('.ts')) {
29
- const built = await transpileFunction({ resource, functionPath: resourceSrc, entryPoint });
30
- importPath = await findFunctionEntryPoint(built.outputDir, resource.name);
31
- cleanup = built.cleanup;
32
- }
33
- let config = {};
34
- try {
35
- const mod = await import(pathToFileURL(importPath).href);
36
- config = (mod.handler || mod.default)?.config;
37
- }
38
- finally {
39
- await cleanup();
40
- }
41
- // Update values not included in the blueprint.
42
- if (resource.event === undefined && config?.event !== undefined) {
43
- resource.event = config.event;
44
- }
45
- if (resource.concurrency === undefined && config?.concurrency !== undefined) {
46
- resource.concurrency = config.concurrency;
47
- }
48
- if (resource.debounce === undefined && config?.debounce !== undefined) {
49
- resource.debounce = config.debounce;
50
- }
51
- if (resource.debounceKey === undefined && config?.debounceKey !== undefined) {
52
- resource.debounceKey = config.debounceKey;
53
- }
54
- // Validate the resource after merging the config values
55
- const errors = validateDurableFunction(resource);
56
- if (errors.length > 0) {
57
- throw new Error(`Invalid durable resource "${resource.name}": ${errors.map((error) => error.message).join('; ')}`);
58
- }
59
- }
60
- };
@@ -1,6 +0,0 @@
1
- import type { BlueprintStudioResource } from '@sanity/blueprints';
2
- import type { AssetPrepResult } from '../types.js';
3
- export declare function prepareStudioAsset({ blueprintFileDir, resource, }: {
4
- blueprintFileDir: string;
5
- resource: BlueprintStudioResource;
6
- }): Promise<AssetPrepResult>;