Skip to content

Commit 6fc21a5

Browse files
committed
Add sourcmaps support for uglifyjs and cleancss
1 parent ec660fe commit 6fc21a5

File tree

6 files changed

+163
-23
lines changed

6 files changed

+163
-23
lines changed

pipeline/compressors/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from pipeline.conf import settings
1717
from pipeline.exceptions import CompressorError
18-
from pipeline.utils import to_class, relpath
18+
from pipeline.utils import to_class, relpath, set_std_streams_blocking
1919

2020
URL_DETECTOR = r"""url\((['"]){0,1}\s*(.*?)["']{0,1}\)"""
2121
URL_REPLACER = r"""url\(__EMBED__(.+?)(\?\d+)?\)"""
@@ -253,7 +253,7 @@ def filter_js(self, js):
253253

254254

255255
class SubProcessCompressor(CompressorBase):
256-
def execute_command(self, command, content):
256+
def execute_command(self, command, content=None, **kwargs):
257257
argument_list = []
258258
for flattening_arg in command:
259259
if isinstance(flattening_arg, string_types):
@@ -263,10 +263,11 @@ def execute_command(self, command, content):
263263
stdin = subprocess.PIPE if content else None
264264

265265
pipe = subprocess.Popen(argument_list, stdout=subprocess.PIPE,
266-
stdin=stdin, stderr=subprocess.PIPE)
266+
stdin=stdin, stderr=subprocess.PIPE, **kwargs)
267267
if content:
268268
content = smart_bytes(content)
269269
stdout, stderr = pipe.communicate(content)
270+
set_std_streams_blocking()
270271
if stderr.strip() and pipe.returncode != 0:
271272
raise CompressorError(stderr)
272273
elif self.verbose:

pipeline/compressors/closure.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,7 @@
88

99
from pipeline.conf import settings
1010
from pipeline.compressors import SubProcessCompressor
11-
12-
13-
source_map_re = re.compile((
14-
"(?:"
15-
"/\\*"
16-
"(?:\\s*\r?\n(?://)?)?"
17-
"(?:%(inner)s)"
18-
"\\s*"
19-
"\\*/"
20-
"|"
21-
"//(?:%(inner)s)"
22-
")"
23-
"\\s*$") % {'inner': r"""[#@] sourceMappingURL=([^\s'"]*)"""})
11+
from pipeline.utils import source_map_re
2412

2513

2614
class ClosureCompressor(SubProcessCompressor):

pipeline/compressors/cssclean.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import unicode_literals
2+
3+
import codecs
4+
import json
5+
import os
6+
7+
from django.contrib.staticfiles.storage import staticfiles_storage
8+
9+
from pipeline.conf import settings
10+
from pipeline.compressors import SubProcessCompressor
11+
from pipeline.utils import source_map_re, relurl
12+
13+
14+
class CleanCSSCompressor(SubProcessCompressor):
15+
16+
def compress_css(self, css):
17+
args = [settings.CLEANCSS_BINARY, settings.CLEANCSS_ARGUMENTS]
18+
return self.execute_command(args, css)
19+
20+
def compress_css_with_source_map(self, paths, output_filename):
21+
output_path = staticfiles_storage.path(output_filename)
22+
output_dir = os.path.dirname(output_path)
23+
if not os.path.exists(output_dir):
24+
os.makedirs(output_dir)
25+
26+
args = [settings.CLEANCSS_BINARY]
27+
args += ['--source-map']
28+
if settings.CLEANCSS_ARGUMENTS:
29+
args += [settings.CLEANCSS_ARGUMENTS]
30+
else:
31+
# At present, without these arguments, cleancss does not
32+
# generate accurate source maps
33+
args += [
34+
'--skip-advanced', '--skip-media-merging',
35+
'--skip-restructuring', '--skip-shorthand-compacting',
36+
'--keep-line-breaks']
37+
args += ['--output', output_path]
38+
args += [staticfiles_storage.path(p) for p in paths]
39+
40+
self.execute_command(args, cwd=output_dir)
41+
42+
source_map_file = "%s.map" % output_path
43+
44+
with codecs.open(output_path, encoding='utf-8') as f:
45+
css = f.read()
46+
with codecs.open(source_map_file, encoding='utf-8') as f:
47+
source_map = f.read()
48+
49+
# Strip out existing source map comment (it will be re-added with packaging)
50+
css = source_map_re.sub('', css)
51+
52+
output_url = "%s/%s" % (
53+
staticfiles_storage.url(os.path.dirname(output_filename)),
54+
os.path.basename(output_path))
55+
56+
# Grab urls from staticfiles storage (in case filenames are hashed)
57+
source_map_data = json.loads(source_map)
58+
for i, source in enumerate(source_map_data['sources']):
59+
source_abs_path = os.path.join(output_dir, source)
60+
source_rel_path = os.path.relpath(
61+
source_abs_path, staticfiles_storage.base_location)
62+
source_url = staticfiles_storage.url(source_rel_path)
63+
source_map_data['sources'][i] = relurl(source_url, output_url)
64+
source_map = json.dumps(source_map_data, indent="\t")
65+
66+
return css, source_map

pipeline/compressors/uglifyjs.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
11
from __future__ import unicode_literals
22

3+
import codecs
4+
import tempfile
5+
6+
from django.contrib.staticfiles.storage import staticfiles_storage
7+
38
from pipeline.conf import settings
49
from pipeline.compressors import SubProcessCompressor
10+
from pipeline.utils import source_map_re, path_depth
511

612

713
class UglifyJSCompressor(SubProcessCompressor):
14+
815
def compress_js(self, js):
9-
command = (settings.UGLIFYJS_BINARY, settings.UGLIFYJS_ARGUMENTS)
16+
command = [settings.UGLIFYJS_BINARY, settings.UGLIFYJS_ARGUMENTS]
1017
if self.verbose:
11-
command += ' --verbose'
18+
command.append(' --verbose')
1219
return self.execute_command(command, js)
20+
21+
def compress_js_with_source_map(self, paths):
22+
source_map_file = tempfile.NamedTemporaryFile()
23+
24+
args = [settings.UGLIFYJS_BINARY]
25+
args += [staticfiles_storage.path(p) for p in paths]
26+
args += ["--source-map", source_map_file.name]
27+
args += ["--source-map-root", staticfiles_storage.base_url]
28+
args += ["--prefix", "%s" % path_depth(staticfiles_storage.base_location)]
29+
30+
args += settings.UGLIFYJS_ARGUMENTS
31+
32+
if self.verbose:
33+
args.append('--verbose')
34+
35+
js = self.execute_command(args)
36+
37+
with codecs.open(source_map_file.name, encoding='utf-8') as f:
38+
source_map = f.read()
39+
40+
source_map_file.close()
41+
42+
# Strip out existing source map comment (it will be re-added with packaging)
43+
js = source_map_re.sub('', js)
44+
45+
return js, source_map

pipeline/conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252
'CSSMIN_BINARY': '/usr/bin/env cssmin',
5353
'CSSMIN_ARGUMENTS': '',
5454

55+
'CLEANCSS_BINARY': '/usr/bin/env cssclean',
56+
'CLEANCSS_ARGUMENTS': '',
57+
5558
'COFFEE_SCRIPT_BINARY': '/usr/bin/env coffee',
5659
'COFFEE_SCRIPT_ARGUMENTS': '',
5760

pipeline/utils.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
from __future__ import unicode_literals
22

3+
try:
4+
import fcntl
5+
except ImportError:
6+
# windows
7+
fcntl = None
38
import importlib
49
import mimetypes
510
import posixpath
6-
7-
try:
8-
from urllib.parse import quote
9-
except ImportError:
10-
from urllib import quote
11+
import os
12+
import re
13+
import sys
1114

1215
from django.utils.encoding import smart_text
16+
from django.utils.six.moves.urllib.parse import urlparse, quote
1317

1418
from pipeline.conf import settings
1519

1620

21+
source_map_re = re.compile((
22+
"(?:"
23+
"/\\*"
24+
"(?:\\s*\r?\n(?://)?)?"
25+
"(?:%(inner)s)"
26+
"\\s*"
27+
"\\*/"
28+
"|"
29+
"//(?:%(inner)s)"
30+
")"
31+
"\\s*$") % {'inner': r"""[#@] sourceMappingURL=([^\s'"]*)"""})
32+
33+
1734
def to_class(class_str):
1835
if not class_str:
1936
return None
@@ -54,3 +71,35 @@ def relpath(path, start=posixpath.curdir):
5471
if not rel_list:
5572
return posixpath.curdir
5673
return posixpath.join(*rel_list)
74+
75+
76+
def relurl(path, start):
77+
base = urlparse(start)
78+
target = urlparse(path)
79+
if base.netloc != target.netloc:
80+
raise ValueError('target and base netlocs do not match')
81+
base_dir = '.' + posixpath.dirname(base.path)
82+
target = '.' + target.path
83+
return posixpath.relpath(target, start=base_dir)
84+
85+
86+
def set_std_streams_blocking():
87+
if not fcntl:
88+
return
89+
for f in (sys.__stdout__, sys.__stderr__):
90+
fileno = f.fileno()
91+
flags = fcntl.fcntl(fileno, fcntl.F_GETFL)
92+
fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
93+
94+
95+
def path_depth(path):
96+
"""Cross-platform compatible path depth count"""
97+
import os
98+
if hasattr(os.path, 'splitunc'):
99+
_, path = os.path.splitunc(path)
100+
parent = os.path.dirname(path)
101+
count = 0
102+
while path != parent:
103+
path, parent = parent, os.path.dirname(parent)
104+
count += 1
105+
return count

0 commit comments

Comments
 (0)