Skip to content

Commit 3b5c5a2

Browse files
committed
Add error output for compiler errors within the browser.
Debugging CSS/JavaScript compilation errors can be difficult when viewed within the context of a Django error page, and if there's a wider problem with other CSS/JavaScript packages, they won't show up until the first compilation error is fixed. This is worse if running in a mode where Pipeline is running but Django error pages aren't being shown. This change introduces an option, PIPELINE.SHOW_ERRORS_INLINE (now enabled by default) that outputs any compilation errors at the top of the page, visually distinguished from the rest of the page, and containing information on the failing package, command line, and output. This makes it much easier to debug what went wrong. The errors are outputted into a hidden div, and then once the page has fully loaded, they're placed at the top. This allows Pipeline usage anywhere on the page to promote errors to the top, in the order in which they're loaded. These also have a default style, but that can be overridden (and in fact the whole error template overridden) by the consuming application.
1 parent f2366b5 commit 3b5c5a2

File tree

8 files changed

+108
-13
lines changed

8 files changed

+108
-13
lines changed

docs/configuration.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ Defaults to ``True``
145145

146146
this only work when PIPELINE_ENABLED is False.
147147

148+
``SHOW_ERRORS_INLINE``
149+
......................
150+
151+
``True`` if errors compiling CSS/JavaScript files should be shown inline at
152+
the top of the browser window, or ``False`` if they should trigger exceptions
153+
(the older behavior).
154+
155+
This only applies when compiling through the ``{% stylesheet %}`` or
156+
``{% javascript %}`` template tags. It won't impact ``collectstatic``.
157+
158+
Defaults to ``settings.DEBUG``.
159+
148160
``CSS_COMPRESSOR``
149161
..................
150162

pipeline/compilers/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django.contrib.staticfiles.storage import staticfiles_storage
99
from django.core.files.base import ContentFile
1010
from django.utils.encoding import smart_bytes
11-
from django.utils.six import string_types
11+
from django.utils.six import string_types, text_type
1212

1313
from pipeline.conf import settings
1414
from pipeline.exceptions import CompilerError
@@ -125,7 +125,9 @@ def execute_command(self, command, cwd=None, stdout_captured=None):
125125
if compiling.returncode != 0:
126126
stdout_captured = None # Don't save erroneous result.
127127
raise CompilerError(
128-
"{0!r} exit code {1}\n{2}".format(argument_list, compiling.returncode, stderr))
128+
"{0!r} exit code {1}\n{2}".format(argument_list, compiling.returncode, stderr),
129+
command=argument_list,
130+
error_output=stderr)
129131

130132
# User wants to see everything that happened.
131133
if self.verbose:
@@ -134,7 +136,8 @@ def execute_command(self, command, cwd=None, stdout_captured=None):
134136
print(stderr)
135137
except OSError as e:
136138
stdout_captured = None # Don't save erroneous result.
137-
raise CompilerError(e)
139+
raise CompilerError(e, command=argument_list,
140+
error_output=text_type(e))
138141
finally:
139142
# Decide what to do with captured stdout.
140143
if stdout:

pipeline/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
'PIPELINE_ROOT': _settings.STATIC_ROOT,
2424
'PIPELINE_URL': _settings.STATIC_URL,
2525

26+
'SHOW_ERRORS_INLINE': _settings.DEBUG,
27+
2628
'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
2729
'JS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
2830
'COMPILERS': [],

pipeline/exceptions.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ class PackageNotFound(PipelineException):
1010

1111

1212
class CompilerError(PipelineException):
13-
pass
13+
def __init__(self, msg, command=None, error_output=None):
14+
super(CompilerError, self).__init__(msg)
15+
16+
self.command = command
17+
self.error_output = error_output.strip()
1418

1519

1620
class CompressorError(PipelineException):

pipeline/jinja2/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def package_css(self, package_name, *args, **kwargs):
3434
package = self.package_for(package_name, 'css')
3535
except PackageNotFound:
3636
return '' # fail silently, do not return anything if an invalid group is specified
37-
return self.render_compressed(package, 'css')
37+
return self.render_compressed(package, package_name, 'css')
3838

3939
def render_css(self, package, path):
4040
template_name = package.template_name or "pipeline/css.jinja"
@@ -55,7 +55,7 @@ def package_js(self, package_name, *args, **kwargs):
5555
package = self.package_for(package_name, 'js')
5656
except PackageNotFound:
5757
return '' # fail silently, do not return anything if an invalid group is specified
58-
return self.render_compressed(package, 'js')
58+
return self.render_compressed(package, package_name, 'js')
5959

6060
def render_js(self, package, path):
6161
template_name = package.template_name or "pipeline/js.jinja"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<div id="django-pipeline-error-{{package_name}}" class="django-pipeline-error"
2+
style="display: none; border: 2px #DD0000 solid; margin: 1em; padding: 1em; background: white;">
3+
<h1>Error compiling {{package_type}} package "{{package_name}}"</h1>
4+
<p><strong>Command:</strong></p>
5+
<pre style="white-space: pre-wrap;">{{command}}</pre>
6+
<p><strong>Errors:</strong></p>
7+
<pre style="white-space: pre-wrap;">{{errors}}</pre>
8+
</div>
9+
10+
<script>
11+
document.addEventListener('readystatechange', function() {
12+
var el,
13+
container;
14+
15+
if (document.readyState !== 'interactive') {
16+
return;
17+
}
18+
19+
el = document.getElementById('django-pipeline-error-{{package_name}}');
20+
container = document.getElementById('django-pipeline-errors');
21+
22+
if (!container) {
23+
container = document.createElement('div');
24+
container.id = 'django-pipeline-errors';
25+
document.body.insertBefore(container, document.body.firstChild);
26+
}
27+
28+
container.appendChild(el);
29+
el.style.display = 'block';
30+
});
31+
</script>

pipeline/templatetags/pipeline.py

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

33
import logging
4+
import subprocess
45

56
from django.contrib.staticfiles.storage import staticfiles_storage
67

78
from django import template
8-
from django.template.base import VariableDoesNotExist
9+
from django.template.base import Context, VariableDoesNotExist
910
from django.template.loader import render_to_string
1011
from django.utils.safestring import mark_safe
1112

1213
from ..collector import default_collector
1314
from ..conf import settings
15+
from ..exceptions import CompilerError
1416
from ..packager import Packager, PackageNotFound
1517
from ..utils import guess_type
1618

@@ -51,7 +53,7 @@ def render(self, context):
5153
except VariableDoesNotExist:
5254
pass
5355

54-
def render_compressed(self, package, package_type):
56+
def render_compressed(self, package, package_name, package_type):
5557
if settings.PIPELINE_ENABLED:
5658
method = getattr(self, "render_{0}".format(package_type))
5759
return method(package, package.output_filename)
@@ -61,10 +63,29 @@ def render_compressed(self, package, package_type):
6163

6264
packager = Packager()
6365
method = getattr(self, "render_individual_{0}".format(package_type))
64-
paths = packager.compile(package.paths)
66+
67+
try:
68+
paths = packager.compile(package.paths)
69+
except CompilerError as e:
70+
if settings.SHOW_ERRORS_INLINE:
71+
method = getattr(self, 'render_error_{0}'.format(
72+
package_type))
73+
74+
return method(package_name, e)
75+
else:
76+
raise
77+
6578
templates = packager.pack_templates(package)
6679
return method(package, paths, templates=templates)
6780

81+
def render_error(self, package_type, package_name, e):
82+
return render_to_string('pipeline/compile_error.html', Context({
83+
'package_type': package_type,
84+
'package_name': package_name,
85+
'command': subprocess.list2cmdline(e.command),
86+
'errors': e.error_output,
87+
}))
88+
6889

6990
class StylesheetNode(PipelineMixin, template.Node):
7091
def __init__(self, name):
@@ -79,7 +100,7 @@ def render(self, context):
79100
except PackageNotFound:
80101
logger.warn("Package %r is unknown. Check PIPELINE_CSS in your settings.", package_name)
81102
return '' # fail silently, do not return anything if an invalid group is specified
82-
return self.render_compressed(package, 'css')
103+
return self.render_compressed(package, package_name, 'css')
83104

84105
def render_css(self, package, path):
85106
template_name = package.template_name or "pipeline/css.html"
@@ -94,6 +115,10 @@ def render_individual_css(self, package, paths, **kwargs):
94115
tags = [self.render_css(package, path) for path in paths]
95116
return '\n'.join(tags)
96117

118+
def render_error_css(self, package_name, e):
119+
return super(StylesheetNode, self).render_error(
120+
'CSS', package_name, e)
121+
97122

98123
class JavascriptNode(PipelineMixin, template.Node):
99124
def __init__(self, name):
@@ -108,7 +133,7 @@ def render(self, context):
108133
except PackageNotFound:
109134
logger.warn("Package %r is unknown. Check PIPELINE_JS in your settings.", package_name)
110135
return '' # fail silently, do not return anything if an invalid group is specified
111-
return self.render_compressed(package, 'js')
136+
return self.render_compressed(package, package_name, 'js')
112137

113138
def render_js(self, package, path):
114139
template_name = package.template_name or "pipeline/js.html"
@@ -132,6 +157,10 @@ def render_individual_js(self, package, paths, templates=None):
132157
tags.append(self.render_inline(package, templates))
133158
return '\n'.join(tags)
134159

160+
def render_error_js(self, package_name, e):
161+
return super(JavascriptNode, self).render_error(
162+
'JavaScript', package_name, e)
163+
135164

136165
@register.tag
137166
def stylesheet(parser, token):

tests/tests/test_compiler.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,16 @@ def setUp(self):
156156
self.compiler = Compiler()
157157

158158
def test_compile(self):
159-
self.assertRaises(CompilerError, self.compiler.compile, [_('pipeline/js/dummy.coffee')])
159+
with self.assertRaises(CompilerError) as cm:
160+
self.compiler.compile([_('pipeline/js/dummy.coffee')])
161+
162+
e = cm.exception
163+
self.assertEqual(
164+
e.command,
165+
['this-exists-nowhere-as-a-command-and-should-fail',
166+
'pipeline/js/dummy.coffee',
167+
'pipeline/js/dummy.junk'])
168+
self.assertEqual(e.error_output, '')
160169

161170
def tearDown(self):
162171
default_collector.clear()
@@ -170,7 +179,12 @@ def setUp(self):
170179
self.compiler = Compiler()
171180

172181
def test_compile(self):
173-
self.assertRaises(CompilerError, self.compiler.compile, [_('pipeline/js/dummy.coffee')])
182+
with self.assertRaises(CompilerError) as cm:
183+
self.compiler.compile([_('pipeline/js/dummy.coffee')])
184+
185+
e = cm.exception
186+
self.assertEqual(e.command, ['/usr/bin/env', 'false'])
187+
self.assertEqual(e.error_output, '')
174188

175189
def tearDown(self):
176190
default_collector.clear()

0 commit comments

Comments
 (0)