File Coverage

blib/lib/Mojolicious/Plugin/AssetPack.pm
Criterion Covered Total %
statement 126 139 90.6
branch 41 54 75.9
condition 19 30 63.3
subroutine 17 20 85.0
pod 5 5 100.0
total 208 248 83.8


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