Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 13 commits into from
Sep 15, 2023
5 changes: 5 additions & 0 deletions reflex/.templates/jinja/web/styles/styles.css.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{%- block imports_styles %}
{% for sheet_name in stylesheets %}
{{- "@import url('" + sheet_name + "'); " }}
{% endfor %}
{% endblock %}
7 changes: 5 additions & 2 deletions reflex/.templates/web/jsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"compilerOptions": {
"baseUrl": "."
"baseUrl": ".",
"paths": {
"@/*": ["public/*"]
}
}
}
}
2 changes: 1 addition & 1 deletion reflex/.templates/web/pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
7 changes: 5 additions & 2 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,8 +611,11 @@ def compile(self):
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))
Expand Down
57 changes: 49 additions & 8 deletions reflex/compiler/compiler.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -116,6 +118,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("/")
ElijahAhianyo marked this conversation as resolved.
Show resolved Hide resolved
)
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.

Expand Down Expand Up @@ -160,21 +206,17 @@ 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.
"""
# Get the path for the output file.
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
Expand Down Expand Up @@ -277,5 +319,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 changes: 3 additions & 0 deletions reflex/compiler/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,6 @@ def get_template(name: str) -> Template:

# Sitemap config file.
SITEMAP_CONFIG = "module.exports = {config}".format

# Code to render the root stylesheet.
STYLE = get_template("web/styles/styles.css.jinja2")
34 changes: 27 additions & 7 deletions reflex/compiler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import os
from typing import Any, Dict, List, Optional, Set, Tuple, Type
from urllib.parse import urlparse

from pydantic.fields import ModelField

Expand All @@ -18,7 +19,6 @@
Main,
Meta,
NextScript,
RawLink,
Title,
)
from reflex.components.component import Component, ComponentStyle, CustomComponent
Expand Down Expand Up @@ -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(),
picklelo marked this conversation as resolved.
Show resolved Hide resolved
Body.create(
ColorModeScript.create(),
Main.create(),
Expand Down Expand Up @@ -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:
picklelo marked this conversation as resolved.
Show resolved Hide resolved
"""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.

Expand Down Expand Up @@ -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 changes: 10 additions & 0 deletions reflex/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,14 @@ def get_fnm_name() -> str | None:
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.
Expand All @@ -148,6 +152,8 @@ def get_fnm_name() -> str | None:
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.
Expand All @@ -172,6 +178,10 @@ def get_fnm_name() -> str | None:
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.
Expand Down
75 changes: 55 additions & 20 deletions tests/compiler/test_compiler.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
Loading