File Coverage

blib/lib/Mojolicious/Plugin/AssetPack/Backcompat.pm
Criterion Covered Total %
statement 181 275 65.8
branch 48 98 48.9
condition 14 46 30.4
subroutine 37 46 80.4
pod 8 8 100.0
total 288 473 60.8


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::AssetPack::Backcompat;
2 2     2   22423 use Mojo::Base 'Mojolicious::Plugin';
  2         3  
  2         12  
3 2     2   325 use Mojo::ByteStream;
  2         3  
  2         72  
4 2     2   8 use Mojo::JSON ();
  2         2  
  2         31  
5 2     2   5 use Mojo::Util ();
  2         2  
  2         28  
6 2     2   763 use Mojolicious::Plugin::AssetPack::Preprocessors;
  2         6  
  2         28  
7 2     2   69 use Cwd ();
  2         3  
  2         40  
8 2     2   6 use File::Basename qw( basename );
  2         2  
  2         86  
9 2     2   7 use File::Path ();
  2         2  
  2         28  
10 2     2   7 use File::Spec::Functions qw( catdir catfile );
  2         2  
  2         108  
11 2   50 2   7 use constant DEBUG => $ENV{MOJO_ASSETPACK_DEBUG} || 0;
  2         3  
  2         137  
12 2   50 2   5 use constant NO_CACHE => $ENV{MOJO_ASSETPACK_NO_CACHE} || 0;
  2         2  
  2         5584  
13              
14             my $MONIKER_RE = qr{^(.+)\.(\w+)$};
15              
16             our $VERSION = '1.22';
17              
18             has base_url => '/packed/';
19             has preprocessors => sub { Mojolicious::Plugin::AssetPack::Preprocessors->new };
20              
21             sub add {
22 2     2 1 5 my ($self, $moniker, @files) = @_;
23              
24 2         11 @files = $self->_expand_wildcards(@files);
25 2     0   4 return $self->tap(sub { $self->{files}{$moniker} = \@files; $self }) if NO_CACHE;
  0         0  
  0         0  
26 2     2   49 return $self->tap(sub { $self->_processed($moniker, $self->_process($moniker, @files)) }
27 2 50       9 ) if $self->minify;
28             return $self->tap(
29 0     0   0 sub { $self->_processed($moniker, $self->_process_many($moniker, @files)) });
  0         0  
30             }
31              
32             sub fetch {
33 0     0 1 0 my $self = shift;
34 0         0 my $url = Mojo::URL->new(shift);
35 0   0     0 my $asset = $self->_handler($url->scheme || 'https')->asset_for($url, $self);
36 0 0       0 return $asset if @_; # internal
37 0         0 return $asset->path; # documented api
38             }
39              
40             sub get {
41 4     4 1 6 my ($self, $moniker, $args) = @_;
42 4         17 my @assets = $self->_processed($moniker);
43              
44 4 50       14 return @assets if $args->{assets};
45 4 50       30 return map { $_->slurp } @assets if $args->{inline};
  3         13  
46 0         0 return map { $self->base_url . basename($_->path) } @assets;
  0         0  
47             }
48              
49             sub headers {
50 0     0 1 0 my ($self, $headers) = @_;
51              
52             $self->_app->hook(
53             after_static => sub {
54 0     0   0 my $c = shift;
55 0         0 my $path = $c->req->url->path->canonicalize;
56 0 0 0     0 return unless $path->[1] and 0 == index "$path", $self->base_url;
57 0         0 my $h = $c->res->headers;
58 0         0 $h->header($_ => $headers->{$_}) for keys %$headers;
59             }
60 0         0 );
61             }
62              
63 4     4 1 54 sub out_dir { shift->{out_dir} }
64              
65             sub purge {
66 0     0 1 0 my ($self, $args) = @_;
67 0 0       0 my $file_re
68             = $self->minify ? qr/^(.*?)-(\w{32})\.min\.(\w+)$/ : qr/^(.*?)-(\w{32})\.(\w+)$/;
69 0         0 my ($PACKED, %existing);
70              
71             # default to not purging, unless in development mode
72 0   0     0 local $args->{always} = $args->{always} // $self->_app->mode eq 'development';
73              
74 0 0       0 return $self unless $args->{always};
75             die '$app->asset->purge() must be called AFTER $app->asset(...)'
76 0 0       0 unless keys %{$self->{asset} || {}};
  0 0       0  
77 0 0 0     0 return $self unless -w $self->out_dir and opendir $PACKED, $self->out_dir;
78 0         0 $existing{$_} = 1 for grep { $_ =~ $file_re } readdir $PACKED;
  0         0  
79 0 0       0 delete $existing{$_} for map { basename $_->path } values %{$self->{asset} || {}};
  0         0  
  0         0  
80              
81 0         0 for my $file (keys %existing) {
82 0         0 unlink catfile $self->out_dir, $file;
83 0   0     0 $self->_app->log->debug("AssetPack purge $file: @{[$! || 'Deleted']}");
  0         0  
84             }
85              
86 0         0 return $self;
87             }
88              
89             sub register {
90 2     2 1 54 my ($self, $app, $config) = @_;
91 2   50     15 my $helper = $config->{helper} || 'asset';
92              
93 2 50       6 if (my $paths = $config->{source_paths}) {
94             $self->{source_paths}
95 0 0       0 = [map { -d $_ ? Cwd::abs_path($_) : $app->home->rel_file($_) } @$paths];
  0         0  
96             }
97              
98 2 50       6 $self->headers($config->{headers}) if $config->{headers};
99 2   33     17 $self->minify($config->{minify} // $app->mode ne 'development');
100 2 50       44 $self->base_url($config->{base_url}) if $config->{base_url};
101 2         9 $self->_build_out_dir($app, $config);
102              
103             $app->helper(
104             $helper => sub {
105 8 100   8   51743 return $self if @_ == 1;
106 6 100 66     66 return shift, $self->add(@_) if @_ > 2 and ref $_[2] ne 'HASH';
107 4         40 return $self->_inject(@_);
108             }
109 2         22 );
110             }
111              
112             sub source_paths {
113 3     3 1 4 my $self = shift;
114 3 50 33     27 return $self->{source_paths} || $self->_app->static->paths unless @_;
115 0         0 $self->{source_paths} = shift;
116 0         0 return $self;
117             }
118              
119             sub _asset {
120 7     7   13 my ($self, $name) = @_;
121 7   66     60 $self->{asset}{$name} ||= Mojolicious::Plugin::AssetPack::Backcompat::Asset->new(
122             path => catfile $self->out_dir,
123             $name
124             );
125             }
126              
127             sub _build_out_dir {
128 2     2   6 my ($self, $app, $config) = @_;
129 2         2 my ($out_dir, $packed);
130              
131 2 50       6 if ($out_dir = $config->{out_dir}) {
132 0         0 my $static_dir = Cwd::abs_path(catdir $out_dir, File::Spec->updir);
133 0         0 push @{$app->static->paths}, $static_dir
134 0 0       0 unless grep { $_ eq $static_dir } @{$app->static->paths};
  0         0  
  0         0  
135             }
136 2 50       4 if (!$out_dir) {
137 2         4 for my $path (@{$app->static->paths}) {
  2         12  
138 2         32 $packed = catdir $path, 'packed';
139 2 50       56 if (-w $path) { $out_dir = Cwd::abs_path($packed); last }
  2         77  
  2         7  
140 0 0 0     0 if (-r $packed) { $out_dir ||= Cwd::abs_path($packed) }
  0         0  
141             }
142             }
143              
144 2 50 33     9 $out_dir ||= $packed or die "[AssetPack] app->static->paths is not set";
145 2 100       326 File::Path::make_path($out_dir) unless -d $out_dir;
146 2         6 $self->{out_dir} = $out_dir;
147             }
148              
149             sub _expand_wildcards {
150 2     2   4 my $self = shift;
151 2         2 my (@files, %seen);
152              
153 2         5 for my $file (@_) {
154 3 50 33     90 if (!-e $file and $file =~ /\*/) {
155 0         0 my @rel = split '/', $file;
156 0         0 my $glob = pop @rel;
157              
158 0         0 for my $path (map { catdir $_, @rel } @{$self->source_paths}) {
  0         0  
  0         0  
159 0         0 my $cwd = Mojolicious::Plugin::AssetPack::Preprocessors::CWD->new($path);
160 0         0 push @files, grep { !$seen{$_} } map { join '/', @rel, $_ } sort glob $glob;
  0         0  
  0         0  
161             }
162             }
163             else {
164 3         365 push @files, $file;
165 3         9 $seen{$file} = 1;
166             }
167             }
168              
169 2         71 return @files;
170             }
171              
172             sub _handler {
173 1     1   3 my ($self, $moniker) = @_;
174 1   33     6 $self->{handler}{$moniker} ||= do {
175 1         4 my $class = "Mojolicious::Plugin::AssetPack::Handler::" . ucfirst $moniker;
176 1 50       93 eval "require $class;1" or die "Could not load $class: $@\n";
177 1         11 $class->new;
178             };
179             }
180              
181             sub _inject {
182 4     4   10 my ($self, $c, $moniker, $args, @attrs) = @_;
183 4 50       20 my $tag_helper = $moniker =~ /\.js/ ? 'javascript' : 'stylesheet';
184              
185             NO_CACHE
186             and $self->_processed($moniker,
187 4         5 $self->_process_many($moniker, @{$self->{files}{$moniker} || []}));
188              
189 4 100       21 return Mojo::ByteStream->new(qq())
190             unless my @res = $self->get($moniker, $args);
191 3 50   3   50 return $c->$tag_helper(@attrs, sub { join '', @res }) if $args->{inline};
  3         161  
192 0         0 return Mojo::ByteStream->new(join "\n", map { $c->$tag_helper($_, @attrs) } @res);
  0         0  
193             }
194              
195             sub _packed {
196 4 50   4   12 my $sorter = ref $_[-1] eq 'CODE' ? pop : sub {@_};
  3     3   95  
197 3         7 my ($self, $needle) = @_;
198              
199 3         4 for my $dir (map { catdir $_, 'packed' } @{$self->_app->static->paths}) {
  7         114  
  3         13  
200 6 100       166 opendir my $DH, $dir or next;
201 4         83 for my $file ($sorter->(map { catfile $dir, $_ } readdir $DH)) {
  10         51  
202 8         196 my $name = basename $file;
203 8 100       77 next unless $name =~ $needle;
204 1         1 $self->_app->log->debug("Using existing asset $file") if DEBUG;
205 1         4 return $self->_asset($name)->path($file);
206             }
207             }
208              
209 2         11 return undef;
210             }
211              
212             sub _process {
213 2     2   5 my ($self, $moniker, @sources) = @_;
214 2         4 my $topic = $moniker;
215 2         19 my ($name, $ext) = $moniker =~ $MONIKER_RE;
216 2         3 my ($asset, $file, @checksum);
217              
218 2         4 eval {
219 2         4 for my $s (@sources) {
220 3         4 $topic = $s;
221 3         15 $s = $self->_source_for_url($s); # rewrite @sources
222 3         39 push @checksum, $self->preprocessors->checksum(_ext($topic), \$s->slurp, $s->path);
223 3         7 warn sprintf "[AssetPack] Checksum $checksum[-1] from %s\n", $s->path if DEBUG;
224             }
225              
226 2 100       11 @checksum = (Mojo::Util::md5_sum(join '', @checksum)) if @checksum > 1;
227 2 50       7 $asset
228             = $self->_packed($self->minify
229             ? qr{^$name-$checksum[0](\.min)?\.$ext$}
230             : qr{^$name-$checksum[0]\.$ext$});
231 2 100       29 return $asset if $asset; # already processed
232              
233 1 50       4 $file = $self->minify ? "$name-$checksum[0].min.$ext" : "$name-$checksum[0].$ext";
234 1         11 $asset = $self->_asset($file);
235             warn sprintf "[AssetPack] Creating %s from %s\n", $file, join ', ',
236 1         9 map { $_->path } @sources
237             if DEBUG;
238              
239 1         3 for my $s (@sources) {
240 1         3 $topic = basename($s->path);
241 1         31 my $content = $s->slurp;
242 1         3 $self->preprocessors->process(_ext($s->path), $self, \$content, $s->path);
243 1         6 $asset->add_chunk($content);
244             }
245              
246 1         7 $self->{processed}{$moniker} = [$file];
247 1         6 $self->_app->log->info(
248 1         32 "AssetPack built @{[$asset->path]} for @{[$self->_app->moniker]}.");
  1         14  
249             };
250              
251 2 50       62 return $asset unless $@;
252              
253 0         0 my $source_paths = join ',', @{$self->source_paths};
  0         0  
254 0         0 my $static_paths = join ',', @{$self->_app->static->paths};
  0         0  
255 0         0 die
256             "[AssetPack/$moniker] $@ {source_paths=[$source_paths], static_paths=[$static_paths]}";
257             }
258              
259             sub _process_many {
260 0     0   0 my ($self, $moniker, @files) = @_;
261 0         0 my $ext = _ext($moniker);
262              
263             return map {
264 0         0 my $topic = $_;
  0         0  
265 0         0 local $_ = $topic; # do not modify input
266 0 0       0 s![^\w-]!_!g if /^https?:/;
267 0         0 s!\.\w+$!!;
268 0         0 $_ = basename $_;
269 0         0 $self->_process("$_.$ext" => $topic);
270             } @files;
271             }
272              
273             sub _processed {
274 6     6   14 my ($self, $moniker, @assets) = @_;
275 6 100       20 return map { $self->_asset($_) } @{$self->{processed}{$moniker} || []} unless @assets;
  3 100       18  
  4         23  
276 2         4 $self->{processed}{$moniker} = [map { basename $_->path } @assets];
  2         7  
277 2         68 return $self;
278             }
279              
280             sub _source_for_url {
281 3     3   13 my ($self, $url) = @_;
282              
283 3 100       10 if ($self->{asset}{$url}) {
284 1         1 warn "[AssetPack] Asset already loaded: $url\n" if DEBUG;
285 1         3 return $self->{asset}{$url};
286             }
287 2 50       88 if (my $scheme = Mojo::URL->new($url)->scheme) {
288 0         0 warn "[AssetPack] Asset from online resource: $url\n" if DEBUG;
289 0         0 return $self->fetch($url, 'internal');
290             }
291              
292 2         215 my @look_in = (@{$self->source_paths}, @{$self->_app->static->paths});
  2         8  
  2         62  
293 2         32 my @path = split '/', $url;
294              
295 2         75 for my $file (map { catfile $_, @path } @look_in) {
  10         31  
296 7 100 66     125 next unless $file and -r $file;
297 1         2 warn "[AssetPack] Asset from disk: $url ($file)\n" if DEBUG;
298 1         10 return $self->_asset("$url")->path($file);
299             }
300              
301 1         2 warn "[AssetPack] Asset from @{[$self->_app->moniker]}: $url\n" if DEBUG;
302 1         13 return $self->_handler('https')->asset_for($url, $self);
303             }
304              
305             # utils
306 4 50   4   119 sub _ext { local $_ = basename $_[0]; /\.(\w+)$/ ? $1 : 'unknown'; }
  4         213  
307              
308             sub _sort_by_mtime {
309 0     0   0 map { $_->[0] } sort { $b->[1] <=> $a->[1] } map { [$_, (stat $_)[9]] } @_;
  0         0  
  0         0  
  0         0  
310             }
311              
312             package Mojolicious::Plugin::AssetPack::Backcompat::Asset;
313              
314 2     2   13 use Mojo::Base -base;
  2         2  
  2         13  
315 2     2   336 use File::Basename 'dirname';
  2         4  
  2         102  
316 2     2   9 use Fcntl qw( O_CREAT O_EXCL O_RDONLY O_RDWR );
  2         3  
  2         94  
317 2     2   11 use IO::File;
  2         2  
  2         1404  
318              
319             has handle => sub {
320             my $self = shift;
321             my $path = $self->path;
322             my $handle = IO::File->new;
323              
324             if (-w $path) {
325             $handle->open($path, O_RDWR) or die "Can't open $path (O_RDWR): $!";
326             }
327             elsif (!-r _ and -w dirname($path)) {
328             $handle->open($path, O_CREAT | O_EXCL | O_RDWR)
329             or die "Can't open $path (O_CREAT|O_EXCL|O_RDWR): $!";
330             }
331             else {
332             $handle->open($path, O_RDONLY) or die "Can't open $path (O_RDONLY): $!";
333             }
334              
335             return $handle;
336             };
337              
338             has path => undef;
339              
340             sub add_chunk {
341 1     1   3 my $self = shift;
342 1 50       4 defined $self->handle->syswrite($_[0]) or die "Can't write to @{[$self->path]}: $!";
  0         0  
343 1         46 return $self;
344             }
345              
346             sub slurp {
347 7     7   11 my $self = shift;
348 7         18 my $handle = $self->handle;
349 7         50 $handle->sysseek(0, 0);
350 7 50       112 defined $handle->sysread(my $content, -s $handle, 0)
351 0         0 or die "Can't read from @{[$self->path]}: $!";
352 7         97 return $content;
353             }
354              
355             sub spurt {
356 1     1   45 my $self = shift;
357 1         5 my $handle = $self->handle;
358 1         13 $handle->truncate(0);
359 1         57 $handle->sysseek(0, 0);
360 1 50       12 defined $handle->syswrite($_[0]) or die "Can't write to @{[$self->path]}: $!";
  0         0  
361 1         44 return $self;
362             }
363              
364             sub _spurt_error_message_for {
365 0     0     my ($self, $ext, $err) = @_;
366              
367 0           $err =~ s!\r!!g;
368 0           $err =~ s!\n+$!!;
369              
370 0 0         if ($ext eq 'js') {
371 0           $err =~ s!'!"!g;
372 0           $err =~ s!\n!\\n!g;
373 0           $err =~ s!\s! !g;
374 0           $err = "alert('$err');console.log('$err');";
375             }
376             else {
377 0           $err =~ s!"!'!g;
378 0           $err =~ s!\n!\\A!g;
379 0           $err =~ s!\s! !g;
380 0           $err
381             = qq(html:before{background:#f00;color:#fff;font-size:14pt;position:fixed;padding:20px;z-index:9999;content:"$err";});
382             }
383              
384 0           $self->spurt($err);
385             }
386              
387             1;
388              
389             =head1 NAME
390              
391             Mojolicious::Plugin::AssetPack::Backcompat - Provides back compat functionality for 0.x series of AssetPack
392              
393             =head1 DESCRIPTION
394              
395             L is only useful for those who use
396             the old version of L.
397              
398             Have a look at L
399             for an explanation behind the change.
400              
401             Please move to the new version of the plugin and/or send me an email and/or
402             create and issue on
403             L if
404             you have reasons for not moving on.
405              
406             =head1 METHODS
407              
408             =head2 add
409              
410             =head2 base_url
411              
412             =head2 fetch
413              
414             =head2 get
415              
416             =head2 headers
417              
418             =head2 out_dir
419              
420             =head2 preprocessors
421              
422             =head2 purge
423              
424             =head2 register
425              
426             =head2 source_paths
427              
428             =head1 SEE ALSO
429              
430             L.
431              
432             =cut