From 8fc1a754de026cb2d455800e75d75e030f1ac8fd Mon Sep 17 00:00:00 2001 From: Sli Date: Wed, 20 Nov 2024 18:24:28 +0100 Subject: [PATCH] Integrates vite manifests to django --- staticfiles/apps.py | 1 + staticfiles/processors.py | 55 +++++++++++++++++++++++++++++++++++++-- staticfiles/storage.py | 29 +++++++++++++++++---- vite.config.mts | 13 +++++---- 4 files changed, 84 insertions(+), 14 deletions(-) diff --git a/staticfiles/apps.py b/staticfiles/apps.py index d4d27d0b..dfec2735 100644 --- a/staticfiles/apps.py +++ b/staticfiles/apps.py @@ -3,6 +3,7 @@ from pathlib import Path from django.contrib.staticfiles.apps import StaticFilesConfig GENERATED_ROOT = Path(__file__).parent.resolve() / "generated" +BUNDLED_ROOT = GENERATED_ROOT / "bundled" IGNORE_PATTERNS_BUNDLED = ["bundled/*"] IGNORE_PATTERNS_SCSS = ["*.scss"] IGNORE_PATTERNS_TYPESCRIPT = ["*.ts"] diff --git a/staticfiles/processors.py b/staticfiles/processors.py index cfbbe1fb..8d458686 100644 --- a/staticfiles/processors.py +++ b/staticfiles/processors.py @@ -1,16 +1,57 @@ +import json import logging import subprocess from dataclasses import dataclass from hashlib import sha1 +from itertools import chain from pathlib import Path -from typing import Iterable +from typing import Iterable, Self import rjsmin import sass from django.conf import settings from sith.urls import api -from staticfiles.apps import GENERATED_ROOT +from staticfiles.apps import BUNDLED_ROOT, GENERATED_ROOT + + +@dataclass +class JsBundlerManifestEntry: + out: str + src: str + + @classmethod + def from_json_entry(cls, entry: dict[str, any]) -> list[Self]: + ret = [ + cls( + out=str(Path("bundled") / entry["file"]), + src=str(Path(*Path(entry["src"]).parts[2:])), + ) + ] + for css in entry.get("css", []): + path = Path("bundled") / css + ret.append( + cls( + out=str(path), + src=str(path.with_stem(entry["name"])), + ) + ) + return ret + + +class JSBundlerManifest: + def __init__(self, manifest: Path): + with open(manifest, "r") as f: + self._manifest = json.load(f) + + self._files = chain( + *[ + JsBundlerManifestEntry.from_json_entry(value) + for value in self._manifest.values() + if value.get("isEntry", False) + ] + ) + self.mapping = {file.src: file.out for file in self._files} class JSBundler: @@ -28,6 +69,16 @@ class JSBundler: logging.getLogger("django").info("Running javascript bundling server") return subprocess.Popen(["npm", "run", "serve"]) + @staticmethod + def get_manifest() -> JSBundlerManifest: + return JSBundlerManifest(BUNDLED_ROOT / ".vite" / "manifest.json") + + @staticmethod + def is_in_bundle(name: str | None) -> bool: + if name is None: + return False + return name.startswith("bundled/") + class Scss: @dataclass diff --git a/staticfiles/storage.py b/staticfiles/storage.py index 3aaca470..0022438c 100644 --- a/staticfiles/storage.py +++ b/staticfiles/storage.py @@ -7,15 +7,27 @@ from django.contrib.staticfiles.storage import ( ) from django.core.files.storage import Storage -from staticfiles.processors import JS, Scss +from staticfiles.processors import JS, JSBundler, Scss class ManifestPostProcessingStorage(ManifestStaticFilesStorage): def url(self, name: str, *, force: bool = False) -> str: - """Get the URL for a file, convert .scss calls to .css ones and .ts to .js""" + """Get the URL for a file, convert .scss calls to .css calls to bundled files to their output ones""" # This name swap has to be done here # Otherwise, the manifest isn't aware of the file and can't work properly + if settings.DEBUG: + try: + manifest = JSBundler.get_manifest() + except Exception as e: + raise Exception( + "Error loading manifest file, the bundler seems to be busy" + ) from e + converted = manifest.mapping.get(name, None) + if converted: + name = converted + path = Path(name) + # Call bundler manifest if path.suffix == ".scss": # Compile scss files automatically in debug mode if settings.DEBUG: @@ -27,11 +39,14 @@ class ManifestPostProcessingStorage(ManifestStaticFilesStorage): ) name = str(path.with_suffix(".css")) - elif path.suffix == ".ts": - name = str(path.with_suffix(".js")) - return super().url(name, force=force) + def hashed_name(self, name, content=None, filename=None): + # Ignore bundled files since they will be added at post process + if JSBundler.is_in_bundle(name): + return name + return super().hashed_name(name, content, filename) + def post_process( self, paths: dict[str, tuple[Storage, str]], *, dry_run: bool = False ): @@ -42,3 +57,7 @@ class ManifestPostProcessingStorage(ManifestStaticFilesStorage): yield from super().post_process(paths, dry_run) if not dry_run: JS.minify() + + manifest = JSBundler.get_manifest() + self.hashed_files.update(manifest.mapping) + self.save_manifest() diff --git a/vite.config.mts b/vite.config.mts index fab81862..43ec5ad9 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -49,6 +49,7 @@ export default { appType: "custom", build: { outDir: outDir, + manifest: true, // goes into .vite/manifest.json in the build folder modulePreload: false, // would require `import 'vite/modulepreload-polyfill'` to always be injected emptyOutDir: true, rollupOptions: { @@ -57,9 +58,9 @@ export default { // Mirror architecture of static folders in generated .js and .css entryFileNames: (chunkInfo: Rollup.PreRenderedChunk) => { if (chunkInfo.facadeModuleId !== null) { - return `${getRelativeAssetPath(chunkInfo.facadeModuleId)}.js`; + return `${getRelativeAssetPath(chunkInfo.facadeModuleId)}.[hash].js`; } - return "[name].js"; + return "[name].[hash].js"; }, assetFileNames: (chunkInfo: Rollup.PreRenderedAsset) => { if ( @@ -67,13 +68,11 @@ export default { chunkInfo.originalFileNames?.length === 1 && collectedFiles.includes(chunkInfo.originalFileNames[0]) ) { - return ( - getRelativeAssetPath(chunkInfo.originalFileNames[0]) + - parse(chunkInfo.names[0]).ext - ); + return `${getRelativeAssetPath(chunkInfo.originalFileNames[0])}.[hash][extname]`; } - return "[name].[ext]"; + return "[name].[hash][extname]"; }, + chunkFileNames: "[name].[hash].js", }, }, },