Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/monken/p5-plack-middleware-assets

Concatenate and minify JavaScript and CSS files
https://github.com/monken/p5-plack-middleware-assets

Last synced: 3 months ago
JSON representation

Concatenate and minify JavaScript and CSS files

Awesome Lists containing this project

README

        

package Plack::Middleware::Assets;

# ABSTRACT: Concatenate and minify JavaScript and CSS files
use strict;
use warnings;

use base 'Plack::Middleware';
use Plack::Util::Accessor
qw( separator filter content minify files key mtime type type_class expires extension );

use Digest::MD5 qw(md5_hex);
use HTTP::Date ();
use Class::Load ();

sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);

# set defaults
$self->separator(1) unless ( defined $self->separator );
$self->minify(1) unless ( defined $self->minify );
$self->filter(1) unless ( defined $self->filter );
$self->_build_content;
return $self;
}

sub _build_content {
my $self = shift;
local $/;

my $type = $self->type
|| (
( grep {/\.css$/} @{ $self->files } ) ? 'css'
: ( grep {/\.js$/} @{ $self->files } ) ? 'js'
: 'plain'
);
$self->type($type);
my $class = __PACKAGE__ . "::Type::$type";
eval { Class::Load::load_class($class) }
or die "$class could not be loaded: $@";
$self->type_class($class);

my $separator = $self->separator;

# use default comment format unless format was specified
$separator = $class->separator
if $separator && $separator !~ /%s/;

$self->content(
join(
"\n",
map {
open my $fh, '<', $_ or die "$_: $!";
( $separator ? sprintf( $separator, $_ ) : '' ) . <$fh>
} @{ $self->files }
)
);

$self->content( $self->_transform('filter') ) if $self->filter;
$self->content( $self->_transform('minify') ) if $self->minify;

$self->key( md5_hex( $self->content ) );
my @mtime = map { ( stat($_) )[9] } @{ $self->files };
$self->mtime( ( reverse( sort(@mtime) ) )[0] );
}

sub _transform {
my ( $self, $transform ) = @_;
return $self->content
if ( $transform ne 'minify'
&& $ENV{PLACK_ENV}
&& $ENV{PLACK_ENV} eq 'development' );
no strict 'refs';
my $run = $self->$transform;
my $method;
if ( ref $run eq 'CODE' ) {
$method = $run;
}
elsif ( $run && $self->type_class->can($transform) ) {
$method = $self->type_class . "::$transform";
}
return $self->content unless ($method);

local $_ = $self->content;
return $method->($_);
}

sub serve {
my $self = shift;
my $type = $self->type;

return [
200,
[ 'Content-Type' => $self->type_class->content_type,
'Content-Length' => length( $self->content ),
'Last-Modified' => HTTP::Date::time2str( $self->mtime ),
'Expires' =>
HTTP::Date::time2str( time + ( $self->expires || 2592000 ) ),
],
[ $self->content ]
];
}

sub call {
my $self = shift;
my $env = shift;

if ( $ENV{PLACK_ENV} && $ENV{PLACK_ENV} eq 'development' ) {
my @mtime = map { ( stat($_) )[9] } @{ $self->files };
$self->_build_content
if ( $self->mtime < ( reverse( sort(@mtime) ) )[0] );
}

$env->{'psgix.assets'} ||= [];
my $extension = $self->extension || $self->type_class->extension;
my $url = '/_asset/' . $self->key . '.' . $extension;
push( @{ $env->{'psgix.assets'} }, $url );
return $self->serve if $env->{PATH_INFO} eq $url;
return $self->app->($env);
}

package Plack::Middleware::Assets::Type::plain;
use strict;
use warnings;

sub content_type {'text/plain'}
sub separator { }
sub extension {'txt'}

package Plack::Middleware::Assets::Type::css;
use strict;
use warnings;
use base 'Plack::Middleware::Assets::Type::plain';
use CSS::Minifier::XS qw(minify);

sub content_type {'text/css'}
sub separator {"/* %s */\n"}
sub extension {'css'}

package Plack::Middleware::Assets::Type::js;
use strict;
use warnings;
use base 'Plack::Middleware::Assets::Type::plain';
use JavaScript::Minifier::XS qw(minify);

sub content_type {'application/javascript'}
sub separator {"/* %s */\n"}
sub extension {'js'}

1;

__END__

=head1 SYNOPSIS

# in app.psgi
use Plack::Builder;

builder {
enable Assets => ( files => [] );
enable Assets => (
files => [],
minify => 0
);
$app;
};

# or customize your assets as desired:

builder {

# concatenate sass files and transform them into css
enable Assets => (
files => [],
type => 'css',
filter => sub { Text::Sass->new->sass2css(shift) },
minify => 0
);

# pass a coderef for a custom minifier
enable Assets => (
files => [],
filter => sub {uc},
minify => sub { s/ +/\t/g; $_ }
);

# concatenate any arbitrary content type
enable Assets => (
files => [],
type => 'less'
);

$app;
};


# since this module ships only with the types css and js,
# you have to implement the less type yourself:

package Plack::Middleware::Assets::Type::less;
use base 'Plack::Middleware::Assets::Type::css';
use CSS::Minifier::XS qw(minify);
use CSS::LESSp ();
sub filter { CSS::LESSp->parse(@_) }

# $env->{'psgix.assets'}->[0] points at the first asset.

=head1 DESCRIPTION

Plack::Middleware::Assets concatenates JavaScript and CSS files
and minifies them.

A C digest is generated and used as the unique url to the
asset. For instance, if the first C is
C, then the unique C url can be used in a
single HTML C element for all js files.

The C<Last-Modified> header is set to the C<mtime> of the most
recently changed file.

The C<Expires> header is set to one month in advance. Set
L</expires> to change the time of expiry.

The concatented and minified content is cached in memory.

=head1 DEVELOPMENT MODE

$ plackup app.psgi

$ starman -E development app.psgi

In development mode the minification is disabled and the
concatenated content is regenerated if there were any changes
to the files.

=head1 CONFIGURATIONS

=over 4

=item separator

By default files are prepended with C</* filename */\n>
before being concatenated.

Set this to false to disable these comments.

If set to a string containing a C<%s>
it will be passed to C<sprintf> with the file name.

separator => "# %s\n"

=item files

Files to concatenate.

=item filter

A coderef that can process/transform the content.

The current content will be passed in as C<$_[0]>
and also available via C<$_> for convenience.

This will be called before it is minified (if C<minify> is enabled).

=item minify

Value to indicate whether to minify or not. Defaults to C<1>.
This can also be a coderef which works the same as L</filter>.

=item type

Type of the asset.
Predefined types include C<css> and C<js>. Additional types can be implemented
by creating a new class in the C<Plack::Middleware::Assets::Type> namespace.
See the L</SYNOPSIS> for an example.

An attempt to guess the correct value is made from the file extensions
but this can be set explicitly if you are using non-standard file extensions.

=item expires

Time in seconds from now (i.e. C<time>) until the resource expires.

=item extension

File extension that is appended to the asset's URI.

=back

=head1 TODO

Allow to concatenate documents from URLs, such that you can have a
L<Plack::Middleware::File::Sass> that converts SASS files to CSS and
concatenate those with other CSS files. Also concatenate content from
CDNs that host common JavaScript libraries.

=head1 SEE ALSO

L<Catalyst::Plugin::Assets>

Inspired by L<Plack::Middleware::JSConcat>