@divriots/jampack 0.31.1 → 0.33.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/LICENSE CHANGED
File without changes
package/README.md CHANGED
File without changes
package/dist/cache.js CHANGED
@@ -1,13 +1,15 @@
1
1
  import { hashSync as hasha } from 'hasha';
2
2
  import path from 'path';
3
- import * as fs from 'fs/promises';
3
+ import * as fsp from 'fs/promises';
4
4
  import { CACHE_VERSIONS } from './packagejson.js';
5
5
  const listOfCategories = ['img', 'img-ext'];
6
6
  function getCacheFolder(state) {
7
7
  return state.args.cache_folder || '.jampack/cache';
8
8
  }
9
9
  async function cleanCache(state, full) {
10
+ var _a;
10
11
  const CACHE_FOLDER = getCacheFolder(state);
12
+ const fs = (_a = state.vfs) !== null && _a !== void 0 ? _a : fsp;
11
13
  if (full) {
12
14
  try {
13
15
  await fs.rm(CACHE_FOLDER, { recursive: true });
@@ -66,9 +68,11 @@ function getLocation(state, hash, category) {
66
68
  return path.join(CACHE_FOLDER, category, getVersionOfCategory(category), hash);
67
69
  }
68
70
  async function getFromCache(state, category, hash) {
71
+ var _a;
69
72
  if (state.args.nocache) {
70
73
  return undefined;
71
74
  }
75
+ const fs = (_a = state.vfs) !== null && _a !== void 0 ? _a : fsp;
72
76
  const location = getLocation(state, hash, category);
73
77
  try {
74
78
  const buffer = await fs.readFile(path.join(location, 'data'));
@@ -81,9 +85,11 @@ async function getFromCache(state, category, hash) {
81
85
  return undefined;
82
86
  }
83
87
  async function addToCache(state, category, hash, data) {
88
+ var _a;
84
89
  if (state.args.nocache) {
85
90
  return;
86
91
  }
92
+ const fs = (_a = state.vfs) !== null && _a !== void 0 ? _a : fsp;
87
93
  const location = getLocation(state, hash, category);
88
94
  await fs.mkdir(location, { recursive: true });
89
95
  await fs.writeFile(path.join(location, 'data'), data.buffer);
package/dist/compress.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as fs from 'fs/promises';
1
+ import * as fsp from 'fs/promises';
2
2
  import * as path from 'path';
3
3
  import { formatBytes } from './utils.js';
4
4
  import { globby } from 'globby';
@@ -8,7 +8,9 @@ import { compressJS } from './compressors/js.js';
8
8
  import { compressHTML } from './compressors/html.js';
9
9
  import { compressImage } from './compressors/images.js';
10
10
  const processFile = async (state, file, stats) => {
11
+ var _a;
11
12
  let writeData = undefined;
13
+ const fs = (_a = state.vfs) !== null && _a !== void 0 ? _a : fsp;
12
14
  try {
13
15
  const ext = path.extname(file);
14
16
  switch (ext) {
@@ -69,6 +71,8 @@ const processFile = async (state, file, stats) => {
69
71
  state.reportSummary(result);
70
72
  };
71
73
  export async function compressFolder(state, exclude) {
74
+ var _a;
75
+ const fs = (_a = state.vfs) !== null && _a !== void 0 ? _a : fsp;
72
76
  const spinner = ora(getProgressText(state)).start();
73
77
  const globs = ['**/**', '!_jampack/**']; // Exclude jampack folder because already compressed
74
78
  if (exclude)
@@ -65,6 +65,12 @@ const default_options = {
65
65
  how: 'native',
66
66
  },
67
67
  },
68
+ video: {
69
+ autoplay_lazyload: {
70
+ when: 'below-the-fold',
71
+ how: 'js',
72
+ },
73
+ },
68
74
  misc: {
69
75
  prefetch_links: 'off',
70
76
  },
@@ -65,6 +65,12 @@ export type Options = {
65
65
  how: 'native' | 'js';
66
66
  };
67
67
  };
68
+ video: {
69
+ autoplay_lazyload: {
70
+ when: 'never' | 'below-the-fold' | 'always';
71
+ how: 'js';
72
+ };
73
+ };
68
74
  misc: {
69
75
  prefetch_links: 'in-viewport' | 'off';
70
76
  };
package/dist/index.js CHANGED
File without changes
package/dist/optimize.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { globby } from 'globby';
2
2
  import * as path from 'path';
3
- import * as fs from 'fs/promises';
3
+ import * as fsp from 'fs/promises';
4
4
  import { getCanonicalCdnForUrl, getTransformer } from 'unpic';
5
5
  import * as cheerio from '@divriots/cheerio';
6
6
  import { isNumeric } from './utils.js';
@@ -13,6 +13,7 @@ import { downloadExternalImage } from './optimizers/img-external.js';
13
13
  import { inlineCriticalCss } from './optimizers/inline-critical-css.js';
14
14
  import { prefetch_links_in_viewport } from './optimizers/prefetch-links.js';
15
15
  import { processIframe } from './optimizers/process-iframe.js';
16
+ import { processVideo } from './optimizers/process-video.js';
16
17
  const UNPIC_DEFAULT_HOST_REGEX = /^https:\/\/n\//g;
17
18
  const ABOVE_FOLD_DATA_ATTR = 'data-abovethefold';
18
19
  function getIntAttr(img, attr) {
@@ -25,76 +26,20 @@ function getIntAttr(img, attr) {
25
26
  return parsed;
26
27
  }
27
28
  async function analyse(state, file) {
29
+ var _a;
28
30
  console.log('▶ ' + file);
29
31
  // For processes to add into
30
32
  const appendToBody = {};
33
+ const fs = (_a = state.vfs) !== null && _a !== void 0 ? _a : fsp;
31
34
  const html = (await fs.readFile(path.join(state.dir, file))).toString();
32
35
  if (!html.includes('<html'))
33
36
  return;
34
37
  const $ = cheerio.load(html, { sourceCodeLocationInfo: true });
35
38
  const theFold = getTheFold($);
36
- const imgs = $('img');
37
- const imgsArray = [];
38
- imgs.each((index, imgElement) => {
39
- imgsArray.push(imgElement);
40
- });
41
- // Process images sequentially
42
- const spinnerImg = ora({ prefixText: ' ' }).start();
43
- for (let i = 0; i < imgsArray.length; i++) {
44
- const imgElement = imgsArray[i];
45
- spinnerImg.text = kleur.dim(`<img> [${i + 1}/${imgsArray.length}] ${$(imgElement).attr('src')}`);
46
- const img = $(imgElement);
47
- const isAboveTheFold = isElementAboveTheFold(img, imgElement, theFold);
48
- img.removeAttr(ABOVE_FOLD_DATA_ATTR);
49
- try {
50
- await processImage(state, file, img, isAboveTheFold);
51
- }
52
- catch (e) {
53
- state.reportIssue(file, {
54
- type: 'erro',
55
- msg: e.message ||
56
- `Unexpected error while processing image: ${JSON.stringify(e)}`,
57
- });
58
- }
59
- }
60
- // Reset spinner
61
- spinnerImg.text = kleur.dim(`<img> [${imgsArray.length}/${imgsArray.length}]`);
62
- // Notify issues
63
- const issues = state.issues.get(file);
64
- if (issues) {
65
- spinnerImg.fail();
66
- console.log(kleur.red(` ${issues.length} issue${issues.length > 1 ? 's' : ''}`));
67
- }
68
- else {
69
- spinnerImg.succeed();
70
- }
71
- const iframes = $('iframe');
72
- const iframesArray = [];
73
- iframes.each((index, ifElement) => {
74
- iframesArray.push(ifElement);
75
- });
76
- // Process iframes sequentially
77
- const spinnerIframe = ora({ prefixText: ' ' }).start();
78
- for (let i = 0; i < iframesArray.length; i++) {
79
- const ifElement = iframesArray[i];
80
- spinnerIframe.text = kleur.dim(`<iframe> [${i + 1}/${iframesArray.length}] ${$(ifElement).attr('src')}`);
81
- const ifr = $(ifElement);
82
- const isAboveTheFold = isElementAboveTheFold(ifr, ifElement, theFold);
83
- ifr.removeAttr(ABOVE_FOLD_DATA_ATTR);
84
- try {
85
- await processIframe(state, file, $, ifElement, isAboveTheFold, appendToBody);
86
- }
87
- catch (e) {
88
- state.reportIssue(file, {
89
- type: 'erro',
90
- msg: e.message ||
91
- `Unexpected error while processing image: ${JSON.stringify(e)}`,
92
- });
93
- }
94
- }
95
- // Reset spinner
96
- spinnerIframe.text = kleur.dim(`<iframe> [${iframesArray.length}/${iframesArray.length}]`);
97
- spinnerIframe.succeed();
39
+ await processTag(state, file, $, 'img', 'src', processImage, theFold, appendToBody);
40
+ await processTag(state, file, $, 'iframe', 'src', processIframe, theFold, appendToBody);
41
+ await processTag(state, file, $, 'video', 'src', processVideo, theFold, appendToBody);
42
+ await processTag(state, file, $, 'meta[property$=":image"], meta[name$=":image"]', 'content', processMetaImage, theFold, appendToBody);
98
43
  // Remove the fold
99
44
  //
100
45
  if (theFold) {
@@ -206,7 +151,7 @@ function getTheFold($) {
206
151
  return 0;
207
152
  }
208
153
  async function processImage(state, htmlfile, img, isAboveTheFold) {
209
- var _a, _b;
154
+ var _a, _b, _c;
210
155
  /*
211
156
  * Attribute 'src'
212
157
  */
@@ -284,28 +229,21 @@ async function processImage(state, htmlfile, img, isAboveTheFold) {
284
229
  /*
285
230
  * Check for external images
286
231
  */
287
- if (!isLocal(attrib_src) &&
288
- isIncluded(attrib_src, config.image.external.src_include, config.image.external.src_exclude)) {
289
- // Don't process external images
290
- if (config.image.external.process === 'off')
232
+ const { external } = state.options.image;
233
+ if (isExternalImage(attrib_src, external)) {
234
+ if (external.process === 'off')
235
+ // Don't process external images
291
236
  return;
292
237
  try {
293
- if (config.image.external.process === 'download') {
294
- // Download external image for local processing
295
- attrib_src = await downloadExternalImage(state, htmlfile, attrib_src);
296
- }
297
- else if (typeof config.image.external.process === 'function') {
298
- // Custom processing for external image
299
- attrib_src = await config.image.external.process(attrib_src);
300
- }
301
- img.attr('src', attrib_src);
238
+ const external_src = await externalImage(attrib_src, state, htmlfile);
239
+ if (external_src)
240
+ img.attr('src', (attrib_src = external_src));
302
241
  }
303
242
  catch (e) {
304
- state.reportIssue(htmlfile, {
243
+ return state.reportIssue(htmlfile, {
305
244
  type: 'warn',
306
245
  msg: `Failed to process external image: ${attrib_src} - ${e.message}`,
307
- });
308
- return; // No more processing
246
+ }); // No more processing
309
247
  }
310
248
  }
311
249
  switch (config.image.cdn.process) {
@@ -357,7 +295,7 @@ async function processImage(state, htmlfile, img, isAboveTheFold) {
357
295
  /*
358
296
  * Loading image
359
297
  */
360
- const originalImage = await Resource.loadResource(state.dir, htmlfile, attrib_src);
298
+ const originalImage = await Resource.loadResource(state, htmlfile, attrib_src);
361
299
  // No file -> give up
362
300
  if (!originalImage) {
363
301
  state.reportIssue(htmlfile, {
@@ -394,7 +332,7 @@ async function processImage(state, htmlfile, img, isAboveTheFold) {
394
332
  const newSrc = path.join(path.dirname(attrib_src), path.basename(attrib_src, path.extname(attrib_src)) +
395
333
  `.${newImage.format}`);
396
334
  if (!state.compressedFiles.has(newFilename) && !state.args.nowrite) {
397
- fs.writeFile(newFilename, newImage.data);
335
+ await ((_b = state.vfs) !== null && _b !== void 0 ? _b : fsp).writeFile(newFilename, newImage.data);
398
336
  }
399
337
  // Mark new file as compressed
400
338
  state.compressedFiles.add(newFilename);
@@ -535,7 +473,7 @@ async function processImage(state, htmlfile, img, isAboveTheFold) {
535
473
  // https://twitter.com/jonsneyers/status/1346389917816008704?s=19
536
474
  const isLossless = mime === 'image/png' ||
537
475
  (mime === 'image/webp' &&
538
- ((_b = (await originalImage.getImageMeta())) === null || _b === void 0 ? void 0 : _b.isLossless));
476
+ ((_c = (await originalImage.getImageMeta())) === null || _c === void 0 ? void 0 : _c.isLossless));
539
477
  if (!isLossless) {
540
478
  sourcesToGenerate.push({
541
479
  mime: 'image/avif',
@@ -566,6 +504,103 @@ async function processImage(state, htmlfile, img, isAboveTheFold) {
566
504
  }
567
505
  }
568
506
  }
507
+ async function processMetaImage(state, htmlfile, meta) {
508
+ var _a, _b, _c;
509
+ const attribute = 'content';
510
+ let content = meta.attr(attribute);
511
+ if (!content)
512
+ return state.reportIssue(htmlfile, {
513
+ type: 'warn',
514
+ msg: `Missing [content] on meta - processing skipped.`,
515
+ });
516
+ // Setup to ignore
517
+ const { src_exclude, src_include, cdn, external } = state.options.image;
518
+ if (!isIncluded(content, src_include, src_exclude))
519
+ return;
520
+ /*
521
+ * Check for external images
522
+ */
523
+ if (isExternalImage(content, external)) {
524
+ if (external.process === 'off')
525
+ // Don't process external images
526
+ return;
527
+ try {
528
+ const external_content = await externalImage(content, state, htmlfile);
529
+ if (external_content)
530
+ meta.attr(attribute, (content = external_content));
531
+ }
532
+ catch (e) {
533
+ return state.reportIssue(htmlfile, {
534
+ type: 'warn',
535
+ msg: `Failed to process external image: ${attribute} - ${e.message}`,
536
+ }); // No more processing
537
+ }
538
+ }
539
+ const propName = meta.attr('property') ? 'property' : 'name';
540
+ const width = meta.closest(`meta[${propName}:width]`).attr(attribute);
541
+ switch (cdn.process) {
542
+ case 'off':
543
+ break;
544
+ case 'optimize':
545
+ let cdnTransformer = cdn.transformer;
546
+ if (cdnTransformer && !cdn.src_include) {
547
+ throw new Error('config.image.cdn.src_include is required when specifying a config.image.cdn.transformer');
548
+ }
549
+ if (!cdnTransformer) {
550
+ const canonical = getCanonicalCdnForUrl(content);
551
+ if (!canonical)
552
+ break;
553
+ cdnTransformer = getTransformer(canonical.cdn);
554
+ }
555
+ if (!cdnTransformer)
556
+ break;
557
+ if (!isIncluded(content, cdn.src_include || /.*/, cdn.src_exclude))
558
+ break;
559
+ if (!width)
560
+ return state.reportIssue(htmlfile, {
561
+ type: 'warn',
562
+ msg: `Missing or malformed'width' attribute for image ${content}, unable to perform CDN transform`,
563
+ });
564
+ meta.attr(attribute, (_b = (_a = cdnTransformer({ url: content, width: +width })) === null || _a === void 0 ? void 0 : _a.toString()) === null || _b === void 0 ? void 0 : _b.replace(UNPIC_DEFAULT_HOST_REGEX, '/'));
565
+ return;
566
+ }
567
+ /*
568
+ * Loading image
569
+ */
570
+ const originalImage = await Resource.loadResource(state, htmlfile, content);
571
+ if (!originalImage)
572
+ return state.reportIssue(htmlfile, {
573
+ type: 'erro',
574
+ msg: `Can't find img on disk ${attribute}="${content}"`,
575
+ });
576
+ /*
577
+ * Compress image
578
+ */
579
+ const newImage = await compressImage(state, await originalImage.getData(), {
580
+ resize: width ? { width: +width } : undefined,
581
+ toFormat: meta.closest(`meta[${propName}:type]`).attr(attribute) === 'image/png'
582
+ ? 'png'
583
+ : 'jpeg',
584
+ });
585
+ if ((newImage === null || newImage === void 0 ? void 0 : newImage.data) && newImage.data.length < (await originalImage.getLen())) {
586
+ meta.attr(attribute, path.join(path.dirname(content), path.basename(content, path.extname(content)) + `.${newImage.format}`));
587
+ const newFilename = path.join(path.dirname(originalImage.filePathAbsolute), path.basename(originalImage.filePathAbsolute, path.extname(originalImage.filePathAbsolute)) + `.${newImage.format}`);
588
+ if (!state.compressedFiles.has(newFilename)) {
589
+ if (!state.args.nowrite)
590
+ await ((_c = state.vfs) !== null && _c !== void 0 ? _c : fsp).writeFile(newFilename, newImage.data);
591
+ // Mark new file as compressed
592
+ state.compressedFiles.add(newFilename);
593
+ // Report compression result
594
+ state.reportSummary({
595
+ action: newFilename !== originalImage.filePathAbsolute
596
+ ? `${await originalImage.getExt()}->${newImage.format}`
597
+ : path.extname(originalImage.filePathAbsolute),
598
+ originalSize: await originalImage.getLen(),
599
+ compressedSize: newImage.data.length,
600
+ });
601
+ }
602
+ }
603
+ }
569
604
  const isIncluded = (src, includeConf, excludeConf) => !!src.match(includeConf) && (!excludeConf || !src.match(excludeConf));
570
605
  async function _generateSrcSet({ options }, startSrc, imageWidth, imageHeight, transformSrc) {
571
606
  // Start from original image
@@ -607,6 +642,7 @@ async function generateSrcSet(state, htmlfile, originalImage, startSrc, startSrc
607
642
  const imageHeight = (meta === null || meta === void 0 ? void 0 : meta.height) || 0;
608
643
  let previousImageSize = startSrcLength || Number.MAX_VALUE;
609
644
  return _generateSrcSet(state, startSrc, imageWidth, imageHeight, async (valueW) => {
645
+ var _a;
610
646
  const src = imageSrc(`@${valueW}w`);
611
647
  const absoluteFilename = translateSrc(state.dir, path.dirname(htmlfile), src);
612
648
  // Don't generate srcset file twice
@@ -621,7 +657,7 @@ async function generateSrcSet(state, htmlfile, originalImage, startSrc, startSrc
621
657
  else {
622
658
  // Write file
623
659
  if (!state.args.nowrite) {
624
- fs.writeFile(absoluteFilename, compressedImage.data);
660
+ await ((_a = state.vfs) !== null && _a !== void 0 ? _a : fsp).writeFile(absoluteFilename, compressedImage.data);
625
661
  }
626
662
  // Set new previous size to beat
627
663
  previousImageSize = compressedImage.data.length;
@@ -765,6 +801,7 @@ async function setImageSize(state, htmlfile, img, image) {
765
801
  throw new Error(`Unexpected issue when resolving image size "${image.src}"`);
766
802
  }
767
803
  export async function optimize(state, include, exclude) {
804
+ var _a;
768
805
  const glob = include ? [include] : ['**/*.{htm,html}'];
769
806
  if (exclude)
770
807
  glob.push('!' + exclude);
@@ -772,5 +809,55 @@ export async function optimize(state, include, exclude) {
772
809
  // Sequential async
773
810
  for (const file of paths) {
774
811
  await analyse(state, file);
812
+ (_a = state.onAnalysedFile) === null || _a === void 0 ? void 0 : _a.call(state, file);
775
813
  }
776
814
  }
815
+ async function processTag(state, file, $, tag, attribute_to_log, processor, theFold, appendToBody) {
816
+ const previous_issues = state.issues.get(file);
817
+ const tags = $(tag);
818
+ const tagsArray = [];
819
+ tags.each((index, tagElement) => {
820
+ tagsArray.push(tagElement);
821
+ });
822
+ // Process tags sequentially
823
+ const spinnerImg = ora({ prefixText: ' ' }).start();
824
+ for (let i = 0; i < tagsArray.length; i++) {
825
+ const tagElement = tagsArray[i];
826
+ spinnerImg.text = kleur.dim(`<${tag}> [${i + 1}/${tagsArray.length}] ${$(tagElement).attr(attribute_to_log)}`);
827
+ const el = $(tagElement);
828
+ const isAboveTheFold = isElementAboveTheFold(el, tagElement, theFold);
829
+ el.removeAttr(ABOVE_FOLD_DATA_ATTR);
830
+ try {
831
+ await processor(state, file, el, isAboveTheFold, appendToBody);
832
+ }
833
+ catch (e) {
834
+ state.reportIssue(file, {
835
+ type: 'erro',
836
+ msg: e.message ||
837
+ `Unexpected error while processing ${tag}: ${JSON.stringify(e)}`,
838
+ });
839
+ }
840
+ }
841
+ // Reset spinner
842
+ spinnerImg.text = kleur.dim(`<${tag}> [${tagsArray.length}/${tagsArray.length}]`);
843
+ // Notify issues
844
+ const issues = state.issues.get(file);
845
+ const newIssues = ((issues === null || issues === void 0 ? void 0 : issues.length) || 0) - ((previous_issues === null || previous_issues === void 0 ? void 0 : previous_issues.length) || 0);
846
+ if (newIssues > 0) {
847
+ spinnerImg.fail();
848
+ }
849
+ else {
850
+ spinnerImg.succeed();
851
+ }
852
+ }
853
+ const isExternalImage = (content, external) => !isLocal(content) &&
854
+ isIncluded(content, external.src_include, external.src_exclude);
855
+ async function externalImage(content, state, htmlfile) {
856
+ const { process } = state.options.image.external;
857
+ if (process === 'download')
858
+ // Download external image for local processing
859
+ return await downloadExternalImage(state, htmlfile, content);
860
+ if (typeof process === 'function')
861
+ // Custom processing for external image
862
+ return await process(content);
863
+ }
@@ -1,12 +1,12 @@
1
1
  import * as path from 'path';
2
- import * as fs from 'fs/promises';
2
+ import * as fsp from 'fs/promises';
3
3
  import { hashSync as hasha } from 'hasha';
4
4
  import { fileTypeFromBuffer } from 'file-type';
5
5
  import { addToCache, getFromCache } from '../cache.js';
6
6
  import { parse } from '../utils/cache-control-parser.js';
7
7
  import '../utils/polyfill-fetch.js';
8
8
  export async function downloadExternalImage(state, htmlfile, href) {
9
- var _a, _b, _c, _d, _e;
9
+ var _a, _b, _c, _d, _e, _f;
10
10
  const hash = hasha(href, {
11
11
  algorithm: 'md5',
12
12
  });
@@ -97,6 +97,6 @@ export async function downloadExternalImage(state, htmlfile, href) {
97
97
  throw new Error('Unknown image format');
98
98
  const htmlFolder = path.dirname(htmlfile);
99
99
  const filename = path.relative(path.join(state.dir, htmlFolder), path.join(state.dir, `_jampack/${contentHash}.${ext}`));
100
- await fs.writeFile(path.join(state.dir, htmlFolder, filename), buffer);
100
+ await ((_f = state.vfs) !== null && _f !== void 0 ? _f : fsp).writeFile(path.join(state.dir, htmlFolder, filename), buffer);
101
101
  return filename;
102
102
  }
@@ -1,3 +1,3 @@
1
1
  import * as cheerio from '@divriots/cheerio';
2
2
  import type { GlobalState } from '../state.js';
3
- export declare function processIframe(state: GlobalState, htmlfile: string, $: cheerio.CheerioAPI, imgElement: cheerio.Element, isAboveTheFold: boolean, appendToBody: Record<string, string>): Promise<void>;
3
+ export declare function processIframe(state: GlobalState, htmlfile: string, iframe: cheerio.Cheerio<cheerio.Element>, isAboveTheFold: boolean, appendToBody: Record<string, string>): Promise<void>;
@@ -1,6 +1,5 @@
1
- import { install_dependency } from '../utils/install-dep.js';
2
- export async function processIframe(state, htmlfile, $, imgElement, isAboveTheFold, appendToBody) {
3
- const iframe = $(imgElement);
1
+ import { install_lozad } from '../utils/install-dep.js';
2
+ export async function processIframe(state, htmlfile, iframe, isAboveTheFold, appendToBody) {
4
3
  // Reset loading attribute
5
4
  iframe.removeAttr('loading');
6
5
  const lazyloadOptions = state.options.iframe.lazyload;
@@ -18,19 +17,7 @@ export async function processIframe(state, htmlfile, $, imgElement, isAboveTheFo
18
17
  iframe.attr('class', 'jampack-lozad');
19
18
  iframe.attr('data-src', src);
20
19
  iframe.removeAttr('src');
21
- await install_dependency(state, htmlfile, {
22
- source: {
23
- npm_package_name: 'lozad',
24
- absolute_path_to_file: '/dist',
25
- filename: 'lozad.es.js',
26
- },
27
- destination: {
28
- folder_name: 'lozad-1.16',
29
- code_loader: `import lozad from "./lozad.es.js";
30
- const observer = lozad('.jampack-lozad', { rootMargin: '100px 0px', threshold: [0.1] });
31
- observer.observe();`,
32
- },
33
- }, appendToBody);
20
+ await install_lozad(state, htmlfile, appendToBody);
34
21
  }
35
22
  }
36
23
  }
@@ -0,0 +1,3 @@
1
+ import * as cheerio from '@divriots/cheerio';
2
+ import type { GlobalState } from '../state.js';
3
+ export declare function processVideo(state: GlobalState, htmlfile: string, video: cheerio.Cheerio<cheerio.Element>, isAboveTheFold: boolean, appendToBody: Record<string, string>): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { install_lozad } from '../utils/install-dep.js';
2
+ export async function processVideo(state, htmlfile, video, isAboveTheFold, appendToBody) {
3
+ const autoplay = video.attr('autoplay');
4
+ if (!autoplay || autoplay === '0' || autoplay === 'false') {
5
+ // If the video is not autoplay we can postpone loading
6
+ // if there is a poster
7
+ if (video.attr('poster')) {
8
+ video.attr('preload', 'none');
9
+ return;
10
+ }
11
+ // TODO create poster for videos without poster
12
+ }
13
+ const lazyloadOptions = state.options.video.autoplay_lazyload;
14
+ if (lazyloadOptions.when === 'never') {
15
+ return;
16
+ }
17
+ if (lazyloadOptions.when === 'always' ||
18
+ (lazyloadOptions.when === 'below-the-fold' && !isAboveTheFold)) {
19
+ if (lazyloadOptions.how === 'js') {
20
+ video.attr('class', 'jampack-lozad');
21
+ const src = video.attr('src');
22
+ if (src) {
23
+ video.attr('data-src', src);
24
+ video.removeAttr('src');
25
+ }
26
+ await install_lozad(state, htmlfile, appendToBody);
27
+ }
28
+ }
29
+ }
package/dist/state.d.ts CHANGED
@@ -29,6 +29,8 @@ export declare class GlobalState {
29
29
  installed_dependencies: Set<string>;
30
30
  summary: Summary;
31
31
  summaryByExtension: Record<string, Summary>;
32
+ vfs?: typeof import('fs/promises');
33
+ onAnalysedFile?: (file: string) => void;
32
34
  reportIssue(sourceFile: string, issue: Issue): void;
33
35
  reportSummary(r: ReportItem): void;
34
36
  }
@@ -10,3 +10,4 @@ export declare function install_dependency(state: GlobalState, html_file: string
10
10
  code_loader: string;
11
11
  };
12
12
  }, appendToBody: Record<string, string>): Promise<void>;
13
+ export declare function install_lozad(state: GlobalState, html_file: string, appendToBody: Record<string, string>): Promise<void>;
@@ -1,12 +1,14 @@
1
- import * as fs from 'fs/promises';
1
+ import * as fsp from 'fs/promises';
2
2
  import * as path from 'path';
3
3
  import { createRequire } from 'node:module';
4
4
  const require = createRequire(import.meta.url);
5
5
  export async function install_dependency(state, html_file, options, appendToBody) {
6
+ var _a;
6
7
  const path_html = path.dirname('/' + html_file);
7
8
  const folder = `/_jampack/${options.destination.folder_name}`;
8
9
  const src_filename = options.source.filename;
9
10
  const url_loader = `${folder}/loader.js`;
11
+ const fs = (_a = state.vfs) !== null && _a !== void 0 ? _a : fsp;
10
12
  // Install dependency in /_jampack if not done yet
11
13
  if (!state.installed_dependencies.has(options.source.npm_package_name)) {
12
14
  const quickLinkDestination = path.join(state.dir, folder, src_filename);
@@ -17,7 +19,12 @@ export async function install_dependency(state, html_file, options, appendToBody
17
19
  await fs.writeFile(path_loader, code_loader);
18
20
  // Write quicklink code
19
21
  const source = require.resolve(`${options.source.npm_package_name}${options.source.absolute_path_to_file}/${src_filename}`);
20
- await fs.copyFile(source, quickLinkDestination);
22
+ if (state.vfs) {
23
+ await state.vfs.writeFile(quickLinkDestination, await fsp.readFile(source));
24
+ }
25
+ else {
26
+ await fs.copyFile(source, quickLinkDestination);
27
+ }
21
28
  state.installed_dependencies.add(options.source.npm_package_name);
22
29
  }
23
30
  // Add custom code to the end of body if not done yet
@@ -25,3 +32,18 @@ export async function install_dependency(state, html_file, options, appendToBody
25
32
  appendToBody[options.source.npm_package_name] = `<script type="module" src="${path.relative(path_html, url_loader)}"></script>`;
26
33
  }
27
34
  }
35
+ export async function install_lozad(state, html_file, appendToBody) {
36
+ return install_dependency(state, html_file, {
37
+ source: {
38
+ npm_package_name: 'lozad',
39
+ absolute_path_to_file: '/dist',
40
+ filename: 'lozad.es.js',
41
+ },
42
+ destination: {
43
+ folder_name: 'lozad-1.16',
44
+ code_loader: `import lozad from "./lozad.es.js";
45
+ const observer = lozad('.jampack-lozad', { rootMargin: '100px 0px', threshold: [0.1] });
46
+ observer.observe();`,
47
+ },
48
+ }, appendToBody);
49
+ }
@@ -1,5 +1,6 @@
1
1
  import { FileExtension, MimeType } from 'file-type';
2
2
  import { ImageFormat } from '../compressors/images.js';
3
+ import { GlobalState } from '../state.js';
3
4
  type ImageMeta = {
4
5
  width: number | undefined;
5
6
  height: number | undefined;
@@ -11,13 +12,14 @@ type ImageMeta = {
11
12
  * File extension, mime and data are loaded only on demand, then cached.
12
13
  */
13
14
  export declare class Resource {
15
+ private state;
14
16
  readonly src: string;
15
17
  readonly filePathAbsolute: string;
16
18
  private ext;
17
19
  private mime;
18
20
  private data;
19
21
  private image_meta;
20
- constructor(src: string, filePathAbsolute: string);
22
+ constructor(state: GlobalState, src: string, filePathAbsolute: string);
21
23
  getData(): Promise<Buffer>;
22
24
  getImageMeta(): Promise<ImageMeta | null | undefined>;
23
25
  getLen(): Promise<number>;
@@ -25,9 +27,8 @@ export declare class Resource {
25
27
  getImageFormat(): Promise<ImageFormat | undefined>;
26
28
  getMime(): Promise<MimeType | 'image/svg+xml'>;
27
29
  private loadFileType;
28
- static loadResource(projectRoot: string, relativeFile: string, src: string): Promise<Resource | undefined>;
30
+ static loadResource(state: GlobalState, relativeFile: string, src: string): Promise<Resource | undefined>;
29
31
  }
30
32
  export declare function isLocal(src: string): boolean;
31
- export declare function fileExists(path: string): Promise<boolean>;
32
33
  export declare function translateSrc(projectRoot: string, htmlRelativePath: string, src: string): string;
33
34
  export {};
@@ -1,6 +1,6 @@
1
1
  import * as url from 'url';
2
2
  import * as path from 'path';
3
- import * as fs from 'fs/promises';
3
+ import * as fsp from 'fs/promises';
4
4
  import { fileTypeFromBuffer } from 'file-type';
5
5
  import sharp from 'sharp';
6
6
  import { AllImageFormat } from '../compressors/images.js';
@@ -8,13 +8,15 @@ import { AllImageFormat } from '../compressors/images.js';
8
8
  * File extension, mime and data are loaded only on demand, then cached.
9
9
  */
10
10
  export class Resource {
11
- constructor(src, filePathAbsolute) {
11
+ constructor(state, src, filePathAbsolute) {
12
+ this.state = state;
12
13
  this.src = src;
13
14
  this.filePathAbsolute = filePathAbsolute;
14
15
  }
15
16
  async getData() {
17
+ var _a;
16
18
  if (this.data === undefined) {
17
- this.data = await fs.readFile(this.filePathAbsolute);
19
+ this.data = await ((_a = this.state.vfs) !== null && _a !== void 0 ? _a : fsp).readFile(this.filePathAbsolute);
18
20
  }
19
21
  return this.data;
20
22
  }
@@ -98,7 +100,7 @@ export class Resource {
98
100
  throw new Error(`Unknown file type "${this.src}"`);
99
101
  }
100
102
  }
101
- static async loadResource(projectRoot, relativeFile, src) {
103
+ static async loadResource(state, relativeFile, src) {
102
104
  if (!isLocal(src)) {
103
105
  throw new Error('src should be local');
104
106
  }
@@ -106,10 +108,10 @@ export class Resource {
106
108
  if (!u.pathname) {
107
109
  throw new Error(`Invalid src format "${src}"`);
108
110
  }
109
- const relativePath = path.join(projectRoot, src.startsWith('/') ? '' : path.dirname(relativeFile), u.pathname);
111
+ const relativePath = path.join(state.dir, src.startsWith('/') ? '' : path.dirname(relativeFile), u.pathname);
110
112
  let absolutePath = path.resolve(relativePath);
111
- if (await fileExists(absolutePath)) {
112
- return new Resource(src, absolutePath);
113
+ if (await fileExists(state, absolutePath)) {
114
+ return new Resource(state, src, absolutePath);
113
115
  }
114
116
  return undefined;
115
117
  }
@@ -118,9 +120,10 @@ export function isLocal(src) {
118
120
  const u = url.parse(src);
119
121
  return !u.host;
120
122
  }
121
- export async function fileExists(path) {
123
+ async function fileExists(state, path) {
124
+ var _a;
122
125
  try {
123
- await fs.stat(path);
126
+ await ((_a = state.vfs) !== null && _a !== void 0 ? _a : fsp).stat(path);
124
127
  }
125
128
  catch (e) {
126
129
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divriots/jampack",
3
- "version": "0.31.1",
3
+ "version": "0.33.0",
4
4
  "cache-version": {
5
5
  "img": "v1",
6
6
  "img-ext": "v1"
@@ -60,18 +60,17 @@
60
60
  "tslib": "^2.6.2",
61
61
  "typescript": "^5.4.5"
62
62
  },
63
+ "engines": {
64
+ "node": ">=14.0.0"
65
+ },
66
+ "bugs": "https://github.com/divriots/jampack/issues",
67
+ "keywords": [],
68
+ "types": "dist/index.d.ts",
63
69
  "scripts": {
64
70
  "watch": "pnpm build --watch",
65
71
  "try": "shx rm -fr demo && cp -R demo_drc demo && node ./dist/index.js ./demo --nocache",
66
72
  "build": "shx rm -rf dist && tsc -b",
67
73
  "lint": "eslint . --ext .ts --config .eslintrc",
68
- "prepublishOnly": "pnpm build",
69
74
  "cleanAllDotJampack": "find -s . | grep '.jampack$' | xargs rm -fr"
70
- },
71
- "engines": {
72
- "node": ">=14.0.0"
73
- },
74
- "bugs": "https://github.com/divriots/jampack/issues",
75
- "keywords": [],
76
- "types": "dist/index.d.ts"
77
- }
75
+ }
76
+ }