@divriots/jampack 0.24.3 → 0.25.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/dist/cache.d.ts +5 -4
- package/dist/cache.js +14 -15
- package/dist/compress.d.ts +2 -1
- package/dist/compress.js +19 -21
- package/dist/compressors/css.d.ts +4 -3
- package/dist/compressors/css.js +5 -5
- package/dist/compressors/html.d.ts +2 -1
- package/dist/compressors/html.js +9 -10
- package/dist/compressors/images.d.ts +2 -1
- package/dist/compressors/images.js +13 -13
- package/dist/compressors/js.d.ts +2 -1
- package/dist/compressors/js.js +2 -3
- package/dist/config-default.js +2 -0
- package/dist/config-types.d.ts +2 -0
- package/dist/config.d.ts +3 -4
- package/dist/config.js +8 -6
- package/dist/index.js +20 -22
- package/dist/optimize.d.ts +2 -1
- package/dist/optimize.js +61 -57
- package/dist/optimizers/img-external.d.ts +2 -1
- package/dist/optimizers/img-external.js +5 -6
- package/dist/optimizers/prefetch-links.d.ts +2 -1
- package/dist/optimizers/prefetch-links.js +7 -6
- package/dist/state.d.ts +3 -2
- package/dist/state.js +7 -5
- package/package.json +3 -2
package/dist/cache.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { GlobalState } from './state.js';
|
|
2
3
|
declare const listOfCategories: readonly ["img", "img-ext"];
|
|
3
4
|
export type Category = (typeof listOfCategories)[number];
|
|
4
5
|
export type CacheData = {
|
|
5
6
|
buffer: Buffer;
|
|
6
7
|
meta: any;
|
|
7
8
|
};
|
|
8
|
-
declare function cleanCache(
|
|
9
|
-
declare function computeCacheHash(buffer: Buffer, options?: any): string;
|
|
10
|
-
declare function getFromCache(category: Category, hash: string): Promise<CacheData | undefined>;
|
|
11
|
-
declare function addToCache(category: Category, hash: string, data: CacheData): Promise<void>;
|
|
9
|
+
declare function cleanCache(state: GlobalState, full?: boolean): Promise<void>;
|
|
10
|
+
declare function computeCacheHash(state: GlobalState, buffer: Buffer, options?: any): string;
|
|
11
|
+
declare function getFromCache(state: GlobalState, category: Category, hash: string): Promise<CacheData | undefined>;
|
|
12
|
+
declare function addToCache(state: GlobalState, category: Category, hash: string, data: CacheData): Promise<void>;
|
|
12
13
|
export { cleanCache, computeCacheHash, getFromCache, addToCache };
|
package/dist/cache.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { hashSync as hasha } from 'hasha';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import * as fs from 'fs/promises';
|
|
4
|
-
import $state from './state.js';
|
|
5
4
|
import { CACHE_VERSIONS } from './packagejson.js';
|
|
6
5
|
const listOfCategories = ['img', 'img-ext'];
|
|
7
|
-
function getCacheFolder() {
|
|
8
|
-
return
|
|
6
|
+
function getCacheFolder(state) {
|
|
7
|
+
return state.args.cache_folder || '.jampack/cache';
|
|
9
8
|
}
|
|
10
|
-
async function cleanCache(full) {
|
|
11
|
-
const CACHE_FOLDER = getCacheFolder();
|
|
9
|
+
async function cleanCache(state, full) {
|
|
10
|
+
const CACHE_FOLDER = getCacheFolder(state);
|
|
12
11
|
if (full) {
|
|
13
12
|
try {
|
|
14
13
|
await fs.rm(CACHE_FOLDER, { recursive: true });
|
|
@@ -49,8 +48,8 @@ async function cleanCache(full) {
|
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
|
-
function computeCacheHash(buffer, options) {
|
|
53
|
-
if (
|
|
51
|
+
function computeCacheHash(state, buffer, options) {
|
|
52
|
+
if (state.args.nocache) {
|
|
54
53
|
return '';
|
|
55
54
|
}
|
|
56
55
|
let hash = `${hasha(buffer, { algorithm: 'sha256' })}`;
|
|
@@ -62,15 +61,15 @@ function computeCacheHash(buffer, options) {
|
|
|
62
61
|
function getVersionOfCategory(category) {
|
|
63
62
|
return CACHE_VERSIONS[category];
|
|
64
63
|
}
|
|
65
|
-
function getLocation(hash, category) {
|
|
66
|
-
const CACHE_FOLDER = getCacheFolder();
|
|
64
|
+
function getLocation(state, hash, category) {
|
|
65
|
+
const CACHE_FOLDER = getCacheFolder(state);
|
|
67
66
|
return path.join(CACHE_FOLDER, category, getVersionOfCategory(category), hash);
|
|
68
67
|
}
|
|
69
|
-
async function getFromCache(category, hash) {
|
|
70
|
-
if (
|
|
68
|
+
async function getFromCache(state, category, hash) {
|
|
69
|
+
if (state.args.nocache) {
|
|
71
70
|
return undefined;
|
|
72
71
|
}
|
|
73
|
-
const location = getLocation(hash, category);
|
|
72
|
+
const location = getLocation(state, hash, category);
|
|
74
73
|
try {
|
|
75
74
|
const buffer = await fs.readFile(path.join(location, 'data'));
|
|
76
75
|
const meta = JSON.parse((await fs.readFile(path.join(location, 'meta'))).toString());
|
|
@@ -81,11 +80,11 @@ async function getFromCache(category, hash) {
|
|
|
81
80
|
}
|
|
82
81
|
return undefined;
|
|
83
82
|
}
|
|
84
|
-
async function addToCache(category, hash, data) {
|
|
85
|
-
if (
|
|
83
|
+
async function addToCache(state, category, hash, data) {
|
|
84
|
+
if (state.args.nocache) {
|
|
86
85
|
return;
|
|
87
86
|
}
|
|
88
|
-
const location = getLocation(hash, category);
|
|
87
|
+
const location = getLocation(state, hash, category);
|
|
89
88
|
await fs.mkdir(location, { recursive: true });
|
|
90
89
|
await fs.writeFile(path.join(location, 'data'), data.buffer);
|
|
91
90
|
await fs.writeFile(path.join(location, 'meta'), JSON.stringify(data.meta));
|
package/dist/compress.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { GlobalState } from './state.js';
|
|
2
|
+
export declare function compressFolder(state: GlobalState, exclude?: string): Promise<void>;
|
package/dist/compress.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { formatBytes } from './utils.js';
|
|
4
|
-
import config from './config.js';
|
|
5
|
-
import $state from './state.js';
|
|
6
4
|
import { globby } from 'globby';
|
|
7
5
|
import ora from 'ora';
|
|
8
6
|
import { compressCSS } from './compressors/css.js';
|
|
9
7
|
import { compressJS } from './compressors/js.js';
|
|
10
8
|
import { compressHTML } from './compressors/html.js';
|
|
11
9
|
import { compressImage } from './compressors/images.js';
|
|
12
|
-
const processFile = async (file, stats) => {
|
|
10
|
+
const processFile = async (state, file, stats) => {
|
|
13
11
|
let writeData = undefined;
|
|
14
12
|
try {
|
|
15
13
|
const ext = path.extname(file);
|
|
@@ -20,9 +18,9 @@ const processFile = async (file, stats) => {
|
|
|
20
18
|
case '.svg':
|
|
21
19
|
case '.webp':
|
|
22
20
|
case '.avif':
|
|
23
|
-
if (
|
|
21
|
+
if (state.options.image.compress) {
|
|
24
22
|
const imgData = await fs.readFile(file);
|
|
25
|
-
const newImage = await compressImage(imgData, {});
|
|
23
|
+
const newImage = await compressImage(state, imgData, {});
|
|
26
24
|
if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) && newImage.data.length < stats.size) {
|
|
27
25
|
writeData = newImage.data;
|
|
28
26
|
}
|
|
@@ -31,19 +29,19 @@ const processFile = async (file, stats) => {
|
|
|
31
29
|
case '.html':
|
|
32
30
|
case '.htm':
|
|
33
31
|
const htmldata = await fs.readFile(file);
|
|
34
|
-
const newhtmlData = await compressHTML(htmldata);
|
|
32
|
+
const newhtmlData = await compressHTML(state, htmldata);
|
|
35
33
|
writeData = newhtmlData;
|
|
36
34
|
break;
|
|
37
35
|
case '.css':
|
|
38
36
|
const cssdata = await fs.readFile(file);
|
|
39
|
-
const newCSS = await compressCSS(cssdata);
|
|
37
|
+
const newCSS = await compressCSS(state, cssdata);
|
|
40
38
|
if (newCSS && newCSS.length < cssdata.length) {
|
|
41
39
|
writeData = newCSS;
|
|
42
40
|
}
|
|
43
41
|
break;
|
|
44
42
|
case '.js':
|
|
45
43
|
const jsdata = await fs.readFile(file);
|
|
46
|
-
const newJS = await compressJS(jsdata.toString());
|
|
44
|
+
const newJS = await compressJS(state, jsdata.toString());
|
|
47
45
|
if (newJS && newJS.length < jsdata.length) {
|
|
48
46
|
writeData = newJS;
|
|
49
47
|
}
|
|
@@ -63,30 +61,30 @@ const processFile = async (file, stats) => {
|
|
|
63
61
|
// Writedata
|
|
64
62
|
if (writeData && writeData.length < result.originalSize) {
|
|
65
63
|
result.compressedSize = writeData.length;
|
|
66
|
-
if (
|
|
64
|
+
if (!state.args.nowrite) {
|
|
67
65
|
await fs.writeFile(file, writeData);
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
state.compressedFiles.add(file);
|
|
69
|
+
state.reportSummary(result);
|
|
72
70
|
};
|
|
73
|
-
export async function compressFolder(exclude) {
|
|
74
|
-
const spinner = ora(getProgressText()).start();
|
|
71
|
+
export async function compressFolder(state, exclude) {
|
|
72
|
+
const spinner = ora(getProgressText(state)).start();
|
|
75
73
|
const globs = ['**/**', '!_jampack/**']; // Exclude jampack folder because already compressed
|
|
76
74
|
if (exclude)
|
|
77
75
|
globs.push('!' + exclude);
|
|
78
|
-
const paths = await globby(globs, { cwd:
|
|
76
|
+
const paths = await globby(globs, { cwd: state.dir, absolute: true });
|
|
79
77
|
// "Parallel" processing
|
|
80
78
|
await Promise.all(paths.map(async (file) => {
|
|
81
|
-
if (
|
|
82
|
-
await processFile(file, await fs.stat(file));
|
|
83
|
-
spinner.text = getProgressText();
|
|
79
|
+
if (!state.compressedFiles.has(file)) {
|
|
80
|
+
await processFile(state, file, await fs.stat(file));
|
|
81
|
+
spinner.text = getProgressText(state);
|
|
84
82
|
}
|
|
85
83
|
}));
|
|
86
|
-
spinner.text = getProgressText();
|
|
84
|
+
spinner.text = getProgressText(state);
|
|
87
85
|
spinner.succeed();
|
|
88
86
|
}
|
|
89
|
-
const getProgressText = () => {
|
|
90
|
-
const gain =
|
|
91
|
-
return `${
|
|
87
|
+
const getProgressText = (state) => {
|
|
88
|
+
const gain = state.summary.dataLenUncompressed - state.summary.dataLenCompressed;
|
|
89
|
+
return `${state.summary.nbFiles} files | ${formatBytes(state.summary.dataLenUncompressed)} → ${formatBytes(state.summary.dataLenCompressed)} | -${formatBytes(gain)} `;
|
|
92
90
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
import {
|
|
3
|
-
export declare
|
|
4
|
-
export declare function
|
|
2
|
+
import { GlobalState } from '../state.js';
|
|
3
|
+
export declare const defaultTargets: () => import("lightningcss").Targets;
|
|
4
|
+
export declare function compressCSS({ targets }: GlobalState, originalCode: Buffer, type?: 'inline' | undefined): Promise<Buffer>;
|
|
5
|
+
export declare function loadConfigCSS(state: GlobalState): void;
|
package/dist/compressors/css.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import browserslist from 'browserslist';
|
|
2
2
|
import { browserslistToTargets, transform as lightcss, transformStyleAttribute as lightcssStyleAttribute, } from 'lightningcss';
|
|
3
|
-
|
|
4
|
-
export async function compressCSS(originalCode, type) {
|
|
3
|
+
export const defaultTargets = () => browserslistToTargets(browserslist('defaults'));
|
|
4
|
+
export async function compressCSS({ targets }, originalCode, type) {
|
|
5
5
|
// Compress with lightningcss
|
|
6
6
|
let lightCSSData = undefined;
|
|
7
7
|
try {
|
|
@@ -28,10 +28,10 @@ export async function compressCSS(originalCode, type) {
|
|
|
28
28
|
}
|
|
29
29
|
let resultBuffer = undefined;
|
|
30
30
|
if (lightCSSData && lightCSSData.length < originalCode.length) {
|
|
31
|
-
resultBuffer = lightCSSData;
|
|
31
|
+
resultBuffer = Buffer.from(lightCSSData);
|
|
32
32
|
}
|
|
33
33
|
return resultBuffer || originalCode;
|
|
34
34
|
}
|
|
35
|
-
export function
|
|
36
|
-
|
|
35
|
+
export function loadConfigCSS(state) {
|
|
36
|
+
const { options } = state;
|
|
37
37
|
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
|
|
2
|
+
import { GlobalState } from '../state.js';
|
|
3
|
+
export declare function compressHTML(state: GlobalState, originalCode: Buffer): Promise<Buffer>;
|
package/dist/compressors/html.js
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
import { minify } from 'html-minifier-terser';
|
|
2
2
|
import { compressCSS } from './css.js';
|
|
3
3
|
import { compressJS } from './js.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const newCode = await compressJS(originalCode);
|
|
4
|
+
async function minifyJSinHTML(state, originalCode) {
|
|
5
|
+
const newCode = await compressJS(state, originalCode);
|
|
7
6
|
if (newCode && newCode.length < originalCode.length)
|
|
8
7
|
return newCode;
|
|
9
8
|
return originalCode;
|
|
10
9
|
}
|
|
11
|
-
async function minifyCSSinHTML(originalCode, type) {
|
|
10
|
+
async function minifyCSSinHTML(state, originalCode, type) {
|
|
12
11
|
// Don't compress media
|
|
13
|
-
if (type
|
|
12
|
+
if (type !== undefined && type !== 'inline')
|
|
14
13
|
return originalCode;
|
|
15
14
|
const originalBuffer = Buffer.from(originalCode);
|
|
16
|
-
const newCSS = await compressCSS(originalBuffer, type);
|
|
15
|
+
const newCSS = await compressCSS(state, originalBuffer, type);
|
|
17
16
|
if (newCSS && newCSS.length > 0 && newCSS.length < originalBuffer.length)
|
|
18
17
|
return newCSS.toString();
|
|
19
18
|
return originalCode;
|
|
20
19
|
}
|
|
21
|
-
export async function compressHTML(originalCode) {
|
|
20
|
+
export async function compressHTML(state, originalCode) {
|
|
22
21
|
const newhtmlData = await minify(originalCode.toString(), {
|
|
23
|
-
minifyCSS: minifyCSSinHTML,
|
|
24
|
-
minifyJS: minifyJSinHTML,
|
|
22
|
+
minifyCSS: (text, type) => minifyCSSinHTML(state, text, type),
|
|
23
|
+
minifyJS: (text) => minifyJSinHTML(state, text),
|
|
25
24
|
sortClassName: true,
|
|
26
|
-
sortAttributes:
|
|
25
|
+
sortAttributes: state.options.html.sort_attributes,
|
|
27
26
|
});
|
|
28
27
|
if (newhtmlData)
|
|
29
28
|
return Buffer.from(newhtmlData, 'utf-8');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
2
|
import sharp from 'sharp';
|
|
3
3
|
import { MimeType } from 'file-type';
|
|
4
|
+
import { GlobalState } from '../state.js';
|
|
4
5
|
export type ImageMimeType = MimeType | 'image/svg+xml';
|
|
5
6
|
export declare const AllImageFormat: string[];
|
|
6
7
|
export type ImageFormat = (typeof AllImageFormat)[number] | undefined;
|
|
@@ -12,4 +13,4 @@ export type ImageOutputOptions = {
|
|
|
12
13
|
resize?: sharp.ResizeOptions;
|
|
13
14
|
toFormat?: 'webp' | 'avif' | 'png' | 'jpeg' | 'unchanged';
|
|
14
15
|
};
|
|
15
|
-
export declare function compressImage(data: Buffer, options: ImageOutputOptions): Promise<Image | undefined>;
|
|
16
|
+
export declare function compressImage(state: GlobalState, data: Buffer, options: ImageOutputOptions): Promise<Image | undefined>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import sharp from 'sharp';
|
|
2
2
|
import { optimize as svgo } from 'svgo';
|
|
3
3
|
import { getFromCache, addToCache, computeCacheHash } from '../cache.js';
|
|
4
|
-
import config from '../config.js';
|
|
5
4
|
export const AllImageFormat = ['webp', 'svg', 'jpg', 'png', 'avif'];
|
|
6
5
|
function createWebpOptions(opt) {
|
|
7
6
|
return {
|
|
@@ -10,10 +9,10 @@ function createWebpOptions(opt) {
|
|
|
10
9
|
effort: opt.effort,
|
|
11
10
|
};
|
|
12
11
|
}
|
|
13
|
-
export async function compressImage(data, options) {
|
|
12
|
+
export async function compressImage(state, data, options) {
|
|
14
13
|
var _a, _b;
|
|
15
|
-
const cacheHash = computeCacheHash(data, options);
|
|
16
|
-
const imageFromCache = await getFromCache('img', cacheHash);
|
|
14
|
+
const cacheHash = computeCacheHash(state, data, options);
|
|
15
|
+
const imageFromCache = await getFromCache(state, 'img', cacheHash);
|
|
17
16
|
if (imageFromCache) {
|
|
18
17
|
return { data: imageFromCache.buffer, format: imageFromCache.meta };
|
|
19
18
|
}
|
|
@@ -27,9 +26,10 @@ export async function compressImage(data, options) {
|
|
|
27
26
|
return undefined;
|
|
28
27
|
}
|
|
29
28
|
let outputFormat;
|
|
29
|
+
const imageOptions = state.options.image;
|
|
30
30
|
// Special case for svg
|
|
31
31
|
if (meta.format === 'svg') {
|
|
32
|
-
if (!
|
|
32
|
+
if (!imageOptions.svg.optimization)
|
|
33
33
|
return undefined;
|
|
34
34
|
try {
|
|
35
35
|
const output = svgo(data.toString(), {
|
|
@@ -60,16 +60,16 @@ export async function compressImage(data, options) {
|
|
|
60
60
|
if (toFormat === 'unchanged') {
|
|
61
61
|
switch (meta.format) {
|
|
62
62
|
case 'png':
|
|
63
|
-
sharpFile = sharpFile.png(
|
|
63
|
+
sharpFile = sharpFile.png(imageOptions.png.options || {});
|
|
64
64
|
outputFormat = 'png';
|
|
65
65
|
break;
|
|
66
66
|
case 'jpeg':
|
|
67
67
|
case 'jpg':
|
|
68
|
-
sharpFile = sharpFile.jpeg(
|
|
68
|
+
sharpFile = sharpFile.jpeg(imageOptions.jpeg.options || {});
|
|
69
69
|
outputFormat = 'jpg';
|
|
70
70
|
break;
|
|
71
71
|
case 'webp':
|
|
72
|
-
sharpFile = sharpFile.webp(createWebpOptions(
|
|
72
|
+
sharpFile = sharpFile.webp(createWebpOptions(imageOptions.webp.options_lossly) || {});
|
|
73
73
|
outputFormat = 'webp';
|
|
74
74
|
break;
|
|
75
75
|
case 'heif':
|
|
@@ -83,17 +83,17 @@ export async function compressImage(data, options) {
|
|
|
83
83
|
// To format
|
|
84
84
|
switch (toFormat) {
|
|
85
85
|
case 'jpeg':
|
|
86
|
-
sharpFile = sharpFile.jpeg({ ...
|
|
86
|
+
sharpFile = sharpFile.jpeg({ ...imageOptions.jpeg.options } || {});
|
|
87
87
|
outputFormat = 'jpg';
|
|
88
88
|
break;
|
|
89
89
|
case 'png':
|
|
90
|
-
sharpFile = sharpFile.png({ ...
|
|
90
|
+
sharpFile = sharpFile.png({ ...imageOptions.png.options } || {});
|
|
91
91
|
outputFormat = 'png';
|
|
92
92
|
break;
|
|
93
93
|
case 'webp':
|
|
94
94
|
sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png'
|
|
95
|
-
?
|
|
96
|
-
:
|
|
95
|
+
? imageOptions.webp.options_lossless
|
|
96
|
+
: imageOptions.webp.options_lossly));
|
|
97
97
|
outputFormat = 'webp';
|
|
98
98
|
break;
|
|
99
99
|
case 'avif':
|
|
@@ -125,7 +125,7 @@ export async function compressImage(data, options) {
|
|
|
125
125
|
format: outputFormat,
|
|
126
126
|
data: await sharpFile.toBuffer(),
|
|
127
127
|
};
|
|
128
|
-
await addToCache('img', cacheHash, {
|
|
128
|
+
await addToCache(state, 'img', cacheHash, {
|
|
129
129
|
buffer: outputImage.data,
|
|
130
130
|
meta: outputImage.format,
|
|
131
131
|
});
|
package/dist/compressors/js.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { GlobalState } from '../state.js';
|
|
2
|
+
export declare function compressJS({ options }: GlobalState, originalCode: string): Promise<string>;
|
package/dist/compressors/js.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import swc from '@swc/core';
|
|
2
2
|
import * as esbuild from 'esbuild';
|
|
3
|
-
|
|
4
|
-
export async function compressJS(originalCode) {
|
|
3
|
+
export async function compressJS({ options }, originalCode) {
|
|
5
4
|
let resultCode = originalCode;
|
|
6
|
-
switch (
|
|
5
|
+
switch (options.js.compressor) {
|
|
7
6
|
case 'esbuild':
|
|
8
7
|
resultCode = (await esbuild.transform(originalCode, {
|
|
9
8
|
minify: true,
|
package/dist/config-default.js
CHANGED
|
@@ -17,6 +17,8 @@ const default_options = {
|
|
|
17
17
|
srcset_min_width: 390 * 2, // HiDPI phone
|
|
18
18
|
srcset_max_width: 1920 * 2, // 4K
|
|
19
19
|
max_width: 99999,
|
|
20
|
+
src_include: /^.*$/,
|
|
21
|
+
src_exclude: /^\/vercel\/image\?/, // Ignore /vervel/image? URLs because not local and most likely already optimized,
|
|
20
22
|
external: {
|
|
21
23
|
process: 'off',
|
|
22
24
|
src_include: /^.*$/,
|
package/dist/config-types.d.ts
CHANGED
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function fast(): void;
|
|
3
|
-
export declare function loadConfig(): Promise<void>;
|
|
4
|
-
export default options;
|
|
1
|
+
import { GlobalState } from './state.js';
|
|
2
|
+
export declare function fast(state: GlobalState): void;
|
|
3
|
+
export declare function loadConfig(state: GlobalState): Promise<void>;
|
package/dist/config.js
CHANGED
|
@@ -3,16 +3,18 @@ import load from '@proload/core';
|
|
|
3
3
|
import deepmerge from 'deepmerge';
|
|
4
4
|
import default_options from './config-default.js';
|
|
5
5
|
import fast_options_override from './config-fast.js';
|
|
6
|
-
|
|
7
|
-
export function fast() {
|
|
8
|
-
|
|
6
|
+
import { loadConfigCSS } from './compressors/css.js';
|
|
7
|
+
export function fast(state) {
|
|
8
|
+
const options = default_options;
|
|
9
|
+
Object.assign(state.options, deepmerge(options, fast_options_override));
|
|
9
10
|
}
|
|
10
|
-
export async function loadConfig() {
|
|
11
|
+
export async function loadConfig(state) {
|
|
12
|
+
const options = default_options;
|
|
11
13
|
const proload = await load('jampack', { mustExist: false });
|
|
12
14
|
if (proload) {
|
|
13
15
|
console.log('Merging default config with:');
|
|
14
16
|
console.log(JSON.stringify(proload.value, null, 2));
|
|
15
|
-
Object.assign(options, deepmerge(options, proload.value));
|
|
17
|
+
Object.assign(state.options, deepmerge(options, proload.value));
|
|
16
18
|
}
|
|
19
|
+
loadConfigCSS(state);
|
|
17
20
|
}
|
|
18
|
-
export default options;
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
2
|
+
import { Command } from '@commander-js/extra-typings';
|
|
3
3
|
import { compressFolder } from './compress.js';
|
|
4
4
|
import { optimize } from './optimize.js';
|
|
5
|
-
import
|
|
5
|
+
import { GlobalState } from './state.js';
|
|
6
6
|
import { table } from 'table';
|
|
7
7
|
import { formatBytes } from './utils.js';
|
|
8
|
-
import
|
|
8
|
+
import { fast, loadConfig } from './config.js';
|
|
9
9
|
import { printTitle } from './logger.js';
|
|
10
10
|
import { exit } from 'process';
|
|
11
11
|
import kleur from 'kleur';
|
|
@@ -13,7 +13,6 @@ import { cleanCache } from './cache.js';
|
|
|
13
13
|
import { VERSION } from './packagejson.js';
|
|
14
14
|
import { mkdirSync } from 'fs';
|
|
15
15
|
import { join } from 'path';
|
|
16
|
-
import { loadConfig as loadConfigCSS } from './compressors/css.js';
|
|
17
16
|
const logo = ` __ __
|
|
18
17
|
|__|____ _____ ___________ ____ | | __
|
|
19
18
|
| \\__ \\ / \\\\____ \\__ \\ _/ ___\\| |/ /
|
|
@@ -43,9 +42,10 @@ program
|
|
|
43
42
|
.option('--cleancache', 'Clean cache before running')
|
|
44
43
|
.option('--nocache', 'Run with no use of cache')
|
|
45
44
|
.action(async (dir, options) => {
|
|
45
|
+
const state = new GlobalState();
|
|
46
46
|
// Arguments
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
state.dir = dir;
|
|
48
|
+
state.args = options;
|
|
49
49
|
// Print options
|
|
50
50
|
if (options) {
|
|
51
51
|
console.log('Options:');
|
|
@@ -53,43 +53,41 @@ program
|
|
|
53
53
|
console.log('');
|
|
54
54
|
}
|
|
55
55
|
// Override default config with config file
|
|
56
|
-
await loadConfig();
|
|
56
|
+
await loadConfig(state);
|
|
57
57
|
// Override config with fast options if set
|
|
58
58
|
if (options.fast) {
|
|
59
|
-
fast();
|
|
59
|
+
fast(state);
|
|
60
60
|
}
|
|
61
61
|
// Clean cache
|
|
62
|
-
await cleanCache(options.cleancache);
|
|
62
|
+
await cleanCache(state, options.cleancache);
|
|
63
63
|
// Make _jampack folder
|
|
64
64
|
try {
|
|
65
65
|
mkdirSync(join(dir, '_jampack'));
|
|
66
66
|
}
|
|
67
67
|
catch (e) {
|
|
68
|
-
console.error('
|
|
68
|
+
console.error('Folder `_jampack` is present in target folder. This means that jampack has already processed this folder. You should always run jampack on clean build of the static website.');
|
|
69
69
|
exit(1);
|
|
70
70
|
}
|
|
71
|
-
// Inject config into modules
|
|
72
|
-
loadConfigCSS(config);
|
|
73
71
|
if (!options.onlycomp) {
|
|
74
72
|
printTitle('PASS 1 - Optimizing');
|
|
75
73
|
console.time('Done');
|
|
76
|
-
await optimize(options.include, options.exclude);
|
|
74
|
+
await optimize(state, options.include, options.exclude);
|
|
77
75
|
console.timeEnd('Done');
|
|
78
76
|
}
|
|
79
77
|
if (!options.onlyoptim && !options.fast) {
|
|
80
78
|
printTitle('PASS 2 - Compressing the rest');
|
|
81
79
|
console.time('Done');
|
|
82
|
-
await compressFolder(options.exclude);
|
|
80
|
+
await compressFolder(state, options.exclude);
|
|
83
81
|
console.timeEnd('Done');
|
|
84
82
|
}
|
|
85
|
-
printSummary();
|
|
86
|
-
printIssues();
|
|
87
|
-
if (options.fail &&
|
|
83
|
+
printSummary(state);
|
|
84
|
+
printIssues(state);
|
|
85
|
+
if (options.fail && state.issues.size > 0) {
|
|
88
86
|
exit(1);
|
|
89
87
|
}
|
|
90
88
|
});
|
|
91
89
|
program.parse();
|
|
92
|
-
function printSummary() {
|
|
90
|
+
function printSummary($state) {
|
|
93
91
|
if ($state.summary.nbFiles > 0) {
|
|
94
92
|
printTitle('Summary');
|
|
95
93
|
const dataTable = [
|
|
@@ -141,22 +139,22 @@ function printSummary() {
|
|
|
141
139
|
console.log(table(dataTable, config));
|
|
142
140
|
}
|
|
143
141
|
}
|
|
144
|
-
function printIssues() {
|
|
142
|
+
function printIssues(state) {
|
|
145
143
|
let issueCount = 0;
|
|
146
|
-
if (
|
|
144
|
+
if (state.issues.size === 0) {
|
|
147
145
|
printTitle('✔ No issues');
|
|
148
146
|
}
|
|
149
147
|
else {
|
|
150
148
|
printTitle('Issues', kleur.bgRed);
|
|
151
149
|
console.log('');
|
|
152
|
-
for (let [file, list] of
|
|
150
|
+
for (let [file, list] of state.issues) {
|
|
153
151
|
console.log('▶ ' + file + '\n');
|
|
154
152
|
list.forEach((issue) => {
|
|
155
153
|
issueCount++;
|
|
156
154
|
console.log(`${kleur.bgYellow(` ${issue.type} `)} ${issue.msg}\n`);
|
|
157
155
|
});
|
|
158
156
|
}
|
|
159
|
-
printTitle(`${issueCount} issue(s) over ${
|
|
157
|
+
printTitle(`${issueCount} issue(s) over ${state.issues.size} files`, kleur.bgYellow);
|
|
160
158
|
console.log('');
|
|
161
159
|
}
|
|
162
160
|
}
|
package/dist/optimize.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { GlobalState } from './state.js';
|
|
2
|
+
export declare function optimize(state: GlobalState, include?: string, exclude?: string): Promise<void>;
|
package/dist/optimize.js
CHANGED
|
@@ -4,10 +4,8 @@ import * as fs from 'fs/promises';
|
|
|
4
4
|
import { getCanonicalCdnForUrl, getTransformer } from 'unpic';
|
|
5
5
|
import * as cheerio from '@divriots/cheerio';
|
|
6
6
|
import { isNumeric } from './utils.js';
|
|
7
|
-
import config from './config.js';
|
|
8
7
|
import { compressImage, } from './compressors/images.js';
|
|
9
8
|
import svgToMiniDataURI from 'mini-svg-data-uri';
|
|
10
|
-
import $state from './state.js';
|
|
11
9
|
import kleur from 'kleur';
|
|
12
10
|
import ora from 'ora';
|
|
13
11
|
import { isLocal, Resource, translateSrc } from './utils/resource.js';
|
|
@@ -25,9 +23,9 @@ function getIntAttr(img, attr) {
|
|
|
25
23
|
return;
|
|
26
24
|
return parsed;
|
|
27
25
|
}
|
|
28
|
-
async function analyse(file) {
|
|
26
|
+
async function analyse(state, file) {
|
|
29
27
|
console.log('▶ ' + file);
|
|
30
|
-
const html = (await fs.readFile(path.join(
|
|
28
|
+
const html = (await fs.readFile(path.join(state.dir, file))).toString();
|
|
31
29
|
if (!html.includes('<html'))
|
|
32
30
|
return;
|
|
33
31
|
const $ = cheerio.load(html, { sourceCodeLocationInfo: true });
|
|
@@ -46,10 +44,10 @@ async function analyse(file) {
|
|
|
46
44
|
const isAboveTheFold = isImgAboveTheFold(img, imgElement, theFold);
|
|
47
45
|
img.removeAttr(ABOVE_FOLD_DATA_ATTR);
|
|
48
46
|
try {
|
|
49
|
-
await processImage(file, img, isAboveTheFold);
|
|
47
|
+
await processImage(state, file, img, isAboveTheFold);
|
|
50
48
|
}
|
|
51
49
|
catch (e) {
|
|
52
|
-
|
|
50
|
+
state.reportIssue(file, {
|
|
53
51
|
type: 'erro',
|
|
54
52
|
msg: e.message ||
|
|
55
53
|
`Unexpected error while processing image: ${JSON.stringify(e)}`,
|
|
@@ -59,7 +57,7 @@ async function analyse(file) {
|
|
|
59
57
|
// Reset spinner
|
|
60
58
|
spinnerImg.text = kleur.dim(`<img> [${imgsArray.length}/${imgsArray.length}]`);
|
|
61
59
|
// Notify issues
|
|
62
|
-
const issues =
|
|
60
|
+
const issues = state.issues.get(file);
|
|
63
61
|
if (issues) {
|
|
64
62
|
spinnerImg.fail();
|
|
65
63
|
console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? 's' : ''}`));
|
|
@@ -82,7 +80,7 @@ async function analyse(file) {
|
|
|
82
80
|
await processIframe(file, $, ifElement, isAboveTheFold);
|
|
83
81
|
}
|
|
84
82
|
catch (e) {
|
|
85
|
-
|
|
83
|
+
state.reportIssue(file, {
|
|
86
84
|
type: 'erro',
|
|
87
85
|
msg: e.message ||
|
|
88
86
|
`Unexpected error while processing image: ${JSON.stringify(e)}`,
|
|
@@ -104,7 +102,8 @@ async function analyse(file) {
|
|
|
104
102
|
const heads = $('head'); // always exists because doc loaded with isDocument=true (default)
|
|
105
103
|
const charsetElements$ = heads.find('meta[charset]');
|
|
106
104
|
const wasCharsetFirst = !charsetElements$.prev().length;
|
|
107
|
-
|
|
105
|
+
const { options } = state;
|
|
106
|
+
if (options.html.add_css_reset_as === 'inline') {
|
|
108
107
|
prependToHead += `<style>:where(img){height:auto;}</style>`;
|
|
109
108
|
}
|
|
110
109
|
if (prependToHead || appendToHead) {
|
|
@@ -115,7 +114,7 @@ async function analyse(file) {
|
|
|
115
114
|
}
|
|
116
115
|
switch (charsetElements$.length) {
|
|
117
116
|
case 0:
|
|
118
|
-
|
|
117
|
+
state.reportIssue(file, {
|
|
119
118
|
type: 'fix',
|
|
120
119
|
msg: 'Adding missing <meta charset="utf-8"> to the top of the <head>',
|
|
121
120
|
});
|
|
@@ -123,7 +122,7 @@ async function analyse(file) {
|
|
|
123
122
|
break;
|
|
124
123
|
case 1:
|
|
125
124
|
if (!wasCharsetFirst) {
|
|
126
|
-
|
|
125
|
+
state.reportIssue(file, {
|
|
127
126
|
type: 'perf',
|
|
128
127
|
msg: 'Moving <meta charset> to the top of the <head>',
|
|
129
128
|
});
|
|
@@ -132,7 +131,7 @@ async function analyse(file) {
|
|
|
132
131
|
heads.prepend(charsetElements$);
|
|
133
132
|
break;
|
|
134
133
|
default:
|
|
135
|
-
|
|
134
|
+
state.reportIssue(file, {
|
|
136
135
|
type: 'fix',
|
|
137
136
|
msg: 'Multiple <meta charset> found in the <head>: taking only the first one and deleting the others',
|
|
138
137
|
});
|
|
@@ -142,8 +141,8 @@ async function analyse(file) {
|
|
|
142
141
|
// Add to <body>
|
|
143
142
|
//
|
|
144
143
|
let appendToBody = '';
|
|
145
|
-
if (
|
|
146
|
-
appendToBody += await prefetch_links_in_viewport(file);
|
|
144
|
+
if (options.misc.prefetch_links === 'in-viewport') {
|
|
145
|
+
appendToBody += await prefetch_links_in_viewport(state, file);
|
|
147
146
|
}
|
|
148
147
|
if (appendToBody) {
|
|
149
148
|
const body = $('body');
|
|
@@ -156,16 +155,16 @@ async function analyse(file) {
|
|
|
156
155
|
let htmlResult = $.html();
|
|
157
156
|
// Inline critical css
|
|
158
157
|
//
|
|
159
|
-
if (
|
|
158
|
+
if (options.css.inline_critical_css) {
|
|
160
159
|
try {
|
|
161
|
-
htmlResult = await inlineCriticalCss(
|
|
160
|
+
htmlResult = await inlineCriticalCss(state.dir, htmlResult);
|
|
162
161
|
}
|
|
163
162
|
catch (e) {
|
|
164
163
|
console.warn('Fail to inline critical CSS', e);
|
|
165
164
|
}
|
|
166
165
|
}
|
|
167
|
-
if (
|
|
168
|
-
await fs.writeFile(path.join(
|
|
166
|
+
if (!state.args.nowrite) {
|
|
167
|
+
await fs.writeFile(path.join(state.dir, file), htmlResult);
|
|
169
168
|
}
|
|
170
169
|
}
|
|
171
170
|
function isImgAboveTheFold(img, imgElement, theFold) {
|
|
@@ -200,7 +199,7 @@ function getTheFold($) {
|
|
|
200
199
|
}
|
|
201
200
|
return 0;
|
|
202
201
|
}
|
|
203
|
-
async function processImage(htmlfile, img, isAboveTheFold) {
|
|
202
|
+
async function processImage(state, htmlfile, img, isAboveTheFold) {
|
|
204
203
|
var _a, _b;
|
|
205
204
|
/*
|
|
206
205
|
* Attribute 'src'
|
|
@@ -214,18 +213,23 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
214
213
|
// Let's not mess with it and hope it's well done
|
|
215
214
|
return; // No warnings
|
|
216
215
|
}
|
|
217
|
-
|
|
216
|
+
state.reportIssue(htmlfile, {
|
|
218
217
|
type: 'warn',
|
|
219
218
|
msg: `Missing [src] on img - processing skipped.`,
|
|
220
219
|
});
|
|
221
220
|
return;
|
|
222
221
|
}
|
|
222
|
+
const config = state.options;
|
|
223
|
+
if (!isIncluded(attrib_src, config.image.src_include, config.image.src_exclude)) {
|
|
224
|
+
// Setup to ignore
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
223
227
|
/*
|
|
224
228
|
* Attribute 'alt'
|
|
225
229
|
*/
|
|
226
230
|
const attrib_alt = img.attr('alt');
|
|
227
231
|
if (attrib_alt === undefined) {
|
|
228
|
-
|
|
232
|
+
state.reportIssue(htmlfile, {
|
|
229
233
|
type: 'a11y',
|
|
230
234
|
msg: `Missing [alt] on img src="${attrib_src}" - Adding alt="" meanwhile.`,
|
|
231
235
|
});
|
|
@@ -257,7 +261,7 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
257
261
|
// Don't touch it
|
|
258
262
|
break;
|
|
259
263
|
default:
|
|
260
|
-
|
|
264
|
+
state.reportIssue(htmlfile, {
|
|
261
265
|
type: 'invalid',
|
|
262
266
|
msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"`,
|
|
263
267
|
});
|
|
@@ -290,7 +294,7 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
290
294
|
break;
|
|
291
295
|
let attrib_width = getIntAttr(img, 'width');
|
|
292
296
|
if (!attrib_width) {
|
|
293
|
-
|
|
297
|
+
state.reportIssue(htmlfile, {
|
|
294
298
|
type: 'warn',
|
|
295
299
|
msg: `Missing or malformed'width' attribute for image ${attrib_src}, unable to perform CDN transform`,
|
|
296
300
|
});
|
|
@@ -298,13 +302,13 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
298
302
|
}
|
|
299
303
|
let attrib_height = getIntAttr(img, 'height');
|
|
300
304
|
if (!attrib_height) {
|
|
301
|
-
|
|
305
|
+
state.reportIssue(htmlfile, {
|
|
302
306
|
type: 'warn',
|
|
303
307
|
msg: `Missing or malformed 'height' attribute for image ${attrib_src}, unable to perform CDN transform`,
|
|
304
308
|
});
|
|
305
309
|
return;
|
|
306
310
|
}
|
|
307
|
-
const new_srcset = await generateSrcSetForCdn(attrib_src, cdnTransformer, attrib_width, attrib_height);
|
|
311
|
+
const new_srcset = await generateSrcSetForCdn(state, attrib_src, cdnTransformer, attrib_width, attrib_height);
|
|
308
312
|
if (new_srcset !== null) {
|
|
309
313
|
img.attr('srcset', new_srcset);
|
|
310
314
|
}
|
|
@@ -322,11 +326,11 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
322
326
|
if (!isIncluded(attrib_src, config.image.external.src_include, config.image.external.src_exclude))
|
|
323
327
|
break;
|
|
324
328
|
try {
|
|
325
|
-
attrib_src = await downloadExternalImage(htmlfile, attrib_src);
|
|
329
|
+
attrib_src = await downloadExternalImage(state, htmlfile, attrib_src);
|
|
326
330
|
img.attr('src', attrib_src);
|
|
327
331
|
}
|
|
328
332
|
catch (e) {
|
|
329
|
-
|
|
333
|
+
state.reportIssue(htmlfile, {
|
|
330
334
|
type: 'warn',
|
|
331
335
|
msg: `Failed to download image: ${attrib_src} - ${e.message}`,
|
|
332
336
|
});
|
|
@@ -338,10 +342,10 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
338
342
|
/*
|
|
339
343
|
* Loading image
|
|
340
344
|
*/
|
|
341
|
-
const originalImage = await Resource.loadResource(
|
|
345
|
+
const originalImage = await Resource.loadResource(state.dir, htmlfile, attrib_src);
|
|
342
346
|
// No file -> give up
|
|
343
347
|
if (!originalImage) {
|
|
344
|
-
|
|
348
|
+
state.reportIssue(htmlfile, {
|
|
345
349
|
type: 'erro',
|
|
346
350
|
msg: `Can't find img on disk src="${attrib_src}"`,
|
|
347
351
|
});
|
|
@@ -365,7 +369,7 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
365
369
|
: 'webp';
|
|
366
370
|
}
|
|
367
371
|
if (config.image.compress) {
|
|
368
|
-
newImage = await compressImage(await originalImage.getData(), {
|
|
372
|
+
newImage = await compressImage(state, await originalImage.getData(), {
|
|
369
373
|
toFormat: srcToFormat,
|
|
370
374
|
resize: { width: config.image.srcset_max_width },
|
|
371
375
|
});
|
|
@@ -374,13 +378,13 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
374
378
|
const newFilename = path.join(path.dirname(originalImage.filePathAbsolute), path.basename(originalImage.filePathAbsolute, path.extname(originalImage.filePathAbsolute)) + `.${newImage.format}`);
|
|
375
379
|
const newSrc = path.join(path.dirname(attrib_src), path.basename(attrib_src, path.extname(attrib_src)) +
|
|
376
380
|
`.${newImage.format}`);
|
|
377
|
-
if (
|
|
381
|
+
if (!state.compressedFiles.has(newFilename) && !state.args.nowrite) {
|
|
378
382
|
fs.writeFile(newFilename, newImage.data);
|
|
379
383
|
}
|
|
380
384
|
// Mark new file as compressed
|
|
381
|
-
|
|
385
|
+
state.compressedFiles.add(newFilename);
|
|
382
386
|
// Report compression result
|
|
383
|
-
|
|
387
|
+
state.reportSummary({
|
|
384
388
|
action: newFilename !== originalImage.filePathAbsolute
|
|
385
389
|
? `${await originalImage.getExt()}->${newImage.format}`
|
|
386
390
|
: path.extname(originalImage.filePathAbsolute),
|
|
@@ -393,7 +397,7 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
393
397
|
// Drop new Image
|
|
394
398
|
newImage = undefined;
|
|
395
399
|
// Report non-compression
|
|
396
|
-
|
|
400
|
+
state.reportSummary({
|
|
397
401
|
action: path.extname(originalImage.filePathAbsolute),
|
|
398
402
|
originalSize: await originalImage.getLen(),
|
|
399
403
|
compressedSize: await originalImage.getLen(),
|
|
@@ -433,7 +437,7 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
433
437
|
img.attr('src', datauri);
|
|
434
438
|
img.removeAttr('loading');
|
|
435
439
|
img.removeAttr('decoding');
|
|
436
|
-
|
|
440
|
+
state.reportSummary({
|
|
437
441
|
action: `${imageToEmbed.format}->embed`,
|
|
438
442
|
originalSize: await originalImage.getLen(),
|
|
439
443
|
compressedSize: imageToEmbed.data.length,
|
|
@@ -450,7 +454,7 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
450
454
|
/*
|
|
451
455
|
* Attribute 'width' & 'height'
|
|
452
456
|
*/
|
|
453
|
-
const [w, h] = await setImageSize(htmlfile, img, originalImage);
|
|
457
|
+
const [w, h] = await setImageSize(state, htmlfile, img, originalImage);
|
|
454
458
|
if (isEmbed) {
|
|
455
459
|
// Image is embed, no need for more processing
|
|
456
460
|
return;
|
|
@@ -475,7 +479,7 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
475
479
|
// of the srcset
|
|
476
480
|
}
|
|
477
481
|
else {
|
|
478
|
-
const new_srcset = await generateSrcSet(htmlfile, originalImage, img.attr('src'), imageLengthToBeatInSrcSet, { toFormat: srcToFormat });
|
|
482
|
+
const new_srcset = await generateSrcSet(state, htmlfile, originalImage, img.attr('src'), imageLengthToBeatInSrcSet, { toFormat: srcToFormat });
|
|
479
483
|
if (new_srcset !== null) {
|
|
480
484
|
img.attr('srcset', new_srcset);
|
|
481
485
|
}
|
|
@@ -529,7 +533,7 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
529
533
|
// Ignore the creation of sources that already exist
|
|
530
534
|
continue;
|
|
531
535
|
}
|
|
532
|
-
const srcset = await generateSrcSet(htmlfile, originalImage, undefined, undefined, {
|
|
536
|
+
const srcset = await generateSrcSet(state, htmlfile, originalImage, undefined, undefined, {
|
|
533
537
|
toFormat: s.format,
|
|
534
538
|
});
|
|
535
539
|
if (!srcset) {
|
|
@@ -543,7 +547,7 @@ async function processImage(htmlfile, img, isAboveTheFold) {
|
|
|
543
547
|
}
|
|
544
548
|
}
|
|
545
549
|
const isIncluded = (src, includeConf, excludeConf) => !!src.match(includeConf) && (!excludeConf || !src.match(excludeConf));
|
|
546
|
-
async function _generateSrcSet(startSrc, imageWidth, imageHeight, transformSrc) {
|
|
550
|
+
async function _generateSrcSet({ options }, startSrc, imageWidth, imageHeight, transformSrc) {
|
|
547
551
|
// Start from original image
|
|
548
552
|
let new_srcset = '';
|
|
549
553
|
if (!imageWidth || !imageHeight) {
|
|
@@ -553,8 +557,8 @@ async function _generateSrcSet(startSrc, imageWidth, imageHeight, transformSrc)
|
|
|
553
557
|
// Start reduction
|
|
554
558
|
const step = 300; //px
|
|
555
559
|
let valueW = !startSrc ? imageWidth : imageWidth - step;
|
|
556
|
-
valueW = Math.min(valueW,
|
|
557
|
-
while (valueW >=
|
|
560
|
+
valueW = Math.min(valueW, options.image.srcset_max_width);
|
|
561
|
+
while (valueW >= options.image.srcset_min_width) {
|
|
558
562
|
let src = await transformSrc(valueW);
|
|
559
563
|
if (src) {
|
|
560
564
|
new_srcset += `, ${src} ${valueW}w`;
|
|
@@ -569,7 +573,7 @@ async function _generateSrcSet(startSrc, imageWidth, imageHeight, transformSrc)
|
|
|
569
573
|
}
|
|
570
574
|
return null;
|
|
571
575
|
}
|
|
572
|
-
async function generateSrcSet(htmlfile, originalImage, startSrc, startSrcLength, options) {
|
|
576
|
+
async function generateSrcSet(state, htmlfile, originalImage, startSrc, startSrcLength, options) {
|
|
573
577
|
const ext = path.extname(originalImage.src);
|
|
574
578
|
const fullbasename = originalImage.src.slice(0, -ext.length);
|
|
575
579
|
const imageSrc = (addition) => {
|
|
@@ -582,12 +586,12 @@ async function generateSrcSet(htmlfile, originalImage, startSrc, startSrcLength,
|
|
|
582
586
|
const imageWidth = (meta === null || meta === void 0 ? void 0 : meta.width) || 0;
|
|
583
587
|
const imageHeight = (meta === null || meta === void 0 ? void 0 : meta.height) || 0;
|
|
584
588
|
let previousImageSize = startSrcLength || Number.MAX_VALUE;
|
|
585
|
-
return _generateSrcSet(startSrc, imageWidth, imageHeight, async (valueW) => {
|
|
589
|
+
return _generateSrcSet(state, startSrc, imageWidth, imageHeight, async (valueW) => {
|
|
586
590
|
const src = imageSrc(`@${valueW}w`);
|
|
587
|
-
const absoluteFilename = translateSrc(
|
|
591
|
+
const absoluteFilename = translateSrc(state.dir, path.dirname(htmlfile), src);
|
|
588
592
|
// Don't generate srcset file twice
|
|
589
|
-
if (
|
|
590
|
-
const compressedImage = await compressImage(await originalImage.getData(), { ...options, resize: { width: valueW } });
|
|
593
|
+
if (!state.compressedFiles.has(absoluteFilename)) {
|
|
594
|
+
const compressedImage = await compressImage(state, await originalImage.getData(), { ...options, resize: { width: valueW } });
|
|
591
595
|
if (!(compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) ||
|
|
592
596
|
compressedImage.data.length >= previousImageSize) {
|
|
593
597
|
// New image is not compressed or bigger than previous image in srcset
|
|
@@ -596,20 +600,20 @@ async function generateSrcSet(htmlfile, originalImage, startSrc, startSrcLength,
|
|
|
596
600
|
}
|
|
597
601
|
else {
|
|
598
602
|
// Write file
|
|
599
|
-
if (
|
|
603
|
+
if (!state.args.nowrite) {
|
|
600
604
|
fs.writeFile(absoluteFilename, compressedImage.data);
|
|
601
605
|
}
|
|
602
606
|
// Set new previous size to beat
|
|
603
607
|
previousImageSize = compressedImage.data.length;
|
|
604
608
|
// Add file to list avoid recompression
|
|
605
|
-
|
|
609
|
+
state.compressedFiles.add(absoluteFilename);
|
|
606
610
|
}
|
|
607
611
|
}
|
|
608
612
|
return src;
|
|
609
613
|
});
|
|
610
614
|
}
|
|
611
|
-
async function generateSrcSetForCdn(startSrc, cdnTransformer, imageWidth, imageHeight) {
|
|
612
|
-
return _generateSrcSet('', imageWidth, imageHeight, (valueW) => {
|
|
615
|
+
async function generateSrcSetForCdn(state, startSrc, cdnTransformer, imageWidth, imageHeight) {
|
|
616
|
+
return _generateSrcSet(state, '', imageWidth, imageHeight, (valueW) => {
|
|
613
617
|
var _a, _b;
|
|
614
618
|
return (_b = (_a = cdnTransformer({
|
|
615
619
|
url: startSrc,
|
|
@@ -617,19 +621,19 @@ async function generateSrcSetForCdn(startSrc, cdnTransformer, imageWidth, imageH
|
|
|
617
621
|
})) === null || _a === void 0 ? void 0 : _a.toString()) === null || _b === void 0 ? void 0 : _b.replace(UNPIC_DEFAULT_HOST_REGEX, '/');
|
|
618
622
|
});
|
|
619
623
|
}
|
|
620
|
-
async function setImageSize(htmlfile, img, image) {
|
|
624
|
+
async function setImageSize(state, htmlfile, img, image) {
|
|
621
625
|
let width = img.attr('width');
|
|
622
626
|
let height = img.attr('height');
|
|
623
627
|
let width_new = undefined;
|
|
624
628
|
let height_new = undefined;
|
|
625
629
|
const img_fmt = await image.getExt();
|
|
626
|
-
if (img_fmt === 'svg' && !
|
|
630
|
+
if (img_fmt === 'svg' && !state.options.image.svg.add_width_and_height) {
|
|
627
631
|
return [];
|
|
628
632
|
}
|
|
629
633
|
// Check valid values
|
|
630
634
|
if (width !== undefined) {
|
|
631
635
|
if (!isNumeric(width)) {
|
|
632
|
-
|
|
636
|
+
state.reportIssue(htmlfile, {
|
|
633
637
|
type: 'fix',
|
|
634
638
|
msg: `Invalid width attribute format: "${width}" - overriding for ${image.src}`,
|
|
635
639
|
});
|
|
@@ -638,7 +642,7 @@ async function setImageSize(htmlfile, img, image) {
|
|
|
638
642
|
}
|
|
639
643
|
if (height !== undefined) {
|
|
640
644
|
if (!isNumeric(height)) {
|
|
641
|
-
|
|
645
|
+
state.reportIssue(htmlfile, {
|
|
642
646
|
type: 'fix',
|
|
643
647
|
msg: `Invalid height attribute format: "${height}" - overriding for ${image.src}`,
|
|
644
648
|
});
|
|
@@ -746,13 +750,13 @@ async function processIframe(htmlfile, $, imgElement, isAboveTheFold) {
|
|
|
746
750
|
iframe.attr('loading', 'lazy');
|
|
747
751
|
}
|
|
748
752
|
}
|
|
749
|
-
export async function optimize(include, exclude) {
|
|
753
|
+
export async function optimize(state, include, exclude) {
|
|
750
754
|
const glob = include ? [include] : ['**/*.{htm,html}'];
|
|
751
755
|
if (exclude)
|
|
752
756
|
glob.push('!' + exclude);
|
|
753
|
-
const paths = await globby(glob, { cwd:
|
|
757
|
+
const paths = await globby(glob, { cwd: state.dir });
|
|
754
758
|
// Sequential async
|
|
755
759
|
for (const file of paths) {
|
|
756
|
-
await analyse(file);
|
|
760
|
+
await analyse(state, file);
|
|
757
761
|
}
|
|
758
762
|
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import { GlobalState } from '../state.js';
|
|
1
2
|
import '../utils/polyfill-fetch.js';
|
|
2
|
-
export declare function downloadExternalImage(htmlfile: string, href: string): Promise<string>;
|
|
3
|
+
export declare function downloadExternalImage(state: GlobalState, htmlfile: string, href: string): Promise<string>;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import * as fs from 'fs/promises';
|
|
3
|
-
import $state from '../state.js';
|
|
4
3
|
import { hashSync as hasha } from 'hasha';
|
|
5
4
|
import { fileTypeFromBuffer } from 'file-type';
|
|
6
5
|
import { addToCache, getFromCache } from '../cache.js';
|
|
7
6
|
import { parse } from '../utils/cache-control-parser.js';
|
|
8
7
|
import '../utils/polyfill-fetch.js';
|
|
9
|
-
export async function downloadExternalImage(htmlfile, href) {
|
|
8
|
+
export async function downloadExternalImage(state, htmlfile, href) {
|
|
10
9
|
var _a, _b, _c, _d, _e;
|
|
11
10
|
const hash = hasha(href, {
|
|
12
11
|
algorithm: 'md5',
|
|
@@ -15,7 +14,7 @@ export async function downloadExternalImage(htmlfile, href) {
|
|
|
15
14
|
let buffer;
|
|
16
15
|
let ext;
|
|
17
16
|
// Image is in cache?
|
|
18
|
-
const dataFromCache = await getFromCache('img-ext', hash);
|
|
17
|
+
const dataFromCache = await getFromCache(state, 'img-ext', hash);
|
|
19
18
|
const cacheControl = (_a = dataFromCache === null || dataFromCache === void 0 ? void 0 : dataFromCache.meta) === null || _a === void 0 ? void 0 : _a.CacheControl;
|
|
20
19
|
if (dataFromCache && cacheControl) {
|
|
21
20
|
const date = (_b = dataFromCache === null || dataFromCache === void 0 ? void 0 : dataFromCache.meta) === null || _b === void 0 ? void 0 : _b.Date;
|
|
@@ -64,7 +63,7 @@ export async function downloadExternalImage(htmlfile, href) {
|
|
|
64
63
|
}
|
|
65
64
|
else {
|
|
66
65
|
// Add downloaded image to cache
|
|
67
|
-
await addToCache('img-ext', hash, {
|
|
66
|
+
await addToCache(state, 'img-ext', hash, {
|
|
68
67
|
buffer,
|
|
69
68
|
meta: {
|
|
70
69
|
Date: resp.headers.get('Date'),
|
|
@@ -97,7 +96,7 @@ export async function downloadExternalImage(htmlfile, href) {
|
|
|
97
96
|
if (!ext)
|
|
98
97
|
throw new Error('Unknown image format');
|
|
99
98
|
const htmlFolder = path.dirname(htmlfile);
|
|
100
|
-
const filename = path.relative(path.join(
|
|
101
|
-
await fs.writeFile(path.join(
|
|
99
|
+
const filename = path.relative(path.join(state.dir, htmlFolder), path.join(state.dir, `_jampack/${contentHash}.${ext}`));
|
|
100
|
+
await fs.writeFile(path.join(state.dir, htmlFolder, filename), buffer);
|
|
102
101
|
return filename;
|
|
103
102
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { GlobalState } from '../state.js';
|
|
2
|
+
export declare function prefetch_links_in_viewport(state: GlobalState, html_file: string): Promise<string>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import $state from '../state.js';
|
|
2
1
|
import * as fs from 'fs/promises';
|
|
3
2
|
import * as path from 'path';
|
|
4
3
|
import { createRequire } from 'node:module';
|
|
@@ -6,13 +5,15 @@ const require = createRequire(import.meta.url);
|
|
|
6
5
|
const src_filename = 'quicklink.mjs';
|
|
7
6
|
const folder = `/_jampack/quicklink-2.3.0`;
|
|
8
7
|
const url_loader = `${folder}/loader.js`;
|
|
9
|
-
export async function prefetch_links_in_viewport(html_file) {
|
|
8
|
+
export async function prefetch_links_in_viewport(state, html_file) {
|
|
10
9
|
const path_html = path.dirname('/' + html_file);
|
|
11
|
-
const quickLinkDestination = path.join(
|
|
12
|
-
const payloadStored = await fs
|
|
10
|
+
const quickLinkDestination = path.join(state.dir, folder, src_filename);
|
|
11
|
+
const payloadStored = await fs
|
|
12
|
+
.access(quickLinkDestination)
|
|
13
|
+
.catch(() => false);
|
|
13
14
|
if (!payloadStored) {
|
|
14
|
-
const path_loader = path.join(
|
|
15
|
-
await fs.mkdir(path.join(
|
|
15
|
+
const path_loader = path.join(state.dir, url_loader);
|
|
16
|
+
await fs.mkdir(path.join(state.dir, folder), { recursive: true });
|
|
16
17
|
// Write loader
|
|
17
18
|
const code_loader = `import { listen } from "./${src_filename}";
|
|
18
19
|
listen();`;
|
package/dist/state.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ type Issue = {
|
|
|
21
21
|
export declare class GlobalState {
|
|
22
22
|
dir: string;
|
|
23
23
|
args: Args;
|
|
24
|
+
options: import("./config-types.js").Options;
|
|
25
|
+
targets: import("lightningcss").Targets;
|
|
24
26
|
compressedFiles: Set<string>;
|
|
25
27
|
issues: Map<string, Issue[]>;
|
|
26
28
|
summary: Summary;
|
|
@@ -28,5 +30,4 @@ export declare class GlobalState {
|
|
|
28
30
|
reportIssue(sourceFile: string, issue: Issue): void;
|
|
29
31
|
reportSummary(r: ReportItem): void;
|
|
30
32
|
}
|
|
31
|
-
|
|
32
|
-
export default state;
|
|
33
|
+
export {};
|
package/dist/state.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { defaultTargets } from './compressors/css.js';
|
|
2
|
+
import default_options from './config-default.js';
|
|
1
3
|
export class GlobalState {
|
|
2
4
|
constructor() {
|
|
3
5
|
this.dir = 'dist';
|
|
4
6
|
this.args = {};
|
|
7
|
+
this.options = default_options;
|
|
8
|
+
this.targets = defaultTargets();
|
|
5
9
|
this.compressedFiles = new Set();
|
|
6
10
|
this.issues = new Map();
|
|
7
11
|
this.summary = {
|
|
@@ -23,9 +27,9 @@ export class GlobalState {
|
|
|
23
27
|
reportSummary(r) {
|
|
24
28
|
const isCompressed = r.compressedSize < r.originalSize ? 1 : 0;
|
|
25
29
|
this.summary.nbFiles++;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
this.summary.nbFilesCompressed += isCompressed;
|
|
31
|
+
this.summary.dataLenUncompressed += r.originalSize;
|
|
32
|
+
this.summary.dataLenCompressed += r.compressedSize;
|
|
29
33
|
if (r.action) {
|
|
30
34
|
let summary = this.summaryByExtension[r.action];
|
|
31
35
|
if (!summary) {
|
|
@@ -44,5 +48,3 @@ export class GlobalState {
|
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
|
-
const state = new GlobalState();
|
|
48
|
-
export default state;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@divriots/jampack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"cache-version": {
|
|
5
5
|
"img": "v1",
|
|
6
6
|
"img-ext": "v1"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"./state": "./dist/state.js"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@commander-js/extra-typings": "^12.0.0",
|
|
29
30
|
"@divriots/cheerio": "1.0.0-rc.12",
|
|
30
31
|
"@proload/core": "^0.3.3",
|
|
31
32
|
"@swc/core": "^1.4.16",
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
"hasha": "^6.0.0",
|
|
41
42
|
"html-minifier-terser": "^7.2.0",
|
|
42
43
|
"kleur": "^4.1.5",
|
|
43
|
-
"lightningcss": "^1.
|
|
44
|
+
"lightningcss": "^1.25.1",
|
|
44
45
|
"mini-svg-data-uri": "^1.4.4",
|
|
45
46
|
"ora": "^8.0.1",
|
|
46
47
|
"quicklink": "^2.3.0",
|