summary refs log tree commit diff
path: root/ui/util/generate-tauri-bindings.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui/util/generate-tauri-bindings.js')
-rw-r--r--ui/util/generate-tauri-bindings.js131
1 files changed, 131 insertions, 0 deletions
diff --git a/ui/util/generate-tauri-bindings.js b/ui/util/generate-tauri-bindings.js
new file mode 100644
index 0000000..11c8b61
--- /dev/null
+++ b/ui/util/generate-tauri-bindings.js
@@ -0,0 +1,131 @@
+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)