From 19ddc25e28e3a6d5d11774d68b023889ad9dc36e Mon Sep 17 00:00:00 2001 From: Benjamin Barrera-Altuna Date: Thu, 23 Apr 2026 23:59:42 -0400 Subject: [PATCH 1/3] fix(app): install base npm package for subpath imports --- reflex/app.py | 48 +++++++++++++++++++++++++++++++++++------ tests/units/test_app.py | 27 +++++++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index bdbb90bfe40..1f75eafc099 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1009,12 +1009,11 @@ def _get_frontend_packages(self, imports: dict[str, set[ImportVar]]): dependencies = constants.PackageJson.DEPENDENCIES dev_dependencies = constants.PackageJson.DEV_DEPENDENCIES page_imports = { - i - for i, tags in imports.items() - if i not in dependencies - and i not in dev_dependencies - and not any(i.startswith(prefix) for prefix in ["/", "$/", "."]) - and i != "" + package_name + for import_name, tags in imports.items() + if (package_name := self._get_frontend_package_name(import_name)) + and package_name not in dependencies + and package_name not in dev_dependencies and any(tag.install for tag in tags) } pinned = {i.rpartition("@")[0] for i in page_imports if "@" in i} @@ -1032,6 +1031,43 @@ def _get_frontend_packages(self, imports: dict[str, set[ImportVar]]): page_imports.update(filtered_frontend_packages) js_runtimes.install_frontend_packages(page_imports, get_config()) + @staticmethod + def _get_frontend_package_name(import_name: str) -> str | None: + """Resolve the npm package name to install for a library import path. + + Args: + import_name: The import path key used in component imports. + + Returns: + The package name that should be installed, including pinned version when + available, or None when the import does not represent an installable npm package. + """ + if import_name == "" or any( + import_name.startswith(prefix) for prefix in ("/", "$/", ".") + ): + return None + + library_name = format.format_library_name(import_name) + if library_name.startswith("@"): + scope, slash, package_and_path = library_name.partition("/") + package_name = ( + f"{scope}/{package_and_path.split('/', maxsplit=1)[0]}" + if slash and package_and_path + else library_name + ) + else: + package_name = library_name.split("/", maxsplit=1)[0] + + if package_name == library_name: + return import_name + + version = ( + import_name[len(library_name) + 1 :] + if import_name.startswith(f"{library_name}@") + else "" + ) + return f"{package_name}@{version}" if version else package_name + def _app_root(self, app_wrappers: dict[tuple[int, str], Component]) -> Component: for component in tuple(app_wrappers.values()): app_wrappers.update(component._get_all_app_wrap_components()) diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 2a3eb5556f4..e930268b1a7 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -24,6 +24,7 @@ from reflex_base.registry import RegistrationContext from reflex_base.style import Style from reflex_base.utils import console, exceptions, format +from reflex_base.utils.imports import ImportVar from reflex_base.vars.base import computed_var from reflex_components_core.base.bare import Bare from reflex_components_core.base.fragment import Fragment @@ -2239,6 +2240,32 @@ def page(): assert expected.split(",") == function_app_definition.split(",") +def test_get_frontend_packages_maps_subpath_imports_to_installable_package_names( + mocker: MockerFixture, +): + """Subpath imports should install the base npm package.""" + conf = rx.Config(app_name="testing") + mocker.patch("reflex.app.get_config", return_value=conf) + install_frontend_packages = mocker.patch( + "reflex.app.js_runtimes.install_frontend_packages" + ) + + app = App(theme=None) + app._get_frontend_packages( + { + "react-map-gl/maplibre": {ImportVar(tag="Map")}, + "@scope/pkg/subpath": {ImportVar(tag="Widget")}, + "react": {ImportVar(tag="useEffect")}, + } + ) + + install_set, _ = install_frontend_packages.call_args.args + assert "react-map-gl" in install_set + assert "@scope/pkg" in install_set + assert "react-map-gl/maplibre" not in install_set + assert "@scope/pkg/subpath" not in install_set + + def test_app_state_determination(): """Test that the stateless status of an app is determined correctly.""" a1 = App() From 75165166b4fcb6f4b3d5a9fc8ae07a7898096b51 Mon Sep 17 00:00:00 2001 From: Benjamin Barrera-Altuna Date: Fri, 24 Apr 2026 00:52:52 -0400 Subject: [PATCH 2/3] fix(app): handle https and versioned subpath imports --- reflex/app.py | 19 ++++++++++------- tests/units/test_app.py | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index 1f75eafc099..26518d0df02 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -1046,6 +1046,8 @@ def _get_frontend_package_name(import_name: str) -> str | None: import_name.startswith(prefix) for prefix in ("/", "$/", ".") ): return None + if import_name.startswith(("https://", "http://")): + return import_name library_name = format.format_library_name(import_name) if library_name.startswith("@"): @@ -1058,15 +1060,18 @@ def _get_frontend_package_name(import_name: str) -> str | None: else: package_name = library_name.split("/", maxsplit=1)[0] + if import_name.startswith(f"{library_name}@"): + version_and_maybe_subpath = import_name[len(library_name) + 1 :] + version, slash, _ = version_and_maybe_subpath.partition("/") + if slash and ":" not in version: + return f"{package_name}@{version}" + if package_name == library_name: + return import_name + return f"{package_name}@{version_and_maybe_subpath}" + if package_name == library_name: return import_name - - version = ( - import_name[len(library_name) + 1 :] - if import_name.startswith(f"{library_name}@") - else "" - ) - return f"{package_name}@{version}" if version else package_name + return package_name def _app_root(self, app_wrappers: dict[tuple[int, str], Component]) -> Component: for component in tuple(app_wrappers.values()): diff --git a/tests/units/test_app.py b/tests/units/test_app.py index e930268b1a7..814dd266760 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -2266,6 +2266,51 @@ def test_get_frontend_packages_maps_subpath_imports_to_installable_package_names assert "@scope/pkg/subpath" not in install_set +def test_get_frontend_packages_keeps_https_imports_unchanged( + mocker: MockerFixture, +): + """URL-based imports should be passed through unchanged.""" + conf = rx.Config(app_name="testing") + mocker.patch("reflex.app.get_config", return_value=conf) + install_frontend_packages = mocker.patch( + "reflex.app.js_runtimes.install_frontend_packages" + ) + + app = App(theme=None) + app._get_frontend_packages( + {"https://cdn.skypack.dev/some-lib": {ImportVar(tag="SomeTag")}} + ) + + install_set, _ = install_frontend_packages.call_args.args + assert "https://cdn.skypack.dev/some-lib" in install_set + assert "https:" not in install_set + + +def test_get_frontend_packages_maps_versioned_subpath_imports_to_pinned_base( + mocker: MockerFixture, +): + """Versioned subpath imports should install the base package with its version.""" + conf = rx.Config(app_name="testing") + mocker.patch("reflex.app.get_config", return_value=conf) + install_frontend_packages = mocker.patch( + "reflex.app.js_runtimes.install_frontend_packages" + ) + + app = App(theme=None) + app._get_frontend_packages( + { + "react-map-gl@1.0.0/maplibre": {ImportVar(tag="Map")}, + "@scope/pkg@2.0.0/subpath": {ImportVar(tag="Widget")}, + } + ) + + install_set, _ = install_frontend_packages.call_args.args + assert "react-map-gl@1.0.0" in install_set + assert "@scope/pkg@2.0.0" in install_set + assert "react-map-gl@1.0.0/maplibre" not in install_set + assert "@scope/pkg@2.0.0/subpath" not in install_set + + def test_app_state_determination(): """Test that the stateless status of an app is determined correctly.""" a1 = App() From e14df6661ce9f112ed77562236099f0794dc9ac5 Mon Sep 17 00:00:00 2001 From: Benjamin Barrera-Altuna Date: Tue, 28 Apr 2026 22:29:44 -0400 Subject: [PATCH 3/3] test(app): apply ruff formatting for pre-commit --- tests/units/test_app.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 814dd266760..14041c77bcd 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -2251,13 +2251,11 @@ def test_get_frontend_packages_maps_subpath_imports_to_installable_package_names ) app = App(theme=None) - app._get_frontend_packages( - { - "react-map-gl/maplibre": {ImportVar(tag="Map")}, - "@scope/pkg/subpath": {ImportVar(tag="Widget")}, - "react": {ImportVar(tag="useEffect")}, - } - ) + app._get_frontend_packages({ + "react-map-gl/maplibre": {ImportVar(tag="Map")}, + "@scope/pkg/subpath": {ImportVar(tag="Widget")}, + "react": {ImportVar(tag="useEffect")}, + }) install_set, _ = install_frontend_packages.call_args.args assert "react-map-gl" in install_set @@ -2277,9 +2275,9 @@ def test_get_frontend_packages_keeps_https_imports_unchanged( ) app = App(theme=None) - app._get_frontend_packages( - {"https://cdn.skypack.dev/some-lib": {ImportVar(tag="SomeTag")}} - ) + app._get_frontend_packages({ + "https://cdn.skypack.dev/some-lib": {ImportVar(tag="SomeTag")} + }) install_set, _ = install_frontend_packages.call_args.args assert "https://cdn.skypack.dev/some-lib" in install_set @@ -2297,12 +2295,10 @@ def test_get_frontend_packages_maps_versioned_subpath_imports_to_pinned_base( ) app = App(theme=None) - app._get_frontend_packages( - { - "react-map-gl@1.0.0/maplibre": {ImportVar(tag="Map")}, - "@scope/pkg@2.0.0/subpath": {ImportVar(tag="Widget")}, - } - ) + app._get_frontend_packages({ + "react-map-gl@1.0.0/maplibre": {ImportVar(tag="Map")}, + "@scope/pkg@2.0.0/subpath": {ImportVar(tag="Widget")}, + }) install_set, _ = install_frontend_packages.call_args.args assert "react-map-gl@1.0.0" in install_set