File Coverage

blib/lib/Mojolicious/Plugin/Webpack.pm
Criterion Covered Total %
statement 63 73 86.3
branch 11 30 36.6
condition 8 19 42.1
subroutine 12 14 85.7
pod 3 3 100.0
total 97 139 69.7


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