File: 0001-defer-rendering-collected-assets.patch

package info (click to toggle)
jinjax 0.60%2Bdfsg-3
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 428 kB
  • sloc: python: 2,865; makefile: 32
file content (148 lines) | stat: -rw-r--r-- 5,409 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
commit 55170f27ecacdb0dc3afc300af2b899cd2cd7658
Author: Dash <dash@wind.garden>
Date:   Sun Aug 17 17:45:12 2025 +0200

    Defer rendering collected assets
    
    Resolves https://github.com/jpsca/jinjax/issues/120.

diff --git a/src/jinjax/catalog.py b/src/jinjax/catalog.py
index 67d0050..fb6984e 100644
--- a/src/jinjax/catalog.py
+++ b/src/jinjax/catalog.py
@@ -155,6 +155,9 @@ class Catalog:
         "use_cache",
         "_cache",
         "_key",
+        # placeholders for delayed asset injection
+        "_assets_placeholder",
+        "_emit_assets_later",
     )
 
     def __init__(
@@ -211,6 +214,9 @@ class Catalog:
 
         self._cache: dict[str, dict] = {}
         self._key = id(self)
+        # prepare delayed asset injection: placeholder and flag
+        self._assets_placeholder = f"@@jinjax_assets_{self._key}@@"
+        self._emit_assets_later = False
 
     def __del__(self) -> None:
         # Safely clean up context variables associated with this catalog
@@ -425,7 +431,11 @@ class Catalog:
         self.collected_css = []
         self.collected_js = []
         self.tmpl_globals = kw.pop("_globals", kw.pop("__globals", None)) or {}
-        return self.irender(__name, caller=caller, **kw)
+        out = self.irender(__name, caller=caller, **kw)
+        if self._emit_assets_later:
+            # inject full assets bundle in place of the placeholder
+            out = self._finalize_assets(out)
+        return out
 
     def irender(
         self,
@@ -564,16 +574,24 @@ class Catalog:
 
     def render_assets(self) -> str:
         """
-        Uses the `collected_css` and `collected_js` lists to generate
-        an HTML fragment with `<link rel="stylesheet" href="{url}">`
-        and `<script type="module" src="{url}"></script>` tags.
+        Placeholder for assets injection. During rendering this emits a
+        unique token; after the full template is rendered, the token is
+        replaced with all collected CSS/JS asset tags, regardless of
+        ordering in the template.
+        """
+        self._emit_assets_later = True
+        return self._assets_placeholder
+
+    def _format_collected_assets(self) -> Markup:
+        """
+        Internal helper to format collected_css and collected_js into
+        HTML <link> and <script> tags.
 
         The URLs are prepended by `root_url` unless they begin with
         "http://" or "https://".
         """
-        html_css = []
-        # Use a set to track rendered URLs to avoid duplicates
-        rendered_urls = set()
+        html_css: list[str] = []
+        rendered_urls: set[str] = set()
 
         for url in self.collected_css:
             if not url.startswith(("http://", "https://")):
@@ -585,7 +603,7 @@ class Catalog:
                 html_css.append(f'<link rel="stylesheet" href="{full_url}">')
                 rendered_urls.add(full_url)
 
-        html_js = []
+        html_js: list[str] = []
         for url in self.collected_js:
             if not url.startswith(("http://", "https://")):
                 full_url = f"{self.root_url}{url}"
@@ -598,6 +616,19 @@ class Catalog:
 
         return Markup("\n".join(html_css + html_js))
 
+    def _finalize_assets(self, html: str) -> str:
+        """
+        Replace the placeholder token in the rendered HTML with the fully
+        formatted asset tags, then reset asset state for the next render.
+        """
+        # format assets fragment and reset state
+        assets_html = str(self._format_collected_assets())
+        self._emit_assets_later = False
+        self.collected_css = []
+        self.collected_js = []
+        # coerce to plain str before replace to avoid Markup.replace escaping
+        return str(html).replace(self._assets_placeholder, assets_html)
+
     # Private
 
     def _fingerprint(self, root: Path, filename: str) -> str:
diff --git a/tests/test_render_assets.py b/tests/test_render_assets.py
index d494aac..f8c3970 100644
--- a/tests/test_render_assets.py
+++ b/tests/test_render_assets.py
@@ -248,3 +248,38 @@ def test_auto_load_assets_for_kebab_cased_names(catalog, folder, autoescape, und
 
     assert  "/static/components/my-component.css" in html
     assert  "/static/components/my-component.js" in html
+
+
+@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
+@pytest.mark.parametrize("autoescape", [True, False])
+def test_render_assets_before_component_usage_injects_assets(catalog, folder, autoescape, undefined):
+    """
+    Ensure that calling render_assets() before any component usage still
+    collects and renders the component assets, irrespective of ordering
+    in the template.
+    """
+    catalog.jinja_env.autoescape = autoescape
+    catalog.jinja_env.undefined = undefined
+
+    # Define a simple component with associated JS asset
+    (folder / "TestComponent.jinja").write_text("<div>Test</div>")
+    (folder / "TestComponent.js").touch()
+
+    # Template invokes render_assets() before using the component
+    (folder / "IssueExample.jinja").write_text(
+        """
+<!DOCTYPE html>
+<html>
+<head>
+    {{ catalog.render_assets() }}
+</head>
+<body>
+    <TestComponent />
+</body>
+</html>
+"""
+    )
+
+    html = catalog.render("IssueExample")
+    # Assets are injected even though render_assets() appears before the component
+    assert "/static/components/TestComponent.js" in html