Skip to content

feat(compiler): mirror memo output paths to Python source modules#6457

Open
FarhanAliRaza wants to merge 16 commits into
reflex-dev:mainfrom
FarhanAliRaza:memoize-file-path-mirror
Open

feat(compiler): mirror memo output paths to Python source modules#6457
FarhanAliRaza wants to merge 16 commits into
reflex-dev:mainfrom
FarhanAliRaza:memoize-file-path-mirror

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented May 5, 2026

Copy link
Copy Markdown
Contributor

Memos now compile into a single JSX file per user module at a path that mirrors the module's dotted name, instead of one file per memo under . The page-side import surface matches the source
layout, which makes debugging easier and lets Vite group co-defined memos in the same chunk.

Memos without a captured source module keep the legacy per-name files and index. A manifest in records emitted
paths so stale files from previous compiles get pruned.

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

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

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?

closes #6218
fixes ENG-9150

Memos now compile into a single JSX file per user module at a path that
mirrors the module's dotted name, instead of one file per memo under
. The page-side import surface matches the source
layout, which makes debugging easier and lets Vite group co-defined
memos in the same chunk.

Memos without a captured source module keep the legacy per-name files
and  index. A manifest in  records emitted
paths so stale files from previous compiles get pruned.
@FarhanAliRaza FarhanAliRaza requested a review from a team as a code owner May 5, 2026 09:56
@codspeed-hq

codspeed-hq Bot commented May 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing FarhanAliRaza:memoize-file-path-mirror (60f98a7) with main (b288bdc)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@greptile-apps

greptile-apps Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR changes how auto-memoized (rx.memo) components are compiled by grouping them into one JSX file per user Python module instead of one file per memo under a flat utils/components/ directory. Output paths now mirror the Python module hierarchy under a new reserved app_components/ subdirectory, and a manifest-driven cleanup mechanism prunes stale files between compiles.

  • New mirrored output paths: Memos with a captured source module land in app_components/<module/path>.jsx; memos that can't be mirrored (framework code, __main__, unsafe names) get per-name files in app_components/_internal/<name>.jsx.
  • Hot-reload correctness: reset_memo_component_classes() clears the cached library on each compile cycle; module_to_mirrored_segments now checks the live sys.modules entry first instead of a process-lifetime-cached find_spec result, so a module switching between regular and package form across saves resolves correctly.
  • Cross-module deduplication fix: auto_memo_components is now keyed by (tag, source_module) instead of just tag, preventing identical-rendering subtrees from different modules from silently overwriting each other's registry entry.

Confidence Score: 5/5

Safe to merge. The mirrored-output path change is well-contained, the manifest-based stale-file cleanup handles both absolute and relative .web directories, and the hot-reload correctness fix directly addresses the scenarios previously observed in local testing.

All three concrete bugs surfaced in prior review rounds are resolved: the relative-path double-prefix in prune_stale_memo_files is gone, the mkstemp fd leak is fixed by closing immediately and reopening by path, and the process-lifetime cached find_spec is replaced with a live sys.modules check. The (tag, source_module) registry key closes the silent-overwrite regression for identical subtrees across modules. Dedicated tests cover the relative-web-dir edge case and the cross-module deduplication scenario.

No files require special attention.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/utils/memo_paths.py New module providing module-to-path translation helpers; handles framework filtering, unsafe segment validation, Windows reserved names, and package detection via live sys.modules lookup. The previously-flagged @functools.cache staleness issue is absent — module_to_mirrored_segments correctly consults sys.modules first before falling back to find_spec.
reflex/compiler/compiler.py _compile_memo_components rewritten to group memos by mirrored segments; adds collision detection (case-insensitive filesystem check + reserved _internal directory guard), strips self-imports, and emits combined JSX per group. reset_memo_component_classes() called at compile start for hot-reload correctness.
reflex/compiler/utils.py Adds get_memo_components_dir, get_memo_module_path, and the manifest-driven prune_stale_memo_files. The previously-flagged relative-path double-prefixing bug is fixed (Path(path).relative_to(web_dir) used directly), and the fd-leak on mkstemp is fixed by closing the raw fd immediately before reopening by path.
packages/reflex-base/src/reflex_base/components/memo.py source_module field added to MemoDefinition (kw_only, Python 3.10+ safe), propagated through all creation paths; _get_memo_component_class gains cache_clear via reset_memo_component_classes; library attribute correctly computed from memo_paths.library_for at class-creation time.
reflex/app.py add_page captures source_module via capture_source_module for callables or resolve_user_module_from_frame(skip=1) for Component instances; stored on UnevaluatedPage._source_module and propagated through merged_with correctly.
packages/reflex-base/src/reflex_base/plugins/compiler.py auto_memo_components dict rekeyed from str to (str, str
tests/units/compiler/test_stale_cleanup.py New test file covering manifest read/write, pruning, directory cleanup, and the relative-web-dir edge case that caused the double-prefixing bug in the old implementation.
packages/reflex-base/src/reflex_base/utils/imports.py ABSOLUTE_IMPORT_PREFIXES constant extracted from inline tuple and extended with /app_components/ so paths from the new mirrored directory receive the $-prefix treatment.

Reviews (4): Last reviewed commit: "chore: regenerate memo.pyi stub hash" | Re-trigger Greptile

Comment thread packages/reflex-base/src/reflex_base/utils/memo_paths.py Outdated
Comment thread reflex/utils/memo_paths.py Outdated
Comment thread reflex/compiler/utils.py
Identical memoizable subtrees on pages from different user modules
produce the same wrapper tag. The auto-memo registry was keyed by tag
alone, so the second registration overwrote the first — only one of
the source modules got a mirrored memo file emitted, and the other
page imported the tag from a JSX file that never declared it. Vite
failed the prod build with MISSING_EXPORT.

Key the registry by (tag, source_module) so each module's mirrored
file gets its own definition, and add an integration test that builds
two pages from distinct user modules sharing a memoizable subtree.
 was d, so once a
module had been resolved its mirrored path was frozen for the
process. A user toggling a module between a regular  and a
package () during dev reload kept the original origin
and emitted memo files to the stale path.

Drop the cache and read  from  first, falling
back to  only when the module isn't loaded —
is rebound on reload, while a cached spec wouldn't be. Also tighten
 to close the mkstemp fd up front so it can't
leak if reopening raises, and re-export  from
 for parity with the rest of the surface.
Memo components now mirror to their source module's combined file, so
the self-referencing memo test can no longer find a per-name
RecursiveBox.jsx path. Join all emitted code and assert on content.
Auto-memoized (rx.memo) components now compile to .web paths mirroring
their defining Python module instead of a shared components.jsx, scoping
the memo registry per module so same-named components no longer collide.
Adds reflex_base.utils.memo_paths to translate source modules into the
mirrored JSX path and $/... specifier.
Comment thread reflex/utils/memo_paths.py Outdated
Comment thread tests/units/utils/test_memo_paths.py Outdated
masenf added 5 commits June 12, 2026 16:49
This is net-new functionality, we don't need to provide compat shims
use more durable module/package names that are less likely to change in the future
1. the import was weird, why not just import directly?
2. the canonical location for _get_memo_component_class has moved to reflex_base
Comment thread reflex/compiler/utils.py

@masenf masenf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to put the modules under a subdir of .web that is not already used, maybe .web/app_components.

The problem arises when I have an app called app and a module called app.root which exports a memo component... this ends up getting overwritten by the app/root.jsx that the framework emits and then the page breaks when you try to load it.

The user's file tree hierarchy really shouldn't be able to break the app, hence keeping it under a subdirectory that is only used for emitting memo components would protect the rest of the app from unexpected changes.

Mirrored memos could compile to paths that overwrite framework output
(a memo in module `app.root` would clobber `.web/app/root.jsx`), and
un-mirrorable memos shared `.web/utils/components`. Move all memo output
under a reserved `app_components/` tree — mirrored memos at
`app_components/<segments>`, un-mirrorable ones at
`app_components/_internal/<name>` — so user module paths can never
collide with framework files.

Add collision detection (reserved internal dir, case-insensitive path
clashes) and reject Windows reserved device names so mirroring fails
loudly instead of silently overwriting. Reset the memo wrapper class
cache each compile so a module flipping to a package across hot reloads
re-resolves its library. Move stale-file pruning after the dry-run
return so `--dry` never mutates `.web` or the manifest.
The is-absolute guard double-prefixed emitted paths to `.web/.web/...`
when get_web_dir() returns the default relative path, so emitted keys
never matched the manifest and live files were wrongly pruned. Emitted
paths already share the web_dir prefix, so strip it directly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compiler: Track provenance of rx.memo components and output to mirrored Python paths

2 participants