@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.
- package/README.md +55 -52
- package/SKILL.md +59 -0
- package/dist/actions/blueprinit/manifest.d.ts +15 -0
- package/dist/actions/blueprinit/manifest.js +82 -0
- package/dist/actions/blueprinit/studio.d.ts +6 -0
- package/dist/actions/blueprinit/studio.js +43 -0
- package/dist/actions/blueprinit/templates.d.ts +34 -0
- package/dist/actions/blueprinit/templates.js +83 -0
- package/dist/actions/blueprinit/workspace.d.ts +16 -0
- package/dist/actions/blueprinit/workspace.js +143 -0
- package/dist/actions/blueprints/blueprint.js +2 -1
- package/dist/actions/blueprints/pre-deploy.d.ts +15 -6
- package/dist/actions/blueprints/pre-deploy.js +29 -70
- package/dist/actions/blueprints/process-assets.d.ts +29 -0
- package/dist/actions/blueprints/process-assets.js +80 -0
- package/dist/actions/blueprints/resources.js +17 -1
- package/dist/actions/sanity/datasets.d.ts +15 -0
- package/dist/actions/sanity/datasets.js +26 -0
- package/dist/commands/blueprints/deploy.js +3 -1
- package/dist/commands/blueprints/destroy.js +1 -1
- package/dist/commands/blueprints/info.d.ts +1 -0
- package/dist/commands/blueprints/info.js +5 -0
- package/dist/commands/blueprints/init.js +1 -2
- package/dist/commands/blueprints/logs.js +1 -0
- package/dist/commands/functions/add.js +3 -3
- package/dist/commands/functions/dev.js +2 -2
- package/dist/commands/functions/logs.d.ts +1 -1
- package/dist/commands/functions/logs.js +3 -0
- package/dist/commands/init.d.ts +17 -0
- package/dist/commands/init.js +59 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +6 -0
- package/dist/cores/blueprinit/adopt.d.ts +6 -0
- package/dist/cores/blueprinit/adopt.js +119 -0
- package/dist/cores/blueprinit/finish.d.ts +14 -0
- package/dist/cores/blueprinit/finish.js +64 -0
- package/dist/cores/blueprinit/fresh.d.ts +3 -0
- package/dist/cores/blueprinit/fresh.js +120 -0
- package/dist/cores/blueprinit/preview.d.ts +15 -0
- package/dist/cores/blueprinit/preview.js +14 -0
- package/dist/cores/blueprinit/prompts.d.ts +5 -0
- package/dist/cores/blueprinit/prompts.js +22 -0
- package/dist/cores/blueprinit/selection.d.ts +15 -0
- package/dist/cores/blueprinit/selection.js +90 -0
- package/dist/cores/blueprinit/types.d.ts +16 -0
- package/dist/cores/blueprinit/types.js +1 -0
- package/dist/cores/blueprinit.d.ts +7 -0
- package/dist/cores/blueprinit.js +48 -0
- package/dist/cores/blueprints/deploy.js +71 -19
- package/dist/cores/blueprints/plan.js +2 -0
- package/dist/cores/functions/add.js +11 -8
- package/dist/server/static/components/api-base.js +1 -0
- package/dist/server/static/components/filters.js +4 -2
- package/dist/server/static/components/payload-panel.js +3 -0
- package/dist/utils/blueprints/extract-durable-config.d.ts +8 -0
- package/dist/utils/blueprints/extract-durable-config.js +60 -0
- package/dist/utils/blueprints/validate-resources.js +5 -2
- package/dist/utils/display/blueprints-formatting.js +8 -4
- package/dist/utils/display/resources-formatting.js +2 -2
- package/dist/utils/style-text.js +1 -0
- package/dist/utils/types.d.ts +8 -2
- package/dist/utils/types.js +8 -2
- package/oclif.manifest.json +151 -15
- package/package.json +29 -15
- package/skills/_functions-build.md +23 -0
- package/skills/_functions-list.md +28 -0
- package/skills/blueprints-add.md +8 -0
- package/skills/blueprints-config.md +33 -0
- package/skills/blueprints-deploy.md +36 -0
- package/skills/blueprints-destroy.md +30 -0
- package/skills/blueprints-doctor.md +31 -0
- package/skills/blueprints-info.md +23 -0
- package/skills/blueprints-init.md +48 -0
- package/skills/blueprints-logs.md +33 -0
- package/skills/blueprints-mint-deploy-token.md +25 -0
- package/skills/blueprints-plan.md +28 -0
- package/skills/blueprints-promote.md +31 -0
- package/skills/blueprints-stacks.md +28 -0
- package/skills/functions-add.md +35 -0
- package/skills/functions-dev.md +21 -0
- package/skills/functions-env-add.md +29 -0
- package/skills/functions-env-list.md +21 -0
- package/skills/functions-env-remove.md +22 -0
- package/skills/functions-logs.md +39 -0
- package/skills/functions-test.md +31 -0
- 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
|
-
|
|
20
|
+
logger(`Deploying "${stackName}" ${niceId(deployedStack.id)}...`);
|
|
20
21
|
try {
|
|
21
|
-
const { ok: checkOk, stack: currentStack } = await getStack({ stackId, auth, logger
|
|
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
|
-
|
|
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
|
|
66
|
+
logger,
|
|
65
67
|
});
|
|
66
68
|
if (!deployOk) {
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
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,
|
|
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
|
-
|
|
83
|
-
|
|
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'));
|
package/dist/utils/style-text.js
CHANGED