Skip to content

Ship the frontend runtime JavaScript as an installable npm package #7609

Description

@masenf

Problem

The JavaScript Reflex ships to every app (state.js, react-theme.js, upload.js, the helpers and components under utils/, style-reset.css, the vite plugins) lives in packages/reflex-base/src/reflex_base/.templates/web/ and is copied verbatim into each app's .web/ directory. That design has several costs:

  • Compiler/runtime drift. Compiled output and the copied runtime are only guaranteed to match by wiping and re-initializing .web on every Reflex version change, which also throws away node_modules and lockfiles.
  • Untestable runtime. The shipped modules import per-app generated artifacts ($/env.json, $/reflex.json, $/utils/context.js), so they cannot be loaded outside a compiled app, let alone unit-tested. state.js importing reflex.json is also one of the two causes of non-reproducible builds in Production builds are not reproducible: every reflex export renames every script #7562.
  • Dependency versions live in Python. react, react-dom, react-router, socket.io-client, universal-cookie and the build toolchain are maintained as PackageJson.DEPENDENCIES/DEV_DEPENDENCIES constants and pinned at the top level of the app's package.json; the REACT_VERSION/REACT_ROUTER_VERSION env vars are the only override. json5 is used but never declared.
  • Path-coupled imports. Component packages and user custom code reference the runtime through $/utils/... specifiers tied to the .web layout.

Proposal

Ship the non-generated frontend runtime as a real npm package, @reflex-dev/reflex-base, at packages/reflex-base/src/reflex_base/frontend/, bundled inside the reflex-base wheel.

Packaging

  • A stdlib-only hatch build hook packs a deterministic npm-format tarball into the wheel. The release build has no JS toolchain, and byte-stable tarballs keep the file: integrity hashes in committed reflex.lock lockfiles valid across rebuilds. The sdist keeps the JS source so wheels rebuild from it.
  • The package version derives from reflex-base's own version.
  • A verify-frontend-tgz release check asserts the wheel bundles exactly the right tarball and leaks neither source nor node_modules.

Install into .web

  • Each run installs the package like any other dependency (file:./<tarball>). Editable installs of reflex-base link the live source directory instead (link: for bun, file: for npm) so framework JS edits hot-reload without a reinstall.
  • The install flow writes the package entry itself and reconciles it: an unchanged entry keeps the frozen-lockfile install byte-for-byte; a superseded tarball frozen-installs first and is rewritten after; an unresolvable entry is rewritten up front. Legacy top-level framework pins are swept, superseded tarballs pruned, and the install cache keyed on the package version.
  • A Reflex version change no longer wipes .web: needs_reinit() narrows to missing-directory cases and a new refresh_web_directory() overwrite-copies the remaining templates, deletes files owned by older versions and re-renders generated configs, keeping node_modules, lockfiles and the project hash.

Runtime decoupling

  • Shipped modules stop importing generated files. A @reflex-dev/reflex-base/runtime registry is populated by the generated utils/context.js via configureReflexRuntime() in its module body; the two module-scope reads of app config (EVENTURL, the embed-mode locationRef seed) become lazy.
  • ColorModeContext moves into react-theme.js and the generated context re-exports it. Compiled components read env values through getEnv() instead of importing env.json.

Compiler and imports

  • Generated code and component import declarations reference the package by bare specifier (@reflex-dev/reflex-base/state, .../react-theme, .../helpers/*, .../components/*, .../style-reset.css). constants.FrontendPackage is the single Python-side source of these specifiers, with a unit test asserting each resolves to a shipped file.
  • Back-compat for user code is three-layered: merge_imports normalizes legacy $/utils/... specifiers in Python import declarations, the generated vite config aliases them for user JS, and the app root keeps the window.__reflex["$/utils/state"] registry key for backend-evaluated dynamic components. Dirs.STATE_PATH and ResetStylesheet remain as deprecated fallbacks.

Dependencies

  • Runtime framework deps resolve transitively through the package manifest; the app-level toolchain is copied from its devDependencies; shiki and @glideapps/glide-data-grid are optional peer deps deduped to the app root.
  • User version control moves to overrides in reflex.lock/package.json. REACT_VERSION/REACT_ROUTER_VERSION are deprecated (warn now, removed in 1.0). react-helmet is only installed when rx.helmet is used.

Tests

Pending work

The implementation lives on branch claude/reflex-frontend-js-package-ockox9 (7 commits, +4775/-348 across 99 files). It was validated end to end with reflex compile and reflex export of a scratch app in source mode, bun and npm installs of the packed tarball, and the wheel in a clean venv. No PR has been opened for it yet.

Related

Activity

  1. added theissue type on Oct 20, 2026
  2. linear-code commented on Oct 20, 2026

    @linear-code
  3. added
    roadmaphttps://github.com/reflex-dev/reflex/issues/2727
    on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    roadmaphttps://github.com/reflex-dev/reflex/issues/2727

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions