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 %} +