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})();