@divriots/jampack 0.2.3 → 0.4.0
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/README.md +34 -59
- package/dist/cache.d.ts +7 -0
- package/dist/cache.js +52 -0
- package/dist/compress.d.ts +4 -2
- package/dist/compress.js +45 -14
- package/dist/config.d.ts +2 -2
- package/dist/index.js +11 -4
- package/dist/optimize.js +209 -111
- package/dist/state.d.ts +6 -4
- package/dist/types.d.ts +2 -2
- package/dist/utils/resource.d.ts +31 -0
- package/dist/utils/resource.js +110 -0
- package/dist/utils.d.ts +0 -2
- package/dist/utils.js +0 -13
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +11 -9
- package/dist/compressors/image.d.ts +0 -1
- package/dist/compressors/image.js +0 -1
- package/dist/optimizers/img.d.ts +0 -1
- package/dist/optimizers/img.js +0 -1
- package/dist/options.d.ts +0 -30
- package/dist/options.js +0 -59
- package/dist/state copy.d.ts +0 -25
- package/dist/state copy.js +0 -45
package/dist/optimize.js
CHANGED
|
@@ -1,30 +1,32 @@
|
|
|
1
|
-
import { globby } from
|
|
2
|
-
import * as path from
|
|
3
|
-
import * as fs from
|
|
4
|
-
import cheerio from
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
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
|
-
const $ = cheerio.load(html, {});
|
|
17
|
-
const
|
|
16
|
+
const $ = cheerio.load(html, { withStartIndices: true });
|
|
17
|
+
const theFold = getTheFold($);
|
|
18
|
+
const imgs = $("img");
|
|
18
19
|
const imgsArray = [];
|
|
19
20
|
imgs.each(async (index, imgElement) => {
|
|
20
21
|
imgsArray.push(imgElement);
|
|
21
22
|
});
|
|
22
23
|
// Process images sequentially
|
|
23
|
-
const spinnerImg = ora({ prefixText:
|
|
24
|
+
const spinnerImg = ora({ prefixText: " " }).start();
|
|
24
25
|
for (let i = 0; i < imgsArray.length; i++) {
|
|
25
26
|
const imgElement = imgsArray[i];
|
|
26
|
-
spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr(
|
|
27
|
-
|
|
27
|
+
spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr("src")} `);
|
|
28
|
+
const isAboveTheFold = imgElement.startIndex < theFold;
|
|
29
|
+
await processImage(file, $, imgElement, isAboveTheFold);
|
|
28
30
|
}
|
|
29
31
|
// Reset spinner
|
|
30
32
|
spinnerImg.text = kleur.dim(`<img> [${imgsArray.length}/${imgsArray.length}]`);
|
|
@@ -32,105 +34,137 @@ async function analyse(file) {
|
|
|
32
34
|
const issues = $state.issues.get(file);
|
|
33
35
|
if (issues) {
|
|
34
36
|
spinnerImg.fail();
|
|
35
|
-
console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ?
|
|
37
|
+
console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? "s" : ""}`));
|
|
36
38
|
}
|
|
37
39
|
else {
|
|
38
40
|
spinnerImg.succeed();
|
|
39
41
|
}
|
|
42
|
+
// Remove the fold
|
|
43
|
+
if (theFold) {
|
|
44
|
+
$("the-fold").remove();
|
|
45
|
+
}
|
|
40
46
|
if (!$state.args.nowrite) {
|
|
41
47
|
await fs.writeFile(path.join($state.dir, file), $.html());
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
|
-
|
|
50
|
+
function getTheFold($) {
|
|
51
|
+
const theFolds = $("the-fold");
|
|
52
|
+
if (!theFolds[0]) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
return theFolds[0].startIndex;
|
|
57
|
+
}
|
|
58
|
+
async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
|
|
45
59
|
try {
|
|
46
60
|
const img = $(imgElement);
|
|
47
61
|
/*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const attrib_src = img.attr(
|
|
62
|
+
* Attribute 'src'
|
|
63
|
+
*/
|
|
64
|
+
const attrib_src = img.attr("src");
|
|
51
65
|
if (!attrib_src) {
|
|
52
|
-
$state.reportIssue(htmlfile, {
|
|
66
|
+
$state.reportIssue(htmlfile, {
|
|
67
|
+
type: "warn",
|
|
68
|
+
msg: `Missing [src] on img - processing skipped.`,
|
|
69
|
+
});
|
|
53
70
|
return;
|
|
54
71
|
}
|
|
55
72
|
/*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const attrib_alt = img.attr(
|
|
73
|
+
* Attribute 'alt'
|
|
74
|
+
*/
|
|
75
|
+
const attrib_alt = img.attr("alt");
|
|
59
76
|
if (attrib_alt === undefined) {
|
|
60
|
-
$state.reportIssue(htmlfile, {
|
|
61
|
-
|
|
77
|
+
$state.reportIssue(htmlfile, {
|
|
78
|
+
type: "a11y",
|
|
79
|
+
msg: `Missing [alt] on img src="${attrib_src}" - Adding alt="" meanwhile.`,
|
|
80
|
+
});
|
|
81
|
+
img.attr("alt", "");
|
|
62
82
|
}
|
|
63
|
-
if (attrib_src.startsWith(
|
|
83
|
+
if (attrib_src.startsWith("data:")) {
|
|
64
84
|
// Data URI image
|
|
65
85
|
// TODO: try to compress it
|
|
66
86
|
return;
|
|
67
87
|
}
|
|
88
|
+
/*
|
|
89
|
+
* Attribute 'loading'
|
|
90
|
+
*/
|
|
91
|
+
const attr_loading = img.attr("loading");
|
|
92
|
+
if (isAboveTheFold) {
|
|
93
|
+
img.removeAttr("loading");
|
|
94
|
+
img.attr("fetchpriority", "high");
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
switch (attr_loading) {
|
|
98
|
+
case undefined:
|
|
99
|
+
// Go lazy by default
|
|
100
|
+
img.attr("loading", "lazy");
|
|
101
|
+
break;
|
|
102
|
+
case "eager":
|
|
103
|
+
img.removeAttr("loading");
|
|
104
|
+
break;
|
|
105
|
+
case "lazy":
|
|
106
|
+
// Don't touch it
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
$state.reportIssue(htmlfile, {
|
|
110
|
+
type: "invalid",
|
|
111
|
+
msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/*
|
|
116
|
+
* Attribute 'decoding'
|
|
117
|
+
*/
|
|
118
|
+
img.attr("decoding", "async");
|
|
68
119
|
if (!isLocal(attrib_src)) {
|
|
69
120
|
// Image not local, don't touch it
|
|
70
121
|
return;
|
|
71
122
|
}
|
|
72
123
|
/*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
img.removeAttr('loading');
|
|
83
|
-
break;
|
|
84
|
-
case 'lazy':
|
|
85
|
-
// Don't touch it
|
|
86
|
-
break;
|
|
87
|
-
default:
|
|
88
|
-
$state.reportIssue(htmlfile, { msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"` });
|
|
89
|
-
}
|
|
90
|
-
/*
|
|
91
|
-
* Attribute 'decoding'
|
|
92
|
-
*/
|
|
93
|
-
img.attr('decoding', 'async');
|
|
94
|
-
/*
|
|
95
|
-
* Loading image
|
|
96
|
-
*/
|
|
97
|
-
const absoluteImgPath = translateSrc($state.dir, path.dirname(htmlfile), attrib_src);
|
|
98
|
-
// file exists?
|
|
99
|
-
try {
|
|
100
|
-
await fs.stat(absoluteImgPath);
|
|
101
|
-
}
|
|
102
|
-
catch (e) {
|
|
103
|
-
$state.reportIssue(htmlfile, { msg: `Can't find img src="${attrib_src}" in ${absoluteImgPath}` });
|
|
124
|
+
* Loading image
|
|
125
|
+
*/
|
|
126
|
+
const originalImage = await Resource.loadResource($state.dir, htmlfile, attrib_src);
|
|
127
|
+
// No file -> give up
|
|
128
|
+
if (!originalImage) {
|
|
129
|
+
$state.reportIssue(htmlfile, {
|
|
130
|
+
type: "erro",
|
|
131
|
+
msg: `Can't find img on disk src="${attrib_src}"`,
|
|
132
|
+
});
|
|
104
133
|
return;
|
|
105
134
|
}
|
|
106
|
-
// Read file
|
|
107
|
-
const originalData = await fs.readFile(absoluteImgPath);
|
|
108
|
-
const sharpFile = sharp(absoluteImgPath);
|
|
109
|
-
const meta = await sharpFile.metadata();
|
|
110
135
|
/*
|
|
111
136
|
* Compress image to WebP
|
|
112
137
|
*/
|
|
113
138
|
let newImage;
|
|
114
|
-
if (!$state.optimizedFiles.has(
|
|
139
|
+
if (!$state.optimizedFiles.has(originalImage.filePathAbsolute)) {
|
|
115
140
|
// Let's avoid to optimize same images twice
|
|
116
|
-
$state.optimizedFiles.add(
|
|
117
|
-
newImage = await compressImage(
|
|
118
|
-
|
|
141
|
+
$state.optimizedFiles.add(originalImage.filePathAbsolute);
|
|
142
|
+
newImage = await compressImage(await originalImage.getData(), {
|
|
143
|
+
toFormat: isAboveTheFold ? "pjpg" : "webp",
|
|
144
|
+
});
|
|
145
|
+
if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) &&
|
|
146
|
+
(newImage.data.length < (await originalImage.getLen()) ||
|
|
147
|
+
isAboveTheFold) // Progressive jpg above the fold should get replaced even if bigger
|
|
148
|
+
) {
|
|
119
149
|
// Do we need to add an new extension?
|
|
120
150
|
const newExtension = `.${newImage.format}`;
|
|
121
|
-
const additionalExtension = path.extname(
|
|
122
|
-
|
|
151
|
+
const additionalExtension = path.extname(originalImage.filePathAbsolute) === newExtension
|
|
152
|
+
? ""
|
|
153
|
+
: newExtension;
|
|
154
|
+
const newFilename = originalImage.filePathAbsolute + additionalExtension;
|
|
123
155
|
if (!$state.args.nowrite) {
|
|
124
156
|
fs.writeFile(newFilename, newImage.data);
|
|
125
157
|
}
|
|
126
158
|
$state.compressedFiles.add(newFilename);
|
|
127
159
|
// Report compression result
|
|
128
160
|
$state.reportSummary({
|
|
129
|
-
action:
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
action: newFilename !== originalImage.filePathAbsolute
|
|
162
|
+
? `${await originalImage.getExt()}->${newImage.format}`
|
|
163
|
+
: path.extname(originalImage.filePathAbsolute),
|
|
164
|
+
originalSize: await originalImage.getLen(),
|
|
165
|
+
compressedSize: newImage.data.length,
|
|
132
166
|
});
|
|
133
|
-
img.attr(
|
|
167
|
+
img.attr("src", attrib_src + additionalExtension);
|
|
134
168
|
}
|
|
135
169
|
}
|
|
136
170
|
/*
|
|
@@ -144,33 +178,33 @@ async function processImage(htmlfile, $, imgElement) {
|
|
|
144
178
|
if (newImage && newImage.data.length <= config.image.embed_size) {
|
|
145
179
|
let datauri = undefined;
|
|
146
180
|
switch (newImage.format) {
|
|
147
|
-
case
|
|
181
|
+
case "svg":
|
|
148
182
|
datauri = svgToMiniDataURI(newImage.data.toString());
|
|
149
183
|
break;
|
|
150
|
-
case
|
|
151
|
-
datauri = `data:image/webp;base64,${newImage.data.toString(
|
|
184
|
+
case "webp":
|
|
185
|
+
datauri = `data:image/webp;base64,${newImage.data.toString("base64")}`;
|
|
152
186
|
break;
|
|
153
|
-
case
|
|
154
|
-
case
|
|
187
|
+
case "jpg":
|
|
188
|
+
case "png":
|
|
155
189
|
// TODO but not possible in current code
|
|
156
190
|
break;
|
|
157
191
|
}
|
|
158
192
|
if (datauri) {
|
|
159
193
|
isEmbed = true;
|
|
160
|
-
img.attr(
|
|
161
|
-
img.removeAttr(
|
|
162
|
-
img.removeAttr(
|
|
163
|
-
$state.reportSummary({
|
|
194
|
+
img.attr("src", datauri);
|
|
195
|
+
img.removeAttr("loading");
|
|
196
|
+
img.removeAttr("decoding");
|
|
197
|
+
$state.reportSummary({
|
|
198
|
+
action: `${newImage.format}->embed`,
|
|
199
|
+
originalSize: await originalImage.getLen(),
|
|
200
|
+
compressedSize: newImage.data.length,
|
|
201
|
+
});
|
|
164
202
|
}
|
|
165
203
|
}
|
|
166
204
|
/*
|
|
167
205
|
* Attribute 'width' & 'height'
|
|
168
206
|
*/
|
|
169
|
-
const [w, h] = setImageSize(img,
|
|
170
|
-
if (w < 0 || h < 0) {
|
|
171
|
-
$state.reportIssue(htmlfile, { msg: `Unexpected error in image size calculation src="${attrib_src}" - can't performance some optmizations.` });
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
207
|
+
const [w, h] = await setImageSize(img, originalImage);
|
|
174
208
|
if (isEmbed) {
|
|
175
209
|
// Image is embed, no need for more processing
|
|
176
210
|
return;
|
|
@@ -178,12 +212,13 @@ async function processImage(htmlfile, $, imgElement) {
|
|
|
178
212
|
//
|
|
179
213
|
// Stop here if svg
|
|
180
214
|
//
|
|
181
|
-
if (
|
|
215
|
+
if ((await originalImage.getExt()) === "svg") {
|
|
182
216
|
return;
|
|
217
|
+
}
|
|
183
218
|
/*
|
|
184
219
|
* Attribute 'srcset'
|
|
185
220
|
*/
|
|
186
|
-
const attr_srcset = img.attr(
|
|
221
|
+
const attr_srcset = img.attr("srcset");
|
|
187
222
|
if (attr_srcset) {
|
|
188
223
|
// If srcset is set, don't touch it.
|
|
189
224
|
// The compress pass will compress the images
|
|
@@ -192,10 +227,10 @@ async function processImage(htmlfile, $, imgElement) {
|
|
|
192
227
|
else {
|
|
193
228
|
// Generate image set
|
|
194
229
|
const ext = path.extname(attrib_src);
|
|
195
|
-
const fullbasename = attrib_src.slice(0, -
|
|
230
|
+
const fullbasename = attrib_src.slice(0, -ext.length);
|
|
196
231
|
const imageSrc = (addition) => `${fullbasename}${addition}.webp`;
|
|
197
232
|
// Start from original image
|
|
198
|
-
let new_srcset =
|
|
233
|
+
let new_srcset = "";
|
|
199
234
|
// Start reduction
|
|
200
235
|
const step = 300; //px
|
|
201
236
|
const ratio = w / h;
|
|
@@ -208,7 +243,7 @@ async function processImage(htmlfile, $, imgElement) {
|
|
|
208
243
|
if (!$state.compressedFiles.has(absoluteFilename)) {
|
|
209
244
|
// Add file to list avoid recompression
|
|
210
245
|
$state.compressedFiles.add(absoluteFilename);
|
|
211
|
-
const compressedImage = await compressImage(
|
|
246
|
+
const compressedImage = await compressImage(await originalImage.getData(), { resize: { width: valueW, height: valueH }, toFormat: "webp" });
|
|
212
247
|
if ((compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) && !$state.args.nowrite) {
|
|
213
248
|
fs.writeFile(absoluteFilename, compressedImage.data);
|
|
214
249
|
}
|
|
@@ -219,17 +254,21 @@ async function processImage(htmlfile, $, imgElement) {
|
|
|
219
254
|
valueH = Math.trunc(valueW / ratio);
|
|
220
255
|
}
|
|
221
256
|
if (new_srcset) {
|
|
222
|
-
img.attr(
|
|
257
|
+
img.attr("srcset", `${img.attr("src")} ${w}w` + new_srcset);
|
|
223
258
|
}
|
|
224
259
|
}
|
|
225
260
|
}
|
|
226
261
|
catch (e) {
|
|
227
|
-
$state.reportIssue(htmlfile, {
|
|
262
|
+
$state.reportIssue(htmlfile, {
|
|
263
|
+
type: "erro",
|
|
264
|
+
msg: e.message ||
|
|
265
|
+
`Unexpected error while processing image: ${JSON.stringify(e)}`,
|
|
266
|
+
});
|
|
228
267
|
}
|
|
229
268
|
}
|
|
230
|
-
function setImageSize(img,
|
|
231
|
-
let width = img.attr(
|
|
232
|
-
let height = img.attr(
|
|
269
|
+
async function setImageSize(img, image) {
|
|
270
|
+
let width = img.attr("width");
|
|
271
|
+
let height = img.attr("height");
|
|
233
272
|
let width_new = undefined;
|
|
234
273
|
let height_new = undefined;
|
|
235
274
|
// Check valid values
|
|
@@ -246,8 +285,12 @@ function setImageSize(img, meta) {
|
|
|
246
285
|
}
|
|
247
286
|
}
|
|
248
287
|
// If we don't have the metadata, we can't do much more
|
|
249
|
-
|
|
250
|
-
|
|
288
|
+
const meta = await image.getImageMeta();
|
|
289
|
+
if (!meta) {
|
|
290
|
+
throw new Error(`Can't get image meta information of "${image.src}" - some optimizations are not possible without this information.`);
|
|
291
|
+
}
|
|
292
|
+
if (meta.width === undefined && meta.height === undefined) {
|
|
293
|
+
throw new Error(`Can't get image width and height of "${image.src}" - some optimizations are not possible without this information.`);
|
|
251
294
|
}
|
|
252
295
|
const originalRatio = meta.width / meta.height;
|
|
253
296
|
if (width !== undefined && height !== undefined) {
|
|
@@ -255,7 +298,7 @@ function setImageSize(img, meta) {
|
|
|
255
298
|
const w = parseInt(width, 10);
|
|
256
299
|
const h = parseInt(height, 10);
|
|
257
300
|
// Is ratio equal?
|
|
258
|
-
const providedRatio = Math.round(w / h * 10) / 10;
|
|
301
|
+
const providedRatio = Math.round((w / h) * 10) / 10;
|
|
259
302
|
const imageRatio = Math.round(originalRatio * 10) / 10;
|
|
260
303
|
if (providedRatio !== imageRatio) {
|
|
261
304
|
//console.warn(`Image aspect ratio in HTML (${providedRatio}) differs from image aspect ratio (${imageRatio}) - fix width and height or let jampack fill them.`);
|
|
@@ -274,26 +317,81 @@ function setImageSize(img, meta) {
|
|
|
274
317
|
}
|
|
275
318
|
else {
|
|
276
319
|
// No width or height provided - set both to image size
|
|
277
|
-
|
|
278
|
-
|
|
320
|
+
if ((await image.getExt()) === "svg") {
|
|
321
|
+
// svg with no height and width has special sizing by browsers
|
|
322
|
+
// They size it inside 300x150 unless they have width and height
|
|
323
|
+
// attributes
|
|
324
|
+
// Load svg
|
|
325
|
+
const c = cheerio.load(await image.getData(), {});
|
|
326
|
+
const svg = c("svg").first();
|
|
327
|
+
const svg_viewbox = svg.attr("viewbox"); // bug in cheerio here, should be "viewBox"
|
|
328
|
+
const svg_width = svg.attr("width");
|
|
329
|
+
const svg_height = svg.attr("height");
|
|
330
|
+
// Calculate aspect ratio from viewbox
|
|
331
|
+
let svg_aspectratio_from_viewbox = undefined;
|
|
332
|
+
if (svg_viewbox) {
|
|
333
|
+
const box = svg_viewbox.split(" ");
|
|
334
|
+
const w = parseInt(box[2], 10);
|
|
335
|
+
const h = parseInt(box[3], 10);
|
|
336
|
+
svg_aspectratio_from_viewbox = w / h;
|
|
337
|
+
}
|
|
338
|
+
// Set size
|
|
339
|
+
//
|
|
340
|
+
if (svg_width &&
|
|
341
|
+
isNumeric(svg_width) &&
|
|
342
|
+
svg_height &&
|
|
343
|
+
isNumeric(svg_height)) {
|
|
344
|
+
// height and width are present
|
|
345
|
+
// use them
|
|
346
|
+
width_new = parseInt(svg_width, 10);
|
|
347
|
+
height_new = parseInt(svg_height, 10);
|
|
348
|
+
}
|
|
349
|
+
else if (svg_width === undefined &&
|
|
350
|
+
svg_height === undefined &&
|
|
351
|
+
svg_aspectratio_from_viewbox) {
|
|
352
|
+
// no height and no width but viewbox is present
|
|
353
|
+
// fit it in default browser box 300x150
|
|
354
|
+
if (svg_aspectratio_from_viewbox >= 2) {
|
|
355
|
+
// fit width
|
|
356
|
+
width_new = 300;
|
|
357
|
+
height_new = width_new / svg_aspectratio_from_viewbox;
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// fit height
|
|
361
|
+
height_new = 150;
|
|
362
|
+
width_new = height_new * svg_aspectratio_from_viewbox;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// no width and no height and no viewbox
|
|
367
|
+
// default browser values
|
|
368
|
+
width_new = 300;
|
|
369
|
+
height_new = 150;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
width_new = meta.width;
|
|
374
|
+
height_new = meta.height;
|
|
375
|
+
}
|
|
279
376
|
}
|
|
280
377
|
// New sizes
|
|
281
378
|
if (width_new !== undefined && height_new !== undefined) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
379
|
+
const result_w = Math.round(width_new);
|
|
380
|
+
const result_h = Math.round(height_new);
|
|
381
|
+
img.attr("width", result_w.toFixed(0));
|
|
382
|
+
img.attr("height", result_h.toFixed(0));
|
|
383
|
+
return [result_w, result_h];
|
|
285
384
|
}
|
|
286
385
|
// Something when wrong
|
|
287
|
-
throw new Error(
|
|
386
|
+
throw new Error(`Unexpected issue when resolving image size "${image.src}"`);
|
|
288
387
|
}
|
|
289
388
|
export async function optimize(exclude) {
|
|
290
|
-
const glob = [
|
|
389
|
+
const glob = ["**/*.{htm,html}"];
|
|
291
390
|
if (exclude)
|
|
292
|
-
glob.push(
|
|
391
|
+
glob.push("!" + exclude);
|
|
293
392
|
const paths = await globby(glob, { cwd: $state.dir });
|
|
294
393
|
// Sequential async
|
|
295
394
|
for (const file of paths) {
|
|
296
395
|
await analyse(file);
|
|
297
396
|
}
|
|
298
|
-
;
|
|
299
397
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type Args = {
|
|
2
2
|
nowrite?: boolean;
|
|
3
|
+
nocache?: boolean;
|
|
3
4
|
};
|
|
4
|
-
export
|
|
5
|
+
export type ReportItem = {
|
|
5
6
|
action: string;
|
|
6
7
|
originalSize: number;
|
|
7
8
|
compressedSize: number;
|
|
8
9
|
};
|
|
9
|
-
|
|
10
|
+
type Summary = {
|
|
10
11
|
nbFiles: number;
|
|
11
12
|
nbFilesCompressed: number;
|
|
12
13
|
dataLenUncompressed: number;
|
|
13
14
|
dataLenCompressed: number;
|
|
14
15
|
};
|
|
15
|
-
|
|
16
|
+
type Issue = {
|
|
17
|
+
type: 'invalid' | 'a11y' | 'perf' | 'erro' | 'warn';
|
|
16
18
|
msg: string;
|
|
17
19
|
};
|
|
18
20
|
export declare class GlobalState {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
export
|
|
3
|
-
export
|
|
2
|
+
export type ImageFormat = 'webp' | 'svg' | 'jpg' | 'png' | undefined;
|
|
3
|
+
export type Image = {
|
|
4
4
|
format: ImageFormat;
|
|
5
5
|
data: Buffer;
|
|
6
6
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { FileExtension, MimeType } from 'file-type';
|
|
3
|
+
type ImageMeta = {
|
|
4
|
+
width: number | undefined;
|
|
5
|
+
height: number | undefined;
|
|
6
|
+
isProgressive: boolean;
|
|
7
|
+
hasAlpha: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* File extension, mime and data are loaded only on demand, then cached.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Resource {
|
|
13
|
+
readonly src: string;
|
|
14
|
+
readonly filePathAbsolute: string;
|
|
15
|
+
private ext;
|
|
16
|
+
private mime;
|
|
17
|
+
private data;
|
|
18
|
+
private image_meta;
|
|
19
|
+
constructor(src: string, filePathAbsolute: string);
|
|
20
|
+
getData(): Promise<Buffer>;
|
|
21
|
+
getImageMeta(): Promise<ImageMeta | null | undefined>;
|
|
22
|
+
getLen(): Promise<number>;
|
|
23
|
+
getExt(): Promise<FileExtension | 'svg'>;
|
|
24
|
+
getMime(): Promise<MimeType | 'image/svg+xml'>;
|
|
25
|
+
private loadFileType;
|
|
26
|
+
static loadResource(projectRoot: string, relativeFile: string, src: string): Promise<Resource | undefined>;
|
|
27
|
+
}
|
|
28
|
+
export declare function isLocal(src: string): boolean;
|
|
29
|
+
export declare function fileExists(path: string): Promise<boolean>;
|
|
30
|
+
export declare function translateSrc(projectRoot: string, htmlRelativePath: string, src: string): string;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as url from 'url';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
5
|
+
import sharp from 'sharp';
|
|
6
|
+
/**
|
|
7
|
+
* File extension, mime and data are loaded only on demand, then cached.
|
|
8
|
+
*/
|
|
9
|
+
export class Resource {
|
|
10
|
+
constructor(src, filePathAbsolute) {
|
|
11
|
+
this.src = src;
|
|
12
|
+
this.filePathAbsolute = filePathAbsolute;
|
|
13
|
+
}
|
|
14
|
+
async getData() {
|
|
15
|
+
if (this.data === undefined) {
|
|
16
|
+
this.data = await fs.readFile(this.filePathAbsolute);
|
|
17
|
+
}
|
|
18
|
+
return this.data;
|
|
19
|
+
}
|
|
20
|
+
async getImageMeta() {
|
|
21
|
+
if (this.image_meta === undefined) {
|
|
22
|
+
const ext = await this.getExt();
|
|
23
|
+
switch (ext) {
|
|
24
|
+
case 'svg':
|
|
25
|
+
case 'gif':
|
|
26
|
+
case 'jpg':
|
|
27
|
+
case 'png':
|
|
28
|
+
case 'webp':
|
|
29
|
+
case 'tif':
|
|
30
|
+
let sharpFile = await sharp(await this.getData(), { animated: true });
|
|
31
|
+
const meta = await sharpFile.metadata();
|
|
32
|
+
this.image_meta = {
|
|
33
|
+
width: meta.width,
|
|
34
|
+
height: meta.height,
|
|
35
|
+
isProgressive: meta.isProgressive || false,
|
|
36
|
+
hasAlpha: meta.hasAlpha || false
|
|
37
|
+
};
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!this.image_meta) {
|
|
42
|
+
console.log(await this.getExt());
|
|
43
|
+
}
|
|
44
|
+
return this.image_meta;
|
|
45
|
+
}
|
|
46
|
+
async getLen() {
|
|
47
|
+
return (await this.getData()).length;
|
|
48
|
+
}
|
|
49
|
+
async getExt() {
|
|
50
|
+
if (this.ext === undefined) {
|
|
51
|
+
await this.loadFileType();
|
|
52
|
+
}
|
|
53
|
+
return this.ext;
|
|
54
|
+
}
|
|
55
|
+
async getMime() {
|
|
56
|
+
if (this.mime === undefined) {
|
|
57
|
+
await this.loadFileType();
|
|
58
|
+
}
|
|
59
|
+
return this.mime;
|
|
60
|
+
}
|
|
61
|
+
async loadFileType() {
|
|
62
|
+
const fileType = await fileTypeFromBuffer(await this.getData());
|
|
63
|
+
if (this.filePathAbsolute.endsWith('.svg')) {
|
|
64
|
+
this.ext = 'svg';
|
|
65
|
+
this.mime = 'image/svg+xml';
|
|
66
|
+
}
|
|
67
|
+
else if (fileType) {
|
|
68
|
+
this.ext = fileType.ext;
|
|
69
|
+
this.mime = fileType.mime;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
throw new Error(`Unknown file type "${this.src}"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
static async loadResource(projectRoot, relativeFile, src) {
|
|
76
|
+
if (!isLocal(src)) {
|
|
77
|
+
throw new Error('src should be local');
|
|
78
|
+
}
|
|
79
|
+
const u = url.parse(src);
|
|
80
|
+
if (!u.pathname) {
|
|
81
|
+
throw new Error(`Invalid src format "${src}"`);
|
|
82
|
+
}
|
|
83
|
+
const relativePath = path.join(projectRoot, src.startsWith('/') ? '' : path.dirname(relativeFile), u.pathname);
|
|
84
|
+
let absolutePath = path.resolve(relativePath);
|
|
85
|
+
if (await fileExists(absolutePath)) {
|
|
86
|
+
return new Resource(src, absolutePath);
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function isLocal(src) {
|
|
92
|
+
const u = url.parse(src);
|
|
93
|
+
return !u.host;
|
|
94
|
+
}
|
|
95
|
+
export async function fileExists(path) {
|
|
96
|
+
try {
|
|
97
|
+
await fs.stat(path);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
export function translateSrc(projectRoot, htmlRelativePath, src) {
|
|
105
|
+
if (!isLocal(src)) {
|
|
106
|
+
throw new Error('Source should be local');
|
|
107
|
+
}
|
|
108
|
+
const srcAbsolutePath = path.join(projectRoot, src.startsWith('/') ? '' : htmlRelativePath, src);
|
|
109
|
+
return path.resolve(srcAbsolutePath);
|
|
110
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
export declare function formatBytes(bytes: number, decimals?: number): string;
|
|
2
|
-
export declare function isLocal(src: string): boolean;
|
|
3
|
-
export declare function translateSrc(projectRoot: string, htmlRelativePath: string, src: string): string;
|
|
4
2
|
export declare function isNumeric(value: string): boolean;
|