@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.
Files changed (82) hide show
  1. package/README.md +49 -77
  2. package/dist/actions/blueprints/assets.d.ts +1 -0
  3. package/dist/actions/blueprints/assets.js +21 -4
  4. package/dist/actions/blueprints/blueprint.d.ts +6 -12
  5. package/dist/actions/blueprints/blueprint.js +38 -45
  6. package/dist/actions/blueprints/index.d.ts +33 -0
  7. package/dist/actions/blueprints/index.js +32 -0
  8. package/dist/actions/blueprints/projects.d.ts +9 -0
  9. package/dist/actions/blueprints/projects.js +12 -0
  10. package/dist/actions/blueprints/stacks.d.ts +0 -12
  11. package/dist/actions/blueprints/stacks.js +3 -30
  12. package/dist/actions/functions/test.d.ts +2 -2
  13. package/dist/actions/functions/test.js +2 -2
  14. package/dist/baseCommands.d.ts +24 -0
  15. package/dist/baseCommands.js +69 -0
  16. package/dist/commands/blueprints/add.d.ts +1 -1
  17. package/dist/commands/blueprints/add.js +7 -6
  18. package/dist/commands/blueprints/config.d.ts +1 -1
  19. package/dist/commands/blueprints/config.js +24 -11
  20. package/dist/commands/blueprints/deploy.d.ts +2 -2
  21. package/dist/commands/blueprints/deploy.js +18 -33
  22. package/dist/commands/blueprints/destroy.d.ts +4 -3
  23. package/dist/commands/blueprints/destroy.js +32 -35
  24. package/dist/commands/blueprints/info.d.ts +2 -2
  25. package/dist/commands/blueprints/info.js +16 -36
  26. package/dist/commands/blueprints/init.d.ts +10 -2
  27. package/dist/commands/blueprints/init.js +85 -26
  28. package/dist/commands/blueprints/logs.d.ts +2 -2
  29. package/dist/commands/blueprints/logs.js +18 -32
  30. package/dist/commands/blueprints/plan.d.ts +2 -2
  31. package/dist/commands/blueprints/plan.js +10 -16
  32. package/dist/commands/blueprints/stacks.d.ts +3 -2
  33. package/dist/commands/blueprints/stacks.js +10 -29
  34. package/dist/commands/functions/env/add.d.ts +2 -2
  35. package/dist/commands/functions/env/add.js +6 -17
  36. package/dist/commands/functions/env/list.d.ts +2 -2
  37. package/dist/commands/functions/env/list.js +10 -17
  38. package/dist/commands/functions/env/remove.d.ts +2 -2
  39. package/dist/commands/functions/env/remove.js +6 -17
  40. package/dist/commands/functions/invoke.d.ts +2 -2
  41. package/dist/commands/functions/invoke.js +7 -14
  42. package/dist/commands/functions/logs.d.ts +3 -7
  43. package/dist/commands/functions/logs.js +21 -37
  44. package/dist/commands/functions/test.d.ts +3 -3
  45. package/dist/commands/functions/test.js +13 -14
  46. package/dist/server/app.js +82 -14
  47. package/dist/server/static/api.js +24 -3
  48. package/dist/server/static/components/function-list.js +4 -4
  49. package/dist/server/static/components/response-panel.js +14 -3
  50. package/dist/server/static/index.html +1 -0
  51. package/dist/utils/build-payload.d.ts +1 -1
  52. package/dist/utils/build-payload.js +3 -3
  53. package/dist/utils/bundle/bundle-function.d.ts +8 -0
  54. package/dist/utils/bundle/bundle-function.js +125 -0
  55. package/dist/utils/bundle/cleanup-source-maps.d.ts +10 -0
  56. package/dist/utils/bundle/cleanup-source-maps.js +53 -0
  57. package/dist/utils/bundle/find-up.d.ts +16 -0
  58. package/dist/utils/bundle/find-up.js +39 -0
  59. package/dist/utils/bundle/verify-handler.d.ts +2 -0
  60. package/dist/utils/bundle/verify-handler.js +13 -0
  61. package/dist/utils/child-process-wrapper.js +8 -6
  62. package/dist/utils/display/blueprints-formatting.js +2 -2
  63. package/dist/utils/display/errors.d.ts +4 -0
  64. package/dist/utils/display/errors.js +27 -0
  65. package/dist/utils/display/index.d.ts +1 -0
  66. package/dist/utils/display/index.js +1 -0
  67. package/dist/utils/functions/find-entry-point.d.ts +11 -0
  68. package/dist/utils/functions/find-entry-point.js +75 -0
  69. package/dist/utils/functions/should-bundle.d.ts +2 -0
  70. package/dist/utils/functions/should-bundle.js +23 -0
  71. package/dist/utils/invoke-local.d.ts +2 -2
  72. package/dist/utils/invoke-local.js +49 -8
  73. package/dist/utils/is-record.d.ts +1 -0
  74. package/dist/utils/is-record.js +3 -0
  75. package/dist/utils/parse-json-object.d.ts +1 -0
  76. package/dist/utils/parse-json-object.js +10 -0
  77. package/dist/utils/types.d.ts +13 -4
  78. package/dist/utils/types.js +9 -3
  79. package/oclif.manifest.json +59 -37
  80. package/package.json +5 -1
  81. package/dist/utils/is-json.d.ts +0 -1
  82. package/dist/utils/is-json.js +0 -12
@@ -1,11 +1,10 @@
1
- import { Args, Command, Flags } from '@oclif/core';
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
- import { validTokenOrErrorMessage } from '../../utils/validated-token.js';
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 { token, error: tokenErr } = await validTokenOrErrorMessage();
27
- if (tokenErr)
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 { deployedStack } = await readBlueprintOnDisk({ getStack: true, token });
32
- if (!deployedStack)
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 { Command } from '@oclif/core';
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 Logs extends Command {
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
- sanityToken: string | undefined;
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, Command, Flags } from '@oclif/core';
1
+ import { Args, Flags } from '@oclif/core';
2
2
  import inquirer from 'inquirer';
3
3
  import Spinner from 'yocto-spinner';
4
- import { readBlueprintOnDisk } from '../../actions/blueprints/blueprint.js';
5
- import { deleteLogs as deleteLogsAction, logs as logsAction } from '../../actions/functions/logs.js';
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 Logs extends Command {
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
- sanityToken;
54
+ externalId;
56
55
  async run() {
57
- const { token, error: tokenErr } = await validTokenOrErrorMessage();
58
- if (tokenErr)
59
- this.error(tokenErr.message);
60
- this.sanityToken = token;
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, { token: this.sanityToken, projectId });
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 { projectId, externalId } = await this.getProjectAndExternalId(name);
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(`${bold(date.toLocaleDateString())} ${bold(blue(date.toLocaleTimeString()))} ${logLevel(level)} ${message}`);
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 { Command } from '@oclif/core';
2
- export default class Test extends Command {
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, Command, Flags } from '@oclif/core';
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 { getFunctionSource } from '../../utils/find-function.js';
5
- export default class Test extends Command {
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: 'The Sanity project to use',
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 { args, flags } = await this.parse(Test);
43
- const { parsedBlueprint } = await readBlueprintOnDisk({ getStack: false });
43
+ const args = this.args;
44
+ const flags = this.flags;
45
+ const parsedBlueprint = this.blueprint.parsedBlueprint;
44
46
  try {
45
- const src = getFunctionSource(parsedBlueprint, args.name);
46
- if (!src) {
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.project,
56
+ projectId: flags['project-id'],
58
57
  },
59
58
  });
60
59
  if (!error) {
@@ -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 { readBlueprintOnDisk } from '../actions/blueprints/blueprint.js';
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 * as http from 'node:http';
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 readBlueprintOnDisk({ getStack: false });
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
- let body = '';
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 response = await invoke(func, event, context);
37
- res.writeHead(200);
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) => response.json())
34
- .then((data) => {
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].src
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.src
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.src ? 'selected' : ''
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.src ? 'selected' : ''
28
- return `<option value="${func.src}" ${selected}>${func.name}</option>`
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
- this.time.innerText = prettyMilliseconds(time)
50
- this.time.dateTime = `PT${time / 1000}S`
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,6 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
+ <meta charset="UTF-8">
4
5
  <title>Sanity Functions</title>
5
6
  <link href="https://unpkg.com/m-@3.2.0/dist/m-.woff2" rel="preload" as="font" crossorigin>
6
7
  <link href="https://unpkg.com/m-@3.2.0/dist/m-.css" rel="stylesheet">
@@ -1,2 +1,2 @@
1
1
  import type { InvokePayloadOptions } from './types.js';
2
- export default function buildPayload(options: InvokePayloadOptions): object | null;
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 isJson from './is-json.js';
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 = isJson(data);
9
+ payload = parseJsonObject(data);
10
10
  }
11
11
  else if (file) {
12
- payload = isJson(readFileSync(join(cwd(), file), 'utf8'));
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
+ }