Summary
getStoreScript() memoises its output per storesDir and nothing ever invalidates it. clearStoreCache() exists and is exported, but it is called only from tests. The dev server also does not watch storesDir or composablesDir. The combined effect is that no edit to a store file — creating the first one, or changing any existing one — reaches the browser without restarting the dev server. Template HMR, meanwhile, is location.reload(), so every save on a stateful page throws away the state.
Impact (real app)
analyticshq documents the first half in config/ui.ts:72-73:
Caveat for the state stage: that null is memoised (
store-loader.js:12+) and nothing callsclearStoreCache(), so the FIRST store file needs a dev-server restart.
In practice it is worse than the note says: the same memo holds the built bundle once stores exist, so every subsequent edit to resources/stores/session.ts also needs a restart.
The second half costs the app on every template save: resources/views/dashboard.stx is ~107 KB and holds date-range, filter-chip and drill-down state entirely in client signals. A one-character edit reloads the document and resets all of it.
Evidence in stx
The memo is permanent — packages/stx/src/store-loader.ts
:18 const _cachedStoreScripts = new Map<string, string>()
:83 const cached = _cachedStoreScripts.get(resolvedDir)
:84 if (cached !== undefined) return cached || null
…
:200 _cachedStoreScripts.set(resolvedDir, code)The invalidator is never called — clearStoreCache() at store-loader.ts:207 and clearComposableCache() at composable-loader.ts:249. Grepping the whole workspace for callers:
packages/stx/test/signal-processing/store-runtime-globals.test.ts:33,38
packages/stx/test/composables/composable-loader.test.ts:44,48,125,145,154,166,226,237
packages/stx/dist/*.d.ts (declarations only)Nothing in packages/stx/src or packages/bun-plugin/src calls either one.
The directories are not watched — packages/bun-plugin/src/serve.ts:1078-1088
for (const dir of [
componentsDir,
layoutsDir,
partialsDir,
publicDir,
'resources/assets',
...(options.watchDirs ?? []),
]) { … startWatcher(dir) }storesDir and composablesDir are absent, and Stacks' dev views server (storage/framework/core/actions/src/dev/views.ts:178-200) passes no watchDirs.
HMR is a reload — packages/bun-plugin/src/serve.ts:987
es.onmessage=function(e){var m=JSON.parse(e.data);if(m.type==='reload')location.reload();else if(m.type==='css')bust()}Two actions only: reload the document, or re-fetch stylesheets via a ?v= bust.
Prior art
- Vite HMR + Vue/React Fast Refresh swap a module and preserve component state across the edit.
- Pinia's
acceptHMRUpdate(store, import.meta.hot)hot-swaps a store definition without losing its state — the direct analogue for what is missing here.
Ask
- Add
storesDirandcomposablesDirto the dev server's watch list atserve.ts:1078-1088. - Call
clearStoreCache()/clearComposableCache()from those watchers before firing the HMR event, so an edit is actually rebuilt. - Key the memo on file mtimes (or drop it in dev entirely) so a stale bundle cannot outlive its source even when a watcher misses an event.
- Ship a
type:'store'HMR message that re-executes the store bundle and re-runsdefineStorein place, rather than reloading the document — the PiniaacceptHMRUpdatecontract. - Longer term: a
type:'fragment'message that swaps the changed template region instead oflocation.reload().