@divriots/jampack 0.2.3 → 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');
@@ -94,13 +101,13 @@ function printIssues() {
94
101
  printTitle('Issues', kleur.bgRed);
95
102
  console.log('');
96
103
  for (let [file, list] of $state.issues) {
97
- console.log(kleur.red('▶ ' + file + '\n'));
104
+ console.log('▶ ' + file + '\n');
98
105
  list.forEach(issue => {
99
106
  issueCount++;
100
- console.log(`❌ ${issue.msg}\n`);
107
+ console.log(`${kleur.bgYellow(` ${issue.type} `)} ${issue.msg}\n`);
101
108
  });
102
109
  }
110
+ printTitle(`${issueCount} issue(s) over ${$state.issues.size} files`, kleur.bgYellow);
103
111
  console.log('');
104
- printTitle(`${issueCount} issue(s) over ${$state.issues.size} files`, kleur.bgRed);
105
112
  }
106
113
  }