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
53 changes: 47 additions & 6 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -1032,6 +1031,48 @@ 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
if import_name.startswith(("https://", "http://")):
return import_name

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 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
Comment thread
masenf marked this conversation as resolved.
Comment on lines +1072 to +1073
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Versioned + subpath imports returned unchanged

When an import carries both a version pin and a subpath, e.g. react-map-gl@1.0.0/maplibre, format.format_library_name strips @1.0.0/maplibre entirely via rpartition("@"), yielding library_name = "react-map-gl". At that point package_name (also "react-map-gl") equals library_name, so the guard if package_name == library_name: return import_name fires and returns "react-map-gl@1.0.0/maplibre" — the original subpath-containing string — defeating the fix.

The condition is meant to short-circuit when there is no subpath to strip, but it does not account for format_library_name having already consumed the subpath as part of the version segment. Checking whether the original import_name contains a / after the version separator would close the gap.

return 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())
Expand Down
68 changes: 68 additions & 0 deletions tests/units/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -2239,6 +2240,73 @@ 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_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()
Expand Down
Loading