Do not let a leftover nocompile marker skip the next CLI compile

YU
3h ago
+65-0

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Description

Closes #7825

A dev reflex run touches .web/nocompile in run_backend so that its backend worker skips a second compile, and only that worker removes it (App._should_compile, reached from App.__call__). If the run stops before the worker has loaded the app, the marker stays. The next reflex run, reflex export or reflex compile then consumes it, skips the compile, and still exits 0 with the previous pages.

This is the change proposed in the issue: get_compiled_app removes the marker before it compiles. Every CLI compile goes through that function and the backend worker does not, so a normal dev run still compiles once: its worker consumes the marker of its own run and skips, as before. It follows the rule the hot-reload marker already uses (reset_dev_backend_reload_marker clears it at the start of a session) rather than relying on the previous run to clean up.

The marker is also removed on reflex compile --dry. That is not new: _should_compile deletes it on a dry run today. What changes is that a dry run with a leftover marker now does the full compile instead of only evaluating the pages.

Reproduction

In an app that has run once, a script sends SIGINT to reflex run as soon as .web/nocompile exists, edits a heading, and runs reflex compile, then does the same with reflex export --frontend-only --no-zip:

# main (99cf380e)
after interrupted run: .web/nocompile exists = True
reflex compile exit code: 0
'Compiling:' progress lines printed: 0
edited heading present in .web/app/routes/_index.jsx: False

after interrupted run: .web/nocompile exists = True
reflex export --frontend-only --no-zip exit code: 0
'Compiling:' progress lines printed: 0
edited heading present in .web/build/client: False

# this branch
after interrupted run: .web/nocompile exists = True
reflex compile exit code: 0
'Compiling:' progress lines printed: 1
edited heading present in .web/app/routes/_index.jsx: True

after interrupted run: .web/nocompile exists = True
reflex export --frontend-only --no-zip exit code: 0
'Compiling:' progress lines printed: 1
edited heading present in .web/build/client: True

Tests

test_get_compiled_app_ignores_stale_nocompile_marker in tests/units/test_prerequisites.py (the existing file for reflex/utils/prerequisites.py). It writes a real rxconfig.py and app module into a temp directory, creates .web/nocompile, calls prerequisites.get_compiled_app and checks that compile_app returned True (a real compile, not the evaluate-only path), that the marker is gone, and that the index page file exists exactly when it is not a dry run. It is parametrised over dry_run. Only the frontend package install is stubbed. Both cases fail on unchanged main with assert False is True.

Commands run locally in the uv sync environment (macOS arm64, Python 3.14.6):

  • pytest tests/units/test_prerequisites.py -k stale_nocompile: 2 passed (2 failed before the change)
  • pytest tests/units --cov --no-cov-on-fail --cov-report=: 15088 passed, 41 skipped, 4 failed. The 4 failures (test_app.py::test_compile_sends_telemetry_when_enabled, test_compile_reports_exception_and_reraises, test_compile_event_features_used_match_populated_app, test_compile_event_features_used_stable_across_hot_reload) come from REFLEX_TELEMETRY_ENABLED=false in the shell; they pass when run with the variable unset.
  • uv run pre-commit run --files <changed files>: ruff format, ruff check, codespell, update-pyi-files, pyright, ty and both pyright type_checking hooks passed (biome format skipped, no JS changed).
  • reflex-release changelog-check and check-headings: passed.
  • A real reflex run on the branch: one Compiling: line at startup and no .web/nocompile left once the backend was up.

Not run: integration tests, Windows, Python 3.11 to 3.13.

Found while testing this, and left out: tests/units/utils/test_migrations.py leaves REFLEX_SKIP_COMPILE set for the rest of the pytest session, which makes real-compile tests that run after it skip their compile. File order hides it today. The new test unsets the variable itself; the fixture is not touched here to keep this PR to one concern, and can go in a separate PR.

Changelog fragment: news/+stale-nocompile-marker.bugfix.md.