import path from 'node:path' import {existsSync, readFileSync, writeFileSync} from 'node:fs' const typedefPath = path.join(import.meta.dirname, '../../target/doc/foxfleet_applib.json') const outputPath = path.join(import.meta.dirname, '../bindings/index.ts') if (!existsSync(typedefPath)) { console.error(`Could not find rustdoc type definitions`) process.exit(1) } const commandPrefix = '::foxfleet_applib::commands' const {root, index} = JSON.parse(readFileSync(typedefPath)) function traverseModuleForFunctions(startIndex, namespace = '') { const node = index[startIndex] if (!node) return [] // If it's a module if (node.inner?.module) { const childItems = node.inner.module.items return childItems.map(childIndex => traverseModuleForFunctions(childIndex, namespace + '::' + node.name)) } // If it's a function if (node.inner?.function) { return {...node, namespace} } return [] } function getParameters(functionDef) { let params = [] const skipped = ['tauri::State', 'tauri::AppHandle'] for (const [name, typedef] of functionDef.inner.function.sig.inputs) { const typeName = typedef.resolved_path?.name if (skipped.includes(typeName)) continue const type = typedef.resolved_path ? convertType(typedef.resolved_path) : convertType(typedef.borrowed_ref) params.push({name, type}) } return params } function getReturnType(functionDef) { if (functionDef.inner.function.sig.output.resolved_path.name === "Result") return convertType(functionDef.inner.function.sig.output.resolved_path.args.angle_bracketed.args[0].type.resolved_path) return convertType(functionDef.inner.function.sig.output.resolved_path) } let imports = [] function convertType(typeDef) { if (typeof typeDef === 'undefined') return undefined const {name, id, args: {angle_bracketed: {args = []} = {}} = {}} = typeDef if(args.length && name !== 'Vec') throw new Error(`Unknown generic type ${name}`) if(name === 'Vec' && !args) throw new Errror(`Unknown inner type for vec`) if(name === 'Vec') return convertType(args[0].type.resolved_path) + '[]' if(name === 'Result') return `Promise<${convertType(args[0].type.resolved_path)}>` if(name === 'String' || typeDef?.type?.primitive === 'str') return 'string' // Otherwise look up the definition and make sure it has the ![derive(TS)] const def = index[id] if (!def.attrs || !def.attrs.includes('#[ts(export)]')) { throw new Error(`Complex type ${name} does not have Typescript definitions exported`) } imports.push(name) return name } function convertCamelCase(name) { return name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) } function convertUpperCamelCase(name) { return name.replace(/(_|^)([a-z])/g, (_, _first, letter) => letter.toUpperCase()) } const publicFunctions = traverseModuleForFunctions(root).flat(Infinity) const commandFunctions = publicFunctions .filter(({namespace}) => namespace.startsWith(commandPrefix)) .map(({namespace, ...rest}) => ({...rest, namespace: namespace.replace(commandPrefix + '::', '')})) const namespaces = {} for(const func of commandFunctions) { if(!namespaces[func.namespace]) namespaces[func.namespace] = [] namespaces[func.namespace].push(func) } const invokeLine = `import { invoke } from '@tauri-apps/api/core';` const namespaceBlocks = Object.keys(namespaces).map(namespace => { const functions = namespaces[namespace] const functionBlocks = functions.map(func => { const params = getParameters(func) const parametersString = params.map(({name, type}) => `${name}: ${type}`).join(', ') let returnTypeString = getReturnType(func) if (!/^Promise<.*>$/.test(returnTypeString)) returnTypeString = `Promise<${returnTypeString}>` return ` export async function ${convertCamelCase(func.name)}(${parametersString}): ${returnTypeString} { return await invoke('${func.name}', {${params.map(param => convertCamelCase(param.name) + ': ' + param.name).join(', ')}}) as any as ${returnTypeString} }` }) return `export namespace ${convertUpperCamelCase(namespace)} {\n${functionBlocks.join('\n')}\n}` }) // This needs to be last so it gets populated by the above calls const importLines = imports.map(type => `import type {${type}} from './${type}.ts'`) const fileData = ` ${invokeLine} ${importLines} ${namespaceBlocks.join('\n\n')} ` writeFileSync(outputPath, fileData)