Menu

Type Safety and Code Generation

Relevant source files

This document describes the code generation systems in Hyprnote that maintain type safety and automate boilerplate creation across the codebase. The system employs multiple generation strategies:

  1. Tauri-Specta: Generates TypeScript bindings from Rust code for type-safe IPC
  2. Content Collections: Processes MDX files with type-safe content access
  3. TanStack Router: Generates file-based routing with routeTree.gen.ts
  4. Documentation Generation: Generates AGENTS.md listings via gen-agents.js

For information about the plugin system architecture that uses these bindings, see Plugin System.


Overview

Hyprnote employs four primary code generation systems:

Tauri-Specta Bindings

Type-safe IPC layer between Rust and TypeScript:

  1. Rust commands, events, and types annotated with specta macros
  2. Test harness exports TypeScript definitions to .gen.ts files
  3. Frontend imports type-safe functions from generated files
  4. Changes to Rust signatures automatically propagate to TypeScript

Content Collections

MDX processing with type safety:

  1. @content-collections/vite plugin processes MDX files
  2. Type-safe accessors like allDocs and allTemplates generated
  3. Build-time validation of content structure
  4. Hot module reload during development

Router Generation

File-based routing with type safety:

  1. @tanstack/router-plugin scans src/routes directory
  2. Generates routeTree.gen.ts with route definitions
  3. Type-safe navigation and route parameters
  4. Auto-updated on file system changes

Documentation Generation

Automated documentation listings:

  1. gen-agents.js script scans repository for AGENTS.md files
  2. Generates centralized documentation index
  3. Links to GitHub source files
  4. Runs as part of build process

Sources: apps/desktop/src-tauri/src/lib.rs191-199 Cargo.toml219-221 apps/web/vite.config.ts1-43 apps/web/scripts/gen-agents.js1-70


Core Libraries

Rust/TypeScript Bindings

LibraryPurposeVersion
spectaCore Rust type serialization to TypeScript2.0.0-rc.22
specta-typescriptTypeScript code generator0.0.9
tauri-spectaTauri integration for specta2.0.0-rc.21

Content Processing

LibraryPurposeVersion
@content-collections/coreCore MDX processing0.11.1
@content-collections/mdxMDX compilation0.2.2
@content-collections/viteVite plugin integration0.2.7

Router Generation

LibraryPurposeVersion
@tanstack/router-pluginRouter code generation1.139.3
@tanstack/react-routerRuntime router1.139.3

These libraries work together to provide compile-time type safety across Rust/TypeScript boundaries, content management, and routing.

Sources: Cargo.toml219-221 apps/web/package.json63-83 apps/desktop/package.json1-137


Architecture

Type Generation Flow

Type Information Extraction: Specta derives extract type information from Rust structs and enums at compile time using procedural macros.

Code Generation Trigger: Running tests with cargo test executes export functions that write TypeScript files.

Frontend Import: TypeScript code imports from generated files, getting full type safety and IDE support.

Sources: apps/desktop/src-tauri/src/lib.rs142-168 plugins/windows/src/lib.rs42-60


Desktop Application Bindings

Specta Builder Configuration

The main desktop application creates a tauri_specta::Builder to collect commands and configure code generation:

Sources: apps/desktop/src-tauri/src/lib.rs142-150

The builder is configured in make_specta_builder():

  • Commands Collection: Uses tauri_specta::collect_commands! macro to gather all annotated commands
  • Error Handling: Set to ErrorHandlingMode::Result to wrap errors in Result<T, E> types
  • Registration: Returns invoke handler for Tauri and mounts events to app handle

Command Definition

Commands are defined in Rust with type safety annotations:

apps/desktop/src-tauri/src/commands.rs35-60

Key annotations:

  • #[tauri::command] - Marks function as Tauri command
  • #[specta::specta] - Includes in type generation
  • Generic R: tauri::Runtime - Platform abstraction
  • Return Result<T, String> - Type-safe error handling

Type Export Test

Code generation is triggered by a test function:

apps/desktop/src-tauri/src/lib.rs152-168

Configuration details:

  • Output Path: ../src/types/tauri.gen.ts (relative to Cargo.toml)
  • Header: Adds // @ts-nocheck comment
  • Formatter: Uses prettier for consistent formatting
  • BigInt Handling: Exports as Number type

Sources: apps/desktop/src-tauri/src/lib.rs35-168 apps/desktop/src-tauri/src/commands.rs35-60


Generated TypeScript Bindings

Desktop Commands

The generated file provides type-safe command invocations:

apps/desktop/src/types/tauri.gen.ts9-34

Structure:

  • commands Object: Contains all exported commands as async functions
  • Type Safety: Parameters and return types match Rust signatures exactly
  • Error Handling: Returns Result<T, E> union type for explicit error handling
  • TAURI_INVOKE: Uses Tauri's core invoke function under the hood

Type Definitions

Generated types mirror Rust definitions:

apps/desktop/src/types/tauri.gen.ts46-47

Naming Convention: TypeScript types use same names as Rust structs

Nested Types: Complex types with nested structures are fully preserved

Result Type Pattern

All commands return a discriminated union for error handling:

apps/desktop/src/types/tauri.gen.ts70-72

Usage Pattern:

Sources: apps/desktop/src/types/tauri.gen.ts1-108


Plugin Bindings

Plugin-Specific Generation

Each plugin maintains its own generated bindings:

Plugin Isolation: Each plugin's types are generated independently

Package Export: Generated files are exported through plugin's package.json

Sources: plugins/windows/src/lib.rs42-103 plugins/windows/Cargo.toml1-41

Windows Plugin Example

The windows plugin demonstrates the pattern:

plugins/windows/src/lib.rs42-60

Key aspects:

  • Plugin Name: Set via .plugin_name("windows")
  • Event Collection: Uses tauri_specta::collect_events! for event types
  • Command Collection: Same pattern as main app
  • Namespace: Events prefixed with plugin:windows:

Generated Plugin Commands

Plugin commands follow the same pattern with additional namespace:

plugins/windows/js/bindings.gen.ts9-66

Command Invocation: Uses plugin:windows|command_name pattern for namespacing

Type Safety: Same Result<T, E> pattern as main commands

Generated Plugin Events

Events get special handling with flexible listening patterns:

plugins/windows/js/bindings.gen.ts71-79

Event Object Structure:

  • listen(callback) - Subscribe to event
  • once(callback) - Subscribe for single emission
  • emit(payload) - Emit event
  • (window) - Window-specific event handling

Sources: plugins/windows/src/lib.rs42-103 plugins/windows/js/bindings.gen.ts1-153


Event Type Safety

Event Definition Pattern

Events are defined with a macro for consistent derives:

plugins/windows/src/events.rs55-70

The common_event_derives! macro applies:

  • Debug - Debug printing
  • serde::Serialize/Deserialize - JSON serialization
  • Clone - Value cloning
  • specta::Type - Type information for codegen
  • tauri_specta::Event - Event-specific metadata

Event Usage in Frontend

Generated event utilities support multiple listening patterns:

Type Safety: Payload types are fully typed based on Rust event struct

Sources: plugins/windows/src/events.rs55-95 plugins/windows/js/bindings.gen.ts103-152


Integration with Router

The generated event system integrates with TanStack Router for cross-window navigation:

apps/desktop/src/routes/__root.tsx51-72

Flow:

  1. Rust emits Navigate event with path and search parameters
  2. TypeScript event listener receives typed payload
  3. Router navigates to typed route with typed search params
  4. Type safety maintained end-to-end

Type-Safe Window Commands

Window operations use generated commands with discriminated unions:

apps/desktop/src/routes/app/onboarding/index.tsx71-87

Type Safety:

  • windowShow accepts AppWindow enum
  • windowDestroy type-checked at compile time
  • Return types handled with Result pattern

Sources: apps/desktop/src/routes/__root.tsx51-72 apps/desktop/src/routes/app/onboarding/index.tsx71-87


Code Generation Workflow

Development Workflow

Process:

  1. Developer modifies Rust command signature or adds new command
  2. Run cargo test to regenerate TypeScript bindings
  3. TypeScript compiler catches any breaking changes in frontend
  4. Developer updates frontend code to match new types
  5. Both Rust and generated TypeScript files committed together

CI Integration

Generated files are checked into version control and validated in CI:

Validation: CI ensures generated files match current Rust code

Pattern Example: packages/obsidian/package.json13 shows validation in generate script

Sources: apps/desktop/src-tauri/src/lib.rs152-168 plugins/windows/src/lib.rs85-103


Type System Mapping

Rust to TypeScript Type Mapping

Rust TypeTypeScript TypeNotes
StringstringDirect mapping
boolbooleanDirect mapping
i32, i64, u32, u64numberConfigured via BigIntExportBehavior
Option<T>T | nullNullable union type
Vec<T>T[]Array type
HashMap<K, V>Partial<{ [key in K]: V }>Object type
structtype or interfaceObject type
enumDiscriminated unionTagged union

Enum Handling

Rust enums map to TypeScript discriminated unions:

plugins/windows/js/bindings.gen.ts87

Rust Definition:

Generated TypeScript:

Struct Handling

Rust structs map to TypeScript object types:

apps/desktop/src/types/tauri.gen.ts46-47

Rust Definition (inferred from TS):

Sources: apps/desktop/src/types/tauri.gen.ts46-47 plugins/windows/js/bindings.gen.ts87-92


Error Handling Pattern

Result Type Wrapper

All commands return a Result<T, E> type for explicit error handling:

apps/desktop/src/types/tauri.gen.ts70-72

Error Handling Modes

The specta builder configures error handling:

apps/desktop/src-tauri/src/lib.rs149

ErrorHandlingMode::Result:

  • Wraps all command results in Result<T, E>
  • Prevents unhandled exceptions
  • Forces frontend to handle errors explicitly

Frontend Error Handling

Example usage pattern:

Type Safety: TypeScript enforces checking result status before accessing data

Sources: apps/desktop/src-tauri/src/lib.rs142-150 apps/desktop/src/types/tauri.gen.ts70-72


Permission System Integration

Permission Definitions

Plugins define permissions that reference generated commands:

plugins/windows/permissions/default.toml1-11

Convention: Permission identifiers like allow-window-show correspond to command names

Capability Configuration

Capabilities reference plugin permissions:

apps/desktop/src-tauri/capabilities/default.json76-77

Type Safety: While not compile-time checked, permissions are validated at build time by Tauri

Sources: plugins/windows/permissions/default.toml1-11 apps/desktop/src-tauri/capabilities/default.json1-109


Build Integration

Plugin Build Script

Each plugin's build.rs lists commands for permission generation:

plugins/windows/build.rs1-13

Purpose: Generates permission schema from command list

Synchronization: Command list must match actual command implementations

Generated Permission Schemas

The build process creates permission documentation:

plugins/windows/permissions/autogenerated/reference.md1-206

Auto-generated: This file is created during plugin build

Documentation: Provides reference for capability configuration

Sources: plugins/windows/build.rs1-13 plugins/windows/permissions/autogenerated/reference.md1-206


Content Collections

Configuration

Content collections are configured in Vite:

apps/web/vite.config.ts13-14

The plugin processes MDX files and generates type-safe accessors.

MDX Processing Flow

MDX Compilation: Processes MDX with remark and rehype plugins for enhanced markdown

Type Generation: Creates TypeScript types for content structure

Hot Reload: Vite HMR updates content without full page reload

Sources: apps/web/vite.config.ts1-43 apps/web/package.json63-68

Generated Content Types

Content collections generate type-safe accessors:

Type Safety: Content structure validated at build time

IDE Support: Full autocomplete for content fields

Sources: apps/web/package.json65-67


Router Code Generation

Plugin Configuration

TanStack Router generates route definitions from file structure:

apps/web/package.json39

The plugin scans the routes directory and generates type-safe routing code.

Route Generation Flow

File-Based Routing: Route structure mirrors file system

Type Generation: Creates TypeScript types for routes and parameters

Hierarchical: Nested routes represented in tree structure

Sources: apps/desktop/src/routeTree.gen.ts1-315 apps/web/package.json39

Generated Route Tree

Example generated route structure:

apps/desktop/src/routeTree.gen.ts25-85

Route Definitions:

  • FileRoutesByFullPath - Maps full paths to route types
  • FileRoutesByTo - Maps navigation targets to routes
  • FileRoutesById - Maps route IDs to route types

Type Safety: Navigation functions type-checked against route structure

Route Type Definitions

Generated types enable type-safe navigation:

apps/desktop/src/routeTree.gen.ts87-123

Usage Pattern:

Sources: apps/desktop/src/routeTree.gen.ts1-315


Documentation Generation

Gen-Agents Script

The gen-agents.js script automates documentation index creation:

apps/web/scripts/gen-agents.js1-70

Process:

  1. Recursively scans repository for AGENTS.md files
  2. Skips configured directories (node_modules, target, dist)
  3. Generates MDX file with table of links
  4. Links point to GitHub source

Generation Flow

Automated Discovery: Finds all AGENTS.md files without manual listing

GitHub Integration: Links directly to source files for easy access

Build Integration: Runs via package.json script

Sources: apps/web/scripts/gen-agents.js1-70 apps/web/package.json11

Generated Documentation

Example generated output structure:

apps/web/content/docs/developers/10.agents.mdx1-32

Table Format:

  • Path column shows location in repository
  • Link column provides direct GitHub access
  • Auto-sorted for consistency

Maintenance: No manual updates needed when adding/removing AGENTS.md files

Sources: apps/web/content/docs/developers/10.agents.mdx1-32 apps/web/scripts/gen-agents.js40-70


Best Practices

Tauri-Specta Annotations

Commands:

Types:

Events:

Content Collections Best Practices

  1. Define content schemas in configuration
  2. Use frontmatter for structured metadata
  3. Validate content structure at build time
  4. Leverage type-safe accessors (allDocs, allTemplates)

Router Generation Best Practices

  1. Follow file naming conventions (__root.tsx, _layout.tsx)
  2. Use route parameters in file names ([id].tsx)
  3. Leverage generated types for navigation
  4. Review generated routeTree.gen.ts for route structure

Documentation Generation Best Practices

  1. Place AGENTS.md files at appropriate directory levels
  2. Keep AGENTS.md content focused on agent-specific instructions
  3. Run gen-agents script before committing documentation changes
  4. Review generated table for completeness

Maintaining Generated Files

  1. Always regenerate after source changes
  2. Commit generated files alongside source changes
  3. Review generated file diffs in pull requests
  4. Configure CI to validate generated files are up to date
  5. Use cargo test for Rust bindings, npm run gen:* for other systems

Frontend Usage

  1. Import from .gen.ts files, never hand-write bindings
  2. Handle Result types explicitly for Tauri commands
  3. Use TypeScript strict mode to catch type errors
  4. Leverage IDE autocomplete for all generated APIs

Sources: apps/desktop/src-tauri/src/lib.rs201-217 plugins/windows/src/events.rs53-68 apps/web/package.json11


Limitations and Considerations

BigInt Handling

The current configuration exports Rust u64 as TypeScript number:

apps/desktop/src-tauri/src/lib.rs162

Trade-off: JavaScript numbers lose precision beyond 2^53, but avoids BigInt complexity

Alternative: Can configure BigIntExportBehavior::BigInt if precision needed

Async by Default

All commands are async on the TypeScript side:

apps/desktop/src/types/tauri.gen.ts10

Reason: Tauri IPC is inherently asynchronous

Impact: Even synchronous Rust functions become async in TypeScript

Serde Limitations

Types must be serde-serializable to cross IPC boundary:

Restrictions:

  • No function pointers
  • No non-serializable types
  • Complex types may need custom serde implementations

Sources: apps/desktop/src-tauri/src/lib.rs162 apps/desktop/src/types/tauri.gen.ts10-17