Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6658.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sync `reflex.lock/package.json` to `.web/package.json` before installing packages to ensure lock file and package.json are aligned.
1 change: 1 addition & 0 deletions packages/reflex-base/news/6658.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`package_json_template` accepts `**additional_keys` to include extra fields (e.g. `name`, `packageManager`, `engines`) in the rendered package.json.
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ def package_json_template(
dependencies: dict[str, str],
dev_dependencies: dict[str, str],
overrides: dict[str, str],
**additional_keys: Any,
):
"""Template for package.json.

Expand All @@ -524,17 +525,21 @@ def package_json_template(
dependencies: The dependencies to include in the package.json file.
dev_dependencies: The devDependencies to include in the package.json file.
overrides: The overrides to include in the package.json file.
additional_keys: Additional keys to include in the package.json file.

Returns:
Rendered package.json content as string.
"""
# Ensure "type" is not duplicated since it's always set to "module"
additional_keys.pop("type", None)
return json.dumps({
"name": "reflex",
"name": additional_keys.pop("name", "reflex"),
"type": "module",
"scripts": scripts,
"dependencies": dependencies,
"devDependencies": dev_dependencies,
"overrides": overrides,
**additional_keys,
})


Expand Down
94 changes: 37 additions & 57 deletions reflex/utils/frontend_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ def initialize_requirements_txt(
constants.Bun.LOCKFILE_PATH,
constants.Node.LOCKFILE_PATH,
)
NO_PRUNE_LOCKFILE_NAMES: tuple[str, ...] = (constants.PackageJson.PATH,)


def get_root_lockfile_path(filename: str) -> Path:
Expand All @@ -297,40 +298,20 @@ def get_web_lockfile_path(filename: str) -> Path:
return get_web_dir() / filename


def get_root_package_json_path() -> Path:
"""Get the persisted package.json path in the app root.

Stored alongside the lockfiles inside ``reflex.lock/`` so resolved
dependency pins survive a fresh ``reflex init``.

Returns:
The persisted package.json path in the app root.
"""
return Path.cwd() / constants.Bun.ROOT_LOCKFILE_DIR / constants.PackageJson.PATH


def get_web_package_json_path() -> Path:
"""Get the package.json path in the .web directory.

Returns:
The package.json path in the .web directory.
"""
return get_web_dir() / constants.PackageJson.PATH


def _copy_if_exists(src: Path, dest: Path) -> bool:
def _copy_if_exists(src: Path, dest: Path, prune: bool = True) -> bool:
"""Copy ``src`` to ``dest`` (creating ``dest`` parents as needed).

Args:
src: The source file. If absent, ``dest`` is removed when present.
dest: The destination file.
prune: Remove destination file that does not exist in source.

Returns:
True if ``dest``'s effective contents changed (created from absence,
overwritten with different bytes, or removed because ``src`` is gone).
"""
if not src.exists():
if dest.exists():
if dest.exists() and prune:
console.debug(f"Removing stale {dest}")
path_ops.rm(dest)
return True
Expand All @@ -346,11 +327,12 @@ def _copy_if_exists(src: Path, dest: Path) -> bool:
return changed


def sync_root_lockfile_to_web(filename: str) -> bool:
def sync_root_lockfile_to_web(filename: str, prune: bool = True) -> bool:
"""Mirror a single persisted lockfile into ``.web``.

Args:
filename: The lockfile basename.
prune: Remove destination file that does not exist in source.

Returns:
True if ``.web``'s copy was meaningfully changed (overwritten with
Expand All @@ -359,7 +341,7 @@ def sync_root_lockfile_to_web(filename: str) -> bool:
cache could exist yet.
"""
return _copy_if_exists(
get_root_lockfile_path(filename), get_web_lockfile_path(filename)
get_root_lockfile_path(filename), get_web_lockfile_path(filename), prune=prune
)


Expand All @@ -370,7 +352,9 @@ def sync_root_lockfiles_to_web() -> bool:
True if any ``.web`` lockfile was meaningfully changed.
"""
# Materialize results so every lockfile is synced
changed = [sync_root_lockfile_to_web(name) for name in LOCKFILE_NAMES]
changed = [sync_root_lockfile_to_web(name) for name in LOCKFILE_NAMES] + [
sync_root_lockfile_to_web(name, prune=False) for name in NO_PRUNE_LOCKFILE_NAMES
]
return any(changed)


Expand All @@ -391,44 +375,34 @@ def sync_web_lockfile_to_root(filename: str):

def sync_web_lockfiles_to_root():
"""Persist every ``.web`` lockfile back to the app root."""
for name in LOCKFILE_NAMES:
for name in LOCKFILE_NAMES + NO_PRUNE_LOCKFILE_NAMES:
sync_web_lockfile_to_root(name)


def sync_web_package_json_to_root():
"""Persist the resolved .web package.json back to the app root.

Captures the dependency pins produced by ``bun add`` so the next
``reflex init`` can restore them as the starting point for the new
package.json.
"""
web_package_json_path = get_web_package_json_path()
if not web_package_json_path.exists():
return

root_package_json_path = get_root_package_json_path()
path_ops.mkdir(root_package_json_path.parent)
console.debug(f"Copying {web_package_json_path} to {root_package_json_path}")
path_ops.cp(web_package_json_path, root_package_json_path)


def _read_persisted_package_json() -> dict:
"""Read the persisted package.json from the app root.

Returns:
The parsed JSON object, or an empty dict if the file is missing or
cannot be parsed.
The parsed JSON object, or an empty dict if the file is missing,
cannot be parsed, or is not a JSON object.
"""
root_package_json_path = get_root_package_json_path()
root_package_json_path = get_root_lockfile_path(constants.PackageJson.PATH)
if not root_package_json_path.exists():
return {}
try:
return json.loads(root_package_json_path.read_text())
parsed = json.loads(root_package_json_path.read_text())
except (json.JSONDecodeError, OSError) as e:
console.warn(
f"Failed to read {root_package_json_path}: {e}; starting with empty dependency lists."
)
return {}
if not isinstance(parsed, dict):
console.warn(
f"Expected {root_package_json_path} to contain a JSON object, "
f"got {type(parsed).__name__}; starting with empty dependency lists."
)
return {}
return parsed


def initialize_web_directory():
Expand All @@ -446,6 +420,7 @@ def initialize_web_directory():

console.debug("Initializing the web directory.")
initialize_package_json()
sync_web_lockfiles_to_root()
Comment thread
greptile-apps[bot] marked this conversation as resolved.

console.debug("Initializing the bun config file.")
initialize_bun_config()
Expand Down Expand Up @@ -519,26 +494,31 @@ def _compile_package_json():
``reflex.lock/package.json`` (when present) so resolved version pins
survive a fresh ``reflex init``. User-added ``scripts`` are preserved;
only the framework-owned ``dev`` and ``export`` entries are refreshed
from constants. ``overrides`` are always refreshed. The framework-managed
entries in ``constants.PackageJson.DEPENDENCIES`` / ``DEV_DEPENDENCIES``
are added later at install time via ``bun add`` so they pick up strict
pins.
from constants. User-added ``overrides`` are kept, with the
framework-owned entries refreshed on top. Any other persisted fields
(e.g. ``packageManager``, ``engines``) are passed through unchanged.
The framework-managed entries in ``constants.PackageJson.DEPENDENCIES``
/ ``DEV_DEPENDENCIES`` are added later at install time via ``bun add``
so they pick up strict pins.

Returns:
Rendered package.json content as string.
"""
persisted = _read_persisted_package_json()
persisted_scripts = persisted.get("scripts") or {}
scripts = {
**persisted_scripts,
**(persisted.pop("scripts", None) or {}),
"dev": constants.PackageJson.Commands.DEV,
"export": constants.PackageJson.Commands.EXPORT,
}
return templates.package_json_template(
scripts=scripts,
dependencies=persisted.get("dependencies") or {},
dev_dependencies=persisted.get("devDependencies") or {},
overrides=constants.PackageJson.OVERRIDES,
dependencies=persisted.pop("dependencies", None) or {},
dev_dependencies=persisted.pop("devDependencies", None) or {},
overrides={
**(persisted.pop("overrides", None) or {}),
**constants.PackageJson.OVERRIDES,
},
**persisted,
)


Expand Down
5 changes: 3 additions & 2 deletions reflex/utils/js_runtimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,9 @@ def _existing_web_package_sections() -> tuple[set[str], set[str]]:
A tuple ``(deps, dev_deps)`` of bare package names. Both empty if
the file is missing or unreadable.
"""
web_pkg_json_path = frontend_skeleton.get_web_package_json_path()
web_pkg_json_path = frontend_skeleton.get_web_lockfile_path(
constants.PackageJson.PATH
)
if not web_pkg_json_path.exists():
return set(), set()
try:
Expand Down Expand Up @@ -757,4 +759,3 @@ def install_frontend_packages(packages: set[str], config: Config):
_sync_root_lockfiles_for_frontend_install()
_install_frontend_packages(set(packages), config, install_package_managers)
frontend_skeleton.sync_web_lockfiles_to_root()
frontend_skeleton.sync_web_package_json_to_root()
Loading
Loading