Compare commits
3 Commits
e41c4e4515
...
backup/leg
| Author | SHA1 | Date | |
|---|---|---|---|
| f7f86e4462 | |||
| 5cc476cedc | |||
| 1ccda13cdf |
3
.vscode/settings.json
vendored
@@ -37,8 +37,7 @@
|
|||||||
"files.associations": {
|
"files.associations": {
|
||||||
".env": "dotenv",
|
".env": "dotenv",
|
||||||
".env.*": "dotenv",
|
".env.*": "dotenv",
|
||||||
"**/tsconfig.json": "jsonc",
|
"**/tsconfig*.json": "jsonc",
|
||||||
"**/tsconfig.*.json": "jsonc",
|
|
||||||
"**/biome.json": "jsonc",
|
"**/biome.json": "jsonc",
|
||||||
"**/opencode.json": "jsonc"
|
"**/opencode.json": "jsonc"
|
||||||
},
|
},
|
||||||
|
|||||||
277
AGENTS.md
@@ -1,200 +1,193 @@
|
|||||||
# AGENTS.md - AI Coding Agent Guidelines
|
# AGENTS.md - AI Coding Agent Guidelines
|
||||||
|
|
||||||
Guidelines for AI agents working in this Bun monorepo.
|
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
- **Monorepo**: Bun workspaces + Turborepo orchestration
|
TanStack Start fullstack app with Tauri desktop shell.
|
||||||
- **Runtime**: Bun (see `mise.toml` for version)
|
|
||||||
- **Apps**:
|
|
||||||
- `apps/server` - TanStack Start fullstack web app (see `apps/server/AGENTS.md`)
|
|
||||||
- `apps/desktop` - Tauri v2 desktop shell, loads server via sidecar (see `apps/desktop/AGENTS.md`)
|
|
||||||
- **Packages**: `packages/utils`, `packages/tsconfig` (shared configs)
|
|
||||||
|
|
||||||
## Build / Lint / Test Commands
|
| Layer | Tech |
|
||||||
|
|-------|------|
|
||||||
|
| Framework | TanStack Start (React SSR, file-based routing) |
|
||||||
|
| Runtime | Bun |
|
||||||
|
| Language | TypeScript (strict mode) |
|
||||||
|
| Styling | Tailwind CSS v4 |
|
||||||
|
| Database | PostgreSQL + Drizzle ORM |
|
||||||
|
| RPC | ORPC (contract-first, type-safe) |
|
||||||
|
| Build | Vite + Turbo |
|
||||||
|
| Linting | Biome |
|
||||||
|
| Desktop | Tauri v2 (optional, see `src-tauri/AGENTS.md`) |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
### Root Commands (via Turbo)
|
|
||||||
```bash
|
```bash
|
||||||
bun dev # Start all apps in dev mode
|
# Development
|
||||||
bun build # Build all apps
|
bun dev # Start Tauri + Vite via Turbo
|
||||||
bun fix # Lint + format (Biome auto-fix)
|
bun dev:vite # Vite only (localhost:3000)
|
||||||
bun typecheck # TypeScript check across monorepo
|
bun db:studio # Drizzle Studio
|
||||||
|
|
||||||
|
# Build
|
||||||
|
bun build # Full build (Vite → compile → Tauri)
|
||||||
|
bun build:vite # Vite only (outputs to .output/)
|
||||||
|
|
||||||
|
# Code Quality
|
||||||
|
bun typecheck # TypeScript check (tsc -b)
|
||||||
|
bun fix # Biome auto-fix (format + lint)
|
||||||
|
|
||||||
|
# Database
|
||||||
|
bun db:generate # Generate migrations from schema
|
||||||
|
bun db:migrate # Run migrations
|
||||||
|
bun db:push # Push schema changes (dev only)
|
||||||
|
|
||||||
|
# Testing (not configured yet)
|
||||||
|
# When adding tests, use Vitest or Bun test runner:
|
||||||
|
# bun test path/to/test.ts # Single file
|
||||||
|
# bun test -t "pattern" # By test name
|
||||||
```
|
```
|
||||||
|
|
||||||
### Server App (`apps/server`)
|
## Code Style
|
||||||
```bash
|
|
||||||
bun dev # Vite dev server (localhost:3000)
|
|
||||||
bun build # Production build → .output/
|
|
||||||
bun fix # Biome auto-fix
|
|
||||||
bun typecheck # TypeScript check
|
|
||||||
|
|
||||||
# Database (Drizzle)
|
### Formatting (Biome enforced)
|
||||||
bun db:generate # Generate migrations from schema
|
|
||||||
bun db:migrate # Run migrations
|
|
||||||
bun db:push # Push schema (dev only)
|
|
||||||
bun db:studio # Open Drizzle Studio
|
|
||||||
```
|
|
||||||
|
|
||||||
### Desktop App (`apps/desktop`)
|
|
||||||
```bash
|
|
||||||
bun dev # Copy sidecar + start Tauri dev
|
|
||||||
bun build # Copy sidecar + build installer
|
|
||||||
|
|
||||||
# Rust (from apps/desktop/src-tauri/)
|
|
||||||
cargo check # Compile check
|
|
||||||
cargo clippy # Linter
|
|
||||||
cargo fmt # Formatter
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
No test framework configured yet. When adding tests:
|
|
||||||
```bash
|
|
||||||
bun test path/to/test.ts # Run single test file
|
|
||||||
bun test -t "pattern" # Run tests matching pattern
|
|
||||||
cargo test test_name -- --nocapture # Rust single test with output
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Style (TypeScript)
|
|
||||||
|
|
||||||
### Formatting (Biome)
|
|
||||||
- **Indent**: 2 spaces
|
- **Indent**: 2 spaces
|
||||||
- **Line endings**: LF
|
- **Line endings**: LF
|
||||||
- **Quotes**: Single `'`
|
- **Quotes**: Single `'string'`
|
||||||
- **Semicolons**: Omit (ASI)
|
- **Semicolons**: As needed (ASI)
|
||||||
- **Arrow parentheses**: Always `(x) => x`
|
- **Arrow parens**: Always `(x) => x`
|
||||||
|
|
||||||
### Imports
|
### Imports
|
||||||
Biome auto-organizes. Order:
|
|
||||||
1. External packages
|
Biome auto-organizes. Order: external → internal (`@/*`) → type-only imports.
|
||||||
2. Internal `@/*` aliases
|
|
||||||
3. Type imports (`import type { ... }`)
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { db } from '@/server/db'
|
import { db } from '@/db'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
```
|
```
|
||||||
|
|
||||||
### TypeScript Strictness
|
### TypeScript
|
||||||
- `strict: true`
|
|
||||||
- `noUncheckedIndexedAccess: true` - array/object access returns `T | undefined`
|
Strict mode with extra checks:
|
||||||
|
- `noUncheckedIndexedAccess: true` - array/object index returns `T | undefined`
|
||||||
- `noImplicitOverride: true`
|
- `noImplicitOverride: true`
|
||||||
- `verbatimModuleSyntax: true`
|
- `verbatimModuleSyntax: true`
|
||||||
- Use `@/*` path aliases (maps to `src/*`)
|
|
||||||
|
|
||||||
### Naming Conventions
|
Path alias: `@/*` → `src/*`
|
||||||
| Type | Convention | Example |
|
|
||||||
|------|------------|---------|
|
### Naming
|
||||||
| Files (utils) | kebab-case | `auth-utils.ts` |
|
|
||||||
| Files (components) | PascalCase | `UserProfile.tsx` |
|
| Entity | Convention | Example |
|
||||||
| Components | PascalCase arrow | `const Button = () => {}` |
|
|--------|------------|---------|
|
||||||
| Functions | camelCase | `getUserById` |
|
| Files (utils) | kebab-case | `utils.ts`, `db-provider.ts` |
|
||||||
| Constants | UPPER_SNAKE | `MAX_RETRIES` |
|
| Files (components) | PascalCase | `NotFound.tsx` |
|
||||||
| Types/Interfaces | PascalCase | `UserProfile` |
|
| Routes | TanStack conventions | `routes/index.tsx`, `routes/__root.tsx` |
|
||||||
|
| Components | PascalCase arrow functions | `const MyComponent = () => {}` |
|
||||||
|
| Functions | camelCase | `handleSubmit` |
|
||||||
|
| Constants | UPPER_SNAKE_CASE | `MAX_RETRIES` |
|
||||||
|
| Types/Interfaces | PascalCase | `TodoItem`, `RouterContext` |
|
||||||
|
|
||||||
### React Patterns
|
### React Patterns
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Components: arrow functions (enforced by Biome)
|
// Components: arrow functions (Biome enforces)
|
||||||
const MyComponent = ({ title }: { title: string }) => {
|
const MyComponent = ({ title }: { title: string }) => {
|
||||||
return <div>{title}</div>
|
return <div>{title}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Routes: TanStack Router file conventions
|
// Routes: createFileRoute
|
||||||
export const Route = createFileRoute('/')({
|
export const Route = createFileRoute('/')({
|
||||||
component: Home,
|
component: Home,
|
||||||
|
loader: async ({ context }) => {
|
||||||
|
await context.queryClient.ensureQueryData(orpc.todo.list.queryOptions())
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Data fetching: TanStack Query
|
// Data fetching: TanStack Query
|
||||||
const { data } = useSuspenseQuery(orpc.todo.list.queryOptions())
|
const query = useSuspenseQuery(orpc.todo.list.queryOptions())
|
||||||
|
const mutation = useMutation(orpc.todo.create.mutationOptions())
|
||||||
```
|
```
|
||||||
|
|
||||||
### Error Handling
|
### ORPC Pattern (Contract-First RPC)
|
||||||
- Use `try-catch` for async operations
|
|
||||||
- Throw descriptive errors
|
|
||||||
- ORPC: Use `ORPCError` with proper codes (`NOT_FOUND`, `INPUT_VALIDATION_FAILED`)
|
|
||||||
- Never use empty catch blocks
|
|
||||||
|
|
||||||
## Code Style (Rust - Tauri)
|
1. **Define contract** (`src/orpc/contracts/my-feature.ts`):
|
||||||
|
```typescript
|
||||||
|
import { oc } from '@orpc/contract'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
- **Indent**: 4 spaces
|
export const get = oc.input(z.object({ id: z.uuid() })).output(schema)
|
||||||
- **Naming**: snake_case (functions), PascalCase (types), SCREAMING_SNAKE (consts)
|
export const create = oc.input(insertSchema).output(schema)
|
||||||
- Use `expect("中文消息")` over `unwrap()`
|
```
|
||||||
- Async: `tokio` runtime, `tauri::async_runtime::spawn`
|
|
||||||
- Run `cargo fmt` and `cargo clippy` before commit
|
|
||||||
|
|
||||||
## Database (Drizzle ORM)
|
2. **Implement handler** (`src/orpc/handlers/my-feature.ts`):
|
||||||
|
```typescript
|
||||||
|
import { os } from '@/orpc/server'
|
||||||
|
import { dbProvider } from '@/orpc/middlewares'
|
||||||
|
|
||||||
|
export const get = os.myFeature.get
|
||||||
|
.use(dbProvider)
|
||||||
|
.handler(async ({ context, input }) => {
|
||||||
|
return await context.db.query.myTable.findFirst(...)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Register** in `contract.ts` and `router.ts`
|
||||||
|
4. **Use** in components via `orpc.myFeature.get.queryOptions()`
|
||||||
|
|
||||||
|
### Drizzle Schema
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
|
|
||||||
import { sql } from 'drizzle-orm'
|
import { sql } from 'drizzle-orm'
|
||||||
|
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
export const myTable = pgTable('my_table', {
|
export const myTable = pgTable('my_table', {
|
||||||
id: uuid().primaryKey().default(sql`uuidv7()`),
|
id: uuid('id').primaryKey().default(sql`uuidv7()`),
|
||||||
name: text().notNull(),
|
name: text('name').notNull(),
|
||||||
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
### Environment Variables
|
||||||
|
|
||||||
- Use `@t3-oss/env-core` with Zod validation in `src/env.ts`
|
- Server: no prefix (e.g., `DATABASE_URL`)
|
||||||
- Server vars: no prefix
|
- Client: `VITE_` prefix required
|
||||||
- Client vars: `VITE_` prefix required
|
- Validated via `@t3-oss/env-core` in `src/env.ts`
|
||||||
- Never commit `.env` files
|
|
||||||
|
|
||||||
## Critical Rules
|
|
||||||
|
|
||||||
**DO:**
|
|
||||||
- Run `bun fix` before committing
|
|
||||||
- Use `@/*` path aliases (not relative imports)
|
|
||||||
- Let React Compiler handle memoization (no manual `useMemo`/`useCallback`)
|
|
||||||
- Include `createdAt`/`updatedAt` on all tables
|
|
||||||
|
|
||||||
**DON'T:**
|
|
||||||
- Edit `src/routeTree.gen.ts` (auto-generated)
|
|
||||||
- Use `as any`, `@ts-ignore`, `@ts-expect-error`
|
|
||||||
- Commit `.env` files
|
|
||||||
- Use empty catch blocks `catch(e) {}`
|
|
||||||
- Use `unwrap()` in Rust without `expect()`
|
|
||||||
|
|
||||||
## Git Workflow
|
|
||||||
|
|
||||||
1. Make changes following style guide
|
|
||||||
2. `bun fix` - auto-format and lint
|
|
||||||
3. `bun typecheck` - verify types
|
|
||||||
4. `bun dev` - test locally
|
|
||||||
5. Commit with descriptive message
|
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
src/
|
||||||
├── apps/
|
├── components/ # Reusable React components
|
||||||
│ ├── server/ # TanStack Start fullstack app
|
├── db/
|
||||||
│ │ ├── src/
|
│ ├── schema/ # Drizzle schema definitions
|
||||||
│ │ │ ├── client/ # ORPC client, Query client
|
│ └── index.ts # Database instance
|
||||||
│ │ │ ├── components/
|
├── integrations/ # TanStack Query/Router setup
|
||||||
│ │ │ ├── routes/ # File-based routing
|
├── lib/ # Utility functions
|
||||||
│ │ │ └── server/ # API layer + database
|
├── orpc/
|
||||||
│ │ │ ├── api/ # ORPC contracts, routers, middlewares
|
│ ├── contracts/ # Input/output schemas
|
||||||
│ │ │ └── db/ # Drizzle schema
|
│ ├── handlers/ # Server procedure implementations
|
||||||
│ │ └── AGENTS.md
|
│ ├── middlewares/ # Middleware (e.g., dbProvider)
|
||||||
│ └── desktop/ # Tauri v2 shell (no frontend src)
|
│ ├── contract.ts # Contract aggregation
|
||||||
│ ├── src-tauri/ # Rust Tauri code
|
│ ├── router.ts # Router composition
|
||||||
│ │ ├── src/ # Rust source
|
│ └── client.ts # Isomorphic client
|
||||||
│ │ └── binaries/ # Sidecar binaries
|
├── routes/ # File-based routes
|
||||||
│ └── AGENTS.md
|
│ ├── __root.tsx # Root layout
|
||||||
├── packages/
|
│ └── api/rpc.$.ts # ORPC HTTP endpoint
|
||||||
│ ├── tsconfig/ # Shared TS configs
|
└── env.ts # Environment validation
|
||||||
│ └── utils/ # Shared utilities
|
|
||||||
├── biome.json # Linting/formatting config
|
|
||||||
├── turbo.json # Turbo task orchestration
|
|
||||||
└── package.json # Workspace root
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## See Also
|
## Critical Rules
|
||||||
|
|
||||||
- `apps/server/AGENTS.md` - Detailed TanStack Start / ORPC patterns
|
- **DO NOT** edit `src/routeTree.gen.ts` (auto-generated)
|
||||||
- `apps/desktop/AGENTS.md` - Rust / Tauri development guide
|
- **DO NOT** commit `.env` files
|
||||||
|
- **MUST** run `bun fix` before commits
|
||||||
|
- **MUST** use `@/*` path alias (not relative imports)
|
||||||
|
- **MUST** use React Compiler (no manual memoization needed)
|
||||||
|
- **MUST** use `Readonly<T>` for immutable props
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
1. Make changes following style guide
|
||||||
|
2. Run `bun fix` (format + lint)
|
||||||
|
3. Run `bun typecheck` (type safety)
|
||||||
|
4. Test with `bun dev`
|
||||||
|
5. Commit with descriptive message
|
||||||
|
|||||||
24
apps/desktop/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
# AGENTS.md - Desktop App Guidelines
|
|
||||||
|
|
||||||
Tauri v2 desktop shell - a lightweight wrapper that loads the server app via sidecar.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
- **Type**: Tauri v2 desktop application (shell only)
|
|
||||||
- **Design**: Tauri provides native desktop APIs; all web logic handled by sidecar
|
|
||||||
- **Sidecar**: The compiled server binary runs as a child process
|
|
||||||
- **Dev mode**: Connects to `localhost:3000` (requires server dev running)
|
|
||||||
- **Prod mode**: Automatically starts sidecar binary
|
|
||||||
|
|
||||||
**This app has NO frontend src** - it loads the server app entirely.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development (from apps/desktop/)
|
|
||||||
bun dev # Copy sidecar + start Tauri dev
|
|
||||||
|
|
||||||
# Build
|
|
||||||
bun build # Copy sidecar + build Tauri installer
|
|
||||||
|
|
||||||
# Rust Commands (from src-tauri/)
|
|
||||||
cargo check # Compile check
|
|
||||||
cargo clippy # Linter
|
|
||||||
cargo fmt # Formatter
|
|
||||||
cargo test # Run tests
|
|
||||||
cargo test test_name -- --nocapture # Single test with output
|
|
||||||
```
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
apps/desktop/
|
|
||||||
├── src-tauri/ # Rust Tauri code
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── main.rs # Entry point (calls lib::run)
|
|
||||||
│ │ ├── lib.rs # Core app logic (plugins, commands, state)
|
|
||||||
│ │ ├── commands/
|
|
||||||
│ │ │ └── mod.rs # Native desktop commands
|
|
||||||
│ │ └── sidecar.rs # Sidecar process management
|
|
||||||
│ ├── binaries/ # Sidecar binaries (copied from server build)
|
|
||||||
│ ├── capabilities/ # Tauri v2 permission config
|
|
||||||
│ ├── icons/ # App icons
|
|
||||||
│ ├── Cargo.toml # Rust dependencies
|
|
||||||
│ └── tauri.conf.json # Tauri configuration
|
|
||||||
├── copy.ts # Script to copy server binary to binaries/
|
|
||||||
├── package.json
|
|
||||||
└── tsconfig.json
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
1. **Start server dev first**: `cd ../server && bun dev`
|
|
||||||
2. **Start Tauri**: `bun dev` (from apps/desktop/)
|
|
||||||
3. Tauri connects to localhost:3000 with HMR support
|
|
||||||
|
|
||||||
## Rust Code Style
|
|
||||||
|
|
||||||
### Formatting
|
|
||||||
- **Indent**: 4 spaces
|
|
||||||
- **Line width**: 100 chars
|
|
||||||
- Run `cargo fmt` before commit
|
|
||||||
|
|
||||||
### Naming
|
|
||||||
| Type | Convention | Example |
|
|
||||||
|------|------------|---------|
|
|
||||||
| Functions/variables | snake_case | `find_available_port` |
|
|
||||||
| Types/structs/enums | PascalCase | `SidecarProcess` |
|
|
||||||
| Constants | SCREAMING_SNAKE | `DEFAULT_PORT` |
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
```rust
|
|
||||||
// Order: std → external crates → internal modules (separated by blank lines)
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
use tauri::Manager;
|
|
||||||
use tauri_plugin_shell::ShellExt;
|
|
||||||
|
|
||||||
use crate::sidecar::SidecarProcess;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
```rust
|
|
||||||
// Use expect() with Chinese error messages
|
|
||||||
let sidecar = app_handle
|
|
||||||
.shell()
|
|
||||||
.sidecar("server")
|
|
||||||
.expect("无法找到 server sidecar");
|
|
||||||
|
|
||||||
// Log with emoji for clear feedback
|
|
||||||
println!("✓ Sidecar 启动成功!");
|
|
||||||
eprintln!("✗ Sidecar 启动失败");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Async Code
|
|
||||||
```rust
|
|
||||||
// Use Tauri's async runtime for spawning
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
let port = find_available_port(3000).await;
|
|
||||||
// ...
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tauri Patterns
|
|
||||||
|
|
||||||
### Command Definition
|
|
||||||
```rust
|
|
||||||
#[tauri::command]
|
|
||||||
fn greet(name: &str) -> String {
|
|
||||||
format!("Hello, {}!", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register in Builder
|
|
||||||
.invoke_handler(tauri::generate_handler![commands::greet])
|
|
||||||
```
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
```rust
|
|
||||||
struct SidecarProcess(Mutex<Option<CommandChild>>);
|
|
||||||
|
|
||||||
// Register state
|
|
||||||
app.manage(SidecarProcess(Mutex::new(None)));
|
|
||||||
|
|
||||||
// Access state
|
|
||||||
if let Some(state) = app_handle.try_state::<SidecarProcess>() {
|
|
||||||
*state.0.lock().unwrap() = Some(child);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sidecar Lifecycle
|
|
||||||
```rust
|
|
||||||
// Start sidecar with environment
|
|
||||||
let sidecar = app_handle
|
|
||||||
.shell()
|
|
||||||
.sidecar("server")
|
|
||||||
.expect("无法找到 server sidecar")
|
|
||||||
.env("PORT", port.to_string());
|
|
||||||
|
|
||||||
// Cleanup on exit
|
|
||||||
match event {
|
|
||||||
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => {
|
|
||||||
if let Some(child) = process.take() {
|
|
||||||
let _ = child.kill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Critical Rules
|
|
||||||
|
|
||||||
**DO:**
|
|
||||||
- Run `cargo fmt` and `cargo clippy` before commit
|
|
||||||
- Use `expect("中文消息")` instead of `unwrap()`
|
|
||||||
- Always cleanup sidecar on app exit
|
|
||||||
- Declare sidecar in `tauri.conf.json` → `bundle.externalBin`
|
|
||||||
|
|
||||||
**DON'T:**
|
|
||||||
- Edit `gen/schemas/` (auto-generated)
|
|
||||||
- Use `unwrap()` in production code without context
|
|
||||||
- Block the async runtime (use `spawn_blocking`)
|
|
||||||
|
|
||||||
## Pre-commit Checklist
|
|
||||||
|
|
||||||
- [ ] `cargo fmt` - formatting
|
|
||||||
- [ ] `cargo clippy` - linting
|
|
||||||
- [ ] `cargo check` - compiles
|
|
||||||
- [ ] `cargo test` - tests pass
|
|
||||||
- [ ] Tauri app starts and exits cleanly
|
|
||||||
@@ -1,470 +0,0 @@
|
|||||||
import * as path from 'node:path'
|
|
||||||
import { Schema } from '@effect/schema'
|
|
||||||
import { $ } from 'bun'
|
|
||||||
import { Console, Context, Data, Effect, Layer } from 'effect'
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Domain Models & Schema
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bun 构建目标后缀
|
|
||||||
*/
|
|
||||||
const BunTargetSuffixSchema = Schema.Literal(
|
|
||||||
'windows-x64',
|
|
||||||
'darwin-arm64',
|
|
||||||
'darwin-x64',
|
|
||||||
'linux-x64',
|
|
||||||
'linux-arm64',
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tauri sidecar 目标三元组
|
|
||||||
*/
|
|
||||||
const TauriTargetSchema = Schema.Literal(
|
|
||||||
'x86_64-pc-windows-msvc',
|
|
||||||
'aarch64-apple-darwin',
|
|
||||||
'x86_64-apple-darwin',
|
|
||||||
'x86_64-unknown-linux-gnu',
|
|
||||||
'aarch64-unknown-linux-gnu',
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 目标映射配置
|
|
||||||
*/
|
|
||||||
const TargetMappingSchema = Schema.Struct({
|
|
||||||
bunSuffix: BunTargetSuffixSchema,
|
|
||||||
tauriTarget: TauriTargetSchema,
|
|
||||||
})
|
|
||||||
|
|
||||||
type TargetMapping = Schema.Schema.Type<typeof TargetMappingSchema>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 复制配置
|
|
||||||
*/
|
|
||||||
const CopyConfigSchema = Schema.Struct({
|
|
||||||
sourceDir: Schema.String.pipe(Schema.nonEmptyString()),
|
|
||||||
targetDir: Schema.String.pipe(Schema.nonEmptyString()),
|
|
||||||
baseName: Schema.String.pipe(Schema.nonEmptyString()),
|
|
||||||
mappings: Schema.Array(TargetMappingSchema).pipe(Schema.minItems(1)),
|
|
||||||
})
|
|
||||||
|
|
||||||
type CopyConfig = Schema.Schema.Type<typeof CopyConfigSchema>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 复制结果
|
|
||||||
*/
|
|
||||||
const CopyResultSchema = Schema.Struct({
|
|
||||||
bunSuffix: BunTargetSuffixSchema,
|
|
||||||
tauriTarget: TauriTargetSchema,
|
|
||||||
sourceFile: Schema.String,
|
|
||||||
targetFile: Schema.String,
|
|
||||||
success: Schema.Boolean,
|
|
||||||
})
|
|
||||||
|
|
||||||
type CopyResult = Schema.Schema.Type<typeof CopyResultSchema>
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Error Models
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
class ConfigError extends Data.TaggedError('ConfigError')<{
|
|
||||||
readonly message: string
|
|
||||||
readonly cause: unknown
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
class FileSystemError extends Data.TaggedError('FileSystemError')<{
|
|
||||||
readonly operation: string
|
|
||||||
readonly path: string
|
|
||||||
readonly cause: unknown
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
class CopyError extends Data.TaggedError('CopyError')<{
|
|
||||||
readonly source: string
|
|
||||||
readonly target: string
|
|
||||||
readonly cause: unknown
|
|
||||||
}> {}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Services
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 配置服务
|
|
||||||
*/
|
|
||||||
class CopyConfigService extends Context.Tag('CopyConfigService')<
|
|
||||||
CopyConfigService,
|
|
||||||
CopyConfig
|
|
||||||
>() {
|
|
||||||
/**
|
|
||||||
* 从原始数据创建并验证配置
|
|
||||||
*/
|
|
||||||
static fromRaw = (raw: unknown) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const decoded = yield* Schema.decodeUnknown(CopyConfigSchema)(raw)
|
|
||||||
return decoded
|
|
||||||
}).pipe(
|
|
||||||
Effect.catchAll((error) =>
|
|
||||||
Effect.fail(
|
|
||||||
new ConfigError({
|
|
||||||
message: '配置验证失败',
|
|
||||||
cause: error,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认配置 Layer
|
|
||||||
*/
|
|
||||||
static readonly Live = Layer.effect(
|
|
||||||
CopyConfigService,
|
|
||||||
CopyConfigService.fromRaw({
|
|
||||||
sourceDir: path.join(__dirname, '..', 'server', 'out'),
|
|
||||||
targetDir: path.join(__dirname, 'src-tauri', 'binaries'),
|
|
||||||
baseName: 'server',
|
|
||||||
mappings: [
|
|
||||||
{
|
|
||||||
bunSuffix: 'windows-x64',
|
|
||||||
tauriTarget: 'x86_64-pc-windows-msvc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bunSuffix: 'darwin-arm64',
|
|
||||||
tauriTarget: 'aarch64-apple-darwin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bunSuffix: 'darwin-x64',
|
|
||||||
tauriTarget: 'x86_64-apple-darwin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bunSuffix: 'linux-x64',
|
|
||||||
tauriTarget: 'x86_64-unknown-linux-gnu',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bunSuffix: 'linux-arm64',
|
|
||||||
tauriTarget: 'aarch64-unknown-linux-gnu',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} satisfies CopyConfig),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件系统服务
|
|
||||||
*/
|
|
||||||
class FileSystemService extends Context.Tag('FileSystemService')<
|
|
||||||
FileSystemService,
|
|
||||||
{
|
|
||||||
readonly ensureDir: (dir: string) => Effect.Effect<void, FileSystemError>
|
|
||||||
readonly fileExists: (
|
|
||||||
filePath: string,
|
|
||||||
) => Effect.Effect<boolean, FileSystemError>
|
|
||||||
readonly dirExists: (
|
|
||||||
dirPath: string,
|
|
||||||
) => Effect.Effect<boolean, FileSystemError>
|
|
||||||
readonly copyFile: (
|
|
||||||
source: string,
|
|
||||||
target: string,
|
|
||||||
) => Effect.Effect<void, CopyError>
|
|
||||||
}
|
|
||||||
>() {
|
|
||||||
static readonly Live = Layer.succeed(FileSystemService, {
|
|
||||||
ensureDir: (dir: string) =>
|
|
||||||
Effect.tryPromise({
|
|
||||||
try: async () => {
|
|
||||||
await $`mkdir -p ${dir}`
|
|
||||||
},
|
|
||||||
catch: (cause: unknown) =>
|
|
||||||
new FileSystemError({
|
|
||||||
operation: 'ensureDir',
|
|
||||||
path: dir,
|
|
||||||
cause,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
fileExists: (filePath: string) =>
|
|
||||||
Effect.tryPromise({
|
|
||||||
try: async () => {
|
|
||||||
const file = Bun.file(filePath)
|
|
||||||
return await file.exists()
|
|
||||||
},
|
|
||||||
catch: (cause: unknown) =>
|
|
||||||
new FileSystemError({
|
|
||||||
operation: 'fileExists',
|
|
||||||
path: filePath,
|
|
||||||
cause,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
dirExists: (dirPath: string) =>
|
|
||||||
Effect.tryPromise({
|
|
||||||
try: async () => {
|
|
||||||
const { default: fs } = await import('node:fs/promises')
|
|
||||||
try {
|
|
||||||
const stat = await fs.stat(dirPath)
|
|
||||||
return stat.isDirectory()
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
catch: (cause: unknown) =>
|
|
||||||
new FileSystemError({
|
|
||||||
operation: 'dirExists',
|
|
||||||
path: dirPath,
|
|
||||||
cause,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
copyFile: (source: string, target: string) =>
|
|
||||||
Effect.tryPromise({
|
|
||||||
try: async () => {
|
|
||||||
await $`cp ${source} ${target}`
|
|
||||||
},
|
|
||||||
catch: (cause: unknown) =>
|
|
||||||
new CopyError({
|
|
||||||
source,
|
|
||||||
target,
|
|
||||||
cause,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 复制服务
|
|
||||||
*/
|
|
||||||
class CopyService extends Context.Tag('CopyService')<
|
|
||||||
CopyService,
|
|
||||||
{
|
|
||||||
readonly copyBinary: (
|
|
||||||
config: CopyConfig,
|
|
||||||
mapping: TargetMapping,
|
|
||||||
) => Effect.Effect<CopyResult, CopyError | FileSystemError>
|
|
||||||
readonly copyAllBinaries: (
|
|
||||||
config: CopyConfig,
|
|
||||||
) => Effect.Effect<ReadonlyArray<CopyResult>, CopyError | FileSystemError>
|
|
||||||
}
|
|
||||||
>() {
|
|
||||||
static readonly Live = Layer.effect(
|
|
||||||
CopyService,
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const fs = yield* FileSystemService
|
|
||||||
|
|
||||||
return {
|
|
||||||
copyBinary: (config: CopyConfig, mapping: TargetMapping) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const { sourceDir, targetDir, baseName } = config
|
|
||||||
const { bunSuffix, tauriTarget } = mapping
|
|
||||||
|
|
||||||
// 确定文件扩展名(Windows 需要 .exe)
|
|
||||||
const ext = tauriTarget.includes('windows') ? '.exe' : ''
|
|
||||||
|
|
||||||
// 构建源文件和目标文件路径
|
|
||||||
const sourceFile = path.join(
|
|
||||||
sourceDir,
|
|
||||||
`${baseName}-${bunSuffix}${ext}`,
|
|
||||||
)
|
|
||||||
const targetFile = path.join(
|
|
||||||
targetDir,
|
|
||||||
`${baseName}-${tauriTarget}${ext}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 检查源文件是否存在
|
|
||||||
const exists = yield* fs.fileExists(sourceFile)
|
|
||||||
if (!exists) {
|
|
||||||
yield* Console.log(`⚠️ 跳过 ${bunSuffix}: 源文件不存在`)
|
|
||||||
return {
|
|
||||||
bunSuffix,
|
|
||||||
tauriTarget,
|
|
||||||
sourceFile,
|
|
||||||
targetFile,
|
|
||||||
success: false,
|
|
||||||
} satisfies CopyResult
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制文件
|
|
||||||
yield* fs.copyFile(sourceFile, targetFile)
|
|
||||||
|
|
||||||
yield* Console.log(`✓ ${bunSuffix} → ${tauriTarget}`)
|
|
||||||
yield* Console.log(` ${sourceFile}`)
|
|
||||||
yield* Console.log(` → ${targetFile}\n`)
|
|
||||||
|
|
||||||
return {
|
|
||||||
bunSuffix,
|
|
||||||
tauriTarget,
|
|
||||||
sourceFile,
|
|
||||||
targetFile,
|
|
||||||
success: true,
|
|
||||||
} satisfies CopyResult
|
|
||||||
}),
|
|
||||||
|
|
||||||
copyAllBinaries: (config: CopyConfig) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const effects = config.mappings.map((mapping) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const { sourceDir, targetDir, baseName } = config
|
|
||||||
const { bunSuffix, tauriTarget } = mapping
|
|
||||||
|
|
||||||
const ext = tauriTarget.includes('windows') ? '.exe' : ''
|
|
||||||
const sourceFile = path.join(
|
|
||||||
sourceDir,
|
|
||||||
`${baseName}-${bunSuffix}${ext}`,
|
|
||||||
)
|
|
||||||
const targetFile = path.join(
|
|
||||||
targetDir,
|
|
||||||
`${baseName}-${tauriTarget}${ext}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
const exists = yield* fs.fileExists(sourceFile)
|
|
||||||
if (!exists) {
|
|
||||||
yield* Console.log(`⚠️ 跳过 ${bunSuffix}: 源文件不存在`)
|
|
||||||
return {
|
|
||||||
bunSuffix,
|
|
||||||
tauriTarget,
|
|
||||||
sourceFile,
|
|
||||||
targetFile,
|
|
||||||
success: false,
|
|
||||||
} satisfies CopyResult
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* fs.copyFile(sourceFile, targetFile)
|
|
||||||
|
|
||||||
yield* Console.log(`✓ ${bunSuffix} → ${tauriTarget}`)
|
|
||||||
yield* Console.log(` ${sourceFile}`)
|
|
||||||
yield* Console.log(` → ${targetFile}\n`)
|
|
||||||
|
|
||||||
return {
|
|
||||||
bunSuffix,
|
|
||||||
tauriTarget,
|
|
||||||
sourceFile,
|
|
||||||
targetFile,
|
|
||||||
success: true,
|
|
||||||
} satisfies CopyResult
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return yield* Effect.all(effects, { concurrency: 'unbounded' })
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 报告服务
|
|
||||||
*/
|
|
||||||
class ReporterService extends Context.Tag('ReporterService')<
|
|
||||||
ReporterService,
|
|
||||||
{
|
|
||||||
readonly printSummary: (
|
|
||||||
results: ReadonlyArray<CopyResult>,
|
|
||||||
) => Effect.Effect<void>
|
|
||||||
}
|
|
||||||
>() {
|
|
||||||
static readonly Live = Layer.succeed(ReporterService, {
|
|
||||||
printSummary: (results: ReadonlyArray<CopyResult>) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const successful = results.filter((r) => r.success)
|
|
||||||
const failed = results.filter((r) => !r.success)
|
|
||||||
|
|
||||||
yield* Console.log('\n📦 复制摘要:')
|
|
||||||
yield* Console.log(` ✅ 成功: ${successful.length}`)
|
|
||||||
yield* Console.log(` ⚠️ 跳过: ${failed.length}`)
|
|
||||||
|
|
||||||
if (successful.length > 0) {
|
|
||||||
yield* Console.log('\n成功复制的文件:')
|
|
||||||
for (const result of successful) {
|
|
||||||
yield* Console.log(
|
|
||||||
` • ${result.bunSuffix} → ${result.tauriTarget}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed.length > 0) {
|
|
||||||
yield* Console.log('\n跳过的文件:')
|
|
||||||
for (const result of failed) {
|
|
||||||
yield* Console.log(` • ${result.bunSuffix} (源文件不存在)`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Main Program
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const program = Effect.gen(function* () {
|
|
||||||
const config = yield* CopyConfigService
|
|
||||||
const fs = yield* FileSystemService
|
|
||||||
const copier = yield* CopyService
|
|
||||||
const reporter = yield* ReporterService
|
|
||||||
|
|
||||||
yield* Console.log('📦 开始复制二进制文件到 Tauri sidecar 目录...\n')
|
|
||||||
|
|
||||||
// 1. 检查源目录
|
|
||||||
const sourceExists = yield* fs.dirExists(config.sourceDir)
|
|
||||||
if (!sourceExists) {
|
|
||||||
yield* Console.error(`❌ 源目录不存在: ${config.sourceDir}`)
|
|
||||||
yield* Console.log(
|
|
||||||
'💡 提示: 请先在 apps/server 中运行 bun run compile 构建服务器二进制文件',
|
|
||||||
)
|
|
||||||
return yield* Effect.fail(
|
|
||||||
new FileSystemError({
|
|
||||||
operation: 'checkSourceDir',
|
|
||||||
path: config.sourceDir,
|
|
||||||
cause: '源目录不存在',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 创建目标目录
|
|
||||||
yield* fs.ensureDir(config.targetDir)
|
|
||||||
yield* Console.log(`✓ 目标目录: ${config.targetDir}\n`)
|
|
||||||
|
|
||||||
// 3. 并行复制所有二进制文件
|
|
||||||
const results = yield* copier.copyAllBinaries(config)
|
|
||||||
|
|
||||||
// 4. 输出摘要
|
|
||||||
yield* reporter.printSummary(results)
|
|
||||||
|
|
||||||
return results
|
|
||||||
})
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Layer Composition
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const MainLayer = Layer.mergeAll(
|
|
||||||
CopyConfigService.Live,
|
|
||||||
FileSystemService.Live,
|
|
||||||
CopyService.Live.pipe(Layer.provide(FileSystemService.Live)),
|
|
||||||
ReporterService.Live,
|
|
||||||
)
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Runner
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const runnable = program.pipe(
|
|
||||||
Effect.provide(MainLayer),
|
|
||||||
Effect.catchTags({
|
|
||||||
ConfigError: (error) =>
|
|
||||||
Console.error(`❌ 配置错误: ${error.message}`, error.cause),
|
|
||||||
FileSystemError: (error) =>
|
|
||||||
Console.error(
|
|
||||||
`❌ 文件系统错误 [${error.operation}]: ${error.path}`,
|
|
||||||
error.cause,
|
|
||||||
),
|
|
||||||
CopyError: (error) =>
|
|
||||||
Console.error(
|
|
||||||
`❌ 复制失败: ${error.source} → ${error.target}`,
|
|
||||||
error.cause,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
Effect.tapErrorCause((cause) => Console.error('❌ 未预期的错误:', cause)),
|
|
||||||
)
|
|
||||||
|
|
||||||
Effect.runPromise(runnable).catch(() => {
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@furtherverse/desktop",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "bun run copy && tauri build",
|
|
||||||
"copy": "rm -rf binaries && bun --bun copy.ts",
|
|
||||||
"dev": "bun run copy && tauri dev"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@effect/schema": "catalog:",
|
|
||||||
"@furtherverse/tsconfig": "workspace:*",
|
|
||||||
"@tauri-apps/cli": "catalog:",
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"effect": "catalog:",
|
|
||||||
"typescript": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@furtherverse/tsconfig/bun.json",
|
|
||||||
"exclude": ["node_modules", "src-tauri"]
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../node_modules/turbo/schema.json",
|
|
||||||
"extends": ["//"],
|
|
||||||
"tasks": {
|
|
||||||
"build": {
|
|
||||||
"dependsOn": ["@furtherverse/server#compile"],
|
|
||||||
"outputs": ["src-tauri/target/release/**"]
|
|
||||||
},
|
|
||||||
"dev": {
|
|
||||||
"dependsOn": ["@furtherverse/server#compile"],
|
|
||||||
"with": ["@furtherverse/server#dev"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
# AGENTS.md - Server App Guidelines
|
|
||||||
|
|
||||||
TanStack Start fullstack web app with ORPC (contract-first RPC).
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
- **Framework**: TanStack Start (React 19 SSR, file-based routing)
|
|
||||||
- **Runtime**: Bun
|
|
||||||
- **Language**: TypeScript (strict mode)
|
|
||||||
- **Styling**: Tailwind CSS v4
|
|
||||||
- **Database**: PostgreSQL + Drizzle ORM
|
|
||||||
- **State**: TanStack Query v5
|
|
||||||
- **RPC**: ORPC (contract-first, type-safe)
|
|
||||||
- **Build**: Vite + Nitro
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
bun dev # Vite dev server (localhost:3000)
|
|
||||||
bun db:studio # Drizzle Studio GUI
|
|
||||||
|
|
||||||
# Build
|
|
||||||
bun build # Production build → .output/
|
|
||||||
bun compile # Compile to standalone binary
|
|
||||||
|
|
||||||
# Code Quality
|
|
||||||
bun fix # Biome auto-fix
|
|
||||||
bun typecheck # TypeScript check
|
|
||||||
|
|
||||||
# Database
|
|
||||||
bun db:generate # Generate migrations from schema
|
|
||||||
bun db:migrate # Run migrations
|
|
||||||
bun db:push # Push schema directly (dev only)
|
|
||||||
|
|
||||||
# Testing (not yet configured)
|
|
||||||
bun test path/to/test.ts # Run single test
|
|
||||||
bun test -t "pattern" # Run tests matching pattern
|
|
||||||
```
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── client/ # Client-side code
|
|
||||||
│ ├── orpc.client.ts # ORPC isomorphic client
|
|
||||||
│ └── query-client.ts # TanStack Query client
|
|
||||||
├── components/ # React components
|
|
||||||
├── routes/ # TanStack Router file routes
|
|
||||||
│ ├── __root.tsx # Root layout
|
|
||||||
│ ├── index.tsx # Home page
|
|
||||||
│ └── api/
|
|
||||||
│ └── rpc.$.ts # ORPC HTTP endpoint
|
|
||||||
├── server/ # Server-side code
|
|
||||||
│ ├── api/ # ORPC layer
|
|
||||||
│ │ ├── contracts/ # Input/output schemas (Zod)
|
|
||||||
│ │ ├── middlewares/ # Middleware (db provider, auth)
|
|
||||||
│ │ ├── routers/ # Handler implementations
|
|
||||||
│ │ ├── context.ts # Request context
|
|
||||||
│ │ ├── server.ts # ORPC server instance
|
|
||||||
│ │ └── types.ts # Type exports
|
|
||||||
│ └── db/
|
|
||||||
│ ├── schema/ # Drizzle table definitions
|
|
||||||
│ └── index.ts # Database instance
|
|
||||||
├── env.ts # Environment variable validation
|
|
||||||
├── router.tsx # Router configuration
|
|
||||||
├── routeTree.gen.ts # Auto-generated (DO NOT EDIT)
|
|
||||||
└── styles.css # Tailwind entry
|
|
||||||
```
|
|
||||||
|
|
||||||
## ORPC Pattern
|
|
||||||
|
|
||||||
### 1. Define Contract (`src/server/api/contracts/feature.contract.ts`)
|
|
||||||
```typescript
|
|
||||||
import { oc } from '@orpc/contract'
|
|
||||||
import { createSelectSchema } from 'drizzle-zod'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { featureTable } from '@/server/db/schema'
|
|
||||||
|
|
||||||
const selectSchema = createSelectSchema(featureTable)
|
|
||||||
|
|
||||||
export const list = oc.input(z.void()).output(z.array(selectSchema))
|
|
||||||
export const create = oc.input(insertSchema).output(selectSchema)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Implement Router (`src/server/api/routers/feature.router.ts`)
|
|
||||||
```typescript
|
|
||||||
import { ORPCError } from '@orpc/server'
|
|
||||||
import { db } from '../middlewares'
|
|
||||||
import { os } from '../server'
|
|
||||||
|
|
||||||
export const list = os.feature.list.use(db).handler(async ({ context }) => {
|
|
||||||
return await context.db.query.featureTable.findMany()
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Register in Index Files
|
|
||||||
```typescript
|
|
||||||
// src/server/api/contracts/index.ts
|
|
||||||
import * as feature from './feature.contract'
|
|
||||||
export const contract = { feature }
|
|
||||||
|
|
||||||
// src/server/api/routers/index.ts
|
|
||||||
import * as feature from './feature.router'
|
|
||||||
export const router = os.router({ feature })
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Use in Components
|
|
||||||
```typescript
|
|
||||||
import { useSuspenseQuery, useMutation } from '@tanstack/react-query'
|
|
||||||
import { orpc } from '@/client/orpc.client'
|
|
||||||
|
|
||||||
const { data } = useSuspenseQuery(orpc.feature.list.queryOptions())
|
|
||||||
const mutation = useMutation(orpc.feature.create.mutationOptions())
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Schema (Drizzle)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
|
|
||||||
import { sql } from 'drizzle-orm'
|
|
||||||
|
|
||||||
export const myTable = pgTable('my_table', {
|
|
||||||
id: uuid().primaryKey().default(sql`uuidv7()`),
|
|
||||||
name: text().notNull(),
|
|
||||||
createdAt: timestamp({ withTimezone: true }).notNull().defaultNow(),
|
|
||||||
updatedAt: timestamp({ withTimezone: true }).notNull().defaultNow().$onUpdateFn(() => new Date()),
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
### Formatting (Biome)
|
|
||||||
- **Indent**: 2 spaces
|
|
||||||
- **Quotes**: Single `'`
|
|
||||||
- **Semicolons**: Omit (ASI)
|
|
||||||
- **Arrow parens**: Always `(x) => x`
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
Biome auto-organizes:
|
|
||||||
1. External packages
|
|
||||||
2. Internal `@/*` aliases
|
|
||||||
3. Type imports (`import type { ... }`)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { db } from '@/server/db'
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
```
|
|
||||||
|
|
||||||
### TypeScript
|
|
||||||
- `strict: true`
|
|
||||||
- `noUncheckedIndexedAccess: true` - array access returns `T | undefined`
|
|
||||||
- Use `@/*` path aliases (maps to `src/*`)
|
|
||||||
|
|
||||||
### Naming
|
|
||||||
| Type | Convention | Example |
|
|
||||||
|------|------------|---------|
|
|
||||||
| Files (utils) | kebab-case | `auth-utils.ts` |
|
|
||||||
| Files (components) | PascalCase | `UserProfile.tsx` |
|
|
||||||
| Components | PascalCase arrow | `const Button = () => {}` |
|
|
||||||
| Functions | camelCase | `getUserById` |
|
|
||||||
| Types | PascalCase | `UserProfile` |
|
|
||||||
|
|
||||||
### React
|
|
||||||
- Use arrow functions for components (Biome enforced)
|
|
||||||
- Use `useSuspenseQuery` for guaranteed data
|
|
||||||
- Let React Compiler handle memoization (no manual `useMemo`/`useCallback`)
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/env.ts - using @t3-oss/env-core
|
|
||||||
import { createEnv } from '@t3-oss/env-core'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
export const env = createEnv({
|
|
||||||
server: {
|
|
||||||
DATABASE_URL: z.string().url(),
|
|
||||||
},
|
|
||||||
clientPrefix: 'VITE_',
|
|
||||||
client: {
|
|
||||||
VITE_API_URL: z.string().optional(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Critical Rules
|
|
||||||
|
|
||||||
**DO:**
|
|
||||||
- Run `bun fix` before committing
|
|
||||||
- Use `@/*` path aliases
|
|
||||||
- Include `createdAt`/`updatedAt` on all tables
|
|
||||||
- Use `ORPCError` with proper codes
|
|
||||||
|
|
||||||
**DON'T:**
|
|
||||||
- Edit `src/routeTree.gen.ts` (auto-generated)
|
|
||||||
- Use `as any`, `@ts-ignore`, `@ts-expect-error`
|
|
||||||
- Commit `.env` files
|
|
||||||
- Use empty catch blocks
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../node_modules/@biomejs/biome/configuration_schema.json",
|
|
||||||
"extends": "//",
|
|
||||||
"files": {
|
|
||||||
"includes": ["**", "!**/routeTree.gen.ts"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@furtherverse/server",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"compile": "bun build.ts",
|
|
||||||
"db:generate": "drizzle-kit generate",
|
|
||||||
"db:migrate": "drizzle-kit migrate",
|
|
||||||
"db:push": "drizzle-kit push",
|
|
||||||
"db:studio": "drizzle-kit studio",
|
|
||||||
"dev": "vite dev",
|
|
||||||
"fix": "biome check --write",
|
|
||||||
"typecheck": "tsc --noEmit"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@furtherverse/utils": "workspace:*",
|
|
||||||
"@orpc/client": "catalog:",
|
|
||||||
"@orpc/contract": "catalog:",
|
|
||||||
"@orpc/openapi": "catalog:",
|
|
||||||
"@orpc/server": "catalog:",
|
|
||||||
"@orpc/tanstack-query": "catalog:",
|
|
||||||
"@orpc/zod": "catalog:",
|
|
||||||
"@t3-oss/env-core": "catalog:",
|
|
||||||
"@tanstack/react-query": "catalog:",
|
|
||||||
"@tanstack/react-router": "catalog:",
|
|
||||||
"@tanstack/react-router-ssr-query": "catalog:",
|
|
||||||
"@tanstack/react-start": "catalog:",
|
|
||||||
"@tauri-apps/api": "catalog:",
|
|
||||||
"drizzle-orm": "catalog:",
|
|
||||||
"drizzle-zod": "catalog:",
|
|
||||||
"postgres": "catalog:",
|
|
||||||
"react": "catalog:",
|
|
||||||
"react-dom": "catalog:",
|
|
||||||
"uuid": "catalog:",
|
|
||||||
"zod": "catalog:"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@effect/platform": "catalog:",
|
|
||||||
"@effect/schema": "catalog:",
|
|
||||||
"@furtherverse/tsconfig": "workspace:*",
|
|
||||||
"@tailwindcss/vite": "catalog:",
|
|
||||||
"@tanstack/devtools-vite": "catalog:",
|
|
||||||
"@tanstack/react-devtools": "catalog:",
|
|
||||||
"@tanstack/react-query-devtools": "catalog:",
|
|
||||||
"@tanstack/react-router-devtools": "catalog:",
|
|
||||||
"@types/bun": "catalog:",
|
|
||||||
"@vitejs/plugin-react": "catalog:",
|
|
||||||
"babel-plugin-react-compiler": "catalog:",
|
|
||||||
"drizzle-kit": "catalog:",
|
|
||||||
"effect": "catalog:",
|
|
||||||
"nitro": "catalog:",
|
|
||||||
"tailwindcss": "catalog:",
|
|
||||||
"typescript": "catalog:",
|
|
||||||
"vite": "catalog:",
|
|
||||||
"vite-tsconfig-paths": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { createORPCClient } from '@orpc/client'
|
|
||||||
import { RPCLink } from '@orpc/client/fetch'
|
|
||||||
import { createRouterClient } from '@orpc/server'
|
|
||||||
import { createIsomorphicFn } from '@tanstack/react-start'
|
|
||||||
import { getRequestHeaders } from '@tanstack/react-start/server'
|
|
||||||
import { router } from '@/server/api/routers'
|
|
||||||
import type { RouterClient } from '@/server/api/types'
|
|
||||||
|
|
||||||
const getORPCClient = createIsomorphicFn()
|
|
||||||
.server(() =>
|
|
||||||
createRouterClient(router, {
|
|
||||||
context: () => ({
|
|
||||||
headers: getRequestHeaders(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.client(() => {
|
|
||||||
const link = new RPCLink({
|
|
||||||
url: `${window.location.origin}/api/rpc`,
|
|
||||||
})
|
|
||||||
return createORPCClient<RouterClient>(link)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const orpc: RouterClient = getORPCClient()
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
|
|
||||||
import { orpc as orpcClient } from './orpc.client'
|
|
||||||
|
|
||||||
export const orpc = createTanstackQueryUtils(orpcClient, {
|
|
||||||
experimental_defaults: {
|
|
||||||
todo: {
|
|
||||||
create: {
|
|
||||||
mutationOptions: {
|
|
||||||
onSuccess: (_, __, ___, ctx) => {
|
|
||||||
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
mutationOptions: {
|
|
||||||
onSuccess: (_, __, ___, ctx) => {
|
|
||||||
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
remove: {
|
|
||||||
mutationOptions: {
|
|
||||||
onSuccess: (_, __, ___, ctx) => {
|
|
||||||
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { OpenAPIHandler } from '@orpc/openapi/fetch'
|
|
||||||
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
|
|
||||||
import { ORPCError, onError, ValidationError } from '@orpc/server'
|
|
||||||
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
|
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { name, version } from '@/../package.json'
|
|
||||||
import { router } from '@/server/api/routers'
|
|
||||||
|
|
||||||
const handler = new OpenAPIHandler(router, {
|
|
||||||
plugins: [
|
|
||||||
new OpenAPIReferencePlugin({
|
|
||||||
docsProvider: 'scalar',
|
|
||||||
schemaConverters: [new ZodToJsonSchemaConverter()],
|
|
||||||
specGenerateOptions: {
|
|
||||||
info: {
|
|
||||||
title: name,
|
|
||||||
version,
|
|
||||||
},
|
|
||||||
// components: {
|
|
||||||
// securitySchemes: {
|
|
||||||
// bearerAuth: {
|
|
||||||
// type: 'http',
|
|
||||||
// scheme: 'bearer',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
docsPath: '/docs',
|
|
||||||
specPath: '/spec.json',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
interceptors: [
|
|
||||||
onError((error) => {
|
|
||||||
console.error(error)
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
clientInterceptors: [
|
|
||||||
onError((error) => {
|
|
||||||
if (
|
|
||||||
error instanceof ORPCError &&
|
|
||||||
error.code === 'BAD_REQUEST' &&
|
|
||||||
error.cause instanceof ValidationError
|
|
||||||
) {
|
|
||||||
// If you only use Zod you can safely cast to ZodIssue[]
|
|
||||||
const zodError = new z.ZodError(
|
|
||||||
error.cause.issues as z.core.$ZodIssue[],
|
|
||||||
)
|
|
||||||
|
|
||||||
throw new ORPCError('INPUT_VALIDATION_FAILED', {
|
|
||||||
status: 422,
|
|
||||||
message: z.prettifyError(zodError),
|
|
||||||
data: z.flattenError(zodError),
|
|
||||||
cause: error.cause,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
error instanceof ORPCError &&
|
|
||||||
error.code === 'INTERNAL_SERVER_ERROR' &&
|
|
||||||
error.cause instanceof ValidationError
|
|
||||||
) {
|
|
||||||
throw new ORPCError('OUTPUT_VALIDATION_FAILED', {
|
|
||||||
cause: error.cause,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/api/$')({
|
|
||||||
server: {
|
|
||||||
handlers: {
|
|
||||||
ANY: async ({ request }) => {
|
|
||||||
const { response } = await handler.handle(request, {
|
|
||||||
prefix: '/api',
|
|
||||||
context: {
|
|
||||||
headers: request.headers,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return response ?? new Response('Not Found', { status: 404 })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { DB } from '@/server/db'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基础 Context - 所有请求都包含的上下文
|
|
||||||
*/
|
|
||||||
export interface BaseContext {
|
|
||||||
headers: Headers
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 数据库 Context - 通过 db middleware 扩展
|
|
||||||
*/
|
|
||||||
export interface DBContext extends BaseContext {
|
|
||||||
db: DB
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 认证 Context - 通过 auth middleware 扩展(未来使用)
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* export interface AuthContext extends DBContext {
|
|
||||||
* userId: string
|
|
||||||
* user: User
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import * as todo from './todo.contract'
|
|
||||||
|
|
||||||
export const contract = {
|
|
||||||
todo,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Contract = typeof contract
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { os } from '@orpc/server'
|
|
||||||
import { getDB } from '@/server/db'
|
|
||||||
|
|
||||||
export const db = os.middleware(async ({ context, next }) => {
|
|
||||||
return next({
|
|
||||||
context: {
|
|
||||||
...context,
|
|
||||||
db: getDB(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './db.middleware'
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { os } from '../server'
|
|
||||||
import * as todo from './todo.router'
|
|
||||||
|
|
||||||
export const router = os.router({
|
|
||||||
todo,
|
|
||||||
})
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { implement } from '@orpc/server'
|
|
||||||
import type { BaseContext } from './context'
|
|
||||||
import { contract } from './contracts'
|
|
||||||
|
|
||||||
export const os = implement(contract).$context<BaseContext>()
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
||||||
import { env } from '@/env'
|
|
||||||
import * as schema from '@/server/db/schema'
|
|
||||||
|
|
||||||
export const createDB = () =>
|
|
||||||
drizzle({
|
|
||||||
connection: {
|
|
||||||
url: env.DATABASE_URL,
|
|
||||||
prepare: true,
|
|
||||||
},
|
|
||||||
schema,
|
|
||||||
})
|
|
||||||
|
|
||||||
export type DB = ReturnType<typeof createDB>
|
|
||||||
|
|
||||||
export const getDB = (() => {
|
|
||||||
let db: DB | null = null
|
|
||||||
|
|
||||||
return (singleton = true): DB => {
|
|
||||||
if (!singleton) {
|
|
||||||
return createDB()
|
|
||||||
}
|
|
||||||
|
|
||||||
db ??= createDB()
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { boolean, pgTable, text } from 'drizzle-orm/pg-core'
|
|
||||||
import { generatedFields } from './utils/field'
|
|
||||||
|
|
||||||
export const todoTable = pgTable('todo', {
|
|
||||||
...generatedFields,
|
|
||||||
title: text('title').notNull(),
|
|
||||||
completed: boolean('completed').notNull().default(false),
|
|
||||||
})
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { sql } from 'drizzle-orm'
|
|
||||||
import { timestamp, uuid } from 'drizzle-orm/pg-core'
|
|
||||||
import { v7 as uuidv7 } from 'uuid'
|
|
||||||
|
|
||||||
// id
|
|
||||||
|
|
||||||
export const id = (name: string) => uuid(name)
|
|
||||||
export const pk = (name: string, strategy?: 'native' | 'extension') => {
|
|
||||||
switch (strategy) {
|
|
||||||
// PG 18+
|
|
||||||
case 'native':
|
|
||||||
return id(name).primaryKey().default(sql`uuidv7()`)
|
|
||||||
|
|
||||||
// PG 13+ with extension
|
|
||||||
case 'extension':
|
|
||||||
return id(name).primaryKey().default(sql`uuid_generate_v7()`)
|
|
||||||
|
|
||||||
// Any PG version
|
|
||||||
default:
|
|
||||||
return id(name)
|
|
||||||
.primaryKey()
|
|
||||||
.$defaultFn(() => uuidv7())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// timestamp
|
|
||||||
|
|
||||||
export const createdAt = (name = 'created_at') =>
|
|
||||||
timestamp(name, { withTimezone: true }).notNull().defaultNow()
|
|
||||||
|
|
||||||
export const updatedAt = (name = 'updated_at') =>
|
|
||||||
timestamp(name, { withTimezone: true })
|
|
||||||
.notNull()
|
|
||||||
.defaultNow()
|
|
||||||
.$onUpdateFn(() => new Date())
|
|
||||||
|
|
||||||
// generated fields
|
|
||||||
|
|
||||||
export const generatedFields = {
|
|
||||||
id: pk('id'),
|
|
||||||
createdAt: createdAt('created_at'),
|
|
||||||
updatedAt: updatedAt('updated_at'),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to create omit keys from generatedFields
|
|
||||||
const createGeneratedFieldKeys = <T extends Record<string, unknown>>(
|
|
||||||
fields: T,
|
|
||||||
): Record<keyof T, true> => {
|
|
||||||
return Object.keys(fields).reduce(
|
|
||||||
(acc, key) => {
|
|
||||||
acc[key as keyof T] = true
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<keyof T, true>,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const generatedFieldKeys = createGeneratedFieldKeys(generatedFields)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@furtherverse/tsconfig/react.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../node_modules/turbo/schema.json",
|
|
||||||
"extends": ["//"],
|
|
||||||
"tasks": {
|
|
||||||
"compile": {
|
|
||||||
"dependsOn": ["build"],
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"useIgnoreFile": true
|
"useIgnoreFile": true
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
|
"includes": ["**", "!**/routeTree.gen.ts"],
|
||||||
"ignoreUnknown": false
|
"ignoreUnknown": false
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ import { Console, Context, Data, Effect, Layer } from 'effect'
|
|||||||
// Domain Models & Schema
|
// Domain Models & Schema
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
const targetMap = {
|
||||||
|
'bun-windows-x64': 'x86_64-pc-windows-msvc',
|
||||||
|
'bun-darwin-arm64': 'aarch64-apple-darwin',
|
||||||
|
'bun-darwin-x64': 'x86_64-apple-darwin',
|
||||||
|
'bun-linux-x64': 'x86_64-unknown-linux-gnu',
|
||||||
|
'bun-linux-arm64': 'aarch64-unknown-linux-gnu',
|
||||||
|
} as const
|
||||||
|
|
||||||
const BunTargetSchema = Schema.Literal(
|
const BunTargetSchema = Schema.Literal(
|
||||||
'bun-windows-x64',
|
'bun-windows-x64',
|
||||||
'bun-darwin-arm64',
|
'bun-darwin-arm64',
|
||||||
@@ -14,19 +22,11 @@ const BunTargetSchema = Schema.Literal(
|
|||||||
'bun-linux-arm64',
|
'bun-linux-arm64',
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 bun target 转换为文件后缀 (去掉 'bun-' 前缀)
|
|
||||||
*/
|
|
||||||
const getTargetSuffix = (target: BunTarget): string => {
|
|
||||||
return target.replace('bun-', '')
|
|
||||||
}
|
|
||||||
|
|
||||||
type BunTarget = Schema.Schema.Type<typeof BunTargetSchema>
|
type BunTarget = Schema.Schema.Type<typeof BunTargetSchema>
|
||||||
|
|
||||||
const BuildConfigSchema = Schema.Struct({
|
const BuildConfigSchema = Schema.Struct({
|
||||||
entrypoint: Schema.String.pipe(Schema.nonEmptyString()),
|
entrypoint: Schema.String.pipe(Schema.nonEmptyString()),
|
||||||
outputDir: Schema.String.pipe(Schema.nonEmptyString()),
|
outputDir: Schema.String.pipe(Schema.nonEmptyString()),
|
||||||
outfile: Schema.String.pipe(Schema.nonEmptyString()),
|
|
||||||
targets: Schema.Array(BunTargetSchema).pipe(Schema.minItems(1)),
|
targets: Schema.Array(BunTargetSchema).pipe(Schema.minItems(1)),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -94,10 +94,10 @@ class BuildConfigService extends Context.Tag('BuildConfigService')<
|
|||||||
BuildConfigService,
|
BuildConfigService,
|
||||||
BuildConfigService.fromRaw({
|
BuildConfigService.fromRaw({
|
||||||
entrypoint: '.output/server/index.mjs',
|
entrypoint: '.output/server/index.mjs',
|
||||||
outputDir: 'out',
|
// outputDir: 'out',
|
||||||
outfile: 'server',
|
outputDir: 'src-tauri/binaries',
|
||||||
targets: ['bun-windows-x64', 'bun-darwin-arm64', 'bun-linux-x64'],
|
targets: ['bun-windows-x64', 'bun-darwin-arm64', 'bun-linux-x64'],
|
||||||
} satisfies BuildConfig),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ class BuildService extends Context.Tag('BuildService')<
|
|||||||
Bun.build({
|
Bun.build({
|
||||||
entrypoints: [config.entrypoint],
|
entrypoints: [config.entrypoint],
|
||||||
compile: {
|
compile: {
|
||||||
outfile: `${config.outfile}-${getTargetSuffix(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: `${config.outfile}-${getTargetSuffix(target)}`,
|
outfile: `app-${targetMap[target]}`,
|
||||||
target: target,
|
target: target,
|
||||||
},
|
},
|
||||||
outdir: config.outputDir,
|
outdir: config.outputDir,
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[install]
|
|
||||||
publicHoistPattern = ["@types/*", "bun-types", "nitro*"]
|
|
||||||
@@ -3,7 +3,7 @@ import { env } from '@/env'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
out: './drizzle',
|
out: './drizzle',
|
||||||
schema: './src/server/db/schema/index.ts',
|
schema: './src/db/schema/index.ts',
|
||||||
dialect: 'postgresql',
|
dialect: 'postgresql',
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: env.DATABASE_URL,
|
url: env.DATABASE_URL,
|
||||||
0
drizzle/.gitkeep
Normal file
@@ -1,4 +1,4 @@
|
|||||||
[tools]
|
[tools]
|
||||||
node = "latest"
|
node = "24"
|
||||||
bun = "1"
|
bun = "1"
|
||||||
rust = 'latest'
|
rust = 'latest'
|
||||||
|
|||||||
95
package.json
@@ -1,65 +1,64 @@
|
|||||||
{
|
{
|
||||||
"name": "@furtherverse/monorepo",
|
"name": "fullstack-starter",
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
|
||||||
"apps/*",
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo build:tauri",
|
||||||
"compile": "turbo run compile",
|
"build:compile": "bun build.ts",
|
||||||
"deploy": "turbo run deploy",
|
"build:tauri": "NO_STRIP=1 tauri build",
|
||||||
"dev": "turbo run dev",
|
"build:vite": "vite build",
|
||||||
"fix": "turbo run fix",
|
"db:generate": "drizzle-kit generate",
|
||||||
"typecheck": "turbo run typecheck"
|
"db:migrate": "drizzle-kit migrate",
|
||||||
|
"db:push": "drizzle-kit push",
|
||||||
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"dev": "turbo dev:tauri",
|
||||||
|
"dev:tauri": "tauri dev",
|
||||||
|
"dev:vite": "vite dev",
|
||||||
|
"fix": "biome check --write",
|
||||||
|
"typecheck": "tsc -b"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@biomejs/biome": "^2.3.11",
|
|
||||||
"turbo": "^2.7.5"
|
|
||||||
},
|
|
||||||
"catalog": {
|
|
||||||
"@biomejs/biome": "^2.3.11",
|
|
||||||
"@effect/platform": "^0.94.2",
|
|
||||||
"@effect/schema": "^0.75.5",
|
|
||||||
"@orpc/client": "^1.13.4",
|
"@orpc/client": "^1.13.4",
|
||||||
"@orpc/contract": "^1.13.4",
|
"@orpc/contract": "^1.13.4",
|
||||||
"@orpc/openapi": "^1.13.4",
|
|
||||||
"@orpc/server": "^1.13.4",
|
"@orpc/server": "^1.13.4",
|
||||||
"@orpc/tanstack-query": "^1.13.4",
|
"@orpc/tanstack-query": "^1.13.4",
|
||||||
"@orpc/zod": "^1.13.4",
|
"@orpc/zod": "^1.13.4",
|
||||||
"@t3-oss/env-core": "^0.13.10",
|
"@t3-oss/env-core": "^0.13.10",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
"@tanstack/devtools-vite": "^0.4.1",
|
"@tanstack/react-router": "^1.158.1",
|
||||||
"@tanstack/react-devtools": "^0.9.2",
|
"@tanstack/react-router-ssr-query": "^1.158.1",
|
||||||
"@tanstack/react-query": "^5.90.19",
|
"@tanstack/react-start": "^1.158.3",
|
||||||
"@tanstack/react-query-devtools": "^5.91.2",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
"@tanstack/react-router": "^1.154.12",
|
|
||||||
"@tanstack/react-router-devtools": "^1.154.12",
|
|
||||||
"@tanstack/react-router-ssr-query": "^1.154.12",
|
|
||||||
"@tanstack/react-start": "^1.154.12",
|
|
||||||
"@tauri-apps/api": "^2.9.1",
|
|
||||||
"@tauri-apps/cli": "^2.9.6",
|
|
||||||
"@types/bun": "^1.3.6",
|
|
||||||
"@vitejs/plugin-react": "^5.1.2",
|
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
|
||||||
"drizzle-kit": "^0.31.8",
|
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"drizzle-zod": "^0.8.3",
|
"drizzle-zod": "^0.8.3",
|
||||||
"effect": "^3.19.15",
|
|
||||||
"nitro": "npm:nitro-nightly@3.0.1-20260122-201913-dfdff9e9",
|
|
||||||
"ohash": "^2.0.11",
|
|
||||||
"postgres": "^3.4.8",
|
"postgres": "^3.4.8",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.4",
|
||||||
"tailwindcss": "^4.1.18",
|
|
||||||
"turbo": "^2.7.5",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"uuid": "^13.0.0",
|
|
||||||
"systeminformation": "^5.30.5",
|
|
||||||
"vite": "^8.0.0-beta.9",
|
|
||||||
"vite-tsconfig-paths": "^6.0.4",
|
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^2.3.14",
|
||||||
|
"@effect/platform": "^0.94.3",
|
||||||
|
"@effect/schema": "^0.75.5",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@tanstack/devtools-vite": "^0.4.1",
|
||||||
|
"@tanstack/react-devtools": "^0.9.4",
|
||||||
|
"@tanstack/react-query-devtools": "^5.91.3",
|
||||||
|
"@tanstack/react-router-devtools": "^1.158.1",
|
||||||
|
"@tauri-apps/cli": "^2.10.0",
|
||||||
|
"@types/bun": "^1.3.8",
|
||||||
|
"@vitejs/plugin-react": "^5.1.3",
|
||||||
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
|
"drizzle-kit": "^0.31.8",
|
||||||
|
"effect": "^3.19.16",
|
||||||
|
"nitro": "npm:nitro-nightly@latest",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"turbo": "^2.8.3",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^8.0.0-beta.13",
|
||||||
|
"vite-tsconfig-paths": "^6.0.5"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@tanstack/query-core": "^5.90.20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"display": "Base",
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "esnext",
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"module": "preserve",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"display": "Bun",
|
|
||||||
"extends": "./base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"types": ["bun-types"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@furtherverse/tsconfig",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"exports": {
|
|
||||||
"./base.json": "./base.json",
|
|
||||||
"./bun.json": "./bun.json",
|
|
||||||
"./react.json": "./react.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
|
||||||
"display": "React",
|
|
||||||
"extends": "./base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@furtherverse/utils",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"imports": {
|
|
||||||
"#*": "./src/*"
|
|
||||||
},
|
|
||||||
"exports": {
|
|
||||||
".": "./src/index.ts",
|
|
||||||
"./*": "./src/*.ts"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ohash": "catalog:",
|
|
||||||
"systeminformation": "catalog:"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@furtherverse/tsconfig": "workspace:*",
|
|
||||||
"typescript": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { hash } from 'ohash'
|
|
||||||
import si from 'systeminformation'
|
|
||||||
|
|
||||||
async function getSystemInfo() {
|
|
||||||
const [uuid, baseboard, bios, system, diskLayout, networkInterfaces] =
|
|
||||||
await Promise.all([
|
|
||||||
si.uuid(),
|
|
||||||
si.baseboard(),
|
|
||||||
si.bios(),
|
|
||||||
si.system(),
|
|
||||||
si.diskLayout(),
|
|
||||||
si.networkInterfaces(),
|
|
||||||
])
|
|
||||||
|
|
||||||
return {
|
|
||||||
uuid,
|
|
||||||
baseboard,
|
|
||||||
bios,
|
|
||||||
system,
|
|
||||||
diskLayout,
|
|
||||||
networkInterfaces,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getHardwareFingerprint() {
|
|
||||||
const systemInfo = await getSystemInfo()
|
|
||||||
|
|
||||||
return hash(systemInfo)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './fingerprint'
|
|
||||||
357
src-tauri/AGENTS.md
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
# AGENTS.md - Tauri Shell 项目开发指南
|
||||||
|
|
||||||
|
本文档为 AI 编程助手和开发者提供项目规范、构建命令和代码风格指南。
|
||||||
|
|
||||||
|
## 项目概览
|
||||||
|
|
||||||
|
- **项目类型**: Tauri v2 桌面应用(轻量级壳子)
|
||||||
|
- **后端**: Rust (Edition 2021)
|
||||||
|
- **架构**: Sidecar 模式 - Sidecar App 承载主要业务逻辑
|
||||||
|
- **设计理念**: Tauri 仅提供原生桌面能力(文件对话框、系统通知等),Web 逻辑全部由 Sidecar App 处理
|
||||||
|
- **开发模式**: 使用 localhost:3000(需手动启动开发服务器)
|
||||||
|
- **生产模式**: 自动启动 Sidecar 二进制
|
||||||
|
- **异步运行时**: Tokio
|
||||||
|
- **Rust 版本**: 1.92.0+
|
||||||
|
- **工具管理**: 使用 mise 管理 Rust 和 Tauri CLI 版本(见 `mise.toml`)
|
||||||
|
|
||||||
|
## 构建、测试、运行命令
|
||||||
|
|
||||||
|
### 开发运行
|
||||||
|
```bash
|
||||||
|
# 开发模式运行 (需要先启动开发服务器)
|
||||||
|
# 终端 1: 启动前端开发服务器
|
||||||
|
bun run dev
|
||||||
|
|
||||||
|
# 终端 2: 启动 Tauri 应用
|
||||||
|
tauri dev
|
||||||
|
|
||||||
|
# 或者使用单命令并行启动(需要配置 package.json)
|
||||||
|
bun run dev:tauri
|
||||||
|
```
|
||||||
|
|
||||||
|
**开发模式说明**:
|
||||||
|
- 开发模式下,Tauri 直接连接到 `localhost:3000`(不启动 sidecar 二进制)
|
||||||
|
- 需要手动运行 `bun run dev` 来启动开发服务器
|
||||||
|
- 支持热重载(HMR),无需重启 Tauri 应用
|
||||||
|
|
||||||
|
### 构建
|
||||||
|
```bash
|
||||||
|
# 开发构建 (debug mode)
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# 生产构建
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Tauri 应用打包 (生成安装程序)
|
||||||
|
tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码检查
|
||||||
|
```bash
|
||||||
|
# 编译检查 (不生成二进制)
|
||||||
|
cargo check
|
||||||
|
|
||||||
|
# Clippy 代码质量检查
|
||||||
|
cargo clippy
|
||||||
|
|
||||||
|
# Clippy 严格模式 (所有警告视为错误)
|
||||||
|
cargo clippy -- -D warnings
|
||||||
|
|
||||||
|
# 代码格式化检查
|
||||||
|
cargo fmt -- --check
|
||||||
|
|
||||||
|
# 自动格式化代码
|
||||||
|
cargo fmt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
```bash
|
||||||
|
# 运行所有测试
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# 运行单个测试 (按名称过滤)
|
||||||
|
cargo test test_function_name
|
||||||
|
|
||||||
|
# 运行特定模块的测试
|
||||||
|
cargo test module_name::
|
||||||
|
|
||||||
|
# 显示测试输出 (包括 println!)
|
||||||
|
cargo test -- --nocapture
|
||||||
|
|
||||||
|
# 运行单个测试并显示输出
|
||||||
|
cargo test test_name -- --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
### 清理
|
||||||
|
```bash
|
||||||
|
# 清理构建产物
|
||||||
|
cargo clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
app-desktop/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.rs # 入口文件 (仅调用 lib::run)
|
||||||
|
│ ├── lib.rs # 核心应用逻辑 (注册插件、命令、状态)
|
||||||
|
│ ├── commands/
|
||||||
|
│ │ └── mod.rs # 原生桌面功能命令 (文件对话框、通知等)
|
||||||
|
│ └── sidecar.rs # Sidecar 进程管理 (启动、端口扫描、清理)
|
||||||
|
├── binaries/ # Sidecar 二进制文件
|
||||||
|
│ └── app-* # Sidecar App 可执行文件 (示例: app)
|
||||||
|
├── capabilities/ # Tauri v2 权限配置
|
||||||
|
│ └── default.json
|
||||||
|
├── icons/ # 应用图标资源
|
||||||
|
├── gen/schemas/ # 自动生成的 Schema (不要手动编辑)
|
||||||
|
├── Cargo.toml # Rust 项目配置
|
||||||
|
├── tauri.conf.json # Tauri 应用配置
|
||||||
|
├── build.rs # Rust 构建脚本
|
||||||
|
└── mise.toml # 开发工具版本管理
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rust 代码风格指南
|
||||||
|
|
||||||
|
### 导入 (Imports)
|
||||||
|
|
||||||
|
- 使用标准库、外部 crate、当前 crate 的顺序,用空行分隔
|
||||||
|
- 按字母顺序排列
|
||||||
|
- 优先使用具体导入而非通配符 `*`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ✅ 推荐
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use tauri::Manager;
|
||||||
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
use tauri_plugin_shell::process::{CommandEvent, CommandChild};
|
||||||
|
|
||||||
|
// ❌ 避免
|
||||||
|
use tauri::*;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命名规范
|
||||||
|
|
||||||
|
- **函数和变量**: `snake_case`
|
||||||
|
- **类型、结构体、枚举、Trait**: `PascalCase`
|
||||||
|
- **常量和静态变量**: `SCREAMING_SNAKE_CASE`
|
||||||
|
- **生命周期参数**: 简短小写字母,如 `'a`, `'b`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ✅ 推荐
|
||||||
|
struct SidecarProcess(Mutex<Option<CommandChild>>);
|
||||||
|
const DEFAULT_PORT: u16 = 3000;
|
||||||
|
async fn find_available_port(start: u16) -> u16 { }
|
||||||
|
|
||||||
|
// ❌ 避免
|
||||||
|
struct sidecar_process { }
|
||||||
|
const defaultPort: u16 = 3000;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 类型注解
|
||||||
|
|
||||||
|
- 函数参数必须有类型注解
|
||||||
|
- 函数返回值必须明确声明 (除非返回 `()`)
|
||||||
|
- 优先使用具体类型而非 `impl Trait` (除非必要)
|
||||||
|
- 使用 `&str` 而非 `String` 作为只读字符串参数
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ✅ 推荐
|
||||||
|
#[tauri::command]
|
||||||
|
fn greet(name: &str) -> String {
|
||||||
|
format!("Hello, {}!", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_port_available(port: u16) -> bool {
|
||||||
|
tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port))
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
|
||||||
|
- 使用 `Result<T, E>` 返回可能失败的操作
|
||||||
|
- 使用 `expect()` 时提供有意义的错误消息 (中文)
|
||||||
|
- 避免 `unwrap()` 在生产代码中,除非逻辑上保证不会 panic
|
||||||
|
- 使用 `?` 操作符传播错误
|
||||||
|
- 记录关键错误信息到控制台
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ✅ 推荐
|
||||||
|
let sidecar = app_handle
|
||||||
|
.shell()
|
||||||
|
.sidecar("app")
|
||||||
|
.expect("无法找到 app sidecar");
|
||||||
|
|
||||||
|
let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败");
|
||||||
|
|
||||||
|
// 日志记录
|
||||||
|
eprintln!("✗ Sidecar App 启动失败");
|
||||||
|
println!("✓ Sidecar App 启动成功!");
|
||||||
|
|
||||||
|
// ❌ 避免
|
||||||
|
let data = read_file().unwrap(); // 无上下文信息
|
||||||
|
```
|
||||||
|
|
||||||
|
### 异步代码
|
||||||
|
|
||||||
|
- 使用 `async/await` 而非手动创建 Future
|
||||||
|
- Tauri 内部使用 `tauri::async_runtime::spawn` 启动异步任务
|
||||||
|
- 使用 Tokio 的异步 API (如 `tokio::net::TcpListener`)
|
||||||
|
- 避免阻塞异步运行时 (使用 `tokio::task::spawn_blocking`)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ✅ 推荐
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let port = find_available_port(3000).await;
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 格式化
|
||||||
|
|
||||||
|
- 使用 `cargo fmt` 自动格式化
|
||||||
|
- 缩进: 4 空格
|
||||||
|
- 行宽: 100 字符 (rustfmt 默认)
|
||||||
|
- 结构体和枚举的字段每行一个 (如果超过一定长度)
|
||||||
|
- 链式调用适当换行提高可读性
|
||||||
|
|
||||||
|
### 注释
|
||||||
|
|
||||||
|
- 使用中文注释说明复杂逻辑
|
||||||
|
- 代码块前添加简短说明注释
|
||||||
|
- 避免显而易见的注释
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ✅ 推荐
|
||||||
|
// 全局状态:存储 Sidecar App 进程句柄
|
||||||
|
struct SidecarProcess(Mutex<Option<CommandChild>>);
|
||||||
|
|
||||||
|
// 检查端口是否可用
|
||||||
|
async fn is_port_available(port: u16) -> bool { }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tauri 特定规范
|
||||||
|
|
||||||
|
### 模块组织
|
||||||
|
|
||||||
|
- **`lib.rs`**: 主入口,负责注册插件、命令、状态管理
|
||||||
|
- **`commands/mod.rs`**: 所有 Tauri 命令集中定义,命令必须是 `pub fn`
|
||||||
|
- **`sidecar.rs`**: Sidecar 进程管理逻辑,导出公共 API(`spawn_sidecar`, `cleanup_sidecar_process`)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// lib.rs - 模块声明
|
||||||
|
mod commands;
|
||||||
|
mod sidecar;
|
||||||
|
|
||||||
|
use sidecar::SidecarProcess;
|
||||||
|
|
||||||
|
// 注册命令时使用模块路径
|
||||||
|
.invoke_handler(tauri::generate_handler![commands::greet])
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命令定义
|
||||||
|
|
||||||
|
- 使用 `#[tauri::command]` 宏标记命令
|
||||||
|
- 命令函数必须是公开的或在 `invoke_handler` 中注册
|
||||||
|
- 参数类型必须实现 `serde::Deserialize`
|
||||||
|
- 返回类型必须实现 `serde::Serialize`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[tauri::command]
|
||||||
|
fn greet(name: &str) -> String {
|
||||||
|
format!("Hello, {}!", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 Builder 中注册
|
||||||
|
.invoke_handler(tauri::generate_handler![greet])
|
||||||
|
```
|
||||||
|
|
||||||
|
### 状态管理
|
||||||
|
|
||||||
|
- 使用 `app.manage()` 注册全局状态
|
||||||
|
- 状态必须实现 `Send + Sync`
|
||||||
|
- 使用 `Mutex` 或 `RwLock` 保证线程安全
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct SidecarProcess(Mutex<Option<CommandChild>>);
|
||||||
|
|
||||||
|
// 注册状态
|
||||||
|
app.manage(SidecarProcess(Mutex::new(None)));
|
||||||
|
|
||||||
|
// 访问状态
|
||||||
|
if let Some(state) = app_handle.try_state::<SidecarProcess>() {
|
||||||
|
*state.0.lock().unwrap() = Some(child);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sidecar 进程管理
|
||||||
|
|
||||||
|
- Sidecar 二进制必须在 `tauri.conf.json` 的 `bundle.externalBin` 中声明
|
||||||
|
- 使用 `app.shell().sidecar()` 启动 sidecar
|
||||||
|
- 在应用退出时清理子进程 (监听 `RunEvent::ExitRequested`)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 启动 sidecar
|
||||||
|
let sidecar = app_handle
|
||||||
|
.shell()
|
||||||
|
.sidecar("app")
|
||||||
|
.expect("无法找到 app sidecar")
|
||||||
|
.env("PORT", port.to_string());
|
||||||
|
|
||||||
|
// 清理进程
|
||||||
|
match event {
|
||||||
|
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => {
|
||||||
|
if let Some(child) = process.take() {
|
||||||
|
let _ = child.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 依赖管理
|
||||||
|
|
||||||
|
- 在 `Cargo.toml` 中明确声明依赖版本
|
||||||
|
- 使用语义化版本 (如 `"2"` 表示兼容 2.x.x)
|
||||||
|
- 仅启用需要的 feature 以减少编译时间和二进制大小
|
||||||
|
|
||||||
|
```toml
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
tokio = { version = "1", features = ["net"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发工具
|
||||||
|
|
||||||
|
推荐安装以下 VSCode 扩展:
|
||||||
|
- `tauri-apps.tauri-vscode` - Tauri 官方支持
|
||||||
|
- `rust-lang.rust-analyzer` - Rust 语言服务器
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **开发环境配置**:
|
||||||
|
- 开发模式下需先启动前端开发服务器(`bun run dev`),再启动 Tauri(`tauri dev`)
|
||||||
|
- 生产构建自动打包 sidecar 二进制,无需额外配置
|
||||||
|
2. **进程生命周期**: 始终在应用退出时清理子进程和资源
|
||||||
|
3. **端口管理**:
|
||||||
|
- 开发模式固定使用 3000 端口(与开发服务器匹配)
|
||||||
|
- 生产模式使用端口扫描避免硬编码端口冲突
|
||||||
|
4. **超时处理**: 异步操作设置合理的超时时间 (如 5 秒)
|
||||||
|
5. **日志**: 使用表情符号 (✓/✗/🔧/🚀) 和中文消息提供清晰的状态反馈
|
||||||
|
6. **错误退出**: 关键错误时调用 `std::process::exit(1)`
|
||||||
|
7. **窗口配置**: 使用 `WebviewWindowBuilder` 动态创建窗口
|
||||||
|
|
||||||
|
## 提交代码前检查清单
|
||||||
|
|
||||||
|
- [ ] `cargo fmt` 格式化通过
|
||||||
|
- [ ] `cargo clippy` 无警告
|
||||||
|
- [ ] `cargo check` 编译通过
|
||||||
|
- [ ] `cargo test` 测试通过
|
||||||
|
- [ ] 更新相关注释和文档
|
||||||
|
- [ ] 检查是否有 `unwrap()` 需要替换为 `expect()`
|
||||||
|
- [ ] 验证 Tauri 应用正常启动和退出
|
||||||
234
apps/desktop/src-tauri/Cargo.lock → src-tauri/Cargo.lock
generated
@@ -43,9 +43,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.100"
|
version = "1.0.101"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "app-desktop"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"tauri",
|
||||||
|
"tauri-build",
|
||||||
|
"tauri-plugin-shell",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atk"
|
name = "atk"
|
||||||
@@ -156,9 +167,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.24.0"
|
version = "1.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
|
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@@ -168,9 +179,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.0"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -229,7 +240,7 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -244,9 +255,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.52"
|
version = "1.2.55"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
|
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -695,15 +706,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.7"
|
version = "0.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.8"
|
version = "1.1.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
@@ -1237,14 +1248,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.19"
|
version = "0.1.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
@@ -1261,9 +1271,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.64"
|
version = "0.1.65"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android_system_properties",
|
"android_system_properties",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
@@ -1285,9 +1295,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ico"
|
name = "ico"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
|
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"png",
|
"png",
|
||||||
@@ -1747,7 +1757,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1795,9 +1805,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
@@ -2267,7 +2277,7 @@ version = "0.11.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"siphasher 1.0.1",
|
"siphasher 1.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2405,9 +2415,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.105"
|
version = "1.0.106"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -2423,9 +2433,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.43"
|
version = "1.0.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -2540,7 +2550,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
"libredox",
|
"libredox",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2565,9 +2575,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.2"
|
version = "1.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -2577,9 +2587,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.13"
|
version = "0.4.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -2588,15 +2598,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.28"
|
version = "0.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2613,7 +2623,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
@@ -2642,12 +2651,6 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ryu"
|
|
||||||
version = "1.0.22"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -2686,9 +2689,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
|
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"ref-cast",
|
"ref-cast",
|
||||||
@@ -2837,18 +2840,6 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_urlencoded"
|
|
||||||
version = "0.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
|
||||||
dependencies = [
|
|
||||||
"form_urlencoded",
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.16.1"
|
version = "3.16.1"
|
||||||
@@ -2861,7 +2852,7 @@ dependencies = [
|
|||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"schemars 0.9.0",
|
"schemars 0.9.0",
|
||||||
"schemars 1.2.0",
|
"schemars 1.2.1",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with_macros",
|
"serde_with_macros",
|
||||||
@@ -2902,17 +2893,6 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "server-desktop"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"tauri",
|
|
||||||
"tauri-build",
|
|
||||||
"tauri-plugin-shell",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servo_arc"
|
name = "servo_arc"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2996,15 +2976,15 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.11"
|
version = "0.4.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
@@ -3014,9 +2994,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.1"
|
version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
@@ -3232,9 +3212,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "2.9.5"
|
version = "2.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033"
|
checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -3271,7 +3251,7 @@ dependencies = [
|
|||||||
"tauri-runtime",
|
"tauri-runtime",
|
||||||
"tauri-runtime-wry",
|
"tauri-runtime-wry",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"url",
|
"url",
|
||||||
@@ -3283,9 +3263,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
name = "tauri-build"
|
||||||
version = "2.5.3"
|
version = "2.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08"
|
checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
@@ -3305,9 +3285,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
name = "tauri-codegen"
|
||||||
version = "2.5.2"
|
version = "2.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf"
|
checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"brotli",
|
"brotli",
|
||||||
@@ -3323,7 +3303,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"time",
|
"time",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -3332,9 +3312,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "2.5.2"
|
version = "2.5.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde"
|
checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -3346,9 +3326,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin"
|
name = "tauri-plugin"
|
||||||
version = "2.5.2"
|
version = "2.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377"
|
checksum = "692a77abd8b8773e107a42ec0e05b767b8d2b7ece76ab36c6c3947e34df9f53f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"glob",
|
"glob",
|
||||||
@@ -3363,9 +3343,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-shell"
|
name = "tauri-plugin-shell"
|
||||||
version = "2.3.4"
|
version = "2.3.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39b76f884a3937e04b631ffdc3be506088fa979369d25147361352f2f352e5ed"
|
checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"log",
|
"log",
|
||||||
@@ -3378,15 +3358,15 @@ dependencies = [
|
|||||||
"shared_child",
|
"shared_child",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-plugin",
|
"tauri-plugin",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.9.2"
|
version = "2.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892"
|
checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cookie",
|
"cookie",
|
||||||
"dpi",
|
"dpi",
|
||||||
@@ -3400,7 +3380,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
@@ -3409,9 +3389,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime-wry"
|
name = "tauri-runtime-wry"
|
||||||
version = "2.9.3"
|
version = "2.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065"
|
checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gtk",
|
"gtk",
|
||||||
"http",
|
"http",
|
||||||
@@ -3436,9 +3416,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "2.8.1"
|
version = "2.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490"
|
checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"brotli",
|
"brotli",
|
||||||
@@ -3464,7 +3444,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"swift-rs",
|
"swift-rs",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"toml 0.9.11+spec-1.1.0",
|
"toml 0.9.11+spec-1.1.0",
|
||||||
"url",
|
"url",
|
||||||
"urlpattern",
|
"urlpattern",
|
||||||
@@ -3505,11 +3485,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.17"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl 2.0.17",
|
"thiserror-impl 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3525,9 +3505,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.17"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3536,9 +3516,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.45"
|
version = "0.3.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
|
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
@@ -3551,15 +3531,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-core"
|
name = "time-core"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.25"
|
version = "0.2.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
|
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
@@ -3780,7 +3760,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3894,9 +3874,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.19.0"
|
version = "1.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -4060,9 +4040,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webkit2gtk"
|
name = "webkit2gtk"
|
||||||
version = "2.0.1"
|
version = "2.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a"
|
checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"cairo-rs",
|
"cairo-rs",
|
||||||
@@ -4084,9 +4064,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webkit2gtk-sys"
|
name = "webkit2gtk-sys"
|
||||||
version = "2.0.1"
|
version = "2.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c"
|
checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"cairo-sys-rs",
|
"cairo-sys-rs",
|
||||||
@@ -4133,7 +4113,7 @@ version = "0.38.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"windows",
|
"windows",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
@@ -4605,9 +4585,9 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wry"
|
name = "wry"
|
||||||
version = "0.53.5"
|
version = "0.54.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2"
|
checksum = "5ed1a195b0375491dd15a7066a10251be217ce743cf4bbbbdcf5391d6473bee0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"block2",
|
"block2",
|
||||||
@@ -4637,7 +4617,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"soup3",
|
"soup3",
|
||||||
"tao-macros",
|
"tao-macros",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.18",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
@@ -4694,18 +4674,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.33"
|
version = "0.8.39"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
|
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.33"
|
version = "0.8.39"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
|
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4768,6 +4748,6 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.14"
|
version = "1.0.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
|
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "server-desktop"
|
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 = "server_desktop_lib"
|
name = "app_desktop_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 974 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 903 B |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -2,5 +2,5 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
server_desktop_lib::run()
|
app_desktop_lib::run()
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ 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("无法找到 app")
|
.expect("无法找到 app")
|
||||||
.env("PORT", port.to_string());
|
.env("PORT", port.to_string());
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "server-desktop",
|
"productName": "app-desktop",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.imbytecat.server-desktop",
|
"identifier": "com.imbytecat.app-desktop",
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": true,
|
"withGlobalTauri": true,
|
||||||
"windows": [],
|
"windows": [],
|
||||||
@@ -20,6 +20,6 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"externalBin": ["binaries/server"]
|
"externalBin": ["binaries/app"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
13
src/db/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||||
|
import * as schema from '@/db/schema'
|
||||||
|
import { env } from '@/env'
|
||||||
|
|
||||||
|
export function createDb() {
|
||||||
|
return drizzle({
|
||||||
|
connection: {
|
||||||
|
url: env.DATABASE_URL,
|
||||||
|
prepare: true,
|
||||||
|
},
|
||||||
|
schema,
|
||||||
|
})
|
||||||
|
}
|
||||||
15
src/db/schema/todo.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { sql } from 'drizzle-orm'
|
||||||
|
import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
export const todoTable = pgTable('todo', {
|
||||||
|
id: uuid('id').primaryKey().default(sql`uuidv7()`),
|
||||||
|
title: text('title').notNull(),
|
||||||
|
completed: boolean('completed').notNull().default(false),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow()
|
||||||
|
.$onUpdateFn(() => new Date()),
|
||||||
|
})
|
||||||
7
src/integrations/tanstack-query/devtools.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { TanStackDevtoolsReactPlugin } from '@tanstack/react-devtools'
|
||||||
|
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
|
||||||
|
|
||||||
|
export const devtools = {
|
||||||
|
name: 'TanStack Query',
|
||||||
|
render: <ReactQueryDevtoolsPanel />,
|
||||||
|
} satisfies TanStackDevtoolsReactPlugin
|
||||||
1
src/integrations/tanstack-query/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './devtools'
|
||||||
7
src/integrations/tanstack-router/devtools.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { TanStackDevtoolsReactPlugin } from '@tanstack/react-devtools'
|
||||||
|
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
|
||||||
|
|
||||||
|
export const devtools = {
|
||||||
|
name: 'TanStack Router',
|
||||||
|
render: <TanStackRouterDevtoolsPanel />,
|
||||||
|
} satisfies TanStackDevtoolsReactPlugin
|
||||||
1
src/integrations/tanstack-router/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './devtools'
|
||||||
0
src/lib/utils.ts
Normal file
53
src/orpc/client.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { createORPCClient } from '@orpc/client'
|
||||||
|
import { RPCLink } from '@orpc/client/fetch'
|
||||||
|
import { createRouterClient } from '@orpc/server'
|
||||||
|
import { createTanstackQueryUtils } from '@orpc/tanstack-query'
|
||||||
|
import { createIsomorphicFn } from '@tanstack/react-start'
|
||||||
|
import { getRequestHeaders } from '@tanstack/react-start/server'
|
||||||
|
import { router } from './router'
|
||||||
|
import type { RouterClient } from './types'
|
||||||
|
|
||||||
|
const getORPCClient = createIsomorphicFn()
|
||||||
|
.server(() =>
|
||||||
|
createRouterClient(router, {
|
||||||
|
context: () => ({
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.client(() => {
|
||||||
|
const link = new RPCLink({
|
||||||
|
url: `${window.location.origin}/api/rpc`,
|
||||||
|
})
|
||||||
|
return createORPCClient<RouterClient>(link)
|
||||||
|
})
|
||||||
|
|
||||||
|
const client: RouterClient = getORPCClient()
|
||||||
|
|
||||||
|
export const orpc = createTanstackQueryUtils(client, {
|
||||||
|
experimental_defaults: {
|
||||||
|
todo: {
|
||||||
|
create: {
|
||||||
|
mutationOptions: {
|
||||||
|
onSuccess: (_, __, ___, ctx) => {
|
||||||
|
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
mutationOptions: {
|
||||||
|
onSuccess: (_, __, ___, ctx) => {
|
||||||
|
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
mutationOptions: {
|
||||||
|
onSuccess: (_, __, ___, ctx) => {
|
||||||
|
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
5
src/orpc/contract.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import * as todo from './contracts/todo'
|
||||||
|
|
||||||
|
export const contract = {
|
||||||
|
todo,
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
createUpdateSchema,
|
createUpdateSchema,
|
||||||
} from 'drizzle-zod'
|
} from 'drizzle-zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { todoTable } from '@/server/db/schema'
|
import { todoTable } from '@/db/schema'
|
||||||
|
|
||||||
const selectSchema = createSelectSchema(todoTable)
|
const selectSchema = createSelectSchema(todoTable)
|
||||||
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
import { ORPCError } from '@orpc/server'
|
import { ORPCError } from '@orpc/server'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { todoTable } from '@/server/db/schema'
|
import { todoTable } from '@/db/schema'
|
||||||
import { db } from '../middlewares'
|
import { baseProcedure } from '@/orpc/procedures'
|
||||||
import { os } from '../server'
|
|
||||||
|
|
||||||
export const list = os.todo.list.use(db).handler(async ({ context }) => {
|
export const list = baseProcedure.todo.list.handler(async ({ context }) => {
|
||||||
const todos = await context.db.query.todoTable.findMany({
|
const todos = await context.db.query.todoTable.findMany({
|
||||||
orderBy: (todos, { desc }) => [desc(todos.createdAt)],
|
orderBy: (todos, { desc }) => [desc(todos.createdAt)],
|
||||||
})
|
})
|
||||||
return todos
|
return todos
|
||||||
})
|
})
|
||||||
|
|
||||||
export const create = os.todo.create
|
export const create = baseProcedure.todo.create.handler(
|
||||||
.use(db)
|
async ({ context, input }) => {
|
||||||
.handler(async ({ context, input }) => {
|
|
||||||
const [newTodo] = await context.db
|
const [newTodo] = await context.db
|
||||||
.insert(todoTable)
|
.insert(todoTable)
|
||||||
.values(input)
|
.values(input)
|
||||||
@@ -24,11 +22,11 @@ export const create = os.todo.create
|
|||||||
}
|
}
|
||||||
|
|
||||||
return newTodo
|
return newTodo
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const update = os.todo.update
|
export const update = baseProcedure.todo.update.handler(
|
||||||
.use(db)
|
async ({ context, input }) => {
|
||||||
.handler(async ({ context, input }) => {
|
|
||||||
const [updatedTodo] = await context.db
|
const [updatedTodo] = await context.db
|
||||||
.update(todoTable)
|
.update(todoTable)
|
||||||
.set(input.data)
|
.set(input.data)
|
||||||
@@ -40,10 +38,11 @@ export const update = os.todo.update
|
|||||||
}
|
}
|
||||||
|
|
||||||
return updatedTodo
|
return updatedTodo
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const remove = os.todo.remove
|
export const remove = baseProcedure.todo.remove.handler(
|
||||||
.use(db)
|
async ({ context, input }) => {
|
||||||
.handler(async ({ context, input }) => {
|
|
||||||
await context.db.delete(todoTable).where(eq(todoTable.id, input.id))
|
await context.db.delete(todoTable).where(eq(todoTable.id, input.id))
|
||||||
})
|
},
|
||||||
|
)
|
||||||
2
src/orpc/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { orpc } from './client'
|
||||||
|
export * from './types'
|
||||||
17
src/orpc/middlewares/db.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createDb } from '@/db'
|
||||||
|
import { os } from '@/orpc/server'
|
||||||
|
|
||||||
|
export type Database = ReturnType<typeof createDb>
|
||||||
|
|
||||||
|
const globalForDb = globalThis as { __db?: Database }
|
||||||
|
|
||||||
|
function getDb(): Database {
|
||||||
|
globalForDb.__db ??= createDb()
|
||||||
|
return globalForDb.__db
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dbProvider = os.middleware(async ({ context, next }) => {
|
||||||
|
return next({
|
||||||
|
context: { ...context, db: getDb() },
|
||||||
|
})
|
||||||
|
})
|
||||||
1
src/orpc/middlewares/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './db'
|
||||||
4
src/orpc/procedures.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { dbProvider } from './middlewares/db'
|
||||||
|
import { os } from './server'
|
||||||
|
|
||||||
|
export const baseProcedure = os.use(dbProvider)
|
||||||
6
src/orpc/router.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import * as todo from './handlers/todo'
|
||||||
|
import { os } from './server'
|
||||||
|
|
||||||
|
export const router = os.router({
|
||||||
|
todo,
|
||||||
|
})
|
||||||
8
src/orpc/server.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { implement } from '@orpc/server'
|
||||||
|
import { contract } from './contract'
|
||||||
|
|
||||||
|
export type ORPCContext = {
|
||||||
|
headers?: Headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export const os = implement(contract).$context<ORPCContext>()
|
||||||
@@ -3,8 +3,9 @@ import type {
|
|||||||
InferContractRouterInputs,
|
InferContractRouterInputs,
|
||||||
InferContractRouterOutputs,
|
InferContractRouterOutputs,
|
||||||
} from '@orpc/contract'
|
} from '@orpc/contract'
|
||||||
import type { Contract } from './contracts'
|
import type { contract } from './contract'
|
||||||
|
|
||||||
|
export type Contract = typeof contract
|
||||||
export type RouterClient = ContractRouterClient<Contract>
|
export type RouterClient = ContractRouterClient<Contract>
|
||||||
export type RouterInputs = InferContractRouterInputs<Contract>
|
export type RouterInputs = InferContractRouterInputs<Contract>
|
||||||
export type RouterOutputs = InferContractRouterOutputs<Contract>
|
export type RouterOutputs = InferContractRouterOutputs<Contract>
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
|
|
||||||
import { Route as rootRouteImport } from './routes/__root'
|
import { Route as rootRouteImport } from './routes/__root'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as ApiSplatRouteImport } from './routes/api/$'
|
|
||||||
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$'
|
import { Route as ApiRpcSplatRouteImport } from './routes/api/rpc.$'
|
||||||
|
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
@@ -18,11 +17,6 @@ const IndexRoute = IndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const ApiSplatRoute = ApiSplatRouteImport.update({
|
|
||||||
id: '/api/$',
|
|
||||||
path: '/api/$',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
|
const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
|
||||||
id: '/api/rpc/$',
|
id: '/api/rpc/$',
|
||||||
path: '/api/rpc/$',
|
path: '/api/rpc/$',
|
||||||
@@ -31,31 +25,27 @@ const ApiRpcSplatRoute = ApiRpcSplatRouteImport.update({
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
|
||||||
'/api/rpc/$': typeof ApiRpcSplatRoute
|
'/api/rpc/$': typeof ApiRpcSplatRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
|
||||||
'/api/rpc/$': typeof ApiRpcSplatRoute
|
'/api/rpc/$': typeof ApiRpcSplatRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/api/$': typeof ApiSplatRoute
|
|
||||||
'/api/rpc/$': typeof ApiRpcSplatRoute
|
'/api/rpc/$': typeof ApiRpcSplatRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/api/$' | '/api/rpc/$'
|
fullPaths: '/' | '/api/rpc/$'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/api/$' | '/api/rpc/$'
|
to: '/' | '/api/rpc/$'
|
||||||
id: '__root__' | '/' | '/api/$' | '/api/rpc/$'
|
id: '__root__' | '/' | '/api/rpc/$'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
ApiSplatRoute: typeof ApiSplatRoute
|
|
||||||
ApiRpcSplatRoute: typeof ApiRpcSplatRoute
|
ApiRpcSplatRoute: typeof ApiRpcSplatRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,13 +58,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/api/$': {
|
|
||||||
id: '/api/$'
|
|
||||||
path: '/api/$'
|
|
||||||
fullPath: '/api/$'
|
|
||||||
preLoaderRoute: typeof ApiSplatRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/api/rpc/$': {
|
'/api/rpc/$': {
|
||||||
id: '/api/rpc/$'
|
id: '/api/rpc/$'
|
||||||
path: '/api/rpc/$'
|
path: '/api/rpc/$'
|
||||||
@@ -87,7 +70,6 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
ApiSplatRoute: ApiSplatRoute,
|
|
||||||
ApiRpcSplatRoute: ApiRpcSplatRoute,
|
ApiRpcSplatRoute: ApiRpcSplatRoute,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { TanStackDevtools } from '@tanstack/react-devtools'
|
import { TanStackDevtools } from '@tanstack/react-devtools'
|
||||||
import type { QueryClient } from '@tanstack/react-query'
|
import type { QueryClient } from '@tanstack/react-query'
|
||||||
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
|
|
||||||
import {
|
import {
|
||||||
createRootRouteWithContext,
|
createRootRouteWithContext,
|
||||||
HeadContent,
|
HeadContent,
|
||||||
Scripts,
|
Scripts,
|
||||||
} from '@tanstack/react-router'
|
} from '@tanstack/react-router'
|
||||||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
|
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { ErrorComponent } from '@/components/Error'
|
import { ErrorComponent } from '@/components/Error'
|
||||||
import { NotFoundComponent } from '@/components/NotFount'
|
import { NotFoundComponent } from '@/components/NotFound'
|
||||||
|
import { devtools as queryDevtools } from '@/integrations/tanstack-query'
|
||||||
|
import { devtools as routerDevtools } from '@/integrations/tanstack-router'
|
||||||
import appCss from '@/styles.css?url'
|
import appCss from '@/styles.css?url'
|
||||||
|
|
||||||
export interface RouterContext {
|
export interface RouterContext {
|
||||||
@@ -27,7 +27,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|||||||
content: 'width=device-width, initial-scale=1',
|
content: 'width=device-width, initial-scale=1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Furtherverse',
|
title: 'Fullstack Starter',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
@@ -54,16 +54,7 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
|||||||
config={{
|
config={{
|
||||||
position: 'bottom-right',
|
position: 'bottom-right',
|
||||||
}}
|
}}
|
||||||
plugins={[
|
plugins={[routerDevtools, queryDevtools]}
|
||||||
{
|
|
||||||
name: 'TanStack Router',
|
|
||||||
render: <TanStackRouterDevtoolsPanel />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'TanStack Query',
|
|
||||||
render: <ReactQueryDevtoolsPanel />,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
@@ -2,7 +2,7 @@ import { ORPCError, onError, ValidationError } from '@orpc/server'
|
|||||||
import { RPCHandler } from '@orpc/server/fetch'
|
import { RPCHandler } from '@orpc/server/fetch'
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { router } from '@/server/api/routers'
|
import { router } from '@/orpc/router'
|
||||||
|
|
||||||
const handler = new RPCHandler(router, {
|
const handler = new RPCHandler(router, {
|
||||||
interceptors: [
|
interceptors: [
|
||||||
@@ -49,9 +49,7 @@ export const Route = createFileRoute('/api/rpc/$')({
|
|||||||
ANY: async ({ request }) => {
|
ANY: async ({ request }) => {
|
||||||
const { response } = await handler.handle(request, {
|
const { response } = await handler.handle(request, {
|
||||||
prefix: '/api/rpc',
|
prefix: '/api/rpc',
|
||||||
context: {
|
context: {},
|
||||||
headers: request.headers,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return response ?? new Response('Not Found', { status: 404 })
|
return response ?? new Response('Not Found', { status: 404 })
|
||||||
@@ -4,7 +4,7 @@ import { isTauri } from '@tauri-apps/api/core'
|
|||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import type { ChangeEventHandler, FormEventHandler } from 'react'
|
import type { ChangeEventHandler, FormEventHandler } from 'react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { orpc } from '@/client/query-client'
|
import { orpc } from '@/orpc'
|
||||||
|
|
||||||
export const Route = createFileRoute('/')({
|
export const Route = createFileRoute('/')({
|
||||||
component: Todos,
|
component: Todos,
|
||||||
34
tsconfig.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
turbo.json
@@ -1,31 +1,31 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/turbo/schema.json",
|
"$schema": "./node_modules/turbo/schema.json",
|
||||||
"dangerouslyDisablePackageManagerCheck": true,
|
"dangerouslyDisablePackageManagerCheck": true,
|
||||||
"globalDependencies": [
|
|
||||||
"package.json",
|
|
||||||
"bun.lock",
|
|
||||||
"turbo.json",
|
|
||||||
"biome.json"
|
|
||||||
],
|
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": {
|
"build:compile": {
|
||||||
"outputs": ["dist/**", ".output/**"]
|
"dependsOn": ["build:vite"],
|
||||||
|
"outputs": ["out/**", "src-tauri/binaries/**"]
|
||||||
},
|
},
|
||||||
"compile": {
|
"build:tauri": {
|
||||||
"dependsOn": ["build"],
|
"dependsOn": ["build:compile"],
|
||||||
"outputs": ["out/**"]
|
"outputs": ["src-tauri/target/release/bundle/**"]
|
||||||
|
},
|
||||||
|
"build:vite": {
|
||||||
|
"outputs": [".output/**"]
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true
|
"persistent": true
|
||||||
},
|
},
|
||||||
"fix": {
|
"dev:tauri": {
|
||||||
"outputs": []
|
"cache": false,
|
||||||
|
"dependsOn": ["build:compile"],
|
||||||
|
"persistent": true,
|
||||||
|
"with": ["dev:vite"]
|
||||||
},
|
},
|
||||||
"typecheck": {
|
"dev:vite": {
|
||||||
"dependsOn": ["^typecheck"],
|
"cache": false,
|
||||||
"inputs": ["tsconfig.json", "tsconfig.*.json", "**/*.{ts,tsx}"],
|
"persistent": true
|
||||||
"outputs": []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ui": "tui"
|
"ui": "tui"
|
||||||
|
|||||||