Compare commits

...

21 Commits

Author SHA1 Message Date
5513979ebc feat: 配置构建任务依赖与输出路径优化缓存和产物管理
- 配置构建任务依赖关系并指定各任务输出路径以优化构建缓存和产物管理。
2026-01-18 17:33:04 +08:00
fb018d2f76 refactor: 更新输出目录路径为 src-tauri/binaries
- 更新输出目录路径为 src-tauri/binaries 并注释掉旧的输出目录配置
2026-01-18 17:27:59 +08:00
02560fb3f6 fix: 修正构建配置中的路径前缀问题
- 修正构建配置中的入口文件和输出目录路径,移除多余的前缀斜杠以确保路径正确解析。
2026-01-18 17:27:46 +08:00
4b69095a8d docs: 更新项目文档,优化中文描述与格式化
- 更新项目文档以反映最新的开发规范、命令和目录结构,优化中文描述并统一格式化。
2026-01-18 17:24:40 +08:00
2acf387ffd feat: 添加桌面端壳层选项Tauri v2并指向桌面特定指南
- 添加桌面端壳层选项Tauri v2并指向桌面特定指南
2026-01-18 17:20:16 +08:00
7656a371a0 fix: 开发模式退出时正确终止依赖任务
- 开发模式下退出时发送异常信号以终止依赖任务,生产模式下正常清理 Sidecar 进程。
2026-01-18 17:04:08 +08:00
8a8b873642 feat: 配置开发环境禁用缓存并启用持久化
- 配置开发环境以禁用缓存并启用持久化,确保开发服务器和Vite服务在运行时保持持续状态。
2026-01-18 16:59:45 +08:00
9d062abe69 refactor: 简化开发与生产模式日志输出
- 简化开发与生产模式的日志输出,移除冗余提示信息。
2026-01-18 16:52:20 +08:00
92da223d1e refactor: 优化侧边栏进程清理时机与错误提示
- 仅在应用退出事件时清理 Sidecar 进程,避免重复执行。
- 在开发模式下优化窗口创建错误提示并增强可读性,同时在生产模式下才执行 Sidecar 进程清理逻辑。
2026-01-18 16:47:51 +08:00
e703ef669a refactor: 移除未使用 dev 配置并优化 Turbo 任务依赖
- 移除未使用的 dev 配置并优化 Turbo 任务依赖关系
2026-01-18 16:40:00 +08:00
bbae8d04ae feat: 添加 Bun 构建产物的忽略规则
- 添加 Bun 构建产物的忽略规则
2026-01-18 16:36:37 +08:00
4035fcb202 refactor: 统一将 Sidecar 模式中的 Server 更名为 App
- 将构建输出文件名从 `server-` 更改为 `app-` 以匹配新的命名规范。
- 将 Sidecar 模式中的 Server 统一更名为 App,以准确反映其作为主业务逻辑载体的角色,并同步更新相关配置、文件命名、日志信息及代码注释。
- 更新允许执行的二进制文件为应用程序二进制文件。
- 将 Sidecar Server 相关的术语和日志信息统一更新为 Sidecar App,以准确反映实际启动的应用程序名称。
- 将外部二进制文件路径从 server 更改为 app
2026-01-18 16:33:48 +08:00
0df0bcb855 refactor: 重构项目结构并更新命名规范
- 更新项目目录名为 app-desktop 以反映新的项目结构命名规范
- 添加应用桌面模块并移除已弃用的 tauri-shell 包依赖
- 更新项目名称和库名称以反映新的应用标识
- 将主函数中的启动逻辑从 tauri_shell_lib 改为 app_desktop_lib
- 更新应用名称和标识符以反映新的项目名称。
2026-01-18 16:27:00 +08:00
a91f7f9d58 refactor: 将构建脚本从 bun 改为 tauri 构建
- 将构建脚本从使用 bun 编译改为使用 tauri 构建
2026-01-18 16:23:43 +08:00
72c566b721 feat: 添加 Tauri 桌面应用支持并设置窗口标题
- 添加 Tauri API 依赖包以支持桌面应用功能
- 添加 Tauri 应用 API 依赖以支持本地应用功能。
- 启用本地与远程访问权限并添加窗口标题设置权限
- 在 Tauri 应用中动态设置窗口标题为“待办事项”
2026-01-18 16:18:17 +08:00
6721a06d7f refactor: 重构任务配置并移除无效ui配置项
- 移除 ui 配置项并重新排序任务配置以确保持久化和缓存设置正确应用。
2026-01-18 16:06:24 +08:00
7f27221081 feat: 替换首页为完整待办事项应用并清理废弃路由
- 将首页替换为功能完整的待办事项应用,支持添加、标记完成、删除任务及进度统计,并优化了UI交互与视觉反馈。
- 删除待办事项功能页面及其相关逻辑实现
- 移除已废弃的 todos 路由相关配置及引用,保持路由树结构与实际路由文件一致。
2026-01-18 16:02:04 +08:00
2ce049965c refactor: 统一使用 PORT 环境变量替代 NITRO_PORT
- 将环境变量从 NITRO_PORT 更改为 PORT
- 将 sidecar 的环境变量从 NITRO_PORT 改为 PORT
2026-01-18 15:58:27 +08:00
10895b2c9f feat: 引入 Turborepo 优化构建流程
- 添加 Turborepo 缓存目录到忽略列表
- 添加 Turbo 2.7.5 版本及其各平台兼容的二进制文件以支持多平台构建和开发环境。
- 使用 turbo 管理构建和开发脚本,统一构建流程并简化脚本配置
- 移除构建配置中的自定义开发和构建命令,使用默认的构建行为。
- 添加 Turbo 配置文件以定义构建和开发任务依赖关系,启用持久化开发模式并禁用包管理器检查。
2026-01-18 15:56:44 +08:00
fc73243687 refactor: 优化服务器启动逻辑并移除冗余端口检测
- 移除冗余的端口占用检测函数并优化开发与生产模式下的服务器启动逻辑,提升代码可读性与启动可靠性。
2026-01-18 15:37:26 +08:00
a30d7c32fd feat: 优化开发与生产模式端口管理及启动逻辑
- 更新开发模式说明,明确开发时需手动启动前端服务器并支持热重载,生产模式自动启动侧车二进制,优化端口管理策略并完善最佳实践文档。
- 根据开发模式自动切换端口检测逻辑,开发模式下直接连接本地3000端口并等待服务器就绪,生产模式下正常启动sidecar并扫描可用端口,提升开发体验和启动可靠性。
- 移除开发环境URL配置,使用默认的开发服务器地址
2026-01-18 15:31:42 +08:00
17 changed files with 549 additions and 502 deletions

6
.gitignore vendored
View File

@@ -6,6 +6,12 @@
# Nitro # Nitro
.output/ .output/
# Bun build
*.bun-build
# Turborepo
.turbo/
### Node ### ### Node ###
# Logs # Logs

350
AGENTS.md
View File

@@ -1,280 +1,237 @@
# AGENTS.md - AI Coding Agent Guidelines # AGENTS.md - AI Coding Agent Guidelines
This document provides comprehensive guidelines for AI coding agents working in this TanStack Start fullstack starter codebase. 本文档为 AI 编程助手提供此 TanStack Start 全栈项目的开发规范和指南。
## Project Overview ## 项目概览
- **Framework**: TanStack Start (React SSR framework with file-based routing) - **框架**: TanStack Start (React SSR 框架,文件路由)
- **Runtime**: Bun - **运行时**: Bun
- **Language**: TypeScript (strict mode, ESNext) - **语言**: TypeScript (strict mode, ESNext)
- **Styling**: Tailwind CSS v4 - **样式**: Tailwind CSS v4
- **Database**: PostgreSQL with Drizzle ORM - **数据库**: PostgreSQL + Drizzle ORM
- **State Management**: TanStack Query - **状态管理**: TanStack Query
- **Routing**: TanStack Router (file-based) - **路由**: TanStack Router (文件路由)
- **RPC**: ORPC (type-safe RPC with contract-first design) - **RPC**: ORPC (类型安全 RPC契约优先)
- **Build Tool**: Vite - **构建工具**: Vite + Turbo
- **Linter/Formatter**: Biome - **代码质量**: Biome (格式化 + Lint)
- **桌面壳** (可选): Tauri v2 (详见 `src-tauri/AGENTS.md`)
## Build, Lint, and Test Commands ## 构建、Lint 和测试命令
### Development ### 开发
```bash ```bash
bun dev # Start development server bun dev # 使用 Turbo 并行启动 Tauri + Vite 开发服务器
bun db:studio # Open Drizzle Studio for database management bun dev:vite # 仅启动 Vite 开发服务器 (localhost:3000)
bun dev:tauri # 启动 Tauri 桌面应用
bun db:studio # 打开 Drizzle Studio 数据库管理界面
``` ```
### Building ### 构建
```bash ```bash
bun build # Build for production (outputs to .output/) bun build # 完整构建 (Vite → 编译 → Tauri 打包)
bun compile # Compile to standalone executable (out/server) bun build:vite # 仅构建 Vite (输出到 .output/)
bun serve # Preview production build bun build:compile # 编译为独立可执行文件 (使用 build.ts)
bun build:tauri # 构建 Tauri 桌面安装包
``` ```
### Code Quality ### 代码质量
```bash ```bash
bun typecheck # Run TypeScript compiler (tsc -b) bun typecheck # 运行 TypeScript 编译器检查 (tsc -b)
bun fix # Run Biome linter and formatter (auto-fix issues) bun fix # 运行 Biome 自动修复格式和 Lint 问题
biome check . # Check without auto-fix biome check . # 检查但不自动修复
biome format --write . # Format code only biome format --write . # 仅格式化代码
``` ```
### Database ### 数据库
```bash ```bash
bun db:generate # Generate migration files from schema bun db:generate # 从 schema 生成迁移文件
bun db:migrate # Run migrations bun db:migrate # 执行数据库迁移
bun db:push # Push schema changes directly (dev only) bun db:push # 直接推送 schema 变更 (仅开发环境)
``` ```
### Testing ### 测试
**Note**: No test framework is currently configured. When adding tests: **注意**: 当前未配置测试框架。添加测试时:
- Use Vitest or Bun's built-in test runner - 使用 Vitest Bun 内置测试运行器
- Run single test file: `bun test path/to/test.ts` - 运行单个测试文件: `bun test path/to/test.ts`
- Run specific test: `bun test -t "test name pattern"` - 运行特定测试: `bun test -t "测试名称模式"`
## Code Style Guidelines ## 代码风格指南
### Formatting (Biome) ### 格式化 (Biome)
**Indentation**: 2 spaces (not tabs) **缩进**: 2 空格 (不使用 tab)
**Line Endings**: LF (Unix-style) **换行符**: LF (Unix 风格)
**Quotes**: Single quotes for strings **引号**: 单引号 `'string'`
**Semicolons**: As needed (ASI - automatic semicolon insertion) **分号**: 按需 (ASI - 自动分号插入)
**Arrow Parens**: Always use parentheses `(x) => x` **箭头函数括号**: 始终使用 `(x) => x`
Example: 示例:
```typescript ```typescript
const myFunc = (value: string) => { const myFunc = (value: string) => {
return value.toUpperCase() return value.toUpperCase()
} }
``` ```
### Import Organization ### 导入组织
Imports are auto-organized by Biome. Order: Biome 自动组织导入。顺序:
1. External dependencies 1. 外部依赖
2. Internal imports using `@/*` alias 2. 内部导入 (使用 `@/*` 别名)
3. Type imports (use `type` keyword when importing only types) 3. 类型导入 (仅导入类型时使用 `type` 关键字)
Example: 示例:
```typescript ```typescript
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start' import { oc } from '@orpc/contract'
import { z } from 'zod'
import { db } from '@/db' import { db } from '@/db'
import { todoTable } from '@/db/schema'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
``` ```
### TypeScript ### TypeScript
**Strict Mode**: Enabled with additional strictness flags **严格模式**: 启用了额外的严格检查
- `strict: true` - `strict: true`
- `noUncheckedIndexedAccess: true` - Array/object indexing returns `T | undefined` - `noUncheckedIndexedAccess: true` - 数组/对象索引返回 `T | undefined`
- `noImplicitOverride: true` - `noImplicitOverride: true`
- `noFallthroughCasesInSwitch: true` - `noFallthroughCasesInSwitch: true`
**Module Resolution**: `bundler` mode with `verbatimModuleSyntax` **模块解析**: `bundler` 模式 + `verbatimModuleSyntax`
- Always use `.ts`/`.tsx` extensions in imports - 导入时始终使用 `.ts`/`.tsx` 扩展名
- Use `@/*` path alias for `src/*` - 使用 `@/*` 路径别名指向 `src/*`
**Type Annotations**: **类型注解**:
- Always annotate function parameters and return types for public APIs - 公共 API 的函数参数和返回类型必须注解
- Prefer explicit types over `any` - 优先使用显式类型而非 `any`
- Use `type` for object shapes, `interface` for extendable contracts - 对象形状用 `type`,可扩展契约用 `interface`
- Use `Readonly<T>` for immutable props - 不可变 props 使用 `Readonly<T>`
Example: ### 命名规范
```typescript
function getTodos(): Promise<Todo[]> {
return db.query.todoTable.findMany()
}
type Props = Readonly<{ - **文件**: 工具函数用 kebab-case组件用 PascalCase
children: ReactNode
}>
```
### Naming Conventions
- **Files**: kebab-case for utilities, PascalCase for components
- `utils.ts`, `todo.tsx`, `NotFound.tsx` - `utils.ts`, `todo.tsx`, `NotFound.tsx`
- **Routes**: Use TanStack Router conventions - **路由**: 遵循 TanStack Router 约定
- `routes/index.tsx``/` - `routes/index.tsx``/`
- `routes/todo.tsx``/todo` - `routes/__root.tsx`根布局
- `routes/__root.tsx` → Root layout - **组件**: PascalCase 箭头函数 (Biome 规则 `useArrowFunction` 强制)
- **Components**: PascalCase function declarations - **函数**: camelCase
- **Functions**: camelCase - **常量**: 真常量用 UPPER_SNAKE_CASE配置对象用 camelCase
- **Constants**: UPPER_SNAKE_CASE for true constants, camelCase for config objects - **类型/接口**: PascalCase
- **Types/Interfaces**: PascalCase
### React Patterns ### React 模式
**Components**: Use arrow functions (enforced by Biome rule `useArrowFunction`) **组件**: 使用箭头函数
```typescript ```typescript
const MyComponent = ({ title }: { title: string }) => { const MyComponent = ({ title }: { title: string }) => {
return <div>{title}</div> return <div>{title}</div>
} }
``` ```
**Server Functions**: Use TanStack Start's `createServerFn` **路由**: 使用 `createFileRoute` 定义路由
```typescript ```typescript
const getTodos = createServerFn({ method: 'GET' }).handler(async () => { export const Route = createFileRoute('/')({
const todos = await db.query.todoTable.findMany() component: Home,
return todos
}) })
``` ```
**Routing**: Use `createFileRoute` for route definitions **数据获取**: 使用 TanStack Query hooks
```typescript - `useSuspenseQuery` - 保证有数据
export const Route = createFileRoute('/todo')({ - `useQuery` - 数据可能为空
component: Todo,
})
```
**Data Fetching**: Use TanStack Query hooks **Props**: 禁止直接修改 props (Biome 规则 `noReactPropAssignments`)
- `useSuspenseQuery` for guaranteed data
- `useQuery` when data might be optional
**Props**: No direct prop mutations (enforced by `noReactPropAssignments`) ### 数据库 Schema (Drizzle)
### Database Schema (Drizzle) -`src/db/schema/*.ts` 定义 schema
-`src/db/schema/index.ts` 导出
- 使用 `drizzle-orm/pg-core` 的 PostgreSQL 类型
- 主键使用 `uuidv7()` (需要 PostgreSQL 扩展)
- 始终包含 `createdAt``updatedAt` 时间戳
- Define schemas in `src/db/schema/*.ts` 示例:
- Export from `src/db/schema/index.ts`
- Use PostgreSQL types from `drizzle-orm/pg-core`
- Use `uuidv7()` for primary keys (requires PostgreSQL extension)
- Always include `createdAt` and `updatedAt` timestamps
Example:
```typescript ```typescript
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core' import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { sql } from 'drizzle-orm' import { sql } from 'drizzle-orm'
export const myTable = pgTable('my_table', { export const myTable = pgTable('my_table', {
id: uuid('id').primaryKey().default(sql`uuidv7()`), id: uuid().primaryKey().default(sql`uuidv7()`),
name: text('name').notNull(), name: text().notNull(),
createdAt: timestamp('created_at', { withTimezone: true }) createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
.notNull() updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
.defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true })
.notNull()
.defaultNow()
.$onUpdateFn(() => new Date()),
}) })
``` ```
### Environment Variables ### 环境变量
- Use `@t3-oss/env-core` for type-safe env validation - 使用 `@t3-oss/env-core` 进行类型安全的环境变量验证
- Define schema in `src/env.ts` - `src/env.ts` 定义 schema
- Server vars: No prefix - 服务端变量: 无前缀
- Client vars: Must have `VITE_` prefix - 客户端变量: 必须有 `VITE_` 前缀
- Always validate with Zod schemas - 使用 Zod schema 验证
### Error Handling ### 错误处理
- Use try-catch for async operations - 异步操作使用 try-catch
- Throw errors with descriptive messages - 抛出带有描述性消息的错误
- Prefer Result types or error boundaries for user-facing errors - 用户界面错误优先使用 Result 类型或错误边界
- Log errors appropriately (avoid logging sensitive data) - 适当记录错误 (避免记录敏感数据)
### Styling (Tailwind CSS) ### 样式 (Tailwind CSS)
- Use Tailwind v4 utility classes - 使用 Tailwind v4 工具类
- Import styles via `@/styles.css?url` - 通过 `@/styles.css?url` 导入样式
- Prefer composition over custom CSS - 优先使用组合而非自定义 CSS
- Use responsive modifiers: `sm:`, `md:`, `lg:` - 响应式修饰符: `sm:`, `md:`, `lg:`
- Use Chinese text for UI when appropriate (as seen in codebase) - UI 文本适当使用中文
## File Structure ## 目录结构
``` ```
src/ src/
├── components/ # Reusable React components ├── components/ # 可复用 React 组件
├── db/ ├── db/
│ ├── schema/ # Drizzle schema definitions │ ├── schema/ # Drizzle schema 定义
│ └── index.ts # Database instance │ └── index.ts # 数据库实例
├── lib/ # Utility functions ├── integrations/ # 第三方集成 (TanStack Query/Router)
├── orpc/ # ORPC (RPC layer) ├── lib/ # 工具函数
│ ├── contracts/ # Contract definitions (input/output schemas) ├── orpc/ # ORPC (RPC 层)
│ ├── handlers/ # Server-side procedure implementations │ ├── contracts/ # 契约定义 (input/output schemas)
│ ├── middlewares/ # Middleware (e.g., DB provider) │ ├── handlers/ # 服务端过程实现
│ ├── contract.ts # Contract aggregation │ ├── middlewares/ # 中间件 (如 DB provider)
│ ├── router.ts # Router composition │ ├── contract.ts # 契约聚合
│ ├── server.ts # Server instance │ ├── router.ts # 路由组合
│ ├── client.ts # Isomorphic client │ ├── server.ts # 服务端实例
│ └── types.ts # Type utilities │ └── client.ts # 同构客户端
├── routes/ # TanStack Router file-based routes ├── routes/ # TanStack Router 文件路由
│ ├── __root.tsx # Root layout │ ├── __root.tsx # 根布局
│ ├── index.tsx # Home page │ ├── index.tsx # 首页
│ └── api/rpc.$.ts # ORPC HTTP endpoint │ └── api/rpc.$.ts # ORPC HTTP 端点
├── env.ts # Environment variable validation ├── env.ts # 环境变量验证
── index.ts # Application entry point ── router.tsx # 路由配置
├── router.tsx # Router configuration
├── routeTree.gen.ts # Auto-generated (DO NOT EDIT)
└── styles.css # Global styles
``` ```
## Important Notes ## 重要提示
- **DO NOT** edit `src/routeTree.gen.ts` - it's auto-generated - **禁止** 编辑 `src/routeTree.gen.ts` - 自动生成
- **DO NOT** commit `.env` files - use `.env.example` for templates - **禁止** 提交 `.env` 文件 - 使用 `.env.example` 作为模板
- **DO** run `bun fix` before committing to ensure code quality - **必须** 在提交前运行 `bun fix`
- **DO** use the `@/*` path alias instead of relative imports - **必须** 使用 `@/*` 路径别名而非相对导入
- **DO** leverage React Compiler (babel-plugin-react-compiler) - avoid manual memoization - **必须** 利用 React Compiler (babel-plugin-react-compiler) - 避免手动 memoization
## Git Workflow ## Git 工作流
1. Make changes following the style guidelines above 1. 按照上述风格指南进行修改
2. Run `bun fix` to auto-format and lint 2. 运行 `bun fix` 自动格式化和 lint
3. Run `bun typecheck` to ensure type safety 3. 运行 `bun typecheck` 确保类型安全
4. Test changes locally with `bun dev` 4. 使用 `bun dev` 本地测试变更
5. Commit with clear, descriptive messages 5. 使用清晰的描述性消息提交
## Common Patterns ## 常见模式
### Adding a New Route ### 创建 ORPC 过程
1. Create `src/routes/my-route.tsx`
2. Export route with `createFileRoute`
3. Route tree auto-updates on save
### Adding Database Table **步骤 1: 定义契约** (`src/orpc/contracts/my-feature.ts`)
1. Create schema in `src/db/schema/my-table.ts`
2. Export from `src/db/schema/index.ts`
3. Run `bun db:generate` to create migration
4. Run `bun db:migrate` to apply migration
### Creating Server Function
```typescript
const myServerFn = createServerFn({ method: 'POST' })
.validator((data) => mySchema.parse(data))
.handler(async ({ data }) => {
// Server-side logic here
return result
})
```
### Creating ORPC Procedures
**Step 1: Define Contract** (`src/orpc/contracts/my-feature.ts`)
```typescript ```typescript
import { oc } from '@orpc/contract' import { oc } from '@orpc/contract'
import { z } from 'zod' import { z } from 'zod'
@@ -285,7 +242,7 @@ export const myContract = {
} }
``` ```
**Step 2: Implement Handler** (`src/orpc/handlers/my-feature.ts`) **步骤 2: 实现处理器** (`src/orpc/handlers/my-feature.ts`)
```typescript ```typescript
import { os } from '@/orpc/server' import { os } from '@/orpc/server'
import { dbProvider } from '@/orpc/middlewares' import { dbProvider } from '@/orpc/middlewares'
@@ -293,24 +250,21 @@ import { dbProvider } from '@/orpc/middlewares'
export const get = os.myFeature.get export const get = os.myFeature.get
.use(dbProvider) .use(dbProvider)
.handler(async ({ context, input }) => { .handler(async ({ context, input }) => {
const item = await context.db.query.myTable.findFirst(...) return await context.db.query.myTable.findFirst(...)
return item
}) })
``` ```
**Step 3: Register in Contract & Router** **步骤 3: 注册到契约和路由**
```typescript ```typescript
// src/orpc/contract.ts // src/orpc/contract.ts
export const contract = { export const contract = { myFeature: myContract }
myFeature: myContract,
}
// src/orpc/router.ts // src/orpc/router.ts
import * as myFeature from './handlers/my-feature' import * as myFeature from './handlers/my-feature'
export const router = os.router({ myFeature }) export const router = os.router({ myFeature })
``` ```
**Step 4: Use in Component** **步骤 4: 在组件中使用**
```typescript ```typescript
import { orpc } from '@/orpc' import { orpc } from '@/orpc'
const query = useSuspenseQuery(orpc.myFeature.get.queryOptions({ id })) const query = useSuspenseQuery(orpc.myFeature.get.queryOptions({ id }))
@@ -319,5 +273,5 @@ const mutation = useMutation(orpc.myFeature.create.mutationOptions())
--- ---
**Last Updated**: 2026-01-18 **最后更新**: 2026-01-18
**Project Version**: Based on package.json dependencies **项目版本**: 基于 package.json 依赖版本

View File

@@ -93,9 +93,9 @@ class BuildConfigService extends Context.Tag('BuildConfigService')<
static readonly Live = Layer.effect( static readonly Live = Layer.effect(
BuildConfigService, BuildConfigService,
BuildConfigService.fromRaw({ BuildConfigService.fromRaw({
entrypoint: './.output/server/index.mjs', entrypoint: '.output/server/index.mjs',
// outputDir: './out', // outputDir: 'out',
outputDir: './src-tauri/binaries', outputDir: 'src-tauri/binaries',
targets: ['bun-windows-x64', 'bun-darwin-arm64', 'bun-linux-x64'], targets: ['bun-windows-x64', 'bun-darwin-arm64', 'bun-linux-x64'],
}), }),
) )
@@ -150,7 +150,7 @@ class BuildService extends Context.Tag('BuildService')<
Bun.build({ Bun.build({
entrypoints: [config.entrypoint], entrypoints: [config.entrypoint],
compile: { compile: {
outfile: `server-${targetMap[target]}`, outfile: `app-${targetMap[target]}`,
target: target, target: target,
}, },
outdir: config.outputDir, outdir: config.outputDir,
@@ -181,7 +181,7 @@ class BuildService extends Context.Tag('BuildService')<
Bun.build({ Bun.build({
entrypoints: [config.entrypoint], entrypoints: [config.entrypoint],
compile: { compile: {
outfile: `server-${targetMap[target]}`, outfile: `app-${targetMap[target]}`,
target: target, target: target,
}, },
outdir: config.outputDir, outdir: config.outputDir,

View File

@@ -15,6 +15,7 @@
"@tanstack/react-router": "^1.151.0", "@tanstack/react-router": "^1.151.0",
"@tanstack/react-router-ssr-query": "^1.151.0", "@tanstack/react-router-ssr-query": "^1.151.0",
"@tanstack/react-start": "^1.151.0", "@tanstack/react-start": "^1.151.0",
"@tauri-apps/api": "^2.9.1",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3", "drizzle-zod": "^0.8.3",
"postgres": "^3.4.8", "postgres": "^3.4.8",
@@ -39,6 +40,7 @@
"effect": "^3.19.14", "effect": "^3.19.14",
"nitro": "npm:nitro-nightly@latest", "nitro": "npm:nitro-nightly@latest",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"turbo": "^2.7.5",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^8.0.0-beta.8", "vite": "^8.0.0-beta.8",
"vite-tsconfig-paths": "^6.0.4", "vite-tsconfig-paths": "^6.0.4",
@@ -532,6 +534,8 @@
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.145.4", "", {}, "sha512-CI75JrfqSluhdGwLssgVeQBaCphgfkMQpi8MCY3UJX1hoGzXa8kHYJcUuIFMOLs1q7zqHy++EVVtMK03osR5wQ=="], "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.145.4", "", {}, "sha512-CI75JrfqSluhdGwLssgVeQBaCphgfkMQpi8MCY3UJX1hoGzXa8kHYJcUuIFMOLs1q7zqHy++EVVtMK03osR5wQ=="],
"@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.9.6", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="], "@tauri-apps/cli": ["@tauri-apps/cli@2.9.6", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="],
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ=="], "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ=="],
@@ -926,6 +930,20 @@
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
"turbo": ["turbo@2.7.5", "", { "optionalDependencies": { "turbo-darwin-64": "2.7.5", "turbo-darwin-arm64": "2.7.5", "turbo-linux-64": "2.7.5", "turbo-linux-arm64": "2.7.5", "turbo-windows-64": "2.7.5", "turbo-windows-arm64": "2.7.5" }, "bin": { "turbo": "bin/turbo" } }, "sha512-7Imdmg37joOloTnj+DPrab9hIaQcDdJ5RwSzcauo/wMOSAgO+A/I/8b3hsGGs6PWQz70m/jkPgdqWsfNKtwwDQ=="],
"turbo-darwin-64": ["turbo-darwin-64@2.7.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-nN3wfLLj4OES/7awYyyM7fkU8U8sAFxsXau2bYJwAWi6T09jd87DgHD8N31zXaJ7LcpyppHWPRI2Ov9MuZEwnQ=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.7.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wCoDHMiTf3FgLAbZHDDx/unNNonSGhsF5AbbYODbxnpYyoKDpEYacUEPjZD895vDhNvYCH0Nnk24YsP4n/cD6g=="],
"turbo-linux-64": ["turbo-linux-64@2.7.5", "", { "os": "linux", "cpu": "x64" }, "sha512-KKPvhOmJMmzWj/yjeO4LywkQ85vOJyhru7AZk/+c4B6OUh/odQ++SiIJBSbTG2lm1CuV5gV5vXZnf/2AMlu3Zg=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.7.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-8PIva4L6BQhiPikUTds9lSFSHXVDAsEvV6QUlgwPsXrtXVQMVi6Sv9p+IxtlWQFvGkdYJUgX9GnK2rC030Xcmw=="],
"turbo-windows-64": ["turbo-windows-64@2.7.5", "", { "os": "win32", "cpu": "x64" }, "sha512-rupskv/mkIUgQXzX/wUiK00mKMorQcK8yzhGFha/D5lm05FEnLx8dsip6rWzMcVpvh+4GUMA56PgtnOgpel2AA=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.7.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-G377Gxn6P42RnCzfMyDvsqQV7j69kVHKlhz9J4RhtJOB5+DyY4yYh/w0oTIxZQ4JRMmhjwLu3w9zncMoQ6nNDw=="],
"type-fest": ["type-fest@5.4.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ=="], "type-fest": ["type-fest@5.4.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-xygQcmneDyzsEuKZrFbRMne5HDqMs++aFzefrJTgEIKjQ3rekM+RPfFCVq2Gp1VIDqddoYeppCj4Pcb+RZW0GQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],

View File

@@ -3,7 +3,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "bun run build:vite && bun run build:compile", "build": "turbo build:tauri",
"build:compile": "bun build.ts", "build:compile": "bun build.ts",
"build:tauri": "tauri build", "build:tauri": "tauri build",
"build:vite": "vite build", "build:vite": "vite build",
@@ -11,7 +11,7 @@
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"dev": "bun run dev:vite", "dev": "turbo dev:tauri",
"dev:tauri": "tauri dev", "dev:tauri": "tauri dev",
"dev:vite": "vite dev", "dev:vite": "vite dev",
"fix": "biome check --write", "fix": "biome check --write",
@@ -28,6 +28,7 @@
"@tanstack/react-router": "^1.151.0", "@tanstack/react-router": "^1.151.0",
"@tanstack/react-router-ssr-query": "^1.151.0", "@tanstack/react-router-ssr-query": "^1.151.0",
"@tanstack/react-start": "^1.151.0", "@tanstack/react-start": "^1.151.0",
"@tauri-apps/api": "^2.9.1",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"drizzle-zod": "^0.8.3", "drizzle-zod": "^0.8.3",
"postgres": "^3.4.8", "postgres": "^3.4.8",
@@ -52,6 +53,7 @@
"effect": "^3.19.14", "effect": "^3.19.14",
"nitro": "npm:nitro-nightly@latest", "nitro": "npm:nitro-nightly@latest",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"turbo": "^2.7.5",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^8.0.0-beta.8", "vite": "^8.0.0-beta.8",
"vite-tsconfig-paths": "^6.0.4" "vite-tsconfig-paths": "^6.0.4"

View File

@@ -6,8 +6,10 @@
- **项目类型**: Tauri v2 桌面应用(轻量级壳子) - **项目类型**: Tauri v2 桌面应用(轻量级壳子)
- **后端**: Rust (Edition 2021) - **后端**: Rust (Edition 2021)
- **架构**: Sidecar 模式 - Sidecar Server 承载主要业务逻辑 - **架构**: Sidecar 模式 - Sidecar App 承载主要业务逻辑
- **设计理念**: Tauri 仅提供原生桌面能力文件对话框、系统通知等Web 逻辑全部由 Sidecar Server 处理 - **设计理念**: Tauri 仅提供原生桌面能力文件对话框、系统通知等Web 逻辑全部由 Sidecar App 处理
- **开发模式**: 使用 localhost:3000需手动启动开发服务器
- **生产模式**: 自动启动 Sidecar 二进制
- **异步运行时**: Tokio - **异步运行时**: Tokio
- **Rust 版本**: 1.92.0+ - **Rust 版本**: 1.92.0+
- **工具管理**: 使用 mise 管理 Rust 和 Tauri CLI 版本(见 `mise.toml` - **工具管理**: 使用 mise 管理 Rust 和 Tauri CLI 版本(见 `mise.toml`
@@ -16,13 +18,22 @@
### 开发运行 ### 开发运行
```bash ```bash
# 开发模式运行 (带 hot-reload) # 开发模式运行 (需要先启动开发服务器)
# 终端 1: 启动前端开发服务器
bun run dev
# 终端 2: 启动 Tauri 应用
tauri dev tauri dev
# 仅运行 Rust 二进制 (不推荐,需要手动启动 Sidecar Server) # 或者使用单命令并行启动(需要配置 package.json
cargo run bun run dev:tauri
``` ```
**开发模式说明**
- 开发模式下Tauri 直接连接到 `localhost:3000`(不启动 sidecar 二进制)
- 需要手动运行 `bun run dev` 来启动开发服务器
- 支持热重载HMR无需重启 Tauri 应用
### 构建 ### 构建
```bash ```bash
# 开发构建 (debug mode) # 开发构建 (debug mode)
@@ -80,7 +91,7 @@ cargo clean
## 项目结构 ## 项目结构
``` ```
tauri-shell/ app-desktop/
├── src/ ├── src/
│ ├── main.rs # 入口文件 (仅调用 lib::run) │ ├── main.rs # 入口文件 (仅调用 lib::run)
│ ├── lib.rs # 核心应用逻辑 (注册插件、命令、状态) │ ├── lib.rs # 核心应用逻辑 (注册插件、命令、状态)
@@ -88,7 +99,7 @@ tauri-shell/
│ │ └── mod.rs # 原生桌面功能命令 (文件对话框、通知等) │ │ └── mod.rs # 原生桌面功能命令 (文件对话框、通知等)
│ └── sidecar.rs # Sidecar 进程管理 (启动、端口扫描、清理) │ └── sidecar.rs # Sidecar 进程管理 (启动、端口扫描、清理)
├── binaries/ # Sidecar 二进制文件 ├── binaries/ # Sidecar 二进制文件
│ └── server-* # Sidecar Server 可执行文件 (示例: server) │ └── app-* # Sidecar App 可执行文件 (示例: app)
├── capabilities/ # Tauri v2 权限配置 ├── capabilities/ # Tauri v2 权限配置
│ └── default.json │ └── default.json
├── icons/ # 应用图标资源 ├── icons/ # 应用图标资源
@@ -171,14 +182,14 @@ async fn is_port_available(port: u16) -> bool {
// ✅ 推荐 // ✅ 推荐
let sidecar = app_handle let sidecar = app_handle
.shell() .shell()
.sidecar("server") .sidecar("app")
.expect("无法找到 server sidecar"); .expect("无法找到 app sidecar");
let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败"); let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败");
// 日志记录 // 日志记录
eprintln!("✗ Sidecar Server 启动失败"); eprintln!("✗ Sidecar App 启动失败");
println!("✓ Sidecar Server 启动成功!"); println!("✓ Sidecar App 启动成功!");
// ❌ 避免 // ❌ 避免
let data = read_file().unwrap(); // 无上下文信息 let data = read_file().unwrap(); // 无上下文信息
@@ -215,7 +226,7 @@ tauri::async_runtime::spawn(async move {
```rust ```rust
// ✅ 推荐 // ✅ 推荐
// 全局状态:存储 Sidecar Server 进程句柄 // 全局状态:存储 Sidecar App 进程句柄
struct SidecarProcess(Mutex<Option<CommandChild>>); struct SidecarProcess(Mutex<Option<CommandChild>>);
// 检查端口是否可用 // 检查端口是否可用
@@ -286,9 +297,9 @@ if let Some(state) = app_handle.try_state::<SidecarProcess>() {
// 启动 sidecar // 启动 sidecar
let sidecar = app_handle let sidecar = app_handle
.shell() .shell()
.sidecar("server") .sidecar("app")
.expect("无法找到 server sidecar") .expect("无法找到 app sidecar")
.env("NITRO_PORT", port.to_string()); .env("PORT", port.to_string());
// 清理进程 // 清理进程
match event { match event {
@@ -323,12 +334,17 @@ tokio = { version = "1", features = ["net"] }
## 最佳实践 ## 最佳实践
1. **进程生命周期**: 始终在应用退出时清理子进程和资源 1. **开发环境配置**:
2. **端口管理**: 使用端口扫描避免硬编码端口冲突 - 开发模式下需先启动前端开发服务器(`bun run dev`),再启动 Tauri`tauri dev`
3. **超时处理**: 异步操作设置合理的超时时间 (如 5 秒) - 生产构建自动打包 sidecar 二进制,无需额外配置
4. **日志**: 使用表情符号 (✓/✗) 和中文消息提供清晰的状态反馈 2. **进程生命周期**: 始终在应用退出时清理子进程和资源
5. **错误退出**: 关键错误时调用 `std::process::exit(1)` 3. **端口管理**:
6. **窗口配置**: 使用 `WebviewWindowBuilder` 动态创建窗口 - 开发模式固定使用 3000 端口(与开发服务器匹配)
- 生产模式使用端口扫描避免硬编码端口冲突
4. **超时处理**: 异步操作设置合理的超时时间 (如 5 秒)
5. **日志**: 使用表情符号 (✓/✗/🔧/🚀) 和中文消息提供清晰的状态反馈
6. **错误退出**: 关键错误时调用 `std::process::exit(1)`
7. **窗口配置**: 使用 `WebviewWindowBuilder` 动态创建窗口
## 提交代码前检查清单 ## 提交代码前检查清单

22
src-tauri/Cargo.lock generated
View File

@@ -47,6 +47,17 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "app-desktop"
version = "0.1.0"
dependencies = [
"serde",
"tauri",
"tauri-build",
"tauri-plugin-shell",
"tokio",
]
[[package]] [[package]]
name = "atk" name = "atk"
version = "0.18.2" version = "0.18.2"
@@ -3423,17 +3434,6 @@ dependencies = [
"wry", "wry",
] ]
[[package]]
name = "tauri-shell"
version = "0.1.0"
dependencies = [
"serde",
"tauri",
"tauri-build",
"tauri-plugin-shell",
"tokio",
]
[[package]] [[package]]
name = "tauri-utils" name = "tauri-utils"
version = "2.8.1" version = "2.8.1"

View File

@@ -1,5 +1,5 @@
[package] [package]
name = "tauri-shell" name = "app-desktop"
version = "0.1.0" version = "0.1.0"
description = "A Tauri App" description = "A Tauri App"
authors = ["imbytecat"] authors = ["imbytecat"]
@@ -11,7 +11,7 @@ edition = "2021"
# The `_lib` suffix may seem redundant but it is necessary # The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name. # to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "tauri_shell_lib" name = "app_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]

View File

@@ -3,13 +3,22 @@
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": ["main"],
"local": true,
"remote": {
"urls": [
"http://localhost:*",
"http://127.0.0.1:*",
"http{s}?://localhost(:\\d+)?/*"
]
},
"permissions": [ "permissions": [
"core:default", "core:default",
"core:window:allow-set-title",
{ {
"identifier": "shell:allow-execute", "identifier": "shell:allow-execute",
"allow": [ "allow": [
{ {
"name": "binaries/server", "name": "binaries/app",
"sidecar": true "sidecar": true
} }
] ]

View File

@@ -25,11 +25,9 @@ pub fn run() {
.expect("error while building tauri application") .expect("error while building tauri application")
.run(|app_handle, event| { .run(|app_handle, event| {
// 监听应用退出事件,清理 Sidecar 进程 // 监听应用退出事件,清理 Sidecar 进程
match event { if let tauri::RunEvent::Exit = event {
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => { // 只在 Exit 事件时清理,避免重复执行
sidecar::cleanup_sidecar_process(app_handle); sidecar::cleanup_sidecar_process(app_handle);
}
_ => {}
} }
}); });
} }

View File

@@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() { fn main() {
tauri_shell_lib::run() app_desktop_lib::run()
} }

View File

@@ -7,7 +7,7 @@ use tauri_plugin_shell::ShellExt;
// ===== 配置常量 ===== // ===== 配置常量 =====
/// Sidecar Server 启动超时时间(秒) /// Sidecar App 启动超时时间(秒)
const STARTUP_TIMEOUT_SECS: u64 = 5; const STARTUP_TIMEOUT_SECS: u64 = 5;
/// 默认起始端口 /// 默认起始端口
@@ -30,7 +30,7 @@ const WINDOW_TITLE: &str = "Tauri App";
/// 全局状态:存储 Sidecar 进程句柄 /// 全局状态:存储 Sidecar 进程句柄
pub struct SidecarProcess(pub Mutex<Option<CommandChild>>); pub struct SidecarProcess(pub Mutex<Option<CommandChild>>);
// 检查端口是否可用 // 检查端口是否可用(未被占用)
async fn is_port_available(port: u16) -> bool { async fn is_port_available(port: u16) -> bool {
tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port)) tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
.await .await
@@ -49,7 +49,36 @@ async fn find_available_port(start: u16) -> u16 {
/// 启动 Sidecar 进程并创建主窗口 /// 启动 Sidecar 进程并创建主窗口
pub fn spawn_sidecar(app_handle: tauri::AppHandle) { pub fn spawn_sidecar(app_handle: tauri::AppHandle) {
// 检测是否为开发模式
let is_dev = cfg!(debug_assertions);
if is_dev {
// 开发模式:直接创建窗口连接到 Vite 开发服务器
println!("🔧 开发模式");
match tauri::WebviewWindowBuilder::new(
&app_handle,
"main",
tauri::WebviewUrl::External("http://localhost:3000".parse().unwrap()),
)
.title(WINDOW_TITLE)
.inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
.center()
.build()
{
Ok(_) => println!("✓ 开发窗口创建成功"),
Err(e) => {
eprintln!("✗ 窗口创建失败: {}", e);
}
}
return;
}
// 生产模式:启动 sidecar 二进制
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
println!("🚀 生产模式");
// 查找可用端口 // 查找可用端口
let port = find_available_port(DEFAULT_PORT).await; let port = find_available_port(DEFAULT_PORT).await;
println!("使用端口: {}", port); println!("使用端口: {}", port);
@@ -57,9 +86,9 @@ pub fn spawn_sidecar(app_handle: tauri::AppHandle) {
// 启动 sidecar // 启动 sidecar
let sidecar = app_handle let sidecar = app_handle
.shell() .shell()
.sidecar("server") .sidecar("app")
.expect("无法找到 server") .expect("无法找到 app")
.env("NITRO_PORT", port.to_string()); .env("PORT", port.to_string());
let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败"); let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败");
@@ -71,17 +100,17 @@ pub fn spawn_sidecar(app_handle: tauri::AppHandle) {
// 监听 stdout等待服务器就绪信号 // 监听 stdout等待服务器就绪信号
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
let timeout = Duration::from_secs(STARTUP_TIMEOUT_SECS); let timeout = Duration::from_secs(STARTUP_TIMEOUT_SECS);
let mut server_ready = false; let mut app_ready = false;
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
if let CommandEvent::Stdout(line) = event { if let CommandEvent::Stdout(line) = event {
let output = String::from_utf8_lossy(&line); let output = String::from_utf8_lossy(&line);
println!("Server: {}", output); println!("App: {}", output);
// 检测服务器启动成功的标志 // 检测 App 启动成功的标志
if output.contains("Listening on:") || output.contains("localhost") { if output.contains("Listening on:") || output.contains("localhost") {
server_ready = true; app_ready = true;
println!("Server 启动成功!"); println!("App 启动成功!");
// 创建主窗口 // 创建主窗口
let url = format!("http://localhost:{}", port); let url = format!("http://localhost:{}", port);
@@ -102,16 +131,13 @@ pub fn spawn_sidecar(app_handle: tauri::AppHandle) {
// 超时检查 // 超时检查
if start_time.elapsed() > timeout { if start_time.elapsed() > timeout {
eprintln!( eprintln!("✗ 启动超时: App 未能在 {} 秒内启动", STARTUP_TIMEOUT_SECS);
"✗ 启动超时: Server 未能在 {} 秒内启动",
STARTUP_TIMEOUT_SECS
);
break; break;
} }
} }
if !server_ready { if !app_ready {
eprintln!("Server 启动失败"); eprintln!("App 启动失败");
std::process::exit(1); std::process::exit(1);
} }
}); });
@@ -119,6 +145,15 @@ pub fn spawn_sidecar(app_handle: tauri::AppHandle) {
/// 清理 Sidecar 进程 (在应用退出时调用) /// 清理 Sidecar 进程 (在应用退出时调用)
pub fn cleanup_sidecar_process(app_handle: &tauri::AppHandle) { pub fn cleanup_sidecar_process(app_handle: &tauri::AppHandle) {
let is_dev = cfg!(debug_assertions);
if is_dev {
// 开发模式退出时发送异常信号exit 1让 Turbo 停止 Vite 服务器
println!("🔧 开发模式退出,终止所有依赖任务...");
std::process::exit(1);
}
// 生产模式:正常清理 sidecar 进程
println!("应用退出,正在清理 Sidecar 进程..."); println!("应用退出,正在清理 Sidecar 进程...");
if let Some(state) = app_handle.try_state::<SidecarProcess>() { if let Some(state) = app_handle.try_state::<SidecarProcess>() {
if let Ok(mut process) = state.0.lock() { if let Ok(mut process) = state.0.lock() {

View File

@@ -1,13 +1,8 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "tauri-shell", "productName": "app-desktop",
"version": "0.1.0", "version": "0.1.0",
"identifier": "com.imbytecat.tauri-shell", "identifier": "com.imbytecat.app-desktop",
"build": {
"beforeDevCommand": "bun run dev:vite",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "bun run build"
},
"app": { "app": {
"withGlobalTauri": true, "withGlobalTauri": true,
"windows": [], "windows": [],
@@ -25,6 +20,6 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"externalBin": ["binaries/server"] "externalBin": ["binaries/app"]
} }
} }

View File

@@ -9,15 +9,9 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as TodosRouteImport } from './routes/todos'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$' import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$'
const TodosRoute = TodosRouteImport.update({
id: '/todos',
path: '/todos',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({ const IndexRoute = IndexRouteImport.update({
id: '/', id: '/',
path: '/', path: '/',
@@ -31,43 +25,32 @@ const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
export interface FileRoutesByFullPath { export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/todos': typeof TodosRoute
'/api/rpc/$': typeof ApiRpcSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/todos': typeof TodosRoute
'/api/rpc/$': typeof ApiRpcSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
'/': typeof IndexRoute '/': typeof IndexRoute
'/todos': typeof TodosRoute
'/api/rpc/$': typeof ApiRpcSplatRoute '/api/rpc/$': typeof ApiRpcSplatRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/todos' | '/api/rpc/$' fullPaths: '/' | '/api/rpc/$'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: '/' | '/todos' | '/api/rpc/$' to: '/' | '/api/rpc/$'
id: '__root__' | '/' | '/todos' | '/api/rpc/$' id: '__root__' | '/' | '/api/rpc/$'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
TodosRoute: typeof TodosRoute
ApiRpcSplatRoute: typeof ApiRpcSplatRoute ApiRpcSplatRoute: typeof ApiRpcSplatRoute
} }
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath { interface FileRoutesByPath {
'/todos': {
id: '/todos'
path: '/todos'
fullPath: '/todos'
preLoaderRoute: typeof TodosRouteImport
parentRoute: typeof rootRouteImport
}
'/': { '/': {
id: '/' id: '/'
path: '/' path: '/'
@@ -87,7 +70,6 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
TodosRoute: TodosRoute,
ApiRpcSplatRoute: ApiRpcSplatRoute, ApiRpcSplatRoute: ApiRpcSplatRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@@ -1,7 +1,215 @@
import { useMutation, useSuspenseQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { isTauri } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window'
import type { ChangeEventHandler, FormEventHandler } from 'react'
import { useEffect, useState } from 'react'
import { orpc } from '@/orpc'
export const Route = createFileRoute('/')({ component: App }) export const Route = createFileRoute('/')({
component: Todos,
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(orpc.todo.list.queryOptions())
},
})
function App() { function Todos() {
return <div>Hello, World!</div> const [newTodoTitle, setNewTodoTitle] = useState('')
const listQuery = useSuspenseQuery(orpc.todo.list.queryOptions())
const createMutation = useMutation(orpc.todo.create.mutationOptions())
const updateMutation = useMutation(orpc.todo.update.mutationOptions())
const deleteMutation = useMutation(orpc.todo.remove.mutationOptions())
useEffect(() => {
if (!isTauri()) return
getCurrentWindow().setTitle('待办事项')
}, [])
const handleCreateTodo: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault()
if (newTodoTitle.trim()) {
createMutation.mutate({ title: newTodoTitle.trim() })
setNewTodoTitle('')
}
}
const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
setNewTodoTitle(e.target.value)
}
const handleToggleTodo = (id: string, currentCompleted: boolean) => {
updateMutation.mutate({
id,
data: { completed: !currentCompleted },
})
}
const handleDeleteTodo = (id: string) => {
deleteMutation.mutate({ id })
}
const todos = listQuery.data
const completedCount = todos.filter((todo) => todo.completed).length
const totalCount = todos.length
const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0
return (
<div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6 font-sans">
<div className="max-w-2xl mx-auto space-y-8">
{/* Header */}
<div className="flex items-end justify-between">
<div>
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">
</h1>
<p className="text-slate-500 mt-1"></p>
</div>
<div className="text-right">
<div className="text-2xl font-semibold text-slate-900">
{completedCount}
<span className="text-slate-400 text-lg">/{totalCount}</span>
</div>
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider">
</div>
</div>
</div>
{/* Add Todo Form */}
<form onSubmit={handleCreateTodo} className="relative group z-10">
<div className="relative transform transition-all duration-200 focus-within:-translate-y-1">
<input
type="text"
value={newTodoTitle}
onChange={handleInputChange}
placeholder="添加新任务..."
className="w-full pl-6 pr-32 py-5 bg-white rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] border-0 ring-1 ring-slate-100 focus:ring-2 focus:ring-indigo-500/50 outline-none transition-all placeholder:text-slate-400 text-lg text-slate-700"
disabled={createMutation.isPending}
/>
<button
type="submit"
disabled={createMutation.isPending || !newTodoTitle.trim()}
className="absolute right-3 top-3 bottom-3 px-6 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-medium transition-all shadow-md shadow-indigo-200 disabled:opacity-50 disabled:shadow-none hover:shadow-lg hover:shadow-indigo-300 active:scale-95"
>
{createMutation.isPending ? '添加中' : '添加'}
</button>
</div>
</form>
{/* Progress Bar (Only visible when there are tasks) */}
{totalCount > 0 && (
<div className="h-1.5 w-full bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-indigo-500 transition-all duration-500 ease-out rounded-full"
style={{ width: `${progress}%` }}
/>
</div>
)}
{/* Todo List */}
<div className="space-y-3">
{todos.length === 0 ? (
<div className="py-20 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-100 mb-4">
<svg
className="w-8 h-8 text-slate-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
</div>
<p className="text-slate-500 text-lg font-medium"></p>
<p className="text-slate-400 text-sm mt-1">
</p>
</div>
) : (
todos.map((todo) => (
<div
key={todo.id}
className={`group relative flex items-center p-4 bg-white rounded-xl border border-slate-100 shadow-sm transition-all duration-200 hover:shadow-md hover:border-slate-200 ${
todo.completed ? 'bg-slate-50/50' : ''
}`}
>
<button
type="button"
onClick={() => handleToggleTodo(todo.id, todo.completed)}
className={`flex-shrink-0 w-6 h-6 rounded-full border-2 transition-all duration-200 flex items-center justify-center mr-4 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ${
todo.completed
? 'bg-indigo-500 border-indigo-500'
: 'border-slate-300 hover:border-indigo-500 bg-white'
}`}
>
{todo.completed && (
<svg
className="w-3.5 h-3.5 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
<div className="flex-1 min-w-0">
<p
className={`text-lg transition-all duration-200 truncate ${
todo.completed
? 'text-slate-400 line-through decoration-slate-300 decoration-2'
: 'text-slate-700'
}`}
>
{todo.title}
</p>
</div>
<div className="flex items-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 absolute right-4 pl-4 bg-gradient-to-l from-white via-white to-transparent sm:static sm:bg-none">
<span className="text-xs text-slate-400 mr-3 hidden sm:inline-block">
{new Date(todo.createdAt).toLocaleDateString('zh-CN')}
</span>
<button
type="button"
onClick={() => handleDeleteTodo(todo.id)}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors focus:outline-none"
title="删除"
>
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</div>
))
)}
</div>
</div>
</div>
)
} }

View File

@@ -1,208 +0,0 @@
import { useMutation, useSuspenseQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'
import type { ChangeEventHandler, FormEventHandler } from 'react'
import { useState } from 'react'
import { orpc } from '@/orpc'
export const Route = createFileRoute('/todos')({
component: Todos,
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(orpc.todo.list.queryOptions())
},
})
function Todos() {
const [newTodoTitle, setNewTodoTitle] = useState('')
const listQuery = useSuspenseQuery(orpc.todo.list.queryOptions())
const createMutation = useMutation(orpc.todo.create.mutationOptions())
const updateMutation = useMutation(orpc.todo.update.mutationOptions())
const deleteMutation = useMutation(orpc.todo.remove.mutationOptions())
const handleCreateTodo: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault()
if (newTodoTitle.trim()) {
createMutation.mutate({ title: newTodoTitle.trim() })
setNewTodoTitle('')
}
}
const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
setNewTodoTitle(e.target.value)
}
const handleToggleTodo = (id: string, currentCompleted: boolean) => {
updateMutation.mutate({
id,
data: { completed: !currentCompleted },
})
}
const handleDeleteTodo = (id: string) => {
deleteMutation.mutate({ id })
}
const todos = listQuery.data
const completedCount = todos.filter((todo) => todo.completed).length
const totalCount = todos.length
const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0
return (
<div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6 font-sans">
<div className="max-w-2xl mx-auto space-y-8">
{/* Header */}
<div className="flex items-end justify-between">
<div>
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">
</h1>
<p className="text-slate-500 mt-1"></p>
</div>
<div className="text-right">
<div className="text-2xl font-semibold text-slate-900">
{completedCount}
<span className="text-slate-400 text-lg">/{totalCount}</span>
</div>
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider">
</div>
</div>
</div>
{/* Add Todo Form */}
<form onSubmit={handleCreateTodo} className="relative group z-10">
<div className="relative transform transition-all duration-200 focus-within:-translate-y-1">
<input
type="text"
value={newTodoTitle}
onChange={handleInputChange}
placeholder="添加新任务..."
className="w-full pl-6 pr-32 py-5 bg-white rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] border-0 ring-1 ring-slate-100 focus:ring-2 focus:ring-indigo-500/50 outline-none transition-all placeholder:text-slate-400 text-lg text-slate-700"
disabled={createMutation.isPending}
/>
<button
type="submit"
disabled={createMutation.isPending || !newTodoTitle.trim()}
className="absolute right-3 top-3 bottom-3 px-6 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-medium transition-all shadow-md shadow-indigo-200 disabled:opacity-50 disabled:shadow-none hover:shadow-lg hover:shadow-indigo-300 active:scale-95"
>
{createMutation.isPending ? '添加中' : '添加'}
</button>
</div>
</form>
{/* Progress Bar (Only visible when there are tasks) */}
{totalCount > 0 && (
<div className="h-1.5 w-full bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-indigo-500 transition-all duration-500 ease-out rounded-full"
style={{ width: `${progress}%` }}
/>
</div>
)}
{/* Todo List */}
<div className="space-y-3">
{todos.length === 0 ? (
<div className="py-20 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-100 mb-4">
<svg
className="w-8 h-8 text-slate-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
</div>
<p className="text-slate-500 text-lg font-medium"></p>
<p className="text-slate-400 text-sm mt-1">
</p>
</div>
) : (
todos.map((todo) => (
<div
key={todo.id}
className={`group relative flex items-center p-4 bg-white rounded-xl border border-slate-100 shadow-sm transition-all duration-200 hover:shadow-md hover:border-slate-200 ${
todo.completed ? 'bg-slate-50/50' : ''
}`}
>
<button
type="button"
onClick={() => handleToggleTodo(todo.id, todo.completed)}
className={`flex-shrink-0 w-6 h-6 rounded-full border-2 transition-all duration-200 flex items-center justify-center mr-4 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ${
todo.completed
? 'bg-indigo-500 border-indigo-500'
: 'border-slate-300 hover:border-indigo-500 bg-white'
}`}
>
{todo.completed && (
<svg
className="w-3.5 h-3.5 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
)}
</button>
<div className="flex-1 min-w-0">
<p
className={`text-lg transition-all duration-200 truncate ${
todo.completed
? 'text-slate-400 line-through decoration-slate-300 decoration-2'
: 'text-slate-700'
}`}
>
{todo.title}
</p>
</div>
<div className="flex items-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 absolute right-4 pl-4 bg-gradient-to-l from-white via-white to-transparent sm:static sm:bg-none">
<span className="text-xs text-slate-400 mr-3 hidden sm:inline-block">
{new Date(todo.createdAt).toLocaleDateString('zh-CN')}
</span>
<button
type="button"
onClick={() => handleDeleteTodo(todo.id)}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors focus:outline-none"
title="删除"
>
<svg
className="w-5 h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</div>
))
)}
</div>
</div>
</div>
)
}

32
turbo.json Normal file
View File

@@ -0,0 +1,32 @@
{
"$schema": "./node_modules/turbo/schema.json",
"dangerouslyDisablePackageManagerCheck": true,
"tasks": {
"build:compile": {
"dependsOn": ["build:vite"],
"outputs": ["out/**", "src-tauri/binaries/**"]
},
"build:tauri": {
"dependsOn": ["build:compile"],
"outputs": ["src-tauri/target/release/bundle/**"]
},
"build:vite": {
"outputs": [".output/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"dev:tauri": {
"cache": false,
"dependsOn": ["build:compile"],
"persistent": true,
"with": ["dev:vite"]
},
"dev:vite": {
"cache": false,
"persistent": true
}
},
"ui": "tui"
}