@divriots/jampack 0.11.7 → 0.12.1

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
@@ -49,7 +49,11 @@ becomes responsive with dimensions:
49
49
 
50
50
  ```html
51
51
  <img src="./redpanda.jpg.webp" alt="Red panda"
52
- srcset="./redpanda.jpg.webp 3872w, ./redpanda@3572w.jpg.webp 3572w, ./redpanda@3272w.jpg.webp 3272w, ./redpanda@2972w.jpg.webp 2972w, ./redpanda@2672w.jpg.webp 2672w, ./redpanda@2372w.jpg.webp 2372w, ./redpanda@2072w.jpg.webp 2072w, ./redpanda@1772w.jpg.webp 1772w, ./redpanda@1472w.jpg.webp 1472w, ./redpanda@1172w.jpg.webp 1172w, ./redpanda@872w.jpg.webp 872w"
52
+ srcset="
53
+ ./redpanda.jpg.webp 3872w, ./redpanda@3572w.jpg.webp 3572w, ./redpanda@3272w.jpg.webp 3272w,
54
+ ./redpanda@2972w.jpg.webp 2972w, ./redpanda@2672w.jpg.webp 2672w, ./redpanda@2372w.jpg.webp 2372w,
55
+ ./redpanda@2072w.jpg.webp 2072w, ./redpanda@1772w.jpg.webp 1772w, ./redpanda@1472w.jpg.webp 1472w,
56
+ ./redpanda@1172w.jpg.webp 1172w, ./redpanda@872w.jpg.webp 872w"
53
57
  sizes="100vw"
54
58
  loading="lazy"
55
59
  decoding="async"
@@ -69,9 +73,24 @@ becomes responsive with multiple formats including AVIF:
69
73
 
70
74
  ```html
71
75
  <picture>
72
- <source type="image/avif" srcset="./redpanda@1936w.avif 1936w, ./redpanda@1636w.avif 1636w, ./redpanda@1336w.avif 1336w, ./redpanda@1036w.avif 1036w, ./redpanda@736w.avif 736w">
73
- <source type="image/webp" srcset="./redpanda@1936w.webp 1936w, ./redpanda@1636w.webp 1636w, ./redpanda@1336w.webp 1336w, ./redpanda@1036w.webp 1036w, ./redpanda@736w.webp 736w">
74
- <img src="./redpanda.jpg" alt="Red panda" loading="lazy" decoding="async" width="1936" height="1296" srcset="./redpanda.jpg 1936w, ./redpanda@1636w.jpg 1636w, ./redpanda@1336w.jpg 1336w, ./redpanda@1036w.jpg 1036w, ./redpanda@736w.jpg 736w" sizes="100vw">
76
+ <source type="image/avif"
77
+ srcset="./redpanda@1936w.avif 1936w, ./redpanda@1636w.avif 1636w,
78
+ ./redpanda@1336w.avif 1336w, ./redpanda@1036w.avif 1036w,
79
+ ./redpanda@736w.avif 736w">
80
+ <source type="image/webp"
81
+ srcset="./redpanda@1936w.webp 1936w, ./redpanda@1636w.webp 1636w,
82
+ ./redpanda@1336w.webp 1336w, ./redpanda@1036w.webp 1036w,
83
+ ./redpanda@736w.webp 736w">
84
+ <img src="./redpanda.jpg"
85
+ alt="Red panda"
86
+ loading="lazy"
87
+ decoding="async"
88
+ width="1936"
89
+ height="1296"
90
+ srcset="./redpanda.jpg 1936w, ./redpanda@1636w.jpg 1636w,
91
+ ./redpanda@1336w.jpg 1336w, ./redpanda@1036w.jpg 1036w,
92
+ ./redpanda@736w.jpg 736w"
93
+ sizes="100vw">
75
94
  </picture>
76
95
  ```
77
96
 
package/dist/cache.d.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
- import type { Image } from './compressors/images.js';
2
+ export type Category = 'img' | 'img-ext';
3
+ export type CacheData = {
4
+ buffer: Buffer;
5
+ meta: any;
6
+ };
3
7
  declare function cleanCache(full: boolean): Promise<void>;
4
8
  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, };
9
+ declare function getFromCache(category: Category, hash: string): Promise<CacheData | undefined>;
10
+ declare function addToCache(category: Category, hash: string, data: CacheData): Promise<void>;
11
+ export { cleanCache, computeCacheHash, getFromCache, addToCache };
package/dist/cache.js CHANGED
@@ -4,6 +4,7 @@ import * as fs from 'fs/promises';
4
4
  import { VERSION } from './version.js';
5
5
  import $state from './state.js';
6
6
  const CACHE_FOLDER = '.jampack/cache';
7
+ const CACHE_FOLDER_VERSION = CACHE_FOLDER + '/' + VERSION;
7
8
  async function cleanCache(full) {
8
9
  if (full) {
9
10
  try {
@@ -32,37 +33,37 @@ function computeCacheHash(buffer, options) {
32
33
  if ($state.args.nocache) {
33
34
  return '';
34
35
  }
35
- let hash = `${VERSION}/${hasha(buffer, { algorithm: 'sha256' })}`;
36
+ let hash = `${hasha(buffer, { algorithm: 'sha256' })}`;
36
37
  if (options) {
37
38
  hash += '/' + hasha(JSON.stringify(options), { algorithm: 'md5' });
38
39
  }
39
40
  return hash;
40
41
  }
41
- function getLocation(hash) {
42
- return path.join(CACHE_FOLDER, hash);
42
+ function getLocation(hash, category) {
43
+ return path.join(CACHE_FOLDER_VERSION, category, hash);
43
44
  }
44
- async function getFromCacheCompressImage(hash) {
45
+ async function getFromCache(category, hash) {
45
46
  if ($state.args.nocache) {
46
47
  return undefined;
47
48
  }
48
- const location = getLocation(hash);
49
+ const location = getLocation(hash, category);
49
50
  try {
50
- const data = await fs.readFile(path.join(location, 'data'));
51
- const format = JSON.parse((await fs.readFile(path.join(location, 'format'))).toString());
52
- return { data, format };
51
+ const buffer = await fs.readFile(path.join(location, 'data'));
52
+ const meta = JSON.parse((await fs.readFile(path.join(location, 'meta'))).toString());
53
+ return { buffer, meta };
53
54
  }
54
55
  catch (e) {
55
56
  // Problem during cache loading or not in cache
56
57
  }
57
58
  return undefined;
58
59
  }
59
- async function addToCacheCompressImage(hash, image) {
60
+ async function addToCache(category, hash, data) {
60
61
  if ($state.args.nocache) {
61
62
  return;
62
63
  }
63
- const location = getLocation(hash);
64
+ const location = getLocation(hash, category);
64
65
  await fs.mkdir(location, { recursive: true });
65
- await fs.writeFile(path.join(location, 'data'), image.data);
66
- await fs.writeFile(path.join(location, 'format'), JSON.stringify(image.format));
66
+ await fs.writeFile(path.join(location, 'data'), data.buffer);
67
+ await fs.writeFile(path.join(location, 'meta'), JSON.stringify(data.meta));
67
68
  }
68
- export { cleanCache, computeCacheHash, getFromCacheCompressImage, addToCacheCompressImage, };
69
+ export { cleanCache, computeCacheHash, getFromCache, addToCache };
@@ -1,6 +1,6 @@
1
1
  import sharp from 'sharp';
2
2
  import { optimize as svgo } from 'svgo';
3
- import { getFromCacheCompressImage, addToCacheCompressImage, computeCacheHash, } from '../cache.js';
3
+ import { getFromCache, addToCache, computeCacheHash } from '../cache.js';
4
4
  import config from '../config.js';
5
5
  export const AllImageFormat = ['webp', 'svg', 'jpg', 'png', 'avif'];
6
6
  function createWebpOptions(opt) {
@@ -13,9 +13,9 @@ function createWebpOptions(opt) {
13
13
  export async function compressImage(data, options) {
14
14
  var _a, _b;
15
15
  const cacheHash = computeCacheHash(data, options);
16
- const imageFromCache = await getFromCacheCompressImage(cacheHash);
16
+ const imageFromCache = await getFromCache('img', cacheHash);
17
17
  if (imageFromCache) {
18
- return imageFromCache;
18
+ return { data: imageFromCache.buffer, format: imageFromCache.meta };
19
19
  }
20
20
  // Load modifiable toFormat
21
21
  let toFormat = options.toFormat || 'unchanged';
@@ -128,7 +128,10 @@ export async function compressImage(data, options) {
128
128
  format: outputFormat,
129
129
  data: await sharpFile.toBuffer(),
130
130
  };
131
- await addToCacheCompressImage(cacheHash, outputImage);
131
+ await addToCache('img', cacheHash, {
132
+ buffer: outputImage.data,
133
+ meta: outputImage.format,
134
+ });
132
135
  // Go
133
136
  return outputImage;
134
137
  }
@@ -11,7 +11,7 @@ export type Options = {
11
11
  embed_size: number;
12
12
  srcset_min_width: number;
13
13
  external: {
14
- process: 'download' | 'cdn-srcset-when-possible' | 'add-dimensions-only' | 'off';
14
+ process: 'off' | 'download' | 'cdn-srcset-when-possible' | 'add-dimensions-only';
15
15
  src_include: RegExp;
16
16
  src_exclude: RegExp | null;
17
17
  };
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import { exit } from 'process';
11
11
  import kleur from 'kleur';
12
12
  import { cleanCache } from './cache.js';
13
13
  import { VERSION } from './version.js';
14
+ import { mkdirSync } from 'fs';
15
+ import { join } from 'path';
14
16
  const logo = ` __ __
15
17
  |__|____ _____ ___________ ____ | | __
16
18
  | \\__ \\ / \\\\____ \\__ \\ _/ ___\\| |/ /
@@ -54,6 +56,14 @@ program
54
56
  }
55
57
  // Clean cache
56
58
  cleanCache(options.cleancache);
59
+ // Create _jampack folder
60
+ try {
61
+ mkdirSync(join($state.dir, '_jampack'));
62
+ }
63
+ catch (e) {
64
+ console.error(e.message);
65
+ exit(1);
66
+ }
57
67
  if (!options.onlycomp) {
58
68
  printTitle('PASS 1 - Optimizing');
59
69
  console.time('Done');
package/dist/optimize.js CHANGED
@@ -10,6 +10,7 @@ import $state from './state.js';
10
10
  import kleur from 'kleur';
11
11
  import ora from 'ora';
12
12
  import { isLocal, Resource, translateSrc } from './utils/resource.js';
13
+ import { downloadExternalImage } from './optimizers/img-external.js';
13
14
  async function analyse(file) {
14
15
  var _a;
15
16
  console.log('▶ ' + file);
@@ -122,7 +123,7 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
122
123
  /*
123
124
  * Attribute 'src'
124
125
  */
125
- const attrib_src = img.attr('src');
126
+ let attrib_src = img.attr('src');
126
127
  if (!attrib_src) {
127
128
  const attrib_data_src = img.attr('data-src');
128
129
  if (attrib_data_src) {
@@ -189,18 +190,25 @@ async function processImage(htmlfile, $, imgElement, isAboveTheFold) {
189
190
  */
190
191
  if (!isLocal(attrib_src)) {
191
192
  switch (config.image.external.process) {
192
- case 'off':
193
- // Don't process external images
193
+ case 'off': // Don't process external images
194
194
  return;
195
- case 'download':
196
- // Download external image for local processing
195
+ case 'download': // Download external image for local processing
196
+ try {
197
+ attrib_src = await downloadExternalImage(attrib_src);
198
+ img.attr('src', attrib_src);
199
+ }
200
+ catch (e) {
201
+ $state.reportIssue(htmlfile, {
202
+ type: 'warn',
203
+ msg: `Failed to download image: ${attrib_src} - ${e.message}`,
204
+ });
205
+ return; // No more processing
206
+ }
197
207
  break;
198
- case 'cdn-srcset-when-possible':
199
- // Use unpic to use CDN capabilities
200
- throw new Error('External images with `cdn-srcset-when-possible` is not implemented yet.');
201
- case 'add-dimensions-only':
202
- // Only add dimensions of image
208
+ case 'add-dimensions-only': // Only add dimensions of image
203
209
  throw new Error('External images with `add-dimensions-only` is not implemented yet.');
210
+ case 'cdn-srcset-when-possible': // Use unpic to use CDN capabilities
211
+ throw new Error('External images with `cdn-srcset-when-possible` is not implemented yet.');
204
212
  }
205
213
  }
206
214
  /*
@@ -0,0 +1 @@
1
+ export declare function downloadExternalImage(href: string): Promise<string>;
@@ -0,0 +1,99 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs/promises';
3
+ import hasha from 'hasha';
4
+ import { fileTypeFromBuffer } from 'file-type';
5
+ import { addToCache, getFromCache } from '../cache.js';
6
+ import { parse } from '../utils/cache-control-parser.js';
7
+ import $state from '../state.js';
8
+ export async function downloadExternalImage(href) {
9
+ var _a, _b, _c, _d, _e;
10
+ const hash = hasha(href, {
11
+ algorithm: 'md5',
12
+ });
13
+ // Default with values from cache
14
+ let buffer;
15
+ let ext;
16
+ // Image is in cache?
17
+ const dataFromCache = await getFromCache('img-ext', hash);
18
+ const cacheControl = (_a = dataFromCache === null || dataFromCache === void 0 ? void 0 : dataFromCache.meta) === null || _a === void 0 ? void 0 : _a.CacheControl;
19
+ if (dataFromCache && cacheControl) {
20
+ const date = (_b = dataFromCache === null || dataFromCache === void 0 ? void 0 : dataFromCache.meta) === null || _b === void 0 ? void 0 : _b.Date;
21
+ const maxAge = cacheControl['max-age'];
22
+ const expires = (_c = dataFromCache === null || dataFromCache === void 0 ? void 0 : dataFromCache.meta) === null || _c === void 0 ? void 0 : _c.Expires;
23
+ let expireTime = NaN;
24
+ if (date && maxAge) {
25
+ expireTime = Date.parse(date) + maxAge * 1000;
26
+ }
27
+ else if (expires) {
28
+ expireTime = Date.parse(expires);
29
+ }
30
+ else {
31
+ // No max-age with Date and no expires
32
+ // No information for cache validity
33
+ // We have to make a request
34
+ }
35
+ // Is cache still valid?
36
+ if (expireTime && Date.now() < expireTime) {
37
+ buffer = dataFromCache.buffer;
38
+ ext = dataFromCache.meta.Extension;
39
+ }
40
+ }
41
+ // No buffer, then no cache hit so let's HTTP request
42
+ if (!buffer) {
43
+ const lastdate = (_d = dataFromCache === null || dataFromCache === void 0 ? void 0 : dataFromCache.meta) === null || _d === void 0 ? void 0 : _d.Date;
44
+ const fetchOption = lastdate
45
+ ? { headers: { 'If-Modified-Since': lastdate } }
46
+ : {};
47
+ console.log('Downloading ' + href);
48
+ const resp = await fetch(href, fetchOption);
49
+ switch (resp.status) {
50
+ case 200: // New image
51
+ const responseBuffer = await resp.arrayBuffer();
52
+ // Detect extension
53
+ ext = (_e = (await fileTypeFromBuffer(responseBuffer))) === null || _e === void 0 ? void 0 : _e.ext;
54
+ buffer = Buffer.from(responseBuffer);
55
+ // Did the server requested no cache?
56
+ const cacheControl = parse(resp.headers.get('Cache-Control') || '');
57
+ const maxAge = cacheControl['max-age'];
58
+ if (cacheControl['no-store'] ||
59
+ cacheControl['no-cache'] ||
60
+ cacheControl['must-revalidate'] ||
61
+ (maxAge !== undefined && maxAge < 1)) {
62
+ // No cache requested
63
+ }
64
+ else {
65
+ // Add downloaded image to cache
66
+ await addToCache('img-ext', hash, {
67
+ buffer,
68
+ meta: {
69
+ Date: resp.headers.get('Date'),
70
+ Extension: ext,
71
+ CacheControl: cacheControl,
72
+ },
73
+ });
74
+ }
75
+ break;
76
+ case 304: // Not modified - image in cache is good to use
77
+ if (dataFromCache) {
78
+ buffer = dataFromCache.buffer;
79
+ ext = dataFromCache.meta.Extension;
80
+ }
81
+ else {
82
+ // This is not possible
83
+ throw new Error('Assert error: 304 responses but no data in cache');
84
+ }
85
+ break;
86
+ default: // Otherwise something is wrong
87
+ throw new Error(resp.statusText);
88
+ }
89
+ }
90
+ // buffer can't be undefined here
91
+ if (!buffer)
92
+ throw new Error('Buffer is undefined');
93
+ // Construct local filename
94
+ if (!ext)
95
+ throw new Error('Unknown image format');
96
+ const filename = `/_jampack/${hash}.${ext}`;
97
+ fs.writeFile(path.join($state.dir, filename), buffer);
98
+ return filename;
99
+ }
@@ -0,0 +1,16 @@
1
+ export interface CacheControl {
2
+ 'max-age'?: number;
3
+ 's-maxage'?: number;
4
+ 'stale-while-revalidate'?: number;
5
+ 'stale-if-error'?: number;
6
+ public?: boolean;
7
+ private?: boolean;
8
+ 'no-store'?: boolean;
9
+ 'no-cache'?: boolean;
10
+ 'must-revalidate'?: boolean;
11
+ 'proxy-revalidate'?: boolean;
12
+ immutable?: boolean;
13
+ 'no-transform'?: boolean;
14
+ }
15
+ export declare const parse: (cacheControlHeader: string) => CacheControl;
16
+ export declare const stringify: (cacheControl: CacheControl) => string;
@@ -0,0 +1,95 @@
1
+ // Copied from https://github.com/etienne-martin/cache-control-parser (MIT licence)
2
+ // The npm package was not ESM - can't be used
3
+ const SUPPORTED_DIRECTIVES = [
4
+ 'max-age',
5
+ 's-maxage',
6
+ 'stale-while-revalidate',
7
+ 'stale-if-error',
8
+ 'public',
9
+ 'private',
10
+ 'no-store',
11
+ 'no-cache',
12
+ 'must-revalidate',
13
+ 'proxy-revalidate',
14
+ 'immutable',
15
+ 'no-transform',
16
+ ];
17
+ export const parse = (cacheControlHeader) => {
18
+ const cacheControl = {};
19
+ const directives = cacheControlHeader
20
+ .toLowerCase()
21
+ .split(',')
22
+ .map((str) => str
23
+ .trim()
24
+ .split('=')
25
+ .map((str) => str.trim()));
26
+ for (const [directive, value] of directives) {
27
+ switch (directive) {
28
+ case 'max-age':
29
+ const maxAge = parseInt(value, 10);
30
+ if (isNaN(maxAge))
31
+ continue;
32
+ cacheControl['max-age'] = maxAge;
33
+ break;
34
+ case 's-maxage':
35
+ const sharedMaxAge = parseInt(value, 10);
36
+ if (isNaN(sharedMaxAge))
37
+ continue;
38
+ cacheControl['s-maxage'] = sharedMaxAge;
39
+ break;
40
+ case 'stale-while-revalidate':
41
+ const staleWhileRevalidate = parseInt(value, 10);
42
+ if (isNaN(staleWhileRevalidate))
43
+ continue;
44
+ cacheControl['stale-while-revalidate'] = staleWhileRevalidate;
45
+ break;
46
+ case 'stale-if-error':
47
+ const staleIfError = parseInt(value, 10);
48
+ if (isNaN(staleIfError))
49
+ continue;
50
+ cacheControl['stale-if-error'] = staleIfError;
51
+ break;
52
+ case 'public':
53
+ cacheControl.public = true;
54
+ break;
55
+ case 'private':
56
+ cacheControl.private = true;
57
+ break;
58
+ case 'no-store':
59
+ cacheControl['no-store'] = true;
60
+ break;
61
+ case 'no-cache':
62
+ cacheControl['no-cache'] = true;
63
+ break;
64
+ case 'must-revalidate':
65
+ cacheControl['must-revalidate'] = true;
66
+ break;
67
+ case 'proxy-revalidate':
68
+ cacheControl['proxy-revalidate'] = true;
69
+ break;
70
+ case 'immutable':
71
+ cacheControl.immutable = true;
72
+ break;
73
+ case 'no-transform':
74
+ cacheControl['no-transform'] = true;
75
+ break;
76
+ }
77
+ }
78
+ return cacheControl;
79
+ };
80
+ export const stringify = (cacheControl) => {
81
+ const directives = [];
82
+ for (const [key, value] of Object.entries(cacheControl)) {
83
+ if (!SUPPORTED_DIRECTIVES.includes(key))
84
+ continue;
85
+ switch (typeof value) {
86
+ case 'boolean':
87
+ directives.push(`${key}`);
88
+ break;
89
+ case 'number':
90
+ directives.push(`${key}=${value}`);
91
+ break;
92
+ }
93
+ }
94
+ return directives.join(', ');
95
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divriots/jampack",
3
- "version": "0.11.7",
3
+ "version": "0.12.1",
4
4
  "description": "Packer for static websites",
5
5
  "author": "Georges Gomes @georges-gomes",
6
6
  "bin": {
@@ -45,7 +45,8 @@
45
45
  "watch": "pnpm build --watch",
46
46
  "try": "shx rm -fr demo && cp -R demo_rf demo && node ./dist/index.js ./demo --nocache --onlyoptim",
47
47
  "build": "shx rm -rf dist && tsc -b",
48
- "lint": "eslint . --ext .ts --config .eslintrc"
48
+ "lint": "eslint . --ext .ts --config .eslintrc",
49
+ "prepublishOnly": "pnpm build"
49
50
  },
50
51
  "engines": {
51
52
  "node": ">=14.0.0"