dev-make 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. #!/usr/bin/perl
  2. use local::lib;
  3. use v5.10;
  4. use strict;
  5. use warnings;
  6. use FindBin qw( $RealBin );
  7. use lib "$RealBin/lib";
  8. use File::Basename qw( basename );
  9. use File::Copy qw( copy );
  10. use File::Find qw( find );
  11. use List::Util qw( any );
  12. use LogBot::Util qw( file_time run slurp spurt touch );
  13. use Mojo::Util qw( getopt );
  14. $| = 1;
  15. my %args;
  16. getopt(\%args, 'rebuild|B|b', 'quiet|q', 'debug|d', 'help|h|?');
  17. $args{debug} ||= $ENV{DEBUG};
  18. $args{deps} = shift // '' eq 'deps';
  19. $args{help} && die <<'EOF';
  20. dev-make [options] ['deps']
  21. build web assets for logbot.
  22. options:
  23. --rebuild|-B|-b : force rebuild
  24. --quiet|-q : no output
  25. --debug|-d : build debug assets (non-compressed)
  26. 'deps' : show build dependencies
  27. EOF
  28. #<<<
  29. my $def = {
  30. 'web/public/static/logbot.min.js' => [
  31. { 'web/build/jquery.min.js' => { js => 'web/jquery/jquery-3.2.1.min.js' } },
  32. { 'web/build/pikaday.min.js' => { js => 'web/pikaday/pikaday.js' } },
  33. { 'web/build/chosen.min.js' => { js => 'web/chosen/chosen.jquery.js' } },
  34. { 'web/build/flot.min.js' => { js => 'web/flot/jquery.flot.js' } },
  35. { 'web/build/css-esc.min.js' => { js => 'web/css-escape/css.escape.js' } },
  36. { 'web/build/logbot.min.js' => { js => 'web/logbot.js' } },
  37. ],
  38. 'web/public/static/logbot.min.css' => [
  39. { 'web/build/pikaday.min.css' => { css => 'web/pikaday/pikaday.scss' } },
  40. { 'web/build/hind.min.css' => { css => 'web/hind/hind.sass' } },
  41. { 'web/build/chosen.min.css' => { css => 'web/chosen/chosen.css' } },
  42. { 'web/build/logbot.min.css' => { lb_css => [qw( web/logbot.sass web/_variables.sass )] } },
  43. ],
  44. _svg => [
  45. qw(
  46. web/svg/*.svg
  47. web/svg/font-awesome/*
  48. web/templates/*.html.ep
  49. web/templates/layouts/*.html.ep
  50. web/templates/shared/*.html.ep
  51. )
  52. ],
  53. _cp => [
  54. qw(
  55. web/chosen/chosen-sprite.png
  56. web/chosen/chosen-sprite@2x.png
  57. web/hind/hind-bold.ttf
  58. web/hind/hind-medium.ttf
  59. web/hind/hind-regular.ttf
  60. web/svg/logbot-favicon.svg
  61. )
  62. ],
  63. };
  64. #>>>
  65. mkdir("$RealBin/web/build");
  66. # switching debug mode on/off forces a rebuild
  67. my $last_mode_file = "$RealBin/web/build/last_mode";
  68. my $last_mode = -e $last_mode_file ? slurp($last_mode_file) : '';
  69. my $this_mode = $args{debug} ? 'debug' : 'production';
  70. if ($this_mode ne $last_mode) {
  71. $args{rebuild} = 1;
  72. l(undef, "$this_mode assets");
  73. }
  74. my @deps;
  75. foreach my $target (sort keys %{$def}) {
  76. if ($target eq '_cp') {
  77. # copy file
  78. foreach my $source_file (@{ $def->{_cp} }) {
  79. $source_file = "$RealBin/$source_file";
  80. (push(@deps, $source_file), next) if $args{deps};
  81. cp($source_file, "$RealBin/web/public/static/" . basename($source_file));
  82. }
  83. } elsif ($target eq '_svg') {
  84. # if any svg or templates are changed, inline svg
  85. my $target_file = "$RealBin/web/build/inline-svg.updated";
  86. foreach my $source_file (expand($def->{_svg})) {
  87. (push(@deps, $source_file), next) if $args{deps};
  88. next unless update($source_file, $target_file);
  89. inline_svg();
  90. touch($target_file);
  91. last;
  92. }
  93. } else {
  94. # concatenated targets
  95. my $target_file = "$RealBin/$target";
  96. my @content;
  97. my $dirty = 0;
  98. foreach my $sub (@{ $def->{$target} }) {
  99. my $sub_target = (keys %{$sub})[0];
  100. my $sub_target_file = "$RealBin/$sub_target";
  101. my $action = (keys %{ $sub->{$sub_target} })[0];
  102. my $action_source = $sub->{$sub_target}->{$action};
  103. if ($action eq 'css') {
  104. my $sub_source_file = "$RealBin/$action_source";
  105. (push(@deps, $sub_source_file), next) if $args{deps};
  106. css($sub_source_file, $sub_target_file) && ($dirty = 1);
  107. } elsif ($action eq 'lb_css') {
  108. foreach my $sub_source_file (expand($action_source)) {
  109. (push(@deps, $sub_source_file), next) if $args{deps};
  110. next unless update($sub_source_file, $sub_target_file);
  111. logbot_css("$RealBin/web/build/logbot.full.css");
  112. css("$RealBin/web/build/logbot.full.css", $sub_target_file);
  113. $dirty = 1;
  114. last;
  115. }
  116. } elsif ($action eq 'js') {
  117. my $sub_source_file = "$RealBin/$action_source";
  118. (push(@deps, $sub_source_file), next) if $args{deps};
  119. js($sub_source_file, $sub_target_file) && ($dirty = 1);
  120. } else {
  121. die $action;
  122. }
  123. push @content, slurp($sub_target_file) unless $args{deps};
  124. }
  125. if ($dirty && !$args{deps}) {
  126. l(undef, $target_file);
  127. spurt($target_file, join('', @content));
  128. }
  129. }
  130. }
  131. (say(join("\n", @deps)), exit) if $args{deps};
  132. spurt($last_mode_file, $this_mode);
  133. sub cp {
  134. my ($source_file, $target_file) = @_;
  135. return unless update($source_file, $target_file);
  136. l($source_file, $target_file);
  137. copy($source_file, $target_file) || die "$!\n";
  138. return 1;
  139. }
  140. sub css {
  141. my ($source_file, $target_file) = @_;
  142. return unless update($source_file, $target_file);
  143. l($source_file, $target_file);
  144. if ($args{debug}) {
  145. run('sass', '--style', 'expanded', $source_file, $target_file);
  146. } else {
  147. run('sass', '--style', 'compressed', $source_file, $target_file);
  148. }
  149. my $css = slurp($target_file);
  150. $css =~ s{/\*.*?\*/}{}gs;
  151. spurt($target_file, $css);
  152. return 1;
  153. }
  154. sub js {
  155. my ($source_file, $target_file) = @_;
  156. return unless update($source_file, $target_file);
  157. l($source_file, $target_file);
  158. if ($args{debug}) {
  159. run('uglifyjs', $source_file, '--beautify', '--output', $target_file);
  160. } else {
  161. run('uglifyjs', $source_file, '--compress', '--mangle', '--output', $target_file);
  162. }
  163. return 1;
  164. }
  165. sub inline_svg {
  166. # inlines svg images into templates
  167. # to use, create an svg element with a class name "svg-filename"
  168. # the filename will be loaded from web/svg and replace the svg element
  169. # only the svg-filename class will be carried over into the inline svg element,
  170. # other classes and ids will be lost
  171. # eg. <svg class="svg-sidebar-collapse"></svg>
  172. my @files;
  173. find(
  174. sub {
  175. my $file = $File::Find::name;
  176. return unless -f $file && -s $file;
  177. return unless $file =~ /\.html\.ep$/;
  178. push @files, $file;
  179. },
  180. "$RealBin/web/templates"
  181. );
  182. foreach my $file (sort @files) {
  183. my $orig = slurp($file);
  184. (my $tmpl = $orig) =~ s{
  185. <svg\sclass="svg-([^"]+)".+?</svg>
  186. }{
  187. load_svg($1)
  188. }gxe;
  189. next if $orig eq $tmpl;
  190. spurt($file, $tmpl);
  191. $file =~ s/^\Q$RealBin//;
  192. l(undef, $file);
  193. }
  194. }
  195. sub logbot_css {
  196. my ($target_file) = @_;
  197. # build css that contains light and dark modes
  198. # the combined sass looks like:
  199. # variables
  200. # body
  201. # logbot.sass unmodified
  202. # body.dark
  203. # logbot.sass with colour variables prefixed by `night-`
  204. #
  205. # to avoid duplicate styles in the body.dark section, non-colour styles are
  206. # then commented out. the sass processor will remove these comments from
  207. # the final minified css.
  208. my @variables = split(/\n/, slurp("$RealBin/web/_variables.sass"));
  209. my @sass = split(/\n/, slurp("$RealBin/web/logbot.sass"));
  210. # remove @import from main sass as we'll be inlining here
  211. @sass = grep { !/^\@import\s+variables/ } @sass;
  212. # indent sass
  213. my @top_level;
  214. my $is_top_level = 0;
  215. foreach my $line (@sass) {
  216. # don't touch top-level :: selectors
  217. $is_top_level = 1 if $line =~ /^::/;
  218. if ($is_top_level) {
  219. $is_top_level = 0 if $line eq '';
  220. push @top_level, $line;
  221. $line = '';
  222. next;
  223. }
  224. # existing body definitions need special treatment
  225. if ($line eq 'body') {
  226. $line = '&.root';
  227. }
  228. $line = ' ' . $line;
  229. }
  230. # build duplicate styles, light and dark
  231. my @combined;
  232. push @combined, @variables;
  233. push @combined, '';
  234. push @combined, @top_level;
  235. push @combined, 'body';
  236. push @combined, @sass;
  237. push @combined, 'body.dark';
  238. foreach my $line (@sass) {
  239. $line =~ s/\$(.+?-colour)/\$night-$1/g;
  240. push @combined, $line;
  241. }
  242. # convert to css (easier to parse)
  243. open(my $sass_cmd, '|-', 'sass', '--stdin', '--indented', 'web/build/logbot.combined.css') or die $!;
  244. print $sass_cmd join("\n", @combined);
  245. close($sass_cmd) or die $!;
  246. # commend out body.dark selectors that don't involve colour
  247. my @css = split(/\n/, slurp("$RealBin/web/build/logbot.combined.css"));
  248. my $selector = '';
  249. foreach my $line (@css) {
  250. next if $line =~ /^\s*}/;
  251. if ($line =~ /{$/) {
  252. ($selector = $line) =~ s/^\s+//;
  253. } else {
  254. next unless $selector =~ /^body\.dark\b/;
  255. next
  256. if $line =~ /#[a-z0-9]{3,6}/
  257. || $line =~ /\brgba?\(/
  258. || $line =~ /^\s*filter:/;
  259. $line = "/* $line */";
  260. }
  261. }
  262. spurt($target_file, join("\n", @css));
  263. }
  264. sub update {
  265. my ($source_file, $target_file) = @_;
  266. return 1 if $args{rebuild};
  267. return 1 if !-e $target_file;
  268. return file_time($source_file) > file_time($target_file);
  269. }
  270. sub expand {
  271. my ($ra_spec) = @_;
  272. my @files;
  273. foreach my $spec (@{$ra_spec}) {
  274. push @files, glob("'$RealBin/$spec'");
  275. }
  276. return @files;
  277. }
  278. sub l {
  279. my ($source_file, $target_file) = @_;
  280. return if $args{quiet};
  281. $source_file =~ s{^\Q$RealBin/}{} if $source_file;
  282. $target_file =~ s{^\Q$RealBin/}{};
  283. say $source_file ? "$source_file → $target_file" : "→ $target_file";
  284. }
  285. sub load_svg {
  286. my ($name) = @_;
  287. my $file =
  288. -e "$RealBin/web/svg/$name.svg" ? "$RealBin/web/svg/$name.svg" : "$RealBin/web/svg/font-awesome/$name.svg";
  289. die "failed to find $name.svg\n" unless -e $file;
  290. my $svg = slurp($file);
  291. $svg =~ s/^<svg /<svg class="svg-$name" /;
  292. $svg =~ s/\s+$//;
  293. return $svg;
  294. }