@divriots/jampack 0.2.1 → 0.2.3

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
@@ -39,11 +39,39 @@ Optimizes static websites for best user experience and best Core Web Vitals scor
39
39
 
40
40
  ## Optimizations
41
41
 
42
- Growing list of optimizations performed:
43
-
44
- | Optimization | Description |
45
- | ------------- | ------------- |
46
- | todo | todo |
42
+ ### PASS 1 - Optimizations
43
+
44
+ This pass takes all `*.{html|.htm}` files and parse them for optimisation.
45
+
46
+ | Optimization | Technical details | Related links | Available |
47
+ | -------------- | --------------------- |:------------:|:---------:|
48
+ | 🔽 `<img>` | | | |
49
+ | Optimize `png` to `WebP` | Given `foo.png`, creates a **near loss less** `foo.png.webp` if size reduced. | [web.dev](https://web.dev/uses-webp-images/) | ✅ |
50
+ | Optimize `jpeg` to `WebP` | Given `foo.jpg`, creates a **lossly** `foo.jpg.webp` if size reduced. | [web.dev](https://web.dev/uses-webp-images/) | ✅ |
51
+ | Optimize `svg` | | | ✅ |
52
+ | 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/) | ✅ |
53
+ | Embed small images | If optimised image size is <500 bytes, image is embed in `[src]` | | ✅ |
54
+ | 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) | ✅ |
55
+ | 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) | ✅ |
56
+ | Set lazy loading by default | Sets `loading="lazy"` if `[loading]` is not set. | [web.dev](https://web.dev/lazy-loading-images/) | ✅ |
57
+ | Set async decoding by default | Sets `decoding="async"`. | | ✅ |
58
+ | Warn on missing `[alt]` | Warn on missing `alt` attribute but adds `alt=""` in case image is considered decorative. | | ✅ |
59
+ | 🔽 `<picture>` | | | |
60
+ | - TODO | | | ❌ |
61
+
62
+ ### PASS 2 - Compression
63
+
64
+ This pass compresses all untouched files without changing filename or format.
65
+
66
+ | Extension | Compressor | Some options |
67
+ | --------------- | --------------------- | ------------ |
68
+ | `.html`,`.htm` | html-minifier-terser | |
69
+ | `.css` | csso | |
70
+ | `.js` | swc | |
71
+ | `.svg` | svgo | |
72
+ | `.jpg`,`.jpeg` | sharp | |
73
+ | `.png` | sharp | |
74
+ | `.webp` | sharp | |
47
75
 
48
76
  ## Quick use
49
77
 
@@ -52,10 +80,29 @@ Growing list of optimizations performed:
52
80
  npx @divriots/jampack ./dist
53
81
  ```
54
82
 
83
+ ## CLI options
84
+
85
+ | Option | Description |
86
+ | ----------- | ------------------------------ |
87
+ | `--onlyoptim` | Only runs optimization (PASS 1) |
88
+ | `--onlycomp` | Only runs compression (PASS 2) |
89
+ | `--fast` | Go fast. Mostly no compression just checks for issues. |
90
+ | `--fail` | Exits with a non-zero return code if issues. |
91
+ | `--nowrite` | Don't write anything to disk (for testing) |
92
+ | `--exclude` | Files to exclude from processing. Expect glob format like `--exclude 'blog/**'` |
93
+
55
94
  ## Configuration
56
95
 
57
96
  No configuration yet. All defaults.
58
97
 
98
+ ## Considerations
99
+
100
+ `jampack` is meant to be used with your static website as is with no particular changes but here are some things you can do to help it extract the best performance of your pages.
101
+
102
+ ### Add `loading="eager"` to your images above the fold
103
+
104
+ `jampack` sets all images to lazy by default. But, [images above the fold should not be lazy](https://web.dev/lazy-loading-images/#effects-on-largest-contentful-paint-lcp). Just add `loading="eager"` to your images above the fold and `jampack` will treat them as high priority.
105
+
59
106
  ## `jampack` use in the wild
60
107
 
61
108
  - [divRIOTS.com](https://divRIOTS.com)
@@ -1,3 +1,4 @@
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, resize: sharp.ResizeOptions, toWebp?: boolean) => Promise<Image | undefined>;
package/dist/compress.js CHANGED
@@ -6,27 +6,10 @@ 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';
9
- import globalState from './state.js';
9
+ import $state from './state.js';
10
10
  import { globby } from 'globby';
11
- import options from './options.js';
12
- const beginProgress = () => {
13
- };
14
- const printProgress = () => {
15
- const gain = globalState.summary.dataLenUncompressed - globalState.summary.dataLenCompressed;
16
- const msg = `${globalState.summary.nbFiles} files | ${formatBytes(globalState.summary.dataLenUncompressed)} → ${formatBytes(globalState.summary.dataLenCompressed)} | -${formatBytes(gain)} `;
17
- if (!process.stdout.clearLine || !process.stdout.cursorTo) {
18
- // In CI we don't have access to clearLine or cursorTo
19
- // Just don't log any progress
20
- }
21
- else {
22
- process.stdout.clearLine(0);
23
- process.stdout.cursorTo(0);
24
- process.stdout.write(msg);
25
- }
26
- };
27
- const endProgress = () => {
28
- process.stdout.write('\n');
29
- };
11
+ import config from './config.js';
12
+ import ora from 'ora';
30
13
  const processFile = async (file, stats) => {
31
14
  let writeData = undefined;
32
15
  try {
@@ -77,22 +60,23 @@ const processFile = async (file, stats) => {
77
60
  // Writedata
78
61
  if (writeData && writeData.length < result.originalSize) {
79
62
  result.compressedSize = writeData.length;
80
- if (!globalState.args.nowrite) {
63
+ if (!$state.args.nowrite) {
81
64
  await fs.writeFile(file, writeData);
82
65
  }
83
66
  }
84
- globalState.compressedFiles.add(file);
85
- globalState.reportItem(result);
86
- printProgress();
67
+ $state.compressedFiles.add(file);
68
+ $state.reportSummary(result);
87
69
  };
88
70
  function createWebpOptions(opt) {
89
71
  return {
90
- nearLossless: (opt === null || opt === void 0 ? void 0 : opt.mode) === 'lossless',
91
- quality: (opt === null || opt === void 0 ? void 0 : opt.quality) || 80,
92
- effort: (opt === null || opt === void 0 ? void 0 : opt.effort) || 4
72
+ nearLossless: opt.mode === 'lossless',
73
+ quality: opt.quality,
74
+ effort: opt.effort
93
75
  };
94
76
  }
95
77
  export const compressImage = async (data, resize, toWebp = false) => {
78
+ if (!config.image.compress)
79
+ return undefined;
96
80
  let sharpFile = await sharp(data, { animated: true });
97
81
  const meta = await sharpFile.metadata();
98
82
  if (meta.pages && meta.pages > 1) {
@@ -123,7 +107,7 @@ export const compressImage = async (data, resize, toWebp = false) => {
123
107
  case 'png':
124
108
  outputFormat = 'png';
125
109
  if (!toWebp) {
126
- sharpFile = sharpFile.png(options.image.png.options || {});
110
+ sharpFile = sharpFile.png(config.image.png.options || {});
127
111
  }
128
112
  doSharp = true;
129
113
  break;
@@ -131,20 +115,20 @@ export const compressImage = async (data, resize, toWebp = false) => {
131
115
  case 'jpg':
132
116
  outputFormat = 'jpg';
133
117
  if (!toWebp) {
134
- sharpFile = sharpFile.jpeg(options.image.jpeg.options || {});
118
+ sharpFile = sharpFile.jpeg(config.image.jpeg.options || {});
135
119
  }
136
120
  doSharp = true;
137
121
  break;
138
122
  case 'webp':
139
123
  outputFormat = 'webp';
140
- sharpFile = sharpFile.webp(createWebpOptions(options.image.webp.options_lossly) || {});
124
+ sharpFile = sharpFile.webp(createWebpOptions(config.image.webp.options_lossly) || {});
141
125
  doSharp = true;
142
126
  toWebp = false; // Don't need to convert to webP again
143
127
  break;
144
128
  }
145
129
  if (doSharp) {
146
130
  if (toWebp) {
147
- sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png' ? options.image.webp.options_lossless : options.image.webp.options_lossly));
131
+ sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png' ? config.image.webp.options_lossless : config.image.webp.options_lossly));
148
132
  outputFormat = 'webp';
149
133
  }
150
134
  if (resize.width && resize.height) {
@@ -159,16 +143,22 @@ const compressImageFile = async (file, toWebP = false) => {
159
143
  return compressImage(buffer, {}, toWebP);
160
144
  };
161
145
  export async function compress(exclude) {
162
- beginProgress();
146
+ const spinner = ora(getProgressText()).start();
163
147
  const globs = ['**/**'];
164
148
  if (exclude)
165
149
  globs.push('!' + exclude);
166
- const paths = await globby(globs, { cwd: globalState.dir, absolute: true });
150
+ const paths = await globby(globs, { cwd: $state.dir, absolute: true });
167
151
  // "Parallel" processing
168
152
  await Promise.all(paths.map(async (file) => {
169
- if (!globalState.compressedFiles.has(file)) {
153
+ if (!$state.compressedFiles.has(file)) {
170
154
  await processFile(file, await fs.stat(file));
155
+ spinner.text = getProgressText();
171
156
  }
172
157
  }));
173
- endProgress();
158
+ spinner.text = getProgressText();
159
+ spinner.succeed();
174
160
  }
161
+ const getProgressText = () => {
162
+ const gain = $state.summary.dataLenUncompressed - $state.summary.dataLenCompressed;
163
+ return `${$state.summary.nbFiles} files | ${formatBytes($state.summary.dataLenUncompressed)} → ${formatBytes($state.summary.dataLenCompressed)} | -${formatBytes(gain)} `;
164
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ export declare type WebpOptions = {
2
+ effort: number;
3
+ mode: 'lossless' | 'lossly';
4
+ quality: number;
5
+ };
6
+ export declare type Options = {
7
+ image: {
8
+ embed_size: number;
9
+ srcset_min_width: number;
10
+ compress: boolean;
11
+ jpeg: {
12
+ options: {
13
+ quality: number;
14
+ mozjpeg: boolean;
15
+ };
16
+ };
17
+ png: {
18
+ options: {
19
+ compressionLevel: number;
20
+ };
21
+ };
22
+ webp: {
23
+ options_lossless: WebpOptions;
24
+ options_lossly: WebpOptions;
25
+ };
26
+ };
27
+ };
28
+ declare const default_options: Options;
29
+ export declare function fast(): void;
30
+ export default default_options;
package/dist/config.js ADDED
@@ -0,0 +1,59 @@
1
+ const default_options = {
2
+ image: {
3
+ embed_size: 400,
4
+ srcset_min_width: 400,
5
+ compress: true,
6
+ jpeg: {
7
+ options: {
8
+ quality: 80,
9
+ mozjpeg: true
10
+ }
11
+ },
12
+ png: {
13
+ options: {
14
+ compressionLevel: 9,
15
+ }
16
+ },
17
+ webp: {
18
+ options_lossless: {
19
+ effort: 4,
20
+ quality: 80,
21
+ mode: 'lossless'
22
+ },
23
+ options_lossly: {
24
+ effort: 4,
25
+ quality: 80,
26
+ mode: 'lossly'
27
+ }
28
+ }
29
+ }
30
+ };
31
+ const fast_options_override = {
32
+ image: {
33
+ embed_size: 0,
34
+ srcset_min_width: 16000,
35
+ compress: false,
36
+ jpeg: {
37
+ options: {
38
+ mozjpeg: false
39
+ }
40
+ },
41
+ png: {
42
+ options: {
43
+ compressionLevel: 0,
44
+ }
45
+ },
46
+ webp: {
47
+ options_lossless: {
48
+ effort: 0,
49
+ },
50
+ options_lossly: {
51
+ effort: 0,
52
+ }
53
+ }
54
+ }
55
+ };
56
+ export function fast() {
57
+ Object.assign(default_options, fast_options_override);
58
+ }
59
+ export default default_options;
package/dist/index.js CHANGED
@@ -2,10 +2,13 @@
2
2
  import { Command } from 'commander';
3
3
  import { compress } from './compress.js';
4
4
  import { optimize } from './optimize.js';
5
- import kleur from 'kleur';
6
- import globalState from './state.js';
5
+ import $state from './state.js';
7
6
  import { table } from 'table';
8
7
  import { formatBytes } from './utils.js';
8
+ import { fast } from './config.js';
9
+ import { printTitle } from './logger.js';
10
+ import { exit } from 'process';
11
+ import kleur from 'kleur';
9
12
  const logo = ` __ __
10
13
  |__|____ _____ ___________ ____ | | __
11
14
  | \\__ \\ / \\\\____ \\__ \\ _/ ___\\| |/ /
@@ -25,42 +28,79 @@ program.command('pack', { isDefault: true })
25
28
  .argument('<dir>', 'Directory to pack')
26
29
  .option('--exclude <exclude>', 'Glob to exclude')
27
30
  .option('--nowrite', 'No write')
28
- .option('--onlycomp', 'Only compress')
29
- .option('--onlyoptim', 'Only optimize')
31
+ .option('--fast', 'Go fast. Mostly no compression just checks for issues.')
32
+ .option('--fail', 'Exits with a non-zero return code if issues.')
33
+ .option('--onlyoptim', 'Only optimize (PASS 1).')
34
+ .option('--onlycomp', 'Only compress (PASS 2).')
30
35
  .action(async (dir, options) => {
31
- globalState.dir = dir;
32
- globalState.args = options;
36
+ $state.dir = dir;
37
+ $state.args = options;
38
+ // Override config with fast options
39
+ if (options.fast) {
40
+ fast();
41
+ }
33
42
  if (!options.onlycomp) {
34
- console.log('');
35
- console.log(kleur.bgGreen(kleur.black(` PASS 1 - Optimizing `)));
43
+ printTitle('PASS 1 - Optimizing');
44
+ console.time('Done');
36
45
  await optimize(options.exclude);
46
+ console.timeEnd('Done');
37
47
  }
38
- if (!options.onlyoptim) {
39
- console.log('');
40
- console.log(kleur.bgGreen(kleur.black(` PASS 2 - Compressing the rest`)));
48
+ if (!options.onlyoptim && !options.fast) {
49
+ printTitle('PASS 2 - Compressing the rest');
50
+ console.time('Done');
41
51
  await compress(options.exclude);
52
+ console.timeEnd('Done');
53
+ }
54
+ printSummary();
55
+ printIssues();
56
+ if (options.failOnIssues && $state.issues.size > 0) {
57
+ exit(1);
42
58
  }
43
- printDetails();
44
59
  });
45
60
  program.parse();
46
- function printDetails() {
47
- const dataTable = [['Action', 'Compressed', 'Original', 'Compressed', 'Gain']];
48
- const config = {
49
- columns: [
50
- { alignment: 'left' },
51
- { alignment: 'right' },
52
- { alignment: 'right' },
53
- { alignment: 'right' },
54
- { alignment: 'right' }
55
- ],
56
- };
57
- Object.entries(globalState.summaryByExtension).forEach(([ext, summary]) => {
58
- if (summary.dataLenCompressed < summary.dataLenUncompressed) {
59
- const row = [ext, `${summary.nbFilesCompressed} / ${summary.nbFiles}`, formatBytes(summary.dataLenUncompressed), formatBytes(summary.dataLenCompressed), '-' + formatBytes(summary.dataLenUncompressed - summary.dataLenCompressed)];
60
- dataTable.push(row);
61
+ function printSummary() {
62
+ if ($state.summary.nbFiles > 0) {
63
+ printTitle('Summary');
64
+ const dataTable = [['Action', 'Compressed', 'Original', 'Compressed', 'Gain']];
65
+ const config = {
66
+ columns: [
67
+ { alignment: 'left' },
68
+ { alignment: 'right' },
69
+ { alignment: 'right' },
70
+ { alignment: 'right' },
71
+ { alignment: 'right' }
72
+ ],
73
+ drawHorizontalLine: (lineIndex, rowCount) => {
74
+ return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount - 1 || lineIndex === rowCount;
75
+ }
76
+ };
77
+ Object.entries($state.summaryByExtension).forEach(([ext, summary]) => {
78
+ if (summary.dataLenCompressed < summary.dataLenUncompressed) {
79
+ const row = [ext, `${summary.nbFilesCompressed} / ${summary.nbFiles}`, formatBytes(summary.dataLenUncompressed), formatBytes(summary.dataLenCompressed), '-' + formatBytes(summary.dataLenUncompressed - summary.dataLenCompressed)];
80
+ dataTable.push(row);
81
+ }
82
+ });
83
+ const total = ['Total', `${$state.summary.nbFilesCompressed} / ${$state.summary.nbFiles}`, formatBytes($state.summary.dataLenUncompressed), formatBytes($state.summary.dataLenCompressed), '-' + formatBytes($state.summary.dataLenUncompressed - $state.summary.dataLenCompressed)];
84
+ dataTable.push(total);
85
+ console.log(table(dataTable, config));
86
+ }
87
+ }
88
+ function printIssues() {
89
+ let issueCount = 0;
90
+ if ($state.issues.size === 0) {
91
+ printTitle('✔ No issues');
92
+ }
93
+ else {
94
+ printTitle('Issues', kleur.bgRed);
95
+ console.log('');
96
+ for (let [file, list] of $state.issues) {
97
+ console.log(kleur.red('▶ ' + file + '\n'));
98
+ list.forEach(issue => {
99
+ issueCount++;
100
+ console.log(`❌ ${issue.msg}\n`);
101
+ });
61
102
  }
62
- });
63
- const total = ['Total', `${globalState.summary.nbFilesCompressed} / ${globalState.summary.nbFiles}`, formatBytes(globalState.summary.dataLenUncompressed), formatBytes(globalState.summary.dataLenCompressed), '-' + formatBytes(globalState.summary.dataLenUncompressed - globalState.summary.dataLenCompressed)];
64
- dataTable.push(total);
65
- console.log(table(dataTable, config));
103
+ console.log('');
104
+ printTitle(`${issueCount} issue(s) over ${$state.issues.size} files`, kleur.bgRed);
105
+ }
66
106
  }
package/dist/logger.d.ts CHANGED
@@ -1,3 +1,9 @@
1
- export declare function info(message: string): void;
2
- export declare function warn(message: string): void;
3
- export declare function error(message: string): void;
1
+ export declare class Logger {
2
+ private prefix;
3
+ Logger(prefix?: string): void;
4
+ debug(message: string): void;
5
+ info(message: string): void;
6
+ warn(message: string): void;
7
+ error(message: string): void;
8
+ }
9
+ export declare function printTitle(msg: string, bgColor?: (x: string | number) => string): void;
package/dist/logger.js CHANGED
@@ -1,9 +1,25 @@
1
- export function info(message) {
2
- console.info(message);
1
+ import kleur from 'kleur';
2
+ export class Logger {
3
+ constructor() {
4
+ this.prefix = '';
5
+ }
6
+ Logger(prefix = '') {
7
+ this.prefix = prefix;
8
+ }
9
+ debug(message) {
10
+ console.debug(this.prefix + message);
11
+ }
12
+ info(message) {
13
+ console.info(this.prefix + message);
14
+ }
15
+ warn(message) {
16
+ console.warn(this.prefix + message);
17
+ }
18
+ error(message) {
19
+ console.error(this.prefix + message);
20
+ }
3
21
  }
4
- export function warn(message) {
5
- console.warn(message);
6
- }
7
- export function error(message) {
8
- console.error(message);
22
+ export function printTitle(msg, bgColor = kleur.bgGreen) {
23
+ console.log('');
24
+ console.log(kleur.black(bgColor(` ${msg} `)));
9
25
  }
package/dist/optimize.js CHANGED
@@ -4,13 +4,15 @@ import * as fs from 'fs/promises';
4
4
  import cheerio from 'cheerio';
5
5
  import { isLocal, isNumeric, translateSrc } from './utils.js';
6
6
  import sharp from "sharp";
7
- import options from './options.js';
7
+ import config from './config.js';
8
8
  import { compressImage } from './compress.js';
9
9
  import svgToMiniDataURI from 'mini-svg-data-uri';
10
- import globalState from './state.js';
10
+ import $state from './state.js';
11
+ import kleur from 'kleur';
12
+ import ora from 'ora';
11
13
  async function analyse(file) {
12
- console.log('Processing ' + file);
13
- const html = (await fs.readFile(path.join(globalState.dir, file))).toString();
14
+ console.log('▶ ' + file);
15
+ const html = (await fs.readFile(path.join($state.dir, file))).toString();
14
16
  const $ = cheerio.load(html, {});
15
17
  const imgs = $('img');
16
18
  const imgsArray = [];
@@ -18,34 +20,46 @@ async function analyse(file) {
18
20
  imgsArray.push(imgElement);
19
21
  });
20
22
  // Process images sequentially
21
- await imgsArray.reduce(async (previousPromise, imgElement) => {
22
- await previousPromise;
23
- return processImage(file, $, imgElement);
24
- }, Promise.resolve());
25
- if (!globalState.args.nowrite) {
26
- await fs.writeFile(path.join(globalState.dir, file), $.html());
23
+ const spinnerImg = ora({ prefixText: ' ' }).start();
24
+ for (let i = 0; i < imgsArray.length; i++) {
25
+ const imgElement = imgsArray[i];
26
+ spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr('src')} `);
27
+ await processImage(file, $, imgElement);
28
+ }
29
+ // Reset spinner
30
+ spinnerImg.text = kleur.dim(`<img> [${imgsArray.length}/${imgsArray.length}]`);
31
+ // Notify issues
32
+ const issues = $state.issues.get(file);
33
+ if (issues) {
34
+ spinnerImg.fail();
35
+ console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? 's' : ''}`));
36
+ }
37
+ else {
38
+ spinnerImg.succeed();
39
+ }
40
+ if (!$state.args.nowrite) {
41
+ await fs.writeFile(path.join($state.dir, file), $.html());
27
42
  }
28
43
  }
29
- async function processImage(file, $, imgElement) {
44
+ async function processImage(htmlfile, $, imgElement) {
30
45
  try {
31
46
  const img = $(imgElement);
32
47
  /*
33
- * Attribute 'alt'
34
- */
35
- const attrib_alt = img.attr('alt');
36
- if (attrib_alt === undefined) {
37
- console.warn('<img> has no alt attribute - adding an empty but you should fix it');
38
- img.attr('alt', "");
39
- }
40
- /*
41
48
  * Attribute 'src'
42
49
  */
43
50
  const attrib_src = img.attr('src');
44
- console.log(`- Image ${attrib_src}`);
45
51
  if (!attrib_src) {
46
- console.warn('<img> has no src attribute');
52
+ $state.reportIssue(htmlfile, { msg: `Missing [src] on img - processing skipped.` });
47
53
  return;
48
54
  }
55
+ /*
56
+ * Attribute 'alt'
57
+ */
58
+ const attrib_alt = img.attr('alt');
59
+ if (attrib_alt === undefined) {
60
+ $state.reportIssue(htmlfile, { msg: `Missing [alt] on img src="${attrib_src}" - Adding alt="" meanwhile.` });
61
+ img.attr('alt', "");
62
+ }
49
63
  if (attrib_src.startsWith('data:')) {
50
64
  // Data URI image
51
65
  // TODO: try to compress it
@@ -71,7 +85,7 @@ async function processImage(file, $, imgElement) {
71
85
  // Don't touch it
72
86
  break;
73
87
  default:
74
- console.warn(`Invalid loading attribute ${attr_loading}`);
88
+ $state.reportIssue(htmlfile, { msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"` });
75
89
  }
76
90
  /*
77
91
  * Attribute 'decoding'
@@ -80,37 +94,38 @@ async function processImage(file, $, imgElement) {
80
94
  /*
81
95
  * Loading image
82
96
  */
83
- const absoluteImgPath = translateSrc(globalState.dir, path.dirname(file), attrib_src);
97
+ const absoluteImgPath = translateSrc($state.dir, path.dirname(htmlfile), attrib_src);
84
98
  // file exists?
85
99
  try {
86
100
  await fs.stat(absoluteImgPath);
87
101
  }
88
102
  catch (e) {
89
- console.error(`Missing img ${absoluteImgPath}`);
103
+ $state.reportIssue(htmlfile, { msg: `Can't find img src="${attrib_src}" in ${absoluteImgPath}` });
90
104
  return;
91
105
  }
106
+ // Read file
107
+ const originalData = await fs.readFile(absoluteImgPath);
92
108
  const sharpFile = sharp(absoluteImgPath);
93
109
  const meta = await sharpFile.metadata();
94
- const originalData = await fs.readFile(absoluteImgPath);
95
110
  /*
96
111
  * Compress image to WebP
97
112
  */
98
113
  let newImage;
99
- if (!globalState.optimizedFiles.has(absoluteImgPath)) {
114
+ if (!$state.optimizedFiles.has(absoluteImgPath)) {
100
115
  // Let's avoid to optimize same images twice
101
- globalState.optimizedFiles.add(absoluteImgPath);
116
+ $state.optimizedFiles.add(absoluteImgPath);
102
117
  newImage = await compressImage(originalData, {}, true);
103
118
  if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) && newImage.data.length < originalData.length) {
104
119
  // Do we need to add an new extension?
105
120
  const newExtension = `.${newImage.format}`;
106
121
  const additionalExtension = path.extname(absoluteImgPath) === newExtension ? '' : newExtension;
107
122
  const newFilename = absoluteImgPath + additionalExtension;
108
- if (!globalState.args.nowrite) {
123
+ if (!$state.args.nowrite) {
109
124
  fs.writeFile(newFilename, newImage.data);
110
125
  }
111
- globalState.compressedFiles.add(newFilename);
126
+ $state.compressedFiles.add(newFilename);
112
127
  // Report compression result
113
- globalState.reportItem({
128
+ $state.reportSummary({
114
129
  action: (newFilename !== absoluteImgPath) ? `${meta.format}->${newImage.format}` : path.extname(absoluteImgPath),
115
130
  originalSize: originalData.length,
116
131
  compressedSize: newImage.data.length
@@ -126,7 +141,7 @@ async function processImage(file, $, imgElement) {
126
141
  * image if it fits the size.
127
142
  */
128
143
  let isEmbed = false;
129
- if (newImage && newImage.data.length <= options.image.embed_size) {
144
+ if (newImage && newImage.data.length <= config.image.embed_size) {
130
145
  let datauri = undefined;
131
146
  switch (newImage.format) {
132
147
  case 'svg':
@@ -145,9 +160,17 @@ async function processImage(file, $, imgElement) {
145
160
  img.attr('src', datauri);
146
161
  img.removeAttr('loading');
147
162
  img.removeAttr('decoding');
148
- globalState.reportItem({ action: `${newImage.format}->embed`, originalSize: originalData.length, compressedSize: newImage.data.length });
163
+ $state.reportSummary({ action: `${newImage.format}->embed`, originalSize: originalData.length, compressedSize: newImage.data.length });
149
164
  }
150
165
  }
166
+ /*
167
+ * Attribute 'width' & 'height'
168
+ */
169
+ const [w, h] = setImageSize(img, meta);
170
+ if (w < 0 || h < 0) {
171
+ $state.reportIssue(htmlfile, { msg: `Unexpected error in image size calculation src="${attrib_src}" - can't performance some optmizations.` });
172
+ return;
173
+ }
151
174
  if (isEmbed) {
152
175
  // Image is embed, no need for more processing
153
176
  return;
@@ -157,14 +180,6 @@ async function processImage(file, $, imgElement) {
157
180
  //
158
181
  if (meta.format === 'svg')
159
182
  return;
160
- /*
161
- * Attribute 'width' & 'height'
162
- */
163
- const [w, h] = setImageSize(img, meta);
164
- if (w < 0 || h < 0) {
165
- console.warn(`Unexpected error in image size calculation - some optimizations can't be done`);
166
- return;
167
- }
168
183
  /*
169
184
  * Attribute 'srcset'
170
185
  */
@@ -186,16 +201,15 @@ async function processImage(file, $, imgElement) {
186
201
  const ratio = w / h;
187
202
  let valueW = w - step;
188
203
  let valueH = Math.trunc(valueW / ratio);
189
- while (valueW > options.image.srcset_min_width) {
204
+ while (valueW > config.image.srcset_min_width) {
190
205
  const src = imageSrc(`@${valueW}w`);
191
- console.log(`Generating srcset ${src}`);
192
- const absoluteFilename = translateSrc(globalState.dir, path.dirname(file), src);
206
+ const absoluteFilename = translateSrc($state.dir, path.dirname(htmlfile), src);
193
207
  // Don't generate srcset file twice
194
- if (!globalState.compressedFiles.has(absoluteFilename)) {
208
+ if (!$state.compressedFiles.has(absoluteFilename)) {
195
209
  // Add file to list avoid recompression
196
- globalState.compressedFiles.add(absoluteFilename);
210
+ $state.compressedFiles.add(absoluteFilename);
197
211
  const compressedImage = await compressImage(originalData, { width: valueW, height: valueH }, true);
198
- if ((compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) && !globalState.args.nowrite) {
212
+ if ((compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) && !$state.args.nowrite) {
199
213
  fs.writeFile(absoluteFilename, compressedImage.data);
200
214
  }
201
215
  }
@@ -210,10 +224,7 @@ async function processImage(file, $, imgElement) {
210
224
  }
211
225
  }
212
226
  catch (e) {
213
- console.error('Error while processing image');
214
- console.error(e);
215
- // exit here
216
- // exit(1);
227
+ $state.reportIssue(htmlfile, { msg: `Unexpected error while processing image: ${JSON.stringify(e)}` });
217
228
  }
218
229
  }
219
230
  function setImageSize(img, meta) {
@@ -224,13 +235,13 @@ function setImageSize(img, meta) {
224
235
  // Check valid values
225
236
  if (width !== undefined) {
226
237
  if (!isNumeric(width)) {
227
- console.warn(`Invalid width attribute "${width}" - overriding`);
238
+ //console.warn(`Invalid width attribute "${width}" - overriding`);
228
239
  width = undefined;
229
240
  }
230
241
  }
231
242
  if (height !== undefined) {
232
243
  if (!isNumeric(height)) {
233
- console.warn(`Invalid height attribute "${height}" - overriding`);
244
+ //console.warn(`Invalid height attribute "${height}" - overriding`);
234
245
  height = undefined;
235
246
  }
236
247
  }
@@ -247,7 +258,7 @@ function setImageSize(img, meta) {
247
258
  const providedRatio = Math.round(w / h * 10) / 10;
248
259
  const imageRatio = Math.round(originalRatio * 10) / 10;
249
260
  if (providedRatio !== imageRatio) {
250
- console.warn(`Image aspect ratio in HTML (${providedRatio}) differs from image aspect ratio (${imageRatio}) - fix width and height or let jampack fill them.`);
261
+ //console.warn(`Image aspect ratio in HTML (${providedRatio}) differs from image aspect ratio (${imageRatio}) - fix width and height or let jampack fill them.`);
251
262
  }
252
263
  return [w, h];
253
264
  }
@@ -279,10 +290,10 @@ export async function optimize(exclude) {
279
290
  const glob = ['**/*.{htm,html}'];
280
291
  if (exclude)
281
292
  glob.push('!' + exclude);
282
- const paths = await globby(glob, { cwd: globalState.dir });
293
+ const paths = await globby(glob, { cwd: $state.dir });
283
294
  // Sequential async
284
- await paths.reduce(async (previousPromise, item) => {
285
- await previousPromise;
286
- return analyse(item);
287
- }, Promise.resolve());
295
+ for (const file of paths) {
296
+ await analyse(file);
297
+ }
298
+ ;
288
299
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/dist/options.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export declare type WebpOptions = {
2
- effort?: number;
3
- mode?: 'lossless' | 'lossly';
4
- quality?: number;
2
+ effort: number;
3
+ mode: 'lossless' | 'lossly';
4
+ quality: number;
5
5
  };
6
6
  export declare type Options = {
7
7
  image: {
@@ -9,21 +9,22 @@ export declare type Options = {
9
9
  srcset_min_width: number;
10
10
  srcset_max_width: number;
11
11
  jpeg: {
12
- options?: {
13
- quality?: number;
14
- mozjpeg?: boolean;
12
+ options: {
13
+ quality: number;
14
+ mozjpeg: boolean;
15
15
  };
16
16
  };
17
17
  png: {
18
- options?: {
19
- compressionLevel?: number;
18
+ options: {
19
+ compressionLevel: number;
20
20
  };
21
21
  };
22
22
  webp: {
23
- options_lossless?: WebpOptions;
24
- options_lossly?: WebpOptions;
23
+ options_lossless: WebpOptions;
24
+ options_lossly: WebpOptions;
25
25
  };
26
26
  };
27
27
  };
28
28
  declare const default_options: Options;
29
+ export declare function fast(): void;
29
30
  export default default_options;
package/dist/options.js CHANGED
@@ -28,4 +28,32 @@ const default_options = {
28
28
  }
29
29
  }
30
30
  };
31
+ const fast_options_override = {
32
+ image: {
33
+ embed_size: 0,
34
+ srcset_min_width: 1024,
35
+ srcset_max_width: 1024,
36
+ jpeg: {
37
+ options: {
38
+ mozjpeg: false
39
+ }
40
+ },
41
+ png: {
42
+ options: {
43
+ compressionLevel: 0,
44
+ }
45
+ },
46
+ webp: {
47
+ options_lossless: {
48
+ effort: 0,
49
+ },
50
+ options_lossly: {
51
+ effort: 0,
52
+ }
53
+ }
54
+ }
55
+ };
56
+ export function fast() {
57
+ Object.assign(default_options, fast_options_override);
58
+ }
31
59
  export default default_options;
package/dist/state.d.ts CHANGED
@@ -6,20 +6,25 @@ export declare type ReportItem = {
6
6
  originalSize: number;
7
7
  compressedSize: number;
8
8
  };
9
- export declare type Summary = {
9
+ declare type Summary = {
10
10
  nbFiles: number;
11
11
  nbFilesCompressed: number;
12
12
  dataLenUncompressed: number;
13
13
  dataLenCompressed: number;
14
14
  };
15
+ declare type Issue = {
16
+ msg: string;
17
+ };
15
18
  export declare class GlobalState {
16
19
  dir: string;
17
20
  args: Args;
18
21
  compressedFiles: Set<string>;
19
22
  optimizedFiles: Set<string>;
23
+ issues: Map<string, Issue[]>;
20
24
  summary: Summary;
21
25
  summaryByExtension: Record<string, Summary>;
22
- reportItem(r: ReportItem): void;
26
+ reportIssue(sourceFile: string, issue: Issue): void;
27
+ reportSummary(r: ReportItem): void;
23
28
  }
24
29
  declare const state: GlobalState;
25
30
  export default state;
package/dist/state.js CHANGED
@@ -4,6 +4,7 @@ export class GlobalState {
4
4
  this.args = {};
5
5
  this.compressedFiles = new Set();
6
6
  this.optimizedFiles = new Set();
7
+ this.issues = new Map();
7
8
  this.summary = {
8
9
  nbFiles: 0,
9
10
  nbFilesCompressed: 0,
@@ -12,7 +13,15 @@ export class GlobalState {
12
13
  };
13
14
  this.summaryByExtension = {};
14
15
  }
15
- reportItem(r) {
16
+ reportIssue(sourceFile, issue) {
17
+ let issueList = this.issues.get(sourceFile);
18
+ if (issueList === undefined) {
19
+ issueList = [];
20
+ this.issues.set(sourceFile, issueList);
21
+ }
22
+ issueList.push(issue);
23
+ }
24
+ reportSummary(r) {
16
25
  const isCompressed = r.compressedSize < r.originalSize ? 1 : 0;
17
26
  this.summary.nbFiles++;
18
27
  state.summary.nbFilesCompressed += isCompressed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divriots/jampack",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Packer for static websites",
5
5
  "author": "Georges Gomes @georges-gomes",
6
6
  "bin": {
@@ -16,23 +16,25 @@
16
16
  "/dist"
17
17
  ],
18
18
  "dependencies": {
19
- "@swc/core": "^1.3.3",
19
+ "@swc/core": "^1.3.4",
20
20
  "@types/cheerio": "^0.22.31",
21
21
  "@types/commander": "^2.12.2",
22
22
  "@types/csso": "^5.0.0",
23
23
  "@types/html-minifier-terser": "^7.0.0",
24
+ "@types/ora": "^3.2.0",
24
25
  "@types/sharp": "^0.31.0",
25
26
  "@types/svgo": "^2.6.4",
26
27
  "@types/table": "^6.3.2",
27
28
  "chalk": "^5.0.1",
28
29
  "cheerio": "0.22.0",
29
- "commander": "^9.4.0",
30
+ "commander": "^9.4.1",
30
31
  "csso": "^5.0.5",
31
32
  "globby": "^13.1.2",
32
33
  "html-minifier-terser": "^7.0.0",
33
34
  "kleur": "^4.1.5",
34
35
  "mini-svg-data-uri": "^1.4.4",
35
- "sharp": "^0.31.0",
36
+ "ora": "^6.1.2",
37
+ "sharp": "^0.31.1",
36
38
  "svgo": "^2.8.0",
37
39
  "table": "^6.8.0"
38
40
  },
@@ -41,7 +43,7 @@
41
43
  "shx": "^0.3.3",
42
44
  "ts-node": "^10.2.1",
43
45
  "tslib": "^2.3.1",
44
- "typescript": "^4.4.3"
46
+ "typescript": "^4.8.4"
45
47
  },
46
48
  "scripts": {
47
49
  "watch": "pnpm build --watch",