rolldown.dts.config.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. // @ts-check
  2. import assert from 'node:assert/strict'
  3. import { parseSync } from 'oxc-parser'
  4. import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
  5. import { dts } from 'rolldown-plugin-dts'
  6. import { createRequire } from 'node:module'
  7. import { fileURLToPath } from 'node:url'
  8. import path from 'node:path'
  9. if (!existsSync('temp/packages')) {
  10. console.warn(
  11. 'no temp dts files found. run `tsc -p tsconfig.build.json --noCheck` first.',
  12. )
  13. process.exit(1)
  14. }
  15. const require = createRequire(import.meta.url)
  16. const __dirname = fileURLToPath(new URL('.', import.meta.url))
  17. const packagesDir = path.resolve(__dirname, 'packages')
  18. const packages = readdirSync('temp/packages')
  19. const targets = process.env.TARGETS ? process.env.TARGETS.split(',') : null
  20. const targetPackages = targets
  21. ? packages.filter(pkg => targets.includes(pkg))
  22. : packages
  23. function resolveExternal(/**@type {string}*/ packageName) {
  24. const pkg = require(`${packagesDir}/${packageName}/package.json`)
  25. return [
  26. ...Object.keys(pkg.dependencies || {}),
  27. ...Object.keys(pkg.devDependencies || {}),
  28. ...Object.keys(pkg.peerDependencies || {}),
  29. ]
  30. }
  31. export default targetPackages.map(
  32. /** @returns {import('rolldown').BuildOptions} */
  33. pkg => {
  34. return {
  35. input: `./temp/packages/${pkg}/src/index${pkg === 'vue' ? '-with-vapor' : ''}.d.ts`,
  36. output: {
  37. file: `packages/${pkg}/dist/${pkg}.d.ts`,
  38. format: 'es',
  39. },
  40. experimental: {
  41. nativeMagicString: true,
  42. },
  43. external: resolveExternal(pkg),
  44. plugins: [dts(), patchTypes(pkg), ...(pkg === 'vue' ? [copyMts()] : [])],
  45. onwarn(warning, warn) {
  46. // during dts rolldown, everything is externalized by default
  47. if (
  48. warning.code === 'UNRESOLVED_IMPORT' &&
  49. !warning.exporter?.startsWith('.')
  50. ) {
  51. return
  52. }
  53. warn(warning)
  54. },
  55. }
  56. },
  57. )
  58. /**
  59. * Patch the dts generated by rolldown-plugin-dts
  60. * 1. Convert all types to inline exports
  61. * and remove them from the big export {} declaration
  62. * otherwise it gets weird in vitepress `defineComponent` call with
  63. * "the inferred type cannot be named without a reference"
  64. * 2. Append custom augmentations (jsx, macros)
  65. *
  66. * @param {string} pkg
  67. * @returns {import('rolldown').Plugin}
  68. */
  69. function patchTypes(pkg) {
  70. return {
  71. name: 'patch-types',
  72. renderChunk(code, chunk, outputOptions, meta) {
  73. const s = meta.magicString
  74. const { program: ast, errors } = parseSync('x.d.ts', code, {
  75. sourceType: 'module',
  76. })
  77. if (errors.length) {
  78. throw new Error(errors.join('\n'))
  79. }
  80. /**
  81. * @param {import('@babel/types').VariableDeclarator | import('@babel/types').TSTypeAliasDeclaration | import('@babel/types').TSInterfaceDeclaration | import('@babel/types').TSDeclareFunction | import('@babel/types').TSInterfaceDeclaration | import('@babel/types').TSEnumDeclaration | import('@babel/types').ClassDeclaration} node
  82. * @param {import('@babel/types').VariableDeclaration} [parentDecl]
  83. */
  84. function processDeclaration(node, parentDecl) {
  85. if (!node.id) {
  86. return
  87. }
  88. assert(node.id.type === 'Identifier')
  89. const name = node.id.name
  90. if (name.startsWith('_')) {
  91. return
  92. }
  93. shouldRemoveExport.add(name)
  94. if (isExported.has(name)) {
  95. const start = (parentDecl || node).start
  96. assert(typeof start === 'number')
  97. // @ts-ignore
  98. s.prependLeft(start, `export `)
  99. }
  100. }
  101. const isExported = new Set()
  102. const shouldRemoveExport = new Set()
  103. // pass 0: check all exported types
  104. for (const node of ast.body) {
  105. if (node.type === 'ExportNamedDeclaration' && !node.source) {
  106. for (let i = 0; i < node.specifiers.length; i++) {
  107. const spec = node.specifiers[i]
  108. if (spec.type === 'ExportSpecifier') {
  109. isExported.add(
  110. 'name' in spec.local ? spec.local.name : spec.local.value,
  111. )
  112. }
  113. }
  114. }
  115. }
  116. // pass 1: add exports
  117. for (const node of ast.body) {
  118. if (node.type === 'VariableDeclaration') {
  119. // @ts-expect-error waiting for oxc-parser to expose types
  120. processDeclaration(node.declarations[0], node)
  121. if (node.declarations.length > 1) {
  122. assert(typeof node.start === 'number')
  123. assert(typeof node.end === 'number')
  124. throw new Error(
  125. `unhandled declare const with more than one declarators:\n${code.slice(
  126. node.start,
  127. node.end,
  128. )}`,
  129. )
  130. }
  131. } else if (
  132. node.type === 'TSTypeAliasDeclaration' ||
  133. node.type === 'TSInterfaceDeclaration' ||
  134. node.type === 'TSDeclareFunction' ||
  135. node.type === 'TSEnumDeclaration' ||
  136. node.type === 'ClassDeclaration'
  137. ) {
  138. // @ts-expect-error waiting for oxc-parser to expose types
  139. processDeclaration(node)
  140. }
  141. }
  142. // pass 2: remove exports
  143. for (const node of ast.body) {
  144. if (node.type === 'ExportNamedDeclaration' && !node.source) {
  145. // Precompute which specifiers are safe to remove.
  146. /** @type {boolean[]} */
  147. const removable = new Array(node.specifiers.length)
  148. let keptCount = 0
  149. for (let i = 0; i < node.specifiers.length; i++) {
  150. const spec = node.specifiers[i]
  151. const localName =
  152. 'name' in spec.local ? spec.local.name : spec.local.value
  153. let canRemove = false
  154. if (
  155. spec.type === 'ExportSpecifier' &&
  156. shouldRemoveExport.has(localName)
  157. ) {
  158. assert(spec.exported.type === 'Identifier')
  159. const exported = spec.exported.name
  160. if (exported === localName) {
  161. canRemove = true
  162. }
  163. }
  164. removable[i] = canRemove
  165. if (!canRemove) keptCount++
  166. }
  167. if (keptCount === 0) {
  168. assert(typeof node.start === 'number')
  169. assert(typeof node.end === 'number')
  170. // @ts-ignore
  171. s.remove(node.start, node.end)
  172. continue
  173. }
  174. // Next kept specifier index for each position (or -1).
  175. /** @type {number[]} */
  176. const nextKeptIndex = new Array(node.specifiers.length).fill(-1)
  177. let nextKept = -1
  178. for (let i = node.specifiers.length - 1; i >= 0; i--) {
  179. if (!removable[i]) nextKept = i
  180. nextKeptIndex[i] = nextKept
  181. }
  182. // Build removal ranges by consecutive removable runs.
  183. /** @type {{ start: number, end: number }[]} */
  184. const ranges = []
  185. let i = 0
  186. let prevKeptIndex = -1
  187. while (i < node.specifiers.length) {
  188. if (!removable[i]) {
  189. prevKeptIndex = i
  190. i++
  191. continue
  192. }
  193. const runStart = i
  194. while (i < node.specifiers.length && removable[i]) i++
  195. const runEnd = i - 1
  196. const first = node.specifiers[runStart]
  197. const last = node.specifiers[runEnd]
  198. assert(typeof first.start === 'number')
  199. assert(typeof last.end === 'number')
  200. const nextKept = nextKeptIndex[runEnd]
  201. if (nextKept !== -1) {
  202. const nextSpec = node.specifiers[nextKept]
  203. assert(typeof nextSpec.start === 'number')
  204. ranges.push({ start: first.start, end: nextSpec.start })
  205. } else if (prevKeptIndex >= 0) {
  206. const prev = node.specifiers[prevKeptIndex]
  207. assert(typeof prev.end === 'number')
  208. ranges.push({ start: prev.end, end: last.end })
  209. } else {
  210. ranges.push({ start: first.start, end: last.end })
  211. }
  212. }
  213. // apply removals from back to front to keep ranges stable
  214. ranges.sort((a, b) => b.start - a.start)
  215. for (const range of ranges) {
  216. // @ts-ignore
  217. s.remove(range.start, range.end)
  218. }
  219. }
  220. }
  221. // @ts-ignore
  222. code = s.toString()
  223. // append pkg specific types
  224. const additionalTypeDir = `packages/${pkg}/types`
  225. if (existsSync(additionalTypeDir)) {
  226. code +=
  227. '\n' +
  228. readdirSync(additionalTypeDir)
  229. .map(file => readFileSync(`${additionalTypeDir}/${file}`, 'utf-8'))
  230. .join('\n')
  231. }
  232. return code
  233. },
  234. }
  235. }
  236. /**
  237. * According to https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#packagejson-exports-imports-and-self-referencing
  238. * the only way to correct provide types for both Node ESM and CJS is to have
  239. * two separate declaration files, so we need to copy vue.d.ts to vue.d.mts
  240. * upon build.
  241. *
  242. * @returns {import('rolldown').Plugin}
  243. */
  244. function copyMts() {
  245. return {
  246. name: 'copy-vue-mts',
  247. writeBundle(_, bundle) {
  248. assert('code' in bundle['vue.d.ts'])
  249. writeFileSync('packages/vue/dist/vue.d.mts', bundle['vue.d.ts'].code)
  250. },
  251. }
  252. }