File Coverage

blib/lib/Mojolicious/Plugin/StaticCompressor.pm
Criterion Covered Total %
statement 107 121 88.4
branch 23 34 67.6
condition 15 28 53.5
subroutine 21 24 87.5
pod 1 1 100.0
total 167 208 80.2


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::StaticCompressor;
2 4     4   4466 use Mojo::Base 'Mojolicious::Plugin';
  4         10  
  4         36  
3              
4 4     4   2151 use warnings;
  4         9  
  4         122  
5 4     4   37 use strict;
  4         6  
  4         112  
6 4     4   23 use utf8;
  4         7  
  4         15  
7             our $VERSION = '1.0.0';
8              
9 4     4   2111 use Encode qw//;
  4         12952  
  4         78  
10 4     4   30 use File::Find qw//;
  4         7  
  4         74  
11 4     4   1052 use FindBin;
  4         1405  
  4         231  
12 4     4   990 use Mojo::IOLoop;
  4         211074  
  4         43  
13              
14 4     4   3028 use Mojolicious::Plugin::StaticCompressor::Container;
  4         11  
  4         8547  
15              
16             our $static; # Instance of Mojo::Asset
17             our %containers; # Hash of Containers
18             our $config; # Hash-ref (Configuration items)
19              
20             sub register {
21 3     3 1 223 my ($self, $app, $conf) = @_;
22            
23             # Initilaize
24 3         11 %containers = ();
25 3         98 $static = $app->static;
26 3         50 $config = _load_options( $app, $conf );
27              
28             # Add "js" helper
29             $app->helper(js => sub {
30 4     4   95415 my $self = shift;
31 4         23 my @file_paths = _generate_list( (@_) );;
32 4         24 return _generate_import('js', 1, \@file_paths);
33 3         40 });
34              
35             # Add "css" helper
36             $app->helper(css => sub {
37 2     2   28353 my $self = shift;
38 2         15 my @file_paths = _generate_list( (@_) );;
39 2         13 return _generate_import('css', 1, \@file_paths);
40 3         360 });
41              
42             # Add "js_nominify" helper
43             $app->helper(js_nominify => sub {
44 2     2   15107 my $self = shift;
45 2         10 my @file_paths = _generate_list( (@_) );;
46 2         11 return _generate_import('js', 0, \@file_paths);
47 3         256 });
48              
49             # Add "css_nominify" helper
50             $app->helper(css_nominify => sub {
51 0     0   0 my $self = shift;
52 0         0 my @file_paths = _generate_list( (@_) );;
53 0         0 return _generate_import('css', 0, \@file_paths);
54 3         240 });
55              
56 3 100       232 unless($config->{is_disable}){ # Enable
57              
58             # Check the cache directory
59 2 50       112 if(!-d $config->{path_cache_dir}){
60 0         0 mkdir $config->{path_cache_dir};
61             }
62            
63             # Add hook
64             $app->hook(
65             before_dispatch => sub {
66 10     10   260183 my $self = shift;
67 10 100 66     49 if($self->req->url->path->contains('/'.$config->{url_path_prefix})
68             && $self->req->url->path =~ /\/$config->{url_path_prefix}\/(.+)$/){
69 5         1938 my $container_key = $1;
70            
71 5         13 eval {
72 5         72 my $cont = Mojolicious::Plugin::StaticCompressor::Container->new(
73             key => $container_key,
74             config => $config,
75             );
76              
77 5 50       23 if(!defined $containers{$cont->get_key()}){
78 0         0 $containers{$cont->get_key()} = $cont;
79             }
80            
81 5         25 $self->render( text => $cont->get_content(), format => $cont->get_extension() );
82             };
83              
84 5 50       5627 if($@){
85 0         0 $self->render( text => $@, status => 400 );
86             }
87             }
88             }
89 2         21 );
90              
91             # Automatic cleanup
92 2         96 _cleanup_old_files();
93              
94             # Start background loop
95 2 50       28 if($config->{is_background}){
96 0         0 _start_background_loop();
97             }
98             }
99             }
100              
101             # Load the options
102             sub _load_options {
103 3     3   8 my ($app, $option) = @_;
104 3         6 my $config = {};
105              
106             # Disable
107 3   50     27 my $disable = $option->{disable} || 0;
108 3   100     19 my $disable_on_devmode = $option->{disable_on_devmode} || 0;
109 3 100 33     65 $config->{is_disable} = ($disable eq 1 || ($disable_on_devmode eq 1 && $app->mode eq 'development')) ? 1 : 0;
110              
111             # Debug
112 3   50     32 $config->{is_debug} = $option->{is_debug} || 0;
113              
114             # Prefix
115 3   50     21 my $prefix = $option->{url_path_prefix} || 'auto_compressed';
116 3         11 $config->{url_path_prefix} = $prefix;
117              
118             # Path of cache directory
119 3   33     26 $config->{path_cache_dir} = $option->{file_cache_path} || $FindBin::Bin.'/'.$prefix.'/';
120 3         11 $config->{path_single_cache_dir} = $config->{path_cache_dir}.'single/';
121              
122             # Background processing
123 3   50     17 $config->{is_background} = $option->{background} || 0;
124 3   50     16 $config->{background_interval_sec} = $option->{background_interval_sec} || 5;
125              
126             # Automatic cleanup
127 3   50     18 $config->{is_auto_cleanup} = $option->{auto_cleanup} || 1;
128              
129             # Expires seconds for automatic cleanup
130 3   50     17 $config->{auto_cleanup_expires_sec} = $option->{auto_cleanup_expires_sec} || 60 * 60 * 24 * 7; # 7days
131              
132             # Others
133 3         7 $config->{mojo_static} = $static;
134              
135 3         7 return $config;
136             }
137              
138             sub _generate_import {
139 8     8   23 my ($extension, $is_minify, $path_files_ref) = @_;
140              
141 8 100       41 if($config->{is_disable}){
142 3         11 return Mojo::ByteStream->new( _generate_import_raw_tag( $extension, $path_files_ref ) );
143             }
144              
145 5         73 my $cont = Mojolicious::Plugin::StaticCompressor::Container->new(
146             extension => $extension,
147             is_minify => $is_minify,
148             path_files_ref => $path_files_ref,
149             config => $config,
150             );
151              
152 5 50       24 if(defined $containers{$cont->get_key()}){
153 0         0 $containers{$cont->get_key()}->update();
154             } else {
155 5         21 $containers{$cont->get_key()} = $cont;
156             }
157              
158 5         36 return Mojo::ByteStream->new( _generate_import_processed_tag( $extension, "/".$config->{url_path_prefix}."/".$cont->get_key() ) );
159             }
160              
161             # Generate of import HTML-tag for processed
162             sub _generate_import_processed_tag {
163 5     5   12 my ($extension, $url) = @_;
164 5 100       32 if ($extension eq 'js'){
    50          
165 4         51 return "<script src=\"$url\"></script>\n";
166             } elsif ($extension eq 'css'){
167 1         14 return "<link rel=\"stylesheet\" href=\"$url\">\n";
168             }
169             }
170              
171             # Generate of import HTML-tag for raw
172             sub _generate_import_raw_tag {
173 3     3   6 my ($extension, $urls_ref) = @_;
174 3         7 my $tag = "";
175 3 100       12 if ($extension eq 'js'){
    50          
176 2         3 foreach(@{$urls_ref}){
  2         5  
177 3         9 $tag .= "<script src=\"$_\"></script>\n";
178             }
179             } elsif ($extension eq 'css'){
180 1         3 foreach(@{$urls_ref}){
  1         3  
181 1         6 $tag .= "<link rel=\"stylesheet\" href=\"$_\">\n";
182             }
183             }
184 3         29 return $tag;
185             }
186              
187             # Start background process loop
188             sub _start_background_loop {
189             my $id = Mojo::IOLoop->recurring( $config->{background_interval_sec} => sub {
190 0     0   0 foreach my $key (keys %containers){
191 0 0       0 if( $containers{$key}->update() ){
192 0         0 warn "[StaticCompressor] Cache updated in background - $key";
193             }
194             }
195 0     0   0 });
196             }
197              
198             # Cleanup
199             sub _cleanup_old_files {
200             File::Find::find(sub {
201 20     20   29 my $path = $File::Find::name;
202 20         25 my $now = time();
203 20 100 66     912 if( -f $path && $path =~ /^(.*)\.(js|css)$/ ){
204 16         319 my $updated_at = (stat($path))[9];
205 16 50       232 if($config->{auto_cleanup_expires_sec} < ($now - $updated_at)){
206 0         0 warn "DELETE: $path";
207             #unlink($config->{path_cache_dir}) || die("Can't delete old file: $path");
208             }
209             }
210 2     2   237 }, $config->{path_cache_dir});
211             }
212              
213             #Generate one dimensional array
214             sub _generate_list{
215 8     8   26 my @temp = @_;
216 8         17 my @file_paths;
217 8         38 while (@temp) {
218 11         23 my $next = shift @temp;
219 11 50       38 if (ref($next) eq 'ARRAY') {
220 0         0 unshift @file_paths, @$next;
221             }
222             else {
223 11         38 push @file_paths, $next;
224             }
225             }
226 8         30 return @file_paths;
227             }
228              
229             1;
230             __END__
231             =head1 NAME
232              
233             Mojolicious::Plugin::StaticCompressor - Automatic JS/CSS minifier & compressor for Mojolicious
234              
235             =head1 SYNOPSIS
236              
237             Into the your Mojolicious application:
238              
239             sub startup {
240             my $self = shift;
241              
242             $self->plugin('StaticCompressor');
243             ~~~
244              
245             (Also, you can read the examples using the Mojolicious::Lite, in a later section.)
246              
247             Then, into the template in your application:
248              
249             <html>
250             <head>
251             ~~~~
252             <%= js '/foo.js', '/bar.js' %> <!-- minified and combined, automatically -->
253             <%= css '/baz.css' %> <!-- minified, automatically -->
254             ~~~~
255             </head>
256              
257             However, this module has just launched development yet. please give me your feedback.
258              
259             =head1 DESCRIPTION
260              
261             This Mojolicious plugin is minifier and compressor for static JavaScript file (.js) and CSS file (.css).
262              
263             =head1 INSTALLATION (from GitHub)
264              
265             $ git clone git://github.com/mugifly/p5-Mojolicious-Plugin-StaticCompressor.git
266             $ cpanm ./p5-Mojolicious-Plugin-StaticCompressor
267              
268             =head1 METHODS
269              
270             Mojolicious::Plugin::StaticCompressor inherits all methods from L<Mojolicious::Plugin> and implements the following new ones.
271              
272             =head2 register
273              
274             Register plugin in L<Mojolicious> application.
275              
276             =head1 HELPERS
277              
278             You can use these helpers on templates and others.
279              
280             =head2 js $file_path [, ...]
281              
282             Example of use on template file:
283              
284             <%= js '/js/foo.js' %>
285              
286             This is just available as substitution for the 'javascript' helper (built-in helper of Mojolicious).
287              
288             However, this helper will output a HTML-tag including the URL which is a compressed files.
289              
290             <script src="/auto_compressed/124015dca008ef1f18be80d7af4a314afec6f6dc"></script>
291              
292             When this script file has output (just received a request), it is minified automatically.
293              
294             Then, minified file are cached in the memory.
295              
296             =head3 Support for multiple files
297              
298             In addition, You can also use this helper with multiple js-files:
299              
300             <%= js '/js/foo.js', '/js/bar.js' %>
301              
302             In this case, this helper will output a single HTML-tag.
303              
304             but, when these file has output, these are combined (and minified) automatically.
305              
306             =head2 css $file_path [, ...]
307              
308             This is just available as substitution for the 'stylesheet' helper (built-in helper of Mojolicious).
309              
310             =head2 js_nominify $file_path [, ...]
311              
312             If you don't want Minify, please use this.
313              
314             This helper is available for purposes that only combine with multiple js-files.
315              
316             =head2 css_nominify $file_path [, ...]
317              
318             If you don't want Minify, please use this.
319              
320             This helper is available for purposes that only combine with multiple css-files.
321              
322             =head1 CONFIGURATION
323              
324             You can set these options when call the plugin from your application.
325              
326             =head2 disable_on_devmode
327              
328             You can disable a combine (and minify) when running your Mojolicious application as 'development' mode (such as a running on the 'morbo'), by using this option:
329              
330             $self->plugin('StaticCompressor', disable_on_devmode => 1);
331              
332             (default: 0 (DISABLE))
333              
334             =head2 url_path_prefix
335              
336             You can set the prefix of directory path which stores away a automatic compressed (and cached) file.
337              
338             The directory that specified here, will be made automatically.
339              
340             (default: "auto_compressed")
341              
342             =head2 background
343              
344             You can allow background processing to this plugin. (This option is EXPERIMENTAL.)
345              
346             If this option is disabled, a delay may occur in front-end-processing because this module will re-process it when static file has re-write.
347              
348             This option will be useful to prevent it with automatic background processing.
349              
350             (default: 0 (DISABLE))
351              
352             =head2 background_interval_sec
353              
354             When you enable "background", this option is available.
355              
356             (default: 604800 sec (7 days))
357              
358             =head2 auto_cleanup
359              
360             This option provides automatic clean-up of old cache file.
361              
362             (default: 1 (ENABLE))
363              
364             =head2 auto_cleanup_expires_sec
365              
366             When you enable "auto_cleanup", this option is available.
367              
368             (default: 604800 sec (7 days))
369              
370             =head1 KNOWN ISSUES
371              
372             =over 4
373              
374             =item * Support for LESS and Sass.
375              
376             =back
377              
378             Your feedback is highly appreciated!
379              
380             https://github.com/mugifly/p5-Mojolicious-Plugin-StaticCompressor/issues
381              
382             =head1 EXAMPLE OF USE
383              
384             Prepared a brief sample app for you, with using Mojolicious::Lite:
385              
386             example/example.pl
387              
388             $ morbo example.pl
389              
390             Let's access to http://localhost:3000/ with your browser.
391              
392             =head1 REQUIREMENTS
393              
394             =over 4
395              
396             =item * Mojolicious v3.8x or later (Operability Confirmed: v3.88, v4.25)
397              
398             =item * Other dependencies (cpan modules).
399              
400             =back
401              
402             =head1 SEE ALSO
403              
404             L<https://github.com/mugifly/p5-Mojolicious-Plugin-StaticCompressor>
405              
406             L<Mojolicious>
407              
408             L<CSS::Minifier>
409              
410             L<JavaScript::Minifier>
411              
412             =head1 CONTRIBUTORS
413              
414             Thank you to:
415              
416             =over 4
417              
418             =item * jakir-hayder L<https://github.com/jakir-hayder>
419              
420             =back
421              
422             =head1 COPYRIGHT AND LICENSE
423              
424             Copyright (C) 2013, Masanori Ohgita (http://ohgita.info/).
425              
426             This library is free software; you can redistribute it and/or modify
427             it under the same terms as Perl itself.
428              
429             Thanks, Perl Mongers & CPAN authors.