@turntrout/subfont 1.5.0 → 1.6.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.
@@ -7,6 +7,7 @@ const compileQuery = require('assetgraph/lib/compileQuery');
7
7
  const findCustomPropertyDefinitions = require('./findCustomPropertyDefinitions');
8
8
  const extractReferencedCustomPropertyNames = require('./extractReferencedCustomPropertyNames');
9
9
  const injectSubsetDefinitions = require('./injectSubsetDefinitions');
10
+ const { makePhaseTracker } = require('./progress');
10
11
  const cssFontParser = require('css-font-parser');
11
12
  const cssListHelpers = require('css-list-helpers');
12
13
  const unquote = require('./unquote');
@@ -66,6 +67,16 @@ function escapeJsStringLiteral(str) {
66
67
  .replace(/</g, '\\x3c');
67
68
  }
68
69
 
70
+ function countUniqueFontUrls(htmlOrSvgAssetTextsWithProps) {
71
+ const urls = new Set();
72
+ for (const item of htmlOrSvgAssetTextsWithProps) {
73
+ for (const fu of item.fontUsages) {
74
+ if (fu.fontUrl) urls.add(fu.fontUrl);
75
+ }
76
+ }
77
+ return urls.size;
78
+ }
79
+
69
80
  function asyncLoadStyleRelationWithFallback(
70
81
  htmlOrSvgAsset,
71
82
  originalRelation,
@@ -396,18 +407,14 @@ async function subsetFonts(
396
407
  const subsetUrl = urltools.ensureTrailingSlash(assetGraph.root + subsetPath);
397
408
 
398
409
  const timings = {};
410
+ const trackPhase = makePhaseTracker(console, debug);
399
411
 
400
- let phaseStart = Date.now();
412
+ const applySourceMapsPhase = trackPhase('applySourceMaps');
401
413
  if (sourceMaps) {
402
414
  await assetGraph.applySourceMaps({ type: 'Css' });
403
415
  }
404
- timings.applySourceMaps = Date.now() - phaseStart;
405
- if (debug && console)
406
- console.log(
407
- `[subfont timing] applySourceMaps: ${timings.applySourceMaps}ms`
408
- );
416
+ timings.applySourceMaps = applySourceMapsPhase.end();
409
417
 
410
- phaseStart = Date.now();
411
418
  // Only run Google Fonts populate if there are actually Google Fonts
412
419
  // references in the graph. This avoids ~30s of wasted work on sites
413
420
  // that use only self-hosted fonts.
@@ -416,6 +423,7 @@ async function subsetFonts(
416
423
  to: { url: { $regex: googleFontsCssUrlRegex } },
417
424
  }).length > 0;
418
425
 
426
+ const googlePopulatePhase = trackPhase('populate (google fonts)');
419
427
  if (hasGoogleFonts) {
420
428
  await assetGraph.populate({
421
429
  followRelations: {
@@ -435,11 +443,9 @@ async function subsetFonts(
435
443
  },
436
444
  });
437
445
  }
438
- timings['populate (google fonts)'] = Date.now() - phaseStart;
439
- if (debug && console)
440
- console.log(
441
- `[subfont timing] populate (google fonts): ${timings['populate (google fonts)']}ms${hasGoogleFonts ? '' : ' (skipped, no Google Fonts found)'}`
442
- );
446
+ timings['populate (google fonts)'] = googlePopulatePhase.end(
447
+ hasGoogleFonts ? null : 'skipped, no Google Fonts found'
448
+ );
443
449
 
444
450
  const htmlOrSvgAssets = assetGraph.findAssets({
445
451
  $or: [
@@ -453,11 +459,9 @@ async function subsetFonts(
453
459
  ],
454
460
  });
455
461
 
456
- if (debug && console)
457
- console.log(
458
- `[subfont timing] Starting collectTextsByPage for ${htmlOrSvgAssets.length} pages`
459
- );
460
- const collectStart = Date.now();
462
+ const collectPhase = trackPhase(
463
+ `collectTextsByPage (${htmlOrSvgAssets.length} pages)`
464
+ );
461
465
  const {
462
466
  htmlOrSvgAssetTextsWithProps,
463
467
  fontFaceDeclarationsByHtmlOrSvgAsset,
@@ -470,14 +474,10 @@ async function subsetFonts(
470
474
  concurrency,
471
475
  chromeArgs,
472
476
  });
473
- timings.collectTextsByPage = Date.now() - collectStart;
477
+ timings.collectTextsByPage = collectPhase.end();
474
478
  timings.collectTextsByPageDetails = subTimings;
475
- if (debug && console)
476
- console.log(
477
- `[subfont timing] collectTextsByPage finished in ${timings.collectTextsByPage}ms`
478
- );
479
479
 
480
- phaseStart = Date.now();
480
+ const omitFallbacksPhase = trackPhase('omitFallbacks processing');
481
481
 
482
482
  const potentiallyOrphanedAssets = new Set();
483
483
  if (omitFallbacks) {
@@ -498,12 +498,9 @@ async function subsetFonts(
498
498
  }
499
499
  }
500
500
 
501
- timings['omitFallbacks processing'] = Date.now() - phaseStart;
502
- if (debug && console)
503
- console.log(
504
- `[subfont timing] omitFallbacks processing: ${timings['omitFallbacks processing']}ms`
505
- );
506
- phaseStart = Date.now();
501
+ timings['omitFallbacks processing'] = omitFallbacksPhase.end();
502
+
503
+ const codepointPhase = trackPhase('codepoint generation');
507
504
 
508
505
  if (fontDisplay) {
509
506
  for (const htmlOrSvgAssetTextWithProps of htmlOrSvgAssetTextsWithProps) {
@@ -602,12 +599,7 @@ async function subsetFonts(
602
599
  }
603
600
  }
604
601
 
605
- timings['codepoint generation'] = Date.now() - phaseStart;
606
- if (debug && console)
607
- console.log(
608
- `[subfont timing] codepoint generation: ${timings['codepoint generation']}ms`
609
- );
610
- phaseStart = Date.now();
602
+ timings['codepoint generation'] = codepointPhase.end();
611
603
 
612
604
  if (onlyInfo) {
613
605
  return {
@@ -625,43 +617,38 @@ async function subsetFonts(
625
617
  };
626
618
  }
627
619
 
620
+ const variationPhase = trackPhase('variation axis usage');
628
621
  const { seenAxisValuesByFontUrlAndAxisName } = getVariationAxisUsage(
629
622
  htmlOrSvgAssetTextsWithProps,
630
623
  parseFontWeightRange,
631
624
  parseFontStretchRange
632
625
  );
633
-
634
- timings['variation axis usage'] = Date.now() - phaseStart;
635
- if (debug && console)
636
- console.log(
637
- `[subfont timing] variation axis usage: ${timings['variation axis usage']}ms`
638
- );
639
- phaseStart = Date.now();
626
+ timings['variation axis usage'] = variationPhase.end();
640
627
 
641
628
  // Generate subsets:
629
+ if (console) {
630
+ const uniqueFontUrls = countUniqueFontUrls(htmlOrSvgAssetTextsWithProps);
631
+ if (uniqueFontUrls > 0) {
632
+ console.log(
633
+ ` Subsetting ${uniqueFontUrls} unique font file${uniqueFontUrls === 1 ? '' : 's'}...`
634
+ );
635
+ }
636
+ }
637
+ const subsetPhase = trackPhase('getSubsetsForFontUsage');
642
638
  await getSubsetsForFontUsage(
643
639
  assetGraph,
644
640
  htmlOrSvgAssetTextsWithProps,
645
641
  formats,
646
642
  seenAxisValuesByFontUrlAndAxisName,
647
643
  cacheDir,
648
- console
644
+ console,
645
+ debug
649
646
  );
647
+ timings.getSubsetsForFontUsage = subsetPhase.end();
650
648
 
651
- timings.getSubsetsForFontUsage = Date.now() - phaseStart;
652
- if (debug && console)
653
- console.log(
654
- `[subfont timing] getSubsetsForFontUsage: ${timings.getSubsetsForFontUsage}ms`
655
- );
656
- phaseStart = Date.now();
657
-
649
+ const warnGlyphsPhase = trackPhase('warnAboutMissingGlyphs');
658
650
  await warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph);
659
- timings.warnAboutMissingGlyphs = Date.now() - phaseStart;
660
- if (debug && console)
661
- console.log(
662
- `[subfont timing] warnAboutMissingGlyphs: ${timings.warnAboutMissingGlyphs}ms`
663
- );
664
- phaseStart = Date.now();
651
+ timings.warnAboutMissingGlyphs = warnGlyphsPhase.end();
665
652
 
666
653
  // Insert subsets:
667
654
 
@@ -711,6 +698,9 @@ async function subsetFonts(
711
698
  index.get(from).push(relation);
712
699
  }
713
700
 
701
+ const insertPhase = trackPhase(
702
+ `insert subsets loop (${htmlOrSvgAssetTextsWithProps.length} pages)`
703
+ );
714
704
  let numFontUsagesWithSubset = 0;
715
705
  for (const {
716
706
  htmlOrSvgAsset,
@@ -845,17 +835,13 @@ async function subsetFonts(
845
835
  }
846
836
  }
847
837
 
848
- timings['insert subsets loop'] = Date.now() - phaseStart;
849
- if (debug && console)
850
- console.log(
851
- `[subfont timing] insert subsets loop: ${timings['insert subsets loop']}ms`
852
- );
853
- phaseStart = Date.now();
838
+ timings['insert subsets loop'] = insertPhase.end();
854
839
 
855
840
  if (numFontUsagesWithSubset === 0) {
856
841
  return { fontInfo: [], timings };
857
842
  }
858
843
 
844
+ const lazyFallbackPhase = trackPhase('lazy load fallback CSS');
859
845
  const relationsToRemove = new Set();
860
846
 
861
847
  // Lazy load the original @font-face declarations of self-hosted fonts (unless omitFallbacks)
@@ -956,12 +942,9 @@ async function subsetFonts(
956
942
  relationsToRemove.add(fallbackHtmlStyle);
957
943
  }
958
944
 
959
- timings['lazy load fallback CSS'] = Date.now() - phaseStart;
960
- if (debug && console)
961
- console.log(
962
- `[subfont timing] lazy load fallback CSS: ${timings['lazy load fallback CSS']}ms`
963
- );
964
- phaseStart = Date.now();
945
+ timings['lazy load fallback CSS'] = lazyFallbackPhase.end();
946
+
947
+ const removeFontFacePhase = trackPhase('remove original @font-face');
965
948
 
966
949
  // Remove the original @font-face blocks, and don't leave behind empty stylesheets:
967
950
  const maybeEmptyCssAssets = new Set();
@@ -984,12 +967,9 @@ async function subsetFonts(
984
967
  }
985
968
  }
986
969
 
987
- timings['remove original @font-face'] = Date.now() - phaseStart;
988
- if (debug && console)
989
- console.log(
990
- `[subfont timing] remove original @font-face: ${timings['remove original @font-face']}ms`
991
- );
992
- phaseStart = Date.now();
970
+ timings['remove original @font-face'] = removeFontFacePhase.end();
971
+
972
+ const googleCleanupPhase = trackPhase('Google Fonts + cleanup');
993
973
 
994
974
  // Async load Google Web Fonts CSS
995
975
  const googleFontStylesheets = assetGraph.findAssets({
@@ -1067,12 +1047,9 @@ async function subsetFonts(
1067
1047
  relation.detach();
1068
1048
  }
1069
1049
 
1070
- timings['Google Fonts + cleanup'] = Date.now() - phaseStart;
1071
- if (debug && console)
1072
- console.log(
1073
- `[subfont timing] Google Fonts + cleanup: ${timings['Google Fonts + cleanup']}ms`
1074
- );
1075
- phaseStart = Date.now();
1050
+ timings['Google Fonts + cleanup'] = googleCleanupPhase.end();
1051
+
1052
+ const injectPhase = trackPhase('inject subset font-family into CSS/SVG');
1076
1053
 
1077
1054
  // Use subsets in font-family:
1078
1055
 
@@ -1220,13 +1197,9 @@ async function subsetFonts(
1220
1197
  }
1221
1198
  }
1222
1199
 
1223
- timings['inject subset font-family'] = Date.now() - phaseStart;
1224
- if (debug && console)
1225
- console.log(
1226
- `[subfont timing] inject subset font-family into CSS/SVG: ${timings['inject subset font-family']}ms`
1227
- );
1228
- phaseStart = Date.now();
1200
+ timings['inject subset font-family'] = injectPhase.end();
1229
1201
 
1202
+ const orphanCleanupPhase = trackPhase('source maps + orphan cleanup');
1230
1203
  if (sourceMaps) {
1231
1204
  await assetGraph.serializeSourceMaps(undefined, {
1232
1205
  type: 'Css',
@@ -1254,11 +1227,7 @@ async function subsetFonts(
1254
1227
  }
1255
1228
  }
1256
1229
 
1257
- timings['source maps + orphan cleanup'] = Date.now() - phaseStart;
1258
- if (debug && console)
1259
- console.log(
1260
- `[subfont timing] source maps + orphan cleanup: ${timings['source maps + orphan cleanup']}ms`
1261
- );
1230
+ timings['source maps + orphan cleanup'] = orphanCleanupPhase.end();
1262
1231
 
1263
1232
  // Hand out some useful info about the detected subsets:
1264
1233
  return {
@@ -118,9 +118,11 @@ async function getSubsetsForFontUsage(
118
118
  formats,
119
119
  seenAxisValuesByFontUrlAndAxisName,
120
120
  cacheDir = null,
121
- console = null
121
+ console = null,
122
+ debug = false
122
123
  ) {
123
124
  const diskCache = cacheDir ? new SubsetDiskCache(cacheDir, console) : null;
125
+ const cacheStats = diskCache ? { hits: 0, misses: 0 } : null;
124
126
 
125
127
  // Collect one canonical fontUsage per font URL
126
128
  const canonicalFontUsageByUrl = new Map();
@@ -240,8 +242,10 @@ async function getSubsetsForFontUsage(
240
242
  const cachedResult = diskCache ? await diskCache.get(cacheKey) : null;
241
243
 
242
244
  if (cachedResult) {
245
+ if (cacheStats) cacheStats.hits++;
243
246
  subsetPromiseMap.set(promiseId, Promise.resolve(cachedResult));
244
247
  } else {
248
+ if (cacheStats) cacheStats.misses++;
245
249
  const subsetCall = subsetFontWithGlyphs(fontBuffer, text, {
246
250
  targetFormat,
247
251
  glyphIds: featureGlyphIds,
@@ -263,6 +267,7 @@ async function getSubsetsForFontUsage(
263
267
  .catch((err) => {
264
268
  err.asset = err.asset || fontAssetsByUrl.get(fontUrl);
265
269
  assetGraph.warn(err);
270
+ return null;
266
271
  })
267
272
  );
268
273
  }
@@ -278,6 +283,14 @@ async function getSubsetsForFontUsage(
278
283
  )
279
284
  );
280
285
 
286
+ if (cacheStats && debug && console) {
287
+ const total = cacheStats.hits + cacheStats.misses;
288
+ const pct = total > 0 ? Math.round((cacheStats.hits * 100) / total) : 0;
289
+ console.log(
290
+ `[subfont timing] subset disk cache: ${cacheStats.hits} hit${cacheStats.hits === 1 ? '' : 's'}, ${cacheStats.misses} miss${cacheStats.misses === 1 ? '' : 'es'} (${pct}% hit rate)`
291
+ );
292
+ }
293
+
281
294
  // Assign subset results to canonical font usages
282
295
  for (const [, fontUsage] of canonicalFontUsageByUrl) {
283
296
  const info = subsetInfoByFontUrl.get(fontUsage.fontUrl);
@@ -8,26 +8,11 @@ const standardVariationAxes = new Set(['wght', 'wdth', 'ital', 'slnt', 'opsz']);
8
8
  // CSS maps oblique to slnt -14.
9
9
  const DEFAULT_OBLIQUE_SLNT = -14;
10
10
 
11
- // When no opsz values are determined from font-size or font-variation-settings,
12
- // the axis is pinned to its default value rather than preserving the full range,
13
- // which can significantly bloat variable font subsets.
11
+ // When no opsz values are determined from font-variation-settings, the axis is
12
+ // pinned to its default value rather than preserving the full range, which can
13
+ // significantly bloat variable font subsets.
14
14
  const ignoredVariationAxes = new Set();
15
15
 
16
- // Parse a CSS font-size value to a numeric px value.
17
- // Returns the number if the value is in absolute units (px, pt), NaN otherwise.
18
- // Relative units (em, rem, %, vw, etc.) cannot be resolved without DOM context.
19
- const PX_PER_PT = 4 / 3;
20
- function parseFontSizePx(value) {
21
- if (typeof value === 'number') return value;
22
- if (typeof value !== 'string') return NaN;
23
- const match = value.match(/^([\d.]+)(px|pt)?$/i);
24
- if (!match) return NaN;
25
- const num = parseFloat(match[1]);
26
- if (Number.isNaN(num) || num <= 0) return NaN;
27
- const unit = (match[2] || 'px').toLowerCase();
28
- return unit === 'pt' ? num * PX_PER_PT : num;
29
- }
30
-
31
16
  function clamp(value, min, max) {
32
17
  return Math.min(Math.max(value, min), max);
33
18
  }
@@ -71,7 +56,6 @@ function getVariationAxisUsage(
71
56
  fontWeights,
72
57
  fontStretches,
73
58
  fontVariationSettings,
74
- fontSizes,
75
59
  props,
76
60
  } of fontUsages) {
77
61
  if (seenFontUrls.has(fontUrl)) continue;
@@ -113,18 +97,6 @@ function getVariationAxisUsage(
113
97
  );
114
98
  }
115
99
 
116
- // Map font-size to the opsz axis. With font-optical-sizing: auto
117
- // (the CSS default), browsers set opsz = font-size in px.
118
- // Only absolute units (px, pt) can be resolved without DOM context.
119
- if (fontSizes) {
120
- for (const fontSize of fontSizes) {
121
- const px = parseFontSizePx(fontSize);
122
- if (!Number.isNaN(px)) {
123
- noteUsedValue(fontUrl, 'opsz', px);
124
- }
125
- }
126
- }
127
-
128
100
  for (const fontVariationSettingsValue of fontVariationSettings) {
129
101
  for (const [axisName, axisValue] of parseFontVariationSettings(
130
102
  fontVariationSettingsValue
@@ -195,7 +167,6 @@ async function getVariationAxisBounds(
195
167
  module.exports = {
196
168
  standardVariationAxes,
197
169
  ignoredVariationAxes,
198
- parseFontSizePx,
199
170
  renderNumberRange,
200
171
  getVariationAxisUsage,
201
172
  getVariationAxisBounds,
package/lib/wasmQueue.js CHANGED
@@ -5,7 +5,10 @@
5
5
  let queue = Promise.resolve();
6
6
 
7
7
  function enqueue(fn) {
8
- return (queue = queue.then(fn, fn));
8
+ return (queue = queue.then(
9
+ () => fn(),
10
+ () => fn()
11
+ ));
9
12
  }
10
13
 
11
14
  module.exports = enqueue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turntrout/subfont",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Automatically subset web fonts to only the characters used on your pages. Fork of Munter/subfont with modern defaults.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -63,6 +63,7 @@
63
63
  "postcss-value-parser": "^4.0.2",
64
64
  "pretty-bytes": "^5.1.0",
65
65
  "puppeteer-core": "^24.39.1",
66
+ "sanitize-filename": "^1.6.4",
66
67
  "specificity": "^0.4.1",
67
68
  "urltools": "^0.4.1",
68
69
  "yargs": "^17.7.2"