| 1 | # Plan 010: Harden `getSlidePath` against an unknown slide (drop the non-null assertion) |
| 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/client/logic/slides.ts packages/client/composables/useNav.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-MED |
| 16 | - **Depends on**: none |
| 17 | - **Category**: bug |
| 18 | - **Planned at**: commit `c63cb120`, 2026-07-10 |
| 19 | |
| 20 | ## Why this matters |
| 21 | |
| 22 | `getSlidePath` casts away the possibility that a slide lookup fails: |
| 23 | `route = getSlide(route)!`. `getSlide` returns `undefined` for an unknown slide |
| 24 | number or `routeAlias`, and the next call reads `route.meta`, throwing |
| 25 | `Cannot read properties of undefined (reading 'meta')`. This is reachable via a |
| 26 | deep link / stale URL / programmatic `go(no)` to a slide that doesn't exist |
| 27 | (e.g. an alias that was renamed). Instead of crashing navigation, it should |
| 28 | resolve to a sensible fallback. |
| 29 | |
| 30 | ## Current state |
| 31 | |
| 32 | `packages/client/logic/slides.ts:10-24`: |
| 33 | ```ts |
| 34 | export function getSlide(no: number | string) { |
| 35 | return slides.value.find( |
| 36 | s => (s.no === +no || s.meta.slide?.frontmatter.routeAlias === no), |
| 37 | ) |
| 38 | } |
| 39 | |
| 40 | export function getSlidePath( |
| 41 | route: SlideRoute | number | string, |
| 42 | presenter: boolean, |
| 43 | exporting: boolean = false, |
| 44 | ) { |
| 45 | if (typeof route === 'number' || typeof route === 'string') |
| 46 | route = getSlide(route)! // ← throws if lookup fails |
| 47 | return getSlideRoutePath(route, presenter, exporting) |
| 48 | } |
| 49 | ``` |
| 50 | - `getSlideRoutePath` is imported from `./slidePath` and immediately reads |
| 51 | `route.meta` (so a `undefined` route crashes there). |
| 52 | - Callers include `packages/client/composables/useNav.ts` (`go()` at ~`:190`, |
| 53 | and `getSlidePath` at ~`:382`), and `useTocTree.ts:17`. |
| 54 | - `slides` comes from the virtual module `#slidev/slides`, so this function is |
| 55 | hard to unit test in isolation (it is currently exercised only via E2E). |
| 56 | - Note: plan 020 will add a lookup Map for `getSlide`; keep this fix compatible |
| 57 | by not changing `getSlide`'s signature. |
| 58 | |
| 59 | ## Commands you will need |
| 60 | |
| 61 | | Purpose | Command | Expected | |
| 62 | |---------|---------|----------| |
| 63 | | Install | `pnpm install` | exit 0 | |
| 64 | | Build | `pnpm build` | exit 0 | |
| 65 | | Typecheck | `pnpm typecheck` | exit 0 (no new `!`-related errors) | |
| 66 | | Lint | `pnpm lint` | exit 0 | |
| 67 | |
| 68 | ## Scope |
| 69 | |
| 70 | **In scope**: |
| 71 | - `packages/client/logic/slides.ts` |
| 72 | |
| 73 | **Out of scope**: |
| 74 | - `getSlide`'s implementation (plan 020 optimizes it). |
| 75 | - `useNav.ts` / `useTocTree.ts` call sites (this fix makes the callee safe; do |
| 76 | not change callers). |
| 77 | |
| 78 | ## Git workflow |
| 79 | |
| 80 | - Branch: `fix/get-slide-path-guard`. |
| 81 | - Conventional commit: `fix(client): guard getSlidePath against unknown slide`. |
| 82 | - Do NOT push/PR unless instructed. |
| 83 | |
| 84 | ## Steps |
| 85 | |
| 86 | ### Step 1: Replace the assertion with a guarded fallback |
| 87 | |
| 88 | Change `getSlidePath` so an unknown lookup falls back to the first slide rather |
| 89 | than throwing: |
| 90 | ```ts |
| 91 | export function getSlidePath( |
| 92 | route: SlideRoute | number | string, |
| 93 | presenter: boolean, |
| 94 | exporting: boolean = false, |
| 95 | ) { |
| 96 | if (typeof route === 'number' || typeof route === 'string') { |
| 97 | const found = getSlide(route) |
| 98 | if (!found) { |
| 99 | console.warn(`[slidev] Unknown slide "${route}", falling back to the first slide`) |
| 100 | route = slides.value[0] |
| 101 | } |
| 102 | else { |
| 103 | route = found |
| 104 | } |
| 105 | } |
| 106 | return getSlideRoutePath(route, presenter, exporting) |
| 107 | } |
| 108 | ``` |
| 109 | Rationale for first-slide fallback: it matches the router's existing behavior of |
| 110 | redirecting empty/`404` paths to `/1` (see `packages/client/setup/routes.ts:88-97`). |
| 111 | |
| 112 | **Verify**: `grep -n "getSlide(route)!" packages/client/logic/slides.ts` returns |
| 113 | no matches. |
| 114 | |
| 115 | ### Step 2: Typecheck |
| 116 | |
| 117 | **Verify**: `pnpm build && pnpm typecheck` exit 0. If `slides.value[0]` can be |
| 118 | `undefined` per the types (empty deck), guard that too (return the current path |
| 119 | or throw a clear, intentional error) — a deck always has ≥1 slide at runtime, so |
| 120 | a `slides.value[0]` access is acceptable, but keep the types honest. |
| 121 | |
| 122 | ## Test plan |
| 123 | |
| 124 | - Because `slides` is a virtual-module ref, no isolated unit test is added here |
| 125 | (consistent with the file having none today; navigation is E2E-covered by |
| 126 | `cypress/e2e/examples/basic.spec.ts`). The fix is verified by: (a) the |
| 127 | assertion is gone, (b) typecheck passes, (c) the fallback path is the same one |
| 128 | the router already uses. If plan 020 introduces a testable `getSlide`, add a |
| 129 | unit test there for the unknown-slide → fallback case. |
| 130 | |
| 131 | ## Done criteria |
| 132 | |
| 133 | - [ ] No `getSlide(route)!` (non-null assertion) remains in `slides.ts` |
| 134 | - [ ] Unknown number/alias resolves to a fallback instead of throwing |
| 135 | - [ ] `pnpm build`, `pnpm typecheck`, `pnpm lint` exit 0 |
| 136 | - [ ] Only `slides.ts` modified (`git status`) |
| 137 | - [ ] `plans/README.md` status row updated |
| 138 | |
| 139 | ## STOP conditions |
| 140 | |
| 141 | Stop and report if: |
| 142 | |
| 143 | - Callers depend on `getSlidePath` throwing for unknown slides (search usages; |
| 144 | none should, but confirm) — if one does, that behavior needs its own decision. |
| 145 | |
| 146 | ## Maintenance notes |
| 147 | |
| 148 | - If plan 020 (getSlide Map) lands first or after, ensure the fallback still uses |
| 149 | whatever `getSlide` returns; don't duplicate lookup logic. |
| 150 | - Reviewer: confirm the warning isn't spammy on legitimate flows (it should only |
| 151 | fire for genuinely unknown targets). |
| 152 |