惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
Martin Fowler
Martin Fowler
爱范儿
爱范儿
IT之家
IT之家
Last Week in AI
Last Week in AI
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
阮一峰的网络日志
阮一峰的网络日志
V
V2EX
aimingoo的专栏
aimingoo的专栏
G
Google Developers Blog
J
Java Code Geeks
Microsoft Azure Blog
Microsoft Azure Blog
美团技术团队
The Cloudflare Blog
MyScale Blog
MyScale Blog
T
The Blog of Author Tim Ferriss
Hugging Face - Blog
Hugging Face - Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
云风的 BLOG
云风的 BLOG
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
腾讯CDC
Microsoft Security Blog
Microsoft Security Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Как я писал генератор TypeScript-биндингов для Tauri
kirillinyakin · 2026-06-16 · via Все публикации подряд на Хабре

Средний

5 мин

151

Всё началось с бага, на который я убил вечер. В Tauri фронтенд вызывает Rust-команды так:

const user = await invoke("get_user", { userId: 42 });

Строка с именем команды, объект с аргументами, никакой проверки типов. У меня в Rust аргумент назывался user_id, и я честно передал user_id с фронта. В рантайме прилетело что-то вроде invalid args 'userId' for command 'get_user' — Tauri по умолчанию переименовывает snake_case-аргументы в camelCase. Это написано в документации, я это даже знал. Но знание, которое не проверяет компилятор, — не знание, а примета.

Дальше по классике: возвращаемый тип у invoke — any, а о переименованном поле в структуре фронт узнаёт уже от пользователей. Захотелось, чтобы всё это генерировалось из Rust-кода само.

Почему не готовые решения

ts-rs умеет экспортировать типы, но требует #[derive(TS)] на каждой структуре и ничего не знает про команды. tauri-specta закрывает и команды, но просит зарегистрировать хендлеры через свои макросы и живёт внутри кода приложения. Оба варианта рабочие, и если они вам подходят — берите их.

Мне хотелось другого: CLI, который натравливаешь на существующий проект и получаешь готовые файлы. Без аннотаций и без единой правки в Rust-коде. Так появился tauri-ts-generator. Вот что он делает:

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct User {
    pub id: u64,
    pub display_name: String,
}

#[tauri::command]
async fn get_user(user_id: u64, state: State<'_, AppState>) -> Result<User, String> {
    // ...
}

превращается в

// types.ts
export interface User {
  id: number;
  displayName: string;
}

// commands.ts
export async function getUser(userId: number): Promise<User> {
  return invoke<User>("get_user", { userId });
}

State исчез из сигнатуры — его инжектит сам Tauri, фронт его не передаёт. Первый же баг-репорт в трекере был, кстати, ровно про это: если объявить State через type-алиас, он просачивался в TS-типы. Result<User, String> стал Promise<User>, ошибка и так прилетит как reject. А display_name стал displayName, потому что на структуре висит rename_all = "camelCase" — генератор смотрит на serde-атрибуты, а не на имена полей в коде.

Как это устроено внутри

Никакой магии с компилятором: генератор парсит исходники через syn. Сканер обходит src-tauri, парсер вытаскивает функции с #[tauri::command] и все структуры, енумы и алиасы, резолвер разруливает модули и use-алиасы, генератор пишет два файла. Примерно 12 тысяч строк Rust. Резолвер, кстати, в какой-то момент разросся до 1378 строк в одном файле, и его пришлось пилить на подмодули — модульная система Rust со всеми её pub use, реэкспортами и use crate as alias оказалась самой неблагодарной частью задачи.

А вот что пришлось научиться понимать про serde (это реальные коммиты, не теоретизирование):

  • #[serde(rename_all)] во всех восьми вариантах, плюс точечный rename на полях;

  • tagged и untagged енумы: #[serde(tag = "type", content = "data")] полностью меняют форму JSON;

  • #[serde(flatten)] — в TS это intersection types;

  • Option<Option<T>>, который serde схлопывает;

  • #[serde(transparent)], newtype- и tuple-структуры;

  • кейс-конверсия с акронимами: serde считает HTTPServer двумя словами, а моя наивная реализация при rename_all = "snake_case" выдавала h_t_t_p_server.

Главный принцип, к которому я пришёл не сразу: генерировать надо не то, как выглядит Rust-код, а то, что serde реально отправит по проводу. Это разные вещи. TS-тип, который красиво повторяет структуру, но расходится с рантайм-JSON хотя бы в одном поле, хуже, чем any: он врёт с уверенным видом.

Война с макросами, или три релиза за один день

Дальше я упёрся в macro_rules!, который штампует десяток однотипных команд. Парсер исходников, естественно, видит макрос и не видит команд.

Ок, думаю, есть же cargo expand — разверну макросы и распарсю результат. Релизю 2.0.1: прогоняю тот же parse_commands по развёрнутому коду. Находит ноль команд.

Полез смотреть в сам expand-вывод (с чего, конечно, стоило начать). Оказалось, #[tauri::command] — процедурный макрос, и при развёртывании он съедает сам себя: в выводе остаётся голая функция без атрибута. А мой парсер фильтровал именно по атрибуту. Я искал маркер, который expand гарантированно уничтожает.

Зато рядом с каждой командой Tauri оставляет след: pub use __cmd__get_user;. Релизю 2.0.2: собираю эти __cmd__-маркеры из use-итемов и матчу обратно к функциям. Работает. Правда, теперь у меня два параллельных обходчика AST с почти одинаковой логикой.

В 2.0.3 объединил их в один walker и заодно обнаружил, что старый рекурсировал во вложенные mod ровно на один уровень, а в impl-блоки не заглядывал вообще. Команда внутри mod a { mod b { ... } } молча терялась. Восемь новых тестов пригвоздили контракт.

Все три релиза вышли 10 мая. Насыщенный был день.

Одну вещь я чинить не стал. Если макро-генерённая команда использует rename_all, это значение восстановить невозможно: код с переименованием генерируется на стороне потребителя, внутри generate_handler!, и в expand-выводе библиотеки его просто нет. Можно было городить эвристику, но я записал это в changelog как known limitation: нужен rename_all — пишите команду руками.

Windows, TOML и \U

Короткая, но любимая. Четыре теста падали только на Windows CI с паникой too few unicode value digits, expected unicode hexadecimal value. Тесты подставляли путь от tempdir в TOML-конфиг:

let config = format!(r#"types_file = "{}""#, dir.path().display());

На Windows путь — это C:\Users\runneradmin\.... А в TOML basic strings (двойные кавычки) бэкслеш — escape-символ, и \U означает начало юникод-эскейпа. Парсер ждёт восемь hex-цифр, получает sers\... и падает. Фикс — одинарные кавычки, literal strings, путь как есть. Один символ в кавычках, зелёный CI.

Тесты

Юнит-тесты на парсер и кейс-конверсию есть, но главную уверенность дают e2e: харнесс собирает во временной папке настоящий мини-проект с Cargo.toml и исходниками, запускает генератор целиком и проверяет выходные файлы. Сейчас тестов 352, и под каждый фикс из истории выше в репозитории лежит e2e, воспроизводящий проблему.

Релизы

Публикация на crates.io — через GitHub Actions по тегу. Локально cargo-release только бампает версию и ставит тег, токена registry на машине нет вообще. Паблиш идемпотентный, и это выстрадано. Релизный ран v2.0.0 опубликовал крейт и упал на следующем шаге, создании GitHub-релиза: 403, токену не хватило прав. А просто перезапустить нельзя — второй заход падал бы уже на «already exists». Отдельно отличился cargo search, через который CI проверял «крейт уже опубликован?»: однажды он вернул пустоту для крейта, который точно есть в индексе, и проверка оказалась хрупче того, от чего защищала. Теперь CI всегда пробует publish и разбирает ответ cargo: «already exists» — не ошибка, едем дальше.

Что в итоге

Генератор живёт на crates.io как tauri-ts-generator, сейчас версия 2.1.0. Внешних PR за время жизни проекта было два. Причём #[ts(optional)] вообще родился из обратной связи: пользователь попросил фичу в issue, а потом сам же прислал PR с доработкой генерации — prop?: T вместо prop: T | undefined. Второй PR принёс поддержку tauri::ipc::Channel<T>, так что стриминг из бэка на фронт теперь тоже типизирован. Чужой pull request в твой пет-проект — отдельное удовольствие.

Из честного: генератор парсит исходники, а не спрашивает компилятор, поэтому всегда найдётся способ его обмануть — алиас на алиас через три крейта, условная компиляция, макросы поверх макросов. Часть таких кейсов закрывает флаг use_cargo_expand, часть закрывать не планирую: предпочитаю простую модель с задокументированными ограничениями.

Код: https://github.com/Dudude-bit/tauri-codegen Крейт: https://crates.io/crates/tauri-ts-generator

Если у вас Tauri-проект и вы тоже устали от строковых invoke — попробуйте. Споткнётся на вашем коде — несите репро в issues.