@sanity/runtime-cli 16.0.3 → 17.1.0-next.g3cb2d2e

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 (41) hide show
  1. package/README.md +27 -22
  2. package/dist/actions/functions/test.js +1 -1
  3. package/dist/baseCommands.js +13 -2
  4. package/dist/commands/blueprints/init.js +5 -2
  5. package/dist/config.d.ts +1 -0
  6. package/dist/config.js +7 -1
  7. package/dist/constants.d.ts +1 -0
  8. package/dist/constants.js +1 -0
  9. package/dist/cores/blueprints/init.js +16 -2
  10. package/dist/cores/functions/test.js +44 -0
  11. package/dist/server/app.js +31 -3
  12. package/dist/server/handlers/invoke.d.ts +2 -2
  13. package/dist/server/handlers/invoke.js +7 -4
  14. package/dist/server/static/components/console-panel.js +59 -8
  15. package/dist/server/static/components/rule-panel.js +11 -8
  16. package/dist/server/static/components/shared-styles.js +24 -21
  17. package/dist/server/static/index.html +1 -1
  18. package/dist/server/static/vendor/design-tokens/color.css +48 -48
  19. package/dist/server/static/vendor/design-tokens/index.css +0 -1
  20. package/dist/server/static/vendor/vendor.bundle.js +3807 -3259
  21. package/dist/server/static/web-socket.js +41 -0
  22. package/dist/utils/functions/child-process-wrapper.js +62 -7
  23. package/dist/utils/functions/{invoke-local.d.ts → invoke/local.d.ts} +1 -1
  24. package/dist/utils/functions/invoke/local.js +306 -0
  25. package/dist/utils/functions/invoke/nested.d.ts +25 -0
  26. package/dist/utils/functions/invoke/nested.js +41 -0
  27. package/dist/utils/functions/invoke/prepare.d.ts +20 -0
  28. package/dist/utils/functions/invoke/prepare.js +90 -0
  29. package/dist/utils/interactive.d.ts +1 -0
  30. package/dist/utils/interactive.js +4 -0
  31. package/dist/utils/isTrueish.d.ts +6 -0
  32. package/dist/utils/isTrueish.js +9 -0
  33. package/dist/utils/logger.d.ts +1 -0
  34. package/dist/utils/logger.js +1 -0
  35. package/dist/utils/style-text.js +4 -5
  36. package/dist/utils/traced-fetch.js +2 -0
  37. package/dist/utils/types.d.ts +62 -4
  38. package/oclif.manifest.json +4 -3
  39. package/package.json +18 -14
  40. package/dist/server/static/hot-reload.js +0 -16
  41. package/dist/utils/functions/invoke-local.js +0 -185
@@ -5,6 +5,7 @@ export declare function Logger(log: (msg: string) => void, flags?: {
5
5
  json?: boolean;
6
6
  }): {
7
7
  (msg: string): void;
8
+ isTraceEnabled: boolean;
8
9
  trace(formatter: unknown, ...args: unknown[]): false | void;
9
10
  verbose(formatter: unknown, ...args: unknown[]): false | void;
10
11
  info(formatter: unknown, ...args: unknown[]): false | void;
@@ -13,6 +13,7 @@ export function Logger(log, flags = {}) {
13
13
  log(msg);
14
14
  };
15
15
  const level = flags.verbose ? LogLevel.VERBOSE : flags.trace ? LogLevel.TRACE : LogLevel.INFO;
16
+ logger.isTraceEnabled = level <= LogLevel.TRACE;
16
17
  logger.trace = (formatter, ...args) => level <= LogLevel.TRACE && logger(format(formatter, ...args));
17
18
  logger.verbose = (formatter, ...args) => level <= LogLevel.VERBOSE && logger(format(formatter, ...args));
18
19
  logger.info = (formatter, ...args) => level <= LogLevel.INFO && logger(format(formatter, ...args));
@@ -1,5 +1,6 @@
1
- import { env, stdout } from 'node:process';
1
+ import { env } from 'node:process';
2
2
  import { styleText as nodeStyleText } from 'node:util';
3
+ import { isInteractive } from './interactive.js';
3
4
  function supportsColor() {
4
5
  /*
5
6
  * https://force-color.org
@@ -32,10 +33,8 @@ function supportsColor() {
32
33
  if (env.NODE_DISABLE_COLORS !== undefined && env.NODE_DISABLE_COLORS !== '') {
33
34
  return false;
34
35
  }
35
- /*
36
- * If the stdout is a TTY, use colors.
37
- */
38
- return typeof stdout?.isTTY === 'boolean' && stdout.isTTY;
36
+ // if interactive, use colors
37
+ return isInteractive;
39
38
  }
40
39
  export const styleText = (format, text) => {
41
40
  if (!supportsColor())
@@ -173,6 +173,8 @@ export function createTracedFetch(logger, options) {
173
173
  ...options,
174
174
  };
175
175
  return async (input, init) => {
176
+ if (!logger.isTraceEnabled)
177
+ return fetch(input, init);
176
178
  const requestId = randomUUID().slice(0, 3);
177
179
  const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
178
180
  const method = init?.method || 'GET';
@@ -98,13 +98,14 @@ export interface InvokeGroqPayloadOptions {
98
98
  before: Record<string, unknown> | null;
99
99
  after: Record<string, unknown> | null;
100
100
  }
101
- export interface InvokeSchedulePayloadOptions {
101
+ export interface InvokeNonGroqPayloadOptions {
102
102
  payload?: Record<string, unknown>;
103
- event: 'scheduled';
103
+ event: NonGroqEventType;
104
104
  }
105
- export type InvokePayloadOptions = InvokeGroqPayloadOptions | InvokeSchedulePayloadOptions;
106
- export type InvokePayloadMetadata = Pick<InvokeGroqPayloadOptions, 'event' | 'before' | 'after'> | Pick<InvokeSchedulePayloadOptions, 'event'>;
105
+ export type InvokePayloadOptions = InvokeGroqPayloadOptions | InvokeNonGroqPayloadOptions;
106
+ export type InvokePayloadMetadata = Pick<InvokeGroqPayloadOptions, 'event' | 'before' | 'after'> | Pick<InvokeNonGroqPayloadOptions, 'event'>;
107
107
  export type EventType = 'create' | 'update' | 'delete';
108
+ export type NonGroqEventType = 'scheduled' | 'nested';
108
109
  export declare function isEventType(arg: string): arg is EventType;
109
110
  /** @internal */
110
111
  export interface InvokeGroqContextOptions {
@@ -140,7 +141,64 @@ export declare function isGroqContextOptions(context: InvokeContextOptions): con
140
141
  export interface InvokeExecutionOptions {
141
142
  forceColor?: boolean;
142
143
  timeout?: number;
144
+ /**
145
+ * Callback used to resolve a nested `context.invoke(name, payload)` made from
146
+ * within a running function. When absent, nested invokes are rejected.
147
+ * @internal
148
+ */
149
+ onNestedInvoke?: NestedInvoke;
150
+ /** Current invocation depth, used to bound nested `context.invoke` recursion. @internal */
151
+ depth?: number;
152
+ /** Function names already on the invoke call stack (innermost last), used to detect cycles. @internal */
153
+ callStack?: string[];
143
154
  }
155
+ /**
156
+ * Re-entrant callback that executes a named function from the blueprint on behalf
157
+ * of a nested `context.invoke(name, payload)` and returns its invocation result.
158
+ * @internal
159
+ */
160
+ export type NestedInvoke = (name: string, payload: Record<string, unknown>, context: InvokeContextOptions, depth: number, callStack: string[]) => Promise<InvocationResponse & {
161
+ timings: Record<string, number>;
162
+ }>;
163
+ /**
164
+ * IPC messages sent from the parent (invoke-local) down to a function child process.
165
+ * @internal
166
+ */
167
+ export type ParentToChildMessage = {
168
+ type: 'start';
169
+ srcPath: string;
170
+ payload: unknown;
171
+ } | {
172
+ type: 'invoke-response';
173
+ id: string;
174
+ ok: true;
175
+ result: {
176
+ json: JsonInvocationResponse | undefined;
177
+ };
178
+ } | {
179
+ type: 'invoke-response';
180
+ id: string;
181
+ ok: false;
182
+ error: InvocationError;
183
+ };
184
+ /**
185
+ * IPC messages sent from a function child process back up to the parent.
186
+ * `result` is terminal and triggers shutdown; `invoke-request` is a nested
187
+ * `context.invoke` awaiting a correlated `invoke-response`.
188
+ * @internal
189
+ */
190
+ export type ChildToParentMessage = {
191
+ type: 'invoke-request';
192
+ id: string;
193
+ name: string;
194
+ payload: Record<string, unknown>;
195
+ } | {
196
+ type: 'result';
197
+ json?: JsonInvocationResponse;
198
+ logs?: string;
199
+ errorLogs?: string;
200
+ error?: InvocationError;
201
+ };
144
202
  /** @internal */
145
203
  export interface InvocationResponse {
146
204
  error: InvocationError | undefined;
@@ -839,13 +839,14 @@
839
839
  "aliases": [],
840
840
  "args": {
841
841
  "dir": {
842
- "description": "Directory to create the local Blueprint in",
842
+ "description": "Directory to create the local Blueprint in (defaults to the current directory)",
843
843
  "name": "dir"
844
844
  }
845
845
  },
846
- "description": "A Blueprint is your local infrastructure-as-code configuration that defines Sanity resources (datasets, functions, etc.). A Stack is the remote deployment target where your Blueprint is applied.\n[NOTE: Currently, accounts are limited to three (3) Stacks per project scope.]\n\nThis is typically the first command you run in a new project. It creates a local Blueprint manifest file (sanity.blueprint.ts, .js, or .json) and provisions a new remote Stack.\nAdditionally, a Blueprint configuration file is created in .sanity/ containing the scope and Stack IDs. This is .gitignored by default.\n\nAfter initialization, use 'blueprints plan' to preview changes, then 'blueprints deploy' to apply them.",
846
+ "description": "A Blueprint is your local infrastructure-as-code configuration that defines Sanity resources (datasets, functions, etc.). A Stack is the remote deployment target where your Blueprint is applied.\n[NOTE: Currently, accounts are limited to three (3) Stacks per project scope.]\n\nThis is typically the first command you run in a new project. It creates a local Blueprint manifest file (sanity.blueprint.ts, .js, or .json) and provisions a new remote Stack.\nAdditionally, a Blueprint configuration file is created in .sanity/ containing the scope and Stack IDs. This is .gitignored by default.\n\nAfter initialization, use 'blueprints plan' to preview changes, then 'blueprints deploy' to apply them.\n\nRunning without a directory prompts to confirm the current directory. Run 'blueprints init .' to initialize in the current directory without a prompt.",
847
847
  "examples": [
848
848
  "<%= config.bin %> <%= command.id %>",
849
+ "<%= config.bin %> <%= command.id %> .",
849
850
  "<%= config.bin %> <%= command.id %> [directory]",
850
851
  "<%= config.bin %> <%= command.id %> --blueprint-type <json|js|ts>",
851
852
  "<%= config.bin %> <%= command.id %> --organization-id <organizationId>",
@@ -2996,5 +2997,5 @@
2996
2997
  ]
2997
2998
  }
2998
2999
  },
2999
- "version": "16.0.3"
3000
+ "version": "17.1.0-next.g3cb2d2e"
3000
3001
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanity/runtime-cli",
3
3
  "description": "Sanity's Runtime CLI for Blueprints and Functions",
4
- "version": "16.0.3",
4
+ "version": "17.1.0-next.g3cb2d2e",
5
5
  "author": "Sanity Runtime Team",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  }
22
22
  },
23
23
  "engines": {
24
- "node": ">=20.19"
24
+ "node": ">=22.12"
25
25
  },
26
26
  "files": [
27
27
  "./bin",
@@ -58,27 +58,27 @@
58
58
  "watch": "tsc --watch"
59
59
  },
60
60
  "dependencies": {
61
- "@architect/hydrate": "^5.0.2",
62
- "@architect/inventory": "^5.0.0",
63
- "@inquirer/prompts": "^8.4.2",
64
- "@oclif/core": "^4.11.0",
65
- "@oclif/plugin-help": "^6.2.46",
61
+ "@architect/hydrate": "~6.0.2",
62
+ "@architect/inventory": "~6.0.0",
63
+ "@inquirer/prompts": "^8.5.2",
64
+ "@oclif/core": "^4.11.6",
65
+ "@oclif/plugin-help": "^6.2.50",
66
66
  "@sanity-labs/design-tokens": "^0.0.2-alpha.2",
67
67
  "@sanity/blueprints": "^0.20.1",
68
68
  "@sanity/blueprints-parser": "^0.4.0",
69
- "@sanity/client": "^7.22.0",
69
+ "@sanity/client": "^7.22.1",
70
70
  "adm-zip": "^0.5.17",
71
71
  "array-treeify": "^0.1.5",
72
72
  "cardinal": "^2.1.1",
73
- "empathic": "^2.0.0",
73
+ "empathic": "^2.0.1",
74
74
  "eventsource": "^4.1.0",
75
- "groq-js": "^1.30.1",
75
+ "groq-js": "^1.30.2",
76
76
  "jiti": "^2.7.0",
77
77
  "mime-types": "^3.0.2",
78
78
  "ora": "^9.4.0",
79
79
  "tar-stream": "^3.2.0",
80
80
  "vite": "^8.0.16",
81
- "ws": "^8.20.0",
81
+ "ws": "^8.21.0",
82
82
  "xdg-basedir": "^5.1.0"
83
83
  },
84
84
  "devDependencies": {
@@ -94,10 +94,10 @@
94
94
  "@types/adm-zip": "^0.5.8",
95
95
  "@types/cardinal": "^2.1.1",
96
96
  "@types/mime-types": "^3.0.1",
97
- "@types/node": "20",
97
+ "@types/node": "^22",
98
98
  "@types/tar-stream": "^3.1.4",
99
99
  "@types/ws": "^8.18.1",
100
- "@vitest/coverage-v8": "^4.1.5",
100
+ "@vitest/coverage-v8": "4.1.9",
101
101
  "codemirror": "^6.0.2",
102
102
  "mentoss": "^0.13.0",
103
103
  "oclif": "^4.23.0",
@@ -107,7 +107,7 @@
107
107
  "shx": "^0.4.0",
108
108
  "ts-node": "^10.9.2",
109
109
  "typescript": "^6.0.3",
110
- "vitest": "4.1.5"
110
+ "vitest": "4.1.9"
111
111
  },
112
112
  "oclif": {
113
113
  "bin": "sanity-run",
@@ -129,5 +129,9 @@
129
129
  "publishConfig": {
130
130
  "access": "public",
131
131
  "provenance": false
132
+ },
133
+ "allowScripts": {
134
+ "fsevents@2.3.2": true,
135
+ "fsevents@2.3.3": true
132
136
  }
133
137
  }
@@ -1,16 +0,0 @@
1
- import apiConstructor from '../api.js'
2
-
3
- const api = apiConstructor()
4
-
5
- // Create WebSocket connection.
6
- const socket = new WebSocket('ws://localhost:8974')
7
-
8
- // Connection opened
9
- socket.addEventListener('open', () => {
10
- console.log('Watching for Blueprint changes')
11
- })
12
-
13
- // Listen for messages
14
- socket.addEventListener('message', (event) => {
15
- if (event.data === 'reload-blueprint') api.blueprint()
16
- })
@@ -1,185 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { performance } from 'node:perf_hooks';
3
- import { cwd } from 'node:process';
4
- import { setTimeout } from 'node:timers';
5
- import { fileURLToPath } from 'node:url';
6
- import * as groq from 'groq-js';
7
- import config from '../../config.js';
8
- import { groupBlueprintResources } from '../blueprints/group-resources.js';
9
- import { isGroqContextOptions, } from '../types.js';
10
- import { findFunctionEntryPoint } from './find-entry-point.js';
11
- import { cleanupTempPackageJson, createTempPackageJson, doesPackageJsonExists, } from './package-json.js';
12
- import { resolveResourceDependencies } from './resolve-dependencies.js';
13
- import { shouldAutoResolveDependencies } from './should-auto-resolve-deps.js';
14
- import { shouldTranspileFunction } from './should-transpile.js';
15
- import { transpileFunction } from './transpile/function.js';
16
- function getChildProcessWrapperPath() {
17
- return fileURLToPath(new URL('./child-process-wrapper.js', import.meta.url));
18
- }
19
- export function sanitizeLogs(logs) {
20
- return logs.replace(/([a-zA-Z0-9]{10})[a-zA-Z0-9]{65,}/g, '$1**********');
21
- }
22
- export async function applyGroqRule(resource, payload, projectId, dataset) {
23
- const { before, after, payload: data = null } = payload;
24
- // If there is no rule set return everything
25
- if (!resource.event)
26
- return data;
27
- // default groq rule is: gimme full doc content. otherwise, parse + eval custom rule
28
- // applying the GROQ filter may result in a slimmer set of documents
29
- if (resource.event.filter || resource.event.projection) {
30
- const query = `*[${resource.event.filter || ''}]${resource.event.projection || ''}`;
31
- try {
32
- const rule = groq.parse(query, { mode: 'delta' });
33
- const sanity = projectId && dataset ? { projectId, dataset } : undefined;
34
- const queryResults = await groq.evaluate(rule, { dataset: [data], before, after, sanity });
35
- const currentFunctionDocumentSet = await queryResults.get();
36
- return currentFunctionDocumentSet[0];
37
- }
38
- catch (groqErr) {
39
- const detail = groqErr instanceof Error && `; ${groqErr.message}`;
40
- throw new Error(`Invalid rule configuration: ${query}${detail}`, { cause: groqErr });
41
- }
42
- }
43
- // default groq rule so just return the data
44
- return data;
45
- }
46
- export default async function invoke(resource, payload, context, options) {
47
- if (!resource.src) {
48
- throw new Error(`Function resource "${resource.name}" is missing the 'src' property.`);
49
- }
50
- let cleanupBundle = async () => { };
51
- let functionPath = '';
52
- let bundleTimings;
53
- // If no package.json exists we will create a temporary one to avoid the
54
- // Module Type warning from node
55
- const existingPackageJson = doesPackageJsonExists(resource);
56
- const shouldTranspile = await shouldTranspileFunction(resource);
57
- if (shouldTranspile) {
58
- const transpileResult = await transpileFunction(resource);
59
- functionPath = await findFunctionEntryPoint(transpileResult.outputDir);
60
- bundleTimings = transpileResult.timings;
61
- cleanupBundle = transpileResult.cleanup;
62
- }
63
- else {
64
- functionPath = await findFunctionEntryPoint(resource.src, resource.displayName ?? resource.name);
65
- }
66
- const shouldResolveDependencies = await shouldAutoResolveDependencies(resource);
67
- if (shouldResolveDependencies && existingPackageJson) {
68
- await resolveResourceDependencies(resource, { transpiled: shouldTranspile });
69
- }
70
- if (!existingPackageJson) {
71
- createTempPackageJson(functionPath);
72
- }
73
- const { forceColor = true, timeout = 10 } = options;
74
- let filteredData = payload?.payload?.data || {};
75
- if (resource.type === 'sanity.function.document' ||
76
- resource.type === 'sanity.function.media-library.asset') {
77
- if (!isGroqContextOptions(context)) {
78
- throw new Error('GROQ-based functions require a context with clientOptions');
79
- }
80
- const { projectId, dataset } = context.clientOptions;
81
- if ('before' in payload) {
82
- filteredData = await applyGroqRule(resource, payload, projectId, dataset);
83
- if (typeof filteredData === 'undefined') {
84
- return {
85
- logs: `⚠️ Filter "${resource.event?.filter}" returned an empty result. Skipping invoke.`,
86
- error: undefined,
87
- json: undefined,
88
- };
89
- }
90
- }
91
- }
92
- return new Promise((resolve, reject) => {
93
- let child;
94
- let timer;
95
- let executionStart;
96
- function start() {
97
- executionStart = performance.now();
98
- child = spawn('node', ['--enable-source-maps', getChildProcessWrapperPath()], {
99
- cwd: cwd(),
100
- stdio: ['pipe', 'pipe', 'pipe', 'ipc'], // changed: use 'pipe' so we can capture output
101
- env: { ...process.env, ...resource.env, FORCE_COLOR: forceColor ? '1' : '0' },
102
- });
103
- // NEW: capture stdout and stderr
104
- let stdout = '';
105
- let stderr = '';
106
- child.stdout?.on('data', (data) => {
107
- stdout += data.toString();
108
- });
109
- child.stderr?.on('data', (data) => {
110
- stderr += data.toString();
111
- });
112
- child.on('message', (data) => {
113
- const executionTimeMs = performance.now() - executionStart;
114
- const { json, logs, error } = JSON.parse(data.toString());
115
- shutdown();
116
- resolve({
117
- json,
118
- logs: sanitizeLogs(logs),
119
- error,
120
- timings: {
121
- ...bundleTimings,
122
- execute: executionTimeMs,
123
- },
124
- });
125
- });
126
- child.on('error', (error) => {
127
- shutdown();
128
- reject(new Error(`encountered error ${error.message}`));
129
- });
130
- child.on('exit', (code) => {
131
- const executionTimeMs = performance.now() - executionStart;
132
- shutdown();
133
- if (code !== 0) {
134
- // use captured stderr if available
135
- const message = stderr.trim().length > 0
136
- ? `Process failed with code ${code}\n${stderr}`
137
- : `exited with code ${code}`;
138
- reject(new Error(message));
139
- }
140
- else {
141
- resolve({
142
- json: undefined,
143
- logs: sanitizeLogs(stdout),
144
- error: undefined,
145
- timings: {
146
- ...bundleTimings,
147
- execute: executionTimeMs,
148
- },
149
- });
150
- }
151
- });
152
- timer = setTimeout(() => {
153
- shutdown();
154
- reject(new Error(`Timeout: Function exceeded the ${timeout}-second time limit.`));
155
- }, timeout * 1000);
156
- const payload = {
157
- data: { ...filteredData },
158
- context: {
159
- ...context,
160
- local: true,
161
- resources: groupBlueprintResources(context.resources ?? []),
162
- ...(isGroqContextOptions(context) && {
163
- clientOptions: {
164
- ...context.clientOptions,
165
- apiHost: config.apiUrl,
166
- },
167
- }),
168
- },
169
- };
170
- child.send(JSON.stringify({ srcPath: functionPath, payload }, null, 2));
171
- }
172
- function shutdown() {
173
- clearTimeout(timer);
174
- if (child && !child.killed) {
175
- child.kill();
176
- }
177
- // If we previously created a temporary package.json, let's clean it up
178
- if (!existingPackageJson) {
179
- cleanupTempPackageJson(functionPath);
180
- }
181
- cleanupBundle().catch((err) => console.warn('Bundle cleanup failed:', err));
182
- }
183
- start();
184
- });
185
- }