@turntrout/subfont 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/CLAUDE.md +53 -0
- package/LICENSE.md +7 -0
- package/README.md +93 -0
- package/lib/FontTracerPool.js +158 -0
- package/lib/HeadlessBrowser.js +223 -0
- package/lib/cli.js +14 -0
- package/lib/collectFeatureGlyphIds.js +137 -0
- package/lib/collectTextsByPage.js +1017 -0
- package/lib/extractReferencedCustomPropertyNames.js +20 -0
- package/lib/extractVisibleText.js +64 -0
- package/lib/findCustomPropertyDefinitions.js +54 -0
- package/lib/fontFaceHelpers.js +292 -0
- package/lib/fontTracerWorker.js +76 -0
- package/lib/gatherStylesheetsWithPredicates.js +87 -0
- package/lib/getCssRulesByProperty.js +343 -0
- package/lib/getFontInfo.js +36 -0
- package/lib/initialValueByProp.js +18 -0
- package/lib/injectSubsetDefinitions.js +65 -0
- package/lib/normalizeFontPropertyValue.js +34 -0
- package/lib/parseCommandLineOptions.js +131 -0
- package/lib/parseFontVariationSettings.js +39 -0
- package/lib/sfntCache.js +29 -0
- package/lib/stripLocalTokens.js +23 -0
- package/lib/subfont.js +571 -0
- package/lib/subsetFontWithGlyphs.js +193 -0
- package/lib/subsetFonts.js +1218 -0
- package/lib/subsetGeneration.js +347 -0
- package/lib/unicodeRange.js +38 -0
- package/lib/unquote.js +23 -0
- package/lib/variationAxes.js +162 -0
- package/lib/warnAboutMissingGlyphs.js +145 -0
- package/lib/wasmQueue.js +11 -0
- package/package.json +113 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const postcssValueParser = require('postcss-value-parser');
|
|
2
|
+
|
|
3
|
+
module.exports = function stripLocalTokens(cssValue) {
|
|
4
|
+
const rootNode = postcssValueParser(cssValue);
|
|
5
|
+
for (let i = 0; i < rootNode.nodes.length; i += 1) {
|
|
6
|
+
const node = rootNode.nodes[i];
|
|
7
|
+
if (node.type === 'function' && node.value.toLowerCase() === 'local') {
|
|
8
|
+
let numTokensToRemove = 1;
|
|
9
|
+
if (i + 1 < rootNode.nodes.length) {
|
|
10
|
+
const nextToken = rootNode.nodes[i + 1];
|
|
11
|
+
if (nextToken.type === 'div' && nextToken.value === ',') {
|
|
12
|
+
numTokensToRemove += 1;
|
|
13
|
+
if (i + 2 < rootNode.nodes.length) {
|
|
14
|
+
rootNode.nodes[i + 2].before = node.before;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
rootNode.nodes.splice(i, numTokensToRemove);
|
|
19
|
+
i -= 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return postcssValueParser.stringify(rootNode);
|
|
23
|
+
};
|
package/lib/subfont.js
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const pathModule = require('path');
|
|
3
|
+
const AssetGraph = require('assetgraph');
|
|
4
|
+
const prettyBytes = require('pretty-bytes');
|
|
5
|
+
const urlTools = require('urltools');
|
|
6
|
+
const util = require('util');
|
|
7
|
+
const subsetFonts = require('./subsetFonts');
|
|
8
|
+
|
|
9
|
+
module.exports = async function subfont(
|
|
10
|
+
{
|
|
11
|
+
root,
|
|
12
|
+
canonicalRoot,
|
|
13
|
+
output,
|
|
14
|
+
debug = false,
|
|
15
|
+
dryRun = false,
|
|
16
|
+
silent = false,
|
|
17
|
+
inlineCss = false,
|
|
18
|
+
fontDisplay = 'swap',
|
|
19
|
+
inPlace = false,
|
|
20
|
+
inputFiles = [],
|
|
21
|
+
recursive = false,
|
|
22
|
+
relativeUrls = false,
|
|
23
|
+
dynamic = false,
|
|
24
|
+
fallbacks = true,
|
|
25
|
+
text,
|
|
26
|
+
sourceMaps = false,
|
|
27
|
+
concurrency,
|
|
28
|
+
chromeFlags = [],
|
|
29
|
+
cache = false,
|
|
30
|
+
},
|
|
31
|
+
console
|
|
32
|
+
) {
|
|
33
|
+
const formats = ['woff2'];
|
|
34
|
+
|
|
35
|
+
function logToConsole(severity, ...args) {
|
|
36
|
+
if (!silent && console) {
|
|
37
|
+
console[severity](...args);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function log(...args) {
|
|
41
|
+
logToConsole('log', ...args);
|
|
42
|
+
}
|
|
43
|
+
function warn(...args) {
|
|
44
|
+
logToConsole('warn', ...args);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let rootUrl = root && urlTools.urlOrFsPathToUrl(root, true);
|
|
48
|
+
// Validate --root path exists early to give a clear error message
|
|
49
|
+
if (root && rootUrl && rootUrl.startsWith('file:')) {
|
|
50
|
+
const rootPath = urlTools.fileUrlToFsPath(rootUrl);
|
|
51
|
+
if (!fs.existsSync(rootPath)) {
|
|
52
|
+
throw new SyntaxError(`The --root path does not exist: ${rootPath}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const outRoot = output && urlTools.urlOrFsPathToUrl(output, true);
|
|
56
|
+
let inputUrls;
|
|
57
|
+
if (inputFiles.length > 0) {
|
|
58
|
+
inputUrls = inputFiles.map((urlOrFsPath) =>
|
|
59
|
+
urlTools.urlOrFsPathToUrl(String(urlOrFsPath), false)
|
|
60
|
+
);
|
|
61
|
+
if (!rootUrl) {
|
|
62
|
+
rootUrl = urlTools.findCommonUrlPrefix(inputUrls);
|
|
63
|
+
|
|
64
|
+
if (rootUrl) {
|
|
65
|
+
if (rootUrl.startsWith('file:')) {
|
|
66
|
+
warn(`Guessing --root from input files: ${rootUrl}`);
|
|
67
|
+
} else {
|
|
68
|
+
rootUrl = urlTools.ensureTrailingSlash(rootUrl);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} else if (rootUrl && rootUrl.startsWith('file:')) {
|
|
73
|
+
inputUrls = [`${rootUrl}**/*.html`];
|
|
74
|
+
warn(`No input files specified, defaulting to ${inputUrls[0]}`);
|
|
75
|
+
} else {
|
|
76
|
+
throw new SyntaxError(
|
|
77
|
+
"No input files and no --root specified (or it isn't file:), cannot proceed.\n"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!inputUrls[0].startsWith('file:') && !outRoot && !dryRun) {
|
|
82
|
+
throw new SyntaxError(
|
|
83
|
+
'--output has to be specified when using non-file input urls'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!inPlace && !outRoot && !dryRun) {
|
|
88
|
+
throw new SyntaxError(
|
|
89
|
+
'Either --output, --in-place, or --dry-run has to be specified'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const assetGraphConfig = {
|
|
94
|
+
root: rootUrl,
|
|
95
|
+
canonicalRoot,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (!rootUrl.startsWith('file:')) {
|
|
99
|
+
assetGraphConfig.canonicalRoot = rootUrl.replace(/\/?$/, '/'); // Ensure trailing slash
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Subfont only needs to follow CSS-related relations during populate.
|
|
103
|
+
const cssRelatedTypes = [
|
|
104
|
+
'HtmlStyle',
|
|
105
|
+
'SvgStyle',
|
|
106
|
+
'CssImport',
|
|
107
|
+
'CssFontFaceSrc',
|
|
108
|
+
'HttpRedirect',
|
|
109
|
+
'HtmlMetaRefresh',
|
|
110
|
+
'HtmlConditionalComment',
|
|
111
|
+
'HtmlNoscript',
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
let followRelationsQuery;
|
|
115
|
+
if (recursive) {
|
|
116
|
+
followRelationsQuery = {
|
|
117
|
+
$or: [
|
|
118
|
+
{
|
|
119
|
+
type: { $in: cssRelatedTypes },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: { $in: [...cssRelatedTypes, 'HtmlAnchor', 'SvgAnchor'] },
|
|
123
|
+
crossorigin: false,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
} else {
|
|
128
|
+
followRelationsQuery = {
|
|
129
|
+
type: { $in: cssRelatedTypes },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const assetGraph = new AssetGraph(assetGraphConfig);
|
|
133
|
+
|
|
134
|
+
function isExtensionlessEnoent(err) {
|
|
135
|
+
return (
|
|
136
|
+
err &&
|
|
137
|
+
err.code === 'ENOENT' &&
|
|
138
|
+
typeof err.path === 'string' &&
|
|
139
|
+
!/\.[^/]+$/.test(err.path)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (silent) {
|
|
144
|
+
assetGraph.on('warn', () => {});
|
|
145
|
+
} else {
|
|
146
|
+
const origEmit = assetGraph.emit;
|
|
147
|
+
assetGraph.emit = function (event, err, ...rest) {
|
|
148
|
+
if (event === 'warn' && isExtensionlessEnoent(err)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return origEmit.call(this, event, err, ...rest);
|
|
152
|
+
};
|
|
153
|
+
await assetGraph.logEvents({ console });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const outerTimings = {};
|
|
157
|
+
|
|
158
|
+
let phaseStart = Date.now();
|
|
159
|
+
await assetGraph.loadAssets(inputUrls);
|
|
160
|
+
outerTimings.loadAssets = Date.now() - phaseStart;
|
|
161
|
+
if (debug) log(`[subfont timing] loadAssets: ${outerTimings.loadAssets}ms`);
|
|
162
|
+
|
|
163
|
+
phaseStart = Date.now();
|
|
164
|
+
await assetGraph.populate({
|
|
165
|
+
followRelations: followRelationsQuery,
|
|
166
|
+
});
|
|
167
|
+
outerTimings['populate (initial)'] = Date.now() - phaseStart;
|
|
168
|
+
if (debug)
|
|
169
|
+
log(
|
|
170
|
+
`[subfont timing] populate (initial): ${outerTimings['populate (initial)']}ms`
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const entrypointAssets = assetGraph.findAssets({ isInitial: true });
|
|
174
|
+
const redirectOrigins = new Set();
|
|
175
|
+
for (const relation of assetGraph
|
|
176
|
+
.findRelations({ type: 'HttpRedirect' })
|
|
177
|
+
.sort((a, b) => a.id - b.id)) {
|
|
178
|
+
if (relation.from.isInitial) {
|
|
179
|
+
assetGraph.info(
|
|
180
|
+
new Error(`${relation.from.url} redirected to ${relation.to.url}`)
|
|
181
|
+
);
|
|
182
|
+
relation.to.isInitial = true;
|
|
183
|
+
relation.from.isInitial = false;
|
|
184
|
+
|
|
185
|
+
redirectOrigins.add(relation.to.origin);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
entrypointAssets.length === redirectOrigins.size &&
|
|
190
|
+
redirectOrigins.size === 1
|
|
191
|
+
) {
|
|
192
|
+
const newRoot = `${[...redirectOrigins][0]}/`;
|
|
193
|
+
if (newRoot !== assetGraph.root) {
|
|
194
|
+
assetGraph.info(
|
|
195
|
+
new Error(
|
|
196
|
+
`All entrypoints redirected, changing root from ${assetGraph.root} to ${newRoot}`
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
assetGraph.root = newRoot;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let sumSizesBefore = 0;
|
|
204
|
+
for (const asset of assetGraph.findAssets({
|
|
205
|
+
isInline: false,
|
|
206
|
+
isLoaded: true,
|
|
207
|
+
type: {
|
|
208
|
+
$in: ['Html', 'Svg', 'Css', 'JavaScript'],
|
|
209
|
+
},
|
|
210
|
+
})) {
|
|
211
|
+
sumSizesBefore += asset.rawSrc.length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!sourceMaps) {
|
|
215
|
+
log(
|
|
216
|
+
'Skipping CSS source map processing for faster execution. Use --source-maps to preserve them.'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
phaseStart = Date.now();
|
|
221
|
+
const { fontInfo, timings: subsetTimings } = await subsetFonts(assetGraph, {
|
|
222
|
+
inlineCss,
|
|
223
|
+
fontDisplay,
|
|
224
|
+
formats,
|
|
225
|
+
omitFallbacks: !fallbacks,
|
|
226
|
+
hrefType: relativeUrls ? 'relative' : 'rootRelative',
|
|
227
|
+
text,
|
|
228
|
+
dynamic,
|
|
229
|
+
console,
|
|
230
|
+
sourceMaps,
|
|
231
|
+
debug,
|
|
232
|
+
concurrency,
|
|
233
|
+
chromeArgs: chromeFlags,
|
|
234
|
+
cacheDir: (() => {
|
|
235
|
+
if (cache && typeof cache === 'string' && cache.length > 0) return cache;
|
|
236
|
+
if (cache && rootUrl && rootUrl.startsWith('file:'))
|
|
237
|
+
return pathModule.join(
|
|
238
|
+
urlTools.fileUrlToFsPath(rootUrl),
|
|
239
|
+
'.subfont-cache'
|
|
240
|
+
);
|
|
241
|
+
if (cache)
|
|
242
|
+
warn(
|
|
243
|
+
'--cache ignored: caching requires a local --root or an explicit cache path'
|
|
244
|
+
);
|
|
245
|
+
return null;
|
|
246
|
+
})(),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const subsetFontsTotal = Date.now() - phaseStart;
|
|
250
|
+
if (debug) log(`[subfont timing] subsetFonts total: ${subsetFontsTotal}ms`);
|
|
251
|
+
|
|
252
|
+
phaseStart = Date.now();
|
|
253
|
+
let sumSizesAfter = 0;
|
|
254
|
+
for (const asset of assetGraph.findAssets({
|
|
255
|
+
isInline: false,
|
|
256
|
+
isLoaded: true,
|
|
257
|
+
type: {
|
|
258
|
+
$in: ['Html', 'Svg', 'Css', 'JavaScript'],
|
|
259
|
+
},
|
|
260
|
+
})) {
|
|
261
|
+
sumSizesAfter += asset.rawSrc.length;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Omit function calls:
|
|
265
|
+
for (const relation of assetGraph.findRelations({
|
|
266
|
+
type: 'JavaScriptStaticUrl',
|
|
267
|
+
to: { isLoaded: true },
|
|
268
|
+
})) {
|
|
269
|
+
relation.omitFunctionCall();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const asset of assetGraph.findAssets({
|
|
273
|
+
isDirty: true,
|
|
274
|
+
isInline: false,
|
|
275
|
+
isLoaded: true,
|
|
276
|
+
type: 'Css',
|
|
277
|
+
})) {
|
|
278
|
+
if (!asset.url.startsWith(assetGraph.root)) {
|
|
279
|
+
assetGraph.info(
|
|
280
|
+
new Error(`Pulling down modified stylesheet ${asset.url}`)
|
|
281
|
+
);
|
|
282
|
+
asset.url = `${assetGraph.root}subfont/${
|
|
283
|
+
asset.baseName || 'index'
|
|
284
|
+
}-${asset.md5Hex.slice(0, 10)}${
|
|
285
|
+
asset.extension || asset.defaultExtension
|
|
286
|
+
}`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!rootUrl.startsWith('file:')) {
|
|
291
|
+
for (const relation of assetGraph.findRelations()) {
|
|
292
|
+
if (
|
|
293
|
+
relation.hrefType === 'protocolRelative' ||
|
|
294
|
+
relation.hrefType === 'absolute'
|
|
295
|
+
) {
|
|
296
|
+
relation.hrefType = 'rootRelative';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await assetGraph.moveAssets(
|
|
301
|
+
{
|
|
302
|
+
type: 'Html',
|
|
303
|
+
isLoaded: true,
|
|
304
|
+
isInline: false,
|
|
305
|
+
fileName: { $or: ['', undefined] },
|
|
306
|
+
},
|
|
307
|
+
(asset, assetGraph) =>
|
|
308
|
+
`${asset.url.replace(/\/?$/, '/')}index${asset.defaultExtension}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
outerTimings['post-subsetFonts processing'] = Date.now() - phaseStart;
|
|
313
|
+
if (debug)
|
|
314
|
+
log(
|
|
315
|
+
`[subfont timing] post-subsetFonts processing: ${outerTimings['post-subsetFonts processing']}ms`
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
phaseStart = Date.now();
|
|
319
|
+
if (!dryRun) {
|
|
320
|
+
await assetGraph.writeAssetsToDisc(
|
|
321
|
+
{
|
|
322
|
+
isLoaded: true,
|
|
323
|
+
isRedirect: { $ne: true },
|
|
324
|
+
url: (url) => url && url.startsWith(assetGraph.root),
|
|
325
|
+
},
|
|
326
|
+
outRoot,
|
|
327
|
+
assetGraph.root
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
outerTimings.writeAssetsToDisc = Date.now() - phaseStart;
|
|
332
|
+
if (debug)
|
|
333
|
+
log(
|
|
334
|
+
`[subfont timing] writeAssetsToDisc: ${outerTimings.writeAssetsToDisc}ms`
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
phaseStart = Date.now();
|
|
338
|
+
if (debug) {
|
|
339
|
+
const compactFontInfo = fontInfo.map(({ fontUsages, ...rest }) => ({
|
|
340
|
+
...rest,
|
|
341
|
+
fontUsages: fontUsages.map(({ codepoints, texts, ...fu }) => ({
|
|
342
|
+
...fu,
|
|
343
|
+
codepoints: codepoints
|
|
344
|
+
? {
|
|
345
|
+
original: `[${codepoints.original.length} codepoints]`,
|
|
346
|
+
used: `[${codepoints.used.length} codepoints]`,
|
|
347
|
+
unused: `[${codepoints.unused.length} codepoints]`,
|
|
348
|
+
page: `[${codepoints.page.length} codepoints]`,
|
|
349
|
+
}
|
|
350
|
+
: undefined,
|
|
351
|
+
texts: texts ? `[${texts.length} entries]` : undefined,
|
|
352
|
+
})),
|
|
353
|
+
}));
|
|
354
|
+
log(util.inspect(compactFontInfo, false, 99));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let totalSavings = sumSizesBefore - sumSizesAfter;
|
|
358
|
+
for (const { assetFileName, fontUsages } of fontInfo) {
|
|
359
|
+
let sumSmallestSubsetSize = 0;
|
|
360
|
+
let sumSmallestOriginalSize = 0;
|
|
361
|
+
let maxUsedCodePoints = 0;
|
|
362
|
+
let maxOriginalCodePoints = 0;
|
|
363
|
+
for (const fontUsage of fontUsages) {
|
|
364
|
+
sumSmallestSubsetSize += fontUsage.smallestSubsetSize || 0;
|
|
365
|
+
sumSmallestOriginalSize += fontUsage.smallestOriginalSize;
|
|
366
|
+
maxUsedCodePoints = Math.max(
|
|
367
|
+
fontUsage.codepoints.used.length,
|
|
368
|
+
maxUsedCodePoints
|
|
369
|
+
);
|
|
370
|
+
maxOriginalCodePoints = Math.max(
|
|
371
|
+
fontUsage.codepoints.original.length,
|
|
372
|
+
maxOriginalCodePoints
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const fontUsagesByFontFamily = {};
|
|
376
|
+
for (const fontUsage of fontUsages) {
|
|
377
|
+
const key = fontUsage.props['font-family'];
|
|
378
|
+
if (!fontUsagesByFontFamily[key]) fontUsagesByFontFamily[key] = [];
|
|
379
|
+
fontUsagesByFontFamily[key].push(fontUsage);
|
|
380
|
+
}
|
|
381
|
+
const numFonts = Object.keys(fontUsagesByFontFamily).length;
|
|
382
|
+
log(
|
|
383
|
+
`${assetFileName}: ${numFonts} font${numFonts === 1 ? '' : 's'} (${
|
|
384
|
+
fontUsages.length
|
|
385
|
+
} variant${fontUsages.length === 1 ? '' : 's'}) in use, ${prettyBytes(
|
|
386
|
+
sumSmallestOriginalSize
|
|
387
|
+
)} total. Created subsets: ${prettyBytes(sumSmallestSubsetSize)} total`
|
|
388
|
+
);
|
|
389
|
+
for (const fontFamily of Object.keys(fontUsagesByFontFamily).sort()) {
|
|
390
|
+
log(` ${fontFamily}:`);
|
|
391
|
+
for (const fontUsage of fontUsagesByFontFamily[fontFamily]) {
|
|
392
|
+
const variantShortName = `${fontUsage.props['font-weight']}${
|
|
393
|
+
fontUsage.props['font-style'] === 'italic' ? 'i' : ' '
|
|
394
|
+
}`;
|
|
395
|
+
let status = ` ${variantShortName}: ${String(
|
|
396
|
+
fontUsage.codepoints.used.length
|
|
397
|
+
).padStart(String(maxUsedCodePoints).length)}/${String(
|
|
398
|
+
fontUsage.codepoints.original.length
|
|
399
|
+
).padStart(String(maxOriginalCodePoints).length)} codepoints used`;
|
|
400
|
+
if (
|
|
401
|
+
fontUsage.codepoints.page.length !== fontUsage.codepoints.used.length
|
|
402
|
+
) {
|
|
403
|
+
status += ` (${fontUsage.codepoints.page.length} on this page)`;
|
|
404
|
+
}
|
|
405
|
+
if (
|
|
406
|
+
fontUsage.smallestOriginalSize !== undefined &&
|
|
407
|
+
fontUsage.smallestSubsetSize !== undefined
|
|
408
|
+
) {
|
|
409
|
+
if (fontUsage.fullyInstanced) {
|
|
410
|
+
status += ', fully instanced';
|
|
411
|
+
} else if (fontUsage.numAxesReduced > 0 || fontUsage.numAxesPinned) {
|
|
412
|
+
const instancingInfos = [];
|
|
413
|
+
if (fontUsage.numAxesPinned > 0) {
|
|
414
|
+
instancingInfos.push(
|
|
415
|
+
`${fontUsage.numAxesPinned} ${
|
|
416
|
+
fontUsage.numAxesPinned === 1 ? 'axis' : 'axes'
|
|
417
|
+
} pinned`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (fontUsage.numAxesReduced) {
|
|
421
|
+
instancingInfos.push(
|
|
422
|
+
`${fontUsage.numAxesReduced}${
|
|
423
|
+
fontUsage.numAxesPinned > 0
|
|
424
|
+
? ''
|
|
425
|
+
: fontUsage.numAxesReduced === 1
|
|
426
|
+
? ' axis'
|
|
427
|
+
: ' axes'
|
|
428
|
+
} reduced`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
status += `, partially instanced (${instancingInfos.join(', ')})`;
|
|
433
|
+
}
|
|
434
|
+
status += `, ${prettyBytes(fontUsage.smallestOriginalSize)} (${
|
|
435
|
+
fontUsage.smallestOriginalFormat
|
|
436
|
+
}) => ${prettyBytes(fontUsage.smallestSubsetSize)} (${
|
|
437
|
+
fontUsage.smallestSubsetFormat
|
|
438
|
+
})`;
|
|
439
|
+
totalSavings +=
|
|
440
|
+
fontUsage.smallestOriginalSize - fontUsage.smallestSubsetSize;
|
|
441
|
+
} else {
|
|
442
|
+
status += ', no subset font created';
|
|
443
|
+
}
|
|
444
|
+
log(status);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
log(
|
|
449
|
+
`HTML/SVG/JS/CSS size increase: ${prettyBytes(
|
|
450
|
+
sumSizesAfter - sumSizesBefore
|
|
451
|
+
)}`
|
|
452
|
+
);
|
|
453
|
+
log(`Total savings: ${prettyBytes(totalSavings)}`);
|
|
454
|
+
outerTimings['output reporting'] = Date.now() - phaseStart;
|
|
455
|
+
if (debug)
|
|
456
|
+
log(
|
|
457
|
+
`[subfont timing] output reporting: ${outerTimings['output reporting']}ms`
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const st = subsetTimings || {};
|
|
461
|
+
const details = st.collectTextsByPageDetails || {};
|
|
462
|
+
const totalElapsed =
|
|
463
|
+
(outerTimings.loadAssets || 0) +
|
|
464
|
+
(outerTimings['populate (initial)'] || 0) +
|
|
465
|
+
subsetFontsTotal +
|
|
466
|
+
(outerTimings['post-subsetFonts processing'] || 0) +
|
|
467
|
+
(outerTimings.writeAssetsToDisc || 0) +
|
|
468
|
+
(outerTimings['output reporting'] || 0);
|
|
469
|
+
|
|
470
|
+
const rows = [
|
|
471
|
+
['loadAssets', outerTimings.loadAssets, 0],
|
|
472
|
+
['populate (initial)', outerTimings['populate (initial)'], 0],
|
|
473
|
+
['subsetFonts total', subsetFontsTotal, 0],
|
|
474
|
+
['collectTextsByPage', st.collectTextsByPage, 1],
|
|
475
|
+
['Stylesheet precompute', details['Stylesheet precompute'], 2],
|
|
476
|
+
['Full tracing', details['Full tracing'], 2],
|
|
477
|
+
['Fast-path extraction', details['Fast-path extraction'], 2],
|
|
478
|
+
['Per-page loop', details['Per-page loop'], 2],
|
|
479
|
+
['Post-processing', details['Post-processing total'], 2],
|
|
480
|
+
['codepoint generation', st['codepoint generation'], 1],
|
|
481
|
+
['getSubsetsForFontUsage', st.getSubsetsForFontUsage, 1],
|
|
482
|
+
['insert subsets loop', st['insert subsets loop'], 1],
|
|
483
|
+
['inject font-family', st['inject subset font-family'], 1],
|
|
484
|
+
['post-subsetFonts', outerTimings['post-subsetFonts processing'], 0],
|
|
485
|
+
['writeAssetsToDisc', outerTimings.writeAssetsToDisc, 0],
|
|
486
|
+
['output reporting', outerTimings['output reporting'], 0],
|
|
487
|
+
];
|
|
488
|
+
|
|
489
|
+
log('\n═══ Subfont Timing Summary ═══');
|
|
490
|
+
for (const [label, ms, indent] of rows) {
|
|
491
|
+
if (ms === undefined) continue;
|
|
492
|
+
const prefix = ' '.repeat(indent + 1);
|
|
493
|
+
const padded = (ms || 0).toLocaleString().padStart(8);
|
|
494
|
+
log(`${prefix}${label}: ${padded}ms`);
|
|
495
|
+
}
|
|
496
|
+
log(' ─────────────────────────────────');
|
|
497
|
+
log(` Total: ${totalElapsed.toLocaleString().padStart(8)}ms`);
|
|
498
|
+
log('═══════════════════════════════\n');
|
|
499
|
+
|
|
500
|
+
if (dryRun) {
|
|
501
|
+
log('\n═══ Dry Run Preview ═══');
|
|
502
|
+
const assetsToWrite = assetGraph.findAssets({
|
|
503
|
+
isLoaded: true,
|
|
504
|
+
isRedirect: { $ne: true },
|
|
505
|
+
url: (url) => url && url.startsWith(assetGraph.root),
|
|
506
|
+
});
|
|
507
|
+
const byType = {};
|
|
508
|
+
let totalOutputSize = 0;
|
|
509
|
+
for (const asset of assetsToWrite) {
|
|
510
|
+
const type = asset.type || 'Other';
|
|
511
|
+
if (!byType[type]) byType[type] = { count: 0, size: 0, files: [] };
|
|
512
|
+
const size = asset.rawSrc ? asset.rawSrc.length : 0;
|
|
513
|
+
byType[type].count += 1;
|
|
514
|
+
byType[type].size += size;
|
|
515
|
+
totalOutputSize += size;
|
|
516
|
+
|
|
517
|
+
if (asset.url && asset.url.includes('/subfont/')) {
|
|
518
|
+
byType[type].files.push(
|
|
519
|
+
` ${asset.url.replace(assetGraph.root, '/')} (${prettyBytes(size)})`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const [type, info] of Object.entries(byType).sort(
|
|
524
|
+
([, a], [, b]) => b.size - a.size
|
|
525
|
+
)) {
|
|
526
|
+
log(
|
|
527
|
+
` ${type}: ${info.count} file${info.count === 1 ? '' : 's'}, ${prettyBytes(info.size)}`
|
|
528
|
+
);
|
|
529
|
+
for (const file of info.files) {
|
|
530
|
+
log(file);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
log(` ─────────────────────────────────`);
|
|
534
|
+
log(` Total output: ${prettyBytes(totalOutputSize)}`);
|
|
535
|
+
|
|
536
|
+
const dirtyHtmlAssets = assetGraph.findAssets({
|
|
537
|
+
isDirty: true,
|
|
538
|
+
isLoaded: true,
|
|
539
|
+
type: { $in: ['Html', 'Svg'] },
|
|
540
|
+
});
|
|
541
|
+
if (dirtyHtmlAssets.length > 0) {
|
|
542
|
+
log(`\n Modified HTML/SVG files (${dirtyHtmlAssets.length}):`);
|
|
543
|
+
for (const asset of dirtyHtmlAssets) {
|
|
544
|
+
log(` ${asset.urlOrDescription}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const subsetCssAssets = assetGraph.findAssets({
|
|
549
|
+
type: 'Css',
|
|
550
|
+
isLoaded: true,
|
|
551
|
+
url: (url) => url && url.includes('/subfont/'),
|
|
552
|
+
});
|
|
553
|
+
if (subsetCssAssets.length > 0) {
|
|
554
|
+
log(
|
|
555
|
+
`\n Subset CSS files that would be created (${subsetCssAssets.length}):`
|
|
556
|
+
);
|
|
557
|
+
for (const css of subsetCssAssets) {
|
|
558
|
+
const fontFaceCount = (css.text.match(/@font-face/g) || []).length;
|
|
559
|
+
log(
|
|
560
|
+
` ${css.url.replace(assetGraph.root, '/')} (${prettyBytes(css.rawSrc.length)}, ${fontFaceCount} @font-face rule${fontFaceCount === 1 ? '' : 's'})`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
log('═══════════════════════════════\n');
|
|
566
|
+
log('Dry run complete — no files were written.');
|
|
567
|
+
} else {
|
|
568
|
+
log('Output written to', outRoot || assetGraph.root);
|
|
569
|
+
}
|
|
570
|
+
return assetGraph;
|
|
571
|
+
};
|