@turntrout/subfont 1.5.1 → 1.7.0

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/CHANGELOG.md CHANGED
@@ -33,3 +33,10 @@ On [TurnTrout.com](https://github.com/alexander-turner/TurnTrout.com) (382 pages
33
33
 
34
34
  - Fixed crash on invalid/corrupt font files during instancing.
35
35
  - Fixed incorrect axis range computation for variable fonts.
36
+ - Fixed OOM / >1h runtimes on large sites. `font-size` was added to
37
+ `font-tracer`'s `propsToReturn` to derive `opsz`, which bucketed every text
38
+ chunk by size and exploded per-page entry counts 10-50x on sites with many
39
+ distinct sizes (headings, dropcaps, smallcaps). `opsz` now falls back to
40
+ pinning at the font default (the pre-regression behaviour); an explicit
41
+ `font-variation-settings: "opsz" …` still narrows the axis. TurnTrout.com
42
+ returned from 46+ min / runner-OOM to ~33 min.
package/README.md CHANGED
@@ -55,27 +55,27 @@ subfont path/to/index.html -i --cache
55
55
 
56
56
  ## Options
57
57
 
58
- | Flag | Default | Description |
59
- | -----------------: | :-----: | :----------------------------------------------------------- |
60
- | `-i, --in-place` | off | Modify files in-place |
61
- | `-o, --output` | | Output directory |
62
- | `--root` | | Path to web root (deduced from input files if not specified) |
63
- | `--canonical-root` | | URI root where the site will be deployed |
64
- | `-r, --recursive` | off | Crawl linked pages |
65
- | `--dynamic` | off | Trace with headless browser |
66
- | `--dry-run` | off | Preview without writing |
67
- | `--fallbacks` | on | Load the full original font for characters not in the subset |
68
- | `--font-display` | `swap` | `auto`/`block`/`swap`/`fallback`/`optional` |
69
- | `--text` | | Extra characters for every subset |
70
- | `--cache [dir]` | off | Cache subset results to disk between runs |
71
- | `--concurrency N` | | Max worker threads (capped by available memory, ~50 MB each) |
72
- | `--chrome-flags` | | Custom Chrome flags for `--dynamic` |
73
- | `--source-maps` | off | Preserve CSS source maps (slower) |
74
- | `--strict` | off | Exit non-zero if any warnings are emitted |
75
- | `-s, --silent` | off | Suppress all console output |
76
- | `-d, --debug` | off | Verbose timing and font glyph detection info |
77
- | `--relative-urls` | off | Emit relative URLs instead of root-relative |
78
- | `--inline-css` | off | Inline the subset @font-face CSS into HTML |
58
+ | Flag | Default | Description |
59
+ | -----------------: | :-----: | :------------------------------------------------------------------------------------ |
60
+ | `-i, --in-place` | off | Modify files in-place |
61
+ | `-o, --output` | | Output directory |
62
+ | `--root` | | Path to web root (deduced from input files if not specified) |
63
+ | `--canonical-root` | | URI root where the site will be deployed |
64
+ | `-r, --recursive` | off | Crawl linked pages |
65
+ | `--dynamic` | off | Trace with headless browser |
66
+ | `--dry-run` | off | Preview without writing |
67
+ | `--fallbacks` | on | Async-load the full original font as a fallback for dynamic content |
68
+ | `--font-display` | `swap` | `auto`/`block`/`swap`/`fallback`/`optional` |
69
+ | `--text` | | Extra characters for every subset |
70
+ | `--cache [dir]` | off | Cache subset results to disk between runs |
71
+ | `--concurrency N` | auto | Max worker threads (defaults to CPU count, capped by available memory at ~50 MB each) |
72
+ | `--chrome-flags` | | Custom Chrome flags for `--dynamic` (comma-separated) |
73
+ | `--source-maps` | off | Preserve CSS source maps (slower) |
74
+ | `--strict` | off | Exit non-zero if any warnings are emitted |
75
+ | `-s, --silent` | off | Suppress all console output |
76
+ | `-d, --debug` | off | Verbose timing and font glyph detection info |
77
+ | `--relative-urls` | off | Emit relative URLs instead of root-relative |
78
+ | `--inline-css` | off | Inline the subset @font-face CSS into HTML |
79
79
 
80
80
  Run `subfont --help` for the full list.
81
81
 
@@ -105,6 +105,37 @@ const assetGraph = await subfont(
105
105
 
106
106
  Returns the [Assetgraph](https://github.com/assetgraph/assetgraph) instance.
107
107
 
108
+ ### Parameters
109
+
110
+ `subfont(options, console)` — the second argument is an optional logger (anything
111
+ with `log`, `warn`, and `error` methods — e.g. the global `console`). Pass
112
+ `null` together with `silent: true` to suppress all output.
113
+
114
+ The `options` object accepts the following keys:
115
+
116
+ | Option | Type | Default | Description |
117
+ | --------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
118
+ | `inputFiles` | `string[]` | `[]` | HTML entry points (file paths or URLs). At least one is required unless `root` is given. |
119
+ | `root` | `string` | deduced | Path or URL to the web root. Deduced from `inputFiles` if omitted. |
120
+ | `canonicalRoot` | `string` | — | URI root where the site will be deployed (used to rewrite absolute URLs). |
121
+ | `output` | `string` | — | Output directory. Mutually exclusive with `inPlace`. |
122
+ | `inPlace` | `boolean` | `false` | Modify input files in place. |
123
+ | `dryRun` | `boolean` | `false` | Trace and compute subsets but do not write any files. |
124
+ | `recursive` | `boolean` | `false` | Crawl linked pages starting from `inputFiles`. |
125
+ | `dynamic` | `boolean` | `false` | Trace JS-rendered content in headless Chrome (via puppeteer). |
126
+ | `fallbacks` | `boolean` | `true` | Async-load the full original font as a fallback for dynamic content. |
127
+ | `fontDisplay` | `string` | `'swap'` | `font-display` CSS value: `auto`, `block`, `swap`, `fallback`, or `optional`. |
128
+ | `text` | `string` | — | Extra characters to include in every subset. |
129
+ | `inlineCss` | `boolean` | `false` | Inline the subset `@font-face` CSS into the HTML document. |
130
+ | `relativeUrls` | `boolean` | `false` | Emit relative URLs instead of root-relative URLs. |
131
+ | `sourceMaps` | `boolean` | `false` | Preserve CSS source maps (slower). |
132
+ | `concurrency` | `number` | auto | Max parallel tracing workers. Defaults to CPU count, capped by available memory (~50 MB per worker). |
133
+ | `chromeFlags` | `string[]` | `[]` | Extra Chrome flags forwarded to puppeteer when `dynamic` is set. |
134
+ | `cache` | `boolean \| string` | `false` | Cache subset results between runs. Pass a path to customize the cache directory; `true` uses `.subfont-cache` inside the `root` directory. |
135
+ | `strict` | `boolean` | `false` | Resolve with a non-zero exit (via the CLI) if any warnings are emitted. |
136
+ | `silent` | `boolean` | `false` | Suppress all log output to `console`. |
137
+ | `debug` | `boolean` | `false` | Emit verbose timing and glyph-detection info. |
138
+
108
139
  ## License
109
140
 
110
141
  MIT -- Original work by [Peter Muller (Munter)](https://github.com/Munter/subfont)
@@ -5,14 +5,18 @@ const { Worker } = require('worker_threads');
5
5
  * Worker pool for running fontTracer in parallel across pages.
6
6
  * Each worker re-parses HTML with jsdom and runs fontTracer independently.
7
7
  */
8
+ const DEFAULT_TASK_TIMEOUT_MS = 60_000;
9
+
8
10
  class FontTracerPool {
9
- constructor(numWorkers) {
11
+ constructor(numWorkers, { taskTimeoutMs = DEFAULT_TASK_TIMEOUT_MS } = {}) {
10
12
  this._workerPath = pathModule.join(__dirname, 'fontTracerWorker.js');
11
13
  this._numWorkers = numWorkers;
14
+ this._taskTimeoutMs = taskTimeoutMs;
12
15
  this._workers = [];
13
16
  this._idle = [];
14
17
  this._pendingTasks = [];
15
18
  this._taskCallbacks = new Map();
19
+ this._taskTimers = new Map();
16
20
  this._taskByWorker = new Map(); // track which taskId each worker is processing
17
21
  this._nextTaskId = 0;
18
22
  }
@@ -46,8 +50,17 @@ class FontTracerPool {
46
50
  await Promise.all(initPromises);
47
51
  }
48
52
 
53
+ _clearTaskTimer(taskId) {
54
+ const timer = this._taskTimers.get(taskId);
55
+ if (timer) {
56
+ clearTimeout(timer);
57
+ this._taskTimers.delete(taskId);
58
+ }
59
+ }
60
+
49
61
  _onWorkerMessage(worker, msg) {
50
62
  this._taskByWorker.delete(worker);
63
+ this._clearTaskTimer(msg.taskId);
51
64
  const cb = this._taskCallbacks.get(msg.taskId);
52
65
  if (cb) {
53
66
  this._taskCallbacks.delete(msg.taskId);
@@ -78,6 +91,7 @@ class FontTracerPool {
78
91
  const taskId = this._taskByWorker.get(worker);
79
92
  this._taskByWorker.delete(worker);
80
93
  if (taskId !== undefined) {
94
+ this._clearTaskTimer(taskId);
81
95
  const cb = this._taskCallbacks.get(taskId);
82
96
  if (cb) {
83
97
  this._taskCallbacks.delete(taskId);
@@ -101,6 +115,33 @@ class FontTracerPool {
101
115
  }
102
116
  }
103
117
 
118
+ _startTaskTimer(taskId) {
119
+ if (this._taskTimeoutMs <= 0) return;
120
+ const timer = setTimeout(() => {
121
+ this._taskTimers.delete(taskId);
122
+ const cb = this._taskCallbacks.get(taskId);
123
+ if (cb) {
124
+ this._taskCallbacks.delete(taskId);
125
+ cb.reject(
126
+ new Error(
127
+ `Font tracing task ${taskId} timed out after ${this._taskTimeoutMs}ms`
128
+ )
129
+ );
130
+ }
131
+ // Terminate the hung worker so it doesn't permanently consume a pool
132
+ // slot. _onWorkerExit will remove it from _workers and _idle.
133
+ for (const [worker, tid] of this._taskByWorker) {
134
+ if (tid === taskId) {
135
+ this._taskByWorker.delete(worker);
136
+ worker.terminate();
137
+ break;
138
+ }
139
+ }
140
+ }, this._taskTimeoutMs);
141
+ timer.unref();
142
+ this._taskTimers.set(taskId, timer);
143
+ }
144
+
104
145
  _dispatchPending() {
105
146
  while (this._idle.length > 0 && this._pendingTasks.length > 0) {
106
147
  const worker = this._idle.pop();
@@ -108,6 +149,7 @@ class FontTracerPool {
108
149
  this._taskByWorker.set(worker, task.message.taskId);
109
150
  try {
110
151
  worker.postMessage(task.message);
152
+ this._startTaskTimer(task.message.taskId);
111
153
  } catch (err) {
112
154
  // postMessage can fail synchronously (e.g. structured clone error).
113
155
  // Return the worker to the idle pool and reject the task.
@@ -149,6 +191,12 @@ class FontTracerPool {
149
191
  }
150
192
 
151
193
  async destroy() {
194
+ // Clear all task timers
195
+ for (const timer of this._taskTimers.values()) {
196
+ clearTimeout(timer);
197
+ }
198
+ this._taskTimers.clear();
199
+
152
200
  // Reject any tasks still waiting in the queue
153
201
  for (const task of this._pendingTasks) {
154
202
  const cb = this._taskCallbacks.get(task.message.taskId);
@@ -13,8 +13,12 @@ async function transferResults(jsHandle) {
13
13
  const results = await jsHandle.jsonValue();
14
14
  for (const [i, result] of results.entries()) {
15
15
  const resultHandle = await jsHandle.getProperty(String(i));
16
- const elementHandle = await resultHandle.getProperty('node');
17
- result.node = elementHandle;
16
+ try {
17
+ const elementHandle = await resultHandle.getProperty('node');
18
+ result.node = elementHandle;
19
+ } finally {
20
+ await resultHandle.dispose();
21
+ }
18
22
  }
19
23
  return results;
20
24
  }
@@ -193,7 +197,11 @@ class HeadlessBrowser {
193
197
  /* istanbul ignore next */
194
198
  () => fontTracer(document)
195
199
  );
196
- return await transferResults(jsHandle);
200
+ try {
201
+ return await transferResults(jsHandle);
202
+ } finally {
203
+ await jsHandle.dispose();
204
+ }
197
205
  } finally {
198
206
  await page.close();
199
207
  }