fontmin-rs 0.1.0 → 0.2.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/dist/cli.mjs ADDED
@@ -0,0 +1,784 @@
1
+ import { _ as woffToTtf, a as inspect, b as findConfig, d as ttfToSvg, f as ttfToWoff, g as woff2ToTtf, n as analyzeCoverage, o as otfToTtf, p as ttfToWoff2, r as eotToTtf, s as subsetTtf, t as optimize, u as ttfToEot, x as loadConfig } from "./optimize-pipeline-4Thm4vQb.mjs";
2
+ import { deliverySlices, glyph, svgs2ttf } from "./plugins.mjs";
3
+ import { mkdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, extname, isAbsolute, parse, relative, resolve, sep } from "node:path";
5
+ import { glob } from "tinyglobby";
6
+ //#region src/cli.mjs
7
+ const INIT_CONFIG_FILE = "fontmin.config.jsonc";
8
+ const FONTMIN_VERSION = "0.2.0";
9
+ const DEFAULT_CACHE_DIR = "node_modules/.cache/fontmin-rs";
10
+ let emitWarnings = true;
11
+ const DEFAULT_INIT_CONFIG = `{
12
+ // Generated by fontmin-rs init.
13
+ "input": ["fonts/*.ttf"],
14
+ "outDir": "build",
15
+ "subset": {
16
+ "text": "Hello",
17
+ "basicText": true
18
+ },
19
+ "outputs": [
20
+ { "format": "woff2" },
21
+ { "format": "woff" },
22
+ { "format": "css" }
23
+ ],
24
+ "css": {
25
+ "fontFamily": "MyFont",
26
+ "fontPath": "./",
27
+ "fontDisplay": "swap"
28
+ },
29
+ "cache": {
30
+ "enabled": true
31
+ }
32
+ }
33
+ `;
34
+ async function runCli(argv = process.argv.slice(2)) {
35
+ const command = argv[0];
36
+ const commandArgs = argv.slice(1);
37
+ try {
38
+ if (command === void 0 || command === "--help" || command === "-h" || commandArgs.includes("--help") || commandArgs.includes("-h")) usage(process.stdout);
39
+ else if (command === "--version" || command === "-V") process.stdout.write(`${FONTMIN_VERSION}\n`);
40
+ else if (command === "doctor") {
41
+ assertNoUnexpectedArgs(commandArgs, 0);
42
+ process.stdout.write("fontmin-rs doctor ok\n");
43
+ } else if (command === "subset") await subsetCommand(commandArgs);
44
+ else if (command === "coverage") await coverageCommand(commandArgs);
45
+ else if (command === "convert") await convertCommand(commandArgs);
46
+ else if (command === "build") await buildCommand(commandArgs);
47
+ else if (command === "inspect") await inspectCommand(commandArgs);
48
+ else if (command === "init") {
49
+ assertNoUnexpectedArgs(commandArgs, 0);
50
+ await initCommand();
51
+ } else if (command === "bench") await benchCommand(commandArgs);
52
+ else {
53
+ usage(process.stderr);
54
+ process.exitCode = 1;
55
+ }
56
+ } catch (error) {
57
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
58
+ process.exitCode = 1;
59
+ }
60
+ }
61
+ async function subsetCommand(args) {
62
+ const output = readOption(args, ["-o", "--output"]);
63
+ const subsetOptions = await subsetOptionsFromArgs(args);
64
+ const basicText = readFlag(args, ["-b", "--basic-text"]);
65
+ assertNoUnexpectedArgs(args);
66
+ const [input] = args;
67
+ requireValue(input, "subset requires an input font");
68
+ requireValue(output, "subset requires -o, --output");
69
+ if (subsetOptions.text === void 0 && subsetOptions.unicodes.length === 0 && !basicText) throw new Error("subset requires --text, --text-file, --unicodes, or --basic-text");
70
+ await writeOutput(output, subsetWithCoverage(await readFile(input), {
71
+ basicText,
72
+ missingGlyphs: subsetOptions.missingGlyphs,
73
+ text: subsetOptions.text,
74
+ unicodes: subsetOptions.unicodes
75
+ }));
76
+ }
77
+ async function coverageCommand(args) {
78
+ const json = readFlag(args, ["--json"]);
79
+ const basicText = readFlag(args, ["-b", "--basic-text"]);
80
+ const options = await subsetOptionsFromArgs(args);
81
+ assertNoUnexpectedArgs(args);
82
+ const [input] = args;
83
+ requireValue(input, "coverage requires an input font");
84
+ if (options.text === void 0 && options.unicodes.length === 0 && !basicText) throw new Error("coverage requires --text, --text-file, --unicodes, or --basic-text");
85
+ const report = analyzeCoverage(await readFile(input), {
86
+ basicText,
87
+ text: options.text,
88
+ unicodes: options.unicodes
89
+ });
90
+ if (json) {
91
+ process.stdout.write(`${JSON.stringify(report, void 0, 2)}\n`);
92
+ return;
93
+ }
94
+ process.stdout.write(`coverage: ${report.coveragePercent.toFixed(2)}% (${report.supported.length}/${report.requested.length})\nrequested: ${report.requested.length}\nsupported: ${report.supported.length}\nmissing: ${report.missing.length}\n`);
95
+ const warning = missingGlyphMessage(report);
96
+ if (warning !== void 0) process.stdout.write(`${warning}\n`);
97
+ }
98
+ async function convertCommand(args) {
99
+ const output = readOption(args, ["-o", "--output"]);
100
+ const format = readOption(args, ["-f", "--format"]);
101
+ const variationCoordinates = parseVariations(readOptions(args, ["--variation"]));
102
+ assertNoUnexpectedArgs(args);
103
+ const [input] = args;
104
+ requireValue(input, "convert requires an input font");
105
+ requireValue(output, "convert requires -o, --output");
106
+ requireValue(format, "convert requires -f, --format");
107
+ await writeOutput(output, convertFont(await readFile(input), format, { variationCoordinates }));
108
+ }
109
+ async function benchCommand(args) {
110
+ const json = readFlag(args, ["--json"]);
111
+ const basicText = readFlag(args, ["-b", "--basic-text"]);
112
+ const subsetOptions = await subsetOptionsFromArgs(args);
113
+ assertNoUnexpectedArgs(args);
114
+ const [input] = args;
115
+ requireValue(input, "bench requires an input font");
116
+ if (subsetOptions.text === void 0 && subsetOptions.unicodes.length === 0 && !basicText) throw new Error("bench requires --text, --text-file, --unicodes, or --basic-text");
117
+ const contents = await readFile(input);
118
+ const startedAt = process.hrtime.bigint();
119
+ const subset = subsetWithCoverage(contents, {
120
+ basicText,
121
+ missingGlyphs: subsetOptions.missingGlyphs,
122
+ text: subsetOptions.text,
123
+ unicodes: subsetOptions.unicodes
124
+ });
125
+ const elapsedMs = Math.round(Number(process.hrtime.bigint() - startedAt) / 1e6);
126
+ const report = {
127
+ elapsedMs,
128
+ inputBytes: contents.byteLength,
129
+ operation: "subset",
130
+ outputBytes: subset.byteLength
131
+ };
132
+ if (json) process.stdout.write(`${JSON.stringify(report)}\n`);
133
+ else process.stdout.write(`fontmin-rs bench subset completed in ${elapsedMs} ms\ninput: ${report.inputBytes} bytes\noutput: ${report.outputBytes} bytes\n`);
134
+ }
135
+ async function buildCommand(args) {
136
+ const showTime = readFlag(args, ["-T", "--show-time"]);
137
+ const silent = readFlag(args, ["--silent"]);
138
+ const startedAt = showTime && !silent ? process.hrtime.bigint() : void 0;
139
+ emitWarnings = !silent;
140
+ try {
141
+ await runBuildCommand(args);
142
+ reportShowTime(startedAt);
143
+ } finally {
144
+ emitWarnings = true;
145
+ }
146
+ }
147
+ async function runBuildCommand(args) {
148
+ const configPath = readOption(args, ["-c", "--config"]);
149
+ const cache = readFlag(args, ["--cache"]);
150
+ const noCache = readFlag(args, ["--no-cache"]);
151
+ const noOriginal = readFlag(args, ["--no-original"]);
152
+ readFlag(args, ["-d", "--deflate-woff"]);
153
+ const cssGlyph = readFlag(args, ["--css-glyph"]);
154
+ const cssUnicodeRanges = parseUnicodeRanges(readOptions(args, ["--css-unicode-range"]));
155
+ const deliverySlices = parseDeliverySlices(readOptions(args, ["--delivery-slice"]));
156
+ const outDir = readOption(args, ["-o", "--out-dir"]);
157
+ const formats = readOption(args, ["--formats"]);
158
+ const preset = readOption(args, ["--preset"]);
159
+ const basicText = readFlag(args, ["-b", "--basic-text"]);
160
+ const fontFamily = readOption(args, ["--font-family"]);
161
+ const fontPath = readOption(args, ["--font-path"]);
162
+ const variationCoordinates = parseVariations(readOptions(args, ["--variation"]));
163
+ const subsetOptions = await subsetOptionsFromArgs(args);
164
+ assertNoUnknownOptions(args);
165
+ if (cache && noCache) throw new Error("build accepts only one of --cache or --no-cache");
166
+ const cacheOverride = cacheOverrideFromFlags(cache, noCache);
167
+ if (configPath !== void 0) {
168
+ if (isIconfontPreset(preset)) {
169
+ await buildIconfontConfigCommand(configPath, {
170
+ fontFamily,
171
+ fontPath,
172
+ formats,
173
+ inputs: [...args],
174
+ cacheOverride,
175
+ cssUnicodeRanges,
176
+ deliverySlices,
177
+ outDir,
178
+ variationCoordinates
179
+ });
180
+ return;
181
+ }
182
+ await buildConfigCommand(configPath, {
183
+ basicText,
184
+ cacheOverride,
185
+ cssGlyph,
186
+ cssUnicodeRanges,
187
+ deliverySlices,
188
+ fontFamily,
189
+ fontPath,
190
+ formats,
191
+ inputs: [...args],
192
+ noOriginal,
193
+ outDir,
194
+ preset,
195
+ subsetOptions,
196
+ variationCoordinates
197
+ });
198
+ return;
199
+ }
200
+ if (args.length === 0) {
201
+ const foundConfigPath = await findConfig();
202
+ if (isIconfontPreset(preset)) {
203
+ await buildIconfontConfigCommand(foundConfigPath, {
204
+ fontFamily,
205
+ fontPath,
206
+ formats,
207
+ cacheOverride,
208
+ cssUnicodeRanges,
209
+ deliverySlices,
210
+ outDir,
211
+ variationCoordinates
212
+ });
213
+ return;
214
+ }
215
+ await buildConfigCommand(foundConfigPath, {
216
+ basicText,
217
+ cacheOverride,
218
+ cssGlyph,
219
+ cssUnicodeRanges,
220
+ deliverySlices,
221
+ fontFamily,
222
+ fontPath,
223
+ formats,
224
+ noOriginal,
225
+ outDir,
226
+ preset,
227
+ subsetOptions,
228
+ variationCoordinates
229
+ });
230
+ return;
231
+ }
232
+ const inputPatterns = [...args];
233
+ const [input] = inputPatterns;
234
+ requireValue(input, "build requires an input font");
235
+ requireValue(outDir, "build requires -o, --out-dir");
236
+ if (isIconfontPreset(preset)) {
237
+ if (formats !== void 0) throw new Error("build accepts only one of --formats or --preset");
238
+ const inputs = await expandInputPaths(inputPatterns, process.cwd());
239
+ const cacheOptions = normalizeCacheOptions(void 0, process.cwd(), cacheOverride);
240
+ await buildIconfontCommand(inputs, outDir, fontFamily, fontPath ?? "./", [], {}, cacheOptions, cssUnicodeRanges);
241
+ return;
242
+ }
243
+ const inputs = await expandInputPaths(inputPatterns, process.cwd());
244
+ const outputFormats = filterOriginalOutput(outputFormatsFromArgs(formats, preset), noOriginal);
245
+ const cacheOptions = normalizeCacheOptions(void 0, process.cwd(), cacheOverride);
246
+ if (outputFormats.length === 0) throw new Error("build requires at least one non-original output format");
247
+ assertCssHasFontOutput(outputFormats);
248
+ await mkdir(outDir, { recursive: true });
249
+ for (const input of inputs) await buildDirectInput(input, outDir, {
250
+ basicText,
251
+ cacheOptions,
252
+ cssGlyph,
253
+ cssUnicodeRanges,
254
+ deliverySlices,
255
+ fontFamily,
256
+ fontPath,
257
+ outputFormats,
258
+ subsetOptions,
259
+ variationCoordinates
260
+ });
261
+ }
262
+ async function buildDirectInput(input, outDir, { basicText, cacheOptions, cssGlyph, cssUnicodeRanges, deliverySlices: deliverySlices$1, fontFamily, fontPath, outputFormats, subsetOptions, variationCoordinates }) {
263
+ const baseName = basename(input, extname(input));
264
+ const shouldSubset = !(subsetOptions.text === void 0 && subsetOptions.unicodes.length === 0 && !basicText);
265
+ const plugins = [normalizeToTtfPlugin(variationCoordinates)];
266
+ if (shouldSubset) plugins.push(glyph({
267
+ basicText,
268
+ missingGlyphs: subsetOptions.missingGlyphs,
269
+ text: subsetOptions.text,
270
+ unicodes: subsetOptions.unicodes
271
+ }));
272
+ if (deliverySlices$1.length > 0) plugins.push(deliverySlices(deliverySlices$1));
273
+ await optimize({
274
+ cache: cacheOptions,
275
+ css: {
276
+ fontFamily: fontFamily ?? baseName,
277
+ fontPath: fontPath ?? "./",
278
+ glyph: cssGlyph,
279
+ unicodeRanges: cssUnicodeRanges
280
+ },
281
+ input: [input],
282
+ outDir,
283
+ outputs: outputFormats,
284
+ plugins
285
+ });
286
+ }
287
+ function normalizeToTtfPlugin(variationCoordinates = {}) {
288
+ return {
289
+ name: "fontmin:normalize-to-ttf",
290
+ native: {
291
+ kind: "builtin",
292
+ name: "normalizeToTtf",
293
+ options: { variationCoordinates }
294
+ }
295
+ };
296
+ }
297
+ function reportShowTime(startedAt) {
298
+ if (startedAt === void 0) return;
299
+ const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
300
+ process.stdout.write(`fontmin-rs build completed in ${Math.round(elapsedMs)} ms\n`);
301
+ }
302
+ async function buildIconfontCommand(inputs, outDir, fontFamily, fontPath, outputs = [], css = {}, cacheOptions = normalizeCacheOptions(void 0, process.cwd()), cssUnicodeRanges = css.unicodeRanges ?? []) {
303
+ if (inputs.length === 0) throw new Error("build requires at least one input font");
304
+ const family = fontFamily ?? "iconfont";
305
+ const fileName = outputFileName(outputs, "ttf", "iconfont.ttf");
306
+ const cssFileName = outputFileName(outputs, "css", `${basename(fileName, extname(fileName))}.${css.target ?? "css"}`);
307
+ await optimize({
308
+ cache: cacheOptions,
309
+ css: {
310
+ ...css,
311
+ asFileName: css.asFileName ?? true,
312
+ fontFamily: family,
313
+ fontPath,
314
+ glyph: true,
315
+ unicodeRanges: cssUnicodeRanges
316
+ },
317
+ input: inputs,
318
+ outDir,
319
+ outputs: [{
320
+ fileName,
321
+ format: "ttf"
322
+ }, {
323
+ fileName: cssFileName,
324
+ format: "css"
325
+ }],
326
+ plugins: [svgs2ttf({ fontName: family })]
327
+ });
328
+ }
329
+ async function buildConfigCommand(configPath, { basicText, cacheOverride, cssGlyph, cssUnicodeRanges = [], deliverySlices = [], fontFamily, fontPath, formats, inputs: inputOverrides = [], noOriginal, outDir: outDirOverride, preset, subsetOptions, variationCoordinates = {} } = {}) {
330
+ const resolvedConfigPath = resolve(configPath);
331
+ const config = await loadConfig(resolvedConfigPath);
332
+ const cwd = typeof config.cwd === "string" ? resolve(config.cwd) : dirname(resolvedConfigPath);
333
+ const inputs = inputOverrides.length > 0 ? inputOverrides : config.input ?? [];
334
+ const outDir = resolve(cwd, outDirOverride ?? config.outDir ?? "build");
335
+ const outputFormats = outputFormatsForConfig(config.outputs, {
336
+ formats,
337
+ noOriginal,
338
+ preset
339
+ });
340
+ const cacheOptions = normalizeCacheOptions(config.cache, cwd, cacheOverride);
341
+ const resolvedVariationCoordinates = {
342
+ ...config.otf?.variationCoordinates,
343
+ ...variationCoordinates
344
+ };
345
+ if (inputs.length === 0) throw new Error("build config requires at least one input");
346
+ const inputPaths = await expandInputPaths(inputs, cwd);
347
+ if (fontFamily !== void 0 || fontPath !== void 0 || cssGlyph === true || cssUnicodeRanges.length > 0) config.css = {
348
+ ...config.css,
349
+ ...cssGlyph === true && { glyph: true },
350
+ ...cssUnicodeRanges.length > 0 && { unicodeRanges: cssUnicodeRanges },
351
+ ...fontFamily !== void 0 && { fontFamily },
352
+ ...fontPath !== void 0 && { fontPath }
353
+ };
354
+ if (deliverySlices.length > 0) config.delivery = { slices: deliverySlices };
355
+ applySubsetOverrides(config, {
356
+ basicText,
357
+ subsetOptions
358
+ });
359
+ if (config.clean === true) await cleanOutputDirectory(cwd, outDir, [resolvedConfigPath, ...inputPaths]);
360
+ await mkdir(outDir, { recursive: true });
361
+ for (const inputPath of inputPaths) await buildConfigInput(inputPath, outDir, outputFormats, config, cwd, cacheOptions, resolvedVariationCoordinates);
362
+ }
363
+ async function buildIconfontConfigCommand(configPath, { cacheOverride, cssUnicodeRanges = [], deliverySlices = [], fontFamily, fontPath, formats, inputs: inputOverrides = [], outDir: outDirOverride }) {
364
+ if (formats !== void 0) throw new Error("build accepts only one of --formats or --preset");
365
+ const resolvedConfigPath = resolve(configPath);
366
+ const config = await loadConfig(resolvedConfigPath);
367
+ const cwd = typeof config.cwd === "string" ? resolve(config.cwd) : dirname(resolvedConfigPath);
368
+ const outDir = resolve(cwd, outDirOverride ?? config.outDir ?? "build");
369
+ const inputs = await expandConfigInputPaths(inputOverrides.length > 0 ? inputOverrides : config.input ?? [], cwd);
370
+ const css = config.css ?? {};
371
+ const cacheOptions = normalizeCacheOptions(config.cache, cwd, cacheOverride);
372
+ if (cssUnicodeRanges.length > 0) css.unicodeRanges = cssUnicodeRanges;
373
+ if (deliverySlices.length > 0) config.delivery = { slices: deliverySlices };
374
+ if (config.clean === true) await cleanOutputDirectory(cwd, outDir, [resolvedConfigPath, ...inputs]);
375
+ await buildIconfontCommand(inputs, outDir, fontFamily ?? css.fontFamily, fontPath ?? css.fontPath ?? "./", config.outputs ?? [], css, cacheOptions, css.unicodeRanges);
376
+ }
377
+ function expandConfigInputPaths(inputs, cwd) {
378
+ return expandInputPaths(inputs, cwd);
379
+ }
380
+ async function expandInputPaths(inputs, cwd) {
381
+ if (inputs.length === 0) throw new Error("build config requires at least one input");
382
+ const paths = [];
383
+ for (const input of inputs) {
384
+ if (typeof input !== "string") throw new TypeError("build config input entries must be file paths");
385
+ paths.push(...await expandInputPath(input, cwd));
386
+ }
387
+ return paths;
388
+ }
389
+ function outputFileName(outputs, format, fallback) {
390
+ const output = outputs.find((output) => output?.format === format);
391
+ if (typeof output?.fileName === "string") return output.fileName;
392
+ if (typeof output?.ext === "string") return `${basename(fallback, extname(fallback))}.${normalizeExtension(output.ext)}`;
393
+ return fallback;
394
+ }
395
+ function applySubsetOverrides(config, { basicText, subsetOptions } = {}) {
396
+ if (subsetOptions === void 0 || subsetOptions.text === void 0 && subsetOptions.unicodes.length === 0 && basicText !== true && subsetOptions.missingGlyphs === void 0) return;
397
+ config.subset = {
398
+ ...config.subset,
399
+ ...subsetOptions.text !== void 0 && { text: subsetOptions.text },
400
+ ...subsetOptions.unicodes.length > 0 && { unicodes: subsetOptions.unicodes },
401
+ ...basicText === true && { basicText },
402
+ ...subsetOptions.missingGlyphs !== void 0 && { missingGlyphs: subsetOptions.missingGlyphs }
403
+ };
404
+ }
405
+ async function buildConfigInput(input, outDir, outputFormats, config, cwd, cacheOptions, variationCoordinates) {
406
+ const subset = await resolveSubsetTextFile(config.subset ?? {}, cwd);
407
+ const baseName = basename(input, extname(input));
408
+ const slices = normalizeDeliverySlices(config.delivery?.slices ?? []);
409
+ const plugins = [normalizeToTtfPlugin(variationCoordinates)];
410
+ if (hasSubsetRequest(subset)) plugins.push(glyph(subset));
411
+ if (slices.length > 0) plugins.push(deliverySlices(slices));
412
+ if (Array.isArray(config.plugins)) plugins.push(...config.plugins);
413
+ await optimize({
414
+ cache: cacheOptions,
415
+ css: {
416
+ ...config.css,
417
+ fontFamily: config.css?.fontFamily ?? baseName
418
+ },
419
+ cwd,
420
+ input: [input],
421
+ outDir,
422
+ outputs: outputConfigsForFormats(config.outputs, outputFormats),
423
+ plugins,
424
+ preserveOriginal: config.preserveOriginal,
425
+ runtime: config.runtime
426
+ });
427
+ }
428
+ function hasSubsetRequest(subset) {
429
+ return subset.text !== void 0 || (subset.unicodes?.length ?? 0) > 0 || (subset.unicodeRanges?.length ?? 0) > 0 || subset.basicText === true;
430
+ }
431
+ function outputConfigsForFormats(outputs, formats) {
432
+ return formats.map((format) => {
433
+ const configuredOutput = outputs?.find((output) => {
434
+ return (typeof output === "string" ? output : output.format) === format;
435
+ });
436
+ if (typeof configuredOutput === "object" && configuredOutput !== null) return configuredOutput;
437
+ return format;
438
+ });
439
+ }
440
+ function normalizeCacheOptions(options, cwd, override) {
441
+ const configuredDir = typeof options === "object" && options !== null && typeof options.dir === "string" ? options.dir : DEFAULT_CACHE_DIR;
442
+ if (override === true) return {
443
+ dir: resolve(cwd, configuredDir),
444
+ enabled: true
445
+ };
446
+ if (override === false || options === void 0 || options === false) return {
447
+ dir: resolve(cwd, configuredDir),
448
+ enabled: false
449
+ };
450
+ if (options === true) return {
451
+ dir: resolve(cwd, DEFAULT_CACHE_DIR),
452
+ enabled: true
453
+ };
454
+ return {
455
+ dir: resolve(cwd, options.dir ?? DEFAULT_CACHE_DIR),
456
+ enabled: options.enabled ?? true
457
+ };
458
+ }
459
+ function cacheOverrideFromFlags(cache, noCache) {
460
+ if (cache) return true;
461
+ if (noCache) return false;
462
+ }
463
+ async function inspectCommand(args) {
464
+ const json = readFlag(args, ["--json"]);
465
+ assertNoUnexpectedArgs(args);
466
+ const [input] = args;
467
+ requireValue(input, "inspect requires an input font");
468
+ const info = inspect(await readFile(input));
469
+ if (json) process.stdout.write(`${JSON.stringify(info, void 0, 2)}\n`);
470
+ else process.stdout.write(`${input}: ${info.format}, ${info.size} bytes, ${info.metadata.glyphCount} glyphs\n`);
471
+ }
472
+ async function initCommand() {
473
+ try {
474
+ await writeFile(INIT_CONFIG_FILE, DEFAULT_INIT_CONFIG, { flag: "wx" });
475
+ } catch (error) {
476
+ if (hasErrorCode(error, "EEXIST")) throw new Error(`${INIT_CONFIG_FILE} already exists`, { cause: error });
477
+ throw error;
478
+ }
479
+ process.stdout.write(`created ${INIT_CONFIG_FILE}\n`);
480
+ }
481
+ async function subsetOptionsFromArgs(args) {
482
+ const subset = await resolveSubsetTextFile({
483
+ text: readOption(args, ["-t", "--text"]),
484
+ textFile: readOption(args, ["--text-file"]),
485
+ unicodes: parseUnicodes(readOption(args, ["--unicodes"]))
486
+ }, process.cwd());
487
+ return {
488
+ ...subset,
489
+ missingGlyphs: parseMissingGlyphPolicy(readOption(args, ["--missing-glyphs"])),
490
+ unicodes: subset.unicodes ?? []
491
+ };
492
+ }
493
+ function subsetWithCoverage(contents, options) {
494
+ const policy = options.missingGlyphs ?? "warn";
495
+ if (policy === "warn") {
496
+ const warning = missingGlyphMessage(analyzeCoverage(contents, options));
497
+ if (warning !== void 0 && emitWarnings) process.stderr.write(`warning: ${warning}\n`);
498
+ }
499
+ return subsetTtf(contents, {
500
+ ...options,
501
+ missingGlyphs: policy === "warn" ? "ignore" : policy
502
+ });
503
+ }
504
+ function missingGlyphMessage(report) {
505
+ if (report.missing.length === 0) return;
506
+ const visible = report.missing.slice(0, 16).map((codePoint) => `U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`).join(", ");
507
+ const remaining = report.missing.length - 16;
508
+ return `missing glyphs for requested Unicode code points: ${visible}${remaining > 0 ? `, and ${remaining} more` : ""}`;
509
+ }
510
+ function parseMissingGlyphPolicy(value) {
511
+ if (value === void 0) return;
512
+ const policy = value.trim().toLowerCase();
513
+ if (policy === "ignore" || policy === "warn" || policy === "error") return policy;
514
+ throw new Error(`missing glyph policy must be \`ignore\`, \`warn\`, or \`error\`: ${value}`);
515
+ }
516
+ async function resolveSubsetTextFile(subset, cwd) {
517
+ if (subset.textFile === void 0) return subset;
518
+ const fileText = await readFile(resolve(cwd, subset.textFile), "utf8");
519
+ return {
520
+ ...subset,
521
+ text: subset.text === void 0 ? fileText : `${subset.text}${fileText}`
522
+ };
523
+ }
524
+ async function expandInputPath(input, cwd) {
525
+ if (!isGlobPattern(input)) return [resolve(cwd, input)];
526
+ const matches = await glob(input, {
527
+ absolute: true,
528
+ cwd,
529
+ onlyFiles: true
530
+ });
531
+ if (matches.length === 0) throw new Error(`fontmin-rs input glob matched no files: ${input}`);
532
+ return matches.sort((left, right) => left.localeCompare(right));
533
+ }
534
+ function isGlobPattern(path) {
535
+ return /[*?[\]{}]/u.test(path);
536
+ }
537
+ function hasErrorCode(error, code) {
538
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
539
+ }
540
+ async function cleanOutputDirectory(cwd, outDir, protectedPaths) {
541
+ const root = resolve(cwd);
542
+ const target = resolve(outDir);
543
+ const targetIsInsideRoot = pathContains(root, target) && target !== root;
544
+ const targetContainsInput = protectedPaths.some((path) => pathContains(target, resolve(path)));
545
+ if (target === parse(target).root || pathContains(target, root) || targetContainsInput) throw new Error(`refusing to clean output directory ${target} because it is the project directory, an input ancestor, or a filesystem root`);
546
+ try {
547
+ const [realRoot, realTarget, ...realProtectedPaths] = await Promise.all([
548
+ realpath(root),
549
+ realpath(target),
550
+ ...protectedPaths.map((path) => realpath(resolve(path)))
551
+ ]);
552
+ if (realTarget === parse(realTarget).root || pathContains(realTarget, realRoot) || realProtectedPaths.some((path) => pathContains(realTarget, path)) || targetIsInsideRoot && !pathContains(realRoot, realTarget)) throw new Error(`refusing to clean output directory ${target} because its resolved location is unsafe for project ${root}`);
553
+ } catch (error) {
554
+ if (!hasErrorCode(error, "ENOENT")) throw error;
555
+ }
556
+ await rm(target, {
557
+ recursive: true,
558
+ force: true
559
+ });
560
+ }
561
+ function pathContains(parent, candidate) {
562
+ const childPath = relative(parent, candidate);
563
+ return childPath === "" || childPath !== ".." && !childPath.startsWith(`..${sep}`) && !isAbsolute(childPath);
564
+ }
565
+ function normalizeExtension(extension) {
566
+ const normalized = extension.replace(/^\.+/u, "");
567
+ if (normalized.length === 0 || normalized === ".." || normalized.includes("/") || normalized.includes("\\")) throw new Error(`output extension must be a file extension: ${extension}`);
568
+ return normalized;
569
+ }
570
+ function outputFormatsFromConfig(outputs) {
571
+ if (outputs === void 0) return [
572
+ "eot",
573
+ "woff",
574
+ "woff2",
575
+ "svg",
576
+ "css"
577
+ ];
578
+ return parseFormats(outputs.map((output) => typeof output === "string" ? output : output.format).join(","));
579
+ }
580
+ function outputFormatsForConfig(outputs, { formats, noOriginal, preset }) {
581
+ const filtered = filterOriginalOutput(formats === void 0 && preset === void 0 ? outputFormatsFromConfig(outputs) : outputFormatsFromArgs(formats, preset), noOriginal);
582
+ if (filtered.length === 0) throw new Error("build requires at least one non-original output format");
583
+ assertCssHasFontOutput(filtered);
584
+ return filtered;
585
+ }
586
+ function assertCssHasFontOutput(formats) {
587
+ if (formats.includes("css") && formats.every((format) => format === "css")) throw new Error("build CSS output requires at least one font format");
588
+ }
589
+ function outputFormatsFromArgs(formats, preset) {
590
+ if (formats !== void 0 && preset !== void 0) throw new Error("build accepts only one of --formats or --preset");
591
+ if (formats !== void 0) return parseFormats(formats);
592
+ if (preset !== void 0) return outputFormatsFromPreset(preset);
593
+ throw new Error("build requires --formats or --preset");
594
+ }
595
+ function outputFormatsFromPreset(value) {
596
+ const preset = value.trim().toLowerCase();
597
+ if (preset === "compat") return [
598
+ "eot",
599
+ "svg",
600
+ "woff",
601
+ "woff2",
602
+ "css"
603
+ ];
604
+ if (preset === "modern-web") return [
605
+ "woff2",
606
+ "woff",
607
+ "css"
608
+ ];
609
+ if (preset === "iconfont") return ["ttf", "css"];
610
+ throw new Error(`unsupported preset \`${preset}\``);
611
+ }
612
+ function isIconfontPreset(value) {
613
+ return value?.trim().toLowerCase() === "iconfont";
614
+ }
615
+ function filterOriginalOutput(formats, noOriginal) {
616
+ return noOriginal ? formats.filter((format) => format !== "ttf") : formats;
617
+ }
618
+ function parseUnicodeRanges(values) {
619
+ return values.map((value) => parseUnicodeRange(value));
620
+ }
621
+ function formatUnicodeEndpoint(codePoint) {
622
+ return codePoint.toString(16).toUpperCase().padStart(4, "0");
623
+ }
624
+ function parseUnicodeRange(value) {
625
+ const match = /^u\+(?<start>[0-9a-f]{1,6})(?:-(?<end>[0-9a-f]{1,6}))?$/iu.exec(value);
626
+ if (match?.groups === void 0) throw new Error(`invalid Unicode range: ${value}`);
627
+ const start = Number.parseInt(match.groups.start, 16);
628
+ const end = Number.parseInt(match.groups.end ?? match.groups.start, 16);
629
+ if (start > end || end > 1114111 || start <= 57343 && end >= 55296) throw new Error(`invalid Unicode range: ${value}`);
630
+ return start === end ? `U+${formatUnicodeEndpoint(start)}` : `U+${formatUnicodeEndpoint(start)}-${formatUnicodeEndpoint(end)}`;
631
+ }
632
+ function parseDeliverySlices(values) {
633
+ const slices = [];
634
+ for (const value of values) {
635
+ const separator = value.indexOf(":");
636
+ if (separator === -1) throw new Error(`delivery slice must use NAME:RANGE[,RANGE...]: ${value}`);
637
+ const name = value.slice(0, separator);
638
+ const unicodeRanges = value.slice(separator + 1).split(",");
639
+ const existing = slices.find((slice) => slice.name === name);
640
+ if (existing === void 0) slices.push({
641
+ name,
642
+ unicodeRanges
643
+ });
644
+ else existing.unicodeRanges.push(...unicodeRanges);
645
+ }
646
+ return normalizeDeliverySlices(slices);
647
+ }
648
+ function normalizeDeliverySlices(values) {
649
+ if (!Array.isArray(values)) throw new TypeError("delivery slices must be an array");
650
+ const names = /* @__PURE__ */ new Set();
651
+ return values.map((value, index) => {
652
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`delivery slice ${index + 1} must be an object`);
653
+ const { name, unicodeRanges } = value;
654
+ if (typeof name !== "string" || name.length === 0 || !/^[A-Za-z0-9_-]+$/u.test(name)) throw new Error(`invalid delivery slice name: ${name}`);
655
+ if (names.has(name)) throw new Error(`duplicate delivery slice name: ${name}`);
656
+ if (!Array.isArray(unicodeRanges) || unicodeRanges.length === 0) throw new Error(`delivery slice \`${name}\` requires at least one Unicode range`);
657
+ names.add(name);
658
+ return {
659
+ name,
660
+ unicodeRanges: parseUnicodeRanges(unicodeRanges)
661
+ };
662
+ });
663
+ }
664
+ function parseUnicodes(value) {
665
+ if (value === void 0) return [];
666
+ const unicodes = value.split(",").map((item) => parseUnicodeCodePoint(item));
667
+ if (unicodes.length === 0) throw new Error("expected at least one unicode code point");
668
+ return unicodes;
669
+ }
670
+ function parseUnicodeCodePoint(value) {
671
+ const item = value.trim();
672
+ if (item.length === 0) throw new Error("empty unicode code point in --unicodes");
673
+ let digits = item;
674
+ let radix = 10;
675
+ if (/^(?:0x|u\+)/iu.test(item)) {
676
+ digits = item.slice(2);
677
+ radix = 16;
678
+ }
679
+ if (!(radix === 16 ? /^[0-9a-f]+$/iu : /^[0-9]+$/u).test(digits)) throw new Error(`invalid unicode code point \`${item}\``);
680
+ const codePoint = Number.parseInt(digits, radix);
681
+ if (!Number.isInteger(codePoint) || codePoint > 4294967295) throw new Error(`invalid unicode code point \`${item}\``);
682
+ return codePoint;
683
+ }
684
+ function parseFormats(value) {
685
+ const formats = value.split(",").map((format) => format.trim().toLowerCase()).filter((format) => format.length > 0);
686
+ if (formats.length === 0) throw new Error("expected at least one output format");
687
+ for (const format of formats) if (format !== "ttf" && format !== "woff" && format !== "woff2" && format !== "eot" && format !== "svg" && format !== "css") throw new Error(`unsupported output format \`${format}\``);
688
+ return formats;
689
+ }
690
+ function convertFont(contents, format, options = {}) {
691
+ const normalized = format.toLowerCase();
692
+ if (normalized === "ttf") {
693
+ if (isTtf(contents)) return contents;
694
+ if (isWoff(contents)) return woffToTtf(contents);
695
+ if (isWoff2(contents)) return woff2ToTtf(contents);
696
+ if (isEot(contents)) return eotToTtf(contents);
697
+ if (isOtf(contents)) return otfToTtf(contents, { variationCoordinates: options.variationCoordinates ?? {} });
698
+ throw new Error("unsupported input format for TTF conversion");
699
+ }
700
+ const ttf = convertFont(contents, "ttf", options);
701
+ if (normalized === "woff") return ttfToWoff(ttf);
702
+ if (normalized === "woff2") return ttfToWoff2(ttf);
703
+ if (normalized === "eot") return ttfToEot(ttf);
704
+ if (normalized === "svg") return ttfToSvg(ttf);
705
+ throw new Error(`unsupported output format \`${format}\``);
706
+ }
707
+ function isTtf(contents) {
708
+ return contents[0] === 0 && contents[1] === 1 && contents[2] === 0 && contents[3] === 0 || contents.subarray(0, 4).toString("ascii") === "true";
709
+ }
710
+ function isOtf(contents) {
711
+ return contents.subarray(0, 4).toString("ascii") === "OTTO";
712
+ }
713
+ function isWoff(contents) {
714
+ return contents.subarray(0, 4).toString("ascii") === "wOFF";
715
+ }
716
+ function isWoff2(contents) {
717
+ return contents.subarray(0, 4).toString("ascii") === "wOF2";
718
+ }
719
+ function isEot(contents) {
720
+ return contents.byteLength >= 12 && (contents[8] === 1 && contents[9] === 0 && contents[10] === 2 && contents[11] === 0 || contents[8] === 2 && contents[9] === 0 && contents[10] === 2 && contents[11] === 0);
721
+ }
722
+ function readOption(args, names) {
723
+ const index = args.findIndex((arg) => names.includes(arg));
724
+ if (index === -1) return;
725
+ const [name] = args.splice(index, 1);
726
+ const [value] = args.splice(index, 1);
727
+ if (value === void 0 || value.startsWith("-")) throw new Error(`${name} requires a value`);
728
+ return value;
729
+ }
730
+ function readOptions(args, names) {
731
+ const values = [];
732
+ while (args.some((arg) => names.includes(arg))) values.push(readOption(args, names));
733
+ return values;
734
+ }
735
+ function readFlag(args, names) {
736
+ const index = args.findIndex((arg) => names.includes(arg));
737
+ if (index === -1) return false;
738
+ args.splice(index, 1);
739
+ return true;
740
+ }
741
+ function assertNoUnknownOptions(args) {
742
+ const option = args.find((arg) => arg.startsWith("-"));
743
+ if (option !== void 0) throw new Error(`unknown option \`${option}\``);
744
+ }
745
+ function assertNoUnexpectedArgs(args, maximumPositionals = 1) {
746
+ assertNoUnknownOptions(args);
747
+ if (args.length > maximumPositionals) throw new Error(`unexpected argument \`${args[maximumPositionals]}\``);
748
+ }
749
+ function parseVariations(values) {
750
+ const coordinates = {};
751
+ for (const value of values) {
752
+ const separator = value.indexOf("=");
753
+ if (separator === -1) throw new Error(`invalid variation \`${value}\`; expected TAG=VALUE`);
754
+ const tag = value.slice(0, separator);
755
+ const rawNumber = value.slice(separator + 1);
756
+ if (tag.length !== 4 || [...tag].some((character) => (character.codePointAt(0) ?? 128) > 127)) throw new Error(`invalid variation axis \`${tag}\`; expected four ASCII characters`);
757
+ if (Object.hasOwn(coordinates, tag)) throw new Error(`duplicate variation axis \`${tag}\``);
758
+ const number = Number(rawNumber);
759
+ if (rawNumber.length === 0 || !Number.isFinite(number)) throw new Error(`invalid variation value \`${rawNumber}\` for axis \`${tag}\``);
760
+ coordinates[tag] = number;
761
+ }
762
+ return coordinates;
763
+ }
764
+ function requireValue(value, message) {
765
+ if (value === void 0 || value.length === 0) throw new Error(message);
766
+ }
767
+ async function writeOutput(output, contents) {
768
+ await mkdir(dirname(output), { recursive: true });
769
+ await writeFile(output, contents);
770
+ }
771
+ function usage(stream) {
772
+ stream.write(`Usage:
773
+ fontmin-rs subset <INPUT> -o|--output <OUTPUT> (-t|--text <TEXT> | --text-file <FILE> | --unicodes <LIST> | -b|--basic-text) [--missing-glyphs <ignore|warn|error>]
774
+ fontmin-rs coverage <INPUT> (-t|--text <TEXT> | --text-file <FILE> | --unicodes <LIST> | -b|--basic-text) [--json]
775
+ fontmin-rs convert <INPUT> -f|--format <ttf|woff|woff2|eot|svg> -o|--output <OUTPUT> [--variation <TAG=VALUE>]...
776
+ fontmin-rs build <INPUT...> [-c|--config <CONFIG>] [-o|--out-dir <OUT_DIR>] [-t|--text <TEXT>] [--text-file <FILE>] [--unicodes <LIST>] [-b|--basic-text] [--missing-glyphs <ignore|warn|error>] [-d|--deflate-woff] [-T|--show-time] [--silent] [--cache] [--no-cache] [--css-glyph] [--css-unicode-range <RANGE>]... [--delivery-slice <NAME:RANGE[,RANGE...]>]... [--variation <TAG=VALUE>]... [--formats <FORMATS>] [--preset <compat|modern-web|iconfont>] [--no-original] [--font-family <FONT_FAMILY>] [--font-path <FONT_PATH>]
777
+ fontmin-rs bench <INPUT> [-t|--text <TEXT>] [--text-file <FILE>] [--unicodes <LIST>] [-b|--basic-text] [--json]
778
+ fontmin-rs inspect <INPUT> [--json]
779
+ fontmin-rs init
780
+ fontmin-rs doctor
781
+ `);
782
+ }
783
+ //#endregion
784
+ export { runCli };