OSDN Git Service

e470a079c35a9377e529d7ce8126878bb9f4cb99
[newslash/newslash.git] / src / newslash_web / lib / Newslash / Plugin / JavaScriptLoader.pm
1 package Newslash::Plugin::JavaScriptLoader;
2 use Mojo::Base 'Newslash::Plugin::Preprocessor';
3
4 use File::Spec;
5 use File::Basename qw(basename);
6 use Encode;
7 use Mojo::JSON qw(decode_json encode_json);
8 use List::Util qw(any);
9 use Mojo::File qw(path);
10
11 use constant CACHE_KEY => "JavaScriptLoader";
12 use constant JS_CONTENT_TYPE => "application/javascript";
13 use Data::Dumper;
14
15 use IO::Uncompress::Gunzip qw(gunzip);
16
17 sub _generate_content {
18     my ($self, $pathname) = @_;
19     $pathname = $self->_strip_md5_from_path($pathname);
20
21     my $js_content;
22     if ($self->{bundle_config}->{$pathname}) {
23         # target is bundle, so pack them
24         $js_content = $self->_pack_files($pathname);
25     }
26     else {
27         # target is single file, load it
28         $js_content = $self->_load_js($pathname);
29     }
30
31     if (!defined $js_content) {
32         $self->{app}->log->error("JavaScriptLoader: generate content failed: $pathname");
33         return;
34     }
35     return $self->_build_content_object($js_content, $pathname, JS_CONTENT_TYPE);
36 }
37
38 sub _load_js {
39     my ($self, $rel_pathname) = @_;
40
41     # check if file exists
42     my $abs_path = $self->_get_absolute_path($rel_pathname);
43     return if (!-f $abs_path || !-r $abs_path);
44
45     # load file
46     open( my $fh, "<:utf8", $abs_path ) or return;
47     my $js_body = do { local($/); <$fh> } ;
48     close($fh);
49
50     return $js_body;
51 }
52
53 sub _pack_files {
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});
57
58     my $config = $self->{bundle_config}->{$pathname};
59     if (!$config) {
60         $self->{app}->log->error("JavaScriptLoader: no settings to pack $pathname...");
61         return;
62     }
63
64     my @result;
65     my %done;
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);
71         $done{$target} = 1;
72     }
73
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.");
78             next;
79         }
80         if ( any { $target->to_abs =~ m/$_/ } $config->{exclude} ) {
81             $self->{app}->log->debug("JavaScriptLoader: $target is match exclude.");
82             next;
83         }
84
85         next if $done{$basename};
86
87         $self->{app}->log->debug("JavaScriptLoader: pack $basename ($target)");
88         push @result, Encode::decode_utf8($target->slurp);
89         $done{$basename} = 1;
90     }
91
92     my $joined = join("\n", @result);
93     $self->{app}->log->debug("JavaScriptLoader: packing to $pathname done.");
94
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;
100         }
101         $self->{bundled}->{$src} = 1;
102     }
103     return $joined;
104 }
105
106
107 sub _load_bundle_config {
108     my ($self) = @_;
109
110     # load config file
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;
115         }
116
117         # read bundle config
118         if (-r $abs_path) {
119             my $json_body = Encode::decode_utf8(path($abs_path)->slurp);
120             $self->{bundle_config} = decode_json($json_body);
121         }
122         else {
123             $self->{app}->log->warn("JavaScriptLoader: cannot read config file: $self->{conf}->{config_file} ($abs_path)");
124         }
125     }
126 }
127
128 sub load_all_bundles {
129     my $self = shift;
130     for my $target (keys %{$self->{bundle_config}}) {
131         my $content = $self->get_content($target, {reload => 1});
132     }
133 }
134
135 sub _is_bundled {
136     my ($self, $target) = @_;
137     my $pool = $self->{bundled} // {};
138     return $pool->{$target};
139 }
140
141 sub _base_url {
142     my $self = shift;
143     my $base_url = $self->{conf}->{base_url};
144     if (substr($base_url, -1, 1) ne "/") {
145         $base_url = $base_url . "/";
146     }
147     return $base_url;
148 }
149
150
151 sub register {
152     my ($self, $app, $conf) = @_;
153
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;
161
162     $self->{conf} = {%$conf, %$cnf};
163     $self->{app} = $app;
164     $self->{mode} = $self->{conf}->{mode} || $app->mode;
165     $self->{compress} = $cnf->{use_compression};
166     $self->{bundled} = {};
167
168     if (!$self->{conf}->{source_directory}) {
169         $app->log->warn("JavaScriptLoader: no source_directory given!");
170     }
171     $self->_load_bundle_config;
172
173     $app->helper('load_js', sub {
174                      my ($c, $name) = @_;
175                      if (substr($name, 0, 1) ne "/") {
176                          $name = $self->_base_url . $name;
177                      }
178
179                      # check if target is bundled
180                      if ($self->_is_bundled($name)) {
181                          $self->{app}->log->debug("JavaScriptLoader::load_js: $name is bundled");
182                          return '';
183                      }
184
185                      my $path = $self->get_md5_path($name);
186                      if (!$path) {
187                          $self->{app}->log->error("JavaScriptLoader::load_js: cannot load $name");
188                          return '';
189                      }
190                      return "<script src='$path'></script>";
191                  });
192
193     my $r = $app->routes;
194     my $base_url = $self->_base_url;
195
196     $r->any("${base_url}*content_path" => sub {
197                 my $c = shift;
198                 my $path = $base_url . $c->stash('content_path');
199
200                 if ($path) {
201                     my $content = $self->get_content($path);
202                     if ($content) {
203                         # check Etag
204                         if ($c->req->headers->if_none_match
205                             && $content->{md5}
206                             && $c->req->headers->if_none_match eq "\"$content->{md5}\"") {
207                             $c->rendered(304);
208                             return;
209                         }
210                         $c->res->headers->content_type($content->{type});
211                         my $output = $content->{content};
212
213                         # check compressable
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}
218                            ) {
219                             # use gzip compression
220                             # Add header
221                             $c->res->headers->append(Vary => 'Accept-Encoding');
222                             $c->res->headers->content_encoding('gzip');
223
224                             $output = $content->{gz_content};
225                         }
226
227                         # add Etag
228                         $c->res->headers->content_type($content->{type});
229                         if ($content->{md5}) {
230                             $c->res->headers->etag("\"$content->{md5}\"");
231                         }
232
233                         # send result
234                         # $c->res->body() needs binary data
235                         if (Encode::is_utf8($output)) {
236                             $output = Encode::encode_utf8($output);
237                         }
238                         $c->res->body($output);
239                         $c->rendered(200);
240                         return;
241                     }
242                 }
243                 $c->rendered(404);
244             });
245
246     $app->helper(javascript_loader => sub { state $javascript_loader = $self; });
247 }
248
249 1;