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
5 changes: 5 additions & 0 deletions docs/wrapping-react/local-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ Some important notes regarding this approach:
- The repo or archive must contain a `package.json` file.
- `prepare` or `build` scripts will NOT be executed. The distribution archive,
directory, or repo must already contain the built javascript files (this is common).
- When pointing at a local _directory_, the package manager links the files in
place instead of copying them, so the bundler resolves that package's own
runtime dependencies from the directory itself. Run `npm install` (or `bun
install`) inside the local package directory, or reference a packed archive
created with `npm pack` instead.

````md alert
# Ensure CSS files are exported in `package.json`
Expand Down
1 change: 1 addition & 0 deletions news/7117.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed local package specifiers such as `@masenf/hello-react@../hello-react` and `@masenf/hello-react@../hello-react.tgz` being truncated at the first slash (to `@masenf/hello-react@..`) before reaching the package manager, so wrapping a React package from a local directory or archive now installs correctly.
29 changes: 23 additions & 6 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,25 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
await self.app(scope, receive, send)


def _is_location_specifier(specifier: str) -> bool:
"""Check whether a dependency specifier points at a location.

A location specifier names where a package comes from (a local path, a
protocol like ``file:``/``github:``/``git+ssh:``, or a git ref) instead of
naming a version. Its slashes belong to the location, so it must be kept
whole rather than split into a version and a package subpath.

Args:
specifier: The part of an import name following ``package@``.

Returns:
Whether the specifier is a location rather than a version or dist-tag.
"""
return (
":" in specifier or "#" in specifier or specifier.startswith((".", "/", "~/"))
)


@dataclasses.dataclass()
class App(MiddlewareMixin, LifespanMixin):
"""The main Reflex app that encapsulates the backend and frontend.
Expand Down Expand Up @@ -1530,13 +1549,11 @@ def _get_frontend_package_name(import_name: str) -> str | None:
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:
specifier = import_name[len(library_name) + 1 :]
version, slash, _ = specifier.partition("/")
if slash and not _is_location_specifier(specifier):
return f"{package_name}@{version}"
if package_name == library_name:
return import_name
return f"{package_name}@{version_and_maybe_subpath}"
return f"{package_name}@{specifier}"

if package_name == library_name:
return import_name
Expand Down
75 changes: 75 additions & 0 deletions tests/units/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3264,6 +3264,81 @@ def test_get_frontend_packages_maps_versioned_subpath_imports_to_pinned_base(
assert "@scope/pkg@2.0.0/subpath" not in install_set


def test_get_frontend_packages_keeps_local_and_git_specifiers_intact(
mocker: MockerFixture,
):
"""Location specifiers must reach the package manager unmodified.

Local paths, ``file:`` URLs and git references contain slashes that are
part of the location, not a package subpath, so they must not be
truncated at the first slash (reflex-dev/reflex#7117).
"""
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"
)

specifiers = [
"@masenf/hello-react@../hello-react",
"@masenf/hello-react@../hello-react.tgz",
"@masenf/hello-react@./vendor/hello-react",
"@masenf/hello-react@/opt/hello-react",
"@masenf/hello-react@~/hello-react",
"@masenf/hello-react@file:../hello-react",
"@masenf/hello-react@github:masenf/hello-react",
"@masenf/hello-react@masenf/hello-react#main",
"local-pkg@../local-pkg",
]

app = App(theme=None)
app._get_frontend_packages({
specifier: {ImportVar(tag="Counter")} for specifier in specifiers
})

install_set, _ = install_frontend_packages.call_args.args
assert install_set == set(specifiers)


def test_get_frontend_packages_maps_subpath_of_local_package_to_its_specifier(
mocker: MockerFixture,
):
"""A subpath import of a locally sourced package installs the local package once."""
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({
"@masenf/hello-react@../hello-react": {ImportVar(tag="Counter")},
"@masenf/hello-react/dist/style.css": {ImportVar(tag="")},
})

install_set, _ = install_frontend_packages.call_args.args
assert install_set == {"@masenf/hello-react@../hello-react"}


def test_get_frontend_packages_maps_scoped_subpath_import_of_local_package(
mocker: MockerFixture,
):
"""A library subpath pinned to a local path installs the base 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({
"@scope/pkg/subpath@../pkg": {ImportVar(tag="Widget")},
})

install_set, _ = install_frontend_packages.call_args.args
assert install_set == {"@scope/pkg@../pkg"}


def test_app_state_determination():
"""Test that the stateless status of an app is determined correctly."""
a1 = App()
Expand Down
Loading