Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions news/6725.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed nested/subfolder stylesheets failing to load on Windows because the generated CSS `@import` used backslash path separators (which CSS treats as escape sequences); the import URL is now always POSIX-normalized.
6 changes: 5 additions & 1 deletion reflex/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,11 @@ def _compile_root_stylesheet(
except ImportError:
failed_to_import_sass = True

str_target_path = "./" + str(target_path)
# Use POSIX separators: this string is emitted verbatim into a CSS
# `@import url(...)`, where a backslash is an escape introducer, not a
# path separator, so `str(target_path)` would break nested stylesheets
# on Windows.
str_target_path = "./" + target_path.as_posix()
sheets.append(str_target_path) if str_target_path not in sheets else None

if failed_to_import_sass:
Expand Down
59 changes: 54 additions & 5 deletions tests/units/compiler/test_compiler.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import importlib.util
import os
from pathlib import Path
from pathlib import Path, PureWindowsPath

import pytest
from pytest_mock import MockerFixture
Expand Down Expand Up @@ -231,8 +231,8 @@ def test_compile_stylesheets_scss_sass(tmp_path: Path, mocker: MockerFixture):
"@layer __reflex_base;\n"
"@import url('./__reflex_style_reset.css');\n"
"@import url('./style.css');\n"
f"@import url('./{Path('preprocess') / Path('styles_a.css')!s}');\n"
f"@import url('./{Path('preprocess') / Path('styles_b.css')!s}');"
"@import url('./preprocess/styles_a.css');\n"
"@import url('./preprocess/styles_b.css');"
),
)

Expand All @@ -252,8 +252,8 @@ def test_compile_stylesheets_scss_sass(tmp_path: Path, mocker: MockerFixture):
"@layer __reflex_base;\n"
"@import url('./__reflex_style_reset.css');\n"
"@import url('./style.css');\n"
f"@import url('./{Path('preprocess') / Path('styles_a.css')!s}');\n"
f"@import url('./{Path('preprocess') / Path('styles_b.css')!s}');"
"@import url('./preprocess/styles_a.css');\n"
"@import url('./preprocess/styles_b.css');"
),
)

Expand All @@ -270,6 +270,55 @@ def test_compile_stylesheets_scss_sass(tmp_path: Path, mocker: MockerFixture):
).read_text() == expected_result


def test_compile_stylesheets_nested_import_uses_posix_separators(
tmp_path: Path, mocker: MockerFixture
):
"""Nested stylesheet imports must use forward slashes on every platform.

A CSS `@import url(...)` treats a backslash as an escape introducer, not a
path separator, so emitting `str(target_path)` breaks nested stylesheets on
Windows (where the relative path is backslash-separated). Simulate that by
forcing the relative path to a `PureWindowsPath` and assert the emitted
import is POSIX-normalized.

Args:
tmp_path: The test directory.
mocker: Pytest mocker object.
"""
project = tmp_path / "test_project"
project.mkdir()

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

nested_dir = assets_dir / "subdir"
nested_dir.mkdir()
(nested_dir / "theme.css").write_text(".root { color: red; }")

mocker.patch("reflex.compiler.compiler.Path.cwd", return_value=project)
mocker.patch(
"reflex.compiler.compiler.get_web_dir",
return_value=project / constants.Dirs.WEB,
)
mocker.patch(
"reflex.compiler.utils.get_web_dir", return_value=project / constants.Dirs.WEB
)

real_relative_to = Path.relative_to
mocker.patch.object(
Path,
"relative_to",
lambda self, *args, **kwargs: PureWindowsPath(
real_relative_to(self, *args, **kwargs)
),
)

_, code = compiler.compile_root_stylesheet(["/subdir/theme.css"])

assert "@import url('./subdir/theme.css');" in code
assert "\\" not in code


def test_compile_stylesheets_exclude_tailwind(tmp_path, mocker: MockerFixture):
"""Test that Tailwind is excluded if tailwind config is explicitly set to None.

Expand Down
Loading