Added Build.pm macro, which will be completely re-written into FilePump
This commit is contained in:
parent
3049a3cce3
commit
fa04ab1288
1 changed files with 478 additions and 0 deletions
478
lib/WebGUI/Macro/Build.pm
Normal file
478
lib/WebGUI/Macro/Build.pm
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
package WebGUI::Macro::Build;
|
||||
|
||||
=head1 LEGAL
|
||||
|
||||
-------------------------------------------------------------------
|
||||
(c) Patrick Donelan
|
||||
-------------------------------------------------------------------
|
||||
http://patspam.com pat@patspam.com
|
||||
-------------------------------------------------------------------
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use Readonly;
|
||||
use File::Assets;
|
||||
use CSS::Minifier::XS; # implicit
|
||||
use JavaScript::Minifier::XS; # implicit
|
||||
use File::Slurp qw(read_file write_file);
|
||||
use Digest::SHA1 qw(sha1_hex);
|
||||
use JSON;
|
||||
|
||||
Readonly my $STATIC => 'static'; # Source files come from uploads/$STATIC
|
||||
Readonly my $MINIFIED => 'minified'; # Build dir is uploads/$MINIFIED
|
||||
Readonly my $ASSETS => 'assets'; # Built JS/CSS files are called $ASSETS.js/css
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Package WebGUI::Macro::Minify
|
||||
|
||||
=head2 DESCRIPTION
|
||||
|
||||
Build tool for maximising YSlow score. CSS and JS are minified into files called assets.css and assets.js in the build dir.
|
||||
|
||||
=head3 FOLDERS
|
||||
|
||||
Build dir lives under /data/domains/site.com/uploads/$STATIC (so that we don't clash with any existing urls).
|
||||
Normally this would be a symlink to your custom static folder.
|
||||
You can create a "frozen" folder inside $STATIC and modify site.com.modproxy.conf to give it a far-future expiry:
|
||||
<Location /uploads/static/frozen>
|
||||
ExpiresActive On
|
||||
ExpiresDefault "access plus 10 years"
|
||||
</Location>
|
||||
Any files in this folder should be revved.
|
||||
|
||||
You will need to manually build mod_expires and modify modproxy.conf to load the module:
|
||||
LoadModule expires_module modules/mod_expires.so
|
||||
|
||||
$MINIFIED folder gets created automatically (it lives under /uploads too).
|
||||
You should also give it a far-future expiry:
|
||||
<Location /uploads/minified>
|
||||
ExpiresActive On
|
||||
ExpiresDefault "access plus 10 years"
|
||||
</Location>
|
||||
|
||||
To further maximise your YSlow score, you should make sure modproxy.conf and modperl.conf both contain:
|
||||
LoadModule deflate_module modules/mod_deflate.so
|
||||
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/x-javascript application/x-shockwave-flash application/javascript
|
||||
FileETag none
|
||||
|
||||
=head3 MODES
|
||||
|
||||
Modes can be specified either as first argument to macro, or via url as ?build=mode.
|
||||
Expects unmixed args, e.g. only css, only js or all generic assets (e.g. images).
|
||||
Assets are symlinked to build dir. They can be specified as file globs (e.g. 'subfolder/*.png')
|
||||
or as entire dirs (e.g. 'subfolder/subsubfolder').
|
||||
Digest cached in wg settings table (see get_digest_field_name).
|
||||
Description of modes:
|
||||
|
||||
=head4 default mode
|
||||
js: re-use previously built $ASSETS.js and cached digest
|
||||
css: re-use previously built $ASSETS.css and cached digest
|
||||
images: re-use symlinked images
|
||||
|
||||
=head4 debug mode
|
||||
js: concat into $ASSETS.js and update digest
|
||||
css: use original css
|
||||
images: symlink images
|
||||
|
||||
=head4 min mode
|
||||
js: minify into $ASSETS.js and update digest
|
||||
css: minify into $ASSETS.css, rewrite CSS image urls and update digest
|
||||
images: symlink images
|
||||
|
||||
=head3 JS Widget Best Practices
|
||||
|
||||
Each widget should be in a sub-dir with js, css and images
|
||||
CSS should use local relative path to images
|
||||
Images should use unique prefix so that they don't clash with other images when copied to minified folder
|
||||
|
||||
=cut
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
sub process {
|
||||
my ( $session, @args ) = @_;
|
||||
|
||||
if ( !@args ) {
|
||||
$session->log->warn('Minify: no args, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
my ( $mode, $type, @files ) = process_macro_args( $session, @args );
|
||||
|
||||
if ( $type eq 'js' ) {
|
||||
return handle_js( $session, $mode, @files );
|
||||
}
|
||||
elsif ( $type eq 'css' ) {
|
||||
return handle_css( $session, $mode, @files );
|
||||
}
|
||||
else {
|
||||
return handle_assets( $session, $mode, @files );
|
||||
}
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 process_macro_args
|
||||
Figures out what to do based on the macro args, form params etc..
|
||||
=cut
|
||||
|
||||
sub process_macro_args {
|
||||
my ( $session, @args ) = @_;
|
||||
|
||||
# Trim whitespace from Macro arguments
|
||||
map {s/^\s+|\s$//g} @args;
|
||||
|
||||
# Get mode and remove any mode-related arguments from @args..
|
||||
my $form_param = $session->form->param('build') || q{};
|
||||
my $mode = 'default';
|
||||
foreach my $valid_mode (qw(min debug)) {
|
||||
if ( $args[0] eq $valid_mode ) {
|
||||
$mode = $valid_mode;
|
||||
shift @args;
|
||||
}
|
||||
elsif ( $form_param eq $valid_mode ) {
|
||||
$mode = $valid_mode;
|
||||
}
|
||||
}
|
||||
my $type = get_type(@args);
|
||||
|
||||
$session->log->debug( "Minify: $mode mode for $type on " . @args . ' files' );
|
||||
|
||||
return ( $mode, $type, @args );
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 get_type
|
||||
Guesses file type (css or js). Anything else (e.g. images) is returns undef.
|
||||
=cut
|
||||
|
||||
sub get_type {
|
||||
my @files = @_;
|
||||
foreach my $file (@files) {
|
||||
return 'css' if $file =~ /\.css$/i;
|
||||
return 'js' if $file =~ /\.js$/i;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 get_digest_field_name ($type)
|
||||
Generates the field name used to store the cached digest in the settings table
|
||||
=cut
|
||||
|
||||
sub get_digest_field_name {
|
||||
my ($type) = @_;
|
||||
return "minify_digest_$type";
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 get_digest
|
||||
Pulls a previously stored digest out of the wg db
|
||||
=cut
|
||||
|
||||
sub get_digest {
|
||||
my ( $session, $type ) = @_;
|
||||
my $digest = $session->db->quickScalar( 'select value from settings where name = ?',
|
||||
[ get_digest_field_name($type) ] );
|
||||
return $digest;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 handle_assets
|
||||
Assets are symlinked to build dir. They can be specified as file globs (e.g. 'subfolder/*.png')
|
||||
or as entire dirs (e.g. 'subfolder/subsubfolder').
|
||||
=cut
|
||||
|
||||
sub handle_assets {
|
||||
my ( $session, $mode, @files ) = @_;
|
||||
|
||||
# Check if we need to do anything extra for mode..
|
||||
if ( $mode ne 'default' ) {
|
||||
|
||||
# N.B. Not needed for CSS images in debug mode, but possibly for js and other things
|
||||
link_assets( $session, @files );
|
||||
}
|
||||
|
||||
return; # no output needed for assets
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 link_assets (@files)
|
||||
Symlink files to the build dir
|
||||
=cut
|
||||
|
||||
sub link_assets {
|
||||
my ( $session, @files ) = @_;
|
||||
my $base_dir = $session->config->get('uploadsPath');
|
||||
my $build_dir = "$base_dir/$MINIFIED";
|
||||
|
||||
my $counter = 0;
|
||||
foreach my $file (@files) {
|
||||
$file =~ s{^[/.]*}{}; # disallow absolute paths
|
||||
$file =~ s{\.\.}{}g; # disallow tree-traversal
|
||||
if ( $file =~ /\*/ ) {
|
||||
$session->log->debug("Processing file glob: $file");
|
||||
while (my $asset = <$base_dir/$STATIC/$file>) { # probably a security hole
|
||||
my $src = $asset;
|
||||
$asset =~ s{.*/}{}; # remove subdirs from path
|
||||
my $dest = "$build_dir/$asset";
|
||||
create_link( $session, $src, $dest );
|
||||
$counter++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$session->log->debug("Processing asset: $file");
|
||||
my $src = "$base_dir/$STATIC/$file";
|
||||
$file =~ s{.*/}{}; # remove subdirs from path
|
||||
my $dest = "$build_dir/$file";
|
||||
create_link( $session, $src, $dest );
|
||||
$counter++;
|
||||
}
|
||||
}
|
||||
$session->log->debug("Minify: linked $counter assets to: $build_dir");
|
||||
return;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 create_link ($src, $dest)
|
||||
Create a single symlink from $src to $dest
|
||||
=cut
|
||||
|
||||
sub create_link {
|
||||
my ( $session, $src, $dest ) = @_;
|
||||
if ( -e $dest && !-l $dest ) {
|
||||
$session->log->error("Destination file exists but is not a symlink: $dest");
|
||||
return;
|
||||
}
|
||||
else {
|
||||
remove( $session, $dest );
|
||||
|
||||
if ( symlink $src, $dest ) {
|
||||
$session->log->debug("Symlinked $src to $dest");
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
$session->log->error("Unable to symlink $src to $dest: $!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
=head2 handle_css ($mode, @files)
|
||||
In debug mode, write out link tags to original (untouched) css files.
|
||||
In min mode, minify css to $ASSETS.css, rewrite CSS image urls and update digest.
|
||||
In default mode, pluck the digest out of the db and do no work.
|
||||
|
||||
This call to the macro should be placed in the HEAD of the document.
|
||||
=cut
|
||||
|
||||
sub handle_css {
|
||||
my ( $session, $mode, @files ) = @_;
|
||||
|
||||
# Check if we need to minify..
|
||||
if ( $mode eq 'min' ) {
|
||||
minify( $session, $mode, 'css', @files );
|
||||
}
|
||||
|
||||
my $base_uri = $session->config->get('uploadsURL');
|
||||
|
||||
my $output = q{};
|
||||
if ( $mode eq 'debug' ) {
|
||||
|
||||
# Use original css in debug mode
|
||||
foreach my $file (@files) {
|
||||
my $original_file_uri = "$base_uri/$STATIC/$file";
|
||||
$output .= qq~\n<link rel="stylesheet" href="$original_file_uri" type="text/css">~;
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
# Use minified css in both 'min' and 'default' modes
|
||||
my $digest = get_digest( $session, 'css' );
|
||||
my $asset_uri = "$base_uri/$MINIFIED/$ASSETS.css?digest=$digest";
|
||||
$output = qq~<link rel="stylesheet" href="$asset_uri" type="text/css">~;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
#----------------------------------------------------------------------------
|
||||
|
||||
=head2 handle_js
|
||||
|
||||
In debug mode, concat js files into $ASSETS.js and update the digest.
|
||||
In min mode, minify js files into $ASSETS.js and update the digest.
|
||||
In default mode, pluck the digest out of the db and do no work.
|
||||
|
||||
This call to the macro should be placed in the bottom of the BODY of the document
|
||||
|
||||
=cut
|
||||
|
||||
sub handle_js {
|
||||
my ( $session, $mode, @files ) = @_;
|
||||
|
||||
# Check if we need to minify
|
||||
if ( $mode ne 'default' ) { # Minify in both 'min' and 'debug' modes
|
||||
minify( $session, $mode, 'js', @files );
|
||||
}
|
||||
|
||||
my $base_uri = $session->config->get('uploadsURL');
|
||||
my $digest = get_digest( $session, 'js' );
|
||||
my $asset_uri = "$base_uri/$MINIFIED/$ASSETS.js?digest=$digest";
|
||||
|
||||
return qq~<script type="text/javascript" src="$asset_uri"></script>~;
|
||||
}
|
||||
|
||||
#----------------------------------------------------------------------------
|
||||
|
||||
=head2 minify
|
||||
Minify js or css. Valid modes are 'debug' or 'min':
|
||||
In min mode:
|
||||
* js: minify into $ASSETS.js
|
||||
* css: minify into $ASSETS.css
|
||||
In debug mode:
|
||||
* js: concat into $ASSETS.js
|
||||
* css: invalid mode
|
||||
=cut
|
||||
|
||||
sub minify {
|
||||
my ( $session, $mode, $type, @files ) = @_;
|
||||
|
||||
# Only handle js and css
|
||||
if ( $type ne 'js' && $type ne 'css' ) {
|
||||
$session->log->error('Invalid type, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
# Valid modes for js: 'debug' and 'min'
|
||||
if ( $type eq 'js' && $mode ne 'debug' && $mode ne 'min' ) {
|
||||
$session->log->error("Invalid mode for $type: $mode");
|
||||
return;
|
||||
}
|
||||
|
||||
# Valid modes for css: 'min'
|
||||
if ( $type eq 'css' && $mode ne 'min' ) {
|
||||
$session->log->error("Invalid mode for $type: $mode");
|
||||
return;
|
||||
}
|
||||
|
||||
my $base_dir = $session->config->get('uploadsPath');
|
||||
my $base_uri = $session->config->get('uploadsURL');
|
||||
my $output_path = "$MINIFIED/$ASSETS";
|
||||
my $asset_path = "$base_dir/$output_path.$type";
|
||||
|
||||
if ( !@files ) {
|
||||
$session->log->error('Minify: No files to process, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
$session->log->debug("minify $mode mode for $type");
|
||||
|
||||
# Start with a clean slate
|
||||
remove( $session, $asset_path );
|
||||
|
||||
my $concat = concat( $base_dir, @files );
|
||||
my $digest = sha1_hex($concat);
|
||||
update_digest( $session, $digest, $type );
|
||||
|
||||
if ( $mode eq 'debug' ) { # only applies to js
|
||||
write_file( $asset_path, $concat );
|
||||
}
|
||||
else {
|
||||
my $assets = File::Assets->new(
|
||||
base => {
|
||||
dir => $base_dir,
|
||||
uri => $base_uri,
|
||||
},
|
||||
minify => 'xs',
|
||||
);
|
||||
|
||||
# Built files go here (if we build at all)
|
||||
$assets->set_output_path($output_path);
|
||||
|
||||
# Process the files..
|
||||
foreach my $file (@files) {
|
||||
$assets->include("$STATIC/$file");
|
||||
}
|
||||
$assets->export();
|
||||
}
|
||||
|
||||
if ( $type eq 'css' ) {
|
||||
rewrite_image_urls( $session, $asset_path, $digest );
|
||||
}
|
||||
|
||||
$session->log->debug( "Minify: ${mode}'d " . @files . " assets to: $asset_path ($digest)" );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#----------------------------------------------------------------------------
|
||||
|
||||
=head2 rewrite_image_urls ( $session, $asset_path, $digest )
|
||||
Append digest query string to the end of all relative CSS image urls in the CSS file specified at $asset_path
|
||||
=cut
|
||||
|
||||
sub rewrite_image_urls {
|
||||
my ( $session, $asset_path, $digest ) = @_;
|
||||
$session->log->debug('Adding digest to CSS image urls');
|
||||
my $content = read_file($asset_path) or $session->log->warn("rewrite_image_urls unable to read $asset_path: $!");
|
||||
$content =~ s{url\(([^/][^)]*)\)}{url($1?digest=$digest)}ig;
|
||||
write_file( $asset_path, $content ) or $session->log->warn("rewrite_image_urls unable to write $asset_path: $!");
|
||||
return;
|
||||
}
|
||||
|
||||
#----------------------------------------------------------------------------
|
||||
|
||||
=head2 remove ($session, $file)
|
||||
Unlink file if it exists
|
||||
=cut
|
||||
|
||||
sub remove {
|
||||
my ( $session, $file ) = @_;
|
||||
if ( -e $file ) {
|
||||
$session->log->debug("Removing file: $file");
|
||||
unlink $file or $session->log->warn("Error removing $file: $!");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#----------------------------------------------------------------------------
|
||||
|
||||
=head2 update_digest ($session, $digest, $type)
|
||||
Update the cached digest in the db
|
||||
=cut
|
||||
|
||||
sub update_digest {
|
||||
my ( $session, $digest, $type ) = @_;
|
||||
my $field_name = get_digest_field_name($type);
|
||||
$session->db->write( 'delete from settings where name = ?', [$field_name] );
|
||||
$session->db->write( 'insert into settings (name, value) values (?,?)', [ $field_name, $digest ] );
|
||||
$session->log->debug("Minify: Set digest: $digest");
|
||||
return;
|
||||
}
|
||||
|
||||
#----------------------------------------------------------------------------
|
||||
|
||||
=head2 concat ( $base_dir, @files)
|
||||
Concatenate the specified @files
|
||||
=cut
|
||||
|
||||
sub concat {
|
||||
my ( $base_dir, @files ) = @_;
|
||||
my @output;
|
||||
foreach my $file (@files) {
|
||||
my $slurped = read_file("$base_dir/$STATIC/$file");
|
||||
push @output, $slurped;
|
||||
}
|
||||
return join( "\n", @output ) . "\n";
|
||||
}
|
||||
|
||||
1;
|
||||
Loading…
Add table
Add a link
Reference in a new issue