@sanity/runtime-cli 17.11.1 → 17.12.1
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 +19 -19
- package/dist/actions/blueprints/assets.d.ts +21 -20
- package/dist/actions/blueprints/assets.js +17 -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 +14 -9
- package/dist/actions/blueprints/pre-deploy.js +83 -26
- package/dist/actions/blueprints/process-assets.d.ts +12 -11
- package/dist/actions/blueprints/process-assets.js +8 -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/cores/blueprints/deploy.js +28 -6
- package/dist/cores/blueprints/destroy.js +4 -2
- package/dist/cores/blueprints/doctor.js +1 -1
- package/dist/cores/blueprints/init.js +3 -1
- package/dist/cores/blueprints/logs.js +11 -5
- package/dist/cores/functions/build.js +4 -5
- package/dist/cores/functions/test.js +33 -21
- package/dist/cores/index.js +3 -1
- package/dist/server/handlers/invoke.js +1 -1
- 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/functions/child-process-wrapper.js +1 -1
- package/dist/utils/functions/child-runtime/durable.js +8 -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 +43 -6
- package/dist/utils/studios/prepare-asset.js +5 -3
- package/dist/utils/types.d.ts +19 -5
- package/dist/utils/types.js +0 -3
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
- package/dist/utils/functions/get-folder-size.d.ts +0 -5
- package/dist/utils/functions/get-folder-size.js +0 -24
|
@@ -4,6 +4,7 @@ import { formatTitle } from '../../utils/display/blueprints-formatting.js';
|
|
|
4
4
|
import { formatLogEntry, formatLogs } from '../../utils/display/logs-formatting.js';
|
|
5
5
|
import { niceId } from '../../utils/display/presenters.js';
|
|
6
6
|
import { styleText } from '../../utils/style-text.js';
|
|
7
|
+
import { createTracedFetch } from '../../utils/traced-fetch.js';
|
|
7
8
|
/** Number of recent logs to show before entering watch mode */
|
|
8
9
|
const WATCH_PREFETCH_LIMIT = 10;
|
|
9
10
|
export async function blueprintLogsCore(options) {
|
|
@@ -11,8 +12,9 @@ export async function blueprintLogsCore(options) {
|
|
|
11
12
|
const { watch = false, verbose = false, limit, since, before } = flags;
|
|
12
13
|
const spinner = log.ora(`Fetching recent logs for Stack deployment ${niceId(stackId)}`).start();
|
|
13
14
|
try {
|
|
15
|
+
const fetchFn = createTracedFetch(log);
|
|
14
16
|
if (watch) {
|
|
15
|
-
const { ok, logs, error } = await getLogs({ stackId, limit: WATCH_PREFETCH_LIMIT }, auth,
|
|
17
|
+
const { ok, logs, error } = await getLogs({ stackId, limit: WATCH_PREFETCH_LIMIT }, auth, fetchFn);
|
|
16
18
|
if (!ok) {
|
|
17
19
|
spinner.fail(`${styleText('red', 'Failed')} to retrieve logs`);
|
|
18
20
|
log.error(`Error: ${error || 'Unknown error'}`);
|
|
@@ -38,9 +40,13 @@ export async function blueprintLogsCore(options) {
|
|
|
38
40
|
stackId,
|
|
39
41
|
knownIds: logs.map((l) => l.id),
|
|
40
42
|
auth,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
fetchFn,
|
|
44
|
+
onShowBanner() {
|
|
45
|
+
log(`Watching logs... ${styleText('bold', 'ctrl+c')} to cancel`);
|
|
46
|
+
},
|
|
47
|
+
onLogEntry(logEntry, previousLog) {
|
|
48
|
+
log(formatLogEntry(logEntry, verbose, previousLog));
|
|
49
|
+
},
|
|
44
50
|
});
|
|
45
51
|
// Return a never-resolving promise so the polling loop keeps running
|
|
46
52
|
return {
|
|
@@ -49,7 +55,7 @@ export async function blueprintLogsCore(options) {
|
|
|
49
55
|
};
|
|
50
56
|
}
|
|
51
57
|
// One-shot fetch (no watch)
|
|
52
|
-
const { ok, logs, error, hasMore } = await getLogs({ stackId, limit, before, after: since }, auth,
|
|
58
|
+
const { ok, logs, error, hasMore } = await getLogs({ stackId, limit, before, after: since }, auth, fetchFn);
|
|
53
59
|
if (!ok) {
|
|
54
60
|
spinner.fail(`${styleText('red', 'Failed')} to retrieve Stack deployment logs`);
|
|
55
61
|
log.error(`Error: ${error || 'Unknown error'}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { CONVERT_BYTES_TO_MB } from '../../constants.js';
|
|
4
4
|
import { findFunctionInResources } from '../../utils/functions/find.js';
|
|
5
5
|
import { prepareAsset } from '../../utils/functions/prepare-asset.js';
|
|
6
6
|
import { isLocalFunctionResource } from '../../utils/types.js';
|
|
@@ -31,14 +31,13 @@ export async function functionBuildCore(options) {
|
|
|
31
31
|
errors.push({ name: resource.name, error: prepResult.error ?? 'Unknown error' });
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
|
-
const
|
|
35
|
-
const buffer = zip.toBuffer();
|
|
34
|
+
const buffer = prepResult.outputBuffer;
|
|
36
35
|
const zipPath = path.join(outDir, `${resource.name}.zip`);
|
|
37
|
-
await fs.promises.writeFile(zipPath,
|
|
36
|
+
await fs.promises.writeFile(zipPath, prepResult.outputBuffer);
|
|
38
37
|
if (prepResult.cleanup) {
|
|
39
38
|
await prepResult.cleanup();
|
|
40
39
|
}
|
|
41
|
-
const sizeMb = (buffer.length /
|
|
40
|
+
const sizeMb = (buffer.length / CONVERT_BYTES_TO_MB).toFixed(1);
|
|
42
41
|
spinner.succeed(`${resource.name}.zip (${sizeMb} MB) → ${zipPath}`);
|
|
43
42
|
built.push({ name: resource.name, size: buffer.length, path: zipPath });
|
|
44
43
|
}
|
|
@@ -7,27 +7,8 @@ import { createDurableJsonCollector } from '../../utils/functions/durable-logs.j
|
|
|
7
7
|
import { findFunctionInResources, getFunctionNames } from '../../utils/functions/find.js';
|
|
8
8
|
import { runFunction } from '../../utils/functions/invoke/nested.js';
|
|
9
9
|
import { buildContext, buildInvokeOptions, collectInvokeInput, defaultInvocationDeps, resolveInvocationPlan, } from '../../utils/functions/invoke/resolve-invoke-type.js';
|
|
10
|
+
import { createTracedFetch } from '../../utils/traced-fetch.js';
|
|
10
11
|
import { isEventType, } from '../../utils/types.js';
|
|
11
|
-
/**
|
|
12
|
-
* CLI lines for a durable run, in emission order; bare `start` events dropped.
|
|
13
|
-
* @param events
|
|
14
|
-
*/
|
|
15
|
-
function renderStepTimeline(events) {
|
|
16
|
-
const lines = [];
|
|
17
|
-
for (const event of events) {
|
|
18
|
-
if (event.kind === 'log') {
|
|
19
|
-
lines.push(`${event.timestamp} ${event.level.toUpperCase()} ${event.message}`);
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
if (event.phase === 'start')
|
|
23
|
-
continue;
|
|
24
|
-
const glyph = STEP_PHASE_GLYPH[event.phase] ?? '•';
|
|
25
|
-
const duration = typeof event.durationMs === 'number' ? ` (${Math.round(event.durationMs)}ms)` : '';
|
|
26
|
-
const detail = event.phase === 'error' && event.error ? ` ${event.error.message}` : '';
|
|
27
|
-
lines.push(` ${glyph} ${event.name} [${event.kind}]${duration}${detail}`);
|
|
28
|
-
}
|
|
29
|
-
return lines;
|
|
30
|
-
}
|
|
31
12
|
export async function functionTestCore(options) {
|
|
32
13
|
const { blueprint, log, error, args, flags, helpText } = options;
|
|
33
14
|
const { name: fnName } = args;
|
|
@@ -56,6 +37,7 @@ export async function functionTestCore(options) {
|
|
|
56
37
|
};
|
|
57
38
|
}
|
|
58
39
|
try {
|
|
40
|
+
const fetchFn = createTracedFetch(log);
|
|
59
41
|
const resource = findFunctionInResources(blueprint.resources, fnName); // throws if not found
|
|
60
42
|
const resolvedResources = await resolveDeployedResources({
|
|
61
43
|
resources: blueprint.resources,
|
|
@@ -63,7 +45,7 @@ export async function functionTestCore(options) {
|
|
|
63
45
|
auth: config.token && blueprint.scopeType && blueprint.scopeId
|
|
64
46
|
? { token: config.token, scopeType: blueprint.scopeType, scopeId: blueprint.scopeId }
|
|
65
47
|
: undefined,
|
|
66
|
-
|
|
48
|
+
fetchFn,
|
|
67
49
|
});
|
|
68
50
|
const input = {
|
|
69
51
|
flags,
|
|
@@ -127,6 +109,36 @@ export async function functionTestCore(options) {
|
|
|
127
109
|
log(`↳ END: invoke('${run.name}')`);
|
|
128
110
|
}
|
|
129
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* CLI lines for a durable run, in emission order.
|
|
114
|
+
* @param events
|
|
115
|
+
*/
|
|
116
|
+
function renderStepTimeline(events) {
|
|
117
|
+
const lines = [];
|
|
118
|
+
for (const event of events) {
|
|
119
|
+
if (event.kind === 'log') {
|
|
120
|
+
lines.push(`${event.timestamp} ${event.level.toUpperCase()} ${event.message}`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (event.phase === 'start') {
|
|
124
|
+
// mark the start of the function being invoked.
|
|
125
|
+
if (event.kind === 'delegate') {
|
|
126
|
+
lines.push(` ${STEP_PHASE_GLYPH[event.phase]} ${event.kind} -> ${event.name}`);
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Only show nested logs for invoked functions.
|
|
131
|
+
// @todo: we may have to show for other events as we add more steps
|
|
132
|
+
if (event.kind === 'delegate') {
|
|
133
|
+
logNestedRuns();
|
|
134
|
+
}
|
|
135
|
+
const glyph = STEP_PHASE_GLYPH[event.phase] ?? '•';
|
|
136
|
+
const duration = typeof event.durationMs === 'number' ? ` (${Math.round(event.durationMs)}ms)` : '';
|
|
137
|
+
const detail = event.phase === 'error' && event.error ? ` ${event.error.message}` : '';
|
|
138
|
+
lines.push(` ${glyph} ${event.name} [${event.kind}]${duration}${detail}`);
|
|
139
|
+
}
|
|
140
|
+
return lines;
|
|
141
|
+
}
|
|
130
142
|
const spinner = log.ora('Executing function...').start();
|
|
131
143
|
const { json: response, logs, error, } = await testAction(resource, invokeOptions, context, {
|
|
132
144
|
timeout: timeout ? timeout : resource.timeout,
|
package/dist/cores/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readLocalBlueprint } from '../actions/blueprints/blueprint.js';
|
|
2
2
|
import { getStack, resolveStackIdByNameOrId } from '../actions/blueprints/stacks.js';
|
|
3
3
|
import { presentBlueprintParserErrors } from '../utils/display/errors.js';
|
|
4
|
+
import { createTracedFetch } from '../utils/traced-fetch.js';
|
|
4
5
|
import { validTokenOrErrorMessage } from '../utils/validated-token.js';
|
|
5
6
|
export async function initBlueprintConfig({ bin, log, token, validateResources = false, validateToken = true, blueprintPath, }) {
|
|
6
7
|
let checkedToken = token;
|
|
@@ -51,7 +52,8 @@ export async function initDeployedBlueprintConfig(config) {
|
|
|
51
52
|
return { ok: false, error: 'Missing Stack deployment configuration for Blueprint' };
|
|
52
53
|
}
|
|
53
54
|
const spinner = config.log.ora('Loading Stack deployment...').start();
|
|
54
|
-
const
|
|
55
|
+
const fetchFn = createTracedFetch(config.log);
|
|
56
|
+
const stackResponse = await getStack({ stackId, auth, fetchFn });
|
|
55
57
|
if (!stackResponse.ok) {
|
|
56
58
|
spinner.fail('Could not load Stack deployment');
|
|
57
59
|
return { ok: false, error: 'Missing Stack deployment' };
|
|
@@ -16,7 +16,7 @@ export async function handleInvokeRequest(functionName, event, metadata, context
|
|
|
16
16
|
resources,
|
|
17
17
|
stackId,
|
|
18
18
|
auth: config.token && scopeType && scopeId ? { token: config.token, scopeType, scopeId } : undefined,
|
|
19
|
-
|
|
19
|
+
fetchFn: fetch,
|
|
20
20
|
});
|
|
21
21
|
const payload = {
|
|
22
22
|
payload: event,
|
|
@@ -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
|
+
}
|
|
@@ -43,7 +43,6 @@ process.on('message', async (data) => {
|
|
|
43
43
|
// `{srcPath, payload}` shape as well as the new `{type: 'start', ...}`.
|
|
44
44
|
const {srcPath, payload, isDurable, noWait} = jsonData
|
|
45
45
|
const {context, ...event} = payload
|
|
46
|
-
|
|
47
46
|
let logs = ''
|
|
48
47
|
let errorLogs = ''
|
|
49
48
|
let json = null
|
|
@@ -87,6 +86,7 @@ process.on('message', async (data) => {
|
|
|
87
86
|
{
|
|
88
87
|
emit: (e) => process.send(JSON.stringify({type: 'step-event', event: e})),
|
|
89
88
|
wait: (ms) => new Promise((resolve) => setTimeout(resolve, noWait ? 0 : ms)),
|
|
89
|
+
invoke: invoker.invoke,
|
|
90
90
|
},
|
|
91
91
|
)
|
|
92
92
|
} else {
|
|
@@ -107,6 +107,14 @@ export async function runDurable(handler, {context, event}, io) {
|
|
|
107
107
|
}
|
|
108
108
|
return state
|
|
109
109
|
}),
|
|
110
|
+
delegate: ({name, handler, event}) =>
|
|
111
|
+
track(name || '', 'delegate', () => {
|
|
112
|
+
const fn = typeof handler === 'string' ? handler : handler?.name
|
|
113
|
+
if (!fn) {
|
|
114
|
+
throw new Error('No function name provided for delegation')
|
|
115
|
+
}
|
|
116
|
+
return io.invoke(fn, event)
|
|
117
|
+
}),
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
return handler({context, event, step, logger})
|
|
@@ -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).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Recursively validates the total size of a folder, including all nested files and subdirectories.
|
|
5
|
+
*/
|
|
6
|
+
export function isFolderLargerThan(folder, maxBytes) {
|
|
7
|
+
if (!statSync(folder).isDirectory()) {
|
|
8
|
+
throw new Error('Provided path is not a directory');
|
|
9
|
+
}
|
|
10
|
+
function recurse(f, bytesSoFar) {
|
|
11
|
+
let totalBytes = bytesSoFar;
|
|
12
|
+
const fileEntries = readdirSync(f);
|
|
13
|
+
for (const file of fileEntries) {
|
|
14
|
+
const filePath = path.join(f.toString(), file);
|
|
15
|
+
const stats = statSync(filePath);
|
|
16
|
+
if (stats.isFile()) {
|
|
17
|
+
totalBytes += stats.size;
|
|
18
|
+
}
|
|
19
|
+
else if (stats.isDirectory()) {
|
|
20
|
+
const result = recurse(filePath, totalBytes);
|
|
21
|
+
totalBytes = result.totalBytes;
|
|
22
|
+
}
|
|
23
|
+
// return early if we have already exceeded the maximum
|
|
24
|
+
if (totalBytes > maxBytes) {
|
|
25
|
+
return {
|
|
26
|
+
totalBytes,
|
|
27
|
+
exceededMax: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
totalBytes,
|
|
33
|
+
exceededMax: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const result = recurse(folder, 0);
|
|
37
|
+
return result.exceededMax;
|
|
38
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import AdmZip from 'adm-zip';
|
|
1
2
|
import type { AssetPrepResult, FunctionResource, InstallerType } from '../types.js';
|
|
2
3
|
export declare function prepareAsset({ blueprintFileDir, resource, installer, }: {
|
|
3
4
|
blueprintFileDir: string;
|
|
4
5
|
resource: FunctionResource;
|
|
5
6
|
installer?: InstallerType;
|
|
6
7
|
}): Promise<AssetPrepResult>;
|
|
8
|
+
export declare function pathToZip(path: string): Promise<AdmZip>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import fs, { existsSync } from 'node:fs';
|
|
2
2
|
import { rm, stat } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import
|
|
4
|
+
import AdmZip from 'adm-zip';
|
|
5
|
+
import { CONVERT_BYTES_TO_MB, MAX_ASSET_SIZE } from '../../constants.js';
|
|
5
6
|
import { detectNativeModules, errorMessage } from './detect-native-modules.js';
|
|
6
|
-
import {
|
|
7
|
+
import { isFolderLargerThan } from './is-folder-larger-than.js';
|
|
7
8
|
import { resolveResourceDependencies } from './resolve-dependencies.js';
|
|
8
9
|
import { shouldAutoResolveDependencies } from './should-auto-resolve-deps.js';
|
|
9
10
|
import { shouldTranspileFunction } from './should-transpile.js';
|
|
@@ -51,16 +52,52 @@ export async function prepareAsset({ blueprintFileDir, resource, installer, }) {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
// First check the size before trying to create the zip file
|
|
56
|
+
const result = isFolderLargerThan(functionPath, MAX_ASSET_SIZE);
|
|
57
|
+
if (result) {
|
|
56
58
|
throw new Error('Resource is larger than max asset size of 200 MB.');
|
|
57
59
|
}
|
|
58
|
-
|
|
60
|
+
const zip = await pathToZip(functionPath);
|
|
61
|
+
// Add up the uncompressed sizes of all entries to get a more accurate size of the asset being uploaded
|
|
62
|
+
const zipEntrySize = zip
|
|
63
|
+
.getEntries()
|
|
64
|
+
.map((entry) => entry.header.size)
|
|
65
|
+
.reduce((a, b) => a + b, 0);
|
|
66
|
+
if (zipEntrySize > MAX_ASSET_SIZE) {
|
|
67
|
+
throw new Error(`Asset size of ${(zipEntrySize / CONVERT_BYTES_TO_MB).toFixed(2)} MB exceeds the maximum allowed size of 200 MB.`);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
outputBuffer: await zip.toBufferPromise(),
|
|
72
|
+
fileExtension: 'zip',
|
|
73
|
+
assetParameter: 'src',
|
|
74
|
+
cleanup,
|
|
75
|
+
};
|
|
59
76
|
}
|
|
60
77
|
catch (err) {
|
|
61
78
|
return { success: false, cleanup, error: err instanceof Error ? err.message : `${err}` };
|
|
62
79
|
}
|
|
63
80
|
}
|
|
81
|
+
export async function pathToZip(path) {
|
|
82
|
+
const stats = await stat(path);
|
|
83
|
+
const zip = new AdmZip(undefined, {
|
|
84
|
+
fs: {
|
|
85
|
+
...fs,
|
|
86
|
+
// @ts-expect-error monkey patching to return matching mtime for builds
|
|
87
|
+
statSync(path, options) {
|
|
88
|
+
const s = fs.statSync(path, options);
|
|
89
|
+
if (s)
|
|
90
|
+
s.mtime = new Date(0);
|
|
91
|
+
return s;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
if (stats.isDirectory())
|
|
96
|
+
zip.addLocalFolder(path);
|
|
97
|
+
else
|
|
98
|
+
zip.addLocalFile(path, '', 'index.js');
|
|
99
|
+
return zip;
|
|
100
|
+
}
|
|
64
101
|
/**
|
|
65
102
|
* Remove file that contains the current date to prevent the hash from changing in the final asset.
|
|
66
103
|
* @param functionPath The path to the built function
|