build.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. // @ts-check
  2. /*
  3. Produces production builds and stitches together d.ts files.
  4. To specify the package to build, simply pass its name and the desired build
  5. formats to output (defaults to `buildOptions.formats` specified in that package,
  6. or ["esm-bundler", "cjs"]):
  7. ```
  8. # name supports fuzzy match. will build all packages with name containing "dom":
  9. nr build dom
  10. # specify the format to output
  11. nr build vue -f cjs
  12. # to specify multiple formats, separate with "+":
  13. nr build vue -f esm-bundler+esm-browser
  14. ```
  15. */
  16. import fs from 'node:fs'
  17. import { parseArgs } from 'node:util'
  18. import path from 'node:path'
  19. import { brotliCompressSync, gzipSync } from 'node:zlib'
  20. import pico from 'picocolors'
  21. import { cpus } from 'node:os'
  22. import { targets as allTargets, exec, fuzzyMatchTarget } from './utils.js'
  23. import { scanEnums } from './inline-enums.js'
  24. import prettyBytes from 'pretty-bytes'
  25. import { spawnSync } from 'node:child_process'
  26. const commit = spawnSync('git', ['rev-parse', '--short=7', 'HEAD'])
  27. .stdout.toString()
  28. .trim()
  29. const { values, positionals: targets } = parseArgs({
  30. allowPositionals: true,
  31. options: {
  32. formats: {
  33. type: 'string',
  34. short: 'f',
  35. },
  36. devOnly: {
  37. type: 'boolean',
  38. short: 'd',
  39. },
  40. prodOnly: {
  41. type: 'boolean',
  42. short: 'p',
  43. },
  44. withTypes: {
  45. type: 'boolean',
  46. short: 't',
  47. },
  48. sourceMap: {
  49. type: 'boolean',
  50. short: 's',
  51. },
  52. release: {
  53. type: 'boolean',
  54. },
  55. all: {
  56. type: 'boolean',
  57. short: 'a',
  58. },
  59. size: {
  60. type: 'boolean',
  61. },
  62. },
  63. })
  64. const {
  65. formats,
  66. all: buildAllMatching,
  67. devOnly,
  68. prodOnly,
  69. withTypes: buildTypes,
  70. sourceMap,
  71. release: isRelease,
  72. size: writeSize,
  73. } = values
  74. const sizeDir = path.resolve('temp/size')
  75. run()
  76. async function run() {
  77. if (writeSize) fs.mkdirSync(sizeDir, { recursive: true })
  78. const removeCache = scanEnums()
  79. try {
  80. const resolvedTargets = targets.length
  81. ? fuzzyMatchTarget(targets, buildAllMatching)
  82. : allTargets
  83. await buildAll(resolvedTargets)
  84. await checkAllSizes(resolvedTargets)
  85. if (buildTypes) {
  86. await exec(
  87. 'pnpm',
  88. [
  89. 'run',
  90. 'build-dts',
  91. ...(targets.length
  92. ? ['--environment', `TARGETS:${resolvedTargets.join(',')}`]
  93. : []),
  94. ],
  95. {
  96. stdio: 'inherit',
  97. },
  98. )
  99. }
  100. } finally {
  101. removeCache()
  102. }
  103. }
  104. /**
  105. * Builds all the targets in parallel.
  106. * @param {Array<string>} targets - An array of targets to build.
  107. * @returns {Promise<void>} - A promise representing the build process.
  108. */
  109. async function buildAll(targets) {
  110. await runParallel(cpus().length, targets, build)
  111. }
  112. /**
  113. * Runs iterator function in parallel.
  114. * @template T - The type of items in the data source
  115. * @param {number} maxConcurrency - The maximum concurrency.
  116. * @param {Array<T>} source - The data source
  117. * @param {(item: T) => Promise<void>} iteratorFn - The iteratorFn
  118. * @returns {Promise<void[]>} - A Promise array containing all iteration results.
  119. */
  120. async function runParallel(maxConcurrency, source, iteratorFn) {
  121. /**@type {Promise<void>[]} */
  122. const ret = []
  123. /**@type {Promise<void>[]} */
  124. const executing = []
  125. for (const item of source) {
  126. const p = Promise.resolve().then(() => iteratorFn(item))
  127. ret.push(p)
  128. if (maxConcurrency <= source.length) {
  129. const e = p.then(() => {
  130. executing.splice(executing.indexOf(e), 1)
  131. })
  132. executing.push(e)
  133. if (executing.length >= maxConcurrency) {
  134. await Promise.race(executing)
  135. }
  136. }
  137. }
  138. return Promise.all(ret)
  139. }
  140. const privatePackages = fs.readdirSync('packages-private')
  141. /**
  142. * Builds the target.
  143. * @param {string} target - The target to build.
  144. * @returns {Promise<void>} - A promise representing the build process.
  145. */
  146. async function build(target) {
  147. const pkgBase = privatePackages.includes(target)
  148. ? `packages-private`
  149. : `packages`
  150. const pkgDir = path.resolve(`${pkgBase}/${target}`)
  151. const pkg = JSON.parse(fs.readFileSync(`${pkgDir}/package.json`, 'utf-8'))
  152. // if this is a full build (no specific targets), ignore private packages
  153. if ((isRelease || !targets.length) && pkg.private) {
  154. return
  155. }
  156. let resolvedFormats
  157. if (formats) {
  158. const isNegation = formats.startsWith('~')
  159. resolvedFormats = (isNegation ? formats.slice(1) : formats).split('+')
  160. const pkgFormats = pkg.buildOptions?.formats
  161. if (pkgFormats) {
  162. if (isNegation) {
  163. resolvedFormats = pkgFormats.filter(f => !resolvedFormats.includes(f))
  164. } else {
  165. resolvedFormats = resolvedFormats.filter(f => pkgFormats.includes(f))
  166. }
  167. }
  168. if (!resolvedFormats.length) {
  169. return
  170. }
  171. }
  172. // if building a specific format, do not remove dist.
  173. if (!formats && fs.existsSync(`${pkgDir}/dist`)) {
  174. fs.rmSync(`${pkgDir}/dist`, { recursive: true })
  175. }
  176. const env =
  177. (pkg.buildOptions && pkg.buildOptions.env) ||
  178. (devOnly ? 'development' : 'production')
  179. await exec(
  180. 'rollup',
  181. [
  182. '-c',
  183. '--environment',
  184. [
  185. `COMMIT:${commit}`,
  186. `NODE_ENV:${env}`,
  187. `TARGET:${target}`,
  188. resolvedFormats ? `FORMATS:${resolvedFormats.join('+')}` : ``,
  189. prodOnly ? `PROD_ONLY:true` : ``,
  190. sourceMap ? `SOURCE_MAP:true` : ``,
  191. ]
  192. .filter(Boolean)
  193. .join(','),
  194. ],
  195. { stdio: 'inherit' },
  196. )
  197. }
  198. /**
  199. * Checks the sizes of all targets.
  200. * @param {string[]} targets - The targets to check sizes for.
  201. * @returns {Promise<void>}
  202. */
  203. async function checkAllSizes(targets) {
  204. if (
  205. devOnly ||
  206. (formats && (formats.startsWith('~') || !formats.includes('global')))
  207. ) {
  208. return
  209. }
  210. console.log()
  211. for (const target of targets) {
  212. await checkSize(target)
  213. }
  214. console.log()
  215. }
  216. /**
  217. * Checks the size of a target.
  218. * @param {string} target - The target to check the size for.
  219. * @returns {Promise<void>}
  220. */
  221. async function checkSize(target) {
  222. const pkgDir = path.resolve(`packages/${target}`)
  223. await checkFileSize(`${pkgDir}/dist/${target}.global.prod.js`)
  224. if (!formats || formats.includes('global-runtime')) {
  225. await checkFileSize(`${pkgDir}/dist/${target}.runtime.global.prod.js`)
  226. }
  227. }
  228. /**
  229. * Checks the file size.
  230. * @param {string} filePath - The path of the file to check the size for.
  231. * @returns {Promise<void>}
  232. */
  233. async function checkFileSize(filePath) {
  234. if (!fs.existsSync(filePath)) {
  235. return
  236. }
  237. const file = fs.readFileSync(filePath)
  238. const fileName = path.basename(filePath)
  239. const gzipped = gzipSync(file)
  240. const brotli = brotliCompressSync(file)
  241. console.log(
  242. `${pico.gray(pico.bold(fileName))} min:${prettyBytes(
  243. file.length,
  244. )} / gzip:${prettyBytes(gzipped.length)} / brotli:${prettyBytes(
  245. brotli.length,
  246. )}`,
  247. )
  248. if (writeSize)
  249. fs.writeFileSync(
  250. path.resolve(sizeDir, `${fileName}.json`),
  251. JSON.stringify({
  252. file: fileName,
  253. size: file.length,
  254. gzip: gzipped.length,
  255. brotli: brotli.length,
  256. }),
  257. 'utf-8',
  258. )
  259. }