@specbook/ui 0.2.0 → 0.3.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 (68) hide show
  1. package/dist-viewer/assets/Host-Cq-4gDuM.js +41 -0
  2. package/dist-viewer/assets/index-BBlFcQUE.js +1 -0
  3. package/dist-viewer/assets/main-CSkB-O_8.js +14 -0
  4. package/dist-viewer/assets/{mount-CGL2tOpN.js → mount-DfwluJy4.js} +2 -2
  5. package/dist-viewer/assets/{panel-C8Ve3LeS.js → panel-9eYU7Gz6.js} +1 -1
  6. package/dist-viewer/assets/panel-hNOojwiM.css +1 -0
  7. package/dist-viewer/assets/registry-DIHX11uY.js +2 -0
  8. package/dist-viewer/assets/worker-BmvGpsJS.js +181 -0
  9. package/dist-viewer/index.html +4 -4
  10. package/dist-viewer/specbook.js +1 -1
  11. package/package.json +6 -5
  12. package/src/app.tsx +65 -12
  13. package/src/branding.ts +12 -4
  14. package/src/components/Catalog.tsx +128 -51
  15. package/src/components/EnvBadge.tsx +21 -3
  16. package/src/components/EnvironmentMenu.tsx +237 -127
  17. package/src/components/Fields.tsx +18 -1
  18. package/src/components/NewEnvironmentDialog.tsx +118 -0
  19. package/src/components/Palette.tsx +7 -2
  20. package/src/custom-environments.ts +63 -0
  21. package/src/embed/panel.tsx +286 -44
  22. package/src/embed/styles.ts +2 -0
  23. package/src/embed/target.ts +82 -0
  24. package/src/embed/theme.ts +75 -0
  25. package/src/embed.ts +44 -15
  26. package/src/engine.ts +274 -32
  27. package/src/environment-list.ts +16 -0
  28. package/src/main.tsx +7 -2
  29. package/src/nav.ts +12 -1
  30. package/src/oauth.ts +9 -8
  31. package/src/panel/Body.tsx +27 -4
  32. package/src/panel/Checks.tsx +79 -0
  33. package/src/panel/CodeSample.tsx +16 -9
  34. package/src/panel/Combo.tsx +52 -13
  35. package/src/panel/ControlsGrid.tsx +54 -9
  36. package/src/panel/ErrorBoundary.tsx +36 -0
  37. package/src/panel/Host.tsx +14 -2
  38. package/src/panel/OAuthSignIn.tsx +6 -2
  39. package/src/panel/Panel.tsx +43 -10
  40. package/src/panel/ParamGrid.tsx +15 -8
  41. package/src/panel/RequestPanel.tsx +91 -36
  42. package/src/panel/RequestView.tsx +2 -1
  43. package/src/panel/ResponseView.tsx +25 -14
  44. package/src/panel/SecretsBar.tsx +10 -9
  45. package/src/panel/SequenceDocs.tsx +7 -6
  46. package/src/panel/SequencePanel.tsx +124 -32
  47. package/src/panel/announce.ts +17 -0
  48. package/src/panel/icons.tsx +9 -0
  49. package/src/panel/short.ts +24 -0
  50. package/src/routing.ts +0 -9
  51. package/src/runtime.ts +19 -1
  52. package/src/secrets.ts +4 -2
  53. package/src/storage.ts +58 -0
  54. package/src/store.ts +8 -0
  55. package/src/styles/app.css +69 -6
  56. package/src/styles/embed.css +116 -0
  57. package/src/styles/panel.css +233 -0
  58. package/src/viewer/manifests.ts +2 -0
  59. package/src/viewer/protocol.ts +6 -0
  60. package/src/viewer/registry.ts +46 -10
  61. package/src/viewer/worker.ts +9 -1
  62. package/src/virtual.d.ts +8 -0
  63. package/dist-viewer/assets/Host-CSrM9wY1.js +0 -37
  64. package/dist-viewer/assets/index-ez9c20v3.js +0 -1
  65. package/dist-viewer/assets/main-38QGGpNT.js +0 -14
  66. package/dist-viewer/assets/panel-C-vBZrep.css +0 -1
  67. package/dist-viewer/assets/registry-ChKCFBt1.js +0 -2
  68. package/dist-viewer/assets/worker-ClGb0tE1.js +0 -181
@@ -0,0 +1,116 @@
1
+ /*
2
+ * How the panel sits on someone else's page. Unlayered on purpose: it must beat the app's layered rules (the full-screen
3
+ * phone sheet, the sheet from the right) which this presentation replaces.
4
+ */
5
+
6
+ /* the browser's top layer when it has one (so no host z-index can cover the panel), else a very high fixed box */
7
+ .sb-top {
8
+ position: fixed;
9
+ inset: 0;
10
+ width: 100%;
11
+ height: 100%;
12
+ max-width: none;
13
+ max-height: none;
14
+ margin: 0;
15
+ padding: 0;
16
+ border: 0;
17
+ overflow: visible;
18
+ background: transparent;
19
+ color: inherit;
20
+ }
21
+ .sb-top.sb-fallback {
22
+ z-index: 2147483000;
23
+ }
24
+ .sb-top[hidden] {
25
+ display: none;
26
+ }
27
+
28
+ .panel-backdrop {
29
+ backdrop-filter: blur(2px);
30
+ }
31
+
32
+ /* desktop: a centered dialog */
33
+ .panel.popover {
34
+ position: fixed;
35
+ inset: 0;
36
+ margin: auto;
37
+ width: min(880px, calc(100vw - 48px));
38
+ min-width: 0;
39
+ height: min(86vh, 780px);
40
+ overflow: hidden;
41
+ border: 1px solid var(--sb-border);
42
+ border-radius: 14px;
43
+ box-shadow:
44
+ 0 24px 64px color-mix(in srgb, black 35%, transparent),
45
+ 0 2px 8px color-mix(in srgb, black 20%, transparent);
46
+ animation: sb-popover-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
47
+ }
48
+ @keyframes sb-popover-in {
49
+ from {
50
+ opacity: 0;
51
+ transform: translateY(8px) scale(0.985);
52
+ }
53
+ }
54
+ @keyframes sb-sheet-in {
55
+ from {
56
+ opacity: 0;
57
+ transform: translateY(24px);
58
+ }
59
+ }
60
+ @media (prefers-reduced-motion: reduce) {
61
+ .panel.popover {
62
+ animation: none;
63
+ }
64
+ }
65
+
66
+ /* phone: a sheet from the bottom, with a handle, clear of the notch and the home bar */
67
+ @media (max-width: 820px) {
68
+ .panel.popover {
69
+ inset: auto 0 0 0;
70
+ margin: 0;
71
+ width: 100%;
72
+ height: min(92dvh, calc(100dvh - env(safe-area-inset-top, 0px) - 12px));
73
+ border-width: 1px 0 0;
74
+ border-radius: 16px 16px 0 0;
75
+ padding-bottom: env(safe-area-inset-bottom, 0px);
76
+ animation-name: sb-sheet-in;
77
+ }
78
+ .panel.popover::before {
79
+ content: "";
80
+ flex: none;
81
+ width: 36px;
82
+ height: 4px;
83
+ margin: 8px auto 0;
84
+ border-radius: 2px;
85
+ background: var(--sb-border);
86
+ }
87
+ }
88
+
89
+ /* a finger, not a pointer: the controls of the header are comfortable targets */
90
+ @media (pointer: coarse) {
91
+ .panel.popover .panel-head .icon {
92
+ min-width: 44px;
93
+ min-height: 44px;
94
+ }
95
+ }
96
+
97
+ .embed-problem {
98
+ margin: var(--sb-space-4);
99
+ color: var(--sb-text);
100
+ }
101
+
102
+ /* what the page's attributes asked for that could not be done, over the foot of the panel */
103
+ .embed-banner {
104
+ position: fixed;
105
+ left: 50%;
106
+ bottom: 12px;
107
+ transform: translateX(-50%);
108
+ z-index: 22;
109
+ max-width: min(640px, calc(100vw - 24px));
110
+ padding: 6px 12px;
111
+ border: 1px solid var(--sb-border);
112
+ border-radius: 8px;
113
+ background: var(--sb-surface-raised, var(--sb-surface));
114
+ color: var(--sb-text);
115
+ font-size: 13px;
116
+ }
@@ -377,6 +377,46 @@
377
377
  gap: var(--sb-space-2);
378
378
  padding: 8px;
379
379
  }
380
+ .panel-error {
381
+ margin: var(--sb-space-4);
382
+ padding: var(--sb-space-4);
383
+ border: 1px solid var(--sb-border);
384
+ border-left: 3px solid var(--sb-danger, #dc2626);
385
+ border-radius: var(--sb-radius-card);
386
+ background: var(--sb-surface);
387
+ }
388
+ .panel-error pre {
389
+ margin: var(--sb-space-2) 0 var(--sb-space-3);
390
+ white-space: pre-wrap;
391
+ overflow-wrap: anywhere;
392
+ font: 12px var(--sb-font-mono);
393
+ color: var(--sb-text-muted);
394
+ }
395
+ button.flows-title {
396
+ width: 100%;
397
+ border: 0;
398
+ background: none;
399
+ color: inherit;
400
+ font: inherit;
401
+ text-align: left;
402
+ cursor: pointer;
403
+ }
404
+ @media (pointer: coarse) {
405
+ button.flows-title {
406
+ min-height: 44px;
407
+ }
408
+ }
409
+ button.flows-title:hover {
410
+ background: var(--sb-surface-raised);
411
+ }
412
+ button.flows-title:focus-visible {
413
+ outline: 2px solid var(--sb-focus-ring);
414
+ outline-offset: -2px;
415
+ }
416
+ .flows-title .caret {
417
+ width: 1em;
418
+ color: var(--sb-text-muted);
419
+ }
380
420
  .flows-title .count {
381
421
  margin-left: auto;
382
422
  font-size: 12px;
@@ -390,6 +430,20 @@
390
430
  .flows .op-row:last-child {
391
431
  border-bottom: 0;
392
432
  }
433
+ /* saved requests: the same block as flows, in the neutral border colour, so flows stay the one accent */
434
+ .flows.requests {
435
+ border-color: var(--sb-border);
436
+ border-left-color: var(--sb-text-muted);
437
+ background: var(--sb-surface-raised);
438
+ }
439
+ .method.req {
440
+ color: var(--sb-text-muted);
441
+ }
442
+ .req-tag {
443
+ margin-left: 6px;
444
+ font-size: 11px;
445
+ font-weight: 500;
446
+ }
393
447
  .flow-play {
394
448
  font-size: 10px;
395
449
  text-align: center;
@@ -970,6 +1024,99 @@
970
1024
  border: 0;
971
1025
  }
972
1026
 
1027
+ /* ---- a failed check: what was expected, what came back, as a colored diff */
1028
+ .visually-hidden {
1029
+ position: absolute;
1030
+ width: 1px;
1031
+ height: 1px;
1032
+ overflow: hidden;
1033
+ clip-path: inset(50%);
1034
+ white-space: nowrap;
1035
+ }
1036
+ .checks .bad {
1037
+ margin-top: var(--sb-space-2);
1038
+ color: inherit;
1039
+ }
1040
+ .check-fail {
1041
+ display: grid;
1042
+ gap: var(--sb-space-2);
1043
+ margin: 0;
1044
+ min-width: 0;
1045
+ padding: var(--sb-space-3);
1046
+ border: 1px solid color-mix(in srgb, var(--sb-danger) 35%, var(--sb-border));
1047
+ border-left: 3px solid var(--sb-danger);
1048
+ border-radius: var(--sb-radius-control);
1049
+ background: color-mix(in srgb, var(--sb-danger) 5%, var(--sb-surface));
1050
+ }
1051
+ .check-head {
1052
+ display: flex;
1053
+ flex-wrap: wrap;
1054
+ align-items: center;
1055
+ gap: var(--sb-space-2);
1056
+ color: var(--sb-danger);
1057
+ }
1058
+ .check-head strong {
1059
+ min-width: 0;
1060
+ overflow-wrap: anywhere;
1061
+ }
1062
+ .diff-legend {
1063
+ display: flex;
1064
+ gap: var(--sb-space-3);
1065
+ font-size: 12px;
1066
+ font-weight: 600;
1067
+ }
1068
+ .diff-key.diff-expected,
1069
+ .diff-line.diff-expected {
1070
+ color: var(--sb-success);
1071
+ }
1072
+ .diff-key.diff-received,
1073
+ .diff-line.diff-received {
1074
+ color: var(--sb-danger);
1075
+ }
1076
+ .diff {
1077
+ margin: 0;
1078
+ padding: var(--sb-space-2) 0;
1079
+ white-space: pre;
1080
+ max-height: 18em;
1081
+ overflow: auto;
1082
+ border-radius: var(--sb-radius-control);
1083
+ background: var(--sb-surface);
1084
+ border: 1px solid var(--sb-border);
1085
+ font: 12.5px / 1.5 var(--sb-font-mono);
1086
+ }
1087
+ .diff-line {
1088
+ display: flex;
1089
+ min-width: max-content;
1090
+ padding: 0 var(--sb-space-3) 0 0;
1091
+ }
1092
+ .diff-line.diff-expected {
1093
+ background: var(--sb-success-subtle);
1094
+ }
1095
+ .diff-line.diff-received {
1096
+ background: var(--sb-danger-subtle);
1097
+ }
1098
+ .diff-line.diff-same {
1099
+ color: var(--sb-text-muted);
1100
+ }
1101
+ .diff-line.diff-skip {
1102
+ color: var(--sb-text-muted);
1103
+ font-style: italic;
1104
+ }
1105
+ .diff-gutter {
1106
+ flex: none;
1107
+ width: 2em;
1108
+ text-align: center;
1109
+ user-select: none;
1110
+ }
1111
+ .diff-text {
1112
+ white-space: pre;
1113
+ }
1114
+ .check-where {
1115
+ font: 12px var(--sb-font-mono);
1116
+ color: var(--sb-text-muted);
1117
+ overflow-wrap: anywhere;
1118
+ }
1119
+
973
1120
  /* ---- shared small controls */
974
1121
  .link {
975
1122
  background: none;
@@ -1462,6 +1609,66 @@
1462
1609
  width: 60%;
1463
1610
  }
1464
1611
  }
1612
+ .controls-head {
1613
+ display: flex;
1614
+ align-items: center;
1615
+ justify-content: space-between;
1616
+ gap: var(--sb-space-2);
1617
+ }
1618
+ .controls-head .gen-all {
1619
+ display: inline-flex;
1620
+ align-items: center;
1621
+ gap: 6px;
1622
+ text-transform: none;
1623
+ letter-spacing: 0;
1624
+ font-size: 12px;
1625
+ padding: 4px 10px;
1626
+ }
1627
+ /* a value with a generator: the cell and the button that makes another, never wider than the column */
1628
+ .cell-gen {
1629
+ display: flex;
1630
+ align-items: center;
1631
+ gap: 4px;
1632
+ min-width: 0;
1633
+ }
1634
+ .cell-gen > :first-child {
1635
+ flex: 1 1 auto;
1636
+ min-width: 0;
1637
+ }
1638
+ .cell-gen .cell {
1639
+ width: 100%;
1640
+ min-width: 0;
1641
+ }
1642
+ .gen {
1643
+ flex: none;
1644
+ display: inline-flex;
1645
+ align-items: center;
1646
+ justify-content: center;
1647
+ width: 28px;
1648
+ height: 28px;
1649
+ margin-right: 4px;
1650
+ padding: 0;
1651
+ color: var(--sb-text-muted);
1652
+ background: transparent;
1653
+ border: 1px solid var(--sb-border);
1654
+ border-radius: var(--sb-radius-control);
1655
+ cursor: pointer;
1656
+ }
1657
+ .gen:hover,
1658
+ .gen:focus-visible {
1659
+ color: var(--sb-accent);
1660
+ border-color: var(--sb-accent);
1661
+ }
1662
+ /* a finger, not a pointer: the button is a comfortable target */
1663
+ @media (pointer: coarse) {
1664
+ .gen {
1665
+ width: 44px;
1666
+ height: 44px;
1667
+ }
1668
+ .controls-head .gen-all {
1669
+ min-height: 44px;
1670
+ }
1671
+ }
1465
1672
  .multi-cell {
1466
1673
  display: flex;
1467
1674
  flex-wrap: wrap;
@@ -1578,6 +1785,8 @@
1578
1785
  max-height: min(70vh, 640px);
1579
1786
  overflow: auto;
1580
1787
  display: grid;
1788
+ /* one column that cannot outgrow the popover: Safari sizes an auto track to its widest field */
1789
+ grid-template-columns: minmax(0, 1fr);
1581
1790
  gap: var(--sb-space-3);
1582
1791
  padding: var(--sb-space-3) var(--sb-space-4);
1583
1792
  background: var(--sb-surface);
@@ -1651,6 +1860,23 @@
1651
1860
  background: var(--sb-danger-subtle);
1652
1861
  color: var(--sb-danger);
1653
1862
  }
1863
+ /* a variable's value, where it comes from */
1864
+ .envrow .src {
1865
+ flex: none;
1866
+ padding: 0 8px;
1867
+ border-radius: 999px;
1868
+ font-size: 11px;
1869
+ font-weight: 600;
1870
+ }
1871
+ .src.inherited {
1872
+ background: var(--sb-surface-raised);
1873
+ color: var(--sb-text-muted);
1874
+ }
1875
+ .envuse {
1876
+ margin: -2px 0 var(--sb-space-2);
1877
+ padding-left: 2px;
1878
+ overflow-wrap: anywhere;
1879
+ }
1654
1880
  .op-lock {
1655
1881
  display: inline-block;
1656
1882
  margin-left: 6px;
@@ -1805,6 +2031,13 @@
1805
2031
  .envrow {
1806
2032
  flex-wrap: wrap;
1807
2033
  }
2034
+ /* a variable: its name above, then the value (an address needs the room) beside its source */
2035
+ .envrow code {
2036
+ flex: 1 0 100%;
2037
+ }
2038
+ .envrow input {
2039
+ flex: 1 1 0;
2040
+ }
1808
2041
  .envrow input,
1809
2042
  .envrow select {
1810
2043
  min-width: 0;
@@ -0,0 +1,2 @@
1
+ // The viewer's registry has its one manifest from the start; there is nothing to fill in.
2
+ export {}
@@ -25,6 +25,12 @@ export interface OpenResult {
25
25
  auth: Record<string, AuthProfileLike>
26
26
  secrets: Record<string, Record<string, never>>
27
27
  defaultProfile?: string
28
+ /**
29
+ * Sign-in addresses the document gives relative to its servers, as variables: the values for `live`,
30
+ * and per other environment. The profiles in `auth` read them as `{{name}}`.
31
+ */
32
+ authVars: Record<string, string>
33
+ authEnvironmentVars: Record<string, Record<string, string>>
28
34
  /** absolute API address, from `servers` (resolved against the spec URL) or `host` */
29
35
  baseUrl?: string
30
36
  /** every usable server the document lists, as environments; the first is `live` */
@@ -3,7 +3,7 @@
3
3
  * the document in a worker and puts the result on `globalThis.__SPECBOOK_VIEWER__` before the app is
4
4
  * imported; this module turns that into the same named exports the dev server and the static build give.
5
5
  */
6
- import { defineOperation, type OpMeta } from '@specbook/core'
6
+ import { defineOperation, environmentsFromServers, type OpMeta } from '@specbook/core'
7
7
  import type { OpenResult } from './protocol'
8
8
 
9
9
  export interface ViewerGlobal {
@@ -32,7 +32,10 @@ const ids = Object.keys(open.metas)
32
32
  export const mode = 'static' as const
33
33
  export const project = { root: '', outDir: '', requestsDir: 'requests' }
34
34
  export const manifests = { [service]: open.manifest }
35
+ export const loadManifests = async (_services?: string[]) => {}
35
36
  export const requests: never[] = []
37
+ export const requestMeta = {}
38
+ export const unresolvedRequests: string[] = []
36
39
  export const sequences: never[] = []
37
40
  export const sequenceMeta = {}
38
41
  export const unresolvedSequences: string[] = []
@@ -134,28 +137,61 @@ export const loadFaker = () =>
134
137
  },
135
138
  }))
136
139
 
137
- // `live` is the document's first server, or the address the person typed for it; the rest keep their own names
140
+ // `live` is the document's first server, or the address the person typed for it; the rest keep their own
141
+ // names. Each address is the variable `baseUrl` in its environment, so a copy of one can point elsewhere.
138
142
  const live = v.baseUrl ?? open.baseUrl
139
- const baseUrl: Record<string, string> = {
140
- ...(live ? { live } : {}),
141
- ...Object.fromEntries(open.servers.filter((s) => s.name !== 'live').map((s) => [s.name, s.url])),
142
- }
143
- const described = Object.fromEntries(
144
- open.servers.filter((s) => s.description).map((s) => [s.name, { description: s.description }]),
143
+ const liveServer = open.servers.find((s) => s.name === 'live')
144
+ const addresses = environmentsFromServers(
145
+ [
146
+ ...(live ? [{ name: 'live', url: live, description: liveServer?.description }] : []),
147
+ ...open.servers.filter((s) => s.name !== 'live'),
148
+ ],
149
+ 'baseUrl',
145
150
  )
151
+ // a sign-in address the document gave relative to its server needs the address the person typed
152
+ const absolute = (value: string) => {
153
+ if (/^https?:\/\//i.test(value) || !v.baseUrl) return value
154
+ try {
155
+ return new URL(value, `${v.baseUrl.replace(/\/+$/, '')}/`).toString()
156
+ } catch {
157
+ return value
158
+ }
159
+ }
160
+ const vars = {
161
+ ...addresses.vars,
162
+ ...Object.fromEntries(Object.entries(open.authVars).map(([k, x]) => [k, absolute(x)])),
163
+ }
164
+ const environmentSettings: Record<string, { description?: string; vars?: Record<string, string> }> =
165
+ {
166
+ ...addresses.environments,
167
+ }
168
+ for (const [name, evars] of Object.entries(open.authEnvironmentVars))
169
+ environmentSettings[name] = {
170
+ ...environmentSettings[name],
171
+ vars: { ...environmentSettings[name]?.vars, ...evars },
172
+ }
173
+ /** Where environments made in this browser are kept: per spec address, or for the tab for an upload. */
174
+ export const environmentStore: { scope: string; session: boolean } | null = v.source
175
+ ? {
176
+ scope: v.source.kind === 'url' ? v.source.url : `upload:${service}`,
177
+ session: v.source.kind !== 'url',
178
+ }
179
+ : null
146
180
  export const loadConfig = () =>
147
181
  Promise.resolve({
148
182
  default: {
149
183
  project: service,
150
184
  secrets: open.secrets,
151
185
  auth: open.auth,
152
- ...(Object.keys(described).length ? { environments: described } : {}),
186
+ ...(Object.keys(vars).length ? { vars } : {}),
187
+ ...(Object.keys(environmentSettings).length ? { environments: environmentSettings } : {}),
153
188
  // one locale, so the header offers no locale choice
154
189
  faker: { default: 'en', locales: { en: ['en', 'base'] } },
155
190
  services: {
156
191
  [service]: {
157
192
  spec: '',
158
- baseUrl,
193
+ // a document that gives no address at all has no live environment either
194
+ baseUrl: live || open.servers.length ? addresses.baseUrl : {},
159
195
  ...(open.defaultProfile ? { auth: open.defaultProfile } : {}),
160
196
  },
161
197
  },
@@ -113,8 +113,14 @@ async function open(id: number, source: Source, signal: AbortSignal): Promise<Op
113
113
  ]),
114
114
  )
115
115
  const sourceUrl = source.kind === 'url' ? source.url : undefined
116
- const { auth, secrets, defaultProfile } = authFromSpec(doc, sourceUrl)
117
116
  const { servers, needsBaseUrl, omitted } = openApiServers(doc, sourceUrl ?? name ?? '')
117
+ const {
118
+ auth,
119
+ secrets,
120
+ defaultProfile,
121
+ vars: authVars,
122
+ environmentVars: authEnvironmentVars,
123
+ } = authFromSpec(doc, sourceUrl, servers)
118
124
  const baseUrl = servers.find((s) => s.name === 'live')?.url
119
125
  return {
120
126
  service,
@@ -125,6 +131,8 @@ async function open(id: number, source: Source, signal: AbortSignal): Promise<Op
125
131
  auth,
126
132
  secrets,
127
133
  defaultProfile,
134
+ authVars,
135
+ authEnvironmentVars,
128
136
  baseUrl,
129
137
  servers,
130
138
  needsBaseUrl,
package/src/virtual.d.ts CHANGED
@@ -6,6 +6,8 @@ declare module 'virtual:specbook/registry' {
6
6
  source?: () => Promise<string>
7
7
  }
8
8
  export const mode: 'dev' | 'static'
9
+ /** A build that publishes only the operations the project's requests and flows use. */
10
+ export const minimal: boolean | undefined
9
11
  export const project: { root: string; outDir: string; requestsDir: string }
10
12
  export const manifests: Record<
11
13
  string,
@@ -28,7 +30,13 @@ declare module 'virtual:specbook/registry' {
28
30
  }[]
29
31
  }
30
32
  >
33
+ /** Fetches the manifests of the named services (all when none are named) into `manifests`. A build starts with it empty. */
34
+ export const loadManifests: (services?: string[]) => Promise<void>
31
35
  export const requests: Entry[]
36
+ /** Which `service/Operation` each request file is for, read from its text (no import), by project-relative file. */
37
+ export const requestMeta: Record<string, string>
38
+ /** Request files whose text does not say; these are imported to be listed. */
39
+ export const unresolvedRequests: string[]
32
40
  export const sequences: Entry[]
33
41
  /** Sequence headers read from the `*.seq.ts` text (no import), keyed by id. */
34
42
  export const sequenceMeta: Record<