Ship the frontend runtime JavaScript as an installable npm package

Open
#7,609 1 comment 1 reaction 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Active
Tech stack
bun, javascript, python, react, vite

Research direction

Start by reading the pending-work checklist and the implementation branch claude/reflex-frontend-js-package-ockox9, then compare its install-flow changes with #7235 and #7451. Rebase the branch onto current main, open a PR, and review the breaking-change fragment and local-packages documentation. Done means the PR is ready and the release pipeline confirms the tarball is bundled in a pre-release wheel.

Written by the indexing model from the issue text.

Description

roadmap

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 #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

  • Vitest unit tests live inside the package (frontend/tests/) and run as a js-unit-tests CI job; state.js becomes unit-testable for the first time. The JS test tooling itself is tracked in #7611.

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.

  • Rebase onto current main: the branch is based on #6919 (late August) and predates later changes to the install flow (for example #7235 and the open #7451).
  • Open the PR; re-check the breaking-change fragment (news/+frontend-npm-package.breaking.md) and the docs/wrapping-react/local-packages.md section describing the file:/link: entry and overrides.
  • Confirm the release pipeline builds the tarball into the wheel (verify-frontend-tgz in scripts/release.py) on an actual pre-release.

Related

  • #7611 JS unit tests for the shipped frontend
  • #7562 / #7564 reproducible production builds
Dominant language
Python
Stars
29k
Forks
1.8k
Avg merge
2d 22h
Merged PRs (30d)
143

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from reflex-dev/reflex

All issues in reflex-dev/reflex

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.