UNPKG

@napi-rs/cli

Version:
274 lines (240 loc) 7.44 kB
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 }