[IMPL] - added support for sass and scss stylesheet languages
- better checking of stylesheets to be compiled - added support for sass and scss stylesheet languages - the stylesheets files are now copied to ".web/styles/" at compile time - relock poetry file for libsass deps - stylesheet compiler unit tests also check the contents of the file
This commit is contained in:
parent
5d88263cd8
commit
613e130b7b
@ -59,6 +59,7 @@ twine = ">=4.0.0,<6.0"
|
||||
tomlkit = ">=0.12.4,<1.0"
|
||||
lazy_loader = ">=0.4"
|
||||
reflex-chakra = ">=0.6.0"
|
||||
libsass = "0.23.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = ">=7.1.2,<9.0"
|
||||
|
@ -4,8 +4,12 @@ from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from re import IGNORECASE as RE_IGNORECASE
|
||||
from re import compile as re_compile
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, Optional, Tuple, Type, Union
|
||||
|
||||
from sass import compile as sass_compile
|
||||
|
||||
from reflex import constants
|
||||
from reflex.compiler import templates, utils
|
||||
from reflex.components.base.fragment import Fragment
|
||||
@ -24,6 +28,8 @@ from reflex.utils.imports import ImportVar
|
||||
from reflex.utils.prerequisites import get_web_dir
|
||||
from reflex.vars.base import LiteralVar, Var
|
||||
|
||||
RE_SASS_SCSS_EXT = re_compile(r"\.s(c|a)ss", flags=RE_IGNORECASE)
|
||||
|
||||
|
||||
def _compile_document_root(root: Component) -> str:
|
||||
"""Compile the document root.
|
||||
@ -189,18 +195,66 @@ def _compile_root_stylesheet(stylesheets: list[str]) -> str:
|
||||
if get_config().tailwind is not None
|
||||
else []
|
||||
)
|
||||
for stylesheet in stylesheets:
|
||||
|
||||
while len(stylesheets):
|
||||
stylesheet = stylesheets.pop(0)
|
||||
if not utils.is_valid_url(stylesheet):
|
||||
# check if stylesheet provided exists.
|
||||
stylesheet_full_path = (
|
||||
Path.cwd() / constants.Dirs.APP_ASSETS / stylesheet.strip("/")
|
||||
)
|
||||
assets_app_path = Path.cwd() / constants.Dirs.APP_ASSETS
|
||||
stylesheet_full_path = assets_app_path / stylesheet.strip("/")
|
||||
|
||||
if not stylesheet_full_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"The stylesheet file {stylesheet_full_path} does not exist."
|
||||
)
|
||||
stylesheet = f"../{constants.Dirs.PUBLIC}/{stylesheet.strip('/')}"
|
||||
elif not stylesheet_full_path.is_file():
|
||||
if stylesheet_full_path.is_dir():
|
||||
# NOTE: this can create an infinite loop, for example:
|
||||
# assets/
|
||||
# | dir_a/
|
||||
# | | dir_c/ (symlink to "assets/dir_a")
|
||||
# | dir_b/
|
||||
# so to avoid the infinite loop, we don't include symbolic links
|
||||
stylesheets += [
|
||||
str(p.relative_to(assets_app_path))
|
||||
for p in stylesheet_full_path.iterdir()
|
||||
if not p.is_symlink()
|
||||
]
|
||||
continue
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f'The stylesheet path "{stylesheet_full_path}" is not a valid path.'
|
||||
)
|
||||
elif (
|
||||
stylesheet_full_path.suffix[1:]
|
||||
not in constants.Reflex.STYLESHEETS_SUPPORTED
|
||||
):
|
||||
raise FileNotFoundError(
|
||||
f'The stylesheet file "{stylesheet_full_path}" is not a valid file.'
|
||||
)
|
||||
|
||||
if (
|
||||
stylesheet_full_path.suffix[1:]
|
||||
in constants.Reflex.STYLESHEETS_SUPPORTED
|
||||
):
|
||||
target = (
|
||||
Path.cwd()
|
||||
/ f"{constants.Dirs.WEB}/{constants.Dirs.STYLES}/{RE_SASS_SCSS_EXT.sub(".css", str(stylesheet)).strip('/')}"
|
||||
)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(
|
||||
data=sass_compile(
|
||||
filename=str(stylesheet_full_path), output_style="compressed"
|
||||
),
|
||||
encoding="utf8",
|
||||
)
|
||||
else:
|
||||
pass
|
||||
|
||||
stylesheet = f"./{RE_SASS_SCSS_EXT.sub(".css", str(stylesheet)).strip('/')}"
|
||||
|
||||
sheets.append(stylesheet) if stylesheet not in sheets else None
|
||||
|
||||
return templates.STYLE.render(stylesheets=sheets)
|
||||
|
||||
|
||||
|
@ -80,6 +80,9 @@ class Reflex(SimpleNamespace):
|
||||
|
||||
RELEASES_URL = "https://api.github.com/repos/reflex-dev/templates/releases"
|
||||
|
||||
# The reflex stylesheet language supported
|
||||
STYLESHEETS_SUPPORTED = ["css", "sass", "scss"]
|
||||
|
||||
|
||||
class ReflexHostingCLI(SimpleNamespace):
|
||||
"""Base constants concerning Reflex Hosting CLI."""
|
||||
|
@ -220,6 +220,9 @@ def setup_frontend(
|
||||
path_ops.cp(
|
||||
src=str(root / constants.Dirs.APP_ASSETS),
|
||||
dest=str(root / prerequisites.get_web_dir() / constants.Dirs.PUBLIC),
|
||||
ignore=tuple(
|
||||
f"*.{ext}" for ext in constants.Reflex.STYLESHEETS_SUPPORTED
|
||||
), # ignore stylesheet files precompiled in the compiler
|
||||
)
|
||||
|
||||
# Set the environment variables in client (env.json).
|
||||
|
@ -28,13 +28,19 @@ def rm(path: str | Path):
|
||||
path.unlink()
|
||||
|
||||
|
||||
def cp(src: str | Path, dest: str | Path, overwrite: bool = True) -> bool:
|
||||
def cp(
|
||||
src: str | Path,
|
||||
dest: str | Path,
|
||||
overwrite: bool = True,
|
||||
ignore: tuple[str, ...] | None = None,
|
||||
) -> bool:
|
||||
"""Copy a file or directory.
|
||||
|
||||
Args:
|
||||
src: The path to the file or directory.
|
||||
dest: The path to the destination.
|
||||
overwrite: Whether to overwrite the destination.
|
||||
ignore: Ignoring files and directories that match one of the glob-style patterns provided
|
||||
|
||||
Returns:
|
||||
Whether the copy was successful.
|
||||
@ -46,7 +52,11 @@ def cp(src: str | Path, dest: str | Path, overwrite: bool = True) -> bool:
|
||||
return False
|
||||
if src.is_dir():
|
||||
rm(dest)
|
||||
shutil.copytree(src, dest)
|
||||
shutil.copytree(
|
||||
src,
|
||||
dest,
|
||||
ignore=shutil.ignore_patterns(*ignore) if ignore is not None else ignore,
|
||||
)
|
||||
else:
|
||||
shutil.copyfile(src, dest)
|
||||
return True
|
||||
|
@ -106,7 +106,7 @@ def test_compile_imports(import_dict: ParsedImportDict, test_dicts: List[dict]):
|
||||
assert sorted(import_dict["rest"]) == test_dict["rest"] # type: ignore
|
||||
|
||||
|
||||
def test_compile_stylesheets(tmp_path, mocker):
|
||||
def test_compile_stylesheets(tmp_path: Path, mocker):
|
||||
"""Test that stylesheets compile correctly.
|
||||
|
||||
Args:
|
||||
@ -119,25 +119,50 @@ def test_compile_stylesheets(tmp_path, mocker):
|
||||
assets_dir = project / "assets"
|
||||
assets_dir.mkdir()
|
||||
|
||||
(assets_dir / "styles.css").touch()
|
||||
assets_preprocess_dir = project / "assets" / "preprocess"
|
||||
assets_preprocess_dir.mkdir()
|
||||
|
||||
(assets_dir / "styles.css").write_text(
|
||||
"button.rt-Button {\n\tborder-radius:unset !important;\n}"
|
||||
)
|
||||
(assets_preprocess_dir / "styles_a.sass").write_text(
|
||||
"button.rt-Button\n\tborder-radius:unset !important"
|
||||
)
|
||||
(assets_preprocess_dir / "styles_b.scss").write_text(
|
||||
"button.rt-Button {\n\tborder-radius:unset !important;\n}"
|
||||
)
|
||||
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",
|
||||
"/preprocess/styles_a.sass",
|
||||
"/preprocess/styles_b.scss",
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css",
|
||||
]
|
||||
|
||||
assert compiler.compile_root_stylesheet(stylesheets) == (
|
||||
str(Path(".web") / "styles" / "styles.css"),
|
||||
"@import url('./tailwind.css'); \n"
|
||||
"@import url('https://fonts.googleapis.com/css?family=Sofia&effect=neon|outline|emboss|shadow-multiple'); \n"
|
||||
"@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css'); \n"
|
||||
"@import url('../public/styles.css'); \n"
|
||||
"@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css'); \n",
|
||||
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('./preprocess/styles_a.css'); \n"
|
||||
f"@import url('./preprocess/styles_b.css'); \n"
|
||||
f"@import url('https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap-theme.min.css'); \n",
|
||||
)
|
||||
|
||||
# NOTE: the css file is also inserted into the s(a|c)ss preprocessor, which compressed the result, which means we don't have the tab, return,... characters.
|
||||
expected_result = "button.rt-Button{border-radius:unset !important}\n"
|
||||
assert (project / ".web" / "styles" / "styles.css").read_text() == expected_result
|
||||
assert (
|
||||
project / ".web" / "styles" / "preprocess" / "styles_a.css"
|
||||
).read_text() == expected_result
|
||||
assert (
|
||||
project / ".web" / "styles" / "preprocess" / "styles_b.css"
|
||||
).read_text() == expected_result
|
||||
|
||||
|
||||
def test_compile_stylesheets_exclude_tailwind(tmp_path, mocker):
|
||||
"""Test that Tailwind is excluded if tailwind config is explicitly set to None.
|
||||
@ -165,7 +190,7 @@ def test_compile_stylesheets_exclude_tailwind(tmp_path, mocker):
|
||||
|
||||
assert compiler.compile_root_stylesheet(stylesheets) == (
|
||||
str(Path(".web") / "styles" / "styles.css"),
|
||||
"@import url('../public/styles.css'); \n",
|
||||
"@import url('./styles.css'); \n",
|
||||
)
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user