返回 slidev
012-hmr-utils-refresh.md
根目录 / plans / 012-hmr-utils-refresh.md
1 # Plan 012: Fix the no-op HMR `utils` refresh (missing `await`)
2
3 > **Executor instructions**: Follow this plan step by step. Run every
4 > verification command and confirm the expected result. If anything in "STOP
5 > conditions" occurs, stop and report. When done, update the status row in
6 > `plans/README.md`.
7 >
8 > **Drift check (run first)**: `git diff --stat c63cb120..HEAD -- packages/slidev/node/vite/loaders.ts packages/slidev/node/options.ts`
9 > On a mismatch with the excerpts below, treat it as a STOP condition.
10
11 ## Status
12
13 - **Priority**: P2
14 - **Effort**: S
15 - **Risk**: LOW
16 - **Depends on**: none
17 - **Category**: bug
18 - **Planned at**: commit `c63cb120`, 2026-07-10
19
20 ## Why this matters
21
22 On hot-update, the loader intends to refresh the derived `utils`
23 (`indexHtml`, `define`, `getLayouts`, katex/shiki options) after the deck data
24 changes. But it calls the **async** `createDataUtils` without `await`:
25 `Object.assign(utils, createDataUtils(options))`. `Object.assign` copies the
26 own-enumerable properties of a *Promise* (there are none), so the refresh does
27 nothing and the promise floats unhandled. It's a genuine no-op; a rejection
28 would surface as an unhandled promise rejection.
29
30 ## Current state
31
32 - `packages/slidev/node/vite/loaders.ts:232-233`, inside the `async handleHotUpdate(ctx)`:
33 ```ts
34 Object.assign(data, newData) // works: newData is a resolved object
35 Object.assign(utils, createDataUtils(options)) // no-op: createDataUtils is async
36 ```
37 - `packages/slidev/node/options.ts:82`:
38 ```ts
39 export async function createDataUtils(resolved: Omit<ResolvedSlidevOptions, 'utils'>): Promise<ResolvedSlidevUtils> { ... }
40 ```
41 - `data` and `utils` were destructured from `options` at `loaders.ts:27`
42 (`const { data, mode, utils, withoutNotes } = options`), so they are the *same
43 object references* as `options.data`/`options.utils`. `Object.assign(data, newData)`
44 therefore updates `options.data` in place, and a subsequent
45 `createDataUtils(options)` reads the fresh data.
46 - `handleHotUpdate` is already `async` and `await`s other work (e.g.
47 `ctx.server.reloadModule` at `:262`), so awaiting here is safe.
48
49 ## Commands you will need
50
51 | Purpose | Command | Expected |
52 |---------|---------|----------|
53 | Install | `pnpm install` | exit 0 |
54 | Build | `pnpm build` | exit 0 |
55 | Typecheck | `pnpm typecheck` | exit 0 |
56 | Lint | `pnpm lint` | exit 0 |
57
58 ## Scope
59
60 **In scope**:
61 - `packages/slidev/node/vite/loaders.ts` (the single line at `:233`)
62
63 **Out of scope**:
64 - Making the refresh *incremental*/cheaper (that's the broader HMR perf work,
65 plan 024). This plan only makes the existing intended refresh actually happen.
66 - Any other line in `handleHotUpdate`.
67
68 ## Git workflow
69
70 - Branch: `fix/hmr-utils-refresh`.
71 - Conventional commit: `fix(server): await utils refresh on hot update`.
72 - Do NOT push/PR unless instructed.
73
74 ## Steps
75
76 ### Step 1: Await the refresh
77
78 Change `loaders.ts:233` to:
79 ```ts
80 Object.assign(utils, await createDataUtils(options))
81 ```
82
83 **Verify**: `grep -n "Object.assign(utils, await createDataUtils" packages/slidev/node/vite/loaders.ts`
84 returns one match; there is no remaining `Object.assign(utils, createDataUtils(options))`
85 without `await`.
86
87 ### Step 2: Build / typecheck / lint
88
89 **Verify**: `pnpm build && pnpm typecheck && pnpm lint` exit 0.
90
91 ## Test plan
92
93 - `handleHotUpdate` requires a live Vite dev server to exercise, and the loader
94 has no unit harness today, so no automated test is added (loader testing is
95 plan 022). The change is a one-token correctness fix; verification is
96 typecheck/build plus a manual HMR sanity check if a dev deck is available
97 (`pnpm demo:dev`, edit a slide, confirm no unhandled-rejection warning and the
98 page updates).
99
100 ## Done criteria
101
102 - [ ] `loaders.ts:233` uses `await createDataUtils(options)`
103 - [ ] No unawaited `Object.assign(utils, createDataUtils(...))` remains
104 - [ ] `pnpm build`, `pnpm typecheck`, `pnpm lint` exit 0
105 - [ ] Only `loaders.ts` modified (`git status`)
106 - [ ] `plans/README.md` status row updated
107
108 ## STOP conditions
109
110 Stop and report if:
111
112 - After awaiting, HMR noticeably regresses (each save now re-runs
113 `setupShiki`/`setupKatex`/`setupIndexHtml`). If that cost is unacceptable, the
114 correct answer may be to make the refresh conditional/incremental instead —
115 report this so it can be folded into plan 024 rather than shipping a slow
116 refresh.
117
118 ## Maintenance notes
119
120 - This line re-derives all utils on every hot update. If profiling later shows it
121 is hot, gate it on the specific `data` changes that actually invalidate a util
122 (config/theme/features), coordinating with plan 024.
123 - Reviewer: confirm `options.data` is the mutated reference so the refreshed
124 utils reflect the new deck.
125
125 lines MARKDOWN