Tiny script for preparing web assets for deployment
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

More flexibility with combining styles

+120 -40
+1
.npmignore
··· 1 + *.js.map
+23
.vscode/launch.json
··· 1 + { 2 + // Use IntelliSense to learn about possible attributes. 3 + // Hover to view descriptions of existing attributes. 4 + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 + "version": "0.2.0", 6 + "configurations": [ 7 + { 8 + "type": "node", 9 + "request": "launch", 10 + "name": "Launch Program", 11 + "skipFiles": [ 12 + "<node_internals>/**" 13 + ], 14 + "program": "${workspaceFolder}/distribution/src/build-shit.js", 15 + "preLaunchTask": "tsc: build - tsconfig.json", 16 + "sourceMaps": true, 17 + "outFiles": [ 18 + "${workspaceFolder}/distribution/**/*.js" 19 + ], 20 + "cwd": "${workspaceFolder}" 21 + } 22 + ] 23 + }
+32 -11
src/build-shit.ts
··· 7 7 8 8 const STYLESDIR = 'styles'; 9 9 const SCRIPTSDIR = 'scripts'; 10 - const IMAGESDIR = path.join(process.cwd(), 'assets', 'images', 'original'); 10 + const IMAGESDIR = path.join('assets', 'images', 'original'); 11 11 const STYLEOUTDIR = process.env['STYLEOUTDIR'] || path.join('assets', 'css'); 12 12 const SCRIPTSOUTDIR = process.env['SCRIPTSOUTDIR'] || path.join('assets', 'js'); 13 13 const WEBPOUTDIR = process.env['IMAGESOUTDIR'] || path.join('assets', 'images', 'webp'); 14 14 const AVIFOUTDIR = process.env['IMAGESOUTDIR'] || path.join('assets', 'images', 'avif'); 15 - const STYLEOUTFILE = process.env['STYLEOUTFILE'] || 'styles.css'; 15 + const STYLEOUTFILE = process.env['STYLEOUTFILE'] || 'styles'; 16 16 17 17 function commandExists(cmd: string): Promise<boolean> { 18 18 return new Promise((resolve, _) => { ··· 56 56 (async () => { 57 57 try { 58 58 const watcher = fsp.watch(STYLESDIR); 59 - for await (const _ of watcher) 60 - await doStyles(); 61 - } catch (err) { 59 + for await (const _ of watcher) { 60 + try { 61 + await doStyles(); 62 + } 63 + catch (err) { 64 + console.error(err); 65 + } 66 + } 67 + } 68 + catch (err) { 62 69 if (isAbortError(err)) 63 70 return; 64 71 throw err; ··· 68 75 (async () => { 69 76 try { 70 77 const watcher = fsp.watch(SCRIPTSDIR); 71 - for await (const _ of watcher) 72 - await doScripts(); 73 - } catch (err) { 78 + for await (const _ of watcher) { 79 + try { 80 + await doScripts(); 81 + } 82 + catch (err) { 83 + console.error(err); 84 + } 85 + } 86 + } 87 + catch (err) { 74 88 if (isAbortError(err)) 75 89 return; 76 90 throw err; ··· 82 96 const watcher = fsp.watch(IMAGESDIR, { 83 97 recursive: true // no Linux ☹️ 84 98 }); 85 - for await (const _ of watcher) 86 - await doImages(); 87 - } catch (err) { 99 + for await (const _ of watcher) { 100 + try { 101 + await doImages(); 102 + } 103 + catch (err) { 104 + console.error(err); 105 + } 106 + } 107 + } 108 + catch (err) { 88 109 if (isAbortError(err)) 89 110 return; 90 111 throw err;
+63 -29
src/index.ts
··· 18 18 input: string; 19 19 } 20 20 21 + type StyleFilesMapping = Record<string, string[]>; 22 + 21 23 async function emptyDir(dir: string) { 22 24 await Promise.all((await fsp.readdir(dir, { withFileTypes: true })).map(f => path.join(dir, f.name)).map(p => fsp.rm(p, { 23 25 recursive: true, ··· 107 109 }); 108 110 } 109 111 110 - // Process images 112 + /** 113 + * Convert images to optimized webp and/or avif files 114 + * @param options options for processing images 115 + */ 111 116 export async function images(options: BatchImageProcessingOptions) { 112 117 await mkdir(options.input); 113 118 await mkdir(options.webpOut) && await emptyDir(options.webpOut); ··· 118 123 } 119 124 } 120 125 121 - export async function processImage(parentDir: string, relativeFile: string, options: ImageProcessingOptions) { 126 + async function processImage(parentDir: string, relativeFile: string, options: ImageProcessingOptions) { 122 127 const infile = path.join(parentDir, relativeFile); 123 128 const dir = path.dirname(relativeFile); 124 129 const outDirWebP = path.join(options.webpOut, dir); ··· 130 135 options.avif && await convertAvif(infile, outDirAvif); 131 136 } 132 137 133 - // Process styles 134 - export async function styles(inputDir: string, outputDir: string, outputFile: string) { 138 + /** 139 + * Process styles. 140 + * Sass is "compiled" and numbered files (^[0-9]+-) are combined into one. 141 + * Use subdirectories to group files that should be combined. 142 + * @param inputDir parent directory of styles 143 + * @param outputDir directory to put processed stylesheets 144 + * @param outputFile filename for combined styles that are a direct child of inputDir 145 + */ 146 + export async function styles(inputDir: string, outputDir: string, outputFile: string = 'styles') { 135 147 await mkdir([outputDir, inputDir]); 136 148 await emptyDir(outputDir); 137 - const styles: string[] = []; 138 - const files = await fsp.readdir(inputDir); 139 - await Promise.all(files.map(f => new Promise(async (res, _) => { 140 - const p = path.join(inputDir, f); 141 - console.log(`Processing style ${p}`); 142 - const style = sass.compile(p).css; 143 - if (f.charAt(0) !== '_') { 144 - if (SQUASH.test(f)) { 145 - styles.push(style); 146 - } 147 - else { 148 - const o = path.join(outputDir, f.substring(0, f.lastIndexOf('.')) + '.css'); 149 - await fsp.writeFile(o, csso.minify(style).css); 150 - console.log(`Wrote ${o}`); 151 - } 152 - } 153 - res(0); 154 - }))); 155 - const out = csso.minify(styles.join('\n')).css; 156 - const outpath = path.join(outputDir, outputFile); 157 - await fsp.writeFile(outpath, out); 158 - console.log(`Wrote ${outpath}`); 149 + const map = await generateStyleGameplan(inputDir, outputFile); 150 + const promises: Promise<any>[] = []; 151 + for (let key in map) { 152 + const files = map[key]!; 153 + const squashable: string[] = []; 154 + const standalonable: string[] = []; 155 + files.forEach(f => { 156 + (SQUASH.test(path.basename(f)) ? squashable : standalonable).push(f); 157 + }); 158 + promises.push(style(squashable, path.join(outputDir, `${key}.css`))); 159 + const standalonePromises = standalonable.map(async f => { 160 + const out = path.join(outputDir, `${f.substring(inputDir.length, f.lastIndexOf('.'))}.css`); 161 + await mkdir(path.dirname(out)); 162 + await style([f], out); 163 + }); 164 + promises.push(...standalonePromises); 165 + } 166 + await Promise.all(promises); 159 167 } 160 168 161 - // Process scripts 169 + async function style(filenames: string[], output: string): Promise<void> { 170 + const styles: string[] = filenames.map(f => { 171 + console.log(`Processing style ${f}`); 172 + return sass.compile(f).css; 173 + }); 174 + await fsp.writeFile(output, csso.minify(styles.join('\n')).css); 175 + console.log(`Wrote ${output}`); 176 + } 177 + 178 + /** 179 + * Minify JS files 180 + * @param inputDir directory containing js to be minified 181 + * @param outputDir directory to place minified js 182 + */ 162 183 export async function scripts(inputDir: string, outputDir: string) { 163 184 await mkdir([outputDir, inputDir]); 164 185 await emptyDir(outputDir); ··· 178 199 }))); 179 200 } 180 201 181 - async function getAllFiles(fullDir: string): Promise<string[]> { 202 + async function generateStyleGameplan(fullDir: string, rootName: string): Promise<StyleFilesMapping> { 203 + const map: StyleFilesMapping = {}; 204 + (await getAllFiles(fullDir)).forEach(f => { 205 + if (path.basename(f).charAt(0) === '_') { 206 + return; 207 + } 208 + const dirname = path.dirname(f); 209 + (map[dirname === '.' ? rootName : dirname] ||= []).push(path.join(fullDir, f)); 210 + }); 211 + return map; 212 + } 213 + 214 + async function getAllFiles(dir: string): Promise<string[]> { 215 + const fullDir = path.resolve(dir); 182 216 if (!path.isAbsolute(fullDir)) { 183 - throw new Error('path must be absolute'); 217 + throw new Error(`path must be absolute. got ${fullDir}`); 184 218 } 185 219 const files: string[] = []; 186 220 const dirs = [''];
+1
tsconfig.json
··· 5 5 "esModuleInterop": true, 6 6 "rootDir": "./", 7 7 "types": ["node"], 8 + "sourceMap": true 8 9 }, 9 10 "include": [ 10 11 "src"