@divriots/jampack 0.1.2 → 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 +29 -89
- 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,79 +4,28 @@ 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
|
-
});
|
|
9
|
+
import globalState from './state.js';
|
|
10
|
+
import { globby } from 'globby';
|
|
11
|
+
const beginProgress = () => {
|
|
26
12
|
};
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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;
|
|
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
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
process.stdout.clearLine(0);
|
|
22
|
+
process.stdout.cursorTo(0);
|
|
23
|
+
process.stdout.write(msg);
|
|
49
24
|
}
|
|
50
|
-
};
|
|
51
|
-
const printProgress = (r) => {
|
|
52
|
-
addProgress(r);
|
|
53
|
-
process.stdout.cursorTo(0);
|
|
54
|
-
process.stdout.write(`${state.total.nbFiles} files | ${formatBytes(state.total.dataLenUncompressed)} → ${formatBytes(state.total.dataLenCompressed)}`);
|
|
55
25
|
};
|
|
56
26
|
const endProgress = () => {
|
|
57
27
|
process.stdout.write('\n');
|
|
58
28
|
};
|
|
59
|
-
const printDetails = () => {
|
|
60
|
-
const dataTable = [['Extension', 'Files', 'Compressed', 'Original', 'Compressed', 'Gain']];
|
|
61
|
-
const config = {
|
|
62
|
-
columns: [
|
|
63
|
-
{ alignment: 'left' },
|
|
64
|
-
{ alignment: 'right' },
|
|
65
|
-
{ alignment: 'right' },
|
|
66
|
-
{ alignment: 'right' },
|
|
67
|
-
{ alignment: 'right' }
|
|
68
|
-
],
|
|
69
|
-
};
|
|
70
|
-
Object.entries(state.detailsByExtension).forEach(([ext, summary]) => {
|
|
71
|
-
if (summary.dataLenCompressed < summary.dataLenUncompressed) {
|
|
72
|
-
const row = [ext, summary.nbFiles, summary.nbFilesCompressed, formatBytes(summary.dataLenUncompressed), formatBytes(summary.dataLenCompressed), '-' + formatBytes(summary.dataLenUncompressed - summary.dataLenCompressed)];
|
|
73
|
-
dataTable.push(row);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
const total = ['Total', state.total.nbFiles, state.total.nbFilesCompressed, formatBytes(state.total.dataLenUncompressed), formatBytes(state.total.dataLenCompressed), '-' + formatBytes(state.total.dataLenUncompressed - state.total.dataLenCompressed)];
|
|
77
|
-
dataTable.push(total);
|
|
78
|
-
console.log(table(dataTable, config));
|
|
79
|
-
};
|
|
80
29
|
const processFile = async (file, stats) => {
|
|
81
30
|
const result = {
|
|
82
31
|
file,
|
|
@@ -122,18 +71,21 @@ const processFile = async (file, stats) => {
|
|
|
122
71
|
}
|
|
123
72
|
catch (e) {
|
|
124
73
|
// console error for the moment
|
|
74
|
+
console.error(`\n${file}`);
|
|
125
75
|
console.error(e);
|
|
126
76
|
}
|
|
127
77
|
// Writedata
|
|
128
78
|
if (writeData && writeData.length < result.originalSize) {
|
|
129
79
|
result.compressedSize = writeData.length;
|
|
130
|
-
|
|
80
|
+
if (!globalState.args.nowrite) {
|
|
81
|
+
await fs.writeFile(file, writeData);
|
|
82
|
+
}
|
|
131
83
|
}
|
|
132
|
-
|
|
133
|
-
|
|
84
|
+
globalState.addFile(result);
|
|
85
|
+
printProgress();
|
|
134
86
|
};
|
|
135
87
|
export const compressImage = async (data, resize) => {
|
|
136
|
-
const sharpFile = await sharp(data);
|
|
88
|
+
const sharpFile = await sharp(data, { animated: true });
|
|
137
89
|
const meta = await sharpFile.metadata();
|
|
138
90
|
switch (meta.format) {
|
|
139
91
|
case 'svg':
|
|
@@ -148,7 +100,7 @@ export const compressImage = async (data, resize) => {
|
|
|
148
100
|
case 'jpeg':
|
|
149
101
|
case 'webp':
|
|
150
102
|
case 'gif':
|
|
151
|
-
return await sharpFile.resize(resize).toBuffer();
|
|
103
|
+
return await sharpFile.resize({ ...resize, fit: 'fill', withoutEnlargement: true }).toBuffer();
|
|
152
104
|
}
|
|
153
105
|
return undefined;
|
|
154
106
|
};
|
|
@@ -156,26 +108,14 @@ const compressImageFile = async (file) => {
|
|
|
156
108
|
const buffer = await fs.readFile(file);
|
|
157
109
|
return compressImage(buffer, {});
|
|
158
110
|
};
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
if (compressedFiles.includes(filePath)) {
|
|
169
|
-
return [];
|
|
170
|
-
}
|
|
171
|
-
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));
|
|
172
118
|
}
|
|
173
119
|
}));
|
|
174
|
-
return files.filter((file) => file.length).flat(1); // Remove empty dir and flat it all
|
|
175
|
-
};
|
|
176
|
-
export async function compress(dir, initResult) {
|
|
177
|
-
beginProgress(initResult);
|
|
178
|
-
await processDirectory(dir);
|
|
179
120
|
endProgress();
|
|
180
|
-
printDetails();
|
|
181
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 {};
|