aboutsummaryrefslogtreecommitdiffstats
path: root/bin/make_css
blob: a98dc6bba5aceefbd7c5929a7337d58b4dee0c1a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/env perl

use strict;
use warnings;
use feature 'say';

BEGIN {
    use File::Basename qw(dirname);
    use File::Spec;
    my $d = dirname(File::Spec->rel2abs($0));
    require "$d/../setenv.pl";
}

use CSS::Sass;
use File::ChangeNotify;
use File::Find::Rule;
use File::Slurp;
use Getopt::Long;
use MIME::Base64;
use MIME::Types;
use Path::Tiny;
use Pod::Usage;

# Store ARGV in case we need to restart later.
my @ARGVorig = @ARGV;

GetOptions(
    'verbose' => \my $verbose,
    'watch' => \my $watch,
    'help|?' => \my $help,
) or pod2usage(2);
pod2usage(1) if $help;

my $mime_types = MIME::Types->new;

my $sass = CSS::Sass->new(
    output_style => SASS_STYLE_COMPRESSED,
    dont_die => 1,
    sass_functions => {
        'inline-image($url)' => sub {
            my ($url) = @_;
            die '$url should be a string' unless $url->isa("CSS::Sass::Value::String");
            # URL is given with reference to the file, which we don't have here. Assume.
            my $data = encode_base64(path("web/cobrands/fixmystreet/$url")->slurp_raw, "");
            my $type = $mime_types->mimeTypeOf($url);
            return "url('data:$type;base64,$data')";
        }
    },
);

# Get directories from the command line, defaulting to 'web' if none.
# We need absolute paths so that the include files lookup works.
my @dirs = map { m{/} ? $_ : "web/cobrands/$_" } @ARGV;
@dirs = 'web' unless @dirs;
@dirs = map { path($_)->absolute->stringify } @dirs;

# Initial compilation, to also discover all the included files.
my %includes;
my %include_to_main;
foreach my $file (File::Find::Rule->file->name(qr/^[^_].*\.scss$/)->in(@dirs)) {
    my @includes = compile($file, $verbose);
    $includes{$file} = \@includes;
    map { push @{$include_to_main{$_}}, $file } @includes ? @includes : $file;
}

# If we're not watching, we're done!
exit unless $watch;

my $watcher = File::ChangeNotify->instantiate_watcher(
    directories => [ @dirs, keys %include_to_main ],
    filter => qr/\.scss$/,
);

say "\033[34mWatching for changes\033[0m";

while ( my @events = $watcher->wait_for_events() ) {
    for my $file (map { $_->path } @events) {
        verbose($file, "%s was updated");
        for my $inc (@{$include_to_main{$file}}) {
            my @includes = compile($inc, 1);
            # From CSS::Sass::Watchdog test, we see includes are sorted
            if (@includes && @{$includes{$inc}} && "@{$includes{$inc}}" ne "@includes") {
                say "\033[34mRestarting to update includes\033[0m";
                exec( $^X, $0, @ARGVorig ) or die "Can't re-exec myself($^X,$0): $!\n";
            }
        }
    }
}

# Given a SCSS file, compile it and generate a .map file,
# show an error if any, and return the list of includes.
sub compile {
    my ($file, $verbose) = @_;
    (my $output_file = $file) =~ s/scss$/css/;
    my $source_map_file = "$output_file.map";

    $sass->options->{source_map_file} = $source_map_file;
    my ($css, $stats) = $sass->compile_file($file);
    unless ($css) {
        warn "\033[31m" . $sass->last_error . "\033[0m";;
        return;
    }

    my $written = write_if_different($output_file, $css);
    if ($written) {
        verbose($file, "    \033[32mupdated\033[0m %s");
    } elsif ($verbose) {
        verbose($file, "  \033[33munchanged\033[0m %s");
    }
    write_if_different($source_map_file, $stats->{source_map_string});
    return @{$stats->{included_files} || []};
}

# Write a file, only if it has changed.
sub write_if_different {
    my ($fn, $data) = @_;
    my $current = File::Slurp::read_file($fn, { binmode => ':utf8', err_mode => 'quiet' });
    if (!$current || $current ne $data) {
        File::Slurp::write_file($fn, { binmode => ':utf8' }, $data);
        return 1;
    }
    return 0;
}

sub verbose {
    my ($file, $format) = @_;
    # Strip most of file path, keep dir/file
    (my $pr = $file) =~ s{.*/(.*/.*)\.scss}{$1};
    say sprintf $format, $pr;
}

__END__

=head1 NAME

make_css - Generate CSS files from SCSS files, watch for changes.

=head1 SYNOPSIS

make_css [options] [dirs ...]

 Options:
   --verbose        display more information
   --watch          wait for file updates and compile automatically
   --help           this help message

If no directories are specified, any .scss files under web/ that do not begin
with a "_" will be processed. "web/cobrands/" may be omitted from a given
directory.

=cut