Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: add customizable app_client_class config value #138

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems a bit redundant?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining the ManifestClient as a class variable allows you to create a subclassed DjangoViteAppClient with a separate subclassed ManifestClient. You can look at the test_custom_app_client_class for an example. If we didn't do this, then we'd have no way to overwrite the logic of the ManifestClient without doing module-level monkeypatching.

Thinking this through, we should probably also add other dependency classes like TagGenerator as class variables as well, to allow them to be easily extensible.


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
Loading