Skip to content

fix(embed): the Starlight, Astro and VitePress plugins need no React installed - #167

Merged
dgesteves merged 5 commits into
mainfrom
fix/od2-50-standalone-dialog
Oct 16, 2026
Merged

dgesteves merged 5 commits into
mainfrom
fix/od2-50-standalone-dialog

Conversation

@dgesteves

@dgesteves dgesteves commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Roadmap item: OD2-50, a dialog without React for non-React hosts.

What was wrong

The Starlight, Astro and VitePress plugins mounted the launcher from ondocs/embed. Its dialog chunk imports react, react-dom, @radix-ui/react-dialog and cmdk, so a Vue or Astro site had to install all four or its build failed. The guides said so: README, astro.md and vitepress.md all had npm i ondocs ai @ai-sdk/openai react react-dom @radix-ui/react-dialog cmdk. The site's bundler then packed React into the dialog chunk anyway.

What changed

  • New build, ondocs/embed/standalone (tsdown.config.ts, package.json exports). It is src/embed/index.ts built a second time, with production React, Radix and cmdk bundled into the dialog's chunk. The types are ondocs/embed's. The entry (dist/embed/standalone.js, 1,773 B gzip -9) imports only its dialog chunk, and the chunk (95,486 B gzip -9) imports nothing. The site's bundler still splits the dialog off and loads it on first use.
  • launcherScript now mounts from ondocs/embed/standalone, and so do the Astro/Starlight integration and the VitePress plugin. optimizeDeps.include follows the new name. They no longer call warnMissingDialogPeers, which would now warn about packages nobody needs. Docusaurus is unchanged and still uses the site's React.
  • Docs. The install line for Starlight, Astro and VitePress is now npm i ondocs ai @ai-sdk/openai, in the README, astro.md, vitepress.md and the setup skill (test/setup-skill.test.ts keeps the skill equal to the guides). Each guide notes that 0.11.0 and earlier still need the four packages. The guides give the measured size on open and say the dialog's stylesheet (3.5 KB gz) is added to the site's CSS on every page; that covers the od-eager-dialog-css finding with wording. script-tag.md's "With a bundler" section documents both modules.
  • scripts/js-weight.mjs: each site now has an on-open budget, open, which --check enforces. VitePress was already measured. It also waits for the page's idle callbacks before it stops counting what the page loads before interaction. Starlight imports its search UI (about 25 KB gzipped) in a requestIdleCallback. On one CI run of this branch that callback ran late, so the script charged it to the dialog: 10.3 KB before interaction and +118.4 KB on open, over the 97 KB budget. With the page's idle callbacks delayed by 1.5 s, the old script measured 10.4 KB and the new one 36.2 KB. The unmodified builds measure the same as before.
  • scripts/consumer-site.mjs: the fresh Starlight, starlight-ondocs and VitePress sites install only ai and @ai-sdk/openai, and fail if react, react-dom, Radix or cmdk end up installed.
  • ondocs init (sitePackages) no longer lists react react-dom @radix-ui/react-dialog cmdk for Starlight, Astro and VitePress. It prints npm i ondocs ai @ai-sdk/openai. Four init snapshots changed.
  • starlight-ondocs no longer asks for react, react-dom, @radix-ui/react-dialog and cmdk as peers. npm installs required peers, so before this every starlight-ondocs site got React. Its README install line is npm i starlight-ondocs ondocs ai @ai-sdk/openai. pnpm-lock.yaml loses those four entries from the packages/starlight-ondocs importer (12 lines removed, nothing else changed).
  • Landing page: the Install step for Astro/Starlight and for VitePress is npm i ondocs ai @ai-sdk/openai.
  • openAskDialog on a site without React. ondocs/embed renders the dialog with the site's React, so a page on such a site that imports openAskDialog from it fails the build. ask-dialog.md now says that a site without React (Astro, Starlight, VitePress) imports it from ondocs/embed/standalone, or uses data-ondocs-open, which needs no import. api.md has a row for ondocs/embed/standalone, and its ondocs/embed row says it needs the site's React, @radix-ui/react-dialog and cmdk.
  • writing-an-integration.md: launcherScript mounts mountAskDialog from ondocs/embed/standalone, with React, Radix and cmdk built into the dialog's chunk, so the site installs none of them. quietUseClient() is described as being for a site that bundles ondocs/react itself, and warnMissingDialogPeers as being for a dialog that renders with the site's React.
  • scripts/deploy-recipes.mjs: the astro-* recipes no longer install React, Radix or cmdk, and fail if any of them is installed. CI now builds a plain Astro site (not Starlight) without React.
  • The JSDoc of OndocsDialogOptions in the Astro, Starlight and VitePress entries names ondocs/embed/standalone. benchmarks.md's Starlight on-open figure is +92.6 KB, as measured.
  • A minor changeset for ondocs and starlight-ondocs, as the release adds a module (ondocs/embed/standalone) and changes what launcherScript emits. CONTRIBUTING says minor for a feature.

src/embed/dialog-module.ts from the planned file list was not needed, because the new build reuses src/embed/index.ts as its entry. Four files outside the planned list changed because the gate requires it. test/vitepress.test.ts and test/integration-entry.test.ts each had a one-line assertion of the old ondocs/embed specifier. test/fixtures/vitepress-site/.vitepress/config.mts maps ondocs's modules to their source, because the tests run before the build; it now maps the new specifier too. skills/ondocs-setup/SKILL.md must match the guides' install line. The example doc copies were regenerated with pnpm examples:sync.

Size, before and after

Measured with node scripts/js-weight.mjs starlight vitepress on an Apple M1 Max (gzip -9, KB of 1,024 bytes), on main (02fba58) and on this branch, after building each example:

Site Before interaction, main → branch On open, main → branch
Starlight 36.1 → 36.1 KB +93.6 → +92.6 KB
VitePress 61.7 → 61.6 KB +96.8 → +95.6 KB

On this branch, Docusaurus measures 177.7 KB / +27.2 KB, Next.js 180 KB / +27.3 KB and the script tag 3.2 KB / +97.8 KB. None of those changed. The new on-open budgets are 31, 97, 100, 31 and 102 KB.

preact/compat: tried, not shipped

The finding proposed building this bundle against preact/compat. I built src/embed/dialog-global.ts with React aliased to preact@10.29.8/compat (already in the lockfile, through VitePress's docsearch). It came out at 41,027 B gzip -9, against 100,044 B for today's embed-dialog.global.js. I then drove both builds through the same scripted run in headless Chrome. Most of it matched: open and focus, Tab trapped, scroll lock, aria-hidden on the page, answer and sources, Escape with focus return, overlay click. The dialog's DOM was the same apart from attribute order. Two things differed:

  • After an answer, typing a follow-up shows the "Follow up" item, but cmdk does not select it under preact. Enter then only selects the item and does not ask. React asks on the first Enter.
  • spellCheck={false} on the input is dropped, because preact removes attributes set to false.

Fixing the first needs a change in dialog-panel.tsx. Running the dialog test suite against preact/compat needs a vitest alias config and preact as a devDependency, which means a lockfile change while OD2-46 is open. Both are outside this item's files, so this PR ships the React build. The install friction goes away now, and the weight cut is a follow-up.

How it was tested

  • New test/standalone-dialog.test.ts (5 tests). It fails on main for the finding's reasons: the install lines name React, the launcher imports ondocs/embed, and nothing builds embed/standalone. The test builds the entry with tsdown from tsdown.config.ts and checks that the entry imports only its chunk and the chunk imports nothing. It checks that the entry is under 2.5 KB gz and that React sits in the chunk. In jsdom it opens the dialog, asks, gets the citation and closes on Escape.
  • pnpm install --frozen-lockfile --prefer-offline (lockfile unchanged), then pnpm validate: lint, format:check, typecheck, test (1270 passed, 10 skipped), build, check:package (publint and attw cover ondocs/embed/standalone), examples:check, template:check and the four example builds. Also pnpm check:doc-imports, node scripts/embed-size.mjs, and node scripts/js-weight.mjs docusaurus starlight vitepress embed --check. Next.js was checked with the same script on a local port of my choice.
  • Browser check, headless Chrome, on the Starlight and VitePress example builds at 390 and 1440 px. The ondocs/embed/standalone chunk loads only when the dialog opens. A stubbed answer (intercepted in the browser, nothing sent to the live endpoint) shows its citation. There is no sideways scroll (scrollWidth 390 and 1440). Focus returns to the navbar button after Escape, the shortcut reopens the dialog, and the console shows no errors.
  • Dev servers: vitepress dev and astro dev prebundle ondocs_embed_standalone.js and its chunk, and the dialog answers. astro dev reloads once on a cold cache, as it does on main (Astro's dev toolbar deps).
  • The consumers CI job builds fresh Starlight and VitePress sites from the packed tarball without React.
  • After the review fixes, on this branch merged with main: pnpm install --frozen-lockfile --prefer-offline, then pnpm validate passed: lint, format:check, typecheck, test (1283 passed, 10 skipped), build, check:package, examples:check, template:check and the four example builds. node scripts/check-doc-imports.mjs resolves the new openAskDialog from ondocs/embed/standalone.
  • test/standalone-dialog.test.ts now also checks starlight-ondocs's README and the landing page's Astro and VitePress install steps. Both checks fail with the old lines. test/init.test.ts checks the Astro-with-adapter install line, and test/plugin-packages.test.ts checks each plugin package's exact peers.
  • node scripts/consumer-site.mjs starlight-ondocs built a fresh Starlight site from the packed tarballs with no react, react-dom, Radix or cmdk in node_modules. node scripts/deploy-recipes.mjs astro-node built and served a plain Astro site without them and answered.
  • A fresh VitePress site (consumer-site.mjs vitepress) with a page importing openAskDialog: from ondocs/embed/standalone, vitepress build passed. From ondocs/embed, it failed with "useSyncExternalStore" is not exported by "__vite-optional-peer-dep:react:ondocs", which is what the docs now say.
  • Landing page, next start on a local port, headless Chrome at 390 and 1440 px: the Astro/Starlight and VitePress Install steps show npm i ondocs ai @ai-sdk/openai, with no sideways scroll (scrollWidth 390 and 1440). The only console errors are the 404 for Vercel's analytics script, which exists only on Vercel.

Trade-offs

  • The packed tarball (npm pack) is 430,008 B on this branch, up from 332,815 B on main, for every user. Most of it is the standalone dialog chunk (306,459 B raw, 95,486 B gzip -9), which React sites never load.
  • An Astro site with React islands now loads a second React when the dialog first opens, and there is no option to use the site's React instead.
  • The types for ondocs/embed/standalone are dist/embed.d.ts, which imports ReactNode and MouseEvent from react (through the options chunk). A TypeScript app without @types/react gets unresolved types unless it sets skipLibCheck.
  • The weight cut is not delivered: the dialog loads about 1 KB less than on main. The preact/compat build (above) is the part of OD2-50 that would cut it, so OD2-50 should stay open, or get a follow-up item, for it.

Follow-ups

  • examples/starlight and examples/vitepress still depend on React, Radix and cmdk. Dropping them changes more of pnpm-lock.yaml.
  • The preact/compat build (above).
  • Moving the dialog's stylesheet into the lazy chunk. An injected <style> would break sites whose CSP has no 'unsafe-inline' in style-src.
  • Release sequencing: the docs deploy from main, so the shorter install lines go live when this merges, before the next release reaches npm. Until then only the "0.11.0 or earlier" note in each guide covers it. On a fresh VitePress site built from 0.11.0, the 3-package install fails the build. The changesets release PR should be merged right after this one.

…installed

The plugins mounted the launcher from ondocs/embed, whose dialog chunk imports
react, react-dom, @radix-ui/react-dialog and cmdk, so a Vue or Astro site had to
install all four or its build failed. A new build, ondocs/embed/standalone, is
ondocs/embed with production React, Radix and cmdk bundled into the dialog's
chunk. launcherScript mounts with it, so the guides now install only ondocs, ai
and @ai-sdk/openai. js-weight.mjs gets an on-open budget for each site, and the
fresh Starlight and VitePress sites in the consumers job install no React.
…m source

The tests run before the package is built, so the fixture aliases ondocs's
modules to their source; the launcher now imports ondocs/embed/standalone.
…d starlight-ondocs

ondocs init still printed react, react-dom, @radix-ui/react-dialog and cmdk
for Starlight, Astro and VitePress, the landing page's install step showed
them, and starlight-ondocs asked for them as required peers, which npm
installs. The plugins mount ondocs/embed/standalone now, so none of them is
needed: init lists ondocs, ai and @ai-sdk/openai, and starlight-ondocs asks
only for Starlight, Astro and the AI SDK.

The docs said openAskDialog from ondocs/embed works in any framework, but on a
site without React that import fails the build. ask-dialog.md now says such a
site imports it from ondocs/embed/standalone or uses data-ondocs-open, api.md
lists the new module and says ondocs/embed needs the site's React, and
writing-an-integration.md says what launcherScript mounts. The astro-*
deploy recipes and the starlight-ondocs consumer site install no React and
check it stays out. The changeset is minor, as the release adds a module.
…nteraction

Starlight imports its search UI, about 25 KB gzipped, in a requestIdleCallback.
On a busy CI runner that callback ran after the network had gone quiet and the
500 ms wait had passed, so the script charged it to the dialog's opening:
Starlight measured 10.3 KB before interaction and +118.4 KB on open, over the
97 KB budget, against 36.1 KB and +92.6 KB on every other run.

The script now waits for an idle callback queued after the page's own, then
until every script requested has loaded and none has been requested for
500 ms. With the page's idle callbacks delayed by 1.5 s, the old script
measured the Starlight example at 10.4 KB before interaction, and this one
36.2 KB. The figures of the unmodified builds are unchanged.
@dgesteves
dgesteves merged commit 953f1ac into main Oct 16, 2026
22 checks passed
@dgesteves
dgesteves deleted the fix/od2-50-standalone-dialog branch October 16, 2026 09:14
@dgesteves-release dgesteves-release Bot mentioned this pull request Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant