@divriots/jampack 0.5.0 → 0.5.1
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.js +33 -12
- package/dist/index.js +35 -12
- package/dist/optimize.js +77 -68
- package/dist/state.js +3 -3
- package/dist/types.d.ts +1 -1
- package/dist/utils/resource.js +2 -1
- package/package.json +2 -2
package/dist/compress.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { optimize as svgo } from
|
|
3
|
+
import { optimize as svgo } from 'svgo';
|
|
4
4
|
import { minify as htmlminifier } from 'html-minifier-terser';
|
|
5
|
-
import { minify as csso } from
|
|
5
|
+
import { minify as csso } from 'csso';
|
|
6
6
|
import { formatBytes } from './utils.js';
|
|
7
7
|
import sharp from 'sharp';
|
|
8
8
|
import swc from '@swc/core';
|
|
@@ -10,7 +10,7 @@ import $state from './state.js';
|
|
|
10
10
|
import { globby } from 'globby';
|
|
11
11
|
import config from './config.js';
|
|
12
12
|
import ora from 'ora';
|
|
13
|
-
import { getFromCacheCompressImage, addToCacheCompressImage, computeCacheHash } from './cache.js';
|
|
13
|
+
import { getFromCacheCompressImage, addToCacheCompressImage, computeCacheHash, } from './cache.js';
|
|
14
14
|
async function compressJS(code) {
|
|
15
15
|
const newjsresult = await swc.minify(code, { compress: true, mangle: true });
|
|
16
16
|
return newjsresult.code;
|
|
@@ -31,6 +31,7 @@ const processFile = async (file, stats) => {
|
|
|
31
31
|
case '.jpeg':
|
|
32
32
|
case '.svg':
|
|
33
33
|
case '.webp':
|
|
34
|
+
case '.avif':
|
|
34
35
|
const newImage = await compressImageFile(file);
|
|
35
36
|
if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) && newImage.data.length < stats.size) {
|
|
36
37
|
writeData = newImage.data;
|
|
@@ -39,7 +40,12 @@ const processFile = async (file, stats) => {
|
|
|
39
40
|
case '.html':
|
|
40
41
|
case '.htm':
|
|
41
42
|
const htmldata = await fs.readFile(file);
|
|
42
|
-
const newhtmlData = await htmlminifier(htmldata.toString(), {
|
|
43
|
+
const newhtmlData = await htmlminifier(htmldata.toString(), {
|
|
44
|
+
minifyCSS: true,
|
|
45
|
+
minifyJS: compressInlineJS,
|
|
46
|
+
sortClassName: true,
|
|
47
|
+
sortAttributes: true,
|
|
48
|
+
});
|
|
43
49
|
writeData = newhtmlData;
|
|
44
50
|
break;
|
|
45
51
|
case '.css':
|
|
@@ -66,7 +72,7 @@ const processFile = async (file, stats) => {
|
|
|
66
72
|
const result = {
|
|
67
73
|
action: path.extname(file),
|
|
68
74
|
originalSize: stats.size,
|
|
69
|
-
compressedSize: stats.size
|
|
75
|
+
compressedSize: stats.size,
|
|
70
76
|
};
|
|
71
77
|
// Writedata
|
|
72
78
|
if (writeData && writeData.length < result.originalSize) {
|
|
@@ -82,7 +88,7 @@ function createWebpOptions(opt) {
|
|
|
82
88
|
return {
|
|
83
89
|
nearLossless: opt.mode === 'lossless',
|
|
84
90
|
quality: opt.quality,
|
|
85
|
-
effort: opt.effort
|
|
91
|
+
effort: opt.effort,
|
|
86
92
|
};
|
|
87
93
|
}
|
|
88
94
|
export const compressImage = async (data, options) => {
|
|
@@ -108,14 +114,15 @@ export const compressImage = async (data, options) => {
|
|
|
108
114
|
case 'svg':
|
|
109
115
|
const newData = svgo(data, {
|
|
110
116
|
multipass: true,
|
|
111
|
-
plugins: [
|
|
112
|
-
|
|
117
|
+
plugins: [
|
|
118
|
+
{
|
|
119
|
+
name: 'preset-default',
|
|
113
120
|
params: {
|
|
114
121
|
overrides: {
|
|
115
122
|
removeViewBox: false,
|
|
116
123
|
},
|
|
117
124
|
},
|
|
118
|
-
}
|
|
125
|
+
},
|
|
119
126
|
],
|
|
120
127
|
});
|
|
121
128
|
if (newData.error || newData.modernError) {
|
|
@@ -144,10 +151,17 @@ export const compressImage = async (data, options) => {
|
|
|
144
151
|
doSharp = true;
|
|
145
152
|
toFormat = 'unchanged'; // Don't need to convert to webP again
|
|
146
153
|
break;
|
|
154
|
+
case 'avif':
|
|
155
|
+
outputFormat = 'avif';
|
|
156
|
+
// TODO but avif is already pretty optimized
|
|
157
|
+
toFormat = 'unchanged';
|
|
158
|
+
break;
|
|
147
159
|
}
|
|
148
160
|
if (doSharp) {
|
|
149
161
|
if (toFormat === 'webp') {
|
|
150
|
-
sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png'
|
|
162
|
+
sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png'
|
|
163
|
+
? config.image.webp.options_lossless
|
|
164
|
+
: config.image.webp.options_lossly));
|
|
151
165
|
outputFormat = 'webp';
|
|
152
166
|
}
|
|
153
167
|
else if (toFormat === 'pjpg') {
|
|
@@ -155,13 +169,20 @@ export const compressImage = async (data, options) => {
|
|
|
155
169
|
outputFormat = 'jpg';
|
|
156
170
|
}
|
|
157
171
|
if (((_a = options.resize) === null || _a === void 0 ? void 0 : _a.width) && ((_b = options.resize) === null || _b === void 0 ? void 0 : _b.height)) {
|
|
158
|
-
sharpFile = sharpFile.resize({
|
|
172
|
+
sharpFile = sharpFile.resize({
|
|
173
|
+
...options.resize,
|
|
174
|
+
fit: 'fill',
|
|
175
|
+
withoutEnlargement: true,
|
|
176
|
+
});
|
|
159
177
|
}
|
|
160
178
|
//
|
|
161
179
|
// Output image processed
|
|
162
180
|
//
|
|
163
181
|
// Add to cache
|
|
164
|
-
const outputImage = {
|
|
182
|
+
const outputImage = {
|
|
183
|
+
format: outputFormat,
|
|
184
|
+
data: await sharpFile.toBuffer(),
|
|
185
|
+
};
|
|
165
186
|
await addToCacheCompressImage(cacheHash, outputImage);
|
|
166
187
|
// Go
|
|
167
188
|
return outputImage;
|
package/dist/index.js
CHANGED
|
@@ -21,11 +21,9 @@ const logo = ` __ __
|
|
|
21
21
|
`;
|
|
22
22
|
console.log(logo);
|
|
23
23
|
const program = new Command();
|
|
24
|
+
program.name('jampack').description('Static website Optimizer').version(VERSION);
|
|
24
25
|
program
|
|
25
|
-
.
|
|
26
|
-
.description('Static website Optimizer')
|
|
27
|
-
.version(VERSION);
|
|
28
|
-
program.command('pack', { isDefault: true })
|
|
26
|
+
.command('pack', { isDefault: true })
|
|
29
27
|
.description('todo')
|
|
30
28
|
.argument('<dir>', 'Directory to pack')
|
|
31
29
|
.option('--exclude <exclude>', 'Glob to exclude')
|
|
@@ -69,26 +67,51 @@ program.parse();
|
|
|
69
67
|
function printSummary() {
|
|
70
68
|
if ($state.summary.nbFiles > 0) {
|
|
71
69
|
printTitle('Summary');
|
|
72
|
-
const dataTable = [
|
|
70
|
+
const dataTable = [
|
|
71
|
+
['Action', 'Compressed', 'Original', 'Compressed', 'Gain'],
|
|
72
|
+
];
|
|
73
73
|
const config = {
|
|
74
74
|
columns: [
|
|
75
75
|
{ alignment: 'left' },
|
|
76
76
|
{ alignment: 'right' },
|
|
77
77
|
{ alignment: 'right' },
|
|
78
78
|
{ alignment: 'right' },
|
|
79
|
-
{ alignment: 'right' }
|
|
79
|
+
{ alignment: 'right' },
|
|
80
80
|
],
|
|
81
81
|
drawHorizontalLine: (lineIndex, rowCount) => {
|
|
82
|
-
return lineIndex === 0 ||
|
|
83
|
-
|
|
82
|
+
return (lineIndex === 0 ||
|
|
83
|
+
lineIndex === 1 ||
|
|
84
|
+
lineIndex === rowCount - 1 ||
|
|
85
|
+
lineIndex === rowCount);
|
|
86
|
+
},
|
|
84
87
|
};
|
|
88
|
+
const unCompressedDataRows = [];
|
|
85
89
|
Object.entries($state.summaryByExtension).forEach(([ext, summary]) => {
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
const gain = summary.dataLenUncompressed - summary.dataLenCompressed;
|
|
91
|
+
const row = [
|
|
92
|
+
ext,
|
|
93
|
+
`${summary.nbFilesCompressed} / ${summary.nbFiles}`,
|
|
94
|
+
formatBytes(summary.dataLenUncompressed),
|
|
95
|
+
formatBytes(summary.dataLenCompressed),
|
|
96
|
+
gain > 0 ? '-' + formatBytes(gain) : '',
|
|
97
|
+
];
|
|
98
|
+
if (gain > 0) {
|
|
88
99
|
dataTable.push(row);
|
|
89
100
|
}
|
|
101
|
+
else {
|
|
102
|
+
unCompressedDataRows.push(row);
|
|
103
|
+
}
|
|
90
104
|
});
|
|
91
|
-
|
|
105
|
+
// Add uncompress rows at the end
|
|
106
|
+
dataTable.push(...unCompressedDataRows);
|
|
107
|
+
const total = [
|
|
108
|
+
'Total',
|
|
109
|
+
`${$state.summary.nbFilesCompressed} / ${$state.summary.nbFiles}`,
|
|
110
|
+
formatBytes($state.summary.dataLenUncompressed),
|
|
111
|
+
formatBytes($state.summary.dataLenCompressed),
|
|
112
|
+
'-' +
|
|
113
|
+
formatBytes($state.summary.dataLenUncompressed - $state.summary.dataLenCompressed),
|
|
114
|
+
];
|
|
92
115
|
dataTable.push(total);
|
|
93
116
|
console.log(table(dataTable, config));
|
|
94
117
|
}
|
|
@@ -103,7 +126,7 @@ function printIssues() {
|
|
|
103
126
|
console.log('');
|
|
104
127
|
for (let [file, list] of $state.issues) {
|
|
105
128
|
console.log('▶ ' + file + '\n');
|
|
106
|
-
list.forEach(issue => {
|
|
129
|
+
list.forEach((issue) => {
|
|
107
130
|
issueCount++;
|
|
108
131
|
console.log(`${kleur.bgYellow(` ${issue.type} `)} ${issue.msg}\n`);
|
|
109
132
|
});
|
package/dist/optimize.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import { globby } from
|
|
2
|
-
import * as path from
|
|
3
|
-
import * as fs from
|
|
4
|
-
import cheerio from
|
|
5
|
-
import { isNumeric } from
|
|
6
|
-
import config from
|
|
7
|
-
import { compressImage } from
|
|
8
|
-
import svgToMiniDataURI from
|
|
9
|
-
import $state from
|
|
10
|
-
import kleur from
|
|
11
|
-
import ora from
|
|
12
|
-
import { isLocal, Resource, translateSrc } from
|
|
1
|
+
import { globby } from 'globby';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import cheerio from 'cheerio';
|
|
5
|
+
import { isNumeric } from './utils.js';
|
|
6
|
+
import config from './config.js';
|
|
7
|
+
import { compressImage } from './compress.js';
|
|
8
|
+
import svgToMiniDataURI from 'mini-svg-data-uri';
|
|
9
|
+
import $state from './state.js';
|
|
10
|
+
import kleur from 'kleur';
|
|
11
|
+
import ora from 'ora';
|
|
12
|
+
import { isLocal, Resource, translateSrc } from './utils/resource.js';
|
|
13
13
|
async function analyse(file) {
|
|
14
|
-
console.log(
|
|
14
|
+
console.log('▶ ' + file);
|
|
15
15
|
const html = (await fs.readFile(path.join($state.dir, file))).toString();
|
|
16
16
|
const $ = cheerio.load(html, {
|
|
17
17
|
withStartIndices: true,
|
|
18
18
|
decodeEntities: false,
|
|
19
19
|
});
|
|
20
20
|
const theFold = getTheFold($);
|
|
21
|
-
const imgs = $(
|
|
21
|
+
const imgs = $('img');
|
|
22
22
|
const imgsArray = [];
|
|
23
23
|
imgs.each(async (index, imgElement) => {
|
|
24
24
|
imgsArray.push(imgElement);
|
|
25
25
|
});
|
|
26
26
|
// Process images sequentially
|
|
27
|
-
const spinnerImg = ora({ prefixText:
|
|
27
|
+
const spinnerImg = ora({ prefixText: ' ' }).start();
|
|
28
28
|
for (let i = 0; i < imgsArray.length; i++) {
|
|
29
29
|
const imgElement = imgsArray[i];
|
|
30
|
-
spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr(
|
|
30
|
+
spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr('src')} `);
|
|
31
31
|
const isAboveTheFold = imgElement.startIndex < theFold;
|
|
32
32
|
await processImage(file, $, imgElement, isAboveTheFold);
|
|
33
33
|
}
|
|
@@ -37,21 +37,21 @@ async function analyse(file) {
|
|
|
37
37
|
const issues = $state.issues.get(file);
|
|
38
38
|
if (issues) {
|
|
39
39
|
spinnerImg.fail();
|
|
40
|
-
console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ?
|
|
40
|
+
console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? 's' : ''}`));
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
43
43
|
spinnerImg.succeed();
|
|
44
44
|
}
|
|
45
45
|
// Remove the fold
|
|
46
46
|
if (theFold) {
|
|
47
|
-
$(
|
|
47
|
+
$('the-fold').remove();
|
|
48
48
|
}
|
|
49
49
|
if (!$state.args.nowrite) {
|
|
50
50
|
await fs.writeFile(path.join($state.dir, file), $.html());
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
function getTheFold($) {
|
|
54
|
-
const theFolds = $(
|
|
54
|
+
const theFolds = $('the-fold');
|
|
55
55
|
if (!theFolds[0]) {
|
|
56
56
|
return 0;
|
|
57
57
|
}
|
|
@@ -64,10 +64,10 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
64
64
|
/*
|
|
65
65
|
* Attribute 'src'
|
|
66
66
|
*/
|
|
67
|
-
const attrib_src = img.attr(
|
|
67
|
+
const attrib_src = img.attr('src');
|
|
68
68
|
if (!attrib_src) {
|
|
69
69
|
$state.reportIssue(htmlfile, {
|
|
70
|
-
type:
|
|
70
|
+
type: 'warn',
|
|
71
71
|
msg: `Missing [src] on img - processing skipped.`,
|
|
72
72
|
});
|
|
73
73
|
return;
|
|
@@ -75,15 +75,15 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
75
75
|
/*
|
|
76
76
|
* Attribute 'alt'
|
|
77
77
|
*/
|
|
78
|
-
const attrib_alt = img.attr(
|
|
78
|
+
const attrib_alt = img.attr('alt');
|
|
79
79
|
if (attrib_alt === undefined) {
|
|
80
80
|
$state.reportIssue(htmlfile, {
|
|
81
|
-
type:
|
|
81
|
+
type: 'a11y',
|
|
82
82
|
msg: `Missing [alt] on img src="${attrib_src}" - Adding alt="" meanwhile.`,
|
|
83
83
|
});
|
|
84
|
-
img.attr(
|
|
84
|
+
img.attr('alt', '');
|
|
85
85
|
}
|
|
86
|
-
if (attrib_src.startsWith(
|
|
86
|
+
if (attrib_src.startsWith('data:')) {
|
|
87
87
|
// Data URI image
|
|
88
88
|
// TODO: try to compress it
|
|
89
89
|
return;
|
|
@@ -91,26 +91,26 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
91
91
|
/*
|
|
92
92
|
* Attribute 'loading'
|
|
93
93
|
*/
|
|
94
|
-
const attr_loading = img.attr(
|
|
94
|
+
const attr_loading = img.attr('loading');
|
|
95
95
|
if (isAboveTheFold) {
|
|
96
|
-
img.removeAttr(
|
|
97
|
-
img.attr(
|
|
96
|
+
img.removeAttr('loading');
|
|
97
|
+
img.attr('fetchpriority', 'high');
|
|
98
98
|
}
|
|
99
99
|
else {
|
|
100
100
|
switch (attr_loading) {
|
|
101
101
|
case undefined:
|
|
102
102
|
// Go lazy by default
|
|
103
|
-
img.attr(
|
|
103
|
+
img.attr('loading', 'lazy');
|
|
104
104
|
break;
|
|
105
|
-
case
|
|
106
|
-
img.removeAttr(
|
|
105
|
+
case 'eager':
|
|
106
|
+
img.removeAttr('loading');
|
|
107
107
|
break;
|
|
108
|
-
case
|
|
108
|
+
case 'lazy':
|
|
109
109
|
// Don't touch it
|
|
110
110
|
break;
|
|
111
111
|
default:
|
|
112
112
|
$state.reportIssue(htmlfile, {
|
|
113
|
-
type:
|
|
113
|
+
type: 'invalid',
|
|
114
114
|
msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"`,
|
|
115
115
|
});
|
|
116
116
|
}
|
|
@@ -118,7 +118,7 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
118
118
|
/*
|
|
119
119
|
* Attribute 'decoding'
|
|
120
120
|
*/
|
|
121
|
-
img.attr(
|
|
121
|
+
img.attr('decoding', 'async');
|
|
122
122
|
if (!isLocal(attrib_src)) {
|
|
123
123
|
// Image not local, don't touch it
|
|
124
124
|
return;
|
|
@@ -130,7 +130,7 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
130
130
|
// No file -> give up
|
|
131
131
|
if (!originalImage) {
|
|
132
132
|
$state.reportIssue(htmlfile, {
|
|
133
|
-
type:
|
|
133
|
+
type: 'erro',
|
|
134
134
|
msg: `Can't find img on disk src="${attrib_src}"`,
|
|
135
135
|
});
|
|
136
136
|
return;
|
|
@@ -145,7 +145,7 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
145
145
|
// Let's avoid to optimize same images twice
|
|
146
146
|
$state.optimizedFiles.add(originalImage.filePathAbsolute);
|
|
147
147
|
newImage = await compressImage(await originalImage.getData(), {
|
|
148
|
-
toFormat: canBeProgressiveJpeg ?
|
|
148
|
+
toFormat: canBeProgressiveJpeg ? 'pjpg' : 'webp',
|
|
149
149
|
});
|
|
150
150
|
if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) &&
|
|
151
151
|
(newImage.data.length < (await originalImage.getLen()) ||
|
|
@@ -154,12 +154,13 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
154
154
|
// Do we need to add an new extension?
|
|
155
155
|
const newExtension = `.${newImage.format}`;
|
|
156
156
|
const additionalExtension = path.extname(originalImage.filePathAbsolute) === newExtension
|
|
157
|
-
?
|
|
157
|
+
? ''
|
|
158
158
|
: newExtension;
|
|
159
159
|
const newFilename = originalImage.filePathAbsolute + additionalExtension;
|
|
160
160
|
if (!$state.args.nowrite) {
|
|
161
161
|
fs.writeFile(newFilename, newImage.data);
|
|
162
162
|
}
|
|
163
|
+
// Mark new file as optimized
|
|
163
164
|
$state.compressedFiles.add(newFilename);
|
|
164
165
|
// Report compression result
|
|
165
166
|
$state.reportSummary({
|
|
@@ -169,7 +170,15 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
169
170
|
originalSize: await originalImage.getLen(),
|
|
170
171
|
compressedSize: newImage.data.length,
|
|
171
172
|
});
|
|
172
|
-
img.attr(
|
|
173
|
+
img.attr('src', attrib_src + additionalExtension);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Report non-compression
|
|
177
|
+
$state.reportSummary({
|
|
178
|
+
action: path.extname(originalImage.filePathAbsolute),
|
|
179
|
+
originalSize: await originalImage.getLen(),
|
|
180
|
+
compressedSize: await originalImage.getLen(),
|
|
181
|
+
});
|
|
173
182
|
}
|
|
174
183
|
}
|
|
175
184
|
/*
|
|
@@ -183,22 +192,22 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
183
192
|
if (newImage && newImage.data.length <= config.image.embed_size) {
|
|
184
193
|
let datauri = undefined;
|
|
185
194
|
switch (newImage.format) {
|
|
186
|
-
case
|
|
195
|
+
case 'svg':
|
|
187
196
|
datauri = svgToMiniDataURI(newImage.data.toString());
|
|
188
197
|
break;
|
|
189
|
-
case
|
|
190
|
-
datauri = `data:image/webp;base64,${newImage.data.toString(
|
|
198
|
+
case 'webp':
|
|
199
|
+
datauri = `data:image/webp;base64,${newImage.data.toString('base64')}`;
|
|
191
200
|
break;
|
|
192
|
-
case
|
|
193
|
-
case
|
|
201
|
+
case 'jpg':
|
|
202
|
+
case 'png':
|
|
194
203
|
// TODO but not possible in current code
|
|
195
204
|
break;
|
|
196
205
|
}
|
|
197
206
|
if (datauri) {
|
|
198
207
|
isEmbed = true;
|
|
199
|
-
img.attr(
|
|
200
|
-
img.removeAttr(
|
|
201
|
-
img.removeAttr(
|
|
208
|
+
img.attr('src', datauri);
|
|
209
|
+
img.removeAttr('loading');
|
|
210
|
+
img.removeAttr('decoding');
|
|
202
211
|
$state.reportSummary({
|
|
203
212
|
action: `${newImage.format}->embed`,
|
|
204
213
|
originalSize: await originalImage.getLen(),
|
|
@@ -217,13 +226,13 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
217
226
|
//
|
|
218
227
|
// Stop here if svg
|
|
219
228
|
//
|
|
220
|
-
if ((await originalImage.getExt()) ===
|
|
229
|
+
if ((await originalImage.getExt()) === 'svg') {
|
|
221
230
|
return;
|
|
222
231
|
}
|
|
223
232
|
/*
|
|
224
233
|
* Attribute 'srcset'
|
|
225
234
|
*/
|
|
226
|
-
const attr_srcset = img.attr(
|
|
235
|
+
const attr_srcset = img.attr('srcset');
|
|
227
236
|
if (attr_srcset) {
|
|
228
237
|
// If srcset is set, don't touch it.
|
|
229
238
|
// The compress pass will compress the images
|
|
@@ -233,9 +242,9 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
233
242
|
// Generate image set
|
|
234
243
|
const ext = path.extname(attrib_src);
|
|
235
244
|
const fullbasename = attrib_src.slice(0, -ext.length);
|
|
236
|
-
const imageSrc = (addition) => `${fullbasename}${addition}.${canBeProgressiveJpeg ?
|
|
245
|
+
const imageSrc = (addition) => `${fullbasename}${addition}.${canBeProgressiveJpeg ? 'jpg' : 'webp'}`;
|
|
237
246
|
// Start from original image
|
|
238
|
-
let new_srcset =
|
|
247
|
+
let new_srcset = '';
|
|
239
248
|
// Start reduction
|
|
240
249
|
const step = 300; //px
|
|
241
250
|
const ratio = w / h;
|
|
@@ -250,7 +259,7 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
250
259
|
$state.compressedFiles.add(absoluteFilename);
|
|
251
260
|
const compressedImage = await compressImage(await originalImage.getData(), {
|
|
252
261
|
resize: { width: valueW, height: valueH },
|
|
253
|
-
toFormat: canBeProgressiveJpeg ?
|
|
262
|
+
toFormat: canBeProgressiveJpeg ? 'pjpg' : 'webp',
|
|
254
263
|
});
|
|
255
264
|
if ((compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) && !$state.args.nowrite) {
|
|
256
265
|
fs.writeFile(absoluteFilename, compressedImage.data);
|
|
@@ -262,28 +271,28 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
262
271
|
valueH = Math.trunc(valueW / ratio);
|
|
263
272
|
}
|
|
264
273
|
if (new_srcset) {
|
|
265
|
-
img.attr(
|
|
274
|
+
img.attr('srcset', `${img.attr('src')} ${w}w` + new_srcset);
|
|
266
275
|
}
|
|
267
276
|
}
|
|
268
277
|
}
|
|
269
278
|
catch (e) {
|
|
270
279
|
$state.reportIssue(htmlfile, {
|
|
271
|
-
type:
|
|
280
|
+
type: 'erro',
|
|
272
281
|
msg: e.message ||
|
|
273
282
|
`Unexpected error while processing image: ${JSON.stringify(e)}`,
|
|
274
283
|
});
|
|
275
284
|
}
|
|
276
285
|
}
|
|
277
286
|
async function setImageSize(htmlfile, img, image) {
|
|
278
|
-
let width = img.attr(
|
|
279
|
-
let height = img.attr(
|
|
287
|
+
let width = img.attr('width');
|
|
288
|
+
let height = img.attr('height');
|
|
280
289
|
let width_new = undefined;
|
|
281
290
|
let height_new = undefined;
|
|
282
291
|
// Check valid values
|
|
283
292
|
if (width !== undefined) {
|
|
284
293
|
if (!isNumeric(width)) {
|
|
285
294
|
$state.reportIssue(htmlfile, {
|
|
286
|
-
type:
|
|
295
|
+
type: 'warn',
|
|
287
296
|
msg: `Invalid width attribute "${width}" format - overriding`,
|
|
288
297
|
});
|
|
289
298
|
width = undefined;
|
|
@@ -292,7 +301,7 @@ async function setImageSize(htmlfile, img, image) {
|
|
|
292
301
|
if (height !== undefined) {
|
|
293
302
|
if (!isNumeric(height)) {
|
|
294
303
|
$state.reportIssue(htmlfile, {
|
|
295
|
-
type:
|
|
304
|
+
type: 'warn',
|
|
296
305
|
msg: `Invalid height attribute "${height}" format - overriding`,
|
|
297
306
|
});
|
|
298
307
|
height = undefined;
|
|
@@ -316,7 +325,7 @@ async function setImageSize(htmlfile, img, image) {
|
|
|
316
325
|
const imageRatio = Math.round(originalRatio * 10) / 10;
|
|
317
326
|
if (providedRatio !== imageRatio) {
|
|
318
327
|
$state.reportIssue(htmlfile, {
|
|
319
|
-
type:
|
|
328
|
+
type: 'warn',
|
|
320
329
|
msg: `Image aspect ratio in HTML (${providedRatio}) differs from image aspect ratio (${imageRatio}) - fix width and height or let jampack fill them.`,
|
|
321
330
|
});
|
|
322
331
|
}
|
|
@@ -334,20 +343,20 @@ async function setImageSize(htmlfile, img, image) {
|
|
|
334
343
|
}
|
|
335
344
|
else {
|
|
336
345
|
// No width or height provided - set both to image size
|
|
337
|
-
if ((await image.getExt()) ===
|
|
346
|
+
if ((await image.getExt()) === 'svg') {
|
|
338
347
|
// svg with no height and width has special sizing by browsers
|
|
339
348
|
// They size it inside 300x150 unless they have width and height
|
|
340
349
|
// attributes
|
|
341
350
|
// Load svg
|
|
342
351
|
const c = cheerio.load(await image.getData(), {});
|
|
343
|
-
const svg = c(
|
|
344
|
-
const svg_viewbox = svg.attr(
|
|
345
|
-
const svg_width = svg.attr(
|
|
346
|
-
const svg_height = svg.attr(
|
|
352
|
+
const svg = c('svg').first();
|
|
353
|
+
const svg_viewbox = svg.attr('viewbox'); // bug in cheerio here, should be "viewBox"
|
|
354
|
+
const svg_width = svg.attr('width');
|
|
355
|
+
const svg_height = svg.attr('height');
|
|
347
356
|
// Calculate aspect ratio from viewbox
|
|
348
357
|
let svg_aspectratio_from_viewbox = undefined;
|
|
349
358
|
if (svg_viewbox) {
|
|
350
|
-
const box = svg_viewbox.split(
|
|
359
|
+
const box = svg_viewbox.split(' ');
|
|
351
360
|
const w = parseInt(box[2], 10);
|
|
352
361
|
const h = parseInt(box[3], 10);
|
|
353
362
|
svg_aspectratio_from_viewbox = w / h;
|
|
@@ -395,17 +404,17 @@ async function setImageSize(htmlfile, img, image) {
|
|
|
395
404
|
if (width_new !== undefined && height_new !== undefined) {
|
|
396
405
|
const result_w = Math.round(width_new);
|
|
397
406
|
const result_h = Math.round(height_new);
|
|
398
|
-
img.attr(
|
|
399
|
-
img.attr(
|
|
407
|
+
img.attr('width', result_w.toFixed(0));
|
|
408
|
+
img.attr('height', result_h.toFixed(0));
|
|
400
409
|
return [result_w, result_h];
|
|
401
410
|
}
|
|
402
411
|
// Something when wrong
|
|
403
412
|
throw new Error(`Unexpected issue when resolving image size "${image.src}"`);
|
|
404
413
|
}
|
|
405
414
|
export async function optimize(exclude) {
|
|
406
|
-
const glob = [
|
|
415
|
+
const glob = ['**/*.{htm,html}'];
|
|
407
416
|
if (exclude)
|
|
408
|
-
glob.push(
|
|
417
|
+
glob.push('!' + exclude);
|
|
409
418
|
const paths = await globby(glob, { cwd: $state.dir });
|
|
410
419
|
// Sequential async
|
|
411
420
|
for (const file of paths) {
|
package/dist/state.js
CHANGED
|
@@ -9,7 +9,7 @@ export class GlobalState {
|
|
|
9
9
|
nbFiles: 0,
|
|
10
10
|
nbFilesCompressed: 0,
|
|
11
11
|
dataLenCompressed: 0,
|
|
12
|
-
dataLenUncompressed: 0
|
|
12
|
+
dataLenUncompressed: 0,
|
|
13
13
|
};
|
|
14
14
|
this.summaryByExtension = {};
|
|
15
15
|
}
|
|
@@ -27,6 +27,7 @@ export class GlobalState {
|
|
|
27
27
|
state.summary.nbFilesCompressed += isCompressed;
|
|
28
28
|
state.summary.dataLenUncompressed += r.originalSize;
|
|
29
29
|
state.summary.dataLenCompressed += r.compressedSize;
|
|
30
|
+
console.log(r.action);
|
|
30
31
|
if (r.action) {
|
|
31
32
|
let summary = this.summaryByExtension[r.action];
|
|
32
33
|
if (!summary) {
|
|
@@ -34,7 +35,7 @@ export class GlobalState {
|
|
|
34
35
|
nbFiles: 0,
|
|
35
36
|
nbFilesCompressed: 0,
|
|
36
37
|
dataLenUncompressed: 0,
|
|
37
|
-
dataLenCompressed: 0
|
|
38
|
+
dataLenCompressed: 0,
|
|
38
39
|
};
|
|
39
40
|
this.summaryByExtension[r.action] = summary;
|
|
40
41
|
}
|
|
@@ -44,7 +45,6 @@ export class GlobalState {
|
|
|
44
45
|
summary.dataLenCompressed += r.compressedSize;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
|
-
;
|
|
48
48
|
}
|
|
49
49
|
const state = new GlobalState();
|
|
50
50
|
export default state;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
export type ImageFormat = 'webp' | 'svg' | 'jpg' | 'png' | undefined;
|
|
2
|
+
export type ImageFormat = 'webp' | 'svg' | 'jpg' | 'png' | 'avif' | undefined;
|
|
3
3
|
export type Image = {
|
|
4
4
|
format: ImageFormat;
|
|
5
5
|
data: Buffer;
|
package/dist/utils/resource.js
CHANGED
|
@@ -27,13 +27,14 @@ export class Resource {
|
|
|
27
27
|
case 'png':
|
|
28
28
|
case 'webp':
|
|
29
29
|
case 'tif':
|
|
30
|
+
case 'avif':
|
|
30
31
|
let sharpFile = await sharp(await this.getData(), { animated: true });
|
|
31
32
|
const meta = await sharpFile.metadata();
|
|
32
33
|
this.image_meta = {
|
|
33
34
|
width: meta.width,
|
|
34
35
|
height: meta.height,
|
|
35
36
|
isProgressive: meta.isProgressive || false,
|
|
36
|
-
hasAlpha: meta.hasAlpha || false
|
|
37
|
+
hasAlpha: meta.hasAlpha || false,
|
|
37
38
|
};
|
|
38
39
|
break;
|
|
39
40
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@divriots/jampack",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Packer for static websites",
|
|
5
5
|
"author": "Georges Gomes @georges-gomes",
|
|
6
6
|
"bin": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"watch": "pnpm build --watch",
|
|
52
|
-
"try": "shx rm -fr demo && cp -R
|
|
52
|
+
"try": "shx rm -fr demo && cp -R demo_avif demo && node ./dist/index.js ./demo",
|
|
53
53
|
"build": "shx rm -rf dist && tsc -b",
|
|
54
54
|
"lint": "eslint . --ext .ts --config .eslintrc"
|
|
55
55
|
},
|