@divriots/jampack 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compress.d.ts +2 -2
- package/dist/compress.js +25 -92
- package/dist/index.js +52 -6
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +9 -0
- package/dist/optimize.d.ts +1 -2
- package/dist/optimize.js +172 -133
- package/dist/options.js +2 -2
- package/dist/state.d.ts +25 -0
- package/dist/state.js +45 -0
- package/dist/utils.js +2 -2
- package/package.json +3 -1
- package/dist/types.d.ts +0 -5
- package/dist/types.js +0 -1
package/dist/compress.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
1
2
|
import sharp from 'sharp';
|
|
2
|
-
import type { Result } from './types.js';
|
|
3
3
|
export declare const compressImage: (data: Buffer, resize: sharp.ResizeOptions) => Promise<Buffer | undefined>;
|
|
4
|
-
export declare function compress(
|
|
4
|
+
export declare function compress(glob: string): Promise<void>;
|
package/dist/compress.js
CHANGED
|
@@ -4,58 +4,21 @@ import { optimize as svgo } from "svgo";
|
|
|
4
4
|
import { minify as htmlminifier } from 'html-minifier-terser';
|
|
5
5
|
import { minify as csso } from "csso";
|
|
6
6
|
import { formatBytes } from './utils.js';
|
|
7
|
-
import { table } from 'table';
|
|
8
7
|
import sharp from 'sharp';
|
|
9
8
|
import swc from '@swc/core';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const beginProgress = (
|
|
13
|
-
state = {
|
|
14
|
-
total: {
|
|
15
|
-
nbFiles: 0,
|
|
16
|
-
nbFilesCompressed: 0,
|
|
17
|
-
dataLenUncompressed: 0,
|
|
18
|
-
dataLenCompressed: 0
|
|
19
|
-
},
|
|
20
|
-
detailsByExtension: {},
|
|
21
|
-
};
|
|
22
|
-
initResults.forEach(r => {
|
|
23
|
-
addProgress(r);
|
|
24
|
-
compressedFiles.push(r.file);
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
const addProgress = (r) => {
|
|
28
|
-
const isCompressed = r.compressedSize < r.originalSize ? 1 : 0;
|
|
29
|
-
state.total.nbFiles++;
|
|
30
|
-
state.total.nbFilesCompressed += isCompressed;
|
|
31
|
-
state.total.dataLenUncompressed += r.originalSize;
|
|
32
|
-
state.total.dataLenCompressed += r.compressedSize;
|
|
33
|
-
const ext = path.extname(r.file);
|
|
34
|
-
if (ext) {
|
|
35
|
-
var summary = state.detailsByExtension[ext];
|
|
36
|
-
if (!summary) {
|
|
37
|
-
summary = {
|
|
38
|
-
nbFiles: 0,
|
|
39
|
-
nbFilesCompressed: 0,
|
|
40
|
-
dataLenUncompressed: 0,
|
|
41
|
-
dataLenCompressed: 0
|
|
42
|
-
};
|
|
43
|
-
state.detailsByExtension[ext] = summary;
|
|
44
|
-
}
|
|
45
|
-
summary.nbFiles++;
|
|
46
|
-
summary.nbFilesCompressed += isCompressed;
|
|
47
|
-
summary.dataLenUncompressed += r.originalSize;
|
|
48
|
-
summary.dataLenCompressed += r.compressedSize;
|
|
49
|
-
}
|
|
9
|
+
import globalState from './state.js';
|
|
10
|
+
import { globby } from 'globby';
|
|
11
|
+
const beginProgress = () => {
|
|
50
12
|
};
|
|
51
|
-
const printProgress = (
|
|
52
|
-
|
|
53
|
-
const msg = `${
|
|
54
|
-
if (!process.stdout.cursorTo) {
|
|
55
|
-
// In CI we don't have access to cursorTo
|
|
56
|
-
|
|
13
|
+
const printProgress = () => {
|
|
14
|
+
const gain = globalState.summary.dataLenUncompressed - globalState.summary.dataLenCompressed;
|
|
15
|
+
const msg = `${globalState.summary.nbFiles} files | ${formatBytes(globalState.summary.dataLenUncompressed)} → ${formatBytes(globalState.summary.dataLenCompressed)} | -${formatBytes(gain)} `;
|
|
16
|
+
if (!process.stdout.clearLine || !process.stdout.cursorTo) {
|
|
17
|
+
// In CI we don't have access to clearLine or cursorTo
|
|
18
|
+
// Just don't log any progress
|
|
57
19
|
}
|
|
58
20
|
else {
|
|
21
|
+
process.stdout.clearLine(0);
|
|
59
22
|
process.stdout.cursorTo(0);
|
|
60
23
|
process.stdout.write(msg);
|
|
61
24
|
}
|
|
@@ -63,27 +26,6 @@ const printProgress = (r) => {
|
|
|
63
26
|
const endProgress = () => {
|
|
64
27
|
process.stdout.write('\n');
|
|
65
28
|
};
|
|
66
|
-
const printDetails = () => {
|
|
67
|
-
const dataTable = [['Extension', 'Files', 'Compressed', 'Original', 'Compressed', 'Gain']];
|
|
68
|
-
const config = {
|
|
69
|
-
columns: [
|
|
70
|
-
{ alignment: 'left' },
|
|
71
|
-
{ alignment: 'right' },
|
|
72
|
-
{ alignment: 'right' },
|
|
73
|
-
{ alignment: 'right' },
|
|
74
|
-
{ alignment: 'right' }
|
|
75
|
-
],
|
|
76
|
-
};
|
|
77
|
-
Object.entries(state.detailsByExtension).forEach(([ext, summary]) => {
|
|
78
|
-
if (summary.dataLenCompressed < summary.dataLenUncompressed) {
|
|
79
|
-
const row = [ext, summary.nbFiles, summary.nbFilesCompressed, formatBytes(summary.dataLenUncompressed), formatBytes(summary.dataLenCompressed), '-' + formatBytes(summary.dataLenUncompressed - summary.dataLenCompressed)];
|
|
80
|
-
dataTable.push(row);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
const total = ['Total', state.total.nbFiles, state.total.nbFilesCompressed, formatBytes(state.total.dataLenUncompressed), formatBytes(state.total.dataLenCompressed), '-' + formatBytes(state.total.dataLenUncompressed - state.total.dataLenCompressed)];
|
|
84
|
-
dataTable.push(total);
|
|
85
|
-
console.log(table(dataTable, config));
|
|
86
|
-
};
|
|
87
29
|
const processFile = async (file, stats) => {
|
|
88
30
|
const result = {
|
|
89
31
|
file,
|
|
@@ -129,18 +71,21 @@ const processFile = async (file, stats) => {
|
|
|
129
71
|
}
|
|
130
72
|
catch (e) {
|
|
131
73
|
// console error for the moment
|
|
74
|
+
console.error(`\n${file}`);
|
|
132
75
|
console.error(e);
|
|
133
76
|
}
|
|
134
77
|
// Writedata
|
|
135
78
|
if (writeData && writeData.length < result.originalSize) {
|
|
136
79
|
result.compressedSize = writeData.length;
|
|
137
|
-
|
|
80
|
+
if (!globalState.args.nowrite) {
|
|
81
|
+
await fs.writeFile(file, writeData);
|
|
82
|
+
}
|
|
138
83
|
}
|
|
139
|
-
|
|
140
|
-
|
|
84
|
+
globalState.addFile(result);
|
|
85
|
+
printProgress();
|
|
141
86
|
};
|
|
142
87
|
export const compressImage = async (data, resize) => {
|
|
143
|
-
const sharpFile = await sharp(data);
|
|
88
|
+
const sharpFile = await sharp(data, { animated: true });
|
|
144
89
|
const meta = await sharpFile.metadata();
|
|
145
90
|
switch (meta.format) {
|
|
146
91
|
case 'svg':
|
|
@@ -155,7 +100,7 @@ export const compressImage = async (data, resize) => {
|
|
|
155
100
|
case 'jpeg':
|
|
156
101
|
case 'webp':
|
|
157
102
|
case 'gif':
|
|
158
|
-
return await sharpFile.resize(resize).toBuffer();
|
|
103
|
+
return await sharpFile.resize({ ...resize, fit: 'fill', withoutEnlargement: true }).toBuffer();
|
|
159
104
|
}
|
|
160
105
|
return undefined;
|
|
161
106
|
};
|
|
@@ -163,26 +108,14 @@ const compressImageFile = async (file) => {
|
|
|
163
108
|
const buffer = await fs.readFile(file);
|
|
164
109
|
return compressImage(buffer, {});
|
|
165
110
|
};
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
if (compressedFiles.includes(filePath)) {
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
return [await processFile(filePath, stats)];
|
|
111
|
+
export async function compress(glob) {
|
|
112
|
+
beginProgress();
|
|
113
|
+
const paths = await globby(glob, { cwd: globalState.dir, absolute: true });
|
|
114
|
+
// "Parallel" processing
|
|
115
|
+
await Promise.all(paths.map(async (file) => {
|
|
116
|
+
if (!globalState.compressedFiles.has(file)) {
|
|
117
|
+
await processFile(file, await fs.stat(file));
|
|
179
118
|
}
|
|
180
119
|
}));
|
|
181
|
-
return files.filter((file) => file.length).flat(1); // Remove empty dir and flat it all
|
|
182
|
-
};
|
|
183
|
-
export async function compress(dir, initResult) {
|
|
184
|
-
beginProgress(initResult);
|
|
185
|
-
await processDirectory(dir);
|
|
186
120
|
endProgress();
|
|
187
|
-
printDetails();
|
|
188
121
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { compress } from './compress.js';
|
|
4
4
|
import { optimize } from './optimize.js';
|
|
5
|
+
import kleur from 'kleur';
|
|
6
|
+
import globalState from './state.js';
|
|
7
|
+
import { table } from 'table';
|
|
8
|
+
import { formatBytes } from './utils.js';
|
|
9
|
+
const logo = ` __ __
|
|
10
|
+
|__|____ _____ ___________ ____ | | __
|
|
11
|
+
| \\__ \\ / \\\\____ \\__ \\ _/ ___\\| |/ /
|
|
12
|
+
| |/ __ \\| Y Y \\ |_> > __ \\\\ \\___| <
|
|
13
|
+
/\\__| (____ /__|_| / __(____ /\\___ >__|_ \\
|
|
14
|
+
\\______| \\/ \\/| | \\/ \\/ \\/
|
|
15
|
+
|__| by ‹div›RIOTS (c)2022
|
|
16
|
+
`;
|
|
17
|
+
console.log(logo);
|
|
5
18
|
const program = new Command();
|
|
6
19
|
program
|
|
7
20
|
.name('jampack')
|
|
@@ -10,11 +23,44 @@ program
|
|
|
10
23
|
program.command('pack', { isDefault: true })
|
|
11
24
|
.description('todo')
|
|
12
25
|
.argument('<dir>', 'Directory to pack')
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
.option('--nowrite', 'No write')
|
|
27
|
+
.option('--onlycomp', 'Only compress')
|
|
28
|
+
.option('--onlyoptim', 'Only optimize')
|
|
29
|
+
.action(async (dir, options) => {
|
|
30
|
+
globalState.dir = dir;
|
|
31
|
+
globalState.args = options;
|
|
32
|
+
if (!options.onlyoptim && !options.onlycomp) {
|
|
33
|
+
console.log(kleur.bgGreen(kleur.black(`\n PASS 1 - Compressing embeddables `)));
|
|
34
|
+
await compress("**/*.{css,svg,js}");
|
|
35
|
+
}
|
|
36
|
+
if (!options.onlycomp) {
|
|
37
|
+
console.log(kleur.bgGreen(kleur.black(`\n PASS 2 - Optimizing `)));
|
|
38
|
+
await optimize();
|
|
39
|
+
}
|
|
40
|
+
if (!options.onlyoptim) {
|
|
41
|
+
console.log(kleur.bgGreen(kleur.black(`\n PASS 3 - Compressing the rest`)));
|
|
42
|
+
await compress("**/**");
|
|
43
|
+
}
|
|
44
|
+
printDetails();
|
|
19
45
|
});
|
|
20
46
|
program.parse();
|
|
47
|
+
function printDetails() {
|
|
48
|
+
const dataTable = [['Extension', 'Compressed', 'Original', 'Compressed', 'Gain']];
|
|
49
|
+
const config = {
|
|
50
|
+
columns: [
|
|
51
|
+
{ alignment: 'left' },
|
|
52
|
+
{ alignment: 'right' },
|
|
53
|
+
{ alignment: 'right' },
|
|
54
|
+
{ alignment: 'right' }
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
Object.entries(globalState.summaryByExtension).forEach(([ext, summary]) => {
|
|
58
|
+
if (summary.dataLenCompressed < summary.dataLenUncompressed) {
|
|
59
|
+
const row = [ext, `${summary.nbFilesCompressed} / ${summary.nbFiles}`, formatBytes(summary.dataLenUncompressed), formatBytes(summary.dataLenCompressed), '-' + formatBytes(summary.dataLenUncompressed - summary.dataLenCompressed)];
|
|
60
|
+
dataTable.push(row);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const total = ['Total', `${globalState.summary.nbFilesCompressed} / ${globalState.summary.nbFiles}`, formatBytes(globalState.summary.dataLenUncompressed), formatBytes(globalState.summary.dataLenCompressed), '-' + formatBytes(globalState.summary.dataLenUncompressed - globalState.summary.dataLenCompressed)];
|
|
64
|
+
dataTable.push(total);
|
|
65
|
+
console.log(table(dataTable, config));
|
|
66
|
+
}
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
package/dist/optimize.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function optimize(dir: string): Promise<Result[]>;
|
|
1
|
+
export declare function optimize(): Promise<void>;
|
package/dist/optimize.js
CHANGED
|
@@ -7,11 +7,11 @@ import sharp from "sharp";
|
|
|
7
7
|
import options from './options.js';
|
|
8
8
|
import { compressImage } from './compress.js';
|
|
9
9
|
import svgToMiniDataURI from 'mini-svg-data-uri';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
async function analyse(
|
|
10
|
+
import globalState from './state.js';
|
|
11
|
+
import { exit } from 'process';
|
|
12
|
+
async function analyse(file) {
|
|
13
13
|
console.log('Processing ' + file);
|
|
14
|
-
const html = (await fs.readFile(path.join(dir, file))).toString();
|
|
14
|
+
const html = (await fs.readFile(path.join(globalState.dir, file))).toString();
|
|
15
15
|
const $ = cheerio.load(html, {});
|
|
16
16
|
const imgs = $('img');
|
|
17
17
|
const imgsArray = [];
|
|
@@ -21,138 +21,178 @@ async function analyse(dir, file) {
|
|
|
21
21
|
// Process images sequentially
|
|
22
22
|
await imgsArray.reduce(async (previousPromise, imgElement) => {
|
|
23
23
|
await previousPromise;
|
|
24
|
-
return processImage(
|
|
24
|
+
return processImage(file, $, imgElement);
|
|
25
25
|
}, Promise.resolve());
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
async function processImage(dir, file, $, imgElement) {
|
|
29
|
-
const img = $(imgElement);
|
|
30
|
-
/*
|
|
31
|
-
* Attribute 'alt'
|
|
32
|
-
*/
|
|
33
|
-
const attrib_alt = img.attr('alt');
|
|
34
|
-
if (attrib_alt === undefined) {
|
|
35
|
-
console.warn('<img> has no alt attribute - adding an empty but you should fix it');
|
|
36
|
-
img.attr('alt', "");
|
|
37
|
-
}
|
|
38
|
-
/*
|
|
39
|
-
* Attribute 'src'
|
|
40
|
-
*/
|
|
41
|
-
const attrib_src = img.attr('src');
|
|
42
|
-
console.log(`- Image ${attrib_src}`);
|
|
43
|
-
if (!attrib_src) {
|
|
44
|
-
console.warn('<img> has no src attribute');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (attrib_src.startsWith('data:')) {
|
|
48
|
-
// Data URI image
|
|
49
|
-
// TODO: try to compress it
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (!isLocal(attrib_src)) {
|
|
53
|
-
// Image not local, don't touch it
|
|
54
|
-
return;
|
|
26
|
+
if (!globalState.args.nowrite) {
|
|
27
|
+
await fs.writeFile(path.join(globalState.dir, file), $.html());
|
|
55
28
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
const attr_loading = img.attr('loading');
|
|
60
|
-
switch (attr_loading) {
|
|
61
|
-
case undefined:
|
|
62
|
-
// Go lazy by default
|
|
63
|
-
img.attr('loading', 'lazy');
|
|
64
|
-
break;
|
|
65
|
-
case 'eager':
|
|
66
|
-
img.removeAttr('loading');
|
|
67
|
-
break;
|
|
68
|
-
case 'lazy':
|
|
69
|
-
// Don't touch it
|
|
70
|
-
break;
|
|
71
|
-
default:
|
|
72
|
-
console.warn(`Invalid loading attribute ${attr_loading}`);
|
|
73
|
-
}
|
|
74
|
-
/*
|
|
75
|
-
* Attribute 'decoding'
|
|
76
|
-
*/
|
|
77
|
-
img.attr('decoding', 'async');
|
|
78
|
-
/*
|
|
79
|
-
* Loading image
|
|
80
|
-
*/
|
|
81
|
-
const absoluteImgPath = translateSrc(dir, path.dirname(file), attrib_src);
|
|
82
|
-
// file exists?
|
|
29
|
+
}
|
|
30
|
+
async function processImage(file, $, imgElement) {
|
|
83
31
|
try {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
32
|
+
const img = $(imgElement);
|
|
33
|
+
/*
|
|
34
|
+
* Attribute 'alt'
|
|
35
|
+
*/
|
|
36
|
+
const attrib_alt = img.attr('alt');
|
|
37
|
+
if (attrib_alt === undefined) {
|
|
38
|
+
console.warn('<img> has no alt attribute - adding an empty but you should fix it');
|
|
39
|
+
img.attr('alt', "");
|
|
40
|
+
}
|
|
41
|
+
/*
|
|
42
|
+
* Attribute 'src'
|
|
43
|
+
*/
|
|
44
|
+
const attrib_src = img.attr('src');
|
|
45
|
+
console.log(`- Image ${attrib_src}`);
|
|
46
|
+
if (!attrib_src) {
|
|
47
|
+
console.warn('<img> has no src attribute');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (attrib_src.startsWith('data:')) {
|
|
51
|
+
// Data URI image
|
|
52
|
+
// TODO: try to compress it
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!isLocal(attrib_src)) {
|
|
56
|
+
// Image not local, don't touch it
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
/*
|
|
60
|
+
* Attribute 'loading'
|
|
61
|
+
*/
|
|
62
|
+
const attr_loading = img.attr('loading');
|
|
63
|
+
switch (attr_loading) {
|
|
64
|
+
case undefined:
|
|
65
|
+
// Go lazy by default
|
|
66
|
+
img.attr('loading', 'lazy');
|
|
67
|
+
break;
|
|
68
|
+
case 'eager':
|
|
69
|
+
img.removeAttr('loading');
|
|
107
70
|
break;
|
|
108
|
-
case '
|
|
109
|
-
|
|
110
|
-
case 'png':
|
|
111
|
-
case 'webp':
|
|
112
|
-
case 'gif':
|
|
113
|
-
// TODO
|
|
71
|
+
case 'lazy':
|
|
72
|
+
// Don't touch it
|
|
114
73
|
break;
|
|
74
|
+
default:
|
|
75
|
+
console.warn(`Invalid loading attribute ${attr_loading}`);
|
|
115
76
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
77
|
+
/*
|
|
78
|
+
* Attribute 'decoding'
|
|
79
|
+
*/
|
|
80
|
+
img.attr('decoding', 'async');
|
|
81
|
+
/*
|
|
82
|
+
* Loading image
|
|
83
|
+
*/
|
|
84
|
+
const absoluteImgPath = translateSrc(globalState.dir, path.dirname(file), attrib_src);
|
|
85
|
+
// file exists?
|
|
86
|
+
try {
|
|
87
|
+
const stats = await fs.stat(absoluteImgPath);
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
console.error(`Missing img ${absoluteImgPath}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const sharpFile = sharp(absoluteImgPath);
|
|
94
|
+
const meta = await sharpFile.metadata();
|
|
95
|
+
const originalData = await fs.readFile(absoluteImgPath);
|
|
96
|
+
/*
|
|
97
|
+
* Embed small images
|
|
98
|
+
*/
|
|
99
|
+
let isEmbed = false;
|
|
100
|
+
if (originalData.length <= options.image.embed_size) {
|
|
101
|
+
let datauri = undefined;
|
|
102
|
+
switch (meta.format) {
|
|
103
|
+
case 'svg':
|
|
104
|
+
const compressedData = await compressImage(originalData, {});
|
|
105
|
+
const svgEmbed = (compressedData && compressedData.length < originalData.length) ? compressedData : originalData;
|
|
106
|
+
datauri = svgToMiniDataURI(svgEmbed.toString());
|
|
107
|
+
break;
|
|
108
|
+
case 'jpg':
|
|
109
|
+
case 'jpeg':
|
|
110
|
+
case 'png':
|
|
111
|
+
case 'webp':
|
|
112
|
+
case 'gif':
|
|
113
|
+
// TODO
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (datauri) {
|
|
117
|
+
isEmbed = true;
|
|
118
|
+
img.attr('src', datauri);
|
|
119
|
+
img.removeAttr('loading');
|
|
120
|
+
img.removeAttr('decoding');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (isEmbed) {
|
|
124
|
+
// Image is embed, no need for more processing
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
/*
|
|
128
|
+
* Attribute 'width' & 'height'
|
|
129
|
+
*/
|
|
130
|
+
const [w, h] = setImageSize(img, meta);
|
|
131
|
+
if (w < 0 || h < 0) {
|
|
132
|
+
console.warn(`Unexpected error in image size calculation - some optimizations can't be done`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
/*
|
|
136
|
+
* Compress image
|
|
137
|
+
*/
|
|
138
|
+
if (!globalState.compressedFiles.has(absoluteImgPath)) {
|
|
139
|
+
// Image not already compressed before
|
|
140
|
+
const result = {
|
|
141
|
+
file: absoluteImgPath,
|
|
142
|
+
originalSize: originalData.length,
|
|
143
|
+
compressedSize: originalData.length
|
|
144
|
+
};
|
|
145
|
+
const newImage = await compressImage(originalData, { width: w, height: h });
|
|
146
|
+
if (newImage && newImage.length < originalData.length) {
|
|
147
|
+
if (!globalState.args.nowrite) {
|
|
148
|
+
fs.writeFile(absoluteImgPath, newImage);
|
|
149
|
+
}
|
|
150
|
+
result.compressedSize = newImage.length;
|
|
151
|
+
}
|
|
152
|
+
globalState.addFile(result);
|
|
153
|
+
}
|
|
154
|
+
const attr_srcset = img.attr('srcset');
|
|
155
|
+
if (attr_srcset) {
|
|
156
|
+
// If srcset is set, don't touch it.
|
|
157
|
+
// Just compress files
|
|
158
|
+
// TODO
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Generate image set
|
|
162
|
+
const ext = path.extname(attrib_src);
|
|
163
|
+
const fullbasename = attrib_src.slice(0, -(ext.length));
|
|
164
|
+
const imageSrc = (addition) => `${fullbasename}${addition}${ext}`;
|
|
165
|
+
// Start from original image
|
|
166
|
+
let new_srcset = '';
|
|
167
|
+
// Start reduction
|
|
168
|
+
const step = 300; //px
|
|
169
|
+
const ratio = w / h;
|
|
170
|
+
let valueW = w - step;
|
|
171
|
+
let valueH = Math.trunc(valueW / ratio);
|
|
172
|
+
while (valueW > options.image.srcset_min_width) {
|
|
173
|
+
const src = imageSrc(`@${valueW}w`);
|
|
174
|
+
const absoluteFilename = translateSrc(globalState.dir, path.dirname(file), src);
|
|
175
|
+
const compressedData = await compressImage(originalData, { width: valueW, height: valueH });
|
|
176
|
+
if (compressedData && !globalState.args.nowrite) {
|
|
177
|
+
fs.writeFile(absoluteFilename, compressedData);
|
|
178
|
+
}
|
|
179
|
+
// Add file to list avoid recompression
|
|
180
|
+
globalState.compressedFiles.add(absoluteFilename);
|
|
181
|
+
new_srcset += `, ${src} ${valueW}w`;
|
|
182
|
+
// reduce size
|
|
183
|
+
valueW -= step;
|
|
184
|
+
valueH = Math.trunc(valueW / ratio);
|
|
185
|
+
}
|
|
186
|
+
if (new_srcset) {
|
|
187
|
+
img.attr('srcset', `${attrib_src} ${w}w` + new_srcset);
|
|
188
|
+
}
|
|
121
189
|
}
|
|
122
190
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
* Attribute 'width' & 'height'
|
|
129
|
-
*/
|
|
130
|
-
const [w, h] = setImageSize(img, meta);
|
|
131
|
-
if (w < 0 || h < 0) {
|
|
132
|
-
console.warn(`Unexpected error in image size calculation - some optimizations can't be done`);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
/*
|
|
136
|
-
* Compress image
|
|
137
|
-
*/
|
|
138
|
-
if (compressedImages.includes(absoluteImgPath)) {
|
|
139
|
-
// Image already compressed. Skip.
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
const fileWidth = meta.width;
|
|
143
|
-
const fileHeight = meta.height;
|
|
144
|
-
const newImage = await compressImage(originalData, { width: w, height: h });
|
|
145
|
-
if (newImage && newImage.length < originalData.length) {
|
|
146
|
-
fs.writeFile(absoluteImgPath, newImage);
|
|
147
|
-
compressedImages.push(absoluteImgPath);
|
|
148
|
-
compressedResults.push({ file: absoluteImgPath, originalSize: originalData.length, compressedSize: newImage.length });
|
|
149
|
-
}
|
|
150
|
-
const attr_srcset = img.attr('srcset');
|
|
151
|
-
if (attr_srcset) {
|
|
152
|
-
// If srcset is set, don't touch it
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
// Generate image set
|
|
191
|
+
catch (e) {
|
|
192
|
+
console.error('Error while processing image');
|
|
193
|
+
console.error(e);
|
|
194
|
+
// exit here until we cover all the issues
|
|
195
|
+
exit(1);
|
|
156
196
|
}
|
|
157
197
|
}
|
|
158
198
|
function setImageSize(img, meta) {
|
|
@@ -169,7 +209,7 @@ function setImageSize(img, meta) {
|
|
|
169
209
|
}
|
|
170
210
|
if (height !== undefined) {
|
|
171
211
|
if (!isNumeric(height)) {
|
|
172
|
-
console.warn(`Invalid
|
|
212
|
+
console.warn(`Invalid height attribute "${height}" - overriding`);
|
|
173
213
|
height = undefined;
|
|
174
214
|
}
|
|
175
215
|
}
|
|
@@ -214,12 +254,11 @@ function setImageSize(img, meta) {
|
|
|
214
254
|
// Something when wrong
|
|
215
255
|
throw new Error('Unexpected issue when resolving image sizes');
|
|
216
256
|
}
|
|
217
|
-
export async function optimize(
|
|
218
|
-
const paths = await globby('**/*.{htm,html}', { cwd: dir });
|
|
257
|
+
export async function optimize() {
|
|
258
|
+
const paths = await globby('**/*.{htm,html}', { cwd: globalState.dir });
|
|
219
259
|
// Sequential async
|
|
220
260
|
await paths.reduce(async (previousPromise, item) => {
|
|
221
261
|
await previousPromise;
|
|
222
|
-
return analyse(
|
|
262
|
+
return analyse(item);
|
|
223
263
|
}, Promise.resolve());
|
|
224
|
-
return compressedResults;
|
|
225
264
|
}
|
package/dist/options.js
CHANGED
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare type Args = {
|
|
2
|
+
nowrite?: boolean;
|
|
3
|
+
};
|
|
4
|
+
export declare type Result = {
|
|
5
|
+
file: string;
|
|
6
|
+
originalSize: number;
|
|
7
|
+
compressedSize: number;
|
|
8
|
+
};
|
|
9
|
+
export declare type Summary = {
|
|
10
|
+
nbFiles: number;
|
|
11
|
+
nbFilesCompressed: number;
|
|
12
|
+
dataLenUncompressed: number;
|
|
13
|
+
dataLenCompressed: number;
|
|
14
|
+
};
|
|
15
|
+
export declare class GlobalState {
|
|
16
|
+
dir: string;
|
|
17
|
+
args: Args;
|
|
18
|
+
compressedFiles: Set<string>;
|
|
19
|
+
compressedFilesResult: Result[];
|
|
20
|
+
summary: Summary;
|
|
21
|
+
summaryByExtension: Record<string, Summary>;
|
|
22
|
+
addFile(r: Result): void;
|
|
23
|
+
}
|
|
24
|
+
declare const state: GlobalState;
|
|
25
|
+
export default state;
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
export class GlobalState {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.dir = 'dist';
|
|
5
|
+
this.args = {};
|
|
6
|
+
this.compressedFiles = new Set();
|
|
7
|
+
this.compressedFilesResult = [];
|
|
8
|
+
this.summary = {
|
|
9
|
+
nbFiles: 0,
|
|
10
|
+
nbFilesCompressed: 0,
|
|
11
|
+
dataLenCompressed: 0,
|
|
12
|
+
dataLenUncompressed: 0
|
|
13
|
+
};
|
|
14
|
+
this.summaryByExtension = {};
|
|
15
|
+
}
|
|
16
|
+
addFile(r) {
|
|
17
|
+
const isCompressed = r.compressedSize < r.originalSize ? 1 : 0;
|
|
18
|
+
this.compressedFiles.add(r.file);
|
|
19
|
+
this.compressedFilesResult.push(r);
|
|
20
|
+
this.summary.nbFiles++;
|
|
21
|
+
state.summary.nbFilesCompressed += isCompressed;
|
|
22
|
+
state.summary.dataLenUncompressed += r.originalSize;
|
|
23
|
+
state.summary.dataLenCompressed += r.compressedSize;
|
|
24
|
+
const ext = path.extname(r.file);
|
|
25
|
+
if (ext) {
|
|
26
|
+
let summary = this.summaryByExtension[ext];
|
|
27
|
+
if (!summary) {
|
|
28
|
+
summary = {
|
|
29
|
+
nbFiles: 0,
|
|
30
|
+
nbFilesCompressed: 0,
|
|
31
|
+
dataLenUncompressed: 0,
|
|
32
|
+
dataLenCompressed: 0
|
|
33
|
+
};
|
|
34
|
+
this.summaryByExtension[ext] = summary;
|
|
35
|
+
}
|
|
36
|
+
summary.nbFiles++;
|
|
37
|
+
summary.nbFilesCompressed += isCompressed;
|
|
38
|
+
summary.dataLenUncompressed += r.originalSize;
|
|
39
|
+
summary.dataLenCompressed += r.compressedSize;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
;
|
|
43
|
+
}
|
|
44
|
+
const state = new GlobalState();
|
|
45
|
+
export default state;
|
package/dist/utils.js
CHANGED
|
@@ -7,7 +7,7 @@ export function formatBytes(bytes, decimals = 2) {
|
|
|
7
7
|
const dm = decimals < 0 ? 0 : decimals;
|
|
8
8
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
9
9
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
10
|
-
return `${
|
|
10
|
+
return `${(bytes / Math.pow(k, i)).toFixed(dm)} ${sizes[i]}`;
|
|
11
11
|
}
|
|
12
12
|
export function isLocal(src) {
|
|
13
13
|
const u = url.parse(src);
|
|
@@ -18,7 +18,7 @@ export function translateSrc(projectRoot, htmlRelativePath, src) {
|
|
|
18
18
|
throw new Error('Source should be local');
|
|
19
19
|
}
|
|
20
20
|
const srcAbsolutePath = path.join(projectRoot, src.startsWith('/') ? '' : htmlRelativePath, src);
|
|
21
|
-
return srcAbsolutePath;
|
|
21
|
+
return path.resolve(srcAbsolutePath);
|
|
22
22
|
}
|
|
23
23
|
export function isNumeric(value) {
|
|
24
24
|
return /^\d+$/.test(value);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@divriots/jampack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Packer for static websites",
|
|
5
5
|
"author": "Georges Gomes @georges-gomes",
|
|
6
6
|
"bin": {
|
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
"@types/sharp": "^0.31.0",
|
|
25
25
|
"@types/svgo": "^2.6.4",
|
|
26
26
|
"@types/table": "^6.3.2",
|
|
27
|
+
"chalk": "^5.0.1",
|
|
27
28
|
"cheerio": "0.22.0",
|
|
28
29
|
"commander": "^9.4.0",
|
|
29
30
|
"csso": "^5.0.5",
|
|
30
31
|
"globby": "^13.1.2",
|
|
31
32
|
"html-minifier-terser": "^7.0.0",
|
|
33
|
+
"kleur": "^4.1.5",
|
|
32
34
|
"mini-svg-data-uri": "^1.4.4",
|
|
33
35
|
"sharp": "^0.31.0",
|
|
34
36
|
"svgo": "^2.8.0",
|
package/dist/types.d.ts
DELETED
package/dist/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|