@divriots/jampack 0.34.1 → 0.35.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.
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { MimeType } from 'file-type';
|
|
1
|
+
import { type ResizeOptions } from 'sharp';
|
|
3
2
|
import { GlobalState } from '../state.js';
|
|
4
|
-
export type ImageMimeType =
|
|
3
|
+
export type ImageMimeType = string;
|
|
5
4
|
export declare const AllImageFormat: string[];
|
|
6
5
|
export type ImageFormat = (typeof AllImageFormat)[number] | undefined;
|
|
7
6
|
export type Image = {
|
|
@@ -9,7 +8,7 @@ export type Image = {
|
|
|
9
8
|
data: Buffer;
|
|
10
9
|
};
|
|
11
10
|
export type ImageOutputOptions = {
|
|
12
|
-
resize?:
|
|
11
|
+
resize?: ResizeOptions;
|
|
13
12
|
toFormat?: 'webp' | 'avif' | 'png' | 'jpeg' | 'unchanged';
|
|
14
13
|
};
|
|
15
14
|
export declare function compressImage(state: GlobalState, data: Buffer, options: ImageOutputOptions): Promise<Image | undefined>;
|
|
@@ -9,6 +9,12 @@ function createWebpOptions(opt) {
|
|
|
9
9
|
effort: opt.effort,
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
+
// PNG -> WebP: near-lossless suits flat graphics (logos, icons, UI) but
|
|
13
|
+
// photo-like PNGs (gradients, photos saved as PNG, typical blog heroes) come
|
|
14
|
+
// out 2-12x bigger than lossy and barely shrink when resized. We encode both
|
|
15
|
+
// and keep lossy only when it is at least 10% smaller, so graphics where the gain
|
|
16
|
+
// would be marginal keep their crisp edges.
|
|
17
|
+
const PNG_TO_WEBP_LOSSY_MAX_RATIO = 0.9;
|
|
12
18
|
export async function compressImage(state, data, options) {
|
|
13
19
|
var _a, _b;
|
|
14
20
|
const cacheHash = computeCacheHash(state, data, options);
|
|
@@ -26,6 +32,8 @@ export async function compressImage(state, data, options) {
|
|
|
26
32
|
return undefined;
|
|
27
33
|
}
|
|
28
34
|
let outputFormat;
|
|
35
|
+
// Second encoding to try; kept only if at least 10% smaller than the first one
|
|
36
|
+
let alternativeWebpOptions;
|
|
29
37
|
const imageOptions = state.options.image;
|
|
30
38
|
// Special case for svg
|
|
31
39
|
if (meta.format === 'svg') {
|
|
@@ -63,8 +71,7 @@ export async function compressImage(state, data, options) {
|
|
|
63
71
|
sharpFile = sharpFile.png(imageOptions.png.options || {});
|
|
64
72
|
outputFormat = 'png';
|
|
65
73
|
break;
|
|
66
|
-
case 'jpeg':
|
|
67
|
-
case 'jpg':
|
|
74
|
+
case 'jpeg': // sharp reports JPEG as 'jpeg', never 'jpg'
|
|
68
75
|
sharpFile = sharpFile.jpeg(imageOptions.jpeg.options || {});
|
|
69
76
|
outputFormat = 'jpg';
|
|
70
77
|
break;
|
|
@@ -72,8 +79,7 @@ export async function compressImage(state, data, options) {
|
|
|
72
79
|
sharpFile = sharpFile.webp(createWebpOptions(imageOptions.webp.options_lossly) || {});
|
|
73
80
|
outputFormat = 'webp';
|
|
74
81
|
break;
|
|
75
|
-
case 'heif':
|
|
76
|
-
case 'avif':
|
|
82
|
+
case 'heif': // sharp reports AVIF as 'heif'
|
|
77
83
|
sharpFile = sharpFile.avif({ effort: 4, quality: 50 }); // TODO create config for avif
|
|
78
84
|
outputFormat = 'avif';
|
|
79
85
|
break;
|
|
@@ -91,9 +97,13 @@ export async function compressImage(state, data, options) {
|
|
|
91
97
|
outputFormat = 'png';
|
|
92
98
|
break;
|
|
93
99
|
case 'webp':
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
if (meta.format === 'png') {
|
|
101
|
+
sharpFile = sharpFile.webp(createWebpOptions(imageOptions.webp.options_lossless));
|
|
102
|
+
alternativeWebpOptions = createWebpOptions(imageOptions.webp.options_lossly);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
sharpFile = sharpFile.webp(createWebpOptions(imageOptions.webp.options_lossly));
|
|
106
|
+
}
|
|
97
107
|
outputFormat = 'webp';
|
|
98
108
|
break;
|
|
99
109
|
case 'avif':
|
|
@@ -120,10 +130,25 @@ export async function compressImage(state, data, options) {
|
|
|
120
130
|
//
|
|
121
131
|
// Output image processed
|
|
122
132
|
//
|
|
133
|
+
let outputData;
|
|
134
|
+
if (alternativeWebpOptions) {
|
|
135
|
+
// See PNG_TO_WEBP_LOSSY_MAX_RATIO
|
|
136
|
+
const [primary, alternative] = await Promise.all([
|
|
137
|
+
sharpFile.clone().toBuffer(),
|
|
138
|
+
sharpFile.clone().webp(alternativeWebpOptions).toBuffer(),
|
|
139
|
+
]);
|
|
140
|
+
outputData =
|
|
141
|
+
alternative.length <= primary.length * PNG_TO_WEBP_LOSSY_MAX_RATIO
|
|
142
|
+
? alternative
|
|
143
|
+
: primary;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
outputData = await sharpFile.toBuffer();
|
|
147
|
+
}
|
|
123
148
|
// Add to cache
|
|
124
149
|
const outputImage = {
|
|
125
150
|
format: outputFormat,
|
|
126
|
-
data:
|
|
151
|
+
data: outputData,
|
|
127
152
|
};
|
|
128
153
|
await addToCache(state, 'img', cacheHash, {
|
|
129
154
|
buffer: outputImage.data,
|
|
@@ -5,8 +5,17 @@ import { fileTypeFromBuffer } from 'file-type';
|
|
|
5
5
|
import { addToCache, getFromCache } from '../cache.js';
|
|
6
6
|
import { parse } from '../utils/cache-control-parser.js';
|
|
7
7
|
import '../utils/polyfill-fetch.js';
|
|
8
|
+
// Headers needed to know when a cached image expires and to revalidate it
|
|
9
|
+
function cacheMetaFromHeaders(headers) {
|
|
10
|
+
return {
|
|
11
|
+
Date: headers.get('Date'),
|
|
12
|
+
Expires: headers.get('Expires'),
|
|
13
|
+
LastModified: headers.get('Last-Modified'),
|
|
14
|
+
ETag: headers.get('ETag'),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
8
17
|
export async function downloadExternalImage(state, htmlfile, href) {
|
|
9
|
-
var _a, _b, _c, _d, _e, _f;
|
|
18
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
10
19
|
const hash = hasha(href, {
|
|
11
20
|
algorithm: 'md5',
|
|
12
21
|
});
|
|
@@ -21,8 +30,13 @@ export async function downloadExternalImage(state, htmlfile, href) {
|
|
|
21
30
|
const maxAge = cacheControl['max-age'];
|
|
22
31
|
const expires = (_c = dataFromCache === null || dataFromCache === void 0 ? void 0 : dataFromCache.meta) === null || _c === void 0 ? void 0 : _c.Expires;
|
|
23
32
|
let expireTime = NaN;
|
|
24
|
-
if (
|
|
25
|
-
|
|
33
|
+
if (cacheControl['no-cache'] || cacheControl['no-store']) {
|
|
34
|
+
// Must revalidate every time (can be set by a 304 refresh)
|
|
35
|
+
}
|
|
36
|
+
else if (maxAge !== undefined) {
|
|
37
|
+
// max-age takes precedence over Expires, and 0 means already stale
|
|
38
|
+
if (date)
|
|
39
|
+
expireTime = Date.parse(date) + maxAge * 1000;
|
|
26
40
|
}
|
|
27
41
|
else if (expires) {
|
|
28
42
|
expireTime = Date.parse(expires);
|
|
@@ -40,17 +54,23 @@ export async function downloadExternalImage(state, htmlfile, href) {
|
|
|
40
54
|
}
|
|
41
55
|
// No buffer, then no cache hit so let's HTTP request
|
|
42
56
|
if (!buffer) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
// Revalidate with the validators the server gave us. The response Date
|
|
58
|
+
// must not be used: the image can change after its Last-Modified but
|
|
59
|
+
// before that Date, and the server would then wrongly answer 304.
|
|
60
|
+
const headers = {};
|
|
61
|
+
if ((_d = dataFromCache === null || dataFromCache === void 0 ? void 0 : dataFromCache.meta) === null || _d === void 0 ? void 0 : _d.ETag) {
|
|
62
|
+
headers['If-None-Match'] = dataFromCache.meta.ETag;
|
|
63
|
+
}
|
|
64
|
+
if ((_e = dataFromCache === null || dataFromCache === void 0 ? void 0 : dataFromCache.meta) === null || _e === void 0 ? void 0 : _e.LastModified) {
|
|
65
|
+
headers['If-Modified-Since'] = dataFromCache.meta.LastModified;
|
|
66
|
+
}
|
|
47
67
|
console.log('Downloading ' + href);
|
|
48
|
-
const resp = await fetch(href,
|
|
68
|
+
const resp = await fetch(href, { headers });
|
|
49
69
|
switch (resp.status) {
|
|
50
70
|
case 200: // New image
|
|
51
71
|
const responseBuffer = await resp.arrayBuffer();
|
|
52
72
|
// Detect extension
|
|
53
|
-
ext = (
|
|
73
|
+
ext = (_f = (await fileTypeFromBuffer(responseBuffer))) === null || _f === void 0 ? void 0 : _f.ext;
|
|
54
74
|
buffer = Buffer.from(responseBuffer);
|
|
55
75
|
// Did the server requested no cache?
|
|
56
76
|
const cacheControl = parse(resp.headers.get('Cache-Control') || '');
|
|
@@ -66,7 +86,7 @@ export async function downloadExternalImage(state, htmlfile, href) {
|
|
|
66
86
|
await addToCache(state, 'img-ext', hash, {
|
|
67
87
|
buffer,
|
|
68
88
|
meta: {
|
|
69
|
-
|
|
89
|
+
...cacheMetaFromHeaders(resp.headers),
|
|
70
90
|
Extension: ext,
|
|
71
91
|
CacheControl: cacheControl,
|
|
72
92
|
},
|
|
@@ -77,6 +97,20 @@ export async function downloadExternalImage(state, htmlfile, href) {
|
|
|
77
97
|
if (dataFromCache) {
|
|
78
98
|
buffer = dataFromCache.buffer;
|
|
79
99
|
ext = dataFromCache.meta.Extension;
|
|
100
|
+
// Refresh the cached metadata with the 304 headers so the image is
|
|
101
|
+
// fresh again and isn't revalidated on every build (RFC 9111 4.3.4)
|
|
102
|
+
const newCacheControl = resp.headers.get('Cache-Control');
|
|
103
|
+
const meta = { ...dataFromCache.meta };
|
|
104
|
+
for (const [key, value] of Object.entries(cacheMetaFromHeaders(resp.headers))) {
|
|
105
|
+
if (value)
|
|
106
|
+
meta[key] = value;
|
|
107
|
+
}
|
|
108
|
+
if (newCacheControl)
|
|
109
|
+
meta.CacheControl = parse(newCacheControl);
|
|
110
|
+
await addToCache(state, 'img-ext', hash, {
|
|
111
|
+
buffer: dataFromCache.buffer,
|
|
112
|
+
meta,
|
|
113
|
+
});
|
|
80
114
|
}
|
|
81
115
|
else {
|
|
82
116
|
// This is not possible
|
|
@@ -97,6 +131,6 @@ export async function downloadExternalImage(state, htmlfile, href) {
|
|
|
97
131
|
throw new Error('Unknown image format');
|
|
98
132
|
const htmlFolder = path.dirname(htmlfile);
|
|
99
133
|
const filename = path.relative(path.join(state.dir, htmlFolder), path.join(state.dir, `_jampack/${contentHash}.${ext}`));
|
|
100
|
-
await ((
|
|
134
|
+
await ((_g = state.vfs) !== null && _g !== void 0 ? _g : fsp).writeFile(path.join(state.dir, htmlFolder, filename), buffer);
|
|
101
135
|
return filename;
|
|
102
136
|
}
|
|
@@ -13,7 +13,7 @@ export async function processIframe(state, htmlfile, iframe, isAboveTheFold, app
|
|
|
13
13
|
else if (lazyloadOptions.how === 'js') {
|
|
14
14
|
const src = iframe.attr('src');
|
|
15
15
|
if (src) {
|
|
16
|
-
iframe.
|
|
16
|
+
iframe.addClass('jampack-lozad');
|
|
17
17
|
iframe.attr('data-src', src);
|
|
18
18
|
iframe.removeAttr('src');
|
|
19
19
|
await install_lozad(state, htmlfile, appendToBody);
|
|
@@ -17,7 +17,7 @@ export async function processVideo(state, htmlfile, video, isAboveTheFold, appen
|
|
|
17
17
|
if (lazyloadOptions.when === 'always' ||
|
|
18
18
|
(lazyloadOptions.when === 'below-the-fold' && !isAboveTheFold)) {
|
|
19
19
|
if (lazyloadOptions.how === 'js') {
|
|
20
|
-
video.
|
|
20
|
+
video.addClass('jampack-lozad');
|
|
21
21
|
const src = video.attr('src');
|
|
22
22
|
if (src) {
|
|
23
23
|
video.attr('data-src', src);
|
package/dist/utils/resource.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { FileExtension, MimeType } from 'file-type';
|
|
2
1
|
import { ImageFormat } from '../compressors/images.js';
|
|
3
2
|
import { GlobalState } from '../state.js';
|
|
3
|
+
type FileExtension = string;
|
|
4
|
+
type MimeType = string;
|
|
4
5
|
type ImageMeta = {
|
|
5
6
|
width: number | undefined;
|
|
6
7
|
height: number | undefined;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@divriots/jampack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"cache-version": {
|
|
5
|
-
"img": "
|
|
5
|
+
"img": "v2",
|
|
6
6
|
"img-ext": "v1"
|
|
7
7
|
},
|
|
8
8
|
"description": "Packer for static websites",
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"@divriots/cheerio": "1.0.0-rc.12",
|
|
31
31
|
"@swc/core": "^1.4.16",
|
|
32
32
|
"add": "^2.0.6",
|
|
33
|
-
"browserslist": "^4.
|
|
33
|
+
"browserslist": "^4.28.7",
|
|
34
34
|
"commander": "^12.0.0",
|
|
35
35
|
"critters": "^0.0.22",
|
|
36
36
|
"deepmerge": "^4.3.1",
|
|
37
|
-
"esbuild": "^0.
|
|
37
|
+
"esbuild": "^0.25.12",
|
|
38
38
|
"escalade": "^3.2.0",
|
|
39
|
-
"file-type": "^
|
|
39
|
+
"file-type": "^21.3.4",
|
|
40
40
|
"globby": "^14.0.1",
|
|
41
41
|
"hasha": "^6.0.0",
|
|
42
42
|
"html-minifier-terser": "^7.2.0",
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
"mini-svg-data-uri": "^1.4.4",
|
|
47
47
|
"ora": "^8.0.1",
|
|
48
48
|
"quicklink": "^2.3.0",
|
|
49
|
-
"sharp": "^0.
|
|
50
|
-
"svgo": "^3.
|
|
49
|
+
"sharp": "^0.35.5",
|
|
50
|
+
"svgo": "^3.3.5",
|
|
51
51
|
"table": "^6.8.2",
|
|
52
|
-
"undici": "^6.
|
|
52
|
+
"undici": "^6.29.1",
|
|
53
53
|
"unpic": "^3.18.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
@@ -69,9 +69,19 @@
|
|
|
69
69
|
"cleanAllDotJampack": "find -s . | grep '.jampack$' | xargs rm -fr"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
|
-
"node": ">=
|
|
72
|
+
"node": ">=20.10.0"
|
|
73
73
|
},
|
|
74
74
|
"bugs": "https://github.com/divriots/jampack/issues",
|
|
75
75
|
"keywords": [],
|
|
76
|
-
"types": "dist/index.d.ts"
|
|
76
|
+
"types": "dist/index.d.ts",
|
|
77
|
+
"packageManager": "pnpm@10.33.4",
|
|
78
|
+
"pnpm": {
|
|
79
|
+
"overrides": {
|
|
80
|
+
"@divriots/cheerio>undici": "^6.29.1"
|
|
81
|
+
},
|
|
82
|
+
"onlyBuiltDependencies": [
|
|
83
|
+
"@swc/core",
|
|
84
|
+
"esbuild"
|
|
85
|
+
]
|
|
86
|
+
}
|
|
77
87
|
}
|