From c0003917813c6f4c7a6784d795c9ca831be744d8 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 13 Jun 2026 13:29:31 +0530 Subject: [PATCH 1/2] fix persisted bun lockfile recovery --- reflex/utils/js_runtimes.py | 41 +++++++++++++++++++++---------- tests/units/test_prerequisites.py | 31 +++++++++++++++-------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/reflex/utils/js_runtimes.py b/reflex/utils/js_runtimes.py index 9d1cc9de557..e6d8e3b1fbf 100644 --- a/reflex/utils/js_runtimes.py +++ b/reflex/utils/js_runtimes.py @@ -459,21 +459,19 @@ def _is_bun_package_manager(package_manager: str) -> bool: def _run_initial_install(primary_package_manager: str, env: dict) -> None: - """Run the initial frozen-lockfile install with a friendly recovery hint. + """Run the initial frozen-lockfile install and repair a mismatched Bun lock. bun reports ``error: lockfile had changes, but lockfile is frozen`` when - the persisted lockfile cannot satisfy the recovered package.json. When - that happens, point the user at ``reflex.lock/package.json`` so they can - delete it and let Reflex regenerate the dep set from scratch on the - next run. + the persisted lockfile cannot satisfy the recovered package.json. Retry + that specific failure without ``--frozen-lockfile`` so Bun reconciles the + pair; the successful install flow then persists both files together. Args: primary_package_manager: Path to the package manager executable. env: Extra environment variables for the subprocess. Raises: - SystemExit: If the install fails. The exit message tells the user - how to recover from a frozen-lockfile mismatch when applicable. + SystemExit: If the install or mismatch recovery fails. """ install_args = [ primary_package_manager, @@ -499,14 +497,31 @@ def _run_initial_install(primary_package_manager: str, env: dict) -> None: if process.returncode == 0: return - if any("lockfile had changes, but lockfile is frozen" in line for line in logs): - root_dir = Path.cwd() / constants.Bun.ROOT_LOCKFILE_DIR - console.error( + if _is_bun_package_manager(primary_package_manager) and any( + "lockfile had changes, but lockfile is frozen" in line for line in logs + ): + console.warn( "The persisted lockfile is out of sync with the recovered " - f"package.json. Delete the [bold]{root_dir}[/bold] directory " - "and rerun so Reflex regenerates it from scratch." + "package.json. Regenerating the lockfile." ) - raise SystemExit(1) + recovery_args = [ + primary_package_manager, + "install", + "--legacy-peer-deps", + ] + recovery_process = processes.new_process( + processes.get_command_with_loglevel(recovery_args), + cwd=get_web_dir(), + shell=constants.IS_WINDOWS, + env=env, + ) + processes.show_status( + "Regenerating frontend lockfile", + recovery_process, + ) + if recovery_process.returncode != 0: + raise SystemExit(1) + return # Replay captured logs so the user can diagnose other failures (mirrors # show_status's default error path, which we suppressed above). diff --git a/tests/units/test_prerequisites.py b/tests/units/test_prerequisites.py index 0079366ce97..29df6ef84ef 100644 --- a/tests/units/test_prerequisites.py +++ b/tests/units/test_prerequisites.py @@ -1096,32 +1096,43 @@ def run_package_manager(args, **kwargs): @pytest.mark.usefixtures("install_packages_env") -def test_run_initial_install_frozen_lockfile_error_helpful_message(monkeypatch, capsys): - """A frozen-lockfile mismatch surfaces a 'delete reflex.lock/package.json' hint.""" +def test_run_initial_install_repairs_frozen_lockfile_mismatch(monkeypatch, capsys): + """A frozen-lockfile mismatch retries without freezing the lockfile.""" class _FakeProcess: - returncode = 1 + def __init__(self, returncode): + self.returncode = returncode + + calls = [] + processes = iter((_FakeProcess(1), _FakeProcess(0))) + + def new_process(args, **kwargs): + calls.append(args) + return next(processes) monkeypatch.setattr( js_runtimes.processes, "new_process", - lambda *args, **kwargs: _FakeProcess(), + new_process, ) monkeypatch.setattr( js_runtimes.processes, "show_status", - lambda message, process, suppress_errors=False: [ - "error: lockfile had changes, but lockfile is frozen\n", - ], + lambda message, process, suppress_errors=False: ( + ["error: lockfile had changes, but lockfile is frozen\n"] + if process.returncode + else [] + ), ) - with pytest.raises(SystemExit): - js_runtimes._run_initial_install("bun", env={}) + js_runtimes._run_initial_install("bun", env={}) captured = capsys.readouterr() output = captured.out + captured.err assert "out of sync" in output - assert constants.Bun.ROOT_LOCKFILE_DIR in output + assert len(calls) == 2 + assert "--frozen-lockfile" in calls[0] + assert "--frozen-lockfile" not in calls[1] @pytest.mark.usefixtures("install_packages_env") From 28561941785c4775fbd4820acde1f2ddc582e843 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 13 Jun 2026 13:36:52 +0530 Subject: [PATCH 2/2] add lockfile recovery failure guidance --- reflex/utils/js_runtimes.py | 6 ++++++ tests/units/test_prerequisites.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/reflex/utils/js_runtimes.py b/reflex/utils/js_runtimes.py index e6d8e3b1fbf..e4d1982a3a8 100644 --- a/reflex/utils/js_runtimes.py +++ b/reflex/utils/js_runtimes.py @@ -520,6 +520,12 @@ def _run_initial_install(primary_package_manager: str, env: dict) -> None: recovery_process, ) if recovery_process.returncode != 0: + root_dir = Path.cwd() / constants.Bun.ROOT_LOCKFILE_DIR + console.error( + "Failed to regenerate the frontend lockfile. Delete the " + f"[bold]{root_dir}[/bold] directory and rerun so Reflex " + "regenerates it from scratch." + ) raise SystemExit(1) return diff --git a/tests/units/test_prerequisites.py b/tests/units/test_prerequisites.py index 29df6ef84ef..8fb73df18c0 100644 --- a/tests/units/test_prerequisites.py +++ b/tests/units/test_prerequisites.py @@ -1135,6 +1135,37 @@ def new_process(args, **kwargs): assert "--frozen-lockfile" not in calls[1] +@pytest.mark.usefixtures("install_packages_env") +def test_run_initial_install_failed_recovery_has_actionable_message( + monkeypatch, capsys +): + """A failed mismatch recovery tells the user how to regenerate from scratch.""" + + class _FakeProcess: + returncode = 1 + + monkeypatch.setattr( + js_runtimes.processes, + "new_process", + lambda *args, **kwargs: _FakeProcess(), + ) + monkeypatch.setattr( + js_runtimes.processes, + "show_status", + lambda message, process, suppress_errors=False: [ + "error: lockfile had changes, but lockfile is frozen\n", + ], + ) + + with pytest.raises(SystemExit): + js_runtimes._run_initial_install("bun", env={}) + + captured = capsys.readouterr() + output = captured.out + captured.err + assert "Failed to regenerate" in output + assert constants.Bun.ROOT_LOCKFILE_DIR in output + + @pytest.mark.usefixtures("install_packages_env") def test_run_initial_install_other_error_replays_logs(monkeypatch, capsys): """Non-frozen-lockfile failures replay the captured logs."""