fontmin-rs 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +30 -0
- package/bin/fontmin-rs.mjs +1425 -0
- package/dist/compat-B1CHDyqO.mjs +1257 -0
- package/dist/compat.d.mts +25 -0
- package/dist/compat.mjs +3 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +4 -0
- package/dist/plugins-BeueRRP6.d.mts +221 -0
- package/dist/plugins.d.mts +2 -0
- package/dist/plugins.mjs +149 -0
- package/dist/presets.d.mts +5 -0
- package/dist/presets.mjs +70 -0
- package/package.json +87 -0
|
@@ -0,0 +1,1425 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { basename, dirname, extname, join, resolve } from 'node:path'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
import {
|
|
8
|
+
eotToTtf,
|
|
9
|
+
generateFontFaceCss,
|
|
10
|
+
inspectFont,
|
|
11
|
+
subsetTtf,
|
|
12
|
+
svgsToTtf,
|
|
13
|
+
ttfToEot,
|
|
14
|
+
ttfToSvg,
|
|
15
|
+
ttfToWoff,
|
|
16
|
+
ttfToWoff2,
|
|
17
|
+
woff2ToTtf,
|
|
18
|
+
woffToTtf,
|
|
19
|
+
} from '@fontmin-rs/binding'
|
|
20
|
+
import { parse as parseJsonc } from 'jsonc-parser'
|
|
21
|
+
import { glob } from 'tinyglobby'
|
|
22
|
+
|
|
23
|
+
const DEFAULT_CONFIG_FILES = [
|
|
24
|
+
'fontmin.config.ts',
|
|
25
|
+
'fontmin.config.mts',
|
|
26
|
+
'fontmin.config.mjs',
|
|
27
|
+
'fontmin.config.cjs',
|
|
28
|
+
'fontmin.config.json',
|
|
29
|
+
'fontmin.config.jsonc',
|
|
30
|
+
]
|
|
31
|
+
const MODULE_CONFIG_EXTENSIONS = new Set(['.ts', '.mts', '.mjs', '.cjs'])
|
|
32
|
+
const INIT_CONFIG_FILE = 'fontmin.config.jsonc'
|
|
33
|
+
const CACHE_SCHEMA_VERSION = 'v1'
|
|
34
|
+
const FONTMIN_VERSION = '0.1.0-beta.1'
|
|
35
|
+
const DEFAULT_CACHE_DIR = 'node_modules/.cache/fontmin-rs'
|
|
36
|
+
const DEFAULT_INIT_CONFIG = `{
|
|
37
|
+
// Generated by fontmin-rs init.
|
|
38
|
+
"input": ["fonts/*.ttf"],
|
|
39
|
+
"outDir": "build",
|
|
40
|
+
"subset": {
|
|
41
|
+
"text": "Hello",
|
|
42
|
+
"basicText": true
|
|
43
|
+
},
|
|
44
|
+
"outputs": [
|
|
45
|
+
{ "format": "woff2" },
|
|
46
|
+
{ "format": "woff" },
|
|
47
|
+
{ "format": "css" }
|
|
48
|
+
],
|
|
49
|
+
"css": {
|
|
50
|
+
"fontFamily": "MyFont",
|
|
51
|
+
"fontPath": "./",
|
|
52
|
+
"fontDisplay": "swap"
|
|
53
|
+
},
|
|
54
|
+
"cache": {
|
|
55
|
+
"enabled": true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
const argv = process.argv.slice(2)
|
|
61
|
+
const command = argv[0]
|
|
62
|
+
const commandArgs = argv.slice(1)
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (command === 'subset') {
|
|
66
|
+
await subsetCommand(commandArgs)
|
|
67
|
+
} else if (command === 'convert') {
|
|
68
|
+
await convertCommand(commandArgs)
|
|
69
|
+
} else if (command === 'build') {
|
|
70
|
+
await buildCommand(commandArgs)
|
|
71
|
+
} else if (command === 'inspect') {
|
|
72
|
+
await inspectCommand(commandArgs)
|
|
73
|
+
} else if (command === 'init') {
|
|
74
|
+
await initCommand()
|
|
75
|
+
} else if (command === 'bench') {
|
|
76
|
+
await benchCommand(commandArgs)
|
|
77
|
+
} else {
|
|
78
|
+
usage()
|
|
79
|
+
process.exitCode = 1
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
83
|
+
process.exitCode = 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function subsetCommand(args) {
|
|
87
|
+
const output = readOption(args, ['-o', '--output'])
|
|
88
|
+
const subsetOptions = await subsetOptionsFromArgs(args)
|
|
89
|
+
const basicText = readFlag(args, ['-b', '--basic-text'])
|
|
90
|
+
const [input] = args
|
|
91
|
+
|
|
92
|
+
requireValue(input, 'subset requires an input font')
|
|
93
|
+
requireValue(output, 'subset requires -o, --output')
|
|
94
|
+
if (
|
|
95
|
+
subsetOptions.text === undefined &&
|
|
96
|
+
subsetOptions.unicodes.length === 0 &&
|
|
97
|
+
!basicText
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'subset requires --text, --text-file, --unicodes, or --basic-text',
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const contents = await readFile(input)
|
|
105
|
+
const subset = subsetTtf(contents, {
|
|
106
|
+
basicText,
|
|
107
|
+
text: subsetOptions.text,
|
|
108
|
+
unicodes: subsetOptions.unicodes,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
await writeOutput(output, subset)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function convertCommand(args) {
|
|
115
|
+
const output = readOption(args, ['-o', '--output'])
|
|
116
|
+
const format = readOption(args, ['-f', '--format'])
|
|
117
|
+
const [input] = args
|
|
118
|
+
|
|
119
|
+
requireValue(input, 'convert requires an input font')
|
|
120
|
+
requireValue(output, 'convert requires -o, --output')
|
|
121
|
+
requireValue(format, 'convert requires -f, --format')
|
|
122
|
+
|
|
123
|
+
const contents = await readFile(input)
|
|
124
|
+
const converted = convertFont(contents, format)
|
|
125
|
+
|
|
126
|
+
await writeOutput(output, converted)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function benchCommand(args) {
|
|
130
|
+
const json = readFlag(args, ['--json'])
|
|
131
|
+
const basicText = readFlag(args, ['-b', '--basic-text'])
|
|
132
|
+
const subsetOptions = await subsetOptionsFromArgs(args)
|
|
133
|
+
const [input] = args
|
|
134
|
+
|
|
135
|
+
requireValue(input, 'bench requires an input font')
|
|
136
|
+
if (
|
|
137
|
+
subsetOptions.text === undefined &&
|
|
138
|
+
subsetOptions.unicodes.length === 0 &&
|
|
139
|
+
!basicText
|
|
140
|
+
) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'bench requires --text, --text-file, --unicodes, or --basic-text',
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const contents = await readFile(input)
|
|
147
|
+
const startedAt = process.hrtime.bigint()
|
|
148
|
+
const subset = subsetTtf(contents, {
|
|
149
|
+
basicText,
|
|
150
|
+
text: subsetOptions.text,
|
|
151
|
+
unicodes: subsetOptions.unicodes,
|
|
152
|
+
})
|
|
153
|
+
const elapsedMs = Math.round(
|
|
154
|
+
Number(process.hrtime.bigint() - startedAt) / 1_000_000,
|
|
155
|
+
)
|
|
156
|
+
const report = {
|
|
157
|
+
elapsedMs,
|
|
158
|
+
inputBytes: contents.byteLength,
|
|
159
|
+
operation: 'subset',
|
|
160
|
+
outputBytes: subset.byteLength,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (json) {
|
|
164
|
+
process.stdout.write(`${JSON.stringify(report)}\n`)
|
|
165
|
+
} else {
|
|
166
|
+
process.stdout.write(
|
|
167
|
+
`fontmin-rs bench subset completed in ${elapsedMs} ms\ninput: ${report.inputBytes} bytes\noutput: ${report.outputBytes} bytes\n`,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function buildCommand(args) {
|
|
173
|
+
const showTime = readFlag(args, ['-T', '--show-time'])
|
|
174
|
+
const silent = readFlag(args, ['--silent'])
|
|
175
|
+
const startedAt = showTime && !silent ? process.hrtime.bigint() : undefined
|
|
176
|
+
|
|
177
|
+
await runBuildCommand(args)
|
|
178
|
+
reportShowTime(startedAt)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function runBuildCommand(args) {
|
|
182
|
+
const configPath = readOption(args, ['-c', '--config'])
|
|
183
|
+
const cache = readFlag(args, ['--cache'])
|
|
184
|
+
const noCache = readFlag(args, ['--no-cache'])
|
|
185
|
+
const noOriginal = readFlag(args, ['--no-original'])
|
|
186
|
+
readFlag(args, ['-d', '--deflate-woff'])
|
|
187
|
+
const cssGlyph = readFlag(args, ['--css-glyph'])
|
|
188
|
+
const outDir = readOption(args, ['-o', '--out-dir'])
|
|
189
|
+
const formats = readOption(args, ['--formats'])
|
|
190
|
+
const preset = readOption(args, ['--preset'])
|
|
191
|
+
const basicText = readFlag(args, ['-b', '--basic-text'])
|
|
192
|
+
const fontFamily = readOption(args, ['--font-family'])
|
|
193
|
+
const fontPath = readOption(args, ['--font-path'])
|
|
194
|
+
const subsetOptions = await subsetOptionsFromArgs(args)
|
|
195
|
+
|
|
196
|
+
if (cache && noCache) {
|
|
197
|
+
throw new Error('build accepts only one of --cache or --no-cache')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const cacheOverride = cacheOverrideFromFlags(cache, noCache)
|
|
201
|
+
|
|
202
|
+
if (configPath !== undefined) {
|
|
203
|
+
if (isIconfontPreset(preset)) {
|
|
204
|
+
await buildIconfontConfigCommand(configPath, {
|
|
205
|
+
fontFamily,
|
|
206
|
+
fontPath,
|
|
207
|
+
formats,
|
|
208
|
+
inputs: [...args],
|
|
209
|
+
cacheOverride,
|
|
210
|
+
outDir,
|
|
211
|
+
})
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await buildConfigCommand(configPath, {
|
|
216
|
+
basicText,
|
|
217
|
+
cacheOverride,
|
|
218
|
+
cssGlyph,
|
|
219
|
+
fontFamily,
|
|
220
|
+
fontPath,
|
|
221
|
+
formats,
|
|
222
|
+
inputs: [...args],
|
|
223
|
+
noOriginal,
|
|
224
|
+
outDir,
|
|
225
|
+
preset,
|
|
226
|
+
subsetOptions,
|
|
227
|
+
})
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (args.length === 0) {
|
|
232
|
+
const foundConfigPath = await findConfig()
|
|
233
|
+
|
|
234
|
+
if (isIconfontPreset(preset)) {
|
|
235
|
+
await buildIconfontConfigCommand(foundConfigPath, {
|
|
236
|
+
fontFamily,
|
|
237
|
+
fontPath,
|
|
238
|
+
formats,
|
|
239
|
+
cacheOverride,
|
|
240
|
+
outDir,
|
|
241
|
+
})
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await buildConfigCommand(foundConfigPath, {
|
|
246
|
+
basicText,
|
|
247
|
+
cacheOverride,
|
|
248
|
+
cssGlyph,
|
|
249
|
+
fontFamily,
|
|
250
|
+
fontPath,
|
|
251
|
+
formats,
|
|
252
|
+
noOriginal,
|
|
253
|
+
outDir,
|
|
254
|
+
preset,
|
|
255
|
+
subsetOptions,
|
|
256
|
+
})
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const inputPatterns = [...args]
|
|
261
|
+
const [input] = inputPatterns
|
|
262
|
+
|
|
263
|
+
requireValue(input, 'build requires an input font')
|
|
264
|
+
requireValue(outDir, 'build requires -o, --out-dir')
|
|
265
|
+
|
|
266
|
+
if (isIconfontPreset(preset)) {
|
|
267
|
+
if (formats !== undefined) {
|
|
268
|
+
throw new Error('build accepts only one of --formats or --preset')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const inputs = await expandInputPaths(inputPatterns, process.cwd())
|
|
272
|
+
const cacheOptions = normalizeCacheOptions(
|
|
273
|
+
undefined,
|
|
274
|
+
process.cwd(),
|
|
275
|
+
cacheOverride,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
await buildIconfontCommand(
|
|
279
|
+
inputs,
|
|
280
|
+
outDir,
|
|
281
|
+
fontFamily,
|
|
282
|
+
fontPath ?? './',
|
|
283
|
+
[],
|
|
284
|
+
{},
|
|
285
|
+
cacheOptions,
|
|
286
|
+
)
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const inputs = await expandInputPaths(inputPatterns, process.cwd())
|
|
291
|
+
const outputFormats = filterOriginalOutput(
|
|
292
|
+
outputFormatsFromArgs(formats, preset),
|
|
293
|
+
noOriginal,
|
|
294
|
+
)
|
|
295
|
+
const cacheOptions = normalizeCacheOptions(
|
|
296
|
+
undefined,
|
|
297
|
+
process.cwd(),
|
|
298
|
+
cacheOverride,
|
|
299
|
+
)
|
|
300
|
+
if (outputFormats.length === 0) {
|
|
301
|
+
throw new Error('build requires at least one non-original output format')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
await mkdir(outDir, { recursive: true })
|
|
305
|
+
|
|
306
|
+
for (const input of inputs) {
|
|
307
|
+
await buildDirectInput(input, outDir, {
|
|
308
|
+
basicText,
|
|
309
|
+
cacheOptions,
|
|
310
|
+
cssGlyph,
|
|
311
|
+
fontFamily,
|
|
312
|
+
fontPath,
|
|
313
|
+
outputFormats,
|
|
314
|
+
subsetOptions,
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function buildDirectInput(
|
|
320
|
+
input,
|
|
321
|
+
outDir,
|
|
322
|
+
{
|
|
323
|
+
basicText,
|
|
324
|
+
cacheOptions,
|
|
325
|
+
cssGlyph,
|
|
326
|
+
fontFamily,
|
|
327
|
+
fontPath,
|
|
328
|
+
outputFormats,
|
|
329
|
+
subsetOptions,
|
|
330
|
+
},
|
|
331
|
+
) {
|
|
332
|
+
const contents = await readFile(input)
|
|
333
|
+
const baseName = basename(input, extname(input))
|
|
334
|
+
const cacheKey = cacheOptions.enabled
|
|
335
|
+
? cacheKeyForBuildInput({
|
|
336
|
+
contents,
|
|
337
|
+
input,
|
|
338
|
+
kind: 'direct',
|
|
339
|
+
options: {
|
|
340
|
+
basicText,
|
|
341
|
+
cssGlyph,
|
|
342
|
+
fontFamily,
|
|
343
|
+
fontPath,
|
|
344
|
+
outputFormats,
|
|
345
|
+
subset: subsetOptions,
|
|
346
|
+
},
|
|
347
|
+
})
|
|
348
|
+
: undefined
|
|
349
|
+
const cachedOutputs =
|
|
350
|
+
cacheKey === undefined
|
|
351
|
+
? undefined
|
|
352
|
+
: await readCachedBuildOutputs(cacheOptions.dir, cacheKey)
|
|
353
|
+
|
|
354
|
+
if (cachedOutputs !== undefined) {
|
|
355
|
+
await writeBuildOutputs(outDir, cachedOutputs)
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const source =
|
|
360
|
+
subsetOptions.text === undefined &&
|
|
361
|
+
subsetOptions.unicodes.length === 0 &&
|
|
362
|
+
!basicText
|
|
363
|
+
? contents
|
|
364
|
+
: subsetTtf(contents, {
|
|
365
|
+
basicText,
|
|
366
|
+
text: subsetOptions.text,
|
|
367
|
+
unicodes: subsetOptions.unicodes,
|
|
368
|
+
})
|
|
369
|
+
const cssGlyphs = cssGlyph ? cssGlyphsFromSubset(subsetOptions) : []
|
|
370
|
+
const cssSources = []
|
|
371
|
+
const outputs = []
|
|
372
|
+
|
|
373
|
+
for (const format of outputFormats.filter(format => format !== 'css')) {
|
|
374
|
+
const fileName = `${baseName}.${format}`
|
|
375
|
+
const output = convertFont(source, format)
|
|
376
|
+
|
|
377
|
+
outputs.push({ contents: output, fileName })
|
|
378
|
+
cssSources.push({
|
|
379
|
+
...(cssGlyphs.length > 0 && { glyphs: cssGlyphs }),
|
|
380
|
+
fileName,
|
|
381
|
+
format,
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (outputFormats.includes('css')) {
|
|
386
|
+
if (cssSources.length === 0) {
|
|
387
|
+
throw new Error('build CSS output requires at least one font format')
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
outputs.push({
|
|
391
|
+
contents: Buffer.from(
|
|
392
|
+
generateFontFaceCss(cssSources, {
|
|
393
|
+
fontFamily: fontFamily ?? baseName,
|
|
394
|
+
fontPath: fontPath ?? './',
|
|
395
|
+
glyph: cssGlyph,
|
|
396
|
+
}),
|
|
397
|
+
),
|
|
398
|
+
fileName: `${baseName}.css`,
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
await writeBuildOutputs(outDir, outputs)
|
|
403
|
+
|
|
404
|
+
if (cacheKey !== undefined) {
|
|
405
|
+
await writeCachedBuildOutputs(cacheOptions.dir, cacheKey, outputs)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function reportShowTime(startedAt) {
|
|
410
|
+
if (startedAt === undefined) {
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000
|
|
415
|
+
|
|
416
|
+
process.stdout.write(
|
|
417
|
+
`fontmin-rs build completed in ${Math.round(elapsedMs)} ms\n`,
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function buildIconfontCommand(
|
|
422
|
+
inputs,
|
|
423
|
+
outDir,
|
|
424
|
+
fontFamily,
|
|
425
|
+
fontPath,
|
|
426
|
+
outputs = [],
|
|
427
|
+
css = {},
|
|
428
|
+
cacheOptions = normalizeCacheOptions(undefined, process.cwd()),
|
|
429
|
+
) {
|
|
430
|
+
if (inputs.length === 0) {
|
|
431
|
+
throw new Error('build requires at least one input font')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const family = fontFamily ?? 'iconfont'
|
|
435
|
+
const icons = await Promise.all(
|
|
436
|
+
inputs.map(async input => ({
|
|
437
|
+
contents: await readFile(input, 'utf8'),
|
|
438
|
+
name: basename(input, extname(input)),
|
|
439
|
+
})),
|
|
440
|
+
)
|
|
441
|
+
const cacheKey = cacheOptions.enabled
|
|
442
|
+
? cacheKeyForIconfontBuild({
|
|
443
|
+
icons,
|
|
444
|
+
inputs,
|
|
445
|
+
options: {
|
|
446
|
+
css,
|
|
447
|
+
fontFamily,
|
|
448
|
+
fontPath,
|
|
449
|
+
outputs,
|
|
450
|
+
},
|
|
451
|
+
})
|
|
452
|
+
: undefined
|
|
453
|
+
const cachedOutputs =
|
|
454
|
+
cacheKey === undefined
|
|
455
|
+
? undefined
|
|
456
|
+
: await readCachedBuildOutputs(cacheOptions.dir, cacheKey)
|
|
457
|
+
|
|
458
|
+
if (cachedOutputs !== undefined) {
|
|
459
|
+
await writeBuildOutputs(outDir, cachedOutputs)
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const ttf = svgsToTtf(icons, {
|
|
464
|
+
fontName: family,
|
|
465
|
+
})
|
|
466
|
+
const fileName = outputFileName(outputs, 'ttf', 'iconfont.ttf')
|
|
467
|
+
const cssFileName = outputFileName(
|
|
468
|
+
outputs,
|
|
469
|
+
'css',
|
|
470
|
+
`${basename(fileName, extname(fileName))}.${css.target ?? 'css'}`,
|
|
471
|
+
)
|
|
472
|
+
const glyphs = icons.map((icon, index) => ({
|
|
473
|
+
name: icon.name,
|
|
474
|
+
unicode: 57_345 + index,
|
|
475
|
+
}))
|
|
476
|
+
const buildOutputs = [
|
|
477
|
+
{
|
|
478
|
+
contents: ttf,
|
|
479
|
+
fileName,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
contents: Buffer.from(
|
|
483
|
+
generateFontFaceCss(
|
|
484
|
+
[
|
|
485
|
+
{
|
|
486
|
+
...(css.base64 === true && { contents: ttf }),
|
|
487
|
+
fileName,
|
|
488
|
+
format: 'ttf',
|
|
489
|
+
glyphs,
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
{
|
|
493
|
+
asFileName: css.asFileName ?? true,
|
|
494
|
+
base64: css.base64,
|
|
495
|
+
fontDisplay: css.fontDisplay,
|
|
496
|
+
fontFamily: family,
|
|
497
|
+
fontPath,
|
|
498
|
+
glyph: true,
|
|
499
|
+
iconPrefix: css.iconPrefix,
|
|
500
|
+
local: css.local,
|
|
501
|
+
},
|
|
502
|
+
),
|
|
503
|
+
),
|
|
504
|
+
fileName: cssFileName,
|
|
505
|
+
},
|
|
506
|
+
]
|
|
507
|
+
|
|
508
|
+
await writeBuildOutputs(outDir, buildOutputs)
|
|
509
|
+
|
|
510
|
+
if (cacheKey !== undefined) {
|
|
511
|
+
await writeCachedBuildOutputs(cacheOptions.dir, cacheKey, buildOutputs)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function buildConfigCommand(
|
|
516
|
+
configPath,
|
|
517
|
+
{
|
|
518
|
+
basicText,
|
|
519
|
+
cacheOverride,
|
|
520
|
+
cssGlyph,
|
|
521
|
+
fontFamily,
|
|
522
|
+
fontPath,
|
|
523
|
+
formats,
|
|
524
|
+
inputs: inputOverrides = [],
|
|
525
|
+
noOriginal,
|
|
526
|
+
outDir: outDirOverride,
|
|
527
|
+
preset,
|
|
528
|
+
subsetOptions,
|
|
529
|
+
} = {},
|
|
530
|
+
) {
|
|
531
|
+
const resolvedConfigPath = resolve(configPath)
|
|
532
|
+
const config = await readConfig(resolvedConfigPath)
|
|
533
|
+
const cwd =
|
|
534
|
+
typeof config.cwd === 'string'
|
|
535
|
+
? resolve(config.cwd)
|
|
536
|
+
: dirname(resolvedConfigPath)
|
|
537
|
+
const inputs =
|
|
538
|
+
inputOverrides.length > 0 ? inputOverrides : (config.input ?? [])
|
|
539
|
+
const outDir = resolve(cwd, outDirOverride ?? config.outDir ?? 'build')
|
|
540
|
+
const outputFormats = outputFormatsForConfig(config.outputs, {
|
|
541
|
+
formats,
|
|
542
|
+
noOriginal,
|
|
543
|
+
preset,
|
|
544
|
+
})
|
|
545
|
+
const cacheOptions = normalizeCacheOptions(config.cache, cwd, cacheOverride)
|
|
546
|
+
|
|
547
|
+
if (inputs.length === 0) {
|
|
548
|
+
throw new Error('build config requires at least one input')
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (fontFamily !== undefined || fontPath !== undefined || cssGlyph === true) {
|
|
552
|
+
config.css = {
|
|
553
|
+
...config.css,
|
|
554
|
+
...(cssGlyph === true && { glyph: true }),
|
|
555
|
+
...(fontFamily !== undefined && { fontFamily }),
|
|
556
|
+
...(fontPath !== undefined && { fontPath }),
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
applySubsetOverrides(config, { basicText, subsetOptions })
|
|
561
|
+
|
|
562
|
+
if (config.clean === true) {
|
|
563
|
+
await rm(outDir, { recursive: true, force: true })
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
await mkdir(outDir, { recursive: true })
|
|
567
|
+
|
|
568
|
+
for (const input of inputs) {
|
|
569
|
+
if (typeof input !== 'string') {
|
|
570
|
+
throw new TypeError('build config input entries must be file paths')
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
for (const inputPath of await expandInputPath(input, cwd)) {
|
|
574
|
+
await buildConfigInput(
|
|
575
|
+
inputPath,
|
|
576
|
+
outDir,
|
|
577
|
+
outputFormats,
|
|
578
|
+
config,
|
|
579
|
+
cwd,
|
|
580
|
+
cacheOptions,
|
|
581
|
+
)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function buildIconfontConfigCommand(
|
|
587
|
+
configPath,
|
|
588
|
+
{
|
|
589
|
+
cacheOverride,
|
|
590
|
+
fontFamily,
|
|
591
|
+
fontPath,
|
|
592
|
+
formats,
|
|
593
|
+
inputs: inputOverrides = [],
|
|
594
|
+
outDir: outDirOverride,
|
|
595
|
+
},
|
|
596
|
+
) {
|
|
597
|
+
if (formats !== undefined) {
|
|
598
|
+
throw new Error('build accepts only one of --formats or --preset')
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const resolvedConfigPath = resolve(configPath)
|
|
602
|
+
const config = await readConfig(resolvedConfigPath)
|
|
603
|
+
const cwd =
|
|
604
|
+
typeof config.cwd === 'string'
|
|
605
|
+
? resolve(config.cwd)
|
|
606
|
+
: dirname(resolvedConfigPath)
|
|
607
|
+
const outDir = resolve(cwd, outDirOverride ?? config.outDir ?? 'build')
|
|
608
|
+
const inputs = await expandConfigInputPaths(
|
|
609
|
+
inputOverrides.length > 0 ? inputOverrides : (config.input ?? []),
|
|
610
|
+
cwd,
|
|
611
|
+
)
|
|
612
|
+
const css = config.css ?? {}
|
|
613
|
+
const cacheOptions = normalizeCacheOptions(config.cache, cwd, cacheOverride)
|
|
614
|
+
|
|
615
|
+
if (config.clean === true) {
|
|
616
|
+
await rm(outDir, { recursive: true, force: true })
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
await buildIconfontCommand(
|
|
620
|
+
inputs,
|
|
621
|
+
outDir,
|
|
622
|
+
fontFamily ?? css.fontFamily,
|
|
623
|
+
fontPath ?? css.fontPath ?? './',
|
|
624
|
+
config.outputs ?? [],
|
|
625
|
+
css,
|
|
626
|
+
cacheOptions,
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function expandConfigInputPaths(inputs, cwd) {
|
|
631
|
+
return expandInputPaths(inputs, cwd)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function expandInputPaths(inputs, cwd) {
|
|
635
|
+
if (inputs.length === 0) {
|
|
636
|
+
throw new Error('build config requires at least one input')
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const paths = []
|
|
640
|
+
|
|
641
|
+
for (const input of inputs) {
|
|
642
|
+
if (typeof input !== 'string') {
|
|
643
|
+
throw new TypeError('build config input entries must be file paths')
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
paths.push(...(await expandInputPath(input, cwd)))
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return paths
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function outputFileName(outputs, format, fallback) {
|
|
653
|
+
const output = outputs.find(output => output?.format === format)
|
|
654
|
+
|
|
655
|
+
if (typeof output?.fileName === 'string') {
|
|
656
|
+
return output.fileName
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (typeof output?.ext === 'string') {
|
|
660
|
+
return `${basename(fallback, extname(fallback))}.${output.ext.replace(/^\./u, '')}`
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return fallback
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function applySubsetOverrides(config, { basicText, subsetOptions } = {}) {
|
|
667
|
+
if (
|
|
668
|
+
subsetOptions === undefined ||
|
|
669
|
+
(subsetOptions.text === undefined &&
|
|
670
|
+
subsetOptions.unicodes.length === 0 &&
|
|
671
|
+
basicText !== true)
|
|
672
|
+
) {
|
|
673
|
+
return
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
config.subset = {
|
|
677
|
+
...config.subset,
|
|
678
|
+
...(subsetOptions.text !== undefined && { text: subsetOptions.text }),
|
|
679
|
+
...(subsetOptions.unicodes.length > 0 && {
|
|
680
|
+
unicodes: subsetOptions.unicodes,
|
|
681
|
+
}),
|
|
682
|
+
...(basicText === true && { basicText }),
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function buildConfigInput(
|
|
687
|
+
input,
|
|
688
|
+
outDir,
|
|
689
|
+
outputFormats,
|
|
690
|
+
config,
|
|
691
|
+
cwd,
|
|
692
|
+
cacheOptions,
|
|
693
|
+
) {
|
|
694
|
+
const contents = await readFile(input)
|
|
695
|
+
const subset = await resolveSubsetTextFile(config.subset ?? {}, cwd)
|
|
696
|
+
const unicodes = subset.unicodes ?? []
|
|
697
|
+
const cacheKey = cacheOptions.enabled
|
|
698
|
+
? cacheKeyForBuildInput({
|
|
699
|
+
contents,
|
|
700
|
+
input,
|
|
701
|
+
kind: 'config',
|
|
702
|
+
options: {
|
|
703
|
+
css: config.css,
|
|
704
|
+
outputFormats,
|
|
705
|
+
outputs: config.outputs,
|
|
706
|
+
subset,
|
|
707
|
+
},
|
|
708
|
+
})
|
|
709
|
+
: undefined
|
|
710
|
+
const cachedOutputs =
|
|
711
|
+
cacheKey === undefined
|
|
712
|
+
? undefined
|
|
713
|
+
: await readCachedBuildOutputs(cacheOptions.dir, cacheKey)
|
|
714
|
+
|
|
715
|
+
if (cachedOutputs !== undefined) {
|
|
716
|
+
await writeBuildOutputs(outDir, cachedOutputs)
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const source =
|
|
721
|
+
subset.text === undefined &&
|
|
722
|
+
unicodes.length === 0 &&
|
|
723
|
+
subset.basicText !== true
|
|
724
|
+
? contents
|
|
725
|
+
: subsetTtf(contents, {
|
|
726
|
+
basicText: subset.basicText,
|
|
727
|
+
text: subset.text,
|
|
728
|
+
unicodes,
|
|
729
|
+
})
|
|
730
|
+
const baseName = basename(input, extname(input))
|
|
731
|
+
const css = config.css ?? {}
|
|
732
|
+
const cssGlyphs = css.glyph === true ? cssGlyphsFromSubset(subset) : []
|
|
733
|
+
const cssSources = []
|
|
734
|
+
const outputs = config.outputs ?? []
|
|
735
|
+
const buildOutputs = []
|
|
736
|
+
|
|
737
|
+
for (const format of outputFormats.filter(format => format !== 'css')) {
|
|
738
|
+
const fileName = outputFileName(outputs, format, `${baseName}.${format}`)
|
|
739
|
+
const output = convertFont(source, format)
|
|
740
|
+
|
|
741
|
+
buildOutputs.push({ contents: output, fileName })
|
|
742
|
+
cssSources.push({
|
|
743
|
+
...(css.base64 === true && { contents: output }),
|
|
744
|
+
...(cssGlyphs.length > 0 && { glyphs: cssGlyphs }),
|
|
745
|
+
fileName,
|
|
746
|
+
format,
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (outputFormats.includes('css')) {
|
|
751
|
+
if (cssSources.length === 0) {
|
|
752
|
+
throw new Error('build CSS output requires at least one font format')
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const firstCssSource = cssSources[0]
|
|
756
|
+
const cssFileName = outputFileName(
|
|
757
|
+
outputs,
|
|
758
|
+
'css',
|
|
759
|
+
`${basename(firstCssSource.fileName, extname(firstCssSource.fileName))}.${css.target ?? 'css'}`,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
buildOutputs.push({
|
|
763
|
+
contents: Buffer.from(
|
|
764
|
+
generateFontFaceCss(cssSources, {
|
|
765
|
+
asFileName: css.asFileName,
|
|
766
|
+
base64: css.base64,
|
|
767
|
+
fontDisplay: css.fontDisplay,
|
|
768
|
+
fontFamily: css.fontFamily ?? baseName,
|
|
769
|
+
fontPath: css.fontPath ?? './',
|
|
770
|
+
glyph: css.glyph,
|
|
771
|
+
iconPrefix: css.iconPrefix,
|
|
772
|
+
local: css.local,
|
|
773
|
+
}),
|
|
774
|
+
),
|
|
775
|
+
fileName: cssFileName,
|
|
776
|
+
})
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
await writeBuildOutputs(outDir, buildOutputs)
|
|
780
|
+
|
|
781
|
+
if (cacheKey !== undefined) {
|
|
782
|
+
await writeCachedBuildOutputs(cacheOptions.dir, cacheKey, buildOutputs)
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function cssGlyphsFromSubset(subset) {
|
|
787
|
+
const seen = new Set()
|
|
788
|
+
const glyphs = []
|
|
789
|
+
|
|
790
|
+
for (const character of subset.text ?? '') {
|
|
791
|
+
const unicode = character.codePointAt(0)
|
|
792
|
+
|
|
793
|
+
if (unicode !== undefined && !seen.has(unicode)) {
|
|
794
|
+
seen.add(unicode)
|
|
795
|
+
glyphs.push({ unicode })
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
for (const unicode of subset.unicodes ?? []) {
|
|
800
|
+
if (!seen.has(unicode)) {
|
|
801
|
+
seen.add(unicode)
|
|
802
|
+
glyphs.push({ unicode })
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return glyphs
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function writeBuildOutputs(outDir, outputs) {
|
|
810
|
+
await mkdir(outDir, { recursive: true })
|
|
811
|
+
|
|
812
|
+
for (const output of outputs) {
|
|
813
|
+
await writeFile(join(outDir, output.fileName), output.contents)
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function readCachedBuildOutputs(cacheDir, key) {
|
|
818
|
+
let manifest = null
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
manifest = JSON.parse(
|
|
822
|
+
await readFile(cacheManifestPath(cacheDir, key), 'utf8'),
|
|
823
|
+
)
|
|
824
|
+
} catch {
|
|
825
|
+
return
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (manifest.version !== CACHE_SCHEMA_VERSION || manifest.key !== key) {
|
|
829
|
+
return
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const entryDir = cacheEntryDir(cacheDir, key)
|
|
833
|
+
const outputs = []
|
|
834
|
+
|
|
835
|
+
try {
|
|
836
|
+
for (const record of manifest.outputs) {
|
|
837
|
+
outputs.push({
|
|
838
|
+
contents: await readFile(join(entryDir, record.cacheFileName)),
|
|
839
|
+
fileName: record.fileName,
|
|
840
|
+
})
|
|
841
|
+
}
|
|
842
|
+
} catch {
|
|
843
|
+
return
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return outputs
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function writeCachedBuildOutputs(cacheDir, key, outputs) {
|
|
850
|
+
const entryDir = cacheEntryDir(cacheDir, key)
|
|
851
|
+
const records = []
|
|
852
|
+
|
|
853
|
+
await mkdir(entryDir, { recursive: true })
|
|
854
|
+
|
|
855
|
+
for (const [index, output] of outputs.entries()) {
|
|
856
|
+
const extension = extname(output.fileName).replace(/^\./u, '') || 'bin'
|
|
857
|
+
const cacheFileName = `${String(index).padStart(3, '0')}.${extension}`
|
|
858
|
+
|
|
859
|
+
await writeFile(join(entryDir, cacheFileName), output.contents)
|
|
860
|
+
records.push({
|
|
861
|
+
cacheFileName,
|
|
862
|
+
fileName: output.fileName,
|
|
863
|
+
})
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
await writeFile(
|
|
867
|
+
cacheManifestPath(cacheDir, key),
|
|
868
|
+
`${JSON.stringify(
|
|
869
|
+
{
|
|
870
|
+
key,
|
|
871
|
+
outputs: records,
|
|
872
|
+
version: CACHE_SCHEMA_VERSION,
|
|
873
|
+
},
|
|
874
|
+
undefined,
|
|
875
|
+
2,
|
|
876
|
+
)}\n`,
|
|
877
|
+
)
|
|
878
|
+
await updateBuildCacheIndex(cacheDir, key, records)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async function updateBuildCacheIndex(cacheDir, key, outputs) {
|
|
882
|
+
const indexPath = cacheIndexPath(cacheDir)
|
|
883
|
+
let index = {
|
|
884
|
+
entries: {},
|
|
885
|
+
version: CACHE_SCHEMA_VERSION,
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
try {
|
|
889
|
+
index = JSON.parse(await readFile(indexPath, 'utf8'))
|
|
890
|
+
} catch {
|
|
891
|
+
// A missing or corrupted cache index can be rebuilt from the next writes.
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (index.version !== CACHE_SCHEMA_VERSION) {
|
|
895
|
+
index = {
|
|
896
|
+
entries: {},
|
|
897
|
+
version: CACHE_SCHEMA_VERSION,
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
index.entries[key] = {
|
|
902
|
+
outputs: outputs.map(output => output.fileName),
|
|
903
|
+
updatedAt: new Date().toISOString(),
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
await mkdir(dirname(indexPath), { recursive: true })
|
|
907
|
+
await writeFile(indexPath, `${JSON.stringify(index, undefined, 2)}\n`)
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function normalizeCacheOptions(options, cwd, override) {
|
|
911
|
+
const configuredDir =
|
|
912
|
+
typeof options === 'object' &&
|
|
913
|
+
options !== null &&
|
|
914
|
+
typeof options.dir === 'string'
|
|
915
|
+
? options.dir
|
|
916
|
+
: DEFAULT_CACHE_DIR
|
|
917
|
+
|
|
918
|
+
if (override === true) {
|
|
919
|
+
return {
|
|
920
|
+
dir: resolve(cwd, configuredDir),
|
|
921
|
+
enabled: true,
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (override === false || options === undefined || options === false) {
|
|
926
|
+
return {
|
|
927
|
+
dir: resolve(cwd, configuredDir),
|
|
928
|
+
enabled: false,
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (options === true) {
|
|
933
|
+
return {
|
|
934
|
+
dir: resolve(cwd, DEFAULT_CACHE_DIR),
|
|
935
|
+
enabled: true,
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return {
|
|
940
|
+
dir: resolve(cwd, options.dir ?? DEFAULT_CACHE_DIR),
|
|
941
|
+
enabled: options.enabled ?? true,
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function cacheOverrideFromFlags(cache, noCache) {
|
|
946
|
+
if (cache) {
|
|
947
|
+
return true
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (noCache) {
|
|
951
|
+
return false
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function cacheKeyForBuildInput({ contents, input, kind, options }) {
|
|
956
|
+
return sha256(
|
|
957
|
+
stableStringify({
|
|
958
|
+
fontminVersion: FONTMIN_VERSION,
|
|
959
|
+
input: {
|
|
960
|
+
hash: sha256(contents),
|
|
961
|
+
path: input,
|
|
962
|
+
},
|
|
963
|
+
kind,
|
|
964
|
+
options,
|
|
965
|
+
schema: CACHE_SCHEMA_VERSION,
|
|
966
|
+
}),
|
|
967
|
+
)
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function cacheKeyForIconfontBuild({ icons, inputs, options }) {
|
|
971
|
+
return sha256(
|
|
972
|
+
stableStringify({
|
|
973
|
+
fontminVersion: FONTMIN_VERSION,
|
|
974
|
+
icons: icons.map((icon, index) => ({
|
|
975
|
+
hash: sha256(icon.contents),
|
|
976
|
+
input: inputs[index],
|
|
977
|
+
name: icon.name,
|
|
978
|
+
})),
|
|
979
|
+
kind: 'iconfont',
|
|
980
|
+
options,
|
|
981
|
+
schema: CACHE_SCHEMA_VERSION,
|
|
982
|
+
}),
|
|
983
|
+
)
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function cacheRoot(cacheDir) {
|
|
987
|
+
return join(cacheDir, CACHE_SCHEMA_VERSION)
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function cacheIndexPath(cacheDir) {
|
|
991
|
+
return join(cacheRoot(cacheDir), 'index.json')
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function cacheEntryDir(cacheDir, key) {
|
|
995
|
+
return join(cacheRoot(cacheDir), key)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function cacheManifestPath(cacheDir, key) {
|
|
999
|
+
return join(cacheEntryDir(cacheDir, key), 'index.json')
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function sha256(input) {
|
|
1003
|
+
return createHash('sha256').update(input).digest('hex')
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function stableStringify(value) {
|
|
1007
|
+
if (value === null || typeof value !== 'object') {
|
|
1008
|
+
return JSON.stringify(value)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (Array.isArray(value)) {
|
|
1012
|
+
return `[${value.map(item => stableStringify(item)).join(',')}]`
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const record = value
|
|
1016
|
+
const entries = Object.keys(record)
|
|
1017
|
+
.sort()
|
|
1018
|
+
.map(key => {
|
|
1019
|
+
return `${JSON.stringify(key)}:${stableStringify(record[key])}`
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
return `{${entries.join(',')}}`
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
async function inspectCommand(args) {
|
|
1026
|
+
const json = readFlag(args, ['--json'])
|
|
1027
|
+
const [input] = args
|
|
1028
|
+
|
|
1029
|
+
requireValue(input, 'inspect requires an input font')
|
|
1030
|
+
|
|
1031
|
+
const contents = await readFile(input)
|
|
1032
|
+
const info = inspectFont(contents)
|
|
1033
|
+
|
|
1034
|
+
if (json) {
|
|
1035
|
+
console.log(JSON.stringify(info, undefined, 2))
|
|
1036
|
+
} else {
|
|
1037
|
+
console.log(
|
|
1038
|
+
`${input}: ${info.format}, ${info.size} bytes, ${info.metadata.glyphCount} glyphs`,
|
|
1039
|
+
)
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function initCommand() {
|
|
1044
|
+
try {
|
|
1045
|
+
await writeFile(INIT_CONFIG_FILE, DEFAULT_INIT_CONFIG, { flag: 'wx' })
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
if (hasErrorCode(error, 'EEXIST')) {
|
|
1048
|
+
throw new Error(`${INIT_CONFIG_FILE} already exists`, { cause: error })
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
throw error
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
process.stdout.write(`created ${INIT_CONFIG_FILE}\n`)
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async function subsetOptionsFromArgs(args) {
|
|
1058
|
+
const subset = await resolveSubsetTextFile(
|
|
1059
|
+
{
|
|
1060
|
+
text: readOption(args, ['-t', '--text']),
|
|
1061
|
+
textFile: readOption(args, ['--text-file']),
|
|
1062
|
+
unicodes: parseUnicodes(readOption(args, ['--unicodes'])),
|
|
1063
|
+
},
|
|
1064
|
+
process.cwd(),
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
return {
|
|
1068
|
+
...subset,
|
|
1069
|
+
unicodes: subset.unicodes ?? [],
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function resolveSubsetTextFile(subset, cwd) {
|
|
1074
|
+
if (subset.textFile === undefined) {
|
|
1075
|
+
return subset
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const fileText = await readFile(resolve(cwd, subset.textFile), 'utf8')
|
|
1079
|
+
|
|
1080
|
+
return {
|
|
1081
|
+
...subset,
|
|
1082
|
+
text: subset.text === undefined ? fileText : `${subset.text}${fileText}`,
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async function readConfig(configPath) {
|
|
1087
|
+
const contents = await readFile(configPath, 'utf8')
|
|
1088
|
+
const extension = extname(configPath)
|
|
1089
|
+
|
|
1090
|
+
if (extension === '.json') {
|
|
1091
|
+
return JSON.parse(contents)
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (extension === '.jsonc') {
|
|
1095
|
+
return parseJsonc(contents)
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (MODULE_CONFIG_EXTENSIONS.has(extension)) {
|
|
1099
|
+
const configModule = await import(pathToFileURL(configPath).href)
|
|
1100
|
+
const loadedConfig = configModule.default ?? configModule.config
|
|
1101
|
+
|
|
1102
|
+
if (loadedConfig === undefined) {
|
|
1103
|
+
throw new Error(`config file \`${configPath}\` does not export a config`)
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return typeof loadedConfig === 'function'
|
|
1107
|
+
? await loadedConfig()
|
|
1108
|
+
: loadedConfig
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
throw new Error(`unsupported config extension \`${extension}\``)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async function findConfig(cwd = process.cwd()) {
|
|
1115
|
+
for (const fileName of DEFAULT_CONFIG_FILES) {
|
|
1116
|
+
const configPath = resolve(cwd, fileName)
|
|
1117
|
+
|
|
1118
|
+
if (await isFile(configPath)) {
|
|
1119
|
+
return configPath
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
throw new Error(`could not find fontmin config in ${cwd}`)
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
async function isFile(path) {
|
|
1127
|
+
try {
|
|
1128
|
+
return (await stat(path)).isFile()
|
|
1129
|
+
} catch {
|
|
1130
|
+
return false
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function expandInputPath(input, cwd) {
|
|
1135
|
+
if (!isGlobPattern(input)) {
|
|
1136
|
+
return [resolve(cwd, input)]
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const matches = await glob(input, {
|
|
1140
|
+
absolute: true,
|
|
1141
|
+
cwd,
|
|
1142
|
+
onlyFiles: true,
|
|
1143
|
+
})
|
|
1144
|
+
|
|
1145
|
+
if (matches.length === 0) {
|
|
1146
|
+
throw new Error(`fontmin-rs input glob matched no files: ${input}`)
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
return matches.sort((left, right) => left.localeCompare(right))
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function isGlobPattern(path) {
|
|
1153
|
+
return /[*?[\]{}]/u.test(path)
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function hasErrorCode(error, code) {
|
|
1157
|
+
return (
|
|
1158
|
+
typeof error === 'object' &&
|
|
1159
|
+
error !== null &&
|
|
1160
|
+
'code' in error &&
|
|
1161
|
+
error.code === code
|
|
1162
|
+
)
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function outputFormatsFromConfig(outputs) {
|
|
1166
|
+
if (outputs === undefined) {
|
|
1167
|
+
return ['eot', 'woff', 'woff2', 'svg', 'css']
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const formats = outputs.map(output =>
|
|
1171
|
+
typeof output === 'string' ? output : output.format,
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
return parseFormats(formats.join(','))
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function outputFormatsForConfig(outputs, { formats, noOriginal, preset }) {
|
|
1178
|
+
const outputFormats =
|
|
1179
|
+
formats === undefined && preset === undefined
|
|
1180
|
+
? outputFormatsFromConfig(outputs)
|
|
1181
|
+
: outputFormatsFromArgs(formats, preset)
|
|
1182
|
+
const filtered = filterOriginalOutput(outputFormats, noOriginal)
|
|
1183
|
+
|
|
1184
|
+
if (filtered.length === 0) {
|
|
1185
|
+
throw new Error('build requires at least one non-original output format')
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return filtered
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function outputFormatsFromArgs(formats, preset) {
|
|
1192
|
+
if (formats !== undefined && preset !== undefined) {
|
|
1193
|
+
throw new Error('build accepts only one of --formats or --preset')
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (formats !== undefined) {
|
|
1197
|
+
return parseFormats(formats)
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
if (preset !== undefined) {
|
|
1201
|
+
return outputFormatsFromPreset(preset)
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
throw new Error('build requires --formats or --preset')
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function outputFormatsFromPreset(value) {
|
|
1208
|
+
const preset = value.trim().toLowerCase()
|
|
1209
|
+
|
|
1210
|
+
if (preset === 'compat') {
|
|
1211
|
+
return ['eot', 'svg', 'woff', 'woff2', 'css']
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (preset === 'modern-web') {
|
|
1215
|
+
return ['woff2', 'woff', 'css']
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (preset === 'iconfont') {
|
|
1219
|
+
return ['ttf', 'css']
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
throw new Error(`unsupported preset \`${preset}\``)
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function isIconfontPreset(value) {
|
|
1226
|
+
return value?.trim().toLowerCase() === 'iconfont'
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function filterOriginalOutput(formats, noOriginal) {
|
|
1230
|
+
return noOriginal ? formats.filter(format => format !== 'ttf') : formats
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function parseUnicodes(value) {
|
|
1234
|
+
if (value === undefined) {
|
|
1235
|
+
return []
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const unicodes = value.split(',').map(item => parseUnicodeCodePoint(item))
|
|
1239
|
+
|
|
1240
|
+
if (unicodes.length === 0) {
|
|
1241
|
+
throw new Error('expected at least one unicode code point')
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return unicodes
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function parseUnicodeCodePoint(value) {
|
|
1248
|
+
const item = value.trim()
|
|
1249
|
+
|
|
1250
|
+
if (item.length === 0) {
|
|
1251
|
+
throw new Error('empty unicode code point in --unicodes')
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
let digits = item
|
|
1255
|
+
let radix = 10
|
|
1256
|
+
|
|
1257
|
+
if (/^(?:0x|u\+)/iu.test(item)) {
|
|
1258
|
+
digits = item.slice(2)
|
|
1259
|
+
radix = 16
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const validDigits = radix === 16 ? /^[0-9a-f]+$/iu : /^[0-9]+$/u
|
|
1263
|
+
|
|
1264
|
+
if (!validDigits.test(digits)) {
|
|
1265
|
+
throw new Error(`invalid unicode code point \`${item}\``)
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const codePoint = Number.parseInt(digits, radix)
|
|
1269
|
+
|
|
1270
|
+
if (!Number.isInteger(codePoint) || codePoint > 4_294_967_295) {
|
|
1271
|
+
throw new Error(`invalid unicode code point \`${item}\``)
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
return codePoint
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function parseFormats(value) {
|
|
1278
|
+
const formats = value
|
|
1279
|
+
.split(',')
|
|
1280
|
+
.map(format => format.trim().toLowerCase())
|
|
1281
|
+
.filter(format => format.length > 0)
|
|
1282
|
+
|
|
1283
|
+
if (formats.length === 0) {
|
|
1284
|
+
throw new Error('expected at least one output format')
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
for (const format of formats) {
|
|
1288
|
+
if (
|
|
1289
|
+
format !== 'ttf' &&
|
|
1290
|
+
format !== 'woff' &&
|
|
1291
|
+
format !== 'woff2' &&
|
|
1292
|
+
format !== 'eot' &&
|
|
1293
|
+
format !== 'svg' &&
|
|
1294
|
+
format !== 'css'
|
|
1295
|
+
) {
|
|
1296
|
+
throw new Error(`unsupported output format \`${format}\``)
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
return formats
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function convertFont(contents, format) {
|
|
1304
|
+
const normalized = format.toLowerCase()
|
|
1305
|
+
|
|
1306
|
+
if (normalized === 'ttf') {
|
|
1307
|
+
if (isTtf(contents)) {
|
|
1308
|
+
return contents
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (isWoff(contents)) {
|
|
1312
|
+
return woffToTtf(contents)
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (isWoff2(contents)) {
|
|
1316
|
+
return woff2ToTtf(contents)
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (isEot(contents)) {
|
|
1320
|
+
return eotToTtf(contents)
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
throw new Error('unsupported input format for TTF conversion')
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (normalized === 'woff') {
|
|
1327
|
+
return ttfToWoff(contents)
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (normalized === 'woff2') {
|
|
1331
|
+
return ttfToWoff2(contents)
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (normalized === 'eot') {
|
|
1335
|
+
return ttfToEot(contents)
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (normalized === 'svg') {
|
|
1339
|
+
return ttfToSvg(contents)
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
throw new Error(`unsupported output format \`${format}\``)
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function isTtf(contents) {
|
|
1346
|
+
return (
|
|
1347
|
+
(contents[0] === 0x00 &&
|
|
1348
|
+
contents[1] === 0x01 &&
|
|
1349
|
+
contents[2] === 0x00 &&
|
|
1350
|
+
contents[3] === 0x00) ||
|
|
1351
|
+
contents.subarray(0, 4).toString('ascii') === 'true'
|
|
1352
|
+
)
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function isWoff(contents) {
|
|
1356
|
+
return contents.subarray(0, 4).toString('ascii') === 'wOFF'
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function isWoff2(contents) {
|
|
1360
|
+
return contents.subarray(0, 4).toString('ascii') === 'wOF2'
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function isEot(contents) {
|
|
1364
|
+
return (
|
|
1365
|
+
contents.byteLength >= 12 &&
|
|
1366
|
+
((contents[8] === 0x01 &&
|
|
1367
|
+
contents[9] === 0x00 &&
|
|
1368
|
+
contents[10] === 0x02 &&
|
|
1369
|
+
contents[11] === 0x00) ||
|
|
1370
|
+
(contents[8] === 0x02 &&
|
|
1371
|
+
contents[9] === 0x00 &&
|
|
1372
|
+
contents[10] === 0x02 &&
|
|
1373
|
+
contents[11] === 0x00))
|
|
1374
|
+
)
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function readOption(args, names) {
|
|
1378
|
+
const index = args.findIndex(arg => names.includes(arg))
|
|
1379
|
+
|
|
1380
|
+
if (index === -1) {
|
|
1381
|
+
return
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const [name] = args.splice(index, 1)
|
|
1385
|
+
const [value] = args.splice(index, 1)
|
|
1386
|
+
|
|
1387
|
+
if (value === undefined || value.startsWith('-')) {
|
|
1388
|
+
throw new Error(`${name} requires a value`)
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
return value
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function readFlag(args, names) {
|
|
1395
|
+
const index = args.findIndex(arg => names.includes(arg))
|
|
1396
|
+
|
|
1397
|
+
if (index === -1) {
|
|
1398
|
+
return false
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
args.splice(index, 1)
|
|
1402
|
+
return true
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function requireValue(value, message) {
|
|
1406
|
+
if (value === undefined || value.length === 0) {
|
|
1407
|
+
throw new Error(message)
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async function writeOutput(output, contents) {
|
|
1412
|
+
await mkdir(dirname(output), { recursive: true })
|
|
1413
|
+
await writeFile(output, contents)
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function usage() {
|
|
1417
|
+
console.error(`Usage:
|
|
1418
|
+
fontmin-rs subset <input.ttf> -o <output.ttf> (-t|--text <text> | --text-file <file> | --unicodes <list> | -b|--basic-text)
|
|
1419
|
+
fontmin-rs convert <input.ttf> -f <ttf|woff|woff2|eot|svg> -o <output>
|
|
1420
|
+
fontmin-rs build <input.ttf> -o <out-dir> --formats <ttf,woff,woff2,eot,svg,css> [-t|--text <text>] [--text-file <file>] [--unicodes <list>] [-b|--basic-text] [-d|--deflate-woff] [-T|--show-time] [--silent] [--no-original] [--cache|--no-cache]
|
|
1421
|
+
fontmin-rs build <input.ttf> -o <out-dir> --preset <compat|modern-web|iconfont> [-t|--text <text>] [--text-file <file>] [--unicodes <list>] [-b|--basic-text] [-d|--deflate-woff] [-T|--show-time] [--silent] [--no-original] [--cache|--no-cache]
|
|
1422
|
+
fontmin-rs bench <input.ttf> [-t|--text <text>] [--text-file <file>] [--unicodes <list>] [-b|--basic-text] [--json]
|
|
1423
|
+
fontmin-rs inspect <input.ttf> [--json]
|
|
1424
|
+
fontmin-rs init`)
|
|
1425
|
+
}
|