rolldown.dts.config.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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. if (!meta?.magicString) {
  74. throw new Error('meta.magicString is missing')
  75. }
  76. const s = meta.magicString
  77. const { program: ast, errors } = parseSync('x.d.ts', code, {
  78. sourceType: 'module',
  79. })
  80. if (errors.length) {
  81. throw new Error(errors.join('\n'))
  82. }
  83. /**
  84. * @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
  85. * @param {import('@babel/types').VariableDeclaration} [parentDecl]
  86. */
  87. function processDeclaration(node, parentDecl) {
  88. if (!node.id) {
  89. return
  90. }
  91. assert(node.id.type === 'Identifier')
  92. const name = node.id.name
  93. if (name.startsWith('_')) {
  94. return
  95. }
  96. shouldRemoveExport.add(name)
  97. if (isExported.has(name)) {
  98. const start = (parentDecl || node).start
  99. assert(typeof start === 'number')
  100. s.prependLeft(start, `export `)
  101. }
  102. }
  103. const isExported = new Set()
  104. const shouldRemoveExport = new Set()
  105. // pass 0: check all exported types
  106. for (const node of ast.body) {
  107. if (node.type === 'ExportNamedDeclaration' && !node.source) {
  108. for (let i = 0; i < node.specifiers.length; i++) {
  109. const spec = node.specifiers[i]
  110. if (spec.type === 'ExportSpecifier') {
  111. isExported.add(
  112. 'name' in spec.local ? spec.local.name : spec.local.value,
  113. )
  114. }
  115. }
  116. }
  117. }
  118. // pass 1: add exports
  119. for (const node of ast.body) {
  120. if (node.type === 'VariableDeclaration') {
  121. // @ts-expect-error waiting for oxc-parser to expose types
  122. processDeclaration(node.declarations[0], node)
  123. if (node.declarations.length > 1) {
  124. assert(typeof node.start === 'number')
  125. assert(typeof node.end === 'number')
  126. throw new Error(
  127. `unhandled declare const with more than one declarators:\n${code.slice(
  128. node.start,
  129. node.end,
  130. )}`,
  131. )
  132. }
  133. } else if (
  134. node.type === 'TSTypeAliasDeclaration' ||
  135. node.type === 'TSInterfaceDeclaration' ||
  136. node.type === 'TSDeclareFunction' ||
  137. node.type === 'TSEnumDeclaration' ||
  138. node.type === 'ClassDeclaration'
  139. ) {
  140. // @ts-expect-error waiting for oxc-parser to expose types
  141. processDeclaration(node)
  142. }
  143. }
  144. // pass 2: remove exports
  145. for (const node of ast.body) {
  146. if (node.type === 'ExportNamedDeclaration' && !node.source) {
  147. let removed = 0
  148. for (let i = 0; i < node.specifiers.length; i++) {
  149. const spec = node.specifiers[i]
  150. const localName =
  151. 'name' in spec.local ? spec.local.name : spec.local.value
  152. if (
  153. spec.type === 'ExportSpecifier' &&
  154. shouldRemoveExport.has(localName)
  155. ) {
  156. assert(spec.exported.type === 'Identifier')
  157. const exported = spec.exported.name
  158. if (exported !== localName) {
  159. // this only happens if we have something like
  160. // type Foo
  161. // export { Foo as Bar }
  162. continue
  163. }
  164. const next = node.specifiers[i + 1]
  165. if (next) {
  166. assert(typeof spec.start === 'number')
  167. assert(typeof next.start === 'number')
  168. s.remove(spec.start, next.start)
  169. } else {
  170. // last one
  171. const prev = node.specifiers[i - 1]
  172. assert(typeof spec.start === 'number')
  173. assert(typeof spec.end === 'number')
  174. s.remove(
  175. prev
  176. ? (assert(typeof prev.end === 'number'), prev.end)
  177. : spec.start,
  178. spec.end,
  179. )
  180. }
  181. removed++
  182. }
  183. }
  184. if (removed === node.specifiers.length) {
  185. assert(typeof node.start === 'number')
  186. assert(typeof node.end === 'number')
  187. s.remove(node.start, node.end)
  188. }
  189. }
  190. }
  191. code = s.toString()
  192. // append pkg specific types
  193. const additionalTypeDir = `packages/${pkg}/types`
  194. if (existsSync(additionalTypeDir)) {
  195. code +=
  196. '\n' +
  197. readdirSync(additionalTypeDir)
  198. .map(file => readFileSync(`${additionalTypeDir}/${file}`, 'utf-8'))
  199. .join('\n')
  200. }
  201. return code
  202. },
  203. }
  204. }
  205. /**
  206. * According to https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#packagejson-exports-imports-and-self-referencing
  207. * the only way to correct provide types for both Node ESM and CJS is to have
  208. * two separate declaration files, so we need to copy vue.d.ts to vue.d.mts
  209. * upon build.
  210. *
  211. * @returns {import('rolldown').Plugin}
  212. */
  213. function copyMts() {
  214. return {
  215. name: 'copy-vue-mts',
  216. writeBundle(_, bundle) {
  217. assert('code' in bundle['vue.d.ts'])
  218. writeFileSync('packages/vue/dist/vue.d.mts', bundle['vue.d.ts'].code)
  219. },
  220. }
  221. }