@turntrout/subfont 1.0.3 → 1.0.5

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.
@@ -159,7 +159,30 @@ class FontTracerPool {
159
159
  }
160
160
  this._pendingTasks = [];
161
161
 
162
- await Promise.all(this._workers.map((w) => w.terminate()));
162
+ // Reject any in-flight tasks still assigned to workers.
163
+ // Clear _taskByWorker before terminate() so _onWorkerExit won't double-reject.
164
+ for (const [, taskId] of this._taskByWorker) {
165
+ const cb = this._taskCallbacks.get(taskId);
166
+ if (cb) {
167
+ this._taskCallbacks.delete(taskId);
168
+ cb.reject(new Error('Worker pool destroyed'));
169
+ }
170
+ }
171
+ this._taskByWorker.clear();
172
+
173
+ // Terminate workers with a 5-second timeout to prevent hanging
174
+ const TERMINATE_TIMEOUT_MS = 5000;
175
+ await Promise.all(
176
+ this._workers.map((w) =>
177
+ Promise.race([
178
+ w.terminate(),
179
+ new Promise((resolve) => {
180
+ const timer = setTimeout(resolve, TERMINATE_TIMEOUT_MS);
181
+ timer.unref();
182
+ }),
183
+ ])
184
+ )
185
+ );
163
186
  this._workers = [];
164
187
  this._idle = [];
165
188
  }
@@ -148,10 +148,10 @@ class HeadlessBrowser {
148
148
  `${request.method()} ${request.url()} returned ${response.status()}`
149
149
  );
150
150
  } else {
151
+ const failure = request.failure();
152
+ const reason = failure ? failure.errorText : 'unknown error';
151
153
  this.console.error(
152
- `${request.method()} ${request.url()} failed: ${
153
- request.failure().errorText
154
- }`
154
+ `${request.method()} ${request.url()} failed: ${reason}`
155
155
  );
156
156
  }
157
157
  });
@@ -180,7 +180,8 @@ class HeadlessBrowser {
180
180
  urlTools.resolveUrl(
181
181
  baseUrl,
182
182
  urlTools.buildRelativeUrl(assetGraph.root, htmlAsset.url)
183
- )
183
+ ),
184
+ { timeout: 30000 }
184
185
  );
185
186
 
186
187
  await page.addScriptTag({
package/lib/cli.js CHANGED
@@ -5,7 +5,7 @@ const { yargs, help, ...options } = require('./parseCommandLineOptions')();
5
5
  require('@gustavnikolaj/async-main-wrap')(require('./subfont'), {
6
6
  processError(err) {
7
7
  yargs.showHelp();
8
- if (err.constructor === SyntaxError) {
8
+ if (err.name === 'UsageError') {
9
9
  // Avoid rendering a stack trace for the wrong usage errors
10
10
  err.customOutput = err.message;
11
11
  }
@@ -397,10 +397,8 @@ async function tracePages(
397
397
  }
398
398
  });
399
399
  await Promise.all(tracePromises);
400
+ } finally {
400
401
  await pool.destroy();
401
- } catch (err) {
402
- await pool.destroy();
403
- throw err;
404
402
  }
405
403
  } else if (pagesNeedingFullTrace.length > 0) {
406
404
  const totalPages = pagesNeedingFullTrace.length;
@@ -1,6 +1,17 @@
1
1
  const parse5 = require('parse5');
2
2
 
3
- const INVISIBLE_ELEMENTS = new Set(['script', 'style', 'svg', 'template']);
3
+ const INVISIBLE_ELEMENTS = new Set([
4
+ 'script',
5
+ 'style',
6
+ 'svg',
7
+ 'template',
8
+ 'head',
9
+ 'noscript',
10
+ 'iframe',
11
+ 'object',
12
+ 'embed',
13
+ 'datalist',
14
+ ]);
4
15
  const TEXT_ATTRIBUTES = new Set([
5
16
  'alt',
6
17
  'title',
@@ -62,3 +73,4 @@ function extractVisibleText(html) {
62
73
  }
63
74
 
64
75
  module.exports = extractVisibleText;
76
+ module.exports.INVISIBLE_ELEMENTS = INVISIBLE_ELEMENTS;
@@ -18,7 +18,9 @@ function stringifyFontFamily(name) {
18
18
  }
19
19
 
20
20
  function maybeCssQuote(value) {
21
- if (/^\w+$/.test(value)) {
21
+ // CSS identifiers must start with a letter or underscore (or hyphen
22
+ // followed by a letter/underscore), not a digit or bare hyphen.
23
+ if (/^[a-zA-Z_][a-zA-Z0-9_-]*$|^-[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value)) {
22
24
  return value;
23
25
  } else {
24
26
  return `'${value.replace(/'/g, "\\'")}'`;
@@ -26,31 +28,29 @@ function maybeCssQuote(value) {
26
28
  }
27
29
 
28
30
  function getPreferredFontUrl(cssFontFaceSrcRelations = []) {
29
- const formatOrder = ['woff2', 'woff', 'truetype', 'opentype'];
30
-
31
- const typeOrder = ['Woff2', 'Woff', 'Ttf', 'Otf'];
32
-
33
- for (const format of formatOrder) {
34
- const relation = cssFontFaceSrcRelations.find(
35
- (r) => r.format && r.format.toLowerCase() === format
36
- );
37
-
38
- if (relation) {
39
- return relation.to.url;
31
+ // Priority: woff2 > woff > truetype > opentype, preferring explicit
32
+ // format() declarations over asset-type guesses.
33
+ const formatPriority = { woff2: 0, woff: 1, truetype: 2, opentype: 3 };
34
+ const typePriority = { Woff2: 4, Woff: 5, Ttf: 6, Otf: 7 };
35
+
36
+ let bestUrl;
37
+ let bestPriority = Infinity;
38
+
39
+ for (const r of cssFontFaceSrcRelations) {
40
+ let priority;
41
+ if (r.format) {
42
+ priority = formatPriority[r.format.toLowerCase()];
40
43
  }
41
- }
42
-
43
- for (const assetType of typeOrder) {
44
- const relation = cssFontFaceSrcRelations.find(
45
- (r) => r.to.type === assetType
46
- );
47
-
48
- if (relation) {
49
- return relation.to.url;
44
+ if (priority === undefined) {
45
+ priority = typePriority[r.to.type];
46
+ }
47
+ if (priority !== undefined && priority < bestPriority) {
48
+ bestPriority = priority;
49
+ bestUrl = r.to.url;
50
50
  }
51
51
  }
52
52
 
53
- return undefined;
53
+ return bestUrl;
54
54
  }
55
55
 
56
56
  // Temporarily switch all relation hrefs to absolute so that
package/lib/subfont.js CHANGED
@@ -6,6 +6,13 @@ const urlTools = require('urltools');
6
6
  const util = require('util');
7
7
  const subsetFonts = require('./subsetFonts');
8
8
 
9
+ class UsageError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'UsageError';
13
+ }
14
+ }
15
+
9
16
  module.exports = async function subfont(
10
17
  {
11
18
  root,
@@ -30,6 +37,13 @@ module.exports = async function subfont(
30
37
  },
31
38
  console
32
39
  ) {
40
+ if (
41
+ concurrency !== undefined &&
42
+ (!Number.isInteger(concurrency) || concurrency < 1)
43
+ ) {
44
+ throw new UsageError('--concurrency must be a positive integer');
45
+ }
46
+
33
47
  const formats = ['woff2'];
34
48
 
35
49
  function logToConsole(severity, ...args) {
@@ -51,7 +65,7 @@ module.exports = async function subfont(
51
65
  try {
52
66
  await fsPromises.access(rootPath);
53
67
  } catch {
54
- throw new SyntaxError(`The --root path does not exist: ${rootPath}`);
68
+ throw new UsageError(`The --root path does not exist: ${rootPath}`);
55
69
  }
56
70
  }
57
71
  const outRoot = output && urlTools.urlOrFsPathToUrl(output, true);
@@ -75,19 +89,19 @@ module.exports = async function subfont(
75
89
  inputUrls = [`${rootUrl}**/*.html`];
76
90
  warn(`No input files specified, defaulting to ${inputUrls[0]}`);
77
91
  } else {
78
- throw new SyntaxError(
92
+ throw new UsageError(
79
93
  "No input files and no --root specified (or it isn't file:), cannot proceed.\n"
80
94
  );
81
95
  }
82
96
 
83
97
  if (!inputUrls[0].startsWith('file:') && !outRoot && !dryRun) {
84
- throw new SyntaxError(
98
+ throw new UsageError(
85
99
  '--output has to be specified when using non-file input urls'
86
100
  );
87
101
  }
88
102
 
89
103
  if (!inPlace && !outRoot && !dryRun) {
90
- throw new SyntaxError(
104
+ throw new UsageError(
91
105
  'Either --output, --in-place, or --dry-run has to be specified'
92
106
  );
93
107
  }
@@ -202,14 +216,15 @@ module.exports = async function subfont(
202
216
  }
203
217
  }
204
218
 
205
- let sumSizesBefore = 0;
206
- for (const asset of assetGraph.findAssets({
219
+ const sizeableAssetQuery = {
207
220
  isInline: false,
208
221
  isLoaded: true,
209
222
  type: {
210
223
  $in: ['Html', 'Svg', 'Css', 'JavaScript'],
211
224
  },
212
- })) {
225
+ };
226
+ let sumSizesBefore = 0;
227
+ for (const asset of assetGraph.findAssets(sizeableAssetQuery)) {
213
228
  sumSizesBefore += asset.rawSrc.length;
214
229
  }
215
230
 
@@ -255,13 +270,7 @@ module.exports = async function subfont(
255
270
 
256
271
  phaseStart = Date.now();
257
272
  let sumSizesAfter = 0;
258
- for (const asset of assetGraph.findAssets({
259
- isInline: false,
260
- isLoaded: true,
261
- type: {
262
- $in: ['Html', 'Svg', 'Css', 'JavaScript'],
263
- },
264
- })) {
273
+ for (const asset of assetGraph.findAssets(sizeableAssetQuery)) {
265
274
  sumSizesAfter += asset.rawSrc.length;
266
275
  }
267
276
 
@@ -575,3 +584,5 @@ module.exports = async function subfont(
575
584
  }
576
585
  return assetGraph;
577
586
  };
587
+
588
+ module.exports.UsageError = UsageError;
@@ -63,6 +63,16 @@ class SubsetDiskCache {
63
63
  try {
64
64
  await fs.writeFile(filePath, buffer);
65
65
  } catch (err) {
66
+ // If the directory was removed after init, retry once
67
+ if (err.code === 'ENOENT') {
68
+ try {
69
+ await fs.mkdir(this._cacheDir, { recursive: true });
70
+ await fs.writeFile(filePath, buffer);
71
+ return;
72
+ } catch {
73
+ // Fall through to warning below
74
+ }
75
+ }
66
76
  if (this._warnedWrite) return;
67
77
  this._warnedWrite = true;
68
78
  if (this._console) {
@@ -14,7 +14,7 @@ const getUnicodeRanges = (codePoints) => {
14
14
  const ranges = [];
15
15
  let start, end;
16
16
 
17
- codePoints.sort((a, b) => a - b);
17
+ codePoints = [...codePoints].sort((a, b) => a - b);
18
18
 
19
19
  for (let i = 0; i < codePoints.length; i++) {
20
20
  start = codePoints[i];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turntrout/subfont",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Automatically subset web fonts to only the characters used on your pages. Fork of Munter/subfont with modern defaults.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"