@turntrout/subfont 1.6.0 → 1.7.1

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