1 package Newslash::Plugin::JavaScriptLoader;
2 use Mojo::Base 'Mojolicious::Plugin';
5 use File::Basename qw(fileparse);
6 use Digest::MD5 qw(md5_hex);
10 use POSIX ":sys_wait_h";
11 use Mojo::Util qw(dumper);
13 use Mojo::JSON qw(decode_json encode_json);
14 use List::Util qw(any);
15 use Mojo::File qw(path);
16 use IO::Compress::Gzip qw(gzip);
18 use constant KVS_PREFIX => "JSL:";
19 use constant DEFAULT_MEM_EXPIRE => 60;
20 use constant DEFAULT_KVS_EXPIRE => 60;
23 return shift->{app}->kvs;
27 return shift->{app}->cache;
31 my ($self, $key, $value) = @_;
32 my $mem_expire = $self->{conf}->{mem_expire};
33 my $kvs_expire = $self->{conf}->{kvs_expire};
34 $mem_expire = DEFAULT_MEM_EXPIRE if !defined $mem_expire;
35 $kvs_expire = DEFAULT_KVS_EXPIRE if !defined $kvs_expire;
37 $self->_kvs->set(KVS_PREFIX . $key, $value, $kvs_expire);
38 $self->_cache->set(KVS_PREFIX . $key, $value, $mem_expire);
42 my ($self, $key) = @_;
43 my $expire = $self->{conf}->{mem_expire};
44 $expire = DEFAULT_MEM_EXPIRE if !defined $expire;
46 my $value = $self->_cache->get(KVS_PREFIX . $key);
47 return $value if defined $value;
49 $value = $self->_kvs->get(KVS_PREFIX . $key);
51 $self->_cache->set(KVS_PREFIX . $key, $value, $expire);
60 my ($self, $basename, $opts) = @_;
64 if (!$opts->{reload}) {
65 $content = $self->get_cache($basename);
66 return $content if defined $content;
69 # no content hits in cache, then read from file(s)
70 if ($self->{bundle_config}->{$basename}) {
71 # target is bundle, so pack them
72 $content = $self->_pack_files($basename);
75 # target is single file, load it
76 $content = $self->_load_js($basename);
79 if (defined $content) {
80 $self->set_cache($basename, $content);
81 $self->set_cache($content->{path}, $content);
88 my ($self, $basename) = @_;
90 # check if file exists
91 my $abs_path = $self->_basename_to_absolute_path($basename);
92 return if (!-f $abs_path || !-r $abs_path);
95 open( my $fh, "<:utf8", $abs_path ) or return;
96 my $js_body = do { local($/); <$fh> } ;
100 if (utf8::is_utf8($js_body)) {
101 $md5 = md5_hex(Encode::encode('utf8', $js_body));
104 $md5 = md5_hex($js_body);
107 if ($self->{mode} eq "development") {
108 my $basedir = $self->{conf}->{source_directory};
109 $basedir =~ s/^public//g;
110 $path = "$basedir/$basename";
113 my ($base, $dir, $ext) = fileparse($basename, qw/\.[^.]*/);
114 $dir = "" if $dir eq "./";
115 my $basedir = $self->cached_basedir;
116 $path = "$basedir/$dir${base}.$md5$ext";
119 my $result = { md5 => $md5,
122 type => "text/javascript",
128 my ($self, $filename) = @_;
129 $self->{app}->log->debug("JavaScriptLoader: packing to $filename...");
130 my $target_dir = path($self->{app}->home, $self->{conf}->{source_directory})->to_abs;
132 my $config = $self->{bundle_config}->{$filename};
134 $self->{app}->log->error("JavaScriptLoader: no settings to pack $filename...");
140 for my $target (@{$config->{prior}}) {
141 my $path = path($target_dir, $target);
142 next if $done{$path->basename};
143 $self->{app}->log->debug("JavaScriptLoader: pack $target ($path)");
144 push @result, decode('utf-8', $path->slurp);
149 for my $target (path($target_dir)->list->each) {
150 my $basename = $target->basename;
151 next if $target->to_abs !~ $config->{target};
152 next if ( any { $target->to_abs =~ m/$_/ } $config->{exclude} );
153 next if $done{$basename};
155 $self->{app}->log->debug("JavaScriptLoader: pack $basename ($target)");
156 push @result, decode('utf-8', $target->slurp);
157 $done{$basename} = 1;
160 my $joined = join("\n", @result);
162 if (utf8::is_utf8($joined)) {
163 $md5 = md5_hex(Encode::encode('utf8', $joined));
166 $md5 = md5_hex($joined);
168 $self->{app}->log->debug("JavaScriptLoader: packing to $filename done. md5: $md5");
170 my @sources = keys %done;
171 $self->{bundled} ||= {};
172 for my $src (@sources) {
173 $self->{bundled}->{$src} = 1;
176 my $basedir = $self->cached_basedir;
177 my $result = { md5 => $md5,
179 path => "$basedir/$filename",
180 type => "text/javascript",
181 sources => \@sources,
190 sub _basename_to_absolute_path {
191 my ($self, $basename) = @_;
192 my $base_dir = $self->{conf}->{base_directory};
194 $base_dir = $self->{app}->home;
196 if (!$base_dir) { # fallback
199 my $src_dir = $self->{conf}->{source_directory};
200 my $path = File::Spec->catdir($base_dir, $src_dir, $basename);
201 $path = File::Spec->rel2abs($path);
206 my ($self, $path) = @_;
207 if ($path =~ m/^(.+)\.([0-9a-f]+)(\.\w+)$/) {
208 return ($1 . $3, $2);
214 my ($self, $path, $md5) = @_;
216 my ($base, $dir, $ext) = fileparse($path, qw/\.[^.]*/);
217 $dir = "" if $dir eq "./";
218 return "${dir}${base}.${md5}${ext}";
221 sub load_bundle_config {
225 if ($self->{conf}->{config_file}) {
226 my $abs_path = $self->{conf}->{config_file};
227 if ($abs_path !~ m|^/|) {
228 $abs_path = path($self->{app}->home, $abs_path)->to_abs;
233 my $json_body = decode('utf-8', path($abs_path)->slurp);
234 $self->{bundle_config} = decode_json($json_body);
235 $self->load_all_bundles;
238 $self->{app}->log->warn("JavaScriptLoader: cannot read config file: $self->{conf}->{config_file} ($abs_path)");
243 sub load_all_bundles {
245 for my $target (keys %{$self->{bundle_config}}) {
246 my $content = $self->get_content($target, {reload => 1});
251 my ($self, $target) = @_;
252 my $pool = $self->{bundled} // {};
253 return $pool->{$target};
257 my ($self, $app, $conf) = @_;
259 # set default config value
260 my $cnf = $app->config->{JavaScriptLoader} ||= {};
261 $cnf->{source_directory} //= "public/js";
262 $cnf->{mode} //= "development";
263 $cnf->{config_file} //= "public/js/bundle-config.json";
264 $cnf->{use_bundle} //= 1;
265 $cnf->{use_compression} ||= 0;
267 $self->{conf} = {%$conf, %$cnf};
269 $self->{mode} = $self->{conf}->{mode} || $app->mode;
270 $self->{compress} = $cnf->{use_compression};
272 $self->{bundled} = {};
274 if (!$self->{conf}->{source_directory}) {
275 $app->log->warn("JavaScriptLoader: no source_directory given!");
277 $self->load_bundle_config;
279 $app->helper('load_js', sub {
281 # check if target is bundled
282 return '' if $self->_is_bundled($name);
284 my $content = $self->get_content($name);
286 my $md5_path = $self->_insert_md5($content->{path},
288 return "<script src='$md5_path'></script>";
290 $self->{app}->log->warn("JavaScriptLoader: cannot load $name");
294 my $r = $app->routes;
295 $r->any($self->cached_basedir . '/*content_path' => sub {
297 my $path = $c->stash('content_path');
299 my $content = $self->get_cache($path);
302 # no cache, so try to generate
303 my ($basename, $md5) = $self->_split_md5($path);
304 $content = $self->get_content($basename);
307 if ($content && $content->{md5} ne $md5) {
312 if ($self->{compress}
313 && $content->{type} =~ m{^text/}
314 && ($c->req->headers->accept_encoding // '') =~ /gzip/i
316 # use gzip compression
318 $c->res->headers->append(Vary => 'Accept-Encoding');
319 $c->res->headers->content_encoding('gzip');
321 # Compress content with gzip
322 my $gzip_in = encode_utf8($content->{content});
323 gzip(\$gzip_in, \my $compressed);
324 $c->res->headers->content_type($content->{type});
325 $c->res->body($compressed);
329 $c->res->headers->content_type($content->{type});
330 $c->res->body(encode_utf8($content->{content}));
339 $app->helper(javascript_loader => sub { state $javascript_loader = $self; });