From 1ecbed86ace75c69c791d5371d0301c280384290 Mon Sep 17 00:00:00 2001 From: Niicck Date: Mon, 16 Oct 2023 15:15:46 -0700 Subject: [PATCH] enable multi-app configs with DjangoViteAppClient --- django_vite/__init__.py | 4 + django_vite/exceptions.py | 6 + django_vite/loader.py | 1032 +++++++++-------------- django_vite/tag_generator.py | 915 +------------------- django_vite/templatetags/django_vite.py | 804 +----------------- pyproject.toml | 1 + tests/conftest.py | 16 +- tests/tests/test_asset_loader.py | 15 +- 8 files changed, 469 insertions(+), 2324 deletions(-) diff --git a/django_vite/__init__.py b/django_vite/__init__.py index e69de29..09f8c32 100644 --- a/django_vite/__init__.py +++ b/django_vite/__init__.py @@ -0,0 +1,4 @@ +from .loader import DjangoViteConfig + + +__all__ = ["DjangoViteConfig"] diff --git a/django_vite/exceptions.py b/django_vite/exceptions.py index 1030ca8..ec13a28 100644 --- a/django_vite/exceptions.py +++ b/django_vite/exceptions.py @@ -8,3 +8,9 @@ class DjangoViteAssetNotFoundError(RuntimeError): """Vite Asset could not be found.""" pass + + +class DjangoViteConfigNotFoundError(RuntimeError): + """DjangoViteConfig not found in DJANGO_VITE settings.""" + + pass diff --git a/django_vite/loader.py b/django_vite/loader.py index c1e9956..75bfeb1 100644 --- a/django_vite/loader.py +++ b/django_vite/loader.py @@ -1,40 +1,27 @@ import json from pathlib import Path -from typing import Dict, List, Callable, NamedTuple, Optional, Type, Union +from typing import Dict, List, Callable, NamedTuple, Optional, Union, Tuple from urllib.parse import urljoin +import warnings -from django import template from django.apps import apps from django.conf import settings -from django.utils.safestring import mark_safe -from django_vite.exceptions import DjangoViteAssetNotFoundError, DjangoViteManifestError +from django_vite.exceptions import ( + DjangoViteManifestError, + DjangoViteAssetNotFoundError, + DjangoViteConfigNotFoundError, +) +from django_vite.tag_generator import Tag, TagGenerator -DEFAULT_CONFIG_KEY = "default" - -register = template.Library() - - -class DjangoViteManifest(NamedTuple): - """ - Represent an entry for a file inside the "manifest.json". - """ - - file: str - src: Optional[str] = None - isEntry: Optional[bool] = False - css: Optional[List[str]] = [] - imports: Optional[List[str]] = [] +DEFAULT_APP_NAME = "default" class DjangoViteConfig(NamedTuple): """ - Represent the Django Vite configuration structure. + The arguments for a DjangoViteAppClient. """ - # Location of Vite compiled assets (only used in Vite production mode). - assets_path: Union[Path, str] - # If using in development or production mode. dev_mode: bool = False @@ -47,75 +34,201 @@ class DjangoViteConfig(NamedTuple): # Default Vite server port. dev_server_port: int = 3000 - # Default Vite server path to HMR script. - ws_client_url: str = "@vite/client" - # Prefix for STATIC_URL. static_url_prefix: str = "" + # Path to your manifest file generated by Vite. + manifest_path: Optional[Union[Path, str]] = None + # Motif in the "manifest.json" to find the polyfills generated by Vite. legacy_polyfills_motif: str = "legacy-polyfills" - # Path to your manifest file generated by Vite. - manifest_path: Union[Path, str] = "" + # Default Vite server path to HMR script. + ws_client_url: str = "@vite/client" # Default Vite server path to React RefreshRuntime for @vitejs/plugin-react. react_refresh_url: str = "@react-refresh" - @property - def static_url(self) -> str: - url = urljoin(settings.STATIC_URL, self.static_url_prefix) - if not url.endswith("/"): - url += "/" - return url - @property - def static_root(self) -> Path: +class ManifestEntry(NamedTuple): + """ + Represent an entry for a file inside the "manifest.json". + """ + + file: str + src: Optional[str] = None + isEntry: Optional[bool] = False + css: Optional[List[str]] = [] + imports: Optional[List[str]] = [] + + +class ManifestClient: + """ + A client for accessing entries in the compiled vite config's "manifest.json". + Only parses manifest.json if dev_mode=False. + + Public Methods: + get(path: str) -- return the ManifestEntry for the given path. + """ + + def __init__(self, config: DjangoViteConfig): + self._config = config + + self.dev_mode = config.dev_mode + self.manifest_path = self._clean_manifest_path() + self.legacy_polyfills_motif = config.legacy_polyfills_motif + + self._entries: Dict[str, ManifestEntry] = {} + self.legacy_polyfills_entry: Optional[ManifestEntry] = None + + if not self.dev_mode: + self._entries, self.legacy_polyfills_entry = self._parse_manifest() + + def _clean_manifest_path(self) -> Path: """ - Compute the static root URL of assets. + Get the manifest_path from the config. + If it wasn't provided, set the default location to + STATIC_ROOT / static_url_prefix / "manifest.json". Returns: - Union[Path, str] -- Static root URL. + Path -- the path to the vite config's manifest.json + """ + initial_manifest_path = self._config.manifest_path + if not initial_manifest_path: + return ( + Path(settings.STATIC_ROOT) + / self._config.static_url_prefix + / "manifest.json" + ) + elif not isinstance(initial_manifest_path, Path): + return Path(initial_manifest_path) + else: + return initial_manifest_path + + def _parse_manifest(self) -> Tuple[Dict[str, ManifestEntry], ManifestEntry]: """ + Read and parse the Vite manifest file. - return ( - Path(self.assets_path) - if self.dev_mode - else Path(settings.STATIC_ROOT) / self.static_url_prefix - ) + Returns: + entries {Dict[str, ManifestEntry]} -- All entries within the manifest - def get_computed_manifest_path(self) -> Union[Path, str]: + legacy_polyfills_entry {ManifestEntry} -- The manifest entry for legacy + polyfills, if it exists within the manifest.json + + Raises: + DjangoViteManifestError: if cannot load the file or JSON in file is + malformed. """ - Compute the path to the "manifest.json". + entries: Dict[str, ManifestEntry] = {} + 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(): + manifest_entry = ManifestEntry(**manifest_entry_data) + entries[path] = manifest_entry + if self.legacy_polyfills_motif in path: + legacy_polyfills_entry = manifest_entry + + return (entries, legacy_polyfills_entry) + + except Exception as error: + raise DjangoViteManifestError( + f"Cannot read Vite manifest file at " + f"{self.manifest_path} : {str(error)}" + ) from error + + def get(self, path: str) -> ManifestEntry: + """ + Gets the manifest_entry for given path. Returns: - Union[Path, str] -- Path to the "manifest.json". + ManifestEntry -- the ManifestEntry for your path + + Raises: + DjangoViteAssetNotFoundError: if cannot find the file path in the manifest + or if manifest was never parsed due to dev_mode=True. """ + if path not in self._entries: + raise DjangoViteAssetNotFoundError( + f"Cannot find {path} in Vite manifest " f"at {self.manifest_path}" + ) - return ( - self.manifest_path - if self.manifest_path - else self.static_root / "manifest.json" - ) + return self._entries[path] -class DjangoViteAssetLoader: +class DjangoViteAppClient: """ - Class handling Vite asset loading. + An interface for generating assets and urls from one vite app. + DjangoViteConfig provides the arguments for the client. """ - _instance = None + def __init__(self, config: DjangoViteConfig) -> None: + self._config = config - _configs = Dict[str, Type[DjangoViteConfig]] - _manifests: Dict[str, Type[DjangoViteManifest]] + self.dev_mode = config.dev_mode + self.dev_server_protocol = config.dev_server_protocol + self.dev_server_host = config.dev_server_host + self.dev_server_port = config.dev_server_port + self.static_url_prefix = config.static_url_prefix + self.ws_client_url = config.ws_client_url + self.react_refresh_url = config.react_refresh_url - def __init__(self) -> None: - raise RuntimeError("Use the instance() method instead.") + self.manifest = ManifestClient(config) + + def _get_dev_server_url( + self, + path: str, + ) -> str: + """ + Generates an URL to an asset served by the Vite development server. + + Keyword Arguments: + path {str} -- Path to the asset. + + Returns: + str -- Full URL to the asset. + """ + static_url_base = urljoin(settings.STATIC_URL, self.static_url_prefix) + if not static_url_base.endswith("/"): + static_url_base += "/" + + return urljoin( + f"{self.dev_server_protocol}://" + f"{self.dev_server_host}:{self.dev_server_port}", + urljoin(static_url_base, path), + ) + + def _get_production_server_url(self, path: str) -> str: + """ + Generates an URL to an asset served during production. + + Keyword Arguments: + path {str} -- Path to the asset. + + Returns: + str -- Full URL to the asset. + """ + + production_server_url = path + if prefix := self.static_url_prefix: + if not prefix.endswith("/"): + prefix += "/" + production_server_url = urljoin(prefix, path) + + if apps.is_installed("django.contrib.staticfiles"): + from django.contrib.staticfiles.storage import staticfiles_storage + + return staticfiles_storage.url(production_server_url) + + return production_server_url def generate_vite_asset( self, path: str, - config_key: str, **kwargs: Dict[str, str], ) -> str: """ @@ -127,7 +240,6 @@ def generate_vite_asset( Arguments: path {str} -- Path to a Vite JS/TS asset to include. - config_key {str} -- Key of the configuration to use. Returns: str -- All tags to import this file in your HTML page. @@ -136,44 +248,30 @@ def generate_vite_asset( **kwargs {Dict[str, str]} -- Adds new attributes to generated script tags. - Raises: - DjangoViteAssetNotFoundError: If cannot find the file path in the - manifest (only in production). - Returns: str -- The ' - - @staticmethod - def _generate_stylesheet_tag(href: str) -> str: - """ - Generates an HTML stylesheet tag for CSS. - - Arguments: - href {str} -- CSS file URL. - - Returns: - str -- CSS link tag. - """ - - return f'' + url = self._get_dev_server_url(self.ws_client_url) - def _generate_stylesheet_preload_tag(href: str) -> str: - """ - Generates an HTML preload tag for CSS. - - Arguments: - href {str} -- CSS file URL. - - Returns: - str -- CSS link tag. - """ - - return f'' - - @staticmethod - def _generate_preload_tag(href: str, attrs: Dict[str, str]) -> str: - attrs_str = " ".join([f'{key}="{value}"' for key, value in attrs.items()]) - - return f'' - - @staticmethod - def _generate_vite_server_url( - path: str, - config: Type[DjangoViteConfig], - ) -> str: - """ - Generates an URL to and asset served by the Vite development server. - - Keyword Arguments: - path {str} -- Path to the asset. - config {Type[DjangoViteConfig]} -- Config object to use. - - Returns: - str -- Full URL to the asset. - """ - - return urljoin( - f"{config.dev_server_protocol}://" - f"{config.dev_server_host}:{config.dev_server_port}", - urljoin(config.static_url, path), + return TagGenerator.script( + url, + attrs={"type": "module", **kwargs}, ) - def generate_vite_react_refresh_url( - self, config_key: str = DEFAULT_CONFIG_KEY - ) -> str: + def generate_vite_react_refresh_url(self) -> str: """ Generates the script for the Vite React Refresh for HMR. Only used in development, in production this method returns @@ -725,234 +546,217 @@ def generate_vite_react_refresh_url( str -- The script or an empty string. config_key {str} -- Key of the configuration to use. """ - config = self._get_config(config_key) - if not config.dev_mode: + if not self.dev_mode: return "" + url = self._get_dev_server_url(self.react_refresh_url) + return f"""""" - @staticmethod - def _generate_production_server_url(path: str, static_url_prefix="") -> str: - """ - Generates an URL to an asset served during production. - - Keyword Arguments: - path {str} -- Path to the asset. - - Returns: - str -- Full URL to the asset. - """ - - production_server_url = path - if prefix := static_url_prefix: - if not static_url_prefix.endswith("/"): - prefix += "/" - production_server_url = urljoin(prefix, path) - - if apps.is_installed("django.contrib.staticfiles"): - from django.contrib.staticfiles.storage import staticfiles_storage - - return staticfiles_storage.url(production_server_url) - - return production_server_url - -@register.simple_tag -@mark_safe -def vite_hmr_client( - config_key: str = DEFAULT_CONFIG_KEY, **kwargs: Dict[str, str] -) -> str: - """ - Generates the script tag for the Vite WS client for HMR. - Only used in development, in production this method returns - an empty string. - - Arguments: - config {str} -- Configuration to use. - - Returns: - str -- The script tag or an empty string. - - Keyword Arguments: - **kwargs {Dict[str, str]} -- Adds new attributes to generated - script tags. - """ - - return DjangoViteAssetLoader.instance().generate_vite_ws_client( - config_key, **kwargs - ) - - -@register.simple_tag -@mark_safe -def vite_asset( - path: str, - config_key: str = DEFAULT_CONFIG_KEY, - **kwargs: Dict[str, str], -) -> str: +class DjangoViteAssetLoader: """ - Generates a ' @staticmethod - def _generate_stylesheet_tag(href: str) -> str: + def stylesheet(href: str) -> Tag: """ Generates an HTML stylesheet tag for CSS. @@ -672,7 +38,8 @@ def _generate_stylesheet_tag(href: str) -> str: return f'' - def _generate_stylesheet_preload_tag(href: str) -> str: + @staticmethod + def stylesheet_preload(href: str) -> Tag: """ Generates an HTML preload tag for CSS. @@ -686,273 +53,7 @@ def _generate_stylesheet_preload_tag(href: str) -> str: return f'' @staticmethod - def _generate_preload_tag(href: str, attrs: Dict[str, str]) -> str: + def preload(href: str, attrs: Dict[str, str]) -> Tag: attrs_str = " ".join([f'{key}="{value}"' for key, value in attrs.items()]) return f'' - - @staticmethod - def _generate_vite_server_url( - path: str, - config: Type[DjangoViteConfig], - ) -> str: - """ - Generates an URL to and asset served by the Vite development server. - - Keyword Arguments: - path {str} -- Path to the asset. - config {Type[DjangoViteConfig]} -- Config object to use. - - Returns: - str -- Full URL to the asset. - """ - - return urljoin( - f"{config.dev_server_protocol}://" - f"{config.dev_server_host}:{config.dev_server_port}", - urljoin(config.static_url, path), - ) - - def generate_vite_react_refresh_url( - self, config_key: str = DEFAULT_CONFIG_KEY - ) -> str: - """ - Generates the script for the Vite React Refresh for HMR. - Only used in development, in production this method returns - an empty string. - - Returns: - str -- The script or an empty string. - config_key {str} -- Key of the configuration to use. - """ - config = self._get_config(config_key) - - if not config.dev_mode: - return "" - - return f"""""" - - @staticmethod - def _generate_production_server_url(path: str, static_url_prefix="") -> str: - """ - Generates an URL to an asset served during production. - - Keyword Arguments: - path {str} -- Path to the asset. - - Returns: - str -- Full URL to the asset. - """ - - production_server_url = path - if prefix := static_url_prefix: - if not static_url_prefix.endswith("/"): - prefix += "/" - production_server_url = urljoin(prefix, path) - - if apps.is_installed("django.contrib.staticfiles"): - from django.contrib.staticfiles.storage import staticfiles_storage - - return staticfiles_storage.url(production_server_url) - - return production_server_url - - -@register.simple_tag -@mark_safe -def vite_hmr_client( - config_key: str = DEFAULT_CONFIG_KEY, **kwargs: Dict[str, str] -) -> str: - """ - Generates the script tag for the Vite WS client for HMR. - Only used in development, in production this method returns - an empty string. - - Arguments: - config {str} -- Configuration to use. - - Returns: - str -- The script tag or an empty string. - - Keyword Arguments: - **kwargs {Dict[str, str]} -- Adds new attributes to generated - script tags. - """ - - return DjangoViteAssetLoader.instance().generate_vite_ws_client( - config_key, **kwargs - ) - - -@register.simple_tag -@mark_safe -def vite_asset( - path: str, - config_key: str = DEFAULT_CONFIG_KEY, - **kwargs: Dict[str, str], -) -> str: - """ - Generates a ' - - @staticmethod - def _generate_stylesheet_tag(href: str) -> str: - """ - Generates an HTML stylesheet tag for CSS. - - Arguments: - href {str} -- CSS file URL. - - Returns: - str -- CSS link tag. - """ - - return f'' - - def _generate_stylesheet_preload_tag(href: str) -> str: - """ - Generates an HTML preload tag for CSS. - - Arguments: - href {str} -- CSS file URL. - - Returns: - str -- CSS link tag. - """ - - return f'' - - @staticmethod - def _generate_preload_tag(href: str, attrs: Dict[str, str]) -> str: - attrs_str = " ".join([f'{key}="{value}"' for key, value in attrs.items()]) - - return f'' - - @staticmethod - def _generate_vite_server_url( - path: str, - config: Type[DjangoViteConfig], - ) -> str: - """ - Generates an URL to and asset served by the Vite development server. - - Keyword Arguments: - path {str} -- Path to the asset. - config {Type[DjangoViteConfig]} -- Config object to use. - - Returns: - str -- Full URL to the asset. - """ - - return urljoin( - f"{config.dev_server_protocol}://" - f"{config.dev_server_host}:{config.dev_server_port}", - urljoin(config.static_url, path), - ) - - def generate_vite_react_refresh_url( - self, config_key: str = DEFAULT_CONFIG_KEY - ) -> str: - """ - Generates the script for the Vite React Refresh for HMR. - Only used in development, in production this method returns - an empty string. - - Returns: - str -- The script or an empty string. - config_key {str} -- Key of the configuration to use. - """ - config = self._get_config(config_key) - - if not config.dev_mode: - return "" - - return f"""""" - - @staticmethod - def _generate_production_server_url(path: str, static_url_prefix="") -> str: - """ - Generates an URL to an asset served during production. - - Keyword Arguments: - path {str} -- Path to the asset. - - Returns: - str -- Full URL to the asset. - """ - - production_server_url = path - if prefix := static_url_prefix: - if not static_url_prefix.endswith("/"): - prefix += "/" - production_server_url = urljoin(prefix, path) - - if apps.is_installed("django.contrib.staticfiles"): - from django.contrib.staticfiles.storage import staticfiles_storage - - return staticfiles_storage.url(production_server_url) - - return production_server_url - - @register.simple_tag @mark_safe -def vite_hmr_client( - config_key: str = DEFAULT_CONFIG_KEY, **kwargs: Dict[str, str] -) -> str: +def vite_hmr_client(app: str = DEFAULT_APP_NAME, **kwargs: Dict[str, str]) -> str: """ Generates the script tag for the Vite WS client for HMR. Only used in development, in production this method returns @@ -786,16 +28,14 @@ def vite_hmr_client( script tags. """ - return DjangoViteAssetLoader.instance().generate_vite_ws_client( - config_key, **kwargs - ) + return DjangoViteAssetLoader.instance().generate_vite_ws_client(app, **kwargs) @register.simple_tag @mark_safe def vite_asset( path: str, - config_key: str = DEFAULT_CONFIG_KEY, + app: str = DEFAULT_APP_NAME, **kwargs: Dict[str, str], ) -> str: """ @@ -823,18 +63,13 @@ def vite_asset( str -- The