// @ts-check import assert from 'node:assert/strict' import { parseSync } from 'oxc-parser' import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs' import { dts } from 'rolldown-plugin-dts' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' import path from 'node:path' if (!existsSync('temp/packages')) { console.warn( 'no temp dts files found. run `tsc -p tsconfig.build.json --noCheck` first.', ) process.exit(1) } const require = createRequire(import.meta.url) const __dirname = fileURLToPath(new URL('.', import.meta.url)) const packagesDir = path.resolve(__dirname, 'packages') const packages = readdirSync('temp/packages') const targets = process.env.TARGETS ? process.env.TARGETS.split(',') : null const targetPackages = targets ? packages.filter(pkg => targets.includes(pkg)) : packages function resolveExternal(/**@type {string}*/ packageName) { const pkg = require(`${packagesDir}/${packageName}/package.json`) return [ ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {}), ...Object.keys(pkg.peerDependencies || {}), ] } export default targetPackages.map( /** @returns {import('rolldown').BuildOptions} */ pkg => { return { input: `./temp/packages/${pkg}/src/index${pkg === 'vue' ? '-with-vapor' : ''}.d.ts`, output: { file: `packages/${pkg}/dist/${pkg}.d.ts`, format: 'es', }, experimental: { nativeMagicString: true, }, external: resolveExternal(pkg), plugins: [dts(), patchTypes(pkg), ...(pkg === 'vue' ? [copyMts()] : [])], onwarn(warning, warn) { // during dts rolldown, everything is externalized by default if ( warning.code === 'UNRESOLVED_IMPORT' && !warning.exporter?.startsWith('.') ) { return } warn(warning) }, } }, ) /** * Patch the dts generated by rolldown-plugin-dts * 1. Convert all types to inline exports * and remove them from the big export {} declaration * otherwise it gets weird in vitepress `defineComponent` call with * "the inferred type cannot be named without a reference" * 2. Append custom augmentations (jsx, macros) * * @param {string} pkg * @returns {import('rolldown').Plugin} */ function patchTypes(pkg) { return { name: 'patch-types', renderChunk(code, chunk, outputOptions, meta) { const s = meta.magicString const { program: ast, errors } = parseSync('x.d.ts', code, { sourceType: 'module', }) if (errors.length) { throw new Error(errors.join('\n')) } /** * @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 * @param {import('@babel/types').VariableDeclaration} [parentDecl] */ function processDeclaration(node, parentDecl) { if (!node.id) { return } assert(node.id.type === 'Identifier') const name = node.id.name if (name.startsWith('_')) { return } shouldRemoveExport.add(name) if (isExported.has(name)) { const start = (parentDecl || node).start assert(typeof start === 'number') // @ts-ignore s.prependLeft(start, `export `) } } const isExported = new Set() const shouldRemoveExport = new Set() // pass 0: check all exported types for (const node of ast.body) { if (node.type === 'ExportNamedDeclaration' && !node.source) { for (let i = 0; i < node.specifiers.length; i++) { const spec = node.specifiers[i] if (spec.type === 'ExportSpecifier') { isExported.add( 'name' in spec.local ? spec.local.name : spec.local.value, ) } } } } // pass 1: add exports for (const node of ast.body) { if (node.type === 'VariableDeclaration') { // @ts-expect-error waiting for oxc-parser to expose types processDeclaration(node.declarations[0], node) if (node.declarations.length > 1) { assert(typeof node.start === 'number') assert(typeof node.end === 'number') throw new Error( `unhandled declare const with more than one declarators:\n${code.slice( node.start, node.end, )}`, ) } } else if ( node.type === 'TSTypeAliasDeclaration' || node.type === 'TSInterfaceDeclaration' || node.type === 'TSDeclareFunction' || node.type === 'TSEnumDeclaration' || node.type === 'ClassDeclaration' ) { // @ts-expect-error waiting for oxc-parser to expose types processDeclaration(node) } } // pass 2: remove exports for (const node of ast.body) { if (node.type === 'ExportNamedDeclaration' && !node.source) { // Precompute which specifiers are safe to remove. /** @type {boolean[]} */ const removable = new Array(node.specifiers.length) let keptCount = 0 for (let i = 0; i < node.specifiers.length; i++) { const spec = node.specifiers[i] const localName = 'name' in spec.local ? spec.local.name : spec.local.value let canRemove = false if ( spec.type === 'ExportSpecifier' && shouldRemoveExport.has(localName) ) { assert(spec.exported.type === 'Identifier') const exported = spec.exported.name if (exported === localName) { canRemove = true } } removable[i] = canRemove if (!canRemove) keptCount++ } if (keptCount === 0) { assert(typeof node.start === 'number') assert(typeof node.end === 'number') // @ts-ignore s.remove(node.start, node.end) continue } // Next kept specifier index for each position (or -1). /** @type {number[]} */ const nextKeptIndex = new Array(node.specifiers.length).fill(-1) let nextKept = -1 for (let i = node.specifiers.length - 1; i >= 0; i--) { if (!removable[i]) nextKept = i nextKeptIndex[i] = nextKept } // Build removal ranges by consecutive removable runs. /** @type {{ start: number, end: number }[]} */ const ranges = [] let i = 0 let prevKeptIndex = -1 while (i < node.specifiers.length) { if (!removable[i]) { prevKeptIndex = i i++ continue } const runStart = i while (i < node.specifiers.length && removable[i]) i++ const runEnd = i - 1 const first = node.specifiers[runStart] const last = node.specifiers[runEnd] assert(typeof first.start === 'number') assert(typeof last.end === 'number') const nextKept = nextKeptIndex[runEnd] if (nextKept !== -1) { const nextSpec = node.specifiers[nextKept] assert(typeof nextSpec.start === 'number') ranges.push({ start: first.start, end: nextSpec.start }) } else if (prevKeptIndex >= 0) { const prev = node.specifiers[prevKeptIndex] assert(typeof prev.end === 'number') ranges.push({ start: prev.end, end: last.end }) } else { ranges.push({ start: first.start, end: last.end }) } } // apply removals from back to front to keep ranges stable ranges.sort((a, b) => b.start - a.start) for (const range of ranges) { // @ts-ignore s.remove(range.start, range.end) } } } // @ts-ignore code = s.toString() // append pkg specific types const additionalTypeDir = `packages/${pkg}/types` if (existsSync(additionalTypeDir)) { code += '\n' + readdirSync(additionalTypeDir) .map(file => readFileSync(`${additionalTypeDir}/${file}`, 'utf-8')) .join('\n') } return code }, } } /** * According to https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#packagejson-exports-imports-and-self-referencing * the only way to correct provide types for both Node ESM and CJS is to have * two separate declaration files, so we need to copy vue.d.ts to vue.d.mts * upon build. * * @returns {import('rolldown').Plugin} */ function copyMts() { return { name: 'copy-vue-mts', writeBundle(_, bundle) { assert('code' in bundle['vue.d.ts']) writeFileSync('packages/vue/dist/vue.d.mts', bundle['vue.d.ts'].code) }, } }