Tiny script for preparing web assets for deployment
1#!/usr/bin/env node
2
3import fs from 'fs';
4import path from 'path';
5import child_process from 'child_process';
6import uglifyjs from "uglify-js";
7import * as sass from 'sass';
8import * as csso from 'csso';
9
10const spawn = child_process.spawn;
11const fsp = fs.promises;
12const STYLESDIR = 'styles';
13const SCRIPTSDIR = 'scripts';
14const IMAGESDIR = path.join('assets', 'images', 'original');
15const STYLEOUTDIR = process.env.STYLEOUTDIR || path.join(import.meta.dirname, 'assets', 'css');
16const SCRIPTSOUTDIR = process.env.SCRIPTSOUTDIR || path.join(import.meta.dirname, 'assets', 'js');
17const IMAGESOUTDIR = process.env.IMAGESOUTDIR || path.join(import.meta.dirname, 'assets', 'images', 'webp');
18const STYLEOUTFILE = process.env.STYLEOUTFILE || 'styles.css';
19const SQUASH = new RegExp('^[0-9]+-');
20
21async function emptyDir(dir: string) {
22 await Promise.all((await fsp.readdir(dir, { withFileTypes: true })).map(f => path.join(dir, f.name)).map(p => fsp.rm(p, {
23 recursive: true,
24 force: true
25 })));
26}
27
28async function mkdir(dir: string | string[]) {
29 if (typeof dir === 'string') {
30 await fsp.mkdir(dir, { recursive: true });
31 }
32 else {
33 await Promise.all(dir.map(mkdir));
34 }
35}
36
37// Process styles
38async function styles() {
39 await mkdir([STYLEOUTDIR, STYLESDIR]);
40 await emptyDir(STYLEOUTDIR);
41 let styles: string[] = [];
42 let files = await fsp.readdir(STYLESDIR);
43 await Promise.all(files.map(f => new Promise(async (res, reject) => {
44 let p = path.join(STYLESDIR, f);
45 console.log(`Processing style ${p}`);
46 let style = sass.compile(p).css;
47 if (f.charAt(0) !== '_') {
48 if (SQUASH.test(f)) {
49 styles.push(style);
50 }
51 else {
52 let o = path.join(STYLEOUTDIR, f.substring(0, f.lastIndexOf('.')) + '.css');
53 await fsp.writeFile(o, csso.minify(style).css);
54 console.log(`Wrote ${o}`);
55 }
56 }
57 res(0);
58 })));
59 let out = csso.minify(styles.join('\n')).css;
60 let outpath = path.join(STYLEOUTDIR, STYLEOUTFILE);
61 await fsp.writeFile(outpath, out);
62 console.log(`Wrote ${outpath}`);
63}
64
65// Process scripts
66async function scripts() {
67 await mkdir([SCRIPTSOUTDIR, SCRIPTSDIR]);
68 await emptyDir(SCRIPTSOUTDIR);
69 let files = await fsp.readdir(SCRIPTSDIR);
70 await Promise.all(files.map(f => new Promise(async (res, reject) => {
71 let p = path.join(SCRIPTSDIR, f);
72 let o = path.join(SCRIPTSOUTDIR, f);
73 console.log(`Processing script ${p}`);
74 try {
75 await fsp.writeFile(o, uglifyjs.minify((await fsp.readFile(p)).toString()).code);
76 console.log(`Wrote ${o}`);
77 }
78 catch (ex) {
79 console.log(`error writing ${o}: ${ex}`);
80 }
81 res(0);
82 })));
83}
84
85// Process images
86async function images(dir = '') {
87 let p = path.join(IMAGESDIR, dir);
88 await mkdir(p);
89 if (dir.length === 0) {
90 await mkdir(IMAGESOUTDIR)
91 await emptyDir(IMAGESOUTDIR);
92 }
93 let files = await fsp.readdir(p, {
94 withFileTypes: true
95 });
96 if (files.length) {
97 await Promise.all(files.map(f => new Promise(async (res, reject) => {
98 if (f.isFile()) {
99 let outDir = path.join(IMAGESOUTDIR, dir);
100 let infile = path.join(p, f.name);
101 let outfile = path.join(outDir, f.name.substring(0, f.name.lastIndexOf('.')) + '.webp');
102 await mkdir(outDir);
103 console.log(`Processing image ${infile}`)
104 let process = spawn('cwebp', ['-mt', '-q', '50', infile, '-o', outfile]);
105 let timeout = setTimeout(() => {
106 reject('Timed out');
107 process.kill();
108 }, 30000);
109 process.on('exit', async (code) => {
110 clearTimeout(timeout);
111 if (code === 0) {
112 console.log(`Wrote ${outfile}`);
113 res(null);
114 }
115 else {
116 reject(code);
117 }
118 });
119 }
120 else if (f.isDirectory()) {
121 images(path.join(dir, f.name)).then(res).catch(reject);
122 }
123 })));
124 }
125}
126
127function isAbortError(err: unknown): boolean {
128 return typeof err === 'object' && err !== null && 'name' in err && err.name === 'AbortError';
129}
130
131(async function () {
132 await Promise.all([styles(), scripts(), images()]);
133 if (process.argv.indexOf('--watch') >= 0) {
134 console.log('watching for changes...');
135 (async () => {
136 try {
137 const watcher = fsp.watch(STYLESDIR);
138 for await (const _ of watcher)
139 await styles();
140 } catch (err) {
141 if (isAbortError(err))
142 return;
143 throw err;
144 }
145 })();
146
147 (async () => {
148 try {
149 const watcher = fsp.watch(SCRIPTSDIR);
150 for await (const _ of watcher)
151 await scripts();
152 } catch (err) {
153 if (isAbortError(err))
154 return;
155 throw err;
156 }
157 })();
158
159 (async () => {
160 try {
161 const watcher = fsp.watch(IMAGESDIR, {
162 recursive: true // no Linux ☹️
163 });
164 for await (const _ of watcher)
165 await images();
166 } catch (err) {
167 if (isAbortError(err))
168 return;
169 throw err;
170 }
171 })();
172 }
173})();