@divriots/jampack 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,9 +51,13 @@ npx @divriots/jampack ./dist
51
51
 
52
52
  For more options see [CLI options](https://jampack.divriots.com/cli-options).
53
53
 
54
- ## Documentation
54
+ <div>
55
+
56
+ ## What can `jampack` do?
55
57
 
56
- Check the [Documentation side](https://jampack.divriots.com).
58
+ See [the list of features](https://jampack.divriots.com).
59
+
60
+ </div>
57
61
 
58
62
  ## `jampack` use in the wild
59
63
 
@@ -68,28 +72,9 @@ Check the [Documentation side](https://jampack.divriots.com).
68
72
 
69
73
  ## Why is it called `jampack`?
70
74
 
71
- - ` jam`: From [Jamstack](https://en.wikipedia.org/wiki/Jamstack).
75
+ - `jam`: From [Jamstack](https://en.wikipedia.org/wiki/Jamstack).
72
76
  - `pack`: Reminescent of the [Executable Packers](https://en.wikipedia.org/wiki/Executable_compression#List_of_executable_packers) from the 90s.
73
77
 
74
78
  ## License
75
79
 
76
80
  This software is released under the terms of the [MIT license](https://github.com/divriots/jampack/blob/main/LICENSE).
77
-
78
-
79
- ## Optimizations
80
-
81
- ### PASS 1 - Optimizations
82
-
83
- This pass takes all `*.{html|.htm}` files and parse them for optimisation.
84
-
85
- | Optimization | Technical details | Related links | Available |
86
- | -------------- | --------------------- |:------------:|:---------:|
87
- | Generate responsive images and set `srcset` | Multiple smaller images are generated exclusively in `WebP` and `srcset` attribute is set accordingly. | [web.dev](https://web.dev/patterns/web-vitals-patterns/images/responsive-images/) | ✅ |
88
- | Embed small images | If optimised image size is <500 bytes, image is embed in `[src]` | | ✅ |
89
- | Set `width` and `height` if missing | Uses image size and aspect ratio to fill values | [web.dev](https://web.dev/optimize-cls/#images-without-dimensions) | ✅ |
90
- | Fix `width` or `height` if not numerical | Uses image size and aspect ratio to fill values | [web.dev](https://web.dev/optimize-cls/#images-without-dimensions) | ✅ |
91
- | Set lazy loading by default | Sets `loading="lazy"` if `[loading]` is not set. | [web.dev](https://web.dev/lazy-loading-images/) | ✅ |
92
- | Set async decoding by default | Sets `decoding="async"`. | | ✅ |
93
- | Warn on missing `[alt]` | Warn on missing `alt` attribute but adds `alt=""` in case image is considered decorative. | | ✅ |
94
- | 🔽 `<picture>` | | | |
95
- | - TODO | | | ❌ |
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import sharp from 'sharp';
3
2
  import type { Image } from './types.js';
4
3
  export declare const compressImage: (data: Buffer, options: {
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 "svgo";
3
+ import { optimize as svgo } from 'svgo';
4
4
  import { minify as htmlminifier } from 'html-minifier-terser';
5
- import { minify as csso } from "csso";
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(), { minifyCSS: true, minifyJS: compressInlineJS, sortClassName: true, sortAttributes: true });
43
+ const newhtmlData = await htmlminifier(htmldata.toString(), {
44
+ minifyCSS: true,
45
+ minifyJS: compressInlineJS,
46
+ sortClassName: true,
47
+ sortAttributes: true,
48
+ });
43
49
  writeData = newhtmlData;
44
50
  break;
45
51
  case '.css':
@@ -66,7 +72,7 @@ const processFile = async (file, stats) => {
66
72
  const result = {
67
73
  action: path.extname(file),
68
74
  originalSize: stats.size,
69
- compressedSize: stats.size
75
+ compressedSize: stats.size,
70
76
  };
71
77
  // Writedata
72
78
  if (writeData && writeData.length < result.originalSize) {
@@ -82,7 +88,7 @@ function createWebpOptions(opt) {
82
88
  return {
83
89
  nearLossless: opt.mode === 'lossless',
84
90
  quality: opt.quality,
85
- effort: opt.effort
91
+ effort: opt.effort,
86
92
  };
87
93
  }
88
94
  export const compressImage = async (data, options) => {
@@ -108,14 +114,15 @@ export const compressImage = async (data, options) => {
108
114
  case 'svg':
109
115
  const newData = svgo(data, {
110
116
  multipass: true,
111
- plugins: [{
112
- name: "preset-default",
117
+ plugins: [
118
+ {
119
+ name: 'preset-default',
113
120
  params: {
114
121
  overrides: {
115
122
  removeViewBox: false,
116
123
  },
117
124
  },
118
- }
125
+ },
119
126
  ],
120
127
  });
121
128
  if (newData.error || newData.modernError) {
@@ -144,10 +151,17 @@ export const compressImage = async (data, options) => {
144
151
  doSharp = true;
145
152
  toFormat = 'unchanged'; // Don't need to convert to webP again
146
153
  break;
154
+ case 'avif':
155
+ outputFormat = 'avif';
156
+ // TODO but avif is already pretty optimized
157
+ toFormat = 'unchanged';
158
+ break;
147
159
  }
148
160
  if (doSharp) {
149
161
  if (toFormat === 'webp') {
150
- sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png' ? config.image.webp.options_lossless : config.image.webp.options_lossly));
162
+ sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png'
163
+ ? config.image.webp.options_lossless
164
+ : config.image.webp.options_lossly));
151
165
  outputFormat = 'webp';
152
166
  }
153
167
  else if (toFormat === 'pjpg') {
@@ -155,13 +169,20 @@ export const compressImage = async (data, options) => {
155
169
  outputFormat = 'jpg';
156
170
  }
157
171
  if (((_a = options.resize) === null || _a === void 0 ? void 0 : _a.width) && ((_b = options.resize) === null || _b === void 0 ? void 0 : _b.height)) {
158
- sharpFile = sharpFile.resize({ ...options.resize, fit: 'fill', withoutEnlargement: true });
172
+ sharpFile = sharpFile.resize({
173
+ ...options.resize,
174
+ fit: 'fill',
175
+ withoutEnlargement: true,
176
+ });
159
177
  }
160
178
  //
161
179
  // Output image processed
162
180
  //
163
181
  // Add to cache
164
- const outputImage = { format: outputFormat, data: await sharpFile.toBuffer() };
182
+ const outputImage = {
183
+ format: outputFormat,
184
+ data: await sharpFile.toBuffer(),
185
+ };
165
186
  await addToCacheCompressImage(cacheHash, outputImage);
166
187
  // Go
167
188
  return outputImage;
package/dist/config.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type WebpOptions = {
2
2
  effort: number;
3
- mode: 'lossless' | 'lossly';
3
+ mode: "lossless" | "lossly";
4
4
  quality: number;
5
5
  };
6
6
  export type Options = {
package/dist/config.js CHANGED
@@ -6,27 +6,27 @@ const default_options = {
6
6
  jpeg: {
7
7
  options: {
8
8
  quality: 80,
9
- mozjpeg: true
10
- }
9
+ mozjpeg: true,
10
+ },
11
11
  },
12
12
  png: {
13
13
  options: {
14
14
  compressionLevel: 9,
15
- }
15
+ },
16
16
  },
17
17
  webp: {
18
18
  options_lossless: {
19
19
  effort: 4,
20
20
  quality: 80,
21
- mode: 'lossless'
21
+ mode: "lossless",
22
22
  },
23
23
  options_lossly: {
24
24
  effort: 4,
25
25
  quality: 80,
26
- mode: 'lossly'
27
- }
28
- }
29
- }
26
+ mode: "lossly",
27
+ },
28
+ },
29
+ },
30
30
  };
31
31
  const fast_options_override = {
32
32
  image: {
@@ -35,13 +35,13 @@ const fast_options_override = {
35
35
  compress: false,
36
36
  jpeg: {
37
37
  options: {
38
- mozjpeg: false
39
- }
38
+ mozjpeg: false,
39
+ },
40
40
  },
41
41
  png: {
42
42
  options: {
43
43
  compressionLevel: 0,
44
- }
44
+ },
45
45
  },
46
46
  webp: {
47
47
  options_lossless: {
@@ -49,9 +49,9 @@ const fast_options_override = {
49
49
  },
50
50
  options_lossly: {
51
51
  effort: 0,
52
- }
53
- }
54
- }
52
+ },
53
+ },
54
+ },
55
55
  };
56
56
  export function fast() {
57
57
  Object.assign(default_options, fast_options_override);
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
- .name('jampack')
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 = [['Action', 'Compressed', 'Original', 'Compressed', 'Gain']];
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 || lineIndex === 1 || lineIndex === rowCount - 1 || lineIndex === rowCount;
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
- if (summary.dataLenCompressed < summary.dataLenUncompressed) {
87
- const row = [ext, `${summary.nbFilesCompressed} / ${summary.nbFiles}`, formatBytes(summary.dataLenUncompressed), formatBytes(summary.dataLenCompressed), '-' + formatBytes(summary.dataLenUncompressed - summary.dataLenCompressed)];
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
- const total = ['Total', `${$state.summary.nbFilesCompressed} / ${$state.summary.nbFiles}`, formatBytes($state.summary.dataLenUncompressed), formatBytes($state.summary.dataLenCompressed), '-' + formatBytes($state.summary.dataLenUncompressed - $state.summary.dataLenCompressed)];
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,30 +1,33 @@
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";
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("▶ " + file);
14
+ console.log('▶ ' + file);
15
15
  const html = (await fs.readFile(path.join($state.dir, file))).toString();
16
- const $ = cheerio.load(html, { withStartIndices: true, decodeEntities: false });
16
+ const $ = cheerio.load(html, {
17
+ withStartIndices: true,
18
+ decodeEntities: false,
19
+ });
17
20
  const theFold = getTheFold($);
18
- const imgs = $("img");
21
+ const imgs = $('img');
19
22
  const imgsArray = [];
20
23
  imgs.each(async (index, imgElement) => {
21
24
  imgsArray.push(imgElement);
22
25
  });
23
26
  // Process images sequentially
24
- const spinnerImg = ora({ prefixText: " " }).start();
27
+ const spinnerImg = ora({ prefixText: ' ' }).start();
25
28
  for (let i = 0; i < imgsArray.length; i++) {
26
29
  const imgElement = imgsArray[i];
27
- spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr("src")} `);
30
+ spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr('src')} `);
28
31
  const isAboveTheFold = imgElement.startIndex < theFold;
29
32
  await processImage(file, $, imgElement, isAboveTheFold);
30
33
  }
@@ -34,21 +37,21 @@ async function analyse(file) {
34
37
  const issues = $state.issues.get(file);
35
38
  if (issues) {
36
39
  spinnerImg.fail();
37
- console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? "s" : ""}`));
40
+ console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? 's' : ''}`));
38
41
  }
39
42
  else {
40
43
  spinnerImg.succeed();
41
44
  }
42
45
  // Remove the fold
43
46
  if (theFold) {
44
- $("the-fold").remove();
47
+ $('the-fold').remove();
45
48
  }
46
49
  if (!$state.args.nowrite) {
47
50
  await fs.writeFile(path.join($state.dir, file), $.html());
48
51
  }
49
52
  }
50
53
  function getTheFold($) {
51
- const theFolds = $("the-fold");
54
+ const theFolds = $('the-fold');
52
55
  if (!theFolds[0]) {
53
56
  return 0;
54
57
  }
@@ -61,10 +64,10 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
61
64
  /*
62
65
  * Attribute 'src'
63
66
  */
64
- const attrib_src = img.attr("src");
67
+ const attrib_src = img.attr('src');
65
68
  if (!attrib_src) {
66
69
  $state.reportIssue(htmlfile, {
67
- type: "warn",
70
+ type: 'warn',
68
71
  msg: `Missing [src] on img - processing skipped.`,
69
72
  });
70
73
  return;
@@ -72,15 +75,15 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
72
75
  /*
73
76
  * Attribute 'alt'
74
77
  */
75
- const attrib_alt = img.attr("alt");
78
+ const attrib_alt = img.attr('alt');
76
79
  if (attrib_alt === undefined) {
77
80
  $state.reportIssue(htmlfile, {
78
- type: "a11y",
81
+ type: 'a11y',
79
82
  msg: `Missing [alt] on img src="${attrib_src}" - Adding alt="" meanwhile.`,
80
83
  });
81
- img.attr("alt", "");
84
+ img.attr('alt', '');
82
85
  }
83
- if (attrib_src.startsWith("data:")) {
86
+ if (attrib_src.startsWith('data:')) {
84
87
  // Data URI image
85
88
  // TODO: try to compress it
86
89
  return;
@@ -88,26 +91,26 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
88
91
  /*
89
92
  * Attribute 'loading'
90
93
  */
91
- const attr_loading = img.attr("loading");
94
+ const attr_loading = img.attr('loading');
92
95
  if (isAboveTheFold) {
93
- img.removeAttr("loading");
94
- img.attr("fetchpriority", "high");
96
+ img.removeAttr('loading');
97
+ img.attr('fetchpriority', 'high');
95
98
  }
96
99
  else {
97
100
  switch (attr_loading) {
98
101
  case undefined:
99
102
  // Go lazy by default
100
- img.attr("loading", "lazy");
103
+ img.attr('loading', 'lazy');
101
104
  break;
102
- case "eager":
103
- img.removeAttr("loading");
105
+ case 'eager':
106
+ img.removeAttr('loading');
104
107
  break;
105
- case "lazy":
108
+ case 'lazy':
106
109
  // Don't touch it
107
110
  break;
108
111
  default:
109
112
  $state.reportIssue(htmlfile, {
110
- type: "invalid",
113
+ type: 'invalid',
111
114
  msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"`,
112
115
  });
113
116
  }
@@ -115,7 +118,7 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
115
118
  /*
116
119
  * Attribute 'decoding'
117
120
  */
118
- img.attr("decoding", "async");
121
+ img.attr('decoding', 'async');
119
122
  if (!isLocal(attrib_src)) {
120
123
  // Image not local, don't touch it
121
124
  return;
@@ -127,34 +130,37 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
127
130
  // No file -> give up
128
131
  if (!originalImage) {
129
132
  $state.reportIssue(htmlfile, {
130
- type: "erro",
133
+ type: 'erro',
131
134
  msg: `Can't find img on disk src="${attrib_src}"`,
132
135
  });
133
136
  return;
134
137
  }
135
138
  /*
136
- * Compress image to WebP
139
+ * Compress image
137
140
  */
138
141
  let newImage;
142
+ const originalImageMeta = await originalImage.getImageMeta();
143
+ const canBeProgressiveJpeg = isAboveTheFold && originalImageMeta && !originalImageMeta.hasAlpha;
139
144
  if (!$state.optimizedFiles.has(originalImage.filePathAbsolute)) {
140
145
  // Let's avoid to optimize same images twice
141
146
  $state.optimizedFiles.add(originalImage.filePathAbsolute);
142
147
  newImage = await compressImage(await originalImage.getData(), {
143
- toFormat: isAboveTheFold ? "pjpg" : "webp",
148
+ toFormat: canBeProgressiveJpeg ? 'pjpg' : 'webp',
144
149
  });
145
150
  if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) &&
146
151
  (newImage.data.length < (await originalImage.getLen()) ||
147
- isAboveTheFold) // Progressive jpg above the fold should get replaced even if bigger
152
+ canBeProgressiveJpeg) // Progressive jpg above the fold should get replaced even if bigger
148
153
  ) {
149
154
  // Do we need to add an new extension?
150
155
  const newExtension = `.${newImage.format}`;
151
156
  const additionalExtension = path.extname(originalImage.filePathAbsolute) === newExtension
152
- ? ""
157
+ ? ''
153
158
  : newExtension;
154
159
  const newFilename = originalImage.filePathAbsolute + additionalExtension;
155
160
  if (!$state.args.nowrite) {
156
161
  fs.writeFile(newFilename, newImage.data);
157
162
  }
163
+ // Mark new file as optimized
158
164
  $state.compressedFiles.add(newFilename);
159
165
  // Report compression result
160
166
  $state.reportSummary({
@@ -164,7 +170,15 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
164
170
  originalSize: await originalImage.getLen(),
165
171
  compressedSize: newImage.data.length,
166
172
  });
167
- img.attr("src", attrib_src + additionalExtension);
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
+ });
168
182
  }
169
183
  }
170
184
  /*
@@ -178,22 +192,22 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
178
192
  if (newImage && newImage.data.length <= config.image.embed_size) {
179
193
  let datauri = undefined;
180
194
  switch (newImage.format) {
181
- case "svg":
195
+ case 'svg':
182
196
  datauri = svgToMiniDataURI(newImage.data.toString());
183
197
  break;
184
- case "webp":
185
- datauri = `data:image/webp;base64,${newImage.data.toString("base64")}`;
198
+ case 'webp':
199
+ datauri = `data:image/webp;base64,${newImage.data.toString('base64')}`;
186
200
  break;
187
- case "jpg":
188
- case "png":
201
+ case 'jpg':
202
+ case 'png':
189
203
  // TODO but not possible in current code
190
204
  break;
191
205
  }
192
206
  if (datauri) {
193
207
  isEmbed = true;
194
- img.attr("src", datauri);
195
- img.removeAttr("loading");
196
- img.removeAttr("decoding");
208
+ img.attr('src', datauri);
209
+ img.removeAttr('loading');
210
+ img.removeAttr('decoding');
197
211
  $state.reportSummary({
198
212
  action: `${newImage.format}->embed`,
199
213
  originalSize: await originalImage.getLen(),
@@ -201,24 +215,24 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
201
215
  });
202
216
  }
203
217
  }
204
- /*
205
- * Attribute 'width' & 'height'
206
- */
207
- const [w, h] = await setImageSize(img, originalImage);
208
218
  if (isEmbed) {
209
219
  // Image is embed, no need for more processing
210
220
  return;
211
221
  }
222
+ /*
223
+ * Attribute 'width' & 'height'
224
+ */
225
+ const [w, h] = await setImageSize(htmlfile, img, originalImage);
212
226
  //
213
227
  // Stop here if svg
214
228
  //
215
- if ((await originalImage.getExt()) === "svg") {
229
+ if ((await originalImage.getExt()) === 'svg') {
216
230
  return;
217
231
  }
218
232
  /*
219
233
  * Attribute 'srcset'
220
234
  */
221
- const attr_srcset = img.attr("srcset");
235
+ const attr_srcset = img.attr('srcset');
222
236
  if (attr_srcset) {
223
237
  // If srcset is set, don't touch it.
224
238
  // The compress pass will compress the images
@@ -228,9 +242,9 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
228
242
  // Generate image set
229
243
  const ext = path.extname(attrib_src);
230
244
  const fullbasename = attrib_src.slice(0, -ext.length);
231
- const imageSrc = (addition) => `${fullbasename}${addition}.webp`;
245
+ const imageSrc = (addition) => `${fullbasename}${addition}.${canBeProgressiveJpeg ? 'jpg' : 'webp'}`;
232
246
  // Start from original image
233
- let new_srcset = "";
247
+ let new_srcset = '';
234
248
  // Start reduction
235
249
  const step = 300; //px
236
250
  const ratio = w / h;
@@ -243,7 +257,10 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
243
257
  if (!$state.compressedFiles.has(absoluteFilename)) {
244
258
  // Add file to list avoid recompression
245
259
  $state.compressedFiles.add(absoluteFilename);
246
- const compressedImage = await compressImage(await originalImage.getData(), { resize: { width: valueW, height: valueH }, toFormat: "webp" });
260
+ const compressedImage = await compressImage(await originalImage.getData(), {
261
+ resize: { width: valueW, height: valueH },
262
+ toFormat: canBeProgressiveJpeg ? 'pjpg' : 'webp',
263
+ });
247
264
  if ((compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) && !$state.args.nowrite) {
248
265
  fs.writeFile(absoluteFilename, compressedImage.data);
249
266
  }
@@ -254,33 +271,39 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
254
271
  valueH = Math.trunc(valueW / ratio);
255
272
  }
256
273
  if (new_srcset) {
257
- img.attr("srcset", `${img.attr("src")} ${w}w` + new_srcset);
274
+ img.attr('srcset', `${img.attr('src')} ${w}w` + new_srcset);
258
275
  }
259
276
  }
260
277
  }
261
278
  catch (e) {
262
279
  $state.reportIssue(htmlfile, {
263
- type: "erro",
280
+ type: 'erro',
264
281
  msg: e.message ||
265
282
  `Unexpected error while processing image: ${JSON.stringify(e)}`,
266
283
  });
267
284
  }
268
285
  }
269
- async function setImageSize(img, image) {
270
- let width = img.attr("width");
271
- let height = img.attr("height");
286
+ async function setImageSize(htmlfile, img, image) {
287
+ let width = img.attr('width');
288
+ let height = img.attr('height');
272
289
  let width_new = undefined;
273
290
  let height_new = undefined;
274
291
  // Check valid values
275
292
  if (width !== undefined) {
276
293
  if (!isNumeric(width)) {
277
- //console.warn(`Invalid width attribute "${width}" - overriding`);
294
+ $state.reportIssue(htmlfile, {
295
+ type: 'warn',
296
+ msg: `Invalid width attribute "${width}" format - overriding`,
297
+ });
278
298
  width = undefined;
279
299
  }
280
300
  }
281
301
  if (height !== undefined) {
282
302
  if (!isNumeric(height)) {
283
- //console.warn(`Invalid height attribute "${height}" - overriding`);
303
+ $state.reportIssue(htmlfile, {
304
+ type: 'warn',
305
+ msg: `Invalid height attribute "${height}" format - overriding`,
306
+ });
284
307
  height = undefined;
285
308
  }
286
309
  }
@@ -301,7 +324,10 @@ async function setImageSize(img, image) {
301
324
  const providedRatio = Math.round((w / h) * 10) / 10;
302
325
  const imageRatio = Math.round(originalRatio * 10) / 10;
303
326
  if (providedRatio !== imageRatio) {
304
- //console.warn(`Image aspect ratio in HTML (${providedRatio}) differs from image aspect ratio (${imageRatio}) - fix width and height or let jampack fill them.`);
327
+ $state.reportIssue(htmlfile, {
328
+ type: 'warn',
329
+ msg: `Image aspect ratio in HTML (${providedRatio}) differs from image aspect ratio (${imageRatio}) - fix width and height or let jampack fill them.`,
330
+ });
305
331
  }
306
332
  return [w, h];
307
333
  }
@@ -317,20 +343,20 @@ async function setImageSize(img, image) {
317
343
  }
318
344
  else {
319
345
  // No width or height provided - set both to image size
320
- if ((await image.getExt()) === "svg") {
346
+ if ((await image.getExt()) === 'svg') {
321
347
  // svg with no height and width has special sizing by browsers
322
348
  // They size it inside 300x150 unless they have width and height
323
349
  // attributes
324
350
  // Load svg
325
351
  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");
352
+ const svg = c('svg').first();
353
+ const svg_viewbox = svg.attr('viewbox'); // bug in cheerio here, should be "viewBox"
354
+ const svg_width = svg.attr('width');
355
+ const svg_height = svg.attr('height');
330
356
  // Calculate aspect ratio from viewbox
331
357
  let svg_aspectratio_from_viewbox = undefined;
332
358
  if (svg_viewbox) {
333
- const box = svg_viewbox.split(" ");
359
+ const box = svg_viewbox.split(' ');
334
360
  const w = parseInt(box[2], 10);
335
361
  const h = parseInt(box[3], 10);
336
362
  svg_aspectratio_from_viewbox = w / h;
@@ -378,17 +404,17 @@ async function setImageSize(img, image) {
378
404
  if (width_new !== undefined && height_new !== undefined) {
379
405
  const result_w = Math.round(width_new);
380
406
  const result_h = Math.round(height_new);
381
- img.attr("width", result_w.toFixed(0));
382
- img.attr("height", result_h.toFixed(0));
407
+ img.attr('width', result_w.toFixed(0));
408
+ img.attr('height', result_h.toFixed(0));
383
409
  return [result_w, result_h];
384
410
  }
385
411
  // Something when wrong
386
412
  throw new Error(`Unexpected issue when resolving image size "${image.src}"`);
387
413
  }
388
414
  export async function optimize(exclude) {
389
- const glob = ["**/*.{htm,html}"];
415
+ const glob = ['**/*.{htm,html}'];
390
416
  if (exclude)
391
- glob.push("!" + exclude);
417
+ glob.push('!' + exclude);
392
418
  const paths = await globby(glob, { cwd: $state.dir });
393
419
  // Sequential async
394
420
  for (const file of paths) {
package/dist/state.js CHANGED
@@ -9,7 +9,7 @@ export class GlobalState {
9
9
  nbFiles: 0,
10
10
  nbFilesCompressed: 0,
11
11
  dataLenCompressed: 0,
12
- dataLenUncompressed: 0
12
+ dataLenUncompressed: 0,
13
13
  };
14
14
  this.summaryByExtension = {};
15
15
  }
@@ -27,6 +27,7 @@ export class GlobalState {
27
27
  state.summary.nbFilesCompressed += isCompressed;
28
28
  state.summary.dataLenUncompressed += r.originalSize;
29
29
  state.summary.dataLenCompressed += r.compressedSize;
30
+ console.log(r.action);
30
31
  if (r.action) {
31
32
  let summary = this.summaryByExtension[r.action];
32
33
  if (!summary) {
@@ -34,7 +35,7 @@ export class GlobalState {
34
35
  nbFiles: 0,
35
36
  nbFilesCompressed: 0,
36
37
  dataLenUncompressed: 0,
37
- dataLenCompressed: 0
38
+ dataLenCompressed: 0,
38
39
  };
39
40
  this.summaryByExtension[r.action] = summary;
40
41
  }
@@ -44,7 +45,6 @@ export class GlobalState {
44
45
  summary.dataLenCompressed += r.compressedSize;
45
46
  }
46
47
  }
47
- ;
48
48
  }
49
49
  const state = new GlobalState();
50
50
  export default state;
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
- export type ImageFormat = 'webp' | 'svg' | 'jpg' | 'png' | undefined;
2
+ export type ImageFormat = 'webp' | 'svg' | 'jpg' | 'png' | 'avif' | undefined;
3
3
  export type Image = {
4
4
  format: ImageFormat;
5
5
  data: Buffer;
@@ -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.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "Packer for static websites",
5
5
  "author": "Georges Gomes @georges-gomes",
6
6
  "bin": {
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "scripts": {
51
51
  "watch": "pnpm build --watch",
52
- "try": "shx rm -fr demo && cp -R demo_ref demo && node ./dist/index.js",
52
+ "try": "shx rm -fr demo && cp -R demo_avif demo && node ./dist/index.js ./demo",
53
53
  "build": "shx rm -rf dist && tsc -b",
54
54
  "lint": "eslint . --ext .ts --config .eslintrc"
55
55
  },