@divriots/jampack 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <div>
2
+
1
3
  ```
2
4
  __ __
3
5
  |__|____ _____ ___________ ____ | | __
@@ -7,14 +9,17 @@
7
9
  \______| \/ \/| | \/ \/ \/
8
10
  |__|
9
11
  ```
12
+
13
+ </div>
14
+
10
15
  [![npm version](https://img.shields.io/npm/v/@divriots/jampack)](https://npmjs.org/package/@divriots/jampack)
11
16
  [![Discord](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://discord.gg/XkQxSU9)
12
17
 
13
- # `jampack`
18
+ # Jampack
14
19
 
15
20
  Optimizes static websites for best user experience and best Core Web Vitals scores.
16
21
 
17
- <div>
22
+ <div id="banner">
18
23
  <div>Brought to you by</div>
19
24
  <a href="https://divRIOTS.com#gh-light-mode-only">
20
25
  <img width="150" height="40" src="https://divRIOTS.com/divriots.svg" alt="‹div›RIOTS" />
@@ -32,47 +37,11 @@ Optimizes static websites for best user experience and best Core Web Vitals scor
32
37
 
33
38
  ## Why `jampack`?
34
39
 
35
- - **Better user experience**. Specially on low-end devices.
40
+ - **Better user experience** for your website visitors. Specially on low-end devices.
36
41
  - **Better SEO**. Page experience now [affects ranking](https://developers.google.com/search/docs/appearance/page-experience).
37
42
  - **Easier maintenance**. Generating top performance webpages requires extra engineering in the composition of the pages that is counterproductive with the maintenance of the website.
38
43
  - **Time saving**. Spend less time optimizing and let `jampack` perform the heaving-lifting for you.
39
44
 
40
- ## Optimizations
41
-
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 | |
75
-
76
45
  ## Quick use
77
46
 
78
47
  ```sh
@@ -80,28 +49,11 @@ This pass compresses all untouched files without changing filename or format.
80
49
  npx @divriots/jampack ./dist
81
50
  ```
82
51
 
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
-
94
- ## Configuration
95
-
96
- No configuration yet. All defaults.
52
+ For more options see [CLI options](https://jampack.divriots.com/cli-options).
97
53
 
98
- ## Considerations
54
+ ## Documentation
99
55
 
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.
56
+ Check the [Documentation side](https://jampack.divriots.com).
105
57
 
106
58
  ## `jampack` use in the wild
107
59
 
@@ -119,3 +71,26 @@ No configuration yet. All defaults.
119
71
  ## License
120
72
 
121
73
  This software is released under the terms of the [MIT license](https://github.com/divriots/jampack/blob/main/LICENSE).
74
+
75
+
76
+ ## Optimizations
77
+
78
+ ### PASS 1 - Optimizations
79
+
80
+ This pass takes all `*.{html|.htm}` files and parse them for optimisation.
81
+
82
+ | Optimization | Technical details | Related links | Available |
83
+ | -------------- | --------------------- |:------------:|:---------:|
84
+ | 🔽 `<img>` | | | |
85
+ | 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/) | ✅ |
86
+ | Optimize `jpeg` to `WebP` | Given `foo.jpg`, creates a **lossly** `foo.jpg.webp` if size reduced. | [web.dev](https://web.dev/uses-webp-images/) | ✅ |
87
+ | Optimize `svg` | | | ✅ |
88
+ | 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/) | ✅ |
89
+ | Embed small images | If optimised image size is <500 bytes, image is embed in `[src]` | | ✅ |
90
+ | 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) | ✅ |
91
+ | 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) | ✅ |
92
+ | Set lazy loading by default | Sets `loading="lazy"` if `[loading]` is not set. | [web.dev](https://web.dev/lazy-loading-images/) | ✅ |
93
+ | Set async decoding by default | Sets `decoding="async"`. | | ✅ |
94
+ | Warn on missing `[alt]` | Warn on missing `alt` attribute but adds `alt=""` in case image is considered decorative. | | ✅ |
95
+ | 🔽 `<picture>` | | | |
96
+ | - TODO | | | ❌ |
@@ -0,0 +1,7 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import type { Image } from "./types.js";
3
+ declare function cleanCache(): Promise<void>;
4
+ declare function computeCacheHash(buffer: Buffer, options?: any): string;
5
+ declare function getFromCacheCompressImage(hash: string): Promise<Image | undefined>;
6
+ declare function addToCacheCompressImage(hash: string, image: Image): Promise<void>;
7
+ export { cleanCache, computeCacheHash, getFromCacheCompressImage, addToCacheCompressImage };
package/dist/cache.js ADDED
@@ -0,0 +1,52 @@
1
+ import hasha from "hasha";
2
+ import path from "path";
3
+ import * as fs from "fs/promises";
4
+ import { VERSION } from './version.js';
5
+ import $state from './state.js';
6
+ const CACHE_FOLDER = ".jampack/cache";
7
+ async function cleanCache() {
8
+ try {
9
+ await fs.rm(CACHE_FOLDER, { recursive: true });
10
+ }
11
+ catch (e) {
12
+ // Nothing to do, probably not present
13
+ }
14
+ }
15
+ function computeCacheHash(buffer, options) {
16
+ if ($state.args.nocache) {
17
+ return '';
18
+ }
19
+ let hash = `${VERSION}/${hasha(buffer, { algorithm: 'sha256' })}`;
20
+ if (options) {
21
+ hash += "/" + hasha(JSON.stringify(options), { algorithm: 'md5' });
22
+ }
23
+ return hash;
24
+ }
25
+ function getLocation(hash) {
26
+ return path.join(CACHE_FOLDER, hash);
27
+ }
28
+ async function getFromCacheCompressImage(hash) {
29
+ if ($state.args.nocache) {
30
+ return undefined;
31
+ }
32
+ const location = getLocation(hash);
33
+ try {
34
+ const data = await fs.readFile(path.join(location, 'data'));
35
+ const format = JSON.parse((await fs.readFile(path.join(location, 'format'))).toString());
36
+ return { data, format };
37
+ }
38
+ catch (e) {
39
+ // Problem during cache loading or not in cache
40
+ }
41
+ return undefined;
42
+ }
43
+ async function addToCacheCompressImage(hash, image) {
44
+ if ($state.args.nocache) {
45
+ return;
46
+ }
47
+ const location = getLocation(hash);
48
+ await fs.mkdir(location, { recursive: true });
49
+ await fs.writeFile(path.join(location, 'data'), image.data);
50
+ await fs.writeFile(path.join(location, 'format'), JSON.stringify(image.format));
51
+ }
52
+ export { cleanCache, computeCacheHash, getFromCacheCompressImage, addToCacheCompressImage };
@@ -1,5 +1,7 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import sharp from 'sharp';
3
2
  import type { Image } from './types.js';
4
- export declare const compressImage: (data: Buffer, resize: sharp.ResizeOptions, toWebp?: boolean) => Promise<Image | undefined>;
3
+ export declare const compressImage: (data: Buffer, options: {
4
+ resize?: sharp.ResizeOptions;
5
+ toFormat?: 'webp' | 'pjpg' | 'unchanged';
6
+ }) => Promise<Image | undefined>;
5
7
  export declare function compress(exclude: string): Promise<void>;
package/dist/compress.js CHANGED
@@ -10,6 +10,17 @@ 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';
14
+ async function compressJS(code) {
15
+ const newjsresult = await swc.minify(code, { compress: true, mangle: true });
16
+ return newjsresult.code;
17
+ }
18
+ async function compressInlineJS(code) {
19
+ const newCode = await compressJS(code);
20
+ if (newCode && newCode.length < code.length)
21
+ return newCode;
22
+ return code;
23
+ }
13
24
  const processFile = async (file, stats) => {
14
25
  let writeData = undefined;
15
26
  try {
@@ -28,7 +39,7 @@ const processFile = async (file, stats) => {
28
39
  case '.html':
29
40
  case '.htm':
30
41
  const htmldata = await fs.readFile(file);
31
- const newhtmlData = await htmlminifier(htmldata.toString(), { minifyCSS: true, minifyJS: true, sortClassName: true, sortAttributes: true });
42
+ const newhtmlData = await htmlminifier(htmldata.toString(), { minifyCSS: true, minifyJS: compressInlineJS, sortClassName: true, sortAttributes: true });
32
43
  writeData = newhtmlData;
33
44
  break;
34
45
  case '.css':
@@ -40,9 +51,9 @@ const processFile = async (file, stats) => {
40
51
  break;
41
52
  case '.js':
42
53
  const jsdata = await fs.readFile(file);
43
- const newjsresult = await swc.minify(jsdata.toString(), { compress: true, mangle: true });
44
- if (newjsresult.code && newjsresult.code.length < jsdata.length) {
45
- writeData = newjsresult.code;
54
+ const newJS = await compressJS(jsdata.toString());
55
+ if (newJS && newJS.length < jsdata.length) {
56
+ writeData = newJS;
46
57
  }
47
58
  break;
48
59
  }
@@ -74,7 +85,15 @@ function createWebpOptions(opt) {
74
85
  effort: opt.effort
75
86
  };
76
87
  }
77
- export const compressImage = async (data, resize, toWebp = false) => {
88
+ export const compressImage = async (data, options) => {
89
+ var _a, _b;
90
+ const cacheHash = computeCacheHash(data, options);
91
+ const imageFromCache = await getFromCacheCompressImage(cacheHash);
92
+ if (imageFromCache) {
93
+ return imageFromCache;
94
+ }
95
+ // Load modifiable toFormat
96
+ let toFormat = options.toFormat || 'unchanged';
78
97
  if (!config.image.compress)
79
98
  return undefined;
80
99
  let sharpFile = await sharp(data, { animated: true });
@@ -106,7 +125,7 @@ export const compressImage = async (data, resize, toWebp = false) => {
106
125
  return { format: 'svg', data: Buffer.from(newData.data, 'utf8') };
107
126
  case 'png':
108
127
  outputFormat = 'png';
109
- if (!toWebp) {
128
+ if (toFormat === 'unchanged') {
110
129
  sharpFile = sharpFile.png(config.image.png.options || {});
111
130
  }
112
131
  doSharp = true;
@@ -114,7 +133,7 @@ export const compressImage = async (data, resize, toWebp = false) => {
114
133
  case 'jpeg':
115
134
  case 'jpg':
116
135
  outputFormat = 'jpg';
117
- if (!toWebp) {
136
+ if (toFormat === 'unchanged') {
118
137
  sharpFile = sharpFile.jpeg(config.image.jpeg.options || {});
119
138
  }
120
139
  doSharp = true;
@@ -123,24 +142,36 @@ export const compressImage = async (data, resize, toWebp = false) => {
123
142
  outputFormat = 'webp';
124
143
  sharpFile = sharpFile.webp(createWebpOptions(config.image.webp.options_lossly) || {});
125
144
  doSharp = true;
126
- toWebp = false; // Don't need to convert to webP again
145
+ toFormat = 'unchanged'; // Don't need to convert to webP again
127
146
  break;
128
147
  }
129
148
  if (doSharp) {
130
- if (toWebp) {
149
+ if (toFormat === 'webp') {
131
150
  sharpFile = sharpFile.webp(createWebpOptions(meta.format === 'png' ? config.image.webp.options_lossless : config.image.webp.options_lossly));
132
151
  outputFormat = 'webp';
133
152
  }
134
- if (resize.width && resize.height) {
135
- sharpFile = sharpFile.resize({ ...resize, fit: 'fill', withoutEnlargement: true });
153
+ else if (toFormat === 'pjpg') {
154
+ sharpFile = sharpFile.jpeg({ ...config.image.jpeg.options, progressive: true } || {});
155
+ outputFormat = 'jpg';
156
+ }
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 });
136
159
  }
137
- return { format: outputFormat, data: await sharpFile.toBuffer() };
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;
138
168
  }
169
+ // Unknown input format or error while processing
139
170
  return undefined;
140
171
  };
141
- const compressImageFile = async (file, toWebP = false) => {
172
+ const compressImageFile = async (file) => {
142
173
  const buffer = await fs.readFile(file);
143
- return compressImage(buffer, {}, toWebP);
174
+ return compressImage(buffer, {});
144
175
  };
145
176
  export async function compress(exclude) {
146
177
  const spinner = ora(getProgressText()).start();
package/dist/config.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- export declare type WebpOptions = {
1
+ export type WebpOptions = {
2
2
  effort: number;
3
3
  mode: 'lossless' | 'lossly';
4
4
  quality: number;
5
5
  };
6
- export declare type Options = {
6
+ export type Options = {
7
7
  image: {
8
8
  embed_size: number;
9
9
  srcset_min_width: number;
package/dist/index.js CHANGED
@@ -9,13 +9,14 @@ import { fast } from './config.js';
9
9
  import { printTitle } from './logger.js';
10
10
  import { exit } from 'process';
11
11
  import kleur from 'kleur';
12
+ import { cleanCache } from './cache.js';
12
13
  const logo = ` __ __
13
14
  |__|____ _____ ___________ ____ | | __
14
15
  | \\__ \\ / \\\\____ \\__ \\ _/ ___\\| |/ /
15
16
  | |/ __ \\| Y Y \\ |_> > __ \\\\ \\___| <
16
17
  /\\__| (____ /__|_| / __(____ /\\___ >__|_ \\
17
18
  \\______| \\/ \\/| | \\/ \\/ \\/
18
- |__| by ‹div›RIOTS (c)2022
19
+ |__| by ‹div›RIOTS (c)2022-2023
19
20
  `;
20
21
  console.log(logo);
21
22
  const program = new Command();
@@ -32,6 +33,8 @@ program.command('pack', { isDefault: true })
32
33
  .option('--fail', 'Exits with a non-zero return code if issues.')
33
34
  .option('--onlyoptim', 'Only optimize (PASS 1).')
34
35
  .option('--onlycomp', 'Only compress (PASS 2).')
36
+ .option('--cleancache', 'Clean cache before running')
37
+ .option('--nocache', 'Run with no use of cache')
35
38
  .action(async (dir, options) => {
36
39
  $state.dir = dir;
37
40
  $state.args = options;
@@ -39,6 +42,10 @@ program.command('pack', { isDefault: true })
39
42
  if (options.fast) {
40
43
  fast();
41
44
  }
45
+ // Clean cache
46
+ if (options.cleancache) {
47
+ cleanCache();
48
+ }
42
49
  if (!options.onlycomp) {
43
50
  printTitle('PASS 1 - Optimizing');
44
51
  console.time('Done');
package/dist/optimize.js CHANGED
@@ -1,30 +1,32 @@
1
- import { globby } from 'globby';
2
- import * as path from 'path';
3
- import * as fs from 'fs/promises';
4
- import cheerio from 'cheerio';
5
- import { isNumeric } from './utils.js';
6
- import config from './config.js';
7
- import { compressImage } from './compress.js';
8
- import svgToMiniDataURI from 'mini-svg-data-uri';
9
- import $state from './state.js';
10
- import kleur from 'kleur';
11
- import ora from 'ora';
12
- import { isLocal, Resource, translateSrc } from './utils/resource.js';
1
+ import { globby } from "globby";
2
+ import * as path from "path";
3
+ import * as fs from "fs/promises";
4
+ import cheerio from "cheerio";
5
+ import { isNumeric } from "./utils.js";
6
+ import config from "./config.js";
7
+ import { compressImage } from "./compress.js";
8
+ import svgToMiniDataURI from "mini-svg-data-uri";
9
+ import $state from "./state.js";
10
+ import kleur from "kleur";
11
+ import ora from "ora";
12
+ import { isLocal, Resource, translateSrc } from "./utils/resource.js";
13
13
  async function analyse(file) {
14
- console.log('▶ ' + file);
14
+ console.log("▶ " + file);
15
15
  const html = (await fs.readFile(path.join($state.dir, file))).toString();
16
- const $ = cheerio.load(html, {});
17
- const imgs = $('img');
16
+ const $ = cheerio.load(html, { withStartIndices: true });
17
+ const theFold = getTheFold($);
18
+ const imgs = $("img");
18
19
  const imgsArray = [];
19
20
  imgs.each(async (index, imgElement) => {
20
21
  imgsArray.push(imgElement);
21
22
  });
22
23
  // Process images sequentially
23
- const spinnerImg = ora({ prefixText: ' ' }).start();
24
+ const spinnerImg = ora({ prefixText: " " }).start();
24
25
  for (let i = 0; i < imgsArray.length; i++) {
25
26
  const imgElement = imgsArray[i];
26
- spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr('src')} `);
27
- await processImage(file, $, imgElement);
27
+ spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr("src")} `);
28
+ const isAboveTheFold = imgElement.startIndex < theFold;
29
+ await processImage(file, $, imgElement, isAboveTheFold);
28
30
  }
29
31
  // Reset spinner
30
32
  spinnerImg.text = kleur.dim(`<img> [${imgsArray.length}/${imgsArray.length}]`);
@@ -32,72 +34,102 @@ async function analyse(file) {
32
34
  const issues = $state.issues.get(file);
33
35
  if (issues) {
34
36
  spinnerImg.fail();
35
- console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? 's' : ''}`));
37
+ console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? "s" : ""}`));
36
38
  }
37
39
  else {
38
40
  spinnerImg.succeed();
39
41
  }
42
+ // Remove the fold
43
+ if (theFold) {
44
+ $("the-fold").remove();
45
+ }
40
46
  if (!$state.args.nowrite) {
41
47
  await fs.writeFile(path.join($state.dir, file), $.html());
42
48
  }
43
49
  }
44
- async function processImage(htmlfile, $, imgElement) {
50
+ function getTheFold($) {
51
+ const theFolds = $("the-fold");
52
+ if (!theFolds[0]) {
53
+ return 0;
54
+ }
55
+ // @ts-ignore
56
+ return theFolds[0].startIndex;
57
+ }
58
+ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
45
59
  try {
46
60
  const img = $(imgElement);
47
61
  /*
48
- * Attribute 'src'
49
- */
50
- const attrib_src = img.attr('src');
62
+ * Attribute 'src'
63
+ */
64
+ const attrib_src = img.attr("src");
51
65
  if (!attrib_src) {
52
- $state.reportIssue(htmlfile, { type: 'warn', msg: `Missing [src] on img - processing skipped.` });
66
+ $state.reportIssue(htmlfile, {
67
+ type: "warn",
68
+ msg: `Missing [src] on img - processing skipped.`,
69
+ });
53
70
  return;
54
71
  }
55
72
  /*
56
- * Attribute 'alt'
57
- */
58
- const attrib_alt = img.attr('alt');
73
+ * Attribute 'alt'
74
+ */
75
+ const attrib_alt = img.attr("alt");
59
76
  if (attrib_alt === undefined) {
60
- $state.reportIssue(htmlfile, { type: 'a11y', msg: `Missing [alt] on img src="${attrib_src}" - Adding alt="" meanwhile.` });
61
- img.attr('alt', "");
77
+ $state.reportIssue(htmlfile, {
78
+ type: "a11y",
79
+ msg: `Missing [alt] on img src="${attrib_src}" - Adding alt="" meanwhile.`,
80
+ });
81
+ img.attr("alt", "");
62
82
  }
63
- if (attrib_src.startsWith('data:')) {
83
+ if (attrib_src.startsWith("data:")) {
64
84
  // Data URI image
65
85
  // TODO: try to compress it
66
86
  return;
67
87
  }
68
88
  /*
69
- * Attribute 'loading'
70
- */
71
- const attr_loading = img.attr('loading');
72
- switch (attr_loading) {
73
- case undefined:
74
- // Go lazy by default
75
- img.attr('loading', 'lazy');
76
- break;
77
- case 'eager':
78
- img.removeAttr('loading');
79
- break;
80
- case 'lazy':
81
- // Don't touch it
82
- break;
83
- default:
84
- $state.reportIssue(htmlfile, { type: 'invalid', msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"` });
89
+ * Attribute 'loading'
90
+ */
91
+ const attr_loading = img.attr("loading");
92
+ if (isAboveTheFold) {
93
+ img.removeAttr("loading");
94
+ img.attr("fetchpriority", "high");
95
+ }
96
+ else {
97
+ switch (attr_loading) {
98
+ case undefined:
99
+ // Go lazy by default
100
+ img.attr("loading", "lazy");
101
+ break;
102
+ case "eager":
103
+ img.removeAttr("loading");
104
+ break;
105
+ case "lazy":
106
+ // Don't touch it
107
+ break;
108
+ default:
109
+ $state.reportIssue(htmlfile, {
110
+ type: "invalid",
111
+ msg: `Invalid [loading]="${attr_loading}" on img src="${attrib_src}"`,
112
+ });
113
+ }
85
114
  }
86
115
  /*
87
- * Attribute 'decoding'
88
- */
89
- img.attr('decoding', 'async');
116
+ * Attribute 'decoding'
117
+ */
118
+ img.attr("decoding", "async");
90
119
  if (!isLocal(attrib_src)) {
91
120
  // Image not local, don't touch it
92
121
  return;
93
122
  }
94
123
  /*
95
- * Loading image
96
- */
124
+ * Loading image
125
+ */
97
126
  const originalImage = await Resource.loadResource($state.dir, htmlfile, attrib_src);
98
127
  // No file -> give up
99
128
  if (!originalImage) {
100
- $state.reportIssue(htmlfile, { type: 'erro', msg: `Can't find img on disk src="${attrib_src}"` });
129
+ $state.reportIssue(htmlfile, {
130
+ type: "erro",
131
+ msg: `Can't find img on disk src="${attrib_src}"`,
132
+ });
101
133
  return;
102
134
  }
103
135
  /*
@@ -107,11 +139,18 @@ async function processImage(htmlfile, $, imgElement) {
107
139
  if (!$state.optimizedFiles.has(originalImage.filePathAbsolute)) {
108
140
  // Let's avoid to optimize same images twice
109
141
  $state.optimizedFiles.add(originalImage.filePathAbsolute);
110
- newImage = await compressImage(await originalImage.getData(), {}, true);
111
- if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) && newImage.data.length < await originalImage.getLen()) {
142
+ newImage = await compressImage(await originalImage.getData(), {
143
+ toFormat: isAboveTheFold ? "pjpg" : "webp",
144
+ });
145
+ if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) &&
146
+ (newImage.data.length < (await originalImage.getLen()) ||
147
+ isAboveTheFold) // Progressive jpg above the fold should get replaced even if bigger
148
+ ) {
112
149
  // Do we need to add an new extension?
113
150
  const newExtension = `.${newImage.format}`;
114
- const additionalExtension = path.extname(originalImage.filePathAbsolute) === newExtension ? '' : newExtension;
151
+ const additionalExtension = path.extname(originalImage.filePathAbsolute) === newExtension
152
+ ? ""
153
+ : newExtension;
115
154
  const newFilename = originalImage.filePathAbsolute + additionalExtension;
116
155
  if (!$state.args.nowrite) {
117
156
  fs.writeFile(newFilename, newImage.data);
@@ -119,11 +158,13 @@ async function processImage(htmlfile, $, imgElement) {
119
158
  $state.compressedFiles.add(newFilename);
120
159
  // Report compression result
121
160
  $state.reportSummary({
122
- action: (newFilename !== originalImage.filePathAbsolute) ? `${await originalImage.getExt()}->${newImage.format}` : path.extname(originalImage.filePathAbsolute),
161
+ action: newFilename !== originalImage.filePathAbsolute
162
+ ? `${await originalImage.getExt()}->${newImage.format}`
163
+ : path.extname(originalImage.filePathAbsolute),
123
164
  originalSize: await originalImage.getLen(),
124
- compressedSize: newImage.data.length
165
+ compressedSize: newImage.data.length,
125
166
  });
126
- img.attr('src', attrib_src + additionalExtension);
167
+ img.attr("src", attrib_src + additionalExtension);
127
168
  }
128
169
  }
129
170
  /*
@@ -137,23 +178,27 @@ async function processImage(htmlfile, $, imgElement) {
137
178
  if (newImage && newImage.data.length <= config.image.embed_size) {
138
179
  let datauri = undefined;
139
180
  switch (newImage.format) {
140
- case 'svg':
181
+ case "svg":
141
182
  datauri = svgToMiniDataURI(newImage.data.toString());
142
183
  break;
143
- case 'webp':
144
- datauri = `data:image/webp;base64,${newImage.data.toString('base64')}`;
184
+ case "webp":
185
+ datauri = `data:image/webp;base64,${newImage.data.toString("base64")}`;
145
186
  break;
146
- case 'jpg':
147
- case 'png':
187
+ case "jpg":
188
+ case "png":
148
189
  // TODO but not possible in current code
149
190
  break;
150
191
  }
151
192
  if (datauri) {
152
193
  isEmbed = true;
153
- img.attr('src', datauri);
154
- img.removeAttr('loading');
155
- img.removeAttr('decoding');
156
- $state.reportSummary({ action: `${newImage.format}->embed`, originalSize: await originalImage.getLen(), compressedSize: newImage.data.length });
194
+ img.attr("src", datauri);
195
+ img.removeAttr("loading");
196
+ img.removeAttr("decoding");
197
+ $state.reportSummary({
198
+ action: `${newImage.format}->embed`,
199
+ originalSize: await originalImage.getLen(),
200
+ compressedSize: newImage.data.length,
201
+ });
157
202
  }
158
203
  }
159
204
  /*
@@ -167,13 +212,13 @@ async function processImage(htmlfile, $, imgElement) {
167
212
  //
168
213
  // Stop here if svg
169
214
  //
170
- if (await originalImage.getExt() === 'svg') {
215
+ if ((await originalImage.getExt()) === "svg") {
171
216
  return;
172
217
  }
173
218
  /*
174
219
  * Attribute 'srcset'
175
220
  */
176
- const attr_srcset = img.attr('srcset');
221
+ const attr_srcset = img.attr("srcset");
177
222
  if (attr_srcset) {
178
223
  // If srcset is set, don't touch it.
179
224
  // The compress pass will compress the images
@@ -182,10 +227,10 @@ async function processImage(htmlfile, $, imgElement) {
182
227
  else {
183
228
  // Generate image set
184
229
  const ext = path.extname(attrib_src);
185
- const fullbasename = attrib_src.slice(0, -(ext.length));
230
+ const fullbasename = attrib_src.slice(0, -ext.length);
186
231
  const imageSrc = (addition) => `${fullbasename}${addition}.webp`;
187
232
  // Start from original image
188
- let new_srcset = '';
233
+ let new_srcset = "";
189
234
  // Start reduction
190
235
  const step = 300; //px
191
236
  const ratio = w / h;
@@ -198,7 +243,7 @@ async function processImage(htmlfile, $, imgElement) {
198
243
  if (!$state.compressedFiles.has(absoluteFilename)) {
199
244
  // Add file to list avoid recompression
200
245
  $state.compressedFiles.add(absoluteFilename);
201
- const compressedImage = await compressImage(await originalImage.getData(), { width: valueW, height: valueH }, true);
246
+ const compressedImage = await compressImage(await originalImage.getData(), { resize: { width: valueW, height: valueH }, toFormat: "webp" });
202
247
  if ((compressedImage === null || compressedImage === void 0 ? void 0 : compressedImage.data) && !$state.args.nowrite) {
203
248
  fs.writeFile(absoluteFilename, compressedImage.data);
204
249
  }
@@ -209,17 +254,21 @@ async function processImage(htmlfile, $, imgElement) {
209
254
  valueH = Math.trunc(valueW / ratio);
210
255
  }
211
256
  if (new_srcset) {
212
- img.attr('srcset', `${img.attr('src')} ${w}w` + new_srcset);
257
+ img.attr("srcset", `${img.attr("src")} ${w}w` + new_srcset);
213
258
  }
214
259
  }
215
260
  }
216
261
  catch (e) {
217
- $state.reportIssue(htmlfile, { type: 'erro', msg: e.message || `Unexpected error while processing image: ${JSON.stringify(e)}` });
262
+ $state.reportIssue(htmlfile, {
263
+ type: "erro",
264
+ msg: e.message ||
265
+ `Unexpected error while processing image: ${JSON.stringify(e)}`,
266
+ });
218
267
  }
219
268
  }
220
269
  async function setImageSize(img, image) {
221
- let width = img.attr('width');
222
- let height = img.attr('height');
270
+ let width = img.attr("width");
271
+ let height = img.attr("height");
223
272
  let width_new = undefined;
224
273
  let height_new = undefined;
225
274
  // Check valid values
@@ -249,7 +298,7 @@ async function setImageSize(img, image) {
249
298
  const w = parseInt(width, 10);
250
299
  const h = parseInt(height, 10);
251
300
  // Is ratio equal?
252
- const providedRatio = Math.round(w / h * 10) / 10;
301
+ const providedRatio = Math.round((w / h) * 10) / 10;
253
302
  const imageRatio = Math.round(originalRatio * 10) / 10;
254
303
  if (providedRatio !== imageRatio) {
255
304
  //console.warn(`Image aspect ratio in HTML (${providedRatio}) differs from image aspect ratio (${imageRatio}) - fix width and height or let jampack fill them.`);
@@ -268,33 +317,38 @@ async function setImageSize(img, image) {
268
317
  }
269
318
  else {
270
319
  // No width or height provided - set both to image size
271
- if (await image.getExt() === 'svg') {
320
+ if ((await image.getExt()) === "svg") {
272
321
  // svg with no height and width has special sizing by browsers
273
322
  // They size it inside 300x150 unless they have width and height
274
323
  // attributes
275
324
  // Load svg
276
325
  const c = cheerio.load(await image.getData(), {});
277
- const svg = c('svg').first();
278
- const svg_viewbox = svg.attr('viewbox'); // bug in cheerio here, should be "viewBox"
279
- const svg_width = svg.attr('width');
280
- const svg_height = svg.attr('height');
326
+ const svg = c("svg").first();
327
+ const svg_viewbox = svg.attr("viewbox"); // bug in cheerio here, should be "viewBox"
328
+ const svg_width = svg.attr("width");
329
+ const svg_height = svg.attr("height");
281
330
  // Calculate aspect ratio from viewbox
282
331
  let svg_aspectratio_from_viewbox = undefined;
283
332
  if (svg_viewbox) {
284
- const box = svg_viewbox.split(' ');
333
+ const box = svg_viewbox.split(" ");
285
334
  const w = parseInt(box[2], 10);
286
335
  const h = parseInt(box[3], 10);
287
336
  svg_aspectratio_from_viewbox = w / h;
288
337
  }
289
338
  // Set size
290
339
  //
291
- if (svg_width && isNumeric(svg_width) && svg_height && isNumeric(svg_height)) {
340
+ if (svg_width &&
341
+ isNumeric(svg_width) &&
342
+ svg_height &&
343
+ isNumeric(svg_height)) {
292
344
  // height and width are present
293
345
  // use them
294
346
  width_new = parseInt(svg_width, 10);
295
347
  height_new = parseInt(svg_height, 10);
296
348
  }
297
- else if (svg_width === undefined && svg_height === undefined && svg_aspectratio_from_viewbox) {
349
+ else if (svg_width === undefined &&
350
+ svg_height === undefined &&
351
+ svg_aspectratio_from_viewbox) {
298
352
  // no height and no width but viewbox is present
299
353
  // fit it in default browser box 300x150
300
354
  if (svg_aspectratio_from_viewbox >= 2) {
@@ -324,21 +378,20 @@ async function setImageSize(img, image) {
324
378
  if (width_new !== undefined && height_new !== undefined) {
325
379
  const result_w = Math.round(width_new);
326
380
  const result_h = Math.round(height_new);
327
- img.attr('width', result_w.toFixed(0));
328
- img.attr('height', result_h.toFixed(0));
381
+ img.attr("width", result_w.toFixed(0));
382
+ img.attr("height", result_h.toFixed(0));
329
383
  return [result_w, result_h];
330
384
  }
331
385
  // Something when wrong
332
386
  throw new Error(`Unexpected issue when resolving image size "${image.src}"`);
333
387
  }
334
388
  export async function optimize(exclude) {
335
- const glob = ['**/*.{htm,html}'];
389
+ const glob = ["**/*.{htm,html}"];
336
390
  if (exclude)
337
- glob.push('!' + exclude);
391
+ glob.push("!" + exclude);
338
392
  const paths = await globby(glob, { cwd: $state.dir });
339
393
  // Sequential async
340
394
  for (const file of paths) {
341
395
  await analyse(file);
342
396
  }
343
- ;
344
397
  }
package/dist/state.d.ts CHANGED
@@ -1,18 +1,19 @@
1
- export declare type Args = {
1
+ export type Args = {
2
2
  nowrite?: boolean;
3
+ nocache?: boolean;
3
4
  };
4
- export declare type ReportItem = {
5
+ export type ReportItem = {
5
6
  action: string;
6
7
  originalSize: number;
7
8
  compressedSize: number;
8
9
  };
9
- declare type Summary = {
10
+ type Summary = {
10
11
  nbFiles: number;
11
12
  nbFilesCompressed: number;
12
13
  dataLenUncompressed: number;
13
14
  dataLenCompressed: number;
14
15
  };
15
- declare type Issue = {
16
+ type Issue = {
16
17
  type: 'invalid' | 'a11y' | 'perf' | 'erro' | 'warn';
17
18
  msg: string;
18
19
  };
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
- export declare type ImageFormat = 'webp' | 'svg' | 'jpg' | 'png' | undefined;
3
- export declare type Image = {
2
+ export type ImageFormat = 'webp' | 'svg' | 'jpg' | 'png' | undefined;
3
+ export type Image = {
4
4
  format: ImageFormat;
5
5
  data: Buffer;
6
6
  };
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  import { FileExtension, MimeType } from 'file-type';
3
- declare type ImageMeta = {
3
+ type ImageMeta = {
4
4
  width: number | undefined;
5
5
  height: number | undefined;
6
6
  isProgressive: boolean;
@@ -74,10 +74,14 @@ export class Resource {
74
74
  }
75
75
  static async loadResource(projectRoot, relativeFile, src) {
76
76
  if (!isLocal(src)) {
77
- throw new Error('Source should be local');
77
+ throw new Error('src should be local');
78
78
  }
79
- const relativePath = path.join(projectRoot, src.startsWith('/') ? '' : path.dirname(relativeFile), src);
80
- const absolutePath = path.resolve(relativePath);
79
+ const u = url.parse(src);
80
+ if (!u.pathname) {
81
+ throw new Error(`Invalid src format "${src}"`);
82
+ }
83
+ const relativePath = path.join(projectRoot, src.startsWith('/') ? '' : path.dirname(relativeFile), u.pathname);
84
+ let absolutePath = path.resolve(relativePath);
81
85
  if (await fileExists(absolutePath)) {
82
86
  return new Resource(src, absolutePath);
83
87
  }
@@ -0,0 +1 @@
1
+ export declare const VERSION: string;
@@ -0,0 +1,3 @@
1
+ // @ts-ignore
2
+ import json from '../package.json' assert { type: 'json' };
3
+ export const VERSION = json.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divriots/jampack",
3
- "version": "0.2.4",
3
+ "version": "0.4.0",
4
4
  "description": "Packer for static websites",
5
5
  "author": "Georges Gomes @georges-gomes",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "/dist"
17
17
  ],
18
18
  "dependencies": {
19
- "@swc/core": "^1.3.4",
19
+ "@swc/core": "^1.3.21",
20
20
  "@types/cheerio": "^0.22.31",
21
21
  "@types/commander": "^2.12.2",
22
22
  "@types/csso": "^5.0.0",
@@ -25,26 +25,27 @@
25
25
  "@types/sharp": "^0.31.0",
26
26
  "@types/svgo": "^2.6.4",
27
27
  "@types/table": "^6.3.2",
28
- "chalk": "^5.0.1",
28
+ "chalk": "^5.1.2",
29
29
  "cheerio": "0.22.0",
30
30
  "commander": "^9.4.1",
31
31
  "csso": "^5.0.5",
32
32
  "file-type": "^18.0.0",
33
33
  "globby": "^13.1.2",
34
- "html-minifier-terser": "^7.0.0",
34
+ "hasha": "^5.2.2",
35
+ "html-minifier-terser": "^7.1.0",
35
36
  "kleur": "^4.1.5",
36
37
  "mini-svg-data-uri": "^1.4.4",
37
38
  "ora": "^6.1.2",
38
- "sharp": "^0.31.1",
39
+ "sharp": "^0.31.2",
39
40
  "svgo": "^2.8.0",
40
- "table": "^6.8.0"
41
+ "table": "^6.8.1"
41
42
  },
42
43
  "devDependencies": {
43
- "@types/node": "^16.9.4",
44
+ "@types/node": "^16.11.59",
44
45
  "shx": "^0.3.3",
45
46
  "ts-node": "^10.2.1",
46
- "tslib": "^2.3.1",
47
- "typescript": "^4.8.4"
47
+ "tslib": "^2.4.1",
48
+ "typescript": "^4.9.3"
48
49
  },
49
50
  "scripts": {
50
51
  "watch": "pnpm build --watch",