mnfst 0.5.75 → 0.5.78

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.
@@ -26,7 +26,7 @@
26
26
  *
27
27
  * Supported formats: pdf (default), png, jpeg, webp, csv, json.
28
28
  *
29
- * Library dependencies (html-to-image, jsPDF) are loaded lazily on first
29
+ * Library dependencies (modern-screenshot, jsPDF) are loaded lazily on first
30
30
  * use from jsDelivr; pages that never export pay nothing.
31
31
  *
32
32
  * Elements with `data-no-export` are excluded from visual snapshots.
@@ -151,12 +151,12 @@ function initializeExportPlugin() {
151
151
  }
152
152
 
153
153
  // Transparent 1×1 PNG, used as a fallback when an inline image fails to fetch.
154
- // html-to-image would otherwise reject the whole snapshot on a single CORS or 404.
154
+ // Without it, a single CORS or 404 image would reject the entire snapshot.
155
155
  const TRANSPARENT_PIXEL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=';
156
156
 
157
- // html-to-image rejects with the raw `image.onerror` Event when the assembled
158
- // SVG fails to decode (oversized clones, malformed inline assets, etc.).
159
- // Translate those into something readable before logging.
157
+ // The snapshot library rejects with the raw `image.onerror` Event when the
158
+ // assembled SVG fails to decode (oversized clones, malformed inline assets,
159
+ // etc.). Translate those into something readable before logging.
160
160
  function describeExportError(err) {
161
161
  if (err && err.message) return err;
162
162
  if (err && typeof Event !== 'undefined' && err instanceof Event) {
@@ -208,36 +208,41 @@ function initializeExportPlugin() {
208
208
  }
209
209
 
210
210
  function snapshotOptions(opts) {
211
- // Filter callback for html-to-image — exclude opt-out elements.
211
+ // Filter callback for modern-screenshot — exclude opt-out elements.
212
212
  const filter = (node) => {
213
213
  if (!node || node.nodeType !== 1) return true;
214
214
  return !(node.hasAttribute && node.hasAttribute('data-no-export'));
215
215
  };
216
216
  const out = {
217
- pixelRatio: Number(opts.resolution) > 0 ? Number(opts.resolution) : 2,
218
- cacheBust: true,
219
- // A single cross-origin image without CORS headers would otherwise
220
- // reject the whole snapshot. Substitute a transparent pixel so the
221
- // export still produces a usable file.
222
- imagePlaceholder: TRANSPARENT_PIXEL,
223
- filter
217
+ scale: Number(opts.resolution) > 0 ? Number(opts.resolution) : 2,
218
+ filter,
219
+ fetch: {
220
+ // Cross-origin assets without CORS headers fall back to this
221
+ // transparent pixel instead of rejecting the entire snapshot.
222
+ placeholderImage: TRANSPARENT_PIXEL,
223
+ // Always bypass the cache so we get fresh, CORS-correct copies.
224
+ bypassingCache: true,
225
+ },
224
226
  };
225
227
  if (opts.backgroundColor) out.backgroundColor = opts.backgroundColor;
226
- if (opts.width) out.canvasWidth = Number(opts.width);
227
- if (opts.height) out.canvasHeight = Number(opts.height);
228
+ if (opts.width) out.width = Number(opts.width);
229
+ if (opts.height) out.height = Number(opts.height);
228
230
  return out;
229
231
  }
230
232
 
231
- // Run an html-to-image call once. If it rejects with a raw Event (the SVG
232
- // failed to decode usually a font or oversized clone issue), retry once
233
- // with safer options before propagating the error. Most real-world failures
234
- // recover on the second attempt.
233
+ // Run a modern-screenshot call once. If it rejects with a raw Event (the
234
+ // assembled SVG failed to decode), retry once with safer options before
235
+ // propagating. Most real-world failures recover on the second attempt.
235
236
  async function snapshotWithFallback(lib, fn, target, so) {
236
237
  try {
237
238
  return await lib[fn](target, so);
238
239
  } catch (err) {
239
240
  if (err && err.message) throw err; // a real Error — don't mask it
240
- const safer = { ...so, skipFonts: true, pixelRatio: Math.min(so.pixelRatio || 2, 1) };
241
+ const safer = {
242
+ ...so,
243
+ scale: Math.min(so.scale || 2, 1),
244
+ font: false,
245
+ };
241
246
  return await lib[fn](target, safer);
242
247
  }
243
248
  }
@@ -263,21 +268,22 @@ function initializeExportPlugin() {
263
268
  }
264
269
 
265
270
  async function exportImage(opts, filename, ext) {
266
- const lib = await loadHtmlToImage();
271
+ const lib = await loadSnapshotLib();
267
272
  const target = resolveTarget(opts);
268
273
  const so = applySafeDimensions(target, snapshotOptions(opts));
269
274
  let dataUrl;
270
- if (ext === 'png') dataUrl = await snapshotWithFallback(lib, 'toPng', target, so);
275
+ if (ext === 'png') dataUrl = await snapshotWithFallback(lib, 'domToPng', target, so);
271
276
  else if (ext === 'jpeg') {
272
277
  if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
273
278
  so.quality = Number(opts.quality) > 0 && Number(opts.quality) <= 1
274
279
  ? Number(opts.quality)
275
280
  : 0.95;
276
- dataUrl = await snapshotWithFallback(lib, 'toJpeg', target, so);
281
+ dataUrl = await snapshotWithFallback(lib, 'domToJpeg', target, so);
277
282
  } else if (ext === 'webp') {
278
- const webpOpts = { ...so, type: 'image/webp', quality: opts.quality || 0.95 };
279
- const blob = await snapshotWithFallback(lib, 'toBlob', target, webpOpts);
280
- dataUrl = URL.createObjectURL(blob);
283
+ so.quality = Number(opts.quality) > 0 && Number(opts.quality) <= 1
284
+ ? Number(opts.quality)
285
+ : 0.95;
286
+ dataUrl = await snapshotWithFallback(lib, 'domToWebp', target, so);
281
287
  }
282
288
  triggerDownload(dataUrl, filename);
283
289
  }
@@ -287,17 +293,17 @@ function initializeExportPlugin() {
287
293
  // own @media print CSS — far more reliable than rasterizing a long page
288
294
  // and embedding it as a single image. The user sees the print dialog and
289
295
  // picks "Save as PDF" (or any installed PDF printer). Element-scoped PDFs
290
- // continue to use html-to-image + jsPDF, which is well-behaved at small
296
+ // continue to use modern-screenshot + jsPDF, which is well-behaved at small
291
297
  // target sizes.
292
298
  async function exportPdf(opts, filename) {
293
299
  if (!opts.target) {
294
300
  return printToPdf(filename);
295
301
  }
296
- const [imgLib, jsPDFCtor] = await Promise.all([loadHtmlToImage(), loadJsPDF()]);
302
+ const [imgLib, jsPDFCtor] = await Promise.all([loadSnapshotLib(), loadJsPDF()]);
297
303
  const target = resolveTarget(opts);
298
304
  const so = applySafeDimensions(target, snapshotOptions(opts));
299
305
  if (!so.backgroundColor) so.backgroundColor = effectivePageBackground();
300
- const dataUrl = await snapshotWithFallback(imgLib, 'toPng', target, so);
306
+ const dataUrl = await snapshotWithFallback(imgLib, 'domToPng', target, so);
301
307
  const img = await loadImage(dataUrl);
302
308
  const orientation = img.width > img.height ? 'landscape' : 'portrait';
303
309
  const pdf = new jsPDFCtor({ orientation, unit: 'pt', format: opts.pageSize || 'a4' });
@@ -423,15 +429,27 @@ function initializeExportPlugin() {
423
429
 
424
430
  // Lazy library loaders — cached promises.
425
431
 
426
- let htmlToImagePromise = null;
427
- function loadHtmlToImage() {
428
- if (htmlToImagePromise) return htmlToImagePromise;
429
- htmlToImagePromise = loadScript('https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js')
430
- .then(() => {
431
- if (!window.htmlToImage) throw new Error('html-to-image failed to load');
432
- return window.htmlToImage;
432
+ // modern-screenshot is an actively maintained TypeScript rewrite of
433
+ // html-to-image with proper CSS AST parsing. It handles @layer rules,
434
+ // CSS custom properties, modern color functions, and url(data:...) values
435
+ // inside cross-origin stylesheets — all of which html-to-image mishandles.
436
+ // It ships ESM-only, so we load via dynamic import() rather than a script
437
+ // tag. jsDelivr serves the ESM bundle directly.
438
+ let snapshotLibPromise = null;
439
+ function loadSnapshotLib() {
440
+ if (snapshotLibPromise) return snapshotLibPromise;
441
+ snapshotLibPromise = import('https://cdn.jsdelivr.net/npm/modern-screenshot@4/dist/index.mjs')
442
+ .then((mod) => {
443
+ if (!mod || typeof mod.domToPng !== 'function') {
444
+ throw new Error('modern-screenshot failed to load (missing expected exports)');
445
+ }
446
+ return mod;
447
+ })
448
+ .catch((err) => {
449
+ snapshotLibPromise = null; // allow retry on next call
450
+ throw err;
433
451
  });
434
- return htmlToImagePromise;
452
+ return snapshotLibPromise;
435
453
  }
436
454
 
437
455
  let jsPDFPromise = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.75",
3
+ "version": "0.5.78",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",