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

[IMPL] - added support for sass and scss stylesheet languages #4292

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0681cc2
[IMPL] - added support for sass and scss stylesheet languages
KronosDev-Pro Nov 2, 2024
707f345
fix f-string bug
KronosDev-Pro Nov 5, 2024
e645d0f
make "libsass" an optional dependency
KronosDev-Pro Nov 5, 2024
7e7687d
remove libsass of deps list
KronosDev-Pro Nov 5, 2024
d6bd371
revert peotry relock
KronosDev-Pro Nov 5, 2024
0fe38d2
fix test caused by optional "libsass" deps
KronosDev-Pro Nov 6, 2024
df9818c
improving `_compile_root_stylesheet` function and add folder styleshe…
KronosDev-Pro Nov 7, 2024
370b138
fix the copy files in assets to public folder
KronosDev-Pro Nov 7, 2024
9bdea04
Merge branch 'main' into add-sass-scss-stylesheet-support
KronosDev-Pro Nov 7, 2024
4df9d5d
remove useless f-string
KronosDev-Pro Nov 7, 2024
c523872
little general improvement
KronosDev-Pro Nov 8, 2024
13b21a1
fix f-string
KronosDev-Pro Nov 11, 2024
0223081
remove useless path search
KronosDev-Pro Nov 11, 2024
4fae418
remove unused var & import
KronosDev-Pro Nov 13, 2024
613e130
[IMPL] - added support for sass and scss stylesheet languages
KronosDev-Pro Nov 2, 2024
81dab90
fix f-string bug
KronosDev-Pro Nov 5, 2024
e815903
make "libsass" an optional dependency
KronosDev-Pro Nov 5, 2024
6e32fcb
remove libsass of deps list
KronosDev-Pro Nov 5, 2024
2ca363c
fix test caused by optional "libsass" deps
KronosDev-Pro Nov 6, 2024
6898a26
improving `_compile_root_stylesheet` function and add folder styleshe…
KronosDev-Pro Nov 7, 2024
59a0cbf
fix the copy files in assets to public folder
KronosDev-Pro Nov 7, 2024
6a2a5ff
remove useless f-string
KronosDev-Pro Nov 7, 2024
f04431a
little general improvement
KronosDev-Pro Nov 8, 2024
3b403cc
fix f-string
KronosDev-Pro Nov 11, 2024
b8e2ddd
remove useless path search
KronosDev-Pro Nov 11, 2024
8b46852
remove unused var & import
KronosDev-Pro Nov 13, 2024
ff18003
Merge branch 'add-sass-scss-stylesheet-support' of https://github.com…
KronosDev-Pro Nov 13, 2024
0dde556
Merge branch 'main' into add-sass-scss-stylesheet-support
KronosDev-Pro Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 87 additions & 11 deletions reflex/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

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 reflex import constants
Expand All @@ -19,11 +21,14 @@
from reflex.config import environment, get_config
from reflex.state import BaseState
from reflex.style import SYSTEM_COLOR_MODE
from reflex.utils import console
from reflex.utils.exec import is_prod_mode
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)
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved


def _compile_document_root(root: Component) -> str:
"""Compile the document root.
Expand Down Expand Up @@ -189,18 +194,89 @@ def _compile_root_stylesheet(stylesheets: list[str]) -> str:
if get_config().tailwind is not None
else []
)
for stylesheet in stylesheets:
if not utils.is_valid_url(stylesheet):
# check if stylesheet provided exists.
stylesheet_full_path = (
Path.cwd() / constants.Dirs.APP_ASSETS / stylesheet.strip("/")
)
if not stylesheet_full_path.exists():
raise FileNotFoundError(
f"The stylesheet file {stylesheet_full_path} does not exist."

try:
sass_compile = None
while len(stylesheets):
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved
stylesheet = stylesheets.pop(0)
if not utils.is_valid_url(stylesheet):
# check if stylesheet provided exists.
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."
)
elif not stylesheet_full_path.is_file():
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved
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() and p.is_dir())
]
continue
else:
raise FileNotFoundError(
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved
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
):
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved
target = (
Path.cwd()
/ constants.Dirs.WEB
/ constants.Dirs.STYLES
/ RE_SASS_SCSS_EXT.sub(".css", str(stylesheet)).strip("/")
)
target.parent.mkdir(parents=True, exist_ok=True)

if stylesheet_full_path.suffix == ".css":
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved
target.write_text(
data=stylesheet_full_path.read_text(),
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved
encoding="utf8",
)
else:
if sass_compile is None:
from sass import compile as sass_compile
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved
else:
pass
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved

target.write_text(
data=sass_compile(
filename=str(stylesheet_full_path),
output_style="compressed",
),
encoding="utf8",
)
else:
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved
pass

stylesheet = (
f"./{RE_SASS_SCSS_EXT.sub('.css', str(stylesheet)).strip('/')}"
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved
)
stylesheet = f"../{constants.Dirs.PUBLIC}/{stylesheet.strip('/')}"
sheets.append(stylesheet) if stylesheet not in sheets else None

sheets.append(stylesheet) if stylesheet not in sheets else None
except ImportError:
KronosDev-Pro marked this conversation as resolved.
Show resolved Hide resolved
console.error(
"""The `libsass` package is required to compile sass/scss stylesheet files. Run `pip install "libsass>=0.23.0"`."""
)

return templates.STYLE.render(stylesheets=sheets)


Expand Down
3 changes: 3 additions & 0 deletions reflex/constants/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ class Reflex(SimpleNamespace):

RELEASES_URL = f"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."""
Expand Down
3 changes: 3 additions & 0 deletions reflex/utils/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
14 changes: 12 additions & 2 deletions reflex/utils/path_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
69 changes: 65 additions & 4 deletions tests/units/compiler/test_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -119,7 +119,9 @@ def test_compile_stylesheets(tmp_path, mocker):
assets_dir = project / "assets"
assets_dir.mkdir()

(assets_dir / "styles.css").touch()
(assets_dir / "styles.css").write_text(
"button.rt-Button {\n\tborder-radius:unset !important;\n}"
)
mocker.patch("reflex.compiler.compiler.Path.cwd", return_value=project)

stylesheets = [
Expand All @@ -134,10 +136,69 @@ def test_compile_stylesheets(tmp_path, mocker):
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/[email protected]/dist/css/bootstrap.min.css'); \n"
f"@import url('../public/styles.css'); \n"
f"@import url('./styles.css'); \n"
f"@import url('https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap-theme.min.css'); \n",
)

assert (project / ".web" / "styles" / "styles.css").read_text() == (
assets_dir / "styles.css"
).read_text()


def test_compile_stylesheets_scss_sass(tmp_path: Path, mocker):
try:
import sass # noqa: F401
except ImportError:
pytest.skip(
"""The `libsass` package is required to compile sass/scss stylesheet files. Run `pip install "libsass>=0.23.0"`."""
)

project = tmp_path / "test_project"
project.mkdir()

assets_dir = project / "assets"
assets_dir.mkdir()

assets_preprocess_dir = assets_dir / "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 = [
"/styles.css",
"/preprocess/styles_a.sass",
"/preprocess/styles_b.scss",
]

assert compiler.compile_root_stylesheet(stylesheets) == (
str(Path(".web") / "styles" / "styles.css"),
f"@import url('./tailwind.css'); \n"
f"@import url('./styles.css'); \n"
f"@import url('./preprocess/styles_a.css'); \n"
f"@import url('./preprocess/styles_b.css'); \n",
)

assert (project / ".web" / "styles" / "styles.css").read_text() == (
assets_dir / "styles.css"
).read_text()

expected_result = "button.rt-Button{border-radius:unset !important}\n"
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.
Expand Down Expand Up @@ -165,7 +226,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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure this won't break anything?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know where this could cause a problem, since all the supported stylesheet files in the app/assets directories and subdirectories are copied or compiled and saved in the .web/styles directory and no longer in the .web/public directories.

and also, I checked the second batch of the tests/integration group and the tests/integration/test_tailwind.py (which is the only one to have tests impacted by the changes) passed the tests without any errors, which is why I don't understand the error in the first batch of the tests/integration group.

)


Expand Down