OSDN Git Service

Plugin::JavaScriptLoader: fix encoding and gzipped content related problem
[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
14 sub _generate_content {
15     my ($self, $pathname) = @_;
16     $pathname = $self->_strip_md5_from_path($pathname);
17
18     my $js_content;
19     if ($self->{bundle_config}->{$pathname}) {
20         # target is bundle, so pack them
21         $js_content = $self->_pack_files($pathname);
22     }
23     else {
24         # target is single file, load it
25         $js_content = $self->_load_js($pathname);
26     }
27
28     if (!defined $js_content) {
29         $self->{app}->log->error("JavaScriptLoader: generate contentfailed: $pathname");
30         return;
31     }
32     return $self->_build_content_object($js_content, $pathname, JS_CONTENT_TYPE);
33 }
34
35 sub _load_js {
36     my ($self, $rel_pathname) = @_;
37
38     # check if file exists
39     my $abs_path = $self->_get_absolute_path($rel_pathname);
40     return if (!-f $abs_path || !-r $abs_path);
41
42     # load file
43     open( my $fh, "<:utf8", $abs_path ) or return;
44     my $js_body = do { local($/); <$fh> } ;
45     close($fh);
46
47     return $js_body;
48 }
49
50 sub _pack_files {
51     my ($self, $pathname) = @_;
52     $self->{app}->log->debug("JavaScriptLoader: packing to $pathname...");
53     my $base_dir = $self->_get_absolute_path($self->{conf}->{source_directory});
54
55     my $config = $self->{bundle_config}->{$pathname};
56     if (!$config) {
57         $self->{app}->log->error("JavaScriptLoader: no settings to pack $pathname...");
58         return;
59     }
60
61     my @result;
62     my %done;
63     for my $target (@{$config->{prior}}) {
64         my $path = path($base_dir, $target);
65         next if $done{$path->basename};
66         $self->{app}->log->debug("JavaScriptLoader: pack $target ($path)");
67         push @result, Encode::decode_utf8($path->slurp);
68         #my $js;
69         #my $fh = FileHandle->new;
70         #if ($fh->open($path, "<:utf8")) {
71         #    $js = do { local $/; <$fh> };
72         #    $fh->close;
73         #}
74         #else {
75         #    $self->{app}->log->error("JavaScriptLoader: cannot open $path");
76         #}
77         #push @result, $js;
78         $done{$target} = 1;
79     }
80
81     for my $target (path($base_dir)->list->each) {
82         my $basename = $target->basename;
83         next if $target->to_abs !~ $config->{target};
84         next if ( any { $target->to_abs =~ m/$_/ } $config->{exclude} );
85         next if $done{$basename};
86
87         $self->{app}->log->debug("JavaScriptLoader: pack $basename ($target)");
88         push @result, Encode::decode_utf8($target->slurp);
89         #my $js;
90         #my $fh = FileHandle->new;
91         #if ($fh->open($target, "<:utf8")) {
92         #    $js = do { local $/; <$fh> };
93         #    $fh->close;
94         #}
95         #else {
96         #    $self->{app}->log->error("JavaScriptLoader: cannot open $target");
97         #}
98         #push @result, $js;
99         $done{$basename} = 1;
100     }
101
102     my $joined = join("\n", @result);
103     $self->{app}->log->debug("JavaScriptLoader: packing to $pathname done.");
104
105     my @sources = keys %done;
106     $self->{bundled} ||= {};
107     for my $src (@sources) {
108         $self->{bundled}->{$src} = 1;
109     }
110
111     return $joined;
112 }
113
114
115 sub _load_bundle_config {
116     my ($self) = @_;
117
118     # load config file
119     if ($self->{conf}->{config_file}) {
120         my $abs_path = $self->{conf}->{config_file};
121         if ($abs_path !~ m|^/|) {
122             $abs_path = path($self->{app}->home, $abs_path)->to_abs;
123         }
124
125         # read bundle config
126         if (-r $abs_path) {
127             my $json_body = Encode::decode_utf8(path($abs_path)->slurp);
128             $self->{bundle_config} = decode_json($json_body);
129             #$self->load_all_bundles;
130         }
131         else {
132             $self->{app}->log->warn("JavaScriptLoader: cannot read config file: $self->{conf}->{config_file} ($abs_path)");
133         }
134     }
135 }
136
137 sub load_all_bundles {
138     my $self = shift;
139     for my $target (keys %{$self->{bundle_config}}) {
140         my $content = $self->get_content($target, {reload => 1});
141     }
142 }
143
144 sub _is_bundled {
145     my ($self, $target) = @_;
146     my $pool = $self->{bundled} // {};
147     return $pool->{$target};
148 }
149
150 sub register {
151     my ($self, $app, $conf) = @_;
152
153     # set default config value
154     my $cnf = $app->config->{JavaScriptLoader} ||= {};
155     $cnf->{source_directory} //= "public/js";
156     $cnf->{mode} //= "development";
157     $cnf->{config_file} //= "public/js/bundle-config.json";
158     $cnf->{use_bundle} //= 1;
159     $cnf->{use_compression} ||= 0;
160
161     $self->{conf} = {%$conf, %$cnf};
162     $self->{app} = $app;
163     $self->{mode} = $self->{conf}->{mode} || $app->mode;
164     $self->{compress} = $cnf->{use_compression};
165     $self->{bundled} = {};
166
167     if (!$self->{conf}->{source_directory}) {
168         $app->log->warn("JavaScriptLoader: no source_directory given!");
169     }
170     $self->_load_bundle_config;
171
172     $app->helper('load_js', sub {
173                      my ($c, $name) = @_;
174                      # check if target is bundled
175                      return '' if $self->_is_bundled($name);
176
177                      my $path = $self->get_md5_path($name);
178                      if (!$path) {
179                          $self->{app}->log->error("JavaScriptLoader: cannot load $name");
180                          return '';
181                      }
182                      return "<script src='$path'></script>";
183                  });
184
185     my $r = $app->routes;
186     my $base_url = $self->{conf}->{base_url};
187     if (substr($base_url, -1, 1) ne "/") {
188         $base_url = $base_url . "/";
189     }
190
191     $r->any("${base_url}*content_path" => sub {
192                 my $c = shift;
193                 my $path = $base_url . $c->stash('content_path');
194
195                 if ($path) {
196                     my $content = $self->get_content($path);
197                     if ($content) {
198                         # check Etag
199                         if ($c->req->headers->if_none_match
200                             && $content->{md5}
201                             && $c->req->headers->if_none_match eq "\"$content->{md5}\"") {
202                             $c->rendered(304);
203                             return;
204                         }
205                         $c->res->headers->content_type($content->{type});
206                         my $output = $content->{content};
207
208                         # check compressable
209                         if ($self->{compress}
210                             && $content->{type} =~ m{^(text/|application/json|application/javascript)}
211                             && ($c->req->headers->accept_encoding // '') =~ /gzip/i
212                             && $content->{gz_content}
213                            ) {
214                             # use gzip compression
215                             # Add header
216                             $c->res->headers->append(Vary => 'Accept-Encoding');
217                             $c->res->headers->content_encoding('gzip');
218
219                             $output = $content->{gz_content};
220                         }
221
222                         # add Etag
223                         $c->res->headers->content_type($content->{type});
224                         if ($content->{md5}) {
225                             $c->res->headers->etag("\"$content->{md5}\"");
226                         }
227
228                         # send result
229                         $c->res->body($output);
230                         $c->rendered(200);
231                         return;
232                     }
233                 }
234                 $c->rendered(404);
235             });
236
237     $app->helper(javascript_loader => sub { state $javascript_loader = $self; });
238 }
239
240 1;