Repository navigation
Ship the frontend runtime JavaScript as an installable npm package #7609
Description
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
.webon every Reflex version change, which also throws awaynode_modulesand 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.jsimportingreflex.jsonis also one of the two causes of non-reproducible builds in Production builds are not reproducible: everyreflex exportrenames 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_DEPENDENCIESconstants and pinned at the top level of the app'spackage.json; theREACT_VERSION/REACT_ROUTER_VERSIONenv vars are the only override.json5is used but never declared. - Path-coupled imports. Component packages and user custom code reference the runtime through
$/utils/...specifiers tied to the.weblayout.
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 committedreflex.locklockfiles 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-tgzrelease check asserts the wheel bundles exactly the right tarball and leaks neither source nornode_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 newrefresh_web_directory()overwrite-copies the remaining templates, deletes files owned by older versions and re-renders generated configs, keepingnode_modules, lockfiles and the project hash.
Runtime decoupling
- Shipped modules stop importing generated files. A
@reflex-dev/reflex-base/runtimeregistry is populated by the generatedutils/context.jsviaconfigureReflexRuntime()in its module body; the two module-scope reads of app config (EVENTURL, the embed-modelocationRefseed) become lazy. ColorModeContextmoves intoreact-theme.jsand the generated context re-exports it. Compiled components read env values throughgetEnv()instead of importingenv.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.FrontendPackageis 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_importsnormalizes legacy$/utils/...specifiers in Python import declarations, the generated vite config aliases them for user JS, and the app root keeps thewindow.__reflex["$/utils/state"]registry key for backend-evaluated dynamic components.Dirs.STATE_PATHandResetStylesheetremain as deprecated fallbacks.
Dependencies
- Runtime framework deps resolve transitively through the package manifest; the app-level toolchain is copied from its
devDependencies;shikiand@glideapps/glide-data-gridare optional peer deps deduped to the app root. - User version control moves to
overridesinreflex.lock/package.json.REACT_VERSION/REACT_ROUTER_VERSIONare deprecated (warn now, removed in 1.0).react-helmetis only installed whenrx.helmetis used.
Tests
- Vitest unit tests live inside the package (
frontend/tests/) and run as ajs-unit-testsCI job;state.jsbecomes unit-testable for the first time. The JS test tooling itself is tracked in Unit-test the shipped frontend JavaScript with standard JS tooling (vitest) #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 fix(events): chained events inherit the originating event's router_data #6919 (late August) and predates later changes to the install flow (for example sync_root_package_json_to_web() compares package.json as text, so every compile re-runs the frontend package install #7235 and the open Save npm dev dependencies with --save-dev and keep them under NODE_ENV=production #7451). - Open the PR; re-check the breaking-change fragment (
news/+frontend-npm-package.breaking.md) and thedocs/wrapping-react/local-packages.mdsection describing thefile:/link:entry andoverrides. - Confirm the release pipeline builds the tarball into the wheel (
verify-frontend-tgzinscripts/release.py) on an actual pre-release.
Related
- Unit-test the shipped frontend JavaScript with standard JS tooling (vitest) #7611 JS unit tests for the shipped frontend
- Production builds are not reproducible: every
reflex exportrenames every script #7562 / Make the production frontend build reproducible #7564 reproducible production builds
Activity
linear-code commentedon Oct 20, 2026
ENG-13448