@sanity/runtime-cli 17.16.0-next.g26925c0 → 17.16.0-next.g91e6dd2

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 CHANGED
@@ -90,7 +90,7 @@ EXAMPLES
90
90
  $ sanity-run blueprints add function --name my-function --fn-type document-create --fn-type document-update --lang js
91
91
  ```
92
92
 
93
- _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/add.ts)_
93
+ _See code: [src/commands/blueprints/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/add.ts)_
94
94
 
95
95
  ## `sanity-run blueprints config`
96
96
 
@@ -127,7 +127,7 @@ EXAMPLES
127
127
  $ sanity-run blueprints config --edit --project-id <projectId> --stack <name-or-id>
128
128
  ```
129
129
 
130
- _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/config.ts)_
130
+ _See code: [src/commands/blueprints/config.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/config.ts)_
131
131
 
132
132
  ## `sanity-run blueprints deploy`
133
133
 
@@ -183,7 +183,7 @@ EXAMPLES
183
183
  $ sanity-run blueprints deploy --new-stack-name <new-name>
184
184
  ```
185
185
 
186
- _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/deploy.ts)_
186
+ _See code: [src/commands/blueprints/deploy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/deploy.ts)_
187
187
 
188
188
  ## `sanity-run blueprints destroy`
189
189
 
@@ -221,7 +221,7 @@ EXAMPLES
221
221
  $ sanity-run blueprints destroy --stack <name-or-id> --project-id <projectId> --force --no-wait
222
222
  ```
223
223
 
224
- _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/destroy.ts)_
224
+ _See code: [src/commands/blueprints/destroy.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/destroy.ts)_
225
225
 
226
226
  ## `sanity-run blueprints doctor`
227
227
 
@@ -253,7 +253,7 @@ EXAMPLES
253
253
  $ sanity-run blueprints doctor --fix
254
254
  ```
255
255
 
256
- _See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/doctor.ts)_
256
+ _See code: [src/commands/blueprints/doctor.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/doctor.ts)_
257
257
 
258
258
  ## `sanity-run blueprints info`
259
259
 
@@ -291,7 +291,7 @@ EXAMPLES
291
291
  $ sanity-run blueprints info --organization-id <orgId> --stack <name-or-id>
292
292
  ```
293
293
 
294
- _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/info.ts)_
294
+ _See code: [src/commands/blueprints/info.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/info.ts)_
295
295
 
296
296
  ## `sanity-run blueprints init [DIR]`
297
297
 
@@ -355,7 +355,7 @@ EXAMPLES
355
355
  $ sanity-run blueprints init old-stack --type <json|js|ts> --project-id <projectId> --stack-id <existingStackId>
356
356
  ```
357
357
 
358
- _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/init.ts)_
358
+ _See code: [src/commands/blueprints/init.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/init.ts)_
359
359
 
360
360
  ## `sanity-run blueprints logs`
361
361
 
@@ -401,7 +401,7 @@ EXAMPLES
401
401
  $ sanity-run blueprints logs --before 2026-05-01T00:00:00Z
402
402
  ```
403
403
 
404
- _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/logs.ts)_
404
+ _See code: [src/commands/blueprints/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/logs.ts)_
405
405
 
406
406
  ## `sanity-run blueprints mint-deploy-token`
407
407
 
@@ -446,7 +446,7 @@ EXAMPLES
446
446
  $ sanity-run blueprints mint-deploy-token --organization-id <orgId>
447
447
  ```
448
448
 
449
- _See code: [src/commands/blueprints/mint-deploy-token.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/mint-deploy-token.ts)_
449
+ _See code: [src/commands/blueprints/mint-deploy-token.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/mint-deploy-token.ts)_
450
450
 
451
451
  ## `sanity-run blueprints plan`
452
452
 
@@ -479,7 +479,7 @@ EXAMPLES
479
479
  $ sanity-run blueprints plan --organization-id <orgId> --stack <name-or-id>
480
480
  ```
481
481
 
482
- _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/plan.ts)_
482
+ _See code: [src/commands/blueprints/plan.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/plan.ts)_
483
483
 
484
484
  ## `sanity-run blueprints promote`
485
485
 
@@ -515,7 +515,7 @@ EXAMPLES
515
515
  $ sanity-run blueprints promote --new-stack-name <new-name>
516
516
  ```
517
517
 
518
- _See code: [src/commands/blueprints/promote.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/promote.ts)_
518
+ _See code: [src/commands/blueprints/promote.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/promote.ts)_
519
519
 
520
520
  ## `sanity-run blueprints stacks`
521
521
 
@@ -556,7 +556,7 @@ EXAMPLES
556
556
  $ sanity-run blueprints stacks --organization-id <organizationId> --include-projects
557
557
  ```
558
558
 
559
- _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/blueprints/stacks.ts)_
559
+ _See code: [src/commands/blueprints/stacks.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/blueprints/stacks.ts)_
560
560
 
561
561
  ## `sanity-run functions add`
562
562
 
@@ -608,7 +608,7 @@ EXAMPLES
608
608
  $ sanity-run functions add --name my-function --type document-create --type document-update --lang js
609
609
  ```
610
610
 
611
- _See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/functions/add.ts)_
611
+ _See code: [src/commands/functions/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/functions/add.ts)_
612
612
 
613
613
  ## `sanity-run functions dev`
614
614
 
@@ -643,7 +643,7 @@ EXAMPLES
643
643
  $ sanity-run functions dev --timeout 60
644
644
  ```
645
645
 
646
- _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/functions/dev.ts)_
646
+ _See code: [src/commands/functions/dev.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/functions/dev.ts)_
647
647
 
648
648
  ## `sanity-run functions env add NAME KEY VALUE`
649
649
 
@@ -676,7 +676,7 @@ EXAMPLES
676
676
  $ sanity-run functions env add --stack <name-or-id> MyFunction API_URL https://api.example.com/
677
677
  ```
678
678
 
679
- _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/functions/env/add.ts)_
679
+ _See code: [src/commands/functions/env/add.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/functions/env/add.ts)_
680
680
 
681
681
  ## `sanity-run functions env list NAME`
682
682
 
@@ -706,7 +706,7 @@ EXAMPLES
706
706
  $ sanity-run functions env list --stack <name-or-id> MyFunction
707
707
  ```
708
708
 
709
- _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/functions/env/list.ts)_
709
+ _See code: [src/commands/functions/env/list.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/functions/env/list.ts)_
710
710
 
711
711
  ## `sanity-run functions env remove NAME KEY`
712
712
 
@@ -738,7 +738,7 @@ EXAMPLES
738
738
  $ sanity-run functions env remove --stack <name-or-id> MyFunction API_URL
739
739
  ```
740
740
 
741
- _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/functions/env/remove.ts)_
741
+ _See code: [src/commands/functions/env/remove.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/functions/env/remove.ts)_
742
742
 
743
743
  ## `sanity-run functions logs [NAME]`
744
744
 
@@ -778,7 +778,7 @@ EXAMPLES
778
778
  $ sanity-run functions logs <name> --delete
779
779
  ```
780
780
 
781
- _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/functions/logs.ts)_
781
+ _See code: [src/commands/functions/logs.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/functions/logs.ts)_
782
782
 
783
783
  ## `sanity-run functions test [NAME]`
784
784
 
@@ -836,7 +836,7 @@ EXAMPLES
836
836
  $ sanity-run functions test <name> --event update --data-before '{ "title": "before" }' --data-after '{ "title": "after" }'
837
837
  ```
838
838
 
839
- _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g26925c0/src/commands/functions/test.ts)_
839
+ _See code: [src/commands/functions/test.ts](https://github.com/sanity-io/runtime-cli/blob/v17.16.0-next.g91e6dd2/src/commands/functions/test.ts)_
840
840
 
841
841
  ## `sanity-run help [COMMAND]`
842
842
 
@@ -1,5 +1,5 @@
1
1
  import type { BlueprintResource } from '@sanity/blueprints';
2
- import type { AuthParams, InstallerType, PrepAssetFn } from '../../utils/types.js';
2
+ import type { AuthParams, InstallerType, PrepAssetFn, ResolvedResource } from '../../utils/types.js';
3
3
  export declare const assetCheckUrl: string;
4
4
  export declare const assetStashUrl: string;
5
5
  export type StashAssetResult = {
@@ -18,8 +18,8 @@ export type StashAssetResult = {
18
18
  success: false;
19
19
  error: string;
20
20
  };
21
- export declare function stashAsset<R extends BlueprintResource>({ resource, prepAsset, auth, blueprintFileDir, fetchFn, installer, }: {
22
- resource: R;
21
+ export declare function stashAsset<R extends BlueprintResource>({ resolvedResource, prepAsset, auth, blueprintFileDir, fetchFn, installer, }: {
22
+ resolvedResource: ResolvedResource<R>;
23
23
  prepAsset: PrepAssetFn<R>;
24
24
  auth: AuthParams;
25
25
  blueprintFileDir: string;
@@ -4,10 +4,10 @@ import getHeaders from '../../utils/get-headers.js';
4
4
  const assetsUrl = blueprintsUrl('assets');
5
5
  export const assetCheckUrl = `${assetsUrl}/check`;
6
6
  export const assetStashUrl = `${assetsUrl}/stash`;
7
- export async function stashAsset({ resource, prepAsset, auth, blueprintFileDir, fetchFn, installer, }) {
7
+ export async function stashAsset({ resolvedResource, prepAsset, auth, blueprintFileDir, fetchFn, installer, }) {
8
8
  let cleanup = async () => { };
9
9
  try {
10
- const prepResult = await prepAsset({ resource, blueprintFileDir, installer });
10
+ const prepResult = await prepAsset({ resolvedResource, blueprintFileDir, installer });
11
11
  if (prepResult.success === false) {
12
12
  await prepResult.cleanup();
13
13
  return {
@@ -26,7 +26,7 @@ export async function stashAsset({ resource, prepAsset, auth, blueprintFileDir,
26
26
  assetParameter: prepResult.assetParameter,
27
27
  metadata: prepResult.metadata,
28
28
  cleanup,
29
- resourceName: resource.name,
29
+ resourceName: resolvedResource.resource.name,
30
30
  auth,
31
31
  fetchFn,
32
32
  });
@@ -18,7 +18,7 @@ const preDeployConfigs = [
18
18
  resourceType: (type) => type in BUNDLE_SPECS,
19
19
  async prepareAsset(options) {
20
20
  return prepareBundleAsset({
21
- resource: options.resource,
21
+ resolvedResource: options.resolvedResource,
22
22
  blueprintFileDir: options.blueprintFileDir,
23
23
  });
24
24
  },
@@ -30,7 +30,7 @@ const preDeployConfigs = [
30
30
  resourceType: (type) => type.startsWith(SANITY_FUNCTION_PREFIX),
31
31
  async prepareAsset(options) {
32
32
  return prepareFunctionAsset({
33
- resource: options.resource,
33
+ resource: options.resolvedResource.resource,
34
34
  blueprintFileDir: options.blueprintFileDir,
35
35
  installer: options.installer,
36
36
  });
@@ -74,7 +74,7 @@ export async function preDeploy(options) {
74
74
  });
75
75
  const assetResources = [];
76
76
  for (const r of resolvedResources) {
77
- const config = getPredeployConfig(r);
77
+ const config = getPredeployConfig(r.resource);
78
78
  if (typeof config !== 'undefined' && typeof config.prepareAsset === 'function') {
79
79
  assetResources.push({ resource: r, prepareAsset: config.prepareAsset });
80
80
  }
@@ -42,13 +42,14 @@ function getAssetConcurrency() {
42
42
  const configured = Number(env.SANITY_ASSET_CONCURRENCY);
43
43
  return Number.isSafeInteger(configured) && configured > 0 ? configured : DEFAULT_ASSET_CONCURRENCY;
44
44
  }
45
- async function processAsset(resource, prepAsset, options) {
45
+ async function processAsset(resolvedResource, prepAsset, options) {
46
+ const { resource } = resolvedResource;
46
47
  const { auth, blueprintFileDir, installer, fetchFn } = options;
47
48
  let assetTimeoutTimer;
48
49
  let result;
49
50
  try {
50
51
  result = await Promise.race([
51
- stashAsset({ resource, prepAsset, blueprintFileDir, auth, fetchFn, installer }),
52
+ stashAsset({ resolvedResource, prepAsset, blueprintFileDir, auth, fetchFn, installer }),
52
53
  new Promise((_, reject) => {
53
54
  assetTimeoutTimer = setTimeout(() => {
54
55
  reject(new Error(`Processing ${resource.name} timed out after ${assetTimeoutS}s`));
@@ -55,7 +55,7 @@ export async function functionTestCore(options) {
55
55
  apiHost: config.apiUrl,
56
56
  token: config.token || undefined,
57
57
  withUserToken,
58
- resources: resolvedResources,
58
+ resolvedResources,
59
59
  blueprintFileDir: dirname(blueprint.fileInfo.blueprintFilePath),
60
60
  log,
61
61
  };
@@ -116,7 +116,9 @@ export async function functionTestCore(options) {
116
116
  }
117
117
  };
118
118
  const logNestedRuns = () => {
119
- for (const run of [...nestedRuns].sort((a, b) => a.order - b.order)) {
119
+ // Remove buffered logs for durable runs
120
+ const runs = plan.isDurable ? nestedRuns.splice(0) : [...nestedRuns];
121
+ for (const run of runs.sort((a, b) => a.order - b.order)) {
120
122
  // Skip if is Durable so log ordering is retained
121
123
  if (run.isLiveDurable)
122
124
  continue;
@@ -155,10 +157,19 @@ export async function functionTestCore(options) {
155
157
  if (event.kind === 'delegate') {
156
158
  logNestedRuns();
157
159
  }
158
- const glyph = STEP_PHASE_GLYPH[event.phase] ?? '•';
160
+ const willRetry = event.phase === 'error' && typeof event.retryDelayMs === 'number';
161
+ const glyph = willRetry ? '⟳' : (STEP_PHASE_GLYPH[event.phase] ?? '•');
159
162
  const duration = typeof event.durationMs === 'number' ? ` (${Math.round(event.durationMs)}ms)` : '';
163
+ const retriesPerformed = (event.attempt ?? 1) - 1;
164
+ let retryNote = '';
165
+ if (willRetry) {
166
+ retryNote = ` retry ${event.attempt} of ${event.maxAttempts}`;
167
+ }
168
+ else if (event.phase === 'error' && retriesPerformed > 0) {
169
+ retryNote = ` failed after ${retriesPerformed} retries`;
170
+ }
160
171
  const detail = event.phase === 'error' && event.error ? ` ${event.error.message}` : '';
161
- lines.push(` ${glyph} ${event.name} [${event.kind}]${duration}${detail}`);
172
+ lines.push(` ${glyph} ${event.name} [${event.kind}]${duration}${detail}${retryNote}`);
162
173
  }
163
174
  return lines;
164
175
  }
@@ -8,7 +8,7 @@ import { findBlueprintFile, loadAndParseBlueprint } from '../actions/blueprints/
8
8
  import { readConfigFile } from '../actions/blueprints/config.js';
9
9
  import config from '../config.js';
10
10
  import { isRecord } from '../utils/is-record.js';
11
- import { isEventType, isGroqContextOptions, } from '../utils/types.js';
11
+ import { isEventType, } from '../utils/types.js';
12
12
  import { handleInvokeRequest } from './handlers/invoke.js';
13
13
  const app = (host, port, validateResources, executionOptions) => {
14
14
  // Send a JSON envelope to every connected UI client.
@@ -75,7 +75,7 @@ const app = (host, port, validateResources, executionOptions) => {
75
75
  const { data, func: functionName, metadata, } = parseInvokeRequest(Buffer.concat(body), dirname(fileInfo.blueprintFilePath));
76
76
  const { context, event } = data;
77
77
  // replace user token if required
78
- if (isGroqContextOptions(context)) {
78
+ if ('clientOptions' in context) {
79
79
  if (context.clientOptions.token) {
80
80
  context.clientOptions.token = config.token || undefined;
81
81
  }
@@ -1,8 +1,9 @@
1
1
  import type { BlueprintResource } from '@sanity/blueprints';
2
+ import type { ResolvedResource } from '../types.js';
2
3
  export type GroupedBlueprintResource = {
3
4
  id: string;
4
5
  name: string;
5
6
  type: string;
6
7
  };
7
8
  export type GroupedBlueprintResources = Record<string, GroupedBlueprintResource[]>;
8
- export declare function groupBlueprintResources(blueprintResources: BlueprintResource[]): GroupedBlueprintResources;
9
+ export declare function groupBlueprintResources(blueprintResources: ResolvedResource<BlueprintResource>[]): GroupedBlueprintResources;
@@ -8,7 +8,8 @@ const BLUEPRINT_TYPE_TO_KEY = {
8
8
  };
9
9
  export function groupBlueprintResources(blueprintResources) {
10
10
  const grouped = {};
11
- for (const resource of blueprintResources) {
11
+ for (const resolvedResource of blueprintResources) {
12
+ const resource = resolvedResource.resource;
12
13
  if (resource.type === 'sanity.access.robot')
13
14
  continue;
14
15
  const key = (resource.type?.startsWith('sanity.function.')
@@ -18,9 +19,8 @@ export function groupBlueprintResources(blueprintResources) {
18
19
  continue;
19
20
  if (!grouped[key])
20
21
  grouped[key] = [];
21
- const externalId = resource.externalId;
22
22
  grouped[key].push({
23
- id: externalId || 'not-deployed',
23
+ id: resolvedResource.externalId || 'not-deployed',
24
24
  name: resource.name,
25
25
  type: resource.type,
26
26
  });
@@ -1,5 +1,5 @@
1
1
  import type { BlueprintResource } from '@sanity/blueprints';
2
- import type { AuthParams } from '../types.js';
2
+ import type { AuthParams, ResolvedResource } from '../types.js';
3
3
  interface ResolveOptions {
4
4
  resources: BlueprintResource[];
5
5
  stackId?: string;
@@ -12,5 +12,5 @@ interface ResolveOptions {
12
12
  * resources that aren't yet deployed are preserved without an externalId.
13
13
  * Any failure to fetch the stack falls back to the raw local resources.
14
14
  */
15
- export declare function resolveDeployedResources({ resources, stackId, auth, fetchFn, }: ResolveOptions): Promise<BlueprintResource[]>;
15
+ export declare function resolveDeployedResources({ resources, stackId, auth, fetchFn, }: ResolveOptions): Promise<ResolvedResource<BlueprintResource>[]>;
16
16
  export {};
@@ -7,11 +7,11 @@ import { getStack } from '../../actions/blueprints/stacks.js';
7
7
  */
8
8
  export async function resolveDeployedResources({ resources, stackId, auth, fetchFn, }) {
9
9
  if (!stackId || !auth?.token || !auth.scopeType || !auth.scopeId)
10
- return resources;
10
+ return resources.map((r) => ({ resource: r }));
11
11
  try {
12
12
  const response = await getStack({ stackId, auth, fetchFn });
13
13
  if (!response.ok || !Array.isArray(response.stack?.resources))
14
- return resources;
14
+ return resources.map((r) => ({ resource: r }));
15
15
  const deployedByKey = new Map();
16
16
  for (const deployed of response.stack.resources) {
17
17
  if (deployed.externalId) {
@@ -20,10 +20,10 @@ export async function resolveDeployedResources({ resources, stackId, auth, fetch
20
20
  }
21
21
  return resources.map((resource) => {
22
22
  const externalId = deployedByKey.get(`${resource.type}:${resource.name}`);
23
- return externalId ? { ...resource, externalId } : resource;
23
+ return { resource, externalId };
24
24
  });
25
25
  }
26
26
  catch {
27
- return resources;
27
+ return resources.map((r) => ({ resource: r }));
28
28
  }
29
29
  }
@@ -1,7 +1,7 @@
1
1
  import type { BlueprintApplicationResource, BlueprintInstallationConfigResource, BlueprintStudioResource } from '@sanity/blueprints';
2
2
  import { type BuildStudioEventListener } from '@sanity/cli-build/_internal/build';
3
3
  import type { Output } from '@sanity/cli-core';
4
- import type { AssetPrepResult } from '../types.js';
4
+ import type { AssetPrepResult, ResolvedResource } from '../types.js';
5
5
  export type BundleResource = BlueprintStudioResource | BlueprintApplicationResource | BlueprintInstallationConfigResource;
6
6
  type BuildContext<R extends BundleResource> = {
7
7
  resource: R;
@@ -28,9 +28,9 @@ export declare const BUNDLE_SPECS: {
28
28
  build: typeof buildInstallationConfigBundle;
29
29
  };
30
30
  };
31
- export declare function prepareBundleAsset<R extends BundleResource>({ blueprintFileDir, resource, }: {
31
+ export declare function prepareBundleAsset<R extends BundleResource>({ blueprintFileDir, resolvedResource, }: {
32
32
  blueprintFileDir: string;
33
- resource: R;
33
+ resolvedResource: ResolvedResource<R>;
34
34
  }): Promise<AssetPrepResult>;
35
35
  declare function buildStudioBundle({ resource, appId, workDir, outDir, output, eventListener, }: BuildContext<BlueprintStudioResource>): Promise<BuildResult>;
36
36
  declare function buildApplicationBundle({ resource, appId, workDir, outDir, output, eventListener, }: BuildContext<BlueprintApplicationResource>): Promise<BuildResult>;
@@ -12,10 +12,11 @@ export const BUNDLE_SPECS = {
12
12
  'sanity.application': { requiresAppId: true, build: buildApplicationBundle },
13
13
  'sanity.installation.config': { requiresAppId: false, build: buildInstallationConfigBundle },
14
14
  };
15
- export async function prepareBundleAsset({ blueprintFileDir, resource, }) {
15
+ export async function prepareBundleAsset({ blueprintFileDir, resolvedResource, }) {
16
+ const resource = resolvedResource.resource;
16
17
  // The lookup can't prove it yields the spec matching this R, so it's asserted once here.
17
18
  const spec = BUNDLE_SPECS[resource.type];
18
- const appId = getAppId(resource);
19
+ const appId = getAppId(resolvedResource);
19
20
  if (spec.requiresAppId && !appId) {
20
21
  return {
21
22
  success: false,
@@ -260,12 +261,12 @@ async function getInstalledSanityVersion(workDir) {
260
261
  return undefined;
261
262
  }
262
263
  }
263
- function getAppId(resource) {
264
- if ('externalId' in resource && typeof resource.externalId === 'string' && resource.externalId) {
265
- return resource.externalId;
264
+ function getAppId(resolvedResource) {
265
+ if (resolvedResource.externalId) {
266
+ return resolvedResource.externalId;
266
267
  }
267
- else if (resource.lifecycle?.ownershipAction?.type === 'attach') {
268
- return resource.lifecycle.ownershipAction.id;
268
+ else if (resolvedResource.resource.lifecycle?.ownershipAction?.type === 'attach') {
269
+ return resolvedResource.resource.lifecycle.ownershipAction.id;
269
270
  }
270
271
  return undefined;
271
272
  }
@@ -1,5 +1,6 @@
1
1
  import {performance} from 'node:perf_hooks'
2
2
  import {inspect} from 'node:util'
3
+ import {createStepHistory} from './step-history.js'
3
4
 
4
5
  /**
5
6
  *
@@ -26,7 +27,9 @@ const formatArgs = (args) =>
26
27
  * @param io
27
28
  */
28
29
  export async function runDurable(handler, {context, event}, io) {
30
+ const history = createStepHistory()
29
31
  let currentStep = null
32
+
30
33
  const streamLog = (level, args) => {
31
34
  const date = new Date()
32
35
  const timestamp = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
@@ -41,7 +44,6 @@ export async function runDurable(handler, {context, event}, io) {
41
44
  })
42
45
  }
43
46
 
44
- // @todo: need to throw error for use of `console.log` in a durable function
45
47
  const logger = {
46
48
  log: (...a) => streamLog('info', a),
47
49
  info: (...a) => streamLog('info', a),
@@ -49,24 +51,57 @@ export async function runDurable(handler, {context, event}, io) {
49
51
  error: (...a) => streamLog('error', a),
50
52
  }
51
53
 
52
- const track = async (name, kind, produce) => {
53
- io.emit({name, kind, phase: 'start'})
54
- const startedAt = performance.now()
54
+ const emitFailure = (pending) => {
55
+ history.settle(pending)
56
+ io.emit(pending.event)
57
+ }
58
+
59
+ const settleUnretried = () => {
60
+ const pending = history.getPending()
61
+ if (pending !== null) {
62
+ emitFailure(pending)
63
+ }
64
+ }
65
+
66
+ const track = async (name, kind, produce, retry) => {
67
+ settleUnretried()
68
+ const {index, saved} = history.nextStep()
69
+
70
+ if (saved?.phase === 'complete') {
71
+ return saved.output
72
+ }
73
+
74
+ const maxAttempts = retry?.maxAttempts ?? 1
75
+ const retries = Math.max((retry?.maxAttempts ?? 1) - 1, 0)
76
+ const waitingOnRetry = typeof saved?.retryDelayMs === 'number'
77
+
78
+ if (saved?.phase === 'error' && !waitingOnRetry) {
79
+ throw saved.output
80
+ }
81
+
82
+ const attempt = (saved?.attempt ?? 0) + 1
55
83
  const prev = currentStep
56
84
  currentStep = name
57
85
 
86
+ io.emit({name, kind, phase: 'start'})
87
+ const startedAt = performance.now()
88
+
58
89
  try {
59
- const value = await produce()
60
- io.emit({
90
+ const value = await produce(attempt)
91
+ const completeEvent = {
61
92
  name,
62
93
  kind,
63
94
  phase: 'complete',
64
95
  output: value,
65
96
  durationMs: performance.now() - startedAt,
66
- })
97
+ }
98
+
99
+ history.recordSuccess(index, completeEvent)
100
+ io.emit(completeEvent)
101
+
67
102
  return value
68
103
  } catch (err) {
69
- io.emit({
104
+ const errorEvent = {
70
105
  name,
71
106
  kind,
72
107
  phase: 'error',
@@ -76,7 +111,21 @@ export async function runDurable(handler, {context, event}, io) {
76
111
  stack: err?.stack ?? '',
77
112
  },
78
113
  durationMs: performance.now() - startedAt,
114
+ attempt,
115
+ retries,
116
+ maxAttempts,
117
+ }
118
+
119
+ const retryPending = history.recordFailure(index, {
120
+ event: errorEvent,
121
+ error: err,
122
+ delayMs: retry?.delay ? durationToMs(retry.delay) : 0,
79
123
  })
124
+
125
+ if (!retryPending) {
126
+ io.emit(errorEvent)
127
+ }
128
+
80
129
  throw err
81
130
  } finally {
82
131
  currentStep = prev
@@ -84,12 +133,21 @@ export async function runDurable(handler, {context, event}, io) {
84
133
  }
85
134
 
86
135
  const step = {
87
- run: ({name, handler: fn}) => track(name || '', 'run', () => fn({logger, attempt: 1})),
88
- wait: ({name, duration}) =>
89
- track(name || '', 'wait', async () => {
90
- await io.wait(durationToMs(duration))
91
- return undefined
92
- }),
136
+ run: ({name, handler: fn, retry}) =>
137
+ track(name || '', 'run', (attempt) => fn({logger, attempt}), retry),
138
+ delegate: ({name, handler, event, retry}) =>
139
+ track(
140
+ name || '',
141
+ 'delegate',
142
+ () => {
143
+ const fn = typeof handler === 'string' ? handler : handler?.name
144
+ if (!fn) {
145
+ throw new Error('No function name provided for delegation')
146
+ }
147
+ return io.invoke(fn, event?.data ?? {})
148
+ },
149
+ retry,
150
+ ),
93
151
  waitForCondition: ({name, initial, poller, next}) =>
94
152
  track(name || '', 'wait-for-condition', async () => {
95
153
  let state = initial
@@ -107,15 +165,34 @@ export async function runDurable(handler, {context, event}, io) {
107
165
  }
108
166
  return state
109
167
  }),
110
- delegate: ({name, handler, event}) =>
111
- track(name || '', 'delegate', () => {
112
- const fn = typeof handler === 'string' ? handler : handler?.name
113
- if (!fn) {
114
- throw new Error('No function name provided for delegation')
115
- }
116
- return io.invoke(fn, event)
168
+ wait: ({name, duration}) =>
169
+ track(name || '', 'wait', async () => {
170
+ await io.wait(durationToMs(duration))
171
+ return undefined
117
172
  }),
118
173
  }
119
174
 
120
- return handler({context, event, step, logger})
175
+ let result
176
+ let done = false
177
+ while (!done) {
178
+ history.startRun()
179
+ try {
180
+ result = await handler({context, event, step, logger})
181
+ settleUnretried()
182
+ done = true
183
+ } catch (err) {
184
+ const pending = history.getPending()
185
+ if (pending === null) {
186
+ throw err
187
+ }
188
+ if (err?.name === 'UnrecoverableError') {
189
+ emitFailure(pending)
190
+ throw err
191
+ }
192
+ io.emit({...pending.event, retryDelayMs: pending.delayMs})
193
+ await io.wait(pending.delayMs)
194
+ }
195
+ }
196
+
197
+ return result
121
198
  }
@@ -0,0 +1,15 @@
1
+ import type {DurableStepEvent} from './types'
2
+
3
+ type PendingRetry = {index: number; event: DurableStepEvent; delayMs: number}
4
+
5
+ export function createStepHistory(): {
6
+ startRun: () => void
7
+ nextStep: () => {index: number; saved: DurableStepEvent | undefined}
8
+ recordSuccess: (index: number, event: DurableStepEvent) => void
9
+ recordFailure: (
10
+ index: number,
11
+ failure: {event: DurableStepEvent; error: unknown; delayMs: number},
12
+ ) => boolean
13
+ getPending: () => PendingRetry | null
14
+ settle: (pending: PendingRetry) => void
15
+ }
@@ -0,0 +1,52 @@
1
+ const createStepHistory = () => {
2
+ const history = new Map([])
3
+ let cursor = 0
4
+ let pending = null
5
+
6
+ const startRun = () => {
7
+ cursor = 0
8
+ }
9
+
10
+ const nextStep = () => {
11
+ const index = cursor
12
+ cursor++
13
+ return {index, saved: history.get(index)}
14
+ }
15
+
16
+ const recordSuccess = (index, event) => {
17
+ history.set(index, event)
18
+ }
19
+
20
+ const recordFailure = (index, {event, error, delayMs}) => {
21
+ const retryable = event.attempt <= event.retries && error?.name !== 'UnrecoverableError'
22
+
23
+ if (retryable) {
24
+ history.set(index, {...event, output: error, retryDelayMs: delayMs})
25
+ pending = {index, event, delayMs}
26
+ } else {
27
+ history.set(index, {...event, output: error})
28
+ }
29
+ return retryable
30
+ }
31
+
32
+ const getPending = () => {
33
+ const item = pending
34
+ pending = null
35
+ return item
36
+ }
37
+
38
+ const settle = ({index, event}) => {
39
+ history.set(index, {...event, output: history.get(index).output})
40
+ }
41
+
42
+ return {
43
+ startRun,
44
+ nextStep,
45
+ recordSuccess,
46
+ recordFailure,
47
+ getPending,
48
+ settle,
49
+ }
50
+ }
51
+
52
+ export {createStepHistory}
@@ -64,10 +64,11 @@ export default async function invoke(resource, payload, context, options) {
64
64
  // Apply the GROQ rule before preparing a build so a filtered-out event skips
65
65
  // the (potentially expensive) transpile entirely.
66
66
  if (isGroqInvocation(resource)) {
67
- if (!isGroqContextOptions(context)) {
67
+ if (!('clientOptions' in context)) {
68
68
  throw new Error('GROQ-based functions require a context with clientOptions');
69
69
  }
70
- const { projectId, dataset } = context.clientOptions;
70
+ const projectId = 'projectId' in context.clientOptions ? context.clientOptions.projectId : undefined;
71
+ const dataset = 'dataset' in context.clientOptions ? context.clientOptions.dataset : undefined;
71
72
  if ('before' in payload) {
72
73
  filteredData = await applyGroqRule(resource, payload, projectId, dataset);
73
74
  const filter = resource.event && 'filter' in resource.event ? resource.event.filter : '';
@@ -173,12 +174,22 @@ export default async function invoke(resource, payload, context, options) {
173
174
  // of `nested.error`. The error isn't lost: it travels back in the returned
174
175
  // result and is logged/streamed to the UI by the caller (app/test core).
175
176
  if (child && !child.killed) {
176
- const response = {
177
- type: 'invoke-response',
178
- id,
179
- ok: true,
180
- result: { json: nested.json },
181
- };
177
+ // step.delegate errors need to be surfaced to the caller for retry behavior and for the durable step to be marked as failed.
178
+ const origin = metadata?.origin || undefined;
179
+ const error = nested.error || undefined;
180
+ const response = origin === 'step.delegate' && error
181
+ ? {
182
+ type: 'invoke-response',
183
+ id,
184
+ ok: false,
185
+ error,
186
+ }
187
+ : {
188
+ type: 'invoke-response',
189
+ id,
190
+ ok: true,
191
+ result: { json: nested.json },
192
+ };
182
193
  child.send(JSON.stringify(response));
183
194
  }
184
195
  }
@@ -1,6 +1,6 @@
1
1
  import type { BlueprintResource } from '@sanity/blueprints';
2
2
  import type { Logger } from '../../logger.js';
3
- import type { EventType, FunctionGroqResource, FunctionResource, FunctionTestFlags, InvokeContextOptions, InvokePayloadOptions } from '../../types.js';
3
+ import type { EventType, FunctionGroqResource, FunctionResource, FunctionTestFlags, InvokeContextOptions, InvokePayloadOptions, ResolvedResource } from '../../types.js';
4
4
  import buildPayload from '../build-payload.js';
5
5
  import { fetchAsset, fetchDocument } from '../fetch-document.js';
6
6
  export type InvocationPlan = {
@@ -23,7 +23,7 @@ export type InputContext = {
23
23
  apiHost: string;
24
24
  token?: string;
25
25
  withUserToken?: boolean;
26
- resources: BlueprintResource[];
26
+ resolvedResources: ResolvedResource<BlueprintResource>[];
27
27
  blueprintFileDir: string;
28
28
  log: Logger;
29
29
  };
@@ -51,7 +51,7 @@ export const buildContext = (plan, input) => {
51
51
  if (plan.kind === 'plain') {
52
52
  return {
53
53
  clientOptions: { ...withToken },
54
- resources: input.resources,
54
+ resources: input.resolvedResources,
55
55
  blueprintFileDir: input.blueprintFileDir,
56
56
  };
57
57
  }
@@ -69,7 +69,7 @@ export const buildContext = (plan, input) => {
69
69
  '',
70
70
  functionResourceType: 'project',
71
71
  functionResourceId: input.projectId || '',
72
- resources: input.resources,
72
+ resources: input.resolvedResources,
73
73
  blueprintFileDir: input.blueprintFileDir,
74
74
  };
75
75
  };
@@ -40,6 +40,11 @@ export interface BlueprintResourceRecord extends BlueprintResource {
40
40
  export interface DeployedResource extends BlueprintResourceRecord {
41
41
  externalId: string;
42
42
  }
43
+ /** Wrap a resource and its externalId if it has one. */
44
+ export interface ResolvedResource<R extends BlueprintResource> {
45
+ resource: R;
46
+ externalId?: string;
47
+ }
43
48
  export declare function isLocalFunctionResource(r: BlueprintResource): r is FunctionResource;
44
49
  export declare function hasEventSource(r: FunctionResource): r is EventFunctionResource;
45
50
  export declare function isScheduleEvent(e: unknown): e is BlueprintScheduledFunctionResource['event'];
@@ -134,8 +139,14 @@ export type InvokePayloadMetadata = Pick<InvokeGroqPayloadOptions, 'event' | 'be
134
139
  export type EventType = 'create' | 'update' | 'delete';
135
140
  export type NonGroqEventType = 'plain' | 'nested' | 'durable';
136
141
  export declare function isEventType(arg: string): arg is EventType;
142
+ interface BaseContextOptions {
143
+ /** All resources declared by the blueprint, used to populate `context.resources` at invoke time. */
144
+ resources?: ResolvedResource<BlueprintResource>[];
145
+ /** The directory where the blueprint manifest is located. */
146
+ blueprintFileDir: string;
147
+ }
137
148
  /** @internal */
138
- export interface InvokeGroqContextOptions {
149
+ export interface InvokeGroqContextOptions extends BaseContextOptions {
139
150
  clientOptions: {
140
151
  apiVersion?: string;
141
152
  dataset?: string;
@@ -151,20 +162,12 @@ export interface InvokeGroqContextOptions {
151
162
  functionResourceType: string;
152
163
  /** The resource ID of the function container; resource ID that houses the function. */
153
164
  functionResourceId: string;
154
- /** All resources declared by the blueprint, used to populate `context.resources` at invoke time. */
155
- resources?: BlueprintResource[];
156
- /** The directory where the blueprint manifest is located. */
157
- blueprintFileDir: string;
158
165
  }
159
166
  /** @internal */
160
- export interface InvokeScheduleContextOptions {
167
+ export interface InvokeScheduleContextOptions extends BaseContextOptions {
161
168
  clientOptions: {
162
169
  token?: string;
163
170
  };
164
- /** All resources declared by the blueprint, used to populate `context.resources` at invoke time. */
165
- resources?: BlueprintResource[];
166
- /** The directory where the blueprint manifest is located. */
167
- blueprintFileDir: string;
168
171
  }
169
172
  export type InvokeContextOptions = InvokeGroqContextOptions | InvokeScheduleContextOptions;
170
173
  export declare function isGroqContextOptions(context: InvokeContextOptions): context is InvokeGroqContextOptions;
@@ -338,7 +341,7 @@ export type AssetPrepResult = {
338
341
  error: string;
339
342
  };
340
343
  export type PrepAssetFn<R extends BlueprintResource> = (options: {
341
- resource: R;
344
+ resolvedResource: ResolvedResource<R>;
342
345
  blueprintFileDir: string;
343
346
  installer?: InstallerType;
344
347
  }) => Promise<AssetPrepResult>;
@@ -355,7 +358,7 @@ export type PreDeployConfig = {
355
358
  prepareResource?: PrepResourceFn<BlueprintResource>;
356
359
  };
357
360
  export type AssetResource<R extends BlueprintResource> = {
358
- resource: R;
361
+ resource: ResolvedResource<R>;
359
362
  prepareAsset: PrepAssetFn<R>;
360
363
  };
361
364
  export type Warning = {
@@ -376,6 +379,10 @@ export interface DurableStepEvent {
376
379
  output?: unknown;
377
380
  error?: InvocationError;
378
381
  durationMs?: number;
382
+ retryDelayMs?: number;
383
+ attempt?: number;
384
+ retries?: number;
385
+ maxAttempts?: number;
379
386
  }
380
387
  export interface DurableLogEvent {
381
388
  kind: 'log';
@@ -38,7 +38,7 @@ export function isEventType(arg) {
38
38
  return ['create', 'update', 'delete'].includes(arg);
39
39
  }
40
40
  export function isGroqContextOptions(context) {
41
- return 'clientOptions' in context;
41
+ return 'eventResourceType' in context;
42
42
  }
43
43
  /** @internal */
44
44
  export var BlueprintParserErrorType;
@@ -3148,5 +3148,5 @@
3148
3148
  ]
3149
3149
  }
3150
3150
  },
3151
- "version": "17.16.0-next.g26925c0"
3151
+ "version": "17.16.0-next.g91e6dd2"
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.16.0-next.g26925c0",
4
+ "version": "17.16.0-next.g91e6dd2",
5
5
  "author": "Sanity Runtime Team",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -100,7 +100,7 @@
100
100
  "@oclif/test": "^4.2.0",
101
101
  "@playwright/test": "^1.63.0",
102
102
  "@sanity-labs/design-tokens": "^0.0.2-alpha.2",
103
- "@sanity/functions": "^1.7.2",
103
+ "@sanity/functions": "^1.8.1",
104
104
  "@types/cardinal": "^2.1.1",
105
105
  "@types/mime-types": "^3.0.1",
106
106
  "@types/node": "^22",