package Newslash::Plugin::JavaScriptLoader;
-use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::Base 'Newslash::Plugin::Preprocessor';
use File::Spec;
-use File::Basename qw(fileparse);
-use Digest::MD5 qw(md5_hex);
-use IPC::Open3;
-use Symbol qw(gensym);
-use IO::Select;
-use POSIX ":sys_wait_h";
-use Mojo::Util qw(dumper);
+use File::Basename qw(basename);
use Encode;
+use Mojo::JSON qw(decode_json encode_json);
+use List::Util qw(any);
+use Mojo::File qw(path);
-use constant KVS_PREFIX => "JSL:";
-use constant DEFAULT_MEM_EXPIRE => 60;
-use constant DEFAULT_KVS_EXPIRE => 60;
+use constant CACHE_KEY => "JavaScriptLoader";
+use constant JS_CONTENT_TYPE => "application/javascript";
-sub _kvs {
- return shift->{app}->kvs;
-}
-
-sub _cache {
- return shift->{app}->cache;
-}
-
-sub set_cache {
- my ($self, $key, $value) = @_;
- my $mem_expire = $self->{conf}->{mem_expire};
- my $kvs_expire = $self->{conf}->{kvs_expire};
- $mem_expire = DEFAULT_MEM_EXPIRE if !defined $mem_expire;
- $kvs_expire = DEFAULT_KVS_EXPIRE if !defined $kvs_expire;
-
- $self->_kvs->set(KVS_PREFIX . $key, $value, $kvs_expire);
- $self->_cache->set(KVS_PREFIX . $key, $value, $mem_expire);
-}
+sub _generate_content {
+ my ($self, $pathname) = @_;
+ $pathname = $self->_strip_md5_from_path($pathname);
-sub get_cache {
- my ($self, $key) = @_;
- my $expire = $self->{conf}->{mem_expire};
- $expire = DEFAULT_MEM_EXPIRE if !defined $expire;
-
- my $value = $self->_cache->get(KVS_PREFIX . $key);
- return $value if defined $value;
-
- $value = $self->_kvs->get(KVS_PREFIX . $key);
- if (defined $value) {
- $self->_cache->set(KVS_PREFIX . $key, $value, $expire);
- return $value;
+ my $js_content;
+ if ($self->{bundle_config}->{$pathname}) {
+ # target is bundle, so pack them
+ $js_content = $self->_pack_files($pathname);
}
-
- # no cache found
- return;
-}
-
-sub get_content {
- my ($self, $basename) = @_;
- my $content = $self->get_cache($basename);
- return $content if defined $content;
-
- $content = $self->_load_js($basename);
- if (defined $content) {
- $self->set_cache($basename, $content);
- $self->set_cache($content->{path}, $content);
- return $content
+ else {
+ # target is single file, load it
+ $js_content = $self->_load_js($pathname);
}
- return;
-}
-sub cached_basedir {
- return "/js_cache";
+ if (!defined $js_content) {
+ $self->{app}->log->error("JavaScriptLoader: generate contentfailed: $pathname");
+ return;
+ }
+ return $self->_build_content_object($js_content, $pathname, JS_CONTENT_TYPE);
}
sub _load_js {
- my ($self, $basename) = @_;
+ my ($self, $rel_pathname) = @_;
# check if file exists
- my $abs_path = $self->_basename_to_absolute_path($basename);
+ my $abs_path = $self->_get_absolute_path($rel_pathname);
return if (!-f $abs_path || !-r $abs_path);
# load file
my $js_body = do { local($/); <$fh> } ;
close($fh);
- my $md5;
- if (utf8::is_utf8($js_body)) {
- $md5 = md5_hex(Encode::encode('utf8', $js_body));
+ return $js_body;
+}
+
+sub _pack_files {
+ my ($self, $pathname) = @_;
+ $self->{app}->log->debug("JavaScriptLoader: packing to $pathname...");
+ my $base_dir = $self->_get_absolute_path($self->{conf}->{source_directory});
+
+ my $config = $self->{bundle_config}->{$pathname};
+ if (!$config) {
+ $self->{app}->log->error("JavaScriptLoader: no settings to pack $pathname...");
+ return;
}
- else {
- $md5 = md5_hex($js_body);
+
+ my @result;
+ my %done;
+ for my $target (@{$config->{prior}}) {
+ my $path = path($base_dir, $target);
+ next if $done{$path->basename};
+ $self->{app}->log->debug("JavaScriptLoader: pack $target ($path)");
+ push @result, Encode::decode_utf8($path->slurp);
+ #my $js;
+ #my $fh = FileHandle->new;
+ #if ($fh->open($path, "<:utf8")) {
+ # $js = do { local $/; <$fh> };
+ # $fh->close;
+ #}
+ #else {
+ # $self->{app}->log->error("JavaScriptLoader: cannot open $path");
+ #}
+ #push @result, $js;
+ $done{$target} = 1;
}
- my $path;
- if ($self->{mode} eq "development") {
- my $basedir = $self->{conf}->{source_directory};
- $basedir =~ s/^public//g;
- $path = "$basedir/$basename";
+
+ for my $target (path($base_dir)->list->each) {
+ my $basename = $target->basename;
+ next if $target->to_abs !~ $config->{target};
+ next if ( any { $target->to_abs =~ m/$_/ } $config->{exclude} );
+ next if $done{$basename};
+
+ $self->{app}->log->debug("JavaScriptLoader: pack $basename ($target)");
+ push @result, Encode::decode_utf8($target->slurp);
+ #my $js;
+ #my $fh = FileHandle->new;
+ #if ($fh->open($target, "<:utf8")) {
+ # $js = do { local $/; <$fh> };
+ # $fh->close;
+ #}
+ #else {
+ # $self->{app}->log->error("JavaScriptLoader: cannot open $target");
+ #}
+ #push @result, $js;
+ $done{$basename} = 1;
}
- else {
- my ($base, $dir, $ext) = fileparse($basename, qw/\.[^.]*/);
- $dir = "" if $dir eq "./";
- my $basedir = $self->cached_basedir;
- $path = "$basedir/$dir${base}.$md5$ext";
+
+ my $joined = join("\n", @result);
+ $self->{app}->log->debug("JavaScriptLoader: packing to $pathname done.");
+
+ my @sources = keys %done;
+ $self->{bundled} ||= {};
+ for my $src (@sources) {
+ $self->{bundled}->{$src} = 1;
}
- my $result = { md5 => $md5,
- content => $js_body,
- path => $path,
- type => "text/javascript",
- };
- return $result;
+ return $joined;
}
-sub _basename_to_absolute_path {
- my ($self, $basename) = @_;
- my $base_dir = $self->{conf}->{base_directory};
- if (!$base_dir) {
- $base_dir = $self->{app}->home;
- }
- if (!$base_dir) { # fallback
- $base_dir = "./";
+
+sub _load_bundle_config {
+ my ($self) = @_;
+
+ # load config file
+ if ($self->{conf}->{config_file}) {
+ my $abs_path = $self->{conf}->{config_file};
+ if ($abs_path !~ m|^/|) {
+ $abs_path = path($self->{app}->home, $abs_path)->to_abs;
+ }
+
+ # read bundle config
+ if (-r $abs_path) {
+ my $json_body = Encode::decode_utf8(path($abs_path)->slurp);
+ $self->{bundle_config} = decode_json($json_body);
+ #$self->load_all_bundles;
+ }
+ else {
+ $self->{app}->log->warn("JavaScriptLoader: cannot read config file: $self->{conf}->{config_file} ($abs_path)");
+ }
}
- my $src_dir = $self->{conf}->{source_directory};
- my $path = File::Spec->catdir($base_dir, $src_dir, $basename);
- $path = File::Spec->rel2abs($path);
- return $path;
}
-sub _split_md5 {
- my ($self, $path) = @_;
- if ($path =~ m/^(.+)\.([0-9a-f]+)(\.\w+)$/) {
- return ($1 . $3, $2);
+sub load_all_bundles {
+ my $self = shift;
+ for my $target (keys %{$self->{bundle_config}}) {
+ my $content = $self->get_content($target, {reload => 1});
}
- return ($path, "");
+}
+
+sub _is_bundled {
+ my ($self, $target) = @_;
+ my $pool = $self->{bundled} // {};
+ return $pool->{$target};
}
sub register {
my ($self, $app, $conf) = @_;
- my $c = $app->config->{JavaScriptLoader};
- if (!$c) {
- $app->log->error("JavaScriptLoader: no config settings found!");
- return;
- }
+ # set default config value
+ my $cnf = $app->config->{JavaScriptLoader} ||= {};
+ $cnf->{source_directory} //= "public/js";
+ $cnf->{mode} //= "development";
+ $cnf->{config_file} //= "public/js/bundle-config.json";
+ $cnf->{use_bundle} //= 1;
+ $cnf->{use_compression} ||= 0;
- $self->{conf} = {%$conf, %$c};
+ $self->{conf} = {%$conf, %$cnf};
$self->{app} = $app;
$self->{mode} = $self->{conf}->{mode} || $app->mode;
+ $self->{compress} = $cnf->{use_compression};
+ $self->{bundled} = {};
if (!$self->{conf}->{source_directory}) {
$app->log->warn("JavaScriptLoader: no source_directory given!");
}
+ $self->_load_bundle_config;
- if ($app->config->{TT2Renderer}) {
- my $tt2r = $app->config->{TT2Renderer}->{self};
- $tt2r->add_NS_function('load_js', sub {
- my ($c, $name) = @_;
- my $content = $self->get_content($name);
- if ($content) {
- return "<script src='$content->{path}'></script>";
- }
- $self->{app}->log->warn("JavaScriptLoader: cannot load $name");
- return '';
- });
- }
+ $app->helper('load_js', sub {
+ my ($c, $name) = @_;
+ # check if target is bundled
+ return '' if $self->_is_bundled($name);
+
+ my $path = $self->get_md5_path($name);
+ if (!$path) {
+ $self->{app}->log->error("JavaScriptLoader: cannot load $name");
+ return '';
+ }
+ return "<script src='$path'></script>";
+ });
my $r = $app->routes;
- $r->any('/js_cache/*content_path' => sub {
+ my $base_url = $self->{conf}->{base_url};
+ if (substr($base_url, -1, 1) ne "/") {
+ $base_url = $base_url . "/";
+ }
+
+ $r->any("${base_url}*content_path" => sub {
my $c = shift;
- my $path = $c->stash('content_path');
+ my $path = $base_url . $c->stash('content_path');
+
if ($path) {
- my $content = $self->get_cache($path);
- if (!$content) {
- # no cache, so try to generate
- my ($basename, $md5) = $self->_split_md5($path);
- $content = $self->get_content($basename);
-
- # check md5
- if ($content && $content->{md5} ne $md5) {
- $content = "";
- }
- }
+ my $content = $self->get_content($path);
if ($content) {
+ # check Etag
+ if ($c->req->headers->if_none_match
+ && $content->{md5}
+ && $c->req->headers->if_none_match eq "\"$content->{md5}\"") {
+ $c->rendered(304);
+ return;
+ }
$c->res->headers->content_type($content->{type});
- $c->res->body(encode_utf8($content->{content}));
+ my $output = $content->{content};
+
+ # check compressable
+ if ($self->{compress}
+ && $content->{type} =~ m{^(text/|application/json|application/javascript)}
+ && ($c->req->headers->accept_encoding // '') =~ /gzip/i
+ && $content->{gz_content}
+ ) {
+ # use gzip compression
+ # Add header
+ $c->res->headers->append(Vary => 'Accept-Encoding');
+ $c->res->headers->content_encoding('gzip');
+
+ $output = $content->{gz_content};
+ }
+
+ # add Etag
+ $c->res->headers->content_type($content->{type});
+ if ($content->{md5}) {
+ $c->res->headers->etag("\"$content->{md5}\"");
+ }
+
+ # send result
+ $c->res->body($output);
$c->rendered(200);
return;
}
$app->helper(javascript_loader => sub { state $javascript_loader = $self; });
}
-
-
1;