| 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 |