@turntrout/subfont 1.0.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 ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 -- Hard fork from [Munter/subfont](https://github.com/Munter/subfont)
4
+
5
+ Published as `@turntrout/subfont`. Based on Munter/subfont v7.2.3.
6
+
7
+ ### Performance
8
+
9
+ On [TurnTrout.com](https://github.com/alexander-turner/TurnTrout.com) (382 pages), font subsetting dropped from 111 minutes to 28 minutes:
10
+
11
+ | | Version | Duration |
12
+ | ------------------------------------------------------------------------------------ | -------------- | -------- |
13
+ | [Before](https://github.com/alexander-turner/TurnTrout.com/actions/runs/23470135763) | Munter/subfont | 111 min |
14
+ | [After](https://github.com/alexander-turner/TurnTrout.com/actions/runs/23518006824) | This fork | 28 min |
15
+
16
+ ### Breaking changes
17
+
18
+ - **woff2-only.** Removed `--browsers` and `--formats`. Every browser supports woff2.
19
+ - **Always-on variable font instancing.** Removed `--instance`. If you use weights 400 and 700 from a 100-900 variable font, the subset shrinks to just that range automatically.
20
+ - **Removed legacy flags:** `--skip-source-map-processing`, `--dryrun`/`--dry`/`--canonicalroot`/`--sourceMaps` aliases, and v5 flag validation.
21
+
22
+ ### New features
23
+
24
+ - **`--cache [dir]`** -- Cache subset results to disk. Speeds up repeat builds.
25
+ - **`--chrome-flags`** -- Custom flags for headless Chrome with `--dynamic`.
26
+ - **`--concurrency N`** -- Control worker thread count for parallel font tracing.
27
+ - **Parallel font tracing** -- Worker pool (up to 8 threads). Pages sharing identical CSS are traced once.
28
+ - **`--root` validation** -- Fails early with a clear error.
29
+ - **Timing summary** -- Printed after every run.
30
+ - **Better `--dry-run`** -- Detailed preview of files, sizes, and CSS changes.
31
+
32
+ ### Bug fixes
33
+
34
+ - Fixed crash on invalid/corrupt font files during instancing.
35
+ - Fixed incorrect axis range computation for variable fonts.
package/CLAUDE.md ADDED
@@ -0,0 +1,53 @@
1
+ # CLAUDE.md — subfont
2
+
3
+ ## Project Overview
4
+
5
+ subfont is a CLI tool and Node.js library that speeds up initial page paint by automatically subsetting local or Google fonts and loading them optimally. It uses puppeteer to trace font usage across pages and generates optimized font subsets.
6
+
7
+ ## Development Commands
8
+
9
+ ```bash
10
+ pnpm install # Install dependencies
11
+ pnpm test # Run mocha tests + lint
12
+ pnpm run lint # ESLint + Prettier check
13
+ pnpm run coverage # Run tests with nyc coverage
14
+ pnpm run check-coverage # Verify coverage thresholds
15
+ ```
16
+
17
+ ## Code Style
18
+
19
+ - **Formatter**: Prettier with single quotes, trailing commas (es5)
20
+ - **Linter**: ESLint via neostandard + eslint-config-prettier
21
+ - **Rules**: `prefer-template`, `prefer-const` (destructuring: all)
22
+ - **Tests**: Mocha with `unexpected` assertion library (not chai/jest)
23
+ - No exclusive tests (`describe.only`, `it.only`) — enforced by eslint-plugin-mocha
24
+
25
+ ## Project Structure
26
+
27
+ - `lib/` — Source code (entry: `lib/subfont.js`, CLI: `lib/cli.js`)
28
+ - `test/` — Mocha test files
29
+ - `testdata/` — HTML fixtures and font files for tests
30
+ - `cases/` — Additional test case data
31
+
32
+ ## Key Architecture
33
+
34
+ - Built on **assetgraph** for HTML/CSS asset graph traversal
35
+ - Uses **puppeteer-core** for headless browser font tracing
36
+ - **font-tracer** traces which fonts are used on each page
37
+ - **subset-font** / **harfbuzzjs** for WOFF2 subsetting
38
+ - `lib/subsetFonts.js` — Main orchestration logic
39
+ - `lib/FontTracerPool.js` — Manages puppeteer browser pool for parallel tracing
40
+
41
+ ## Testing Notes
42
+
43
+ - Tests have a 5-minute timeout (configured in `.mocharc.yml`)
44
+ - Tests use `httpception` for HTTP mocking and `unexpected` for assertions
45
+ - Some tests require puppeteer browser binaries (installed via `pnpm install`)
46
+ - Coverage thresholds are enforced via `nyc check-coverage`
47
+
48
+ ## Conventions
49
+
50
+ - CommonJS modules (`require`/`module.exports`), not ESM
51
+ - Node.js >= 18 required
52
+ - Use `const` by default; `let` only when reassignment is needed
53
+ - Template literals preferred over string concatenation
package/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2017 Peter Brandt Müller
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # `@turntrout/subfont`
2
+
3
+ [![Build Status](https://github.com/alexander-turner/subfont/actions/workflows/ci.yml/badge.svg)](https://github.com/alexander-turner/subfont/actions/workflows/ci.yml)
4
+
5
+ A faster fork of [subfont](https://github.com/Munter/subfont) that subsets web fonts to only the characters used on your pages. Adds parallel tracing, disk caching, woff2-only output, and always-on variable font instancing.
6
+
7
+ ## Performance
8
+
9
+ On [TurnTrout.com](https://github.com/alexander-turner/TurnTrout.com) (382 pages, 20+ font variants), switching to this fork cut font subsetting from **111 minutes to 28 minutes**:
10
+
11
+ | | Version | Duration |
12
+ | -----------------------------------------------------------------------------------: | :------------: | :------- |
13
+ | [Before](https://github.com/alexander-turner/TurnTrout.com/actions/runs/23470135763) | Munter/subfont | 111 min |
14
+ | [After](https://github.com/alexander-turner/TurnTrout.com/actions/runs/23518006824) | This fork | 28 min |
15
+
16
+ ## Install
17
+
18
+ ```
19
+ npm install -g @turntrout/subfont
20
+ ```
21
+
22
+ Requires Node.js >= 18.
23
+
24
+ ## Usage
25
+
26
+ ```bash
27
+ # Optimize build artifacts in-place (recommended)
28
+ subfont path/to/dist/index.html -i
29
+
30
+ # Preview without writing
31
+ subfont path/to/dist/index.html --dry-run
32
+
33
+ # Output to a separate directory
34
+ subfont path/to/index.html -o path/to/output
35
+
36
+ # Crawl all linked pages
37
+ subfont path/to/index.html -i --recursive
38
+
39
+ # Trace JS-rendered content in headless Chrome
40
+ subfont path/to/index.html -i --dynamic
41
+
42
+ # Cache subset results between runs
43
+ subfont path/to/index.html -i --cache
44
+ ```
45
+
46
+ ## Options
47
+
48
+ | Flag | Default | Description |
49
+ | ----------------: | :-----: | :----------------------------------------------------------- |
50
+ | `-i, --in-place` | off | Modify files in-place |
51
+ | `-o, --output` | | Output directory |
52
+ | `-r, --recursive` | off | Crawl linked pages |
53
+ | `--dynamic` | off | Trace with headless browser |
54
+ | `--dry-run` | off | Preview without writing |
55
+ | `--fallbacks` | on | Load the full original font for characters not in the subset |
56
+ | `--font-display` | `swap` | `auto`/`block`/`swap`/`fallback`/`optional` |
57
+ | `--text` | | Extra characters for every subset |
58
+ | `--cache [dir]` | off | Cache subset results to disk between runs |
59
+ | `--concurrency N` | | Max worker threads for parallel font tracing |
60
+ | `--chrome-flags` | | Custom Chrome flags for `--dynamic` |
61
+ | `--source-maps` | off | Preserve CSS source maps (slower) |
62
+
63
+ Run `subfont --help` for the full list.
64
+
65
+ To include extra characters in a specific font's subset, add `-subfont-text` to its `@font-face`:
66
+
67
+ ```css
68
+ @font-face {
69
+ font-family: Roboto;
70
+ src: url(roboto.woff2) format('woff2');
71
+ -subfont-text: '0123456789';
72
+ }
73
+ ```
74
+
75
+ ## Programmatic API
76
+
77
+ ```js
78
+ const subfont = require('@turntrout/subfont');
79
+
80
+ const assetGraph = await subfont(
81
+ {
82
+ inputFiles: ['path/to/index.html'],
83
+ inPlace: true,
84
+ },
85
+ console
86
+ );
87
+ ```
88
+
89
+ Returns the [Assetgraph](https://github.com/assetgraph/assetgraph) instance.
90
+
91
+ ## License
92
+
93
+ MIT -- Original work by [Peter Muller (Munter)](https://github.com/Munter/subfont)
@@ -0,0 +1,158 @@
1
+ const pathModule = require('path');
2
+ const { Worker } = require('worker_threads');
3
+
4
+ /**
5
+ * Worker pool for running fontTracer in parallel across pages.
6
+ * Each worker re-parses HTML with jsdom and runs fontTracer independently.
7
+ */
8
+ class FontTracerPool {
9
+ constructor(numWorkers) {
10
+ this._workerPath = pathModule.join(__dirname, 'fontTracerWorker.js');
11
+ this._numWorkers = numWorkers;
12
+ this._workers = [];
13
+ this._idle = [];
14
+ this._pendingTasks = [];
15
+ this._taskCallbacks = new Map();
16
+ this._taskByWorker = new Map(); // track which taskId each worker is processing
17
+ this._nextTaskId = 0;
18
+ }
19
+
20
+ async init() {
21
+ const initPromises = [];
22
+ for (let i = 0; i < this._numWorkers; i++) {
23
+ const worker = new Worker(this._workerPath);
24
+ this._workers.push(worker);
25
+
26
+ const initPromise = new Promise((resolve, reject) => {
27
+ const onError = reject;
28
+ const onMessage = (msg) => {
29
+ if (msg.type === 'ready') {
30
+ worker.off('message', onMessage);
31
+ worker.off('error', onError);
32
+ worker.on('message', (msg) => this._onWorkerMessage(worker, msg));
33
+ worker.on('exit', (code) => this._onWorkerExit(worker, code));
34
+ this._idle.push(worker);
35
+ resolve();
36
+ }
37
+ };
38
+ worker.on('message', onMessage);
39
+ worker.on('error', onError);
40
+ });
41
+
42
+ worker.postMessage({ type: 'init' });
43
+
44
+ initPromises.push(initPromise);
45
+ }
46
+ await Promise.all(initPromises);
47
+ }
48
+
49
+ _onWorkerMessage(worker, msg) {
50
+ this._taskByWorker.delete(worker);
51
+ const cb = this._taskCallbacks.get(msg.taskId);
52
+ if (cb) {
53
+ this._taskCallbacks.delete(msg.taskId);
54
+ if (msg.type === 'result') {
55
+ cb.resolve(msg.textByProps);
56
+ } else if (msg.type === 'error') {
57
+ cb.reject(new Error(`Worker error: ${msg.error}\n${msg.stack}`));
58
+ }
59
+ }
60
+ // Worker is now idle, check for pending tasks
61
+ this._idle.push(worker);
62
+ this._dispatchPending();
63
+ }
64
+
65
+ _onWorkerExit(worker, code) {
66
+ // Remove crashed worker from tracking
67
+ const workerIdx = this._workers.indexOf(worker);
68
+ if (workerIdx !== -1) {
69
+ this._workers.splice(workerIdx, 1);
70
+ }
71
+ const idleIdx = this._idle.indexOf(worker);
72
+ if (idleIdx !== -1) {
73
+ this._idle.splice(idleIdx, 1);
74
+ }
75
+
76
+ if (code !== 0) {
77
+ // Reject the task that was in-flight on this worker
78
+ const taskId = this._taskByWorker.get(worker);
79
+ this._taskByWorker.delete(worker);
80
+ if (taskId !== undefined) {
81
+ const cb = this._taskCallbacks.get(taskId);
82
+ if (cb) {
83
+ this._taskCallbacks.delete(taskId);
84
+ cb.reject(new Error(`Worker exited with code ${code}`));
85
+ }
86
+ }
87
+
88
+ // If no workers remain, reject all pending tasks
89
+ if (this._workers.length === 0) {
90
+ for (const task of this._pendingTasks) {
91
+ const cb = this._taskCallbacks.get(task.message.taskId);
92
+ if (cb) {
93
+ this._taskCallbacks.delete(task.message.taskId);
94
+ cb.reject(
95
+ new Error('All workers have crashed, no workers available')
96
+ );
97
+ }
98
+ }
99
+ this._pendingTasks = [];
100
+ }
101
+ }
102
+ }
103
+
104
+ _dispatchPending() {
105
+ while (this._idle.length > 0 && this._pendingTasks.length > 0) {
106
+ const worker = this._idle.pop();
107
+ const task = this._pendingTasks.shift();
108
+ this._taskByWorker.set(worker, task.message.taskId);
109
+ try {
110
+ worker.postMessage(task.message);
111
+ } catch (err) {
112
+ // postMessage can fail synchronously (e.g. structured clone error).
113
+ // Return the worker to the idle pool and reject the task.
114
+ this._taskByWorker.delete(worker);
115
+ this._idle.push(worker);
116
+ const cb = this._taskCallbacks.get(task.message.taskId);
117
+ if (cb) {
118
+ this._taskCallbacks.delete(task.message.taskId);
119
+ cb.reject(err);
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Run fontTracer on the given HTML text + stylesheets in a worker.
127
+ * Returns a promise that resolves to textByProps.
128
+ */
129
+ trace(htmlText, stylesheetsWithPredicates) {
130
+ const taskId = this._nextTaskId++;
131
+ // Serialize stylesheets to plain data — asset objects contain DOM/PostCSS
132
+ // trees that cannot be transferred via structured clone.
133
+ const serializedStylesheets = stylesheetsWithPredicates.map((entry) => ({
134
+ text: entry.text || (entry.asset && entry.asset.text) || '',
135
+ predicates: entry.predicates || {},
136
+ }));
137
+ const message = {
138
+ type: 'trace',
139
+ taskId,
140
+ htmlText,
141
+ stylesheetsWithPredicates: serializedStylesheets,
142
+ };
143
+
144
+ return new Promise((resolve, reject) => {
145
+ this._taskCallbacks.set(taskId, { resolve, reject });
146
+ this._pendingTasks.push({ message });
147
+ this._dispatchPending();
148
+ });
149
+ }
150
+
151
+ async destroy() {
152
+ await Promise.all(this._workers.map((w) => w.terminate()));
153
+ this._workers = [];
154
+ this._idle = [];
155
+ }
156
+ }
157
+
158
+ module.exports = FontTracerPool;
@@ -0,0 +1,223 @@
1
+ const urlTools = require('urltools');
2
+ const puppeteer = require('puppeteer-core');
3
+ const pathModule = require('path');
4
+ const {
5
+ install,
6
+ uninstall,
7
+ Browser,
8
+ detectBrowserPlatform,
9
+ Cache,
10
+ } = require('@puppeteer/browsers');
11
+
12
+ async function transferResults(jsHandle) {
13
+ const results = await jsHandle.jsonValue();
14
+ for (const [i, result] of results.entries()) {
15
+ const resultHandle = await jsHandle.getProperty(String(i));
16
+ const elementHandle = await resultHandle.getProperty('node');
17
+ result.node = elementHandle;
18
+ }
19
+ return results;
20
+ }
21
+
22
+ async function downloadOrLocatePreferredBrowserRevision(extraArgs = []) {
23
+ if (process.env.PUPPETEER_EXECUTABLE_PATH) {
24
+ return puppeteer.launch({
25
+ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
26
+ args: ['--no-sandbox', '--disable-setuid-sandbox', ...extraArgs],
27
+ });
28
+ }
29
+ const cacheDir = pathModule.resolve(__dirname, '..', 'puppeteer-browsers');
30
+ const platform = detectBrowserPlatform();
31
+ const cache = new Cache(cacheDir);
32
+ const installed = cache.getInstalledBrowsers();
33
+ let executablePath;
34
+ const chromeEntry = installed.find((b) => b.browser === Browser.CHROME);
35
+ if (chromeEntry) {
36
+ executablePath = chromeEntry.executablePath;
37
+ } else {
38
+ // Check the default puppeteer cache (~/.cache/puppeteer) before downloading
39
+ const defaultCacheDir = pathModule.join(
40
+ require('os').homedir(),
41
+ '.cache',
42
+ 'puppeteer'
43
+ );
44
+ const defaultCache = new Cache(defaultCacheDir);
45
+ const defaultInstalled = defaultCache.getInstalledBrowsers();
46
+ const defaultChromeEntry = defaultInstalled.find(
47
+ (b) => b.browser === Browser.CHROME
48
+ );
49
+ if (defaultChromeEntry) {
50
+ executablePath = defaultChromeEntry.executablePath;
51
+ } else {
52
+ console.log('Downloading Chrome');
53
+ const result = await install({
54
+ browser: Browser.CHROME,
55
+ buildId: 'stable',
56
+ cacheDir,
57
+ platform,
58
+ });
59
+ executablePath = result.executablePath;
60
+
61
+ // Clean up older Chrome versions that may have accumulated from
62
+ // previous runs with different stable buildIds.
63
+ const allInstalled = cache.getInstalledBrowsers();
64
+ for (const entry of allInstalled) {
65
+ if (
66
+ entry.browser === Browser.CHROME &&
67
+ entry.executablePath !== executablePath
68
+ ) {
69
+ try {
70
+ await uninstall({
71
+ browser: entry.browser,
72
+ buildId: entry.buildId,
73
+ cacheDir,
74
+ });
75
+ console.log(`Removed old Chrome ${entry.buildId}`);
76
+ } catch {
77
+ // Ignore cleanup errors — the old version may be in use or locked
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
83
+ return puppeteer.launch({
84
+ executablePath,
85
+ args: ['--no-sandbox', '--disable-setuid-sandbox', ...extraArgs],
86
+ });
87
+ }
88
+
89
+ class HeadlessBrowser {
90
+ constructor({ console, chromeArgs = [] }) {
91
+ this.console = console;
92
+ this._chromeArgs = chromeArgs;
93
+ }
94
+
95
+ _ensureBrowserDownloaded() {}
96
+
97
+ _launchBrowserMemoized() {
98
+ // Make sure we only download and launch one browser per HeadlessBrowser instance
99
+ return (this._launchPromise =
100
+ this._launchPromise ||
101
+ downloadOrLocatePreferredBrowserRevision(this._chromeArgs));
102
+ }
103
+
104
+ async tracePage(htmlAsset) {
105
+ const assetGraph = htmlAsset.assetGraph;
106
+ const browser = await this._launchBrowserMemoized();
107
+ const page = await browser.newPage();
108
+
109
+ try {
110
+ // Make up a base url to map to the assetgraph root.
111
+ // Use the canonical root if available, so that it'll be
112
+ // easier to handle absolute and protocol-relative urls pointing
113
+ // at it, as well as fall through to the actual domain if some
114
+ // assets aren't found in the graph.
115
+ const baseUrl = assetGraph.canonicalRoot
116
+ ? assetGraph.canonicalRoot.replace(/\/?$/, '/')
117
+ : 'https://example.com/';
118
+
119
+ // Intercept all requests made by the headless browser, and
120
+ // fake a response from the assetgraph instance if the corresponding
121
+ // asset is found there:
122
+ await page.setRequestInterception(true);
123
+ page.on('request', (request) => {
124
+ const url = request.url();
125
+ if (url.startsWith(baseUrl)) {
126
+ let agUrl = url.replace(baseUrl, assetGraph.root);
127
+ if (/\/$/.test(agUrl)) {
128
+ agUrl += 'index.html';
129
+ }
130
+ const asset = assetGraph.findAssets({
131
+ isLoaded: true,
132
+ url: agUrl,
133
+ })[0];
134
+ if (asset) {
135
+ request.respond({
136
+ status: 200,
137
+ contentType: asset.contentType,
138
+ body: asset.rawSrc,
139
+ });
140
+ } else {
141
+ // Asset not in graph — return 404 instead of letting the
142
+ // request hit the network (baseUrl is synthetic).
143
+ request.respond({ status: 404, body: '' });
144
+ }
145
+ return;
146
+ }
147
+ if (url.startsWith('file:')) {
148
+ request.continue();
149
+ return;
150
+ }
151
+ // External request — abort to avoid hanging on DNS/network.
152
+ // The requestfailed handler will log it.
153
+ request.abort('failed');
154
+ });
155
+
156
+ page.on('requestfailed', (request) => {
157
+ const response = request.response();
158
+ if (response && response.status() > 400) {
159
+ this.console.error(
160
+ `${request.method()} ${request.url()} returned ${response.status()}`
161
+ );
162
+ } else {
163
+ this.console.error(
164
+ `${request.method()} ${request.url()} failed: ${
165
+ request.failure().errorText
166
+ }`
167
+ );
168
+ }
169
+ });
170
+
171
+ page.on('pageerror', (err) => {
172
+ // Puppeteer v24+ passes Error objects; format stack to match v19 style
173
+ if (err instanceof Error && err.stack) {
174
+ // Normalize "at <anonymous> (url:line:col)" to "at url:line:col"
175
+ const normalized = err.stack.replace(
176
+ /at <anonymous> \((.+)\)/g,
177
+ 'at $1'
178
+ );
179
+ this.console.error(normalized);
180
+ } else if (err instanceof Error) {
181
+ this.console.error(`${err.name}: ${err.message}`);
182
+ } else {
183
+ this.console.error(err);
184
+ }
185
+ });
186
+ page.on('error', this.console.error);
187
+
188
+ // Prevent the CSP of the page from rejecting our injection of font-tracer
189
+ await page.setBypassCSP(true);
190
+
191
+ await page.goto(
192
+ urlTools.resolveUrl(
193
+ baseUrl,
194
+ urlTools.buildRelativeUrl(assetGraph.root, htmlAsset.url)
195
+ )
196
+ );
197
+
198
+ await page.addScriptTag({
199
+ path: require.resolve('font-tracer/dist/fontTracer.browser.js'),
200
+ });
201
+
202
+ const jsHandle = await page.evaluateHandle(
203
+ /* global fontTracer */
204
+ /* istanbul ignore next */
205
+ () => fontTracer(document)
206
+ );
207
+ return await transferResults(jsHandle);
208
+ } finally {
209
+ await page.close();
210
+ }
211
+ }
212
+
213
+ async close() {
214
+ const launchPromise = this._launchPromise;
215
+ if (launchPromise) {
216
+ this._launchPromise = undefined;
217
+ const browser = await launchPromise;
218
+ await browser.close();
219
+ }
220
+ }
221
+ }
222
+
223
+ module.exports = HeadlessBrowser;
package/lib/cli.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { yargs, help, ...options } = require('./parseCommandLineOptions')();
4
+
5
+ require('@gustavnikolaj/async-main-wrap')(require('./subfont'), {
6
+ processError(err) {
7
+ yargs.showHelp();
8
+ if (err.constructor === SyntaxError) {
9
+ // Avoid rendering a stack trace for the wrong usage errors
10
+ err.customOutput = err.message;
11
+ }
12
+ return err;
13
+ },
14
+ })(options, console);