dtsc
Version:
Generate bundled TypeScript declaration files
135 lines (128 loc) • 5.06 kB
text/typescript
import { EOL } from 'os'
export interface BundleOptions {
files: string[]
source: string
exclude?: string[]
dependencies?: string[]
}
export async function bundle(options: BundleOptions) {
const { files, source, exclude = [] } = options
const moduleRE = `["'](${files.join('|')})(\\.[jt]s)?["']`
const internalImport = new RegExp('import\\(' + moduleRE + '\\)\\.', 'g')
const internalExport = new RegExp('^ {4}export .+ from ' + moduleRE + ';$')
const internalInject = new RegExp('^declare module ' + moduleRE + ' {$')
const importMap: Record<string, Record<string, string>> = {}
const namespaceMap: Record<string, string> = {}
let prolog = '', cap: RegExpExecArray | null
let current: string, temporary: string[] | null
let identifier: string, isExportDefault: boolean
const platforms: Record<string, Record<string, string[]>> = {}
const output = source.split(/\r?\n/g).filter((line) => {
// Step 1: collect informations
if (isExportDefault) {
if (line === ' }') isExportDefault = false
return false
} else if (temporary) {
if (line === '}') return temporary = null
temporary.push(line)
} else if ((cap = /^declare module ["'](.+)["'] \{( \})?$/.exec(line))) {
// ^1
// ignore empty module declarations
if (cap[2]) return temporary = null
if (exclude.includes(cap[1])) return temporary = null
current = cap[1]
const segments = current.split(/\//g)
const lastName = segments.pop()!
if (['node', 'browser'].includes(lastName) && segments.length) {
temporary = (platforms[segments.join('/')] ||= {})[lastName] = []
} else {
return true
}
} else if ((cap = /^ {4}import ["'](.+)["'];$/.exec(line))) {
// ^1
// import module directly
if (!files.includes(cap[1])) prolog += line.trimStart() + EOL
} else if ((cap = /^ {4}import (type )?\* as (.+) from ["'](.+)["'];$/.exec(line))) {
// ^1 ^2 ^3
// import as namespace
if (files.includes(cap[3])) {
// mark internal module as namespace
namespaceMap[cap[3]] = cap[2]
} else if (!prolog.includes(line.trimStart())) {
// preserve external module imports once
prolog += line.trimStart() + EOL
}
} else if ((cap = /^ {4}import (type )?(\S*)(?:, *)?(?:\{(.*)\})? from ["'](.+)["'];$/.exec(line))) {
// ^1 ^2 ^3 ^4
// ignore internal imports
if (files.includes(cap[4])) return
// handle aliases from external imports
const map = importMap[cap[4]] ||= {}
cap[2] && Object.defineProperty(map, 'default', { value: cap[2] })
cap[3] && cap[3].split(',').map((part) => {
part = part.trim()
if (part.startsWith('type ')) {
part = part.slice(5)
}
if (part.includes(' as ')) {
const [left, right] = part.split(' as ')
map[left.trimEnd()] = right.trimStart()
} else {
map[part] = part
}
})
} else if (line.startsWith('///')) {
prolog += line + EOL
} else if (line.startsWith('#!')) {
return false
} else if (line.startsWith(' export default ')) {
if (current === 'index') return true
if (line.endsWith('{')) isExportDefault = true
return false
} else {
return line.trim() !== 'export {};'
}
}).map((line) => {
// Step 2: flatten module declarations
if ((cap = /^declare module ["'](.+)["'] \{$/.exec(line))) {
if ((identifier = namespaceMap[cap[1]])) {
return `declare namespace ${identifier} {`
} else {
return ''
}
} else if (line === '}') {
return identifier ? '}' : ''
} else if (!internalExport.exec(line)) {
if (!identifier) line = line.slice(4)
return line
.replace(internalImport, '')
.replace(/import\("index"\)/g, "import('.')")
.replace(/^(module|class|namespace|const|global|function|interface) /, (_) => `declare ${_}`)
} else {
return ''
}
}).map((line) => {
if ((cap = internalInject.exec(line))) {
identifier = '@internal'
return ''
} else if (line === '}') {
return identifier ? identifier = '' : '}'
} else {
if (identifier) line = line.slice(4)
return line.replace(/^((class|namespace|interface) .+ \{)$/, (_) => `export ${_}`)
}
}).filter(line => line).join(EOL)
Object.entries(importMap).forEach(([name, map]) => {
const output: string[] = []
const entries = Object.entries(map)
if (map.default) output.push(map.default)
if (entries.length) {
output.push('{ ' + entries.map(([left, right]) => {
if (left === right) return left
return `${left} as ${right}`
}).join(', ') + ' }')
}
prolog += `import ${output.join(', ')} from '${name}';${EOL}`
})
return prolog + output + EOL
}