@sanity/runtime-cli 17.15.0 → 17.16.0-app-functions.gab0267c

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 (66) hide show
  1. package/README.md +19 -19
  2. package/dist/actions/blueprints/assets.d.ts +3 -3
  3. package/dist/actions/blueprints/assets.js +3 -3
  4. package/dist/actions/blueprints/blueprint.d.ts +20 -9
  5. package/dist/actions/blueprints/blueprint.js +25 -79
  6. package/dist/actions/blueprints/config.d.ts +1 -0
  7. package/dist/actions/blueprints/config.js +7 -1
  8. package/dist/actions/blueprints/pre-deploy.js +3 -3
  9. package/dist/actions/blueprints/process-assets.js +3 -2
  10. package/dist/actions/blueprints/stacks.d.ts +9 -3
  11. package/dist/actions/blueprints/stacks.js +11 -8
  12. package/dist/actions/blueprints/wait-for-operation.d.ts +1 -0
  13. package/dist/actions/blueprints/wait-for-operation.js +1 -1
  14. package/dist/baseCommands.js +3 -3
  15. package/dist/config.d.ts +1 -2
  16. package/dist/config.js +1 -2
  17. package/dist/cores/blueprints/deploy.d.ts +8 -0
  18. package/dist/cores/blueprints/deploy.js +12 -4
  19. package/dist/cores/blueprints/destroy.d.ts +14 -1
  20. package/dist/cores/blueprints/destroy.js +2 -1
  21. package/dist/cores/blueprints/doctor.js +2 -2
  22. package/dist/cores/blueprints/init.js +11 -2
  23. package/dist/cores/functions/test.js +43 -9
  24. package/dist/functions/artifacts.d.ts +13 -0
  25. package/dist/functions/artifacts.js +26 -0
  26. package/dist/functions/build.d.ts +15 -0
  27. package/dist/functions/build.js +27 -0
  28. package/dist/functions/contractPlugin.d.ts +7 -0
  29. package/dist/functions/contractPlugin.js +32 -0
  30. package/dist/functions/contracts/schema.d.ts +22 -0
  31. package/dist/functions/contracts/schema.js +99 -0
  32. package/dist/functions/contracts/validation.d.ts +3 -0
  33. package/dist/functions/contracts/validation.js +66 -0
  34. package/dist/functions/contracts.d.ts +20 -0
  35. package/dist/functions/contracts.js +119 -0
  36. package/dist/functions/deploy.d.ts +15 -0
  37. package/dist/functions/deploy.js +60 -0
  38. package/dist/functions/stack.d.ts +37 -0
  39. package/dist/functions/stack.js +148 -0
  40. package/dist/server/app.js +4 -4
  41. package/dist/server/static/components/app.css +8 -1
  42. package/dist/utils/blueprints/group-resources.d.ts +2 -1
  43. package/dist/utils/blueprints/group-resources.js +3 -3
  44. package/dist/utils/blueprints/resolve-deployed-resources.d.ts +2 -2
  45. package/dist/utils/blueprints/resolve-deployed-resources.js +4 -4
  46. package/dist/utils/bundles/prepare-asset.d.ts +3 -3
  47. package/dist/utils/bundles/prepare-asset.js +8 -7
  48. package/dist/utils/functions/child-process-wrapper.js +1 -1
  49. package/dist/utils/functions/child-runtime/durable.js +99 -22
  50. package/dist/utils/functions/child-runtime/invoke.d.ts +1 -0
  51. package/dist/utils/functions/child-runtime/invoke.js +26 -9
  52. package/dist/utils/functions/child-runtime/step-history.d.ts +15 -0
  53. package/dist/utils/functions/child-runtime/step-history.js +52 -0
  54. package/dist/utils/functions/extract-durable-config.js +1 -4
  55. package/dist/utils/functions/invoke/local.js +27 -11
  56. package/dist/utils/functions/invoke/nested.js +2 -1
  57. package/dist/utils/functions/invoke/resolve-invoke-type.d.ts +2 -2
  58. package/dist/utils/functions/invoke/resolve-invoke-type.js +2 -2
  59. package/dist/utils/functions/transpile/function.d.ts +7 -1
  60. package/dist/utils/functions/transpile/function.js +15 -9
  61. package/dist/utils/types.d.ts +24 -13
  62. package/dist/utils/types.js +1 -1
  63. package/oclif.manifest.json +1 -1
  64. package/package.json +24 -3
  65. package/dist/actions/blueprints/resolve.d.ts +0 -51
  66. package/dist/actions/blueprints/resolve.js +0 -52
@@ -0,0 +1,119 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import ts from 'typescript-compiler';
4
+ import { findFunctionEntryPoint } from '../utils/functions/find-entry-point.js';
5
+ import { contractType, schemaFromType } from './contracts/schema.js';
6
+ import { jsonGuard, validatorSource } from './contracts/validation.js';
7
+ /** Discover input and output types without loading or executing function code. */
8
+ export async function generateFunctionContracts(options) {
9
+ const entries = await Promise.all(options.functions.map(async (fn) => ({
10
+ ...fn,
11
+ entryPoint: await findFunctionEntryPoint(path.resolve(options.workDir, fn.src), fn.name),
12
+ })));
13
+ const config = readProjectConfig(options.workDir);
14
+ const program = ts.createProgram(entries.map((fn) => fn.entryPoint), {
15
+ target: ts.ScriptTarget.ESNext,
16
+ module: ts.ModuleKind.ESNext,
17
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
18
+ ...config?.options,
19
+ allowJs: true,
20
+ checkJs: true,
21
+ noEmit: true,
22
+ strict: true,
23
+ strictNullChecks: true,
24
+ skipLibCheck: true,
25
+ composite: false,
26
+ incremental: false,
27
+ declaration: false,
28
+ declarationMap: false,
29
+ tsBuildInfoFile: undefined,
30
+ });
31
+ assertDiagnostics(ts.getPreEmitDiagnostics(program), options.workDir);
32
+ const checker = program.getTypeChecker();
33
+ return entries.map((fn) => readContract(checker, program, fn));
34
+ }
35
+ /** Check app consumers after regenerating their function registry. */
36
+ export function checkFunctionTypes(workDir) {
37
+ const config = readProjectConfig(workDir);
38
+ if (!config)
39
+ return;
40
+ const program = ts.createProgram({
41
+ rootNames: config.fileNames,
42
+ options: { ...config.options, noEmit: true, incremental: false, tsBuildInfoFile: undefined },
43
+ projectReferences: config.projectReferences,
44
+ });
45
+ assertDiagnostics(ts.getPreEmitDiagnostics(program), workDir);
46
+ }
47
+ function readProjectConfig(workDir) {
48
+ const configPath = ts.findConfigFile(workDir, ts.sys.fileExists);
49
+ if (!configPath)
50
+ return undefined;
51
+ const config = ts.readConfigFile(configPath, ts.sys.readFile);
52
+ if (config.error)
53
+ assertDiagnostics([config.error], workDir);
54
+ const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configPath));
55
+ assertDiagnostics(parsed.errors, workDir);
56
+ return parsed;
57
+ }
58
+ function assertDiagnostics(diagnostics, workDir) {
59
+ const errors = diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
60
+ if (errors.length)
61
+ throw new Error(ts.formatDiagnostics(errors, {
62
+ getCanonicalFileName: (file) => file,
63
+ getCurrentDirectory: () => workDir,
64
+ getNewLine: () => '\n',
65
+ }));
66
+ }
67
+ function readContract(checker, program, fn) {
68
+ const source = program.getSourceFile(fn.entryPoint);
69
+ const module = source && checker.getSymbolAtLocation(source);
70
+ const exported = module && checker.getExportsOfModule(module).find((item) => item.getName() === 'handler');
71
+ if (!source || !exported)
72
+ throw new Error(`Function "${fn.name}" must export handler.`);
73
+ const signatures = checker.getTypeOfSymbolAtLocation(exported, source).getCallSignatures();
74
+ if (signatures.length !== 1)
75
+ throw new Error(`Function "${fn.name}" must have one handler signature.`);
76
+ const signature = signatures[0];
77
+ if (signature.parameters.length > 1)
78
+ throw new Error(`Function "${fn.name}" must accept a single FunctionArgs envelope.`);
79
+ let input = { kind: 'void' };
80
+ const parameter = signature.parameters[0];
81
+ if (parameter) {
82
+ const args = checker.getTypeOfSymbolAtLocation(parameter, source);
83
+ const event = args.getProperty('event');
84
+ const eventType = event && checker.getTypeOfSymbolAtLocation(event, source);
85
+ const data = eventType?.getProperty('data');
86
+ if (!data)
87
+ throw new Error(`Function "${fn.name}" must annotate its input with FunctionArgs<Input>.`);
88
+ input = schemaFromType(checker, checker.getTypeOfSymbolAtLocation(data, source), `${fn.name} input`, true);
89
+ }
90
+ const isObjectInput = (schema) => schema.kind === 'object' || (schema.kind === 'union' && schema.variants.every(isObjectInput));
91
+ if (input.kind !== 'void' && !isObjectInput(input))
92
+ throw new Error(`Function "${fn.name}" input must be a JSON object or omitted. Put scalar values inside an input object.`);
93
+ const returnType = checker.getReturnTypeOfSignature(signature);
94
+ const output = schemaFromType(checker, checker.getAwaitedType(returnType) ?? returnType, `${fn.name} output`, true);
95
+ return { ...fn, input, output };
96
+ }
97
+ const generatedHeader = '// Generated by Sanity CLI. Do not edit.\n';
98
+ /** Emit a standalone declaration registry and browser-safe validators. */
99
+ export async function writeFunctionContracts(workDir, contracts) {
100
+ const declarations = `${generatedHeader}import '@sanity/sdk'\n\ndeclare module '@sanity/sdk' {\n interface AppFunctionRegistry {\n${contracts.map((fn) => ` ${JSON.stringify(fn.name)}: {input: ${contractType(fn.input)}; output: ${contractType(fn.output)}}`).join('\n')}\n }\n}\n`;
101
+ const client = `${generatedHeader}${jsonGuard}\nexport const functionContracts = Object.fromEntries([\n${contracts.map((fn) => `[${JSON.stringify(fn.name)}, {hasInput: ${fn.input.kind !== 'void'}, validateInput: ${validatorSource(fn.input)}, validateOutput: ${validatorSource(fn.output)}}]`).join(',\n')}\n])\n`;
102
+ await writeGenerated(path.join(workDir, 'sanity.functions.d.ts'), declarations);
103
+ await writeGenerated(path.join(workDir, '.sanity', 'function-contracts.js'), client);
104
+ }
105
+ async function writeGenerated(file, content) {
106
+ try {
107
+ const previous = await readFile(file, 'utf8');
108
+ if (!previous.startsWith(generatedHeader))
109
+ throw new Error(`Refusing to overwrite non-generated file ${file}. Move it before generating function contracts.`);
110
+ if (previous === content)
111
+ return;
112
+ }
113
+ catch (error) {
114
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
115
+ throw error;
116
+ }
117
+ await mkdir(path.dirname(file), { recursive: true });
118
+ await writeFile(file, content);
119
+ }
@@ -0,0 +1,15 @@
1
+ import type { CoreResult } from '../cores/index.js';
2
+ /** Deploy already-built PubSub functions to an app-owned organization stack. */
3
+ export declare function deployFunctions(options: {
4
+ appId: string;
5
+ organizationId: string;
6
+ stackId: string;
7
+ workDir: string;
8
+ functions: {
9
+ name: string;
10
+ src: string;
11
+ }[];
12
+ token: string;
13
+ fetchFn: typeof fetch;
14
+ log: (message: string) => void;
15
+ }): Promise<CoreResult>;
@@ -0,0 +1,60 @@
1
+ import { getStack } from '../actions/blueprints/stacks.js';
2
+ import { deployResourcesCore } from '../cores/blueprints/deploy.js';
3
+ import { Logger } from '../utils/logger.js';
4
+ import { readFunctionBuild } from './artifacts.js';
5
+ import { assertAppFunctionStack } from './stack.js';
6
+ /** Deploy already-built PubSub functions to an app-owned organization stack. */
7
+ export async function deployFunctions(options) {
8
+ const { organizationId, stackId, workDir, fetchFn } = options;
9
+ const auth = {
10
+ token: options.token,
11
+ scopeType: 'organization',
12
+ scopeId: organizationId,
13
+ };
14
+ const result = await getStack({ stackId, auth, fetchFn });
15
+ if (!result.ok)
16
+ return { success: false, error: result.error || 'Could not read app function stack' };
17
+ const stack = result.stack;
18
+ try {
19
+ assertAppFunctionStack(stack, options);
20
+ }
21
+ catch (error) {
22
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
23
+ }
24
+ if (stack.resources.some((resource) => resource.type !== 'sanity.function.pubsub')) {
25
+ return {
26
+ success: false,
27
+ error: 'The app function stack contains other resource types. Use a dedicated app function stack.',
28
+ };
29
+ }
30
+ const resources = [];
31
+ for (const fn of options.functions) {
32
+ let src;
33
+ try {
34
+ src = (await readFunctionBuild({ ...fn, workDir })).src;
35
+ }
36
+ catch (error) {
37
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
38
+ }
39
+ resources.push({
40
+ name: fn.name,
41
+ type: 'sanity.function.pubsub',
42
+ src,
43
+ transpile: false,
44
+ autoResolveDeps: false,
45
+ });
46
+ }
47
+ return deployResourcesCore({
48
+ auth,
49
+ stackId,
50
+ scopeType: 'organization',
51
+ scopeId: organizationId,
52
+ deployedStack: stack,
53
+ resources,
54
+ workDir,
55
+ fetchFn,
56
+ bin: 'sanity',
57
+ flags: {},
58
+ log: Logger(options.log, { json: true }),
59
+ });
60
+ }
@@ -0,0 +1,37 @@
1
+ import type { CoreResult } from '../cores/index.js';
2
+ import type { Stack, StackSummary } from '../utils/types.js';
3
+ interface AppFunctionIdentity {
4
+ appId: string;
5
+ organizationId: string;
6
+ }
7
+ /** Shared ownership check for provisioning and deploying an app's function stack. */
8
+ export declare function assertAppFunctionStack(stack: Stack | StackSummary, { appId, organizationId }: AppFunctionIdentity): void;
9
+ /** Resolve or create a dedicated app stack using the Blueprint stack operations. */
10
+ export declare function ensureFunctionStack(options: AppFunctionIdentity & {
11
+ token: string;
12
+ fetchFn: typeof fetch;
13
+ stackId?: string;
14
+ dryRun?: boolean;
15
+ }): Promise<{
16
+ created: boolean;
17
+ id: string;
18
+ } | null>;
19
+ export { BLUEPRINTS_API_VERSION } from '../config.js';
20
+ /** Scope and transport for looking up an app's function stack. */
21
+ export interface FunctionStackOptions extends AppFunctionIdentity {
22
+ token: string;
23
+ fetchFn: typeof fetch;
24
+ stackId?: string;
25
+ }
26
+ /** Read-only stack metadata for an undeploy plan. */
27
+ export interface FunctionStackInfo {
28
+ id: string;
29
+ functions: string[];
30
+ }
31
+ /** Find an app-owned stack without creating one, and verify all resources are app functions. */
32
+ export declare function inspectFunctionStack(options: FunctionStackOptions): Promise<FunctionStackInfo | null>;
33
+ /** Destroy an app's stack using Blueprint deletion and operation polling. Missing stacks are already clean. */
34
+ export declare function destroyFunctionStack(options: FunctionStackOptions & {
35
+ stackId: string;
36
+ log: (message: string) => void;
37
+ }): Promise<CoreResult>;
@@ -0,0 +1,148 @@
1
+ import { createStack, destroyStack, getStack, listStacks } from '../actions/blueprints/stacks.js';
2
+ import { waitForDestruction } from '../cores/blueprints/destroy.js';
3
+ import { Logger } from '../utils/logger.js';
4
+ /** Shared ownership check for provisioning and deploying an app's function stack. */
5
+ export function assertAppFunctionStack(stack, { appId, organizationId }) {
6
+ if (!stack ||
7
+ typeof stack.id !== 'string' ||
8
+ !stack.id ||
9
+ stack.name !== appFunctionStackName(appId) ||
10
+ stack.scopeType !== 'organization' ||
11
+ stack.scopeId !== organizationId) {
12
+ throw new Error(`The function stack does not belong to app "${appId}" in organization "${organizationId}". Check .sanity/app-functions.json.`);
13
+ }
14
+ }
15
+ /** Resolve or create a dedicated app stack using the Blueprint stack operations. */
16
+ export async function ensureFunctionStack(options) {
17
+ const { appId, organizationId, token, fetchFn, stackId, dryRun = false } = options;
18
+ const auth = { token, scopeType: 'organization', scopeId: organizationId };
19
+ const name = appFunctionStackName(appId);
20
+ const logger = Logger(() => { }, { json: true });
21
+ const resolved = (stack, created) => {
22
+ assertAppFunctionStack(stack, options);
23
+ return { created, id: stack.id };
24
+ };
25
+ if (stackId) {
26
+ const result = await getStack({ stackId, auth, fetchFn });
27
+ if (result.ok)
28
+ return resolved(result.stack, false);
29
+ if (result.response?.status !== 404)
30
+ throw stackRequestError(result);
31
+ }
32
+ async function findStack() {
33
+ let cursor;
34
+ const cursors = new Set();
35
+ do {
36
+ const result = await listStacks(auth, logger, { fetchFn, cursor, limit: 500 });
37
+ if (!result.ok)
38
+ throw stackRequestError(result);
39
+ if (!Array.isArray(result.stacks))
40
+ throw new Error('Invalid Blueprint stack list response.');
41
+ const stack = result.stacks.find((item) => item?.name === name);
42
+ if (stack)
43
+ return stack;
44
+ if (result.response?.headers.get('x-has-more') !== 'true')
45
+ return undefined;
46
+ cursor = result.response.headers.get('x-cursor') ?? undefined;
47
+ if (!cursor || cursors.has(cursor))
48
+ throw new Error('Cannot read the next page of Blueprint stacks. Try again.');
49
+ cursors.add(cursor);
50
+ } while (cursor);
51
+ return undefined;
52
+ }
53
+ const existing = await findStack();
54
+ if (existing)
55
+ return resolved(existing, false);
56
+ if (dryRun)
57
+ return null;
58
+ const result = await createStack({
59
+ auth,
60
+ logger,
61
+ fetchFn,
62
+ stackMutation: {
63
+ name,
64
+ scopeType: auth.scopeType,
65
+ scopeId: auth.scopeId,
66
+ document: { resources: [] },
67
+ },
68
+ });
69
+ if (result.ok)
70
+ return resolved(result.stack, true);
71
+ if (result.response?.status === 409) {
72
+ const concurrent = await findStack();
73
+ if (concurrent)
74
+ return resolved(concurrent, false);
75
+ }
76
+ throw stackRequestError(result);
77
+ }
78
+ function appFunctionStackName(appId) {
79
+ return `app-${appId}`;
80
+ }
81
+ function stackRequestError(result) {
82
+ const status = result.response ? ` (${result.response.status})` : '';
83
+ return new Error(`Blueprint stack request failed${status}: ${result.error || result.response?.statusText || 'Unknown error'}`);
84
+ }
85
+ export { BLUEPRINTS_API_VERSION } from '../config.js';
86
+ /** Find an app-owned stack without creating one, and verify all resources are app functions. */
87
+ export async function inspectFunctionStack(options) {
88
+ const found = await ensureFunctionStack({ ...options, dryRun: true });
89
+ if (!found)
90
+ return null;
91
+ const stack = await readOwnedFunctionStack({ ...options, stackId: found.id });
92
+ return stack ? { id: stack.id, functions: stack.resources.map((resource) => resource.name) } : null;
93
+ }
94
+ /** Destroy an app's stack using Blueprint deletion and operation polling. Missing stacks are already clean. */
95
+ export async function destroyFunctionStack(options) {
96
+ try {
97
+ const stack = await readOwnedFunctionStack(options);
98
+ if (!stack)
99
+ return { success: true };
100
+ const log = Logger(options.log, { json: true });
101
+ const auth = {
102
+ token: options.token,
103
+ scopeType: 'organization',
104
+ scopeId: options.organizationId,
105
+ };
106
+ const result = await destroyStack({
107
+ stackId: stack.id,
108
+ auth,
109
+ logger: log,
110
+ fetchFn: options.fetchFn,
111
+ });
112
+ if (!result.ok) {
113
+ if (result.response?.status === 404)
114
+ return { success: true };
115
+ return { success: false, error: result.error, requestId: result.requestId };
116
+ }
117
+ return await waitForDestruction({
118
+ stackId: stack.id,
119
+ stackName: stack.name,
120
+ operationId: result.stack.operationId,
121
+ requestId: result.requestId,
122
+ auth,
123
+ log,
124
+ bin: 'sanity',
125
+ fetchFn: options.fetchFn,
126
+ });
127
+ }
128
+ catch (error) {
129
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
130
+ }
131
+ }
132
+ async function readOwnedFunctionStack(options) {
133
+ const auth = {
134
+ token: options.token,
135
+ scopeType: 'organization',
136
+ scopeId: options.organizationId,
137
+ };
138
+ const result = await getStack({ stackId: options.stackId, auth, fetchFn: options.fetchFn });
139
+ if (!result.ok) {
140
+ if (result.response?.status === 404)
141
+ return null;
142
+ throw stackRequestError(result);
143
+ }
144
+ assertAppFunctionStack(result.stack, options);
145
+ if (result.stack.resources.some((resource) => resource.type !== 'sanity.function.pubsub'))
146
+ throw new Error('The app function stack contains other resource types. Manage it with sanity blueprints instead.');
147
+ return result.stack;
148
+ }
@@ -1,14 +1,14 @@
1
1
  import { existsSync, readFileSync, watchFile } from 'node:fs';
2
2
  import * as http from 'node:http';
3
3
  import { dirname } from 'node:path';
4
+ import { resolveStackConfig } from '@sanity/blueprints/resolve';
4
5
  import { default as mime } from 'mime-types';
5
6
  import { WebSocket, WebSocketServer } from 'ws';
6
7
  import { findBlueprintFile, loadAndParseBlueprint } from '../actions/blueprints/blueprint.js';
7
8
  import { readConfigFile } from '../actions/blueprints/config.js';
8
- import { resolveIds } from '../actions/blueprints/resolve.js';
9
9
  import config from '../config.js';
10
10
  import { isRecord } from '../utils/is-record.js';
11
- import { isEventType, isGroqContextOptions, } from '../utils/types.js';
11
+ import { isEventType, } from '../utils/types.js';
12
12
  import { handleInvokeRequest } from './handlers/invoke.js';
13
13
  const app = (host, port, validateResources, executionOptions) => {
14
14
  // Send a JSON envelope to every connected UI client.
@@ -47,7 +47,7 @@ const app = (host, port, validateResources, executionOptions) => {
47
47
  const blueprint = await loadAndParseBlueprint(undefined, { validateResources });
48
48
  const { parsedBlueprint, fileInfo, module: blueprintModule } = blueprint;
49
49
  const blueprintConfig = readConfigFile(fileInfo.blueprintFilePath);
50
- const { projectId, organizationId } = resolveIds({
50
+ const { projectId, organizationId } = resolveStackConfig({
51
51
  module: blueprintModule,
52
52
  config: blueprintConfig,
53
53
  });
@@ -75,7 +75,7 @@ const app = (host, port, validateResources, executionOptions) => {
75
75
  const { data, func: functionName, metadata, } = parseInvokeRequest(Buffer.concat(body), dirname(fileInfo.blueprintFilePath));
76
76
  const { context, event } = data;
77
77
  // replace user token if required
78
- if (isGroqContextOptions(context)) {
78
+ if ('clientOptions' in context) {
79
79
  if (context.clientOptions.token) {
80
80
  context.clientOptions.token = config.token || undefined;
81
81
  }
@@ -191,7 +191,7 @@ body {
191
191
  .border-right {
192
192
  border-right-width: 1px;
193
193
  border-right-style: solid;
194
- border-top-color: var(--card-border-color);
194
+ border-right-color: var(--card-border-color);
195
195
  }
196
196
 
197
197
  .hr-border {
@@ -413,6 +413,13 @@ ol[type='content'] li:hover:not(.selected:not(.selected)) {
413
413
  border-right: none;
414
414
  }
415
415
 
416
+ fieldset {
417
+ & :is(input:not([role='switch']), select, textarea) {
418
+ border: 1px solid light-dark(var(--gray-200), var(--gray-800)) !important;
419
+ border-radius: var(--radius-2);
420
+ }
421
+ }
422
+
416
423
  & :is(input:not([is='switch']), select, textarea) {
417
424
  border: 1px solid light-dark(var(--gray-200), var(--gray-800)) !important;
418
425
  border-radius: var(--radius-2);
@@ -1,8 +1,9 @@
1
1
  import type { BlueprintResource } from '@sanity/blueprints';
2
+ import type { ResolvedResource } from '../types.js';
2
3
  export type GroupedBlueprintResource = {
3
4
  id: string;
4
5
  name: string;
5
6
  type: string;
6
7
  };
7
8
  export type GroupedBlueprintResources = Record<string, GroupedBlueprintResource[]>;
8
- export declare function groupBlueprintResources(blueprintResources: BlueprintResource[]): GroupedBlueprintResources;
9
+ export declare function groupBlueprintResources(blueprintResources: ResolvedResource<BlueprintResource>[]): GroupedBlueprintResources;
@@ -8,7 +8,8 @@ const BLUEPRINT_TYPE_TO_KEY = {
8
8
  };
9
9
  export function groupBlueprintResources(blueprintResources) {
10
10
  const grouped = {};
11
- for (const resource of blueprintResources) {
11
+ for (const resolvedResource of blueprintResources) {
12
+ const resource = resolvedResource.resource;
12
13
  if (resource.type === 'sanity.access.robot')
13
14
  continue;
14
15
  const key = (resource.type?.startsWith('sanity.function.')
@@ -18,9 +19,8 @@ export function groupBlueprintResources(blueprintResources) {
18
19
  continue;
19
20
  if (!grouped[key])
20
21
  grouped[key] = [];
21
- const externalId = resource.externalId;
22
22
  grouped[key].push({
23
- id: externalId || 'not-deployed',
23
+ id: resolvedResource.externalId || 'not-deployed',
24
24
  name: resource.name,
25
25
  type: resource.type,
26
26
  });
@@ -1,5 +1,5 @@
1
1
  import type { BlueprintResource } from '@sanity/blueprints';
2
- import type { AuthParams } from '../types.js';
2
+ import type { AuthParams, ResolvedResource } from '../types.js';
3
3
  interface ResolveOptions {
4
4
  resources: BlueprintResource[];
5
5
  stackId?: string;
@@ -12,5 +12,5 @@ interface ResolveOptions {
12
12
  * resources that aren't yet deployed are preserved without an externalId.
13
13
  * Any failure to fetch the stack falls back to the raw local resources.
14
14
  */
15
- export declare function resolveDeployedResources({ resources, stackId, auth, fetchFn, }: ResolveOptions): Promise<BlueprintResource[]>;
15
+ export declare function resolveDeployedResources({ resources, stackId, auth, fetchFn, }: ResolveOptions): Promise<ResolvedResource<BlueprintResource>[]>;
16
16
  export {};
@@ -7,11 +7,11 @@ import { getStack } from '../../actions/blueprints/stacks.js';
7
7
  */
8
8
  export async function resolveDeployedResources({ resources, stackId, auth, fetchFn, }) {
9
9
  if (!stackId || !auth?.token || !auth.scopeType || !auth.scopeId)
10
- return resources;
10
+ return resources.map((r) => ({ resource: r }));
11
11
  try {
12
12
  const response = await getStack({ stackId, auth, fetchFn });
13
13
  if (!response.ok || !Array.isArray(response.stack?.resources))
14
- return resources;
14
+ return resources.map((r) => ({ resource: r }));
15
15
  const deployedByKey = new Map();
16
16
  for (const deployed of response.stack.resources) {
17
17
  if (deployed.externalId) {
@@ -20,10 +20,10 @@ export async function resolveDeployedResources({ resources, stackId, auth, fetch
20
20
  }
21
21
  return resources.map((resource) => {
22
22
  const externalId = deployedByKey.get(`${resource.type}:${resource.name}`);
23
- return externalId ? { ...resource, externalId } : resource;
23
+ return { resource, externalId };
24
24
  });
25
25
  }
26
26
  catch {
27
- return resources;
27
+ return resources.map((r) => ({ resource: r }));
28
28
  }
29
29
  }
@@ -1,7 +1,7 @@
1
1
  import type { BlueprintApplicationResource, BlueprintInstallationConfigResource, BlueprintStudioResource } from '@sanity/blueprints';
2
2
  import { type BuildStudioEventListener } from '@sanity/cli-build/_internal/build';
3
3
  import type { Output } from '@sanity/cli-core';
4
- import type { AssetPrepResult } from '../types.js';
4
+ import type { AssetPrepResult, ResolvedResource } from '../types.js';
5
5
  export type BundleResource = BlueprintStudioResource | BlueprintApplicationResource | BlueprintInstallationConfigResource;
6
6
  type BuildContext<R extends BundleResource> = {
7
7
  resource: R;
@@ -28,9 +28,9 @@ export declare const BUNDLE_SPECS: {
28
28
  build: typeof buildInstallationConfigBundle;
29
29
  };
30
30
  };
31
- export declare function prepareBundleAsset<R extends BundleResource>({ blueprintFileDir, resource, }: {
31
+ export declare function prepareBundleAsset<R extends BundleResource>({ blueprintFileDir, resolvedResource, }: {
32
32
  blueprintFileDir: string;
33
- resource: R;
33
+ resolvedResource: ResolvedResource<R>;
34
34
  }): Promise<AssetPrepResult>;
35
35
  declare function buildStudioBundle({ resource, appId, workDir, outDir, output, eventListener, }: BuildContext<BlueprintStudioResource>): Promise<BuildResult>;
36
36
  declare function buildApplicationBundle({ resource, appId, workDir, outDir, output, eventListener, }: BuildContext<BlueprintApplicationResource>): Promise<BuildResult>;
@@ -12,10 +12,11 @@ export const BUNDLE_SPECS = {
12
12
  'sanity.application': { requiresAppId: true, build: buildApplicationBundle },
13
13
  'sanity.installation.config': { requiresAppId: false, build: buildInstallationConfigBundle },
14
14
  };
15
- export async function prepareBundleAsset({ blueprintFileDir, resource, }) {
15
+ export async function prepareBundleAsset({ blueprintFileDir, resolvedResource, }) {
16
+ const resource = resolvedResource.resource;
16
17
  // The lookup can't prove it yields the spec matching this R, so it's asserted once here.
17
18
  const spec = BUNDLE_SPECS[resource.type];
18
- const appId = getAppId(resource);
19
+ const appId = getAppId(resolvedResource);
19
20
  if (spec.requiresAppId && !appId) {
20
21
  return {
21
22
  success: false,
@@ -260,12 +261,12 @@ async function getInstalledSanityVersion(workDir) {
260
261
  return undefined;
261
262
  }
262
263
  }
263
- function getAppId(resource) {
264
- if ('externalId' in resource && typeof resource.externalId === 'string' && resource.externalId) {
265
- return resource.externalId;
264
+ function getAppId(resolvedResource) {
265
+ if (resolvedResource.externalId) {
266
+ return resolvedResource.externalId;
266
267
  }
267
- else if (resource.lifecycle?.ownershipAction?.type === 'attach') {
268
- return resource.lifecycle.ownershipAction.id;
268
+ else if (resolvedResource.resource.lifecycle?.ownershipAction?.type === 'attach') {
269
+ return resolvedResource.resource.lifecycle.ownershipAction.id;
269
270
  }
270
271
  return undefined;
271
272
  }
@@ -86,7 +86,7 @@ process.on('message', async (data) => {
86
86
  {
87
87
  emit: (e) => process.send(JSON.stringify({type: 'step-event', event: e})),
88
88
  wait: (ms) => new Promise((resolve) => setTimeout(resolve, noWait ? 0 : ms)),
89
- invoke: invoker.invoke,
89
+ invoke: invoker.delegate,
90
90
  },
91
91
  )
92
92
  } else {