Skip to content

Commit

Permalink
add customizable app_client_class config value
Browse files Browse the repository at this point in the history
  • Loading branch information
Niicck committed Jun 21, 2024
1 parent abf0a5d commit 013bb43
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 36 deletions.
77 changes: 45 additions & 32 deletions django_vite/core/asset_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.apps import apps
from django.conf import settings
from django.core.checks import Warning
from django.utils.module_loading import import_string

from django_vite.core.exceptions import (
DjangoViteManifestError,
Expand Down Expand Up @@ -50,6 +51,9 @@ class DjangoViteConfig(NamedTuple):
# Default Vite server path to React RefreshRuntime for @vitejs/plugin-react.
react_refresh_url: str = "@react-refresh"

# The DjangoViteAppClient class to use to parse the manifest and load assets.
app_client_class: str = "django_vite.core.asset_loader.DjangoViteAppClient"


class ManifestEntry(NamedTuple):
"""
Expand Down Expand Up @@ -136,6 +140,14 @@ def check(self) -> List[Warning]:
)
]

def load_manifest(self):
"""
Read the Vite manifest.json file.
"""
with open(self.manifest_path, "r") as manifest_file:
manifest_content = manifest_file.read()
return json.loads(manifest_content)

class ParsedManifestOutput(NamedTuple):
# all entries within the manifest
entries: Dict[str, ManifestEntry] = {}
Expand Down Expand Up @@ -163,22 +175,20 @@ def _parse_manifest(self) -> ParsedManifestOutput:
legacy_polyfills_entry: Optional[ManifestEntry] = None

try:
with open(self.manifest_path, "r") as manifest_file:
manifest_content = manifest_file.read()
manifest_json = json.loads(manifest_content)

for path, manifest_entry_data in manifest_json.items():
filtered_manifest_entry_data = {
key: value
for key, value in manifest_entry_data.items()
if key in ManifestEntry._fields
}
manifest_entry = ManifestEntry(**filtered_manifest_entry_data)
entries[path] = manifest_entry
if self.legacy_polyfills_motif in path:
legacy_polyfills_entry = manifest_entry

return self.ParsedManifestOutput(entries, legacy_polyfills_entry)
manifest = self.load_manifest()

for path, manifest_entry_data in manifest.items():
filtered_manifest_entry_data = {
key: value
for key, value in manifest_entry_data.items()
if key in ManifestEntry._fields
}
manifest_entry = ManifestEntry(**filtered_manifest_entry_data)
entries[path] = manifest_entry
if self.legacy_polyfills_motif in path:
legacy_polyfills_entry = manifest_entry

return self.ParsedManifestOutput(entries, legacy_polyfills_entry)

except Exception as error:
raise DjangoViteManifestError(
Expand Down Expand Up @@ -212,6 +222,8 @@ class DjangoViteAppClient:
DjangoViteConfig provides the arguments for the client.
"""

ManifestClient = ManifestClient

def __init__(
self, config: DjangoViteConfig, app_name: str = DEFAULT_APP_NAME
) -> None:
Expand All @@ -226,9 +238,9 @@ def __init__(
self.ws_client_url = config.ws_client_url
self.react_refresh_url = config.react_refresh_url

self.manifest = ManifestClient(config, app_name)
self.manifest = self.ManifestClient(config, app_name)

def _get_dev_server_url(
def get_dev_server_url(
self,
path: str,
) -> str:
Expand All @@ -251,7 +263,7 @@ def _get_dev_server_url(
urljoin(static_url_base, path),
)

def _get_production_server_url(self, path: str) -> str:
def get_production_server_url(self, path: str) -> str:
"""
Generates an URL to an asset served during production.
Expand Down Expand Up @@ -302,7 +314,7 @@ def generate_vite_asset(
this asset in your page.
"""
if self.dev_mode:
url = self._get_dev_server_url(path)
url = self.get_dev_server_url(path)
return TagGenerator.script(
url,
attrs={"type": "module", **kwargs},
Expand All @@ -316,7 +328,7 @@ def generate_vite_asset(
tags.extend(self._load_css_files_of_asset(path))

# Add the script by itself
url = self._get_production_server_url(manifest_entry.file)
url = self.get_production_server_url(manifest_entry.file)
tags.append(
TagGenerator.script(
url,
Expand All @@ -335,7 +347,7 @@ def generate_vite_asset(
for dep in manifest_entry.imports:
dep_manifest_entry = self.manifest.get(dep)
dep_file = dep_manifest_entry.file
url = self._get_production_server_url(dep_file)
url = self.get_production_server_url(dep_file)
tags.append(
TagGenerator.preload(
url,
Expand Down Expand Up @@ -381,7 +393,7 @@ def preload_vite_asset(
}

manifest_file = manifest_entry.file
url = self._get_production_server_url(manifest_file)
url = self.get_production_server_url(manifest_file)
tags.append(
TagGenerator.preload(
url,
Expand All @@ -396,7 +408,7 @@ def preload_vite_asset(
for dep in manifest_entry.imports:
dep_manifest_entry = self.manifest.get(dep)
dep_file = dep_manifest_entry.file
url = self._get_production_server_url(dep_file)
url = self.get_production_server_url(dep_file)
tags.append(
TagGenerator.preload(
url,
Expand Down Expand Up @@ -461,7 +473,7 @@ def _generate_css_files_of_asset(

for css_path in manifest_entry.css:
if css_path not in already_processed:
url = self._get_production_server_url(css_path)
url = self.get_production_server_url(css_path)
tags.append(tag_generator(url))
already_processed.append(css_path)

Expand All @@ -480,11 +492,11 @@ def generate_vite_asset_url(self, path: str) -> str:
"""

if self.dev_mode:
return self._get_dev_server_url(path)
return self.get_dev_server_url(path)

manifest_entry = self.manifest.get(path)

return self._get_production_server_url(manifest_entry.file)
return self.get_production_server_url(manifest_entry.file)

def generate_vite_legacy_polyfills(
self,
Expand Down Expand Up @@ -520,7 +532,7 @@ def generate_vite_legacy_polyfills(
)

scripts_attrs = {"nomodule": "", "crossorigin": "", **kwargs}
url = self._get_production_server_url(polyfills_manifest_entry.file)
url = self.get_production_server_url(polyfills_manifest_entry.file)

return TagGenerator.script(
url,
Expand Down Expand Up @@ -558,7 +570,7 @@ def generate_vite_legacy_asset(

manifest_entry = self.manifest.get(path)
scripts_attrs = {"nomodule": "", "crossorigin": "", **kwargs}
url = self._get_production_server_url(manifest_entry.file)
url = self.get_production_server_url(manifest_entry.file)

return TagGenerator.script(
url,
Expand All @@ -582,7 +594,7 @@ def generate_vite_ws_client(self, **kwargs: Dict[str, str]) -> str:
if not self.dev_mode:
return ""

url = self._get_dev_server_url(self.ws_client_url)
url = self.get_dev_server_url(self.ws_client_url)

return TagGenerator.script(
url,
Expand All @@ -607,7 +619,7 @@ def generate_vite_react_refresh_url(self, **kwargs: Dict[str, str]) -> str:
if not self.dev_mode:
return ""

url = self._get_dev_server_url(self.react_refresh_url)
url = self.get_dev_server_url(self.react_refresh_url)
attrs_str = attrs_to_str(kwargs)

return f"""<script type="module" {attrs_str}>
Expand Down Expand Up @@ -691,7 +703,8 @@ def _apply_django_vite_settings(cls):
for app_name, config in django_vite_settings.items():
if not isinstance(config, DjangoViteConfig):
config = DjangoViteConfig(**config)
cls._instance._apps[app_name] = DjangoViteAppClient(config, app_name)
app_client_class = import_string(config.app_client_class)
cls._instance._apps[app_name] = app_client_class(config, app_name)

@classmethod
def _apply_legacy_django_vite_settings(cls):
Expand Down
Empty file added tests/__init__.py
Empty file.
Empty file added tests/tests/__init__.py
Empty file.
6 changes: 4 additions & 2 deletions tests/tests/test_asset_loader.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import pytest

from django_vite.core.asset_loader import DjangoViteConfig, ManifestClient
from django_vite.templatetags.django_vite import DjangoViteAssetLoader
from django_vite.core.asset_loader import (
DjangoViteConfig,
DjangoViteAssetLoader,
)
from django_vite.apps import check_loader_instance


Expand Down
72 changes: 72 additions & 0 deletions tests/tests/test_custom_app_client_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import pytest

from django_vite.core.asset_loader import (
DjangoViteAppClient,
DjangoViteAssetLoader,
ManifestClient,
)


def mock_get_manifest_from_url():
"""
Pretend that we're fetching manifest.json from an external source.
"""
return {
"src/mock_external_entry.js": {
"css": ["assets/entry-0ed1a6fd.css"],
"file": "assets/entry-5c085aac.js",
"isEntry": True,
"src": "entry.js",
},
"src/mock_external_entry.css": {
"file": "assets/entry-0ed1a6fd.css",
"src": "entry.css",
},
}


class CustomManifestClient(ManifestClient):
"""
Custom ManifestClient that loads manifest.json from an external source.
"""

def load_manifest(self):
return mock_get_manifest_from_url()


class CustomAppClient(DjangoViteAppClient):
"""
Custom AppClient with a Custom ManifestClient.
"""

ManifestClient = CustomManifestClient


def test_app_client_class(patch_settings):
patch_settings(
{
"DJANGO_VITE": {
"default": {
"app_client_class": "tests.tests.test_custom_app_client_class.CustomAppClient",
}
}
}
)
DjangoViteAssetLoader._instance = None
assert (
"src/mock_external_entry.js"
in DjangoViteAssetLoader.instance()._apps["default"].manifest._entries
)


def test_invalid_app_client_class(patch_settings):
with pytest.raises(ModuleNotFoundError):
patch_settings(
{
"DJANGO_VITE": {
"default": {
"app_client_class": "django_vite.invalid.CustomAppClient",
}
}
}
)
5 changes: 3 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ envlist =
codestyle,
lint,
{py38,py39}-django{32,40,41,42},
{py310,py311}-django{41,42,-latest},
{py312}-django{42,-latest},
{py310,py311}-django{41,42,50,-latest},
{py312}-django{42,50,-latest},
isolated_build = true
minversion = 1.9

Expand All @@ -28,6 +28,7 @@ deps =
django40: Django>=4.0,<4.1
django41: Django>=4.1,<4.2
django42: Django>=4.2,<4.3
django50: Django>=5.0,<5.1
django-latest: https://github.com/django/django/archive/main.tar.gz
commands =
pytest {posargs:tests}
Expand Down

0 comments on commit 013bb43

Please sign in to comment.