@turntrout/subfont 1.7.0 → 1.7.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.
@@ -1,95 +1,101 @@
1
- const urltools = require('urltools');
2
-
3
- const fontverter = require('fontverter');
4
-
5
- const compileQuery = require('assetgraph/lib/compileQuery');
6
-
7
- const findCustomPropertyDefinitions = require('./findCustomPropertyDefinitions');
8
- const extractReferencedCustomPropertyNames = require('./extractReferencedCustomPropertyNames');
9
- const injectSubsetDefinitions = require('./injectSubsetDefinitions');
10
- const { makePhaseTracker } = require('./progress');
11
- const cssFontParser = require('css-font-parser');
12
- const cssListHelpers = require('css-list-helpers');
13
- const unquote = require('./unquote');
14
- const normalizeFontPropertyValue = require('./normalizeFontPropertyValue');
15
- const unicodeRange = require('./unicodeRange');
16
- const getFontInfo = require('./getFontInfo');
17
- const collectTextsByPage = require('./collectTextsByPage');
18
-
19
- const escapeJsStringLiteral = require('./escapeJsStringLiteral');
20
- const {
21
- maybeCssQuote,
22
- getFontFaceDeclarationText,
23
- getUnusedVariantsStylesheet,
24
- getFontUsageStylesheet,
25
- getCodepoints,
26
- cssAssetIsEmpty,
27
- parseFontWeightRange,
28
- parseFontStretchRange,
29
- hashHexPrefix,
30
- } = require('./fontFaceHelpers');
31
- const { getVariationAxisUsage } = require('./variationAxes');
32
- const { getSubsetsForFontUsage } = require('./subsetGeneration');
33
- const subsetFontWithGlyphs = require('./subsetFontWithGlyphs');
34
-
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ const urltools = __importStar(require("urltools"));
36
+ const fontverter = __importStar(require("fontverter"));
37
+ const compileQuery = require("assetgraph/lib/compileQuery");
38
+ const findCustomPropertyDefinitions = require("./findCustomPropertyDefinitions");
39
+ const extractReferencedCustomPropertyNames = require("./extractReferencedCustomPropertyNames");
40
+ const injectSubsetDefinitions = require("./injectSubsetDefinitions");
41
+ const progress_1 = require("./progress");
42
+ const cssFontParser = __importStar(require("css-font-parser"));
43
+ const cssListHelpers = __importStar(require("css-list-helpers"));
44
+ const unquote = require("./unquote");
45
+ const normalizeFontPropertyValue = require("./normalizeFontPropertyValue");
46
+ const unicodeRange = require("./unicodeRange");
47
+ const getFontInfo = require("./getFontInfo");
48
+ const collectTextsByPage = require("./collectTextsByPage");
49
+ const escapeJsStringLiteral = require("./escapeJsStringLiteral");
50
+ const fontFaceHelpers_1 = require("./fontFaceHelpers");
51
+ const variationAxes_1 = require("./variationAxes");
52
+ const subsetGeneration_1 = require("./subsetGeneration");
53
+ const subsetFontWithGlyphs = require("./subsetFontWithGlyphs");
54
+ const warnAboutMissingGlyphs = require("./warnAboutMissingGlyphs");
35
55
  const googleFontsCssUrlRegex = /^(?:https?:)?\/\/fonts\.googleapis\.com\/css/;
36
-
37
56
  function getParents(asset, assetQuery) {
38
- const assetMatcher = compileQuery(assetQuery);
39
- const seenAssets = new Set();
40
- const parents = [];
41
- (function visit(asset) {
42
- if (seenAssets.has(asset)) {
43
- return;
44
- }
45
- seenAssets.add(asset);
46
-
47
- for (const incomingRelation of asset.incomingRelations) {
48
- if (assetMatcher(incomingRelation.from)) {
49
- parents.push(incomingRelation.from);
50
- } else {
51
- visit(incomingRelation.from);
52
- }
53
- }
54
- })(asset);
55
-
56
- return parents;
57
+ const assetMatcher = compileQuery(assetQuery);
58
+ const seenAssets = new Set();
59
+ const parents = [];
60
+ (function visit(asset) {
61
+ if (seenAssets.has(asset)) {
62
+ return;
63
+ }
64
+ seenAssets.add(asset);
65
+ for (const incomingRelation of asset.incomingRelations) {
66
+ if (assetMatcher(incomingRelation.from)) {
67
+ parents.push(incomingRelation.from);
68
+ }
69
+ else {
70
+ visit(incomingRelation.from);
71
+ }
72
+ }
73
+ })(asset);
74
+ return parents;
57
75
  }
58
-
59
76
  function countUniqueFontUrls(htmlOrSvgAssetTextsWithProps) {
60
- const urls = new Set();
61
- for (const item of htmlOrSvgAssetTextsWithProps) {
62
- for (const fu of item.fontUsages) {
63
- if (fu.fontUrl) urls.add(fu.fontUrl);
77
+ const urls = new Set();
78
+ for (const item of htmlOrSvgAssetTextsWithProps) {
79
+ for (const fu of item.fontUsages) {
80
+ if (fu.fontUrl)
81
+ urls.add(fu.fontUrl);
82
+ }
64
83
  }
65
- }
66
- return urls.size;
84
+ return urls.size;
67
85
  }
68
-
69
- function asyncLoadStyleRelationWithFallback(
70
- htmlOrSvgAsset,
71
- originalRelation,
72
- hrefType
73
- ) {
74
- // Async load google font stylesheet
75
- // Insert async CSS loading <script>
76
- const href = escapeJsStringLiteral(
77
- htmlOrSvgAsset.assetGraph.buildHref(
78
- originalRelation.to.url,
79
- htmlOrSvgAsset.url,
80
- { hrefType }
81
- )
82
- );
83
- const mediaAssignment = originalRelation.media
84
- ? `el.media = '${escapeJsStringLiteral(originalRelation.media)}';`
85
- : '';
86
- const asyncCssLoadingRelation = htmlOrSvgAsset.addRelation(
87
- {
88
- type: 'HtmlScript',
89
- hrefType: 'inline',
90
- to: {
91
- type: 'JavaScript',
92
- text: `
86
+ function asyncLoadStyleRelationWithFallback(htmlOrSvgAsset, originalRelation, hrefType) {
87
+ // Async load google font stylesheet
88
+ // Insert async CSS loading <script>
89
+ const href = escapeJsStringLiteral(htmlOrSvgAsset.assetGraph.buildHref(originalRelation.to.url, htmlOrSvgAsset.url, { hrefType }));
90
+ const mediaAssignment = originalRelation.media
91
+ ? `el.media = '${escapeJsStringLiteral(originalRelation.media)}';`
92
+ : '';
93
+ const asyncCssLoadingRelation = htmlOrSvgAsset.addRelation({
94
+ type: 'HtmlScript',
95
+ hrefType: 'inline',
96
+ to: {
97
+ type: 'JavaScript',
98
+ text: `
93
99
  (function () {
94
100
  var el = document.createElement('link');
95
101
  el.href = '${href}'.toString('url');
@@ -98,1195 +104,887 @@ function asyncLoadStyleRelationWithFallback(
98
104
  document.body.appendChild(el);
99
105
  }())
100
106
  `,
101
- },
102
- },
103
- 'lastInBody'
104
- );
105
-
106
- // Insert <noscript> fallback sync CSS loading
107
- const noScriptFallbackRelation = htmlOrSvgAsset.addRelation(
108
- {
109
- type: 'HtmlNoscript',
110
- to: {
111
- type: 'Html',
112
- text: '',
113
- },
114
- },
115
- 'lastInBody'
116
- );
117
-
118
- noScriptFallbackRelation.to.addRelation(
119
- {
120
- type: 'HtmlStyle',
121
- media: originalRelation.media,
122
- to: originalRelation.to,
123
- hrefType,
124
- },
125
- 'last'
126
- );
127
-
128
- noScriptFallbackRelation.inline();
129
- asyncCssLoadingRelation.to.minify();
130
- htmlOrSvgAsset.markDirty();
107
+ },
108
+ }, 'lastInBody');
109
+ // Insert <noscript> fallback sync CSS loading
110
+ const noScriptFallbackRelation = htmlOrSvgAsset.addRelation({
111
+ type: 'HtmlNoscript',
112
+ to: {
113
+ type: 'Html',
114
+ text: '',
115
+ },
116
+ }, 'lastInBody');
117
+ noScriptFallbackRelation.to.addRelation({
118
+ type: 'HtmlStyle',
119
+ media: originalRelation.media,
120
+ to: originalRelation.to,
121
+ hrefType,
122
+ }, 'last');
123
+ noScriptFallbackRelation.inline();
124
+ asyncCssLoadingRelation.to.minify();
125
+ htmlOrSvgAsset.markDirty();
131
126
  }
132
-
133
127
  const extensionByFormat = {
134
- truetype: '.ttf',
135
- woff: '.woff',
136
- woff2: '.woff2',
128
+ truetype: '.ttf',
129
+ woff: '.woff',
130
+ woff2: '.woff2',
137
131
  };
138
-
139
- async function createSelfHostedGoogleFontsCssAsset(
140
- assetGraph,
141
- googleFontsCssAsset,
142
- formats,
143
- hrefType,
144
- subsetUrl
145
- ) {
146
- const lines = [];
147
- for (const cssFontFaceSrc of assetGraph.findRelations({
148
- from: googleFontsCssAsset,
149
- type: 'CssFontFaceSrc',
150
- })) {
151
- lines.push(`@font-face {`);
152
- const fontFaceDeclaration = cssFontFaceSrc.node;
153
- fontFaceDeclaration.walkDecls((declaration) => {
154
- const propName = declaration.prop.toLowerCase();
155
- if (propName !== 'src') {
156
- lines.push(` ${propName}: ${declaration.value};`);
157
- }
158
- });
159
- // Convert to all formats in parallel
160
- const convertedFonts = await Promise.all(
161
- formats.map((format) =>
162
- fontverter.convert(cssFontFaceSrc.to.rawSrc, format)
163
- )
164
- );
165
- const srcFragments = [];
166
- for (let fi = 0; fi < formats.length; fi++) {
167
- const format = formats[fi];
168
- const rawSrc = convertedFonts[fi];
169
- const url = assetGraph.resolveUrl(
170
- subsetUrl,
171
- `${cssFontFaceSrc.to.baseName}-${hashHexPrefix(rawSrc)}${
172
- extensionByFormat[format]
173
- }`
174
- );
175
- const fontAsset =
176
- assetGraph.findAssets({ url })[0] ||
177
- (await assetGraph.addAsset({
178
- url,
179
- rawSrc,
180
- }));
181
- srcFragments.push(
182
- `url(${assetGraph.buildHref(fontAsset.url, subsetUrl, {
183
- hrefType,
184
- })}) format('${format}')`
185
- );
132
+ async function createSelfHostedGoogleFontsCssAsset(assetGraph, googleFontsCssAsset, formats, hrefType, subsetUrl) {
133
+ const lines = [];
134
+ for (const cssFontFaceSrc of assetGraph.findRelations({
135
+ from: googleFontsCssAsset,
136
+ type: 'CssFontFaceSrc',
137
+ })) {
138
+ lines.push(`@font-face {`);
139
+ const fontFaceDeclaration = cssFontFaceSrc.node;
140
+ fontFaceDeclaration.walkDecls((declaration) => {
141
+ const propName = declaration.prop.toLowerCase();
142
+ if (propName !== 'src') {
143
+ lines.push(` ${propName}: ${declaration.value};`);
144
+ }
145
+ });
146
+ // Convert to all formats in parallel
147
+ const convertedFonts = await Promise.all(formats.map((format) => fontverter.convert(cssFontFaceSrc.to.rawSrc, format)));
148
+ const srcFragments = [];
149
+ for (let fi = 0; fi < formats.length; fi++) {
150
+ const format = formats[fi];
151
+ const rawSrc = convertedFonts[fi];
152
+ const url = assetGraph.resolveUrl(subsetUrl, `${cssFontFaceSrc.to.baseName}-${(0, fontFaceHelpers_1.hashHexPrefix)(rawSrc)}${extensionByFormat[format]}`);
153
+ const fontAsset = assetGraph.findAssets({ url })[0] ||
154
+ (await assetGraph.addAsset({
155
+ url,
156
+ rawSrc,
157
+ }));
158
+ srcFragments.push(`url(${assetGraph.buildHref(fontAsset.url, subsetUrl, {
159
+ hrefType,
160
+ })}) format('${format}')`);
161
+ }
162
+ lines.push(` src: ${srcFragments.join(', ')};`);
163
+ lines.push(` unicode-range: ${unicodeRange((await getFontInfo(cssFontFaceSrc.to.rawSrc)).characterSet)};`);
164
+ lines.push('}');
186
165
  }
187
- lines.push(` src: ${srcFragments.join(', ')};`);
188
- lines.push(
189
- ` unicode-range: ${unicodeRange(
190
- (await getFontInfo(cssFontFaceSrc.to.rawSrc)).characterSet
191
- )};`
192
- );
193
- lines.push('}');
194
- }
195
- const text = lines.join('\n');
196
- const fallbackAsset = assetGraph.addAsset({
197
- type: 'Css',
198
- url: assetGraph.resolveUrl(
199
- subsetUrl,
200
- `fallback-${hashHexPrefix(text)}.css`
201
- ),
202
- text,
203
- });
204
- return fallbackAsset;
166
+ const text = lines.join('\n');
167
+ const fallbackAsset = assetGraph.addAsset({
168
+ type: 'Css',
169
+ url: assetGraph.resolveUrl(subsetUrl, `fallback-${(0, fontFaceHelpers_1.hashHexPrefix)(text)}.css`),
170
+ text,
171
+ });
172
+ return fallbackAsset;
205
173
  }
206
-
207
174
  const validFontDisplayValues = [
208
- 'auto',
209
- 'block',
210
- 'swap',
211
- 'fallback',
212
- 'optional',
175
+ 'auto',
176
+ 'block',
177
+ 'swap',
178
+ 'fallback',
179
+ 'optional',
213
180
  ];
214
-
215
- const warnAboutMissingGlyphs = require('./warnAboutMissingGlyphs');
216
-
217
181
  // Create (or retrieve from disk cache) the subset CSS asset for a set of
218
182
  // fontUsages, relocating the font binary to its hashed URL under subsetUrl.
219
- async function getOrCreateSubsetCssAsset({
220
- assetGraph,
221
- subsetCssText,
222
- subsetFontUsages,
223
- formats,
224
- subsetUrl,
225
- hrefType,
226
- inlineCss,
227
- fontUrlsUsedOnEveryPage,
228
- numPages,
229
- subsetCssAssetCache,
230
- }) {
231
- let cssAsset = subsetCssAssetCache.get(subsetCssText);
232
- if (cssAsset) return cssAsset;
233
-
234
- cssAsset = assetGraph.addAsset({
235
- type: 'Css',
236
- url: `${subsetUrl}subfontTemp.css`,
237
- text: subsetCssText,
238
- });
239
-
240
- await cssAsset.minify();
241
-
242
- for (const [i, fontRelation] of cssAsset.outgoingRelations.entries()) {
243
- const fontAsset = fontRelation.to;
244
- if (!fontAsset.isLoaded) {
245
- // An unused variant that does not exist, don't try to hash
246
- fontRelation.hrefType = hrefType;
247
- continue;
183
+ async function getOrCreateSubsetCssAsset({ assetGraph, subsetCssText, subsetFontUsages, formats, subsetUrl, hrefType, inlineCss, fontUrlsUsedOnEveryPage, numPages, subsetCssAssetCache, }) {
184
+ let cssAsset = subsetCssAssetCache.get(subsetCssText);
185
+ if (cssAsset)
186
+ return cssAsset;
187
+ cssAsset = assetGraph.addAsset({
188
+ type: 'Css',
189
+ url: `${subsetUrl}subfontTemp.css`,
190
+ text: subsetCssText,
191
+ });
192
+ await cssAsset.minify();
193
+ for (const [i, fontRelation] of cssAsset.outgoingRelations.entries()) {
194
+ const fontAsset = fontRelation.to;
195
+ if (!fontAsset.isLoaded) {
196
+ // An unused variant that does not exist, don't try to hash
197
+ fontRelation.hrefType = hrefType;
198
+ continue;
199
+ }
200
+ const fontUsage = subsetFontUsages[i];
201
+ if (formats.length === 1 &&
202
+ fontUsage &&
203
+ fontUsage.fontUrl &&
204
+ (!inlineCss || numPages === 1) &&
205
+ fontUrlsUsedOnEveryPage.has(fontUsage.fontUrl)) {
206
+ // We're only outputting one font format, we're not inlining the subfont CSS (or there's only one page), and this font is used on every page -- keep it inline in the subfont CSS
207
+ continue;
208
+ }
209
+ const extension = (fontAsset.contentType ?? '').split('/').pop();
210
+ const nameProps = ['font-family', 'font-weight', 'font-style']
211
+ .map((prop) => fontRelation.node.nodes?.find((decl) => decl.prop === prop))
212
+ .map((decl) => decl?.value);
213
+ const fontWeightRangeStr = nameProps[1]
214
+ .split(/\s+/)
215
+ .map((token) => normalizeFontPropertyValue('font-weight', token))
216
+ .join('_');
217
+ const fileNamePrefix = `${unquote(cssFontParser.parseFontFamily(nameProps[0])[0])
218
+ .replace(/__subset$/, '')
219
+ .replace(/[^a-z0-9_-]/gi, '_')}-${fontWeightRangeStr}${nameProps[2] === 'italic' ? 'i' : ''}`;
220
+ const fontFileName = `${fileNamePrefix}-${fontAsset.md5Hex.slice(0, 10)}.${extension}`;
221
+ // If it's not inline, it's one of the unused variants that gets a mirrored declaration added
222
+ // for the __subset @font-face. Do not move it to /subfont/
223
+ if (fontAsset.isInline) {
224
+ const fontAssetUrl = subsetUrl + fontFileName;
225
+ const existingFontAsset = assetGraph.findAssets({
226
+ url: fontAssetUrl,
227
+ })[0];
228
+ if (existingFontAsset) {
229
+ fontRelation.to = existingFontAsset;
230
+ assetGraph.removeAsset(fontAsset);
231
+ }
232
+ else {
233
+ fontAsset.url = subsetUrl + fontFileName;
234
+ }
235
+ }
236
+ fontRelation.hrefType = hrefType;
248
237
  }
249
-
250
- const fontUsage = subsetFontUsages[i];
251
- if (
252
- formats.length === 1 &&
253
- fontUsage &&
254
- (!inlineCss || numPages === 1) &&
255
- fontUrlsUsedOnEveryPage.has(fontUsage.fontUrl)
256
- ) {
257
- // We're only outputting one font format, we're not inlining the subfont CSS (or there's only one page), and this font is used on every page -- keep it inline in the subfont CSS
258
- continue;
238
+ const cssAssetUrl = `${subsetUrl}fonts-${cssAsset.md5Hex.slice(0, 10)}.css`;
239
+ const existingCssAsset = assetGraph.findAssets({ url: cssAssetUrl })[0];
240
+ if (existingCssAsset) {
241
+ assetGraph.removeAsset(cssAsset);
242
+ cssAsset = existingCssAsset;
259
243
  }
260
-
261
- const extension = fontAsset.contentType.split('/').pop();
262
-
263
- const nameProps = ['font-family', 'font-weight', 'font-style']
264
- .map((prop) => fontRelation.node.nodes.find((decl) => decl.prop === prop))
265
- .map((decl) => decl.value);
266
-
267
- const fontWeightRangeStr = nameProps[1]
268
- .split(/\s+/)
269
- .map((token) => normalizeFontPropertyValue('font-weight', token))
270
- .join('_');
271
- const fileNamePrefix = `${unquote(
272
- cssFontParser.parseFontFamily(nameProps[0])[0]
273
- )
274
- .replace(/__subset$/, '')
275
- .replace(/[^a-z0-9_-]/gi, '_')}-${fontWeightRangeStr}${
276
- nameProps[2] === 'italic' ? 'i' : ''
277
- }`;
278
-
279
- const fontFileName = `${fileNamePrefix}-${fontAsset.md5Hex.slice(
280
- 0,
281
- 10
282
- )}.${extension}`;
283
-
284
- // If it's not inline, it's one of the unused variants that gets a mirrored declaration added
285
- // for the __subset @font-face. Do not move it to /subfont/
286
- if (fontAsset.isInline) {
287
- const fontAssetUrl = subsetUrl + fontFileName;
288
- const existingFontAsset = assetGraph.findAssets({
289
- url: fontAssetUrl,
290
- })[0];
291
- if (existingFontAsset) {
292
- fontRelation.to = existingFontAsset;
293
- assetGraph.removeAsset(fontAsset);
294
- } else {
295
- fontAsset.url = subsetUrl + fontFileName;
296
- }
244
+ else {
245
+ cssAsset.url = cssAssetUrl;
297
246
  }
298
-
299
- fontRelation.hrefType = hrefType;
300
- }
301
-
302
- const cssAssetUrl = `${subsetUrl}fonts-${cssAsset.md5Hex.slice(0, 10)}.css`;
303
- const existingCssAsset = assetGraph.findAssets({ url: cssAssetUrl })[0];
304
- if (existingCssAsset) {
305
- assetGraph.removeAsset(cssAsset);
306
- cssAsset = existingCssAsset;
307
- } else {
308
- cssAsset.url = cssAssetUrl;
309
- }
310
- subsetCssAssetCache.set(subsetCssText, cssAsset);
311
- return cssAsset;
247
+ subsetCssAssetCache.set(subsetCssText, cssAsset);
248
+ return cssAsset;
312
249
  }
313
-
314
250
  // Insert <link rel="preload"> hints for every woff2 subset flagged as
315
251
  // preload-worthy, so the browser starts fetching them during HTML parse.
316
- function addSubsetFontPreloads({
317
- cssAsset,
318
- fontUsages,
319
- htmlOrSvgAsset,
320
- subsetUrl,
321
- hrefType,
322
- insertionPoint,
323
- }) {
324
- if (htmlOrSvgAsset.type !== 'Html') return insertionPoint;
325
-
326
- // Only <link rel="preload"> for woff2 subset files whose original
327
- // font-family is marked for preloading.
328
- for (const fontRelation of cssAsset.outgoingRelations) {
329
- if (fontRelation.hrefType === 'inline') continue;
330
-
331
- const fontAsset = fontRelation.to;
332
- if (
333
- fontAsset.contentType !== 'font/woff2' ||
334
- !fontRelation.to.url.startsWith(subsetUrl)
335
- ) {
336
- continue;
337
- }
338
-
339
- const originalFontFamily = unquote(
340
- fontRelation.node.nodes.find((node) => node.prop === 'font-family').value
341
- ).replace(/__subset$/, '');
342
- if (
343
- !fontUsages.some(
344
- (fontUsage) =>
345
- fontUsage.fontFamilies.has(originalFontFamily) && fontUsage.preload
346
- )
347
- ) {
348
- continue;
252
+ function addSubsetFontPreloads({ cssAsset, fontUsages, htmlOrSvgAsset, subsetUrl, hrefType, insertionPoint, }) {
253
+ if (htmlOrSvgAsset.type !== 'Html')
254
+ return insertionPoint;
255
+ // Only <link rel="preload"> for woff2 subset files whose original
256
+ // font-family is marked for preloading.
257
+ for (const fontRelation of cssAsset.outgoingRelations) {
258
+ if (fontRelation.hrefType === 'inline')
259
+ continue;
260
+ const fontAsset = fontRelation.to;
261
+ if (fontAsset.contentType !== 'font/woff2' ||
262
+ !fontRelation.to.url.startsWith(subsetUrl)) {
263
+ continue;
264
+ }
265
+ const familyDecl = fontRelation.node.nodes?.find((node) => node.prop === 'font-family');
266
+ const originalFontFamily = unquote(familyDecl?.value ?? '').replace(/__subset$/, '');
267
+ if (!fontUsages.some((fontUsage) => fontUsage.fontFamilies.has(originalFontFamily) && fontUsage.preload)) {
268
+ continue;
269
+ }
270
+ const htmlPreloadLink = htmlOrSvgAsset.addRelation({
271
+ type: 'HtmlPreloadLink',
272
+ hrefType,
273
+ to: fontAsset,
274
+ as: 'font',
275
+ }, insertionPoint ? 'before' : 'firstInHead', insertionPoint);
276
+ insertionPoint = insertionPoint || htmlPreloadLink;
349
277
  }
350
-
351
- const htmlPreloadLink = htmlOrSvgAsset.addRelation(
352
- {
353
- type: 'HtmlPreloadLink',
354
- hrefType,
355
- to: fontAsset,
356
- as: 'font',
357
- },
358
- insertionPoint ? 'before' : 'firstInHead',
359
- insertionPoint
360
- );
361
- insertionPoint = insertionPoint || htmlPreloadLink;
362
- }
363
- return insertionPoint;
278
+ return insertionPoint;
364
279
  }
365
-
366
280
  // Skip Google Fonts populate when no Google Fonts references exist —
367
281
  // otherwise assetgraph spends ~30s network-walking for nothing on sites
368
282
  // that only self-host. Returns whether the populate ran so callers can
369
283
  // annotate their phase timing.
370
284
  async function populateGoogleFontsIfPresent(assetGraph) {
371
- const hasGoogleFonts =
372
- assetGraph.findRelations({
373
- to: { url: { $regex: googleFontsCssUrlRegex } },
285
+ const hasGoogleFonts = assetGraph.findRelations({
286
+ to: { url: { $regex: googleFontsCssUrlRegex } },
374
287
  }).length > 0;
375
- if (!hasGoogleFonts) return false;
376
-
377
- await assetGraph.populate({
378
- followRelations: {
379
- $or: [
380
- { to: { url: { $regex: googleFontsCssUrlRegex } } },
381
- {
382
- type: 'CssFontFaceSrc',
383
- from: { url: { $regex: googleFontsCssUrlRegex } },
288
+ if (!hasGoogleFonts)
289
+ return false;
290
+ await assetGraph.populate({
291
+ followRelations: {
292
+ $or: [
293
+ { to: { url: { $regex: googleFontsCssUrlRegex } } },
294
+ {
295
+ type: 'CssFontFaceSrc',
296
+ from: { url: { $regex: googleFontsCssUrlRegex } },
297
+ },
298
+ ],
384
299
  },
385
- ],
386
- },
387
- });
388
- return true;
300
+ });
301
+ return true;
389
302
  }
390
-
391
303
  // Strip every original @font-face rule when --no-fallbacks is set. The
392
304
  // severed assets are returned via the `potentiallyOrphanedAssets` set so
393
305
  // the final orphan sweep can remove anything left dangling.
394
- function removeOriginalFontFaceRules(
395
- htmlOrSvgAssets,
396
- fontFaceDeclarationsByHtmlOrSvgAsset,
397
- potentiallyOrphanedAssets
398
- ) {
399
- for (const htmlOrSvgAsset of htmlOrSvgAssets) {
400
- const accumulatedFontFaceDeclarations =
401
- fontFaceDeclarationsByHtmlOrSvgAsset.get(htmlOrSvgAsset);
402
- for (const { relations } of accumulatedFontFaceDeclarations) {
403
- for (const relation of relations) {
404
- potentiallyOrphanedAssets.add(relation.to);
405
- if (relation.node.parentNode) {
406
- relation.node.parentNode.removeChild(relation.node);
306
+ function removeOriginalFontFaceRules(htmlOrSvgAssets, fontFaceDeclarationsByHtmlOrSvgAsset, potentiallyOrphanedAssets) {
307
+ for (const htmlOrSvgAsset of htmlOrSvgAssets) {
308
+ const accumulatedFontFaceDeclarations = fontFaceDeclarationsByHtmlOrSvgAsset.get(htmlOrSvgAsset);
309
+ if (!accumulatedFontFaceDeclarations)
310
+ continue;
311
+ for (const { relations } of accumulatedFontFaceDeclarations) {
312
+ for (const relation of relations) {
313
+ potentiallyOrphanedAssets.add(relation.to);
314
+ if (relation.node.parentNode) {
315
+ relation.node.parentNode.removeChild(relation.node);
316
+ }
317
+ relation.remove();
318
+ }
407
319
  }
408
- relation.remove();
409
- }
320
+ htmlOrSvgAsset.markDirty();
410
321
  }
411
- htmlOrSvgAsset.markDirty();
412
- }
413
322
  }
414
-
415
323
  // Rewrite CSS source-map relations to the caller's chosen hrefType so they
416
324
  // align with the rest of the emitted assets. Only invoked when sourceMaps is
417
325
  // enabled — subsetFonts normally skips source-map serialization for speed.
418
326
  async function rewriteCssSourceMaps(assetGraph, hrefType) {
419
- await assetGraph.serializeSourceMaps(undefined, {
420
- type: 'Css',
421
- outgoingRelations: {
422
- $where: (relations) =>
423
- relations.some((relation) => relation.type === 'CssSourceMappingUrl'),
424
- },
425
- });
426
- for (const relation of assetGraph.findRelations({
427
- type: 'SourceMapSource',
428
- })) {
429
- relation.hrefType = hrefType;
430
- }
431
- for (const relation of assetGraph.findRelations({
432
- type: 'CssSourceMappingUrl',
433
- hrefType: { $in: ['relative', 'inline'] },
434
- })) {
435
- relation.hrefType = hrefType;
436
- }
327
+ await assetGraph.serializeSourceMaps(undefined, {
328
+ type: 'Css',
329
+ outgoingRelations: {
330
+ $where: (relations) => relations.some((relation) => relation.type === 'CssSourceMappingUrl'),
331
+ },
332
+ });
333
+ for (const relation of assetGraph.findRelations({
334
+ type: 'SourceMapSource',
335
+ })) {
336
+ relation.hrefType = hrefType;
337
+ }
338
+ for (const relation of assetGraph.findRelations({
339
+ type: 'CssSourceMappingUrl',
340
+ hrefType: { $in: ['relative', 'inline'] },
341
+ })) {
342
+ relation.hrefType = hrefType;
343
+ }
437
344
  }
438
-
439
345
  // Remove assets whose last incoming relation was severed during subset
440
346
  // injection (original @font-face rules, merged Google Fonts CSS, etc.) so
441
347
  // the emitted site doesn't ship with dangling files.
442
348
  function removeOrphanedAssets(assetGraph, potentiallyOrphanedAssets) {
443
- for (const asset of potentiallyOrphanedAssets) {
444
- if (asset.incomingRelations.length === 0) {
445
- assetGraph.removeAsset(asset);
349
+ for (const asset of potentiallyOrphanedAssets) {
350
+ if (asset.incomingRelations.length === 0) {
351
+ assetGraph.removeAsset(asset);
352
+ }
446
353
  }
447
- }
448
354
  }
449
-
450
355
  // Shape the per-page fontUsages into the external fontInfo report: strip
451
356
  // internal bookkeeping (subsets buffer, feature-tag scratch) and flatten
452
357
  // each page to { assetFileName, fontUsages }.
453
358
  function buildFontInfoReport(htmlOrSvgAssetTextsWithProps) {
454
- return htmlOrSvgAssetTextsWithProps.map(({ fontUsages, htmlOrSvgAsset }) => ({
455
- assetFileName: htmlOrSvgAsset.nonInlineAncestor.urlOrDescription,
456
- fontUsages: fontUsages.map((fontUsage) =>
457
- (({ subsets, hasFontFeatureSettings, fontFeatureTags, ...rest }) => rest)(
458
- fontUsage
459
- )
460
- ),
461
- }));
359
+ return htmlOrSvgAssetTextsWithProps.map(({ fontUsages, htmlOrSvgAsset }) => ({
360
+ assetFileName: htmlOrSvgAsset.nonInlineAncestor.urlOrDescription,
361
+ fontUsages: fontUsages.map((fontUsage) => (({ subsets, hasFontFeatureSettings, fontFeatureTags, ...rest }) => rest)(fontUsage)),
362
+ }));
462
363
  }
463
-
464
- async function subsetFonts(
465
- assetGraph,
466
- {
467
- formats = ['woff2'],
468
- subsetPath = 'subfont/',
469
- omitFallbacks = false,
470
- inlineCss,
471
- fontDisplay,
472
- hrefType = 'rootRelative',
473
- onlyInfo,
474
- dynamic,
475
- console = global.console,
476
- text,
477
- sourceMaps = false,
478
- debug = false,
479
- concurrency,
480
- chromeArgs = [],
481
- cacheDir = null,
482
- } = {}
483
- ) {
484
- if (!validFontDisplayValues.includes(fontDisplay)) {
485
- fontDisplay = undefined;
486
- }
487
-
488
- // Pre-warm the WASM pool: start compiling harfbuzz WASM while
489
- // collectTextsByPage traces fonts. Compilation (~50-200ms) overlaps
490
- // with tracing work rather than appearing on the critical path.
491
- // Catch silently — the error will surface when subsetFontWithGlyphs
492
- // is actually called, where it's properly handled.
493
- subsetFontWithGlyphs.warmup().catch(() => {});
494
-
495
- const subsetUrl = urltools.ensureTrailingSlash(assetGraph.root + subsetPath);
496
-
497
- const timings = {};
498
- const trackPhase = makePhaseTracker(console, debug);
499
-
500
- const applySourceMapsPhase = trackPhase('applySourceMaps');
501
- if (sourceMaps) {
502
- await assetGraph.applySourceMaps({ type: 'Css' });
503
- }
504
- timings.applySourceMaps = applySourceMapsPhase.end();
505
-
506
- const googlePopulatePhase = trackPhase('populate (google fonts)');
507
- const hasGoogleFonts = await populateGoogleFontsIfPresent(assetGraph);
508
- timings['populate (google fonts)'] = googlePopulatePhase.end(
509
- hasGoogleFonts ? null : 'skipped, no Google Fonts found'
510
- );
511
-
512
- const htmlOrSvgAssets = assetGraph.findAssets({
513
- $or: [
514
- {
515
- type: 'Html',
516
- isInline: false,
517
- },
518
- {
519
- type: 'Svg',
520
- },
521
- ],
522
- });
523
-
524
- const collectPhase = trackPhase(
525
- `collectTextsByPage (${htmlOrSvgAssets.length} pages)`
526
- );
527
- const {
528
- htmlOrSvgAssetTextsWithProps,
529
- fontFaceDeclarationsByHtmlOrSvgAsset,
530
- subTimings,
531
- } = await collectTextsByPage(assetGraph, htmlOrSvgAssets, {
532
- text,
533
- console,
534
- dynamic,
535
- debug,
536
- concurrency,
537
- chromeArgs,
538
- });
539
- timings.collectTextsByPage = collectPhase.end();
540
- timings.collectTextsByPageDetails = subTimings;
541
-
542
- const omitFallbacksPhase = trackPhase('omitFallbacks processing');
543
- const potentiallyOrphanedAssets = new Set();
544
- if (omitFallbacks) {
545
- removeOriginalFontFaceRules(
546
- htmlOrSvgAssets,
547
- fontFaceDeclarationsByHtmlOrSvgAsset,
548
- potentiallyOrphanedAssets
549
- );
550
- }
551
- timings['omitFallbacks processing'] = omitFallbacksPhase.end();
552
-
553
- const codepointPhase = trackPhase('codepoint generation');
554
-
555
- if (fontDisplay) {
556
- for (const htmlOrSvgAssetTextWithProps of htmlOrSvgAssetTextsWithProps) {
557
- for (const fontUsage of htmlOrSvgAssetTextWithProps.fontUsages) {
558
- fontUsage.props['font-display'] = fontDisplay;
559
- }
560
- }
561
- }
562
-
563
- // Pre-compute the global codepoints (original, used, unused) once per fontUrl
564
- // since fontUsage.text is the same global union on every page.
565
- // Pre-index all loaded assets by URL for O(1) lookups instead of O(n) scans.
566
- const loadedAssetsByUrl = new Map();
567
- for (const asset of assetGraph.findAssets({ isLoaded: true })) {
568
- if (asset.url) loadedAssetsByUrl.set(asset.url, asset);
569
- }
570
- const codepointFontAssetByUrl = new Map();
571
- for (const htmlOrSvgAssetTextWithProps of htmlOrSvgAssetTextsWithProps) {
572
- for (const fontUsage of htmlOrSvgAssetTextWithProps.fontUsages) {
573
- if (
574
- fontUsage.fontUrl &&
575
- !codepointFontAssetByUrl.has(fontUsage.fontUrl)
576
- ) {
577
- const originalFont = loadedAssetsByUrl.get(fontUsage.fontUrl);
578
- if (originalFont) {
579
- codepointFontAssetByUrl.set(fontUsage.fontUrl, originalFont);
364
+ async function subsetFonts(assetGraph, { formats = ['woff2'], subsetPath = 'subfont/', omitFallbacks = false, inlineCss = false, fontDisplay, hrefType = 'rootRelative', onlyInfo, dynamic, console = global.console, text, sourceMaps = false, debug = false, concurrency, chromeArgs = [], cacheDir = null, } = {}) {
365
+ if (fontDisplay && !validFontDisplayValues.includes(fontDisplay)) {
366
+ fontDisplay = undefined;
367
+ }
368
+ // Pre-warm the WASM pool: start compiling harfbuzz WASM while
369
+ // collectTextsByPage traces fonts. Compilation (~50-200ms) overlaps
370
+ // with tracing work rather than appearing on the critical path.
371
+ // Catch silently — the error will surface when subsetFontWithGlyphs
372
+ // is actually called, where it's properly handled.
373
+ subsetFontWithGlyphs.warmup().catch(() => { });
374
+ const subsetUrl = urltools.ensureTrailingSlash(assetGraph.root + subsetPath);
375
+ const timings = {};
376
+ const trackPhase = (0, progress_1.makePhaseTracker)(console, debug);
377
+ const applySourceMapsPhase = trackPhase('applySourceMaps');
378
+ if (sourceMaps) {
379
+ await assetGraph.applySourceMaps({ type: 'Css' });
380
+ }
381
+ timings.applySourceMaps = applySourceMapsPhase.end();
382
+ const googlePopulatePhase = trackPhase('populate (google fonts)');
383
+ const hasGoogleFonts = await populateGoogleFontsIfPresent(assetGraph);
384
+ timings['populate (google fonts)'] = googlePopulatePhase.end(hasGoogleFonts ? null : 'skipped, no Google Fonts found');
385
+ const htmlOrSvgAssets = assetGraph.findAssets({
386
+ $or: [
387
+ {
388
+ type: 'Html',
389
+ isInline: false,
390
+ },
391
+ {
392
+ type: 'Svg',
393
+ },
394
+ ],
395
+ });
396
+ const collectPhase = trackPhase(`collectTextsByPage (${htmlOrSvgAssets.length} pages)`);
397
+ const { htmlOrSvgAssetTextsWithProps, fontFaceDeclarationsByHtmlOrSvgAsset, subTimings, } = await collectTextsByPage(assetGraph, htmlOrSvgAssets, {
398
+ text,
399
+ console,
400
+ dynamic,
401
+ debug,
402
+ concurrency,
403
+ chromeArgs,
404
+ });
405
+ timings.collectTextsByPage = collectPhase.end();
406
+ timings.collectTextsByPageDetails = subTimings;
407
+ const omitFallbacksPhase = trackPhase('omitFallbacks processing');
408
+ const potentiallyOrphanedAssets = new Set();
409
+ if (omitFallbacks) {
410
+ removeOriginalFontFaceRules(htmlOrSvgAssets, fontFaceDeclarationsByHtmlOrSvgAsset, potentiallyOrphanedAssets);
411
+ }
412
+ timings['omitFallbacks processing'] = omitFallbacksPhase.end();
413
+ const codepointPhase = trackPhase('codepoint generation');
414
+ if (fontDisplay) {
415
+ for (const htmlOrSvgAssetTextWithProps of htmlOrSvgAssetTextsWithProps) {
416
+ for (const fontUsage of htmlOrSvgAssetTextWithProps.fontUsages) {
417
+ fontUsage.props['font-display'] = fontDisplay;
418
+ }
580
419
  }
581
- }
582
420
  }
583
- }
584
-
585
- // getFontInfo internally serializes harfbuzzjs WASM calls (which are
586
- // not concurrency-safe), so Promise.all here just queues them up
587
- // and avoids awaiting each individually in the loop below.
588
- const fontInfoPromises = new Map();
589
- for (const [fontUrl, fontAsset] of codepointFontAssetByUrl) {
590
- if (fontAsset.isLoaded) {
591
- fontInfoPromises.set(
592
- fontUrl,
593
- getFontInfo(fontAsset.rawSrc).catch((err) => {
594
- err.asset = err.asset || fontAsset;
595
- assetGraph.warn(err);
596
- return null;
597
- })
598
- );
421
+ // Pre-compute the global codepoints (original, used, unused) once per fontUrl
422
+ // since fontUsage.text is the same global union on every page.
423
+ // Pre-index all loaded assets by URL for O(1) lookups instead of O(n) scans.
424
+ const loadedAssetsByUrl = new Map();
425
+ for (const asset of assetGraph.findAssets({ isLoaded: true })) {
426
+ if (asset.url)
427
+ loadedAssetsByUrl.set(asset.url, asset);
599
428
  }
600
- }
601
- const fontInfoResults = new Map(
602
- await Promise.all(
603
- [...fontInfoPromises].map(async ([key, promise]) => [key, await promise])
604
- )
605
- );
606
-
607
- // Build global codepoints synchronously from pre-fetched results
608
- const globalCodepointsByFontUrl = new Map();
609
- const codepointsCache = new Map();
610
- for (const htmlOrSvgAssetTextWithProps of htmlOrSvgAssetTextsWithProps) {
611
- for (const fontUsage of htmlOrSvgAssetTextWithProps.fontUsages) {
612
- let cached = globalCodepointsByFontUrl.get(fontUsage.fontUrl);
613
- if (!cached) {
614
- cached = { originalCodepoints: null };
615
- const fontInfo = fontInfoResults.get(fontUsage.fontUrl);
616
- if (fontInfo) {
617
- cached.originalCodepoints = fontInfo.characterSet;
618
- cached.usedCodepoints = getCodepoints(fontUsage.text);
619
- const usedCodepointsSet = new Set(cached.usedCodepoints);
620
- cached.unusedCodepoints = cached.originalCodepoints.filter(
621
- (n) => !usedCodepointsSet.has(n)
622
- );
623
- }
624
- globalCodepointsByFontUrl.set(fontUsage.fontUrl, cached);
625
- }
626
-
627
- if (cached.originalCodepoints) {
628
- // Cache getCodepoints result by pageText string to avoid
629
- // recomputing for pages with identical text per font
630
- let pageCodepoints = codepointsCache.get(fontUsage.pageText);
631
- if (!pageCodepoints) {
632
- pageCodepoints = getCodepoints(fontUsage.pageText);
633
- codepointsCache.set(fontUsage.pageText, pageCodepoints);
429
+ const codepointFontAssetByUrl = new Map();
430
+ for (const htmlOrSvgAssetTextWithProps of htmlOrSvgAssetTextsWithProps) {
431
+ for (const fontUsage of htmlOrSvgAssetTextWithProps.fontUsages) {
432
+ if (fontUsage.fontUrl &&
433
+ !codepointFontAssetByUrl.has(fontUsage.fontUrl)) {
434
+ const originalFont = loadedAssetsByUrl.get(fontUsage.fontUrl);
435
+ if (originalFont) {
436
+ codepointFontAssetByUrl.set(fontUsage.fontUrl, originalFont);
437
+ }
438
+ }
634
439
  }
635
- fontUsage.codepoints = {
636
- original: cached.originalCodepoints,
637
- used: cached.usedCodepoints,
638
- unused: cached.unusedCodepoints,
639
- page: pageCodepoints,
640
- };
641
- } else {
642
- fontUsage.codepoints = {
643
- original: [],
644
- used: [],
645
- unused: [],
646
- page: [],
647
- };
648
- }
649
- }
650
- }
651
-
652
- timings['codepoint generation'] = codepointPhase.end();
653
-
654
- if (onlyInfo) {
655
- return {
656
- fontInfo: htmlOrSvgAssetTextsWithProps.map(
657
- ({ fontUsages, htmlOrSvgAsset }) => ({
658
- assetFileName: htmlOrSvgAsset.nonInlineAncestor.urlOrDescription,
659
- fontUsages: fontUsages.map((fontUsage) =>
660
- (({ hasFontFeatureSettings, fontFeatureTags, ...rest }) => rest)(
661
- fontUsage
662
- )
663
- ),
664
- })
665
- ),
666
- timings,
667
- };
668
- }
669
-
670
- const variationPhase = trackPhase('variation axis usage');
671
- const { seenAxisValuesByFontUrlAndAxisName } = getVariationAxisUsage(
672
- htmlOrSvgAssetTextsWithProps,
673
- parseFontWeightRange,
674
- parseFontStretchRange
675
- );
676
- timings['variation axis usage'] = variationPhase.end();
677
-
678
- // Generate subsets:
679
- if (console) {
680
- const uniqueFontUrls = countUniqueFontUrls(htmlOrSvgAssetTextsWithProps);
681
- if (uniqueFontUrls > 0) {
682
- console.log(
683
- ` Subsetting ${uniqueFontUrls} unique font file${uniqueFontUrls === 1 ? '' : 's'}...`
684
- );
685
- }
686
- }
687
- const subsetPhase = trackPhase('getSubsetsForFontUsage');
688
- await getSubsetsForFontUsage(
689
- assetGraph,
690
- htmlOrSvgAssetTextsWithProps,
691
- formats,
692
- seenAxisValuesByFontUrlAndAxisName,
693
- cacheDir,
694
- console,
695
- debug
696
- );
697
- timings.getSubsetsForFontUsage = subsetPhase.end();
698
-
699
- const warnGlyphsPhase = trackPhase('warnAboutMissingGlyphs');
700
- await warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph);
701
- timings.warnAboutMissingGlyphs = warnGlyphsPhase.end();
702
-
703
- // Insert subsets:
704
-
705
- // Pre-compute which fontUrls are used (with text) on every page.
706
- // Set intersection: O(pages × fonts_per_page) vs the old every+some approach.
707
- const fontUrlsUsedOnEveryPage = new Set();
708
- if (htmlOrSvgAssetTextsWithProps.length > 0) {
709
- const firstPageFontUrls = new Set();
710
- for (const fu of htmlOrSvgAssetTextsWithProps[0].fontUsages) {
711
- if (fu.pageText) firstPageFontUrls.add(fu.fontUrl);
712
- }
713
- for (const fontUrl of firstPageFontUrls) {
714
- fontUrlsUsedOnEveryPage.add(fontUrl);
715
440
  }
716
- for (let i = 1; i < htmlOrSvgAssetTextsWithProps.length; i++) {
717
- const pageFontUrls = new Set();
718
- for (const fu of htmlOrSvgAssetTextsWithProps[i].fontUsages) {
719
- if (fu.pageText) pageFontUrls.add(fu.fontUrl);
720
- }
721
- for (const fontUrl of fontUrlsUsedOnEveryPage) {
722
- if (!pageFontUrls.has(fontUrl)) {
723
- fontUrlsUsedOnEveryPage.delete(fontUrl);
441
+ const fontInfoPromises = new Map();
442
+ for (const [fontUrl, fontAsset] of codepointFontAssetByUrl) {
443
+ if (fontAsset.isLoaded) {
444
+ fontInfoPromises.set(fontUrl,
445
+ // Catch-clause idiom: TypeScript types caught errors as `unknown`.
446
+ // eslint-disable-next-line no-restricted-syntax
447
+ getFontInfo(fontAsset.rawSrc).catch((rawErr) => {
448
+ const err = rawErr;
449
+ err.asset = err.asset || fontAsset;
450
+ assetGraph.warn(err);
451
+ return null;
452
+ }));
724
453
  }
725
- }
726
454
  }
727
- }
728
-
729
- // Cache subset CSS assets by their source text to avoid redundant
730
- // addAsset/minify/removeAsset cycles for pages sharing identical CSS.
731
- const subsetCssAssetCache = new Map();
732
-
733
- // Cache the heavy CSS-text assembly (including base64-encoded font data)
734
- // keyed by the shared accumulatedFontFaceDeclarations array. Pages grouped
735
- // under the same stylesheet config produce byte-identical output, so this
736
- // collapses the per-page string build from O(pages) to O(unique configs).
737
- const subsetCssTextCache = new WeakMap();
738
-
739
- // Pre-index relations by source asset to avoid O(allRelations) scans
740
- // in the per-page injection loop below. Build indices once, then use
741
- // O(1) lookups per page instead of repeated assetGraph.findRelations.
742
- const styleRelsByAsset = new Map();
743
- const noscriptRelsByAsset = new Map();
744
- const preloadRelsByAsset = new Map();
745
- const relTypeToIndex = {
746
- HtmlStyle: styleRelsByAsset,
747
- SvgStyle: styleRelsByAsset,
748
- HtmlNoscript: noscriptRelsByAsset,
749
- HtmlPrefetchLink: preloadRelsByAsset,
750
- HtmlPreloadLink: preloadRelsByAsset,
751
- };
752
- for (const relation of assetGraph.findRelations({
753
- type: { $in: Object.keys(relTypeToIndex) },
754
- })) {
755
- const index = relTypeToIndex[relation.type];
756
- const from = relation.from;
757
- if (!index.has(from)) index.set(from, []);
758
- index.get(from).push(relation);
759
- }
760
-
761
- const insertPhase = trackPhase(
762
- `insert subsets loop (${htmlOrSvgAssetTextsWithProps.length} pages)`
763
- );
764
- let numFontUsagesWithSubset = 0;
765
- for (const {
766
- htmlOrSvgAsset,
767
- fontUsages,
768
- accumulatedFontFaceDeclarations,
769
- } of htmlOrSvgAssetTextsWithProps) {
770
- const styleRels = styleRelsByAsset.get(htmlOrSvgAsset) || [];
771
- let insertionPoint = styleRels[0];
772
-
773
- // Fall back to inserting before a <noscript> that contains a stylesheet
774
- // when no direct stylesheet relation exists (assetgraph#1251)
775
- if (!insertionPoint && htmlOrSvgAsset.type === 'Html') {
776
- for (const htmlNoScript of noscriptRelsByAsset.get(htmlOrSvgAsset) ||
777
- []) {
778
- const noscriptStyleRels = styleRelsByAsset.get(htmlNoScript.to) || [];
779
- if (noscriptStyleRels.length > 0) {
780
- insertionPoint = htmlNoScript;
781
- break;
455
+ const fontInfoResults = new Map(await Promise.all([...fontInfoPromises].map(async ([key, promise]) => [key, await promise])));
456
+ // Build global codepoints synchronously from pre-fetched results
457
+ const globalCodepointsByFontUrl = new Map();
458
+ const codepointsCache = new Map();
459
+ for (const htmlOrSvgAssetTextWithProps of htmlOrSvgAssetTextsWithProps) {
460
+ for (const fontUsage of htmlOrSvgAssetTextWithProps.fontUsages) {
461
+ let cached = globalCodepointsByFontUrl.get(fontUsage.fontUrl);
462
+ if (!cached) {
463
+ cached = { originalCodepoints: null };
464
+ const fontInfo = fontUsage.fontUrl
465
+ ? fontInfoResults.get(fontUsage.fontUrl)
466
+ : undefined;
467
+ if (fontInfo) {
468
+ const originalCodepoints = fontInfo.characterSet;
469
+ const usedCodepoints = (0, fontFaceHelpers_1.getCodepoints)(fontUsage.text);
470
+ const usedCodepointsSet = new Set(usedCodepoints);
471
+ cached.originalCodepoints = originalCodepoints;
472
+ cached.usedCodepoints = usedCodepoints;
473
+ cached.unusedCodepoints = originalCodepoints.filter((n) => !usedCodepointsSet.has(n));
474
+ }
475
+ globalCodepointsByFontUrl.set(fontUsage.fontUrl, cached);
476
+ }
477
+ if (cached.originalCodepoints) {
478
+ // Cache getCodepoints result by pageText string to avoid
479
+ // recomputing for pages with identical text per font
480
+ const pageText = fontUsage.pageText ?? '';
481
+ let pageCodepoints = codepointsCache.get(pageText);
482
+ if (!pageCodepoints) {
483
+ pageCodepoints = (0, fontFaceHelpers_1.getCodepoints)(pageText);
484
+ codepointsCache.set(pageText, pageCodepoints);
485
+ }
486
+ fontUsage.codepoints = {
487
+ original: cached.originalCodepoints,
488
+ used: cached.usedCodepoints ?? [],
489
+ unused: cached.unusedCodepoints ?? [],
490
+ page: pageCodepoints,
491
+ };
492
+ }
493
+ else {
494
+ fontUsage.codepoints = {
495
+ original: [],
496
+ used: [],
497
+ unused: [],
498
+ page: [],
499
+ };
500
+ }
782
501
  }
783
- }
784
502
  }
785
- const subsetFontUsages = fontUsages.filter(
786
- (fontUsage) => fontUsage.subsets
787
- );
788
- const subsetFontUsagesSet = new Set(subsetFontUsages);
789
- const unsubsettedFontUsages = fontUsages.filter(
790
- (fontUsage) => !subsetFontUsagesSet.has(fontUsage)
791
- );
792
-
793
- // Remove all existing preload hints to fonts that might have new subsets
794
- const fontUrls = new Set(fontUsages.map((fu) => fu.fontUrl));
795
- for (const relation of preloadRelsByAsset.get(htmlOrSvgAsset) || []) {
796
- if (!relation.to || !fontUrls.has(relation.to.url)) continue;
797
-
798
- if (relation.type === 'HtmlPrefetchLink') {
799
- const err = new Error(
800
- `Detached ${relation.node.outerHTML}. Will be replaced with preload with JS fallback.\nIf you feel this is wrong, open an issue at https://github.com/alexander-turner/subfont/issues`
801
- );
802
- err.asset = relation.from;
803
- err.relation = relation;
804
- assetGraph.info(err);
805
- }
806
- relation.detach();
807
- }
808
-
809
- const unsubsettedFontUsagesToPreload = unsubsettedFontUsages.filter(
810
- (fontUsage) => fontUsage.preload
811
- );
812
-
813
- if (unsubsettedFontUsagesToPreload.length > 0) {
814
- // Insert <link rel="preload">
815
- for (const fontUsage of unsubsettedFontUsagesToPreload) {
816
- // Always preload unsubsetted font files, they might be any format, so can't be clever here
817
- const preloadRelation = htmlOrSvgAsset.addRelation(
818
- {
819
- type: 'HtmlPreloadLink',
820
- hrefType,
821
- to: fontUsage.fontUrl,
822
- as: 'font',
823
- },
824
- insertionPoint ? 'before' : 'firstInHead',
825
- insertionPoint
826
- );
827
- insertionPoint = insertionPoint || preloadRelation;
828
- }
829
- }
830
-
831
- if (subsetFontUsages.length === 0) {
832
- continue;
833
- }
834
- numFontUsagesWithSubset += subsetFontUsages.length;
835
-
836
- let cssTextParts = subsetCssTextCache.get(accumulatedFontFaceDeclarations);
837
- if (!cssTextParts) {
838
- cssTextParts = {
839
- subset: getFontUsageStylesheet(subsetFontUsages),
840
- unused: getUnusedVariantsStylesheet(
841
- fontUsages,
842
- accumulatedFontFaceDeclarations
843
- ),
844
- };
845
- subsetCssTextCache.set(accumulatedFontFaceDeclarations, cssTextParts);
846
- }
847
- let subsetCssText = cssTextParts.subset;
848
- const unusedVariantsCss = cssTextParts.unused;
849
- if (!inlineCss && !omitFallbacks) {
850
- // This can go into the same stylesheet because we won't reload all __subset suffixed families in the JS preload fallback
851
- subsetCssText += unusedVariantsCss;
852
- }
853
-
854
- const cssAsset = await getOrCreateSubsetCssAsset({
855
- assetGraph,
856
- subsetCssText,
857
- subsetFontUsages,
858
- formats,
859
- subsetUrl,
860
- hrefType,
861
- inlineCss,
862
- fontUrlsUsedOnEveryPage,
863
- numPages: htmlOrSvgAssetTextsWithProps.length,
864
- subsetCssAssetCache,
865
- });
866
-
867
- insertionPoint = addSubsetFontPreloads({
868
- cssAsset,
869
- fontUsages,
870
- htmlOrSvgAsset,
871
- subsetUrl,
872
- hrefType,
873
- insertionPoint,
874
- });
875
- const cssRelation = htmlOrSvgAsset.addRelation(
876
- {
877
- type: `${htmlOrSvgAsset.type}Style`,
878
- hrefType:
879
- inlineCss || htmlOrSvgAsset.type === 'Svg' ? 'inline' : hrefType,
880
- to: cssAsset,
881
- },
882
- insertionPoint ? 'before' : 'firstInHead',
883
- insertionPoint
884
- );
885
- insertionPoint = insertionPoint || cssRelation;
886
-
887
- if (!omitFallbacks && inlineCss && unusedVariantsCss) {
888
- // The fallback CSS for unused variants needs to go into its own stylesheet after the crude version of the JS-based preload "polyfill"
889
- const cssAsset = htmlOrSvgAsset.addRelation(
890
- {
891
- type: 'HtmlStyle',
892
- to: {
893
- type: 'Css',
894
- text: unusedVariantsCss,
895
- },
896
- },
897
- 'after',
898
- cssRelation
899
- ).to;
900
- for (const relation of cssAsset.outgoingRelations) {
901
- relation.hrefType = hrefType;
902
- }
503
+ timings['codepoint generation'] = codepointPhase.end();
504
+ if (onlyInfo) {
505
+ return {
506
+ fontInfo: htmlOrSvgAssetTextsWithProps.map(({ fontUsages, htmlOrSvgAsset }) => ({
507
+ assetFileName: htmlOrSvgAsset.nonInlineAncestor.urlOrDescription,
508
+ fontUsages: fontUsages.map((fontUsage) => (({ hasFontFeatureSettings, fontFeatureTags, ...rest }) => rest)(fontUsage)),
509
+ })),
510
+ timings,
511
+ };
903
512
  }
904
- }
905
-
906
- timings['insert subsets loop'] = insertPhase.end();
907
-
908
- if (numFontUsagesWithSubset === 0) {
909
- return { fontInfo: [], timings };
910
- }
911
-
912
- const lazyFallbackPhase = trackPhase('lazy load fallback CSS');
913
- const relationsToRemove = new Set();
914
-
915
- // Lazy load the original @font-face declarations of self-hosted fonts (unless omitFallbacks)
916
- const originalRelations = new Set();
917
- const fallbackCssAssetCache = new Map();
918
- for (const htmlOrSvgAsset of htmlOrSvgAssets) {
919
- const accumulatedFontFaceDeclarations =
920
- fontFaceDeclarationsByHtmlOrSvgAsset.get(htmlOrSvgAsset);
921
- const containedRelationsByFontFaceRule = new Map();
922
- for (const { relations } of accumulatedFontFaceDeclarations) {
923
- for (const relation of relations) {
924
- if (
925
- relation.from.hostname === 'fonts.googleapis.com' || // Google Web Fonts handled separately below
926
- containedRelationsByFontFaceRule.has(relation.node)
927
- ) {
928
- continue;
513
+ const variationPhase = trackPhase('variation axis usage');
514
+ const { seenAxisValuesByFontUrlAndAxisName } = (0, variationAxes_1.getVariationAxisUsage)(htmlOrSvgAssetTextsWithProps, fontFaceHelpers_1.parseFontWeightRange, fontFaceHelpers_1.parseFontStretchRange);
515
+ timings['variation axis usage'] = variationPhase.end();
516
+ // Generate subsets:
517
+ if (console) {
518
+ const uniqueFontUrls = countUniqueFontUrls(htmlOrSvgAssetTextsWithProps);
519
+ if (uniqueFontUrls > 0) {
520
+ console.log(` Subsetting ${uniqueFontUrls} unique font file${uniqueFontUrls === 1 ? '' : 's'}...`);
929
521
  }
930
- originalRelations.add(relation);
931
- containedRelationsByFontFaceRule.set(
932
- relation.node,
933
- relation.from.outgoingRelations.filter(
934
- (otherRelation) => otherRelation.node === relation.node
935
- )
936
- );
937
- }
938
- }
939
-
940
- if (
941
- containedRelationsByFontFaceRule.size === 0 ||
942
- omitFallbacks ||
943
- htmlOrSvgAsset.type !== 'Html'
944
- ) {
945
- continue;
946
522
  }
947
-
948
- // Group @font-face rules by their enclosing @media context so the
949
- // fallback CSS preserves the original media-conditional loading.
950
- // Walk up the ancestor chain in case the rule is nested (e.g.
951
- // inside @supports inside @media).
952
- const rulesByMedia = new Map(); // media params (or '') → [cssText, ...]
953
- for (const rule of containedRelationsByFontFaceRule.keys()) {
954
- let mediaKey = '';
955
- let ancestor = rule.parent;
956
- while (ancestor) {
957
- if (
958
- ancestor.type === 'atrule' &&
959
- ancestor.name.toLowerCase() === 'media'
960
- ) {
961
- mediaKey = ancestor.params;
962
- break;
523
+ const subsetPhase = trackPhase('getSubsetsForFontUsage');
524
+ await (0, subsetGeneration_1.getSubsetsForFontUsage)(assetGraph, htmlOrSvgAssetTextsWithProps, formats, seenAxisValuesByFontUrlAndAxisName, cacheDir, console, debug);
525
+ timings.getSubsetsForFontUsage = subsetPhase.end();
526
+ const warnGlyphsPhase = trackPhase('warnAboutMissingGlyphs');
527
+ await warnAboutMissingGlyphs(htmlOrSvgAssetTextsWithProps, assetGraph);
528
+ timings.warnAboutMissingGlyphs = warnGlyphsPhase.end();
529
+ // Insert subsets:
530
+ // Pre-compute which fontUrls are used (with text) on every page.
531
+ // Set intersection: O(pages × fonts_per_page) vs the old every+some approach.
532
+ const fontUrlsUsedOnEveryPage = new Set();
533
+ if (htmlOrSvgAssetTextsWithProps.length > 0) {
534
+ const firstPageFontUrls = new Set();
535
+ for (const fu of htmlOrSvgAssetTextsWithProps[0]
536
+ .fontUsages) {
537
+ if (fu.pageText && fu.fontUrl)
538
+ firstPageFontUrls.add(fu.fontUrl);
539
+ }
540
+ for (const fontUrl of firstPageFontUrls) {
541
+ fontUrlsUsedOnEveryPage.add(fontUrl);
542
+ }
543
+ for (let i = 1; i < htmlOrSvgAssetTextsWithProps.length; i++) {
544
+ const pageFontUrls = new Set();
545
+ for (const fu of htmlOrSvgAssetTextsWithProps[i]
546
+ .fontUsages) {
547
+ if (fu.pageText && fu.fontUrl)
548
+ pageFontUrls.add(fu.fontUrl);
549
+ }
550
+ for (const fontUrl of fontUrlsUsedOnEveryPage) {
551
+ if (!pageFontUrls.has(fontUrl)) {
552
+ fontUrlsUsedOnEveryPage.delete(fontUrl);
553
+ }
554
+ }
963
555
  }
964
- ancestor = ancestor.parent;
965
- }
966
- if (!rulesByMedia.has(mediaKey)) rulesByMedia.set(mediaKey, []);
967
- rulesByMedia
968
- .get(mediaKey)
969
- .push(
970
- getFontFaceDeclarationText(
971
- rule,
972
- containedRelationsByFontFaceRule.get(rule)
973
- )
974
- );
975
- }
976
- let fallbackCssText = '';
977
- for (const [media, texts] of rulesByMedia) {
978
- if (media) {
979
- fallbackCssText += `@media ${media}{${texts.join('')}}`;
980
- } else {
981
- fallbackCssText += texts.join('');
982
- }
983
- }
984
-
985
- let cssAsset = fallbackCssAssetCache.get(fallbackCssText);
986
- if (!cssAsset) {
987
- cssAsset = assetGraph.addAsset({
988
- type: 'Css',
989
- text: fallbackCssText,
990
- });
991
- for (const relation of cssAsset.outgoingRelations) {
992
- relation.hrefType = hrefType;
993
- }
994
- await cssAsset.minify();
995
- cssAsset.url = `${subsetUrl}fallback-${cssAsset.md5Hex.slice(0, 10)}.css`;
996
- fallbackCssAssetCache.set(fallbackCssText, cssAsset);
997
- }
998
-
999
- // Create a <link rel="stylesheet"> that asyncLoadStyleRelationWithFallback can convert to async with noscript fallback:
1000
- const fallbackHtmlStyle = htmlOrSvgAsset.addRelation({
1001
- type: 'HtmlStyle',
1002
- to: cssAsset,
1003
- });
1004
-
1005
- asyncLoadStyleRelationWithFallback(
1006
- htmlOrSvgAsset,
1007
- fallbackHtmlStyle,
1008
- hrefType
1009
- );
1010
- relationsToRemove.add(fallbackHtmlStyle);
1011
- }
1012
-
1013
- timings['lazy load fallback CSS'] = lazyFallbackPhase.end();
1014
-
1015
- const removeFontFacePhase = trackPhase('remove original @font-face');
1016
-
1017
- // Remove the original @font-face blocks, and don't leave behind empty stylesheets:
1018
- const maybeEmptyCssAssets = new Set();
1019
- for (const relation of originalRelations) {
1020
- const cssAsset = relation.from;
1021
- if (relation.node.parent) {
1022
- relation.node.parent.removeChild(relation.node);
1023
- }
1024
- relation.remove();
1025
- cssAsset.markDirty();
1026
- maybeEmptyCssAssets.add(cssAsset);
1027
- }
1028
-
1029
- for (const cssAsset of maybeEmptyCssAssets) {
1030
- if (cssAssetIsEmpty(cssAsset)) {
1031
- for (const incomingRelation of cssAsset.incomingRelations) {
1032
- incomingRelation.detach();
1033
- }
1034
- assetGraph.removeAsset(cssAsset);
1035
556
  }
1036
- }
1037
-
1038
- timings['remove original @font-face'] = removeFontFacePhase.end();
1039
-
1040
- const googleCleanupPhase = trackPhase('Google Fonts + cleanup');
1041
-
1042
- // Async load Google Web Fonts CSS. Skip the regex findAssets scan and
1043
- // the surrounding loop entirely when no Google Fonts were detected up
1044
- // front the final detach loop below still runs because other phases
1045
- // (lazy fallback CSS) populate relationsToRemove.
1046
- const googleFontStylesheets = hasGoogleFonts
1047
- ? assetGraph.findAssets({
1048
- type: 'Css',
1049
- url: { $regex: googleFontsCssUrlRegex },
1050
- })
1051
- : [];
1052
- const selfHostedGoogleCssByUrl = new Map();
1053
- for (const googleFontStylesheet of googleFontStylesheets) {
1054
- const seenPages = new Set(); // Only do the work once for each font on each page
1055
- for (const googleFontStylesheetRelation of googleFontStylesheet.incomingRelations) {
1056
- let htmlParents;
1057
-
1058
- if (googleFontStylesheetRelation.type === 'CssImport') {
1059
- // Gather Html parents. Relevant if we are dealing with CSS @import relations
1060
- htmlParents = getParents(googleFontStylesheetRelation.to, {
1061
- type: { $in: ['Html', 'Svg'] },
1062
- isInline: false,
1063
- isLoaded: true,
1064
- });
1065
- } else if (
1066
- ['Html', 'Svg'].includes(googleFontStylesheetRelation.from.type)
1067
- ) {
1068
- htmlParents = [googleFontStylesheetRelation.from];
1069
- } else {
1070
- htmlParents = [];
1071
- }
1072
- for (const htmlParent of htmlParents) {
1073
- if (seenPages.has(htmlParent)) continue;
1074
- seenPages.add(htmlParent);
1075
- relationsToRemove.add(googleFontStylesheetRelation);
1076
-
1077
- if (omitFallbacks) continue;
1078
-
1079
- let selfHostedGoogleFontsCssAsset = selfHostedGoogleCssByUrl.get(
1080
- googleFontStylesheetRelation.to.url
1081
- );
1082
- if (!selfHostedGoogleFontsCssAsset) {
1083
- selfHostedGoogleFontsCssAsset =
1084
- await createSelfHostedGoogleFontsCssAsset(
1085
- assetGraph,
1086
- googleFontStylesheetRelation.to,
1087
- formats,
1088
- hrefType,
1089
- subsetUrl
1090
- );
1091
- await selfHostedGoogleFontsCssAsset.minify();
1092
- selfHostedGoogleCssByUrl.set(
1093
- googleFontStylesheetRelation.to.url,
1094
- selfHostedGoogleFontsCssAsset
1095
- );
557
+ // Cache subset CSS assets by their source text to avoid redundant
558
+ // addAsset/minify/removeAsset cycles for pages sharing identical CSS.
559
+ const subsetCssAssetCache = new Map();
560
+ // Cache the heavy CSS-text assembly (including base64-encoded font data)
561
+ // keyed by the shared accumulatedFontFaceDeclarations array. Pages grouped
562
+ // under the same stylesheet config produce byte-identical output, so this
563
+ // collapses the per-page string build from O(pages) to O(unique configs).
564
+ const subsetCssTextCache = new WeakMap();
565
+ // Pre-index relations by source asset to avoid O(allRelations) scans
566
+ // in the per-page injection loop below. Build indices once, then use
567
+ // O(1) lookups per page instead of repeated assetGraph.findRelations.
568
+ const styleRelsByAsset = new Map();
569
+ const noscriptRelsByAsset = new Map();
570
+ const preloadRelsByAsset = new Map();
571
+ const relTypeToIndex = {
572
+ HtmlStyle: styleRelsByAsset,
573
+ SvgStyle: styleRelsByAsset,
574
+ HtmlNoscript: noscriptRelsByAsset,
575
+ HtmlPrefetchLink: preloadRelsByAsset,
576
+ HtmlPreloadLink: preloadRelsByAsset,
577
+ };
578
+ for (const relation of assetGraph.findRelations({
579
+ type: { $in: Object.keys(relTypeToIndex) },
580
+ })) {
581
+ const index = relTypeToIndex[relation.type];
582
+ const from = relation.from;
583
+ if (!index.has(from))
584
+ index.set(from, []);
585
+ index.get(from).push(relation);
586
+ }
587
+ const insertPhase = trackPhase(`insert subsets loop (${htmlOrSvgAssetTextsWithProps.length} pages)`);
588
+ let numFontUsagesWithSubset = 0;
589
+ for (const { htmlOrSvgAsset, fontUsages, accumulatedFontFaceDeclarations, } of htmlOrSvgAssetTextsWithProps) {
590
+ const styleRels = styleRelsByAsset.get(htmlOrSvgAsset) || [];
591
+ let insertionPoint = styleRels[0];
592
+ // Fall back to inserting before a <noscript> that contains a stylesheet
593
+ // when no direct stylesheet relation exists (assetgraph#1251)
594
+ if (!insertionPoint && htmlOrSvgAsset.type === 'Html') {
595
+ for (const htmlNoScript of noscriptRelsByAsset.get(htmlOrSvgAsset) ||
596
+ []) {
597
+ const noscriptStyleRels = styleRelsByAsset.get(htmlNoScript.to) || [];
598
+ if (noscriptStyleRels.length > 0) {
599
+ insertionPoint = htmlNoScript;
600
+ break;
601
+ }
602
+ }
1096
603
  }
1097
- const selfHostedFallbackRelation = htmlParent.addRelation(
1098
- {
1099
- type: `${htmlParent.type}Style`,
1100
- to: selfHostedGoogleFontsCssAsset,
604
+ const subsetFontUsages = fontUsages.filter((fontUsage) => fontUsage.subsets);
605
+ const subsetFontUsagesSet = new Set(subsetFontUsages);
606
+ const unsubsettedFontUsages = fontUsages.filter((fontUsage) => !subsetFontUsagesSet.has(fontUsage));
607
+ // Remove all existing preload hints to fonts that might have new subsets
608
+ const fontUrls = new Set(fontUsages.map((fu) => fu.fontUrl));
609
+ for (const relation of preloadRelsByAsset.get(htmlOrSvgAsset) || []) {
610
+ if (!relation.to || !fontUrls.has(relation.to.url))
611
+ continue;
612
+ if (relation.type === 'HtmlPrefetchLink') {
613
+ const err = new Error(`Detached ${relation.node.outerHTML}. Will be replaced with preload with JS fallback.\nIf you feel this is wrong, open an issue at https://github.com/alexander-turner/subfont/issues`);
614
+ err.asset = relation.from;
615
+ err.relation = relation;
616
+ assetGraph.info(err);
617
+ }
618
+ relation.detach();
619
+ }
620
+ const unsubsettedFontUsagesToPreload = unsubsettedFontUsages.filter((fontUsage) => fontUsage.preload);
621
+ if (unsubsettedFontUsagesToPreload.length > 0) {
622
+ // Insert <link rel="preload">
623
+ for (const fontUsage of unsubsettedFontUsagesToPreload) {
624
+ // Always preload unsubsetted font files, they might be any format, so can't be clever here
625
+ const preloadRelation = htmlOrSvgAsset.addRelation({
626
+ type: 'HtmlPreloadLink',
627
+ hrefType,
628
+ to: fontUsage.fontUrl,
629
+ as: 'font',
630
+ }, insertionPoint ? 'before' : 'firstInHead', insertionPoint);
631
+ insertionPoint = insertionPoint || preloadRelation;
632
+ }
633
+ }
634
+ if (subsetFontUsages.length === 0) {
635
+ continue;
636
+ }
637
+ numFontUsagesWithSubset += subsetFontUsages.length;
638
+ let cssTextParts = subsetCssTextCache.get(accumulatedFontFaceDeclarations);
639
+ if (!cssTextParts) {
640
+ cssTextParts = {
641
+ subset: (0, fontFaceHelpers_1.getFontUsageStylesheet)(subsetFontUsages),
642
+ unused: (0, fontFaceHelpers_1.getUnusedVariantsStylesheet)(fontUsages, accumulatedFontFaceDeclarations),
643
+ };
644
+ subsetCssTextCache.set(accumulatedFontFaceDeclarations, cssTextParts);
645
+ }
646
+ let subsetCssText = cssTextParts.subset;
647
+ const unusedVariantsCss = cssTextParts.unused;
648
+ if (!inlineCss && !omitFallbacks) {
649
+ // This can go into the same stylesheet because we won't reload all __subset suffixed families in the JS preload fallback
650
+ subsetCssText += unusedVariantsCss;
651
+ }
652
+ const cssAsset = await getOrCreateSubsetCssAsset({
653
+ assetGraph,
654
+ subsetCssText,
655
+ subsetFontUsages,
656
+ formats,
657
+ subsetUrl,
658
+ hrefType,
659
+ inlineCss,
660
+ fontUrlsUsedOnEveryPage,
661
+ numPages: htmlOrSvgAssetTextsWithProps.length,
662
+ subsetCssAssetCache,
663
+ });
664
+ insertionPoint = addSubsetFontPreloads({
665
+ cssAsset,
666
+ fontUsages,
667
+ htmlOrSvgAsset,
668
+ subsetUrl,
1101
669
  hrefType,
1102
- },
1103
- 'lastInBody'
1104
- );
1105
- relationsToRemove.add(selfHostedFallbackRelation);
1106
- if (htmlParent.type === 'Html') {
1107
- asyncLoadStyleRelationWithFallback(
1108
- htmlParent,
1109
- selfHostedFallbackRelation,
1110
- hrefType
1111
- );
670
+ insertionPoint,
671
+ });
672
+ const cssRelation = htmlOrSvgAsset.addRelation({
673
+ type: `${htmlOrSvgAsset.type}Style`,
674
+ hrefType: inlineCss || htmlOrSvgAsset.type === 'Svg' ? 'inline' : hrefType,
675
+ to: cssAsset,
676
+ }, insertionPoint ? 'before' : 'firstInHead', insertionPoint);
677
+ insertionPoint = insertionPoint || cssRelation;
678
+ if (!omitFallbacks && inlineCss && unusedVariantsCss) {
679
+ // The fallback CSS for unused variants needs to go into its own stylesheet after the crude version of the JS-based preload "polyfill"
680
+ const cssAsset = htmlOrSvgAsset.addRelation({
681
+ type: 'HtmlStyle',
682
+ to: {
683
+ type: 'Css',
684
+ text: unusedVariantsCss,
685
+ },
686
+ }, 'after', cssRelation).to;
687
+ for (const relation of cssAsset.outgoingRelations) {
688
+ relation.hrefType = hrefType;
689
+ }
1112
690
  }
1113
- }
1114
691
  }
1115
- googleFontStylesheet.unload();
1116
- }
1117
-
1118
- // Clean up, making sure not to detach the same relation twice, eg. when multiple pages use the same stylesheet that imports a font
1119
- for (const relation of relationsToRemove) {
1120
- relation.detach();
1121
- }
1122
-
1123
- timings['Google Fonts + cleanup'] = googleCleanupPhase.end();
1124
-
1125
- const injectPhase = trackPhase('inject subset font-family into CSS/SVG');
1126
-
1127
- // Use subsets in font-family:
1128
-
1129
- const webfontNameMap = Object.create(null);
1130
-
1131
- for (const { fontUsages } of htmlOrSvgAssetTextsWithProps) {
1132
- for (const { subsets, fontFamilies, props } of fontUsages) {
1133
- if (subsets) {
1134
- for (const fontFamily of fontFamilies) {
1135
- webfontNameMap[fontFamily.toLowerCase()] =
1136
- `${props['font-family']}__subset`;
692
+ timings['insert subsets loop'] = insertPhase.end();
693
+ if (numFontUsagesWithSubset === 0) {
694
+ return { fontInfo: [], timings };
695
+ }
696
+ const lazyFallbackPhase = trackPhase('lazy load fallback CSS');
697
+ const relationsToRemove = new Set();
698
+ // Lazy load the original @font-face declarations of self-hosted fonts (unless omitFallbacks)
699
+ const originalRelations = new Set();
700
+ const fallbackCssAssetCache = new Map();
701
+ for (const htmlOrSvgAsset of htmlOrSvgAssets) {
702
+ const accumulatedFontFaceDeclarations = fontFaceDeclarationsByHtmlOrSvgAsset.get(htmlOrSvgAsset);
703
+ if (!accumulatedFontFaceDeclarations)
704
+ continue;
705
+ const containedRelationsByFontFaceRule = new Map();
706
+ for (const { relations } of accumulatedFontFaceDeclarations) {
707
+ for (const relation of relations) {
708
+ if (relation.from.hostname ===
709
+ 'fonts.googleapis.com' || // Google Web Fonts handled separately below
710
+ containedRelationsByFontFaceRule.has(relation.node)) {
711
+ continue;
712
+ }
713
+ originalRelations.add(relation);
714
+ containedRelationsByFontFaceRule.set(relation.node, relation.from.outgoingRelations.filter((otherRelation) => otherRelation.node === relation.node));
715
+ }
1137
716
  }
1138
- }
1139
- }
1140
- }
1141
-
1142
- let customPropertyDefinitions; // Avoid computing this unless necessary
1143
- const cssAssetsDirtiedByCustomProps = new Set();
1144
- // Inject subset font name before original webfont in SVG font-family attributes
1145
- const svgAssets = assetGraph.findAssets({ type: 'Svg' });
1146
- for (const svgAsset of svgAssets) {
1147
- if (!svgAsset.isLoaded) continue;
1148
- let changesMade = false;
1149
- for (const element of Array.from(
1150
- svgAsset.parseTree.querySelectorAll('[font-family]')
1151
- )) {
1152
- const fontFamilies = cssListHelpers.splitByCommas(
1153
- element.getAttribute('font-family')
1154
- );
1155
- for (let i = 0; i < fontFamilies.length; i += 1) {
1156
- const subsetFontFamily =
1157
- webfontNameMap[
1158
- cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()
1159
- ];
1160
- if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
1161
- fontFamilies.splice(
1162
- i,
1163
- omitFallbacks ? 1 : 0,
1164
- maybeCssQuote(subsetFontFamily)
1165
- );
1166
- i += 1;
1167
- element.setAttribute('font-family', fontFamilies.join(', '));
1168
- changesMade = true;
717
+ if (containedRelationsByFontFaceRule.size === 0 ||
718
+ omitFallbacks ||
719
+ htmlOrSvgAsset.type !== 'Html') {
720
+ continue;
721
+ }
722
+ // Group @font-face rules by their enclosing @media context so the
723
+ // fallback CSS preserves the original media-conditional loading.
724
+ // Walk up the ancestor chain in case the rule is nested (e.g.
725
+ // inside @supports inside @media).
726
+ const rulesByMedia = new Map();
727
+ for (const rule of containedRelationsByFontFaceRule.keys()) {
728
+ let mediaKey = '';
729
+ let ancestor = rule.parent;
730
+ while (ancestor) {
731
+ if (ancestor.type === 'atrule' &&
732
+ ancestor.name?.toLowerCase() === 'media') {
733
+ mediaKey = ancestor.params ?? '';
734
+ break;
735
+ }
736
+ ancestor = ancestor.parent;
737
+ }
738
+ if (!rulesByMedia.has(mediaKey))
739
+ rulesByMedia.set(mediaKey, []);
740
+ rulesByMedia
741
+ .get(mediaKey)
742
+ .push((0, fontFaceHelpers_1.getFontFaceDeclarationText)(rule, containedRelationsByFontFaceRule.get(rule) ?? []));
743
+ }
744
+ let fallbackCssText = '';
745
+ for (const [media, texts] of rulesByMedia) {
746
+ if (media) {
747
+ fallbackCssText += `@media ${media}{${texts.join('')}}`;
748
+ }
749
+ else {
750
+ fallbackCssText += texts.join('');
751
+ }
1169
752
  }
1170
- }
753
+ let cssAsset = fallbackCssAssetCache.get(fallbackCssText);
754
+ if (!cssAsset) {
755
+ cssAsset = assetGraph.addAsset({
756
+ type: 'Css',
757
+ text: fallbackCssText,
758
+ });
759
+ for (const relation of cssAsset.outgoingRelations) {
760
+ relation.hrefType = hrefType;
761
+ }
762
+ await cssAsset.minify();
763
+ cssAsset.url = `${subsetUrl}fallback-${cssAsset.md5Hex.slice(0, 10)}.css`;
764
+ fallbackCssAssetCache.set(fallbackCssText, cssAsset);
765
+ }
766
+ // Create a <link rel="stylesheet"> that asyncLoadStyleRelationWithFallback can convert to async with noscript fallback:
767
+ const fallbackHtmlStyle = htmlOrSvgAsset.addRelation({
768
+ type: 'HtmlStyle',
769
+ to: cssAsset,
770
+ });
771
+ asyncLoadStyleRelationWithFallback(htmlOrSvgAsset, fallbackHtmlStyle, hrefType);
772
+ relationsToRemove.add(fallbackHtmlStyle);
773
+ }
774
+ timings['lazy load fallback CSS'] = lazyFallbackPhase.end();
775
+ const removeFontFacePhase = trackPhase('remove original @font-face');
776
+ // Remove the original @font-face blocks, and don't leave behind empty stylesheets:
777
+ const maybeEmptyCssAssets = new Set();
778
+ for (const relation of originalRelations) {
779
+ const cssAsset = relation.from;
780
+ if (relation.node.parent) {
781
+ relation.node.parent.removeChild(relation.node);
782
+ }
783
+ relation.remove();
784
+ cssAsset.markDirty();
785
+ maybeEmptyCssAssets.add(cssAsset);
1171
786
  }
1172
- if (changesMade) {
1173
- svgAsset.markDirty();
787
+ for (const cssAsset of maybeEmptyCssAssets) {
788
+ if ((0, fontFaceHelpers_1.cssAssetIsEmpty)(cssAsset)) {
789
+ for (const incomingRelation of cssAsset.incomingRelations) {
790
+ incomingRelation.detach();
791
+ }
792
+ assetGraph.removeAsset(cssAsset);
793
+ }
1174
794
  }
1175
- }
1176
-
1177
- // Inject subset font name before original webfont in CSS
1178
- const cssAssets = assetGraph.findAssets({
1179
- type: 'Css',
1180
- isLoaded: true,
1181
- });
1182
- const parseTreeToAsset = new Map();
1183
- for (const cssAsset of cssAssets) {
1184
- parseTreeToAsset.set(cssAsset.parseTree, cssAsset);
1185
- }
1186
- for (const cssAsset of cssAssets) {
1187
- let changesMade = false;
1188
- cssAsset.eachRuleInParseTree((cssRule) => {
1189
- if (cssRule.parent.type !== 'rule' || cssRule.type !== 'decl') return;
1190
-
1191
- const propName = cssRule.prop.toLowerCase();
1192
- if (
1193
- (propName === 'font' || propName === 'font-family') &&
1194
- cssRule.value.includes('var(')
1195
- ) {
1196
- if (!customPropertyDefinitions) {
1197
- customPropertyDefinitions = findCustomPropertyDefinitions(cssAssets);
795
+ timings['remove original @font-face'] = removeFontFacePhase.end();
796
+ const googleCleanupPhase = trackPhase('Google Fonts + cleanup');
797
+ // Async load Google Web Fonts CSS. Skip the regex findAssets scan and
798
+ // the surrounding loop entirely when no Google Fonts were detected up
799
+ // front — the final detach loop below still runs because other phases
800
+ // (lazy fallback CSS) populate relationsToRemove.
801
+ const googleFontStylesheets = hasGoogleFonts
802
+ ? assetGraph.findAssets({
803
+ type: 'Css',
804
+ url: { $regex: googleFontsCssUrlRegex },
805
+ })
806
+ : [];
807
+ const selfHostedGoogleCssByUrl = new Map();
808
+ for (const googleFontStylesheet of googleFontStylesheets) {
809
+ const seenPages = new Set(); // Only do the work once for each font on each page
810
+ for (const googleFontStylesheetRelation of googleFontStylesheet.incomingRelations) {
811
+ let htmlParents;
812
+ if (googleFontStylesheetRelation.type === 'CssImport') {
813
+ // Gather Html parents. Relevant if we are dealing with CSS @import relations
814
+ htmlParents = getParents(googleFontStylesheetRelation.to, {
815
+ type: { $in: ['Html', 'Svg'] },
816
+ isInline: false,
817
+ isLoaded: true,
818
+ });
819
+ }
820
+ else if (['Html', 'Svg'].includes(googleFontStylesheetRelation.from.type ?? '')) {
821
+ htmlParents = [googleFontStylesheetRelation.from];
822
+ }
823
+ else {
824
+ htmlParents = [];
825
+ }
826
+ for (const htmlParent of htmlParents) {
827
+ if (seenPages.has(htmlParent))
828
+ continue;
829
+ seenPages.add(htmlParent);
830
+ relationsToRemove.add(googleFontStylesheetRelation);
831
+ if (omitFallbacks)
832
+ continue;
833
+ let selfHostedGoogleFontsCssAsset = selfHostedGoogleCssByUrl.get(googleFontStylesheetRelation.to.url);
834
+ if (!selfHostedGoogleFontsCssAsset) {
835
+ selfHostedGoogleFontsCssAsset =
836
+ await createSelfHostedGoogleFontsCssAsset(assetGraph, googleFontStylesheetRelation.to, formats, hrefType, subsetUrl);
837
+ await selfHostedGoogleFontsCssAsset.minify();
838
+ selfHostedGoogleCssByUrl.set(googleFontStylesheetRelation.to.url, selfHostedGoogleFontsCssAsset);
839
+ }
840
+ const selfHostedFallbackRelation = htmlParent.addRelation({
841
+ type: `${htmlParent.type}Style`,
842
+ to: selfHostedGoogleFontsCssAsset,
843
+ hrefType,
844
+ }, 'lastInBody');
845
+ relationsToRemove.add(selfHostedFallbackRelation);
846
+ if (htmlParent.type === 'Html') {
847
+ asyncLoadStyleRelationWithFallback(htmlParent, selfHostedFallbackRelation, hrefType);
848
+ }
849
+ }
1198
850
  }
1199
- for (const customPropertyName of extractReferencedCustomPropertyNames(
1200
- cssRule.value
1201
- )) {
1202
- for (const relatedCssRule of [
1203
- cssRule,
1204
- ...(customPropertyDefinitions[customPropertyName] || []),
1205
- ]) {
1206
- const modifiedValue = injectSubsetDefinitions(
1207
- relatedCssRule.value,
1208
- webfontNameMap,
1209
- omitFallbacks // replaceOriginal
1210
- );
1211
- if (modifiedValue !== relatedCssRule.value) {
1212
- relatedCssRule.value = modifiedValue;
1213
- const ownerAsset = parseTreeToAsset.get(relatedCssRule.root());
1214
- if (ownerAsset) {
1215
- cssAssetsDirtiedByCustomProps.add(ownerAsset);
1216
- }
851
+ googleFontStylesheet.unload();
852
+ }
853
+ // Clean up, making sure not to detach the same relation twice, eg. when multiple pages use the same stylesheet that imports a font
854
+ for (const relation of relationsToRemove) {
855
+ relation.detach();
856
+ }
857
+ timings['Google Fonts + cleanup'] = googleCleanupPhase.end();
858
+ const injectPhase = trackPhase('inject subset font-family into CSS/SVG');
859
+ // Use subsets in font-family:
860
+ const webfontNameMap = Object.create(null);
861
+ for (const { fontUsages } of htmlOrSvgAssetTextsWithProps) {
862
+ for (const { subsets, fontFamilies, props } of fontUsages) {
863
+ if (subsets) {
864
+ for (const fontFamily of fontFamilies) {
865
+ webfontNameMap[fontFamily.toLowerCase()] =
866
+ `${props['font-family']}__subset`;
867
+ }
1217
868
  }
1218
- }
1219
869
  }
1220
- } else if (propName === 'font-family') {
1221
- const fontFamilies = cssListHelpers.splitByCommas(cssRule.value);
1222
- for (let i = 0; i < fontFamilies.length; i += 1) {
1223
- const subsetFontFamily =
1224
- webfontNameMap[
1225
- cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()
1226
- ];
1227
- if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
1228
- fontFamilies.splice(
1229
- i,
1230
- omitFallbacks ? 1 : 0,
1231
- maybeCssQuote(subsetFontFamily)
1232
- );
1233
- i += 1;
1234
- cssRule.value = fontFamilies.join(', ');
1235
- changesMade = true;
1236
- }
870
+ }
871
+ let customPropertyDefinitions;
872
+ const cssAssetsDirtiedByCustomProps = new Set();
873
+ // Inject subset font name before original webfont in SVG font-family attributes
874
+ const svgAssets = assetGraph.findAssets({ type: 'Svg' });
875
+ for (const svgAsset of svgAssets) {
876
+ if (!svgAsset.isLoaded)
877
+ continue;
878
+ let changesMade = false;
879
+ for (const element of Array.from(svgAsset.parseTree.querySelectorAll('[font-family]'))) {
880
+ const fontFamilies = cssListHelpers.splitByCommas(element.getAttribute('font-family'));
881
+ for (let i = 0; i < fontFamilies.length; i += 1) {
882
+ const subsetFontFamily = webfontNameMap[cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()];
883
+ if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
884
+ fontFamilies.splice(i, omitFallbacks ? 1 : 0, (0, fontFaceHelpers_1.maybeCssQuote)(subsetFontFamily));
885
+ i += 1;
886
+ element.setAttribute('font-family', fontFamilies.join(', '));
887
+ changesMade = true;
888
+ }
889
+ }
1237
890
  }
1238
- } else if (propName === 'font') {
1239
- const fontProperties = cssFontParser.parseFont(cssRule.value);
1240
- const fontFamilies =
1241
- fontProperties && fontProperties['font-family'].map(unquote);
1242
- if (!fontFamilies) return;
1243
-
1244
- const subsetFontFamily = webfontNameMap[fontFamilies[0].toLowerCase()];
1245
- if (!subsetFontFamily || fontFamilies.includes(subsetFontFamily))
1246
- return;
1247
-
1248
- // Rebuild the font shorthand with the subset family prepended
1249
- if (omitFallbacks) {
1250
- fontFamilies.shift();
891
+ if (changesMade) {
892
+ svgAsset.markDirty();
1251
893
  }
1252
- fontFamilies.unshift(subsetFontFamily);
1253
- const stylePrefix = fontProperties['font-style']
1254
- ? `${fontProperties['font-style']} `
1255
- : '';
1256
- const weightPrefix = fontProperties['font-weight']
1257
- ? `${fontProperties['font-weight']} `
1258
- : '';
1259
- const lineHeightSuffix = fontProperties['line-height']
1260
- ? `/${fontProperties['line-height']}`
1261
- : '';
1262
- cssRule.value = `${stylePrefix}${weightPrefix}${
1263
- fontProperties['font-size']
1264
- }${lineHeightSuffix} ${fontFamilies.map(maybeCssQuote).join(', ')}`;
1265
- changesMade = true;
1266
- }
894
+ }
895
+ // Inject subset font name before original webfont in CSS
896
+ const cssAssets = assetGraph.findAssets({
897
+ type: 'Css',
898
+ isLoaded: true,
1267
899
  });
1268
- if (changesMade) {
1269
- cssAsset.markDirty();
900
+ const parseTreeToAsset = new Map();
901
+ for (const cssAsset of cssAssets) {
902
+ parseTreeToAsset.set(cssAsset.parseTree, cssAsset);
903
+ }
904
+ for (const cssAsset of cssAssets) {
905
+ let changesMade = false;
906
+ cssAsset.eachRuleInParseTree((cssRule) => {
907
+ if (cssRule.parent.type !== 'rule' || cssRule.type !== 'decl')
908
+ return;
909
+ const propName = cssRule.prop.toLowerCase();
910
+ if ((propName === 'font' || propName === 'font-family') &&
911
+ cssRule.value.includes('var(')) {
912
+ if (!customPropertyDefinitions) {
913
+ customPropertyDefinitions = findCustomPropertyDefinitions(cssAssets);
914
+ }
915
+ for (const customPropertyName of extractReferencedCustomPropertyNames(cssRule.value)) {
916
+ for (const relatedCssRule of [
917
+ cssRule,
918
+ ...(customPropertyDefinitions[customPropertyName] || []),
919
+ ]) {
920
+ const modifiedValue = injectSubsetDefinitions(relatedCssRule.value, webfontNameMap, omitFallbacks // replaceOriginal
921
+ );
922
+ if (modifiedValue !== relatedCssRule.value) {
923
+ relatedCssRule.value = modifiedValue;
924
+ const ownerAsset = parseTreeToAsset.get(relatedCssRule.root());
925
+ if (ownerAsset) {
926
+ cssAssetsDirtiedByCustomProps.add(ownerAsset);
927
+ }
928
+ }
929
+ }
930
+ }
931
+ }
932
+ else if (propName === 'font-family') {
933
+ const fontFamilies = cssListHelpers.splitByCommas(cssRule.value);
934
+ for (let i = 0; i < fontFamilies.length; i += 1) {
935
+ const subsetFontFamily = webfontNameMap[cssFontParser.parseFontFamily(fontFamilies[i])[0].toLowerCase()];
936
+ if (subsetFontFamily && !fontFamilies.includes(subsetFontFamily)) {
937
+ fontFamilies.splice(i, omitFallbacks ? 1 : 0, (0, fontFaceHelpers_1.maybeCssQuote)(subsetFontFamily));
938
+ i += 1;
939
+ cssRule.value = fontFamilies.join(', ');
940
+ changesMade = true;
941
+ }
942
+ }
943
+ }
944
+ else if (propName === 'font') {
945
+ const fontProperties = cssFontParser.parseFont(cssRule.value);
946
+ const fontFamilies = fontProperties && fontProperties['font-family'].map(unquote);
947
+ if (!fontFamilies)
948
+ return;
949
+ const subsetFontFamily = webfontNameMap[fontFamilies[0].toLowerCase()];
950
+ if (!subsetFontFamily || fontFamilies.includes(subsetFontFamily))
951
+ return;
952
+ // Rebuild the font shorthand with the subset family prepended
953
+ if (omitFallbacks) {
954
+ fontFamilies.shift();
955
+ }
956
+ fontFamilies.unshift(subsetFontFamily);
957
+ const stylePrefix = fontProperties['font-style']
958
+ ? `${fontProperties['font-style']} `
959
+ : '';
960
+ const weightPrefix = fontProperties['font-weight']
961
+ ? `${fontProperties['font-weight']} `
962
+ : '';
963
+ const lineHeightSuffix = fontProperties['line-height']
964
+ ? `/${fontProperties['line-height']}`
965
+ : '';
966
+ cssRule.value = `${stylePrefix}${weightPrefix}${fontProperties['font-size']}${lineHeightSuffix} ${fontFamilies.map(fontFaceHelpers_1.maybeCssQuote).join(', ')}`;
967
+ changesMade = true;
968
+ }
969
+ });
970
+ if (changesMade) {
971
+ cssAsset.markDirty();
972
+ }
1270
973
  }
1271
- }
1272
-
1273
- for (const dirtiedAsset of cssAssetsDirtiedByCustomProps) {
1274
- dirtiedAsset.markDirty();
1275
- }
1276
-
1277
- timings['inject subset font-family'] = injectPhase.end();
1278
-
1279
- const orphanCleanupPhase = trackPhase('source maps + orphan cleanup');
1280
- if (sourceMaps) {
1281
- await rewriteCssSourceMaps(assetGraph, hrefType);
1282
- }
1283
- removeOrphanedAssets(assetGraph, potentiallyOrphanedAssets);
1284
- timings['source maps + orphan cleanup'] = orphanCleanupPhase.end();
1285
-
1286
- return {
1287
- fontInfo: buildFontInfoReport(htmlOrSvgAssetTextsWithProps),
1288
- timings,
1289
- };
974
+ for (const dirtiedAsset of cssAssetsDirtiedByCustomProps) {
975
+ dirtiedAsset.markDirty();
976
+ }
977
+ timings['inject subset font-family'] = injectPhase.end();
978
+ const orphanCleanupPhase = trackPhase('source maps + orphan cleanup');
979
+ if (sourceMaps) {
980
+ await rewriteCssSourceMaps(assetGraph, hrefType);
981
+ }
982
+ removeOrphanedAssets(assetGraph, potentiallyOrphanedAssets);
983
+ timings['source maps + orphan cleanup'] = orphanCleanupPhase.end();
984
+ return {
985
+ fontInfo: buildFontInfoReport(htmlOrSvgAssetTextsWithProps),
986
+ timings,
987
+ };
1290
988
  }
1291
-
1292
989
  module.exports = subsetFonts;
990
+ //# sourceMappingURL=subsetFonts.js.map