@turntrout/subfont 1.0.3 → 1.0.4

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,19 @@ class FontTracerPool {
159
159
  }
160
160
  this._pendingTasks = [];
161
161
 
162
- await Promise.all(this._workers.map((w) => w.terminate()));
162
+ // Terminate workers with a 5-second timeout to prevent hanging
163
+ const TERMINATE_TIMEOUT_MS = 5000;
164
+ await Promise.all(
165
+ this._workers.map((w) =>
166
+ Promise.race([
167
+ w.terminate(),
168
+ new Promise((resolve) => {
169
+ const timer = setTimeout(resolve, TERMINATE_TIMEOUT_MS);
170
+ timer.unref();
171
+ }),
172
+ ])
173
+ )
174
+ );
163
175
  this._workers = [];
164
176
  this._idle = [];
165
177
  }
@@ -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
  });
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,16 @@
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
+ ]);
4
14
  const TEXT_ATTRIBUTES = new Set([
5
15
  'alt',
6
16
  'title',
@@ -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
  }
@@ -575,3 +589,5 @@ module.exports = async function subfont(
575
589
  }
576
590
  return assetGraph;
577
591
  };
592
+
593
+ 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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turntrout/subfont",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
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"