@sanity/runtime-cli 4.4.0 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -77
- package/dist/actions/blueprints/assets.d.ts +1 -0
- package/dist/actions/blueprints/assets.js +21 -4
- package/dist/actions/blueprints/blueprint.d.ts +6 -12
- package/dist/actions/blueprints/blueprint.js +38 -45
- package/dist/actions/blueprints/index.d.ts +33 -0
- package/dist/actions/blueprints/index.js +32 -0
- package/dist/actions/blueprints/projects.d.ts +9 -0
- package/dist/actions/blueprints/projects.js +12 -0
- package/dist/actions/blueprints/stacks.d.ts +0 -12
- package/dist/actions/blueprints/stacks.js +3 -30
- package/dist/actions/functions/test.d.ts +2 -2
- package/dist/actions/functions/test.js +2 -2
- package/dist/baseCommands.d.ts +24 -0
- package/dist/baseCommands.js +69 -0
- package/dist/commands/blueprints/add.d.ts +1 -1
- package/dist/commands/blueprints/add.js +7 -6
- package/dist/commands/blueprints/config.d.ts +1 -1
- package/dist/commands/blueprints/config.js +24 -11
- package/dist/commands/blueprints/deploy.d.ts +2 -2
- package/dist/commands/blueprints/deploy.js +18 -33
- package/dist/commands/blueprints/destroy.d.ts +4 -3
- package/dist/commands/blueprints/destroy.js +32 -35
- package/dist/commands/blueprints/info.d.ts +2 -2
- package/dist/commands/blueprints/info.js +16 -36
- package/dist/commands/blueprints/init.d.ts +10 -2
- package/dist/commands/blueprints/init.js +85 -26
- package/dist/commands/blueprints/logs.d.ts +2 -2
- package/dist/commands/blueprints/logs.js +18 -32
- package/dist/commands/blueprints/plan.d.ts +2 -2
- package/dist/commands/blueprints/plan.js +10 -16
- package/dist/commands/blueprints/stacks.d.ts +3 -2
- package/dist/commands/blueprints/stacks.js +10 -29
- package/dist/commands/functions/env/add.d.ts +2 -2
- package/dist/commands/functions/env/add.js +6 -17
- package/dist/commands/functions/env/list.d.ts +2 -2
- package/dist/commands/functions/env/list.js +10 -17
- package/dist/commands/functions/env/remove.d.ts +2 -2
- package/dist/commands/functions/env/remove.js +6 -17
- package/dist/commands/functions/invoke.d.ts +2 -2
- package/dist/commands/functions/invoke.js +7 -14
- package/dist/commands/functions/logs.d.ts +3 -7
- package/dist/commands/functions/logs.js +21 -37
- package/dist/commands/functions/test.d.ts +3 -3
- package/dist/commands/functions/test.js +13 -14
- package/dist/server/app.js +82 -14
- package/dist/server/static/api.js +24 -3
- package/dist/server/static/components/function-list.js +4 -4
- package/dist/server/static/components/response-panel.js +14 -3
- package/dist/server/static/index.html +1 -0
- package/dist/utils/build-payload.d.ts +1 -1
- package/dist/utils/build-payload.js +3 -3
- package/dist/utils/bundle/bundle-function.d.ts +8 -0
- package/dist/utils/bundle/bundle-function.js +125 -0
- package/dist/utils/bundle/cleanup-source-maps.d.ts +10 -0
- package/dist/utils/bundle/cleanup-source-maps.js +53 -0
- package/dist/utils/bundle/find-up.d.ts +16 -0
- package/dist/utils/bundle/find-up.js +39 -0
- package/dist/utils/bundle/verify-handler.d.ts +2 -0
- package/dist/utils/bundle/verify-handler.js +13 -0
- package/dist/utils/child-process-wrapper.js +8 -6
- package/dist/utils/display/blueprints-formatting.js +2 -2
- package/dist/utils/display/errors.d.ts +4 -0
- package/dist/utils/display/errors.js +27 -0
- package/dist/utils/display/index.d.ts +1 -0
- package/dist/utils/display/index.js +1 -0
- package/dist/utils/functions/find-entry-point.d.ts +11 -0
- package/dist/utils/functions/find-entry-point.js +75 -0
- package/dist/utils/functions/should-bundle.d.ts +2 -0
- package/dist/utils/functions/should-bundle.js +23 -0
- package/dist/utils/invoke-local.d.ts +2 -2
- package/dist/utils/invoke-local.js +49 -8
- package/dist/utils/is-record.d.ts +1 -0
- package/dist/utils/is-record.js +3 -0
- package/dist/utils/parse-json-object.d.ts +1 -0
- package/dist/utils/parse-json-object.js +10 -0
- package/dist/utils/types.d.ts +13 -4
- package/dist/utils/types.js +9 -3
- package/oclif.manifest.json +59 -37
- package/package.json +5 -1
- package/dist/utils/is-json.d.ts +0 -1
- package/dist/utils/is-json.js +0 -12
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { Args,
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import Spinner from 'yocto-spinner';
|
|
3
|
-
import { readBlueprintOnDisk } from '../../actions/blueprints/blueprint.js';
|
|
4
3
|
import { invoke } from '../../actions/functions/invoke.js';
|
|
4
|
+
import { DeployedBlueprintCommand } from '../../baseCommands.js';
|
|
5
5
|
import { red } from '../../utils/display/colors.js';
|
|
6
6
|
import { findFunctionByName } from '../../utils/find-function.js';
|
|
7
|
-
|
|
8
|
-
export default class Invoke extends Command {
|
|
7
|
+
export default class InvokeCommand extends DeployedBlueprintCommand {
|
|
9
8
|
static args = {
|
|
10
9
|
name: Args.string({ description: 'The name of the Sanity Function', required: true }),
|
|
11
10
|
};
|
|
@@ -23,17 +22,11 @@ export default class Invoke extends Command {
|
|
|
23
22
|
}),
|
|
24
23
|
};
|
|
25
24
|
async run() {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
this.error(tokenErr.message);
|
|
29
|
-
const { args, flags } = await this.parse(Invoke);
|
|
25
|
+
const args = this.args;
|
|
26
|
+
const flags = this.flags;
|
|
30
27
|
const spinner = Spinner({ text: `Invoking function "${args.name}"` }).start();
|
|
31
|
-
const {
|
|
32
|
-
|
|
33
|
-
this.error('Stack not found'); // returns
|
|
34
|
-
const { projectId } = deployedStack;
|
|
35
|
-
const { externalId } = findFunctionByName(deployedStack, args.name);
|
|
36
|
-
const result = await invoke(externalId, { data: flags.data, file: flags.file }, { token, projectId });
|
|
28
|
+
const { externalId } = findFunctionByName(this.deployedStack, args.name);
|
|
29
|
+
const result = await invoke(externalId, { data: flags.data, file: flags.file }, this.auth);
|
|
37
30
|
if (result.ok) {
|
|
38
31
|
spinner.success(`Invocation of ${args.name} succeeded`);
|
|
39
32
|
if (result.json?.data?.type === 'Buffer') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DeployedBlueprintCommand } from '../../baseCommands.js';
|
|
2
2
|
type RunDeleteOptions = {
|
|
3
3
|
force: boolean;
|
|
4
4
|
};
|
|
@@ -6,7 +6,7 @@ type RunGetOptions = {
|
|
|
6
6
|
limit: number;
|
|
7
7
|
json?: boolean;
|
|
8
8
|
};
|
|
9
|
-
export default class
|
|
9
|
+
export default class LogsCommand extends DeployedBlueprintCommand<typeof LogsCommand> {
|
|
10
10
|
static args: {
|
|
11
11
|
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
12
12
|
};
|
|
@@ -18,13 +18,9 @@ export default class Logs extends Command {
|
|
|
18
18
|
delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
19
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
20
|
};
|
|
21
|
-
|
|
21
|
+
externalId: string | undefined;
|
|
22
22
|
run(): Promise<void>;
|
|
23
23
|
runDeleteLogs(name: string, options: RunDeleteOptions): Promise<void>;
|
|
24
24
|
runGetLogs(name: string, options: RunGetOptions): Promise<void>;
|
|
25
|
-
getProjectAndExternalId(name: string): Promise<{
|
|
26
|
-
projectId?: string;
|
|
27
|
-
externalId?: string;
|
|
28
|
-
}>;
|
|
29
25
|
}
|
|
30
26
|
export {};
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { Args,
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import Spinner from 'yocto-spinner';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { deleteLogs as deleteLogsAction, logs as getLogsAction, } from '../../actions/functions/logs.js';
|
|
5
|
+
import { DeployedBlueprintCommand } from '../../baseCommands.js';
|
|
6
6
|
import { formatTitle } from '../../utils/display/blueprints-formatting.js';
|
|
7
7
|
import { blue, bold, green, red, yellow } from '../../utils/display/colors.js';
|
|
8
8
|
import { findFunctionByName } from '../../utils/find-function.js';
|
|
9
|
-
import { validTokenOrErrorMessage } from '../../utils/validated-token.js';
|
|
10
9
|
function logLevel(level) {
|
|
11
10
|
if (level === 'ERROR') {
|
|
12
11
|
return red(level);
|
|
@@ -16,7 +15,7 @@ function logLevel(level) {
|
|
|
16
15
|
}
|
|
17
16
|
return green(level);
|
|
18
17
|
}
|
|
19
|
-
export default class
|
|
18
|
+
export default class LogsCommand extends DeployedBlueprintCommand {
|
|
20
19
|
static args = {
|
|
21
20
|
name: Args.string({ description: 'The name of the Sanity Function', required: true }),
|
|
22
21
|
};
|
|
@@ -52,13 +51,12 @@ export default class Logs extends Command {
|
|
|
52
51
|
required: false,
|
|
53
52
|
}),
|
|
54
53
|
};
|
|
55
|
-
|
|
54
|
+
externalId;
|
|
56
55
|
async run() {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
const { args, flags } = await this.parse(Logs);
|
|
56
|
+
const args = this.args;
|
|
57
|
+
const flags = this.flags;
|
|
58
|
+
const { externalId } = findFunctionByName(this.deployedStack, args.name);
|
|
59
|
+
this.externalId = externalId;
|
|
62
60
|
if (flags.delete === true) {
|
|
63
61
|
await this.runDeleteLogs(args.name, flags);
|
|
64
62
|
}
|
|
@@ -67,6 +65,8 @@ export default class Logs extends Command {
|
|
|
67
65
|
}
|
|
68
66
|
}
|
|
69
67
|
async runDeleteLogs(name, options) {
|
|
68
|
+
if (!this.externalId)
|
|
69
|
+
this.error('Unable to delete logs. Unable to determine function ID.');
|
|
70
70
|
if (!options.force) {
|
|
71
71
|
const { certain } = await inquirer.prompt({
|
|
72
72
|
type: 'confirm',
|
|
@@ -77,14 +77,8 @@ export default class Logs extends Command {
|
|
|
77
77
|
if (!certain)
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
|
-
const { projectId, externalId } = await this.getProjectAndExternalId(name);
|
|
81
|
-
if (!projectId || !externalId) {
|
|
82
|
-
this.error('Stack not found');
|
|
83
|
-
}
|
|
84
|
-
if (!this.sanityToken)
|
|
85
|
-
this.error('Unable to delete logs. Missing API token.');
|
|
86
80
|
const spinner = Spinner({ text: `Deleting logs for function ${yellow(name)}` }).start();
|
|
87
|
-
const { ok, error } = await deleteLogsAction(externalId,
|
|
81
|
+
const { ok, error } = await deleteLogsAction(this.externalId, this.auth);
|
|
88
82
|
if (!ok) {
|
|
89
83
|
spinner.error(`${red('Failed')} to retrieve logs`);
|
|
90
84
|
this.log(`Error: ${error || 'Unknown error'}`);
|
|
@@ -93,14 +87,10 @@ export default class Logs extends Command {
|
|
|
93
87
|
spinner.success('Logs deleted');
|
|
94
88
|
}
|
|
95
89
|
async runGetLogs(name, options) {
|
|
90
|
+
if (!this.externalId)
|
|
91
|
+
this.error('Unable to retrieve logs. Unable to determine function ID.');
|
|
96
92
|
const spinner = Spinner({ text: `Finding logs for function "${name}"` }).start();
|
|
97
|
-
const {
|
|
98
|
-
if (!projectId || !externalId) {
|
|
99
|
-
this.error('Stack not found');
|
|
100
|
-
}
|
|
101
|
-
if (!this.sanityToken)
|
|
102
|
-
this.error('Unable to retrieve logs. Missing API token.');
|
|
103
|
-
const { ok, error, logs, total } = await logsAction(externalId, { limit: options.limit }, { token: this.sanityToken, projectId });
|
|
93
|
+
const { ok, error, logs, total } = await getLogsAction(this.externalId, { limit: options.limit }, this.auth);
|
|
104
94
|
if (!ok) {
|
|
105
95
|
spinner.error(`${red('Failed')} to retrieve logs`);
|
|
106
96
|
this.log(`Error: ${error || 'Unknown error'}`);
|
|
@@ -121,22 +111,16 @@ export default class Logs extends Command {
|
|
|
121
111
|
for (const log of filteredLogs) {
|
|
122
112
|
const { time, level, message } = log;
|
|
123
113
|
const date = new Date(time);
|
|
124
|
-
this.log(
|
|
114
|
+
this.log([
|
|
115
|
+
bold(date.toLocaleDateString()),
|
|
116
|
+
bold(blue(date.toLocaleTimeString())),
|
|
117
|
+
logLevel(level),
|
|
118
|
+
message,
|
|
119
|
+
].join(' '));
|
|
125
120
|
}
|
|
126
121
|
}
|
|
127
122
|
else {
|
|
128
123
|
this.log(JSON.stringify(filteredLogs, null, 2));
|
|
129
124
|
}
|
|
130
125
|
}
|
|
131
|
-
async getProjectAndExternalId(name) {
|
|
132
|
-
if (!this.sanityToken)
|
|
133
|
-
this.error('Missing API token.');
|
|
134
|
-
const { deployedStack } = await readBlueprintOnDisk({ getStack: true, token: this.sanityToken });
|
|
135
|
-
if (!deployedStack) {
|
|
136
|
-
return {};
|
|
137
|
-
}
|
|
138
|
-
const { projectId } = deployedStack;
|
|
139
|
-
const { externalId } = findFunctionByName(deployedStack, name);
|
|
140
|
-
return { projectId, externalId };
|
|
141
|
-
}
|
|
142
126
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export default class
|
|
1
|
+
import { BlueprintCommand } from '../../baseCommands.js';
|
|
2
|
+
export default class TestCommand extends BlueprintCommand<typeof TestCommand> {
|
|
3
3
|
static args: {
|
|
4
4
|
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
5
|
};
|
|
@@ -11,7 +11,7 @@ export default class Test extends Command {
|
|
|
11
11
|
timeout: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
api: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
dataset: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
-
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<void>;
|
|
17
17
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Args,
|
|
2
|
-
import { readBlueprintOnDisk } from '../../actions/blueprints/blueprint.js';
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
2
|
import { testAction } from '../../actions/functions/test.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { BlueprintCommand } from '../../baseCommands.js';
|
|
4
|
+
import { findFunctionByName } from '../../utils/find-function.js';
|
|
5
|
+
export default class TestCommand extends BlueprintCommand {
|
|
6
6
|
static args = {
|
|
7
7
|
name: Args.string({ description: 'The name of the Sanity Function', required: true }),
|
|
8
8
|
};
|
|
@@ -33,20 +33,19 @@ export default class Test extends Command {
|
|
|
33
33
|
description: 'The Sanity dataset to use',
|
|
34
34
|
required: false,
|
|
35
35
|
}),
|
|
36
|
-
project: Flags.string({
|
|
37
|
-
description: '
|
|
36
|
+
'project-id': Flags.string({
|
|
37
|
+
description: 'Sanity Project ID to use',
|
|
38
|
+
aliases: ['project', 'projectId'],
|
|
38
39
|
required: false,
|
|
39
40
|
}),
|
|
40
41
|
};
|
|
41
42
|
async run() {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
43
|
+
const args = this.args;
|
|
44
|
+
const flags = this.flags;
|
|
45
|
+
const parsedBlueprint = this.blueprint.parsedBlueprint;
|
|
44
46
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
this.error(`Error: Function ${args.name} has no source code`);
|
|
48
|
-
}
|
|
49
|
-
const { json, logs, error } = await testAction(src, {
|
|
47
|
+
const resource = findFunctionByName(parsedBlueprint, args.name);
|
|
48
|
+
const { json, logs, error } = await testAction(resource, {
|
|
50
49
|
data: flags.data,
|
|
51
50
|
file: flags.file,
|
|
52
51
|
timeout: flags.timeout,
|
|
@@ -54,7 +53,7 @@ export default class Test extends Command {
|
|
|
54
53
|
clientOptions: {
|
|
55
54
|
apiVersion: flags.api,
|
|
56
55
|
dataset: flags.dataset,
|
|
57
|
-
projectId: flags
|
|
56
|
+
projectId: flags['project-id'],
|
|
58
57
|
},
|
|
59
58
|
});
|
|
60
59
|
if (!error) {
|
package/dist/server/app.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import * as http from 'node:http';
|
|
2
3
|
import { default as mime } from 'mime-types';
|
|
3
|
-
import {
|
|
4
|
+
import { readLocalBlueprint } from '../actions/blueprints/blueprint.js';
|
|
4
5
|
import config from '../config.js';
|
|
6
|
+
import { findFunctionByName } from '../utils/find-function.js';
|
|
5
7
|
import invoke from '../utils/invoke-local.js';
|
|
6
|
-
import
|
|
8
|
+
import { isRecord } from '../utils/is-record.js';
|
|
7
9
|
const host = 'localhost';
|
|
8
10
|
const app = (port) => {
|
|
9
11
|
const requestListener = async (req, res) => {
|
|
@@ -11,33 +13,43 @@ const app = (port) => {
|
|
|
11
13
|
switch (true) {
|
|
12
14
|
case req.url === '/blueprint': {
|
|
13
15
|
try {
|
|
14
|
-
const { parsedBlueprint } = await
|
|
16
|
+
const { parsedBlueprint } = await readLocalBlueprint();
|
|
15
17
|
res.setHeader('Content-Type', 'application/json');
|
|
16
18
|
res.writeHead(200);
|
|
17
|
-
res.end(JSON.stringify(parsedBlueprint));
|
|
19
|
+
res.end(JSON.stringify(parsedBlueprint)); // Use parsedBlueprint directly
|
|
18
20
|
}
|
|
19
|
-
catch {
|
|
21
|
+
catch (error) {
|
|
20
22
|
res.writeHead(404);
|
|
21
|
-
res.end();
|
|
23
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }));
|
|
22
24
|
}
|
|
23
25
|
break;
|
|
24
26
|
}
|
|
25
27
|
case req.url === '/invoke': {
|
|
26
28
|
if (req.method === 'POST') {
|
|
27
|
-
|
|
28
|
-
req.on('data', (data) =>
|
|
29
|
-
body += data;
|
|
30
|
-
});
|
|
29
|
+
const body = [];
|
|
30
|
+
req.on('data', (data) => body.push(data));
|
|
31
31
|
req.on('end', async () => {
|
|
32
|
-
const { data, func } = JSON.parse(body);
|
|
33
|
-
const { context, event } = data;
|
|
34
32
|
res.setHeader('Content-Type', 'application/json');
|
|
35
33
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
34
|
+
const { data, func: functionName } = parseInvokeRequest(Buffer.concat(body));
|
|
35
|
+
const { context, event } = data;
|
|
36
|
+
const start = performance.now();
|
|
37
|
+
const { parsedBlueprint } = await readLocalBlueprint();
|
|
38
|
+
const resource = findFunctionByName(parsedBlueprint, functionName);
|
|
39
|
+
const readBlueprintTime = performance.now() - start;
|
|
40
|
+
const response = await invoke(resource, event, context);
|
|
41
|
+
const timings = { ...response.timings, 'blueprint:read': readBlueprintTime };
|
|
42
|
+
const timingHeaders = [];
|
|
43
|
+
for (const [key, value] of Object.entries(timings)) {
|
|
44
|
+
timingHeaders.push(`${key.replace(/:/g, '-')};dur=${Math.abs(value).toFixed(1)}`);
|
|
45
|
+
}
|
|
46
|
+
if (timingHeaders.length > 0) {
|
|
47
|
+
res.setHeader('Server-Timing', timingHeaders.join(', '));
|
|
48
|
+
}
|
|
38
49
|
res.end(JSON.stringify(response));
|
|
39
50
|
}
|
|
40
51
|
catch (error) {
|
|
52
|
+
console.error(error);
|
|
41
53
|
const response = { logs: '', error: '' };
|
|
42
54
|
if (error instanceof Error) {
|
|
43
55
|
response.logs = error.message;
|
|
@@ -112,4 +124,60 @@ const app = (port) => {
|
|
|
112
124
|
const server = http.createServer(requestListener);
|
|
113
125
|
server.listen(port, host, () => { });
|
|
114
126
|
};
|
|
127
|
+
function parseInvokeRequest(body) {
|
|
128
|
+
let json;
|
|
129
|
+
try {
|
|
130
|
+
json = JSON.parse(body.toString('utf8'));
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
throw new Error('Request body is not valid JSON', { cause: error });
|
|
134
|
+
}
|
|
135
|
+
if (!isRecord(json)) {
|
|
136
|
+
throw new Error('Request body is not valid, must be an object');
|
|
137
|
+
}
|
|
138
|
+
if (!('func' in json)) {
|
|
139
|
+
throw new Error('Request body is not valid, missing `func` field');
|
|
140
|
+
}
|
|
141
|
+
if (!('data' in json)) {
|
|
142
|
+
throw new Error('Request body is not valid, missing `data` field');
|
|
143
|
+
}
|
|
144
|
+
const { data, func } = json;
|
|
145
|
+
if (typeof func !== 'string') {
|
|
146
|
+
throw new Error('Request body is not valid, `func` field is not a string');
|
|
147
|
+
}
|
|
148
|
+
if (!isRecord(data)) {
|
|
149
|
+
throw new Error('Request body is not valid, `data` field is not an object');
|
|
150
|
+
}
|
|
151
|
+
const { context, event } = data;
|
|
152
|
+
if (!isRecord(context)) {
|
|
153
|
+
throw new Error('Request body is not valid, `context` field is not an object');
|
|
154
|
+
}
|
|
155
|
+
if (!isRecord(event)) {
|
|
156
|
+
throw new Error('Request body is not valid, `event` field is not an object');
|
|
157
|
+
}
|
|
158
|
+
if (!('clientOptions' in context)) {
|
|
159
|
+
throw new Error('Request body is not valid, `context.clientOptions` field is missing');
|
|
160
|
+
}
|
|
161
|
+
if (!isRecord(context.clientOptions)) {
|
|
162
|
+
throw new Error('Request body is not valid, `context.clientOptions` field is not an object');
|
|
163
|
+
}
|
|
164
|
+
const { projectId, dataset, apiVersion } = context.clientOptions;
|
|
165
|
+
if (typeof projectId !== 'string' && typeof projectId !== 'undefined') {
|
|
166
|
+
throw new Error('Request body is not valid, `context.clientOptions.projectId` field is not a string');
|
|
167
|
+
}
|
|
168
|
+
if (typeof dataset !== 'string' && typeof dataset !== 'undefined') {
|
|
169
|
+
throw new Error('Request body is not valid, `context.clientOptions.dataset` field is not a string');
|
|
170
|
+
}
|
|
171
|
+
if (typeof apiVersion !== 'string' && typeof apiVersion !== 'undefined') {
|
|
172
|
+
throw new Error('Request body is not valid, `context.clientOptions.apiVersion` field is not a string');
|
|
173
|
+
}
|
|
174
|
+
const clientOptions = {
|
|
175
|
+
...context.clientOptions,
|
|
176
|
+
projectId,
|
|
177
|
+
dataset,
|
|
178
|
+
// Prefer `undefined` over empty string, triggering the right warnings in the client
|
|
179
|
+
apiVersion: apiVersion || undefined,
|
|
180
|
+
};
|
|
181
|
+
return { func, data: { context: { ...context, clientOptions }, event } };
|
|
182
|
+
}
|
|
115
183
|
export default app;
|
|
@@ -30,12 +30,15 @@ function invoke(payloadText = '{}') {
|
|
|
30
30
|
},
|
|
31
31
|
method: 'POST',
|
|
32
32
|
})
|
|
33
|
-
.then((response) =>
|
|
34
|
-
|
|
33
|
+
.then((response) => {
|
|
34
|
+
return response.json().then((data) => ({data, timings: getServerTimings(response)}))
|
|
35
|
+
})
|
|
36
|
+
.then(({data, timings}) => {
|
|
35
37
|
store.inprogress = false
|
|
36
38
|
store.result = {
|
|
37
39
|
...data,
|
|
38
40
|
time: Date.now() - start,
|
|
41
|
+
timings,
|
|
39
42
|
}
|
|
40
43
|
})
|
|
41
44
|
}
|
|
@@ -47,7 +50,7 @@ function blueprint() {
|
|
|
47
50
|
const functions = blueprint?.resources.filter((r) => r.type.startsWith('sanity.function.'))
|
|
48
51
|
|
|
49
52
|
store.functions = functions
|
|
50
|
-
store.selectedIndex = functions[0].
|
|
53
|
+
store.selectedIndex = functions[0].name
|
|
51
54
|
})
|
|
52
55
|
.catch(() => {
|
|
53
56
|
store.functions = []
|
|
@@ -77,3 +80,21 @@ function datasets(selectedProject) {
|
|
|
77
80
|
store.datasets = []
|
|
78
81
|
})
|
|
79
82
|
}
|
|
83
|
+
|
|
84
|
+
function getServerTimings(response) {
|
|
85
|
+
const timings = {}
|
|
86
|
+
const serverTiming = response.headers.get('Server-Timing')
|
|
87
|
+
if (!serverTiming) {
|
|
88
|
+
return timings
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const entry of serverTiming.split(',')) {
|
|
92
|
+
const [name, ...params] = entry.split(';')
|
|
93
|
+
const durationParam = params.find((p) => p.startsWith('dur='))
|
|
94
|
+
if (durationParam) {
|
|
95
|
+
timings[name.trim()] = Number.parseFloat(durationParam.slice(4))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return timings
|
|
100
|
+
}
|
|
@@ -9,7 +9,7 @@ class FunctionList extends ApiBaseElement {
|
|
|
9
9
|
functionClicked = (event) => {
|
|
10
10
|
// eslint-disable-next-line unicorn/prefer-dom-node-text-content
|
|
11
11
|
const target = this.api.store.functions.find((func) => func.name === event.srcElement.innerText)
|
|
12
|
-
this.api.store.selectedIndex = target.
|
|
12
|
+
this.api.store.selectedIndex = target.name
|
|
13
13
|
}
|
|
14
14
|
functionSelected = (event) => {
|
|
15
15
|
this.api.store.selectedIndex = event.srcElement.value
|
|
@@ -18,14 +18,14 @@ class FunctionList extends ApiBaseElement {
|
|
|
18
18
|
if (this.api.store.functions.length > 0) {
|
|
19
19
|
this.list.innerHTML = this.api.store.functions
|
|
20
20
|
.map((func) => {
|
|
21
|
-
const selected = this.api.store.selectedIndex === func.
|
|
21
|
+
const selected = this.api.store.selectedIndex === func.name ? 'selected' : ''
|
|
22
22
|
return `<li class="function-list-item ${selected}" style="padding: 16px 24px;">${func.name}</li>`
|
|
23
23
|
})
|
|
24
24
|
.join('')
|
|
25
25
|
this.select.innerHTML = this.api.store.functions
|
|
26
26
|
.map((func) => {
|
|
27
|
-
const selected = this.api.store.selectedIndex === func.
|
|
28
|
-
return `<option value="${func.
|
|
27
|
+
const selected = this.api.store.selectedIndex === func.name ? 'selected' : ''
|
|
28
|
+
return `<option value="${func.name}" ${selected}>${func.name}</option>`
|
|
29
29
|
})
|
|
30
30
|
.join('')
|
|
31
31
|
} else {
|
|
@@ -34,7 +34,7 @@ class ResponsePanel extends ApiBaseElement {
|
|
|
34
34
|
updateResponse = ({result}) => {
|
|
35
35
|
if (!result) return
|
|
36
36
|
|
|
37
|
-
const {error, json, time} = result
|
|
37
|
+
const {error, json, logs, time, timings} = result
|
|
38
38
|
if (!error) {
|
|
39
39
|
const transaction = this.api.store.response.state.update({
|
|
40
40
|
changes: {
|
|
@@ -46,8 +46,19 @@ class ResponsePanel extends ApiBaseElement {
|
|
|
46
46
|
this.api.store.response.dispatch(transaction)
|
|
47
47
|
|
|
48
48
|
this.size.innerText = json ? prettyBytes(JSON.stringify(json).length) : ''
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
|
|
50
|
+
if (timings && 'bundle' in timings && 'execute' in timings) {
|
|
51
|
+
const bundleTime = prettyMilliseconds(timings.bundle)
|
|
52
|
+
const executeTime = prettyMilliseconds(timings.execute)
|
|
53
|
+
this.time.innerText = `${executeTime} (+${bundleTime} bundle time)`
|
|
54
|
+
this.time.dateTime = `PT${executeTime / 1000}S`
|
|
55
|
+
} else if (timings && 'execute' in timings) {
|
|
56
|
+
this.time.innerText = prettyMilliseconds(timings.execute)
|
|
57
|
+
this.time.dateTime = `PT${timings.execute / 1000}S`
|
|
58
|
+
} else {
|
|
59
|
+
this.time.innerText = prettyMilliseconds(time)
|
|
60
|
+
this.time.dateTime = `PT${time / 1000}S`
|
|
61
|
+
}
|
|
51
62
|
} else {
|
|
52
63
|
const transaction = this.api.store.response.state.update({
|
|
53
64
|
changes: {from: 0, to: this.api.store.response.state.doc.length, insert: ''},
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { InvokePayloadOptions } from './types.js';
|
|
2
|
-
export default function buildPayload(options: InvokePayloadOptions):
|
|
2
|
+
export default function buildPayload(options: InvokePayloadOptions): Record<string, unknown> | null;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { cwd } from 'node:process';
|
|
4
|
-
import
|
|
4
|
+
import { parseJsonObject } from './parse-json-object.js';
|
|
5
5
|
export default function buildPayload(options) {
|
|
6
6
|
const { data, file } = options;
|
|
7
7
|
let payload = {};
|
|
8
8
|
if (data) {
|
|
9
|
-
payload =
|
|
9
|
+
payload = parseJsonObject(data);
|
|
10
10
|
}
|
|
11
11
|
else if (file) {
|
|
12
|
-
payload =
|
|
12
|
+
payload = parseJsonObject(readFileSync(join(cwd(), file), 'utf8'));
|
|
13
13
|
}
|
|
14
14
|
return payload;
|
|
15
15
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LocalFunctionResource } from '../types.js';
|
|
2
|
+
export declare function bundleFunction(resource: LocalFunctionResource): Promise<{
|
|
3
|
+
type: string;
|
|
4
|
+
outputDir: string;
|
|
5
|
+
warnings: string[];
|
|
6
|
+
cleanup: () => Promise<void>;
|
|
7
|
+
timings: Record<string, number>;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { performance } from 'node:perf_hooks';
|
|
4
|
+
import { cwd } from 'node:process';
|
|
5
|
+
import { build as viteBuild } from 'vite';
|
|
6
|
+
import tsConfigPaths from 'vite-tsconfig-paths';
|
|
7
|
+
import { findFunctionEntryPoint } from '../functions/find-entry-point.js';
|
|
8
|
+
import { cleanupSourceMaps } from './cleanup-source-maps.js';
|
|
9
|
+
import { findDirUp } from './find-up.js';
|
|
10
|
+
import { verifyHandler } from './verify-handler.js';
|
|
11
|
+
export async function bundleFunction(resource) {
|
|
12
|
+
if (!resource.src)
|
|
13
|
+
throw new Error('Resource src is required');
|
|
14
|
+
if (!resource.name)
|
|
15
|
+
throw new Error('Resource name is required');
|
|
16
|
+
const timings = {};
|
|
17
|
+
const bundleStart = performance.now();
|
|
18
|
+
const sourcePath = path.resolve(cwd(), resource.src);
|
|
19
|
+
const stats = await stat(sourcePath);
|
|
20
|
+
const fnDisplayName = resource.displayName ?? resource.name;
|
|
21
|
+
const findEntryStart = performance.now();
|
|
22
|
+
const entry = await findFunctionEntryPoint(sourcePath, fnDisplayName);
|
|
23
|
+
timings['bundle:findEntry'] = performance.now() - findEntryStart;
|
|
24
|
+
const entryDir = stats.isFile() ? path.dirname(sourcePath) : sourcePath;
|
|
25
|
+
const outputPathStart = performance.now();
|
|
26
|
+
const outputDir = await getBundleOutputPath(entryDir, resource.name);
|
|
27
|
+
const outputFile = path.join(outputDir, getOutputFilename(entry));
|
|
28
|
+
const fnRootDir = (await findDirUp('node_modules', entryDir)) || entryDir;
|
|
29
|
+
timings['bundle:setupOutput'] = performance.now() - outputPathStart;
|
|
30
|
+
async function cleanupTmpDir() {
|
|
31
|
+
// Feel a certain way about leaving things uncleaned, but helps with debugging for now
|
|
32
|
+
// await rm(outputDir, {recursive: true, force: true}).catch(logCleanupFailure)
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const viteStart = performance.now();
|
|
36
|
+
const result = await viteBuild({
|
|
37
|
+
root: fnRootDir,
|
|
38
|
+
logLevel: 'silent',
|
|
39
|
+
build: {
|
|
40
|
+
target: 'node20',
|
|
41
|
+
outDir: outputDir,
|
|
42
|
+
emptyOutDir: false,
|
|
43
|
+
minify: false,
|
|
44
|
+
sourcemap: true,
|
|
45
|
+
ssr: true,
|
|
46
|
+
rollupOptions: {
|
|
47
|
+
input: entry,
|
|
48
|
+
output: {
|
|
49
|
+
format: 'esm',
|
|
50
|
+
entryFileNames: getOutputFilename(entry),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
ssr: {
|
|
55
|
+
noExternal: true,
|
|
56
|
+
resolve: {
|
|
57
|
+
conditions: ['sanity-function', 'vite'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
plugins: [tsConfigPaths()],
|
|
61
|
+
});
|
|
62
|
+
timings['bundle:build'] = performance.now() - viteStart;
|
|
63
|
+
const verifyStart = performance.now();
|
|
64
|
+
await verifyHandler(result);
|
|
65
|
+
timings['bundle:verify'] = performance.now() - verifyStart;
|
|
66
|
+
const pkgStart = performance.now();
|
|
67
|
+
await writeBundledPackageJson(entryDir, outputFile);
|
|
68
|
+
timings['bundle:writePackage'] = performance.now() - pkgStart;
|
|
69
|
+
const cleanupStart = performance.now();
|
|
70
|
+
await cleanupSourceMaps(sourcePath, outputDir);
|
|
71
|
+
timings['bundle:cleanupMaps'] = performance.now() - cleanupStart;
|
|
72
|
+
timings.bundle = performance.now() - bundleStart;
|
|
73
|
+
return {
|
|
74
|
+
type: 'success',
|
|
75
|
+
outputDir,
|
|
76
|
+
warnings: [],
|
|
77
|
+
cleanup: cleanupTmpDir,
|
|
78
|
+
timings,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
await cleanupTmpDir();
|
|
83
|
+
throw new Error(`Bundling of function failed: ${err instanceof Error ? err.message : err}`, {
|
|
84
|
+
cause: err,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function writeBundledPackageJson(inputDir, outputFilePath) {
|
|
89
|
+
const baseName = path.basename(outputFilePath);
|
|
90
|
+
let original;
|
|
91
|
+
try {
|
|
92
|
+
const pkgJsonPath = path.join(inputDir, 'package.json');
|
|
93
|
+
original = JSON.parse(await readFile(pkgJsonPath, 'utf-8'));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
original = undefined;
|
|
97
|
+
}
|
|
98
|
+
const bundled = {
|
|
99
|
+
// One could argue that we should strip this down significantly.
|
|
100
|
+
// Theoretically though, a function may reach into it to get dependency versions
|
|
101
|
+
// and whatnot, so maybe it makes more sense to keep it as close to the original
|
|
102
|
+
// as possible?
|
|
103
|
+
...original,
|
|
104
|
+
main: baseName, // This should never be the input file, always the built output name
|
|
105
|
+
type: 'module', // We explicitly create ESM output
|
|
106
|
+
};
|
|
107
|
+
const pkgJsonOutputPath = path.join(path.dirname(outputFilePath), 'package.json');
|
|
108
|
+
await writeFile(pkgJsonOutputPath, JSON.stringify(bundled, null, 2));
|
|
109
|
+
}
|
|
110
|
+
async function getBundleOutputPath(entryDir, fnName) {
|
|
111
|
+
const tmpPath = path.resolve(entryDir, '.build', `function-${fnName}`);
|
|
112
|
+
await rm(tmpPath, { recursive: true, force: true }).catch(logCleanupFailure);
|
|
113
|
+
await mkdir(tmpPath, { recursive: true });
|
|
114
|
+
return tmpPath;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Minor convenience/niceness to keep the same input filename, but change the extension
|
|
118
|
+
*/
|
|
119
|
+
function getOutputFilename(entryFileName) {
|
|
120
|
+
const baseName = path.basename(entryFileName, path.extname(entryFileName));
|
|
121
|
+
return baseName ? `${baseName}.js` : 'index.js';
|
|
122
|
+
}
|
|
123
|
+
function logCleanupFailure(err) {
|
|
124
|
+
console.warn(`[warn] Failed to clean up temporary files: ${err instanceof Error ? err.message : err}`);
|
|
125
|
+
}
|