@specbook/ui 0.3.0 → 0.5.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.
- package/dist-viewer/THIRD-PARTY-NOTICES.txt +0 -66
- package/dist-viewer/assets/Host-ClcN3YPO.js +41 -0
- package/dist-viewer/assets/{index-BBlFcQUE.js → index-BcHcUWbc.js} +1 -1
- package/dist-viewer/assets/main-CAlriyIm.js +15 -0
- package/dist-viewer/assets/{mount-DfwluJy4.js → mount-D3holu10.js} +2 -2
- package/dist-viewer/assets/panel-l1xvY4kI.css +1 -0
- package/dist-viewer/assets/{worker-BmvGpsJS.js → worker-13v89DyZ.js} +13 -13
- package/dist-viewer/index.html +4 -4
- package/dist-viewer/specbook.js +1 -1
- package/package.json +4 -4
- package/src/app.tsx +12 -3
- package/src/branding.ts +2 -1
- package/src/engine.ts +17 -0
- package/src/panel/OperationDocs.tsx +1 -1
- package/src/panel/RequestPanel.tsx +45 -5
- package/src/panel/SequencePanel.tsx +8 -1
- package/src/store.ts +2 -0
- package/src/styles/app.css +14 -1
- package/src/styles/panel.css +2 -4
- package/src/viewer/embed.ts +9 -2
- package/src/viewer/mount.ts +3 -18
- package/src/viewer/registry.ts +9 -13
- package/dist-viewer/assets/Host-Cq-4gDuM.js +0 -41
- package/dist-viewer/assets/faker-B3c03STC.js +0 -11
- package/dist-viewer/assets/main-CSkB-O_8.js +0 -14
- package/dist-viewer/assets/panel-hNOojwiM.css +0 -1
- package/dist-viewer/assets/registry-DIHX11uY.js +0 -2
- package/src/viewer/faker-en.ts +0 -3
- /package/dist-viewer/assets/{panel-9eYU7Gz6.js → panel-nV09I7Wr.js} +0 -0
package/dist-viewer/index.html
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
<title>Specbook</title>
|
|
8
8
|
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
|
|
9
9
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
|
10
|
-
<script type="module" crossorigin src="./assets/index-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="./assets/panel-
|
|
12
|
-
<link rel="modulepreload" crossorigin href="./assets/mount-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="./assets/panel-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-BcHcUWbc.js"></script>
|
|
11
|
+
<link rel="modulepreload" crossorigin href="./assets/panel-nV09I7Wr.js">
|
|
12
|
+
<link rel="modulepreload" crossorigin href="./assets/mount-D3holu10.js">
|
|
13
|
+
<link rel="stylesheet" crossorigin href="./assets/panel-l1xvY4kI.css">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="./assets/index-Ds6DZpGP.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
package/dist-viewer/specbook.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,r as t,t as n}from"./assets/mount-
|
|
1
|
+
import{a as e,r as t,t as n}from"./assets/mount-D3holu10.js";function r(){let e=document.getElementById(`specbook-config`);if(e?.textContent)try{return JSON.parse(e.textContent)}catch{}let t=document.querySelector(`script[src$="specbook.js"][data-spec]`);return{spec:t?.dataset.spec,baseUrl:t?.dataset.baseUrl,defaultEnv:t?.dataset.defaultEnv}}function i(e){let t=e?document.querySelector(e):document.getElementById(`app`);if(t)return t;let n=document.createElement(`div`);return n.id=`app`,document.body.append(n),n}var a=(e,t,n=!1)=>{e.textContent=t,e.style.cssText=`font:16px system-ui,sans-serif;padding:24px;color:${n?`#b42318`:`#5b6270`}`};async function o(){if(await e({stripWhenUnclaimed:!1}))return;let o=r(),s=i(o.target);if(!o.spec)return a(s,`Specbook: no spec was given (set "spec" in #specbook-config).`,!0);let c;try{c=new URL(o.spec,location.href).toString()}catch{return a(s,`Specbook: "${o.spec}" is not a valid address.`,!0)}a(s,`Loading the API…`);let l=new t;try{let e=await l.open({kind:`url`,url:c}).promise;s.textContent=``,s.removeAttribute(`style`),await n({result:e,worker:l,source:{kind:`url`,url:c},baseUrl:o.baseUrl,defaultEnv:typeof o.defaultEnv==`string`?o.defaultEnv:void 0,shareable:!1,setTitle:!1})}catch(e){a(s,`Specbook: ${e instanceof Error?e.message:String(e)}`,!0)}}o();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specbook/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The Specbook docs app, spec viewer and embeddable specbook.js.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"specbook",
|
|
@@ -29,14 +29,14 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@preact/signals": "2.11.3",
|
|
32
|
-
"@specbook/codegen": "0.
|
|
33
|
-
"@specbook/core": "0.
|
|
32
|
+
"@specbook/codegen": "0.5.0",
|
|
33
|
+
"@specbook/core": "0.5.0",
|
|
34
34
|
"preact": "11.0.1",
|
|
35
35
|
"prism-code-editor": "^5.4.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@faker-js/faker": "9.3.0",
|
|
39
|
-
"@specbook/vite": "0.
|
|
39
|
+
"@specbook/vite": "0.5.0",
|
|
40
40
|
"@vitest/browser-playwright": "5.0.3",
|
|
41
41
|
"playwright": "1.64.0",
|
|
42
42
|
"typescript": "7.0.2",
|
package/src/app.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
environmentNote,
|
|
20
20
|
environmentRisk,
|
|
21
21
|
environments,
|
|
22
|
+
fakerInUse,
|
|
22
23
|
locale,
|
|
23
24
|
newEnvironmentOpen,
|
|
24
25
|
seed,
|
|
@@ -108,6 +109,8 @@ export function App() {
|
|
|
108
109
|
.finally(loadRest)
|
|
109
110
|
engine.environments(version.value).then((e) => {
|
|
110
111
|
environments.value = e
|
|
112
|
+
const first = globalThis.__SPECBOOK_VIEWER__?.defaultEnv
|
|
113
|
+
if (first && e.includes(first)) env.value = first
|
|
111
114
|
})
|
|
112
115
|
engine.environmentRisks(version.value).then((r) => {
|
|
113
116
|
environmentRisk.value = r
|
|
@@ -289,12 +292,17 @@ export function App() {
|
|
|
289
292
|
{registry.loadFaker && (
|
|
290
293
|
<span
|
|
291
294
|
class="row muted"
|
|
292
|
-
title=
|
|
295
|
+
title={
|
|
296
|
+
fakerInUse.value
|
|
297
|
+
? 'Generated values such as {{faker.person.fullName}}: new on every run, or fixed so the same number always gives the same data. `specbook test --seed` reproduces a run.'
|
|
298
|
+
: 'Applies when the open request or flow uses {{faker.*}}'
|
|
299
|
+
}
|
|
293
300
|
>
|
|
294
301
|
<label class="row">
|
|
295
|
-
|
|
302
|
+
Generated values
|
|
296
303
|
<select
|
|
297
|
-
aria-label="
|
|
304
|
+
aria-label="Generated values"
|
|
305
|
+
disabled={!fakerInUse.value}
|
|
298
306
|
value={seed.value === undefined ? 'random' : 'fixed'}
|
|
299
307
|
onChange={(e) => {
|
|
300
308
|
const fixed = (e.target as HTMLSelectElement).value === 'fixed'
|
|
@@ -311,6 +319,7 @@ export function App() {
|
|
|
311
319
|
class="seed-input"
|
|
312
320
|
aria-label="Seed number"
|
|
313
321
|
title="The same number gives the same data every run"
|
|
322
|
+
disabled={!fakerInUse.value}
|
|
314
323
|
value={seed.value}
|
|
315
324
|
onInput={(e) => {
|
|
316
325
|
const n = (e.target as HTMLInputElement).valueAsNumber
|
package/src/branding.ts
CHANGED
|
@@ -87,7 +87,8 @@ export function applyBranding(
|
|
|
87
87
|
}
|
|
88
88
|
style.textContent = cfg ? brandCss(cfg) : ''
|
|
89
89
|
const name = resolved.name ?? fallbackName
|
|
90
|
-
|
|
90
|
+
if (globalThis.__SPECBOOK_VIEWER__?.setTitle !== false)
|
|
91
|
+
document.title = resolved.title ? `${name} · ${resolved.title}` : name
|
|
91
92
|
if (resolved.favicon || resolved.logo) {
|
|
92
93
|
let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]')
|
|
93
94
|
if (!link) {
|
package/src/engine.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
runSequence,
|
|
27
27
|
type Sequence,
|
|
28
28
|
type SpecbookConfig,
|
|
29
|
+
setFaker,
|
|
29
30
|
type Transport,
|
|
30
31
|
usesFaker,
|
|
31
32
|
withMock,
|
|
@@ -75,6 +76,8 @@ export interface Registry {
|
|
|
75
76
|
loadConfig: (v: number) => Promise<{ default: any }>
|
|
76
77
|
/** Project's @faker-js/faker (ESM), when installed. */
|
|
77
78
|
loadFaker?: ((v: number) => Promise<Record<string, any>>) | null
|
|
79
|
+
/** Said when a run uses `{{faker.*}}` and there is no faker (the hosted viewer ships none). */
|
|
80
|
+
fakerUnavailable?: string
|
|
78
81
|
mode?: 'dev' | 'static'
|
|
79
82
|
/** variables from `.specbook/environments/<env>.env`, baked in at build time */
|
|
80
83
|
environmentVars?: Record<string, Record<string, string>>
|
|
@@ -138,6 +141,20 @@ const text = (v: unknown) =>
|
|
|
138
141
|
v !== null && typeof v === 'object' ? JSON.stringify(v) : String(v ?? '')
|
|
139
142
|
|
|
140
143
|
export function createEngine(reg: Registry, opts: EngineOptions = {}) {
|
|
144
|
+
// no faker to load: a `{{faker.*}}` ends the run with the registry's message, not "install @faker-js/faker"
|
|
145
|
+
if (!reg.loadFaker && reg.fakerUnavailable) {
|
|
146
|
+
const message = reg.fakerUnavailable
|
|
147
|
+
setFaker(
|
|
148
|
+
new Proxy(
|
|
149
|
+
{},
|
|
150
|
+
{
|
|
151
|
+
get() {
|
|
152
|
+
throw new Error(message)
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
141
158
|
const mockLoader = {
|
|
142
159
|
statuses: (service: string, opId: string) =>
|
|
143
160
|
reg.manifests[service]?.operations.find((o) => o.id === opId)?.statuses ?? [],
|
|
@@ -60,7 +60,7 @@ function SavedRequestNote({ variant, html }: { variant: Variant; html?: string }
|
|
|
60
60
|
return (
|
|
61
61
|
<section class="saved-note-doc">
|
|
62
62
|
<h2>
|
|
63
|
-
|
|
63
|
+
Request <code>{variant.name}</code>
|
|
64
64
|
</h2>
|
|
65
65
|
<div class="doc-custom" dangerouslySetInnerHTML={{ __html: html }} />
|
|
66
66
|
</section>
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
collectPlaceholders,
|
|
3
|
+
type Encoding,
|
|
4
|
+
type Example,
|
|
5
|
+
type RunResult,
|
|
6
|
+
rootName,
|
|
7
|
+
} from '@specbook/core'
|
|
2
8
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
|
3
9
|
import { EnvBadge } from '../components/EnvBadge'
|
|
4
10
|
import type { Variant } from '../engine'
|
|
@@ -7,7 +13,16 @@ import { engine, isStatic, registry, saveFile } from '../runtime'
|
|
|
7
13
|
import { type Defs, type JsonSchema, propsOf, unwrap } from '../schema'
|
|
8
14
|
import { statusClass, statusName } from '../status'
|
|
9
15
|
import { kv } from '../storage'
|
|
10
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
env,
|
|
18
|
+
envSettingsOpen,
|
|
19
|
+
envVarsVersion,
|
|
20
|
+
fakerInUse,
|
|
21
|
+
features,
|
|
22
|
+
locale,
|
|
23
|
+
seed,
|
|
24
|
+
version,
|
|
25
|
+
} from '../store'
|
|
11
26
|
import { announcement, describeRun } from './announce'
|
|
12
27
|
import { Body } from './Body'
|
|
13
28
|
import { DraftCodeSample } from './CodeSample'
|
|
@@ -235,13 +250,36 @@ export function RequestPanel({ service, op, variants, initial, autoRun }: Props)
|
|
|
235
250
|
.filter(
|
|
236
251
|
(n) => !n.startsWith('faker.') && !n.startsWith('secrets.') && !n.startsWith('session.'),
|
|
237
252
|
)
|
|
238
|
-
.map(
|
|
253
|
+
.map(rootName)
|
|
239
254
|
.filter((n, i, a) => a.indexOf(n) === i)
|
|
240
255
|
} catch {
|
|
241
256
|
return []
|
|
242
257
|
}
|
|
243
258
|
}, [draft, encoding])
|
|
244
259
|
|
|
260
|
+
// the header's Generated values control applies while this request can use faker: a `{{faker.*}}` in what is
|
|
261
|
+
// typed, or an `init`/`data` function in a saved request
|
|
262
|
+
const usesFakerNow = useMemo(() => {
|
|
263
|
+
const input = saved?.def.input
|
|
264
|
+
if (Object.values(input?.init ?? {}).some((v) => typeof v === 'function')) return true
|
|
265
|
+
if (typeof input?.data === 'function') return true
|
|
266
|
+
if (!draft) return false
|
|
267
|
+
try {
|
|
268
|
+
const parts = requestParts(draft, encoding)
|
|
269
|
+
return [
|
|
270
|
+
...collectPlaceholders([parts.path, parts.query, parts.headers, parts.data, vars]),
|
|
271
|
+
].some((n) => n.startsWith('faker.'))
|
|
272
|
+
} catch {
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
}, [saved, draft, encoding, vars])
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
fakerInUse.value = usesFakerNow
|
|
278
|
+
return () => {
|
|
279
|
+
fakerInUse.value = false
|
|
280
|
+
}
|
|
281
|
+
}, [usesFakerNow])
|
|
282
|
+
|
|
245
283
|
// placeholders nothing supplies: not in config or the environment's files. They stay listed while
|
|
246
284
|
// you type, so the field you are filling does not disappear under you.
|
|
247
285
|
// a saved request brings the values of its own `init`: they are what a run uses, so nothing is missing
|
|
@@ -435,7 +473,7 @@ export function RequestPanel({ service, op, variants, initial, autoRun }: Props)
|
|
|
435
473
|
</option>
|
|
436
474
|
))}
|
|
437
475
|
{variants.map((v) => (
|
|
438
|
-
<option value={`saved:${v.file}`}>{v.name} ·
|
|
476
|
+
<option value={`saved:${v.file}`}>{v.name} · request</option>
|
|
439
477
|
))}
|
|
440
478
|
</select>
|
|
441
479
|
</label>
|
|
@@ -520,7 +558,9 @@ export function RequestPanel({ service, op, variants, initial, autoRun }: Props)
|
|
|
520
558
|
|
|
521
559
|
{saved && (
|
|
522
560
|
<p class="muted lock">
|
|
523
|
-
A
|
|
561
|
+
A request from your project: it runs with its own values and checks. Edit its file to
|
|
562
|
+
change it.
|
|
563
|
+
{env.value === 'mock' && saved.def.mockHook ? ' Its mock hook shapes the answer.' : ''}
|
|
524
564
|
</p>
|
|
525
565
|
)}
|
|
526
566
|
|
|
@@ -4,7 +4,7 @@ import { EnvBadge } from '../components/EnvBadge'
|
|
|
4
4
|
import { defaultFor, type FieldSpec, specFromControl } from '../components/Fields'
|
|
5
5
|
import type { SequenceInfo } from '../engine'
|
|
6
6
|
import { engine } from '../runtime'
|
|
7
|
-
import { env, locale, seed, version } from '../store'
|
|
7
|
+
import { env, fakerInUse, locale, seed, version } from '../store'
|
|
8
8
|
import { announcement, describeRun } from './announce'
|
|
9
9
|
import { Failure } from './Checks'
|
|
10
10
|
import { ControlsGrid } from './ControlsGrid'
|
|
@@ -80,6 +80,13 @@ export function SequencePanel({
|
|
|
80
80
|
// a flow with inputs can run on their defaults alone, as `specbook test` does
|
|
81
81
|
const hasInputs = Object.keys(specs).length > 0 || seq.nodes.some((n) => n.kind === 'input')
|
|
82
82
|
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
fakerInUse.value = hasGenerated
|
|
85
|
+
return () => {
|
|
86
|
+
fakerInUse.value = false
|
|
87
|
+
}
|
|
88
|
+
}, [hasGenerated])
|
|
89
|
+
|
|
83
90
|
/** Every input at its default; a generated one needs faker, which is made ready first. */
|
|
84
91
|
const freshInputs = async () => {
|
|
85
92
|
if (hasGenerated)
|
package/src/store.ts
CHANGED
|
@@ -10,6 +10,8 @@ export const environmentRisk = signal<Record<string, 'production' | 'sandbox'>>(
|
|
|
10
10
|
export const environmentNote = signal<Record<string, string>>({})
|
|
11
11
|
export const locale = signal<string | undefined>(undefined)
|
|
12
12
|
export const seed = signal<number | undefined>(undefined)
|
|
13
|
+
/** Whether the request or flow that is open can use generated values; the header's control is enabled while it is. */
|
|
14
|
+
export const fakerInUse = signal(false)
|
|
13
15
|
/** Bumped when an environment's variables change, so what is derived from them (addresses, origins) is redone. */
|
|
14
16
|
export const envVarsVersion = signal(0)
|
|
15
17
|
/** Whether Environment settings is open; set from elsewhere to point a person at it. */
|
package/src/styles/app.css
CHANGED
|
@@ -684,6 +684,19 @@
|
|
|
684
684
|
text-overflow: ellipsis;
|
|
685
685
|
white-space: nowrap;
|
|
686
686
|
}
|
|
687
|
+
/* room for it: a name of ordinary length stays whole and the search field gives way instead, while a
|
|
688
|
+
longer one still ends in an ellipsis. Narrower than this only the logo is left. */
|
|
689
|
+
@media (min-width: 360px) {
|
|
690
|
+
.brand {
|
|
691
|
+
flex: 0 0 auto;
|
|
692
|
+
}
|
|
693
|
+
.brand-name {
|
|
694
|
+
max-width: 9ch;
|
|
695
|
+
}
|
|
696
|
+
.header .palette-btn {
|
|
697
|
+
min-width: 64px;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
687
700
|
.palette-btn {
|
|
688
701
|
flex: 1;
|
|
689
702
|
min-width: 0;
|
|
@@ -736,7 +749,7 @@
|
|
|
736
749
|
width: 56%;
|
|
737
750
|
min-width: 0;
|
|
738
751
|
}
|
|
739
|
-
/*
|
|
752
|
+
/* Generated values is a row inside a row (its label and, once fixed, a seed field): the inner one fills it */
|
|
740
753
|
.header-controls .row > .row {
|
|
741
754
|
flex: 1 1 100%;
|
|
742
755
|
}
|
package/src/styles/panel.css
CHANGED
|
@@ -1971,12 +1971,10 @@
|
|
|
1971
1971
|
.panel-head .spacer {
|
|
1972
1972
|
display: none;
|
|
1973
1973
|
}
|
|
1974
|
-
|
|
1974
|
+
/* the Environment badge in the panel is the one switch on a phone: a bare, unlabeled copy here only confuses */
|
|
1975
|
+
.panel-head label.row {
|
|
1975
1976
|
display: none;
|
|
1976
1977
|
}
|
|
1977
|
-
.panel-head select {
|
|
1978
|
-
max-width: 34%;
|
|
1979
|
-
}
|
|
1980
1978
|
.stepper {
|
|
1981
1979
|
gap: 0;
|
|
1982
1980
|
}
|
package/src/viewer/embed.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* <script type="module" src="https://ui.specbook.dev/v1/specbook.js"></script>
|
|
8
8
|
*
|
|
9
9
|
* The script takes the page over (it owns the document, as `/docs` does). `spec` may be relative.
|
|
10
|
-
* `baseUrl` gives the API address when the document has none, `target` names another container
|
|
10
|
+
* `baseUrl` gives the API address when the document has none, `target` names another container, and
|
|
11
|
+
* `defaultEnv` (`"live"` or the name of a server) picks the environment shown first instead of `mock`.
|
|
11
12
|
*/
|
|
12
13
|
import { handleOAuthCallback } from '../oauth-callback'
|
|
13
14
|
import { SpecWorker } from './client'
|
|
@@ -17,6 +18,7 @@ interface EmbedConfig {
|
|
|
17
18
|
spec?: string
|
|
18
19
|
baseUrl?: string
|
|
19
20
|
target?: string
|
|
21
|
+
defaultEnv?: string
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function readConfig(): EmbedConfig {
|
|
@@ -29,7 +31,11 @@ function readConfig(): EmbedConfig {
|
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
const script = document.querySelector<HTMLScriptElement>('script[src$="specbook.js"][data-spec]')
|
|
32
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
spec: script?.dataset.spec,
|
|
36
|
+
baseUrl: script?.dataset.baseUrl,
|
|
37
|
+
defaultEnv: script?.dataset.defaultEnv,
|
|
38
|
+
}
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
function container(sel?: string): HTMLElement {
|
|
@@ -72,6 +78,7 @@ async function start() {
|
|
|
72
78
|
worker,
|
|
73
79
|
source: { kind: 'url', url },
|
|
74
80
|
baseUrl: cfg.baseUrl,
|
|
81
|
+
defaultEnv: typeof cfg.defaultEnv === 'string' ? cfg.defaultEnv : undefined,
|
|
75
82
|
shareable: false,
|
|
76
83
|
setTitle: false,
|
|
77
84
|
})
|
package/src/viewer/mount.ts
CHANGED
|
@@ -12,6 +12,7 @@ export async function mountOpened(o: {
|
|
|
12
12
|
worker: SpecWorker
|
|
13
13
|
source: Source
|
|
14
14
|
baseUrl?: string
|
|
15
|
+
defaultEnv?: string
|
|
15
16
|
shareable?: boolean
|
|
16
17
|
setTitle?: boolean
|
|
17
18
|
}): Promise<void> {
|
|
@@ -19,6 +20,8 @@ export async function mountOpened(o: {
|
|
|
19
20
|
const g: ViewerGlobal = {
|
|
20
21
|
open: o.result,
|
|
21
22
|
baseUrl: o.baseUrl,
|
|
23
|
+
defaultEnv: o.defaultEnv,
|
|
24
|
+
setTitle: o.setTitle,
|
|
22
25
|
source:
|
|
23
26
|
o.shareable === false
|
|
24
27
|
? undefined
|
|
@@ -31,22 +34,4 @@ export async function mountOpened(o: {
|
|
|
31
34
|
globalThis.__SPECBOOK_VIEWER__ = g
|
|
32
35
|
if (o.setTitle !== false) document.title = `${o.result.title} · Specbook`
|
|
33
36
|
await import('../main')
|
|
34
|
-
warmFaker()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Mock responses are generated with the spec, so the first mock request needs no faker. Faker (a
|
|
39
|
-
* sizeable chunk) is only used by "Dynamic" examples and `{{faker.*}}` values, so it is fetched when the
|
|
40
|
-
* page has been idle for a moment instead of up front: by the time someone picks Dynamic it is cached.
|
|
41
|
-
*/
|
|
42
|
-
function warmFaker() {
|
|
43
|
-
if ((navigator as Navigator & { connection?: { saveData?: boolean } }).connection?.saveData)
|
|
44
|
-
return
|
|
45
|
-
const idle: (f: () => void) => void =
|
|
46
|
-
(window as unknown as { requestIdleCallback?: (f: () => void) => void }).requestIdleCallback ??
|
|
47
|
-
((f) => setTimeout(f, 1500))
|
|
48
|
-
idle(() => {
|
|
49
|
-
// the registry's loader is the same import the run would make
|
|
50
|
-
void import('./registry').then((r) => r.loadFaker()).catch(() => {})
|
|
51
|
-
})
|
|
52
37
|
}
|
package/src/viewer/registry.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface ViewerGlobal {
|
|
|
12
12
|
definitions(): Promise<Record<string, any>>
|
|
13
13
|
/** the base URL the person gave, when the document has none of its own */
|
|
14
14
|
baseUrl?: string
|
|
15
|
+
/** the environment to show first, when it is one the document offers (otherwise `mock`) */
|
|
16
|
+
defaultEnv?: string
|
|
17
|
+
/** false on a page that is not the viewer's own (an embed), where the host's title stays */
|
|
18
|
+
setTitle?: boolean
|
|
15
19
|
/** where the document came from, which decides whether a link to this view can be shared */
|
|
16
20
|
source?: import('./share').ViewerSource
|
|
17
21
|
}
|
|
@@ -124,18 +128,12 @@ export const operations = Object.fromEntries(
|
|
|
124
128
|
) as Record<string, () => Promise<Record<string, any>>>
|
|
125
129
|
|
|
126
130
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
131
|
+
* The hosted viewer and the embed ship no faker: a spec's mock and example values come from the generator, and a
|
|
132
|
+
* `{{faker.*}}` someone types is refused with this message instead of being sent as it is.
|
|
129
133
|
*/
|
|
130
|
-
export const loadFaker =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// a run asks for its locale and `base`, the fallback every locale chain ends in
|
|
134
|
-
locales: {
|
|
135
|
-
en: () => Promise.resolve(m.en),
|
|
136
|
-
base: () => Promise.resolve(m.base),
|
|
137
|
-
},
|
|
138
|
-
}))
|
|
134
|
+
export const loadFaker = null
|
|
135
|
+
export const fakerUnavailable =
|
|
136
|
+
'Generated values ({{faker.…}}) work in a Specbook project, not in the hosted viewer. Type a value instead.'
|
|
139
137
|
|
|
140
138
|
// `live` is the document's first server, or the address the person typed for it; the rest keep their own
|
|
141
139
|
// names. Each address is the variable `baseUrl` in its environment, so a copy of one can point elsewhere.
|
|
@@ -185,8 +183,6 @@ export const loadConfig = () =>
|
|
|
185
183
|
auth: open.auth,
|
|
186
184
|
...(Object.keys(vars).length ? { vars } : {}),
|
|
187
185
|
...(Object.keys(environmentSettings).length ? { environments: environmentSettings } : {}),
|
|
188
|
-
// one locale, so the header offers no locale choice
|
|
189
|
-
faker: { default: 'en', locales: { en: ['en', 'base'] } },
|
|
190
186
|
services: {
|
|
191
187
|
[service]: {
|
|
192
188
|
spec: '',
|