@sanity/runtime-cli 17.7.1 → 17.8.0-blueprinit.g0993fa5
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 +26 -21
- package/dist/actions/blueprinit/manifest.d.ts +34 -0
- package/dist/actions/blueprinit/manifest.js +107 -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 +73 -0
- package/dist/actions/blueprinit/templates.js +138 -0
- package/dist/actions/blueprinit/workspace.d.ts +24 -0
- package/dist/actions/blueprinit/workspace.js +146 -0
- package/dist/actions/blueprints/stacks.d.ts +2 -0
- package/dist/actions/blueprints/stacks.js +1 -0
- package/dist/actions/sanity/datasets.d.ts +15 -0
- package/dist/actions/sanity/datasets.js +26 -0
- package/dist/actions/sanity/projects.d.ts +11 -0
- package/dist/actions/sanity/projects.js +48 -22
- package/dist/baseCommands.d.ts +6 -0
- package/dist/baseCommands.js +9 -1
- package/dist/commands/blueprints/stacks.d.ts +2 -1
- package/dist/commands/blueprints/stacks.js +14 -6
- package/dist/commands/init.d.ts +20 -0
- package/dist/commands/init.js +72 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +5 -0
- package/dist/cores/blueprinit/adopt.d.ts +6 -0
- package/dist/cores/blueprinit/adopt.js +154 -0
- package/dist/cores/blueprinit/finish.d.ts +15 -0
- package/dist/cores/blueprinit/finish.js +73 -0
- package/dist/cores/blueprinit/fresh.d.ts +3 -0
- package/dist/cores/blueprinit/fresh.js +173 -0
- package/dist/cores/blueprinit/preview.d.ts +24 -0
- package/dist/cores/blueprinit/preview.js +97 -0
- package/dist/cores/blueprinit/prompts.d.ts +15 -0
- package/dist/cores/blueprinit/prompts.js +43 -0
- package/dist/cores/blueprinit/selection.d.ts +15 -0
- package/dist/cores/blueprinit/selection.js +90 -0
- package/dist/cores/blueprinit/stack-name.d.ts +39 -0
- package/dist/cores/blueprinit/stack-name.js +56 -0
- package/dist/cores/blueprinit/target.d.ts +33 -0
- package/dist/cores/blueprinit/target.js +80 -0
- package/dist/cores/blueprinit/types.d.ts +18 -0
- package/dist/cores/blueprinit/types.js +1 -0
- package/dist/cores/blueprinit.d.ts +7 -0
- package/dist/cores/blueprinit.js +55 -0
- package/dist/cores/blueprints/stacks.d.ts +8 -3
- package/dist/cores/blueprints/stacks.js +282 -85
- package/dist/cores/functions/test.d.ts +2 -19
- package/dist/cores/functions/test.js +55 -111
- package/dist/server/app.js +4 -6
- package/dist/server/static/components/payload-panel.js +1 -1
- package/dist/utils/display/intro.d.ts +6 -0
- package/dist/utils/display/intro.js +63 -0
- package/dist/utils/display/prompt.d.ts +4 -1
- package/dist/utils/display/prompt.js +16 -2
- package/dist/utils/external/npmjs.d.ts +7 -0
- package/dist/utils/external/npmjs.js +10 -0
- package/dist/utils/functions/child-process-wrapper.js +18 -6
- package/dist/utils/functions/child-runtime/durable.d.ts +18 -0
- package/dist/utils/functions/child-runtime/durable.js +46 -0
- package/dist/utils/functions/invoke/local.js +26 -9
- package/dist/utils/functions/invoke/resolve-invoke-type.d.ts +51 -0
- package/dist/utils/functions/invoke/resolve-invoke-type.js +131 -0
- package/dist/utils/progress.js +6 -3
- package/dist/utils/types.d.ts +46 -2
- package/oclif.manifest.json +155 -5
- package/package.json +1 -1
- package/skills/blueprints-stacks.md +14 -2
- package/skills/init.md +170 -0
package/dist/server/app.js
CHANGED
|
@@ -28,7 +28,7 @@ const app = (host, port, validateResources, executionOptions) => {
|
|
|
28
28
|
*/
|
|
29
29
|
const nestedInvoke = async (name, payload, ctx, depth, callStack) => {
|
|
30
30
|
const order = ++nestedOrder;
|
|
31
|
-
const result = await handleInvokeRequest(name,
|
|
31
|
+
const result = await handleInvokeRequest(name, payload, { event: 'nested' }, ctx, validateResources, executionOptions, depth, callStack, nestedInvoke);
|
|
32
32
|
broadcast({
|
|
33
33
|
type: 'nested-invoke',
|
|
34
34
|
name,
|
|
@@ -372,13 +372,13 @@ function parseInvokeRequest(body, blueprintFileDir) {
|
|
|
372
372
|
}
|
|
373
373
|
const metadataEvent = metadata.event;
|
|
374
374
|
if (typeof metadataEvent !== 'string' ||
|
|
375
|
-
(!isEventType(metadataEvent) && metadataEvent !== '
|
|
375
|
+
(!isEventType(metadataEvent) && metadataEvent !== 'plain')) {
|
|
376
376
|
throw new Error('Request body is not valid, `metadata.event` field is not one of `create`, `update`, `delete`, or `scheduled`');
|
|
377
377
|
}
|
|
378
378
|
let before = null;
|
|
379
379
|
let after = null;
|
|
380
380
|
// Only GROQ-based events (create, update, delete) have before and after fields
|
|
381
|
-
if (metadataEvent !== '
|
|
381
|
+
if (metadataEvent !== 'plain') {
|
|
382
382
|
if (!('before' in metadata)) {
|
|
383
383
|
throw new Error('Request body is not valid, `metadata.before` field is missing');
|
|
384
384
|
}
|
|
@@ -418,9 +418,7 @@ function parseInvokeRequest(body, blueprintFileDir) {
|
|
|
418
418
|
},
|
|
419
419
|
event,
|
|
420
420
|
},
|
|
421
|
-
metadata: metadataEvent === '
|
|
422
|
-
? { event: metadataEvent }
|
|
423
|
-
: { event: metadataEvent, before, after },
|
|
421
|
+
metadata: metadataEvent === 'plain' ? { event: metadataEvent } : { event: metadataEvent, before, after },
|
|
424
422
|
};
|
|
425
423
|
}
|
|
426
424
|
export { app, buildApiUrl, parseDocumentUrl };
|
|
@@ -127,7 +127,7 @@ class PayloadPanel extends ApiBaseElement {
|
|
|
127
127
|
// for certain other specific function types, set the payload to something specific, or reload whatever was loaded into its document payload before
|
|
128
128
|
switch (selectedFunction.type) {
|
|
129
129
|
case this.SANITY_FUNCTION_SYNC_TAG_INVALIDATE:
|
|
130
|
-
this.#updateCodeView(this.api.store.payload, {
|
|
130
|
+
this.#updateCodeView(this.api.store.payload, {syncTags: ['s1:1234', 's2:5678']})
|
|
131
131
|
break
|
|
132
132
|
default:
|
|
133
133
|
this.#updateCodeView(this.api.store.payload, this.documentCacheByFunction[selectedIndex])
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** A left-to-right reveal of the wordmark; the last frame is the finished line. */
|
|
2
|
+
export declare function introFrames(): string[];
|
|
3
|
+
/** Print the intro, animated when the terminal can show it and static when it cannot. */
|
|
4
|
+
export declare function playIntro({ json }?: {
|
|
5
|
+
json?: boolean;
|
|
6
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { env, stdout } from 'node:process';
|
|
2
|
+
import { onExit } from 'signal-exit';
|
|
3
|
+
import { isInteractive } from '../interactive.js';
|
|
4
|
+
import { styleText } from '../style-text.js';
|
|
5
|
+
const INDENT = ' ';
|
|
6
|
+
const WORDMARK = 'sanity';
|
|
7
|
+
const TAIL = ' with blueprints';
|
|
8
|
+
/** The leading edge of the reveal, densest first. */
|
|
9
|
+
const GRADIENT = ['▓', '▒', '░'];
|
|
10
|
+
const REVEAL_PER_FRAME = 1;
|
|
11
|
+
const FRAME_MS = 45;
|
|
12
|
+
/** Time the finished wordmark holds before the command's own output follows. */
|
|
13
|
+
const HOLD_MS = 250;
|
|
14
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
15
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
16
|
+
const CLEAR_LINE = '\x1b[2K';
|
|
17
|
+
function style(revealed, edge) {
|
|
18
|
+
const wordmark = revealed.slice(0, WORDMARK.length);
|
|
19
|
+
const tail = revealed.slice(WORDMARK.length);
|
|
20
|
+
return INDENT + styleText(['bold', 'red'], wordmark) + styleText('dim', tail + edge);
|
|
21
|
+
}
|
|
22
|
+
/** A left-to-right reveal of the wordmark; the last frame is the finished line. */
|
|
23
|
+
export function introFrames() {
|
|
24
|
+
const full = WORDMARK + TAIL;
|
|
25
|
+
const frames = [];
|
|
26
|
+
for (let revealed = REVEAL_PER_FRAME; revealed < full.length; revealed += REVEAL_PER_FRAME) {
|
|
27
|
+
const edge = GRADIENT.slice(0, Math.min(GRADIENT.length, full.length - revealed)).join('');
|
|
28
|
+
frames.push(style(full.slice(0, revealed), edge));
|
|
29
|
+
}
|
|
30
|
+
frames.push(style(full, ''));
|
|
31
|
+
return frames;
|
|
32
|
+
}
|
|
33
|
+
function animates() {
|
|
34
|
+
return isInteractive && env.TERM !== 'dumb' && !env.CI;
|
|
35
|
+
}
|
|
36
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
|
+
/** Print the intro, animated when the terminal can show it and static when it cannot. */
|
|
38
|
+
export async function playIntro({ json = false } = {}) {
|
|
39
|
+
if (json)
|
|
40
|
+
return;
|
|
41
|
+
const frames = introFrames();
|
|
42
|
+
const finished = frames.at(-1) ?? '';
|
|
43
|
+
if (!animates()) {
|
|
44
|
+
stdout.write(`\n${finished}\n\n`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
stdout.write(`\n${HIDE_CURSOR}`);
|
|
48
|
+
const cancelExitHook = onExit(() => {
|
|
49
|
+
stdout.write(SHOW_CURSOR);
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
for (const frame of frames) {
|
|
53
|
+
stdout.write(`\r${CLEAR_LINE}${frame}`);
|
|
54
|
+
await sleep(FRAME_MS);
|
|
55
|
+
}
|
|
56
|
+
await sleep(HOLD_MS);
|
|
57
|
+
stdout.write('\n\n');
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
stdout.write(SHOW_CURSOR);
|
|
61
|
+
cancelExitHook();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -24,10 +24,13 @@ export declare function promptForOrganization({ token, knownOrganizationId, logg
|
|
|
24
24
|
/**
|
|
25
25
|
* Asks the user whether to scope a Blueprint to an organization or a project.
|
|
26
26
|
*/
|
|
27
|
-
export declare function promptForScope({ allowBack, hasProjects, }?: {
|
|
27
|
+
export declare function promptForScope({ allowBack, hasProjects, message, }?: {
|
|
28
28
|
allowBack?: boolean;
|
|
29
29
|
hasProjects?: boolean;
|
|
30
|
+
message?: string;
|
|
30
31
|
}): Promise<ScopeType | Back>;
|
|
32
|
+
/** Offers a recovery path after the selected scope cannot be read. */
|
|
33
|
+
export declare function promptForScopeRecovery(): Promise<'exit' | 'retry'>;
|
|
31
34
|
/**
|
|
32
35
|
* Prompts the user to pick a project from a pre-fetched list of projects.
|
|
33
36
|
* Use after promptForOrganization when the user chose project scope.
|
|
@@ -121,7 +121,7 @@ export async function promptForOrganization({ token, knownOrganizationId, logger
|
|
|
121
121
|
/**
|
|
122
122
|
* Asks the user whether to scope a Blueprint to an organization or a project.
|
|
123
123
|
*/
|
|
124
|
-
export async function promptForScope({ allowBack = false, hasProjects = true, } = {}) {
|
|
124
|
+
export async function promptForScope({ allowBack = false, hasProjects = true, message = 'How would you like to scope your Blueprint?', } = {}) {
|
|
125
125
|
const choices = [
|
|
126
126
|
{
|
|
127
127
|
name: 'Organization',
|
|
@@ -136,12 +136,26 @@ export async function promptForScope({ allowBack = false, hasProjects = true, }
|
|
|
136
136
|
},
|
|
137
137
|
];
|
|
138
138
|
const answer = await select({
|
|
139
|
-
message
|
|
139
|
+
message,
|
|
140
140
|
choices: withBackChoice(choices, allowBack),
|
|
141
141
|
default: 'organization',
|
|
142
142
|
});
|
|
143
143
|
return answer;
|
|
144
144
|
}
|
|
145
|
+
/** Offers a recovery path after the selected scope cannot be read. */
|
|
146
|
+
export async function promptForScopeRecovery() {
|
|
147
|
+
return await select({
|
|
148
|
+
message: 'What would you like to do?',
|
|
149
|
+
choices: [
|
|
150
|
+
{
|
|
151
|
+
name: 'Choose another organization or project',
|
|
152
|
+
value: 'retry',
|
|
153
|
+
},
|
|
154
|
+
{ name: 'Exit', value: 'exit' },
|
|
155
|
+
],
|
|
156
|
+
default: 'retry',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
145
159
|
/**
|
|
146
160
|
* Prompts the user to pick a project from a pre-fetched list of projects.
|
|
147
161
|
* Use after promptForOrganization when the user chose project scope.
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
import type { Logger } from '../logger.js';
|
|
2
2
|
export declare function getLatestNpmVersion(pkg: string, logger: Logger): Promise<string>;
|
|
3
|
+
/**
|
|
4
|
+
* The latest version as a caret range, the form `sanity init` writes.
|
|
5
|
+
*
|
|
6
|
+
* `getLatestNpmVersion` answers `latest` when the registry cannot be read, and `^latest`
|
|
7
|
+
* is not a range, so that answer passes through unchanged.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getLatestNpmRange(pkg: string, logger: Logger): Promise<string>;
|
|
@@ -16,3 +16,13 @@ export async function getLatestNpmVersion(pkg, logger) {
|
|
|
16
16
|
return 'latest';
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* The latest version as a caret range, the form `sanity init` writes.
|
|
21
|
+
*
|
|
22
|
+
* `getLatestNpmVersion` answers `latest` when the registry cannot be read, and `^latest`
|
|
23
|
+
* is not a range, so that answer passes through unchanged.
|
|
24
|
+
*/
|
|
25
|
+
export async function getLatestNpmRange(pkg, logger) {
|
|
26
|
+
const version = await getLatestNpmVersion(pkg, logger);
|
|
27
|
+
return version === 'latest' ? version : `^${version}`;
|
|
28
|
+
}
|
|
@@ -3,6 +3,7 @@ import {isAbsolute, join} from 'node:path'
|
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
import {pathToFileURL} from 'node:url'
|
|
5
5
|
import {installConsole} from './child-runtime/console.js'
|
|
6
|
+
import {runDurable} from './child-runtime/durable.js'
|
|
6
7
|
import {createInvoker} from './child-runtime/invoke.js'
|
|
7
8
|
import {createResourcesApi} from './child-runtime/resources.js'
|
|
8
9
|
|
|
@@ -40,7 +41,7 @@ process.on('message', async (data) => {
|
|
|
40
41
|
|
|
41
42
|
// Otherwise this is the initial start message. Support the legacy untyped
|
|
42
43
|
// `{srcPath, payload}` shape as well as the new `{type: 'start', ...}`.
|
|
43
|
-
const {srcPath, payload} = jsonData
|
|
44
|
+
const {srcPath, payload, isDurable} = jsonData
|
|
44
45
|
const {context, ...event} = payload
|
|
45
46
|
|
|
46
47
|
let logs = ''
|
|
@@ -78,11 +79,22 @@ process.on('message', async (data) => {
|
|
|
78
79
|
|
|
79
80
|
// Allow the handler to invoke other blueprint functions by name.
|
|
80
81
|
context.invoke = invoker.invoke
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
|
|
83
|
+
if (isDurable) {
|
|
84
|
+
json = await runDurable(
|
|
85
|
+
handler,
|
|
86
|
+
{context, event, logger: console},
|
|
87
|
+
{
|
|
88
|
+
emit: (e) => process.send(JSON.stringify({type: 'step-event', event: e})),
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
} else {
|
|
92
|
+
json = await handler({
|
|
93
|
+
context,
|
|
94
|
+
event,
|
|
95
|
+
done: async () => new Response(null, {status: 204}),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
86
98
|
|
|
87
99
|
// Restore streams
|
|
88
100
|
process.stdout.write = originalStdoutWrite
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type {DurableContext, DurableHandler, DurableLogger, GenericEvent} from '@sanity/functions'
|
|
2
|
+
import type {DurableStepEvent} from '../../types.js'
|
|
3
|
+
|
|
4
|
+
type RunDurableArgs = {
|
|
5
|
+
context: DurableContext
|
|
6
|
+
event: GenericEvent
|
|
7
|
+
logger: DurableLogger
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type DurableIO = {
|
|
11
|
+
emit: (event: DurableStepEvent) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function runDurable(
|
|
15
|
+
handler: DurableHandler,
|
|
16
|
+
args: RunDurableArgs,
|
|
17
|
+
io: DurableIO,
|
|
18
|
+
): Promise<unknown>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {performance} from 'node:perf_hooks'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs a durable function handler
|
|
5
|
+
* @param handler - callback being run
|
|
6
|
+
* @param context - data to send to the operation
|
|
7
|
+
* @param event - the step being run
|
|
8
|
+
* @param logger
|
|
9
|
+
* @param io
|
|
10
|
+
*/
|
|
11
|
+
export async function runDurable(handler, {context, event, logger}, io) {
|
|
12
|
+
const track = async (name, kind, produce) => {
|
|
13
|
+
io.emit({name, kind, phase: 'start'})
|
|
14
|
+
const startedAt = performance.now()
|
|
15
|
+
try {
|
|
16
|
+
const value = await produce()
|
|
17
|
+
io.emit({
|
|
18
|
+
name,
|
|
19
|
+
kind,
|
|
20
|
+
phase: 'complete',
|
|
21
|
+
output: value,
|
|
22
|
+
durationMs: performance.now() - startedAt,
|
|
23
|
+
})
|
|
24
|
+
return value
|
|
25
|
+
} catch (err) {
|
|
26
|
+
io.emit({
|
|
27
|
+
name,
|
|
28
|
+
kind,
|
|
29
|
+
phase: 'error',
|
|
30
|
+
error: {
|
|
31
|
+
name: err?.name ?? 'Error',
|
|
32
|
+
message: err?.message ?? String(err),
|
|
33
|
+
stack: err?.stack ?? '',
|
|
34
|
+
},
|
|
35
|
+
durationMs: performance.now() - startedAt,
|
|
36
|
+
})
|
|
37
|
+
throw err
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const step = {
|
|
42
|
+
run: ({name, handler: fn}) => track(name || 'step-run', 'run', () => fn({logger, attempt: 1})),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return handler({context, event, step, logger})
|
|
46
|
+
}
|
|
@@ -5,9 +5,11 @@ import { setTimeout } from 'node:timers';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import * as groq from 'groq-js';
|
|
7
7
|
import config from '../../../config.js';
|
|
8
|
+
import { SANITY_FUNCTION_DURABLE } from '../../../constants.js';
|
|
8
9
|
import { groupBlueprintResources } from '../../blueprints/group-resources.js';
|
|
9
10
|
import { isGroqContextOptions, } from '../../types.js';
|
|
10
11
|
import { acquireFunctionBuild } from './prepare.js';
|
|
12
|
+
import { isGroqInvocation } from './resolve-invoke-type.js';
|
|
11
13
|
function getChildProcessWrapperPath() {
|
|
12
14
|
return fileURLToPath(new URL('../child-process-wrapper.js', import.meta.url));
|
|
13
15
|
}
|
|
@@ -16,13 +18,16 @@ export function sanitizeLogs(logs) {
|
|
|
16
18
|
}
|
|
17
19
|
export async function applyGroqRule(resource, payload, projectId, dataset) {
|
|
18
20
|
const { before, after, payload: data = null } = payload;
|
|
21
|
+
const event = resource.event;
|
|
19
22
|
// If there is no rule set return everything
|
|
20
|
-
if (!
|
|
23
|
+
if (!event)
|
|
21
24
|
return data;
|
|
22
25
|
// default groq rule is: gimme full doc content. otherwise, parse + eval custom rule
|
|
23
26
|
// applying the GROQ filter may result in a slimmer set of documents
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
const filter = 'filter' in event ? event.filter : undefined;
|
|
28
|
+
const projection = 'projection' in event ? event.projection : undefined;
|
|
29
|
+
if (filter || projection) {
|
|
30
|
+
const query = `*[${filter || ''}]${projection || ''}`;
|
|
26
31
|
try {
|
|
27
32
|
const rule = groq.parse(query, { mode: 'delta' });
|
|
28
33
|
const sanity = projectId && dataset ? { projectId, dataset } : undefined;
|
|
@@ -42,21 +47,24 @@ export default async function invoke(resource, payload, context, options) {
|
|
|
42
47
|
if (!resource.src) {
|
|
43
48
|
throw new Error(`Function resource "${resource.name}" is missing the 'src' property.`);
|
|
44
49
|
}
|
|
45
|
-
const { forceColor = true, timeout = 10, onNestedInvoke, depth = 0, callStack = [] } = options;
|
|
50
|
+
const { forceColor = true, timeout = 10, onNestedInvoke, depth = 0, callStack = [], onStepEvent = () => { }, } = options;
|
|
51
|
+
// `payload.payload` is the event body for every function type: the document for
|
|
52
|
+
// GROQ-based events, whatever the producer sent for the rest. The handler always
|
|
53
|
+
// receives it as `event.data` (see the `payload` built in `start()` below).
|
|
54
|
+
let filteredData = payload?.payload || {};
|
|
46
55
|
// Apply the GROQ rule before preparing a build so a filtered-out event skips
|
|
47
56
|
// the (potentially expensive) transpile entirely.
|
|
48
|
-
|
|
49
|
-
if (resource.type === 'sanity.function.document' ||
|
|
50
|
-
resource.type === 'sanity.function.media-library.asset') {
|
|
57
|
+
if (isGroqInvocation(resource)) {
|
|
51
58
|
if (!isGroqContextOptions(context)) {
|
|
52
59
|
throw new Error('GROQ-based functions require a context with clientOptions');
|
|
53
60
|
}
|
|
54
61
|
const { projectId, dataset } = context.clientOptions;
|
|
55
62
|
if ('before' in payload) {
|
|
56
63
|
filteredData = await applyGroqRule(resource, payload, projectId, dataset);
|
|
64
|
+
const filter = resource.event && 'filter' in resource.event ? resource.event.filter : '';
|
|
57
65
|
if (typeof filteredData === 'undefined') {
|
|
58
66
|
return {
|
|
59
|
-
logs: `⚠️ Filter "${
|
|
67
|
+
logs: `⚠️ Filter "${filter}" returned an empty result. Skipping invoke.`,
|
|
60
68
|
error: undefined,
|
|
61
69
|
json: undefined,
|
|
62
70
|
};
|
|
@@ -208,6 +216,10 @@ export default async function invoke(resource, payload, context, options) {
|
|
|
208
216
|
catch {
|
|
209
217
|
return;
|
|
210
218
|
}
|
|
219
|
+
if (msg?.type === 'step-event') {
|
|
220
|
+
onStepEvent(msg.event);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
211
223
|
// Nested invoke request — handle without tearing down the child.
|
|
212
224
|
if (msg?.type === 'invoke-request') {
|
|
213
225
|
handleNestedInvoke({ id: msg.id, name: msg.name, payload: msg.payload });
|
|
@@ -289,7 +301,12 @@ export default async function invoke(resource, payload, context, options) {
|
|
|
289
301
|
}),
|
|
290
302
|
},
|
|
291
303
|
};
|
|
292
|
-
const startMessage = {
|
|
304
|
+
const startMessage = {
|
|
305
|
+
type: 'start',
|
|
306
|
+
srcPath: functionPath,
|
|
307
|
+
payload,
|
|
308
|
+
isDurable: resource.type === SANITY_FUNCTION_DURABLE,
|
|
309
|
+
};
|
|
293
310
|
child.send(JSON.stringify(startMessage));
|
|
294
311
|
}
|
|
295
312
|
function shutdown() {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { BlueprintResource } from '@sanity/blueprints';
|
|
2
|
+
import type { Logger } from '../../logger.js';
|
|
3
|
+
import type { EventType, FunctionGroqResource, FunctionResource, FunctionTestFlags, InvokeContextOptions, InvokePayloadOptions } from '../../types.js';
|
|
4
|
+
import buildPayload from '../build-payload.js';
|
|
5
|
+
import { fetchAsset, fetchDocument } from '../fetch-document.js';
|
|
6
|
+
export type InvocationPlan = {
|
|
7
|
+
kind: 'groq';
|
|
8
|
+
inputSource: 'document' | 'media';
|
|
9
|
+
isDurable: boolean;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'plain';
|
|
12
|
+
isDurable: false;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'durable';
|
|
15
|
+
inputSource: 'payload';
|
|
16
|
+
isDurable: true;
|
|
17
|
+
};
|
|
18
|
+
export type InputContext = {
|
|
19
|
+
flags: FunctionTestFlags;
|
|
20
|
+
projectId?: string;
|
|
21
|
+
organizationId?: string;
|
|
22
|
+
api?: string;
|
|
23
|
+
apiHost: string;
|
|
24
|
+
token?: string;
|
|
25
|
+
withUserToken?: boolean;
|
|
26
|
+
resources: BlueprintResource[];
|
|
27
|
+
blueprintFileDir: string;
|
|
28
|
+
log: Logger;
|
|
29
|
+
};
|
|
30
|
+
type InvocationDeps = {
|
|
31
|
+
fetchDocument: typeof fetchDocument;
|
|
32
|
+
fetchAsset: typeof fetchAsset;
|
|
33
|
+
buildPayload: typeof buildPayload;
|
|
34
|
+
};
|
|
35
|
+
export declare const defaultInvocationDeps: InvocationDeps;
|
|
36
|
+
export interface InvokeInput {
|
|
37
|
+
payload: Record<string, unknown>;
|
|
38
|
+
before: Record<string, unknown> | null;
|
|
39
|
+
after: Record<string, unknown> | null;
|
|
40
|
+
}
|
|
41
|
+
export declare const resolveInvocationPlan: (resource: FunctionResource) => InvocationPlan;
|
|
42
|
+
/**
|
|
43
|
+
* True when the function's event is delivered through a GROQ rule (a document or
|
|
44
|
+
* media-library event, whether plain or durable), so its payload is a document the
|
|
45
|
+
* rule may narrow before the handler sees it.
|
|
46
|
+
*/
|
|
47
|
+
export declare const isGroqInvocation: (resource: FunctionResource) => resource is FunctionGroqResource;
|
|
48
|
+
export declare const collectInvokeInput: (plan: InvocationPlan, input: InputContext, deps: InvocationDeps) => Promise<InvokeInput>;
|
|
49
|
+
export declare const buildContext: (plan: InvocationPlan, input: InputContext) => InvokeContextOptions;
|
|
50
|
+
export declare const buildInvokeOptions: (plan: InvocationPlan, invokeInput: InvokeInput, eventType: EventType) => InvokePayloadOptions;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { cwd } from 'node:process';
|
|
4
|
+
import { SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_DURABLE, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_PUBSUB, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, } from '../../../constants.js';
|
|
5
|
+
import { parseJsonObject } from '../../parse-json-object.js';
|
|
6
|
+
import buildPayload from '../build-payload.js';
|
|
7
|
+
import { fetchAsset, fetchDocument } from '../fetch-document.js';
|
|
8
|
+
export const defaultInvocationDeps = { fetchDocument, fetchAsset, buildPayload };
|
|
9
|
+
const assertNever = (resource) => {
|
|
10
|
+
throw new Error(`No invocation plan for function type: ${resource.type}`);
|
|
11
|
+
};
|
|
12
|
+
const resolveDurableInvocationPlan = (event) => {
|
|
13
|
+
switch (event?.type) {
|
|
14
|
+
case 'document':
|
|
15
|
+
return { kind: 'groq', inputSource: 'document', isDurable: true };
|
|
16
|
+
case 'media-library':
|
|
17
|
+
return { kind: 'groq', inputSource: 'media', isDurable: true };
|
|
18
|
+
case 'sync-tag-invalidate':
|
|
19
|
+
case undefined:
|
|
20
|
+
return { kind: 'durable', inputSource: 'payload', isDurable: true };
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export const resolveInvocationPlan = (resource) => {
|
|
24
|
+
switch (resource.type) {
|
|
25
|
+
case SANITY_FUNCTION_DOCUMENT:
|
|
26
|
+
return { kind: 'groq', inputSource: 'document', isDurable: false };
|
|
27
|
+
case SANITY_FUNCTION_MEDIA_LIBRARY_ASSET:
|
|
28
|
+
return { kind: 'groq', inputSource: 'media', isDurable: false };
|
|
29
|
+
case SANITY_FUNCTION_DURABLE:
|
|
30
|
+
return resolveDurableInvocationPlan(resource.event);
|
|
31
|
+
case SANITY_FUNCTION_SCHEDULED:
|
|
32
|
+
case SANITY_FUNCTION_PUBSUB:
|
|
33
|
+
case SANITY_FUNCTION_SYNC_TAG_INVALIDATE:
|
|
34
|
+
return { kind: 'plain', isDurable: false };
|
|
35
|
+
default:
|
|
36
|
+
return assertNever(resource);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* True when the function's event is delivered through a GROQ rule (a document or
|
|
41
|
+
* media-library event, whether plain or durable), so its payload is a document the
|
|
42
|
+
* rule may narrow before the handler sees it.
|
|
43
|
+
*/
|
|
44
|
+
export const isGroqInvocation = (resource) => resolveInvocationPlan(resource).kind === 'groq';
|
|
45
|
+
export const collectInvokeInput = async (plan, input, deps) => {
|
|
46
|
+
const source = plan.kind === 'groq' ? plan.inputSource : 'none';
|
|
47
|
+
return resolveInvokeInput(source, input, deps);
|
|
48
|
+
};
|
|
49
|
+
export const buildContext = (plan, input) => {
|
|
50
|
+
const withToken = input.withUserToken ? { token: input.token } : {};
|
|
51
|
+
if (plan.kind === 'plain') {
|
|
52
|
+
return {
|
|
53
|
+
clientOptions: { ...withToken },
|
|
54
|
+
resources: input.resources,
|
|
55
|
+
blueprintFileDir: input.blueprintFileDir,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const isMedia = plan.kind === 'groq' && plan.inputSource === 'media';
|
|
59
|
+
return {
|
|
60
|
+
clientOptions: {
|
|
61
|
+
apiVersion: input.api,
|
|
62
|
+
dataset: input.flags.dataset,
|
|
63
|
+
projectId: input.projectId,
|
|
64
|
+
organizationId: input.organizationId,
|
|
65
|
+
...withToken,
|
|
66
|
+
},
|
|
67
|
+
eventResourceType: isMedia ? 'media-library' : 'dataset',
|
|
68
|
+
eventResourceId: (isMedia ? input.flags['media-library-id'] : `${input.projectId}.${input.flags.dataset}`) ||
|
|
69
|
+
'',
|
|
70
|
+
functionResourceType: 'project',
|
|
71
|
+
functionResourceId: input.projectId || '',
|
|
72
|
+
resources: input.resources,
|
|
73
|
+
blueprintFileDir: input.blueprintFileDir,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
export const buildInvokeOptions = (plan, invokeInput, eventType) => {
|
|
77
|
+
switch (plan.kind) {
|
|
78
|
+
case 'groq':
|
|
79
|
+
return {
|
|
80
|
+
event: eventType,
|
|
81
|
+
payload: invokeInput.payload,
|
|
82
|
+
before: invokeInput.before,
|
|
83
|
+
after: invokeInput.after,
|
|
84
|
+
};
|
|
85
|
+
case 'plain':
|
|
86
|
+
// Cron functions have no payload, but pubsub and sync-tag-invalidate carry
|
|
87
|
+
// whatever the producer sent, so pass it through for all of them.
|
|
88
|
+
return { event: 'plain', payload: invokeInput.payload };
|
|
89
|
+
case 'durable':
|
|
90
|
+
return { event: 'durable', payload: invokeInput.payload };
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const resolveInvokeInput = async (source, input, deps) => {
|
|
94
|
+
const { flags } = input;
|
|
95
|
+
const fetchById = (id) => source === 'document'
|
|
96
|
+
? deps.fetchDocument(id, {
|
|
97
|
+
projectId: input.projectId,
|
|
98
|
+
dataset: flags.dataset,
|
|
99
|
+
apiVersion: input.api,
|
|
100
|
+
apiHost: input.apiHost,
|
|
101
|
+
token: input.token,
|
|
102
|
+
}, input.log)
|
|
103
|
+
: deps.fetchAsset(id, {
|
|
104
|
+
mediaLibraryId: flags['media-library-id'],
|
|
105
|
+
apiVersion: input.api,
|
|
106
|
+
apiHost: input.apiHost,
|
|
107
|
+
token: input.token,
|
|
108
|
+
}, input.log);
|
|
109
|
+
let before = null;
|
|
110
|
+
let after = null;
|
|
111
|
+
if (flags['data-before'] && flags['data-after']) {
|
|
112
|
+
before = parseJsonObject(flags['data-before']);
|
|
113
|
+
after = parseJsonObject(flags['data-after']);
|
|
114
|
+
}
|
|
115
|
+
else if (flags['file-before'] && flags['file-after']) {
|
|
116
|
+
before = parseJsonObject(readFileSync(join(cwd(), flags['file-before']), 'utf8'));
|
|
117
|
+
after = parseJsonObject(readFileSync(join(cwd(), flags['file-after']), 'utf8'));
|
|
118
|
+
}
|
|
119
|
+
else if (flags['document-id-before'] && flags['document-id-after']) {
|
|
120
|
+
before = await fetchById(flags['document-id-before']);
|
|
121
|
+
after = await fetchById(flags['document-id-after']);
|
|
122
|
+
}
|
|
123
|
+
let payload;
|
|
124
|
+
if (after)
|
|
125
|
+
payload = after;
|
|
126
|
+
else if (flags['document-id'])
|
|
127
|
+
payload = await fetchById(flags['document-id']);
|
|
128
|
+
else
|
|
129
|
+
payload = deps.buildPayload({ data: flags.data, file: flags.file });
|
|
130
|
+
return { payload, before, after };
|
|
131
|
+
};
|
package/dist/utils/progress.js
CHANGED
|
@@ -2,6 +2,9 @@ import { stderr } from 'node:process';
|
|
|
2
2
|
import { onExit } from 'signal-exit';
|
|
3
3
|
const ST = '\x1b\\';
|
|
4
4
|
export const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
5
|
+
// Ora clobbers stderr.write and messes with OSC output.
|
|
6
|
+
// Keep the original writer so progress sequences do not collide.
|
|
7
|
+
const writeToStderr = stderr.write.bind(stderr);
|
|
5
8
|
const STATES = {
|
|
6
9
|
clear: 0,
|
|
7
10
|
normal: 1,
|
|
@@ -24,7 +27,7 @@ function startHeartbeat(sequence) {
|
|
|
24
27
|
return;
|
|
25
28
|
heartbeat = setInterval(() => {
|
|
26
29
|
if (stderr.isTTY && heartbeatSequence)
|
|
27
|
-
|
|
30
|
+
writeToStderr(heartbeatSequence);
|
|
28
31
|
}, HEARTBEAT_INTERVAL_MS);
|
|
29
32
|
heartbeat.unref();
|
|
30
33
|
}
|
|
@@ -37,7 +40,7 @@ function write(state, value = 0) {
|
|
|
37
40
|
exitHookRegistered = true;
|
|
38
41
|
function clear() {
|
|
39
42
|
stopHeartbeat();
|
|
40
|
-
|
|
43
|
+
writeToStderr(`\x1b]9;4;0;0${ST}`);
|
|
41
44
|
}
|
|
42
45
|
// arc/utils SIGINT handler clobbers our signal-exit; luckily it then calls process.exit()
|
|
43
46
|
process.once('exit', clear); // triggered by arc/utils SIGINT handler
|
|
@@ -47,7 +50,7 @@ function write(state, value = 0) {
|
|
|
47
50
|
const active = state === 'normal' || state === 'paused' || state === 'busy';
|
|
48
51
|
if (!active)
|
|
49
52
|
stopHeartbeat();
|
|
50
|
-
|
|
53
|
+
writeToStderr(sequence);
|
|
51
54
|
if (active)
|
|
52
55
|
startHeartbeat(sequence);
|
|
53
56
|
}
|