Skip to content

Commit a586703

Browse files
authored
chore: make nuxt-init spinner frames deterministic (#1497)
1 parent 3d2b345 commit a586703

7 files changed

Lines changed: 56 additions & 11 deletions

File tree

capture/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Each SVG is scrubbed of ports, LAN addresses, home and temporary directories, an
4646

4747
`nuxt module search` is recorded against `capture/fixture-data/modules.json` instead of the live `api.nuxt.com`. You can refresh the fixture when the docs should show newer modules.
4848

49+
`capture/lib/fetch-stub.mjs` also applies a latency floor (`CAPTURE_FETCH_LATENCY`) where a scenario asks for one, so a request cannot outrun the spinner covering it and leave the recording without one.
50+
4951
`nuxt-dev-install-module` stubs everything the auto-install flow reaches for: the modules DB and the npm registry are answered from `capture/fixture-data/`, and `capture/fixture-data/fake-npm/` shadows `npm` on `PATH` to fake the install itself, so the recording needs no network and the fixture app is restored afterwards.
5052

5153
## Before and after comparisons

capture/captures.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ export const captures: Capture[] = [
254254
animated: true,
255255
rows: 24,
256256
scrub: DEFAULT_SCRUB,
257+
env: {
258+
NODE_OPTIONS: `--import=${new URL('lib/fetch-stub.mjs', import.meta.url).href}`,
259+
CAPTURE_FETCH_LATENCY: '250',
260+
},
257261
async drive({ session }) {
258262
await session.waitFor(/Which template/, 60_000)
259263
await session.wait(1200)

capture/lib/fetch-stub.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,32 @@
44
//
55
// CAPTURE_FETCH_STUBS is a JSON object mapping a URL prefix to the absolute
66
// path of a JSON file served as the response body.
7+
//
8+
// CAPTURE_FETCH_LATENCY is a floor, in milliseconds, on how long every request
9+
// takes. A prompt spinner is only ever drawn from its 80ms repaint timer, so a
10+
// request that resolves sooner than that leaves no trace in the recording at
11+
// all: the floor keeps the spinner on screen whatever the link is doing.
712

813
import { readFileSync } from 'node:fs'
914

1015
const stubs = Object.entries(JSON.parse(process.env.CAPTURE_FETCH_STUBS ?? '{}'))
16+
const latency = Number(process.env.CAPTURE_FETCH_LATENCY ?? 0)
1117
const realFetch = globalThis.fetch
1218

1319
globalThis.fetch = async (input, init) => {
1420
const url = typeof input === 'string' ? input : input.url ?? String(input)
21+
const settled = latency > 0 ? new Promise(resolve => setTimeout(resolve, latency)) : undefined
1522
for (const [prefix, file] of stubs) {
1623
if (url.startsWith(prefix)) {
17-
return new Response(readFileSync(file, 'utf8'), {
24+
const body = readFileSync(file, 'utf8')
25+
await settled
26+
return new Response(body, {
1827
status: 200,
1928
headers: { 'content-type': 'application/json' },
2029
})
2130
}
2231
}
23-
return realFetch(input, init)
32+
const response = await realFetch(input, init)
33+
await settled
34+
return response
2435
}

capture/lib/scrub.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ const RULES: Record<string, ScrubRule> = {
8181
steps: [
8282
{ pattern: /[]/g, replacement: () => '⠋' },
8383
{ pattern: /[]/g, replacement: () => '◐' },
84+
// A prompt spinner also animates a trailing run of up to three dots, so
85+
// how many of them a recording caught is a function of how long the work
86+
// took. The message itself is what identifies the line.
87+
{ pattern: /^(\s*\s.*?)\.{1,3}(\s*)$/g, replacement: match => match[1]! + match[2]! },
8488
],
8589
},
8690
qr: {

capture/output/nuxt-init.svg

Lines changed: 18 additions & 7 deletions
Loading

capture/output/nuxt-init.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
styles: 5e2e229d1874e510
1+
styles: e4d222f3658202ee
22
.d$b.
33
i$$A$$L .d$b
44
.$$F` `$$L.$$A$$.

capture/record.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,19 @@ async function ensureDevServer(): Promise<void> {
123123
await devServer.wait(1500)
124124
}
125125

126+
/**
127+
* A capture's `NODE_OPTIONS` adds to the ambient value rather than replacing
128+
* it: scenarios use it to preload a loader, while the environment may already
129+
* carry options the recorded CLI needs to reach the network at all (proxy
130+
* support, TLS roots) or to run at all (heap limits).
131+
*/
132+
function captureEnv(capture: Capture): Record<string, string> | undefined {
133+
if (!capture.env?.NODE_OPTIONS || !process.env.NODE_OPTIONS) {
134+
return capture.env
135+
}
136+
return { ...capture.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS} ${capture.env.NODE_OPTIONS}` }
137+
}
138+
126139
async function runCapture(capture: Capture): Promise<void> {
127140
const columns = capture.columns ?? Number(values.columns)
128141
const rows = capture.rows ?? 24
@@ -142,7 +155,7 @@ async function runCapture(capture: Capture): Promise<void> {
142155
cwd,
143156
columns,
144157
rows,
145-
env: capture.env,
158+
env: captureEnv(capture),
146159
})
147160

148161
// Whatever happens, the session must not outlive its capture: a leaked dev

0 commit comments

Comments
 (0)