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 all 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
65 changes: 61 additions & 4 deletions reflex/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from reflex.config import environment, get_config
from reflex.state import BaseState
from reflex.style import SYSTEM_COLOR_MODE
from reflex.utils import console, path_ops
from reflex.utils.exec import is_prod_mode
from reflex.utils.imports import ImportVar
from reflex.utils.prerequisites import get_web_dir
Expand Down Expand Up @@ -189,18 +190,74 @@ def _compile_root_stylesheet(stylesheets: list[str]) -> str:
if get_config().tailwind is not None
else []
)

failed_to_import_sass = False
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("/")
)
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('/')}"

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

if (
stylesheet_full_path.suffix[1:].lower()
in constants.Reflex.STYLESHEETS_SUPPORTED
):
target = (
Path.cwd()
/ constants.Dirs.WEB
/ constants.Dirs.STYLES
/ (stylesheet.rsplit(".", 1)[0].strip("/") + ".css")
)
target.parent.mkdir(parents=True, exist_ok=True)

if stylesheet_full_path.suffix == ".css":
path_ops.cp(src=stylesheet_full_path, dest=target, overwrite=True)
else:
try:
from sass import compile as sass_compile

target.write_text(
data=sass_compile(
filename=str(stylesheet_full_path),
output_style="compressed",
),
encoding="utf8",
)
except ImportError:
failed_to_import_sass = True
else:
raise FileNotFoundError(
f'The stylesheet file "{stylesheet_full_path}" is not a valid file.'
)

stylesheet = f"./{stylesheet.rsplit('.', 1)[0].strip('/')}.css"

sheets.append(stylesheet) if stylesheet not in sheets else None

if failed_to_import_sass:
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 @@ -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."""
Expand Down
3 changes: 1 addition & 2 deletions reflex/utils/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,10 @@ def setup_frontend(
"""
# Create the assets dir if it doesn't exist.
path_ops.mkdir(constants.Dirs.APP_ASSETS)

# Copy asset files to public folder.
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),
)

# 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
82 changes: 78 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,82 @@ def test_compile_stylesheets(tmp_path, mocker):
"@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/[email protected]/dist/css/bootstrap.min.css'); \n"
"@import url('../public/styles.css'); \n"
"@import url('./styles.css'); \n"
"@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"),
"@import url('./tailwind.css'); \n"
"@import url('./styles.css'); \n"
"@import url('./preprocess/styles_a.css'); \n"
"@import url('./preprocess/styles_b.css'); \n",
)

stylesheets = [
"/styles.css",
"/preprocess", # this is a folder containing "styles_a.sass" and "styles_b.scss"
]

assert compiler.compile_root_stylesheet(stylesheets) == (
str(Path(".web") / "styles" / "styles.css"),
"@import url('./tailwind.css'); \n"
"@import url('./styles.css'); \n"
"@import url('./preprocess/styles_b.css'); \n"
"@import url('./preprocess/styles_a.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 +239,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
Loading