Skip to content
Merged
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/7317.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix stylesheet edits in `assets/` not applying in dev mode until a manual reload. The global stylesheet `<link rel="preload">` is now emitted only in production builds, so Vite's CSS hot update swaps the real stylesheet link again.
17 changes: 14 additions & 3 deletions reflex/compiler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from reflex.istate.storage import Cookie, LocalStorage, SessionStorage
from reflex.state import BaseState
from reflex.utils import path_ops
from reflex.utils.exec import is_prod_mode
from reflex.utils.prerequisites import get_web_dir

if TYPE_CHECKING:
Expand Down Expand Up @@ -796,11 +797,21 @@ def create_document_root(
}
),
)
# Always include the framework meta and link tags.
# Always include the framework meta and link tags. The preload hint is a
# production-only optimization: in dev, Vite's css-update swaps the first
# link matching the stylesheet path, so it must be the stylesheet link.
always_head_components = [
ReactMeta.create(),
Link.create(
rel="preload", custom_attrs={"as": "style"}, href=global_styles_href
*(
[
Link.create(
rel="preload",
custom_attrs={"as": "style"},
href=global_styles_href,
)
]
if is_prod_mode()
else []
),
Link.create(
rel="stylesheet",
Expand Down
30 changes: 21 additions & 9 deletions tests/units/compiler/test_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,17 @@ def test_compile_nonexistent_stylesheet(tmp_path, mocker: MockerFixture):
compiler.compile_root_stylesheet(stylesheets)


@pytest.fixture
def dev_mode(monkeypatch: pytest.MonkeyPatch) -> None:
"""Pin dev mode, whose document head omits the stylesheet preload.

Args:
monkeypatch: Selects dev mode and restores the previous mode afterwards.
"""
monkeypatch.setenv("REFLEX_ENV_MODE", constants.Env.DEV.value)


@pytest.mark.usefixtures("dev_mode")
def test_create_document_root():
"""Test that the document root is created correctly."""
# Test with no components.
Expand All @@ -773,7 +784,7 @@ def test_create_document_root():
assert isinstance(lang, LiteralStringVar)
assert lang.equals(Var.create("en"))
# No children in head.
assert len(root.children[0].children) == 7
assert len(root.children[0].children) == 6
Comment thread
FarhanAliRaza marked this conversation as resolved.
Resolved
assert isinstance(root.children[0].children[1], Meta)
char_set = root.children[0].children[1].char_set # pyright: ignore [reportAttributeAccessIssue]
assert isinstance(char_set, LiteralStringVar)
Expand All @@ -784,8 +795,7 @@ def test_create_document_root():
assert name.equals(Var.create("viewport"))
assert isinstance(root.children[0].children[3], document.Meta)
assert isinstance(root.children[0].children[4], Link)
assert isinstance(root.children[0].children[5], Link)
assert isinstance(root.children[0].children[6], Links)
assert isinstance(root.children[0].children[5], Links)


def test_add_meta_accepts_dynamic_description():
Expand Down Expand Up @@ -822,6 +832,7 @@ def test_add_meta_drops_empty_description():
assert not any(isinstance(child, Description) for child in page.children)


@pytest.mark.usefixtures("dev_mode")
def test_create_document_root_with_scripts():
# Test with components.
comps = [
Expand All @@ -834,7 +845,7 @@ def test_create_document_root_with_scripts():
html_custom_attrs={"project": "reflex"},
)
assert isinstance(root, Html)
assert len(root.children[0].children) == 9
assert len(root.children[0].children) == 8
names = [c.tag for c in root.children[0].children]
assert names == [
"script",
Expand All @@ -844,7 +855,6 @@ def test_create_document_root_with_scripts():
"meta",
"Meta",
"link",
"link",
"Links",
]
lang = root.lang # pyright: ignore [reportAttributeAccessIssue]
Expand All @@ -854,6 +864,7 @@ def test_create_document_root_with_scripts():
assert root.custom_attrs == {"project": "reflex"}


@pytest.mark.usefixtures("dev_mode")
def test_create_document_root_with_meta_char_set():
# Test with components.
comps = [
Expand All @@ -863,12 +874,13 @@ def test_create_document_root_with_meta_char_set():
head_components=comps,
)
assert isinstance(root, Html)
assert len(root.children[0].children) == 7
assert len(root.children[0].children) == 6
names = [c.tag for c in root.children[0].children]
assert names == ["script", "meta", "meta", "Meta", "link", "link", "Links"]
assert names == ["script", "meta", "meta", "Meta", "link", "Links"]
assert str(root.children[0].children[1].char_set) == '"cp1252"' # pyright: ignore [reportAttributeAccessIssue]


@pytest.mark.usefixtures("dev_mode")
def test_create_document_root_with_meta_viewport():
# Test with components.
comps = [
Expand All @@ -879,9 +891,9 @@ def test_create_document_root_with_meta_viewport():
head_components=comps,
)
assert isinstance(root, Html)
assert len(root.children[0].children) == 8
assert len(root.children[0].children) == 7
names = [c.tag for c in root.children[0].children]
assert names == ["script", "meta", "meta", "meta", "Meta", "link", "link", "Links"]
assert names == ["script", "meta", "meta", "meta", "Meta", "link", "Links"]
assert str(root.children[0].children[1].http_equiv) == '"refresh"' # pyright: ignore [reportAttributeAccessIssue]
assert str(root.children[0].children[2].name) == '"viewport"' # pyright: ignore [reportAttributeAccessIssue]
assert str(root.children[0].children[2].content) == '"foo"' # pyright: ignore [reportAttributeAccessIssue]
Expand Down
37 changes: 34 additions & 3 deletions tests/units/compiler/test_compiler_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from copy import deepcopy

import pytest
from reflex_base import constants
from reflex_base.components.memo import create_passthrough_component_memo
from reflex_base.registry import RegistrationContext
from reflex_components_core.base.fragment import Fragment
Expand Down Expand Up @@ -126,12 +127,28 @@ def test_restore_bundled_libraries_ignores_missing_artifact(
utils._restore_bundled_libraries()


def test_document_preloads_the_global_stylesheet():
"""Render-blocking CSS should be discoverable alongside early resource hints."""
def _global_stylesheet_links() -> list[list[str]]:
"""Render the framework link tags of a fresh document head.

Returns:
The rendered props of every ``Link`` in the head.
"""
head = create_document_root().children[0]
links = [
return [
child.render()["props"] for child in head.children if isinstance(child, Link)
]


def test_document_preloads_the_global_stylesheet_in_prod(
monkeypatch: pytest.MonkeyPatch,
):
"""Production builds hint the render-blocking CSS ahead of the stylesheet link.

Args:
monkeypatch: Selects prod mode and restores the previous mode afterwards.
"""
monkeypatch.setenv("REFLEX_ENV_MODE", constants.Env.PROD.value)
links = _global_stylesheet_links()
preload = next(props for props in links if 'rel:"preload"' in props)
stylesheet = next(props for props in links if 'rel:"stylesheet"' in props)
assert next(prop for prop in preload if prop.startswith("href:")) == next(
Expand All @@ -140,6 +157,20 @@ def test_document_preloads_the_global_stylesheet():
assert 'as:"style"' in preload


def test_document_does_not_preload_the_global_stylesheet_in_dev(
monkeypatch: pytest.MonkeyPatch,
):
"""Dev builds link the stylesheet once so Vite's css-update swaps that link.

Args:
monkeypatch: Selects dev mode and restores the previous mode afterwards.
"""
monkeypatch.setenv("REFLEX_ENV_MODE", constants.Env.DEV.value)
links = _global_stylesheet_links()
Comment thread
FarhanAliRaza marked this conversation as resolved.
Resolved
assert not any('rel:"preload"' in props for props in links)
assert sum('rel:"stylesheet"' in props for props in links) == 1


class CompileStateState(State):
"""State fixture exercising async computed vars during compile_state."""

Expand Down
Loading