File Coverage

blib/lib/Mojo/Alien/webpack.pm
Criterion Covered Total %
statement 36 135 26.6
branch 1 58 1.7
condition 2 32 6.2
subroutine 10 24 41.6
pod 7 7 100.0
total 56 256 21.8


line stmt bran cond sub pod time code
1             package Mojo::Alien::webpack;
2 7     7   647776 use Mojo::Base -base;
  7         49  
  7         42  
3              
4 7     7   933 use Carp qw(croak);
  7         13  
  7         388  
5 7     7   2474 use File::chdir;
  7         14686  
  7         659  
6 7     7   2425 use Mojo::Alien::npm;
  7         21  
  7         50  
7 7     7   292 use Mojo::File qw(path tempfile);
  7         13  
  7         285  
8 7     7   2322 use Mojo::Loader;
  7         22838  
  7         280  
9 7     7   2427 use POSIX ':sys_wait_h';
  7         32150  
  7         34  
10 7     7   8124 use Time::HiRes qw(sleep);
  7         17  
  7         65  
11              
12 7   50 7   1221 use constant DEBUG => ($ENV{MOJO_ROLLUP_DEBUG} || $ENV{MOJO_WEBPACK_DEBUG}) && 1;
  7         17  
  7         15825  
13              
14             # TODO
15             our $VERSION = $Mojolicious::Plugin::Webpack::VERSION || '0.01';
16              
17             has assets_dir => sub { shift->config->dirname->child('assets') };
18              
19             has binary => sub {
20             my $self = shift;
21             return $ENV{MOJO_WEBPACK_BINARY} if $ENV{MOJO_WEBPACK_BINARY};
22             my $bin = $self->config->to_abs->dirname->child(qw(node_modules .bin webpack));
23             $self->_d('%s %s', -e $bin ? 'Found' : 'Not installed', $bin) if DEBUG;
24             return -e $bin ? $bin->to_string : 'webpack';
25             };
26              
27             has config => sub { path->to_abs->child('webpack.config.js') };
28              
29             has dependencies => sub {
30             return {
31             core => [qw(webpack webpack-cli)],
32             css => [qw(css-loader mini-css-extract-plugin css-minimizer-webpack-plugin)],
33             eslint => [qw(eslint eslint-webpack-plugin)],
34             js => [qw(@babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader terser-webpack-plugin)],
35             sass => [qw(css-loader mini-css-extract-plugin css-minimizer-webpack-plugin sass sass-loader)],
36             vue => [qw(vue vue-loader vue-template-compiler)],
37             };
38             };
39              
40             has include => sub { +[] };
41             has mode => sub { $ENV{NODE_ENV} || 'development' };
42              
43             has npm => sub {
44             my $self = shift;
45             Mojo::Alien::npm->new(config => $self->config->dirname->child('package.json'), mode => $self->mode);
46             };
47              
48             has out_dir => sub { shift->config->dirname->child('dist') };
49              
50             sub asset_map {
51 1     1 1 5 my $self = shift;
52              
53 1         1 my %assets;
54 1         3 for my $path ($self->out_dir->list_tree->each) {
55 1         287 my $rel_path = File::Spec->abs2rel($path, $self->out_dir);
56 1         89 my $name = $rel_path;
57 1         9 $name =~ s!(.*)\W(\w+)\.(\w+)$!$1.$3!; # (prefix, checksum, ext)
58 1 50 50     6 my $mode = ($2 // '') eq 'development' ? 'development' : 'production';
59 1         4 $assets{$rel_path} = {ext => lc $3, name => $name, mode => $mode, mtime => $path->stat->mtime, path => $path};
60             }
61              
62 1         232 return \%assets;
63             }
64              
65             sub build {
66 0     0 1   my $self = shift;
67 0 0         croak "Can't call build() after watch()" if $self->pid;
68              
69 0           ($!, $?) = (0, 0);
70 0           $self->_run($self->_cmd_build);
71 0 0         croak "$self->{basename} $! (exit=$?)" if $!;
72 0 0 0       croak "$self->{basename} failed exit=$?" if !$! and $?;
73              
74 0           return $self;
75             }
76              
77             sub exec {
78 0     0 1   my $self = shift;
79 0           my @cmd = ($self->_cmd_build, '--watch');
80 0           my $home = $self->config->dirname->to_string;
81              
82 0 0         chdir $home or die "Can't chdir to $home: $!";
83 0           $ENV{NODE_ENV} = $self->mode;
84 0           $ENV{WEBPACK_ASSETS_DIR} = $self->assets_dir->to_string;
85 0           $ENV{WEBPACK_OUT_DIR} = $self->out_dir->to_string;
86 0           $self->_d('(%s) cd %s && %s', $$, $home, join ' ', @_) if DEBUG;
87 0           { CORE::exec(@cmd) }
  0            
88 0           die "Can't exec @cmd: $!";
89             }
90              
91             sub init {
92 0     0 1   my $self = shift;
93              
94 0           $self->npm->init;
95 0           $self->_render_file($self->_config_template_name, $self->config);
96              
97 0           my $dependencies = $self->npm->dependencies;
98 0           my @includes = @{$self->include};
  0            
99 0 0         unshift @includes, 'core' unless grep { $_ eq 'core' } @includes;
  0            
100 0           my ($conf_d, @includes_names, %seen) = ($self->_config_include_dir);
101 0           for my $include (@includes) {
102 0 0         for my $package (@{$self->dependencies->{$include} || []}) {
  0            
103 0 0         next if $seen{$package}++;
104 0 0         $self->npm->install($package) unless $dependencies->{$package}{version};
105             }
106              
107 0 0         my $exists = $self->_resources->{"include/$include.js"} ? 'exists' : 'does not exist';
108 0           my $file = $conf_d->child("$include.js");
109 0           $self->_d('Template %s.js %s', $include, $exists) if DEBUG;
110 0 0         $self->_render_file("include/$include.js", $file) if $exists eq 'exists';
111 0 0         push @includes_names, $include if -e $file;
112             }
113              
114 0           my $include_src = ("module.exports = function(config, opts) {\n");
115 0           $include_src .= " require('./$_')(config, opts);\n" for @includes_names;
116 0           $include_src .= "};\n";
117 0           $self->_render_file('include.js', $self->_config_include_dir->child('include.js'), $include_src);
118 0           return $self;
119             }
120              
121             sub pid {
122 0     0 1   my $self = shift;
123 0 0         return 0 unless $self->{pid};
124 0           my $r = waitpid $self->{pid}, WNOHANG; # -1 == no such process, >0 if terminated
125 0 0 0       return $r == -1 && delete $self->{pid} ? 0 : $r ? 0 : $self->{pid};
    0          
126             }
127              
128             sub stop {
129 0     0 1   my ($self, $tries) = @_;
130              
131 0   0       $tries ||= 100;
132 0           while (--$tries) {
133 0 0         return $self unless my $pid = $self->pid;
134 0           local $!;
135 0           kill 15, $pid;
136 0           waitpid $pid, 0;
137 0   0       sleep $ENV{MOJO_WEBPACK_STOP_INTERVAL} || 0.1;
138             }
139              
140 0   0       $self->{basename} ||= path($self->binary)->basename;
141 0           croak "Couldn't stop $self->{basename} with pid @{[$self->pid]}";
  0            
142             }
143              
144             sub watch {
145 0     0 1   my $self = shift;
146 0 0         return $self if $self->pid;
147              
148 0           my $home = $self->config->dirname->to_string;
149 0 0         croak "Can't chdir $home: No such file or directory" unless -d $home;
150              
151 0           my @cmd = ($self->_cmd_build, '--watch');
152 0 0         croak "Can't fork: $!" unless defined(my $pid = fork);
153 0 0         return $self if $self->{pid} = $pid; # Parent
154 0           return $self->exec; # Child
155             }
156              
157             sub _cmd_build {
158 0     0     my $self = shift;
159 0           $self->init;
160              
161 0           my @cmd = ($self->binary);
162 0 0         croak "Can't run $cmd[0]" unless -x $cmd[0];
163              
164 0   0       $self->{basename} ||= path($cmd[0])->basename;
165 0           push @cmd, '--config' => $self->config->to_string;
166 0 0         push @cmd, qw(--progress --profile --verbose) if $ENV{MOJO_WEBPACK_VERBOSE};
167 0           return @cmd;
168             }
169              
170 0     0     sub _config_include_dir { shift->assets_dir->child('webpack.config.d') }
171 0     0     sub _config_template_name {'webpack.config.js'}
172 0     0     sub _d { my ($class, $format) = (shift, shift); warn sprintf "[Webpack] $format\n", @_ }
  0            
173              
174             sub _render_file {
175 0     0     my ($self, $name, $file, $content) = @_;
176              
177 0 0         if (-e $file) {
178 0 0         my $version = $file->slurp =~ m!// Autogenerated.*(\d+\.\d+)! ? $1 : -1;
179 0           $self->_d('File %s has version %s', $file, $version) if DEBUG;
180 0 0         return $self if $version == -1;
181 0 0 0       return $self if $version == $VERSION and !$content and !$ENV{MOJO_WEBPACK_REGENERATE};
      0        
182             }
183              
184 0           $self->_d('Render %s to %s', $name, $file) if DEBUG;
185 0 0         $file->dirname->make_path unless -d $file->dirname;
186 0   0       $content //= $self->_resources->{$name};
187 0           $file->spurt(sprintf "// Autogenerated by %s %s\n%s", ref($self), $VERSION, $content);
188 0           return $self;
189             }
190              
191             sub _resources {
192 0   0 0     state $resources = Mojo::Loader::data_section(ref($_[0]) || $_[0]);
193             }
194              
195             sub _run {
196 0     0     my ($self, @cmd) = @_;
197 0           local $CWD = $self->config->dirname->to_string;
198 0           local $ENV{NODE_ENV} = $self->mode;
199 0           local $ENV{WEBPACK_ASSETS_DIR} = $self->assets_dir->to_string;
200 0           local $ENV{WEBPACK_OUT_DIR} = $self->out_dir->to_string;
201 0           $self->_d('cd %s && %s', $CWD, join ' ', @cmd) if DEBUG;
202 0 0         open my $WEBPACK, '-|', @cmd or die "Can't run @cmd: $!";
203 0 0         return $WEBPACK if defined wantarray;
204 0           DEBUG && print while <$WEBPACK>;
205             }
206              
207 0     0     sub DESTROY { shift->stop }
208              
209             1;
210              
211             =encoding utf8
212              
213             =head1 NAME
214              
215             Mojo::Alien::webpack - Runs the external nodejs program webpack
216              
217             =head1 SYNOPSIS
218              
219             use Mojo::Alien::webpack;
220             my $webpack = Mojo::Alien::webpack->new;
221              
222             # Run once
223             $webpack->build;
224              
225             # Build when webpack see files change
226             $webpack->watch;
227              
228             =head1 DESCRIPTION
229              
230             L is a class for runnig the external nodejs program
231             L.
232              
233             =head1 ATTRIBUTES
234              
235             =head2 assets_dir
236              
237             $path = $webpack->assets_dir;
238             $webpack = $webpack->assets_dir($webpack->config->dirname->child('assets'))
239              
240             Location to source assetsa and partial webpack.config.js files.
241              
242             =head2 binary
243              
244             $array_ref = $webpack->binary;
245             $webpack = $webpack->binary('webpack');
246              
247             The path to the webpack executable. Defaults to C
248             environment variable, or "webpack" inside "./node_modules". Fallback to just
249             "webpack".
250              
251             =head2 config
252              
253             $path = $webpack->config;
254             $webpack = $webpack->config(path->to_abs->child('webpack.config.js'));
255              
256             Holds an I path to
257             L.
258              
259             =head2 dependencies
260              
261             $hash_ref = $webpack->dependencies;
262              
263             A hash where the keys can match the items in L and the values are
264             lists of packages to install. Keys that does I match items in L
265             will be ignored. This attribute will be used by L.
266              
267             These dependencies are predefined:
268              
269             core | webpack webpack-cli
270             css | css-loader mini-css-extract-plugin css-minimizer-webpack-plugin
271             eslint | eslint-webpack-plugin
272             js | @babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader terser-webpack-plugin
273             sass | css-loader mini-css-extract-plugin css-minimizer-webpack-plugin sass sass-loader
274             vue | vue vue-loader vue-template-compiler
275              
276             =head2 include
277              
278             $array_ref = $webpack->include;
279             $webpack = $webpack->include([qw(js css)]);
280              
281             L can be used to install dependencies and load other webpack config
282             files. The config files included must exist in the "webpack.config.d" sub
283             directory inside L. Here is an example of which files that will be
284             included if they exists:
285              
286             # Including "js" and "css" will look for the files below
287             $webpack->include[qw(js css)]);
288              
289             # - assets/webpack.config.d/package-babel-loader.js
290             # - assets/webpack.config.d/package-terser-webpack-plugin.js
291             # - assets/webpack.config.d/package-css-loader.js
292             # - assets/webpack.config.d/package-css-minimizer-webpack-plugin.js
293             # - assets/webpack.config.d/js.js
294             # - assets/webpack.config.d/css.js
295              
296             The L feature is currently EXPERIMENTAL.
297              
298             =head2 mode
299              
300             $str = $webpack->mode;
301             $webpack = $webpack->mode('development');
302              
303             Should be either "development" or "production". Will be used as "NODE_ENV"
304             environment variable when calling L or L.
305              
306             =head2 npm
307              
308             $npm = $webpack->npm;
309              
310             A L object used by L.
311              
312             =head2 out_dir
313              
314             $path = $webpack->out_dir;
315             $webpack = $webpack->out_dir(path('dist')->to_abs);
316              
317             Location to write output assets to.
318              
319             =head1 METHODS
320              
321             =head2 asset_map
322              
323             $hash_ref = $webpack->asset_map;
324              
325             Parses the filenames in L and returns a hash ref with information
326             about the generated assets. Example return value:
327              
328             {
329             'relative/output.development.js' => { # Key is relative path to out_dir()
330             ext => 'css', # File extension
331             mode => 'development', # or "production"
332             mtime => 1616976114, # File modification epoch timestamp
333             name => 'relative/output.js', # Name of asset, without checksum or mode
334             path => '/path/to/entry-name.development.js', # Absolute path to asset
335             },
336             }
337              
338             Note that this method is currently EXPERIMENTAL.
339              
340             =head2 build
341              
342             $webpack->build;
343              
344             Will build the assets or croaks on errors. Automatically calls L.
345              
346             =head2 exec
347              
348             $webpack->exec;
349              
350             This method will replace the current process, instead of starting webpack
351             inside a fork. This method is called by L inside a fork.
352              
353             =head2 init
354              
355             $webpack = $webpack->init;
356              
357             Will install "webpack" and "webpack-cli" and create a default L. Does
358             nothing if this is already done.
359              
360             This method is automatically called by L and L.
361              
362             =head2 pid
363              
364             $int = $webpack->pid;
365              
366             Returns the PID of the webpack process started by L.
367              
368             =head2 stop
369              
370             $webpack->stop;
371              
372             Will stop the process started by L. Does nothing if L has not
373             been called.
374              
375             =head2 watch
376              
377             $webpack->watch;
378              
379             Forks a new process that runs "webpack watch". This means that any changes will
380             generate new assets. This is much more efficient than calling L over
381             and over again. Automatically calls L.
382              
383             =head1 SEE ALSO
384              
385             L and L.
386              
387             =cut
388              
389             __DATA__