Problem
The Live Component stub generator (the script that produces app/client/.live-stubs/Live*.js) serializes static defaultState by copying the source expression as-is, instead of evaluating it or restricting to JSON-serializable literals. When a default-state field references a module-scoped constant or any other server-only identifier, the generated client stub ends up with an undefined reference and throws at runtime.
Reproduce
A minimal server-side Live Component:
// app/server/live/LiveDirBrowser.ts
import { LiveComponent } from "@core/types/types"
// Module-scoped constant — only meaningful on the server
const IS_WIN = typeof process !== "undefined" && process.platform === "win32"
export class LiveDirBrowser extends LiveComponent<typeof LiveDirBrowser.defaultState> {
static componentName = "LiveDirBrowser"
static publicActions = ["cd", "up", "refresh"] as const
static defaultState = {
currentPath: "",
isWindows: IS_WIN, // <-- referência a const do módulo
drives: [] as string[],
}
// ... actions
}
The auto-generated client stub becomes:
// app/client/.live-stubs/LiveDirBrowser.js
export class LiveDirBrowser {
static componentName = 'LiveDirBrowser'
static defaultState = {
currentPath: "",
isWindows: IS_WIN, // <-- IS_WIN is not defined in the browser bundle
drives: [],
}
static publicActions = ["cd", "up", "refresh"]
}
At runtime in the browser:
SES_UNCAUGHT_EXCEPTION: ReferenceError: IS_WIN is not defined
<anonymous> LiveDirBrowser.js:10
<anonymous> LiveDirBrowser.js:1
The component crashes before Live.use() can attach the proxy, breaking the page.
Workaround
Replace the reference with a literal in defaultState, then set the real value in the constructor via this.setState({ isWindows: IS_WIN }):
static defaultState = {
isWindows: false, // literal placeholder
...
}
constructor(...) {
super(...)
this.setState({ isWindows: IS_WIN })
}
This works because the stub copies the literal false faithfully, and the real value flows from the server via the normal state-sync path. But the workaround is non-obvious — devs hit a cryptic ReferenceError first.
Expected behavior
One of:
- Best: actually evaluate
defaultState at generation time (e.g. import the module, read the static, JSON-serialize). Any non-serializable value would then fail loudly at generation with a clear error pointing to the offending field.
- Acceptable: at minimum, detect identifiers in the source AST of
defaultState that aren't in scope of pure literals (Identifier nodes that aren't null/undefined/true/false/numeric/string literals or as casts of those) and fail the generator with error: defaultState field 'isWindows' references identifier 'IS_WIN' which won't exist in the client bundle. Use a literal in defaultState and set the real value in the constructor via this.setState(...).
- Minimum: document this constraint in the Live Component docs ("
defaultState must only contain JSON-literal values; module-scoped references will leak into the client stub and crash").
Environment
@fluxstack/live@0.8.0
- Bun 1.3.12 on Windows 11
- FluxStack project from
create-fluxstack template
Notes
This is the second variant of the same class of bug I've hit: anything that isn't a pure literal in defaultState (function calls, expressions, identifier references, even process.platform directly) silently breaks the client. A generation-time check would have saved hours of debugging.
Problem
The Live Component stub generator (the script that produces
app/client/.live-stubs/Live*.js) serializesstatic defaultStateby copying the source expression as-is, instead of evaluating it or restricting to JSON-serializable literals. When a default-state field references a module-scoped constant or any other server-only identifier, the generated client stub ends up with an undefined reference and throws at runtime.Reproduce
A minimal server-side Live Component:
The auto-generated client stub becomes:
At runtime in the browser:
The component crashes before
Live.use()can attach the proxy, breaking the page.Workaround
Replace the reference with a literal in
defaultState, then set the real value in the constructor viathis.setState({ isWindows: IS_WIN }):This works because the stub copies the literal
falsefaithfully, and the real value flows from the server via the normal state-sync path. But the workaround is non-obvious — devs hit a crypticReferenceErrorfirst.Expected behavior
One of:
defaultStateat generation time (e.g. import the module, read the static, JSON-serialize). Any non-serializable value would then fail loudly at generation with a clear error pointing to the offending field.defaultStatethat aren't in scope of pure literals (Identifier nodes that aren'tnull/undefined/true/false/numeric/string literals orascasts of those) and fail the generator witherror: defaultState field 'isWindows' references identifier 'IS_WIN' which won't exist in the client bundle. Use a literal in defaultState and set the real value in the constructor via this.setState(...).defaultStatemust only contain JSON-literal values; module-scoped references will leak into the client stub and crash").Environment
@fluxstack/live@0.8.0create-fluxstacktemplateNotes
This is the second variant of the same class of bug I've hit: anything that isn't a pure literal in
defaultState(function calls, expressions, identifier references, evenprocess.platformdirectly) silently breaks the client. A generation-time check would have saved hours of debugging.