@sanity/runtime-cli 17.4.0 → 17.6.0-blueprinit.g77b7a64

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.
Files changed (86) hide show
  1. package/README.md +55 -52
  2. package/SKILL.md +59 -0
  3. package/dist/actions/blueprinit/manifest.d.ts +15 -0
  4. package/dist/actions/blueprinit/manifest.js +82 -0
  5. package/dist/actions/blueprinit/studio.d.ts +6 -0
  6. package/dist/actions/blueprinit/studio.js +43 -0
  7. package/dist/actions/blueprinit/templates.d.ts +34 -0
  8. package/dist/actions/blueprinit/templates.js +83 -0
  9. package/dist/actions/blueprinit/workspace.d.ts +16 -0
  10. package/dist/actions/blueprinit/workspace.js +143 -0
  11. package/dist/actions/blueprints/blueprint.js +2 -1
  12. package/dist/actions/blueprints/pre-deploy.d.ts +15 -6
  13. package/dist/actions/blueprints/pre-deploy.js +29 -70
  14. package/dist/actions/blueprints/process-assets.d.ts +29 -0
  15. package/dist/actions/blueprints/process-assets.js +80 -0
  16. package/dist/actions/blueprints/resources.js +17 -1
  17. package/dist/actions/sanity/datasets.d.ts +15 -0
  18. package/dist/actions/sanity/datasets.js +26 -0
  19. package/dist/commands/blueprints/deploy.js +3 -1
  20. package/dist/commands/blueprints/destroy.js +1 -1
  21. package/dist/commands/blueprints/info.d.ts +1 -0
  22. package/dist/commands/blueprints/info.js +5 -0
  23. package/dist/commands/blueprints/init.js +1 -2
  24. package/dist/commands/blueprints/logs.js +1 -0
  25. package/dist/commands/functions/add.js +3 -3
  26. package/dist/commands/functions/dev.js +2 -2
  27. package/dist/commands/functions/logs.d.ts +1 -1
  28. package/dist/commands/functions/logs.js +3 -0
  29. package/dist/commands/init.d.ts +17 -0
  30. package/dist/commands/init.js +59 -0
  31. package/dist/constants.d.ts +5 -0
  32. package/dist/constants.js +6 -0
  33. package/dist/cores/blueprinit/adopt.d.ts +6 -0
  34. package/dist/cores/blueprinit/adopt.js +119 -0
  35. package/dist/cores/blueprinit/finish.d.ts +14 -0
  36. package/dist/cores/blueprinit/finish.js +64 -0
  37. package/dist/cores/blueprinit/fresh.d.ts +3 -0
  38. package/dist/cores/blueprinit/fresh.js +120 -0
  39. package/dist/cores/blueprinit/preview.d.ts +15 -0
  40. package/dist/cores/blueprinit/preview.js +14 -0
  41. package/dist/cores/blueprinit/prompts.d.ts +5 -0
  42. package/dist/cores/blueprinit/prompts.js +22 -0
  43. package/dist/cores/blueprinit/selection.d.ts +15 -0
  44. package/dist/cores/blueprinit/selection.js +90 -0
  45. package/dist/cores/blueprinit/types.d.ts +16 -0
  46. package/dist/cores/blueprinit/types.js +1 -0
  47. package/dist/cores/blueprinit.d.ts +7 -0
  48. package/dist/cores/blueprinit.js +48 -0
  49. package/dist/cores/blueprints/deploy.js +71 -19
  50. package/dist/cores/blueprints/plan.js +2 -0
  51. package/dist/cores/functions/add.js +11 -8
  52. package/dist/server/static/components/api-base.js +1 -0
  53. package/dist/server/static/components/filters.js +4 -2
  54. package/dist/server/static/components/payload-panel.js +3 -0
  55. package/dist/utils/blueprints/extract-durable-config.d.ts +8 -0
  56. package/dist/utils/blueprints/extract-durable-config.js +60 -0
  57. package/dist/utils/blueprints/validate-resources.js +5 -2
  58. package/dist/utils/display/blueprints-formatting.js +8 -4
  59. package/dist/utils/display/resources-formatting.js +2 -2
  60. package/dist/utils/style-text.js +1 -0
  61. package/dist/utils/types.d.ts +8 -2
  62. package/dist/utils/types.js +8 -2
  63. package/oclif.manifest.json +151 -15
  64. package/package.json +29 -15
  65. package/skills/_functions-build.md +23 -0
  66. package/skills/_functions-list.md +28 -0
  67. package/skills/blueprints-add.md +8 -0
  68. package/skills/blueprints-config.md +33 -0
  69. package/skills/blueprints-deploy.md +36 -0
  70. package/skills/blueprints-destroy.md +30 -0
  71. package/skills/blueprints-doctor.md +31 -0
  72. package/skills/blueprints-info.md +23 -0
  73. package/skills/blueprints-init.md +48 -0
  74. package/skills/blueprints-logs.md +33 -0
  75. package/skills/blueprints-mint-deploy-token.md +25 -0
  76. package/skills/blueprints-plan.md +28 -0
  77. package/skills/blueprints-promote.md +31 -0
  78. package/skills/blueprints-stacks.md +28 -0
  79. package/skills/functions-add.md +35 -0
  80. package/skills/functions-dev.md +21 -0
  81. package/skills/functions-env-add.md +29 -0
  82. package/skills/functions-env-list.md +21 -0
  83. package/skills/functions-env-remove.md +22 -0
  84. package/skills/functions-logs.md +39 -0
  85. package/skills/functions-test.md +31 -0
  86. package/skills/init.md +155 -0
@@ -0,0 +1,90 @@
1
+ import { select } from '@inquirer/prompts';
2
+ export async function chooseOrganization(groups, organizationId, interactive) {
3
+ if (groups.length === 0) {
4
+ return failure('No organizations found for this account.');
5
+ }
6
+ if (organizationId) {
7
+ const organization = groups.find((group) => group.organization.id === organizationId);
8
+ return organization
9
+ ? { ok: true, value: organization }
10
+ : failure(`No organization "${organizationId}" found for this account.`);
11
+ }
12
+ if (groups.length === 1)
13
+ return { ok: true, value: groups[0] };
14
+ if (!interactive) {
15
+ return failure('Multiple organizations found.', ['Pass --organization-id <id> to choose one.']);
16
+ }
17
+ const selectedId = await select({
18
+ message: 'Choose a Sanity organization:',
19
+ choices: groups.map(({ organization, projects }) => ({
20
+ name: organization.name,
21
+ value: organization.id,
22
+ description: `${projects?.length ?? 0} projects`,
23
+ })),
24
+ });
25
+ const selected = groups.find(({ organization }) => organization.id === selectedId);
26
+ return selected ? { ok: true, value: selected } : failure('Organization not found.');
27
+ }
28
+ export async function chooseProject(projects, projectId, organizationName, interactive) {
29
+ if (projects.length === 0) {
30
+ return failure(`No projects found in ${organizationName}.`);
31
+ }
32
+ if (projectId) {
33
+ const project = projects.find(({ id }) => id === projectId);
34
+ return project
35
+ ? { ok: true, value: project }
36
+ : failure(`No project "${projectId}" found in ${organizationName}.`);
37
+ }
38
+ if (projects.length === 1)
39
+ return { ok: true, value: projects[0] };
40
+ if (!interactive) {
41
+ return failure('Multiple projects found.', ['Pass --project-id <id> to choose one.']);
42
+ }
43
+ const selectedId = await select({
44
+ message: 'Choose a Sanity project:',
45
+ choices: projects.map((project) => ({ name: project.displayName, value: project.id })),
46
+ });
47
+ const selected = projects.find(({ id }) => id === selectedId);
48
+ return selected ? { ok: true, value: selected } : failure('Project not found.');
49
+ }
50
+ export async function chooseDataset(datasets, datasetName, projectName, interactive) {
51
+ if (datasets.length === 0) {
52
+ return failure(`No datasets found in ${projectName}.`);
53
+ }
54
+ if (datasetName) {
55
+ const dataset = datasets.find(({ name }) => name === datasetName);
56
+ return dataset
57
+ ? { ok: true, value: dataset }
58
+ : failure(`No dataset "${datasetName}" found in ${projectName}.`);
59
+ }
60
+ if (datasets.length === 1)
61
+ return { ok: true, value: datasets[0] };
62
+ if (!interactive) {
63
+ return failure('Multiple datasets found.', ['Pass --dataset <name> to choose one.']);
64
+ }
65
+ const production = datasets.find(({ name }) => name === 'production');
66
+ const orderedDatasets = production
67
+ ? [production, ...datasets.filter(({ name }) => name !== 'production')]
68
+ : datasets;
69
+ const selectedName = await select({
70
+ message: 'Choose a Sanity dataset:',
71
+ choices: orderedDatasets.map((dataset) => ({
72
+ name: dataset.name,
73
+ value: dataset.name,
74
+ description: dataset.aclMode,
75
+ })),
76
+ });
77
+ const selected = datasets.find(({ name }) => name === selectedName);
78
+ return selected ? { ok: true, value: selected } : failure('Dataset not found.');
79
+ }
80
+ export function findProject(groups, projectId) {
81
+ for (const { organization, projects = [] } of groups) {
82
+ const project = projects.find(({ id }) => id === projectId);
83
+ if (project)
84
+ return { organization, project };
85
+ }
86
+ return undefined;
87
+ }
88
+ function failure(message, suggestions) {
89
+ return { ok: false, error: { message, suggestions } };
90
+ }
@@ -0,0 +1,16 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ export interface BlueprinitFlags {
3
+ yes?: boolean;
4
+ organizationId?: string;
5
+ projectId?: string;
6
+ datasetName?: string;
7
+ stackName?: string;
8
+ studioName?: string;
9
+ }
10
+ export interface BlueprinitFlowOptions {
11
+ bin: string;
12
+ dir: string;
13
+ flags: BlueprinitFlags;
14
+ log: Logger;
15
+ token: string;
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { BlueprinitFlags } from './blueprinit/types.js';
2
+ import type { CoreConfig, CoreResult } from './index.js';
3
+ export interface BlueprinitOptions extends CoreConfig {
4
+ dir?: string;
5
+ flags?: BlueprinitFlags;
6
+ }
7
+ export declare function blueprinitCore(options: BlueprinitOptions): Promise<CoreResult>;
@@ -0,0 +1,48 @@
1
+ import { cwd } from 'node:process';
2
+ import { detectStudio } from '../actions/blueprinit/studio.js';
3
+ import { findBlueprintFile } from '../actions/blueprints/blueprint.js';
4
+ import { readConfigFile } from '../actions/blueprints/config.js';
5
+ import { validTokenOrErrorMessage } from '../utils/validated-token.js';
6
+ import { adoptFlow } from './blueprinit/adopt.js';
7
+ import { freshFlow } from './blueprinit/fresh.js';
8
+ export async function blueprinitCore(options) {
9
+ const { bin, log, flags = {}, dir = cwd() } = options;
10
+ try {
11
+ const existingBlueprint = findBlueprintFile(dir);
12
+ if (existingBlueprint) {
13
+ const blueprintConfig = readConfigFile(existingBlueprint.blueprintFilePath);
14
+ if (!blueprintConfig?.stackId) {
15
+ return {
16
+ success: false,
17
+ error: `A Blueprint exists here (${existingBlueprint.fileName}), but it is not connected to a Stack.`,
18
+ suggestions: [`Finish setup with: npx ${bin} blueprints init`],
19
+ };
20
+ }
21
+ log(`A blueprint already exists here (${existingBlueprint.fileName}).`);
22
+ log('');
23
+ log('Use these instead:');
24
+ log(` npx ${bin} blueprints config --edit`);
25
+ log(` npx ${bin} blueprints doctor`);
26
+ return {
27
+ success: true,
28
+ json: { alreadyInitialized: true, blueprintFile: existingBlueprint.fileName },
29
+ };
30
+ }
31
+ const tokenCheck = await validTokenOrErrorMessage(log);
32
+ if (!tokenCheck.ok) {
33
+ return {
34
+ success: false,
35
+ error: 'No Sanity session found.',
36
+ suggestions: ['Sign in first: npx sanity login', 'Then run init again.'],
37
+ };
38
+ }
39
+ log('Setting up your Sanity workspace.');
40
+ log('');
41
+ const flowOptions = { bin, dir, flags, log, token: tokenCheck.value };
42
+ const studio = detectStudio(dir);
43
+ return studio ? await adoptFlow({ ...flowOptions, studio }) : await freshFlow(flowOptions);
44
+ }
45
+ catch (error) {
46
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
47
+ }
48
+ }
@@ -3,22 +3,23 @@ import { preDeploy } from '../../actions/blueprints/pre-deploy.js';
3
3
  import { getStack, updateStack } from '../../actions/blueprints/stacks.js';
4
4
  import { waitForOperation } from '../../actions/blueprints/wait-for-operation.js';
5
5
  import { checkUserPermission } from '../../actions/sanity/access.js';
6
+ import { extractDurableConfig } from '../../utils/blueprints/extract-durable-config.js';
6
7
  import { createHintCollector } from '../../utils/blueprints/hints.js';
7
- import { niceId } from '../../utils/display/presenters.js';
8
+ import { check, niceId, severe } from '../../utils/display/presenters.js';
8
9
  import { CODE_OPERATION_UNCONFIRMED, EXIT_OPERATION_UNCONFIRMED } from '../../utils/exit-codes.js';
9
10
  import { styleText } from '../../utils/style-text.js';
10
11
  import { getWarnings } from '../../utils/warnings.js';
11
12
  export async function blueprintDeployCore(options) {
12
- const { bin = 'sanity', log, auth, stackId, scopeType, scopeId, deployedStack, blueprint, flags, } = options;
13
+ const { bin = 'sanity', log: logger, auth, stackId, scopeType, scopeId, deployedStack, blueprint, flags, } = options;
13
14
  const { verbose } = flags;
14
15
  const noWait = flags['no-wait'] || false;
15
16
  const stackName = flags['new-stack-name'] ?? deployedStack.name;
16
17
  const installer = flags['fn-installer'] || undefined;
17
18
  const blueprintFileDir = dirname(blueprint.fileInfo.blueprintFilePath);
18
19
  const workspaceWarnings = getWarnings({ dir: blueprintFileDir });
19
- log(`Deploying "${stackName}" ${niceId(deployedStack.id)}...`);
20
+ logger(`Deploying "${stackName}" ${niceId(deployedStack.id)}...`);
20
21
  try {
21
- const { ok: checkOk, stack: currentStack } = await getStack({ stackId, auth, logger: log });
22
+ const { ok: checkOk, stack: currentStack } = await getStack({ stackId, auth, logger });
22
23
  if (!checkOk) {
23
24
  return { success: false, error: 'Failed to check current Stack status' };
24
25
  }
@@ -34,23 +35,24 @@ export async function blueprintDeployCore(options) {
34
35
  };
35
36
  }
36
37
  const resources = [...blueprint.resources];
38
+ await extractDurableConfig(resources, blueprintFileDir);
37
39
  const preDeployResult = await preDeploy({
38
40
  scopeType,
39
41
  scopeId,
40
- resources,
42
+ resources, // mutated by preDeploy
41
43
  stackId,
42
44
  stackName,
43
45
  auth,
44
46
  blueprintFileDir,
45
47
  installer,
46
- log,
48
+ logger,
47
49
  bin,
48
50
  verbose,
51
+ onAssetEvent: createAssetEventHandler(logger),
49
52
  });
50
- if (!preDeployResult?.success) {
53
+ if (!preDeployResult?.success)
51
54
  return preDeployResult;
52
- }
53
- const spinner = log.ora('Deploying...').start();
55
+ const deploySpinner = logger.ora('Deploying...').start();
54
56
  const { ok: deployOk, stack, error: deployError, } = await updateStack({
55
57
  stackId,
56
58
  stackMutation: {
@@ -61,10 +63,10 @@ export async function blueprintDeployCore(options) {
61
63
  userMessage: flags.message,
62
64
  },
63
65
  auth,
64
- logger: log,
66
+ logger,
65
67
  });
66
68
  if (!deployOk) {
67
- spinner.fail(`${styleText('red', 'Failed')} to update Stack deployment`);
69
+ deploySpinner.fail(`${styleText('red', 'Failed')} to update Stack deployment`);
68
70
  const hints = createHintCollector(bin);
69
71
  if (deployError) {
70
72
  hints.inspectMessage(deployError);
@@ -79,18 +81,18 @@ export async function blueprintDeployCore(options) {
79
81
  ],
80
82
  };
81
83
  }
82
- spinner.stop().clear();
84
+ deploySpinner.stop().clear();
83
85
  const legacyPermissionsNoticePromise = CHECK_LEGACY_DEPLOY_PERMISSION
84
86
  ? checkLegacyDeployPermission({
85
87
  auth,
86
88
  scopeType,
87
89
  scopeId,
88
- log,
90
+ log: logger,
89
91
  })
90
92
  : Promise.resolve(null);
91
93
  if (noWait) {
92
- log(styleText(['bold', 'green'], 'Stack deployment started!'));
93
- log(`Use \`npx ${bin} blueprints info\` to check status`);
94
+ logger(styleText(['bold', 'green'], 'Stack deployment started!'));
95
+ logger(`Use \`npx ${bin} blueprints info\` to check status`);
94
96
  const legacyWarning = await legacyPermissionsNoticePromise;
95
97
  const warnings = [...(legacyWarning ? [legacyWarning] : []), ...workspaceWarnings];
96
98
  return {
@@ -100,19 +102,19 @@ export async function blueprintDeployCore(options) {
100
102
  warnings: warnings.length ? warnings : undefined,
101
103
  };
102
104
  }
103
- log(styleText('dim', 'Stack deployment progress:'));
104
- log('');
105
+ logger(styleText('dim', 'Stack deployment progress:'));
106
+ logger('');
105
107
  const outcome = await waitForOperation({
106
108
  stackId,
107
109
  operationId: stack.operationId,
108
110
  auth,
109
- log,
111
+ log: logger,
110
112
  bin,
111
113
  verbose,
112
114
  progressNoun: 'deployment',
113
115
  });
114
116
  if (outcome.type === 'completed') {
115
- log(styleText(['bold', 'green'], 'Stack deployment completed!'));
117
+ logger(styleText(['bold', 'green'], 'Stack deployment completed!'));
116
118
  const legacyWarning = await legacyPermissionsNoticePromise;
117
119
  const warnings = [...(legacyWarning ? [legacyWarning] : []), ...workspaceWarnings];
118
120
  return {
@@ -151,6 +153,56 @@ export async function blueprintDeployCore(options) {
151
153
  return { success: false, error: errorMessage };
152
154
  }
153
155
  }
156
+ function createAssetEventHandler(logger) {
157
+ let spinner;
158
+ const results = [];
159
+ return (event) => {
160
+ switch (event.type) {
161
+ case 'progress': {
162
+ const { completed, total, slow } = event.progress;
163
+ const text = [
164
+ `Processing assets (${completed}/${total})`,
165
+ slow ? ', this can take a moment' : '',
166
+ ].join('');
167
+ if (spinner)
168
+ spinner.text = text;
169
+ else
170
+ spinner = logger.ora(text).start();
171
+ return;
172
+ }
173
+ case 'result': {
174
+ results.push(event.result);
175
+ return;
176
+ }
177
+ case 'complete': {
178
+ spinner?.stop();
179
+ spinner = undefined;
180
+ break;
181
+ }
182
+ }
183
+ // done. print report
184
+ for (const result of results) {
185
+ const { resource } = result;
186
+ if (!result.success) {
187
+ const summary = result.failureKind === 'upload'
188
+ ? `Failed uploading ${resource.name} asset`
189
+ : `Failed processing ${resource.name} asset`;
190
+ logger(` ${severe(summary)}`);
191
+ logger.error(` ${result.error}`);
192
+ continue;
193
+ }
194
+ logger(` ${check(`${resource.name} ${niceId(resource.src)}`)}`);
195
+ logger(` Source: ${resource.src}`);
196
+ if (result.hash) {
197
+ const hash = result.hash;
198
+ const h = hash.length > 24 ? `${hash.slice(0, 8)}...${hash.slice(-12)}` : hash;
199
+ logger.verbose(` Hash: ${h}`);
200
+ }
201
+ if (result.exists)
202
+ logger.verbose(' Asset unchanged');
203
+ }
204
+ };
205
+ }
154
206
  const CHECK_LEGACY_DEPLOY_PERMISSION = false;
155
207
  const BLUEPRINTS_PERMISSION_ENFORCEMENT_DATE = 'July 1, 2026';
156
208
  async function checkLegacyDeployPermission({ auth, scopeType, scopeId, log, }) {
@@ -1,5 +1,6 @@
1
1
  import { dirname } from 'node:path';
2
2
  import { planStack, resolveStackIdByNameOrId } from '../../actions/blueprints/stacks.js';
3
+ import { extractDurableConfig } from '../../utils/blueprints/extract-durable-config.js';
3
4
  import { createHintCollector } from '../../utils/blueprints/hints.js';
4
5
  import { formatDeploymentPlan, formatResourceTree, hasActionableChanges, } from '../../utils/display/blueprints-formatting.js';
5
6
  import { styleText } from '../../utils/style-text.js';
@@ -9,6 +10,7 @@ export async function blueprintPlanCore(options) {
9
10
  const { verbose: _verbose = false } = flags;
10
11
  const { scopeType, scopeId, stackId: blueprintStackId, parsedBlueprint, resources, fileInfo, } = blueprint;
11
12
  log(`${styleText(['bold', 'blueBright'], 'Local Blueprint')} ${styleText('dim', `(${fileInfo.fileName})`)}`);
13
+ await extractDurableConfig(resources, dirname(fileInfo.blueprintFilePath));
12
14
  log(formatResourceTree(resources));
13
15
  if (!token || !scopeType || !scopeId) {
14
16
  log(styleText('dim', 'Unable to retrieve live Stack deployment for comparison'));
@@ -5,7 +5,7 @@ import { checkbox, confirm, input, select } from '@inquirer/prompts';
5
5
  import { highlight } from 'cardinal';
6
6
  import { createFunctionResource } from '../../actions/blueprints/resources.js';
7
7
  import { verifyExampleExists, writeExample } from '../../actions/sanity/examples.js';
8
- import { EVENT_DOCUMENT_CREATE, EVENT_DOCUMENT_DELETE, EVENT_DOCUMENT_UPDATE, EVENT_MEDIA_LIBRARY_ASSET_CREATE, EVENT_MEDIA_LIBRARY_ASSET_DELETE, EVENT_MEDIA_LIBRARY_ASSET_UPDATE, EVENT_SCHEDULED, EVENT_SYNC_TAG_INVALIDATE, FUNCTION_TYPES, LABEL_DOCUMENT_CREATE, LABEL_DOCUMENT_DELETE, LABEL_DOCUMENT_UPDATE, LABEL_MEDIA_LIBRARY_ASSET_CREATE, LABEL_MEDIA_LIBRARY_ASSET_DELETE, LABEL_MEDIA_LIBRARY_ASSET_UPDATE, LABEL_SCHEDULED, LABEL_SYNC_TAG_INVALIDATE, MAP_EVENT_TO_FUNCTION_TYPE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, } from '../../constants.js';
8
+ import { EVENT_DOCUMENT_CREATE, EVENT_DOCUMENT_DELETE, EVENT_DOCUMENT_UPDATE, EVENT_MEDIA_LIBRARY_ASSET_CREATE, EVENT_MEDIA_LIBRARY_ASSET_DELETE, EVENT_MEDIA_LIBRARY_ASSET_UPDATE, EVENT_PUBSUB, EVENT_SCHEDULED, EVENT_SYNC_TAG_INVALIDATE, FUNCTION_TYPES, LABEL_DOCUMENT_CREATE, LABEL_DOCUMENT_DELETE, LABEL_DOCUMENT_UPDATE, LABEL_MEDIA_LIBRARY_ASSET_CREATE, LABEL_MEDIA_LIBRARY_ASSET_DELETE, LABEL_MEDIA_LIBRARY_ASSET_UPDATE, LABEL_PUBSUB, LABEL_SCHEDULED, LABEL_SYNC_TAG_INVALIDATE, MAP_EVENT_TO_FUNCTION_TYPE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_PUBSUB, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, } from '../../constants.js';
9
9
  import { check, indent, warn } from '../../utils/display/presenters.js';
10
10
  import { validateFunctionName } from '../../utils/functions/validate-name.js';
11
11
  import { styleText } from '../../utils/style-text.js';
@@ -21,6 +21,9 @@ export const generateFunctionBlueprintResourceTemplate = (fnName, eventNames) =>
21
21
  case SANITY_FUNCTION_MEDIA_LIBRARY_ASSET:
22
22
  definer = `defineMediaLibraryAssetFunction({name: '${fnName}', event: {on: [${eventOns.join(', ')}], resource: {type: 'media-library', id: 'my-media-library-id'}}}), // ← add this line`;
23
23
  break;
24
+ case SANITY_FUNCTION_PUBSUB:
25
+ definer = `definePubSubFunction({name: '${fnName}'}), // ← add this line`;
26
+ break;
24
27
  case SANITY_FUNCTION_SCHEDULED:
25
28
  definer = `defineScheduledFunction({name: '${fnName}', event: {expression: '0 0 * * *'}}), // ← add this line`;
26
29
  break;
@@ -158,7 +161,7 @@ export async function functionAddCore(options) {
158
161
  }
159
162
  const eventSources = new Set(fnTypes.map((t) => t.substring(0, t.lastIndexOf('-'))));
160
163
  if (eventSources.size > 1) {
161
- throw new Error('Invalid function type. Cannot mix document-*, media-library-asset-*, sync-tag-* and/or scheduled-* types.');
164
+ throw new Error('Invalid function type. Cannot mix document-*, media-library-asset-*, pub-sub, sync-tag-* and/or scheduled-* types.');
162
165
  }
163
166
  let addHelpers;
164
167
  let installCommand;
@@ -224,7 +227,7 @@ async function promptForFunctionName() {
224
227
  const functionName = await input({
225
228
  message: 'Enter function name:',
226
229
  validate: (input) => validateFunctionName(input) ||
227
- 'Invalid function name. Must be 6+ characters, no special characters, no spaces',
230
+ 'Invalid function name. Must be 3+ characters, no special characters, no spaces',
228
231
  });
229
232
  return functionName;
230
233
  }
@@ -242,6 +245,7 @@ async function promptForFunctionType() {
242
245
  { name: LABEL_MEDIA_LIBRARY_ASSET_CREATE, value: EVENT_MEDIA_LIBRARY_ASSET_CREATE },
243
246
  { name: LABEL_MEDIA_LIBRARY_ASSET_UPDATE, value: EVENT_MEDIA_LIBRARY_ASSET_UPDATE },
244
247
  { name: LABEL_MEDIA_LIBRARY_ASSET_DELETE, value: EVENT_MEDIA_LIBRARY_ASSET_DELETE },
248
+ { name: LABEL_PUBSUB, value: EVENT_PUBSUB },
245
249
  { name: LABEL_SCHEDULED, value: EVENT_SCHEDULED },
246
250
  { name: LABEL_SYNC_TAG_INVALIDATE, value: EVENT_SYNC_TAG_INVALIDATE },
247
251
  ],
@@ -249,11 +253,10 @@ async function promptForFunctionType() {
249
253
  if (choices.length === 0) {
250
254
  return 'You must choose at least one function type / document change event';
251
255
  }
252
- const hasMedia = choices.some((c) => String(c.value).startsWith('media-library'));
253
- const hasDocument = choices.some((c) => String(c.value).startsWith('document'));
254
- const hasSchedule = choices.some((c) => String(c.value).startsWith('scheduled'));
255
- if ((hasMedia && hasDocument) || (hasMedia && hasSchedule) || (hasDocument && hasSchedule)) {
256
- return 'You cannot mix Document, Media Library Asset, and Scheduled events together in one Function';
256
+ // same grouping as the check in functionAddCore, so the prompt cannot drift from it
257
+ const eventSources = new Set(choices.map((c) => String(c.value).substring(0, String(c.value).lastIndexOf('-'))));
258
+ if (eventSources.size > 1) {
259
+ return 'You cannot mix Document, Media Library Asset, PubSub, Scheduled, and Sync Tag Invalidate events together in one Function';
257
260
  }
258
261
  return true;
259
262
  },
@@ -9,6 +9,7 @@ export class ApiBaseElement extends HTMLElement {
9
9
  this.api = api
10
10
  this.SANITY_FUNCTION_DOCUMENT = 'sanity.function.document'
11
11
  this.SANITY_FUNCTION_MEDIA_LIBRARY_ASSET = 'sanity.function.media-library.asset'
12
+ this.SANITY_FUNCTION_PUBSUB = 'sanity.function.pubsub'
12
13
  this.SANITY_FUNCTION_SCHEDULED = 'sanity.function.cron'
13
14
  this.SANITY_FUNCTION_SYNC_TAG_INVALIDATE = 'sanity.function.sync-tag-invalidate'
14
15
  }
@@ -79,6 +79,7 @@ class FiltersComponent extends ApiBaseElement {
79
79
  const mediaFunction = this.api.store.selectedFunctionType?.startsWith(
80
80
  this.SANITY_FUNCTION_MEDIA_LIBRARY_ASSET,
81
81
  )
82
+ const pubsubFunction = this.api.store.selectedFunctionType === this.SANITY_FUNCTION_PUBSUB
82
83
  const scheduleFunction = this.api.store.selectedFunctionType === this.SANITY_FUNCTION_SCHEDULED
83
84
  const stiFunction =
84
85
  this.api.store.selectedFunctionType === this.SANITY_FUNCTION_SYNC_TAG_INVALIDATE
@@ -89,15 +90,16 @@ class FiltersComponent extends ApiBaseElement {
89
90
  mediaFunction,
90
91
  scheduleFunction,
91
92
  stiFunction,
93
+ pubsubFunction,
92
94
  )
93
95
  }
94
96
 
95
- buildFilters = (docFunction, mediaFunction, scheduleFunction, stiFunction) => {
97
+ buildFilters = (docFunction, mediaFunction, scheduleFunction, stiFunction, pubsubFunction) => {
96
98
  return `
97
99
  <legend class="config-label">Client Options</legend>
98
100
  <div id="dynamic-dropdowns" class="flex gap-2">
99
101
  ${
100
- docFunction || scheduleFunction || stiFunction
102
+ docFunction || scheduleFunction || stiFunction || pubsubFunction
101
103
  ? `<select-dropdown
102
104
  label="Project"
103
105
  store-key="projects"
@@ -164,6 +164,9 @@ class PayloadPanel extends ApiBaseElement {
164
164
  const payloadTitle = this.shadowRoot.querySelector('#payloadTitle')
165
165
  let title = 'Document'
166
166
  switch (selectedFunctionType) {
167
+ case this.SANITY_FUNCTION_PUBSUB:
168
+ title = 'PubSub Payload'
169
+ break
167
170
  case this.SANITY_FUNCTION_SYNC_TAG_INVALIDATE:
168
171
  title = 'Sync Tag Payload'
169
172
  break
@@ -0,0 +1,8 @@
1
+ import { type BlueprintResource } from '@sanity/blueprints';
2
+ /**
3
+ * Extracts the blueprint config from the function source code and mutates it to the resource object.
4
+ * This is used for durable functions that may not have a config defined in the blueprint.
5
+ * @param resources
6
+ * @param blueprintFileDir
7
+ */
8
+ export declare const extractDurableConfig: (resources: BlueprintResource[], blueprintFileDir: string) => Promise<void>;
@@ -0,0 +1,60 @@
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,5 +1,5 @@
1
- import { validateCorsOrigin, validateDataset, validateDocumentFunction, validateDocumentWebhook, validateMediaLibraryAssetFunction, validateProject, validateRole, validateScheduledFunction, } from '@sanity/blueprints';
2
- import { SANITY_ACCESS_ROLE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_SCHEDULED, SANITY_PROJECT, SANITY_PROJECT_CORS, SANITY_PROJECT_DATASET, SANITY_PROJECT_WEBHOOK, } from '../../constants.js';
1
+ import { validateCorsOrigin, validateDataset, validateDocumentFunction, validateDocumentWebhook, validateDurableFunction, validateMediaLibraryAssetFunction, validateProject, validateRole, validateScheduledFunction, } from '@sanity/blueprints';
2
+ import { SANITY_ACCESS_ROLE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_DURABLE, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_SCHEDULED, SANITY_PROJECT, SANITY_PROJECT_CORS, SANITY_PROJECT_DATASET, SANITY_PROJECT_WEBHOOK, } from '../../constants.js';
3
3
  const RESOURCE_VALIDATORS = {
4
4
  [SANITY_ACCESS_ROLE]: {
5
5
  validate: validateRole,
@@ -25,6 +25,9 @@ const RESOURCE_VALIDATORS = {
25
25
  [SANITY_PROJECT_WEBHOOK]: {
26
26
  validate: validateDocumentWebhook,
27
27
  },
28
+ [SANITY_FUNCTION_DURABLE]: {
29
+ validate: validateDurableFunction,
30
+ },
28
31
  };
29
32
  export function validateResources(resources) {
30
33
  const errors = [];
@@ -1,5 +1,5 @@
1
1
  import { treeify } from 'array-treeify';
2
- import { SANITY_ACCESS_ROBOT, SANITY_ACCESS_ROLE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, SANITY_PROJECT, SANITY_PROJECT_CORS, SANITY_PROJECT_DATASET, SANITY_PROJECT_WEBHOOK, } from '../../constants.js';
2
+ 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, } from '../../constants.js';
3
3
  import { styleText } from '../style-text.js';
4
4
  import { isCorsOriginResource, isDatasetResource, isRobotResource, isRoleResource, isWebhookResource, } from '../types.js';
5
5
  import { formatDateTime, formatDuration } from './dates.js';
@@ -27,8 +27,10 @@ const RESOURCE_CATEGORIES = {
27
27
  },
28
28
  [SANITY_FUNCTION_DOCUMENT]: functionCategory,
29
29
  [SANITY_FUNCTION_MEDIA_LIBRARY_ASSET]: functionCategory,
30
+ [SANITY_FUNCTION_PUBSUB]: functionCategory,
30
31
  [SANITY_FUNCTION_SCHEDULED]: functionCategory,
31
32
  [SANITY_FUNCTION_SYNC_TAG_INVALIDATE]: functionCategory,
33
+ [SANITY_FUNCTION_DURABLE]: functionCategory,
32
34
  [SANITY_PROJECT]: {
33
35
  label: 'Projects',
34
36
  displayNameAttribute: 'displayName',
@@ -72,15 +74,15 @@ function resourceName(res, displayNameAttribute) {
72
74
  nameParts.push(styleText('green', `"${displayName}"`));
73
75
  return nameParts.join(' ');
74
76
  }
75
- function deployedResourceName(res, displayNameAttribute, _verbose = false) {
77
+ function deployedResourceName(res, displayNameAttribute, verbose = false) {
76
78
  const nameParts = [styleText(['bold', 'green'], res.name)];
77
79
  const displayName = displayNameAttribute &&
78
80
  typeof res.parameters[displayNameAttribute] === 'string' &&
79
81
  res.parameters[displayNameAttribute];
80
82
  if (displayName && displayName !== res.name)
81
83
  nameParts.push(styleText('green', `"${displayName}"`));
82
- // if (verbose)
83
- nameParts.push(idList(res));
84
+ if (verbose)
85
+ nameParts.push(idList(res));
84
86
  return nameParts.join(' ');
85
87
  }
86
88
  function idList(res) {
@@ -215,11 +217,13 @@ export function formatStacksListing(stacks, currentStackId) {
215
217
  const IGNORED_PARAMS = {
216
218
  [SANITY_FUNCTION_DOCUMENT]: new Set(['src']),
217
219
  [SANITY_FUNCTION_MEDIA_LIBRARY_ASSET]: new Set(['src']),
220
+ [SANITY_FUNCTION_PUBSUB]: new Set(['src']),
218
221
  [SANITY_FUNCTION_SCHEDULED]: new Set(['src']),
219
222
  };
220
223
  const ASSET_RESOURCE_TYPES = new Set([
221
224
  SANITY_FUNCTION_DOCUMENT,
222
225
  SANITY_FUNCTION_MEDIA_LIBRARY_ASSET,
226
+ SANITY_FUNCTION_PUBSUB,
223
227
  SANITY_FUNCTION_SCHEDULED,
224
228
  ]);
225
229
  function stringifyUnknown(val) {
@@ -1,4 +1,4 @@
1
- import { isLocalFunctionResource, isScheduleEvent } from '../../utils/types.js';
1
+ import { hasEventSource, isLocalFunctionResource, isScheduleEvent, } from '../../utils/types.js';
2
2
  import { styleText } from '../style-text.js';
3
3
  function formatLabel(label) {
4
4
  return styleText('dim', `${label}:`);
@@ -46,7 +46,7 @@ export function arrayifyFunction(fn) {
46
46
  details.push(formatLabeledValue('memory', fn.memory));
47
47
  if (fn.timeout)
48
48
  details.push(formatLabeledValue('timeout', fn.timeout));
49
- if (fn.event) {
49
+ if (hasEventSource(fn) && fn.event) {
50
50
  const eventDetails = arrayifyEvent(fn.event);
51
51
  if (eventDetails) {
52
52
  details.push(formatLabel('event'));
@@ -1,3 +1,4 @@
1
+ // TODO: move to ./display
1
2
  import { env } from 'node:process';
2
3
  import { styleText as nodeStyleText } from 'node:util';
3
4
  import { isInteractive } from './interactive.js';