Skip to content

Commit d6aeafe

Browse files
committed
Add sourcemap support
1 parent d068a01 commit d6aeafe

File tree

5 files changed

+121
-20
lines changed

5 files changed

+121
-20
lines changed

pipeline/compressors/__init__.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import posixpath
66
import re
7+
import warnings
78

89
from itertools import takewhile
910

@@ -55,29 +56,46 @@ def css_compressor(self):
5556

5657
def compress_js(self, paths, templates=None, **kwargs):
5758
"""Concatenate and compress JS files"""
59+
compressor = self.js_compressor
60+
61+
if settings.PIPELINE_OUTPUT_SOURCEMAPS:
62+
if hasattr(compressor, 'compress_js_with_source_map'):
63+
if templates:
64+
warnings.warn("Source maps are not supported with javascript templates")
65+
else:
66+
return compressor(verbose=self.verbose).compress_js_with_source_map(paths)
67+
5868
js = self.concatenate(paths)
5969
if templates:
6070
js = js + self.compile_templates(templates)
6171

6272
if not settings.PIPELINE_DISABLE_WRAPPER:
6373
js = "(function() {\n%s\n}).call(this);" % js
6474

65-
compressor = self.js_compressor
6675
if compressor:
6776
js = getattr(compressor(verbose=self.verbose), 'compress_js')(js)
6877

69-
return js
78+
return js, None
7079

7180
def compress_css(self, paths, output_filename, variant=None, **kwargs):
7281
"""Concatenate and compress CSS files"""
73-
css = self.concatenate_and_rewrite(paths, output_filename, variant)
7482
compressor = self.css_compressor
83+
84+
if settings.PIPELINE_OUTPUT_SOURCEMAPS:
85+
if hasattr(compressor, 'compress_css_with_source_map'):
86+
if variant == "datauri":
87+
warnings.warn("Source maps are not supported with datauri variant")
88+
else:
89+
return (compressor(verbose=self.verbose)
90+
.compress_css_with_source_map(paths, output_filename))
91+
92+
css = self.concatenate_and_rewrite(paths, output_filename, variant)
7593
if compressor:
7694
css = getattr(compressor(verbose=self.verbose), 'compress_css')(css)
7795
if not variant:
78-
return css
96+
return css, None
7997
elif variant == "datauri":
80-
return self.with_data_uri(css)
98+
return self.with_data_uri(css), None
8199
else:
82100
raise CompressorError("\"%s\" is not a valid variant" % variant)
83101

@@ -233,10 +251,11 @@ def filter_js(self, js):
233251

234252

235253
class SubProcessCompressor(CompressorBase):
236-
def execute_command(self, command, content):
254+
def execute_command(self, command, content, shell=True):
237255
import subprocess
238-
pipe = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE,
239-
stdin=subprocess.PIPE, stderr=subprocess.PIPE)
256+
stdin = subprocess.PIPE if content else None
257+
pipe = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
258+
stdin=stdin, stderr=subprocess.PIPE)
240259
if content:
241260
content = smart_bytes(content)
242261
stdout, stderr = pipe.communicate(content)

pipeline/compressors/closure.py

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

3+
import os
4+
import re
5+
import shlex
6+
import tempfile
7+
8+
from django.contrib.staticfiles.storage import staticfiles_storage
9+
from django.utils import six
10+
311
from pipeline.conf import settings
412
from pipeline.compressors import SubProcessCompressor
513

614

15+
source_map_re = re.compile((
16+
"(?:"
17+
"/\\*"
18+
"(?:\\s*\r?\n(?://)?)?"
19+
"(?:%(inner)s)"
20+
"\\s*"
21+
"\\*/"
22+
"|"
23+
"//(?:%(inner)s)"
24+
")"
25+
"\\s*$") % {'inner': r"""[#@] sourceMappingURL=([^\s'"]*)"""})
26+
27+
728
class ClosureCompressor(SubProcessCompressor):
29+
830
def compress_js(self, js):
931
command = '%s %s' % (settings.PIPELINE_CLOSURE_BINARY, settings.PIPELINE_CLOSURE_ARGUMENTS)
1032
return self.execute_command(command, js)
33+
34+
def compress_js_with_source_map(self, paths):
35+
args = re.split(r'\s+', settings.PIPELINE_CLOSURE_BINARY)
36+
if settings.PIPELINE_CLOSURE_ARGUMENTS:
37+
if isinstance(settings.PIPELINE_CLOSURE_ARGUMENTS, six.string_types):
38+
args += shlex.split(settings.PIPELINE_CLOSURE_ARGUMENTS)
39+
else:
40+
args += settings.PIPELINE_CLOSURE_ARGUMENTS
41+
abs_paths = []
42+
for path in paths:
43+
abs_path = staticfiles_storage.path(path)
44+
args += [
45+
'--source_map_location_mapping',
46+
"%s|%s" % (abs_path, staticfiles_storage.url(path))]
47+
abs_paths.append(abs_path)
48+
with open(abs_path) as f:
49+
content = f.read()
50+
matches = source_map_re.search(content)
51+
if matches:
52+
input_source_map = filter(None, matches.groups())[0]
53+
input_source_map_file = os.path.join(os.path.dirname(abs_path), input_source_map)
54+
args += [
55+
'--source_map_input',
56+
"%s|%s" % (abs_path, input_source_map_file)]
57+
58+
temp_file = tempfile.NamedTemporaryFile()
59+
60+
args += ["--create_source_map", temp_file.name]
61+
for path in abs_paths:
62+
args += ["--js", path]
63+
64+
js = self.execute_command(args, None, shell=False)
65+
66+
with open(temp_file.name) as f:
67+
source_map = f.read()
68+
69+
temp_file.close()
70+
71+
return js, source_map

pipeline/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
'PIPELINE_DISABLE_WRAPPER': False,
2727

28+
'PIPELINE_OUTPUT_SOURCEMAPS': False,
29+
2830
'PIPELINE_CSSTIDY_BINARY': '/usr/bin/env csstidy',
2931
'PIPELINE_CSSTIDY_ARGUMENTS': '--template=highest',
3032

pipeline/packager.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import unicode_literals
2+
import os.path
23

34
from django.contrib.staticfiles.storage import staticfiles_storage
45
from django.contrib.staticfiles.finders import find
@@ -92,24 +93,38 @@ def individual_url(self, filename):
9293

9394
def pack_stylesheets(self, package, **kwargs):
9495
return self.pack(package, self.compressor.compress_css, css_compressed,
96+
compress_type='css',
9597
output_filename=package.output_filename,
9698
variant=package.variant, **kwargs)
9799

98100
def compile(self, paths, force=False):
99101
return self.compiler.compile(paths, force=force)
100102

101-
def pack(self, package, compress, signal, **kwargs):
103+
def pack(self, package, compress, signal, compress_type, **kwargs):
102104
output_filename = package.output_filename
103105
if self.verbose:
104106
print("Saving: %s" % output_filename)
105107
paths = self.compile(package.paths, force=True)
106-
content = compress(paths, **kwargs)
108+
content, source_map = compress(paths, **kwargs)
109+
if source_map is not None:
110+
source_map_output_filename = output_filename + '.map'
111+
if self.verbose:
112+
print("Saving: %s" % source_map_output_filename)
113+
self.save_file(source_map_output_filename, source_map)
114+
source_map_comment = "sourceMappingURL=%s" % (
115+
os.path.basename(staticfiles_storage.url(source_map_output_filename)))
116+
if compress_type == 'js':
117+
content += "\n//# %s" % source_map_comment
118+
else:
119+
content += "\n/*# %s */" % source_map_comment
120+
yield source_map_output_filename
107121
self.save_file(output_filename, content)
108122
signal.send(sender=self, package=package, **kwargs)
109-
return output_filename
123+
yield output_filename
110124

111125
def pack_javascripts(self, package, **kwargs):
112-
return self.pack(package, self.compressor.compress_js, js_compressed, templates=package.templates, **kwargs)
126+
return self.pack(package, self.compressor.compress_js, js_compressed,
127+
compress_type='js', templates=package.templates, **kwargs)
113128

114129
def pack_templates(self, package):
115130
return self.compressor.compile_templates(package.templates)

pipeline/storage.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,22 @@ def post_process(self, paths, dry_run=False, **options):
2121
packager = Packager(storage=self)
2222
for package_name in packager.packages['css']:
2323
package = packager.package_for('css', package_name)
24-
output_file = package.output_filename
2524
if self.packing:
26-
packager.pack_stylesheets(package)
27-
paths[output_file] = (self, output_file)
28-
yield output_file, output_file, True
25+
output_files = packager.pack_stylesheets(package)
26+
else:
27+
output_files = [package.output_filename]
28+
for output_file in output_files:
29+
paths[output_file] = (self, output_file)
30+
yield output_file, output_file, True
2931
for package_name in packager.packages['js']:
3032
package = packager.package_for('js', package_name)
31-
output_file = package.output_filename
3233
if self.packing:
33-
packager.pack_javascripts(package)
34-
paths[output_file] = (self, output_file)
35-
yield output_file, output_file, True
34+
output_files = packager.pack_javascripts(package)
35+
else:
36+
output_files = [package.output_filename]
37+
for output_file in output_files:
38+
paths[output_file] = (self, output_file)
39+
yield output_file, output_file, True
3640

3741
super_class = super(PipelineMixin, self)
3842
if hasattr(super_class, 'post_process'):

0 commit comments

Comments
 (0)