Bundle.pm 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. package Trog::Bundle;
  2. use 5.040;
  3. use re '/aasx';
  4. use warnings FATAL => 'all';
  5. use Digest::SHA ();
  6. use File::Slurper ();
  7. use JavaScript::Minifier::XS ();
  8. use List::Util ();
  9. use Trog::Log qw{:all};
  10. use Trog::Themes ();
  11. =head1 Trog::Bundle
  12. The scripts that a page names, served as one response.
  13. A route names its scripts in a list, and finish_render() in Trog::Routes::HTML
  14. links the bundle of them at the href that scripts() returns. The route
  15. C</bundle/{version}/{names}.js> serves that bundle.
  16. =cut
  17. # The names of a bundle, as its href carries them, to the bundle.
  18. my %bundles;
  19. # The number of bundles kept. A site has a few lists of scripts, and anyone can
  20. # ask for another one, so the cache is emptied when it grows past this.
  21. my $max_bundles = 16;
  22. =head2 scripts(@names) = HASHREF or undef
  23. The bundle of the scripts that @names names, each of them a file name under
  24. F<www/scripts>, such as C<post.js>. The theme's copy of a script is used in
  25. place of the stock one. The order is the order of @names, and a name that is
  26. given twice is used once.
  27. Returns undef if no name is a script. A name that is not a script is left out,
  28. with a warning.
  29. The hash has these keys:
  30. href where the bundle is served, which changes when its body does
  31. version a digest of the body, which is in the href
  32. body the scripts, each one minified unless its name ends in .min.js
  33. A process builds each bundle once, and builds it again when a file in it
  34. changes.
  35. =cut
  36. sub scripts (@names) {
  37. my ( @kept, @files );
  38. foreach my $name ( List::Util::uniq(@names) ) {
  39. my $file = $name =~ m/\A\w[\w.-]*[.]js\z/ ? Trog::Themes::themed_script($name) =~ s{\A/}{www/}r : undef;
  40. if ( !$file || !-f $file ) { ## no critic (ProhibitFiletest_f) -- a name that is not a file is left out
  41. WARN("There is no script $name to bundle");
  42. next;
  43. }
  44. push( @kept, $name );
  45. push( @files, $file );
  46. }
  47. return undef unless @files;
  48. my $names = join( '+', map { s/[.]js\z//r } @kept );
  49. my $signature = join( "\0", map { join( ':', $_, ( stat $_ )[ 7, 9 ] ) } @files );
  50. return $bundles{$names} if $bundles{$names} && $bundles{$names}{signature} eq $signature;
  51. # A newline before the semicolon, because a file can end in a line comment.
  52. my $body = join(
  53. "\n;\n",
  54. map {
  55. my $js = File::Slurper::read_binary($_);
  56. m/[.]min[.]js\z/ ? $js : JavaScript::Minifier::XS::minify($js)
  57. } @files
  58. );
  59. my $version = substr( Digest::SHA::sha256_hex($body), 0, 16 );
  60. %bundles = () if keys %bundles >= $max_bundles;
  61. return $bundles{$names} = {
  62. href => "/bundle/$version/$names.js",
  63. version => $version,
  64. body => $body,
  65. signature => $signature,
  66. };
  67. }