File Coverage

blib/lib/Mojolicious/Plugin/AssetPack.pm
Criterion Covered Total %
statement 121 135 89.6
branch 33 52 63.4
condition 12 28 42.8
subroutine 18 20 90.0
pod 4 4 100.0
total 188 239 78.6


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::AssetPack;
2 11     11   129245 use Mojo::Base 'Mojolicious::Plugin';
  11         31  
  11         95  
3              
4 11     11   1527 use Mojo::Util 'trim';
  11         38  
  11         477  
5 11     11   3120 use Mojolicious::Plugin::AssetPack::Asset::Null;
  11         35  
  11         144  
6 11     11   4030 use Mojolicious::Plugin::AssetPack::Store;
  11         37  
  11         90  
7 11     11   442 use Mojolicious::Plugin::AssetPack::Util qw(diag has_ro load_module DEBUG);
  11         28  
  11         27562  
8              
9             our $VERSION = '2.00';
10              
11             my %TAG_TEMPLATE;
12             $TAG_TEMPLATE{css} = [qw(link rel stylesheet href)];
13             $TAG_TEMPLATE{ico} = [qw(link rel icon href)];
14             $TAG_TEMPLATE{js} = [qw(script src)];
15             $TAG_TEMPLATE{$_} = [qw(img src)] for qw(gif jpg jpeg png svg);
16             $TAG_TEMPLATE{$_} = [qw(source src)] for qw(mp3 mp4 ogg ogv webm);
17              
18             has minify => sub { shift->_app->mode eq 'development' ? 0 : 1 };
19              
20             has route => sub {
21             shift->_app->routes->route('/asset/:checksum/*name')->via(qw(HEAD GET))
22             ->name('assetpack')->to(cb => \&_serve);
23             };
24              
25             has store => sub {
26             my $self = shift;
27             Mojolicious::Plugin::AssetPack::Store->new(
28             classes => [@{$self->_app->static->classes}],
29             paths => [$self->_app->home->rel_file('assets')],
30             ua => $self->ua,
31             );
32             };
33              
34             has tag_for => sub {
35             my $self = shift;
36             my $favicon = $self->pipe('Favicon') ? 1 : 0;
37              
38             Scalar::Util::weaken($self);
39             return sub {
40             my ($asset, $c, $args, @attrs) = @_;
41             return $self->pipe('Favicon')->render($c)
42             if $args->{topic} eq 'favicon.ico' and $favicon;
43             my $url = $asset->url_for($c);
44             my @template = @{$TAG_TEMPLATE{$_->format} || $TAG_TEMPLATE{css}};
45             splice @template, 1, 0, type => $c->app->types->type($asset->format)
46             if $template[0] eq 'source';
47             return $c->tag(@template, Mojo::URL->new("$args->{base_url}$url"), @attrs);
48             };
49             };
50              
51             has_ro ua => sub { Mojo::UserAgent->new->max_redirects(3) };
52              
53             sub pipe {
54 9     9 1 30 my ($self, $needle) = @_;
55 9         24 return +(grep { $_ =~ /::$needle\b/ } @{$self->{pipes}})[0];
  14         202  
  9         31  
56             }
57              
58             sub process {
59 15     15 1 56 my ($self, $topic, @input) = @_;
60              
61 15 100       96 $self->route unless $self->{route_added}++;
62 15 100       3865 return $self->_process_from_def($topic) unless @input;
63              
64             # TODO: The idea with blessed($_) is that maybe the user can pass inn
65             # Mojolicious::Plugin::AssetPack::Sprites object, with images to generate
66             # CSS from?
67 11         99 my $assets = Mojo::Collection->new;
68 11         84 for my $url (@input) {
69 11 100       75 my $asset = Scalar::Util::blessed($url) ? $url : $self->store->asset($url);
70 11 100       178 die qq(Could not find input asset "$url".) unless Scalar::Util::blessed($asset);
71 10         64 push @$assets, $asset;
72             }
73              
74 10 50   0   43 return $self->tap(sub { $_->{input}{$topic} = $assets }) if $self->{lazy};
  0         0  
75 10         39 return $self->_process($topic => $assets);
76             }
77              
78 0     0 1 0 sub processed { $_[0]->{by_topic}{$_[1]} }
79              
80             sub register {
81 13     13 1 5250 my ($self, $app, $config) = @_;
82 13   100     88 my $helper = $config->{helper} || 'asset';
83              
84 13 50       57 if ($app->renderer->helpers->{$helper}) {
85 0         0 return $app->log->debug("AssetPack: Helper $helper() is already registered.");
86             }
87              
88 13         238 $self->{input} = {};
89 13   50     165 $self->{lazy} ||= $ENV{MOJO_ASSETPACK_LAZY} // $config->{lazy} || 0;
      33        
90 13         74 $app->defaults('assetpack.helper' => $helper);
91 13         574 $self->ua->server->app($app);
92 13         879 Scalar::Util::weaken($self->ua->server->{app});
93              
94 13 100 100     155 if (my $proxy = $config->{proxy} // {}) {
95             local $ENV{NO_PROXY} = $proxy->{no_proxy} || join ',', grep {$_} $ENV{NO_PROXY},
96 12   33     93 $ENV{no_proxy}, '127.0.0.1', '::1', 'localhost';
97 12         29 diag 'Detecting proxy settings. (NO_PROXY=%s)', $ENV{NO_PROXY} if DEBUG;
98 12         43 $self->ua->proxy->detect;
99             }
100              
101 13 50       708 if ($config->{pipes}) {
102 13         52 $self->_pipes($config->{pipes});
103 13 100   111   113 $app->helper($helper => sub { @_ == 1 ? $self : $self->_render_tags(@_) });
  111         315228  
104             }
105             else {
106 0         0 $app->log->warn(
107             'https://metacpan.org/release/Mojolicious-Plugin-AssetPack-Backcompat is required');
108             Test::More::diag("Loading DEPRECATED Mojolicious::Plugin::AssetPack::Backcompat.")
109 0 0 0     0 if $ENV{HARNESS_ACTIVE} and UNIVERSAL::can(qw(Test::More diag));
110 0         0 require Mojolicious::Plugin::AssetPack::Backcompat;
111 0         0 @Mojolicious::Plugin::AssetPack::ISA = ('Mojolicious::Plugin::AssetPack::Backcompat');
112 0         0 return $self->SUPER::register($app, $config);
113             }
114             }
115              
116 45     45   415 sub _app { shift->ua->server->app }
117              
118             sub _correct_mode {
119 4     4   10 my ($self, $args) = @_;
120              
121 4         16 while ($args =~ /\[(\w+)([!=]+)([^\]]+)/g) {
122 0 0       0 my $v = $1 eq 'minify' ? $self->minify : $self->_app->$1;
123 0         0 diag "Checking $1: $v $2 $3" if DEBUG == 2;
124 0 0 0     0 return 0 if $2 eq '!=' and $v eq $3;
125 0 0 0     0 return 0 if $2 ne '!=' and $v ne $3; # default to testing equality
126             }
127              
128 4         11 return 1;
129             }
130              
131             sub _pipes {
132 13     13   32 my ($self, $names) = @_;
133              
134             $self->{pipes} = [
135             map {
136 13 50       32 my $class = load_module /::/ ? $_ : "Mojolicious::Plugin::AssetPack::Pipe::$_";
  18         123  
137 18         51 diag 'Loading pipe "%s".', $class if DEBUG;
138 18         209 my $pipe = $class->new(assetpack => $self);
139 18         280 Scalar::Util::weaken($pipe->{assetpack});
140 18         80 $pipe;
141             } @$names
142             ];
143             }
144              
145             sub _process {
146 10     10   29 my ($self, $topic, $input) = @_;
147 10         50 my $assets = Mojo::Collection->new(@$input); # Do not mess up input
148              
149 10         68 local $Mojolicious::Plugin::AssetPack::Util::TOPIC = $topic; # Used by diag()
150              
151 10         24 for my $asset (@$assets) {
152 10 50       57 if (my $prev = $self->{by_topic}{$topic}) {
153 0         0 delete $asset->{$_} for qw(checksum format);
154 0         0 $asset->content($self->store->asset($asset->url));
155             }
156 10         37 $asset->checksum;
157             }
158              
159 10         43 for my $method (qw(before_process process after_process)) {
160 30         230 for my $pipe (@{$self->{pipes}}) {
  30         72  
161 45 100       554 next unless $pipe->can($method);
162 15         54 local $pipe->{topic} = $topic;
163 15         29 diag '%s->%s("%s")', ref $pipe, $method, $topic if DEBUG;
164 15         60 $pipe->$method($assets);
165             }
166             }
167              
168 10         38 my @checksum = map { $_->checksum } @$assets;
  10         33  
169 10         67 $self->_app->log->debug(qq(Processed asset "$topic". [@checksum])) if DEBUG;
170 10         56 $self->{by_checksum}{$_->checksum} = $_ for @$assets;
171 10         64 $self->{by_topic}{$topic} = $assets;
172 10         35 $self->store->persist;
173 10         67 $self;
174             }
175              
176             sub _process_from_def {
177 4     4   10 my $self = shift;
178 4   50     33 my $file = shift || 'assetpack.def';
179 4         15 my $asset = $self->store->file($file);
180 4         1069 my $topic = '';
181 4         9 my %process;
182              
183 4 50       13 die qq(Unable to load "$file".) unless $asset;
184 4         8 diag qq(Loading asset definitions from "$file".) if DEBUG;
185              
186 4         15 for (split /\r?\n/, $asset->slurp) {
187 8         108 s/\s*\#.*//;
188 8 100       48 if (/^\<(\S*)\s+(\S+)\s*(.*)/) {
    50          
189 4         20 my ($class, $url, $args) = ($1, $2, $3);
190 4 50       15 next unless $self->_correct_mode($args);
191 4         14 my $asset = $self->store->asset($url);
192 4 100       73 die qq(Could not find input asset "$url".) unless Scalar::Util::blessed($asset);
193 3 50       9 bless $asset, 'Mojolicious::Plugin::AssetPack::Asset::Null' if $class eq '<';
194 3         5 push @{$process{$topic}}, $asset;
  3         17  
195             }
196 4         17 elsif (/^\!\s*(.+)/) { $topic = trim $1; }
197             }
198              
199 3         12 $self->process($_ => @{$process{$_}}) for keys %process;
  3         18  
200 3         25 $self;
201             }
202              
203             sub _render_tags {
204 72     72   229 my ($self, $c, $topic, @attrs) = @_;
205 72         504 my $route = $self->route;
206              
207 72 50       784 $self->_process($topic => $self->{input}{$topic}) if $self->{lazy};
208              
209 72   66     276 my $assets = $self->{by_topic}{$topic} ||= $self->_static_asset($topic);
210 72   100     269 my %args = (base_url => $route->pattern->defaults->{base_url} || '', topic => $topic);
211 72         963 $args{base_url} =~ s!/+$!!;
212              
213 72     72   1247 return $assets->grep(sub { !$_->isa('Mojolicious::Plugin::AssetPack::Asset::Null') })
214 72         452 ->map($self->tag_for, $c, \%args, @attrs)->join("\n");
215             }
216              
217             sub _serve {
218 20     20   221178 my $c = shift;
219 20         89 my $checksum = $c->stash('checksum');
220 20         243 my $helper = $c->stash('assetpack.helper');
221 20         311 my $self = $c->$helper;
222              
223 20 100       160 if (my $f = $self->{by_checksum}{$checksum}) {
224 18         82 $self->store->serve_asset($c, $f);
225 18         86 return $c->rendered;
226             }
227              
228 2         7 my $topic = $c->stash('name');
229 2 100       23 if ($self->{by_topic}{$topic}) {
230 1         11 return $c->render(text => "// Invalid checksum for topic '$topic'\n", status => 404);
231             }
232              
233 1         9 $c->render(text => "// No such asset '$topic'\n", status => 404);
234             }
235              
236             sub _static_asset {
237 8     8   17 my ($self, $topic) = @_;
238 8 50       24 my $asset = $self->store->asset($topic)
239             or die qq(No assets registered by topic "$topic".);
240 8         135 my $assets = Mojo::Collection->new($asset);
241 8         60 $self->{by_checksum}{$_->checksum} = $_ for @$assets;
242 8         30 return $assets;
243             }
244              
245             1;
246              
247             =encoding utf8
248              
249             =head1 NAME
250              
251             Mojolicious::Plugin::AssetPack - Compress and convert css, less, sass, javascript and coffeescript files
252              
253             =head1 VERSION
254              
255             2.00
256              
257             =head1 SYNOPSIS
258              
259             =head2 Application
260              
261             use Mojolicious::Lite;
262              
263             # Load plugin and pipes in the right order
264             plugin AssetPack => {
265             pipes => [qw(Less Sass Css CoffeeScript Riotjs JavaScript Combine)]
266             };
267              
268             # define asset
269             app->asset->process(
270             # virtual name of the asset
271             "app.css" => (
272              
273             # source files used to create the asset
274             "sass/bar.scss",
275             "https://github.com/Dogfalo/materialize/blob/master/sass/materialize.scss",
276             )
277             );
278              
279             =head2 Template
280              
281            
282            
283             %= asset "app.css"
284            
285             <%= content %>
286            
287              
288             =head1 DESCRIPTION
289              
290             L is L
291             for processing static assets. The idea is that JavaScript and CSS files should
292             be served as one minified file to save bandwidth and roundtrip time to the
293             server.
294              
295             There are many external tools for doing this, but integrating them with
296             L can be a struggle: You want to serve the source files directly
297             while developing, but a minified version in production. This assetpack plugin
298             will handle all of that automatically for you.
299              
300             Your application creates and refers to an asset by its topic (virtual asset
301             name). The process of building actual assets from their components is
302             delegated to "pipe objects". Please see
303             L for a complete list.
304              
305             =head1 BREAKING CHANGES
306              
307             =head2 assetpack.db (v1.47)
308              
309             C no longer track files downloaded from the internet. It will
310             mostly "just work", but in some cases version 1.47 might download assets that
311             have already been downloaded with AssetPack version 1.46 and earlier.
312              
313             The goal is to remove C completely.
314              
315             =head1 GUIDES
316              
317             =over 2
318              
319             =item * L
320              
321             The tutorial will give an introduction to how AssetPack can be used.
322              
323             =item * L
324              
325             The "developing" guide will give insight on how to do effective development with
326             AssetPack and more details about the internals in this plugin.
327              
328             =item * L
329              
330             The cookbook has various receipes on how to cook with AssetPack.
331              
332             =back
333              
334             =head1 HELPERS
335              
336             =head2 asset
337              
338             $self = $app->asset;
339             $self = $c->asset;
340             $bytestream = $c->asset($topic, @args);
341             $bytestream = $c->asset("app.css", media => "print");
342              
343             C is the main entry point to this plugin. It can either be used to
344             access the L instance or as a tag helper.
345              
346             The helper name "asset" can be customized by specifying "helper" when
347             L the plugin.
348              
349             See L for more details.
350              
351             =head1 ATTRIBUTES
352              
353             =head2 minify
354              
355             $bool = $self->minify;
356             $self = $self->minify($bool);
357              
358             Set this to true to combine and minify the assets. Defaults to false if
359             L is "development" and true otherwise.
360              
361             See L
362             for more details.
363              
364             =head2 route
365              
366             $route = $self->route;
367             $self = $self->route($route);
368              
369             A L object used to serve assets. The default route
370             responds to HEAD and GET requests and calls
371             L on L
372             to serve the asset.
373              
374             The default route will be built and added to the L
375             when L is called the first time.
376              
377             See L
378             for an example on how to customize this route.
379              
380             =head2 store
381              
382             $obj = $self->store;
383             $self = $self->store(Mojolicious::Plugin::AssetPack::Store->new);
384              
385             Holds a L object used to locate, store
386             and serve assets.
387              
388             =head2 tag_for
389              
390             $self = $self->tag_for(sub { my ($asset, $c, \%args, @attrs) = @_; });
391             $code = $self->tag_for;
392              
393             Holds a sub reference that returns a L object containing the
394             markup required to render an asset.
395              
396             C<$asset> is a L object, C<$c> is an
397             L object and C<@attrs> can contain a list of
398             HTML attributes. C<%args> currently contains:
399              
400             =over 4
401              
402             =item * base_url
403              
404             See L.
405              
406             =item * topic
407              
408             Name of the current topic.
409              
410             =back
411              
412             =head2 ua
413              
414             $ua = $self->ua;
415              
416             Holds a L which can be used to fetch assets either from local
417             application or from remote web servers.
418              
419             =head1 METHODS
420              
421             =head2 pipe
422              
423             $obj = $self->pipe($name);
424             $obj = $self->pipe("Css");
425              
426             Will return a registered pipe by C<$name> or C if none could be found.
427              
428             =head2 process
429              
430             $self = $self->process($topic => @assets);
431             $self = $self->process($definition_file);
432              
433             Used to process assets. A C<$definition_file> can be used to define C<$topic>
434             and C<@assets> in a separate file. See
435             L for more
436             details.
437              
438             C<$definition_file> defaults to "assetpack.def".
439              
440             =head2 processed
441              
442             $collection = $self->processed($topic);
443              
444             Can be used to retrieve a L object, with zero or more
445             L objects. Returns undef if C<$topic> is
446             not defined with L.
447              
448             =head2 register
449              
450             $self->register($app, \%config);
451              
452             Used to register the plugin in the application. C<%config> can contain:
453              
454             =over 2
455              
456             =item * helper
457              
458             Name of the helper to add to the application. Default is "asset".
459              
460             =item * pipes
461              
462             This argument is mandatory and need to contain a complete list of pipes that is
463             needed. Example:
464              
465             $app->plugin(AssetPack => {pipes => [qw(Sass Css Combine)]);
466              
467             See L for a complete
468             list of available pipes.
469              
470             =item * proxy
471              
472             A hash of proxy settings. Set this to C<0> to disable proxy detection.
473             Currently only "no_proxy" is supported, which will set which requests that
474             should bypass the proxy (if any proxy is detected). Default is to bypass all
475             requests to localhost.
476              
477             See L for more information.
478              
479             =back
480              
481             =head1 SEE ALSO
482              
483             L,
484             L,
485             L and
486             L.
487              
488             =head1 COPYRIGHT AND LICENSE
489              
490             Copyright (C) 2014, Jan Henning Thorsen
491              
492             This program is free software, you can redistribute it and/or modify it under
493             the terms of the Artistic License version 2.0.
494              
495             =head1 AUTHOR
496              
497             Jan Henning Thorsen - C
498              
499             Alexander Rymasheusky
500              
501             Mark Grimes - C
502              
503             Per Edin - C
504              
505             Viktor Turskyi
506              
507             =cut