@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.
- package/README.md +43 -43
- package/lib/FontTracerPool.d.ts +37 -0
- package/lib/FontTracerPool.d.ts.map +1 -0
- package/lib/FontTracerPool.js +212 -173
- package/lib/FontTracerPool.js.map +1 -0
- package/lib/HeadlessBrowser.js +11 -3
- package/lib/cli.d.ts +3 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +15 -12
- package/lib/cli.js.map +1 -0
- package/lib/collectTextsByPage.js +425 -352
- package/lib/escapeJsStringLiteral.js +13 -0
- package/lib/extractVisibleText.js +6 -2
- package/lib/fontConverter.js +25 -0
- package/lib/fontConverterWorker.js +16 -0
- package/lib/fontFaceHelpers.js +16 -4
- package/lib/gatherStylesheetsWithPredicates.js +4 -5
- package/lib/normalizeFontPropertyValue.js +1 -1
- package/lib/sfntCache.js +10 -7
- package/lib/subfont.d.ts +33 -0
- package/lib/subfont.d.ts.map +1 -0
- package/lib/subfont.js +533 -591
- package/lib/subfont.js.map +1 -0
- package/lib/subsetFontWithGlyphs.d.ts +17 -0
- package/lib/subsetFontWithGlyphs.d.ts.map +1 -0
- package/lib/subsetFontWithGlyphs.js +231 -253
- package/lib/subsetFontWithGlyphs.js.map +1 -0
- package/lib/subsetFonts.d.ts +59 -0
- package/lib/subsetFonts.d.ts.map +1 -0
- package/lib/subsetFonts.js +921 -1180
- package/lib/subsetFonts.js.map +1 -0
- package/lib/subsetGeneration.d.ts +39 -0
- package/lib/subsetGeneration.d.ts.map +1 -0
- package/lib/subsetGeneration.js +294 -324
- package/lib/subsetGeneration.js.map +1 -0
- package/lib/unquote.js +9 -4
- package/lib/warnAboutMissingGlyphs.js +36 -25
- package/lib/wasmQueue.js +6 -2
- package/package.json +11 -3
package/lib/subsetFonts.js
CHANGED
|
@@ -1,106 +1,101 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
128
|
+
truetype: '.ttf',
|
|
129
|
+
woff: '.woff',
|
|
130
|
+
woff2: '.woff2',
|
|
148
131
|
};
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
(
|
|
202
|
-
|
|
203
|
-
);
|
|
204
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
175
|
+
'auto',
|
|
176
|
+
'block',
|
|
177
|
+
'swap',
|
|
178
|
+
'fallback',
|
|
179
|
+
'optional',
|
|
224
180
|
];
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
326
|
-
function addSubsetFontPreloads({
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
427
|
-
if (hasGoogleFonts) {
|
|
288
|
+
if (!hasGoogleFonts)
|
|
289
|
+
return false;
|
|
428
290
|
await assetGraph.populate({
|
|
429
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
|
726
|
-
|
|
727
|
-
);
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
//
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
)
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
(
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
if (
|
|
1135
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
1159
|
-
|
|
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
|
-
|
|
1194
|
-
|
|
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
|
-
|
|
902
|
+
parseTreeToAsset.set(cssAsset.parseTree, cssAsset);
|
|
1197
903
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
|
1217
|
-
|
|
1218
|
-
hrefType: { $in: ['relative', 'inline'] },
|
|
1219
|
-
})) {
|
|
1220
|
-
relation.hrefType = hrefType;
|
|
974
|
+
for (const dirtiedAsset of cssAssetsDirtiedByCustomProps) {
|
|
975
|
+
dirtiedAsset.markDirty();
|
|
1221
976
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
1249
|
-
module.exports._escapeJsStringLiteral = escapeJsStringLiteral;
|
|
990
|
+
//# sourceMappingURL=subsetFonts.js.map
|