@divriots/jampack 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -68,6 +68,8 @@ See [the list of features](https://jampack.divriots.com).
68
68
  - [WebComponents.dev](https://WebComponents.dev)
69
69
  - [LWC.studio](https://lwc.studio)
70
70
  - [Components.studio](https://components.studio)
71
+ - [keycloak.ch](https://keycloak.ch)
72
+
71
73
  > Add yours here
72
74
 
73
75
  ## Why is it called `jampack`?
@@ -1,7 +1,8 @@
1
+ /// <reference types="node" resolution-mode="require"/>
1
2
  import sharp from 'sharp';
2
3
  import type { Image } from './types.js';
3
4
  export declare const compressImage: (data: Buffer, options: {
4
5
  resize?: sharp.ResizeOptions;
5
- toFormat?: 'webp' | 'pjpg' | 'unchanged';
6
+ toFormat?: 'webp' | 'pjpg' | 'avif' | 'unchanged';
6
7
  }) => Promise<Image | undefined>;
7
8
  export declare function compress(exclude: string): Promise<void>;
package/dist/compress.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
- import { optimize as svgo } from "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) => {
@@ -102,72 +108,92 @@ export const compressImage = async (data, options) => {
102
108
  // Skip animated images for the moment.
103
109
  return undefined;
104
110
  }
105
- let doSharp = false;
106
111
  let outputFormat;
107
- switch (meta.format) {
108
- case 'svg':
109
- const newData = svgo(data, {
110
- multipass: true,
111
- plugins: [{
112
- name: "preset-default",
113
- params: {
114
- overrides: {
115
- removeViewBox: false,
116
- },
112
+ // Special case for svg
113
+ if (meta.format === 'svg') {
114
+ const newData = svgo(data, {
115
+ multipass: true,
116
+ plugins: [
117
+ {
118
+ name: 'preset-default',
119
+ params: {
120
+ overrides: {
121
+ removeViewBox: false,
117
122
  },
118
- }
119
- ],
120
- });
121
- if (newData.error || newData.modernError) {
122
- console.log(`Error processing svg ${data}`);
123
- return undefined;
124
- }
125
- return { format: 'svg', data: Buffer.from(newData.data, 'utf8') };
126
- case 'png':
127
- outputFormat = 'png';
128
- if (toFormat === 'unchanged') {
123
+ },
124
+ },
125
+ ],
126
+ });
127
+ if (newData.error || newData.modernError) {
128
+ console.log(`Error processing svg ${data}`);
129
+ return undefined;
130
+ }
131
+ return { format: 'svg', data: Buffer.from(newData.data, 'utf8') };
132
+ }
133
+ // The bitmap images
134
+ if (toFormat === 'unchanged') {
135
+ switch (meta.format) {
136
+ case 'png':
129
137
  sharpFile = sharpFile.png(config.image.png.options || {});
130
- }
131
- doSharp = true;
132
- break;
133
- case 'jpeg':
134
- case 'jpg':
135
- outputFormat = 'jpg';
136
- if (toFormat === 'unchanged') {
138
+ outputFormat = 'png';
139
+ break;
140
+ case 'jpeg':
141
+ case 'jpg':
137
142
  sharpFile = sharpFile.jpeg(config.image.jpeg.options || {});
138
- }
139
- doSharp = true;
140
- break;
141
- case 'webp':
142
- outputFormat = 'webp';
143
- sharpFile = sharpFile.webp(createWebpOptions(config.image.webp.options_lossly) || {});
144
- doSharp = true;
145
- toFormat = 'unchanged'; // Don't need to convert to webP again
146
- break;
147
- }
148
- if (doSharp) {
149
- if (toFormat === 'webp') {
150
- sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png' ? config.image.webp.options_lossless : config.image.webp.options_lossly));
151
- outputFormat = 'webp';
152
- }
153
- else if (toFormat === 'pjpg') {
154
- sharpFile = sharpFile.jpeg({ ...config.image.jpeg.options, progressive: true } || {});
155
- outputFormat = 'jpg';
143
+ outputFormat = 'jpg';
144
+ break;
145
+ case 'webp':
146
+ sharpFile = sharpFile.webp(createWebpOptions(config.image.webp.options_lossly) || {});
147
+ outputFormat = 'webp';
148
+ break;
149
+ case 'avif':
150
+ sharpFile = sharpFile.avif();
151
+ outputFormat = 'avif';
152
+ break;
156
153
  }
157
- 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 });
154
+ }
155
+ else {
156
+ // To format
157
+ switch (toFormat) {
158
+ case 'pjpg':
159
+ sharpFile = sharpFile.jpeg({ ...config.image.jpeg.options, progressive: true } || {});
160
+ outputFormat = 'jpg';
161
+ break;
162
+ case 'webp':
163
+ sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png'
164
+ ? config.image.webp.options_lossless
165
+ : config.image.webp.options_lossly));
166
+ outputFormat = 'webp';
167
+ break;
168
+ case 'avif':
169
+ sharpFile = sharpFile.avif({ effort: 6 });
170
+ outputFormat = 'avif';
171
+ break;
159
172
  }
160
- //
161
- // Output image processed
162
- //
163
- // Add to cache
164
- const outputImage = { format: outputFormat, data: await sharpFile.toBuffer() };
165
- await addToCacheCompressImage(cacheHash, outputImage);
166
- // Go
167
- return outputImage;
168
173
  }
169
- // Unknown input format or error while processing
170
- return undefined;
174
+ // Unknow input format or output format
175
+ // Can't do
176
+ if (!outputFormat)
177
+ return undefined;
178
+ // If resize is requested
179
+ if (((_a = options.resize) === null || _a === void 0 ? void 0 : _a.width) && ((_b = options.resize) === null || _b === void 0 ? void 0 : _b.height)) {
180
+ sharpFile = sharpFile.resize({
181
+ ...options.resize,
182
+ fit: 'fill',
183
+ withoutEnlargement: true,
184
+ });
185
+ }
186
+ //
187
+ // Output image processed
188
+ //
189
+ // Add to cache
190
+ const outputImage = {
191
+ format: outputFormat,
192
+ data: await sharpFile.toBuffer(),
193
+ };
194
+ await addToCacheCompressImage(cacheHash, outputImage);
195
+ // Go
196
+ return outputImage;
171
197
  };
172
198
  const compressImageFile = async (file) => {
173
199
  const buffer = await fs.readFile(file);
package/dist/index.js CHANGED
@@ -21,11 +21,9 @@ const logo = ` __ __
21
21
  `;
22
22
  console.log(logo);
23
23
  const program = new Command();
24
+ program.name('jampack').description('Static website Optimizer').version(VERSION);
24
25
  program
25
- .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,33 +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
16
  const $ = cheerio.load(html, {
17
17
  withStartIndices: true,
18
18
  decodeEntities: false,
19
19
  });
20
20
  const theFold = getTheFold($);
21
- const imgs = $("img");
21
+ const imgs = $('img');
22
22
  const imgsArray = [];
23
23
  imgs.each(async (index, imgElement) => {
24
24
  imgsArray.push(imgElement);
25
25
  });
26
26
  // Process images sequentially
27
- const spinnerImg = ora({ prefixText: " " }).start();
27
+ const spinnerImg = ora({ prefixText: ' ' }).start();
28
28
  for (let i = 0; i < imgsArray.length; i++) {
29
29
  const imgElement = imgsArray[i];
30
- spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr("src")} `);
30
+ spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr('src')} `);
31
31
  const isAboveTheFold = imgElement.startIndex < theFold;
32
32
  await processImage(file, $, imgElement, isAboveTheFold);
33
33
  }
@@ -37,21 +37,21 @@ async function analyse(file) {
37
37
  const issues = $state.issues.get(file);
38
38
  if (issues) {
39
39
  spinnerImg.fail();
40
- console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? "s" : ""}`));
40
+ console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? 's' : ''}`));
41
41
  }
42
42
  else {
43
43
  spinnerImg.succeed();
44
44
  }
45
45
  // Remove the fold
46
46
  if (theFold) {
47
- $("the-fold").remove();
47
+ $('the-fold').remove();
48
48
  }
49
49
  if (!$state.args.nowrite) {
50
50
  await fs.writeFile(path.join($state.dir, file), $.html());
51
51
  }
52
52
  }
53
53
  function getTheFold($) {
54
- const theFolds = $("the-fold");
54
+ const theFolds = $('the-fold');
55
55
  if (!theFolds[0]) {
56
56
  return 0;
57
57
  }
@@ -64,10 +64,10 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
64
64
  /*
65
65
  * Attribute 'src'
66
66
  */
67
- const attrib_src = img.attr("src");
67
+ const attrib_src = img.attr('src');
68
68
  if (!attrib_src) {
69
69
  $state.reportIssue(htmlfile, {
70
- type: "warn",
70
+ type: 'warn',
71
71
  msg: `Missing [src] on img - processing skipped.`,
72
72
  });
73
73
  return;
@@ -75,15 +75,15 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
75
75
  /*
76
76
  * Attribute 'alt'
77
77
  */
78
- const attrib_alt = img.attr("alt");
78
+ const attrib_alt = img.attr('alt');
79
79
  if (attrib_alt === undefined) {
80
80
  $state.reportIssue(htmlfile, {
81
- type: "a11y",
81
+ type: 'a11y',
82
82
  msg: `Missing [alt] on img src="${attrib_src}" - Adding alt="" meanwhile.`,
83
83
  });
84
- img.attr("alt", "");
84
+ img.attr('alt', '');
85
85
  }
86
- if (attrib_src.startsWith("data:")) {
86
+ if (attrib_src.startsWith('data:')) {
87
87
  // Data URI image
88
88
  // TODO: try to compress it
89
89
  return;
@@ -91,26 +91,26 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
91
91
  /*
92
92
  * Attribute 'loading'
93
93
  */
94
- const attr_loading = img.attr("loading");
94
+ const attr_loading = img.attr('loading');
95
95
  if (isAboveTheFold) {
96
- img.removeAttr("loading");
97
- img.attr("fetchpriority", "high");
96
+ img.removeAttr('loading');
97
+ img.attr('fetchpriority', 'high');
98
98
  }
99
99
  else {
100
100
  switch (attr_loading) {
101
101
  case undefined:
102
102
  // Go lazy by default
103
- img.attr("loading", "lazy");
103
+ img.attr('loading', 'lazy');
104
104
  break;
105
- case "eager":
106
- img.removeAttr("loading");
105
+ case 'eager':
106
+ img.removeAttr('loading');
107
107
  break;
108
- case "lazy":
108
+ case 'lazy':
109
109
  // Don't touch it
110
110
  break;
111
111
  default:
112
112
  $state.reportIssue(htmlfile, {
113
- type: "invalid",
113
+ type: 'invalid',
114
114
  msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"`,
115
115
  });
116
116
  }
@@ -118,7 +118,7 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
118
118
  /*
119
119
  * Attribute 'decoding'
120
120
  */
121
- img.attr("decoding", "async");
121
+ img.attr('decoding', 'async');
122
122
  if (!isLocal(attrib_src)) {
123
123
  // Image not local, don't touch it
124
124
  return;
@@ -130,7 +130,7 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
130
130
  // No file -> give up
131
131
  if (!originalImage) {
132
132
  $state.reportIssue(htmlfile, {
133
- type: "erro",
133
+ type: 'erro',
134
134
  msg: `Can't find img on disk src="${attrib_src}"`,
135
135
  });
136
136
  return;
@@ -145,7 +145,7 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
145
145
  // Let's avoid to optimize same images twice
146
146
  $state.optimizedFiles.add(originalImage.filePathAbsolute);
147
147
  newImage = await compressImage(await originalImage.getData(), {
148
- toFormat: canBeProgressiveJpeg ? "pjpg" : "webp",
148
+ toFormat: canBeProgressiveJpeg ? 'pjpg' : 'webp',
149
149
  });
150
150
  if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) &&
151
151
  (newImage.data.length < (await originalImage.getLen()) ||
@@ -154,12 +154,13 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
154
154
  // Do we need to add an new extension?
155
155
  const newExtension = `.${newImage.format}`;
156
156
  const additionalExtension = path.extname(originalImage.filePathAbsolute) === newExtension
157
- ? ""
157
+ ? ''
158
158
  : newExtension;
159
159
  const newFilename = originalImage.filePathAbsolute + additionalExtension;
160
160
  if (!$state.args.nowrite) {
161
161
  fs.writeFile(newFilename, newImage.data);
162
162
  }
163
+ // Mark new file as optimized
163
164
  $state.compressedFiles.add(newFilename);
164
165
  // Report compression result
165
166
  $state.reportSummary({
@@ -169,7 +170,15 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
169
170
  originalSize: await originalImage.getLen(),
170
171
  compressedSize: newImage.data.length,
171
172
  });
172
- img.attr("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
+ });
173
182
  }
174
183
  }
175
184
  /*
@@ -183,22 +192,22 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
183
192
  if (newImage && newImage.data.length <= config.image.embed_size) {
184
193
  let datauri = undefined;
185
194
  switch (newImage.format) {
186
- case "svg":
195
+ case 'svg':
187
196
  datauri = svgToMiniDataURI(newImage.data.toString());
188
197
  break;
189
- case "webp":
190
- datauri = `data:image/webp;base64,${newImage.data.toString("base64")}`;
198
+ case 'webp':
199
+ datauri = `data:image/webp;base64,${newImage.data.toString('base64')}`;
191
200
  break;
192
- case "jpg":
193
- case "png":
201
+ case 'jpg':
202
+ case 'png':
194
203
  // TODO but not possible in current code
195
204
  break;
196
205
  }
197
206
  if (datauri) {
198
207
  isEmbed = true;
199
- img.attr("src", datauri);
200
- img.removeAttr("loading");
201
- img.removeAttr("decoding");
208
+ img.attr('src', datauri);
209
+ img.removeAttr('loading');
210
+ img.removeAttr('decoding');
202
211
  $state.reportSummary({
203
212
  action: `${newImage.format}->embed`,
204
213
  originalSize: await originalImage.getLen(),
@@ -217,13 +226,13 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
217
226
  //
218
227
  // Stop here if svg
219
228
  //
220
- if ((await originalImage.getExt()) === "svg") {
229
+ if ((await originalImage.getExt()) === 'svg') {
221
230
  return;
222
231
  }
223
232
  /*
224
233
  * Attribute 'srcset'
225
234
  */
226
- const attr_srcset = img.attr("srcset");
235
+ const attr_srcset = img.attr('srcset');
227
236
  if (attr_srcset) {
228
237
  // If srcset is set, don't touch it.
229
238
  // The compress pass will compress the images
@@ -231,16 +240,22 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
231
240
  }
232
241
  else {
233
242
  // Generate image set
243
+ // Avif => Avif
244
+ // png, jpg, ... => WebP
245
+ const targetFormat = (await originalImage.getMime()) === 'image/avif' ? 'avif' : 'webp';
234
246
  const ext = path.extname(attrib_src);
235
247
  const fullbasename = attrib_src.slice(0, -ext.length);
236
- const imageSrc = (addition) => `${fullbasename}${addition}.${canBeProgressiveJpeg ? "jpg" : "webp"}`;
248
+ const imageSrc = (addition) => `${fullbasename}${addition}.${canBeProgressiveJpeg ? 'jpg' : targetFormat}`;
237
249
  // Start from original image
238
- let new_srcset = "";
250
+ let new_srcset = '';
239
251
  // Start reduction
240
252
  const step = 300; //px
241
253
  const ratio = w / h;
242
254
  let valueW = w - step;
243
255
  let valueH = Math.trunc(valueW / ratio);
256
+ let previousImageSize = (newImage === null || newImage === void 0 ? void 0 : newImage.data)
257
+ ? Math.min(newImage.data.length, await originalImage.getLen())
258
+ : await originalImage.getLen();
244
259
  while (valueW > config.image.srcset_min_width) {
245
260
  const src = imageSrc(`@${valueW}w`);
246
261
  const absoluteFilename = translateSrc($state.dir, path.dirname(htmlfile), src);
@@ -250,11 +265,19 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
250
265
  $state.compressedFiles.add(absoluteFilename);
251
266
  const compressedImage = await compressImage(await originalImage.getData(), {
252
267
  resize: { width: valueW, height: valueH },
253
- toFormat: canBeProgressiveJpeg ? "pjpg" : "webp",
268
+ toFormat: canBeProgressiveJpeg ? 'pjpg' : targetFormat,
254
269
  });
255
- if ((compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) && !$state.args.nowrite) {
270
+ if (!(compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) ||
271
+ compressedImage.data.length >= previousImageSize) {
272
+ // jampack was not able to compress below the original image
273
+ // Let's move on
274
+ continue;
275
+ }
276
+ if (!$state.args.nowrite) {
256
277
  fs.writeFile(absoluteFilename, compressedImage.data);
257
278
  }
279
+ // Add previous smaller file size
280
+ previousImageSize = compressedImage.data.length;
258
281
  }
259
282
  new_srcset += `, ${src} ${valueW}w`;
260
283
  // reduce size
@@ -262,28 +285,28 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
262
285
  valueH = Math.trunc(valueW / ratio);
263
286
  }
264
287
  if (new_srcset) {
265
- img.attr("srcset", `${img.attr("src")} ${w}w` + new_srcset);
288
+ img.attr('srcset', `${img.attr('src')} ${w}w` + new_srcset);
266
289
  }
267
290
  }
268
291
  }
269
292
  catch (e) {
270
293
  $state.reportIssue(htmlfile, {
271
- type: "erro",
294
+ type: 'erro',
272
295
  msg: e.message ||
273
296
  `Unexpected error while processing image: ${JSON.stringify(e)}`,
274
297
  });
275
298
  }
276
299
  }
277
300
  async function setImageSize(htmlfile, img, image) {
278
- let width = img.attr("width");
279
- let height = img.attr("height");
301
+ let width = img.attr('width');
302
+ let height = img.attr('height');
280
303
  let width_new = undefined;
281
304
  let height_new = undefined;
282
305
  // Check valid values
283
306
  if (width !== undefined) {
284
307
  if (!isNumeric(width)) {
285
308
  $state.reportIssue(htmlfile, {
286
- type: "warn",
309
+ type: 'warn',
287
310
  msg: `Invalid width attribute "${width}" format - overriding`,
288
311
  });
289
312
  width = undefined;
@@ -292,7 +315,7 @@ async function setImageSize(htmlfile, img, image) {
292
315
  if (height !== undefined) {
293
316
  if (!isNumeric(height)) {
294
317
  $state.reportIssue(htmlfile, {
295
- type: "warn",
318
+ type: 'warn',
296
319
  msg: `Invalid height attribute "${height}" format - overriding`,
297
320
  });
298
321
  height = undefined;
@@ -316,7 +339,7 @@ async function setImageSize(htmlfile, img, image) {
316
339
  const imageRatio = Math.round(originalRatio * 10) / 10;
317
340
  if (providedRatio !== imageRatio) {
318
341
  $state.reportIssue(htmlfile, {
319
- type: "warn",
342
+ type: 'warn',
320
343
  msg: `Image aspect ratio in HTML (${providedRatio}) differs from image aspect ratio (${imageRatio}) - fix width and height or let jampack fill them.`,
321
344
  });
322
345
  }
@@ -334,20 +357,20 @@ async function setImageSize(htmlfile, img, image) {
334
357
  }
335
358
  else {
336
359
  // No width or height provided - set both to image size
337
- if ((await image.getExt()) === "svg") {
360
+ if ((await image.getExt()) === 'svg') {
338
361
  // svg with no height and width has special sizing by browsers
339
362
  // They size it inside 300x150 unless they have width and height
340
363
  // attributes
341
364
  // Load svg
342
365
  const c = cheerio.load(await image.getData(), {});
343
- const svg = c("svg").first();
344
- const svg_viewbox = svg.attr("viewbox"); // bug in cheerio here, should be "viewBox"
345
- const svg_width = svg.attr("width");
346
- const svg_height = svg.attr("height");
366
+ const svg = c('svg').first();
367
+ const svg_viewbox = svg.attr('viewbox'); // bug in cheerio here, should be "viewBox"
368
+ const svg_width = svg.attr('width');
369
+ const svg_height = svg.attr('height');
347
370
  // Calculate aspect ratio from viewbox
348
371
  let svg_aspectratio_from_viewbox = undefined;
349
372
  if (svg_viewbox) {
350
- const box = svg_viewbox.split(" ");
373
+ const box = svg_viewbox.split(' ');
351
374
  const w = parseInt(box[2], 10);
352
375
  const h = parseInt(box[3], 10);
353
376
  svg_aspectratio_from_viewbox = w / h;
@@ -395,17 +418,17 @@ async function setImageSize(htmlfile, img, image) {
395
418
  if (width_new !== undefined && height_new !== undefined) {
396
419
  const result_w = Math.round(width_new);
397
420
  const result_h = Math.round(height_new);
398
- img.attr("width", result_w.toFixed(0));
399
- img.attr("height", result_h.toFixed(0));
421
+ img.attr('width', result_w.toFixed(0));
422
+ img.attr('height', result_h.toFixed(0));
400
423
  return [result_w, result_h];
401
424
  }
402
425
  // Something when wrong
403
426
  throw new Error(`Unexpected issue when resolving image size "${image.src}"`);
404
427
  }
405
428
  export async function optimize(exclude) {
406
- const glob = ["**/*.{htm,html}"];
429
+ const glob = ['**/*.{htm,html}'];
407
430
  if (exclude)
408
- glob.push("!" + exclude);
431
+ glob.push('!' + exclude);
409
432
  const paths = await globby(glob, { cwd: $state.dir });
410
433
  // Sequential async
411
434
  for (const file of paths) {
package/dist/state.js CHANGED
@@ -9,7 +9,7 @@ export class GlobalState {
9
9
  nbFiles: 0,
10
10
  nbFilesCompressed: 0,
11
11
  dataLenCompressed: 0,
12
- dataLenUncompressed: 0
12
+ dataLenUncompressed: 0,
13
13
  };
14
14
  this.summaryByExtension = {};
15
15
  }
@@ -34,7 +34,7 @@ export class GlobalState {
34
34
  nbFiles: 0,
35
35
  nbFilesCompressed: 0,
36
36
  dataLenUncompressed: 0,
37
- dataLenCompressed: 0
37
+ dataLenCompressed: 0,
38
38
  };
39
39
  this.summaryByExtension[r.action] = summary;
40
40
  }
@@ -44,7 +44,6 @@ export class GlobalState {
44
44
  summary.dataLenCompressed += r.compressedSize;
45
45
  }
46
46
  }
47
- ;
48
47
  }
49
48
  const state = new GlobalState();
50
49
  export default state;
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
- export type ImageFormat = 'webp' | 'svg' | 'jpg' | 'png' | undefined;
2
+ export type ImageFormat = 'webp' | 'svg' | 'jpg' | 'png' | 'avif' | undefined;
3
3
  export type Image = {
4
4
  format: ImageFormat;
5
5
  data: Buffer;
@@ -27,13 +27,14 @@ export class Resource {
27
27
  case 'png':
28
28
  case 'webp':
29
29
  case 'tif':
30
+ case 'avif':
30
31
  let sharpFile = await sharp(await this.getData(), { animated: true });
31
32
  const meta = await sharpFile.metadata();
32
33
  this.image_meta = {
33
34
  width: meta.width,
34
35
  height: meta.height,
35
36
  isProgressive: meta.isProgressive || false,
36
- hasAlpha: meta.hasAlpha || false
37
+ hasAlpha: meta.hasAlpha || false,
37
38
  };
38
39
  break;
39
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divriots/jampack",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Packer for static websites",
5
5
  "author": "Georges Gomes @georges-gomes",
6
6
  "bin": {
@@ -47,16 +47,16 @@
47
47
  "tslib": "^2.4.1",
48
48
  "typescript": "^4.9.3"
49
49
  },
50
- "scripts": {
51
- "watch": "pnpm build --watch",
52
- "try": "shx rm -fr demo && cp -R demo_ref demo && node ./dist/index.js",
53
- "build": "shx rm -rf dist && tsc -b",
54
- "lint": "eslint . --ext .ts --config .eslintrc"
55
- },
56
50
  "engines": {
57
51
  "node": ">=14.0.0"
58
52
  },
59
53
  "bugs": "https://github.com/divriots/jampack/issues",
60
54
  "keywords": [],
61
- "types": "dist/index.d.ts"
62
- }
55
+ "types": "dist/index.d.ts",
56
+ "scripts": {
57
+ "watch": "pnpm build --watch",
58
+ "try": "shx rm -fr demo && cp -R demo_avif demo && node ./dist/index.js ./demo --nocache",
59
+ "build": "shx rm -rf dist && tsc -b",
60
+ "lint": "eslint . --ext .ts --config .eslintrc"
61
+ }
62
+ }