NAME

App::SlimPacker::Minify - PPI-based Perl source minifier and bundle-aware renaming engine

VERSION

Version 0.07

SYNOPSIS

use App::SlimPacker::Minify qw(process minify_file);

my $min = minify_file('lib/My/Module.pm');     # read a file and minify it
my $min = process($source);                    # same, from a string

my $min = process($source, rename_subs => 1);  # also rename subs/methods
my $min = process($source, rename_vars => 0);  # only strip comments,
                                               # POD and whitespace

Given

package My::Greeter;
use strict;
use warnings;

# Build a greeter
sub new {
    my ($class, %args) = @_;
    return bless {%args}, $class;
}

sub greeting_for {
    my ($self, $name) = @_;
    return "Hello, $name!";
}
1;

process($source, rename_subs => 1) returns

package My::Greeter;use strict;use warnings;sub new{my($c,%d)=@_;return bless{%d},$c;}sub a{my($e,$f)=@_;return "Hello, $f!";}1;

DESCRIPTION

A PPI-based minifier for Perl source. It removes comments, POD and whitespace and renames my variables to short names. It can also rename subs, methods, our globals and package names.

A single file can only be renamed so far: a sub or package name may be used by other files. When you minify a whole program at once, as slimpack does, "collect_bundle_globals(@sources)" collects names across all files so they can be renamed consistently. See "EXAMPLES".

App::SlimPacker re-exports all of these functions.

EXAMPLES

Minifying several files together

use Path::Tiny ();
use App::SlimPacker::Minify
    qw(process collect_bundle_globals rename_module_name rename_module_path);

my %src = (
    'My/Greeter.pm' => Path::Tiny::path('lib/My/Greeter.pm')->slurp_utf8,
    'main.pl'       => Path::Tiny::path('main.pl')->slurp_utf8,
);

my ($globals, $lexicals, $subs, $modules) = collect_bundle_globals(values %src);

# $globals is keyed by package name, which module renaming changes too
my %globals = map { rename_module_name($_, $modules) => $globals->{$_} }
    keys %{ $globals };

for my $file (sort keys %src) {
    my $min = process($src{$file},
        rename_subs     => 1,
        global_rename   => \%globals,
        global_lexicals => $lexicals,
        global_subs     => $subs,
        global_modules  => $modules,
    );
    # package names changed, so the file names must too:
    # My/Greeter.pm -> c/d.pm, main.pl stays main.pl
    my $out = Path::Tiny::path('out', rename_module_path($file, $modules));
    $out->parent->mkpath;
    $out->spew_utf8($min);
}

With a main.pl of

use My::Greeter;
my $greeter = My::Greeter->new;
print $greeter->greeting_for('world'), "\n";

this writes

# out/c/d.pm
package c::d;...sub a{my($e,$f)=@_;return "Hello, $f!";}1;

# out/main.pl
use c::d;my$c=c::d->new;print$c->a('world'),"\n";

and perl -Iout out/main.pl still prints Hello, world!.

FUNCTIONS

process($source, %options)

Minifies Perl source code using PPI. Returns the minified string.

Options:

rename_vars => 0|1

Rename my variables to short names (a, b, ... aa, ...). Enabled by default. Set to 0 to keep variable names intact (useful for fatlib core modules). Alias: rename.

rewrite => 0|1

Apply an additional, optional pass that rewrites code into shorter equivalent forms. Disabled by default. The rewrites are all value-preserving and built as a self-contained, removable step so the whole pass can be switched off (rewrite = 0>) or deleted (the _rewrite_doc method and its one call site in _minify_doc) if it ever causes problems.

Currently: foreach -> for; m/.../ -> /.../ when preceded by =~ or !~ (the m is optional with / delimiters); trailing ; before a closing } is dropped; a $x += 1; statement in a non-final position becomes $x++; (and -= 1> -> --); $x = $x OP $y; with a single-token RHS becomes $x OP= $y; for . + - * / %; and parentheses are dropped from statement-terminal builtin calls that take a variable argument (print($x), return($y), push(@a, $x)).

global_rename_vars => \%map

Alias: global_rename.

global_rename => \%map

Also rename package globals using a map built by "collect_bundle_globals(@sources)".

global_lexicals => \%lexicals

Bundle-wide set of my/state names, used to leave an unqualified reference untouched when a lexical of the same name exists anywhere in the bundle. Returned as the second value of "collect_bundle_globals(@sources)"; only meaningful together with global_rename.

rename_subs => 0|1

Rename sub and method names to short names as well. Off by default here; slimpack and minify turn it on unless given --no-rename-subs.

global_subs => \%subs

Bundle-wide name => short_name map for "rename_subs", returned as the third value of "collect_bundle_globals(@sources)". Passing it renames the subs it lists even without rename_subs.

global_modules => \%modinfo

Rename package names (My::Greeter becomes c::d) using the struct returned as the fourth value of "collect_bundle_globals(@sources)". There is no separate on/off option: packages are renamed when this is passed. slimpack passes it unless given --no-rename-modules.

rename_exports => 0|1

Extend "rename_subs" to the export/import protocol. Off by default.

process_deps($source, %options)

Minifies and resolves dependencies in a single PPI parse. Returns ($minified, @deps) where $minified is what "process($source, %options)" would return for the same source and @deps is what "module_deps($source)" would return. Use it when both quantities are needed for the same file to avoid parsing it twice; this is what the bundler does for every reachable module.

collect_bundle_globals(@sources)

Returns ($map, $lexicals, $subs, $modinfo) for bundle-aware renaming. @sources is the original text of every module that will land in one bundle (the boot program included). $lexicals is a hashref of every my/state name found anywhere in those sources. $map is package => { global_name => short_name } describing a rename of the our-declared package globals, sized to fit the longest name in the bundle. Postfix declarations like return if our $flag are covered too: PPI classifies them as an ordinary statement (PPI::Statement::Break) rather than a variable declaration, so they are picked up from the keyword's first symbol instead. Feed both back into "process($source, %options)" (and "process_deps($source, %options)") as global_rename / global_lexicals.

Renaming package globals is only safe when the bundle knows its whole dependency graph: minifying a single file on its own leaves our globals alone, but every module packed together can agree on one short name. Referenced from an interpolating string, the quotation is rewritten to match. $VERSION, $AUTOLOAD, @ISA, ALL_CAPS, control-variable ($^...) and single-character names are never renamed, and a global is skipped if its name is a my lexical anywhere in the bundle. The generated short names avoid every bundle lexical and reserved name, so an unqualified reference can never become ambiguous.

$subs is a flat name => short_name hash for "rename_subs", collected the same way as "process($source, %options)" but over all @sources at once, so cross-module calls keep agreeing on one short name. Short names avoid every Perl keyword, all sub names seen in @sources, and every word appearing inside a string-like token (so a runtime string dispatch can never alias a renamed sub). Does not renumber: the rename_subs pass over each module consumes slots from the same name_gen sequence as the variable pass. Pass it to "process($source, %options)" / "process_deps($source, %options)" as global_subs.

$modinfo is the module-rename struct for global_modules: { map => { component => short }, pkgs => [ package names, longest first ] }. Every namespace component of a declared bundled package gets a short name, the leaf included (the loader's %INC keys are rewritten from the same map). Pass it to "process($source, %options)" / "process_deps($source, %options)" as global_modules.

An optional trailing hashref selects behaviour: rename_exports => 1 records import/export protocol words (use ... qw(), @EXPORT_OK = qw(...), can('x')) as sub-rename candidates instead of freezing them, matching the rename_exports option of "process($source, %options)".

rename_module_name($name, $modinfo)

Helper for global_modules: given a Perl-style class name (App::Saisons, App::Saisons::UI::something) and the $modinfo struct, returns the renamed form if the name matches one of the declared bundles, the original text otherwise.

The name matches when it is a declared package, a Pkg::... prefix of one (a renamed prefix plus an untouched tail, e.g. bk::bl::UI::Foo), or a strict namespace ancestor of one (App::Saisons given App::Saisons::UI). A declared package maps every component, the leaf included (see global_modules); ancestors map every component too and tails keep whatever came after the matched prefix.

Note $modinfo->{pkgs} must be sorted longest-first for correct matching; "collect_bundle_globals(@sources)" produces it that way.

minify_file($path, %options)

Reads the file at $path and runs it through "process($source, %options)" with the given options. Dies if the file cannot be read. Returns the minified string.

A leading #! shebang line is preserved: process strips it as a comment, and a minified script must stay runnable.

module_deps($source)

Returns a list of module names statically declared as dependencies in the given Perl source. Extracts modules from use, require, use base, use parent, and string-form require "Foo/Bar.pm". Pragmas (strict, warnings, lib, etc.) are skipped.

name_gen($n)

Returns a short variable name for the index $n: 0 -> a, 1 -> b, 25 -> z, 26 -> aa, etc.

needs_space($left_token, $right_token)

Returns 1 if a space is needed between two PPI tokens to prevent them from merging into a single token, 0 otherwise.

AUTHOR

Nicolas Mendoza, <mendoza at pvv.ntnu.no>

LICENSE AND COPYRIGHT

This software is licensed under the Artistic License 2.0. See the LICENSE file in this distribution for the full text.

SEE ALSO

App::SlimPacker, slimpack, PPI