@divriots/jampack 0.2.0 → 0.2.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/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,4 +1,5 @@
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>;
4
- export declare function compress(glob: string): Promise<void>;
5
+ export declare function compress(exclude: string): Promise<void>;
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) {
@@ -158,14 +142,23 @@ const compressImageFile = async (file, toWebP = false) => {
158
142
  const buffer = await fs.readFile(file);
159
143
  return compressImage(buffer, {}, toWebP);
160
144
  };
161
- export async function compress(glob) {
162
- beginProgress();
163
- const paths = await globby(glob, { cwd: globalState.dir, absolute: true });
145
+ export async function compress(exclude) {
146
+ const spinner = ora(getProgressText()).start();
147
+ const globs = ['**/**'];
148
+ if (exclude)
149
+ globs.push('!' + exclude);
150
+ const paths = await globby(globs, { cwd: $state.dir, absolute: true });
164
151
  // "Parallel" processing
165
152
  await Promise.all(paths.map(async (file) => {
166
- if (!globalState.compressedFiles.has(file)) {
153
+ if (!$state.compressedFiles.has(file)) {
167
154
  await processFile(file, await fs.stat(file));
155
+ spinner.text = getProgressText();
168
156
  }
169
157
  }));
170
- endProgress();
158
+ spinner.text = getProgressText();
159
+ spinner.succeed();
171
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,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
  | \\__ \\ / \\\\____ \\__ \\ _/ ___\\| |/ /
@@ -23,43 +26,80 @@ program
23
26
  program.command('pack', { isDefault: true })
24
27
  .description('todo')
25
28
  .argument('<dir>', 'Directory to pack')
29
+ .option('--exclude <exclude>', 'Glob to exclude')
26
30
  .option('--nowrite', 'No write')
27
- .option('--onlycomp', 'Only compress')
28
- .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).')
29
35
  .action(async (dir, options) => {
30
- globalState.dir = dir;
31
- globalState.args = options;
36
+ $state.dir = dir;
37
+ $state.args = options;
38
+ // Override config with fast options
39
+ if (options.fast) {
40
+ fast();
41
+ }
32
42
  if (!options.onlycomp) {
33
- console.log('');
34
- console.log(kleur.bgGreen(kleur.black(` PASS 1 - Optimizing `)));
35
- await optimize();
43
+ printTitle('PASS 1 - Optimizing');
44
+ console.time('Done');
45
+ await optimize(options.exclude);
46
+ console.timeEnd('Done');
36
47
  }
37
48
  if (!options.onlyoptim) {
38
- console.log('');
39
- console.log(kleur.bgGreen(kleur.black(` PASS 2 - Compressing the rest`)));
40
- await compress("**/**");
49
+ printTitle('PASS 2 - Compressing the rest');
50
+ console.time('Done');
51
+ await compress(options.exclude);
52
+ console.timeEnd('Done');
41
53
  }
42
- printDetails();
54
+ printSummary();
55
+ printWarningsAndErrors(options);
43
56
  });
44
57
  program.parse();
45
- function printDetails() {
46
- const dataTable = [['Action', 'Compressed', 'Original', 'Compressed', 'Gain']];
47
- const config = {
48
- columns: [
49
- { alignment: 'left' },
50
- { alignment: 'right' },
51
- { alignment: 'right' },
52
- { alignment: 'right' },
53
- { alignment: 'right' }
54
- ],
55
- };
56
- Object.entries(globalState.summaryByExtension).forEach(([ext, summary]) => {
57
- if (summary.dataLenCompressed < summary.dataLenUncompressed) {
58
- const row = [ext, `${summary.nbFilesCompressed} / ${summary.nbFiles}`, formatBytes(summary.dataLenUncompressed), formatBytes(summary.dataLenCompressed), '-' + formatBytes(summary.dataLenUncompressed - summary.dataLenCompressed)];
59
- dataTable.push(row);
58
+ function printSummary() {
59
+ if ($state.summary.nbFiles > 0) {
60
+ printTitle('Summary');
61
+ const dataTable = [['Action', 'Compressed', 'Original', 'Compressed', 'Gain']];
62
+ const config = {
63
+ columns: [
64
+ { alignment: 'left' },
65
+ { alignment: 'right' },
66
+ { alignment: 'right' },
67
+ { alignment: 'right' },
68
+ { alignment: 'right' }
69
+ ],
70
+ drawHorizontalLine: (lineIndex, rowCount) => {
71
+ return lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount - 1 || lineIndex === rowCount;
72
+ }
73
+ };
74
+ Object.entries($state.summaryByExtension).forEach(([ext, summary]) => {
75
+ if (summary.dataLenCompressed < summary.dataLenUncompressed) {
76
+ const row = [ext, `${summary.nbFilesCompressed} / ${summary.nbFiles}`, formatBytes(summary.dataLenUncompressed), formatBytes(summary.dataLenCompressed), '-' + formatBytes(summary.dataLenUncompressed - summary.dataLenCompressed)];
77
+ dataTable.push(row);
78
+ }
79
+ });
80
+ const total = ['Total', `${$state.summary.nbFilesCompressed} / ${$state.summary.nbFiles}`, formatBytes($state.summary.dataLenUncompressed), formatBytes($state.summary.dataLenCompressed), '-' + formatBytes($state.summary.dataLenUncompressed - $state.summary.dataLenCompressed)];
81
+ dataTable.push(total);
82
+ console.log(table(dataTable, config));
83
+ }
84
+ }
85
+ function printWarningsAndErrors(options) {
86
+ let issueCount = 0;
87
+ if ($state.issues.size === 0) {
88
+ printTitle('✔ No issues');
89
+ }
90
+ else {
91
+ printTitle('Issues', kleur.bgRed);
92
+ console.log('');
93
+ for (let [file, list] of $state.issues) {
94
+ console.log(kleur.red('▶ ' + file + '\n'));
95
+ list.forEach(issue => {
96
+ issueCount++;
97
+ console.log(`${issueCount}) ${issue.msg}\n`);
98
+ });
60
99
  }
61
- });
62
- const total = ['Total', `${globalState.summary.nbFilesCompressed} / ${globalState.summary.nbFiles}`, formatBytes(globalState.summary.dataLenUncompressed), formatBytes(globalState.summary.dataLenCompressed), '-' + formatBytes(globalState.summary.dataLenUncompressed - globalState.summary.dataLenCompressed)];
63
- dataTable.push(total);
64
- console.log(table(dataTable, config));
100
+ printTitle(`${issueCount} issue(s) over ${$state.issues.size} files`, kleur.bgRed);
101
+ if (options.failOnIssues) {
102
+ exit(1);
103
+ }
104
+ }
65
105
  }
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(bgColor(kleur.black(` ${msg} `)));
9
25
  }
@@ -1 +1 @@
1
- export declare function optimize(): Promise<void>;
1
+ export declare function optimize(exclude?: string): Promise<void>;
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}" - Added 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,13 +94,13 @@ 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
  }
92
106
  const sharpFile = sharp(absoluteImgPath);
@@ -96,21 +110,21 @@ async function processImage(file, $, imgElement) {
96
110
  * Compress image to WebP
97
111
  */
98
112
  let newImage;
99
- if (!globalState.optimizedFiles.has(absoluteImgPath)) {
113
+ if (!$state.optimizedFiles.has(absoluteImgPath)) {
100
114
  // Let's avoid to optimize same images twice
101
- globalState.optimizedFiles.add(absoluteImgPath);
115
+ $state.optimizedFiles.add(absoluteImgPath);
102
116
  newImage = await compressImage(originalData, {}, true);
103
117
  if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) && newImage.data.length < originalData.length) {
104
118
  // Do we need to add an new extension?
105
119
  const newExtension = `.${newImage.format}`;
106
120
  const additionalExtension = path.extname(absoluteImgPath) === newExtension ? '' : newExtension;
107
121
  const newFilename = absoluteImgPath + additionalExtension;
108
- if (!globalState.args.nowrite) {
122
+ if (!$state.args.nowrite) {
109
123
  fs.writeFile(newFilename, newImage.data);
110
124
  }
111
- globalState.compressedFiles.add(newFilename);
125
+ $state.compressedFiles.add(newFilename);
112
126
  // Report compression result
113
- globalState.reportItem({
127
+ $state.reportSummary({
114
128
  action: (newFilename !== absoluteImgPath) ? `${meta.format}->${newImage.format}` : path.extname(absoluteImgPath),
115
129
  originalSize: originalData.length,
116
130
  compressedSize: newImage.data.length
@@ -126,7 +140,7 @@ async function processImage(file, $, imgElement) {
126
140
  * image if it fits the size.
127
141
  */
128
142
  let isEmbed = false;
129
- if (newImage && newImage.data.length <= options.image.embed_size) {
143
+ if (newImage && newImage.data.length <= config.image.embed_size) {
130
144
  let datauri = undefined;
131
145
  switch (newImage.format) {
132
146
  case 'svg':
@@ -145,7 +159,7 @@ async function processImage(file, $, imgElement) {
145
159
  img.attr('src', datauri);
146
160
  img.removeAttr('loading');
147
161
  img.removeAttr('decoding');
148
- globalState.reportItem({ action: `${newImage.format}->embed`, originalSize: originalData.length, compressedSize: newImage.data.length });
162
+ $state.reportSummary({ action: `${newImage.format}->embed`, originalSize: originalData.length, compressedSize: newImage.data.length });
149
163
  }
150
164
  }
151
165
  if (isEmbed) {
@@ -162,7 +176,7 @@ async function processImage(file, $, imgElement) {
162
176
  */
163
177
  const [w, h] = setImageSize(img, meta);
164
178
  if (w < 0 || h < 0) {
165
- console.warn(`Unexpected error in image size calculation - some optimizations can't be done`);
179
+ $state.reportIssue(htmlfile, { msg: `Unexpected error in image size calculation src="${attrib_src}" - can't performance some optmizations.` });
166
180
  return;
167
181
  }
168
182
  /*
@@ -186,16 +200,15 @@ async function processImage(file, $, imgElement) {
186
200
  const ratio = w / h;
187
201
  let valueW = w - step;
188
202
  let valueH = Math.trunc(valueW / ratio);
189
- while (valueW > options.image.srcset_min_width) {
203
+ while (valueW > config.image.srcset_min_width) {
190
204
  const src = imageSrc(`@${valueW}w`);
191
- console.log(`Generating srcset ${src}`);
192
- const absoluteFilename = translateSrc(globalState.dir, path.dirname(file), src);
205
+ const absoluteFilename = translateSrc($state.dir, path.dirname(htmlfile), src);
193
206
  // Don't generate srcset file twice
194
- if (!globalState.compressedFiles.has(absoluteFilename)) {
207
+ if (!$state.compressedFiles.has(absoluteFilename)) {
195
208
  // Add file to list avoid recompression
196
- globalState.compressedFiles.add(absoluteFilename);
209
+ $state.compressedFiles.add(absoluteFilename);
197
210
  const compressedImage = await compressImage(originalData, { width: valueW, height: valueH }, true);
198
- if ((compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) && !globalState.args.nowrite) {
211
+ if ((compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) && !$state.args.nowrite) {
199
212
  fs.writeFile(absoluteFilename, compressedImage.data);
200
213
  }
201
214
  }
@@ -210,10 +223,7 @@ async function processImage(file, $, imgElement) {
210
223
  }
211
224
  }
212
225
  catch (e) {
213
- console.error('Error while processing image');
214
- console.error(e);
215
- // exit here
216
- // exit(1);
226
+ $state.reportIssue(htmlfile, { msg: `Unexpected error while processing image: ${JSON.stringify(e)}` });
217
227
  }
218
228
  }
219
229
  function setImageSize(img, meta) {
@@ -224,13 +234,13 @@ function setImageSize(img, meta) {
224
234
  // Check valid values
225
235
  if (width !== undefined) {
226
236
  if (!isNumeric(width)) {
227
- console.warn(`Invalid width attribute "${width}" - overriding`);
237
+ //console.warn(`Invalid width attribute "${width}" - overriding`);
228
238
  width = undefined;
229
239
  }
230
240
  }
231
241
  if (height !== undefined) {
232
242
  if (!isNumeric(height)) {
233
- console.warn(`Invalid height attribute "${height}" - overriding`);
243
+ //console.warn(`Invalid height attribute "${height}" - overriding`);
234
244
  height = undefined;
235
245
  }
236
246
  }
@@ -247,7 +257,7 @@ function setImageSize(img, meta) {
247
257
  const providedRatio = Math.round(w / h * 10) / 10;
248
258
  const imageRatio = Math.round(originalRatio * 10) / 10;
249
259
  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.`);
260
+ //console.warn(`Image aspect ratio in HTML (${providedRatio}) differs from image aspect ratio (${imageRatio}) - fix width and height or let jampack fill them.`);
251
261
  }
252
262
  return [w, h];
253
263
  }
@@ -275,11 +285,14 @@ function setImageSize(img, meta) {
275
285
  // Something when wrong
276
286
  throw new Error('Unexpected issue when resolving image sizes');
277
287
  }
278
- export async function optimize() {
279
- const paths = await globby('**/*.{htm,html}', { cwd: globalState.dir });
288
+ export async function optimize(exclude) {
289
+ const glob = ['**/*.{htm,html}'];
290
+ if (exclude)
291
+ glob.push('!' + exclude);
292
+ const paths = await globby(glob, { cwd: $state.dir });
280
293
  // Sequential async
281
- await paths.reduce(async (previousPromise, item) => {
282
- await previousPromise;
283
- return analyse(item);
284
- }, Promise.resolve());
294
+ for (const file of paths) {
295
+ await analyse(file);
296
+ }
297
+ ;
285
298
  }
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.0",
3
+ "version": "0.2.2",
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",