1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
|
from urllib.parse import quote, urljoin
import os
import json
import logging
import subprocess
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import ContentFile
from django.template import Context
from django.templatetags.static import PrefixNode
from django.utils.encoding import force_bytes
from sass_processor.utils import get_custom_functions
from .storage import SassFileStorage, find_file
from .apps import APPS_INCLUDE_DIRS
try:
import sass
except ImportError:
sass = None
logger = logging.getLogger('sass-processor')
class SassProcessor:
storage = SassFileStorage()
include_paths = list(getattr(settings, 'SASS_PROCESSOR_INCLUDE_DIRS', []))
try:
sass_precision = int(settings.SASS_PRECISION)
except (AttributeError, TypeError, ValueError):
sass_precision = None
sass_output_style = getattr(
settings,
'SASS_OUTPUT_STYLE',
'nested' if settings.DEBUG else 'compressed')
processor_enabled = getattr(settings, 'SASS_PROCESSOR_ENABLED', settings.DEBUG)
sass_extensions = ('.scss', '.sass')
node_npx_path = getattr(settings, 'NODE_NPX_PATH', 'npx')
def __init__(self, path=None):
self._path = path
nmd = [d[1] for d in getattr(settings, 'STATICFILES_DIRS', [])
if isinstance(d, (list, tuple)) and d[0] == 'node_modules']
self.node_modules_dir = nmd[0] if len(nmd) else None
def __call__(self, path):
basename, ext = os.path.splitext(path)
filename = find_file(path)
if filename is None:
raise FileNotFoundError("Unable to locate file {path}".format(path=path))
if ext not in self.sass_extensions:
# return the given path, since it ends neither in `.scss` nor in `.sass`
return path
# compare timestamp of sourcemap file with all its dependencies, and check if we must recompile
css_filename = basename + '.css'
if not self.processor_enabled:
return css_filename
sourcemap_filename = css_filename + '.map'
base = os.path.dirname(filename)
if find_file(css_filename) and self.is_latest(sourcemap_filename, base):
return css_filename
# with offline compilation, raise an error, if css file could not be found.
if sass is None:
msg = "Offline compiled file `{}` is missing and libsass has not been installed."
raise ImproperlyConfigured(msg.format(css_filename))
# otherwise compile the SASS/SCSS file into .css and store it
filename_map = filename.replace(ext, '.css.map')
compile_kwargs = {
'filename': filename,
'source_map_filename': filename_map,
'include_paths': self.include_paths + APPS_INCLUDE_DIRS,
'custom_functions': get_custom_functions(),
}
if self.sass_precision:
compile_kwargs['precision'] = self.sass_precision
if self.sass_output_style:
compile_kwargs['output_style'] = self.sass_output_style
content, sourcemap = sass.compile(**compile_kwargs)
content, sourcemap = force_bytes(content), force_bytes(sourcemap)
# autoprefix CSS files using postcss in external JavaScript process
if self.node_npx_path and os.path.isdir(self.node_modules_dir or ''):
os.environ['NODE_PATH'] = self.node_modules_dir
try:
options = [self.node_npx_path, 'postcss', '--use', 'autoprefixer']
if not settings.DEBUG:
options.append('--no-map')
proc = subprocess.Popen(options, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
proc.stdin.write(content)
proc.stdin.close()
autoprefixed_content = proc.stdout.read()
proc.wait()
except (FileNotFoundError, BrokenPipeError) as exc:
logger.warning("Unable to postcss {}. Reason: {}".format(filename, exc))
else:
if len(autoprefixed_content) >= len(content):
content = autoprefixed_content
if self.storage.exists(css_filename):
self.storage.delete(css_filename)
self.storage.save(css_filename, ContentFile(content))
if self.storage.exists(sourcemap_filename):
self.storage.delete(sourcemap_filename)
self.storage.save(sourcemap_filename, ContentFile(sourcemap))
return css_filename
def resolve_path(self, context=None):
if context is None:
context = Context()
return self._path.resolve(context)
def is_sass(self):
_, ext = os.path.splitext(self.resolve_path())
return ext in self.sass_extensions
def is_latest(self, sourcemap_filename, base):
sourcemap_file = find_file(sourcemap_filename)
if not sourcemap_file or not os.path.isfile(sourcemap_file):
return False
sourcemap_mtime = os.stat(sourcemap_file).st_mtime
with open(sourcemap_file, 'r') as fp:
sourcemap = json.load(fp)
for srcfilename in sourcemap.get('sources'):
srcfilename = os.path.join(base, srcfilename)
if not os.path.isfile(srcfilename) or os.stat(srcfilename).st_mtime > sourcemap_mtime:
# at least one of the source is younger that the sourcemap referring it
return False
return True
@classmethod
def handle_simple(cls, path):
if apps.is_installed('django.contrib.staticfiles'):
from django.contrib.staticfiles.storage import staticfiles_storage
return staticfiles_storage.url(path)
else:
return urljoin(PrefixNode.handle_simple('STATIC_URL'), quote(path))
_sass_processor = SassProcessor()
def sass_processor(filename):
path = _sass_processor(filename)
return SassProcessor.handle_simple(path)
|