diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index e373bbc..0bcc3ed 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -12,4 +12,4 @@ jobs:
- uses: "actions/setup-python@v4"
with:
python-version: 3.x
- - uses: "pre-commit/action@v2.0.3"
+ - uses: "pre-commit/action@v3.0.0"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..5c05bc7
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,31 @@
+name: Run Test Matrix
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+concurrency:
+ group: ${{ github.head_ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install tox tox-gh-actions
+ - name: Test with tox
+ run: tox
diff --git a/.gitignore b/.gitignore
index c18dd8d..3659bc9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,5 @@
__pycache__/
+.tox
+.vscode
+.coverage
+*.egg-info
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e5d7f53..c1391dc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: "https://github.com/pre-commit/pre-commit-hooks"
- rev: "v4.1.0"
+ rev: "v4.4.0"
hooks:
- id: "check-toml"
- id: "check-yaml"
@@ -10,15 +10,10 @@ repos:
- id: "trailing-whitespace"
- repo: "https://github.com/psf/black"
- rev: "22.3.0"
+ rev: "23.3.0"
hooks:
- id: "black"
- - repo: "https://github.com/PyCQA/flake8"
- rev: "4.0.1"
- hooks:
- - id: "flake8"
-
- repo: "https://github.com/commitizen-tools/commitizen"
rev: master
hooks:
diff --git a/django_vite/apps.py b/django_vite/apps.py
index ab781bc..4bc1bdc 100644
--- a/django_vite/apps.py
+++ b/django_vite/apps.py
@@ -1,6 +1,9 @@
+from contextlib import suppress
+
from django.apps import AppConfig
from django.core.checks import Warning, register
+from .exceptions import DjangoViteManifestError
from .templatetags.django_vite import DjangoViteAssetLoader
@@ -9,12 +12,10 @@ class DjangoViteAppConfig(AppConfig):
verbose_name = "Django Vite"
def ready(self) -> None:
- try:
- # Create Loader instance at startup to prevent threading problems.
+ with suppress(DjangoViteManifestError):
+ # Create Loader instance at startup to prevent threading problems,
+ # but do not crash while doing so.
DjangoViteAssetLoader.instance()
- except RuntimeError:
- # Just continue, the system check below outputs a warning.
- pass
@register
@@ -25,7 +26,7 @@ def check_loader_instance(**kwargs):
# Make Loader instance at startup to prevent threading problems
DjangoViteAssetLoader.instance()
return []
- except RuntimeError as exception:
+ except DjangoViteManifestError as exception:
return [
Warning(
exception,
diff --git a/django_vite/exceptions.py b/django_vite/exceptions.py
new file mode 100644
index 0000000..1030ca8
--- /dev/null
+++ b/django_vite/exceptions.py
@@ -0,0 +1,10 @@
+class DjangoViteManifestError(RuntimeError):
+ """Manifest parsing failed."""
+
+ pass
+
+
+class DjangoViteAssetNotFoundError(RuntimeError):
+ """Vite Asset could not be found."""
+
+ pass
diff --git a/django_vite/templatetags/django_vite.py b/django_vite/templatetags/django_vite.py
index 0941cbc..fdb3304 100644
--- a/django_vite/templatetags/django_vite.py
+++ b/django_vite/templatetags/django_vite.py
@@ -1,6 +1,6 @@
import json
from pathlib import Path
-from typing import Dict, List, Callable
+from typing import Callable, Dict, List
from urllib.parse import urljoin
from django import template
@@ -8,8 +8,9 @@
from django.conf import settings
from django.utils.safestring import mark_safe
-register = template.Library()
+from django_vite.exceptions import DjangoViteAssetNotFoundError, DjangoViteManifestError
+register = template.Library()
# If using in development or production mode.
DJANGO_VITE_DEV_MODE = getattr(settings, "DJANGO_VITE_DEV_MODE", False)
@@ -25,9 +26,7 @@
)
# Default Vite server port.
-DJANGO_VITE_DEV_SERVER_PORT = getattr(
- settings, "DJANGO_VITE_DEV_SERVER_PORT", 3000
-)
+DJANGO_VITE_DEV_SERVER_PORT = getattr(settings, "DJANGO_VITE_DEV_SERVER_PORT", 3000)
# Default Vite server path to HMR script.
DJANGO_VITE_WS_CLIENT_URL = getattr(
@@ -42,12 +41,10 @@
# Must be included in your "STATICFILES_DIRS".
# In Django production mode this folder need to be collected as static
# files using "python manage.py collectstatic".
-DJANGO_VITE_ASSETS_PATH = Path(getattr(settings, "DJANGO_VITE_ASSETS_PATH"))
+DJANGO_VITE_ASSETS_PATH = Path(settings.DJANGO_VITE_ASSETS_PATH)
# Prefix for STATIC_URL
-DJANGO_VITE_STATIC_URL_PREFIX = getattr(
- settings, "DJANGO_VITE_STATIC_URL_PREFIX", ""
-)
+DJANGO_VITE_STATIC_URL_PREFIX = getattr(settings, "DJANGO_VITE_STATIC_URL_PREFIX", "")
DJANGO_VITE_STATIC_ROOT = (
DJANGO_VITE_ASSETS_PATH
@@ -68,9 +65,7 @@
settings, "DJANGO_VITE_LEGACY_POLYFILLS_MOTIF", "legacy-polyfills"
)
-DJANGO_VITE_STATIC_URL = urljoin(
- settings.STATIC_URL, DJANGO_VITE_STATIC_URL_PREFIX
-)
+DJANGO_VITE_STATIC_URL = urljoin(settings.STATIC_URL, DJANGO_VITE_STATIC_URL_PREFIX)
# Make sure 'DJANGO_VITE_STATIC_URL' finish with a '/'
if DJANGO_VITE_STATIC_URL[-1] != "/":
@@ -110,7 +105,7 @@ def generate_vite_asset(
script tags.
Raises:
- RuntimeError: If cannot find the file path in the
+ DjangoViteAssetNotFoundError: If cannot find the file path in the
manifest (only in production).
Returns:
@@ -125,7 +120,7 @@ def generate_vite_asset(
)
if not self._manifest or path not in self._manifest:
- raise RuntimeError(
+ raise DjangoViteAssetNotFoundError(
f"Cannot find {path} in Vite manifest "
f"at {DJANGO_VITE_MANIFEST_PATH}"
)
@@ -158,9 +153,7 @@ def generate_vite_asset(
for dep in manifest_entry.get("imports", []):
dep_manifest_entry = self._manifest[dep]
dep_file = dep_manifest_entry["file"]
- url = DjangoViteAssetLoader._generate_production_server_url(
- dep_file
- )
+ url = DjangoViteAssetLoader._generate_production_server_url(dep_file)
tags.append(
DjangoViteAssetLoader._generate_preload_tag(
url,
@@ -188,7 +181,7 @@ def preload_vite_asset(
str -- All tags to preload this file in your HTML page.
Raises:
- RuntimeError: If cannot find the file path in the
+ DjangoViteAssetNotFoundError: if cannot find the file path in the
manifest.
Returns:
@@ -199,7 +192,7 @@ def preload_vite_asset(
return ""
if not self._manifest or path not in self._manifest:
- raise RuntimeError(
+ raise DjangoViteAssetNotFoundError(
f"Cannot find {path} in Vite manifest "
f"at {DJANGO_VITE_MANIFEST_PATH}"
)
@@ -216,9 +209,7 @@ def preload_vite_asset(
}
manifest_file = manifest_entry["file"]
- url = DjangoViteAssetLoader._generate_production_server_url(
- manifest_file
- )
+ url = DjangoViteAssetLoader._generate_production_server_url(manifest_file)
tags.append(
DjangoViteAssetLoader._generate_preload_tag(
url,
@@ -233,9 +224,7 @@ def preload_vite_asset(
for dep in manifest_entry.get("imports", []):
dep_manifest_entry = self._manifest[dep]
dep_file = dep_manifest_entry["file"]
- url = DjangoViteAssetLoader._generate_production_server_url(
- dep_file
- )
+ url = DjangoViteAssetLoader._generate_production_server_url(dep_file)
tags.append(
DjangoViteAssetLoader._generate_preload_tag(
url,
@@ -291,10 +280,8 @@ def _generate_css_files_of_asset(
if "css" in manifest_entry:
for css_path in manifest_entry["css"]:
if css_path not in already_processed:
- url = (
- DjangoViteAssetLoader._generate_production_server_url(
- css_path
- )
+ url = DjangoViteAssetLoader._generate_production_server_url(
+ css_path
)
tags.append(tag_generator(url))
@@ -311,7 +298,7 @@ def generate_vite_asset_url(self, path: str) -> str:
path {str} -- Path to a Vite asset.
Raises:
- RuntimeError: If cannot find the asset path in the
+ DjangoViteAssetNotFoundError: If cannot find the asset path in the
manifest (only in production).
Returns:
@@ -322,7 +309,7 @@ def generate_vite_asset_url(self, path: str) -> str:
return DjangoViteAssetLoader._generate_vite_server_url(path)
if not self._manifest or path not in self._manifest:
- raise RuntimeError(
+ raise DjangoViteAssetNotFoundError(
f"Cannot find {path} in Vite manifest "
f"at {DJANGO_VITE_MANIFEST_PATH}"
)
@@ -346,7 +333,7 @@ def generate_vite_legacy_polyfills(
script tags.
Raises:
- RuntimeError: If polyfills path not found inside
+ DjangoViteAssetNotFoundError: If polyfills path not found inside
the 'manifest.json' (only in production).
Returns:
@@ -367,7 +354,7 @@ def generate_vite_legacy_polyfills(
attrs=scripts_attrs,
)
- raise RuntimeError(
+ raise DjangoViteAssetNotFoundError(
f"Vite legacy polyfills not found in manifest "
f"at {DJANGO_VITE_MANIFEST_PATH}"
)
@@ -391,7 +378,7 @@ def generate_vite_legacy_asset(
script tags.
Raises:
- RuntimeError: If cannot find the asset path in the
+ DjangoViteAssetNotFoundError: If cannot find the asset path in the
manifest (only in production).
Returns:
@@ -402,7 +389,7 @@ def generate_vite_legacy_asset(
return ""
if not self._manifest or path not in self._manifest:
- raise RuntimeError(
+ raise DjangoViteAssetNotFoundError(
f"Cannot find {path} in Vite manifest "
f"at {DJANGO_VITE_MANIFEST_PATH}"
)
@@ -422,19 +409,19 @@ def _parse_manifest(self) -> None:
Read and parse the Vite manifest file.
Raises:
- RuntimeError: if cannot load the file or JSON in file is malformed.
+ DjangoViteManifestError: if cannot load the file or JSON in file is
+ malformed.
"""
try:
- manifest_file = open(DJANGO_VITE_MANIFEST_PATH, "r")
- manifest_content = manifest_file.read()
- manifest_file.close()
+ with open(DJANGO_VITE_MANIFEST_PATH, "r") as manifest_file:
+ manifest_content = manifest_file.read()
self._manifest = json.loads(manifest_content)
except Exception as error:
- raise RuntimeError(
+ raise DjangoViteManifestError(
f"Cannot read Vite manifest file at "
f"{DJANGO_VITE_MANIFEST_PATH} : {str(error)}"
- )
+ ) from error
@classmethod
def instance(cls):
@@ -496,9 +483,7 @@ def _generate_script_tag(src: str, attrs: Dict[str, str]) -> str:
str -- The script tag.
"""
- attrs_str = " ".join(
- [f'{key}="{value}"' for key, value in attrs.items()]
- )
+ attrs_str = " ".join([f'{key}="{value}"' for key, value in attrs.items()])
return f''
@@ -532,9 +517,7 @@ def _generate_stylesheet_preload_tag(href: str) -> str:
@staticmethod
def _generate_preload_tag(href: str, attrs: Dict[str, str]) -> str:
- attrs_str = " ".join(
- [f'{key}="{value}"' for key, value in attrs.items()]
- )
+ attrs_str = " ".join([f'{key}="{value}"' for key, value in attrs.items()])
return f''
@@ -682,7 +665,6 @@ def vite_preload_asset(
manifest (only in production).
"""
-
assert path is not None
return DjangoViteAssetLoader.instance().preload_vite_asset(path)
@@ -731,9 +713,7 @@ def vite_legacy_polyfills(**kwargs: Dict[str, str]) -> str:
str -- The script tag to the polyfills.
"""
- return DjangoViteAssetLoader.instance().generate_vite_legacy_polyfills(
- **kwargs
- )
+ return DjangoViteAssetLoader.instance().generate_vite_legacy_polyfills(**kwargs)
@register.simple_tag
@@ -765,9 +745,7 @@ def vite_legacy_asset(
assert path is not None
- return DjangoViteAssetLoader.instance().generate_vite_legacy_asset(
- path, **kwargs
- )
+ return DjangoViteAssetLoader.instance().generate_vite_legacy_asset(path, **kwargs)
@register.simple_tag
diff --git a/pyproject.toml b/pyproject.toml
index a8f43fe..02d02fa 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,2 +1,31 @@
+[tool.pytest.ini_options]
+DJANGO_SETTINGS_MODULE = "tests.settings"
+django_find_project = false
+pythonpath = "."
+addopts = '''
+ --cov=django_vite
+ --cov-report html
+ --cov-report term-missing
+ --cov-branch
+'''
+
[tool.black]
-line-length = 79
+line-length = 88
+
+[tool.ruff]
+select = [
+ "E", # pycodestyle
+ "F", # pyflakes
+ "C", # pyupgrade
+ "B", # bugbear,
+ "PT", # pytest,
+ "SIM", # simplify,
+ "DJ", # django,
+ "I", # isort
+]
+
+# do not autofix the following violations due to bad DX:
+unfixable = [
+ "F401", # Module imported but unused
+ "F841", # Unused variables
+]
diff --git a/setup.py b/setup.py
index bbfd4b1..8b39525 100644
--- a/setup.py
+++ b/setup.py
@@ -11,7 +11,7 @@
setup(
name="django-vite",
version="2.1.3",
- description="Integration of ViteJS in a Django project.",
+ description="Integration of Vite in a Django project.",
long_description=README,
long_description_content_type="text/markdown",
author="MrBin99",
@@ -20,13 +20,24 @@
include_package_data=True,
packages=find_packages(),
requires=[
- "Django (>=1.11)",
+ "Django (>=3.2)",
],
install_requires=[
- "Django>=1.11",
+ "Django>=3.2",
],
classifiers=[
"License :: OSI Approved :: Apache Software License",
+ "Framework :: Django",
+ "Framework :: Django :: 3.2",
+ "Framework :: Django :: 4.0",
+ "Framework :: Django :: 4.1",
+ "Framework :: Django :: 4.2",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
],
- extras_require={"dev": ["black", "flake8"]},
+ extras_require={"dev": ["black"]},
)
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..2eedec1
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,46 @@
+from typing import Dict, Any
+import pytest
+from importlib import reload
+from django.apps import apps
+from django_vite.templatetags import django_vite
+
+
+def reload_django_vite():
+ reload(django_vite)
+ django_vite_app_config = apps.get_app_config("django_vite")
+ django_vite_app_config.ready()
+
+
+@pytest.fixture()
+def patch_settings(settings):
+ """
+ 1. Patch new settings into django.conf.settings.
+ 2. Reload django_vite module so that variables on the module level that use settings
+ get recalculated.
+ 3. Restore the original settings once the test is over.
+
+ TODO: refactor django_vite so that we don't set variables on the module level using
+ settings.
+ """
+ __PYTEST_EMPTY__ = "__PYTEST_EMPTY__"
+ original_settings_cache = {}
+
+ def _patch_settings(new_settings: Dict[str, Any]):
+ for key, value in new_settings.items():
+ original_settings_cache[key] = getattr(settings, key, __PYTEST_EMPTY__)
+ setattr(settings, key, value)
+ reload_django_vite()
+
+ yield _patch_settings
+
+ for key, value in original_settings_cache.items():
+ if value == __PYTEST_EMPTY__:
+ delattr(settings, key)
+ else:
+ setattr(settings, key, value)
+ reload_django_vite()
+
+
+@pytest.fixture()
+def dev_mode_off(patch_settings):
+ patch_settings({"DJANGO_VITE_DEV_MODE": False})
diff --git a/tests/data/staticfiles/custom-motif-polyfills-manifest.json b/tests/data/staticfiles/custom-motif-polyfills-manifest.json
new file mode 100644
index 0000000..c954ade
--- /dev/null
+++ b/tests/data/staticfiles/custom-motif-polyfills-manifest.json
@@ -0,0 +1,24 @@
+{
+ "../../vite/custom-motif-legacy": {
+ "file": "assets/polyfills-legacy-6e7a4b9c.js",
+ "isEntry": true,
+ "src": "../../vite/custom-motif-legacy"
+ },
+ "src/entry-legacy.ts": {
+ "file": "assets/entry-legacy-4c50596f.js",
+ "isEntry": true,
+ "src": "src/entry-legacy.ts"
+ },
+ "src/entry.css": {
+ "file": "assets/entry-5e7d9c21.css",
+ "src": "src/entry.css"
+ },
+ "src/entry.ts": {
+ "css": [
+ "assets/entry-5e7d9c21.css"
+ ],
+ "file": "assets/entry-8a2f6b3d.js",
+ "isEntry": true,
+ "src": "src/entry.ts"
+ }
+}
diff --git a/tests/data/staticfiles/custom/prefix/manifest.json b/tests/data/staticfiles/custom/prefix/manifest.json
new file mode 100644
index 0000000..fbe4a74
--- /dev/null
+++ b/tests/data/staticfiles/custom/prefix/manifest.json
@@ -0,0 +1,72 @@
+{
+ "src/entry.ts": {
+ "css": ["assets/entry-74d0d5dd.css"],
+ "file": "assets/entry-29e38a60.js",
+ "imports": [
+ "_vue.esm-bundler-96356fb1.js",
+ "_index-62f37ad0.js",
+ "_vue.esm-bundler-49e6b475.js",
+ "__plugin-vue_export-helper-c27b6911.js",
+ "_messages-a0a9e13b.js",
+ "_use-outside-click-224980bf.js",
+ "_use-resolve-button-type-c5656cba.js",
+ "_use-event-listener-153dc639.js",
+ "_hidden-84ddb9a5.js",
+ "_apiClient-01d20438.js",
+ "_pinia-5d7892fd.js"
+ ],
+ "isEntry": true,
+ "src": "entry.ts"
+ },
+ "src/entry.css": {
+ "file": "assets/entry-74d0d5dd.css",
+ "src": "entry.css"
+ },
+ "src/extra.css": {
+ "file": "assets/extra-a9f3b2c1.css",
+ "src": "extra.css"
+ },
+ "_vue.esm-bundler-96356fb1.js": {
+ "file": "vue.esm-bundler-96356fb1.js"
+ },
+ "_index-62f37ad0.js": {
+ "file": "index-62f37ad0.js",
+ "imports": ["_vue.esm-bundler-49e6b475.js"]
+ },
+ "_vue.esm-bundler-49e6b475.js": {
+ "file": "vue.esm-bundler-49e6b475.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js"]
+ },
+ "__plugin-vue_export-helper-c27b6911.js": {
+ "file": "_plugin-vue_export-helper-c27b6911.js"
+ },
+ "_messages-a0a9e13b.js": {
+ "css": ["assets/extra-a9f3b2c1.css"],
+ "file": "messages-a0a9e13b.js",
+ "imports": ["_pinia-5d7892fd.js", "_vue.esm-bundler-96356fb1.js"]
+ },
+ "_pinia-5d7892fd.js": {
+ "file": "pinia-5d7892fd.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js"]
+ },
+ "_use-outside-click-224980bf.js": {
+ "file": "use-outside-click-224980bf.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js"]
+ },
+ "_use-resolve-button-type-c5656cba.js": {
+ "file": "use-resolve-button-type-c5656cba.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"]
+ },
+ "_use-event-listener-153dc639.js": {
+ "file": "use-event-listener-153dc639.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"]
+ },
+ "_hidden-84ddb9a5.js": {
+ "file": "hidden-84ddb9a5.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"]
+ },
+ "_apiClient-01d20438.js": {
+ "css": ["assets/extra-a9f3b2c1.css"],
+ "file": "apiClient-01d20438.js"
+ }
+}
diff --git a/tests/data/staticfiles/manifest.json b/tests/data/staticfiles/manifest.json
new file mode 100644
index 0000000..fbe4a74
--- /dev/null
+++ b/tests/data/staticfiles/manifest.json
@@ -0,0 +1,72 @@
+{
+ "src/entry.ts": {
+ "css": ["assets/entry-74d0d5dd.css"],
+ "file": "assets/entry-29e38a60.js",
+ "imports": [
+ "_vue.esm-bundler-96356fb1.js",
+ "_index-62f37ad0.js",
+ "_vue.esm-bundler-49e6b475.js",
+ "__plugin-vue_export-helper-c27b6911.js",
+ "_messages-a0a9e13b.js",
+ "_use-outside-click-224980bf.js",
+ "_use-resolve-button-type-c5656cba.js",
+ "_use-event-listener-153dc639.js",
+ "_hidden-84ddb9a5.js",
+ "_apiClient-01d20438.js",
+ "_pinia-5d7892fd.js"
+ ],
+ "isEntry": true,
+ "src": "entry.ts"
+ },
+ "src/entry.css": {
+ "file": "assets/entry-74d0d5dd.css",
+ "src": "entry.css"
+ },
+ "src/extra.css": {
+ "file": "assets/extra-a9f3b2c1.css",
+ "src": "extra.css"
+ },
+ "_vue.esm-bundler-96356fb1.js": {
+ "file": "vue.esm-bundler-96356fb1.js"
+ },
+ "_index-62f37ad0.js": {
+ "file": "index-62f37ad0.js",
+ "imports": ["_vue.esm-bundler-49e6b475.js"]
+ },
+ "_vue.esm-bundler-49e6b475.js": {
+ "file": "vue.esm-bundler-49e6b475.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js"]
+ },
+ "__plugin-vue_export-helper-c27b6911.js": {
+ "file": "_plugin-vue_export-helper-c27b6911.js"
+ },
+ "_messages-a0a9e13b.js": {
+ "css": ["assets/extra-a9f3b2c1.css"],
+ "file": "messages-a0a9e13b.js",
+ "imports": ["_pinia-5d7892fd.js", "_vue.esm-bundler-96356fb1.js"]
+ },
+ "_pinia-5d7892fd.js": {
+ "file": "pinia-5d7892fd.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js"]
+ },
+ "_use-outside-click-224980bf.js": {
+ "file": "use-outside-click-224980bf.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js"]
+ },
+ "_use-resolve-button-type-c5656cba.js": {
+ "file": "use-resolve-button-type-c5656cba.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"]
+ },
+ "_use-event-listener-153dc639.js": {
+ "file": "use-event-listener-153dc639.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"]
+ },
+ "_hidden-84ddb9a5.js": {
+ "file": "hidden-84ddb9a5.js",
+ "imports": ["_vue.esm-bundler-96356fb1.js", "_use-outside-click-224980bf.js"]
+ },
+ "_apiClient-01d20438.js": {
+ "css": ["assets/extra-a9f3b2c1.css"],
+ "file": "apiClient-01d20438.js"
+ }
+}
diff --git a/tests/data/staticfiles/polyfills-manifest.json b/tests/data/staticfiles/polyfills-manifest.json
new file mode 100644
index 0000000..ddebd6a
--- /dev/null
+++ b/tests/data/staticfiles/polyfills-manifest.json
@@ -0,0 +1,24 @@
+{
+ "../../vite/legacy-polyfills-legacy": {
+ "file": "assets/polyfills-legacy-f4c2b91e.js",
+ "isEntry": true,
+ "src": "../../vite/legacy-polyfills-legacy"
+ },
+ "src/entry-legacy.ts": {
+ "file": "assets/entry-legacy-32083566.js",
+ "isEntry": true,
+ "src": "src/entry-legacy.ts"
+ },
+ "src/entry.css": {
+ "file": "assets/entry-74d0d5dd.css",
+ "src": "src/entry.css"
+ },
+ "src/entry.ts": {
+ "css": [
+ "assets/entry-74d0d5dd.css"
+ ],
+ "file": "assets/entry-2e8a3a7a.js",
+ "isEntry": true,
+ "src": "src/entry.ts"
+ }
+}
diff --git a/tests/settings.py b/tests/settings.py
new file mode 100644
index 0000000..541df85
--- /dev/null
+++ b/tests/settings.py
@@ -0,0 +1,31 @@
+import os
+from pathlib import Path
+
+BASE_DIR = Path(__file__).resolve().parent
+
+STATIC_URL = "/static/"
+USE_TZ = True
+
+INSTALLED_APPS = [
+ "django_vite",
+]
+
+TEMPLATE_DEBUG = True
+TEMPLATE_DIRS = (os.path.join(BASE_DIR, "templates"),)
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": TEMPLATE_DIRS,
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [],
+ "debug": TEMPLATE_DEBUG,
+ },
+ },
+]
+
+STATIC_ROOT = BASE_DIR / "data" / "staticfiles"
+
+DJANGO_VITE_DEV_MODE = True
+DJANGO_VITE_ASSETS_PATH = "/"
diff --git a/tests/tests/templatetags/test_vite_asset.py b/tests/tests/templatetags/test_vite_asset.py
new file mode 100644
index 0000000..7923954
--- /dev/null
+++ b/tests/tests/templatetags/test_vite_asset.py
@@ -0,0 +1,124 @@
+import pytest
+from bs4 import BeautifulSoup
+from django.template import Context, Template, TemplateSyntaxError
+from django_vite.exceptions import DjangoViteAssetNotFoundError
+
+
+def test_vite_asset_returns_dev_tags():
+ template = Template(
+ """
+ {% load django_vite %}
+ {% vite_asset "src/entry.ts" %}
+ """
+ )
+ html = template.render(Context({}))
+ soup = BeautifulSoup(html, "html.parser")
+ script_tag = soup.find("script")
+ assert script_tag["src"] == "http://localhost:3000/static/src/entry.ts"
+ assert script_tag["type"] == "module"
+
+
+@pytest.mark.usefixtures("dev_mode_off")
+def test_vite_asset_returns_production_tags():
+ template = Template(
+ """
+ {% load django_vite %}
+ {% vite_asset "src/entry.ts" %}
+ """
+ )
+ html = template.render(Context({}))
+ soup = BeautifulSoup(html, "html.parser")
+ script_tag = soup.find("script")
+ assert script_tag["src"] == "assets/entry-29e38a60.js"
+ assert script_tag["type"] == "module"
+ links = soup.find_all("link")
+ assert len(links) == 13
+
+
+def test_vite_asset_raises_without_path():
+ with pytest.raises(TemplateSyntaxError):
+ Template(
+ """
+ {% load django_vite %}
+ {% vite_asset %}
+ """
+ )
+
+
+@pytest.mark.usefixtures("dev_mode_off")
+def test_vite_asset_raises_nonexistent_entry():
+ with pytest.raises(DjangoViteAssetNotFoundError):
+ template = Template(
+ """
+ {% load django_vite %}
+ {% vite_asset "src/fake.ts" %}
+ """
+ )
+ template.render(Context({}))
+
+
+@pytest.mark.parametrize("prefix", ["custom/prefix", "custom/prefix/"])
+def test_vite_asset_dev_prefix(prefix, patch_settings):
+ patch_settings(
+ {
+ "DJANGO_VITE_STATIC_URL_PREFIX": prefix,
+ }
+ )
+ template = Template(
+ """
+ {% load django_vite %}
+ {% vite_asset "src/entry.ts" %}
+ """
+ )
+ html = template.render(Context({}))
+ soup = BeautifulSoup(html, "html.parser")
+ script_tag = soup.find("script")
+ assert (
+ script_tag["src"] == "http://localhost:3000/static/custom/prefix/src/entry.ts"
+ )
+ assert script_tag["type"] == "module"
+
+
+@pytest.mark.usefixtures("dev_mode_off")
+@pytest.mark.parametrize("prefix", ["custom/prefix", "custom/prefix/"])
+def test_vite_asset_production_prefix(prefix, patch_settings):
+ patch_settings(
+ {
+ "DJANGO_VITE_STATIC_URL_PREFIX": prefix,
+ }
+ )
+ template = Template(
+ """
+ {% load django_vite %}
+ {% vite_asset "src/entry.ts" %}
+ """
+ )
+ html = template.render(Context({}))
+ soup = BeautifulSoup(html, "html.parser")
+ script_tag = soup.find("script")
+ assert script_tag["src"] == "custom/prefix/assets/entry-29e38a60.js"
+ assert script_tag["type"] == "module"
+ links = soup.find_all("link")
+ assert len(links) == 13
+
+
+@pytest.mark.usefixtures("dev_mode_off")
+def test_vite_asset_production_staticfiles_storage(patch_settings):
+ patch_settings(
+ {
+ "INSTALLED_APPS": ["django_vite", "django.contrib.staticfiles"],
+ }
+ )
+ template = Template(
+ """
+ {% load django_vite %}
+ {% vite_asset "src/entry.ts" %}
+ """
+ )
+ html = template.render(Context({}))
+ soup = BeautifulSoup(html, "html.parser")
+ script_tag = soup.find("script")
+ assert script_tag["src"] == "/static/assets/entry-29e38a60.js"
+ assert script_tag["type"] == "module"
+ links = soup.find_all("link")
+ assert len(links) == 13
diff --git a/tests/tests/templatetags/test_vite_asset_url.py b/tests/tests/templatetags/test_vite_asset_url.py
new file mode 100644
index 0000000..e0eef07
--- /dev/null
+++ b/tests/tests/templatetags/test_vite_asset_url.py
@@ -0,0 +1,43 @@
+import pytest
+from bs4 import BeautifulSoup
+from django.template import Context, Template
+from django_vite.exceptions import DjangoViteAssetNotFoundError
+
+
+def test_vite_asset_url_returns_dev_url():
+ template = Template(
+ """
+ {% load django_vite %}
+