build-fonts.mjs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import fs from 'node:fs/promises'
  2. import { createWriteStream } from 'node:fs'
  3. import { basename, join } from 'node:path'
  4. import { tmpdir } from 'node:os'
  5. import { execSync } from 'node:child_process'
  6. import yauzl from 'yauzl'
  7. // brew install woff2
  8. const families = [
  9. {
  10. folder: 'dm-mono',
  11. url: 'https://github.com/googlefonts/dm-mono',
  12. fonts: [
  13. 'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-Italic.ttf',
  14. 'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-Medium.ttf',
  15. 'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-MediumItalic.ttf',
  16. 'https://github.com/googlefonts/dm-mono/raw/refs/heads/main/exports/DMMono-Regular.ttf',
  17. ],
  18. },
  19. {
  20. folder: 'font-awesome',
  21. url: 'https://fontawesome.com/v4/',
  22. archive: 'https://fontawesome.com/v4/assets/font-awesome-4.7.0.zip',
  23. fonts: ['font-awesome-4.7.0/fonts/fontawesome-webfont.woff2'],
  24. },
  25. {
  26. folder: 'inter',
  27. url: 'https://github.com/rsms/inter/releases/tag/v4.1',
  28. archive:
  29. 'https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip',
  30. fonts: ['web/Inter-Regular.woff2', 'web/Inter-SemiBold.woff2'],
  31. license:
  32. 'https://raw.githubusercontent.com/rsms/inter/refs/heads/master/LICENSE.txt',
  33. },
  34. {
  35. folder: 'lato',
  36. url: 'https://www.latofonts.com/',
  37. archive: 'https://www.latofonts.com/files/Lato2OFLWeb.zip',
  38. fonts: [
  39. 'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-Bold.woff2',
  40. 'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-BoldItalic.woff2',
  41. 'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-Italic.woff2',
  42. 'Lato2OFLWeb/LatoLatin/fonts/LatoLatin-Regular.woff2',
  43. ],
  44. },
  45. {
  46. folder: 'merriweather',
  47. url: 'https://github.com/SorkinType/Merriweather',
  48. fonts: [
  49. 'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-Bold.woff2',
  50. 'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-BoldItalic.woff2',
  51. 'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-Italic.woff2',
  52. 'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/fonts/webfonts/Merriweather-Regular.woff2',
  53. ],
  54. license:
  55. 'https://github.com/SorkinType/Merriweather/raw/refs/heads/master/OFL.txt',
  56. },
  57. {
  58. folder: 'noto-sans',
  59. url: 'https://notofonts.github.io/#latin-greek-cyrillic',
  60. fonts: [
  61. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Italic.ttf',
  62. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Medium.ttf',
  63. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-MediumItalic.ttf',
  64. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-Regular.ttf',
  65. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-SemiBold.ttf',
  66. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSans/full/ttf/NotoSans-SemiBoldItalic.ttf',
  67. ],
  68. license:
  69. 'https://raw.githubusercontent.com/notofonts/latin-greek-cyrillic/refs/heads/main/OFL.txt',
  70. },
  71. {
  72. folder: 'noto-serif',
  73. url: 'https://notofonts.github.io/#latin-greek-cyrillic',
  74. fonts: [
  75. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSerif/unhinted/slim-variable-ttf/NotoSerif%5Bwght%5D.ttf',
  76. 'https://github.com/notofonts/notofonts.github.io/raw/refs/heads/main/fonts/NotoSerif/unhinted/slim-variable-ttf/NotoSerif-Italic%5Bwght%5D.ttf',
  77. ],
  78. license:
  79. 'https://raw.githubusercontent.com/notofonts/latin-greek-cyrillic/refs/heads/main/OFL.txt',
  80. },
  81. {
  82. folder: 'open-dyslexic-mono',
  83. url: 'https://github.com/antijingoist/opendyslexic',
  84. fonts: [
  85. 'https://github.com/antijingoist/open-dyslexic/blob/master/otf/OpenDyslexicMono-Regular.otf',
  86. ],
  87. license: 'https://github.com/antijingoist/opendyslexic/blob/master/OFL.txt',
  88. },
  89. {
  90. folder: 'open-sans',
  91. url: 'https://github.com/googlefonts/opensans',
  92. fonts: [
  93. 'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-Bold.ttf',
  94. 'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-Light.ttf',
  95. 'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-Regular.ttf',
  96. 'https://github.com/googlefonts/opensans/raw/refs/heads/main/fonts/ttf/OpenSans-SemiBold.ttf',
  97. ],
  98. license:
  99. 'https://raw.githubusercontent.com/googlefonts/opensans/refs/heads/main/OFL.txt',
  100. },
  101. {
  102. folder: 'source-code-pro',
  103. url: 'https://github.com/adobe-fonts/source-code-pro',
  104. fonts: [
  105. 'https://github.com/adobe-fonts/source-code-pro/raw/refs/heads/release/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2',
  106. ],
  107. license:
  108. 'https://raw.githubusercontent.com/adobe-fonts/source-code-pro/refs/heads/release/LICENSE.md',
  109. },
  110. {
  111. folder: 'STIXTwoMath',
  112. url: 'https://github.com/stipub/stixfonts',
  113. fonts: [
  114. 'https://github.com/stipub/stixfonts/raw/refs/heads/master/fonts/static_otf_woff2/STIXTwoMath-Regular.woff2',
  115. ],
  116. license:
  117. 'https://raw.githubusercontent.com/stipub/stixfonts/refs/heads/master/OFL.txt',
  118. },
  119. ]
  120. const fetchFile = async (url, path) => {
  121. console.log(`${url}\n${path}`)
  122. const response = await fetch(url)
  123. if (!response.ok) {
  124. throw new Error(response.statusText)
  125. }
  126. await fs.writeFile(path, response.body)
  127. }
  128. for (const family of families) {
  129. if (!family.folder) {
  130. throw new Error('Missing family information')
  131. }
  132. await fs.mkdir(family.folder, { recursive: true })
  133. const fonts = new Set(family.fonts)
  134. if (family.archive) {
  135. const dir = await fs.mkdtemp(join(tmpdir(), 'fonts-'))
  136. const filename = decodeURIComponent(basename(family.archive))
  137. const path = `${dir}/${filename}`
  138. await fetchFile(family.archive, path)
  139. await new Promise((resolve, reject) => {
  140. yauzl.open(path, { lazyEntries: true }, (err, zipfile) => {
  141. if (err) {
  142. reject(err)
  143. } else {
  144. zipfile.on('entry', entry => {
  145. if (fonts.has(entry.fileName)) {
  146. console.log(entry.fileName)
  147. zipfile.openReadStream(entry, (err, readStream) => {
  148. if (err) {
  149. reject(err)
  150. } else {
  151. const path = `${family.folder}/${basename(entry.fileName)}`
  152. const output = createWriteStream(path)
  153. readStream.on('end', async () => {
  154. output.close()
  155. if (path.endsWith('.ttf')) {
  156. execSync(`woff2_compress "${path}"`)
  157. await fs.unlink(path)
  158. }
  159. zipfile.readEntry()
  160. })
  161. readStream.pipe(output)
  162. }
  163. })
  164. } else {
  165. zipfile.readEntry()
  166. }
  167. })
  168. zipfile.on('error', reject)
  169. zipfile.on('end', resolve)
  170. zipfile.readEntry()
  171. }
  172. })
  173. })
  174. await fs.unlink(path)
  175. } else {
  176. for (const url of fonts) {
  177. const filename = decodeURIComponent(basename(url))
  178. const path = `${family.folder}/${filename}`
  179. await fetchFile(url, path)
  180. if (path.endsWith('.ttf')) {
  181. execSync(`woff2_compress "${path}"`)
  182. await fs.unlink(path)
  183. }
  184. }
  185. }
  186. if (family.license) {
  187. const url = family.license
  188. const filename = decodeURIComponent(basename(url))
  189. const path = `${family.folder}/${filename}`
  190. await fetchFile(url, path)
  191. }
  192. }