Răsfoiți Sursa

Upgrade to NextJS 14 (#2142)

Thomas Brandého 1 an în urmă
părinte
comite
39cc1b2f12

+ 1 - 0
reflex/.templates/web/next.config.js

@@ -3,4 +3,5 @@ module.exports = {
   compress: true,
   reactStrictMode: true,
   trailingSlash: true,
+  output: "",
 };

+ 3 - 3
reflex/constants/installer.py

@@ -95,8 +95,8 @@ class PackageJson(SimpleNamespace):
         """The commands to define in package.json."""
 
         DEV = "next dev"
-        EXPORT = "next build && next export -o _static"
-        EXPORT_SITEMAP = "next build && next-sitemap && next export -o _static"
+        EXPORT = "next build"
+        EXPORT_SITEMAP = "next build && next-sitemap"
         PROD = "next start"
 
     PATH = os.path.join(Dirs.WEB, "package.json")
@@ -106,7 +106,7 @@ class PackageJson(SimpleNamespace):
         "focus-visible": "5.2.0",
         "framer-motion": "10.16.4",
         "json5": "2.2.3",
-        "next": "13.5.4",
+        "next": "14.0.1",
         "next-sitemap": "4.1.8",
         "next-themes": "0.2.0",
         "react": "18.2.0",

+ 3 - 0
reflex/reflex.py

@@ -184,6 +184,7 @@ def _run(
     console.rule("[bold]Starting Reflex App")
 
     if frontend:
+        prerequisites.update_next_config()
         # Get the app module.
         prerequisites.get_app()
 
@@ -337,6 +338,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_app()
         # Set up .web directory and install frontend dependencies.

+ 1 - 1
reflex/testing.py

@@ -677,7 +677,7 @@ class AppHarnessProd(AppHarness):
     frontend_server: Optional[Subdir404TCPServer] = None
 
     def _run_frontend(self):
-        web_root = self.app_path / reflex.constants.Dirs.WEB / "_static"
+        web_root = self.app_path / reflex.constants.Dirs.WEB_STATIC
         error_page_map = {
             404: web_root / "404.html",
         }

+ 24 - 19
reflex/utils/build.py

@@ -35,21 +35,25 @@ def set_os_env(**kwargs):
         os.environ[key.upper()] = value
 
 
-def generate_sitemap_config(deploy_url: str):
+def generate_sitemap_config(deploy_url: str, export=False):
     """Generate the sitemap config file.
 
     Args:
         deploy_url: The URL of the deployed app.
+        export: If the sitemap are generated for an export.
     """
     # Import here to avoid circular imports.
     from reflex.compiler import templates
 
-    config = json.dumps(
-        {
-            "siteUrl": deploy_url,
-            "generateRobotsTxt": True,
-        }
-    )
+    config = {
+        "siteUrl": deploy_url,
+        "generateRobotsTxt": True,
+    }
+
+    if export:
+        config["outDir"] = constants.Dirs.STATIC
+
+    config = json.dumps(config)
 
     with open(constants.Next.SITEMAP_CONFIG_FILE, "w") as f:
         f.write(templates.SITEMAP_CONFIG(config=config))
@@ -115,7 +119,7 @@ def _zip(
 
     with progress, zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipf:
         for file in files_to_zip:
-            console.debug(f"{target}: {file}")
+            console.debug(f"{target}: {file}", progress=progress)
             progress.advance(task)
             zipf.write(file, os.path.relpath(file, root_dir))
 
@@ -145,22 +149,23 @@ def export(
     command = "export"
 
     if frontend:
-        # Generate a sitemap if a deploy URL is provided.
-        if deploy_url is not None:
-            generate_sitemap_config(deploy_url)
-            command = "export-sitemap"
-
         checkpoints = [
             "Linting and checking ",
-            "Compiled successfully",
+            "Creating an optimized production build",
             "Route (pages)",
+            "prerendered as static HTML",
             "Collecting page data",
-            "automatically rendered as static HTML",
-            'Copying "static build" directory',
-            'Copying "public" directory',
             "Finalizing page optimization",
-            "Export successful",
+            "Collecting build traces",
         ]
+
+        # Generate a sitemap if a deploy URL is provided.
+        if deploy_url is not None:
+            generate_sitemap_config(deploy_url, export=zip)
+            command = "export-sitemap"
+
+            checkpoints.extend(["Loading next-sitemap", "Generation completed"])
+
         # Start the subprocess with the progress bar.
         process = processes.new_process(
             [prerequisites.get_package_manager(), "run", command],
@@ -181,7 +186,7 @@ def export(
                 target=os.path.join(
                     zip_dest_dir, constants.ComponentName.FRONTEND.zip()
                 ),
-                root_dir=".web/_static",
+                root_dir=constants.Dirs.WEB_STATIC,
                 files_to_exclude=files_to_exclude,
                 exclude_venv_dirs=False,
             )

+ 5 - 1
reflex/utils/console.py

@@ -45,7 +45,11 @@ def debug(msg: str, **kwargs):
         kwargs: Keyword arguments to pass to the print function.
     """
     if _LOG_LEVEL <= LogLevel.DEBUG:
-        print(f"[blue]Debug: {msg}[/blue]", **kwargs)
+        msg_ = f"[blue]Debug: {msg}[/blue]"
+        if progress := kwargs.pop("progress", None):
+            progress.console.print(msg_, **kwargs)
+        else:
+            print(msg_, **kwargs)
 
 
 def info(msg: str, **kwargs):

+ 27 - 28
reflex/utils/prerequisites.py

@@ -25,7 +25,7 @@ from redis.asyncio import Redis
 
 from reflex import constants, model
 from reflex.compiler import templates
-from reflex.config import Config, get_config
+from reflex.config import get_config
 from reflex.utils import console, path_ops, processes
 
 
@@ -288,15 +288,7 @@ def initialize_web_directory():
 
     path_ops.mkdir(constants.Dirs.WEB_ASSETS)
 
-    # update nextJS config based on rxConfig
-    next_config_file = os.path.join(constants.Dirs.WEB, constants.Next.CONFIG_FILE)
-
-    with open(next_config_file, "r") as file:
-        next_config = file.read()
-        next_config = update_next_config(next_config, get_config())
-
-    with open(next_config_file, "w") as file:
-        file.write(next_config)
+    update_next_config()
 
     # Initialize the reflex json file.
     init_reflex_json()
@@ -337,27 +329,34 @@ def init_reflex_json():
     path_ops.update_json_file(constants.Reflex.JSON, reflex_json)
 
 
-def update_next_config(next_config: str, config: Config) -> str:
-    """Update Next.js config from Reflex config. Is its own function for testing.
+def update_next_config(export=False):
+    """Update Next.js config from Reflex config.
 
     Args:
-        next_config: Content of next.config.js.
-        config: A reflex Config object.
-
-    Returns:
-        The next_config updated from config.
+        export: if the method run during reflex export.
     """
-    next_config = re.sub(
-        "compress: (true|false)",
-        f'compress: {"true" if config.next_compression else "false"}',
-        next_config,
-    )
-    next_config = re.sub(
-        'basePath: ".*?"',
-        f'basePath: "{config.frontend_path or ""}"',
-        next_config,
-    )
-    return next_config
+    next_config_file = os.path.join(constants.Dirs.WEB, constants.Next.CONFIG_FILE)
+
+    next_config = _update_next_config(get_config(), export=export)
+
+    with open(next_config_file, "w") as file:
+        file.write(next_config)
+        file.write("\n")
+
+
+def _update_next_config(config, export=False):
+    next_config = {
+        "basePath": config.frontend_path or "",
+        "compress": config.next_compression,
+        "reactStrictMode": True,
+        "trailingSlash": True,
+    }
+    if export:
+        next_config["output"] = "export"
+        next_config["distDir"] = constants.Dirs.STATIC
+
+    next_config_json = re.sub(r'"([^"]+)"(?=:)', r"\1", json.dumps(next_config))
+    return f"module.exports = {next_config_json};"
 
 
 def remove_existing_bun_installation():

+ 5 - 4
reflex/utils/processes.py

@@ -193,12 +193,13 @@ def run_concurrently(*fns: Union[Callable, Tuple]) -> None:
         pass
 
 
-def stream_logs(message: str, process: subprocess.Popen):
+def stream_logs(message: str, process: subprocess.Popen, progress=None):
     """Stream the logs for a process.
 
     Args:
         message: The message to display.
         process: The process.
+        progress: The ongoing progress bar if one is being used.
 
     Yields:
         The lines of the process output.
@@ -209,11 +210,11 @@ def stream_logs(message: str, process: subprocess.Popen):
     # Store the tail of the logs.
     logs = collections.deque(maxlen=512)
     with process:
-        console.debug(message)
+        console.debug(message, progress=progress)
         if process.stdout is None:
             return
         for line in process.stdout:
-            console.debug(line, end="")
+            console.debug(line, end="", progress=progress)
             logs.append(line)
             yield line
 
@@ -260,7 +261,7 @@ def show_progress(message: str, process: subprocess.Popen, checkpoints: List[str
     # Iterate over the process output.
     with console.progress() as progress:
         task = progress.add_task(f"{message}: ", total=len(checkpoints))
-        for line in stream_logs(message, process):
+        for line in stream_logs(message, process, progress=progress):
             # Check for special strings and update the progress bar.
             for special_string in checkpoints:
                 if special_string in line:

+ 20 - 70
tests/test_prerequisites.py

@@ -4,106 +4,56 @@ import pytest
 
 from reflex import constants
 from reflex.config import Config
-from reflex.utils.prerequisites import initialize_requirements_txt, update_next_config
+from reflex.utils.prerequisites import _update_next_config, initialize_requirements_txt
 
 
 @pytest.mark.parametrize(
-    "template_next_config, reflex_config, expected_next_config",
+    "config, export, expected_output",
     [
         (
-            """
-                module.exports = {
-                    basePath: "",
-                    compress: true,
-                    reactStrictMode: true,
-                    trailingSlash: true,
-                };
-            """,
             Config(
                 app_name="test",
             ),
-            """
-                module.exports = {
-                    basePath: "",
-                    compress: true,
-                    reactStrictMode: true,
-                    trailingSlash: true,
-                };
-            """,
+            False,
+            'module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true};',
         ),
         (
-            """
-                module.exports = {
-                    basePath: "",
-                    compress: true,
-                    reactStrictMode: true,
-                    trailingSlash: true,
-                };
-            """,
             Config(
                 app_name="test",
                 next_compression=False,
             ),
-            """
-                module.exports = {
-                    basePath: "",
-                    compress: false,
-                    reactStrictMode: true,
-                    trailingSlash: true,
-                };
-            """,
+            False,
+            'module.exports = {basePath: "", compress: false, reactStrictMode: true, trailingSlash: true};',
         ),
         (
-            """
-                module.exports = {
-                    basePath: "",
-                    compress: true,
-                    reactStrictMode: true,
-                    trailingSlash: true,
-                };
-            """,
             Config(
                 app_name="test",
                 frontend_path="/test",
             ),
-            """
-                module.exports = {
-                    basePath: "/test",
-                    compress: true,
-                    reactStrictMode: true,
-                    trailingSlash: true,
-                };
-            """,
+            False,
+            'module.exports = {basePath: "/test", compress: true, reactStrictMode: true, trailingSlash: true};',
         ),
         (
-            """
-                module.exports = {
-                    basePath: "",
-                    compress: true,
-                    reactStrictMode: true,
-                    trailingSlash: true,
-                };
-            """,
             Config(
                 app_name="test",
                 frontend_path="/test",
                 next_compression=False,
             ),
-            """
-                module.exports = {
-                    basePath: "/test",
-                    compress: false,
-                    reactStrictMode: true,
-                    trailingSlash: true,
-                };
-            """,
+            False,
+            'module.exports = {basePath: "/test", compress: false, reactStrictMode: true, trailingSlash: true};',
+        ),
+        (
+            Config(
+                app_name="test",
+            ),
+            True,
+            'module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true, output: "export", distDir: "_static"};',
         ),
     ],
 )
-def test_update_next_config(template_next_config, reflex_config, expected_next_config):
-    assert (
-        update_next_config(template_next_config, reflex_config) == expected_next_config
-    )
+def test_update_next_config(config, export, expected_output):
+    output = _update_next_config(config, export=export)
+    assert output == expected_output
 
 
 def test_initialize_requirements_txt(mocker):