@turntrout/subfont 1.10.2 → 1.10.3
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/lib/HeadlessBrowser.d.ts +2 -0
- package/lib/HeadlessBrowser.d.ts.map +1 -1
- package/lib/HeadlessBrowser.js +64 -58
- package/lib/HeadlessBrowser.js.map +1 -1
- package/lib/collectTextsByPage.d.ts.map +1 -1
- package/lib/collectTextsByPage.js +136 -132
- package/lib/collectTextsByPage.js.map +1 -1
- package/lib/getCssRulesByProperty.d.ts.map +1 -1
- package/lib/getCssRulesByProperty.js +234 -207
- package/lib/getCssRulesByProperty.js.map +1 -1
- package/lib/parseCommandLineOptions.d.ts.map +1 -1
- package/lib/parseCommandLineOptions.js +29 -14
- package/lib/parseCommandLineOptions.js.map +1 -1
- package/lib/subfont.d.ts.map +1 -1
- package/lib/subfont.js +347 -311
- package/lib/subfont.js.map +1 -1
- package/lib/subsetFontWithGlyphs.d.ts.map +1 -1
- package/lib/subsetFontWithGlyphs.js +60 -48
- package/lib/subsetFontWithGlyphs.js.map +1 -1
- package/lib/subsetFonts.d.ts.map +1 -1
- package/lib/subsetFonts.js +338 -280
- package/lib/subsetFonts.js.map +1 -1
- package/lib/subsetGeneration.d.ts.map +1 -1
- package/lib/subsetGeneration.js +151 -127
- package/lib/subsetGeneration.js.map +1 -1
- package/lib/warnAboutMissingGlyphs.d.ts.map +1 -1
- package/lib/warnAboutMissingGlyphs.js +132 -112
- package/lib/warnAboutMissingGlyphs.js.map +1 -1
- package/package.json +1 -1
package/lib/subfont.js
CHANGED
|
@@ -49,21 +49,7 @@ class UsageError extends Error {
|
|
|
49
49
|
this.name = 'UsageError';
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
if (concurrency !== undefined &&
|
|
54
|
-
(!Number.isInteger(concurrency) || concurrency < 1)) {
|
|
55
|
-
throw new UsageError('--concurrency must be a positive integer');
|
|
56
|
-
}
|
|
57
|
-
// Prevent postcss plugins (colormin, convert-values, etc.) invoked by
|
|
58
|
-
// cssnano from walking the filesystem for a "browserslist" config.
|
|
59
|
-
// Under pnpm, `node_modules/.bin/browserslist` is a shell shim that
|
|
60
|
-
// browserslist mis-parses as browser queries, throwing
|
|
61
|
-
// BrowserslistError and silently aborting CSS minification.
|
|
62
|
-
// Setting BROWSERSLIST short-circuits the walk entirely.
|
|
63
|
-
if (!process.env.BROWSERSLIST && !process.env.BROWSERSLIST_CONFIG) {
|
|
64
|
-
process.env.BROWSERSLIST = 'defaults';
|
|
65
|
-
}
|
|
66
|
-
const formats = ['woff2'];
|
|
52
|
+
function makeLogger(silent, console) {
|
|
67
53
|
// Variadic console-style helpers: console.log / .warn accept any argument.
|
|
68
54
|
/* eslint-disable no-restricted-syntax */
|
|
69
55
|
function logToConsole(severity, ...args) {
|
|
@@ -71,20 +57,15 @@ const subfont = async function subfont({ root, canonicalRoot, output, debug = fa
|
|
|
71
57
|
console[severity](...args);
|
|
72
58
|
}
|
|
73
59
|
}
|
|
74
|
-
|
|
75
|
-
logToConsole('log', ...args)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
logToConsole('warn', ...args);
|
|
79
|
-
}
|
|
60
|
+
return {
|
|
61
|
+
log: (...args) => logToConsole('log', ...args),
|
|
62
|
+
warn: (...args) => logToConsole('warn', ...args),
|
|
63
|
+
};
|
|
80
64
|
/* eslint-enable no-restricted-syntax */
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
warn(`--concurrency ${concurrency} exceeds estimated safe limit of ${maxConcurrency} (each worker uses ~50 MB; ${Math.round(os.freemem() / (1024 * 1024 * 1024))} GB free, ${os.cpus().length} CPUs). Proceeding anyway — reduce if you hit OOM.`);
|
|
84
|
-
}
|
|
85
|
-
let rootUrl = root && urlTools.urlOrFsPathToUrl(root, true);
|
|
65
|
+
}
|
|
66
|
+
async function resolveInputUrls(inputFiles, rootUrl, warn) {
|
|
86
67
|
// Validate --root path exists early to give a clear error message
|
|
87
|
-
if (
|
|
68
|
+
if (rootUrl && rootUrl.startsWith('file:')) {
|
|
88
69
|
const rootPath = urlTools.fileUrlToFsPath(rootUrl);
|
|
89
70
|
try {
|
|
90
71
|
await fsPromises.access(rootPath);
|
|
@@ -93,7 +74,6 @@ const subfont = async function subfont({ root, canonicalRoot, output, debug = fa
|
|
|
93
74
|
throw new UsageError(`The --root path does not exist: ${rootPath}`);
|
|
94
75
|
}
|
|
95
76
|
}
|
|
96
|
-
const outRoot = output && urlTools.urlOrFsPathToUrl(output, true);
|
|
97
77
|
let inputUrls;
|
|
98
78
|
if (inputFiles.length > 0) {
|
|
99
79
|
inputUrls = inputFiles.map((urlOrFsPath) => urlTools.urlOrFsPathToUrl(String(urlOrFsPath), false));
|
|
@@ -116,61 +96,45 @@ const subfont = async function subfont({ root, canonicalRoot, output, debug = fa
|
|
|
116
96
|
else {
|
|
117
97
|
throw new UsageError("No input files and no --root specified (or it isn't file:), cannot proceed.\n");
|
|
118
98
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
99
|
+
return { inputUrls, rootUrl };
|
|
100
|
+
}
|
|
101
|
+
// Subfont only needs to follow CSS-related relations during populate.
|
|
102
|
+
const cssRelatedTypes = [
|
|
103
|
+
'HtmlStyle',
|
|
104
|
+
'SvgStyle',
|
|
105
|
+
'CssImport',
|
|
106
|
+
'CssFontFaceSrc',
|
|
107
|
+
'HttpRedirect',
|
|
108
|
+
'HtmlMetaRefresh',
|
|
109
|
+
'HtmlConditionalComment',
|
|
110
|
+
'HtmlNoscript',
|
|
111
|
+
];
|
|
112
|
+
function buildFollowRelationsQuery(recursive) {
|
|
113
|
+
if (!recursive) {
|
|
114
|
+
return { type: { $in: cssRelatedTypes } };
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
$or: [
|
|
118
|
+
{ type: { $in: cssRelatedTypes } },
|
|
119
|
+
{
|
|
120
|
+
type: { $in: [...cssRelatedTypes, 'HtmlAnchor', 'SvgAnchor'] },
|
|
121
|
+
crossorigin: false,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
128
124
|
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
'SvgStyle',
|
|
136
|
-
'CssImport',
|
|
137
|
-
'CssFontFaceSrc',
|
|
138
|
-
'HttpRedirect',
|
|
139
|
-
'HtmlMetaRefresh',
|
|
140
|
-
'HtmlConditionalComment',
|
|
141
|
-
'HtmlNoscript',
|
|
142
|
-
];
|
|
143
|
-
let followRelationsQuery;
|
|
144
|
-
if (recursive) {
|
|
145
|
-
followRelationsQuery = {
|
|
146
|
-
$or: [
|
|
147
|
-
{
|
|
148
|
-
type: { $in: cssRelatedTypes },
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
type: { $in: [...cssRelatedTypes, 'HtmlAnchor', 'SvgAnchor'] },
|
|
152
|
-
crossorigin: false,
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
followRelationsQuery = {
|
|
159
|
-
type: { $in: cssRelatedTypes },
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
const assetGraph = new AssetGraph(assetGraphConfig);
|
|
163
|
-
// Catch-clause idiom: error values are `unknown` until narrowed.
|
|
125
|
+
}
|
|
126
|
+
// Catch-clause idiom: error values are `unknown` until narrowed.
|
|
127
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
128
|
+
function isExtensionlessEnoent(err) {
|
|
129
|
+
if (typeof err !== 'object' || err === null)
|
|
130
|
+
return false;
|
|
164
131
|
// eslint-disable-next-line no-restricted-syntax
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
typeof e.path === 'string' &&
|
|
172
|
-
!/\.[^/]+$/.test(e.path));
|
|
173
|
-
}
|
|
132
|
+
const e = err;
|
|
133
|
+
return (e.code === 'ENOENT' &&
|
|
134
|
+
typeof e.path === 'string' &&
|
|
135
|
+
!/\.[^/]+$/.test(e.path));
|
|
136
|
+
}
|
|
137
|
+
async function installWarningHandlers(assetGraph, silent, strict, console) {
|
|
174
138
|
let sawWarning = false;
|
|
175
139
|
const origEmit = assetGraph.emit;
|
|
176
140
|
// EventEmitter.emit forwards arbitrary varargs.
|
|
@@ -189,28 +153,18 @@ const subfont = async function subfont({ root, canonicalRoot, output, debug = fa
|
|
|
189
153
|
else {
|
|
190
154
|
await assetGraph.logEvents({ console, stopOnWarning: strict });
|
|
191
155
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// the same way it suppresses other subfont output.
|
|
196
|
-
const trackPhase = (0, progress_1.makePhaseTracker)({ log }, debug);
|
|
197
|
-
const loadAssetsPhase = trackPhase('loadAssets');
|
|
198
|
-
await assetGraph.loadAssets(inputUrls);
|
|
199
|
-
outerTimings.loadAssets = loadAssetsPhase.end();
|
|
200
|
-
const populatePhase = trackPhase('populate (initial)');
|
|
201
|
-
await assetGraph.populate({
|
|
202
|
-
followRelations: followRelationsQuery,
|
|
203
|
-
});
|
|
204
|
-
outerTimings['populate (initial)'] = populatePhase.end();
|
|
156
|
+
return () => sawWarning;
|
|
157
|
+
}
|
|
158
|
+
function handleInitialRedirects(assetGraph) {
|
|
205
159
|
const entrypointAssets = assetGraph.findAssets({ isInitial: true });
|
|
206
160
|
const redirectOrigins = new Set();
|
|
207
161
|
for (const relation of assetGraph.findRelations({ type: 'HttpRedirect' }).sort((a, b) => a.id - b.id)) {
|
|
208
|
-
if (relation.from.isInitial)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
162
|
+
if (!relation.from.isInitial)
|
|
163
|
+
continue;
|
|
164
|
+
assetGraph.info(new Error(`${relation.from.url} redirected to ${relation.to.url}`));
|
|
165
|
+
relation.to.isInitial = true;
|
|
166
|
+
relation.from.isInitial = false;
|
|
167
|
+
redirectOrigins.add(relation.to.origin);
|
|
214
168
|
}
|
|
215
169
|
if (entrypointAssets.length === redirectOrigins.size &&
|
|
216
170
|
redirectOrigins.size === 1) {
|
|
@@ -220,53 +174,21 @@ const subfont = async function subfont({ root, canonicalRoot, output, debug = fa
|
|
|
220
174
|
assetGraph.root = newRoot;
|
|
221
175
|
}
|
|
222
176
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
isLoaded: true,
|
|
226
|
-
type: {
|
|
227
|
-
$in: ['Html', 'Svg', 'Css', 'JavaScript'],
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
let sumSizesBefore = 0;
|
|
231
|
-
for (const asset of assetGraph.findAssets(sizeableAssetQuery)) {
|
|
232
|
-
sumSizesBefore += asset.rawSrc.length;
|
|
233
|
-
}
|
|
234
|
-
if (!sourceMaps) {
|
|
235
|
-
log('Skipping CSS source map processing for faster execution. Use --source-maps to preserve them.');
|
|
236
|
-
}
|
|
237
|
-
let cacheDir = null;
|
|
177
|
+
}
|
|
178
|
+
function resolveCacheDir(cache, rootUrl, warn) {
|
|
238
179
|
if (cache && typeof cache === 'string' && cache.length > 0) {
|
|
239
|
-
|
|
180
|
+
return cache;
|
|
240
181
|
}
|
|
241
|
-
|
|
242
|
-
|
|
182
|
+
if (cache && rootUrl && rootUrl.startsWith('file:')) {
|
|
183
|
+
return pathModule.join(urlTools.fileUrlToFsPath(rootUrl), '.subfont-cache');
|
|
243
184
|
}
|
|
244
|
-
|
|
185
|
+
if (cache) {
|
|
245
186
|
warn('--cache ignored: caching requires a local --root or an explicit cache path');
|
|
246
187
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
formats,
|
|
252
|
-
omitFallbacks: !fallbacks,
|
|
253
|
-
hrefType: relativeUrls ? 'relative' : 'rootRelative',
|
|
254
|
-
text,
|
|
255
|
-
dynamic,
|
|
256
|
-
console,
|
|
257
|
-
sourceMaps,
|
|
258
|
-
debug,
|
|
259
|
-
concurrency,
|
|
260
|
-
chromeArgs: chromeFlags,
|
|
261
|
-
cacheDir,
|
|
262
|
-
});
|
|
263
|
-
const subsetFontsTotal = subsetPhase.end();
|
|
264
|
-
const postProcessingPhase = trackPhase('post-subsetFonts processing');
|
|
265
|
-
let sumSizesAfter = 0;
|
|
266
|
-
for (const asset of assetGraph.findAssets(sizeableAssetQuery)) {
|
|
267
|
-
sumSizesAfter += asset.rawSrc.length;
|
|
268
|
-
}
|
|
269
|
-
// Omit function calls:
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
async function runPostProcessing(assetGraph, rootUrl) {
|
|
191
|
+
// Omit function calls
|
|
270
192
|
for (const relation of assetGraph.findRelations({
|
|
271
193
|
type: 'JavaScriptStaticUrl',
|
|
272
194
|
to: { isLoaded: true },
|
|
@@ -279,11 +201,11 @@ const subfont = async function subfont({ root, canonicalRoot, output, debug = fa
|
|
|
279
201
|
isLoaded: true,
|
|
280
202
|
type: 'Css',
|
|
281
203
|
})) {
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
204
|
+
if (asset.url.startsWith(assetGraph.root))
|
|
205
|
+
continue;
|
|
206
|
+
assetGraph.info(new Error(`Pulling down modified stylesheet ${asset.url}`));
|
|
207
|
+
const safeName = sanitizeFilename(asset.baseName || '', { replacement: '_' }) || 'index';
|
|
208
|
+
asset.url = `${assetGraph.root}subfont/${safeName}-${asset.md5Hex.slice(0, 10)}${asset.extension || asset.defaultExtension}`;
|
|
287
209
|
}
|
|
288
210
|
if (rootUrl && !rootUrl.startsWith('file:')) {
|
|
289
211
|
for (const relation of assetGraph.findRelations({
|
|
@@ -298,78 +220,98 @@ const subfont = async function subfont({ root, canonicalRoot, output, debug = fa
|
|
|
298
220
|
fileName: { $or: ['', undefined] },
|
|
299
221
|
}, (asset) => `${asset.url.replace(/\/?$/, '/')}index${asset.defaultExtension}`);
|
|
300
222
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
fu.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
fullyInstanced: fu.fullyInstanced,
|
|
342
|
-
numAxesPinned: fu.numAxesPinned,
|
|
343
|
-
numAxesReduced: fu.numAxesReduced,
|
|
344
|
-
smallestOriginalFormat: fu.smallestOriginalFormat,
|
|
345
|
-
smallestSubsetFormat: fu.smallestSubsetFormat,
|
|
346
|
-
smallestOriginalSize: fu.smallestOriginalSize,
|
|
347
|
-
smallestSubsetSize: fu.smallestSubsetSize,
|
|
348
|
-
codepoints: {
|
|
349
|
-
original: fu.codepoints.original.length,
|
|
350
|
-
used: fu.codepoints.used.length,
|
|
351
|
-
unused: fu.codepoints.unused.length,
|
|
352
|
-
},
|
|
353
|
-
pageCount: 0,
|
|
354
|
-
samplePages: [],
|
|
355
|
-
};
|
|
356
|
-
byVariant.set(key, entry);
|
|
357
|
-
}
|
|
358
|
-
entry.pageCount += 1;
|
|
359
|
-
if (entry.samplePages.length < SAMPLE_PAGES) {
|
|
360
|
-
entry.samplePages.push(assetFileName);
|
|
361
|
-
}
|
|
223
|
+
}
|
|
224
|
+
// One entry per unique (fontUrl, props) variant. A variable-font URL can
|
|
225
|
+
// back multiple variants, so fontUrl alone is too coarse. Codepoint unions
|
|
226
|
+
// and subset sizes are per-font, so the remaining per-page variation worth
|
|
227
|
+
// surfacing is just which pages reference the variant.
|
|
228
|
+
function printVariantSummary(fontInfo, log) {
|
|
229
|
+
const SAMPLE_PAGES = 5;
|
|
230
|
+
const byVariant = new Map();
|
|
231
|
+
for (const { assetFileName, fontUsages } of fontInfo) {
|
|
232
|
+
for (const fu of fontUsages) {
|
|
233
|
+
const p = fu.props || {};
|
|
234
|
+
const key = [
|
|
235
|
+
fu.fontUrl || '[inline]',
|
|
236
|
+
p['font-family'],
|
|
237
|
+
p['font-weight'],
|
|
238
|
+
p['font-style'],
|
|
239
|
+
p['font-stretch'],
|
|
240
|
+
].join('\0');
|
|
241
|
+
let entry = byVariant.get(key);
|
|
242
|
+
if (!entry) {
|
|
243
|
+
entry = {
|
|
244
|
+
fontUrl: fu.fontUrl,
|
|
245
|
+
props: fu.props,
|
|
246
|
+
preload: fu.preload,
|
|
247
|
+
fullyInstanced: fu.fullyInstanced,
|
|
248
|
+
numAxesPinned: fu.numAxesPinned,
|
|
249
|
+
numAxesReduced: fu.numAxesReduced,
|
|
250
|
+
smallestOriginalFormat: fu.smallestOriginalFormat,
|
|
251
|
+
smallestSubsetFormat: fu.smallestSubsetFormat,
|
|
252
|
+
smallestOriginalSize: fu.smallestOriginalSize,
|
|
253
|
+
smallestSubsetSize: fu.smallestSubsetSize,
|
|
254
|
+
codepoints: {
|
|
255
|
+
original: fu.codepoints.original.length,
|
|
256
|
+
used: fu.codepoints.used.length,
|
|
257
|
+
unused: fu.codepoints.unused.length,
|
|
258
|
+
},
|
|
259
|
+
pageCount: 0,
|
|
260
|
+
samplePages: [],
|
|
261
|
+
};
|
|
262
|
+
byVariant.set(key, entry);
|
|
362
263
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (remaining > 0) {
|
|
367
|
-
entry.samplePages.push(`...and ${remaining} more`);
|
|
264
|
+
entry.pageCount += 1;
|
|
265
|
+
if (entry.samplePages.length < SAMPLE_PAGES) {
|
|
266
|
+
entry.samplePages.push(assetFileName);
|
|
368
267
|
}
|
|
369
268
|
}
|
|
370
|
-
log(`Font variants (aggregated across ${fontInfo.length} page${fontInfo.length === 1 ? '' : 's'}):`);
|
|
371
|
-
log(util.inspect([...byVariant.values()], false, 99));
|
|
372
269
|
}
|
|
270
|
+
for (const entry of byVariant.values()) {
|
|
271
|
+
const remaining = entry.pageCount - entry.samplePages.length;
|
|
272
|
+
if (remaining > 0) {
|
|
273
|
+
entry.samplePages.push(`...and ${remaining} more`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
log(`Font variants (aggregated across ${fontInfo.length} page${fontInfo.length === 1 ? '' : 's'}):`);
|
|
277
|
+
log(util.inspect([...byVariant.values()], false, 99));
|
|
278
|
+
}
|
|
279
|
+
function buildInstancingSuffix(fontUsage) {
|
|
280
|
+
const numAxesReduced = fontUsage.numAxesReduced ?? 0;
|
|
281
|
+
const numAxesPinned = fontUsage.numAxesPinned ?? 0;
|
|
282
|
+
if (fontUsage.fullyInstanced) {
|
|
283
|
+
return ', fully instanced';
|
|
284
|
+
}
|
|
285
|
+
if (numAxesReduced === 0 && numAxesPinned === 0) {
|
|
286
|
+
return '';
|
|
287
|
+
}
|
|
288
|
+
const instancingInfos = [];
|
|
289
|
+
if (numAxesPinned > 0) {
|
|
290
|
+
instancingInfos.push(`${numAxesPinned} ${numAxesPinned === 1 ? 'axis' : 'axes'} pinned`);
|
|
291
|
+
}
|
|
292
|
+
if (numAxesReduced) {
|
|
293
|
+
instancingInfos.push(`${numAxesReduced}${numAxesPinned > 0 ? '' : numAxesReduced === 1 ? ' axis' : ' axes'} reduced`);
|
|
294
|
+
}
|
|
295
|
+
return `, partially instanced (${instancingInfos.join(', ')})`;
|
|
296
|
+
}
|
|
297
|
+
function describeFontUsageStatus(fontUsage, usedPad, originalPad) {
|
|
298
|
+
const variantShortName = `${fontUsage.props['font-weight']}${fontUsage.props['font-style'] === 'italic' ? 'i' : ' '}`;
|
|
299
|
+
let status = ` ${variantShortName}: ${String(fontUsage.codepoints.used.length).padStart(usedPad)}/${String(fontUsage.codepoints.original.length).padStart(originalPad)} codepoints used`;
|
|
300
|
+
if (fontUsage.codepoints.page.length !== fontUsage.codepoints.used.length) {
|
|
301
|
+
status += ` (${fontUsage.codepoints.page.length} on this page)`;
|
|
302
|
+
}
|
|
303
|
+
let savings = 0;
|
|
304
|
+
if (fontUsage.smallestSubsetSize !== undefined) {
|
|
305
|
+
status += buildInstancingSuffix(fontUsage);
|
|
306
|
+
status += `, ${prettyBytes(fontUsage.smallestOriginalSize)} (${fontUsage.smallestOriginalFormat}) => ${prettyBytes(fontUsage.smallestSubsetSize)} (${fontUsage.smallestSubsetFormat})`;
|
|
307
|
+
savings = fontUsage.smallestOriginalSize - fontUsage.smallestSubsetSize;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
status += ', no subset font created';
|
|
311
|
+
}
|
|
312
|
+
return { status, savings };
|
|
313
|
+
}
|
|
314
|
+
function printPerAssetFontReport(fontInfo, sumSizesBefore, sumSizesAfter, log) {
|
|
373
315
|
let totalSavings = sumSizesBefore - sumSizesAfter;
|
|
374
316
|
for (const { assetFileName, fontUsages } of fontInfo) {
|
|
375
317
|
let sumSmallestSubsetSize = 0;
|
|
@@ -396,45 +338,15 @@ const subfont = async function subfont({ root, canonicalRoot, output, debug = fa
|
|
|
396
338
|
for (const fontFamily of Object.keys(fontUsagesByFontFamily).sort()) {
|
|
397
339
|
log(` ${fontFamily}:`);
|
|
398
340
|
for (const fontUsage of fontUsagesByFontFamily[fontFamily]) {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
if (fontUsage.codepoints.page.length !== fontUsage.codepoints.used.length) {
|
|
402
|
-
status += ` (${fontUsage.codepoints.page.length} on this page)`;
|
|
403
|
-
}
|
|
404
|
-
if (fontUsage.smallestSubsetSize !== undefined) {
|
|
405
|
-
const numAxesReduced = fontUsage.numAxesReduced ?? 0;
|
|
406
|
-
const numAxesPinned = fontUsage.numAxesPinned ?? 0;
|
|
407
|
-
if (fontUsage.fullyInstanced) {
|
|
408
|
-
status += ', fully instanced';
|
|
409
|
-
}
|
|
410
|
-
else if (numAxesReduced > 0 || numAxesPinned) {
|
|
411
|
-
const instancingInfos = [];
|
|
412
|
-
if (numAxesPinned > 0) {
|
|
413
|
-
instancingInfos.push(`${numAxesPinned} ${numAxesPinned === 1 ? 'axis' : 'axes'} pinned`);
|
|
414
|
-
}
|
|
415
|
-
if (numAxesReduced) {
|
|
416
|
-
instancingInfos.push(`${numAxesReduced}${numAxesPinned > 0
|
|
417
|
-
? ''
|
|
418
|
-
: numAxesReduced === 1
|
|
419
|
-
? ' axis'
|
|
420
|
-
: ' axes'} reduced`);
|
|
421
|
-
}
|
|
422
|
-
status += `, partially instanced (${instancingInfos.join(', ')})`;
|
|
423
|
-
}
|
|
424
|
-
status += `, ${prettyBytes(fontUsage.smallestOriginalSize)} (${fontUsage.smallestOriginalFormat}) => ${prettyBytes(fontUsage.smallestSubsetSize)} (${fontUsage.smallestSubsetFormat})`;
|
|
425
|
-
totalSavings +=
|
|
426
|
-
fontUsage.smallestOriginalSize - fontUsage.smallestSubsetSize;
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
status += ', no subset font created';
|
|
430
|
-
}
|
|
341
|
+
const { status, savings } = describeFontUsageStatus(fontUsage, usedPad, originalPad);
|
|
342
|
+
totalSavings += savings;
|
|
431
343
|
log(status);
|
|
432
344
|
}
|
|
433
345
|
}
|
|
434
346
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
347
|
+
return totalSavings;
|
|
348
|
+
}
|
|
349
|
+
function printTimingSummary(outerTimings, subsetFontsTotal, subsetTimings, log) {
|
|
438
350
|
const st = subsetTimings ?? {};
|
|
439
351
|
const detailsRaw = st.collectTextsByPageDetails;
|
|
440
352
|
const details = detailsRaw && typeof detailsRaw === 'object' ? detailsRaw : {};
|
|
@@ -466,73 +378,197 @@ const subfont = async function subfont({ root, canonicalRoot, output, debug = fa
|
|
|
466
378
|
['writeAssetsToDisc', outerTimings.writeAssetsToDisc, 0],
|
|
467
379
|
['output reporting', outerTimings['output reporting'], 0],
|
|
468
380
|
];
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
381
|
+
log('\n═══ Subfont Timing Summary ═══');
|
|
382
|
+
for (const [label, ms, indent] of rows) {
|
|
383
|
+
if (ms === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
const prefix = ' '.repeat(indent + 1);
|
|
386
|
+
const padded = (ms || 0).toLocaleString().padStart(8);
|
|
387
|
+
log(`${prefix}${label}: ${padded}ms`);
|
|
388
|
+
}
|
|
389
|
+
log(' ─────────────────────────────────');
|
|
390
|
+
log(` Total: ${totalElapsed.toLocaleString().padStart(8)}ms`);
|
|
391
|
+
log('═══════════════════════════════\n');
|
|
392
|
+
}
|
|
393
|
+
function printDryRunPreview(assetGraph, log) {
|
|
394
|
+
log('\n═══ Dry Run Preview ═══');
|
|
395
|
+
const assetsToWrite = assetGraph.findAssets({
|
|
396
|
+
isLoaded: true,
|
|
397
|
+
isRedirect: { $ne: true },
|
|
398
|
+
url: (url) => url && url.startsWith(assetGraph.root),
|
|
399
|
+
});
|
|
400
|
+
const byType = {};
|
|
401
|
+
let totalOutputSize = 0;
|
|
402
|
+
for (const asset of assetsToWrite) {
|
|
403
|
+
const type = asset.type || 'Other';
|
|
404
|
+
if (!byType[type])
|
|
405
|
+
byType[type] = { count: 0, size: 0, files: [] };
|
|
406
|
+
const size = asset.rawSrc ? asset.rawSrc.length : 0;
|
|
407
|
+
byType[type].count += 1;
|
|
408
|
+
byType[type].size += size;
|
|
409
|
+
totalOutputSize += size;
|
|
410
|
+
if (asset.url && asset.url.includes('/subfont/')) {
|
|
411
|
+
byType[type].files.push(` ${asset.url.replace(assetGraph.root, '/')} (${prettyBytes(size)})`);
|
|
477
412
|
}
|
|
478
|
-
log(' ─────────────────────────────────');
|
|
479
|
-
log(` Total: ${totalElapsed.toLocaleString().padStart(8)}ms`);
|
|
480
|
-
log('═══════════════════════════════\n');
|
|
481
413
|
}
|
|
482
|
-
|
|
483
|
-
log(
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
isRedirect: { $ne: true },
|
|
487
|
-
url: (url) => url && url.startsWith(assetGraph.root),
|
|
488
|
-
});
|
|
489
|
-
const byType = {};
|
|
490
|
-
let totalOutputSize = 0;
|
|
491
|
-
for (const asset of assetsToWrite) {
|
|
492
|
-
const type = asset.type || 'Other';
|
|
493
|
-
if (!byType[type])
|
|
494
|
-
byType[type] = { count: 0, size: 0, files: [] };
|
|
495
|
-
const size = asset.rawSrc ? asset.rawSrc.length : 0;
|
|
496
|
-
byType[type].count += 1;
|
|
497
|
-
byType[type].size += size;
|
|
498
|
-
totalOutputSize += size;
|
|
499
|
-
if (asset.url && asset.url.includes('/subfont/')) {
|
|
500
|
-
byType[type].files.push(` ${asset.url.replace(assetGraph.root, '/')} (${prettyBytes(size)})`);
|
|
501
|
-
}
|
|
414
|
+
for (const [type, info] of Object.entries(byType).sort(([, a], [, b]) => b.size - a.size)) {
|
|
415
|
+
log(` ${type}: ${info.count} file${info.count === 1 ? '' : 's'}, ${prettyBytes(info.size)}`);
|
|
416
|
+
for (const file of info.files) {
|
|
417
|
+
log(file);
|
|
502
418
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
419
|
+
}
|
|
420
|
+
log(` ─────────────────────────────────`);
|
|
421
|
+
log(` Total output: ${prettyBytes(totalOutputSize)}`);
|
|
422
|
+
const dirtyHtmlAssets = assetGraph.findAssets({
|
|
423
|
+
isDirty: true,
|
|
424
|
+
isLoaded: true,
|
|
425
|
+
type: { $in: ['Html', 'Svg'] },
|
|
426
|
+
});
|
|
427
|
+
if (dirtyHtmlAssets.length > 0) {
|
|
428
|
+
log(`\n Modified HTML/SVG files (${dirtyHtmlAssets.length}):`);
|
|
429
|
+
for (const asset of dirtyHtmlAssets) {
|
|
430
|
+
log(` ${asset.urlOrDescription}`);
|
|
508
431
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
432
|
+
}
|
|
433
|
+
const subsetCssAssets = assetGraph.findAssets({
|
|
434
|
+
type: 'Css',
|
|
435
|
+
isLoaded: true,
|
|
436
|
+
url: (url) => url && url.includes('/subfont/'),
|
|
437
|
+
});
|
|
438
|
+
if (subsetCssAssets.length > 0) {
|
|
439
|
+
log(`\n Subset CSS files that would be created (${subsetCssAssets.length}):`);
|
|
440
|
+
for (const css of subsetCssAssets) {
|
|
441
|
+
const fontFaceCount = (css.text.match(/@font-face/g) || []).length;
|
|
442
|
+
log(` ${css.url.replace(assetGraph.root, '/')} (${prettyBytes(css.rawSrc.length)}, ${fontFaceCount} @font-face rule${fontFaceCount === 1 ? '' : 's'})`);
|
|
521
443
|
}
|
|
522
|
-
|
|
523
|
-
|
|
444
|
+
}
|
|
445
|
+
log('═══════════════════════════════\n');
|
|
446
|
+
log('Dry run complete — no files were written.');
|
|
447
|
+
}
|
|
448
|
+
async function resolveRootsAndInputs(root, output, inputFiles, inPlace, dryRun, warn) {
|
|
449
|
+
let rootUrl = root && urlTools.urlOrFsPathToUrl(root, true);
|
|
450
|
+
const outRoot = output && urlTools.urlOrFsPathToUrl(output, true);
|
|
451
|
+
const resolved = await resolveInputUrls(inputFiles, rootUrl, warn);
|
|
452
|
+
rootUrl = resolved.rootUrl;
|
|
453
|
+
const inputUrls = resolved.inputUrls;
|
|
454
|
+
if (!inputUrls[0].startsWith('file:') && !outRoot && !dryRun) {
|
|
455
|
+
throw new UsageError('--output has to be specified when using non-file input urls');
|
|
456
|
+
}
|
|
457
|
+
if (!inPlace && !outRoot && !dryRun) {
|
|
458
|
+
throw new UsageError('Either --output, --in-place, or --dry-run has to be specified');
|
|
459
|
+
}
|
|
460
|
+
return { rootUrl, outRoot, inputUrls };
|
|
461
|
+
}
|
|
462
|
+
function printRunReport(fontInfo, sumSizesBefore, sumSizesAfter, debug, log) {
|
|
463
|
+
if (debug)
|
|
464
|
+
printVariantSummary(fontInfo, log);
|
|
465
|
+
const totalSavings = printPerAssetFontReport(fontInfo, sumSizesBefore, sumSizesAfter, log);
|
|
466
|
+
log(`HTML/SVG/JS/CSS size increase: ${prettyBytes(sumSizesAfter - sumSizesBefore)}`);
|
|
467
|
+
log(`Total savings: ${prettyBytes(totalSavings)}`);
|
|
468
|
+
}
|
|
469
|
+
const subfont = async function subfont({ root, canonicalRoot, output, debug = false, dryRun = false, silent = false, inlineCss = false, fontDisplay = 'swap', inPlace = false, inputFiles = [], recursive = false, relativeUrls = false, dynamic = false, fallbacks = true, text, sourceMaps = false, concurrency, chromeFlags = [], cache = false, strict = false, }, console) {
|
|
470
|
+
if (concurrency !== undefined &&
|
|
471
|
+
(!Number.isInteger(concurrency) || concurrency < 1)) {
|
|
472
|
+
throw new UsageError('--concurrency must be a positive integer');
|
|
473
|
+
}
|
|
474
|
+
// Prevent postcss plugins (colormin, convert-values, etc.) invoked by
|
|
475
|
+
// cssnano from walking the filesystem for a "browserslist" config.
|
|
476
|
+
// Under pnpm, `node_modules/.bin/browserslist` is a shell shim that
|
|
477
|
+
// browserslist mis-parses as browser queries, throwing
|
|
478
|
+
// BrowserslistError and silently aborting CSS minification.
|
|
479
|
+
// Setting BROWSERSLIST short-circuits the walk entirely.
|
|
480
|
+
if (!process.env.BROWSERSLIST && !process.env.BROWSERSLIST_CONFIG) {
|
|
481
|
+
process.env.BROWSERSLIST = 'defaults';
|
|
482
|
+
}
|
|
483
|
+
const { log, warn } = makeLogger(silent, console);
|
|
484
|
+
const maxConcurrency = (0, concurrencyLimit_1.getMaxConcurrency)();
|
|
485
|
+
if (concurrency !== undefined && concurrency > maxConcurrency) {
|
|
486
|
+
warn(`--concurrency ${concurrency} exceeds estimated safe limit of ${maxConcurrency} (each worker uses ~50 MB; ${Math.round(os.freemem() / (1024 * 1024 * 1024))} GB free, ${os.cpus().length} CPUs). Proceeding anyway — reduce if you hit OOM.`);
|
|
487
|
+
}
|
|
488
|
+
const { rootUrl, outRoot, inputUrls } = await resolveRootsAndInputs(root, output, inputFiles, inPlace, dryRun, warn);
|
|
489
|
+
const assetGraph = new AssetGraph({
|
|
490
|
+
root: rootUrl,
|
|
491
|
+
// Non-file roots get an explicit trailing-slash canonicalRoot so
|
|
492
|
+
// relative-URL resolution lines up with how the deployed site reads.
|
|
493
|
+
canonicalRoot: rootUrl && !rootUrl.startsWith('file:')
|
|
494
|
+
? rootUrl.replace(/\/?$/, '/')
|
|
495
|
+
: canonicalRoot,
|
|
496
|
+
});
|
|
497
|
+
const getSawWarning = await installWarningHandlers(assetGraph, silent, strict, console);
|
|
498
|
+
const outerTimings = {};
|
|
499
|
+
// The tracker writes with console.log (duck-typed). Route it through
|
|
500
|
+
// the silent-aware log wrapper so --silent suppresses phase markers
|
|
501
|
+
// the same way it suppresses other subfont output.
|
|
502
|
+
const trackPhase = (0, progress_1.makePhaseTracker)({ log }, debug);
|
|
503
|
+
const loadAssetsPhase = trackPhase('loadAssets');
|
|
504
|
+
await assetGraph.loadAssets(inputUrls);
|
|
505
|
+
outerTimings.loadAssets = loadAssetsPhase.end();
|
|
506
|
+
const populatePhase = trackPhase('populate (initial)');
|
|
507
|
+
await assetGraph.populate({
|
|
508
|
+
followRelations: buildFollowRelationsQuery(recursive),
|
|
509
|
+
});
|
|
510
|
+
outerTimings['populate (initial)'] = populatePhase.end();
|
|
511
|
+
handleInitialRedirects(assetGraph);
|
|
512
|
+
const sizeableAssetQuery = {
|
|
513
|
+
isInline: false,
|
|
514
|
+
isLoaded: true,
|
|
515
|
+
type: { $in: ['Html', 'Svg', 'Css', 'JavaScript'] },
|
|
516
|
+
};
|
|
517
|
+
let sumSizesBefore = 0;
|
|
518
|
+
for (const asset of assetGraph.findAssets(sizeableAssetQuery)) {
|
|
519
|
+
sumSizesBefore += asset.rawSrc.length;
|
|
520
|
+
}
|
|
521
|
+
if (!sourceMaps) {
|
|
522
|
+
log('Skipping CSS source map processing for faster execution. Use --source-maps to preserve them.');
|
|
523
|
+
}
|
|
524
|
+
const cacheDir = resolveCacheDir(cache, rootUrl, warn);
|
|
525
|
+
const subsetPhase = trackPhase('subsetFonts total');
|
|
526
|
+
const { fontInfo, timings: subsetTimings } = await subsetFonts(assetGraph, {
|
|
527
|
+
inlineCss,
|
|
528
|
+
fontDisplay,
|
|
529
|
+
formats: ['woff2'],
|
|
530
|
+
omitFallbacks: !fallbacks,
|
|
531
|
+
hrefType: relativeUrls ? 'relative' : 'rootRelative',
|
|
532
|
+
text,
|
|
533
|
+
dynamic,
|
|
534
|
+
console,
|
|
535
|
+
sourceMaps,
|
|
536
|
+
debug,
|
|
537
|
+
concurrency,
|
|
538
|
+
chromeArgs: chromeFlags,
|
|
539
|
+
cacheDir,
|
|
540
|
+
});
|
|
541
|
+
const subsetFontsTotal = subsetPhase.end();
|
|
542
|
+
const postProcessingPhase = trackPhase('post-subsetFonts processing');
|
|
543
|
+
let sumSizesAfter = 0;
|
|
544
|
+
for (const asset of assetGraph.findAssets(sizeableAssetQuery)) {
|
|
545
|
+
sumSizesAfter += asset.rawSrc.length;
|
|
546
|
+
}
|
|
547
|
+
await runPostProcessing(assetGraph, rootUrl);
|
|
548
|
+
outerTimings['post-subsetFonts processing'] = postProcessingPhase.end();
|
|
549
|
+
if (strict && getSawWarning()) {
|
|
550
|
+
// In non-silent mode, assetgraph's logEvents normally exits earlier via
|
|
551
|
+
// stopOnWarning. This guard covers silent mode and warnings that slipped
|
|
552
|
+
// past a transform boundary.
|
|
553
|
+
throw new Error('subfont: --strict was set and one or more warnings were emitted; refusing to write output.');
|
|
554
|
+
}
|
|
555
|
+
const writePhase = trackPhase('writeAssetsToDisc');
|
|
556
|
+
if (!dryRun) {
|
|
557
|
+
await assetGraph.writeAssetsToDisc({
|
|
524
558
|
isLoaded: true,
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
log
|
|
535
|
-
|
|
559
|
+
isRedirect: { $ne: true },
|
|
560
|
+
url: (url) => url && url.startsWith(assetGraph.root),
|
|
561
|
+
}, outRoot, assetGraph.root);
|
|
562
|
+
}
|
|
563
|
+
outerTimings.writeAssetsToDisc = writePhase.end();
|
|
564
|
+
const reportingPhase = trackPhase('output reporting');
|
|
565
|
+
printRunReport(fontInfo, sumSizesBefore, sumSizesAfter, debug, log);
|
|
566
|
+
outerTimings['output reporting'] = reportingPhase.end();
|
|
567
|
+
if (debug) {
|
|
568
|
+
printTimingSummary(outerTimings, subsetFontsTotal, subsetTimings, log);
|
|
569
|
+
}
|
|
570
|
+
if (dryRun) {
|
|
571
|
+
printDryRunPreview(assetGraph, log);
|
|
536
572
|
}
|
|
537
573
|
else {
|
|
538
574
|
log('Output written to', outRoot || assetGraph.root);
|