build-fonts.mjs 7.5 KB

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