Summary
Two related problems in the store runtime's debug story.
devtools.tsimplements a full store panel —registerStore,recordStoreMutation,timeTravel, aselectedStoreUI state — and the signals runtime never calls any of it.devToolsState.storesis therefore always empty, so the store tree, the mutation timeline and time travel are all permanently dead.- In place of that, the runtime emits unconditional
console.log— nine[stx:store]lines including one on every singleuseStore()call, plus two more in the DOMContentLoaded path. All eleven ship indistand fire in production.
So the framework produces log noise for end users and no usable debug surface for developers.
Impact (real app)
analyticshq debugged its store initialization by rendering the SPA in a headless browser and reading console output — resources/stores/session.ts:43:
Verified in a headless browser, not by reading the bundle.
The findings had to be written down as comments so they would not be re-discovered. resources/stores/session.ts:36-43 records the failure class a store panel would have made obvious in one glance:
That failure is silent in the worst way: the ReferenceError is thrown inside the store IIFE, so defineStore never completes and useStore('session') reports "Store not found" somewhere else entirely.
Roughly 24 lines of that store file are comments preserving debugging results. Meanwhile the app's production console carries a [stx:store] useStore: session line for each of the six useStore('session') call sites (account.stx:14, dashboard.stx:790, register.stx:13, pricing.stx:21, login.stx:13, sites/new.stx:17), plus the registration and persistence lines, on every page load.
Evidence in stx
The devtools store API has no caller. packages/stx/src/devtools.ts:348-374:
export function registerStore(id, initialState, getters = {}, actions = []): void {
if (!devToolsState.enabled) return
…
devToolsState.stores.set(id, info) // :371 — the only writer
recordEvent({ type: 'store:registered', … })
}$ grep -n "registerStore" packages/stx/src/signals.ts
(no match)defineStore lives at signals.ts:5753-5949 and never touches devtools. The consequence is at devtools.ts:702-705:
function timeTravel(historyIndex: number, storeId: string): void {
const store = devToolsState.stores.get(storeId)
if (!store) return // ← always takenThe runtime does maintain its own registry (window.stx._stores / window.__STX_STORES__, signals.ts:5945-5947), and devtools.ts:730 reads __STX_STORES__ — but only inside timeTravel, which has already returned by then.
Unconditional logging. packages/stx/src/signals.ts:
:5754 console.log('[stx:store] defineStore called:', id, …)
:5757 console.log('[stx:store] returning existing store:', id)
:5834 console.log('[stx:store] setup complete:', id, …)
:5883 console.log('[stx:store] hydrating from SSR:', id, …)
:5904 console.log('[stx:store] restoring persisted state:', id, …)
:5913 console.log('[stx:store] no persisted state found:', id, …)
:5930 console.log('[stx:store] persisting:', id, …)
:5948 console.log('[stx:store] registered:', id, …)
:5971 console.log('[stx:store] useStore:', id) ← every store access
:6420 console.log('[DOMContentLoaded] processElement on:', el.tagName, …)
:6523 console.log('[stx] running global mountCallbacks:', mountCallbacks.length)None is gated on a debug flag. Verified in the published build:
$ grep -o "console.log(.\[stx:store\]" node_modules/@stacksjs/stx/dist/signals.js | wc -l
9
$ grep -c "\[DOMContentLoaded\] processElement on:" node_modules/@stacksjs/stx/dist/signals.js
1
$ grep -c "running global mountCallbacks" node_modules/@stacksjs/stx/dist/signals.js
1This is the same class as #1786 (the :class binding's debug log), in a different file and at 11× the volume.
Prior art
- Vue Devtools Pinia panel: state tree, mutation timeline, time travel — populated automatically by
defineStore, with no logging. - Redux DevTools: every dispatched action recorded, replayable.
- TanStack Query Devtools: query cache, fetch status, staleness.
Ask
- Call
registerStore(id, initialState, getters, actions)fromdefineStore(signals.ts:5753) andrecordStoreMutationfrom each signal setter in the store's returned object, so the panel #1747 shipped has data. - Gate every
[stx:store]/[DOMContentLoaded]/mountCallbacksconsole.logbehind the existing devtools-enabled flag, and strip them in production builds.console.logon everyuseStore()is a per-call cost in a hot path. - Surface a real error when a store IIFE throws: today a
ReferenceErrorinside the shared bundle aborts everydefineStoreand the developer seesStore not foundfrom an unrelateduseStoreelsewhere (signals.ts:5966-5969). Detect that the bundle failed to execute and say so.
Related: #1747 (the devtools extension this would populate), #1754, #1786 (the same class of shipped debug log).