1 package Newslash::Plugin::JavaScriptLoader;
2 use Mojo::Base 'Newslash::Plugin::Preprocessor';
5 use File::Basename qw(basename);
7 use Mojo::JSON qw(decode_json encode_json);
8 use List::Util qw(any);
9 use Mojo::File qw(path);
11 use constant CACHE_KEY => "JavaScriptLoader";
12 use constant JS_CONTENT_TYPE => "application/javascript";
15 use IO::Uncompress::Gunzip qw(gunzip);
17 sub _generate_content {
18 my ($self, $pathname) = @_;
19 $pathname = $self->_strip_md5_from_path($pathname);
22 if ($self->{bundle_config}->{$pathname}) {
23 # target is bundle, so pack them
24 $js_content = $self->_pack_files($pathname);
27 # target is single file, load it
28 $js_content = $self->_load_js($pathname);
31 if (!defined $js_content) {
32 $self->{app}->log->error("JavaScriptLoader: generate content failed: $pathname");
35 return $self->_build_content_object($js_content, $pathname, JS_CONTENT_TYPE);
39 my ($self, $rel_pathname) = @_;
41 # check if file exists
42 my $abs_path = $self->_get_absolute_path($rel_pathname);
43 return if (!-f $abs_path || !-r $abs_path);
46 open( my $fh, "<:utf8", $abs_path ) or return;
47 my $js_body = do { local($/); <$fh> } ;
54 my ($self, $pathname) = @_;
55 $self->{app}->log->debug("JavaScriptLoader: packing to $pathname...");
56 my $base_dir = $self->_get_absolute_path($self->{conf}->{source_directory});
58 my $config = $self->{bundle_config}->{$pathname};
60 $self->{app}->log->error("JavaScriptLoader: no settings to pack $pathname...");
66 for my $target (@{$config->{prior}}) {
67 my $path = path($base_dir, $target);
68 next if $done{$path->basename};
69 $self->{app}->log->debug("JavaScriptLoader: pack $target ($path)");
70 push @result, Encode::decode_utf8($path->slurp);
74 for my $target (path($base_dir)->list->each) {
75 my $basename = $target->basename;
76 if ($target->to_abs !~ m/$config->{target}/ ) {
77 $self->{app}->log->debug("JavaScriptLoader: $target is not match target.");
80 if ( any { $target->to_abs =~ m/$_/ } $config->{exclude} ) {
81 $self->{app}->log->debug("JavaScriptLoader: $target is match exclude.");
85 next if $done{$basename};
87 $self->{app}->log->debug("JavaScriptLoader: pack $basename ($target)");
88 push @result, Encode::decode_utf8($target->slurp);
92 my $joined = join("\n", @result);
93 $self->{app}->log->debug("JavaScriptLoader: packing to $pathname done.");
95 my @sources = keys %done;
96 $self->{bundled} //= {};
97 for my $src (@sources) {
98 if (substr($src, 0, 1) ne "/") {
99 $src = $self->_base_url . $src;
101 $self->{bundled}->{$src} = 1;
107 sub _load_bundle_config {
111 if ($self->{conf}->{config_file}) {
112 my $abs_path = $self->{conf}->{config_file};
113 if ($abs_path !~ m|^/|) {
114 $abs_path = path($self->{app}->home, $abs_path)->to_abs;
119 my $json_body = Encode::decode_utf8(path($abs_path)->slurp);
120 $self->{bundle_config} = decode_json($json_body);
123 $self->{app}->log->warn("JavaScriptLoader: cannot read config file: $self->{conf}->{config_file} ($abs_path)");
128 sub load_all_bundles {
130 for my $target (keys %{$self->{bundle_config}}) {
131 my $content = $self->get_content($target, {reload => 1});
136 my ($self, $target) = @_;
137 my $pool = $self->{bundled} // {};
138 return $pool->{$target};
143 my $base_url = $self->{conf}->{base_url};
144 if (substr($base_url, -1, 1) ne "/") {
145 $base_url = $base_url . "/";
152 my ($self, $app, $conf) = @_;
154 # set default config value
155 my $cnf = $app->config->{JavaScriptLoader} ||= {};
156 $cnf->{source_directory} //= "public/js";
157 $cnf->{mode} //= "development";
158 $cnf->{config_file} //= "public/js/bundle-config.json";
159 $cnf->{use_bundle} //= 1;
160 $cnf->{use_compression} ||= 0;
162 $self->{conf} = {%$conf, %$cnf};
164 $self->{mode} = $self->{conf}->{mode} || $app->mode;
165 $self->{compress} = $cnf->{use_compression};
166 $self->{bundled} = {};
168 if (!$self->{conf}->{source_directory}) {
169 $app->log->warn("JavaScriptLoader: no source_directory given!");
171 $self->_load_bundle_config;
173 $app->helper('load_js', sub {
175 if (substr($name, 0, 1) ne "/") {
176 $name = $self->_base_url . $name;
179 # check if target is bundled
180 if ($self->_is_bundled($name)) {
181 $self->{app}->log->debug("JavaScriptLoader::load_js: $name is bundled");
185 my $path = $self->get_md5_path($name);
187 $self->{app}->log->error("JavaScriptLoader::load_js: cannot load $name");
190 return "<script src='$path'></script>";
193 my $r = $app->routes;
194 my $base_url = $self->_base_url;
196 $r->any("${base_url}*content_path" => sub {
198 my $path = $base_url . $c->stash('content_path');
201 my $content = $self->get_content($path);
204 if ($c->req->headers->if_none_match
206 && $c->req->headers->if_none_match eq "\"$content->{md5}\"") {
210 $c->res->headers->content_type($content->{type});
211 my $output = $content->{content};
214 if ($self->{compress}
215 && $content->{type} =~ m{^(text/|application/json|application/javascript)}
216 && ($c->req->headers->accept_encoding // '') =~ /gzip/i
217 && $content->{gz_content}
219 # use gzip compression
221 $c->res->headers->append(Vary => 'Accept-Encoding');
222 $c->res->headers->content_encoding('gzip');
224 $output = $content->{gz_content};
228 $c->res->headers->content_type($content->{type});
229 if ($content->{md5}) {
230 $c->res->headers->etag("\"$content->{md5}\"");
234 # $c->res->body() needs binary data
235 if (Encode::is_utf8($output)) {
236 $output = Encode::encode_utf8($output);
238 $c->res->body($output);
246 $app->helper(javascript_loader => sub { state $javascript_loader = $self; });