Skip to content

Commit 7f7c010

Browse files
committed
Add Vite asset loading code.
1 parent e62954d commit 7f7c010

File tree

3 files changed

+276
-0
lines changed

3 files changed

+276
-0
lines changed

django_vite/__init__.py

Whitespace-only changes.

django_vite/templatetags/__init__.py

Whitespace-only changes.
+276
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import json
2+
from os.path import join as path_join
3+
from typing import Dict, Optional
4+
from urllib.parse import urljoin
5+
6+
from django import template
7+
from django.conf import settings
8+
from django.utils.safestring import mark_safe
9+
10+
register = template.Library()
11+
12+
13+
# If using in development or production mode.
14+
DJANGO_VITE_DEV_MODE = getattr(settings, "DJANGO_VITE_DEV_MODE", False)
15+
16+
# Default Vite server protocol (http or https)
17+
DJANGO_VITE_DEV_SERVER_PROTOCOL = getattr(
18+
settings, "DJANGO_VITE_DEV_SERVER_PROTOCOL", "http"
19+
)
20+
21+
# Default vite server hostname.
22+
DJANGO_VITE_DEV_SERVER_HOST = getattr(
23+
settings, "DJANGO_VITE_DEV_SERVER_HOST", "localhost"
24+
)
25+
26+
# Default Vite server port.
27+
DJANGO_VITE_DEV_SERVER_PORT = getattr(settings, "DJANGO_VITE_DEV_SERVER_PORT", 3000)
28+
29+
# Default Vite server path to HMR script.
30+
DJANGO_VITE_WS_CLIENT_URL = getattr(
31+
settings, "DJANGO_VITE_WS_CLIENT_URL", "@vite/client"
32+
)
33+
34+
# Location of Vite compiled assets (only used in Vite production mode).
35+
# Must be included in your "STATICFILES_DIRS".
36+
# In Django production mode this folder need to be collected as static
37+
# files using "python manage.py collectstatic".
38+
DJANGO_VITE_ASSETS_PATH = getattr(settings, "DJANGO_VITE_ASSETS_PATH")
39+
40+
# Path to your manifest file generated by Vite.
41+
# Should by in "DJANGO_VITE_ASSETS_PATH".
42+
DJANGO_VITE_MANIFEST_PATH = getattr(
43+
settings,
44+
"DJANGO_VITE_MANIFEST_PATH",
45+
path_join(
46+
DJANGO_VITE_ASSETS_PATH if settings.DEBUG else settings.STATIC_ROOT,
47+
"manifest.json",
48+
),
49+
)
50+
51+
52+
class DjangoViteAssetLoader:
53+
"""
54+
Class handling Vite asset loading.
55+
"""
56+
57+
_instance = None
58+
59+
def __init__(self) -> None:
60+
raise RuntimeError("Use the instance() method instead.")
61+
62+
def generate_vite_asset(
63+
self, path: str, scripts_attrs: Optional[Dict[str, str]] = None
64+
) -> str:
65+
"""
66+
Generates all assets include tags for the file in argument.
67+
Generates all scripts tags for this file and all its dependencies
68+
(JS and CSS) by reading the manifest file (for production only).
69+
In development Vite imports all dependencies by itself.
70+
Place this tag in <head> section of yout page
71+
(this function marks automaticaly <script> as "async" and "defer").
72+
73+
Arguments:
74+
path {str} -- Path to a Vite asset to include.
75+
76+
Returns:
77+
str -- All tags to import this asset in yout HTML page.
78+
79+
Keyword Arguments:
80+
scripts_attrs {Optional[Dict[str, str]]} -- Override attributes added to scripts tags. (default: {None})
81+
82+
Raises:
83+
RuntimeError: If cannot find the asset path in the manifest (only in production).
84+
85+
Returns:
86+
str -- All tags to import this asset in yout HTML page.
87+
"""
88+
89+
if DJANGO_VITE_DEV_MODE:
90+
return DjangoViteAssetLoader._generate_script_tag(
91+
DjangoViteAssetLoader._generate_vite_server_url(path),
92+
{"type": "module", "async": "", "defer": ""},
93+
)
94+
95+
if path not in self._manifest:
96+
raise RuntimeError(
97+
f"Cannot find {path} in Vite manifest "
98+
f"at {DJANGO_VITE_MANIFEST_PATH}"
99+
)
100+
101+
tags = []
102+
manifest_entry = self._manifest[path]
103+
scripts_attrs = scripts_attrs or {"async": "", "defer": ""}
104+
105+
# Add dependent CSS
106+
if "css" in manifest_entry:
107+
for css_path in manifest_entry["css"]:
108+
tags.append(
109+
DjangoViteAssetLoader._generate_stylesheet_tag(
110+
urljoin(settings.STATIC_URL, css_path)
111+
)
112+
)
113+
114+
# Add the script by itself
115+
tags.append(
116+
DjangoViteAssetLoader._generate_script_tag(
117+
urljoin(settings.STATIC_URL, manifest_entry["file"]),
118+
attrs=scripts_attrs,
119+
)
120+
)
121+
122+
return "\n".join(tags)
123+
124+
def _parse_manifest(self) -> None:
125+
"""
126+
Read and parse the Vite manifest file.
127+
128+
Raises:
129+
RuntimeError: if cannot load the file or JSON in file is malformed.
130+
"""
131+
132+
try:
133+
manifest_file = open(DJANGO_VITE_MANIFEST_PATH, "r")
134+
manifest_content = manifest_file.read()
135+
manifest_file.close()
136+
self._manifest = json.loads(manifest_content)
137+
except Exception as error:
138+
raise RuntimeError(
139+
f"Cannot read Vite manifest file at "
140+
f"{DJANGO_VITE_MANIFEST_PATH} : {str(error)}"
141+
)
142+
143+
@classmethod
144+
def instance(cls):
145+
"""
146+
Singleton.
147+
Uses singleton to keep parsed manifest in memory after
148+
the first time it's loaded.
149+
150+
Returns:
151+
DjangoViteAssetLoader -- only instance of the class.
152+
"""
153+
154+
if cls._instance is None:
155+
cls._instance = cls.__new__(cls)
156+
cls._instance._manifest = None
157+
158+
# Manifest is only used in production.
159+
if not DJANGO_VITE_DEV_MODE:
160+
cls._instance._parse_manifest()
161+
162+
return cls._instance
163+
164+
@classmethod
165+
def generate_vite_ws_client(cls) -> str:
166+
"""
167+
Generates the script tag for the Vite WS client for HMR.
168+
Only used in development, in production this method returns
169+
an empty string.
170+
171+
Returns:
172+
str -- The script tag or an empty string.
173+
"""
174+
175+
if not DJANGO_VITE_DEV_MODE:
176+
return ""
177+
178+
return cls._generate_script_tag(
179+
cls._generate_vite_server_url(DJANGO_VITE_WS_CLIENT_URL),
180+
{"type": "module"},
181+
)
182+
183+
@staticmethod
184+
def _generate_script_tag(src: str, attrs: Optional[Dict[str, str]] = None) -> str:
185+
"""
186+
Generates an HTML script tag.
187+
188+
Arguments:
189+
src {str} -- Source of the script.
190+
191+
Keyword Arguments:
192+
attrs {Optional[Dict[str, str]]} -- List of custom attributes for the tag (default: {None})
193+
194+
Returns:
195+
str -- The script tag.
196+
"""
197+
198+
attrs_str = (
199+
" ".join([f'{key}="{value}"' for key, value in attrs.items()])
200+
if attrs is not None
201+
else ""
202+
)
203+
204+
return f'<script {attrs_str} src="{src}"></script>'
205+
206+
@staticmethod
207+
def _generate_stylesheet_tag(href: str) -> str:
208+
"""
209+
Generates and HTML <link> stylesheet tag for CSS.
210+
211+
Arguments:
212+
href {str} -- CSS file URL.
213+
214+
Returns:
215+
str -- CSS link tag.
216+
"""
217+
218+
return f'<link rel="stylesheet" href="{href}" />'
219+
220+
@staticmethod
221+
def _generate_vite_server_url(path: Optional[str] = None) -> str:
222+
"""
223+
Generates an URL to and asset served by the Vite development server.
224+
225+
Keyword Arguments:
226+
path {Optional[str]} -- Path to the asset. (default: {None})
227+
228+
Returns:
229+
str -- Full URL to the asset.
230+
"""
231+
232+
return urljoin(
233+
f"{DJANGO_VITE_DEV_SERVER_PROTOCOL}://"
234+
f"{DJANGO_VITE_DEV_SERVER_HOST}:{DJANGO_VITE_DEV_SERVER_PORT}",
235+
urljoin(settings.STATIC_URL, path if path is not None else ""),
236+
)
237+
238+
239+
@register.simple_tag
240+
@mark_safe
241+
def vite_hmr_client() -> str:
242+
"""
243+
Generates the script tag for the Vite WS client for HMR.
244+
Only used in development, in production this method returns
245+
an empty string.
246+
247+
Returns:
248+
str -- The script tag or an empty string.
249+
"""
250+
251+
return DjangoViteAssetLoader.generate_vite_ws_client()
252+
253+
254+
@register.simple_tag
255+
@mark_safe
256+
def vite_asset(path: str, scripts_attrs: Optional[Dict[str, str]] = None) -> str:
257+
"""
258+
Generates all assets include tags for the file in argument.
259+
Generates all scripts tags for this file and all its dependencies
260+
(JS and CSS) by reading the manifest file (for production only).
261+
In development Vite imports all dependencies by itself.
262+
Place this tag in <head> section of yout page
263+
(this function marks automaticaly <script> as "async" and "defer").
264+
265+
Arguments:
266+
path {str} -- Path to a Vite asset to include.
267+
268+
Returns:
269+
str -- All tags to import this asset in yout HTML page.
270+
"""
271+
272+
assert path is not None
273+
274+
return DjangoViteAssetLoader.instance().generate_vite_asset(
275+
path, scripts_attrs=scripts_attrs
276+
)

0 commit comments

Comments
 (0)