Skip to content

Stub generator copies non-literal expressions into client bundle (ReferenceError at runtime) #48

Description

@MarcosBrendonDePaula

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:

  1. 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.
  2. 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(...).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions