123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- #!/usr/bin/perl
- use local::lib;
- use v5.10;
- use strict;
- use warnings;
- use FindBin qw( $RealBin );
- use lib "$RealBin/lib";
- use File::Basename qw( basename );
- use File::Copy qw( copy );
- use File::Find qw( find );
- use List::Util qw( any );
- use LogBot::Util qw( file_time run slurp spurt touch );
- use Mojo::Util qw( getopt );
- $| = 1;
- my %args;
- getopt(\%args, 'rebuild|B|b', 'quiet|q', 'debug|d', 'help|h|?');
- $args{debug} ||= $ENV{DEBUG};
- $args{deps} = shift // '' eq 'deps';
- $args{help} && die <<'EOF';
- dev-make [options] ['deps']
- build web assets for logbot.
- options:
- --rebuild|-B|-b : force rebuild
- --quiet|-q : no output
- --debug|-d : build debug assets (non-compressed)
- 'deps' : show build dependencies
- EOF
- #<<<
- my $def = {
- 'web/public/static/logbot.min.js' => [
- { 'web/build/jquery.min.js' => { js => 'web/jquery/jquery-3.2.1.min.js' } },
- { 'web/build/pikaday.min.js' => { js => 'web/pikaday/pikaday.js' } },
- { 'web/build/chosen.min.js' => { js => 'web/chosen/chosen.jquery.js' } },
- { 'web/build/flot.min.js' => { js => 'web/flot/jquery.flot.js' } },
- { 'web/build/css-esc.min.js' => { js => 'web/css-escape/css.escape.js' } },
- { 'web/build/logbot.min.js' => { js => 'web/logbot.js' } },
- ],
- 'web/public/static/logbot.min.css' => [
- { 'web/build/pikaday.min.css' => { css => 'web/pikaday/pikaday.scss' } },
- { 'web/build/hind.min.css' => { css => 'web/hind/hind.sass' } },
- { 'web/build/chosen.min.css' => { css => 'web/chosen/chosen.css' } },
- { 'web/build/logbot.min.css' => { lb_css => [qw( web/logbot.sass web/_variables.sass )] } },
- ],
- _svg => [
- qw(
- web/svg/*.svg
- web/svg/font-awesome/*
- web/templates/*.html.ep
- web/templates/layouts/*.html.ep
- web/templates/shared/*.html.ep
- )
- ],
- _cp => [
- qw(
- web/chosen/chosen-sprite.png
- web/chosen/chosen-sprite@2x.png
- web/hind/hind-bold.ttf
- web/hind/hind-medium.ttf
- web/hind/hind-regular.ttf
- web/svg/logbot-favicon.svg
- )
- ],
- };
- #>>>
- mkdir("$RealBin/web/build");
- # switching debug mode on/off forces a rebuild
- my $last_mode_file = "$RealBin/web/build/last_mode";
- my $last_mode = -e $last_mode_file ? slurp($last_mode_file) : '';
- my $this_mode = $args{debug} ? 'debug' : 'production';
- if ($this_mode ne $last_mode) {
- $args{rebuild} = 1;
- l(undef, "$this_mode assets");
- }
- my @deps;
- foreach my $target (sort keys %{$def}) {
- if ($target eq '_cp') {
- # copy file
- foreach my $source_file (@{ $def->{_cp} }) {
- $source_file = "$RealBin/$source_file";
- (push(@deps, $source_file), next) if $args{deps};
- cp($source_file, "$RealBin/web/public/static/" . basename($source_file));
- }
- } elsif ($target eq '_svg') {
- # if any svg or templates are changed, inline svg
- my $target_file = "$RealBin/web/build/inline-svg.updated";
- foreach my $source_file (expand($def->{_svg})) {
- (push(@deps, $source_file), next) if $args{deps};
- next unless update($source_file, $target_file);
- inline_svg();
- touch($target_file);
- last;
- }
- } else {
- # concatenated targets
- my $target_file = "$RealBin/$target";
- my @content;
- my $dirty = 0;
- foreach my $sub (@{ $def->{$target} }) {
- my $sub_target = (keys %{$sub})[0];
- my $sub_target_file = "$RealBin/$sub_target";
- my $action = (keys %{ $sub->{$sub_target} })[0];
- my $action_source = $sub->{$sub_target}->{$action};
- if ($action eq 'css') {
- my $sub_source_file = "$RealBin/$action_source";
- (push(@deps, $sub_source_file), next) if $args{deps};
- css($sub_source_file, $sub_target_file) && ($dirty = 1);
- } elsif ($action eq 'lb_css') {
- foreach my $sub_source_file (expand($action_source)) {
- (push(@deps, $sub_source_file), next) if $args{deps};
- next unless update($sub_source_file, $sub_target_file);
- logbot_css("$RealBin/web/build/logbot.full.css");
- css("$RealBin/web/build/logbot.full.css", $sub_target_file);
- $dirty = 1;
- last;
- }
- } elsif ($action eq 'js') {
- my $sub_source_file = "$RealBin/$action_source";
- (push(@deps, $sub_source_file), next) if $args{deps};
- js($sub_source_file, $sub_target_file) && ($dirty = 1);
- } else {
- die $action;
- }
- push @content, slurp($sub_target_file) unless $args{deps};
- }
- if ($dirty && !$args{deps}) {
- l(undef, $target_file);
- spurt($target_file, join('', @content));
- }
- }
- }
- (say(join("\n", @deps)), exit) if $args{deps};
- spurt($last_mode_file, $this_mode);
- sub cp {
- my ($source_file, $target_file) = @_;
- return unless update($source_file, $target_file);
- l($source_file, $target_file);
- copy($source_file, $target_file) || die "$!\n";
- return 1;
- }
- sub css {
- my ($source_file, $target_file) = @_;
- return unless update($source_file, $target_file);
- l($source_file, $target_file);
- if ($args{debug}) {
- run('sass', '--style', 'expanded', $source_file, $target_file);
- } else {
- run('sass', '--style', 'compressed', $source_file, $target_file);
- }
- my $css = slurp($target_file);
- $css =~ s{/\*.*?\*/}{}gs;
- spurt($target_file, $css);
- return 1;
- }
- sub js {
- my ($source_file, $target_file) = @_;
- return unless update($source_file, $target_file);
- l($source_file, $target_file);
- if ($args{debug}) {
- run('uglifyjs', $source_file, '--beautify', '--output', $target_file);
- } else {
- run('uglifyjs', $source_file, '--compress', '--mangle', '--output', $target_file);
- }
- return 1;
- }
- sub inline_svg {
- # inlines svg images into templates
- # to use, create an svg element with a class name "svg-filename"
- # the filename will be loaded from web/svg and replace the svg element
- # only the svg-filename class will be carried over into the inline svg element,
- # other classes and ids will be lost
- # eg. <svg class="svg-sidebar-collapse"></svg>
- my @files;
- find(
- sub {
- my $file = $File::Find::name;
- return unless -f $file && -s $file;
- return unless $file =~ /\.html\.ep$/;
- push @files, $file;
- },
- "$RealBin/web/templates"
- );
- foreach my $file (sort @files) {
- my $orig = slurp($file);
- (my $tmpl = $orig) =~ s{
- <svg\sclass="svg-([^"]+)".+?</svg>
- }{
- load_svg($1)
- }gxe;
- next if $orig eq $tmpl;
- spurt($file, $tmpl);
- $file =~ s/^\Q$RealBin//;
- l(undef, $file);
- }
- }
- sub logbot_css {
- my ($target_file) = @_;
- # build css that contains light and dark modes
- # the combined sass looks like:
- # variables
- # body
- # logbot.sass unmodified
- # body.dark
- # logbot.sass with colour variables prefixed by `night-`
- #
- # to avoid duplicate styles in the body.dark section, non-colour styles are
- # then commented out. the sass processor will remove these comments from
- # the final minified css.
- my @variables = split(/\n/, slurp("$RealBin/web/_variables.sass"));
- my @sass = split(/\n/, slurp("$RealBin/web/logbot.sass"));
- # remove @import from main sass as we'll be inlining here
- @sass = grep { !/^\@import\s+variables/ } @sass;
- # indent sass
- my @top_level;
- my $is_top_level = 0;
- foreach my $line (@sass) {
- # don't touch top-level :: selectors
- $is_top_level = 1 if $line =~ /^::/;
- if ($is_top_level) {
- $is_top_level = 0 if $line eq '';
- push @top_level, $line;
- $line = '';
- next;
- }
- # existing body definitions need special treatment
- if ($line eq 'body') {
- $line = '&.root';
- }
- $line = ' ' . $line;
- }
- # build duplicate styles, light and dark
- my @combined;
- push @combined, @variables;
- push @combined, '';
- push @combined, @top_level;
- push @combined, 'body';
- push @combined, @sass;
- push @combined, 'body.dark';
- foreach my $line (@sass) {
- $line =~ s/\$(.+?-colour)/\$night-$1/g;
- push @combined, $line;
- }
- # convert to css (easier to parse)
- open(my $sass_cmd, '|-', 'sass', '--stdin', '--indented', 'web/build/logbot.combined.css') or die $!;
- print $sass_cmd join("\n", @combined);
- close($sass_cmd) or die $!;
- # commend out body.dark selectors that don't involve colour
- my @css = split(/\n/, slurp("$RealBin/web/build/logbot.combined.css"));
- my $selector = '';
- foreach my $line (@css) {
- next if $line =~ /^\s*}/;
- if ($line =~ /{$/) {
- ($selector = $line) =~ s/^\s+//;
- } else {
- next unless $selector =~ /^body\.dark\b/;
- next
- if $line =~ /#[a-z0-9]{3,6}/
- || $line =~ /\brgba?\(/
- || $line =~ /^\s*filter:/;
- $line = "/* $line */";
- }
- }
- spurt($target_file, join("\n", @css));
- }
- sub update {
- my ($source_file, $target_file) = @_;
- return 1 if $args{rebuild};
- return 1 if !-e $target_file;
- return file_time($source_file) > file_time($target_file);
- }
- sub expand {
- my ($ra_spec) = @_;
- my @files;
- foreach my $spec (@{$ra_spec}) {
- push @files, glob("'$RealBin/$spec'");
- }
- return @files;
- }
- sub l {
- my ($source_file, $target_file) = @_;
- return if $args{quiet};
- $source_file =~ s{^\Q$RealBin/}{} if $source_file;
- $target_file =~ s{^\Q$RealBin/}{};
- say $source_file ? "$source_file → $target_file" : "→ $target_file";
- }
- sub load_svg {
- my ($name) = @_;
- my $file =
- -e "$RealBin/web/svg/$name.svg" ? "$RealBin/web/svg/$name.svg" : "$RealBin/web/svg/font-awesome/$name.svg";
- die "failed to find $name.svg\n" unless -e $file;
- my $svg = slurp($file);
- $svg =~ s/^<svg /<svg class="svg-$name" /;
- $svg =~ s/\s+$//;
- return $svg;
- }
|