@sanity/runtime-cli 4.3.3 → 4.3.5-bundle.0

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 (51) hide show
  1. package/README.md +17 -17
  2. package/dist/actions/blueprints/assets.d.ts +1 -0
  3. package/dist/actions/blueprints/assets.js +21 -4
  4. package/dist/actions/functions/test.d.ts +2 -2
  5. package/dist/actions/functions/test.js +2 -2
  6. package/dist/commands/blueprints/config.d.ts +1 -1
  7. package/dist/commands/blueprints/config.js +8 -8
  8. package/dist/commands/blueprints/deploy.js +3 -4
  9. package/dist/commands/blueprints/init.d.ts +1 -1
  10. package/dist/commands/blueprints/init.js +8 -8
  11. package/dist/commands/functions/logs.d.ts +1 -1
  12. package/dist/commands/functions/logs.js +8 -8
  13. package/dist/commands/functions/test.js +3 -6
  14. package/dist/server/app.js +80 -12
  15. package/dist/server/static/api.js +24 -3
  16. package/dist/server/static/components/app.css +915 -54
  17. package/dist/server/static/components/dataset-dropdown.js +5 -3
  18. package/dist/server/static/components/filters.d.ts +1 -0
  19. package/dist/server/static/components/filters.js +20 -0
  20. package/dist/server/static/components/function-list.js +7 -7
  21. package/dist/server/static/components/payload-panel.js +18 -17
  22. package/dist/server/static/components/projects-dropdown.js +5 -3
  23. package/dist/server/static/components/response-panel.js +38 -28
  24. package/dist/server/static/index.html +11 -30
  25. package/dist/server/static/vendor/vendor.bundle.d.ts +2 -2
  26. package/dist/utils/build-payload.d.ts +1 -1
  27. package/dist/utils/build-payload.js +3 -3
  28. package/dist/utils/bundle/bundle-function.d.ts +8 -0
  29. package/dist/utils/bundle/bundle-function.js +125 -0
  30. package/dist/utils/bundle/cleanup-source-maps.d.ts +10 -0
  31. package/dist/utils/bundle/cleanup-source-maps.js +53 -0
  32. package/dist/utils/bundle/find-up.d.ts +16 -0
  33. package/dist/utils/bundle/find-up.js +39 -0
  34. package/dist/utils/bundle/verify-handler.d.ts +2 -0
  35. package/dist/utils/bundle/verify-handler.js +13 -0
  36. package/dist/utils/child-process-wrapper.js +8 -6
  37. package/dist/utils/functions/find-entry-point.d.ts +11 -0
  38. package/dist/utils/functions/find-entry-point.js +75 -0
  39. package/dist/utils/functions/should-bundle.d.ts +2 -0
  40. package/dist/utils/functions/should-bundle.js +23 -0
  41. package/dist/utils/invoke-local.d.ts +2 -2
  42. package/dist/utils/invoke-local.js +48 -7
  43. package/dist/utils/is-record.d.ts +1 -0
  44. package/dist/utils/is-record.js +3 -0
  45. package/dist/utils/parse-json-object.d.ts +1 -0
  46. package/dist/utils/parse-json-object.js +10 -0
  47. package/dist/utils/types.d.ts +3 -1
  48. package/oclif.manifest.json +1 -1
  49. package/package.json +4 -1
  50. package/dist/utils/is-json.d.ts +0 -1
  51. package/dist/utils/is-json.js +0 -12
@@ -2,9 +2,11 @@
2
2
  import {ApiBaseElement} from './api-base.js'
3
3
 
4
4
  const template = `<fieldset class="mar-t-sm">
5
- <label>Dataset</label>
6
- <select id="dataset">
7
- </select>
5
+ <label class="config-label">
6
+ <span>Dataset</span>
7
+ <select id="datasetname" style="background: transparent; color: light-dark(var(--gray-950), var(--gray-300));">
8
+ </select>
9
+ </label>
8
10
  </fieldset>
9
11
  `
10
12
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ class FiltersComponent extends HTMLElement {
2
+ connectedCallback() {
3
+ this.innerHTML = `
4
+ <form style="display:flex; gap: 8px;padding-left: var(--space-3); padding-bottom: var(--space-3); border-bottom: 1px solid var(--card-border-color);">
5
+ <dataset-dropdown></dataset-dropdown>
6
+ <fieldset class="mar-t-sm">
7
+ <label class="config-label">
8
+ <span>API Version</span>
9
+
10
+ <input name="apiVersion" id="apiversion" style="background: transparent; border-color: light-dark(var(--gray-200), var(--gray-700))">
11
+ </label>
12
+ </fieldset>
13
+ <projects-dropdown></projects-dropdown>
14
+
15
+ </form>
16
+ `
17
+ }
18
+ }
19
+
20
+ customElements.define('filters-component', FiltersComponent)
@@ -1,15 +1,15 @@
1
1
  /* globals customElements */
2
2
  import {ApiBaseElement} from './api-base.js'
3
3
 
4
- const template = `<ol class="hidden-lg" type="content"></ol>
5
- <fieldset class="pad-sm hidden block-lg"><select></select></fieldset>
4
+ const template = `<ol class="hidden-lg" type="content" style="padding:0 16px;"></ol>
5
+ <fieldset class="hidden block-lg" style="padding:0 var(--space-3); margin-bottom: var(--space-3);"><select></select></fieldset>
6
6
  `
7
7
 
8
8
  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' : ''
22
- return `<li class="pad-sm ${selected}">${func.name}</li>`
21
+ const selected = this.api.store.selectedIndex === func.name ? 'selected' : ''
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 {
@@ -2,19 +2,19 @@
2
2
  import {EditorView, basicSetup, json} from '../vendor/vendor.bundle.js'
3
3
  import {ApiBaseElement} from './api-base.js'
4
4
 
5
- const template = `<m-box>
6
- <h2 class="mar-t-0">Payload</h2>
7
- <div id="payload" name="payload"></div>
8
- <form class="mar-t-sm">
9
- <fieldset>
10
- <label>API Version</label>
11
- <input name="apiVersion" id="apiversion">
12
- </fieldset>
13
- <projects-dropdown></projects-dropdown>
14
- <dataset-dropdown></dataset-dropdown>
15
- </form>
16
- <button ord="primary" class="mar-t-sm sanity-button">Invoke</button>
17
- </m-box>
5
+ const template = `<div class="gutter-gradient relative h-100 max-h-100 y-scroll border-top border-top-none-l">
6
+ <div class="bg gutter-gradient sticky top-0 right-0 left-0 z-100">
7
+ <div class="flex items-center space-between" style="padding: 8px 20px 8px 48px;">
8
+ <h2 class="config-label mar-t-0 mar-b-0">Event</h2>
9
+ <button ord="primary" class="sanity-button">
10
+ <span>Run</span>
11
+ <svg data-sanity-icon="play" width="1em" height="1em" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 18.5V6.5L17.5 12.5L7.5 18.5Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"></path></svg>
12
+ </button>
13
+ </div>
14
+ <hr class='hr-border' style='margin-left: 31px; ' />
15
+ </div>
16
+ <div id="payload" name="payload" class='max-h-100 min-h-0'></div>
17
+ </div>
18
18
  `
19
19
  class PayloadPanel extends ApiBaseElement {
20
20
  invoke = () => {
@@ -35,16 +35,17 @@ class PayloadPanel extends ApiBaseElement {
35
35
  this.button.innerHTML = '<network-spinner></network-spinner>'
36
36
  } else {
37
37
  this.button.removeAttribute('disabled')
38
- this.button.innerText = 'Invoke'
38
+ this.button.innerHTML = `<span>Run</span>
39
+ <svg data-sanity-icon="play" width="1em" height="1em" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 18.5V6.5L17.5 12.5L7.5 18.5Z" fill="currentColor" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"></path></svg>`
39
40
  }
40
41
  }
41
42
 
42
43
  connectedCallback() {
43
44
  this.innerHTML = template
44
45
  this.payload = this.querySelector('#payload')
45
- this.apiVersion = this.querySelector('#apiversion')
46
- this.datasetname = this.querySelector('#datasetname')
47
- this.productId = this.querySelector('#project')
46
+ this.apiVersion = document.querySelector('#apiversion')
47
+ this.datasetname = document.querySelector('#datasetname')
48
+ this.productId = document.querySelector('#project')
48
49
  this.button = this.querySelector('button')
49
50
  this.button.addEventListener('click', this.invoke)
50
51
  this.api.subscribe(this.updateButtonText, ['inprogress'])
@@ -2,9 +2,11 @@
2
2
  import {ApiBaseElement} from './api-base.js'
3
3
 
4
4
  const template = `<fieldset class="mar-t-sm">
5
- <label>Project</label>
6
- <select id="project">
7
- </select>
5
+ <label class='config-label'>
6
+ <span>Project</span>
7
+ <select id="project" style="background: transparent; color: light-dark(var(--gray-950), var(--gray-300));">
8
+ </select>
9
+ </label>
8
10
  </fieldset>
9
11
  `
10
12
 
@@ -10,35 +10,35 @@ import {
10
10
  } from '../vendor/vendor.bundle.js'
11
11
  import {ApiBaseElement} from './api-base.js'
12
12
 
13
- const template = `<m-box>
14
- <m-tabs role="tablist">
15
- <button id="a" role="tab" aria-selected="true">Response</button>
16
- <button id="b" role="tab">Console</button>
17
- </m-tabs>
18
- <div role="tabpanel" data-tab-id="a" class="pad-t-sm">
19
- <div class="mar-b-sm">
20
- <span id="time"></span> <span id="size"></span>
13
+ const template = `<div class="border-left border-top border-top-none-l h-100 gutter-gradient" style='height: 100%; max-height: 100%; overflow: hidden; display:grid;grid-template-rows: 50% 50%; grid-template-columns: 1fr;' >
14
+ <!-- Response Section -->
15
+ <div style='overflow-y:scroll;'>
16
+ <div style='padding-bottom: var(--space-6)'>
17
+ <h3 class="config-label" style='display: none; margin-top: 0;'>Response</h3>
18
+ <header class='flex space-between'>
19
+ <dl class='slab-stat'>
20
+ <dt style='white-space:nowrap;'>Response size</dt>
21
+ <dd id="size" style='white-space:nowrap;'></dd>
22
+ </dl>
23
+ <dl class='slab-stat'>
24
+ <dt>Time</dt>
25
+ <dd><time id="time" datetime=""></time></dd>
26
+ </dl>
27
+ </header>
28
+ <div id="response" name="response" class="cm-s-dracula"></div>
21
29
  </div>
22
- <div id="response" name="response"></div>
30
+ </div>
31
+
32
+ <!-- Console Section -->
33
+ <div style='position: relative; border-top: 1px solid var(--card-border-color); background: var(--base-background-color); padding: var(--space-0) var(--space-3) var(--space-6) var(--space-4);overflow-y:scroll;'>
34
+ <h3 class="config-label" style="padding-top: var(--space-3); padding-bottom: var(--space-3); z-index: 32; background: var(--base-background-color); position: sticky; top: 0; left: 0; right: 0; margin-top:0;margin-bottom:0;">Console</h3>
35
+ <pre style="padding: 0; margin: 0; white-space: pre-wrap; word-wrap: break-word;"></pre>
23
36
  </div>
24
- <div role="tabpanel" data-tab-id="b" class="pad-t-sm" hidden><pre></pre></div>
25
- </m-box>
37
+ </div>
26
38
  `
27
39
  class ResponsePanel extends ApiBaseElement {
28
- switchTab = (e) => {
29
- const selectedTabId = e.target.closest('[role=tab]').id
30
-
31
- // Select the tab and its panel
32
- for (const tab of e.currentTarget.querySelectorAll('[role=tab]')) {
33
- tab.ariaSelected = tab.id === selectedTabId
34
- }
35
-
36
- for (const panel of document.querySelectorAll('[role=tabpanel')) {
37
- panel.hidden = panel.dataset.tabId !== selectedTabId
38
- }
39
- }
40
40
  updateResponse = ({result}) => {
41
- const {error, json, logs, time} = result
41
+ const {error, json, logs, time, timings} = result
42
42
  if (!error) {
43
43
  const transaction = this.api.store.response.state.update({
44
44
  changes: {
@@ -50,7 +50,20 @@ class ResponsePanel extends ApiBaseElement {
50
50
  this.api.store.response.dispatch(transaction)
51
51
 
52
52
  this.size.innerText = json ? prettyBytes(JSON.stringify(json).length) : ''
53
- this.time.innerText = prettyMilliseconds(time)
53
+
54
+ if (timings && 'bundle' in timings && 'execute' in timings) {
55
+ const bundleTime = prettyMilliseconds(timings.bundle)
56
+ const executeTime = prettyMilliseconds(timings.execute)
57
+ this.time.innerText = `${executeTime} (+${bundleTime} bundle time)`
58
+ this.time.dateTime = `PT${executeTime / 1000}S`
59
+ } else if (timings && 'execute' in timings) {
60
+ this.time.innerText = prettyMilliseconds(timings.execute)
61
+ this.time.dateTime = `PT${timings.execute / 1000}S`
62
+ } else {
63
+ this.time.innerText = prettyMilliseconds(time)
64
+ this.time.dateTime = `PT${time / 1000}S`
65
+ }
66
+
54
67
  this.consoleTab.innerText = logs
55
68
  } else {
56
69
  this.consoleTab.innerText = error?.details?.error
@@ -63,8 +76,6 @@ class ResponsePanel extends ApiBaseElement {
63
76
  this.size = this.querySelector('#size')
64
77
  this.time = this.querySelector('#time')
65
78
  this.consoleTab = this.querySelector('pre')
66
- this.tabs = this.querySelector('m-tabs')
67
- this.tabs.addEventListener('click', this.switchTab)
68
79
  this.api.subscribe(this.updateResponse, ['result'])
69
80
 
70
81
  this.api.store.response = new EditorView({
@@ -75,7 +86,6 @@ class ResponsePanel extends ApiBaseElement {
75
86
  }
76
87
 
77
88
  disconnectedCallback() {
78
- this.tabs.removeEventListener('click', this.switchTab)
79
89
  this.api.unsubscribe(this.updateResponse)
80
90
  }
81
91
  }
@@ -1,9 +1,11 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
+ <meta charset="UTF-8">
4
5
  <link href="https://unpkg.com/m-@3.2.0/dist/m-.woff2" rel="preload" as="font" crossorigin>
5
- <link href="./components/app.css" rel="stylesheet">
6
6
  <link href="https://unpkg.com/m-@3.2.0/dist/m-.css" rel="stylesheet">
7
+ <link href="./components/app.css" rel="stylesheet">
8
+
7
9
  </head>
8
10
  <body>
9
11
  <header data-slot="header">
@@ -14,39 +16,18 @@
14
16
  <span class="logo-text pad-xs">Functions</span>
15
17
  </div>
16
18
  </header>
17
- <nav>
18
- <m-container slot="left-sidebar">
19
- <m-box class="pad-0">
20
- <function-list></function-list>
21
- </m-box>
22
- </m-container>
19
+ <filters-component></filters-component>
20
+ <nav style="padding-top: var(--size-5); border-right: 1px solid var(--card-border-color);max-height: 100%; overflow-y: scroll;">
21
+ <h4 class="config-label mar-t-0" style="padding-left: var(--space-3); margin-bottom:var(--space-3);">Functions</h4>
22
+ <function-list></function-list>
23
23
  </nav>
24
- <main>
25
- <m-container>
26
- <m-row>
27
- <m-col>
28
- <payload-panel></payload-panel>
29
- </m-col>
30
- </m-row>
31
- </m-container>
32
- <m-container>
33
- <m-row>
34
- <m-col>
35
- <response-panel></response-panel>
36
- </m-col>
37
- </m-row>
38
- </m-container>
24
+ <main style='display: grid; grid-template-columns: 1fr 1fr;grid-template-rows: 1fr;max-height: 100%;overflow:hidden;'>
25
+ <payload-panel></payload-panel>
26
+ <response-panel></response-panel>
39
27
  </main>
40
- <footer>
41
- <div class="pad-sm">
42
- <svg viewBox="0 0 105 22" fill="none" xmlns="http://www.w3.org/2000/svg" height="1em" class="SanityLogo__Icon-sc-1s154od-0 ghCmDY"><title>Sanity</title><path opacity="0.7" d="M78.1793 7.99261V21.0028H73.9031V10.2138L78.1793 7.99261Z" fill="currentColor"></path><path opacity="0.7" d="M20.9511 21.33L30.944 16.1051L29.7121 12.9141L23.1332 15.9821L20.9511 21.33Z" fill="currentColor"></path><path opacity="0.5" d="M73.9031 10.2027L84.7443 4.65477L82.9126 1.5571L73.9031 5.95997V10.2027Z" fill="currentColor"></path><path opacity="0.7" d="M43.3705 6.96233V21.0028H39.2927V1.00714L43.3705 6.96233Z" fill="currentColor"></path><path opacity="0.5" d="M27.1299 6.18617L20.9511 21.33L17.7731 18.5943L25.1353 1.00714L27.1299 6.18617Z" fill="currentColor"></path><path d="M25.1353 1.00714H29.3477L37.1386 21.0028H32.8269L25.1353 1.00714Z" fill="currentColor"></path><path d="M44.0012 1.00714L52.9824 14.6682V21.0028L39.2927 1.00714H44.0012Z" fill="currentColor"></path><path d="M64.9183 1.00714H60.6739V21.0063H64.9183V1.00714Z" fill="currentColor"></path><path d="M73.9031 4.65474H67.37V1.00714H82.5867L84.7443 4.65474H78.1793H73.9031Z" fill="currentColor"></path><path opacity="0.5" d="M97.2754 13.4153V21.0028H93.0629V13.4153" fill="currentColor"></path><path d="M93.0629 13.4152L100.191 1.00714H104.666L97.2754 13.4152H93.0629Z" fill="currentColor"></path><path opacity="0.7" d="M93.063 13.4152L85.7363 1.00714H90.3456L95.3092 9.51008L93.063 13.4152Z" fill="currentColor"></path><path d="M1.96126 3.31479C1.96126 6.09921 3.71145 7.75595 7.21536 8.62956L10.9283 9.47533C14.2444 10.2236 16.2639 12.0822 16.2639 15.1103C16.2897 16.4295 15.8531 17.7173 15.0274 18.7579C15.0274 15.7368 13.4367 14.1044 9.59972 13.1229L5.95409 12.3085C3.03475 11.6541 0.781478 10.1262 0.781478 6.83709C0.766123 5.56693 1.18116 4.32781 1.96126 3.31479" fill="currentColor"></path><path opacity="0.7" d="M52.9824 13.6415V1.00714H57.0602V21.0028H52.9824V13.6415Z" fill="currentColor"></path><path opacity="0.7" d="M12.7458 14.3689C14.3294 15.3643 15.0238 16.7565 15.0238 18.7544C13.713 20.4041 11.4101 21.33 8.70333 21.33C4.14718 21.33 0.958577 19.1268 0.25 15.2982H4.62547C5.18878 17.0559 6.68034 17.8703 8.67144 17.8703C11.1019 17.8703 12.7174 16.5964 12.7493 14.3619" fill="currentColor"></path><path opacity="0.7" d="M4.23567 7.44267C3.5125 7.02045 2.9192 6.41375 2.51873 5.68697C2.11827 4.96019 1.92558 4.14045 1.96113 3.31476C3.22594 1.67891 5.42608 0.679993 8.10804 0.679993C12.7492 0.679993 15.4347 3.08852 16.0972 6.47856H11.8883C11.4242 5.14203 10.2621 4.10136 8.14347 4.10136C5.87957 4.10136 4.33487 5.39611 4.24629 7.44267" fill="currentColor"></path></svg>
43
28
 
44
- <div class="footer-text">
45
- Copyright © 2025 Sanity AS. All rights reserved.
46
- </div>
47
- </div>
48
- </footer>
49
29
 
30
+ <script src="./components/filters.js" type="module"></script>
50
31
  <script src="./components/dataset-dropdown.js" type="module"></script>
51
32
  <script src="./components/projects-dropdown.js" type="module"></script>
52
33
  <script src="./components/function-list.js" type="module"></script>
@@ -1018,9 +1018,9 @@ declare class ViewState {
1018
1018
  viewport: Viewport | undefined;
1019
1019
  lineGaps: any[];
1020
1020
  lineGapDeco: any;
1021
- updateForViewport(): 0 | 2;
1021
+ updateForViewport(): 2 | 0;
1022
1022
  viewports: (Viewport | undefined)[] | undefined;
1023
- updateScaler(): 0 | 2;
1023
+ updateScaler(): 2 | 0;
1024
1024
  updateViewportLines(): void;
1025
1025
  viewportLines: any[] | undefined;
1026
1026
  update(update: any, scrollTarget?: null): void;
@@ -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
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * "Clean up" source maps by removing absolute paths and making paths relative to the
3
+ * _input_ (eg source) rather than the _output_ (eg bundle) directory. Note that this
4
+ * process is not critical since the source content is inlined, but it helps with
5
+ * debugging to have (approximate) paths to the original source files.
6
+ *
7
+ * @param inputDir - The directory where the source files are located
8
+ * @param outputDir - The directory where the bundled files are located
9
+ */
10
+ export declare function cleanupSourceMaps(inputDir: string, outputDir: string): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { isAbsolute, join, relative } from 'node:path';
3
+ /**
4
+ * "Clean up" source maps by removing absolute paths and making paths relative to the
5
+ * _input_ (eg source) rather than the _output_ (eg bundle) directory. Note that this
6
+ * process is not critical since the source content is inlined, but it helps with
7
+ * debugging to have (approximate) paths to the original source files.
8
+ *
9
+ * @param inputDir - The directory where the source files are located
10
+ * @param outputDir - The directory where the bundled files are located
11
+ */
12
+ export async function cleanupSourceMaps(inputDir, outputDir) {
13
+ const entries = await fs.readdir(outputDir, { withFileTypes: true });
14
+ const sourceMaps = entries.filter((entry) => entry.isFile() && entry.name.endsWith('.map'));
15
+ for (const entry of sourceMaps) {
16
+ const filePath = join(outputDir, entry.name);
17
+ let raw;
18
+ try {
19
+ raw = await fs.readFile(filePath, 'utf8');
20
+ }
21
+ catch {
22
+ return;
23
+ }
24
+ let map;
25
+ try {
26
+ const json = JSON.parse(raw);
27
+ map = isRelevantSourceMap(json) ? json : undefined;
28
+ }
29
+ catch {
30
+ return;
31
+ }
32
+ if (!map) {
33
+ return;
34
+ }
35
+ map.sources = map.sources.map((source) => {
36
+ const fullPath = isAbsolute(source) ? source : join(outputDir, source);
37
+ return relative(inputDir, fullPath);
38
+ });
39
+ try {
40
+ await fs.writeFile(filePath, JSON.stringify(map));
41
+ }
42
+ catch {
43
+ // ignore write errors
44
+ }
45
+ }
46
+ }
47
+ function isRelevantSourceMap(map) {
48
+ return (typeof map === 'object' &&
49
+ map !== null &&
50
+ 'sources' in map &&
51
+ Array.isArray(map.sources) &&
52
+ map.sources.every((source) => typeof source === 'string'));
53
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Walks up the directory tree from a starting point to find a given file/directory.
3
+ *
4
+ * @param fileName - The name of the file/directory to find.
5
+ * @param startDir - The directory to start searching from (default is the current working directory).
6
+ * @returns The path to the file if found, otherwise undefined.
7
+ */
8
+ export declare function findUp(fileName: string, startDir?: string): Promise<string | undefined>;
9
+ /**
10
+ * Finds the directory containing a specific file/directory by walking up the directory tree.
11
+ *
12
+ * @param fileName - The name of the file/directory to find.
13
+ * @param startDir - The directory to start searching from (default is the current working directory).
14
+ * @returns The directory containing the file if found, otherwise undefined.
15
+ */
16
+ export declare function findDirUp(fileName: string, startDir?: string): Promise<string | undefined>;
@@ -0,0 +1,39 @@
1
+ import { access } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ /**
4
+ * Walks up the directory tree from a starting point to find a given file/directory.
5
+ *
6
+ * @param fileName - The name of the file/directory to find.
7
+ * @param startDir - The directory to start searching from (default is the current working directory).
8
+ * @returns The path to the file if found, otherwise undefined.
9
+ */
10
+ export async function findUp(fileName, startDir = process.cwd()) {
11
+ let dir = path.resolve(startDir);
12
+ while (true) {
13
+ const candidate = path.join(dir, fileName);
14
+ try {
15
+ await access(candidate);
16
+ return candidate;
17
+ }
18
+ catch {
19
+ const parent = path.dirname(dir);
20
+ if (parent === dir)
21
+ break; // Reached root
22
+ dir = parent;
23
+ }
24
+ }
25
+ return undefined;
26
+ }
27
+ /**
28
+ * Finds the directory containing a specific file/directory by walking up the directory tree.
29
+ *
30
+ * @param fileName - The name of the file/directory to find.
31
+ * @param startDir - The directory to start searching from (default is the current working directory).
32
+ * @returns The directory containing the file if found, otherwise undefined.
33
+ */
34
+ export async function findDirUp(fileName, startDir = process.cwd()) {
35
+ const filePath = await findUp(fileName, startDir);
36
+ if (!filePath)
37
+ return undefined;
38
+ return path.dirname(filePath);
39
+ }
@@ -0,0 +1,2 @@
1
+ import type { build } from 'vite';
2
+ export declare function verifyHandler(result: Awaited<ReturnType<typeof build>>): Promise<void>;
@@ -0,0 +1,13 @@
1
+ export async function verifyHandler(result) {
2
+ if ('close' in result) {
3
+ throw new Error('Incorrect build output, got watcher');
4
+ }
5
+ const outputs = (Array.isArray(result) ? result : [result]).flatMap(({ output }) => output);
6
+ const bundledIndex = outputs.find((output) => output.type === 'chunk' && output.isEntry && output.name === 'index');
7
+ if (!bundledIndex || bundledIndex.type !== 'chunk') {
8
+ throw new Error('Unexpected build output, no bundled index found');
9
+ }
10
+ if (!bundledIndex.exports.includes('handler')) {
11
+ throw new Error('Unexpected build output, no `handler` export found');
12
+ }
13
+ }