File Coverage

blib/lib/Mojolicious/Plugin/Browserify/Processor.pm
Criterion Covered Total %
statement 40 125 32.0
branch 1 42 2.3
condition 2 16 12.5
subroutine 11 19 57.8
pod 3 3 100.0
total 57 205 27.8


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Browserify::Processor;
2              
3             =head1 NAME
4              
5             Mojolicious::Plugin::Browserify::Processor - An AssetPack processor for browserify
6              
7             =head1 DESCRIPTION
8              
9             L is a
10             L preprocessor.
11              
12             =head1 SYNOPSIS
13              
14             use Mojolicious::Lite;
15              
16             plugin "AssetPack";
17             app->asset->preprocessors->remove($_) for qw( js jsx );
18              
19             my $browserify = Mojolicious::Plugin::Browserify::Processor->new;
20             app->asset->preprocessors->add($browserify);
21             app->asset("app.js" => "/js/main.js");
22              
23             get "/app" => "app_js_inlined";
24             app->start;
25              
26             __DATA__
27             @@ app_js_inlined.js.ep
28             %= asset "app.js" => {inline => 1}
29              
30             See also L for a simpler API.
31              
32             =cut
33              
34 14     14   548266 use Mojo::Base 'Mojolicious::Plugin::AssetPack::Preprocessor';
  14         19  
  14         80  
35 14     14   151192 use Mojo::Util;
  14         28  
  14         576  
36 14     14   60 use Cwd ();
  14         24  
  14         208  
37 14     14   141 use File::Basename 'dirname';
  14         65  
  14         646  
38 14     14   125 use File::Path 'make_path';
  14         15  
  14         575  
39 14     14   68 use File::Spec;
  14         10  
  14         246  
40 14     14   6659 use File::Which ();
  14         11334  
  14         368  
41 14   50 14   71 use constant DEBUG => $ENV{MOJO_ASSETPACK_DEBUG} || 0;
  14         17  
  14         733  
42 14     14   55 use constant CACHE_DIR => '.browserify';
  14         14  
  14         22255  
43              
44             =head1 ATTRIBUTES
45              
46             =head2 browserify_args
47              
48             $array_ref = $self->browserify_args;
49             $self= $self->browserify_args([ -g => "reactify" ]);
50              
51             Command line arguments that will be passed on to C.
52              
53             =head2 environment
54              
55             $str = $self->environment;
56             $self = $self->environment($str);
57              
58             Should be either "production" or "development" (default). This variable will
59             be passed on as C to C.
60              
61             =head2 extensions
62              
63             $array_ref = $self->extensions;
64             $self = $self->extensions([qw( js jsx )]);
65              
66             Specifies the extensions browserify should look for.
67              
68             =head2 executable
69              
70             $path = $self->executable;
71              
72             Holds the path to the "browserify" executable. Default to just "browserify".
73             C can also be found in C<./node_modules/.bin/browserify>, in the
74             current project directory.
75              
76             =cut
77              
78             has browserify_args => sub { [] };
79             has environment => sub { $ENV{MOJO_MODE} || $ENV{NODE_ENV} || 'development' };
80             has extensions => sub { ['js'] };
81             has executable => sub { shift->_executable('browserify') || 'browserify' };
82              
83             =head1 METHODS
84              
85             =head2 can_process
86              
87             $bool = $self->can_process;
88              
89             Returns true if browserify can be executed.
90              
91             =cut
92              
93 0 0   0 1 0 sub can_process { -f $_[0]->executable ? 1 : 0 }
94              
95             =head2 checksum
96              
97             $str = $self->checksum($text, $path);
98              
99             Returns the checksum for a given chunk of C<$text>. C<$text> is a
100             scalar ref containing the text from the asset. The default is
101             to use L.
102              
103             =cut
104              
105             sub checksum {
106 0     0 1 0 my ($self, $text, $path) = @_;
107 0         0 my $map = {};
108              
109 0         0 $self->_node_module_path;
110 0         0 $self->_find_node_modules($text, $path, $map);
111 0         0 Mojo::Util::md5_sum($$text, join '', map { Mojo::Util::slurp($map->{$_}) } sort keys %$map);
  0         0  
112             }
113              
114             =head2 process
115              
116             Used to process the JavaScript using C.
117              
118             =cut
119              
120             sub process {
121 0     0 1 0 my ($self, $assetpack, $text, $path) = @_;
122 0         0 my $environment = $self->environment;
123 0         0 my $cache_dir = File::Spec->catdir($assetpack->out_dir, CACHE_DIR);
124 0         0 my $map = {};
125 0         0 my @extra = @{$self->browserify_args};
  0         0  
126 0         0 my ($err, @modules);
127              
128 0         0 local $ENV{NODE_ENV} = $environment;
129 0 0 0     0 mkdir $cache_dir or die "mkdir $cache_dir: $!" unless -d $cache_dir;
130 0         0 $self->_node_module_path;
131 0         0 $self->_find_node_modules($text, $path, $map);
132 0         0 $self->{node_modules} = $map;
133              
134             # make external bundles from node_modules
135 0         0 for my $module (grep {/^\w/} sort keys %$map) {
  0         0  
136 0         0 my @external = map { -x => $_ } grep { $_ ne $module } keys %$map;
  0         0  
  0         0  
137 0         0 push @modules, $self->_outfile($assetpack, "$module-$environment.js");
138 0 0 0     0 next if -e $modules[-1] and (stat _)[9] >= (stat $map->{$module})[9];
139 0         0 make_path(dirname $modules[-1]);
140 0         0 $self->_run([$self->executable, @extra, @external, -r => $module, -o => $modules[-1]], undef, undef, \$err);
141             }
142              
143 0 0       0 if (!length $err) {
144              
145             # make application bundle which reference external bundles
146 0         0 push @extra, map { -x => $_ } grep {/^\w/} sort keys %$map;
  0         0  
  0         0  
147 0         0 $self->_run([$self->executable, @extra, -e => $path], undef, $text, \$err);
148             }
149 0 0       0 if (length $err) {
    0          
150 0         0 $self->_make_js_error($err, $text);
151             }
152             elsif (length $$text) {
153              
154             # bundle application and external bundles
155 0         0 $$text = join "\n", (map { Mojo::Util::slurp($_) } @modules), $$text;
  0         0  
156 0 0       0 $self->_minify($text, $path) if $assetpack->minify;
157             }
158              
159 0         0 return $self;
160             }
161              
162             sub _executable {
163 4     4   11 my ($self, $name) = @_;
164 4   33     32 my $paths = $self->{node_module_path} || $self->_node_module_path;
165              
166 4         13 for my $p (@$paths) {
167 0         0 my $local = Cwd::abs_path(File::Spec->catfile($p, '.bin', $name));
168 0 0 0     0 return $local if $local and -e $local;
169             }
170              
171 4         21 return File::Which::which($name);
172             }
173              
174             sub _find_node_modules {
175 0     0   0 my ($self, $text, $path, $uniq) = @_;
176              
177 0         0 while ($$text =~ m!\brequire\s*\(\s*(["'])(.+?)\1\s*\)\s*!g) {
178 0         0 my $module = $2;
179 0         0 warn "[Browserify] require($module) from $path\n" if DEBUG;
180 0 0       0 next if $uniq->{$module};
181 0 0       0 $module =~ /^\w/
182             ? $self->_follow_system_node_module($module, $path, $uniq)
183             : $self->_follow_relative_node_module($module, $path, $uniq);
184             }
185              
186 0         0 return keys %$uniq;
187             }
188              
189             sub _follow_relative_node_module {
190 0     0   0 my ($self, $module, $path, $uniq) = @_;
191 0         0 my $base = $module;
192              
193 0 0       0 unless (File::Spec->file_name_is_absolute($base)) {
194 0         0 $base = File::Spec->catfile(dirname($path), $module);
195             }
196              
197 0         0 for my $ext ("", map {".$_"} @{$self->extensions}) {
  0         0  
  0         0  
198 0         0 my $file = File::Spec->catfile(split '/', "$base$ext");
199 0 0       0 return if $uniq->{"$module$ext"};
200 0 0       0 next unless -f $file;
201 0         0 $uniq->{"$module$ext"} = $file;
202 0         0 my $js = Mojo::Util::slurp($file);
203 0         0 return $self->_find_node_modules(\$js, $file, $uniq);
204             }
205              
206 0         0 die "Could not find JavaScript module '$module'";
207             }
208              
209             sub _follow_system_node_module {
210 0     0   0 my ($self, $module, $path, $uniq) = @_;
211 0         0 my $p;
212              
213 0         0 for my $prefix (@{$self->{node_module_path}}) {
  0         0  
214 0 0       0 return $uniq->{$module} = $p if -e ($p = File::Spec->catfile($prefix, $module, 'package.json'));
215 0 0       0 return $uniq->{$module} = $p if -e ($p = File::Spec->catfile($prefix, $module, 'index.js'));
216 0 0       0 return $uniq->{$module} = $p if -e ($p = File::Spec->catfile($prefix, "$module.js"));
217             }
218              
219 0         0 die "Could not find JavaScript module '$module' in @{$self->{node_module_path}}";
  0         0  
220             }
221              
222             sub _minify {
223 0     0   0 my ($self, $text, $path) = @_;
224 0         0 my $uglifyjs = $self->_executable('uglifyjs');
225 0         0 my $err = '';
226              
227 0 0       0 if ($uglifyjs) {
228 0         0 $self->_run([$uglifyjs, qw( -m -c )], $text, $text, \$err);
229             }
230             else {
231 0         0 require JavaScript::Minifier::XS;
232 0         0 $$text = JavaScript::Minifier::XS::minify($$text);
233 0 0       0 $err = 'JavaScript::Minifier::XS failed' unless $$text;
234             }
235              
236 0 0       0 if (length $err) {
237 0         0 $self->_make_js_error($err, $text);
238             }
239             }
240              
241             sub _node_module_path {
242 4     4   5 my $self = shift;
243 4         95 my @cwd = File::Spec->splitdir(Cwd::getcwd);
244 4         7 my @path;
245              
246 4         5 do {
247 16         84 my $p = File::Spec->catdir(@cwd, 'node_modules');
248 16         17 pop @cwd;
249 16 50       173 push @path, $p if -d $p;
250             } while (@cwd);
251              
252 4         6 warn "[Browserify] node_module_path=[@path]\n" if DEBUG;
253 4         28 return $self->{node_module_path} = \@path;
254             }
255              
256             sub _outfile {
257 0     0     my ($self, $assetpack, $name) = @_;
258 0           my $path = $assetpack->{static}->file(File::Spec->catfile(CACHE_DIR, $name));
259              
260 0 0 0       return $path if $path and -e $path;
261 0           return File::Spec->catfile($assetpack->out_dir, CACHE_DIR, $name);
262             }
263              
264             =head1 COPYRIGHT AND LICENSE
265              
266             Copyright (C) 2014, Jan Henning Thorsen
267              
268             This program is free software, you can redistribute it and/or modify it under
269             the terms of the Artistic License version 2.0.
270              
271             =head1 AUTHOR
272              
273             Jan Henning Thorsen - C
274              
275             =cut
276              
277             1;