@sanity/runtime-cli 17.12.0 → 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.
Files changed (48) hide show
  1. package/README.md +19 -19
  2. package/dist/actions/blueprints/assets.d.ts +21 -20
  3. package/dist/actions/blueprints/assets.js +17 -87
  4. package/dist/actions/blueprints/logs-polling.d.ts +3 -5
  5. package/dist/actions/blueprints/logs-polling.js +4 -9
  6. package/dist/actions/blueprints/logs.d.ts +1 -2
  7. package/dist/actions/blueprints/logs.js +1 -3
  8. package/dist/actions/blueprints/operations-state.d.ts +10 -0
  9. package/dist/actions/blueprints/operations-state.js +42 -0
  10. package/dist/actions/blueprints/operations.d.ts +2 -9
  11. package/dist/actions/blueprints/operations.js +1 -15
  12. package/dist/actions/blueprints/pre-deploy.d.ts +14 -9
  13. package/dist/actions/blueprints/pre-deploy.js +83 -26
  14. package/dist/actions/blueprints/process-assets.d.ts +12 -11
  15. package/dist/actions/blueprints/process-assets.js +8 -7
  16. package/dist/actions/blueprints/stacks.d.ts +4 -4
  17. package/dist/actions/blueprints/stacks.js +2 -4
  18. package/dist/actions/blueprints/wait-for-operation.d.ts +1 -10
  19. package/dist/actions/blueprints/wait-for-operation.js +56 -101
  20. package/dist/baseCommands.js +6 -1
  21. package/dist/cores/blueprints/deploy.js +28 -6
  22. package/dist/cores/blueprints/destroy.js +4 -2
  23. package/dist/cores/blueprints/doctor.js +1 -1
  24. package/dist/cores/blueprints/init.js +3 -1
  25. package/dist/cores/blueprints/logs.js +11 -5
  26. package/dist/cores/functions/build.js +4 -5
  27. package/dist/cores/functions/test.js +3 -1
  28. package/dist/cores/index.js +3 -1
  29. package/dist/server/handlers/invoke.js +1 -1
  30. package/dist/utils/blueprints/resolve-deployed-resources.d.ts +2 -3
  31. package/dist/utils/blueprints/resolve-deployed-resources.js +2 -2
  32. package/dist/utils/blueprints/wait-for.d.ts +60 -0
  33. package/dist/utils/blueprints/wait-for.js +115 -0
  34. package/dist/utils/functions/child-runtime/durable.js +2 -2
  35. package/dist/utils/functions/invoke/assert-console.d.ts +5 -0
  36. package/dist/utils/functions/invoke/assert-console.js +60 -0
  37. package/dist/utils/functions/invoke/local.js +9 -0
  38. package/dist/utils/functions/is-folder-larger-than.d.ts +5 -0
  39. package/dist/utils/functions/is-folder-larger-than.js +38 -0
  40. package/dist/utils/functions/prepare-asset.d.ts +2 -0
  41. package/dist/utils/functions/prepare-asset.js +43 -6
  42. package/dist/utils/studios/prepare-asset.js +5 -3
  43. package/dist/utils/types.d.ts +18 -4
  44. package/dist/utils/types.js +0 -3
  45. package/oclif.manifest.json +1 -1
  46. package/package.json +2 -2
  47. package/dist/utils/functions/get-folder-size.d.ts +0 -5
  48. 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, log);
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
- log,
42
- showBanner: true,
43
- verbose,
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, log);
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 { pathToZip } from '../../actions/blueprints/assets.js';
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 zip = await pathToZip(prepResult.outputPath);
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, buffer);
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 / 1_048_576).toFixed(1);
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,6 +7,7 @@ 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
  export async function functionTestCore(options) {
12
13
  const { blueprint, log, error, args, flags, helpText } = options;
@@ -36,6 +37,7 @@ export async function functionTestCore(options) {
36
37
  };
37
38
  }
38
39
  try {
40
+ const fetchFn = createTracedFetch(log);
39
41
  const resource = findFunctionInResources(blueprint.resources, fnName); // throws if not found
40
42
  const resolvedResources = await resolveDeployedResources({
41
43
  resources: blueprint.resources,
@@ -43,7 +45,7 @@ export async function functionTestCore(options) {
43
45
  auth: config.token && blueprint.scopeType && blueprint.scopeId
44
46
  ? { token: config.token, scopeType: blueprint.scopeType, scopeId: blueprint.scopeId }
45
47
  : undefined,
46
- logger: log,
48
+ fetchFn,
47
49
  });
48
50
  const input = {
49
51
  flags,
@@ -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 stackResponse = await getStack({ stackId, auth, logger: config.log });
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
- logger: silentLogger,
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
- logger: Logger;
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, logger, }: ResolveOptions): Promise<BlueprintResource[]>;
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, logger, }) {
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, logger });
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
+ }
@@ -107,13 +107,13 @@ export async function runDurable(handler, {context, event}, io) {
107
107
  }
108
108
  return state
109
109
  }),
110
- delegate: ({name, handler, input}) =>
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, input)
116
+ return io.invoke(fn, event)
117
117
  }),
118
118
  }
119
119
 
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Asserts that a given source file does not contain any `console` usage.
3
+ * @param src
4
+ */
5
+ export declare const assertConsole: (src: string) => void;
@@ -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,5 @@
1
+ import { type PathLike } from 'node:fs';
2
+ /**
3
+ * Recursively validates the total size of a folder, including all nested files and subdirectories.
4
+ */
5
+ export declare function isFolderLargerThan(folder: PathLike, maxBytes: number): boolean;
@@ -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 { MAX_ASSET_SIZE } from '../../constants.js';
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 { getFolderSize } from './get-folder-size.js';
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
- const size = getFolderSize(functionPath);
55
- if (size > MAX_ASSET_SIZE) {
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
- return { success: true, outputPath: functionPath, cleanup };
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
@@ -1,5 +1,5 @@
1
1
  import { createWriteStream } from 'node:fs';
2
- import { mkdtemp, rm, stat } from 'node:fs/promises';
2
+ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import path, { basename, dirname, join } from 'node:path';
5
5
  import { pipeline } from 'node:stream/promises';
@@ -105,11 +105,13 @@ export async function prepareStudioAsset({ blueprintFileDir, resource, }) {
105
105
  }
106
106
  // tar/gzip/write the built output
107
107
  await pipeline(pack(dirname(outDir), { entries: [basename(outDir)] }), createGzip(), createWriteStream(outFile));
108
+ const buffer = await readFile(outFile);
108
109
  return {
109
110
  success: true,
110
111
  cleanup,
111
- // studio must be pre built (e.g. with `npm run build`) and tarred/gzipped
112
- outputPath: outFile,
112
+ outputBuffer: buffer,
113
+ fileExtension: 'tar.gz',
114
+ assetParameter: 'src',
113
115
  };
114
116
  }
115
117
  catch (error) {
@@ -20,8 +20,6 @@ export type FunctionResource = FunctionGroqResource | BlueprintPubSubFunctionRes
20
20
  export type EventFunctionResource = Exclude<FunctionResource, BlueprintPubSubFunctionResource>;
21
21
  /** The `event` of any function resource that declares one */
22
22
  export type FunctionResourceEvent = EventFunctionResource['event'];
23
- /** Resource types that carry a deployable asset (have src: string) */
24
- export type AssetResource = FunctionResource | BlueprintStudioResource;
25
23
  /** @internal */
26
24
  export interface AuthParams {
27
25
  token: string;
@@ -52,7 +50,6 @@ export declare function isRoleResource(r: unknown): r is BlueprintRoleResource;
52
50
  export declare function isDatasetResource(r: unknown): r is BlueprintDatasetResource;
53
51
  export declare function isStudioResource(r: BlueprintResource): r is BlueprintStudioResource;
54
52
  export declare function isDurableFunctionResource(r: unknown): r is BlueprintDurableFunctionResource;
55
- export declare function isAssetResource(r: BlueprintResource): r is AssetResource;
56
53
  export declare function isWebhookResource(r: unknown): r is BlueprintDocumentWebhookResource;
57
54
  interface StackBase {
58
55
  id: string;
@@ -301,13 +298,30 @@ export declare const INSTALLER_OPTIONS: readonly ['npm', 'pnpm', 'yarn'];
301
298
  export type InstallerType = 'npm' | 'pnpm' | 'yarn';
302
299
  export type AssetPrepResult = {
303
300
  success: true;
304
- outputPath: string;
301
+ outputBuffer: Buffer;
302
+ fileExtension: string;
303
+ assetParameter: string;
305
304
  cleanup: () => Promise<void>;
306
305
  } | {
307
306
  success: false;
308
307
  cleanup: () => Promise<void>;
309
308
  error: string;
310
309
  };
310
+ export type PrepAssetFn<R extends BlueprintResource> = (options: {
311
+ resource: R;
312
+ blueprintFileDir: string;
313
+ installer?: InstallerType;
314
+ }) => Promise<AssetPrepResult>;
315
+ export type PreDeployConfig = {
316
+ /** The resource type this config applies to or a function that returns true if this config applies to the given type. */
317
+ resourceType: string | ((type: string) => boolean);
318
+ /** Function that takes a resource and returns a buffer to be sent to blueprints. */
319
+ prepareAsset?: PrepAssetFn<BlueprintResource>;
320
+ };
321
+ export type AssetResource<R extends BlueprintResource> = {
322
+ resource: R;
323
+ prepareAsset: PrepAssetFn<R>;
324
+ };
311
325
  export type Warning = {
312
326
  code: string;
313
327
  suggestions: string[];