@divriots/jampack 0.5.0 → 0.5.2
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/LICENSE +0 -0
- package/README.md +2 -0
- package/dist/compress.d.ts +2 -1
- package/dist/compress.js +91 -65
- package/dist/index.js +35 -12
- package/dist/optimize.js +92 -69
- package/dist/state.js +2 -3
- package/dist/types.d.ts +1 -1
- package/dist/utils/resource.js +2 -1
- package/package.json +9 -9
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ See [the list of features](https://jampack.divriots.com).
|
|
|
68
68
|
- [WebComponents.dev](https://WebComponents.dev)
|
|
69
69
|
- [LWC.studio](https://lwc.studio)
|
|
70
70
|
- [Components.studio](https://components.studio)
|
|
71
|
+
- [keycloak.ch](https://keycloak.ch)
|
|
72
|
+
|
|
71
73
|
> Add yours here
|
|
72
74
|
|
|
73
75
|
## Why is it called `jampack`?
|
package/dist/compress.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
1
2
|
import sharp from 'sharp';
|
|
2
3
|
import type { Image } from './types.js';
|
|
3
4
|
export declare const compressImage: (data: Buffer, options: {
|
|
4
5
|
resize?: sharp.ResizeOptions;
|
|
5
|
-
toFormat?: 'webp' | 'pjpg' | 'unchanged';
|
|
6
|
+
toFormat?: 'webp' | 'pjpg' | 'avif' | 'unchanged';
|
|
6
7
|
}) => Promise<Image | undefined>;
|
|
7
8
|
export declare function compress(exclude: string): Promise<void>;
|
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) => {
|
|
@@ -102,72 +108,92 @@ export const compressImage = async (data, options) => {
|
|
|
102
108
|
// Skip animated images for the moment.
|
|
103
109
|
return undefined;
|
|
104
110
|
}
|
|
105
|
-
let doSharp = false;
|
|
106
111
|
let outputFormat;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
// Special case for svg
|
|
113
|
+
if (meta.format === 'svg') {
|
|
114
|
+
const newData = svgo(data, {
|
|
115
|
+
multipass: true,
|
|
116
|
+
plugins: [
|
|
117
|
+
{
|
|
118
|
+
name: 'preset-default',
|
|
119
|
+
params: {
|
|
120
|
+
overrides: {
|
|
121
|
+
removeViewBox: false,
|
|
117
122
|
},
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
if (newData.error || newData.modernError) {
|
|
128
|
+
console.log(`Error processing svg ${data}`);
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
return { format: 'svg', data: Buffer.from(newData.data, 'utf8') };
|
|
132
|
+
}
|
|
133
|
+
// The bitmap images
|
|
134
|
+
if (toFormat === 'unchanged') {
|
|
135
|
+
switch (meta.format) {
|
|
136
|
+
case 'png':
|
|
129
137
|
sharpFile = sharpFile.png(config.image.png.options || {});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
case 'jpg':
|
|
135
|
-
outputFormat = 'jpg';
|
|
136
|
-
if (toFormat === 'unchanged') {
|
|
138
|
+
outputFormat = 'png';
|
|
139
|
+
break;
|
|
140
|
+
case 'jpeg':
|
|
141
|
+
case 'jpg':
|
|
137
142
|
sharpFile = sharpFile.jpeg(config.image.jpeg.options || {});
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (doSharp) {
|
|
149
|
-
if (toFormat === 'webp') {
|
|
150
|
-
sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png' ? config.image.webp.options_lossless : config.image.webp.options_lossly));
|
|
151
|
-
outputFormat = 'webp';
|
|
152
|
-
}
|
|
153
|
-
else if (toFormat === 'pjpg') {
|
|
154
|
-
sharpFile = sharpFile.jpeg({ ...config.image.jpeg.options, progressive: true } || {});
|
|
155
|
-
outputFormat = 'jpg';
|
|
143
|
+
outputFormat = 'jpg';
|
|
144
|
+
break;
|
|
145
|
+
case 'webp':
|
|
146
|
+
sharpFile = sharpFile.webp(createWebpOptions(config.image.webp.options_lossly) || {});
|
|
147
|
+
outputFormat = 'webp';
|
|
148
|
+
break;
|
|
149
|
+
case 'avif':
|
|
150
|
+
sharpFile = sharpFile.avif();
|
|
151
|
+
outputFormat = 'avif';
|
|
152
|
+
break;
|
|
156
153
|
}
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// To format
|
|
157
|
+
switch (toFormat) {
|
|
158
|
+
case 'pjpg':
|
|
159
|
+
sharpFile = sharpFile.jpeg({ ...config.image.jpeg.options, progressive: true } || {});
|
|
160
|
+
outputFormat = 'jpg';
|
|
161
|
+
break;
|
|
162
|
+
case 'webp':
|
|
163
|
+
sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png'
|
|
164
|
+
? config.image.webp.options_lossless
|
|
165
|
+
: config.image.webp.options_lossly));
|
|
166
|
+
outputFormat = 'webp';
|
|
167
|
+
break;
|
|
168
|
+
case 'avif':
|
|
169
|
+
sharpFile = sharpFile.avif({ effort: 6 });
|
|
170
|
+
outputFormat = 'avif';
|
|
171
|
+
break;
|
|
159
172
|
}
|
|
160
|
-
//
|
|
161
|
-
// Output image processed
|
|
162
|
-
//
|
|
163
|
-
// Add to cache
|
|
164
|
-
const outputImage = { format: outputFormat, data: await sharpFile.toBuffer() };
|
|
165
|
-
await addToCacheCompressImage(cacheHash, outputImage);
|
|
166
|
-
// Go
|
|
167
|
-
return outputImage;
|
|
168
173
|
}
|
|
169
|
-
//
|
|
170
|
-
|
|
174
|
+
// Unknow input format or output format
|
|
175
|
+
// Can't do
|
|
176
|
+
if (!outputFormat)
|
|
177
|
+
return undefined;
|
|
178
|
+
// If resize is requested
|
|
179
|
+
if (((_a = options.resize) === null || _a === void 0 ? void 0 : _a.width) && ((_b = options.resize) === null || _b === void 0 ? void 0 : _b.height)) {
|
|
180
|
+
sharpFile = sharpFile.resize({
|
|
181
|
+
...options.resize,
|
|
182
|
+
fit: 'fill',
|
|
183
|
+
withoutEnlargement: true,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
//
|
|
187
|
+
// Output image processed
|
|
188
|
+
//
|
|
189
|
+
// Add to cache
|
|
190
|
+
const outputImage = {
|
|
191
|
+
format: outputFormat,
|
|
192
|
+
data: await sharpFile.toBuffer(),
|
|
193
|
+
};
|
|
194
|
+
await addToCacheCompressImage(cacheHash, outputImage);
|
|
195
|
+
// Go
|
|
196
|
+
return outputImage;
|
|
171
197
|
};
|
|
172
198
|
const compressImageFile = async (file) => {
|
|
173
199
|
const buffer = await fs.readFile(file);
|
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
|
|
@@ -231,16 +240,22 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
231
240
|
}
|
|
232
241
|
else {
|
|
233
242
|
// Generate image set
|
|
243
|
+
// Avif => Avif
|
|
244
|
+
// png, jpg, ... => WebP
|
|
245
|
+
const targetFormat = (await originalImage.getMime()) === 'image/avif' ? 'avif' : 'webp';
|
|
234
246
|
const ext = path.extname(attrib_src);
|
|
235
247
|
const fullbasename = attrib_src.slice(0, -ext.length);
|
|
236
|
-
const imageSrc = (addition) => `${fullbasename}${addition}.${canBeProgressiveJpeg ?
|
|
248
|
+
const imageSrc = (addition) => `${fullbasename}${addition}.${canBeProgressiveJpeg ? 'jpg' : targetFormat}`;
|
|
237
249
|
// Start from original image
|
|
238
|
-
let new_srcset =
|
|
250
|
+
let new_srcset = '';
|
|
239
251
|
// Start reduction
|
|
240
252
|
const step = 300; //px
|
|
241
253
|
const ratio = w / h;
|
|
242
254
|
let valueW = w - step;
|
|
243
255
|
let valueH = Math.trunc(valueW / ratio);
|
|
256
|
+
let previousImageSize = (newImage === null || newImage === void 0 ? void 0 : newImage.data)
|
|
257
|
+
? Math.min(newImage.data.length, await originalImage.getLen())
|
|
258
|
+
: await originalImage.getLen();
|
|
244
259
|
while (valueW > config.image.srcset_min_width) {
|
|
245
260
|
const src = imageSrc(`@${valueW}w`);
|
|
246
261
|
const absoluteFilename = translateSrc($state.dir, path.dirname(htmlfile), src);
|
|
@@ -250,11 +265,19 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
250
265
|
$state.compressedFiles.add(absoluteFilename);
|
|
251
266
|
const compressedImage = await compressImage(await originalImage.getData(), {
|
|
252
267
|
resize: { width: valueW, height: valueH },
|
|
253
|
-
toFormat: canBeProgressiveJpeg ?
|
|
268
|
+
toFormat: canBeProgressiveJpeg ? 'pjpg' : targetFormat,
|
|
254
269
|
});
|
|
255
|
-
if ((compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data)
|
|
270
|
+
if (!(compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) ||
|
|
271
|
+
compressedImage.data.length >= previousImageSize) {
|
|
272
|
+
// jampack was not able to compress below the original image
|
|
273
|
+
// Let's move on
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (!$state.args.nowrite) {
|
|
256
277
|
fs.writeFile(absoluteFilename, compressedImage.data);
|
|
257
278
|
}
|
|
279
|
+
// Add previous smaller file size
|
|
280
|
+
previousImageSize = compressedImage.data.length;
|
|
258
281
|
}
|
|
259
282
|
new_srcset += `, ${src} ${valueW}w`;
|
|
260
283
|
// reduce size
|
|
@@ -262,28 +285,28 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
262
285
|
valueH = Math.trunc(valueW / ratio);
|
|
263
286
|
}
|
|
264
287
|
if (new_srcset) {
|
|
265
|
-
img.attr(
|
|
288
|
+
img.attr('srcset', `${img.attr('src')} ${w}w` + new_srcset);
|
|
266
289
|
}
|
|
267
290
|
}
|
|
268
291
|
}
|
|
269
292
|
catch (e) {
|
|
270
293
|
$state.reportIssue(htmlfile, {
|
|
271
|
-
type:
|
|
294
|
+
type: 'erro',
|
|
272
295
|
msg: e.message ||
|
|
273
296
|
`Unexpected error while processing image: ${JSON.stringify(e)}`,
|
|
274
297
|
});
|
|
275
298
|
}
|
|
276
299
|
}
|
|
277
300
|
async function setImageSize(htmlfile, img, image) {
|
|
278
|
-
let width = img.attr(
|
|
279
|
-
let height = img.attr(
|
|
301
|
+
let width = img.attr('width');
|
|
302
|
+
let height = img.attr('height');
|
|
280
303
|
let width_new = undefined;
|
|
281
304
|
let height_new = undefined;
|
|
282
305
|
// Check valid values
|
|
283
306
|
if (width !== undefined) {
|
|
284
307
|
if (!isNumeric(width)) {
|
|
285
308
|
$state.reportIssue(htmlfile, {
|
|
286
|
-
type:
|
|
309
|
+
type: 'warn',
|
|
287
310
|
msg: `Invalid width attribute "${width}" format - overriding`,
|
|
288
311
|
});
|
|
289
312
|
width = undefined;
|
|
@@ -292,7 +315,7 @@ async function setImageSize(htmlfile, img, image) {
|
|
|
292
315
|
if (height !== undefined) {
|
|
293
316
|
if (!isNumeric(height)) {
|
|
294
317
|
$state.reportIssue(htmlfile, {
|
|
295
|
-
type:
|
|
318
|
+
type: 'warn',
|
|
296
319
|
msg: `Invalid height attribute "${height}" format - overriding`,
|
|
297
320
|
});
|
|
298
321
|
height = undefined;
|
|
@@ -316,7 +339,7 @@ async function setImageSize(htmlfile, img, image) {
|
|
|
316
339
|
const imageRatio = Math.round(originalRatio * 10) / 10;
|
|
317
340
|
if (providedRatio !== imageRatio) {
|
|
318
341
|
$state.reportIssue(htmlfile, {
|
|
319
|
-
type:
|
|
342
|
+
type: 'warn',
|
|
320
343
|
msg: `Image aspect ratio in HTML (${providedRatio}) differs from image aspect ratio (${imageRatio}) - fix width and height or let jampack fill them.`,
|
|
321
344
|
});
|
|
322
345
|
}
|
|
@@ -334,20 +357,20 @@ async function setImageSize(htmlfile, img, image) {
|
|
|
334
357
|
}
|
|
335
358
|
else {
|
|
336
359
|
// No width or height provided - set both to image size
|
|
337
|
-
if ((await image.getExt()) ===
|
|
360
|
+
if ((await image.getExt()) === 'svg') {
|
|
338
361
|
// svg with no height and width has special sizing by browsers
|
|
339
362
|
// They size it inside 300x150 unless they have width and height
|
|
340
363
|
// attributes
|
|
341
364
|
// Load svg
|
|
342
365
|
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(
|
|
366
|
+
const svg = c('svg').first();
|
|
367
|
+
const svg_viewbox = svg.attr('viewbox'); // bug in cheerio here, should be "viewBox"
|
|
368
|
+
const svg_width = svg.attr('width');
|
|
369
|
+
const svg_height = svg.attr('height');
|
|
347
370
|
// Calculate aspect ratio from viewbox
|
|
348
371
|
let svg_aspectratio_from_viewbox = undefined;
|
|
349
372
|
if (svg_viewbox) {
|
|
350
|
-
const box = svg_viewbox.split(
|
|
373
|
+
const box = svg_viewbox.split(' ');
|
|
351
374
|
const w = parseInt(box[2], 10);
|
|
352
375
|
const h = parseInt(box[3], 10);
|
|
353
376
|
svg_aspectratio_from_viewbox = w / h;
|
|
@@ -395,17 +418,17 @@ async function setImageSize(htmlfile, img, image) {
|
|
|
395
418
|
if (width_new !== undefined && height_new !== undefined) {
|
|
396
419
|
const result_w = Math.round(width_new);
|
|
397
420
|
const result_h = Math.round(height_new);
|
|
398
|
-
img.attr(
|
|
399
|
-
img.attr(
|
|
421
|
+
img.attr('width', result_w.toFixed(0));
|
|
422
|
+
img.attr('height', result_h.toFixed(0));
|
|
400
423
|
return [result_w, result_h];
|
|
401
424
|
}
|
|
402
425
|
// Something when wrong
|
|
403
426
|
throw new Error(`Unexpected issue when resolving image size "${image.src}"`);
|
|
404
427
|
}
|
|
405
428
|
export async function optimize(exclude) {
|
|
406
|
-
const glob = [
|
|
429
|
+
const glob = ['**/*.{htm,html}'];
|
|
407
430
|
if (exclude)
|
|
408
|
-
glob.push(
|
|
431
|
+
glob.push('!' + exclude);
|
|
409
432
|
const paths = await globby(glob, { cwd: $state.dir });
|
|
410
433
|
// Sequential async
|
|
411
434
|
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
|
}
|
|
@@ -34,7 +34,7 @@ export class GlobalState {
|
|
|
34
34
|
nbFiles: 0,
|
|
35
35
|
nbFilesCompressed: 0,
|
|
36
36
|
dataLenUncompressed: 0,
|
|
37
|
-
dataLenCompressed: 0
|
|
37
|
+
dataLenCompressed: 0,
|
|
38
38
|
};
|
|
39
39
|
this.summaryByExtension[r.action] = summary;
|
|
40
40
|
}
|
|
@@ -44,7 +44,6 @@ export class GlobalState {
|
|
|
44
44
|
summary.dataLenCompressed += r.compressedSize;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
;
|
|
48
47
|
}
|
|
49
48
|
const state = new GlobalState();
|
|
50
49
|
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.2",
|
|
4
4
|
"description": "Packer for static websites",
|
|
5
5
|
"author": "Georges Gomes @georges-gomes",
|
|
6
6
|
"bin": {
|
|
@@ -47,16 +47,16 @@
|
|
|
47
47
|
"tslib": "^2.4.1",
|
|
48
48
|
"typescript": "^4.9.3"
|
|
49
49
|
},
|
|
50
|
-
"scripts": {
|
|
51
|
-
"watch": "pnpm build --watch",
|
|
52
|
-
"try": "shx rm -fr demo && cp -R demo_ref demo && node ./dist/index.js",
|
|
53
|
-
"build": "shx rm -rf dist && tsc -b",
|
|
54
|
-
"lint": "eslint . --ext .ts --config .eslintrc"
|
|
55
|
-
},
|
|
56
50
|
"engines": {
|
|
57
51
|
"node": ">=14.0.0"
|
|
58
52
|
},
|
|
59
53
|
"bugs": "https://github.com/divriots/jampack/issues",
|
|
60
54
|
"keywords": [],
|
|
61
|
-
"types": "dist/index.d.ts"
|
|
62
|
-
|
|
55
|
+
"types": "dist/index.d.ts",
|
|
56
|
+
"scripts": {
|
|
57
|
+
"watch": "pnpm build --watch",
|
|
58
|
+
"try": "shx rm -fr demo && cp -R demo_avif demo && node ./dist/index.js ./demo --nocache",
|
|
59
|
+
"build": "shx rm -rf dist && tsc -b",
|
|
60
|
+
"lint": "eslint . --ext .ts --config .eslintrc"
|
|
61
|
+
}
|
|
62
|
+
}
|