@turntrout/subfont 1.7.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/FontTracerPool.d.ts +37 -0
- package/lib/FontTracerPool.d.ts.map +1 -0
- package/lib/FontTracerPool.js +210 -219
- package/lib/FontTracerPool.js.map +1 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +15 -12
- package/lib/cli.js.map +1 -0
- package/lib/subfont.d.ts +33 -0
- package/lib/subfont.d.ts.map +1 -0
- package/lib/subfont.js +530 -629
- package/lib/subfont.js.map +1 -0
- package/lib/subsetFontWithGlyphs.d.ts +17 -0
- package/lib/subsetFontWithGlyphs.d.ts.map +1 -0
- package/lib/subsetFontWithGlyphs.js +223 -256
- package/lib/subsetFontWithGlyphs.js.map +1 -0
- package/lib/subsetFonts.d.ts +59 -0
- package/lib/subsetFonts.d.ts.map +1 -0
- package/lib/subsetFonts.js +898 -1200
- package/lib/subsetFonts.js.map +1 -0
- package/lib/subsetGeneration.d.ts +39 -0
- package/lib/subsetGeneration.d.ts.map +1 -0
- package/lib/subsetGeneration.js +294 -324
- package/lib/subsetGeneration.js.map +1 -0
- package/package.json +11 -2
package/lib/subfont.js
CHANGED
|
@@ -1,646 +1,547 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = 'UsageError';
|
|
50
|
+
}
|
|
18
51
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
warn(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
'
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
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
|
+
}
|
|
366
|
+
}
|
|
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`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
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;
|
|
379
378
|
for (const { assetFileName, fontUsages } of fontInfo) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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;
|
|
407
430
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
431
|
+
else {
|
|
432
|
+
status += ', no subset font created';
|
|
433
|
+
}
|
|
434
|
+
log(status);
|
|
435
|
+
}
|
|
413
436
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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`);
|
|
417
480
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
fontUsage.codepoints.used.length,
|
|
443
|
-
maxUsedCodePoints
|
|
444
|
-
);
|
|
445
|
-
maxOriginalCodePoints = Math.max(
|
|
446
|
-
fontUsage.codepoints.original.length,
|
|
447
|
-
maxOriginalCodePoints
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
const fontUsagesByFontFamily = {};
|
|
451
|
-
for (const fontUsage of fontUsages) {
|
|
452
|
-
const key = fontUsage.props['font-family'];
|
|
453
|
-
if (!fontUsagesByFontFamily[key]) fontUsagesByFontFamily[key] = [];
|
|
454
|
-
fontUsagesByFontFamily[key].push(fontUsage);
|
|
455
|
-
}
|
|
456
|
-
const numFonts = Object.keys(fontUsagesByFontFamily).length;
|
|
457
|
-
log(
|
|
458
|
-
`${assetFileName}: ${numFonts} font${numFonts === 1 ? '' : 's'} (${
|
|
459
|
-
fontUsages.length
|
|
460
|
-
} variant${fontUsages.length === 1 ? '' : 's'}) in use, ${prettyBytes(
|
|
461
|
-
sumSmallestOriginalSize
|
|
462
|
-
)} total. Created subsets: ${prettyBytes(sumSmallestSubsetSize)} total`
|
|
463
|
-
);
|
|
464
|
-
for (const fontFamily of Object.keys(fontUsagesByFontFamily).sort()) {
|
|
465
|
-
log(` ${fontFamily}:`);
|
|
466
|
-
for (const fontUsage of fontUsagesByFontFamily[fontFamily]) {
|
|
467
|
-
const variantShortName = `${fontUsage.props['font-weight']}${
|
|
468
|
-
fontUsage.props['font-style'] === 'italic' ? 'i' : ' '
|
|
469
|
-
}`;
|
|
470
|
-
let status = ` ${variantShortName}: ${String(
|
|
471
|
-
fontUsage.codepoints.used.length
|
|
472
|
-
).padStart(String(maxUsedCodePoints).length)}/${String(
|
|
473
|
-
fontUsage.codepoints.original.length
|
|
474
|
-
).padStart(String(maxOriginalCodePoints).length)} codepoints used`;
|
|
475
|
-
if (
|
|
476
|
-
fontUsage.codepoints.page.length !== fontUsage.codepoints.used.length
|
|
477
|
-
) {
|
|
478
|
-
status += ` (${fontUsage.codepoints.page.length} on this page)`;
|
|
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
|
+
}
|
|
479
505
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (fontUsage.fullyInstanced) {
|
|
485
|
-
status += ', fully instanced';
|
|
486
|
-
} else if (fontUsage.numAxesReduced > 0 || fontUsage.numAxesPinned) {
|
|
487
|
-
const instancingInfos = [];
|
|
488
|
-
if (fontUsage.numAxesPinned > 0) {
|
|
489
|
-
instancingInfos.push(
|
|
490
|
-
`${fontUsage.numAxesPinned} ${
|
|
491
|
-
fontUsage.numAxesPinned === 1 ? 'axis' : 'axes'
|
|
492
|
-
} pinned`
|
|
493
|
-
);
|
|
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);
|
|
494
510
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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}`);
|
|
505
523
|
}
|
|
506
|
-
|
|
507
|
-
status += `, partially instanced (${instancingInfos.join(', ')})`;
|
|
508
|
-
}
|
|
509
|
-
status += `, ${prettyBytes(fontUsage.smallestOriginalSize)} (${
|
|
510
|
-
fontUsage.smallestOriginalFormat
|
|
511
|
-
}) => ${prettyBytes(fontUsage.smallestSubsetSize)} (${
|
|
512
|
-
fontUsage.smallestSubsetFormat
|
|
513
|
-
})`;
|
|
514
|
-
totalSavings +=
|
|
515
|
-
fontUsage.smallestOriginalSize - fontUsage.smallestSubsetSize;
|
|
516
|
-
} else {
|
|
517
|
-
status += ', no subset font created';
|
|
518
524
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
(outerTimings.writeAssetsToDisc || 0) +
|
|
539
|
-
(outerTimings['output reporting'] || 0);
|
|
540
|
-
|
|
541
|
-
const rows = [
|
|
542
|
-
['loadAssets', outerTimings.loadAssets, 0],
|
|
543
|
-
['populate (initial)', outerTimings['populate (initial)'], 0],
|
|
544
|
-
['subsetFonts total', subsetFontsTotal, 0],
|
|
545
|
-
['collectTextsByPage', st.collectTextsByPage, 1],
|
|
546
|
-
['Stylesheet precompute', details['Stylesheet precompute'], 2],
|
|
547
|
-
['Full tracing', details['Full tracing'], 2],
|
|
548
|
-
['Fast-path extraction', details['Fast-path extraction'], 2],
|
|
549
|
-
['Per-page loop', details['Per-page loop'], 2],
|
|
550
|
-
['Post-processing', details['Post-processing total'], 2],
|
|
551
|
-
['codepoint generation', st['codepoint generation'], 1],
|
|
552
|
-
['getSubsetsForFontUsage', st.getSubsetsForFontUsage, 1],
|
|
553
|
-
['insert subsets loop', st['insert subsets loop'], 1],
|
|
554
|
-
['inject font-family', st['inject subset font-family'], 1],
|
|
555
|
-
['post-subsetFonts', outerTimings['post-subsetFonts processing'], 0],
|
|
556
|
-
['writeAssetsToDisc', outerTimings.writeAssetsToDisc, 0],
|
|
557
|
-
['output reporting', outerTimings['output reporting'], 0],
|
|
558
|
-
];
|
|
559
|
-
|
|
560
|
-
if (debug) {
|
|
561
|
-
log('\n═══ Subfont Timing Summary ═══');
|
|
562
|
-
for (const [label, ms, indent] of rows) {
|
|
563
|
-
if (ms === undefined) continue;
|
|
564
|
-
const prefix = ' '.repeat(indent + 1);
|
|
565
|
-
const padded = (ms || 0).toLocaleString().padStart(8);
|
|
566
|
-
log(`${prefix}${label}: ${padded}ms`);
|
|
567
|
-
}
|
|
568
|
-
log(' ─────────────────────────────────');
|
|
569
|
-
log(` Total: ${totalElapsed.toLocaleString().padStart(8)}ms`);
|
|
570
|
-
log('═══════════════════════════════\n');
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (dryRun) {
|
|
574
|
-
log('\n═══ Dry Run Preview ═══');
|
|
575
|
-
const assetsToWrite = assetGraph.findAssets({
|
|
576
|
-
isLoaded: true,
|
|
577
|
-
isRedirect: { $ne: true },
|
|
578
|
-
url: (url) => url && url.startsWith(assetGraph.root),
|
|
579
|
-
});
|
|
580
|
-
const byType = {};
|
|
581
|
-
let totalOutputSize = 0;
|
|
582
|
-
for (const asset of assetsToWrite) {
|
|
583
|
-
const type = asset.type || 'Other';
|
|
584
|
-
if (!byType[type]) byType[type] = { count: 0, size: 0, files: [] };
|
|
585
|
-
const size = asset.rawSrc ? asset.rawSrc.length : 0;
|
|
586
|
-
byType[type].count += 1;
|
|
587
|
-
byType[type].size += size;
|
|
588
|
-
totalOutputSize += size;
|
|
589
|
-
|
|
590
|
-
if (asset.url && asset.url.includes('/subfont/')) {
|
|
591
|
-
byType[type].files.push(
|
|
592
|
-
` ${asset.url.replace(assetGraph.root, '/')} (${prettyBytes(size)})`
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
for (const [type, info] of Object.entries(byType).sort(
|
|
597
|
-
([, a], [, b]) => b.size - a.size
|
|
598
|
-
)) {
|
|
599
|
-
log(
|
|
600
|
-
` ${type}: ${info.count} file${info.count === 1 ? '' : 's'}, ${prettyBytes(info.size)}`
|
|
601
|
-
);
|
|
602
|
-
for (const file of info.files) {
|
|
603
|
-
log(file);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
log(` ─────────────────────────────────`);
|
|
607
|
-
log(` Total output: ${prettyBytes(totalOutputSize)}`);
|
|
608
|
-
|
|
609
|
-
const dirtyHtmlAssets = assetGraph.findAssets({
|
|
610
|
-
isDirty: true,
|
|
611
|
-
isLoaded: true,
|
|
612
|
-
type: { $in: ['Html', 'Svg'] },
|
|
613
|
-
});
|
|
614
|
-
if (dirtyHtmlAssets.length > 0) {
|
|
615
|
-
log(`\n Modified HTML/SVG files (${dirtyHtmlAssets.length}):`);
|
|
616
|
-
for (const asset of dirtyHtmlAssets) {
|
|
617
|
-
log(` ${asset.urlOrDescription}`);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const subsetCssAssets = assetGraph.findAssets({
|
|
622
|
-
type: 'Css',
|
|
623
|
-
isLoaded: true,
|
|
624
|
-
url: (url) => url && url.includes('/subfont/'),
|
|
625
|
-
});
|
|
626
|
-
if (subsetCssAssets.length > 0) {
|
|
627
|
-
log(
|
|
628
|
-
`\n Subset CSS files that would be created (${subsetCssAssets.length}):`
|
|
629
|
-
);
|
|
630
|
-
for (const css of subsetCssAssets) {
|
|
631
|
-
const fontFaceCount = (css.text.match(/@font-face/g) || []).length;
|
|
632
|
-
log(
|
|
633
|
-
` ${css.url.replace(assetGraph.root, '/')} (${prettyBytes(css.rawSrc.length)}, ${fontFaceCount} @font-face rule${fontFaceCount === 1 ? '' : 's'})`
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
log('═══════════════════════════════\n');
|
|
639
|
-
log('Dry run complete — no files were written.');
|
|
640
|
-
} else {
|
|
641
|
-
log('Output written to', outRoot || assetGraph.root);
|
|
642
|
-
}
|
|
643
|
-
return assetGraph;
|
|
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;
|
|
644
544
|
};
|
|
645
|
-
|
|
646
|
-
module.exports
|
|
545
|
+
subfont.UsageError = UsageError;
|
|
546
|
+
module.exports = subfont;
|
|
547
|
+
//# sourceMappingURL=subfont.js.map
|