|
|
@@ -9,7 +9,6 @@ use File::Slurper ();
|
|
|
use JavaScript::Minifier::XS ();
|
|
|
use List::Util ();
|
|
|
|
|
|
-use Trog::Log qw{:all};
|
|
|
use Trog::Themes ();
|
|
|
|
|
|
=head1 Trog::Bundle
|
|
|
@@ -22,22 +21,53 @@ C</bundle/{version}/{names}.js> serves that bundle.
|
|
|
|
|
|
=cut
|
|
|
|
|
|
-# The names of a bundle, as its href carries them, to the bundle.
|
|
|
+# The names of each bundle that a page links, as its href carries them, to the
|
|
|
+# bundle.
|
|
|
my %bundles;
|
|
|
|
|
|
-# The number of bundles kept. A site has a few lists of scripts, and anyone can
|
|
|
-# ask for another one, so the cache is emptied when it grows past this.
|
|
|
-my $max_bundles = 16;
|
|
|
+# The file of each name, or undef for a name that is not a script.
|
|
|
+my sub files_of (@names) {
|
|
|
+ return map {
|
|
|
+ my $file = m/\A\w[\w.-]*[.]js\z/ ? Trog::Themes::themed_script($_) =~ s{\A/}{www/}r : undef;
|
|
|
+ $file && -f $file ? $file : undef; ## no critic (ProhibitFiletest_f) -- a name that is not a file is not a script
|
|
|
+ } @names;
|
|
|
+}
|
|
|
+
|
|
|
+# The names as an href carries them, and what changes when a file does.
|
|
|
+my sub identify ( $names, $files ) {
|
|
|
+ return (
|
|
|
+ join( '+', map { s/[.]js\z//r } @$names ),
|
|
|
+ join( "\0", map { join( ':', $_, ( stat $_ )[ 7, 9 ] ) } @$files ),
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+my sub build ( $key, $signature, @files ) {
|
|
|
+
|
|
|
+ # A newline before the semicolon, because a file can end in a line comment.
|
|
|
+ my $body = join(
|
|
|
+ "\n;\n",
|
|
|
+ map {
|
|
|
+ my $js = File::Slurper::read_binary($_);
|
|
|
+ m/[.]min[.]js\z/ ? $js : JavaScript::Minifier::XS::minify($js)
|
|
|
+ } @files
|
|
|
+ );
|
|
|
+ my $version = substr( Digest::SHA::sha256_hex($body), 0, 16 );
|
|
|
+ return {
|
|
|
+ href => "/bundle/$version/$key.js",
|
|
|
+ version => $version,
|
|
|
+ body => $body,
|
|
|
+ signature => $signature,
|
|
|
+ };
|
|
|
+}
|
|
|
|
|
|
=head2 scripts(@names) = HASHREF or undef
|
|
|
|
|
|
-The bundle of the scripts that @names names, each of them a file name under
|
|
|
-F<www/scripts>, such as C<post.js>. The theme's copy of a script is used in
|
|
|
-place of the stock one. The order is the order of @names, and a name that is
|
|
|
-given twice is used once.
|
|
|
+The bundle that a page links, of the scripts that @names names. Each name is a
|
|
|
+file name under F<www/scripts>, such as C<post.js>. The theme's copy of a
|
|
|
+script is used in place of the stock one. The order is the order of @names, and
|
|
|
+a name that is given twice is used once.
|
|
|
|
|
|
-Returns undef if no name is a script. A name that is not a script is left out,
|
|
|
-with a warning.
|
|
|
+Returns undef for an empty list, and dies if a name is not a script.
|
|
|
|
|
|
The hash has these keys:
|
|
|
|
|
|
@@ -51,39 +81,37 @@ changes.
|
|
|
=cut
|
|
|
|
|
|
sub scripts (@names) {
|
|
|
- my ( @kept, @files );
|
|
|
- foreach my $name ( List::Util::uniq(@names) ) {
|
|
|
- my $file = $name =~ m/\A\w[\w.-]*[.]js\z/ ? Trog::Themes::themed_script($name) =~ s{\A/}{www/}r : undef;
|
|
|
- if ( !$file || !-f $file ) { ## no critic (ProhibitFiletest_f) -- a name that is not a file is left out
|
|
|
- WARN("There is no script $name to bundle");
|
|
|
- next;
|
|
|
- }
|
|
|
- push( @kept, $name );
|
|
|
- push( @files, $file );
|
|
|
- }
|
|
|
- return undef unless @files;
|
|
|
+ @names = List::Util::uniq(@names);
|
|
|
+ return undef unless @names;
|
|
|
|
|
|
- my $names = join( '+', map { s/[.]js\z//r } @kept );
|
|
|
- my $signature = join( "\0", map { join( ':', $_, ( stat $_ )[ 7, 9 ] ) } @files );
|
|
|
- return $bundles{$names} if $bundles{$names} && $bundles{$names}{signature} eq $signature;
|
|
|
+ my @files = files_of(@names);
|
|
|
+ my @missing = map { $names[$_] } grep { !defined $files[$_] } 0 .. $#names;
|
|
|
+ die "There is no script to bundle named @missing\n" if @missing;
|
|
|
|
|
|
- # A newline before the semicolon, because a file can end in a line comment.
|
|
|
- my $body = join(
|
|
|
- "\n;\n",
|
|
|
- map {
|
|
|
- my $js = File::Slurper::read_binary($_);
|
|
|
- m/[.]min[.]js\z/ ? $js : JavaScript::Minifier::XS::minify($js)
|
|
|
- } @files
|
|
|
- );
|
|
|
- my $version = substr( Digest::SHA::sha256_hex($body), 0, 16 );
|
|
|
+ my ( $key, $signature ) = identify( \@names, \@files );
|
|
|
+ return $bundles{$key} if $bundles{$key} && $bundles{$key}{signature} eq $signature;
|
|
|
+ return $bundles{$key} = build( $key, $signature, @files );
|
|
|
+}
|
|
|
|
|
|
- %bundles = () if keys %bundles >= $max_bundles;
|
|
|
- return $bundles{$names} = {
|
|
|
- href => "/bundle/$version/$names.js",
|
|
|
- version => $version,
|
|
|
- body => $body,
|
|
|
- signature => $signature,
|
|
|
- };
|
|
|
+=head2 served(@names) = HASHREF or undef
|
|
|
+
|
|
|
+The bundle that the route serves for @names, as scripts() describes it, or undef
|
|
|
+if a name is not a script.
|
|
|
+
|
|
|
+It is the bundle that scripts() built, if that one is current. Otherwise it is
|
|
|
+built for this request and not kept, because anyone can put any list in a URL.
|
|
|
+
|
|
|
+=cut
|
|
|
+
|
|
|
+sub served (@names) {
|
|
|
+ @names = List::Util::uniq(@names);
|
|
|
+ my @files = files_of(@names);
|
|
|
+ return undef if !@names || List::Util::any { !defined } @files;
|
|
|
+
|
|
|
+ my ( $key, $signature ) = identify( \@names, \@files );
|
|
|
+ my $linked = $bundles{$key};
|
|
|
+ return $linked if $linked && $linked->{signature} eq $signature;
|
|
|
+ return build( $key, $signature, @files );
|
|
|
}
|
|
|
|
|
|
=head2 save($tpsgi, $bundle)
|