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/7566.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid rerunning npm installs during source-only reloads when npm omits an empty `devDependencies` field from `package.json`.
9 changes: 8 additions & 1 deletion reflex/utils/frontend_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

logger = logging.getLogger(__name__)

_DEV_DEPENDENCIES_KEY = "devDependencies"


def initialize_gitignore(
gitignore_file: Path = constants.GitIgnore.FILE,
Expand Down Expand Up @@ -368,7 +370,12 @@ def sync_root_package_json_to_web() -> bool:

output_path = get_web_lockfile_path(constants.PackageJson.PATH)
rendered = _compile_package_json()
if _read_package_json_object(output_path) == json.loads(rendered):
existing_package_json = _read_package_json_object(output_path)
rendered_package_json = json.loads(rendered)
for package_json in (existing_package_json, rendered_package_json):
if package_json.get(_DEV_DEPENDENCIES_KEY) == {}:
package_json.pop(_DEV_DEPENDENCIES_KEY)
if existing_package_json == rendered_package_json:
return False
Comment thread
greptile-apps[bot] marked this conversation as resolved.

changed = output_path.exists()
Expand Down
19 changes: 19 additions & 0 deletions tests/units/utils/test_frontend_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ def test_sync_root_package_json_ignores_formatting(
assert package_json_path.stat().st_mtime_ns == original_mtime


def test_sync_root_package_json_ignores_empty_dev_dependencies(
package_json_path: Path,
):
"""An absent and empty devDependencies field are equivalent for npm.

Args:
package_json_path: The rendered package.json path.
"""
package_json = json.loads(package_json_path.read_text())
package_json.pop("devDependencies", None)
package_json_path.write_text(json.dumps(package_json, indent=2))
original_content = package_json_path.read_text()
original_mtime = package_json_path.stat().st_mtime_ns

assert frontend_skeleton.sync_root_package_json_to_web() is False
assert package_json_path.read_text() == original_content
assert package_json_path.stat().st_mtime_ns == original_mtime


@pytest.mark.parametrize(
("field", "value"),
[
Expand Down
42 changes: 41 additions & 1 deletion tests/units/utils/test_js_runtimes.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Tests for reflex.utils.js_runtimes install gating."""

import json
from pathlib import Path
from unittest import mock

import pytest
from reflex_base import constants

from reflex.utils import js_runtimes
from reflex.utils import frontend_skeleton, js_runtimes

BUN_PATH = Path("/usr/bin/bun")
NPM_PATH = Path("/usr/bin/npm")
Expand Down Expand Up @@ -89,6 +91,44 @@ def _patch_unsupported_node(monkeypatch) -> None:
monkeypatch.setattr(js_runtimes, "get_node_version", lambda: None)


def test_npm_empty_dev_dependencies_preserves_frontend_install_cache(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
"""An npm manifest omitting empty devDependencies must not invalidate installs.

Args:
tmp_path: The temporary app directory.
monkeypatch: The pytest monkeypatch fixture.
"""
monkeypatch.chdir(tmp_path)
web_dir = tmp_path / constants.Dirs.WEB
web_dir.mkdir()
monkeypatch.setattr(frontend_skeleton, "get_web_dir", lambda: web_dir)
monkeypatch.setattr(js_runtimes, "get_web_dir", lambda: web_dir)

root_package_json = frontend_skeleton.get_root_lockfile_path(
constants.PackageJson.PATH
)
root_package_json.parent.mkdir()
root_package_json.write_text(json.dumps({"dependencies": {"test-pkg": "1.0.0"}}))

npm_package_json = json.loads(frontend_skeleton._compile_package_json())
npm_package_json.pop("devDependencies", None)
web_package_json = frontend_skeleton.get_web_lockfile_path(
constants.PackageJson.PATH
)
web_package_json.write_text(json.dumps(npm_package_json, indent=2))

cache_file = js_runtimes._frontend_packages_cache_path()
cache_file.touch()
original_content = web_package_json.read_text()

js_runtimes._sync_root_lockfiles_for_frontend_install()

assert cache_file.exists()
assert web_package_json.read_text() == original_content


def test_install_never_starts_with_unsupported_node(monkeypatch, install_mocks, caplog):
"""A failed node-version preflight must leave no lockfile side effects.

Expand Down
Loading