[REF-2392] Expose next.config.js transpilePackages key (#3006)
This commit is contained in:
parent
d7abcd45de
commit
1a11941577
@ -729,9 +729,12 @@ class App(Base):
|
|||||||
for render, kwargs in DECORATED_PAGES:
|
for render, kwargs in DECORATED_PAGES:
|
||||||
self.add_page(render, **kwargs)
|
self.add_page(render, **kwargs)
|
||||||
|
|
||||||
def compile_(self):
|
def compile_(self, export: bool = False):
|
||||||
"""Compile the app and output it to the pages folder.
|
"""Compile the app and output it to the pages folder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
export: Whether to compile the app for export.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
RuntimeError: When any page uses state, but no rx.State subclass is defined.
|
RuntimeError: When any page uses state, but no rx.State subclass is defined.
|
||||||
"""
|
"""
|
||||||
@ -937,6 +940,17 @@ class App(Base):
|
|||||||
# Install frontend packages.
|
# Install frontend packages.
|
||||||
self.get_frontend_packages(all_imports)
|
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:
|
for output_path, code in compile_results:
|
||||||
compiler_utils.write_page(output_path, code)
|
compiler_utils.write_page(output_path, code)
|
||||||
|
|
||||||
|
@ -64,6 +64,9 @@ class BaseComponent(Base, ABC):
|
|||||||
# List here the non-react dependency needed by `library`
|
# List here the non-react dependency needed by `library`
|
||||||
lib_dependencies: List[str] = []
|
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.
|
# The tag to use when rendering the component.
|
||||||
tag: Optional[str] = None
|
tag: Optional[str] = None
|
||||||
|
|
||||||
@ -987,6 +990,20 @@ class Component(BaseComponent, ABC):
|
|||||||
if getattr(self, prop) is not None
|
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:
|
def _get_dependencies_imports(self) -> imports.ImportDict:
|
||||||
"""Get the imports from lib_dependencies for installing.
|
"""Get the imports from lib_dependencies for installing.
|
||||||
|
|
||||||
@ -994,7 +1011,14 @@ class Component(BaseComponent, ABC):
|
|||||||
The dependencies imports of the component.
|
The dependencies imports of the component.
|
||||||
"""
|
"""
|
||||||
return {
|
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:
|
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.
|
# If the tag is dot-qualified, only import the left-most name.
|
||||||
tag = self.tag.partition(".")[0] if self.tag else None
|
tag = self.tag.partition(".")[0] if self.tag else None
|
||||||
alias = self.alias.partition(".")[0] if self.alias 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
|
@staticmethod
|
||||||
def _get_app_wrap_components() -> dict[tuple[int, str], Component]:
|
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.
|
# Do NOT import the main library/tag statically.
|
||||||
if self.library is not None:
|
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(
|
return imports.merge_imports(
|
||||||
dynamic_import,
|
dynamic_import,
|
||||||
@ -1529,10 +1564,7 @@ class NoSSRComponent(Component):
|
|||||||
if self.library is None:
|
if self.library is None:
|
||||||
raise ValueError("Undefined library for NoSSRComponent")
|
raise ValueError("Undefined library for NoSSRComponent")
|
||||||
|
|
||||||
import_name_parts = [p for p in self.library.rpartition("@") if p != ""]
|
import_name = format.format_library_name(self.library)
|
||||||
import_name = (
|
|
||||||
import_name_parts[0] if import_name_parts[0] != "@" else self.library
|
|
||||||
)
|
|
||||||
|
|
||||||
library_import = f"const {self.alias if self.alias else self.tag} = dynamic(() => import('{import_name}')"
|
library_import = f"const {self.alias if self.alias else self.tag} = dynamic(() => import('{import_name}')"
|
||||||
mod_import = (
|
mod_import = (
|
||||||
|
@ -178,7 +178,6 @@ def _run(
|
|||||||
prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
|
prerequisites.check_latest_package_version(constants.Reflex.MODULE_NAME)
|
||||||
|
|
||||||
if frontend:
|
if frontend:
|
||||||
prerequisites.update_next_config()
|
|
||||||
# Get the app module.
|
# Get the app module.
|
||||||
prerequisites.get_compiled_app()
|
prerequisites.get_compiled_app()
|
||||||
|
|
||||||
|
@ -50,10 +50,8 @@ def export(
|
|||||||
console.rule("[bold]Compiling production app and preparing for export.")
|
console.rule("[bold]Compiling production app and preparing for export.")
|
||||||
|
|
||||||
if frontend:
|
if frontend:
|
||||||
# Update some parameters for export
|
|
||||||
prerequisites.update_next_config(export=True)
|
|
||||||
# Ensure module can be imported and app.compile() is called.
|
# 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.
|
# Set up .web directory and install frontend dependencies.
|
||||||
build.setup_frontend(Path.cwd())
|
build.setup_frontend(Path.cwd())
|
||||||
|
|
||||||
|
@ -54,6 +54,10 @@ class ImportVar(Base):
|
|||||||
# whether this import should be rendered or not
|
# whether this import should be rendered or not
|
||||||
render: Optional[bool] = True
|
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
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""The name of the import.
|
"""The name of the import.
|
||||||
@ -72,7 +76,16 @@ class ImportVar(Base):
|
|||||||
Returns:
|
Returns:
|
||||||
The hash of the var.
|
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]]
|
ImportDict = Dict[str, List[ImportVar]]
|
||||||
|
@ -19,7 +19,7 @@ from datetime import datetime
|
|||||||
from fileinput import FileInput
|
from fileinput import FileInput
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Callable, Optional
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
@ -35,6 +35,7 @@ from reflex.base import Base
|
|||||||
from reflex.compiler import templates
|
from reflex.compiler import templates
|
||||||
from reflex.config import Config, get_config
|
from reflex.config import Config, get_config
|
||||||
from reflex.utils import console, path_ops, processes
|
from reflex.utils import console, path_ops, processes
|
||||||
|
from reflex.utils.format import format_library_name
|
||||||
|
|
||||||
CURRENTLY_INSTALLING_NODE = False
|
CURRENTLY_INSTALLING_NODE = False
|
||||||
|
|
||||||
@ -227,11 +228,12 @@ def get_app(reload: bool = False) -> ModuleType:
|
|||||||
return app
|
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.
|
"""Get the app module based on the default config after first compiling it.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
reload: Re-import the app module from disk
|
reload: Re-import the app module from disk
|
||||||
|
export: Compile the app for export
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The compiled app based on the default config.
|
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
|
# 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).
|
# before compiling the app in a thread to avoid event loop error (REF-2172).
|
||||||
app._apply_decorated_pages()
|
app._apply_decorated_pages()
|
||||||
app.compile_()
|
app.compile_(export=export)
|
||||||
return app_module
|
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)
|
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.
|
"""Update Next.js config from Reflex config.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
export: if the method run during reflex export.
|
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_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:
|
with open(next_config_file, "w") as file:
|
||||||
file.write(next_config)
|
file.write(next_config)
|
||||||
file.write("\n")
|
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 = {
|
next_config = {
|
||||||
"basePath": config.frontend_path or "",
|
"basePath": config.frontend_path or "",
|
||||||
"compress": config.next_compression,
|
"compress": config.next_compression,
|
||||||
"reactStrictMode": True,
|
"reactStrictMode": True,
|
||||||
"trailingSlash": True,
|
"trailingSlash": True,
|
||||||
}
|
}
|
||||||
|
if transpile_packages:
|
||||||
|
next_config["transpilePackages"] = list(
|
||||||
|
set((format_library_name(p) for p in transpile_packages))
|
||||||
|
)
|
||||||
if export:
|
if export:
|
||||||
next_config["output"] = "export"
|
next_config["output"] = "export"
|
||||||
next_config["distDir"] = constants.Dirs.STATIC
|
next_config["distDir"] = constants.Dirs.STATIC
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
|
import re
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
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 isinstance((third_text := page2_fragment_wrapper.children[0]), Text)
|
||||||
assert str(third_text.children[0].contents) == "{`third`}" # type: ignore
|
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
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
from unittest.mock import Mock, mock_open
|
from unittest.mock import Mock, mock_open
|
||||||
|
|
||||||
@ -61,6 +63,30 @@ def test_update_next_config(config, export, expected_output):
|
|||||||
assert output == 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):
|
def test_initialize_requirements_txt_no_op(mocker):
|
||||||
# File exists, reflex is included, do nothing
|
# File exists, reflex is included, do nothing
|
||||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||||
|
@ -836,7 +836,7 @@ def test_state_with_initial_computed_var(
|
|||||||
(f"{BaseVar(_var_name='var', _var_type=str)}", "${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')}",
|
f"testing f-string with {BaseVar(_var_name='myvar', _var_type=int)._var_set_state('state')}",
|
||||||
'testing f-string with $<reflex.Var>{"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}</reflex.Var>{state.myvar}',
|
'testing f-string with $<reflex.Var>{"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}</reflex.Var>{state.myvar}',
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
f"testing local f-string {BaseVar(_var_name='x', _var_is_local=True, _var_type=str)}",
|
f"testing local f-string {BaseVar(_var_name='x', _var_is_local=True, _var_type=str)}",
|
||||||
|
Loading…
Reference in New Issue
Block a user