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