File Coverage

blib/lib/Mojolicious/Plugin/Webpack.pm
Criterion Covered Total %
statement 69 94 73.4
branch 16 44 36.3
condition 9 22 40.9
subroutine 14 17 82.3
pod 2 2 100.0
total 110 179 61.4


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Webpack;
2 2     2   3635 use Mojo::Base 'Mojolicious::Plugin';
  2         6  
  2         18  
3              
4 2     2   538 use Carp qw(carp croak);
  2         4  
  2         138  
5 2     2   12 use Mojo::File qw(path);
  2         4  
  2         98  
6 2     2   22 use Mojo::JSON qw(decode_json encode_json);
  2         6  
  2         110  
7 2     2   13 use Mojo::Path;
  2         4  
  2         19  
8 2     2   55 use Mojo::Util;
  2         4  
  2         159  
9              
10 2   50 2   16 use constant DEBUG => $ENV{MOJO_WEBPACK_DEBUG} && 1;
  2         7  
  2         3852  
11              
12             our @CARP_NOT;
13             our $VERSION = '1.00';
14              
15             has engine => undef;
16              
17             has _asset_map_file => sub {
18             my $self = shift;
19             return $self->engine->assets_dir->child(sprintf 'asset_map.%s.json', $self->engine->mode);
20             };
21              
22             sub register {
23 3     3 1 1079 my ($self, $app, $config) = @_;
24              
25             # Setup up the plugin
26 3   50     25 my $asset_path = ($config->{asset_path} ||= '/asset'); # EXPERIMENTAL
27 3         8 $asset_path .= '/';
28 3         21 $app->routes->any([qw(HEAD GET)] => "$asset_path*name")->name('webpack.asset');
29              
30 3   50 1   1646 $app->helper(($config->{helper} || 'asset') => sub { $self->_helper(@_) });
  1         24457  
31 3         333 $self->_build_engine($app, $config);
32 3         12 $app->plugins->emit_hook(before_webpack_start => $self);
33              
34             # Build or read built assets
35 3 0       114 my $build_method = !$ENV{MOJO_WEBPACK_BUILD} ? '' : $ENV{MOJO_WEBPACK_BUILD} =~ m!\b(watch)\b!i ? $1 : 'build';
    50          
36 3 50       12 if ($build_method) {
37 0 0       0 $build_method = 'exec' if $build_method eq 'watch';
38 0         0 $self->{mtime} = -1;
39 0         0 $self->engine->_d('MOJO_WEBPACK_BUILD=%s', $build_method) if DEBUG;
40 0         0 $self->engine->$build_method; # "exec" will take over the current process
41 0 0       0 $self->_read_asset_map if $self->_write_asset_map;
42             }
43             else {
44 3         8 $self->{mtime} = undef;
45 3         6 $self->engine->_d('Read assets from %s', $self->_asset_map_file) if DEBUG;
46 3 50       23 $self->_write_asset_map if defined $ENV{MOJO_WEBPACK_BUILD};
47 3         13 $self->_read_asset_map;
48             }
49              
50             # Add "Cache-Control" response header
51 3 50 50     736 my $cache_control = defined $ENV{MOJO_WEBPACK_BUILD} ? 'no-cache' : $config->{cache_control} // 'max-age=86400';
52             $cache_control && $app->hook(
53             after_static => sub {
54 1     1   15389 my $c = shift;
55 1 50       18 $c->res->headers->cache_control($cache_control) if index($c->req->url->path, $asset_path) == 0;
56             }
57 3 50       39 );
58             }
59              
60             sub url_for {
61 0     0 1 0 my ($self, $c, $name) = @_;
62 0 0       0 _unknown_asset($name) unless my $asset = $self->_asset_map->{$name};
63 0         0 return $c->url_for('webpack.asset', {name => $asset->{name}});
64             }
65              
66             sub _asset_map {
67 1     1   3 my $self = shift;
68              
69             # Production mode
70 1 50 33     15 return $self->{asset_map} if !$self->{mtime} and $self->{asset_map};
71              
72             # Development mode
73 0 0       0 $self->_read_asset_map if $self->_write_asset_map;
74 0         0 return $self->{asset_map};
75             }
76              
77             sub _build_engine {
78 3     3   7 my ($self, $app, $config) = @_;
79              
80             # Custom engine
81 3   50     15 my $engine = $config->{engine} || 'Mojo::Alien::webpack';
82              
83             # Build default engine
84 3   50     300 $engine = eval "require $engine;$engine->new" || die "Could not load engine $engine: $@";
85 3         106 $engine->assets_dir($app->home->rel_file('assets'));
86 3 50       229 $engine->config($app->home->rel_file($engine->isa('Mojo::Alien::rollup') ? 'rollup.config.js' : 'webpack.config.js'));
87 3   50     129 $engine->include($config->{process} || ['js']);
88 3 100       30 $engine->mode($app->mode eq 'development' ? 'development' : 'production');
89 3         86 $engine->out_dir(path($app->static->paths->[0], grep $_, split '/', $config->{asset_path})->to_abs);
90              
91 3         306 map { $engine->_d('%s = %s', $_, $engine->$_) } qw(config assets_dir out_dir mode) if DEBUG;
92 3         24 return $self->{engine} = $engine;
93             }
94              
95             sub _unknown_asset {
96 0     0   0 local @CARP_NOT = qw(Mojolicious::Plugin::EPRenderer Mojolicious::Plugin::Webpack Mojolicious::Renderer);
97 0         0 croak qq(Unknown asset name "$_[0]".);
98             }
99              
100             sub _helper {
101 1     1   5 my ($self, $c, $name, @args) = @_;
102 1 50       6 return $self if @_ == 2;
103 1 50       9 return $self->$name($c, @args) if $name =~ m!^\w+$!;
104              
105 1 50       7 _unknown_asset($name) unless my $asset = $self->_asset_map->{$name};
106 1   50     6 my $helper = $asset->{helper} || 'url_for';
107 1         10 return $c->$helper($c->url_for('webpack.asset', {name => $asset->{name}}), @args);
108             }
109              
110             sub _read_asset_map {
111 3     3   7 my $self = shift;
112              
113 3 100       6 if (my $asset_map = eval { decode_json $self->_asset_map_file->slurp }) {
  3         14  
114 1         589 $self->{asset_map} = {};
115 1         3 map { $self->{asset_map}{$_} = $asset_map->{production}{$_} } keys %{$asset_map->{production}};
  1         5  
  1         7  
116 1 50       5 map { $self->{asset_map}{$_} = $asset_map->{development}{$_} } keys %{$asset_map->{development}}
  0         0  
  0         0  
117             if $self->engine->mode eq 'development';
118 1         12 return;
119             }
120              
121             # Failed to read asset_map
122 2         730 $@ =~ s!at \S* line.*!!s;
123 2         5 local $Carp::CarpLevel = $Carp::CarpLevel + 2;
124 2         42 carp sprintf qq([Mojolicious::Plugin::Webpack] Sure %s has been run for mode "%s"? %s),
125             path($self->engine->binary)->basename, $self->engine->mode, $@;
126             }
127              
128             sub _write_asset_map {
129 0     0     my $self = shift;
130              
131 0           my $produced = $self->engine->asset_map;
132 0           my ($mtime, %asset_map) = (0);
133 0           for my $rel_name (keys %$produced) {
134 0           my $asset = $produced->{$rel_name};
135 0 0         my $helper = $asset->{ext} eq 'js' ? 'javascript' : $asset->{ext} eq 'css' ? 'stylesheet' : 'image';
    0          
136 0           $asset_map{$asset->{mode}}{$asset->{name}} = {helper => $helper, mtime => $asset->{mtime}, name => $rel_name};
137 0           $mtime += $asset->{mtime};
138             }
139              
140 0 0 0       return 0 if $self->{mtime} and $self->{mtime} == $mtime;
141 0           $self->_asset_map_file->spurt(encode_json \%asset_map);
142 0           return $self->{mtime} = $mtime;
143             }
144              
145             1;
146              
147             =encoding utf8
148              
149             =head1 NAME
150              
151             Mojolicious::Plugin::Webpack - Mojolicious ♥ Webpack
152              
153             =head1 SYNOPSIS
154              
155             =head2 Define entrypoint
156              
157             Create a file "./assets/index.js" relative to you application directory with
158             the following content:
159              
160             console.log('Cool beans!');
161              
162             =head2 Application
163              
164             Your L application need to load the
165             L plugin and tell it what kind of assets it
166             should be able to process:
167              
168             $app->plugin(Webpack => {process => [qw(js css)]});
169              
170             See L for more configuration options.
171              
172             =head2 Template
173              
174             To include the generated assets in your template, you can use the L
175             helper:
176              
177             %= asset "cool_beans.css"
178             %= asset "cool_beans.js"
179              
180             =head2 Run the application
181              
182             You can start the application using C, C or any Mojolicious
183             server you want, but if you want rapid development you should use
184             C, which is an alternative to C:
185              
186             $ mojo webpack -h
187             $ mojo webpack ./script/myapp.pl
188              
189             However if you want to use another daemon and force C to run, you need
190             to set the C environment variable to "1". Example:
191              
192             MOJO_WEBPACK_BUILD=1 ./script/myapp.pl daemon
193              
194             =head2 Testing
195              
196             If you want to make sure you have built all the assets, you can make a test
197             file like "build-assets.t":
198              
199             use Test::More;
200             use Test::Mojo;
201              
202             # Run with TEST_BUILD_ASSETS=1 prove -vl t/build-assets.t
203             plan skip_all => "TEST_BUILD_ASSETS=1" unless $ENV{TEST_BUILD_ASSETS};
204              
205             # Load the app and make a test object
206             $ENV{MOJO_MODE} = 'production';
207             $ENV{MOJO_WEBPACK_BUILD} = 1;
208             use FindBin;
209             require "$FindBin::Bin/../script/myapp.pl";
210             my $t = Test::Mojo->new;
211              
212             # Find all the tags and make sure they can be loaded
213             $t->get_ok("/")->status_is(200);
214             $t->element_count_is('script[src], link[href][rel=stylesheet]', 2);
215             $t->tx->res->dom->find("script[src], link[href][rel=stylesheet]")->each(sub {
216             $t->get_ok($_->{href} || $_->{src})->status_is(200);
217             });
218              
219             done_testing;
220              
221             =head1 DESCRIPTION
222              
223             L is a L plugin to make it easier to
224             work with L or L. This plugin
225             will...
226              
227             =over 2
228              
229             =item 1.
230              
231             Generate a minimal C and a Webpack or Rollup config file. Doing
232             this manually is possible, but it can be quite time consuming to figure out all
233             the bits and pieces if you are not already familiar with Webpack.
234              
235             =item 2.
236              
237             Load the entrypoint "./assets/index.js", which is the starting point of your
238             client side application. The entry file can load JavaScript, CSS, SASS, ... as
239             long as the appropriate processing plugin is loaded.
240              
241             =item 3.
242              
243             It can be difficult to know exactly which plugins to use with Webpack. Because
244             of this L has some predefined rules for which
245             Nodejs dependencies to fetch and install. None of the nodejs modules are
246             required in production though, so it will only be installed while developing.
247              
248             =item 4.
249              
250             While developing, the webpack executable will be started automatically next to
251             L. Webpack will be started with the appropriate switches
252             to watch your source files and re-compile on change.
253              
254             =back
255              
256             =head2 Rollup
257              
258             L is an alternative to Webpack. Both
259             accomplish more or less the same thing, but in different ways.
260              
261             To be able to use rollup, you have to load this plugin with a different engine:
262              
263             $app->plugin(Webpack => {engine => 'Mojo::Alien::rollup', process => [qw(js css)]});
264              
265             =head2 Notice
266              
267             L is currently EXPERIMENTAL.
268              
269             =head1 HELPERS
270              
271             =head2 asset
272              
273             # Call a method or access an attribute in this class
274             my $path = $app->asset->engine->out_dir;
275              
276             # Call a method, but from inside a mojo template
277             %= asset->url_for($c, "cool_beans.css")
278              
279             # Generate a HTML tag
280             my $bytestream = $c->asset("cool_beans.js", @args);
281              
282             # Generate a HTML tag, but from inside a mojo template
283             %= asset "cool_beans.css", media => "print"
284             %= asset(url_for => "cool_beans.css")
285              
286             The most basic usage of this helper is to create a HTML tag using
287             L or
288             L if a valid asset name is passed in.
289              
290             On the other hand, the helper will return the plugin instance if no arguments
291             are passed in, allowing you to call any of the L or access the
292             L.
293              
294             =head1 HOOKS
295              
296             =head2 before_webpack_start
297              
298             $app->before_webpack_start(sub { my $webpack = shift; ... });
299              
300             Emitted right before the plugin starts building or loading in the generated
301             assets. Useful if you want to change any of the L attributes from the
302             defaults.
303              
304             =head1 ATTRIBUTES
305              
306             =head2 engine
307              
308             $engine = $webpack->engine;
309              
310             Returns a L or L object.
311              
312             =head1 METHODS
313              
314             =head2 register
315              
316             $webpack->register($app, \%config);
317             $app->plugin("Webpack", \%config);
318              
319             Used to register this plugin into your L app.
320              
321             The C<%config> passed when loading this plugin can have any of these
322             attributes:
323              
324             =over 2
325              
326             =item * asset_path
327              
328             Can be used to specify an alternative static directory to output the built
329             assets to.
330              
331             Default: "/asset".
332              
333             =item * cache_control
334              
335             Used to set the response "Cache-Control" header for built assets.
336              
337             Default: "no-cache" while developing and "max-age=86400" in production.
338              
339             =item * engine
340              
341             Must be a valid engine class name. Examples:
342              
343             $app->plugin("Webpack", {engine => 'Mojo::Alien::rollup'});
344             $app->plugin("Webpack", {engine => 'Mojo::Alien::webpack'});
345              
346             Default: L.
347              
348             =item * helper
349              
350             Name of the helper that will be added to your application.
351              
352             Default: "asset".
353              
354             =item * process
355              
356             Used to specify L or L.
357              
358             Default: C<['js']>.
359              
360             =back
361              
362             =head2 url_for
363              
364             $url = $webpack->url_for($c, $asset_name);
365              
366             Returns a L for a given asset.
367              
368             =head1 AUTHOR
369              
370             Jan Henning Thorsen
371              
372             =head1 COPYRIGHT AND LICENSE
373              
374             Copyright (C) Jan Henning Thorsen
375              
376             This program is free software, you can redistribute it and/or modify it under
377             the terms of the Artistic License version 2.0.
378              
379             =head1 SEE ALSO
380              
381             L, L.
382              
383             =cut