@sanity/runtime-cli 17.0.0 → 17.1.0-next.gcfbd613

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 (81) hide show
  1. package/README.md +24 -23
  2. package/dist/actions/blueprints/assets.d.ts +2 -1
  3. package/dist/actions/blueprints/assets.js +2 -2
  4. package/dist/actions/blueprints/blueprint.d.ts +1 -1
  5. package/dist/actions/blueprints/wait-for-operation.js +4 -0
  6. package/dist/actions/functions/test.js +1 -1
  7. package/dist/actions/sanity/examples.d.ts +4 -4
  8. package/dist/baseCommands.d.ts +4 -4
  9. package/dist/baseCommands.js +13 -2
  10. package/dist/commands/blueprints/add.d.ts +1 -1
  11. package/dist/commands/blueprints/config.d.ts +1 -1
  12. package/dist/commands/blueprints/deploy.d.ts +1 -1
  13. package/dist/commands/blueprints/destroy.d.ts +1 -1
  14. package/dist/commands/blueprints/doctor.d.ts +0 -1
  15. package/dist/commands/blueprints/doctor.js +3 -7
  16. package/dist/commands/blueprints/info.d.ts +1 -1
  17. package/dist/commands/blueprints/logs.d.ts +1 -1
  18. package/dist/commands/blueprints/mint-deploy-token.d.ts +1 -1
  19. package/dist/commands/blueprints/plan.d.ts +1 -1
  20. package/dist/commands/blueprints/promote.d.ts +1 -1
  21. package/dist/commands/blueprints/stacks.d.ts +1 -1
  22. package/dist/commands/functions/add.d.ts +1 -1
  23. package/dist/commands/functions/build.d.ts +1 -1
  24. package/dist/commands/functions/dev.d.ts +1 -1
  25. package/dist/commands/functions/env/add.d.ts +1 -1
  26. package/dist/commands/functions/env/list.d.ts +1 -1
  27. package/dist/commands/functions/env/remove.d.ts +1 -1
  28. package/dist/commands/functions/logs.d.ts +1 -1
  29. package/dist/commands/functions/test.d.ts +1 -1
  30. package/dist/config.d.ts +1 -0
  31. package/dist/config.js +7 -1
  32. package/dist/constants.d.ts +1 -0
  33. package/dist/constants.js +1 -0
  34. package/dist/cores/blueprints/deploy.js +5 -4
  35. package/dist/cores/blueprints/doctor.js +15 -12
  36. package/dist/cores/functions/build.js +3 -2
  37. package/dist/cores/functions/test.js +48 -2
  38. package/dist/server/app.js +39 -5
  39. package/dist/server/handlers/invoke.d.ts +2 -2
  40. package/dist/server/handlers/invoke.js +7 -4
  41. package/dist/server/static/components/console-panel.js +59 -8
  42. package/dist/server/static/components/rule-panel.js +11 -8
  43. package/dist/server/static/components/shared-styles.js +24 -21
  44. package/dist/server/static/favicon-dark.svg +1 -0
  45. package/dist/server/static/favicon.svg +1 -0
  46. package/dist/server/static/index.html +2 -2
  47. package/dist/server/static/vendor/vendor.bundle.js +115 -61
  48. package/dist/server/static/web-socket.js +41 -0
  49. package/dist/utils/functions/child-process-wrapper.js +62 -7
  50. package/dist/utils/functions/find-entry-point.d.ts +1 -1
  51. package/dist/utils/functions/find-entry-point.js +10 -12
  52. package/dist/utils/functions/{invoke-local.d.ts → invoke/local.d.ts} +1 -1
  53. package/dist/utils/functions/invoke/local.js +306 -0
  54. package/dist/utils/functions/invoke/nested.d.ts +25 -0
  55. package/dist/utils/functions/invoke/nested.js +41 -0
  56. package/dist/utils/functions/invoke/prepare.d.ts +20 -0
  57. package/dist/utils/functions/invoke/prepare.js +101 -0
  58. package/dist/utils/functions/package-json.d.ts +1 -2
  59. package/dist/utils/functions/package-json.js +4 -6
  60. package/dist/utils/functions/prepare-asset.d.ts +2 -2
  61. package/dist/utils/functions/prepare-asset.js +18 -7
  62. package/dist/utils/functions/resolve-dependencies.d.ts +4 -1
  63. package/dist/utils/functions/resolve-dependencies.js +5 -6
  64. package/dist/utils/functions/resource-to-arc.d.ts +1 -1
  65. package/dist/utils/functions/resource-to-arc.js +4 -5
  66. package/dist/utils/functions/should-transpile.d.ts +7 -1
  67. package/dist/utils/functions/should-transpile.js +10 -10
  68. package/dist/utils/functions/transpile/function.d.ts +4 -1
  69. package/dist/utils/functions/transpile/function.js +9 -14
  70. package/dist/utils/functions/transpile/verify-handler.js +1 -1
  71. package/dist/utils/isTrueish.d.ts +6 -0
  72. package/dist/utils/isTrueish.js +9 -0
  73. package/dist/utils/logger.d.ts +7 -5
  74. package/dist/utils/logger.js +33 -1
  75. package/dist/utils/progress.d.ts +5 -0
  76. package/dist/utils/progress.js +40 -0
  77. package/dist/utils/types.d.ts +67 -5
  78. package/oclif.manifest.json +5 -4
  79. package/package.json +28 -23
  80. package/dist/server/static/hot-reload.js +0 -16
  81. package/dist/utils/functions/invoke-local.js +0 -185
@@ -19,7 +19,8 @@ export async function blueprintDeployCore(options) {
19
19
  const noWait = flags['no-wait'] || false;
20
20
  const stackName = flags['new-stack-name'] ?? deployedStack.name;
21
21
  const installer = flags['fn-installer'] || undefined;
22
- const workspaceWarnings = getWarnings({ dir: dirname(blueprint.fileInfo.blueprintFilePath) });
22
+ const blueprintFileDir = dirname(blueprint.fileInfo.blueprintFilePath);
23
+ const workspaceWarnings = getWarnings({ dir: blueprintFileDir });
23
24
  log(`Deploying "${stackName}" ${niceId(deployedStack.id)}...`);
24
25
  try {
25
26
  const { ok: checkOk, stack: currentStack } = await getStack({ stackId, auth, logger: log });
@@ -42,7 +43,7 @@ export async function blueprintDeployCore(options) {
42
43
  if (assetResources.length > 0) {
43
44
  log('Processing assets...');
44
45
  for (const resource of assetResources) {
45
- const preDeployResult = await preDeploy(resource, { auth, installer, log });
46
+ const preDeployResult = await preDeploy(resource, { auth, blueprintFileDir, installer, log });
46
47
  if (!preDeployResult.success)
47
48
  return preDeployResult;
48
49
  const idx = resources.indexOf(resource);
@@ -152,7 +153,7 @@ export async function blueprintDeployCore(options) {
152
153
  }
153
154
  }
154
155
  async function preDeploy(resource, options) {
155
- const { auth, installer, log } = options;
156
+ const { auth, blueprintFileDir, installer, log } = options;
156
157
  const fnSpinner = log.ora({ text: `Processing ${resource.name}...`, prefixText: ' ' }).start();
157
158
  const warnTimer = setTimeout(() => {
158
159
  fnSpinner.text = `Still processing ${resource.name}, this can take a moment...`;
@@ -161,7 +162,7 @@ async function preDeploy(resource, options) {
161
162
  let result;
162
163
  try {
163
164
  result = await Promise.race([
164
- stashAsset({ resource, auth, logger: log, installer }),
165
+ stashAsset({ resource, blueprintFileDir, auth, logger: log, installer }),
165
166
  new Promise((_, reject) => {
166
167
  assetTimeoutTimer = setTimeout(() => {
167
168
  reject(new Error(`Processing ${resource.name} timed out after ${assetTimeoutS}s`));
@@ -2,13 +2,13 @@ import { readFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import { arch, cwd, env, version as nodeVersion, platform } from 'node:process';
4
4
  import * as resolve from 'empathic/resolve';
5
- import ora from 'ora';
6
5
  import { loadAndParseBlueprint, } from '../../actions/blueprints/blueprint.js';
7
6
  import { readConfigFile } from '../../actions/blueprints/config.js';
8
7
  import { resolveIds } from '../../actions/blueprints/resolve.js';
9
8
  import { getStack } from '../../actions/blueprints/stacks.js';
10
9
  import config, { RUNTIME_CLI_VERSION } from '../../config.js';
11
10
  import { formatDateTime } from '../../utils/display/dates.js';
11
+ import { presentBlueprintParserErrors } from '../../utils/display/errors.js';
12
12
  import { advise, check, filePathRelativeToCwd, niceId, renderSection, severe, unsure, } from '../../utils/display/presenters.js';
13
13
  import { styleText } from '../../utils/style-text.js';
14
14
  import { createTracedFetch } from '../../utils/traced-fetch.js';
@@ -55,7 +55,7 @@ export async function blueprintDoctorCore(options) {
55
55
  envRows.push(['CLI', bin]);
56
56
  const configRows = [];
57
57
  const stackRows = [];
58
- const spinner = ora('Running diagnostics...').start();
58
+ const spinner = log.ora('Running diagnostics...').start();
59
59
  // --- ONLINE ---
60
60
  const fetchFn = createTracedFetch(log);
61
61
  try {
@@ -109,6 +109,7 @@ export async function blueprintDoctorCore(options) {
109
109
  status: false,
110
110
  detail: `${blueprint.errors.length} error(s)`,
111
111
  };
112
+ log.verbose(presentBlueprintParserErrors(blueprint.errors));
112
113
  }
113
114
  }
114
115
  catch {
@@ -186,6 +187,8 @@ export async function blueprintDoctorCore(options) {
186
187
  stackRows.push(['Operation', `${op.status}${timestamp}`]);
187
188
  if (op.userMessage)
188
189
  stackRows.push([' Message', `"${op.userMessage}"`]);
190
+ if (v && op.systemMessage)
191
+ stackRows.push([' System', `"${op.systemMessage}"`]);
189
192
  }
190
193
  if (stack.resources) {
191
194
  stackRows.push(['Resources', `${stack.resources.length}`]);
@@ -275,20 +278,20 @@ export async function blueprintDoctorCore(options) {
275
278
  }
276
279
  // --- RENDER REPORT ---
277
280
  spinner.stop();
278
- // Environment (verbose)
279
- log.verbose(renderSection('Environment', envRows));
280
- log.verbose('');
281
- // Configuration (verbose)
281
+ // Environment
282
+ log(renderSection('Environment', envRows));
283
+ log('');
284
+ // Configuration
282
285
  if (configRows.length > 0) {
283
- log.verbose(renderSection('Configuration', configRows));
284
- log.verbose('');
286
+ log(renderSection('Configuration', configRows));
287
+ log('');
285
288
  }
286
- // Deployment (verbose)
289
+ // Deployment
287
290
  if (stackRows.length > 0) {
288
- log.verbose(renderSection('Deployment', stackRows));
289
- log.verbose('');
291
+ log(renderSection('Deployment', stackRows));
292
+ log('');
290
293
  }
291
- // Checks (always visible)
294
+ // Checks
292
295
  const maxLabel = Math.max(...Object.values(diagLookup).map((l) => l.length));
293
296
  log(styleText('bold', 'Checks'));
294
297
  let allGood = true;
@@ -18,13 +18,14 @@ export async function functionBuildCore(options) {
18
18
  functions = allFunctions;
19
19
  }
20
20
  const installer = flags['fn-installer'] || undefined;
21
- const outDir = flags['out-dir'] || path.join(path.dirname(blueprint.fileInfo.blueprintFilePath), '.build');
21
+ const blueprintFileDir = path.dirname(blueprint.fileInfo.blueprintFilePath);
22
+ const outDir = flags['out-dir'] || path.join(blueprintFileDir, '.build');
22
23
  await fs.promises.mkdir(outDir, { recursive: true });
23
24
  const errors = [];
24
25
  const built = [];
25
26
  for (const resource of functions) {
26
27
  const spinner = log.ora(`Building ${resource.name}...`).start();
27
- const prepResult = await prepareAsset({ resource }, { installer });
28
+ const prepResult = await prepareAsset({ resource, blueprintFileDir, installer });
28
29
  if (prepResult.success === false) {
29
30
  spinner.fail(`Failed to build ${resource.name}`);
30
31
  errors.push({ name: resource.name, error: prepResult.error ?? 'Unknown error' });
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
2
+ import { dirname, join } from 'node:path';
3
3
  import { cwd } from 'node:process';
4
4
  import { testAction } from '../../actions/functions/test.js';
5
5
  import config from '../../config.js';
@@ -8,6 +8,7 @@ import { resolveDeployedResources } from '../../utils/blueprints/resolve-deploye
8
8
  import buildPayload from '../../utils/functions/build-payload.js';
9
9
  import { fetchAsset, fetchDocument } from '../../utils/functions/fetch-document.js';
10
10
  import { findFunctionInResources, getFunctionNames } from '../../utils/functions/find.js';
11
+ import { runFunction } from '../../utils/functions/invoke/nested.js';
11
12
  import { parseJsonObject } from '../../utils/parse-json-object.js';
12
13
  import { isEventType, } from '../../utils/types.js';
13
14
  export async function functionTestCore(options) {
@@ -49,6 +50,7 @@ export async function functionTestCore(options) {
49
50
  : undefined,
50
51
  logger: log,
51
52
  });
53
+ const blueprintFileDir = dirname(blueprint.fileInfo.blueprintFilePath);
52
54
  const contextOptions = docFunction || mediaFunction
53
55
  ? {
54
56
  clientOptions: {
@@ -62,8 +64,9 @@ export async function functionTestCore(options) {
62
64
  functionResourceType: 'project',
63
65
  functionResourceId: projectId || '',
64
66
  resources: resolvedResources,
67
+ blueprintFileDir,
65
68
  }
66
- : { clientOptions: {}, resources: resolvedResources };
69
+ : { clientOptions: {}, resources: resolvedResources, blueprintFileDir };
67
70
  // If the user sets the flag to use the real token set it in our options
68
71
  if (withUserToken) {
69
72
  contextOptions.clientOptions.token = config.token || undefined;
@@ -146,12 +149,54 @@ export async function functionTestCore(options) {
146
149
  : {
147
150
  event: 'scheduled',
148
151
  };
152
+ // Records each nested context.invoke() so they can be surfaced in order after the spinner resolves.
153
+ const nestedRuns = [];
154
+ let nestedOrder = 0;
155
+ /**
156
+ * Re-entrant callback that resolves a nested context.invoke(name, payload):
157
+ * it finds the named function in the same blueprint, reuses the already
158
+ * resolved resources, and runs it via the shared invoke-chain helper (which
159
+ * applies the depth/cycle guard and extends the call stack). Passing itself
160
+ * as onNestedInvoke keeps it recursable. Every run is recorded so it can be
161
+ * surfaced after the spinner resolves, including failed ones — a downstream
162
+ * error is fire-and-forget (it doesn't reject the caller) but is still shown.
163
+ */
164
+ const runNested = async (nestedName, nestedPayload, ctx, depth, callStack) => {
165
+ const order = ++nestedOrder;
166
+ const nestedResource = findFunctionInResources(blueprint.resources, nestedName);
167
+ const result = await runFunction(nestedResource, { payload: { data: nestedPayload }, event: 'nested' }, { ...ctx, resources: resolvedResources }, { depth, callStack, onNestedInvoke: runNested });
168
+ nestedRuns.push({ order, name: nestedName, logs: result.logs, error: result.error });
169
+ return result;
170
+ };
171
+ const logNestedRuns = () => {
172
+ for (const run of [...nestedRuns].sort((a, b) => a.order - b.order)) {
173
+ log(`↳ BEGIN: invoke('${run.name}')`);
174
+ // Show any logs received
175
+ if (run.logs?.trim()) {
176
+ log(run.logs.trim());
177
+ }
178
+ // Before any error occurs
179
+ if (run.error) {
180
+ log(run.error.stack || run.error.message || run.error.name);
181
+ }
182
+ log(`↳ END: invoke('${run.name}')`);
183
+ }
184
+ };
149
185
  const spinner = log.ora('Executing function...').start();
150
186
  const { json: response, logs, error, } = await testAction(resource, invokeOptions, contextOptions, {
151
187
  timeout: timeout ? timeout : resource.timeout,
188
+ onNestedInvoke: runNested,
189
+ depth: 0,
190
+ callStack: [fnName],
152
191
  });
153
192
  if (error) {
154
193
  spinner.fail('Function execution failed.');
194
+ // Surface the function's own logs even on failure
195
+ if (logs?.trim()) {
196
+ log('Logs:');
197
+ log(logs);
198
+ }
199
+ logNestedRuns();
155
200
  const errorMessage = error.stack || error.message || error.name;
156
201
  const isTimeout = error.message?.includes('Timeout');
157
202
  return {
@@ -166,6 +211,7 @@ export async function functionTestCore(options) {
166
211
  spinner.succeed('Function execution succeeded.');
167
212
  log('Logs:');
168
213
  log(logs || '');
214
+ logNestedRuns();
169
215
  if (response) {
170
216
  log('Response:');
171
217
  log(JSON.stringify(response, null, 2));
@@ -1,7 +1,8 @@
1
1
  import { existsSync, readFileSync, watchFile } from 'node:fs';
2
2
  import * as http from 'node:http';
3
+ import { dirname } from 'node:path';
3
4
  import { default as mime } from 'mime-types';
4
- import { WebSocketServer } from 'ws';
5
+ import { WebSocket, WebSocketServer } from 'ws';
5
6
  import { findBlueprintFile, loadAndParseBlueprint } from '../actions/blueprints/blueprint.js';
6
7
  import { readConfigFile } from '../actions/blueprints/config.js';
7
8
  import { resolveIds } from '../actions/blueprints/resolve.js';
@@ -10,6 +11,35 @@ import { isRecord } from '../utils/is-record.js';
10
11
  import { isEventType, isGroqContextOptions, } from '../utils/types.js';
11
12
  import { handleInvokeRequest } from './handlers/invoke.js';
12
13
  const app = (host, port, validateResources, executionOptions) => {
14
+ const wss = new WebSocketServer({ port: 8974 });
15
+ // Send a JSON envelope to every connected UI client.
16
+ const broadcast = (envelope) => {
17
+ const message = JSON.stringify(envelope);
18
+ for (const client of wss.clients) {
19
+ if (client.readyState === WebSocket.OPEN)
20
+ client.send(message);
21
+ }
22
+ };
23
+ let nestedOrder = 0;
24
+ /**
25
+ * Re-entrant callback that resolves a nested `context.invoke(name, payload)` by
26
+ * re-entering the invoke pipeline (which spawns its own child) and streams the
27
+ * nested run to the UI. Passing `nestedInvoke` to itself keeps it recursable.
28
+ */
29
+ const nestedInvoke = async (name, payload, ctx, depth, callStack) => {
30
+ const order = ++nestedOrder;
31
+ const result = await handleInvokeRequest(name, { data: payload }, { event: 'nested' }, ctx, validateResources, executionOptions, depth, callStack, nestedInvoke);
32
+ broadcast({
33
+ type: 'nested-invoke',
34
+ name,
35
+ order,
36
+ logs: result.logs,
37
+ json: result.json,
38
+ error: result.error,
39
+ timings: result.timings,
40
+ });
41
+ return result;
42
+ };
13
43
  const requestListener = async (req, res) => {
14
44
  res.setHeader('Content-Type', 'application/json');
15
45
  switch (true) {
@@ -39,7 +69,11 @@ const app = (host, port, validateResources, executionOptions) => {
39
69
  req.on('end', async () => {
40
70
  res.setHeader('Content-Type', 'application/json');
41
71
  try {
42
- const { data, func: functionName, metadata } = parseInvokeRequest(Buffer.concat(body));
72
+ const fileInfo = findBlueprintFile();
73
+ if (!fileInfo) {
74
+ throw Error('Could not find a Blueprint manifest (sanity.blueprint.ts, .js, or .json). Run `blueprints init` to create one.');
75
+ }
76
+ const { data, func: functionName, metadata, } = parseInvokeRequest(Buffer.concat(body), dirname(fileInfo.blueprintFilePath));
43
77
  const { context, event } = data;
44
78
  // replace user token if required
45
79
  if (isGroqContextOptions(context)) {
@@ -50,7 +84,7 @@ const app = (host, port, validateResources, executionOptions) => {
50
84
  delete context.clientOptions.token;
51
85
  }
52
86
  }
53
- const result = await handleInvokeRequest(functionName, event, metadata, context, validateResources, executionOptions);
87
+ const result = await handleInvokeRequest(functionName, event, metadata, context, validateResources, executionOptions, 0, [], nestedInvoke);
54
88
  // Add Server-Timing header
55
89
  const timingHeaders = [];
56
90
  for (const [key, value] of Object.entries(result.timings)) {
@@ -235,7 +269,6 @@ const app = (host, port, validateResources, executionOptions) => {
235
269
  };
236
270
  const server = http.createServer(requestListener);
237
271
  server.listen(port, host, () => { });
238
- const wss = new WebSocketServer({ port: 8974 });
239
272
  wss.on('connection', function connection(ws) {
240
273
  ws.on('error', console.error);
241
274
  const fileInfo = findBlueprintFile();
@@ -284,7 +317,7 @@ function buildApiUrl(projectId, dataset, docId, apiUrl) {
284
317
  function buildAssetUrl(mediaLibraryId, docId, apiUrl) {
285
318
  return `${apiUrl}/v2025-03-24/media-libraries/${mediaLibraryId}/doc/${docId}`;
286
319
  }
287
- function parseInvokeRequest(body) {
320
+ function parseInvokeRequest(body, blueprintFileDir) {
288
321
  let json;
289
322
  try {
290
323
  json = JSON.parse(body.toString('utf8'));
@@ -376,6 +409,7 @@ function parseInvokeRequest(body) {
376
409
  context: {
377
410
  ...context,
378
411
  clientOptions,
412
+ blueprintFileDir,
379
413
  // Provide default values for required properties if not present
380
414
  eventResourceType: typeof context.eventResourceType === 'string' ? context.eventResourceType : '',
381
415
  eventResourceId: typeof context.eventResourceId === 'string' ? context.eventResourceId : '',
@@ -1,4 +1,4 @@
1
- import type { InvocationResponse, InvokeContextOptions, InvokeExecutionOptions, InvokePayloadMetadata } from '../../utils/types.js';
2
- export declare function handleInvokeRequest(functionName: string, event: Record<string, unknown>, metadata: InvokePayloadMetadata, context: InvokeContextOptions, validateResources: boolean, executionOptions?: Partial<InvokeExecutionOptions>): Promise<InvocationResponse & {
1
+ import type { InvocationResponse, InvokeContextOptions, InvokeExecutionOptions, InvokePayloadMetadata, NestedInvoke } from '../../utils/types.js';
2
+ export declare function handleInvokeRequest(functionName: string, event: Record<string, unknown>, metadata: InvokePayloadMetadata, context: InvokeContextOptions, validateResources: boolean, executionOptions?: Partial<InvokeExecutionOptions>, depth?: number, callStack?: string[], onNestedInvoke?: NestedInvoke): Promise<InvocationResponse & {
3
3
  timings: Record<string, number>;
4
4
  }>;
@@ -2,10 +2,10 @@ import { readLocalBlueprint } from '../../actions/blueprints/blueprint.js';
2
2
  import config from '../../config.js';
3
3
  import { resolveDeployedResources } from '../../utils/blueprints/resolve-deployed-resources.js';
4
4
  import { findFunctionInResources } from '../../utils/functions/find.js';
5
- import invoke from '../../utils/functions/invoke-local.js';
5
+ import { runFunction } from '../../utils/functions/invoke/nested.js';
6
6
  import { Logger } from '../../utils/logger.js';
7
7
  const silentLogger = Logger(() => { });
8
- export async function handleInvokeRequest(functionName, event, metadata, context, validateResources, executionOptions) {
8
+ export async function handleInvokeRequest(functionName, event, metadata, context, validateResources, executionOptions, depth = 0, callStack = [], onNestedInvoke) {
9
9
  const start = performance.now();
10
10
  const { resources, stackId, scopeType, scopeId } = await readLocalBlueprint(silentLogger, {
11
11
  resources: validateResources,
@@ -22,9 +22,12 @@ export async function handleInvokeRequest(functionName, event, metadata, context
22
22
  payload: event,
23
23
  ...metadata,
24
24
  };
25
- const response = await invoke(resource, payload, { ...context, resources: resolvedResources }, {
25
+ const response = await runFunction(resource, payload, { ...context, resources: resolvedResources }, {
26
26
  forceColor: executionOptions?.forceColor ?? false,
27
- timeout: executionOptions?.timeout ?? resource.timeout,
27
+ timeout: executionOptions?.timeout,
28
+ depth,
29
+ callStack,
30
+ onNestedInvoke,
28
31
  });
29
32
  const timings = { ...response.timings, 'blueprint:read': readBlueprintTime };
30
33
  return { ...response, timings };
@@ -1,4 +1,4 @@
1
- /* globals customElements document */
1
+ /* globals customElements document window */
2
2
  import {ApiBaseElement} from './api-base.js'
3
3
  import {getSharedStyleSheets} from './shared-styles.js'
4
4
 
@@ -36,6 +36,16 @@ template.innerHTML = `
36
36
  `
37
37
 
38
38
  class ConsolePanel extends ApiBaseElement {
39
+ /**
40
+ * Nested context.invoke() output for the current run, kept as a list so the
41
+ * final render can order it by call order. Nested runs finish innermost-first
42
+ * (A → B → C resolves C then B), so each carries an `order` stamped when it was
43
+ * initiated; the final render sorts by it. Buffered separately so it survives
44
+ * the result render, which would otherwise overwrite the console when Preserve
45
+ * Log is off.
46
+ */
47
+ nestedEntries = []
48
+
39
49
  constructor() {
40
50
  super()
41
51
  this.attachShadow({mode: 'open'}).appendChild(template.content.cloneNode(true))
@@ -45,25 +55,59 @@ class ConsolePanel extends ApiBaseElement {
45
55
  if (!this.consoleOutput || !result) return
46
56
 
47
57
  const {error, logs} = result
48
- let update = ''
58
+ // Always show the logs. Then append the error, rather than replacing the logs with it.
59
+ let update = logs ?? ''
49
60
  if (error) {
50
- // Display error details in the console
51
- update = (error.stack || error.message || error.name) ?? 'An error occurred.'
52
- } else {
53
- // Display regular logs
54
- update = logs ?? '' // Handle case where logs might be null/undefined
61
+ const errText = (error.stack || error.message || error.name) ?? 'An error occurred.'
62
+ update = update.trim() ? `${update.trimEnd()}\n${errText}` : errText
55
63
  }
56
64
 
57
65
  if (this.api.store.preserveLog) {
66
+ // Nested output was already appended live during the run; keep it in place.
58
67
  this.consoleOutput.innerText = this.consoleOutput.innerText + update
59
68
  } else {
60
- this.consoleOutput.innerText = update
69
+ // Re-render so the nested output is shown in call order rather than the
70
+ // completion order it streamed in (and so it survives this overwrite).
71
+ this.consoleOutput.innerText = update + this.renderNestedEntries()
72
+ }
73
+ }
74
+
75
+ // Join the nested runs in call order (the order they were initiated).
76
+ renderNestedEntries = () =>
77
+ [...this.nestedEntries]
78
+ .sort((a, b) => a.order - b.order)
79
+ .map((e) => e.text)
80
+ .join('')
81
+
82
+ updateNested = (event) => {
83
+ const nestedInvoke = event.detail
84
+ if (!this.consoleOutput || !nestedInvoke) return
85
+
86
+ const {name, order, logs, json, error} = nestedInvoke
87
+ // Always show the logs. Then append the error, rather than replacing the logs with it.
88
+ let body = logs ?? ''
89
+ if (error) {
90
+ const errText = (error.stack || error.message || error.name) ?? 'An error occurred.'
91
+ body = body.trim() ? `${body.trimEnd()}\n${errText}` : errText
92
+ } else if (typeof json !== 'undefined') {
93
+ body += `${body && !body.endsWith('\n') ? '\n' : ''}→ returned ${JSON.stringify(json)}\n`
61
94
  }
95
+
96
+ const text = `↳ BEGIN: invoke('${name}')\n${body.trim()}\n↳ END: invoke('${name}')\n`
97
+ this.nestedEntries.push({order, text})
98
+ // Append live for immediate feedback; the final result render reorders.
99
+ this.consoleOutput.innerText = this.consoleOutput.innerText + text
100
+ }
101
+
102
+ // A new invocation is starting; drop the previous run's nested output.
103
+ resetNested = ({inprogress}) => {
104
+ if (inprogress) this.nestedEntries = []
62
105
  }
63
106
 
64
107
  clear = () => {
65
108
  const backUp = this.api.store.preserveLog
66
109
  this.api.store.result = {logs: undefined, error: undefined}
110
+ this.nestedEntries = []
67
111
  this.consoleOutput.innerText = ''
68
112
  this.api.store.preserveLog = backUp
69
113
  }
@@ -75,9 +119,14 @@ class ConsolePanel extends ApiBaseElement {
75
119
  this.consoleOutput = this.shadowRoot.querySelector('#console-output')
76
120
  this.addEventListener('clear-console', this.clear)
77
121
 
122
+ // Nested context.invoke() runs arrive as a stream of discrete DOM events
123
+ // (not state) so none are dropped when several land in one frame.
124
+ window.addEventListener('nested-invoke', this.updateNested)
125
+
78
126
  // Subscribe to changes in the result state to update the console
79
127
  if (this.api) {
80
128
  this.api.subscribe(this.updateConsole, ['result'])
129
+ this.api.subscribe(this.resetNested, ['inprogress'])
81
130
  // Initial update in case result is already populated
82
131
  if (this.api.store.result) {
83
132
  this.updateConsole({result: this.api.store.result})
@@ -90,9 +139,11 @@ class ConsolePanel extends ApiBaseElement {
90
139
 
91
140
  disconnectedCallback() {
92
141
  this.removeEventListener('clear-console', this.clear)
142
+ window.removeEventListener('nested-invoke', this.updateNested)
93
143
  // Unsubscribe when the element is removed from the DOM
94
144
  if (this.api) {
95
145
  this.api.unsubscribe(this.updateConsole)
146
+ this.api.unsubscribe(this.resetNested)
96
147
  }
97
148
  }
98
149
  }
@@ -43,14 +43,17 @@ class RulePanel extends ApiBaseElement {
43
43
  break
44
44
  }
45
45
 
46
- const transaction = rule.state.update({
47
- changes: {
48
- from: 0,
49
- insert: func.event ? JSON.stringify(func.event, null, 2) : '',
50
- to: rule.state.doc.length,
51
- },
52
- })
53
- rule.dispatch(transaction)
46
+ // Guard against the sate not being setup
47
+ if (rule?.state) {
48
+ const transaction = rule.state.update({
49
+ changes: {
50
+ from: 0,
51
+ insert: func.event ? JSON.stringify(func.event, null, 2) : '',
52
+ to: rule.state.doc.length,
53
+ },
54
+ })
55
+ rule.dispatch(transaction)
56
+ }
54
57
  }
55
58
 
56
59
  async connectedCallback() {
@@ -1,31 +1,34 @@
1
1
  /* globals fetch */
2
2
 
3
- let cachedStyleSheets = null
3
+ let sharedStyleSheetsPromise = null
4
4
 
5
- export async function getSharedStyleSheets() {
6
- if (cachedStyleSheets) {
7
- return cachedStyleSheets
8
- }
5
+ async function loadSharedStyleSheets() {
6
+ // Fetch both stylesheets
7
+ const [mCssResponse, appCssResponse] = await Promise.all([
8
+ fetch('./vendor/m-.css'),
9
+ fetch('./components/app.css'),
10
+ ])
9
11
 
10
- try {
11
- // Fetch both stylesheets
12
- const [mCssResponse, appCssResponse] = await Promise.all([
13
- fetch('./vendor/m-.css'),
14
- fetch('./components/app.css'),
15
- ])
12
+ const [mCssText, appCssText] = await Promise.all([mCssResponse.text(), appCssResponse.text()])
16
13
 
17
- const [mCssText, appCssText] = await Promise.all([mCssResponse.text(), appCssResponse.text()])
14
+ // Create CSSStyleSheet objects
15
+ const mSheet = new CSSStyleSheet()
16
+ const appSheet = new CSSStyleSheet()
18
17
 
19
- // Create CSSStyleSheet objects
20
- const mSheet = new CSSStyleSheet()
21
- const appSheet = new CSSStyleSheet()
18
+ await Promise.all([mSheet.replace(mCssText), appSheet.replace(appCssText)])
22
19
 
23
- await Promise.all([mSheet.replace(mCssText), appSheet.replace(appCssText)])
20
+ return [mSheet, appSheet]
21
+ }
24
22
 
25
- cachedStyleSheets = [mSheet, appSheet]
26
- return cachedStyleSheets
27
- } catch (error) {
28
- console.error('Failed to load shared stylesheets:', error)
29
- return []
23
+ export function getSharedStyleSheets() {
24
+ if (!sharedStyleSheetsPromise) {
25
+ sharedStyleSheetsPromise = loadSharedStyleSheets().catch((error) => {
26
+ console.error('Failed to load shared stylesheets:', error)
27
+ // Reset so a later caller can retry instead of being stuck with [].
28
+ sharedStyleSheetsPromise = null
29
+ return []
30
+ })
30
31
  }
32
+
33
+ return sharedStyleSheetsPromise
31
34
  }
@@ -1,4 +1,5 @@
1
1
  <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <title>Sanity.io logo</title>
2
3
  <g clip-path="url(#clip0_2_1533)">
3
4
  <rect width="32" height="32" fill="#FFFFFF"/>
4
5
  <rect width="32" height="32" fill="#FFFFFF"/>
@@ -1,4 +1,5 @@
1
1
  <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <title>Sanity.io logo</title>
2
3
  <g clip-path="url(#clip0_2_1533)">
3
4
  <rect width="32" height="32" fill="#0B0B0B"/>
4
5
  <rect width="32" height="32" fill="#0B0B0B"/>
@@ -33,7 +33,7 @@
33
33
  <filters-component></filters-component>
34
34
  <input type="checkbox" id="nav-toggle" class="hidden">
35
35
  <nav id="sidebar" class="relative y-scroll max-h-100 pad-t-5 border-right overflow-auto nav-min-width">
36
- <label for="nav-toggle" id="nav-toggle-label" title="Toggle Functions list" class="z-10 nav-label"></label>
36
+ <label for="nav-toggle" id="nav-toggle-label" aria-label="Toggle Functions list" title="Toggle Functions list" class="z-10 nav-label"></label>
37
37
  <h4 class="config-label mar-t-0 mar-b-3 pad-l-3">Functions</h4>
38
38
  <function-list></function-list>
39
39
  </nav>
@@ -64,6 +64,6 @@
64
64
  <script src="./components/clear-button.js" type="module"></script>
65
65
  <script src="./components/help-button.js" type="module"></script>
66
66
  <script src="./components/fetch-button.js" type="module"></script>
67
- <script src="./hot-reload.js" type="module"></script>
67
+ <script src="./web-socket.js" type="module"></script>
68
68
  </body>
69
69
  </html>