Tiny script for preparing web assets for deployment
at v1.0.0 6.2 kB view raw
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('assets', 'css'); 16const SCRIPTSOUTDIR = process.env.SCRIPTSOUTDIR || path.join('assets', 'js'); 17const IMAGESOUTDIR = process.env.IMAGESOUTDIR || path.join('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 37function getFileExtension(filename: string) { 38 const split = filename.split('.'); 39 return split[split.length - 1].toLowerCase(); 40} 41 42// Process styles 43async function styles() { 44 await mkdir([STYLEOUTDIR, STYLESDIR]); 45 await emptyDir(STYLEOUTDIR); 46 const styles: string[] = []; 47 const files = await fsp.readdir(STYLESDIR); 48 await Promise.all(files.map(f => new Promise(async (res, reject) => { 49 const p = path.join(STYLESDIR, f); 50 console.log(`Processing style ${p}`); 51 const style = sass.compile(p).css; 52 if (f.charAt(0) !== '_') { 53 if (SQUASH.test(f)) { 54 styles.push(style); 55 } 56 else { 57 const o = path.join(STYLEOUTDIR, f.substring(0, f.lastIndexOf('.')) + '.css'); 58 await fsp.writeFile(o, csso.minify(style).css); 59 console.log(`Wrote ${o}`); 60 } 61 } 62 res(0); 63 }))); 64 const out = csso.minify(styles.join('\n')).css; 65 const outpath = path.join(STYLEOUTDIR, STYLEOUTFILE); 66 await fsp.writeFile(outpath, out); 67 console.log(`Wrote ${outpath}`); 68} 69 70// Process scripts 71async function scripts() { 72 await mkdir([SCRIPTSOUTDIR, SCRIPTSDIR]); 73 await emptyDir(SCRIPTSOUTDIR); 74 const files = await fsp.readdir(SCRIPTSDIR); 75 await Promise.all(files.map(f => new Promise(async (res, reject) => { 76 const p = path.join(SCRIPTSDIR, f); 77 const o = path.join(SCRIPTSOUTDIR, f); 78 console.log(`Processing script ${p}`); 79 try { 80 await fsp.writeFile(o, uglifyjs.minify((await fsp.readFile(p)).toString()).code); 81 console.log(`Wrote ${o}`); 82 } 83 catch (ex) { 84 console.log(`error writing ${o}: ${ex}`); 85 } 86 res(0); 87 }))); 88} 89 90// Process images 91async function images(dir = '') { 92 const p = path.join(IMAGESDIR, dir); 93 await mkdir(p); 94 if (dir.length === 0) { 95 await mkdir(IMAGESOUTDIR) 96 await emptyDir(IMAGESOUTDIR); 97 } 98 const files = await fsp.readdir(p, { 99 withFileTypes: true 100 }); 101 if (files.length) { 102 await Promise.all(files.map(f => new Promise(async (res, reject) => { 103 if (f.isFile()) { 104 const outDir = path.join(IMAGESOUTDIR, dir); 105 const infile = path.join(p, f.name); 106 const extension = getFileExtension(infile); 107 const outfile = path.join(outDir, f.name.substring(0, f.name.lastIndexOf('.')) + '.webp'); 108 await mkdir(outDir); 109 console.log(`Processing image ${infile}`) 110 const libwebpArgs = ['-mt']; 111 if (extension === 'jpeg' || extension === 'jpg') { 112 libwebpArgs.push('-q', '60'); 113 } 114 else { 115 libwebpArgs.push('-near_lossless', '55'); 116 } 117 libwebpArgs.push(infile, '-o', outfile); 118 const proc = spawn('cwebp', libwebpArgs); 119 const timeout = setTimeout(() => { 120 reject('Timed out'); 121 proc.kill(); 122 }, parseInt(process.env['CWEBPTIMEOUT']) || 30000); 123 proc.on('exit', async (code) => { 124 clearTimeout(timeout); 125 if (code === 0) { 126 console.log(`Wrote ${outfile}`); 127 res(null); 128 } 129 else { 130 reject(code); 131 } 132 }); 133 } 134 else if (f.isDirectory()) { 135 images(path.join(dir, f.name)).then(res).catch(reject); 136 } 137 }))); 138 } 139} 140 141function isAbortError(err: unknown): boolean { 142 return typeof err === 'object' && err !== null && 'name' in err && err.name === 'AbortError'; 143} 144 145(async function () { 146 await Promise.all([styles(), scripts(), images()]); 147 if (process.argv.indexOf('--watch') >= 0) { 148 console.log('watching for changes...'); 149 (async () => { 150 try { 151 const watcher = fsp.watch(STYLESDIR); 152 for await (const _ of watcher) 153 await styles(); 154 } catch (err) { 155 if (isAbortError(err)) 156 return; 157 throw err; 158 } 159 })(); 160 161 (async () => { 162 try { 163 const watcher = fsp.watch(SCRIPTSDIR); 164 for await (const _ of watcher) 165 await scripts(); 166 } catch (err) { 167 if (isAbortError(err)) 168 return; 169 throw err; 170 } 171 })(); 172 173 (async () => { 174 try { 175 const watcher = fsp.watch(IMAGESDIR, { 176 recursive: true // no Linux ☹️ 177 }); 178 for await (const _ of watcher) 179 await images(); 180 } catch (err) { 181 if (isAbortError(err)) 182 return; 183 throw err; 184 } 185 })(); 186 } 187})();