返回 slidev
010-getslidepath-guard.md
根目录 / plans / 010-getslidepath-guard.md
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
152 lines MARKDOWN