+25
.tangled/workflows/npm_publish.yml
+25
.tangled/workflows/npm_publish.yml
···
1
+
when:
2
+
- event: ["push"]
3
+
branch: ["master"]
4
+
5
+
engine: "nixery"
6
+
7
+
dependencies:
8
+
nixpkgs:
9
+
- nodejs
10
+
- gnugrep
11
+
12
+
steps:
13
+
- name: "Install dependencies"
14
+
command: "npm install"
15
+
16
+
- name: "tsc"
17
+
command: "npx tsc && echo 'done.'"
18
+
19
+
- name: "npm publish"
20
+
command: "git log -1 --pretty=%B | grep -q '^publish new version' && npm set //registry.npmjs.org/:_authToken=${NPM_TOKEN} && npm publish || exit 0"
21
+
22
+
clone:
23
+
skip: false
24
+
depth: 3
25
+
submodules: false
+3
-3
README.md
+3
-3
README.md
···
1
1
# build-shit
2
2
3
-
[](https://github.com/CorySanin/build-shit/blob/master/LICENSE)
3
+
[](https://tangled.sh/@sanin.dev/build-shit/blob/master/LICENSE)
4
4
[](https://www.npmjs.com/package/forking-build-shit)
5
5
[](https://www.npmjs.com/package/forking-build-shit)
6
6
7
-
A build script for preparing files for production. It handles CSS and JavaScript minification, Sass compilation, and image conversion to WebP.
7
+
A build script for preparing files for production. It handles CSS and JavaScript minification, Sass compilation, and image conversion to WebP and AVIF.
8
8
9
-
WebP conversion requires libwebp.
9
+
WebP conversion requires libwebp. AVIF conversion requires avifenc.
10
10
11
11
## Installation
12
12
+25
-24
package-lock.json
+25
-24
package-lock.json
···
1
1
{
2
2
"name": "forking-build-shit",
3
-
"version": "1.0.0",
3
+
"version": "1.0.5",
4
4
"lockfileVersion": 3,
5
5
"requires": true,
6
6
"packages": {
7
7
"": {
8
8
"name": "forking-build-shit",
9
-
"version": "1.0.0",
9
+
"version": "1.0.5",
10
10
"license": "MIT",
11
11
"dependencies": {
12
12
"csso": "5.0.5",
13
-
"sass": "1.89.1",
13
+
"sass": "1.93.2",
14
14
"uglify-js": "3.19.3"
15
15
},
16
16
"bin": {
17
-
"build-shit": "bin/build-shit.js"
17
+
"build-shit": "bin/build-shit.js",
18
+
"forking-build-shit": "bin/build-shit.js"
18
19
},
19
20
"devDependencies": {
20
-
"@sindresorhus/tsconfig": "7.0.0",
21
+
"@sindresorhus/tsconfig": "8.0.1",
21
22
"@types/csso": "^5.0.4",
22
-
"@types/node": "^22.15.30",
23
+
"@types/node": "^24.7.2",
23
24
"@types/uglify-js": "^3.17.5",
24
-
"typescript": "5.8.3"
25
+
"typescript": "5.9.3"
25
26
}
26
27
},
27
28
"node_modules/@parcel/watcher": {
···
321
322
}
322
323
},
323
324
"node_modules/@sindresorhus/tsconfig": {
324
-
"version": "7.0.0",
325
-
"resolved": "https://registry.npmjs.org/@sindresorhus/tsconfig/-/tsconfig-7.0.0.tgz",
326
-
"integrity": "sha512-i5K04hLAP44Af16zmDjG07E1NHuDgCM07SJAT4gY0LZSRrWYzwt4qkLem6TIbIVh0k51RkN2bF+lP+lM5eC9fw==",
325
+
"version": "8.0.1",
326
+
"resolved": "https://registry.npmjs.org/@sindresorhus/tsconfig/-/tsconfig-8.0.1.tgz",
327
+
"integrity": "sha512-EcJpJuPR+Ot2DGJwQNRMVrWMxiYluGEQrgHeFHvKkKJcHCL/J3fmAKtN5WmAHIN7oxtwSEvNfjJgwffmxKBw9Q==",
327
328
"dev": true,
328
329
"license": "MIT",
329
330
"engines": {
330
-
"node": ">=18"
331
+
"node": ">=20"
331
332
},
332
333
"funding": {
333
334
"url": "https://github.com/sponsors/sindresorhus"
···
351
352
}
352
353
},
353
354
"node_modules/@types/node": {
354
-
"version": "22.15.30",
355
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
356
-
"integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==",
355
+
"version": "24.7.2",
356
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
357
+
"integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
357
358
"dev": true,
358
359
"license": "MIT",
359
360
"dependencies": {
360
-
"undici-types": "~6.21.0"
361
+
"undici-types": "~7.14.0"
361
362
}
362
363
},
363
364
"node_modules/@types/uglify-js": {
···
544
545
}
545
546
},
546
547
"node_modules/sass": {
547
-
"version": "1.89.1",
548
-
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.1.tgz",
549
-
"integrity": "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==",
548
+
"version": "1.93.2",
549
+
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
550
+
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
550
551
"license": "MIT",
551
552
"dependencies": {
552
553
"chokidar": "^4.0.0",
···
596
597
}
597
598
},
598
599
"node_modules/typescript": {
599
-
"version": "5.8.3",
600
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
601
-
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
600
+
"version": "5.9.3",
601
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
602
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
602
603
"dev": true,
603
604
"license": "Apache-2.0",
604
605
"bin": {
···
622
623
}
623
624
},
624
625
"node_modules/undici-types": {
625
-
"version": "6.21.0",
626
-
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
627
-
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
626
+
"version": "7.14.0",
627
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
628
+
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
628
629
"dev": true,
629
630
"license": "MIT"
630
631
}
+11
-10
package.json
+11
-10
package.json
···
1
1
{
2
2
"name": "forking-build-shit",
3
-
"version": "1.0.0",
3
+
"version": "1.0.5",
4
4
"description": "Tiny script for preparing web assets for deployment",
5
-
"homepage": "https://github.com/CorySanin/build-shit#readme",
5
+
"homepage": "https://tangled.org/@sanin.dev/build-shit#readme",
6
6
"bugs": {
7
-
"url": "https://github.com/CorySanin/build-shit/issues"
7
+
"url": "https://tangled.org/@sanin.dev/build-shit/issues"
8
8
},
9
9
"dependencies": {
10
10
"csso": "5.0.5",
11
-
"sass": "1.89.1",
11
+
"sass": "1.93.2",
12
12
"uglify-js": "3.19.3"
13
13
},
14
14
"devDependencies": {
15
-
"@sindresorhus/tsconfig": "7.0.0",
15
+
"@sindresorhus/tsconfig": "8.0.1",
16
16
"@types/csso": "^5.0.4",
17
-
"@types/node": "^22.15.30",
17
+
"@types/node": "^24.7.2",
18
18
"@types/uglify-js": "^3.17.5",
19
-
"typescript": "5.8.3"
19
+
"typescript": "5.9.3"
20
20
},
21
21
"repository": {
22
22
"type": "git",
23
-
"url": "git+https://github.com/CorySanin/build-shit.git"
23
+
"url": "git+https://tangled.org/@sanin.dev/build-shit"
24
24
},
25
25
"license": "MIT",
26
26
"author": {
27
27
"name": "Cory Sanin",
28
-
"email": "corysanin@artixlinux.org",
28
+
"email": "corysanin@outlook.com",
29
29
"url": "https://sanin.dev"
30
30
},
31
31
"type": "module",
32
32
"main": "bin/build-shit.js",
33
33
"bin": {
34
-
"build-shit": "./bin/build-shit.js"
34
+
"build-shit": "./bin/build-shit.js",
35
+
"forking-build-shit": "./bin/build-shit.js"
35
36
},
36
37
"scripts": {
37
38
"build": "tsc"
+114
-49
src/build-shit.ts
+114
-49
src/build-shit.ts
···
11
11
const fsp = fs.promises;
12
12
const STYLESDIR = 'styles';
13
13
const SCRIPTSDIR = 'scripts';
14
-
const IMAGESDIR = path.join('assets', 'images', 'original');
14
+
const IMAGESDIR = path.join(process.cwd(), 'assets', 'images', 'original');
15
15
const STYLEOUTDIR = process.env.STYLEOUTDIR || path.join('assets', 'css');
16
16
const SCRIPTSOUTDIR = process.env.SCRIPTSOUTDIR || path.join('assets', 'js');
17
-
const IMAGESOUTDIR = process.env.IMAGESOUTDIR || path.join('assets', 'images', 'webp');
17
+
const WEBPOUTDIR = process.env.IMAGESOUTDIR || path.join('assets', 'images', 'webp');
18
+
const AVIFOUTDIR = process.env.IMAGESOUTDIR || path.join('assets', 'images', 'avif');
18
19
const STYLEOUTFILE = process.env.STYLEOUTFILE || 'styles.css';
19
20
const SQUASH = new RegExp('^[0-9]+-');
20
21
···
23
24
recursive: true,
24
25
force: true
25
26
})));
27
+
return true;
26
28
}
27
29
28
30
async function mkdir(dir: string | string[]) {
···
32
34
else {
33
35
await Promise.all(dir.map(mkdir));
34
36
}
37
+
return true;
35
38
}
36
39
37
40
function getFileExtension(filename: string) {
···
72
75
await mkdir([SCRIPTSOUTDIR, SCRIPTSDIR]);
73
76
await emptyDir(SCRIPTSOUTDIR);
74
77
const files = await fsp.readdir(SCRIPTSDIR);
75
-
await Promise.all(files.map(f => new Promise(async (res, reject) => {
78
+
await Promise.all(files.filter(f => f.toLowerCase().endsWith('.js')).map(f => new Promise(async (res, _) => {
76
79
const p = path.join(SCRIPTSDIR, f);
77
80
const o = path.join(SCRIPTSOUTDIR, f);
78
81
console.log(`Processing script ${p}`);
···
87
90
})));
88
91
}
89
92
93
+
async function getAllFiles(fullDir: string): Promise<string[]> {
94
+
if (!path.isAbsolute(fullDir)) {
95
+
throw new Error('path must be absolute');
96
+
}
97
+
const files: string[] = [];
98
+
const dirs = [''];
99
+
for (let i = 0; i < dirs.length; i++) {
100
+
const parent = dirs[i];
101
+
const dir = path.join(fullDir, parent);
102
+
const dirEnts = await fsp.readdir(dir, { withFileTypes: true });
103
+
dirEnts.forEach(de => (de.isDirectory() ? dirs : files).push(path.join(parent, de.name)));
104
+
}
105
+
return files;
106
+
}
107
+
90
108
// Process images
91
-
async 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);
109
+
async function images(webp: boolean, avif: boolean, dir: string = IMAGESDIR) {
110
+
await mkdir(dir);
111
+
await mkdir(WEBPOUTDIR) && await emptyDir(WEBPOUTDIR);
112
+
await mkdir(AVIFOUTDIR) && await emptyDir(AVIFOUTDIR);
113
+
const releativeFiles = await getAllFiles(dir);
114
+
if (releativeFiles.length) {
115
+
await Promise.all(releativeFiles.map(f => processImage(dir, f, webp, avif)));
97
116
}
98
-
const files = await fsp.readdir(p, {
99
-
withFileTypes: true
117
+
}
118
+
119
+
async function processImage(parentDir: string, relativeFile: string, webp: boolean, avif: boolean) {
120
+
const infile = path.join(parentDir, relativeFile);
121
+
const dir = path.dirname(relativeFile);
122
+
const outDirWebP = path.join(WEBPOUTDIR, dir);
123
+
const outDirAvif = path.join(AVIFOUTDIR, dir);
124
+
webp && await mkdir(outDirWebP);
125
+
avif && await mkdir(outDirAvif);
126
+
console.log(`Processing image ${infile}`);
127
+
webp && await convertWebP(infile, outDirWebP);
128
+
avif && await convertAvif(infile, outDirAvif);
129
+
}
130
+
131
+
function convertWebP(infile: string, outDir: string) {
132
+
return new Promise((resolve, reject) => {
133
+
const filename = path.basename(infile);
134
+
const extension = getFileExtension(filename);
135
+
const outfile = path.join(outDir, filename.substring(0, filename.lastIndexOf('.')) + '.webp');
136
+
const libwebpArgs = ['-mt'];
137
+
if (extension === 'jpeg' || extension === 'jpg') {
138
+
libwebpArgs.push('-q', '60');
139
+
}
140
+
else {
141
+
libwebpArgs.push('-near_lossless', '55');
142
+
}
143
+
libwebpArgs.push(infile, '-o', outfile);
144
+
const proc = spawn('cwebp', libwebpArgs);
145
+
const timeout = setTimeout(() => {
146
+
proc.kill();
147
+
reject(new Error(`process timed out`));
148
+
}, parseInt(process.env['CWEBPTIMEOUT']) || 30000);
149
+
proc.on('exit', async (code) => {
150
+
clearTimeout(timeout);
151
+
if (code === 0) {
152
+
console.log(`Wrote ${outfile}`);
153
+
resolve(true);
154
+
}
155
+
else {
156
+
reject(new Error(`process ended with code ${code}`));
157
+
}
158
+
});
100
159
});
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
-
});
160
+
}
161
+
162
+
function convertAvif(infile: string, outDir: string) {
163
+
return new Promise((resolve, reject) => {
164
+
const filename = path.basename(infile);
165
+
const extension = getFileExtension(filename);
166
+
const outfile = path.join(outDir, filename.substring(0, filename.lastIndexOf('.')) + '.avif');
167
+
const avifencArgs = '--speed 6 --jobs all --depth 8 --cicp 1/13/6 --codec aom'.split(' ');
168
+
if (extension === 'jpeg' || extension === 'jpg') {
169
+
avifencArgs.push('--advanced', 'cq-level=28', '-q', '40', '--yuv', '420');
170
+
}
171
+
else {
172
+
avifencArgs.push('--advanced', 'cq-level=30', '-q', '45', '--yuv', '444');
173
+
}
174
+
avifencArgs.push(infile, outfile);
175
+
console.log(`avifenc ${avifencArgs.join(' ')}`);
176
+
const proc = spawn('avifenc', avifencArgs);
177
+
const timeout = setTimeout(() => {
178
+
proc.kill();
179
+
reject(new Error(`process timed out`));
180
+
}, parseInt(process.env['AVIFENCTIMEOUT']) || 30000);
181
+
proc.on('exit', async (code) => {
182
+
clearTimeout(timeout);
183
+
if (code === 0) {
184
+
console.log(`Wrote ${outfile}`);
185
+
resolve(true);
133
186
}
134
-
else if (f.isDirectory()) {
135
-
images(path.join(dir, f.name)).then(res).catch(reject);
187
+
else {
188
+
reject(new Error(`process ended with code ${code}`));
136
189
}
137
-
})));
138
-
}
190
+
});
191
+
});
192
+
}
193
+
194
+
function commandExists(cmd: string): Promise<boolean> {
195
+
return new Promise((resolve, _) => {
196
+
const proc = spawn('which', cmd.split(' '));
197
+
proc.on('exit', async (code) => resolve(code === 0));
198
+
});
139
199
}
140
200
141
201
function isAbortError(err: unknown): boolean {
···
143
203
}
144
204
145
205
(async function () {
146
-
await Promise.all([styles(), scripts(), images()]);
206
+
const webp = await commandExists('cwebp');
207
+
const avif = await commandExists('avifenc');
208
+
if (!webp && ! avif) {
209
+
console.error('WARNING: no image encoding software found.');
210
+
}
211
+
await Promise.all([styles(), scripts(), images(webp, avif)]);
147
212
if (process.argv.indexOf('--watch') >= 0) {
148
213
console.log('watching for changes...');
149
214
(async () => {
···
176
241
recursive: true // no Linux ☹️
177
242
});
178
243
for await (const _ of watcher)
179
-
await images();
244
+
await images(webp, avif);
180
245
} catch (err) {
181
246
if (isAbortError(err))
182
247
return;