From b7261ec62996fbf43cba8de05e651a14b8b48d35 Mon Sep 17 00:00:00 2001 From: thomas girod Date: Wed, 14 Aug 2024 18:50:46 +0200 Subject: [PATCH] custom manifest static files storage that also minify scss and js files --- .github/workflows/deploy.yml | 3 +- .github/workflows/taiste.yml | 3 +- core/management/commands/compilestatic.py | 69 --------------------- core/templates/core/base.jinja | 2 +- core/templatetags/renderer.py | 2 +- docs/howto/prod.md | 26 ++++++-- sith/settings.py | 11 +++- sith/storage.py | 73 +++++++++++++++++++++++ 8 files changed, 107 insertions(+), 82 deletions(-) delete mode 100644 core/management/commands/compilestatic.py create mode 100644 sith/storage.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2ebeca97..ca9fac48 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -40,8 +40,7 @@ jobs: poetry install --with prod --without docs,tests poetry run ./manage.py install_xapian poetry run ./manage.py migrate - echo "yes" | poetry run ./manage.py collectstatic - poetry run ./manage.py compilestatic + poetry run ./manage.py collectstatic --clear --noinput poetry run ./manage.py compilemessages sudo systemctl restart uwsgi diff --git a/.github/workflows/taiste.yml b/.github/workflows/taiste.yml index b83682ec..cfecf289 100644 --- a/.github/workflows/taiste.yml +++ b/.github/workflows/taiste.yml @@ -39,8 +39,7 @@ jobs: poetry install --with prod --without docs,tests poetry run ./manage.py install_xapian poetry run ./manage.py migrate - echo "yes" | poetry run ./manage.py collectstatic - poetry run ./manage.py compilestatic + poetry run ./manage.py collectstatic --clear --noinput poetry run ./manage.py compilemessages sudo systemctl restart uwsgi diff --git a/core/management/commands/compilestatic.py b/core/management/commands/compilestatic.py deleted file mode 100644 index d2f64bfd..00000000 --- a/core/management/commands/compilestatic.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright 2017 -# - Sli -# -# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, -# http://ae.utbm.fr. -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License a published by the Free Software -# Foundation; either version 3 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple -# Place - Suite 330, Boston, MA 02111-1307, USA. -# -# - -import sys - -import sass -from django.conf import settings -from django.core.management.base import BaseCommand - - -class Command(BaseCommand): - """Compiles scss in static folder for production.""" - - help = "Compile scss files from static folder" - - def compile(self, filename: str): - args = { - "filename": filename, - "include_paths": settings.STATIC_ROOT.name, - "output_style": "compressed", - } - if settings.SASS_PRECISION: - args["precision"] = settings.SASS_PRECISION - return sass.compile(**args) - - def handle(self, *args, **options): - if not settings.STATIC_ROOT.is_dir(): - raise Exception( - "No static folder availaible, please use collectstatic before compiling scss" - ) - to_exec = list(settings.STATIC_ROOT.rglob("*.scss")) - if len(to_exec) == 0: - self.stdout.write("Nothing to compile.") - sys.exit(0) - self.stdout.write("---- Compiling scss files ---") - for file in to_exec: - # remove existing css files that will be replaced - # keeping them while compiling the scss would break - # import statements resolution - css_file = file.with_suffix(".css") - if css_file.exists(): - css_file.unlink() - compiled_files = {file: self.compile(str(file.resolve())) for file in to_exec} - for file, scss in compiled_files.items(): - file.replace(file.with_suffix(".css")).write_text(scss) - self.stdout.write( - "Files compiled : \n" + "\n- ".join(str(f) for f in compiled_files) - ) diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index aa1d2e05..d5a8b2a0 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -18,7 +18,7 @@ {% endblock %} - + diff --git a/core/templatetags/renderer.py b/core/templatetags/renderer.py index beef3f43..e1bf826e 100644 --- a/core/templatetags/renderer.py +++ b/core/templatetags/renderer.py @@ -107,4 +107,4 @@ def scss(path): if storage.exists(css_path): storage.delete(css_path) storage.save(css_path, ContentFile(content)) - return static(css_path) + return static(str(css_path)) diff --git a/docs/howto/prod.md b/docs/howto/prod.md index 2e892fff..82c0f49a 100644 --- a/docs/howto/prod.md +++ b/docs/howto/prod.md @@ -11,17 +11,31 @@ Nous utilisons du SCSS dans le projet. En environnement de développement (`DEBUG=True`), le SCSS est compilé à chaque fois que le fichier est demandé. Pour la production, le projet considère -que chacun des fichiers est déjà compilé et, -pour ce faire, il est nécessaire -d'utiliser les commandes suivantes dans l'ordre : +que chacun des fichiers est déjà compilé. +C'est pourquoi le SCSS est automatiquement compilé lors +de la récupération des fichiers statiques. +Les fichiers JS sont également automatiquement minifiés. + +Il peut être judicieux de supprimer les anciens fichiers +statiques avant de collecter les nouveaux. +Pour ça, ajoutez le flag `--clear` à la commande `collectstatic` : ```bash -python ./manage.py collectstatic # Pour récupérer tous les fichiers statiques -python ./manage.py compilestatic # Pour compiler les fichiers SCSS qu'ils contiennent +python ./manage.py collectstatic --clear ``` !!!tip Le dossier où seront enregistrés ces fichiers statiques peut être changé en modifiant la variable - `STATIC_ROOT` dans les paramètres. \ No newline at end of file + `STATIC_ROOT` dans les paramètres. + +!!!warning + + La minification des fichiers JS nécessite la présence + de `uglifyJS` sur la machine. + Pour l'installer, faites la commande suivante (nécessite nodeJS) : + + ```bash + npm install uglifyjs -g + ``` \ No newline at end of file diff --git a/sith/settings.py b/sith/settings.py index dcbd3831..78fbf9b6 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -273,6 +273,15 @@ STATICFILES_FINDERS = [ "sith.finders.ScssFinder", ] +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "sith.storage.SithStorage", + }, +} + # Auth configuration AUTH_USER_MODEL = "core.User" AUTH_ANONYMOUS_MODEL = "core.models.AnonymousUser" @@ -716,7 +725,7 @@ if TESTING: "BACKEND": "django.core.files.storage.InMemoryStorage", }, "staticfiles": { - "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + "BACKEND": "sith.storage.SithStorage", }, } diff --git a/sith/storage.py b/sith/storage.py new file mode 100644 index 00000000..be2370f5 --- /dev/null +++ b/sith/storage.py @@ -0,0 +1,73 @@ +import logging +import subprocess +import warnings + +import sass +from django.conf import settings +from django.contrib.staticfiles.storage import ( + ManifestStaticFilesStorage, +) +from django.core.files.storage import Storage + + +class SithStorage(ManifestStaticFilesStorage): + def _compile_scss(self): + to_exec = list(settings.STATIC_ROOT.rglob("*.scss")) + if len(to_exec) == 0: + return + for file in to_exec: + # remove existing css files that will be replaced + # keeping them while compiling the scss would break + # import statements resolution + css_file = file.with_suffix(".css") + if css_file.exists(): + css_file.unlink() + scss_paths = [p.resolve() for p in to_exec if p.suffix == ".scss"] + base_args = {"output_style": "compressed", "precision": settings.SASS_PRECISION} + compiled_files = { + p: sass.compile(filename=str(p), **base_args) for p in scss_paths + } + for file, scss in compiled_files.items(): + file.replace(file.with_suffix(".css")).write_text(scss) + + # once the files are compiled, the manifest must be updated + # to have the right suffix + new_entries = { + k.replace(".scss", ".css"): self.hashed_files.pop(k).replace( + ".scss", ".css" + ) + for k in list(self.hashed_files.keys()) + if k.endswith(".scss") + } + self.hashed_files.update(new_entries) + self.save_manifest() + + @staticmethod + def _minify_js(): + try: + subprocess.run(["uglifyjs", "-v"]) + except FileNotFoundError: + warnings.warn( + "Couldn't minify JS files. Make sure UglifyJs is installed.", + stacklevel=1, + ) + return + to_exec = [ + p for p in settings.STATIC_ROOT.rglob("*.js") if ".min" not in p.suffixes + ] + for path in to_exec: + p = path.resolve() + subprocess.run(["uglifyjs", p, "-o", p]) + logging.getLogger("main").info(f"Minified {path}") + + def post_process( + self, paths: dict[str, tuple[Storage, str]], *, dry_run: bool = False + ): + # Whether we get the files that were processed by ManifestFilesMixin + # by calling super() or whether we get them from the manifest file + # makes no difference - we have to open the manifest file anyway + # because we need to update the paths stored inside it. + yield from super().post_process(paths, dry_run) + + self._compile_scss() + self._minify_js()