fix(docs): the tidying sweep in the code-example subsystem (#139b) - #398
Merged
Conversation
The remaining seven items from #139 plus everything in its follow-up comment. Two of them turned out to be real bugs rather than tidying, and one reported item turned out not to be a bug at all. The prettier worker could hang the page. `app/plugins/prettier.ts` parks a `[resolve, reject]` pair under each message's `uid` and clears it only when a reply arrives, so a message that produces no reply is not a failed format — it is a promise that never settles. The worker had no error handling anywhere: a throw in `prettier.format`, or a blocked CDN, meant `CodeExample.vue` awaited forever. Upstream `nuxt/ui`, which this file was forked from, has since fixed it, and its shape is better than the one here: one try/catch around the whole handler, and lazy loading inside the format path instead of a start-up queue guarded by a flag that was never reset on failure. Adopted. The prettier versions had drifted: the worker hard-coded `3.7.4` from the CDN while the server prerendered with the installed `3.9.6`, so the same snippet could be formatted one way before hydration and another way after. The version is now substituted at build time from the resolved package, verified against a real `docs:build` — the emitted bundle asks for `prettier@3.9.6` and no placeholder survives. (Upstream still has this drift.) The dedent was a hard-coded `.slice(2)`, correct only while every example is indented with exactly two spaces; a four-space or tab-indented example would have been mangled rather than dedented. Now measured from the region's non-blank lines. Output is byte-identical on all 11 shipped examples. `upperName` is NOT unused — it is called four times in `chat/Chat.vue`. Left alone. Deleted the ones that really are unreferenced: `cachedParseMarkdown.ts`, `extractSections.ts`, `normalizeComponentName.ts` (which is where the reported always-true condition lived, so that goes with it), `useDocs.ts`, and `HOOK_PLACEHOLDER`. Also: the `'tip'` branch in `processLinks` was dead, because callouts are rewritten to blockquotes earlier in the same pass; `processLinks` was typed with the DOM `Node` rather than the MDC tuple it actually walks; `await useCodeExample()` added a Suspense barrier for a synchronous function; `b24Config` was an empty untyped object passed to two functions whose parameter is optional, so it is gone rather than typed; `_configResolved: any` is now a structural type for the single field read (Vite is not a direct dependency here, so its own `ResolvedConfig` does not resolve); and the wildcard CORS header came off `code-examples.get.ts`, whose every caller is same-origin. `prepareHref`'s reported empty-path case is not currently reachable — every branch requires a `/docs/` segment first — but the guard is what lets the non-null assertions go away, so it is in. Closes #139 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
QA was right that the largest change shipped untested. The page↔worker message routing moved out of `app/plugins/prettier.ts` — which cannot be imported outside Vite, because of its `?worker&inline` specifier — into `app/utils/prettierWorkerApi.ts`, the same split `codeTransform.ts` got in #393. Six tests now pin what the page does with each reply shape, including the one that never used to arrive. Verified by mutation: making the error branch unreachable fails the suite. Two limits are written into that spec rather than left for a reader to assume: it cannot prove the worker sends `{ uid, error }` on every failure path (that half needs a browser), and it cannot see whether a settled entry is removed from `handlers`, since a promise cannot settle twice — deleting that line leaves the file green. `.github/contributing/documentation.md` said body lines are "dedented by exactly two spaces, so keep the wrapper at one level of indentation". That was the behaviour this PR removed, so the guide contradicted the code — and told contributors to work around a limitation that no longer exists. The `useNavigation.ts` dead-branch removal went in unexplained, which in a commit that names every other change reads as a drive-by. Documented in place: no entry in the `categories` map carries `restApiVersion`, so the test was always false while the code read as though category-level version badges were supported. The real per-page filtering is untouched. Claims about upstream `nuxt/ui` are now pinned to the commit they were checked against (`b751eae`, 2026-08-25) instead of being stated as timeless fact, and the fork's own former CDN version is named as `3.7.4` rather than being confused with upstream's `3.8.2`. Filed #399 for what the security review correctly separated from this diff: the worker executes six third-party modules from a CDN with no SRI, and the site sets no CSP. Pre-existing and inherited from the fork; the comment in the worker now points at the issue rather than staying silent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
The extraction carried a cast that hid a real hole. `handleMessage` in the
worker returns `undefined` for any `type` it does not recognise and the reply is
posted regardless, so an unknown type arrives as
`{ uid, message: undefined, error: undefined }`. The routing branched only on
`error`, then asserted `message as string` — resolving a promise with an
`undefined` that `CodeExample.vue` would call string methods on.
Unreachable today, since `format` is the only message type ever sent. But the
old code was `any`, where the mismatch was at least visible; writing `as string`
is what turns the next added message type into a silent landmine rather than a
failure with a name on it. Now checked, and rejected with the uid in the message.
Test added.
Also dropped `export type { SimplePrettier }` from the plugin — a re-export with
no importer, left behind by the extraction — and updated
`.github/contributing/testing.md`, which enumerated the `*.unit.spec.ts`
exception by naming three files and had silently stopped being the full list.
It now names `test/integration/docs/` as a group and says the list is
illustrative, since the suffix is the actual rule.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The remaining seven items from #139, plus everything in its follow-up comment. #393 took the four correctness items; this is the rest.
Two of them turned out to be real bugs rather than tidying, and one reported item turned out not to be a bug at all.
Two bug fixes (not tidying)
The prettier worker could hang the page
app/plugins/prettier.tsparks a[resolve, reject]pair under each message'suidand clears it only when a reply arrives. So a message that produces no reply is not a failed format — it is a promise that never settles, andCodeExample.vueawaits it forever. The worker had no error handling anywhere: a throw inprettier.format(an ordinary thing while a snippet is being edited) or a blocked CDN wedged the component.Worth noting where the fix came from: upstream
nuxt/ui, which this file was forked from, has already fixed this, and its shape is better than what I wrote first. I had built a message queue with manual draining on load failure; upstream has no queue at all — loading is lazy inside the format path, and one try/catch around the whole handler covers both a load failure and a format failure. Adopted rather than reinvented, and pinned to the upstream commit compared against (b751eae, 2026-08-25) so the claim stays checkable.CodeExample.vuealready falls back to unformatted code on a rejection, so the visible effect is: hangs forever → renders unformatted.The dedent assumed two-space indentation
.slice(2)per line is right only while every example is written with exactly two spaces. A four-space example would have stayed indented; a tab-indented one would have had characters eaten. It is now measured from the region's non-blank lines (blank lines skipped — an empty line has no indentation and would drag the minimum to zero).Output is byte-identical on all 11 shipped examples, checked by running the old and new implementations side by side.
Also fixed: the two prettier versions had drifted
The worker hard-coded
3.7.4from the CDN; the server prerenders with the installed^3.9.6. The same snippet could come out formatted one way before hydration and another way after.The version is now substituted at build time from the resolved package, so
pnpm upmoves both sides together. Verified on a clean rebuild (.outputand the vite cache removed first): the emitted bundle asks forprettier@3.9.6and no__PRETTIER_VERSION__placeholder survives anywhere in the output. Upstream still carries this drift —3.8.2against^3.9.6— so this part is a deliberate divergence, recorded in #400.One reported item was wrong
upperNameis not unused —chat/Chat.vuecalls it four times. Left alone.The tidying
'tip'branch inprocessLinkswas dead: callouts are rewritten to blockquotes earlier in the same pass, so it could never match.processLinkswas typed with the global DOMNode, but it walks MDC[tag, attrs, ...children]tuples. It type-checked only because every access is an index inside anArray.isArraybranch.classon category nav entries tested'restApiVersion' in category, but no entry in thecategoriesmap has that field — always false, alwaysundefined, while reading as though category-level version badges were supported. Removed, with the reasoning left in place; the real per-page filtering infilterChildrenByRestapiVersionis untouched.await useCodeExample()added a Suspense barrier for a synchronous function.b24Configwas an empty untyped{}passed to two functions whose parameter is optional — dropped rather than typed, since an empty object configures nothing and the untyped literal was the actual risk._configResolved: anyis now a structural type for the single field read. Not Vite's ownResolvedConfig: Vite is not a direct dependency here — it arrives through Nuxt — so that import does not resolve.Access-Control-Allow-Origincame offcode-examples.get.ts; every caller is same-origin (thefetchCodeExamplecomposable and the server-side MCPb24-jssdk-get-exampletool). Upstream keeps it, which is why the reason for diverging is written down next to the handler.prepareHref's reported empty-path case is not currently reachable — every branch requires a/docs/segment first — but the guard is what lets the non-null assertions go away, so it is in. Said plainly rather than claimed as a fix..github/contributing/documentation.mdtold contributors that body lines are "dedented by exactly two spaces, so keep the wrapper at one level of indentation" — the behaviour this PR removes. Updated, so the guide stops describing a limitation that no longer exists.Tests
The message routing moved out of
app/plugins/prettier.ts, which cannot be imported outside Vite because of its?worker&inlinespecifier, intoapp/utils/prettierWorkerApi.ts— the same splitcodeTransform.tsgot in #393. Six tests pin what the page does with each reply shape, including the one that never used to arrive.Five more cover the dedent: four-space, tab, minimum-taken-from-every-line, the blank-line case, and one pinning a known quirk (the trailing
.trim()eats the first line's remaining indent when that line is deeper than the minimum — no example opens on a nested line, so nothing shipped is affected).Both specs state what they do not cover rather than leaving a green run to be over-read: the worker tests cannot prove the worker sends
{ uid, error }on every failure path (that half needs a browser), and cannot see whether a settled entry is removed fromhandlers, since a promise cannot settle twice — deleting that line leaves the file green, confirmed by mutation.Checks
pnpm run typecheck— all eight passes, 0 errorsjsSdk:unit+jsSdk:types+skills:unit— 661 tests, no type errorspnpm run docs:buildfrom a cleaned.output— used to verify the version substitution reaches the bundlelint(1 pre-existing unrelated warning indocs/server/api/ai.post.ts, untouched),docs-lint --strict,lint:md,md-internal-linksFollow-ups filed
nuxt/uisomewhere more durable than three separate file comments.Closes #139
🤖 Generated with Claude Code
https://claude.ai/code/session_01F22e2ft66y7nuBJjzdThBr