+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
+7
-2
README.md
+7
-2
README.md
···
1
1
# build-shit
2
-
A build script for preparing files for production. It handles CSS and JavaScript minification, Sass compilation, and image conversion to WebP.
2
+
3
+
[](https://tangled.sh/@sanin.dev/build-shit/blob/master/LICENSE)
4
+
[](https://www.npmjs.com/package/forking-build-shit)
5
+
[](https://www.npmjs.com/package/forking-build-shit)
6
+
7
+
A build script for preparing files for production. It handles CSS and JavaScript minification, Sass compilation, and image conversion to WebP and AVIF.
3
8
4
-
WebP conversion requires libwebp.
9
+
WebP conversion requires libwebp. AVIF conversion requires avifenc.
5
10
6
11
## Installation
7
12
+25
-24
package-lock.json
+25
-24
package-lock.json
···
1
1
{
2
2
"name": "forking-build-shit",
3
-
"version": "0.0.1",
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": "0.0.1",
9
+
"version": "1.0.5",
10
10
"license": "MIT",
11
11
"dependencies": {
12
12
"csso": "5.0.5",
13
-
"sass": "1.86.0",
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.13.10",
23
+
"@types/node": "^24.7.2",
23
24
"@types/uglify-js": "^3.17.5",
24
-
"typescript": "5.8.2"
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.13.10",
355
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
356
-
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
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.20.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.86.0",
548
-
"resolved": "https://registry.npmjs.org/sass/-/sass-1.86.0.tgz",
549
-
"integrity": "sha512-zV8vGUld/+mP4KbMLJMX7TyGCuUp7hnkOScgCMsWuHtns8CWBoz+vmEhoGMXsaJrbUP8gj+F1dLvVe79sK8UdA==",
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.2",
600
-
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
601
-
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
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.20.0",
626
-
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
627
-
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
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": "0.0.1",
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.86.0",
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.13.10",
17
+
"@types/node": "^24.7.2",
18
18
"@types/uglify-js": "^3.17.5",
19
-
"typescript": "5.8.2"
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"
+131
-52
src/build-shit.ts
+131
-52
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');
15
-
const STYLEOUTDIR = process.env.STYLEOUTDIR || path.join(import.meta.dirname, 'assets', 'css');
16
-
const SCRIPTSOUTDIR = process.env.SCRIPTSOUTDIR || path.join(import.meta.dirname, 'assets', 'js');
17
-
const IMAGESOUTDIR = process.env.IMAGESOUTDIR || path.join(import.meta.dirname, 'assets', 'images', 'webp');
14
+
const IMAGESDIR = path.join(process.cwd(), 'assets', 'images', 'original');
15
+
const STYLEOUTDIR = process.env.STYLEOUTDIR || path.join('assets', 'css');
16
+
const SCRIPTSOUTDIR = process.env.SCRIPTSOUTDIR || path.join('assets', 'js');
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;
38
+
}
39
+
40
+
function getFileExtension(filename: string) {
41
+
const split = filename.split('.');
42
+
return split[split.length - 1].toLowerCase();
35
43
}
36
44
37
45
// Process styles
38
46
async function styles() {
39
47
await mkdir([STYLEOUTDIR, STYLESDIR]);
40
48
await emptyDir(STYLEOUTDIR);
41
-
let styles: string[] = [];
42
-
let files = await fsp.readdir(STYLESDIR);
49
+
const styles: string[] = [];
50
+
const files = await fsp.readdir(STYLESDIR);
43
51
await Promise.all(files.map(f => new Promise(async (res, reject) => {
44
-
let p = path.join(STYLESDIR, f);
52
+
const p = path.join(STYLESDIR, f);
45
53
console.log(`Processing style ${p}`);
46
-
let style = sass.compile(p).css;
54
+
const style = sass.compile(p).css;
47
55
if (f.charAt(0) !== '_') {
48
56
if (SQUASH.test(f)) {
49
57
styles.push(style);
50
58
}
51
59
else {
52
-
let o = path.join(STYLEOUTDIR, f.substring(0, f.lastIndexOf('.')) + '.css');
60
+
const o = path.join(STYLEOUTDIR, f.substring(0, f.lastIndexOf('.')) + '.css');
53
61
await fsp.writeFile(o, csso.minify(style).css);
54
62
console.log(`Wrote ${o}`);
55
63
}
56
64
}
57
65
res(0);
58
66
})));
59
-
let out = csso.minify(styles.join('\n')).css;
60
-
let outpath = path.join(STYLEOUTDIR, STYLEOUTFILE);
67
+
const out = csso.minify(styles.join('\n')).css;
68
+
const outpath = path.join(STYLEOUTDIR, STYLEOUTFILE);
61
69
await fsp.writeFile(outpath, out);
62
70
console.log(`Wrote ${outpath}`);
63
71
}
···
66
74
async function scripts() {
67
75
await mkdir([SCRIPTSOUTDIR, SCRIPTSDIR]);
68
76
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);
77
+
const files = await fsp.readdir(SCRIPTSDIR);
78
+
await Promise.all(files.filter(f => f.toLowerCase().endsWith('.js')).map(f => new Promise(async (res, _) => {
79
+
const p = path.join(SCRIPTSDIR, f);
80
+
const o = path.join(SCRIPTSOUTDIR, f);
73
81
console.log(`Processing script ${p}`);
74
82
try {
75
83
await fsp.writeFile(o, uglifyjs.minify((await fsp.readFile(p)).toString()).code);
···
82
90
})));
83
91
}
84
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
+
85
108
// Process images
86
-
async 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);
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)));
92
116
}
93
-
let files = await fsp.readdir(p, {
94
-
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
+
});
95
159
});
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
-
});
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);
119
186
}
120
-
else if (f.isDirectory()) {
121
-
images(path.join(dir, f.name)).then(res).catch(reject);
187
+
else {
188
+
reject(new Error(`process ended with code ${code}`));
122
189
}
123
-
})));
124
-
}
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
+
});
125
199
}
126
200
127
201
function isAbortError(err: unknown): boolean {
···
129
203
}
130
204
131
205
(async function () {
132
-
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)]);
133
212
if (process.argv.indexOf('--watch') >= 0) {
134
213
console.log('watching for changes...');
135
214
(async () => {
···
162
241
recursive: true // no Linux ☹️
163
242
});
164
243
for await (const _ of watcher)
165
-
await images();
244
+
await images(webp, avif);
166
245
} catch (err) {
167
246
if (isAbortError(err))
168
247
return;