Skip to content

Commit 7c1bb9f

Browse files
committed
Add sourcemap support
1 parent d60a9dc commit 7c1bb9f

File tree

15 files changed

+320
-44
lines changed

15 files changed

+320
-44
lines changed

pipeline/compilers/coffee.py

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

3+
import os
4+
35
from pipeline.conf import settings
46
from pipeline.compilers import SubProcessCompiler
57

@@ -13,10 +15,16 @@ def match_file(self, path):
1315
def compile_file(self, infile, outfile, outdated=False, force=False):
1416
if not outdated and not force:
1517
return # File doesn't need to be recompiled
18+
19+
args = list(settings.COFFEE_SCRIPT_ARGUMENTS)
20+
if settings.OUTPUT_SOURCEMAPS and not(set(args) & set(['-m', '--map'])):
21+
args.append('--map')
22+
1623
command = (
1724
settings.COFFEE_SCRIPT_BINARY,
18-
"-cp",
19-
settings.COFFEE_SCRIPT_ARGUMENTS,
25+
"-c",
26+
"-o", os.path.dirname(outfile),
27+
args,
2028
infile,
2129
)
22-
return self.execute_command(command, stdout_captured=outfile)
30+
return self.execute_command(command, cwd=os.path.dirname(outfile))

pipeline/compilers/es6.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@ def match_file(self, path):
1313
def compile_file(self, infile, outfile, outdated=False, force=False):
1414
if not outdated and not force:
1515
return # File doesn't need to be recompiled
16+
17+
args = list(settings.BABEL_ARGUMENTS)
18+
19+
sourcemap_flags = set(['-s', '--source-maps'])
20+
if settings.OUTPUT_SOURCEMAPS and not(set(args) & sourcemap_flags):
21+
args += ['--source-maps', 'true']
22+
1623
command = (
1724
settings.BABEL_BINARY,
18-
settings.BABEL_ARGUMENTS,
25+
args,
1926
infile,
2027
"-o",
2128
outfile

pipeline/compilers/less.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,15 @@ def match_file(self, filename):
1414

1515
def compile_file(self, infile, outfile, outdated=False, force=False):
1616
# Pipe to file rather than provide outfile arg due to a bug in lessc
17+
args = list(settings.LESS_ARGUMENTS)
18+
19+
if settings.OUTPUT_SOURCEMAPS and '--source-map' not in args:
20+
args += ['--source-map']
21+
1722
command = (
1823
settings.LESS_BINARY,
19-
settings.LESS_ARGUMENTS,
24+
args,
2025
infile,
26+
outfile,
2127
)
22-
return self.execute_command(command, cwd=dirname(infile), stdout_captured=outfile)
28+
return self.execute_command(command, cwd=dirname(infile))

pipeline/compilers/livescript.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import unicode_literals
22

3+
from os.path import dirname, basename
4+
import json
5+
36
from pipeline.conf import settings
47
from pipeline.compilers import SubProcessCompiler
58

@@ -13,10 +16,25 @@ def match_file(self, path):
1316
def compile_file(self, infile, outfile, outdated=False, force=False):
1417
if not outdated and not force:
1518
return # File doesn't need to be recompiled
19+
20+
args = list(settings.LIVE_SCRIPT_ARGUMENTS)
21+
if settings.OUTPUT_SOURCEMAPS and not(set(args) & set(['-m', '--map'])):
22+
args += ['--map', 'linked']
23+
1624
command = (
1725
settings.LIVE_SCRIPT_BINARY,
18-
"-cp",
19-
settings.LIVE_SCRIPT_ARGUMENTS,
26+
"-c",
27+
"-o", dirname(outfile),
28+
args,
2029
infile,
2130
)
22-
return self.execute_command(command, stdout_captured=outfile)
31+
ret = self.execute_command(command, cwd=dirname(outfile))
32+
33+
if settings.OUTPUT_SOURCEMAPS:
34+
with open("%s.map" % outfile) as f:
35+
source_map = json.loads(f.read())
36+
source_map['sources'] = map(basename, source_map['sources'])
37+
with open("%s.map" % outfile, mode='w') as f:
38+
f.write(json.dumps(source_map))
39+
40+
return ret

pipeline/compilers/sass.py

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

3+
import re
34
from os.path import dirname
45

56
from pipeline.conf import settings
@@ -9,14 +10,36 @@
910
class SASSCompiler(SubProcessCompiler):
1011
output_extension = 'css'
1112

13+
_sass_types = {}
14+
15+
@property
16+
def sass_type(self):
17+
bin = " ".join(settings.SASS_BINARY)
18+
if bin not in self._sass_types:
19+
if re.search(r'node\-sass', bin):
20+
self._sass_types[bin] = 'node'
21+
elif re.search(r'sassc', bin):
22+
self._sass_types[bin] = 'libsass'
23+
else:
24+
self._sass_types[bin] = 'ruby'
25+
return self._sass_types[bin]
26+
1227
def match_file(self, filename):
1328
return filename.endswith(('.scss', '.sass'))
1429

1530
def compile_file(self, infile, outfile, outdated=False, force=False):
16-
command = (
17-
settings.SASS_BINARY,
18-
settings.SASS_ARGUMENTS,
19-
infile,
20-
outfile
21-
)
31+
args = list(settings.SASS_ARGUMENTS)
32+
33+
if settings.OUTPUT_SOURCEMAPS:
34+
if self.sass_type == 'node':
35+
if '--source-map' not in args:
36+
args += ['--source-map', 'true']
37+
elif self.sass_type == 'libsass':
38+
if not(set(args) & set(['-m', 'g', '--sourcemap'])):
39+
args += ['--sourcemap']
40+
else:
41+
if not any([re.search(r'^\-\-sourcemap', a) for a in args]):
42+
args += ['--sourcemap=auto']
43+
44+
command = (settings.SASS_BINARY, args, infile, outfile)
2245
return self.execute_command(command, cwd=dirname(infile))

pipeline/compilers/stylus.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ def match_file(self, filename):
1313
return filename.endswith('.styl')
1414

1515
def compile_file(self, infile, outfile, outdated=False, force=False):
16+
args = list(settings.STYLUS_ARGUMENTS)
17+
18+
sourcemap_flags = set(['-s', '--sourcemap'])
19+
if settings.OUTPUT_SOURCEMAPS and not(set(args) & sourcemap_flags):
20+
args += ['--sourcemap']
21+
1622
command = (
1723
settings.STYLUS_BINARY,
18-
settings.STYLUS_ARGUMENTS,
24+
args,
1925
infile
2026
)
2127
return self.execute_command(command, cwd=dirname(infile))

pipeline/compressors/__init__.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import posixpath
66
import re
77
import subprocess
8+
import warnings
89

910
from itertools import takewhile
1011

@@ -57,29 +58,46 @@ def css_compressor(self):
5758

5859
def compress_js(self, paths, templates=None, **kwargs):
5960
"""Concatenate and compress JS files"""
61+
compressor = self.js_compressor
62+
63+
if settings.OUTPUT_SOURCEMAPS:
64+
if hasattr(compressor, 'compress_js_with_source_map'):
65+
if templates:
66+
warnings.warn("Source maps are not supported with javascript templates")
67+
else:
68+
return compressor(verbose=self.verbose).compress_js_with_source_map(paths)
69+
6070
js = self.concatenate(paths)
6171
if templates:
6272
js = js + self.compile_templates(templates)
6373

6474
if not settings.DISABLE_WRAPPER:
6575
js = "(function() {\n%s\n}).call(this);" % js
6676

67-
compressor = self.js_compressor
6877
if compressor:
6978
js = getattr(compressor(verbose=self.verbose), 'compress_js')(js)
7079

71-
return js
80+
return js, None
7281

7382
def compress_css(self, paths, output_filename, variant=None, **kwargs):
7483
"""Concatenate and compress CSS files"""
75-
css = self.concatenate_and_rewrite(paths, output_filename, variant)
7684
compressor = self.css_compressor
85+
86+
if settings.OUTPUT_SOURCEMAPS:
87+
if hasattr(compressor, 'compress_css_with_source_map'):
88+
if variant == "datauri":
89+
warnings.warn("Source maps are not supported with datauri variant")
90+
else:
91+
return (compressor(verbose=self.verbose)
92+
.compress_css_with_source_map(paths, output_filename))
93+
94+
css = self.concatenate_and_rewrite(paths, output_filename, variant)
7795
if compressor:
7896
css = getattr(compressor(verbose=self.verbose), 'compress_css')(css)
7997
if not variant:
80-
return css
98+
return css, None
8199
elif variant == "datauri":
82-
return self.with_data_uri(css)
100+
return self.with_data_uri(css), None
83101
else:
84102
raise CompressorError("\"%s\" is not a valid variant" % variant)
85103

@@ -235,16 +253,17 @@ def filter_js(self, js):
235253

236254

237255
class SubProcessCompressor(CompressorBase):
238-
def execute_command(self, command, content):
256+
def execute_command(self, command, content=None, **kwargs):
239257
argument_list = []
240258
for flattening_arg in command:
241259
if isinstance(flattening_arg, string_types):
242260
argument_list.append(flattening_arg)
243261
else:
244262
argument_list.extend(flattening_arg)
263+
stdin = subprocess.PIPE if content else None
245264

246265
pipe = subprocess.Popen(argument_list, stdout=subprocess.PIPE,
247-
stdin=subprocess.PIPE, stderr=subprocess.PIPE)
266+
stdin=stdin, stderr=subprocess.PIPE, **kwargs)
248267
if content:
249268
content = smart_bytes(content)
250269
stdout, stderr = pipe.communicate(content)

pipeline/compressors/cleancss.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,66 @@
11
from __future__ import unicode_literals
22

3+
import codecs
4+
import json
5+
import os
6+
7+
from django.contrib.staticfiles.storage import staticfiles_storage
8+
39
from pipeline.conf import settings
410
from pipeline.compressors import SubProcessCompressor
11+
from pipeline.utils import source_map_re, relurl
512

613

714
class CleanCSSCompressor(SubProcessCompressor):
815

916
def compress_css(self, css):
1017
args = [settings.CLEANCSS_BINARY, settings.CLEANCSS_ARGUMENTS]
1118
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)
65+
66+
return css, source_map

pipeline/compressors/closure.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,56 @@
11
from __future__ import unicode_literals
22

3+
import os
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
511

612

713
class ClosureCompressor(SubProcessCompressor):
14+
815
def compress_js(self, js):
916
command = (settings.CLOSURE_BINARY, settings.CLOSURE_ARGUMENTS)
1017
return self.execute_command(command, js)
18+
19+
def compress_js_with_source_map(self, paths):
20+
args = [settings.CLOSURE_BINARY, settings.CLOSURE_ARGUMENTS]
21+
22+
location_maps = set([])
23+
24+
abs_paths = []
25+
for path in paths:
26+
abs_path = staticfiles_storage.path(path)
27+
location_maps.add("%s|%s" % (
28+
os.path.dirname(abs_path),
29+
staticfiles_storage.url(os.path.dirname(path))))
30+
abs_paths.append(abs_path)
31+
with open(abs_path) as f:
32+
content = f.read()
33+
matches = source_map_re.search(content)
34+
if matches:
35+
input_source_map = filter(None, matches.groups())[0]
36+
input_source_map_file = os.path.join(os.path.dirname(abs_path), input_source_map)
37+
args += [
38+
'--source_map_input',
39+
"%s|%s" % (abs_path, input_source_map_file)]
40+
for location_map in location_maps:
41+
args += ['--source_map_location_mapping', location_map]
42+
43+
temp_file = tempfile.NamedTemporaryFile()
44+
45+
args += ["--create_source_map", temp_file.name]
46+
for path in abs_paths:
47+
args += ["--js", path]
48+
49+
js = self.execute_command(args, None)
50+
51+
with open(temp_file.name) as f:
52+
source_map = f.read()
53+
54+
temp_file.close()
55+
56+
return js, source_map

0 commit comments

Comments
 (0)