@napi-rs/cli
Version:
Cli tools for napi-rs
274 lines (240 loc) • 7.44 kB
text/typescript
import { sortBy } from 'es-toolkit'
import { readFileAsync } from './misc.js'
const TOP_LEVEL_NAMESPACE = '__TOP_LEVEL_MODULE__'
export const DEFAULT_TYPE_DEF_HEADER = `/* auto-generated by NAPI-RS */
/* eslint-disable */
`
enum TypeDefKind {
Const = 'const',
Enum = 'enum',
StringEnum = 'string_enum',
Interface = 'interface',
Type = 'type',
Fn = 'fn',
Struct = 'struct',
Extends = 'extends',
Impl = 'impl',
}
interface TypeDefLine {
kind: TypeDefKind
name: string
original_name?: string
def: string
extends?: string
js_doc?: string
js_mod?: string
}
function prettyPrint(
line: TypeDefLine,
constEnum: boolean,
ident: number,
ambient = false,
): string {
let s = line.js_doc ?? ''
switch (line.kind) {
case TypeDefKind.Interface:
s += `export interface ${line.name} {\n${line.def}\n}`
break
case TypeDefKind.Type:
s += `export type ${line.name} = \n${line.def}`
break
case TypeDefKind.Enum:
const enumName = constEnum ? 'const enum' : 'enum'
s += `${exportDeclare(ambient)} ${enumName} ${line.name} {\n${line.def}\n}`
break
case TypeDefKind.StringEnum:
if (constEnum) {
s += `${exportDeclare(ambient)} const enum ${line.name} {\n${line.def}\n}`
} else {
s += `export type ${line.name} = ${line.def.replaceAll(/.*=/g, '').replaceAll(',', '|')};`
}
break
case TypeDefKind.Struct:
const extendsDef = line.extends ? ` extends ${line.extends}` : ''
if (line.extends) {
// Extract generic params from extends type like Iterator<T, TResult, TNext>
const genericMatch = line.extends.match(/Iterator<(.+)>$/)
if (genericMatch) {
const [T, TResult, TNext] = genericMatch[1]
.split(',')
.map((p) => p.trim())
line.def =
line.def +
`\nnext(value?: ${TNext}): IteratorResult<${T}, ${TResult}>`
}
}
s += `${exportDeclare(ambient)} class ${line.name}${extendsDef} {\n${line.def}\n}`
if (line.original_name && line.original_name !== line.name) {
s += `\nexport type ${line.original_name} = ${line.name}`
}
break
case TypeDefKind.Fn:
s += `${exportDeclare(ambient)} ${line.def}`
break
default:
s += line.def
}
return correctStringIdent(s, ident)
}
function exportDeclare(ambient: boolean): string {
if (ambient) {
return 'export'
}
return 'export declare'
}
export async function processTypeDef(
intermediateTypeFile: string,
constEnum: boolean,
) {
const exports: string[] = []
const defs = await readIntermediateTypeFile(intermediateTypeFile)
const groupedDefs = preprocessTypeDef(defs)
const dts =
sortBy(Array.from(groupedDefs), [([namespace]) => namespace])
.map(([namespace, defs]) => {
if (namespace === TOP_LEVEL_NAMESPACE) {
return defs
.map((def) => {
switch (def.kind) {
case TypeDefKind.Const:
case TypeDefKind.Enum:
case TypeDefKind.StringEnum:
case TypeDefKind.Fn:
case TypeDefKind.Struct: {
exports.push(def.name)
if (def.original_name && def.original_name !== def.name) {
exports.push(def.original_name)
}
break
}
default:
break
}
return prettyPrint(def, constEnum, 0)
})
.join('\n\n')
} else {
exports.push(namespace)
let declaration = ''
declaration += `export declare namespace ${namespace} {\n`
for (const def of defs) {
declaration += prettyPrint(def, constEnum, 2, true) + '\n'
}
declaration += '}'
return declaration
}
})
.join('\n\n') + '\n'
return {
dts,
exports,
}
}
async function readIntermediateTypeFile(file: string) {
const content = await readFileAsync(file, 'utf8')
const defs = content
.split('\n')
.filter(Boolean)
.map((line) => {
line = line.trim()
const parsed = JSON.parse(line) as TypeDefLine
// Convert escaped newlines back to actual newlines in js_doc fields
if (parsed.js_doc) {
parsed.js_doc = parsed.js_doc.replace(/\\n/g, '\n')
}
// Convert escaped newlines to actual newlines in def fields for struct/class/interface/type types
// where \n represents method/field separators that should be actual newlines
if (parsed.def) {
parsed.def = parsed.def.replace(/\\n/g, '\n')
}
return parsed
})
// move all `struct` def to the very top
// and order the rest alphabetically.
return defs.sort((a, b) => {
if (a.kind === TypeDefKind.Struct) {
if (b.kind === TypeDefKind.Struct) {
return a.name.localeCompare(b.name)
}
return -1
} else if (b.kind === TypeDefKind.Struct) {
return 1
} else {
return a.name.localeCompare(b.name)
}
})
}
function preprocessTypeDef(defs: TypeDefLine[]): Map<string, TypeDefLine[]> {
const namespaceGrouped = new Map<string, TypeDefLine[]>()
const classDefs = new Map<string, TypeDefLine>()
for (const def of defs) {
const namespace = def.js_mod ?? TOP_LEVEL_NAMESPACE
if (!namespaceGrouped.has(namespace)) {
namespaceGrouped.set(namespace, [])
}
const group = namespaceGrouped.get(namespace)!
if (def.kind === TypeDefKind.Struct) {
group.push(def)
classDefs.set(def.name, def)
} else if (def.kind === TypeDefKind.Extends) {
const classDef = classDefs.get(def.name)
if (classDef) {
classDef.extends = def.def
}
} else if (def.kind === TypeDefKind.Impl) {
// merge `impl` into class definition
const classDef = classDefs.get(def.name)
if (classDef) {
if (classDef.def) {
classDef.def += '\n'
}
classDef.def += def.def
// Convert any remaining \n sequences in the merged def to actual newlines
if (classDef.def) {
classDef.def = classDef.def.replace(/\\n/g, '\n')
}
}
} else {
group.push(def)
}
}
return namespaceGrouped
}
export function correctStringIdent(src: string, ident: number): string {
let bracketDepth = 0
const result = src
.split('\n')
.map((line) => {
line = line.trim()
if (line === '') {
return ''
}
const isInMultilineComment = line.startsWith('*')
const isClosingBracket = line.endsWith('}')
const isOpeningBracket = line.endsWith('{')
const isTypeDeclaration = line.endsWith('=')
const isTypeVariant = line.startsWith('|')
let rightIndent = ident
if ((isOpeningBracket || isTypeDeclaration) && !isInMultilineComment) {
bracketDepth += 1
rightIndent += (bracketDepth - 1) * 2
} else {
if (
isClosingBracket &&
bracketDepth > 0 &&
!isInMultilineComment &&
!isTypeVariant
) {
bracketDepth -= 1
}
rightIndent += bracketDepth * 2
}
if (isInMultilineComment) {
rightIndent += 1
}
const s = `${' '.repeat(rightIndent)}${line}`
return s
})
.join('\n')
return result
}