The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Mojolicious::Plugin::StaticCompressor;
use Mojo::Base 'Mojolicious::Plugin';
use utf8;

our $VERSION = 0.0.2;

use Encode qw();
use CSS::Minifier qw();
use JavaScript::Minifier qw();
use Mojo::Util qw();

our $importInfos; # Hash ref	- import-key <-> {file-infos, file-type}
our $IS_DISABLE;
our $URL_PATH_PREFIX;

sub register {
	my ($self, $app, $conf) = @_;

	# Load options	-	disable_on_devmode
	my $disable_on_devmode = $conf->{disable_on_devmode} || 0;

	# Load options	-	url_path_prefix
	$URL_PATH_PREFIX = $conf->{url_path_prefix} || 'auto_compressed';

	# Initilaize
	$importInfos = {};

	# Initialize file cache (cache each a single file)
	my $cache = Mojo::Cache->new(max_keys => 100);

	$IS_DISABLE = ($disable_on_devmode eq 1 && $app->mode eq 'development') ? 1 : 0;
	
	# Add hook
	$app->hook(
		before_dispatch => sub {
			my $self = shift;
			if($self->req->url->path->contains('/'.$URL_PATH_PREFIX)
				&& $self->req->url->path =~ /\/$URL_PATH_PREFIX\/((nomin\-|)\w+)$/){

				my $import_key = $1;
				my $is_enable_minify = $2 eq 'nomin-' ? 0 : 1;
				
				if (exists $importInfos->{$import_key}) {
					# Found the file-type and file-paths, from importInfos
					my $file_type = $importInfos->{$import_key}->{type};
					my @file_infos = @{$importInfos->{$import_key}->{infos}};

					my $output = "";

					foreach my $info(@file_infos){
						my $path = $info->{path};

						# Check file date
						my $f; my $file_updated_at;
						eval {
							$f = $self->app->static->file($path);
							$file_updated_at = (stat($f->path()))[9];
						}; if($@){ die ("Can't read static file: $path\n$@");}

						# Generate of file cache-key
						my $cache_key  = ($is_enable_minify eq 1) ? $path : 'nomin-'.$path;

						# Find a file on cache
						if (my $content = $cache->get($cache_key)){ # If found a file on cache...
							my $cache_updated_at = $info->{updated_at};
							if ($file_updated_at eq $cache_updated_at){ # cache is latest
								# Combine
								$output .= $content;
								next;
							}
						}

						# If not found a file on cache, or cache were old...

						$info->{updated_at} = $file_updated_at;

						# Read a file from static dir
						my $content = $f->slurp();
						# Decoding
						$content = Encode::decode_utf8($content);

						# Minify
						$content = minify($file_type, $content) if ($is_enable_minify);

						# Add to cache
						$cache->set($cache_key, $content);

						# Combine
						$output .= $content;
					}

					$self->render(text => $output, format => $file_type);
				}
			}
		}
	);

	# Add "js" helper
	$app->helper(js => sub {
		my $self = shift;
		my @file_paths = @_;
		return generate_import('js', 1, \@file_paths);
	});

	# Add "css" helper
	$app->helper(css => sub {
		my $self = shift;
		my @file_paths = @_;
		return generate_import('css', 1, \@file_paths);
	});

	# Add "js_nominify" helper
	$app->helper(js_nominify => sub {
		my $self = shift;
		my @file_paths = @_;
		return generate_import('js', 0, \@file_paths);
	});

	# Add "css_nominify" helper
	$app->helper(css_nominify => sub {
		my $self = shift;
		my @file_paths = @_;
		return generate_import('css', 0, \@file_paths);
	});
}

# Generate of import-key & return import HTML-tag
sub generate_import {
	my ($file_type, $is_enable_minify, $paths_ref) = @_;
	if($IS_DISABLE){
		# If disable mode... Return RAW import HTML-tag
		return Mojo::ByteStream->new(generate_import_tag_raw($file_type, $paths_ref));
	}

	# Generate of import-key from file-paths
	my $import_key = generate_import_key($is_enable_minify, $paths_ref);
	
	# Add import-key to importInfos hash
	add_import_info($import_key, $file_type, $paths_ref);
	
	# Return import HTML-tag
	return Mojo::ByteStream->new(generate_import_tag_compress($file_type, $import_key));
}

# Generate of import-key
sub generate_import_key {
	my ($is_enable_minify, $file_paths_ref) = @_;

	my $key = "";
	{
		$, = "_";
		$key = "@$file_paths_ref";
	}
	$key = Mojo::Util::sha1_sum($key);

	if($is_enable_minify eq 0){
		$key = "nomin-".$key;
	}
	return $key;
}

# Add import-info into importInfos
sub add_import_info {
	my ($import_key, $file_type, $file_paths_ref) = @_;

	unless (exists($importInfos->{$import_key})){
		my @file_infos;
		foreach my $file_path(@{$file_paths_ref}){
			push(@file_infos, {
				path => $file_path,
				updated_at => 0,
			});
		}

		$importInfos->{$import_key} = {
			infos => \@file_infos,
			type => $file_type,
		};
	}
}

# Generate of import HTML-tag - minify / compressed url
sub generate_import_tag_compress {
	my ($file_type, $import_key) = @_;
	return return generate_import_tag_html_code($file_type, "/$URL_PATH_PREFIX/$import_key");
}

# Generate of import HTML-tag - RAW url (for DISABLE mode)
sub generate_import_tag_raw {
	my ($file_type, $files_paths_ref) = @_;
	my $output = "";
	foreach(@{$files_paths_ref}){
		$output .= generate_import_tag_html_code($file_type, $_);
	}
	return $output;
}

# Generate of import HTML-tag
sub generate_import_tag_html_code {
	my ($file_type, $url) = @_;
	if ($file_type eq 'js'){
		return "<script src=\"$url\"></script>\n";
	} elsif ($file_type eq 'css'){
		return "<link rel=\"stylesheet\" href=\"$url\">\n";
	}
}

# Minify a code
sub minify {
	my ($file_type, $content) = @_;
	if($file_type eq 'js'){
		return minify_js($content);
	} elsif ($file_type eq 'css'){
		return minify_css($content);
	}
}

# Minify a javascript code
sub minify_js {
	my $content = shift;
	return JavaScript::Minifier::minify(input => $content);
}

# Minify a css code
sub minify_css {
	my $content = shift;
	return CSS::Minifier::minify(input => $content);
}

1;
__END__
=head1 NAME

Mojolicious::Plugin::StaticCompressor - Automatic JS/CSS minifier & compressor for Mojolicious

=head1 SYNOPSIS

Into the your Mojolicious application:

  sub startup {
    my $self = shift;

    $self->plugin('StaticCompressor');
    ~~~

(Also, you can read the examples using the Mojolicious::Lite, in a later section.)

Then, into the template in your application:

  <html>
  <head>
    ~~~~
    <%= js '/foo.js', '/bar.js' %> <!-- minified and combined, automatically -->
    <%= css '/baz.css' %> <!-- minified, automatically -->
    ~~~~
  </head>

However, this module has just launched development yet. please give me your feedback.

=head1 DISCRIPTION

This Mojolicious plugin is minifier and compressor for static JavaScript file (.js) and CSS file (.css).

=head1 INSTALLATION (from GitHub)

  $ git clone git://github.com/mugifly/p5-Mojolicious-Plugin-StaticCompressor.git
  $ cpanm ./p5-Mojolicious-Plugin-StaticCompressor

=head1 HELPERS

You can use these helpers on templates and others.

=head2 js $file_path [, ...]

Example of use on template file:

  <%= js '/js/foo.js' %>

This is just available as substitution for the 'javascript' helper (built-in helper of Mojolicious).

However, this helper will output a HTML-tag including the URL which is a compressed files. 

  <script src="/auto_compressed/124015dca008ef1f18be80d7af4a314afec6f6dc"></script>

When this script file has output (just received a request), it is minified automatically.

Then, minified file are cached in the memory.

=head3 Support for multiple files

In addition, You can also use this helper with multiple js-files:

  <%= js '/js/foo.js', '/js/bar.js' %>

In this case, this helper will output a single HTML-tag.

but, when these file has output, these are combined (and minified) automatically.

=head2 css $file_path [, ...]

This is just available as substitution for the 'stylesheet' helper (built-in helper of Mojolicious).

=head2 js_nominify $file_path [, ...]

If you don't want Minify, please use this.

This helper is available for purposes that only combine with multiple js-files.

=head2 css_nominify $file_path [, ...]

If you don't want Minify, please use this.

This helper is available for purposes that only combine with multiple css-files.

=head1 CONFIGURATION

=head2 disable_on_devmode

You can disable a combine (and minify) when running your Mojolicious application as 'development' mode (such as a running on  the 'morbo'), by using this option:

  $self->plugin('StaticCompressor', disable_on_devmode => 1);

(default: 0)

=head1 KNOWN ISSUES

=over 4

=item * Implement the disk cache. (Currently is memory cache only.)

=item * Improvement of the load latency in the first.

=item * Support for LESS and Sass.

=back

Your feedback is highly appreciated!

https://github.com/mugifly/p5-Mojolicious-Plugin-StaticCompressor/issues

=head1 EXAMPLE OF USE

Prepared a brief sample app for you, with using Mojolicious::Lite:

example/example.pl

  $ morbo example.pl

Let's access to http://localhost:3000/ with your browser.

=head1 REQUIREMENTS

=over 4

=item * Mojolicious v3.8x or later (Operability Confirmed: v3.88)

=item * Other dependencies (cpan modules).

=back

=head1 SEE ALSO

L<https://github.com/mugifly/p5-Mojolicious-Plugin-StaticCompressor>

L<Mojolicious>

L<CSS::Minifier>

L<JavaScript::Minifier>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2013, Masanori Ohgita (http://ohgita.info/).

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

Thanks, Perl Mongers & CPAN authors.