Browse Source

Move custom styles to root App file(_app.js) (#1764)

Elijah Ahianyo 1 year ago
parent
commit
74d227d2fd

+ 5 - 0
reflex/.templates/jinja/web/styles/styles.css.jinja2

@@ -0,0 +1,5 @@
+{%- block imports_styles %}
+{% for sheet_name in stylesheets %}
+    {{- "@import url('" + sheet_name + "'); " }}
+{% endfor %}
+{% endblock %}

+ 5 - 2
reflex/.templates/web/jsconfig.json

@@ -1,5 +1,8 @@
 {
   "compilerOptions": {
-    "baseUrl": "."
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["public/*"]
+    }
   }
-}
+}

+ 1 - 1
reflex/.templates/web/pages/_app.js

@@ -4,7 +4,7 @@ import theme from "/utils/theme";
 import { clientStorage, initialEvents, initialState, StateContext, EventLoopContext } from "/utils/context.js";
 import { useEventLoop } from "utils/state";
 
-import '../styles/tailwind.css'
+import '/styles/styles.css'
 
 const GlobalStyles = css`
   /* Hide the blue border around Chakra components. */

+ 5 - 2
reflex/app.py

@@ -612,8 +612,11 @@ class App(Base):
         for component in custom_components:
             all_imports.update(component.get_imports())
 
-        # Compile the root document with base styles and fonts
-        compile_results.append(compiler.compile_document_root(self.stylesheets))
+        # Compile the root stylesheet with base styles.
+        compile_results.append(compiler.compile_root_stylesheet(self.stylesheets))
+
+        # Compile the root document.
+        compile_results.append(compiler.compile_document_root())
 
         # Compile the theme.
         compile_results.append(compiler.compile_theme(self.style))

+ 49 - 8
reflex/compiler/compiler.py

@@ -1,6 +1,8 @@
 """Compiler for the reflex apps."""
 from __future__ import annotations
 
+import os
+from pathlib import Path
 from typing import List, Set, Tuple, Type
 
 from reflex import constants
@@ -118,6 +120,50 @@ def _compile_page(
     )
 
 
+def compile_root_stylesheet(stylesheets: List[str]) -> Tuple[str, str]:
+    """Compile the root stylesheet.
+
+    Args:
+        stylesheets: The stylesheets to include in the root stylesheet.
+
+    Returns:
+        The path and code of the compiled root stylesheet.
+    """
+    output_path = utils.get_root_stylesheet_path()
+
+    code = _compile_root_stylesheet(stylesheets)
+
+    return output_path, code
+
+
+def _compile_root_stylesheet(stylesheets: List[str]) -> str:
+    """Compile the root stylesheet.
+
+    Args:
+        stylesheets: The stylesheets to include in the root stylesheet.
+
+    Returns:
+        The compiled root stylesheet.
+
+    Raises:
+        FileNotFoundError: If a specified stylesheet in assets directory does not exist.
+    """
+    sheets = [constants.TAILWIND_ROOT_STYLE_PATH]
+    for stylesheet in stylesheets:
+        if not utils.is_valid_url(stylesheet):
+            # check if stylesheet provided exists.
+            stylesheet_full_path = (
+                Path.cwd() / constants.APP_ASSETS_DIR / stylesheet.strip("/")
+            )
+            if not os.path.exists(stylesheet_full_path):
+                raise FileNotFoundError(
+                    f"The stylesheet file {stylesheet_full_path} does not exist."
+                )
+            stylesheet = f"@/{stylesheet.strip('/')}"
+        sheets.append(stylesheet) if stylesheet not in sheets else None
+    return templates.STYLE.render(stylesheets=sheets)
+
+
 def _compile_components(components: Set[CustomComponent]) -> str:
     """Compile the components.
 
@@ -162,12 +208,9 @@ def _compile_tailwind(
     )
 
 
-def compile_document_root(stylesheets: List[str]) -> Tuple[str, str]:
+def compile_document_root() -> Tuple[str, str]:
     """Compile the document root.
 
-    Args:
-        stylesheets: The stylesheets to include in the document root.
-
     Returns:
         The path and code of the compiled document root.
     """
@@ -175,8 +218,7 @@ def compile_document_root(stylesheets: List[str]) -> Tuple[str, str]:
     output_path = utils.get_page_path(constants.DOCUMENT_ROOT)
 
     # Create the document root.
-    document_root = utils.create_document_root(stylesheets)
-
+    document_root = utils.create_document_root()
     # Compile the document root.
     code = _compile_document_root(document_root)
     return output_path, code
@@ -279,5 +321,4 @@ def compile_tailwind(
 
 def purge_web_pages_dir():
     """Empty out .web directory."""
-    template_files = ["_app.js"]
-    utils.empty_dir(constants.WEB_PAGES_DIR, keep_files=template_files)
+    utils.empty_dir(constants.WEB_PAGES_DIR, keep_files=["_app.js"])

+ 3 - 0
reflex/compiler/templates.py

@@ -77,3 +77,6 @@ COMPONENTS = get_template("web/pages/custom_component.js.jinja2")
 
 # Sitemap config file.
 SITEMAP_CONFIG = "module.exports = {config}".format
+
+# Code to render the root stylesheet.
+STYLE = get_template("web/styles/styles.css.jinja2")

+ 27 - 7
reflex/compiler/utils.py

@@ -3,6 +3,7 @@ from __future__ import annotations
 
 import os
 from typing import Any, Dict, List, Optional, Set, Tuple, Type
+from urllib.parse import urlparse
 
 from pydantic.fields import ModelField
 
@@ -18,7 +19,6 @@ from reflex.components.base import (
     Main,
     Meta,
     NextScript,
-    RawLink,
     Title,
 )
 from reflex.components.component import Component, ComponentStyle, CustomComponent
@@ -257,18 +257,14 @@ def compile_custom_component(
     )
 
 
-def create_document_root(stylesheets: List[str]) -> Component:
+def create_document_root() -> Component:
     """Create the document root.
 
-    Args:
-        stylesheets: The list of stylesheets to include in the document root.
-
     Returns:
         The document root.
     """
-    sheets = [RawLink.create(rel="stylesheet", href=href) for href in stylesheets]
     return Html.create(
-        DocumentHead.create(*sheets),
+        DocumentHead.create(),
         Body.create(
             ColorModeScript.create(),
             Main.create(),
@@ -324,6 +320,17 @@ def get_theme_path() -> str:
     return os.path.join(constants.WEB_UTILS_DIR, constants.THEME + constants.JS_EXT)
 
 
+def get_root_stylesheet_path() -> str:
+    """Get the path of the app root file.
+
+    Returns:
+        The path of the app root file.
+    """
+    return os.path.join(
+        constants.STYLES_DIR, constants.STYLESHEET_ROOT + constants.CSS_EXT
+    )
+
+
 def get_context_path() -> str:
     """Get the path of the context / initial state file.
 
@@ -415,3 +422,16 @@ def empty_dir(path: str, keep_files: Optional[List[str]] = None):
     for element in directory_contents:
         if element not in keep_files:
             path_ops.rm(os.path.join(path, element))
+
+
+def is_valid_url(url) -> bool:
+    """Check if a url is valid.
+
+    Args:
+        url: The Url to check.
+
+    Returns:
+        Whether url is valid.
+    """
+    result = urlparse(url)
+    return all([result.scheme, result.netloc])

+ 10 - 0
reflex/constants.py

@@ -126,10 +126,14 @@ WEB_STATIC_DIR = os.path.join(WEB_DIR, STATIC_DIR)
 WEB_UTILS_DIR = os.path.join(WEB_DIR, UTILS_DIR)
 # The directory where the assets are located.
 WEB_ASSETS_DIR = os.path.join(WEB_DIR, "public")
+# The directory where styles are located.
+STYLES_DIR = os.path.join(WEB_DIR, "styles")
 # The Tailwind config.
 TAILWIND_CONFIG = os.path.join(WEB_DIR, "tailwind.config.js")
 # Default Tailwind content paths
 TAILWIND_CONTENT = ["./pages/**/*.{js,ts,jsx,tsx}"]
+# Relative tailwind style path to root stylesheet in STYLES_DIR.
+TAILWIND_ROOT_STYLE_PATH = "./tailwind.css"
 # The NextJS config file
 NEXT_CONFIG_FILE = "next.config.js"
 # The sitemap config file.
@@ -148,6 +152,8 @@ ENV_JSON = os.path.join(WEB_DIR, "env.json")
 JS_EXT = ".js"
 # The extension for python files.
 PY_EXT = ".py"
+# The extension for css files.
+CSS_EXT = ".css"
 # The expected variable name where the rx.App is stored.
 APP_VAR = "app"
 # The expected variable name where the API object is stored for deployment.
@@ -172,6 +178,10 @@ HYDRATE = "hydrate"
 IS_HYDRATED = "is_hydrated"
 # The name of the index page.
 INDEX_ROUTE = "index"
+# The name of the app root page.
+APP_ROOT = "_app"
+# The root stylesheet filename.
+STYLESHEET_ROOT = "styles"
 # The name of the document root page.
 DOCUMENT_ROOT = "_document"
 # The name of the theme page.

+ 55 - 20
tests/compiler/test_compiler.py

@@ -1,8 +1,9 @@
+import os
 from typing import List, Set
 
 import pytest
 
-from reflex.compiler import utils
+from reflex.compiler import compiler, utils
 from reflex.utils import imports
 from reflex.vars import ImportVar
 
@@ -106,22 +107,56 @@ def test_compile_imports(import_dict: imports.ImportDict, test_dicts: List[dict]
         assert import_dict["rest"] == test_dict["rest"]
 
 
-# @pytest.mark.parametrize(
-#     "name,value,output",
-#     [
-#         ("foo", "bar", 'const foo = "bar"'),
-#         ("num", 1, "const num = 1"),
-#         ("check", False, "const check = false"),
-#         ("arr", [1, 2, 3], "const arr = [1, 2, 3]"),
-#         ("obj", {"foo": "bar"}, 'const obj = {"foo": "bar"}'),
-#     ],
-# )
-# def test_compile_constant_declaration(name: str, value: str, output: str):
-#     """Test the compile_constant_declaration function.
-
-#     Args:
-#         name: The name of the constant.
-#         value: The value of the constant.
-#         output: The expected output.
-#     """
-#     assert utils.compile_constant_declaration(name, value) == output
+def test_compile_stylesheets(tmp_path, mocker):
+    """Test that stylesheets compile correctly.
+
+    Args:
+        tmp_path: The test directory.
+        mocker: Pytest mocker object.
+    """
+    project = tmp_path / "test_project"
+    project.mkdir()
+
+    assets_dir = project / "assets"
+    assets_dir.mkdir()
+
+    (assets_dir / "styles.css").touch()
+
+    mocker.patch("reflex.compiler.compiler.Path.cwd", return_value=project)
+
+    stylesheets = [
+        "https://fonts.googleapis.com/css?family=Sofia&effect=neon|outline|emboss|shadow-multiple",
+        "https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css",
+        "/styles.css",
+        "https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css",
+    ]
+
+    assert compiler.compile_root_stylesheet(stylesheets) == (
+        os.path.join(".web", "styles", "styles.css"),
+        f"@import url('./tailwind.css'); \n"
+        f"@import url('https://fonts.googleapis.com/css?family=Sofia&effect=neon|outline|emboss|shadow-multiple'); \n"
+        f"@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css'); \n"
+        f"@import url('@/styles.css'); \n"
+        f"@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css'); \n",
+    )
+
+
+def test_compile_nonexistent_stylesheet(tmp_path, mocker):
+    """Test that an error is thrown for non-existent stylesheets.
+
+    Args:
+        tmp_path: The test directory.
+        mocker: Pytest mocker object.
+    """
+    project = tmp_path / "test_project"
+    project.mkdir()
+
+    assets_dir = project / "assets"
+    assets_dir.mkdir()
+
+    mocker.patch("reflex.compiler.compiler.Path.cwd", return_value=project)
+
+    stylesheets = ["/styles.css"]
+
+    with pytest.raises(FileNotFoundError):
+        compiler.compile_root_stylesheet(stylesheets)