@sanity/runtime-cli 17.12.0 → 17.13.0-next.g06897b0
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 +21 -21
- package/dist/actions/blueprints/assets.d.ts +23 -20
- package/dist/actions/blueprints/assets.js +18 -87
- package/dist/actions/blueprints/logs-polling.d.ts +3 -5
- package/dist/actions/blueprints/logs-polling.js +4 -9
- package/dist/actions/blueprints/logs.d.ts +1 -2
- package/dist/actions/blueprints/logs.js +1 -3
- package/dist/actions/blueprints/operations-state.d.ts +10 -0
- package/dist/actions/blueprints/operations-state.js +42 -0
- package/dist/actions/blueprints/operations.d.ts +2 -9
- package/dist/actions/blueprints/operations.js +1 -15
- package/dist/actions/blueprints/pre-deploy.d.ts +19 -11
- package/dist/actions/blueprints/pre-deploy.js +95 -27
- package/dist/actions/blueprints/process-assets.d.ts +13 -11
- package/dist/actions/blueprints/process-assets.js +9 -7
- package/dist/actions/blueprints/stacks.d.ts +4 -4
- package/dist/actions/blueprints/stacks.js +2 -4
- package/dist/actions/blueprints/wait-for-operation.d.ts +1 -10
- package/dist/actions/blueprints/wait-for-operation.js +56 -101
- package/dist/baseCommands.js +6 -1
- package/dist/commands/blueprints/deploy.js +2 -0
- package/dist/commands/functions/dev.js +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +6 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/cores/blueprints/config.js +1 -1
- package/dist/cores/blueprints/deploy.d.ts +2 -0
- package/dist/cores/blueprints/deploy.js +38 -7
- package/dist/cores/blueprints/destroy.js +4 -2
- package/dist/cores/blueprints/doctor.js +2 -2
- package/dist/cores/blueprints/init.js +3 -1
- package/dist/cores/blueprints/logs.js +11 -5
- package/dist/cores/blueprints/mint-deploy-token.js +2 -1
- package/dist/cores/functions/build.js +4 -5
- package/dist/cores/functions/test.js +3 -1
- package/dist/cores/index.js +3 -1
- package/dist/server/app.d.ts +2 -1
- package/dist/server/app.js +4 -1
- package/dist/server/handlers/invoke.js +1 -1
- package/dist/server/static/vendor/vendor.bundle.js +21791 -25197
- package/dist/server/static/web-socket.js +4 -2
- package/dist/utils/blueprints/deploy-metadata.d.ts +10 -0
- package/dist/utils/blueprints/deploy-metadata.js +17 -0
- package/dist/utils/blueprints/resolve-deployed-resources.d.ts +2 -3
- package/dist/utils/blueprints/resolve-deployed-resources.js +2 -2
- package/dist/utils/blueprints/wait-for.d.ts +60 -0
- package/dist/utils/blueprints/wait-for.js +115 -0
- package/dist/utils/display/blueprints-formatting.d.ts +1 -0
- package/dist/utils/display/blueprints-formatting.js +17 -3
- package/dist/utils/display/dates.d.ts +0 -2
- package/dist/utils/display/dates.js +0 -25
- package/dist/utils/display/functions-formatting.js +1 -1
- package/dist/utils/display/prompt.js +5 -2
- package/dist/utils/display/resources-formatting.d.ts +6 -0
- package/dist/utils/display/resources-formatting.js +17 -0
- package/dist/utils/functions/child-runtime/durable.js +2 -2
- package/dist/utils/functions/dependency-info.d.ts +30 -0
- package/dist/utils/functions/dependency-info.js +57 -0
- package/dist/utils/functions/invoke/assert-console.d.ts +5 -0
- package/dist/utils/functions/invoke/assert-console.js +60 -0
- package/dist/utils/functions/invoke/local.js +9 -0
- package/dist/utils/functions/is-folder-larger-than.d.ts +5 -0
- package/dist/utils/functions/is-folder-larger-than.js +38 -0
- package/dist/utils/functions/prepare-asset.d.ts +2 -0
- package/dist/utils/functions/prepare-asset.js +59 -23
- package/dist/utils/studios/prepare-asset.js +36 -9
- package/dist/utils/types.d.ts +35 -4
- package/dist/utils/types.js +0 -3
- package/oclif.manifest.json +2 -2
- package/package.json +17 -19
- package/skills/functions-dev.md +1 -1
- package/dist/utils/functions/get-folder-size.d.ts +0 -5
- package/dist/utils/functions/get-folder-size.js +0 -24
|
@@ -2,8 +2,10 @@ import apiConstructor from '../api.js'
|
|
|
2
2
|
|
|
3
3
|
const api = apiConstructor()
|
|
4
4
|
|
|
5
|
-
// Create WebSocket connection
|
|
6
|
-
|
|
5
|
+
// Create WebSocket connection on the same origin the emulator is served from,
|
|
6
|
+
// so it follows the --host and --port the server was started with.
|
|
7
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
8
|
+
const socket = new WebSocket(`${protocol}//${window.location.host}`)
|
|
7
9
|
|
|
8
10
|
// Connection opened
|
|
9
11
|
socket.addEventListener('open', () => {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facts about the environment that runs a deploy.
|
|
3
|
+
* Sent as the `client` part of deploy metadata. Keys with an `undefined` value drop out of the JSON body.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getClientMetadata({ cliVersion, dir, }: {
|
|
6
|
+
/** Version of the CLI that hosts this plugin, when known */
|
|
7
|
+
cliVersion?: string;
|
|
8
|
+
/** Directory of the Blueprint file, used to detect the package manager */
|
|
9
|
+
dir: string;
|
|
10
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { platform, versions } from 'node:process';
|
|
2
|
+
import config, { RUNTIME_CLI_VERSION } from '../../config.js';
|
|
3
|
+
import { detectPackageManager } from '../package-manager.js';
|
|
4
|
+
/**
|
|
5
|
+
* Facts about the environment that runs a deploy.
|
|
6
|
+
* Sent as the `client` part of deploy metadata. Keys with an `undefined` value drop out of the JSON body.
|
|
7
|
+
*/
|
|
8
|
+
export function getClientMetadata({ cliVersion, dir, }) {
|
|
9
|
+
return {
|
|
10
|
+
cliVersion,
|
|
11
|
+
runtimeCliVersion: RUNTIME_CLI_VERSION,
|
|
12
|
+
nodeVersion: versions.node,
|
|
13
|
+
nodePackageManager: detectPackageManager(dir),
|
|
14
|
+
platform,
|
|
15
|
+
ci: config.isCi,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { BlueprintResource } from '@sanity/blueprints';
|
|
2
|
-
import type { Logger } from '../logger.js';
|
|
3
2
|
import type { AuthParams } from '../types.js';
|
|
4
3
|
interface ResolveOptions {
|
|
5
4
|
resources: BlueprintResource[];
|
|
6
5
|
stackId?: string;
|
|
7
6
|
auth?: AuthParams;
|
|
8
|
-
|
|
7
|
+
fetchFn: typeof fetch;
|
|
9
8
|
}
|
|
10
9
|
/**
|
|
11
10
|
* Returns the local blueprint resources, annotated with externalIds from the
|
|
@@ -13,5 +12,5 @@ interface ResolveOptions {
|
|
|
13
12
|
* resources that aren't yet deployed are preserved without an externalId.
|
|
14
13
|
* Any failure to fetch the stack falls back to the raw local resources.
|
|
15
14
|
*/
|
|
16
|
-
export declare function resolveDeployedResources({ resources, stackId, auth,
|
|
15
|
+
export declare function resolveDeployedResources({ resources, stackId, auth, fetchFn, }: ResolveOptions): Promise<BlueprintResource[]>;
|
|
17
16
|
export {};
|
|
@@ -5,11 +5,11 @@ import { getStack } from '../../actions/blueprints/stacks.js';
|
|
|
5
5
|
* resources that aren't yet deployed are preserved without an externalId.
|
|
6
6
|
* Any failure to fetch the stack falls back to the raw local resources.
|
|
7
7
|
*/
|
|
8
|
-
export async function resolveDeployedResources({ resources, stackId, auth,
|
|
8
|
+
export async function resolveDeployedResources({ resources, stackId, auth, fetchFn, }) {
|
|
9
9
|
if (!stackId || !auth?.token || !auth.scopeType || !auth.scopeId)
|
|
10
10
|
return resources;
|
|
11
11
|
try {
|
|
12
|
-
const response = await getStack({ stackId, auth,
|
|
12
|
+
const response = await getStack({ stackId, auth, fetchFn });
|
|
13
13
|
if (!response.ok || !Array.isArray(response.stack?.resources))
|
|
14
14
|
return resources;
|
|
15
15
|
const deployedByKey = new Map();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Logger } from '../logger.js';
|
|
2
|
+
import type { BlueprintLog } from '../types.js';
|
|
3
|
+
/** Delay before the first operation poll. Override with SANITY_OPERATION_POLL_INITIAL_DELAY_MS. */
|
|
4
|
+
export declare const OPERATION_POLL_INITIAL_DELAY_MS: number;
|
|
5
|
+
/** Interval between operation status polls. Override with SANITY_OPERATION_POLL_INTERVAL_MS. */
|
|
6
|
+
export declare const OPERATION_POLL_INTERVAL_MS: number;
|
|
7
|
+
/** Consecutive failed polls tolerated before giving up on confirming status. */
|
|
8
|
+
export declare const OPERATION_POLL_MAX_CONSECUTIVE_ERRORS = 5;
|
|
9
|
+
/** Result of waiting for a deploy/destroy operation to settle. */
|
|
10
|
+
export type OperationOutcome = {
|
|
11
|
+
type: 'completed';
|
|
12
|
+
} | {
|
|
13
|
+
type: 'failed';
|
|
14
|
+
logHints: string[];
|
|
15
|
+
} | {
|
|
16
|
+
type: 'unconfirmed';
|
|
17
|
+
error?: string;
|
|
18
|
+
};
|
|
19
|
+
type EndState = 'completed' | 'failed';
|
|
20
|
+
export type StateInfo = {
|
|
21
|
+
state: EndState;
|
|
22
|
+
} | {
|
|
23
|
+
state: 'pending';
|
|
24
|
+
/** Used to avoid repeatedly outputting pending status messages. */
|
|
25
|
+
latestStatus: string;
|
|
26
|
+
verboseMessage: string;
|
|
27
|
+
} | {
|
|
28
|
+
state: 'not_found';
|
|
29
|
+
verboseError: string;
|
|
30
|
+
} | {
|
|
31
|
+
state: 'error';
|
|
32
|
+
error: string | null;
|
|
33
|
+
verboseError: string;
|
|
34
|
+
};
|
|
35
|
+
type WaitForOptions = {
|
|
36
|
+
/**
|
|
37
|
+
* Requests the latest state and returns it to the main loop for processing.
|
|
38
|
+
* @returns The current state of the thing we are waiting for
|
|
39
|
+
*/
|
|
40
|
+
onRequestLatestState: () => Promise<StateInfo>;
|
|
41
|
+
/**
|
|
42
|
+
* Called when the waitFor logic should start reading logs.
|
|
43
|
+
* @returns A function that stops the polling process when invoked.
|
|
44
|
+
*/
|
|
45
|
+
onStartLogPolling: (options: {
|
|
46
|
+
onActivity?: () => void;
|
|
47
|
+
onLogEntry?: (log: BlueprintLog, previousLog: BlueprintLog | undefined) => void;
|
|
48
|
+
}) => () => void;
|
|
49
|
+
getIdleMessage: () => string;
|
|
50
|
+
};
|
|
51
|
+
export type WaitForFn = (options: WaitForOptions) => Promise<OperationOutcome>;
|
|
52
|
+
export declare function createWaitFor({ bin, log, verbose, initialDelayMs, pollIntervalMs, idleSpinnerDelayMs, }: {
|
|
53
|
+
bin: string;
|
|
54
|
+
log: Logger;
|
|
55
|
+
verbose?: boolean;
|
|
56
|
+
initialDelayMs?: number;
|
|
57
|
+
pollIntervalMs?: number;
|
|
58
|
+
idleSpinnerDelayMs?: number;
|
|
59
|
+
}): WaitForFn;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
+
import { formatLogEntry } from '../display/logs-formatting.js';
|
|
3
|
+
import { runCommand } from '../package-manager.js';
|
|
4
|
+
import { createHintCollector } from './hints.js';
|
|
5
|
+
const envMs = (value, fallback) => {
|
|
6
|
+
if (!value)
|
|
7
|
+
return fallback;
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
10
|
+
};
|
|
11
|
+
/** Delay before the first operation poll. Override with SANITY_OPERATION_POLL_INITIAL_DELAY_MS. */
|
|
12
|
+
export const OPERATION_POLL_INITIAL_DELAY_MS = envMs(process.env.SANITY_OPERATION_POLL_INITIAL_DELAY_MS, 1000);
|
|
13
|
+
/** Interval between operation status polls. Override with SANITY_OPERATION_POLL_INTERVAL_MS. */
|
|
14
|
+
export const OPERATION_POLL_INTERVAL_MS = envMs(process.env.SANITY_OPERATION_POLL_INTERVAL_MS, 1500);
|
|
15
|
+
/** Consecutive failed polls tolerated before giving up on confirming status. */
|
|
16
|
+
export const OPERATION_POLL_MAX_CONSECUTIVE_ERRORS = 5;
|
|
17
|
+
const IDLE_SPINNER_DELAY_MS = 30_000;
|
|
18
|
+
const IDLE_SPINNER_UPDATE_INTERVAL_MS = 15_000;
|
|
19
|
+
export function createWaitFor({ bin, log, verbose, initialDelayMs = OPERATION_POLL_INITIAL_DELAY_MS, pollIntervalMs = OPERATION_POLL_INTERVAL_MS, idleSpinnerDelayMs = IDLE_SPINNER_DELAY_MS, }) {
|
|
20
|
+
return async (options) => {
|
|
21
|
+
const logHints = createHintCollector(bin);
|
|
22
|
+
let logStreamCleanup = null;
|
|
23
|
+
let idleSpinner;
|
|
24
|
+
const stopIdleSpinner = (restoreOperationProgress = false) => {
|
|
25
|
+
if (!idleSpinner)
|
|
26
|
+
return;
|
|
27
|
+
idleSpinner.stop();
|
|
28
|
+
idleSpinner = undefined;
|
|
29
|
+
if (restoreOperationProgress)
|
|
30
|
+
log.showProgress();
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
log.showProgress();
|
|
34
|
+
let lastLogAt = Date.now();
|
|
35
|
+
let nextIdleSpinnerUpdateAt = idleSpinnerDelayMs;
|
|
36
|
+
let consecutiveErrors = 0;
|
|
37
|
+
let lastStatus;
|
|
38
|
+
logStreamCleanup = options.onStartLogPolling({
|
|
39
|
+
onActivity: () => {
|
|
40
|
+
lastLogAt = Date.now();
|
|
41
|
+
nextIdleSpinnerUpdateAt = idleSpinnerDelayMs;
|
|
42
|
+
stopIdleSpinner(true);
|
|
43
|
+
},
|
|
44
|
+
onLogEntry: (logEntry, previousLog) => {
|
|
45
|
+
logHints.inspectLog(logEntry);
|
|
46
|
+
log(formatLogEntry(logEntry, verbose, previousLog));
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
// balk in case read replica lags
|
|
50
|
+
await sleep(initialDelayMs);
|
|
51
|
+
while (true) {
|
|
52
|
+
const stateInfo = await options.onRequestLatestState();
|
|
53
|
+
if (stateInfo.state === 'completed') {
|
|
54
|
+
if (logStreamCleanup)
|
|
55
|
+
logStreamCleanup();
|
|
56
|
+
stopIdleSpinner();
|
|
57
|
+
log('');
|
|
58
|
+
return { type: 'completed' };
|
|
59
|
+
}
|
|
60
|
+
if (stateInfo.state === 'failed') {
|
|
61
|
+
if (logStreamCleanup)
|
|
62
|
+
logStreamCleanup();
|
|
63
|
+
stopIdleSpinner();
|
|
64
|
+
log('');
|
|
65
|
+
return { type: 'failed', logHints: logHints.getSuggestions() };
|
|
66
|
+
}
|
|
67
|
+
// tolerate a few consecutive errors; the operation was accepted and may still be running
|
|
68
|
+
if (stateInfo.state === 'error') {
|
|
69
|
+
consecutiveErrors += 1;
|
|
70
|
+
log.verbose(`${stateInfo.verboseError}; retrying (${consecutiveErrors}/${OPERATION_POLL_MAX_CONSECUTIVE_ERRORS}).`);
|
|
71
|
+
if (consecutiveErrors >= OPERATION_POLL_MAX_CONSECUTIVE_ERRORS) {
|
|
72
|
+
if (logStreamCleanup)
|
|
73
|
+
logStreamCleanup();
|
|
74
|
+
stopIdleSpinner();
|
|
75
|
+
log('');
|
|
76
|
+
return { type: 'unconfirmed', error: stateInfo.error ?? undefined };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
consecutiveErrors = 0;
|
|
81
|
+
if (stateInfo.state === 'not_found') {
|
|
82
|
+
log.verbose(stateInfo.verboseError);
|
|
83
|
+
}
|
|
84
|
+
else if (stateInfo.state === 'pending' && stateInfo.latestStatus !== lastStatus) {
|
|
85
|
+
log.verbose(stateInfo.verboseMessage);
|
|
86
|
+
lastStatus = stateInfo.latestStatus;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const idleFor = Date.now() - lastLogAt;
|
|
90
|
+
if (idleFor >= nextIdleSpinnerUpdateAt) {
|
|
91
|
+
const elapsedIdleMs = idleSpinnerDelayMs +
|
|
92
|
+
Math.floor((idleFor - idleSpinnerDelayMs) / IDLE_SPINNER_UPDATE_INTERVAL_MS) *
|
|
93
|
+
IDLE_SPINNER_UPDATE_INTERVAL_MS;
|
|
94
|
+
const elapsedIdleSeconds = elapsedIdleMs / 1000;
|
|
95
|
+
const text = `No new activity for ${elapsedIdleSeconds} seconds. ${options.getIdleMessage()} You can safely exit and check status later with \`${runCommand(bin)} blueprints info\`.`;
|
|
96
|
+
if (idleSpinner)
|
|
97
|
+
idleSpinner.start(text);
|
|
98
|
+
else
|
|
99
|
+
idleSpinner = log.ora(text).start();
|
|
100
|
+
nextIdleSpinnerUpdateAt = elapsedIdleMs + IDLE_SPINNER_UPDATE_INTERVAL_MS;
|
|
101
|
+
}
|
|
102
|
+
await sleep(pollIntervalMs);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (logStreamCleanup)
|
|
107
|
+
logStreamCleanup();
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
stopIdleSpinner();
|
|
112
|
+
log.clearProgress();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { DeploymentPlan } from '../../actions/blueprints/stacks.js';
|
|
|
4
4
|
import { type BlueprintResourceRecord, type Stack, type StackSummary } from '../types.js';
|
|
5
5
|
export declare function formatTitle(title: string, name: string): string;
|
|
6
6
|
export declare function formatDeployedResourceTree(resources: BlueprintResourceRecord[] | undefined, verbose?: boolean): string;
|
|
7
|
+
export declare function formatStudioUrls(resources: BlueprintResourceRecord[] | undefined): string[];
|
|
7
8
|
export declare function formatResourceTree(resources: BlueprintResource[] | undefined, verbose?: boolean): string;
|
|
8
9
|
export declare function formatStackInfo(stack: Stack | StackSummary | Blueprint, isCurrentStack?: boolean): string;
|
|
9
10
|
export declare function formatStacksListing(stacks: StackSummary[], currentStackId?: string): string;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { formatDateTime, formatElapsed } from '@sanity/cli-core/dates';
|
|
1
2
|
import { treeify } from 'array-treeify';
|
|
2
3
|
import { SANITY_ACCESS_ROBOT, SANITY_ACCESS_ROLE, SANITY_FUNCTION_DOCUMENT, SANITY_FUNCTION_DURABLE, SANITY_FUNCTION_MEDIA_LIBRARY_ASSET, SANITY_FUNCTION_PUBSUB, SANITY_FUNCTION_SCHEDULED, SANITY_FUNCTION_SYNC_TAG_INVALIDATE, SANITY_PROJECT, SANITY_PROJECT_CORS, SANITY_PROJECT_DATASET, SANITY_PROJECT_WEBHOOK, SANITY_STUDIO, } from '../../constants.js';
|
|
3
4
|
import { styleText } from '../style-text.js';
|
|
4
5
|
import { isCorsOriginResource, isDatasetResource, isRobotResource, isRoleResource, isStudioResource, isWebhookResource, } from '../types.js';
|
|
5
|
-
import { formatDateTime, formatDuration } from './dates.js';
|
|
6
6
|
import { capitalize, niceId, renderSection } from './presenters.js';
|
|
7
|
-
import { arrayifyCors, arrayifyDataset, arrayifyFunction, arrayifyRobot, arrayifyRole, arrayifyStudio, arrayifyWebhook, } from './resources-formatting.js';
|
|
7
|
+
import { arrayifyCors, arrayifyDataset, arrayifyFunction, arrayifyRobot, arrayifyRole, arrayifyStudio, arrayifyWebhook, studioHost, studioUrl, } from './resources-formatting.js';
|
|
8
8
|
const functionCategory = {
|
|
9
9
|
label: 'Functions',
|
|
10
10
|
displayNameAttribute: 'displayName',
|
|
@@ -165,6 +165,19 @@ export function formatDeployedResourceTree(resources, verbose = false) {
|
|
|
165
165
|
(res) => ({ name: res.name, type: res.type, ...(res.resolvedParameters ?? res.parameters) }), verbose);
|
|
166
166
|
return `${treeify(output)}\n`;
|
|
167
167
|
}
|
|
168
|
+
export function formatStudioUrls(resources) {
|
|
169
|
+
if (!resources)
|
|
170
|
+
return [];
|
|
171
|
+
const lines = [];
|
|
172
|
+
for (const res of resources) {
|
|
173
|
+
if (res.type !== SANITY_STUDIO)
|
|
174
|
+
continue;
|
|
175
|
+
const host = studioHost(res.resolvedParameters ?? res.parameters);
|
|
176
|
+
if (host)
|
|
177
|
+
lines.push(`Studio ${styleText(['bold', 'green'], res.name)} ${studioUrl(host)}`);
|
|
178
|
+
}
|
|
179
|
+
return lines;
|
|
180
|
+
}
|
|
168
181
|
export function formatResourceTree(resources, verbose = false) {
|
|
169
182
|
if (!resources || resources.length === 0)
|
|
170
183
|
return ' Zero resources';
|
|
@@ -207,7 +220,8 @@ export function formatStackInfo(stack, isCurrentStack = false) {
|
|
|
207
220
|
if (time)
|
|
208
221
|
parts.push(styleText('dim', formatDateTime(time)));
|
|
209
222
|
if (op.completedAt && op.createdAt) {
|
|
210
|
-
|
|
223
|
+
const duration = new Date(op.completedAt).getTime() - new Date(op.createdAt).getTime();
|
|
224
|
+
parts.push(styleText('yellow', `(${formatElapsed(duration)})`));
|
|
211
225
|
}
|
|
212
226
|
rows.push(['Operation', parts.join(' ')]);
|
|
213
227
|
if (op.userMessage)
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
type DateInput = string | number | Date;
|
|
2
2
|
export declare function formatDate(input: DateInput): string;
|
|
3
3
|
export declare function formatTime(input: DateInput): string;
|
|
4
|
-
export declare function formatDateTime(input: DateInput): string;
|
|
5
|
-
export declare function formatDuration(startDate: string, endDate: string): string;
|
|
6
4
|
export {};
|
|
@@ -19,28 +19,3 @@ export function formatTime(input) {
|
|
|
19
19
|
const s = String(d.getSeconds()).padStart(2, '0');
|
|
20
20
|
return `${h}:${m}:${s}`;
|
|
21
21
|
}
|
|
22
|
-
export function formatDateTime(input) {
|
|
23
|
-
const d = toDate(input);
|
|
24
|
-
if (Number.isNaN(d.getTime()))
|
|
25
|
-
return '';
|
|
26
|
-
return `${formatDate(d)} ${formatTime(d)}`;
|
|
27
|
-
}
|
|
28
|
-
export function formatDuration(startDate, endDate) {
|
|
29
|
-
const start = new Date(startDate).getTime();
|
|
30
|
-
const end = new Date(endDate).getTime();
|
|
31
|
-
if (start > end)
|
|
32
|
-
return 'Invalid duration';
|
|
33
|
-
const durationMs = end - start;
|
|
34
|
-
if (durationMs < 1000)
|
|
35
|
-
return `${durationMs}ms`;
|
|
36
|
-
if (durationMs < 60000)
|
|
37
|
-
return `${Math.round(durationMs / 1000)}s`;
|
|
38
|
-
if (durationMs < 3600000) {
|
|
39
|
-
const minutes = Math.floor(durationMs / 60000);
|
|
40
|
-
const seconds = Math.floor((durationMs % 60000) / 1000);
|
|
41
|
-
return `${minutes}m ${seconds}s`;
|
|
42
|
-
}
|
|
43
|
-
const hours = Math.floor(durationMs / 3600000);
|
|
44
|
-
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
|
45
|
-
return `${hours}h ${minutes}m`;
|
|
46
|
-
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { formatDateTime } from '@sanity/cli-core/dates';
|
|
1
2
|
import { SANITY_FUNCTION_PREFIX } from '../../constants.js';
|
|
2
3
|
import { styleText } from '../style-text.js';
|
|
3
|
-
import { formatDateTime } from './dates.js';
|
|
4
4
|
import { niceId, renderSection } from './presenters.js';
|
|
5
5
|
export function formatFunctionsList(functions, options = {}) {
|
|
6
6
|
if (!options.verbose)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { confirm, input, Separator, select } from '@inquirer/prompts';
|
|
2
2
|
import { createEmptyStack, listStacks } from '../../actions/blueprints/stacks.js';
|
|
3
3
|
import { groupProjectsByOrganization } from '../../actions/sanity/projects.js';
|
|
4
|
+
import { STACKS_DOCS_URL } from '../../constants.js';
|
|
4
5
|
import { styleText } from '../style-text.js';
|
|
5
6
|
import { niceId } from './presenters.js';
|
|
6
7
|
// --- Wizard infrastructure ---
|
|
7
8
|
export const BACK = Symbol('back');
|
|
9
|
+
/** Shown under the highlighted Stack choice; a printed line would break the wizard's two-line erase on Back */
|
|
10
|
+
const STACK_DEFINITION = `A Stack is the deployed copy of a Blueprint: the live resources on Sanity's side. ${STACKS_DOCS_URL}`;
|
|
8
11
|
/** Thrown when the user picks "Cancel" in a prompt. Cores translate it to a clean result. */
|
|
9
12
|
export class CancelledError extends Error {
|
|
10
13
|
constructor(message = 'Cancelled by user.') {
|
|
@@ -270,7 +273,7 @@ export async function promptForStack({ scopeType, scopeId, token, logger, allowB
|
|
|
270
273
|
if (stacks.length > 0) {
|
|
271
274
|
const stackChoices = [
|
|
272
275
|
new Separator(styleText('underline', 'Create a new Stack:')),
|
|
273
|
-
{ name: styleText('bold', 'New Stack'), value: NEW_STACK_ID },
|
|
276
|
+
{ name: styleText('bold', 'New Stack'), value: NEW_STACK_ID, description: STACK_DEFINITION },
|
|
274
277
|
new Separator(styleText('underline', 'Use an existing Stack:')),
|
|
275
278
|
...stacks.map((s) => ({
|
|
276
279
|
name: `"${s.name}" ${niceId(s.id)} ${styleText('dim', `(${s.resourceCount} ${s.resourceCount === 1 ? 'resource' : 'resources'})`)}`,
|
|
@@ -301,7 +304,7 @@ export async function promptForStack({ scopeType, scopeId, token, logger, allowB
|
|
|
301
304
|
{
|
|
302
305
|
name: 'Create the first Stack',
|
|
303
306
|
value: NEW_STACK_ID,
|
|
304
|
-
description:
|
|
307
|
+
description: STACK_DEFINITION,
|
|
305
308
|
},
|
|
306
309
|
{ name: 'Cancel', value: CANCEL, description: 'Stop here and change nothing' },
|
|
307
310
|
], allowBack),
|
|
@@ -5,5 +5,11 @@ export declare function arrayifyDataset(resource: BlueprintDatasetResource): Tre
|
|
|
5
5
|
export declare function arrayifyFunction(fn: BlueprintResource): TreeInput;
|
|
6
6
|
export declare function arrayifyRobot(resource: BlueprintRobotResource): TreeInput;
|
|
7
7
|
export declare function arrayifyRole(resource: BlueprintRoleResource): TreeInput;
|
|
8
|
+
/**
|
|
9
|
+
* Hostname label of a Studio: the resolved `appHost` when present, else the authored `slug`.
|
|
10
|
+
* undefined when a `$.resources` reference
|
|
11
|
+
*/
|
|
12
|
+
export declare function studioHost(params: Record<string, unknown>): string | undefined;
|
|
13
|
+
export declare function studioUrl(host: string): string;
|
|
8
14
|
export declare function arrayifyStudio(resource: BlueprintStudioResource): TreeInput;
|
|
9
15
|
export declare function arrayifyWebhook(resource: BlueprintDocumentWebhookResource): TreeInput;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import config from '../../config.js';
|
|
1
2
|
import { hasEventSource, isLocalFunctionResource, isScheduleEvent, } from '../../utils/types.js';
|
|
2
3
|
import { styleText } from '../style-text.js';
|
|
3
4
|
import { niceId } from './presenters.js';
|
|
@@ -120,8 +121,24 @@ export function arrayifyRole(resource) {
|
|
|
120
121
|
}
|
|
121
122
|
return details;
|
|
122
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Hostname label of a Studio: the resolved `appHost` when present, else the authored `slug`.
|
|
126
|
+
* undefined when a `$.resources` reference
|
|
127
|
+
*/
|
|
128
|
+
export function studioHost(params) {
|
|
129
|
+
const host = params.appHost ?? params.slug;
|
|
130
|
+
if (typeof host !== 'string' || host.length === 0 || host.startsWith('$.'))
|
|
131
|
+
return undefined;
|
|
132
|
+
return host;
|
|
133
|
+
}
|
|
134
|
+
export function studioUrl(host) {
|
|
135
|
+
return `https://${host}.${config.studioDomain}`;
|
|
136
|
+
}
|
|
123
137
|
export function arrayifyStudio(resource) {
|
|
124
138
|
const details = [formatLabeledValue('appHost', resource.slug)];
|
|
139
|
+
const host = studioHost(resource);
|
|
140
|
+
if (host)
|
|
141
|
+
details.push(formatLabeledValue('url', studioUrl(host)));
|
|
125
142
|
if (resource.project)
|
|
126
143
|
details.push(formatLabeledValue('project', niceId(resource.project)));
|
|
127
144
|
return details;
|
|
@@ -107,13 +107,13 @@ export async function runDurable(handler, {context, event}, io) {
|
|
|
107
107
|
}
|
|
108
108
|
return state
|
|
109
109
|
}),
|
|
110
|
-
delegate: ({name, handler,
|
|
110
|
+
delegate: ({name, handler, event}) =>
|
|
111
111
|
track(name || '', 'delegate', () => {
|
|
112
112
|
const fn = typeof handler === 'string' ? handler : handler?.name
|
|
113
113
|
if (!fn) {
|
|
114
114
|
throw new Error('No function name provided for delegation')
|
|
115
115
|
}
|
|
116
|
-
return io.invoke(fn,
|
|
116
|
+
return io.invoke(fn, event)
|
|
117
117
|
}),
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Where a function's dependency list came from. */
|
|
2
|
+
export type DependencySource = 'manifest' | 'autoinstall' | 'bundled' | 'none';
|
|
3
|
+
export type DependencyInfo = {
|
|
4
|
+
dependencySource: DependencySource;
|
|
5
|
+
dependencyCount: number;
|
|
6
|
+
/** Declared versions */
|
|
7
|
+
dependencies?: Record<string, string>;
|
|
8
|
+
/** Source files the Arc treeshaker parsed - autoinstall only */
|
|
9
|
+
parsedFileCount?: number;
|
|
10
|
+
};
|
|
11
|
+
/** The package.json Architect generates during autoinstall. */
|
|
12
|
+
export type AutoInstallManifest = {
|
|
13
|
+
dependencies?: Record<string, string>;
|
|
14
|
+
_parsed?: string[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Read and delete the manifest Architect generated during autoinstall.
|
|
18
|
+
* Arc moves it to `node_modules/_arc-autoinstall/` after the install.
|
|
19
|
+
* Has a timestamp, so it must be deleted to avoid hash changes on every deploy.
|
|
20
|
+
*/
|
|
21
|
+
export declare function takeAutoInstallManifest(installDir: string): Promise<AutoInstallManifest | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* Describe a function's dependencies from the manifest that declared them.
|
|
24
|
+
* The user's own package.json wins. Otherwise the Architect autoinstall manifest is used.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getDependencyInfo({ sourceDir, autoInstall, bundled, }: {
|
|
27
|
+
sourceDir: string;
|
|
28
|
+
autoInstall?: AutoInstallManifest;
|
|
29
|
+
bundled: boolean;
|
|
30
|
+
}): Promise<DependencyInfo>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Read and delete the manifest Architect generated during autoinstall.
|
|
5
|
+
* Arc moves it to `node_modules/_arc-autoinstall/` after the install.
|
|
6
|
+
* Has a timestamp, so it must be deleted to avoid hash changes on every deploy.
|
|
7
|
+
*/
|
|
8
|
+
export async function takeAutoInstallManifest(installDir) {
|
|
9
|
+
const file = path.join(installDir, 'node_modules', '_arc-autoinstall', 'package.json');
|
|
10
|
+
let manifest;
|
|
11
|
+
try {
|
|
12
|
+
manifest = JSON.parse(await readFile(file, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// no autoinstall happened, or the file is unreadable
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
await rm(file, { force: true });
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// best effort
|
|
22
|
+
}
|
|
23
|
+
return manifest;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Describe a function's dependencies from the manifest that declared them.
|
|
27
|
+
* The user's own package.json wins. Otherwise the Architect autoinstall manifest is used.
|
|
28
|
+
*/
|
|
29
|
+
export async function getDependencyInfo({ sourceDir, autoInstall, bundled, }) {
|
|
30
|
+
const declared = await readDeclaredDependencies(sourceDir);
|
|
31
|
+
if (declared && Object.keys(declared).length > 0) {
|
|
32
|
+
return {
|
|
33
|
+
dependencySource: 'manifest',
|
|
34
|
+
dependencyCount: Object.keys(declared).length,
|
|
35
|
+
dependencies: declared,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (autoInstall) {
|
|
39
|
+
const dependencies = autoInstall.dependencies ?? {};
|
|
40
|
+
return {
|
|
41
|
+
dependencySource: 'autoinstall',
|
|
42
|
+
dependencyCount: Object.keys(dependencies).length,
|
|
43
|
+
dependencies,
|
|
44
|
+
parsedFileCount: autoInstall._parsed?.length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { dependencySource: bundled ? 'bundled' : 'none', dependencyCount: 0 };
|
|
48
|
+
}
|
|
49
|
+
async function readDeclaredDependencies(dir) {
|
|
50
|
+
try {
|
|
51
|
+
const pkg = JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf8'));
|
|
52
|
+
return pkg.dependencies;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
3
|
+
import { parseSync, Visitor } from 'vite';
|
|
4
|
+
const EXTENSIONS = ['', '.ts', '.js', '.mjs', '.cjs'];
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a local module if it exists.
|
|
7
|
+
* @param fromFile
|
|
8
|
+
* @param specifier
|
|
9
|
+
*/
|
|
10
|
+
const resolveLocalModule = (fromFile, specifier) => {
|
|
11
|
+
const base = resolve(dirname(fromFile), specifier);
|
|
12
|
+
const candidates = [
|
|
13
|
+
base,
|
|
14
|
+
base.replace(/\.js$/, '.ts'),
|
|
15
|
+
...EXTENSIONS.map((ext) => base + ext),
|
|
16
|
+
...EXTENSIONS.map((ext) => resolve(base, `index${ext}`)),
|
|
17
|
+
];
|
|
18
|
+
return candidates.find((candidate) => EXTENSIONS.includes(extname(candidate)) && existsSync(candidate) && !candidate.endsWith('/'));
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Asserts that a given source file does not contain any `console` usage.
|
|
22
|
+
* @param src
|
|
23
|
+
*/
|
|
24
|
+
export const assertConsole = (src) => {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const stack = [src];
|
|
27
|
+
while (stack.length > 0) {
|
|
28
|
+
const file = stack.pop();
|
|
29
|
+
if (!file || seen.has(file)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
seen.add(file);
|
|
33
|
+
const { program, module: esm, errors } = parseSync(file, readFileSync(file, 'utf8'));
|
|
34
|
+
if (errors.length > 0) {
|
|
35
|
+
throw new Error(`Failed to parse ${file}: ${errors.map((e) => e.message).join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
new Visitor({
|
|
38
|
+
MemberExpression(node) {
|
|
39
|
+
if (node.object.type === 'Identifier' && node.object.name === 'console') {
|
|
40
|
+
// @todo: improve error message to include doc link.
|
|
41
|
+
const err = new Error('Console logging is not allowed in Durable functions. ' +
|
|
42
|
+
`at ${file} ` +
|
|
43
|
+
'Use the provided durable logger `logger.log()` or `ctx.log()` instead.');
|
|
44
|
+
err.stack = undefined;
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
}).visit(program);
|
|
49
|
+
const isLocal = (str) => str.startsWith('./') || str.startsWith('../');
|
|
50
|
+
const imports = esm.staticImports.map((item) => item.moduleRequest.value);
|
|
51
|
+
const reExports = esm.staticExports.flatMap((exp) => exp.entries.flatMap((entry) => entry.moduleRequest && !entry.isType ? [entry.moduleRequest.value] : []));
|
|
52
|
+
const specs = [...imports, ...reExports].filter(isLocal);
|
|
53
|
+
for (const spec of specs) {
|
|
54
|
+
const resolved = resolveLocalModule(file, spec);
|
|
55
|
+
if (resolved) {
|
|
56
|
+
stack.push(resolved);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
2
3
|
import { performance } from 'node:perf_hooks';
|
|
3
4
|
import { cwd } from 'node:process';
|
|
4
5
|
import { setTimeout } from 'node:timers';
|
|
@@ -8,6 +9,8 @@ import config from '../../../config.js';
|
|
|
8
9
|
import { SANITY_FUNCTION_DURABLE } from '../../../constants.js';
|
|
9
10
|
import { groupBlueprintResources } from '../../blueprints/group-resources.js';
|
|
10
11
|
import { isGroqContextOptions, } from '../../types.js';
|
|
12
|
+
import { findFunctionEntryPoint } from '../find-entry-point.js';
|
|
13
|
+
import { assertConsole } from './assert-console.js';
|
|
11
14
|
import { acquireFunctionBuild } from './prepare.js';
|
|
12
15
|
import { isGroqInvocation } from './resolve-invoke-type.js';
|
|
13
16
|
function getChildProcessWrapperPath() {
|
|
@@ -48,6 +51,12 @@ export default async function invoke(resource, payload, context, options) {
|
|
|
48
51
|
throw new Error(`Function resource "${resource.name}" is missing the 'src' property.`);
|
|
49
52
|
}
|
|
50
53
|
const { forceColor = true, timeout = 10, onNestedInvoke, depth = 0, callStack = [], onStepEvent = () => { }, noWait = false, } = options;
|
|
54
|
+
// Ensure a user when running a durable is using the provided logger and not the `console`
|
|
55
|
+
if (resource.type === SANITY_FUNCTION_DURABLE) {
|
|
56
|
+
const sourcePath = resolve(context.blueprintFileDir, resource.src);
|
|
57
|
+
const entry = await findFunctionEntryPoint(sourcePath, resource?.displayName || resource.name);
|
|
58
|
+
assertConsole(entry);
|
|
59
|
+
}
|
|
51
60
|
// `payload.payload` is the event body for every function type: the document for
|
|
52
61
|
// GROQ-based events, whatever the producer sent for the rest. The handler always
|
|
53
62
|
// receives it as `event.data` (see the `payload` built in `start()` below).
|