diff --git a/reflex/app.py b/reflex/app.py
index 50000e507..c52f0ef8a 100644
--- a/reflex/app.py
+++ b/reflex/app.py
@@ -729,9 +729,12 @@ class App(Base):
for render, kwargs in DECORATED_PAGES:
self.add_page(render, **kwargs)
- def compile_(self):
+ def compile_(self, export: bool = False):
"""Compile the app and output it to the pages folder.
+ Args:
+ export: Whether to compile the app for export.
+
Raises:
RuntimeError: When any page uses state, but no rx.State subclass is defined.
"""
@@ -937,6 +940,17 @@ class App(Base):
# Install frontend packages.
self.get_frontend_packages(all_imports)
+ # Setup the next.config.js
+ transpile_packages = [
+ package
+ for package, import_vars in all_imports.items()
+ if any(import_var.transpile for import_var in import_vars)
+ ]
+ prerequisites.update_next_config(
+ export=export,
+ transpile_packages=transpile_packages,
+ )
+
for output_path, code in compile_results:
compiler_utils.write_page(output_path, code)
diff --git a/reflex/components/component.py b/reflex/components/component.py
index 057a55361..a940f4a86 100644
--- a/reflex/components/component.py
+++ b/reflex/components/component.py
@@ -64,6 +64,9 @@ class BaseComponent(Base, ABC):
# List here the non-react dependency needed by `library`
lib_dependencies: List[str] = []
+ # List here the dependencies that need to be transpiled by Next.js
+ transpile_packages: List[str] = []
+
# The tag to use when rendering the component.
tag: Optional[str] = None
@@ -987,6 +990,20 @@ class Component(BaseComponent, ABC):
if getattr(self, prop) is not None
]
+ def _should_transpile(self, dep: str | None) -> bool:
+ """Check if a dependency should be transpiled.
+
+ Args:
+ dep: The dependency to check.
+
+ Returns:
+ True if the dependency should be transpiled.
+ """
+ return (
+ dep in self.transpile_packages
+ or format.format_library_name(dep or "") in self.transpile_packages
+ )
+
def _get_dependencies_imports(self) -> imports.ImportDict:
"""Get the imports from lib_dependencies for installing.
@@ -994,7 +1011,14 @@ class Component(BaseComponent, ABC):
The dependencies imports of the component.
"""
return {
- dep: [ImportVar(tag=None, render=False)] for dep in self.lib_dependencies
+ dep: [
+ ImportVar(
+ tag=None,
+ render=False,
+ transpile=self._should_transpile(dep),
+ )
+ ]
+ for dep in self.lib_dependencies
}
def _get_hooks_imports(self) -> imports.ImportDict:
@@ -1250,7 +1274,12 @@ class Component(BaseComponent, ABC):
# If the tag is dot-qualified, only import the left-most name.
tag = self.tag.partition(".")[0] if self.tag else None
alias = self.alias.partition(".")[0] if self.alias else None
- return ImportVar(tag=tag, is_default=self.is_default, alias=alias)
+ return ImportVar(
+ tag=tag,
+ is_default=self.is_default,
+ alias=alias,
+ transpile=self._should_transpile(self.library),
+ )
@staticmethod
def _get_app_wrap_components() -> dict[tuple[int, str], Component]:
@@ -1514,7 +1543,13 @@ class NoSSRComponent(Component):
# Do NOT import the main library/tag statically.
if self.library is not None:
- _imports[self.library] = [imports.ImportVar(tag=None, render=False)]
+ _imports[self.library] = [
+ imports.ImportVar(
+ tag=None,
+ render=False,
+ transpile=self._should_transpile(self.library),
+ ),
+ ]
return imports.merge_imports(
dynamic_import,
@@ -1529,10 +1564,7 @@ class NoSSRComponent(Component):
if self.library is None:
raise ValueError("Undefined library for NoSSRComponent")
- import_name_parts = [p for p in self.library.rpartition("@") if p != ""]
- import_name = (
- import_name_parts[0] if import_name_parts[0] != "@" else self.library
- )
+ import_name = format.format_library_name(self.library)
library_import = f"const {self.alias if self.alias else self.tag} = dynamic(() => import('{import_name}')"
mod_import = (
diff --git a/reflex/reflex.py b/reflex/reflex.py
index 754d96af9..d11dc6ab3 100644
--- a/reflex/reflex.py
+++ b/reflex/reflex.py
@@ -178,7 +178,6 @@ def _run(
prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
if frontend:
- prerequisites.update_next_config()
# Get the app module.
prerequisites.get_compiled_app()
diff --git a/reflex/utils/export.py b/reflex/utils/export.py
index 6deb77174..3116f4859 100644
--- a/reflex/utils/export.py
+++ b/reflex/utils/export.py
@@ -50,10 +50,8 @@ def export(
console.rule("[bold]Compiling production app and preparing for export.")
if frontend:
- # Update some parameters for export
- prerequisites.update_next_config(export=True)
# Ensure module can be imported and app.compile() is called.
- prerequisites.get_compiled_app()
+ prerequisites.get_compiled_app(export=True)
# Set up .web directory and install frontend dependencies.
build.setup_frontend(Path.cwd())
diff --git a/reflex/utils/imports.py b/reflex/utils/imports.py
index 66ac62213..8d678b4eb 100644
--- a/reflex/utils/imports.py
+++ b/reflex/utils/imports.py
@@ -54,6 +54,10 @@ class ImportVar(Base):
# whether this import should be rendered or not
render: Optional[bool] = True
+ # whether this import package should be added to transpilePackages in next.config.js
+ # https://nextjs.org/docs/app/api-reference/next-config-js/transpilePackages
+ transpile: Optional[bool] = False
+
@property
def name(self) -> str:
"""The name of the import.
@@ -72,7 +76,16 @@ class ImportVar(Base):
Returns:
The hash of the var.
"""
- return hash((self.tag, self.is_default, self.alias, self.install, self.render))
+ return hash(
+ (
+ self.tag,
+ self.is_default,
+ self.alias,
+ self.install,
+ self.render,
+ self.transpile,
+ )
+ )
ImportDict = Dict[str, List[ImportVar]]
diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py
index 399724148..21c25c031 100644
--- a/reflex/utils/prerequisites.py
+++ b/reflex/utils/prerequisites.py
@@ -19,7 +19,7 @@ from datetime import datetime
from fileinput import FileInput
from pathlib import Path
from types import ModuleType
-from typing import Callable, Optional
+from typing import Callable, List, Optional
import httpx
import pkg_resources
@@ -35,6 +35,7 @@ from reflex.base import Base
from reflex.compiler import templates
from reflex.config import Config, get_config
from reflex.utils import console, path_ops, processes
+from reflex.utils.format import format_library_name
CURRENTLY_INSTALLING_NODE = False
@@ -227,11 +228,12 @@ def get_app(reload: bool = False) -> ModuleType:
return app
-def get_compiled_app(reload: bool = False) -> ModuleType:
+def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType:
"""Get the app module based on the default config after first compiling it.
Args:
reload: Re-import the app module from disk
+ export: Compile the app for export
Returns:
The compiled app based on the default config.
@@ -241,7 +243,7 @@ def get_compiled_app(reload: bool = False) -> ModuleType:
# For py3.8 and py3.9 compatibility when redis is used, we MUST add any decorator pages
# before compiling the app in a thread to avoid event loop error (REF-2172).
app._apply_decorated_pages()
- app.compile_()
+ app.compile_(export=export)
return app_module
@@ -562,28 +564,37 @@ def init_reflex_json(project_hash: int | None):
path_ops.update_json_file(constants.Reflex.JSON, reflex_json)
-def update_next_config(export=False):
+def update_next_config(export=False, transpile_packages: Optional[List[str]] = None):
"""Update Next.js config from Reflex config.
Args:
export: if the method run during reflex export.
+ transpile_packages: list of packages to transpile via next.config.js.
"""
next_config_file = os.path.join(constants.Dirs.WEB, constants.Next.CONFIG_FILE)
- next_config = _update_next_config(get_config(), export=export)
+ next_config = _update_next_config(
+ get_config(), export=export, transpile_packages=transpile_packages
+ )
with open(next_config_file, "w") as file:
file.write(next_config)
file.write("\n")
-def _update_next_config(config, export=False):
+def _update_next_config(
+ config: Config, export: bool = False, transpile_packages: Optional[List[str]] = None
+):
next_config = {
"basePath": config.frontend_path or "",
"compress": config.next_compression,
"reactStrictMode": True,
"trailingSlash": True,
}
+ if transpile_packages:
+ next_config["transpilePackages"] = list(
+ set((format_library_name(p) for p in transpile_packages))
+ )
if export:
next_config["output"] = "export"
next_config["distDir"] = constants.Dirs.STATIC
diff --git a/tests/test_app.py b/tests/test_app.py
index 9cd0cfb50..1e1df01bf 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -1,7 +1,9 @@
from __future__ import annotations
import io
+import json
import os.path
+import re
import unittest.mock
import uuid
from pathlib import Path
@@ -1444,3 +1446,55 @@ def test_add_page_component_returning_tuple():
)
assert isinstance((third_text := page2_fragment_wrapper.children[0]), Text)
assert str(third_text.children[0].contents) == "{`third`}" # type: ignore
+
+
+@pytest.mark.parametrize("export", (True, False))
+def test_app_with_transpile_packages(compilable_app, export):
+ class C1(rx.Component):
+ library = "foo@1.2.3"
+ tag = "Foo"
+ transpile_packages: List[str] = ["foo"]
+
+ class C2(rx.Component):
+ library = "bar@4.5.6"
+ tag = "Bar"
+ transpile_packages: List[str] = ["bar@4.5.6"]
+
+ class C3(rx.NoSSRComponent):
+ library = "baz@7.8.10"
+ tag = "Baz"
+ transpile_packages: List[str] = ["baz@7.8.9"]
+
+ class C4(rx.NoSSRComponent):
+ library = "quuc@2.3.4"
+ tag = "Quuc"
+ transpile_packages: List[str] = ["quuc"]
+
+ class C5(rx.Component):
+ library = "quuc"
+ tag = "Quuc"
+
+ app, web_dir = compilable_app
+ page = Fragment.create(
+ C1.create(), C2.create(), C3.create(), C4.create(), C5.create()
+ )
+ app.add_page(page, route="/")
+ app.compile_(export=export)
+
+ next_config = (web_dir / "next.config.js").read_text()
+ transpile_packages_match = re.search(r"transpilePackages: (\[.*?\])", next_config)
+ transpile_packages_json = transpile_packages_match.group(1) # type: ignore
+ transpile_packages = sorted(json.loads(transpile_packages_json))
+
+ assert transpile_packages == [
+ "bar",
+ "foo",
+ "quuc",
+ ]
+
+ if export:
+ assert 'output: "export"' in next_config
+ assert f'distDir: "{constants.Dirs.STATIC}"' in next_config
+ else:
+ assert 'output: "export"' not in next_config
+ assert f'distDir: "{constants.Dirs.STATIC}"' not in next_config
diff --git a/tests/test_prerequisites.py b/tests/test_prerequisites.py
index 711826cbc..28608c48c 100644
--- a/tests/test_prerequisites.py
+++ b/tests/test_prerequisites.py
@@ -1,3 +1,5 @@
+import json
+import re
import tempfile
from unittest.mock import Mock, mock_open
@@ -61,6 +63,30 @@ def test_update_next_config(config, export, expected_output):
assert output == expected_output
+@pytest.mark.parametrize(
+ ("transpile_packages", "expected_transpile_packages"),
+ (
+ (
+ ["foo", "@bar/baz"],
+ ["@bar/baz", "foo"],
+ ),
+ (
+ ["foo", "@bar/baz", "foo", "@bar/baz@3.2.1"],
+ ["@bar/baz", "foo"],
+ ),
+ ),
+)
+def test_transpile_packages(transpile_packages, expected_transpile_packages):
+ output = _update_next_config(
+ Config(app_name="test"),
+ transpile_packages=transpile_packages,
+ )
+ transpile_packages_match = re.search(r"transpilePackages: (\[.*?\])", output)
+ transpile_packages_json = transpile_packages_match.group(1) # type: ignore
+ actual_transpile_packages = sorted(json.loads(transpile_packages_json))
+ assert actual_transpile_packages == expected_transpile_packages
+
+
def test_initialize_requirements_txt_no_op(mocker):
# File exists, reflex is included, do nothing
mocker.patch("pathlib.Path.exists", return_value=True)
diff --git a/tests/test_var.py b/tests/test_var.py
index 61286dc83..68271d2ba 100644
--- a/tests/test_var.py
+++ b/tests/test_var.py
@@ -836,7 +836,7 @@ def test_state_with_initial_computed_var(
(f"{BaseVar(_var_name='var', _var_type=str)}", "${var}"),
(
f"testing f-string with {BaseVar(_var_name='myvar', _var_type=int)._var_set_state('state')}",
- 'testing f-string with ${"state": "state", "interpolations": [], "imports": {"/utils/context": [{"tag": "StateContexts", "is_default": false, "alias": null, "install": true, "render": true}], "react": [{"tag": "useContext", "is_default": false, "alias": null, "install": true, "render": true}]}, "hooks": {"const state = useContext(StateContexts.state)": null}, "string_length": 13}{state.myvar}',
+ 'testing f-string with ${"state": "state", "interpolations": [], "imports": {"/utils/context": [{"tag": "StateContexts", "is_default": false, "alias": null, "install": true, "render": true, "transpile": false}], "react": [{"tag": "useContext", "is_default": false, "alias": null, "install": true, "render": true, "transpile": false}]}, "hooks": {"const state = useContext(StateContexts.state)": null}, "string_length": 13}{state.myvar}',
),
(
f"testing local f-string {BaseVar(_var_name='x', _var_is_local=True, _var_type=str)}",