Integrates vite manifests to django

This commit is contained in:
Antoine Bartuccio 2024-11-20 18:24:28 +01:00
parent ca8c1c9d92
commit 8fc1a754de
4 changed files with 84 additions and 14 deletions

View File

@ -3,6 +3,7 @@ from pathlib import Path
from django.contrib.staticfiles.apps import StaticFilesConfig from django.contrib.staticfiles.apps import StaticFilesConfig
GENERATED_ROOT = Path(__file__).parent.resolve() / "generated" GENERATED_ROOT = Path(__file__).parent.resolve() / "generated"
BUNDLED_ROOT = GENERATED_ROOT / "bundled"
IGNORE_PATTERNS_BUNDLED = ["bundled/*"] IGNORE_PATTERNS_BUNDLED = ["bundled/*"]
IGNORE_PATTERNS_SCSS = ["*.scss"] IGNORE_PATTERNS_SCSS = ["*.scss"]
IGNORE_PATTERNS_TYPESCRIPT = ["*.ts"] IGNORE_PATTERNS_TYPESCRIPT = ["*.ts"]

View File

@ -1,16 +1,57 @@
import json
import logging import logging
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from hashlib import sha1 from hashlib import sha1
from itertools import chain
from pathlib import Path from pathlib import Path
from typing import Iterable from typing import Iterable, Self
import rjsmin import rjsmin
import sass import sass
from django.conf import settings from django.conf import settings
from sith.urls import api 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: class JSBundler:
@ -28,6 +69,16 @@ class JSBundler:
logging.getLogger("django").info("Running javascript bundling server") logging.getLogger("django").info("Running javascript bundling server")
return subprocess.Popen(["npm", "run", "serve"]) 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: class Scss:
@dataclass @dataclass

View File

@ -7,15 +7,27 @@ from django.contrib.staticfiles.storage import (
) )
from django.core.files.storage import Storage from django.core.files.storage import Storage
from staticfiles.processors import JS, Scss from staticfiles.processors import JS, JSBundler, Scss
class ManifestPostProcessingStorage(ManifestStaticFilesStorage): class ManifestPostProcessingStorage(ManifestStaticFilesStorage):
def url(self, name: str, *, force: bool = False) -> str: 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 # This name swap has to be done here
# Otherwise, the manifest isn't aware of the file and can't work properly # 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) path = Path(name)
# Call bundler manifest
if path.suffix == ".scss": if path.suffix == ".scss":
# Compile scss files automatically in debug mode # Compile scss files automatically in debug mode
if settings.DEBUG: if settings.DEBUG:
@ -27,11 +39,14 @@ class ManifestPostProcessingStorage(ManifestStaticFilesStorage):
) )
name = str(path.with_suffix(".css")) name = str(path.with_suffix(".css"))
elif path.suffix == ".ts":
name = str(path.with_suffix(".js"))
return super().url(name, force=force) 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( def post_process(
self, paths: dict[str, tuple[Storage, str]], *, dry_run: bool = False 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) yield from super().post_process(paths, dry_run)
if not dry_run: if not dry_run:
JS.minify() JS.minify()
manifest = JSBundler.get_manifest()
self.hashed_files.update(manifest.mapping)
self.save_manifest()

View File

@ -49,6 +49,7 @@ export default {
appType: "custom", appType: "custom",
build: { build: {
outDir: outDir, 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 modulePreload: false, // would require `import 'vite/modulepreload-polyfill'` to always be injected
emptyOutDir: true, emptyOutDir: true,
rollupOptions: { rollupOptions: {
@ -57,9 +58,9 @@ export default {
// Mirror architecture of static folders in generated .js and .css // Mirror architecture of static folders in generated .js and .css
entryFileNames: (chunkInfo: Rollup.PreRenderedChunk) => { entryFileNames: (chunkInfo: Rollup.PreRenderedChunk) => {
if (chunkInfo.facadeModuleId !== null) { 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) => { assetFileNames: (chunkInfo: Rollup.PreRenderedAsset) => {
if ( if (
@ -67,13 +68,11 @@ export default {
chunkInfo.originalFileNames?.length === 1 && chunkInfo.originalFileNames?.length === 1 &&
collectedFiles.includes(chunkInfo.originalFileNames[0]) collectedFiles.includes(chunkInfo.originalFileNames[0])
) { ) {
return ( return `${getRelativeAssetPath(chunkInfo.originalFileNames[0])}.[hash][extname]`;
getRelativeAssetPath(chunkInfo.originalFileNames[0]) +
parse(chunkInfo.names[0]).ext
);
} }
return "[name].[ext]"; return "[name].[hash][extname]";
}, },
chunkFileNames: "[name].[hash].js",
}, },
}, },
}, },