@sanity/runtime-cli 17.16.0-app-functions.ge0a25ea → 17.16.0-next.g05d3b7f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -19
- package/dist/actions/blueprints/stacks.d.ts +2 -7
- package/dist/actions/blueprints/stacks.js +6 -10
- package/dist/actions/blueprints/wait-for-operation.d.ts +0 -1
- package/dist/actions/blueprints/wait-for-operation.js +1 -1
- package/dist/cores/blueprints/deploy.d.ts +0 -8
- package/dist/cores/blueprints/deploy.js +4 -12
- package/dist/cores/functions/logs.d.ts +7 -0
- package/dist/cores/functions/logs.js +17 -6
- package/dist/utils/functions/child-runtime/durable.js +43 -2
- package/dist/utils/functions/child-runtime/getBatchResults.js +75 -0
- package/dist/utils/functions/transpile/function.d.ts +1 -7
- package/dist/utils/functions/transpile/function.js +12 -15
- package/oclif.manifest.json +1 -1
- package/package.json +2 -23
- package/dist/functions/artifacts.d.ts +0 -13
- package/dist/functions/artifacts.js +0 -26
- package/dist/functions/build.d.ts +0 -15
- package/dist/functions/build.js +0 -27
- package/dist/functions/contractPlugin.d.ts +0 -7
- package/dist/functions/contractPlugin.js +0 -32
- package/dist/functions/contracts/schema.d.ts +0 -22
- package/dist/functions/contracts/schema.js +0 -99
- package/dist/functions/contracts/validation.d.ts +0 -3
- package/dist/functions/contracts/validation.js +0 -66
- package/dist/functions/contracts.d.ts +0 -20
- package/dist/functions/contracts.js +0 -119
- package/dist/functions/deploy.d.ts +0 -15
- package/dist/functions/deploy.js +0 -60
- package/dist/functions/stack.d.ts +0 -18
- package/dist/functions/stack.js +0 -84
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": "17.16.0-
|
|
4
|
+
"version": "17.16.0-next.g05d3b7f",
|
|
5
5
|
"author": "Sanity Runtime Team",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -14,26 +14,6 @@
|
|
|
14
14
|
"main": "dist/index.js",
|
|
15
15
|
"types": "dist/index.d.ts",
|
|
16
16
|
"exports": {
|
|
17
|
-
"./functions/stack": {
|
|
18
|
-
"types": "./dist/functions/stack.d.ts",
|
|
19
|
-
"import": "./dist/functions/stack.js",
|
|
20
|
-
"require": "./dist/functions/stack.js"
|
|
21
|
-
},
|
|
22
|
-
"./functions/contracts": {
|
|
23
|
-
"types": "./dist/functions/contracts.d.ts",
|
|
24
|
-
"import": "./dist/functions/contracts.js",
|
|
25
|
-
"require": "./dist/functions/contracts.js"
|
|
26
|
-
},
|
|
27
|
-
"./functions/deploy": {
|
|
28
|
-
"types": "./dist/functions/deploy.d.ts",
|
|
29
|
-
"import": "./dist/functions/deploy.js",
|
|
30
|
-
"require": "./dist/functions/deploy.js"
|
|
31
|
-
},
|
|
32
|
-
"./functions/build": {
|
|
33
|
-
"types": "./dist/functions/build.d.ts",
|
|
34
|
-
"import": "./dist/functions/build.js",
|
|
35
|
-
"require": "./dist/functions/build.js"
|
|
36
|
-
},
|
|
37
17
|
".": {
|
|
38
18
|
"types": "./dist/index.d.ts",
|
|
39
19
|
"import": "./dist/index.js",
|
|
@@ -106,7 +86,6 @@
|
|
|
106
86
|
"strip-ansi": "^7.2.0",
|
|
107
87
|
"tar-fs": "^3.1.3",
|
|
108
88
|
"tar-stream": "^3.2.1",
|
|
109
|
-
"typescript-compiler": "npm:typescript@^6.0.3",
|
|
110
89
|
"vite": "^8.3.0",
|
|
111
90
|
"ws": "^8.21.3",
|
|
112
91
|
"xdg-basedir": "^5.1.0"
|
|
@@ -121,7 +100,7 @@
|
|
|
121
100
|
"@oclif/test": "^4.2.0",
|
|
122
101
|
"@playwright/test": "^1.63.0",
|
|
123
102
|
"@sanity-labs/design-tokens": "^0.0.2-alpha.2",
|
|
124
|
-
"@sanity/functions": "^1.
|
|
103
|
+
"@sanity/functions": "^1.10.1",
|
|
125
104
|
"@types/cardinal": "^2.1.1",
|
|
126
105
|
"@types/mime-types": "^3.0.1",
|
|
127
106
|
"@types/node": "^22",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export declare const functionOutputFileName = "index.js";
|
|
2
|
-
/** Metadata for a compiled function, resolved without loading its handler. */
|
|
3
|
-
export interface FunctionBuild {
|
|
4
|
-
name: string;
|
|
5
|
-
src: string;
|
|
6
|
-
entryPoint: string;
|
|
7
|
-
}
|
|
8
|
-
/** Validate artifacts produced by buildFunction before packaging or deployment. */
|
|
9
|
-
export declare function readFunctionBuild(options: {
|
|
10
|
-
name: string;
|
|
11
|
-
src: string;
|
|
12
|
-
workDir: string;
|
|
13
|
-
}): Promise<FunctionBuild>;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { findFunctionEntryPoint } from '../utils/functions/find-entry-point.js';
|
|
4
|
-
export const functionOutputFileName = 'index.js';
|
|
5
|
-
/** Validate artifacts produced by buildFunction before packaging or deployment. */
|
|
6
|
-
export async function readFunctionBuild(options) {
|
|
7
|
-
const { name, workDir } = options;
|
|
8
|
-
const src = path.resolve(workDir, options.src);
|
|
9
|
-
try {
|
|
10
|
-
const pkg = JSON.parse(await readFile(path.join(src, 'package.json'), 'utf8'));
|
|
11
|
-
if (!pkg ||
|
|
12
|
-
typeof pkg !== 'object' ||
|
|
13
|
-
!('main' in pkg) ||
|
|
14
|
-
pkg.main !== functionOutputFileName ||
|
|
15
|
-
!('type' in pkg) ||
|
|
16
|
-
pkg.type !== 'module')
|
|
17
|
-
throw new Error('The function package metadata does not match its build format.');
|
|
18
|
-
const entryPoint = await findFunctionEntryPoint(src, name);
|
|
19
|
-
if (entryPoint !== path.join(src, functionOutputFileName))
|
|
20
|
-
throw new Error(`The compiled entry ${functionOutputFileName} is missing.`);
|
|
21
|
-
return { name, src, entryPoint };
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
throw new Error(`Function "${name}" is not built. Run sanity build first.`, { cause: error });
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type FunctionContract } from './contracts.js';
|
|
2
|
-
export interface BuildFunctionOptions {
|
|
3
|
-
name: string;
|
|
4
|
-
contract?: FunctionContract;
|
|
5
|
-
/** Function directory or entry file, relative to workDir. */
|
|
6
|
-
src: string;
|
|
7
|
-
workDir: string;
|
|
8
|
-
/** Build output directory, relative to workDir. Its contents are replaced. */
|
|
9
|
-
outDir: string;
|
|
10
|
-
}
|
|
11
|
-
/** Build a standalone ESM function bundle without a blueprint, stack, or ZIP. */
|
|
12
|
-
export declare function buildFunction({ name, src, workDir, outDir, contract, }: BuildFunctionOptions): Promise<{
|
|
13
|
-
outputDir: string;
|
|
14
|
-
}>;
|
|
15
|
-
export { type FunctionBuild, readFunctionBuild } from './artifacts.js';
|
package/dist/functions/build.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { findFunctionEntryPoint } from '../utils/functions/find-entry-point.js';
|
|
3
|
-
import { transpileFunction } from '../utils/functions/transpile/function.js';
|
|
4
|
-
import { functionOutputFileName } from './artifacts.js';
|
|
5
|
-
import { functionContractPlugin } from './contractPlugin.js';
|
|
6
|
-
import { generateFunctionContracts } from './contracts.js';
|
|
7
|
-
/** Build a standalone ESM function bundle without a blueprint, stack, or ZIP. */
|
|
8
|
-
export async function buildFunction({ name, src, workDir, outDir, contract, }) {
|
|
9
|
-
const functionPath = path.resolve(workDir, src);
|
|
10
|
-
const entryPoint = await findFunctionEntryPoint(functionPath, name);
|
|
11
|
-
const resolvedContract = contract ?? (await generateFunctionContracts({ workDir, functions: [{ name, src }] }))[0];
|
|
12
|
-
if (resolvedContract.entryPoint !== entryPoint || resolvedContract.name !== name)
|
|
13
|
-
throw new Error(`Function contract does not match "${name}".`);
|
|
14
|
-
const validation = functionContractPlugin(resolvedContract);
|
|
15
|
-
const result = await transpileFunction({
|
|
16
|
-
resource: { name, src, type: 'sanity.function.pubsub' },
|
|
17
|
-
functionPath,
|
|
18
|
-
entryPoint: validation.entryPoint,
|
|
19
|
-
plugins: [validation.plugin],
|
|
20
|
-
workDir,
|
|
21
|
-
outDir: path.resolve(workDir, outDir),
|
|
22
|
-
outputFileName: functionOutputFileName,
|
|
23
|
-
bundle: true,
|
|
24
|
-
});
|
|
25
|
-
return { outputDir: result.outputDir };
|
|
26
|
-
}
|
|
27
|
-
export { readFunctionBuild } from './artifacts.js';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from 'vite';
|
|
2
|
-
import type { FunctionContract } from './contracts.js';
|
|
3
|
-
/** Build a validating PubSub entry around the original handler. */
|
|
4
|
-
export declare function functionContractPlugin(contract: FunctionContract): {
|
|
5
|
-
entryPoint: string;
|
|
6
|
-
plugin: Plugin;
|
|
7
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { jsonGuard, validatorSource } from './contracts/validation.js';
|
|
2
|
-
/** Build a validating PubSub entry around the original handler. */
|
|
3
|
-
export function functionContractPlugin(contract) {
|
|
4
|
-
const entryPoint = `virtual:sanity-function-contract:${contract.name}`;
|
|
5
|
-
const resolvedId = `\0${entryPoint}`;
|
|
6
|
-
const source = `import {handler as implementation} from ${JSON.stringify(contract.entryPoint.replaceAll('\\', '/'))}
|
|
7
|
-
${jsonGuard}
|
|
8
|
-
const validateInput = ${validatorSource(contract.input)}
|
|
9
|
-
const validateOutput = ${validatorSource(contract.output)}
|
|
10
|
-
export async function handler(envelope) {
|
|
11
|
-
let input = envelope?.event?.data
|
|
12
|
-
${contract.input.kind === 'void' ? "if (input !== null && typeof input === 'object' && !Array.isArray(input) && Object.keys(input).length === 0) input = undefined" : ''}
|
|
13
|
-
if (!validateInput(input)) throw new TypeError(${JSON.stringify(`Invalid input for function "${contract.name}". Match its declared input type.`)})
|
|
14
|
-
const result = await implementation(envelope)
|
|
15
|
-
if (!validateOutput(result)) throw new TypeError(${JSON.stringify(`Function "${contract.name}" returned data that does not match its declared return type.`)})
|
|
16
|
-
return result
|
|
17
|
-
}`;
|
|
18
|
-
return {
|
|
19
|
-
entryPoint,
|
|
20
|
-
plugin: {
|
|
21
|
-
name: 'sanity/function-contract',
|
|
22
|
-
resolveId(id) {
|
|
23
|
-
if (id === entryPoint)
|
|
24
|
-
return resolvedId;
|
|
25
|
-
},
|
|
26
|
-
load(id) {
|
|
27
|
-
if (id === resolvedId)
|
|
28
|
-
return source;
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript-compiler';
|
|
2
|
-
export type ContractSchema = {
|
|
3
|
-
kind: 'string' | 'number' | 'boolean' | 'null' | 'void' | 'never';
|
|
4
|
-
} | {
|
|
5
|
-
kind: 'literal';
|
|
6
|
-
value: string | number | boolean;
|
|
7
|
-
} | {
|
|
8
|
-
kind: 'array';
|
|
9
|
-
items: ContractSchema;
|
|
10
|
-
} | {
|
|
11
|
-
kind: 'union';
|
|
12
|
-
variants: ContractSchema[];
|
|
13
|
-
} | {
|
|
14
|
-
kind: 'object';
|
|
15
|
-
properties: Record<string, {
|
|
16
|
-
optional: boolean;
|
|
17
|
-
schema: ContractSchema;
|
|
18
|
-
}>;
|
|
19
|
-
additional?: ContractSchema;
|
|
20
|
-
};
|
|
21
|
-
export declare function schemaFromType(checker: ts.TypeChecker, type: ts.Type, location: string, allowVoid?: boolean, parents?: Set<ts.Type>): ContractSchema;
|
|
22
|
-
export declare function contractType(schema: ContractSchema): string;
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript-compiler';
|
|
2
|
-
export function schemaFromType(checker, type, location, allowVoid = false, parents = new Set()) {
|
|
3
|
-
const fail = (reason) => {
|
|
4
|
-
throw new Error(`${location}: ${reason} (${checker.typeToString(type)}). Use a concrete JSON-compatible type.`);
|
|
5
|
-
};
|
|
6
|
-
const flags = type.flags;
|
|
7
|
-
if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.TypeParameter))
|
|
8
|
-
return fail('Unresolved or untyped contract');
|
|
9
|
-
if (flags & ts.TypeFlags.Never)
|
|
10
|
-
return { kind: 'never' };
|
|
11
|
-
if (flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined)) {
|
|
12
|
-
if (!allowVoid)
|
|
13
|
-
return fail('Undefined is only supported for optional object fields or a void result');
|
|
14
|
-
return { kind: 'void' };
|
|
15
|
-
}
|
|
16
|
-
if (flags & ts.TypeFlags.Null)
|
|
17
|
-
return { kind: 'null' };
|
|
18
|
-
if (flags & ts.TypeFlags.StringLiteral)
|
|
19
|
-
return { kind: 'literal', value: type.value };
|
|
20
|
-
if (flags & ts.TypeFlags.NumberLiteral)
|
|
21
|
-
return { kind: 'literal', value: type.value };
|
|
22
|
-
if (flags & ts.TypeFlags.BooleanLiteral)
|
|
23
|
-
return { kind: 'literal', value: checker.typeToString(type) === 'true' };
|
|
24
|
-
if (flags & ts.TypeFlags.String)
|
|
25
|
-
return { kind: 'string' };
|
|
26
|
-
if (flags & ts.TypeFlags.Number)
|
|
27
|
-
return { kind: 'number' };
|
|
28
|
-
if (flags & ts.TypeFlags.Boolean)
|
|
29
|
-
return { kind: 'boolean' };
|
|
30
|
-
if (parents.has(type) || parents.size > 64)
|
|
31
|
-
return fail('Recursive or excessively nested contract');
|
|
32
|
-
const next = new Set(parents).add(type);
|
|
33
|
-
if (type.isUnion())
|
|
34
|
-
return {
|
|
35
|
-
kind: 'union',
|
|
36
|
-
variants: type.types.map((item) => schemaFromType(checker, item, location, allowVoid, next)),
|
|
37
|
-
};
|
|
38
|
-
if (checker.isTupleType(type))
|
|
39
|
-
return fail('Tuple contracts are not supported; use an array');
|
|
40
|
-
if (checker.isArrayType(type)) {
|
|
41
|
-
const element = checker.getTypeArguments(type)[0];
|
|
42
|
-
return { kind: 'array', items: schemaFromType(checker, element, `${location}[]`, false, next) };
|
|
43
|
-
}
|
|
44
|
-
if (!(flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)))
|
|
45
|
-
return fail('Unsupported contract');
|
|
46
|
-
if (type.getCallSignatures().length || type.getConstructSignatures().length)
|
|
47
|
-
return fail('Functions and constructors cannot cross the function boundary');
|
|
48
|
-
if (type
|
|
49
|
-
.getSymbol()
|
|
50
|
-
?.getDeclarations()
|
|
51
|
-
?.some((declaration) => ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)))
|
|
52
|
-
return fail('Class instances cannot cross the function boundary');
|
|
53
|
-
if (checker.getIndexTypeOfType(type, ts.IndexKind.Number))
|
|
54
|
-
return fail('Numeric index signatures are not supported; use an array or string-keyed record');
|
|
55
|
-
const properties = Object.create(null);
|
|
56
|
-
for (const property of checker.getPropertiesOfType(type)) {
|
|
57
|
-
const name = property.getName();
|
|
58
|
-
if (name.startsWith('__@'))
|
|
59
|
-
return fail('Symbol properties cannot cross the function boundary');
|
|
60
|
-
const declaration = property.valueDeclaration ?? property.declarations?.[0];
|
|
61
|
-
if (!declaration)
|
|
62
|
-
return fail(`Cannot resolve property ${name}`);
|
|
63
|
-
const propertyType = checker.getTypeOfSymbolAtLocation(property, declaration);
|
|
64
|
-
const optional = !!(property.flags & ts.SymbolFlags.Optional);
|
|
65
|
-
const members = optional
|
|
66
|
-
? (propertyType.isUnion() ? propertyType.types : [propertyType]).filter((member) => !(member.flags & ts.TypeFlags.Undefined))
|
|
67
|
-
: [propertyType];
|
|
68
|
-
const schemas = members.map((member) => schemaFromType(checker, member, `${location}.${name}`, false, next));
|
|
69
|
-
properties[name] = {
|
|
70
|
-
optional,
|
|
71
|
-
schema: schemas.length === 0
|
|
72
|
-
? { kind: 'never' }
|
|
73
|
-
: schemas.length === 1
|
|
74
|
-
? schemas[0]
|
|
75
|
-
: { kind: 'union', variants: schemas },
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
const indexType = checker.getIndexTypeOfType(type, ts.IndexKind.String);
|
|
79
|
-
const additional = indexType
|
|
80
|
-
? schemaFromType(checker, indexType, `${location}[key]`, false, next)
|
|
81
|
-
: undefined;
|
|
82
|
-
return { kind: 'object', properties, ...(additional ? { additional } : {}) };
|
|
83
|
-
}
|
|
84
|
-
export function contractType(schema) {
|
|
85
|
-
switch (schema.kind) {
|
|
86
|
-
case 'literal':
|
|
87
|
-
return JSON.stringify(schema.value);
|
|
88
|
-
case 'array':
|
|
89
|
-
return `(${contractType(schema.items)})[]`;
|
|
90
|
-
case 'union':
|
|
91
|
-
return schema.variants.map(contractType).join(' | ');
|
|
92
|
-
case 'object':
|
|
93
|
-
return `{${Object.entries(schema.properties)
|
|
94
|
-
.map(([name, property]) => `${JSON.stringify(name)}${property.optional ? '?' : ''}: ${contractType(property.schema)};`)
|
|
95
|
-
.join(' ')}${schema.additional ? `[key: string]: ${contractType(schema.additional)};` : ''}}`;
|
|
96
|
-
default:
|
|
97
|
-
return schema.kind;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import type { ContractSchema } from './schema.js';
|
|
2
|
-
export declare const jsonGuard = "function isJson(value, seen = new WeakSet(), depth = 0) {\n if (depth > 100) return false\n if (value === undefined || value === null) return true\n if (typeof value === 'string' || typeof value === 'boolean') return true\n if (typeof value === 'number') return Number.isFinite(value)\n if (typeof value !== 'object' || seen.has(value) || Object.getOwnPropertySymbols(value).length) return false\n seen.add(value)\n try {\n if (Array.isArray(value)) {\n if (Object.keys(value).length !== value.length) return false\n for (let index = 0; index < value.length; index++) {\n if (!Object.hasOwn(value, index) || value[index] === undefined || !isJson(value[index], seen, depth + 1)) return false\n }\n return true\n }\n const prototype = Object.getPrototypeOf(value)\n return (prototype === Object.prototype || prototype === null) && Object.values(value).every(item => isJson(item, seen, depth + 1))\n } finally { seen.delete(value) }\n}";
|
|
3
|
-
export declare function validatorSource(schema: ContractSchema): string;
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
// Self-contained code emitted into browser contracts and function bundles.
|
|
2
|
-
export const jsonGuard = `function isJson(value, seen = new WeakSet(), depth = 0) {
|
|
3
|
-
if (depth > 100) return false
|
|
4
|
-
if (value === undefined || value === null) return true
|
|
5
|
-
if (typeof value === 'string' || typeof value === 'boolean') return true
|
|
6
|
-
if (typeof value === 'number') return Number.isFinite(value)
|
|
7
|
-
if (typeof value !== 'object' || seen.has(value) || Object.getOwnPropertySymbols(value).length) return false
|
|
8
|
-
seen.add(value)
|
|
9
|
-
try {
|
|
10
|
-
if (Array.isArray(value)) {
|
|
11
|
-
if (Object.keys(value).length !== value.length) return false
|
|
12
|
-
for (let index = 0; index < value.length; index++) {
|
|
13
|
-
if (!Object.hasOwn(value, index) || value[index] === undefined || !isJson(value[index], seen, depth + 1)) return false
|
|
14
|
-
}
|
|
15
|
-
return true
|
|
16
|
-
}
|
|
17
|
-
const prototype = Object.getPrototypeOf(value)
|
|
18
|
-
return (prototype === Object.prototype || prototype === null) && Object.values(value).every(item => isJson(item, seen, depth + 1))
|
|
19
|
-
} finally { seen.delete(value) }
|
|
20
|
-
}`;
|
|
21
|
-
export function validatorSource(schema) {
|
|
22
|
-
return `(value) => isJson(value) && (${validationExpression(schema, 'value')})`;
|
|
23
|
-
}
|
|
24
|
-
function validationExpression(schema, value, depth = 0) {
|
|
25
|
-
switch (schema.kind) {
|
|
26
|
-
case 'void':
|
|
27
|
-
return `${value} === undefined`;
|
|
28
|
-
case 'never':
|
|
29
|
-
return 'false';
|
|
30
|
-
case 'null':
|
|
31
|
-
return `${value} === null`;
|
|
32
|
-
case 'literal':
|
|
33
|
-
return `${value} === ${JSON.stringify(schema.value)}`;
|
|
34
|
-
case 'number':
|
|
35
|
-
return `(typeof ${value} === 'number' && Number.isFinite(${value}))`;
|
|
36
|
-
case 'string':
|
|
37
|
-
case 'boolean':
|
|
38
|
-
return `typeof ${value} === '${schema.kind}'`;
|
|
39
|
-
case 'union':
|
|
40
|
-
return `(${schema.variants.map((item) => `(${validationExpression(item, value, depth + 1)})`).join(' || ')})`;
|
|
41
|
-
case 'array': {
|
|
42
|
-
const item = `item${depth}`;
|
|
43
|
-
return `(Array.isArray(${value}) && ${value}.every(${item} => ${validationExpression(schema.items, item, depth + 1)}))`;
|
|
44
|
-
}
|
|
45
|
-
case 'object': {
|
|
46
|
-
const checks = [
|
|
47
|
-
`${value} !== null`,
|
|
48
|
-
`typeof ${value} === 'object'`,
|
|
49
|
-
`!Array.isArray(${value})`,
|
|
50
|
-
];
|
|
51
|
-
for (const [name, property] of Object.entries(schema.properties)) {
|
|
52
|
-
const member = `${value}[${JSON.stringify(name)}]`;
|
|
53
|
-
const present = `Object.hasOwn(${value}, ${JSON.stringify(name)})`;
|
|
54
|
-
const check = validationExpression(property.schema, member, depth + 1);
|
|
55
|
-
checks.push(property.optional
|
|
56
|
-
? `(!${present} || ${member} === undefined || (${check}))`
|
|
57
|
-
: `(${present} && (${check}))`);
|
|
58
|
-
}
|
|
59
|
-
if (schema.additional) {
|
|
60
|
-
const key = `key${depth}`;
|
|
61
|
-
checks.push(`Object.keys(${value}).every(${key} => ${JSON.stringify(Object.keys(schema.properties))}.includes(${key}) || (${validationExpression(schema.additional, `${value}[${key}]`, depth + 1)}))`);
|
|
62
|
-
}
|
|
63
|
-
return `(${checks.join(' && ')})`;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { type ContractSchema } from './contracts/schema.js';
|
|
2
|
-
export interface FunctionContract {
|
|
3
|
-
name: string;
|
|
4
|
-
src: string;
|
|
5
|
-
entryPoint: string;
|
|
6
|
-
input: ContractSchema;
|
|
7
|
-
output: ContractSchema;
|
|
8
|
-
}
|
|
9
|
-
/** Discover input and output types without loading or executing function code. */
|
|
10
|
-
export declare function generateFunctionContracts(options: {
|
|
11
|
-
workDir: string;
|
|
12
|
-
functions: {
|
|
13
|
-
name: string;
|
|
14
|
-
src: string;
|
|
15
|
-
}[];
|
|
16
|
-
}): Promise<FunctionContract[]>;
|
|
17
|
-
/** Check app consumers after regenerating their function registry. */
|
|
18
|
-
export declare function checkFunctionTypes(workDir: string): void;
|
|
19
|
-
/** Emit a standalone declaration registry and browser-safe validators. */
|
|
20
|
-
export declare function writeFunctionContracts(workDir: string, contracts: FunctionContract[]): Promise<void>;
|
|
@@ -1,119 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
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>;
|
package/dist/functions/deploy.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { Stack, StackSummary } from '../utils/types.js';
|
|
2
|
-
interface AppFunctionIdentity {
|
|
3
|
-
appId: string;
|
|
4
|
-
organizationId: string;
|
|
5
|
-
}
|
|
6
|
-
/** Shared ownership check for provisioning and deploying an app's function stack. */
|
|
7
|
-
export declare function assertAppFunctionStack(stack: Stack | StackSummary, { appId, organizationId }: AppFunctionIdentity): void;
|
|
8
|
-
/** Resolve or create a dedicated app stack using the Blueprint stack operations. */
|
|
9
|
-
export declare function ensureFunctionStack(options: AppFunctionIdentity & {
|
|
10
|
-
token: string;
|
|
11
|
-
fetchFn: typeof fetch;
|
|
12
|
-
stackId?: string;
|
|
13
|
-
dryRun?: boolean;
|
|
14
|
-
}): Promise<{
|
|
15
|
-
created: boolean;
|
|
16
|
-
id: string;
|
|
17
|
-
} | null>;
|
|
18
|
-
export { BLUEPRINTS_API_VERSION } from '../config.js';
|