diff --git a/reflex/.templates/web/next.config.js b/reflex/.templates/web/next.config.js index 65c4f4fd5e..fed02db3e3 100644 --- a/reflex/.templates/web/next.config.js +++ b/reflex/.templates/web/next.config.js @@ -3,4 +3,5 @@ module.exports = { compress: true, reactStrictMode: true, trailingSlash: true, + output: "", }; diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index c93733f546..99639745b5 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -95,8 +95,8 @@ class Commands(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 Commands(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", diff --git a/reflex/reflex.py b/reflex/reflex.py index fdf7609474..91e410dcc1 100644 --- a/reflex/reflex.py +++ b/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. diff --git a/reflex/testing.py b/reflex/testing.py index dfea886709..88e3a9e35a 100644 --- a/reflex/testing.py +++ b/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", } diff --git a/reflex/utils/build.py b/reflex/utils/build.py index e4830ef63f..ad3c3ca2c0 100644 --- a/reflex/utils/build.py +++ b/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, ) diff --git a/reflex/utils/console.py b/reflex/utils/console.py index 64a0c7fcc1..1580da706d 100644 --- a/reflex/utils/console.py +++ b/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): diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index ec7d81ee19..52e8f73175 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -25,7 +25,7 @@ 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(): diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index 6c04d12d6c..794840015a 100644 --- a/reflex/utils/processes.py +++ b/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: diff --git a/tests/test_prerequisites.py b/tests/test_prerequisites.py index 593129b7d8..ff2c446613 100644 --- a/tests/test_prerequisites.py +++ b/tests/test_prerequisites.py @@ -4,106 +4,56 @@ 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):