Skip to content

fix(plugin): reload preview on external Stencil builds via staticDirs#53

Open
davidpett wants to merge 4 commits into
stenciljs:mainfrom
davidpett:fix/hmr-external-build
Open

fix(plugin): reload preview on external Stencil builds via staticDirs#53
davidpett wants to merge 4 commits into
stenciljs:mainfrom
davidpett:fix/hmr-external-build

Conversation

@davidpett

@davidpett davidpett commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Two related fixes for the Stencil Storybook dev preview:

  1. HMR for externally-built Stencil — reload the preview when a consumer builds Stencil via a separate stencil build --watch and serves the prebuilt output through Storybook staticDirs (e.g. staticDirs: ['../dist/esm']) with a lazy defineCustomElements() in preview.tsx.
  2. No spurious reload on first preview load — pre-bundle the deps Storybook/Stencil otherwise discover lazily, so Vite doesn't run a mid-session dep re-optimization that tears down the iframe right as the first story renders.

1. External-build HMR

In the external-build / lazy setup, stories never import a component .tsx into Vite's module graph, so the injected unplugin-stencil never compiles the project and its dependency maps stay empty. On a rebuild the external stencil --watch rewrites dist/esm/*.js, but those are static assets Vite doesn't track as modules — so the existing eager / lazy / style reload paths in stencilPreviewReloadPlugin can't fire and the preview never reloads.

Changes in packages/plugin/src/preset.ts:

  • Resolve the configured staticDirs to absolute paths in viteFinal (via options.presets.apply('staticDirs'), relative to configDir) and pass them to stencilPreviewReloadPlugin.
  • Explicitly server.watcher.add() those dirs — dist/esm lives outside Vite's root, so chokidar won't watch it otherwise.
  • On a *.js change inside a watched static dir, invalidate the cached dist/esm modules and send the existing stencil:reload event.
  • Debounce the reload (one external rebuild rewrites many chunks), and skip it while a lazy in-graph rebuild is pending to avoid double reloads.

Auto-detected / opt-in: a no-op when no staticDirs are configured.

2. Pre-bundle deps to avoid mid-session reload

storybook/internal/docs-tools, @stencil/core, and @stencil/core/internal/client were discovered lazily on the first preview load, triggering Vite dep re-optimizations (optimized dependencies changed. reloading) that tore down the iframe as the first story rendered. This surfaced as a CI flake in the HMR e2e suite (the reload raced the initial <my-component> render) but also affects real users as a surprise full reload on first load.

  • Add optimizeDeps.include for those deps so all optimization happens at startup and the preview renders once, without a reload.
  • Harden the HMR e2e before hook (tests/hmr.e2e.ts) to re-enter the iframe and re-query across any reload, as defense-in-depth.

Test plan

  • example-lazy (external stencil build --watch + staticDirs: ['../dist/esm']): preview renders with no startup reload; editing a component reloads the preview.
  • example (eager, in-graph): editing a component/style reloads once (no double-fire / regression).
  • No reloading messages observed in either suite after pre-bundling.
  • CI build_and_test green.

Consumers that run `stencil build --watch` separately and serve the
prebuilt output through Storybook `staticDirs` (e.g. `['../dist/esm']`)
with a lazy `defineCustomElements()` never import a component `.tsx`
into Vite's module graph. unplugin-stencil therefore never compiles the
project, its style/component dependency maps stay empty, and the
existing eager/lazy/style reload paths can't fire — so `dist/esm`
rebuilds on disk while the preview never reloads.

Watch the resolved `staticDirs` directories directly: register them with
the Vite watcher (they live outside the root, so chokidar won't track
them otherwise) and, on a `*.js` change, invalidate the cached dist/esm
modules and send the `stencil:reload` event. Reloads are debounced since
one external rebuild rewrites many chunks at once, and skipped while a
lazy in-graph rebuild is pending to avoid double reloads.

Auto-detected and opt-in: a no-op when no `staticDirs` are configured.
Condense the verbose comments added for the external-build reload path
and read `options.configType` inline instead of destructuring.
The preview can hit a late Vite dep-optimization pass ("optimized
dependencies changed. reloading") just after boot, tearing down the
iframe mid-render and leaving the `before` hook waiting on a detached
document until it times out. Re-enter the frame and re-query each tick
so a reload here no longer flakes the suite.
…ion reload

Storybook and Stencil deps (`storybook/internal/docs-tools`,
`@stencil/core`, `@stencil/core/internal/client`) were discovered lazily
on the first preview load, triggering a Vite dep re-optimization
("optimized dependencies changed. reloading") that tore down the iframe
just as the first story rendered. Pre-bundle them via `optimizeDeps`
so all optimization happens at startup and the preview renders once,
without a surprise reload — which also stabilizes the HMR e2e suite.
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