forked from imbytecat/fullstack-starter
refactor: flatten monorepo into standalone project
This commit is contained in:
@@ -9,9 +9,6 @@
|
|||||||
# Bun build
|
# Bun build
|
||||||
*.bun-build
|
*.bun-build
|
||||||
|
|
||||||
# Turborepo
|
|
||||||
.turbo/
|
|
||||||
|
|
||||||
### Node ###
|
### Node ###
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
|
|||||||
@@ -1,81 +1,52 @@
|
|||||||
# AGENTS.md - AI Coding Agent Guidelines
|
# AGENTS.md - AI Coding Agent Guidelines
|
||||||
|
|
||||||
Guidelines for AI agents working in this Bun monorepo.
|
Guidelines for AI agents working in this project.
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
> **This project uses [Bun](https://bun.sh) exclusively as both the JavaScript runtime and package manager. Do NOT use Node.js / npm / yarn / pnpm. All commands start with `bun` — use `bun install` for dependencies and `bun run <script>` for scripts. Always prefer `bun run <script>` over `bun <script>` to avoid conflicts with Bun built-in subcommands (e.g. `bun build` invokes Bun's bundler, NOT your package.json script). Never use `npm`, `npx`, or `node`.**
|
> **This project uses [Bun](https://bun.sh) exclusively as both the JavaScript runtime and package manager. Do NOT use Node.js / npm / yarn / pnpm. All commands start with `bun` — use `bun install` for dependencies and `bun run <script>` for scripts. Always prefer `bun run <script>` over `bun <script>` to avoid conflicts with Bun built-in subcommands (e.g. `bun build` invokes Bun's bundler, NOT your package.json script). Never use `npm`, `npx`, or `node`.**
|
||||||
|
|
||||||
- **Monorepo**: Bun workspaces + Turborepo orchestration
|
- **Framework**: TanStack Start (React 19 SSR, file-based routing)
|
||||||
- **Runtime**: Bun (see `mise.toml` for version) — **NOT Node.js**
|
- **Runtime**: Bun (see `mise.toml` for version) — **NOT Node.js**
|
||||||
- **Package Manager**: Bun — **NOT npm / yarn / pnpm**
|
- **Package Manager**: Bun — **NOT npm / yarn / pnpm**
|
||||||
- **Apps**:
|
- **Language**: TypeScript (strict mode)
|
||||||
- `apps/server` - TanStack Start fullstack web app (see `apps/server/AGENTS.md`)
|
- **Styling**: Tailwind CSS v4
|
||||||
- `apps/desktop` - Electron desktop shell, sidecar server pattern (see `apps/desktop/AGENTS.md`)
|
- **Database**: PostgreSQL + Drizzle ORM v1 beta (`drizzle-orm/postgres-js`, RQBv2)
|
||||||
- **Packages**: `packages/tsconfig` (shared TS configs)
|
- **State**: TanStack Query v5
|
||||||
|
- **RPC**: ORPC (contract-first, type-safe)
|
||||||
|
- **Build**: Vite + Nitro
|
||||||
|
|
||||||
## Build / Lint / Test Commands
|
## Build / Lint / Test Commands
|
||||||
|
|
||||||
### Root Commands (via Turbo)
|
|
||||||
```bash
|
```bash
|
||||||
bun run dev # Start all apps in dev mode
|
# Development
|
||||||
bun run build # Build all apps
|
bun run dev # Vite dev server (localhost:3000)
|
||||||
bun run compile # Compile server to standalone binary (current platform)
|
bun run db:studio # Drizzle Studio GUI
|
||||||
bun run compile:darwin # Compile server for macOS (arm64 + x64)
|
|
||||||
bun run compile:linux # Compile server for Linux (x64 + arm64)
|
|
||||||
bun run compile:windows # Compile server for Windows x64
|
|
||||||
bun run dist # Package desktop distributable (current platform)
|
|
||||||
bun run dist:linux # Package desktop for Linux (x64 + arm64)
|
|
||||||
bun run dist:mac # Package desktop for macOS (arm64 + x64)
|
|
||||||
bun run dist:win # Package desktop for Windows x64
|
|
||||||
bun run fix # Lint + format (Biome auto-fix)
|
|
||||||
bun run typecheck # TypeScript check across monorepo
|
|
||||||
```
|
|
||||||
|
|
||||||
### Server App (`apps/server`)
|
# Build
|
||||||
```bash
|
bun run build # Production build → .output/
|
||||||
bun run dev # Vite dev server (localhost:3000)
|
bun run compile # Compile to standalone binary (current platform, depends on build)
|
||||||
bun run build # Production build -> .output/
|
bun run compile:darwin # Compile for macOS (arm64 + x64)
|
||||||
bun run compile # Compile to standalone binary (current platform)
|
bun run compile:darwin:arm64 # Compile for macOS arm64
|
||||||
bun run compile:darwin # Compile for macOS (arm64 + x64)
|
bun run compile:darwin:x64 # Compile for macOS x64
|
||||||
bun run compile:darwin:arm64 # Compile for macOS arm64
|
bun run compile:linux # Compile for Linux (x64 + arm64)
|
||||||
bun run compile:darwin:x64 # Compile for macOS x64
|
bun run compile:linux:arm64 # Compile for Linux arm64
|
||||||
bun run compile:linux # Compile for Linux (x64 + arm64)
|
bun run compile:linux:x64 # Compile for Linux x64
|
||||||
bun run compile:linux:arm64 # Compile for Linux arm64
|
bun run compile:windows # Compile for Windows (default: x64)
|
||||||
bun run compile:linux:x64 # Compile for Linux x64
|
bun run compile:windows:x64 # Compile for Windows x64
|
||||||
bun run compile:windows # Compile for Windows (default: x64)
|
|
||||||
bun run compile:windows:x64 # Compile for Windows x64
|
# Code Quality
|
||||||
bun run fix # Biome auto-fix
|
bun run fix # Biome auto-fix
|
||||||
bun run typecheck # TypeScript check
|
bun run typecheck # TypeScript check
|
||||||
|
|
||||||
# Database (Drizzle)
|
# Database (Drizzle)
|
||||||
bun run db:generate # Generate migrations from schema
|
bun run db:generate # Generate migrations from schema
|
||||||
bun run db:migrate # Run migrations
|
bun run db:migrate # Run migrations
|
||||||
bun run db:push # Push schema (dev only)
|
bun run db:push # Push schema directly (dev only)
|
||||||
bun run db:studio # Open Drizzle Studio
|
|
||||||
```
|
|
||||||
|
|
||||||
### Desktop App (`apps/desktop`)
|
# Testing (not yet configured)
|
||||||
```bash
|
bun test path/to/test.ts # Run single test
|
||||||
bun run dev # electron-vite dev mode (requires server dev running)
|
bun test -t "pattern" # Run tests matching pattern
|
||||||
bun run build # electron-vite build (main + preload)
|
|
||||||
bun run dist # Build + package for current platform
|
|
||||||
bun run dist:linux # Build + package for Linux (x64 + arm64)
|
|
||||||
bun run dist:linux:x64 # Build + package for Linux x64
|
|
||||||
bun run dist:linux:arm64 # Build + package for Linux arm64
|
|
||||||
bun run dist:mac # Build + package for macOS (arm64 + x64)
|
|
||||||
bun run dist:mac:arm64 # Build + package for macOS arm64
|
|
||||||
bun run dist:mac:x64 # Build + package for macOS x64
|
|
||||||
bun run dist:win # Build + package for Windows x64
|
|
||||||
bun run fix # Biome auto-fix
|
|
||||||
bun run typecheck # TypeScript check
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Style (TypeScript)
|
## Code Style (TypeScript)
|
||||||
@@ -119,6 +90,54 @@ import type { ReactNode } from 'react'
|
|||||||
- ORPC: Use `ORPCError` with proper codes (`NOT_FOUND`, `INPUT_VALIDATION_FAILED`)
|
- ORPC: Use `ORPCError` with proper codes (`NOT_FOUND`, `INPUT_VALIDATION_FAILED`)
|
||||||
- Never use empty catch blocks
|
- Never use empty catch blocks
|
||||||
|
|
||||||
|
## ORPC Pattern
|
||||||
|
|
||||||
|
### 1. Define Contract (`src/server/api/contracts/feature.contract.ts`)
|
||||||
|
```typescript
|
||||||
|
import { oc } from '@orpc/contract'
|
||||||
|
import { createSelectSchema } from 'drizzle-orm/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({
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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'
|
||||||
|
|
||||||
|
const { data } = useSuspenseQuery(orpc.feature.list.queryOptions())
|
||||||
|
const mutation = useMutation(orpc.feature.create.mutationOptions())
|
||||||
|
```
|
||||||
|
|
||||||
## Database (Drizzle ORM v1 beta + postgres-js)
|
## Database (Drizzle ORM v1 beta + postgres-js)
|
||||||
|
|
||||||
- **ORM**: Drizzle ORM `1.0.0-beta` (RQBv2)
|
- **ORM**: Drizzle ORM `1.0.0-beta` (RQBv2)
|
||||||
@@ -127,7 +146,11 @@ import type { ReactNode } from 'react'
|
|||||||
- **Relations**: Defined via `defineRelations()` in `src/server/db/relations.ts` (contains schema info, so `drizzle()` only needs `{ relations }`)
|
- **Relations**: Defined via `defineRelations()` in `src/server/db/relations.ts` (contains schema info, so `drizzle()` only needs `{ relations }`)
|
||||||
- **Query style**: RQBv2 object syntax (`orderBy: { createdAt: 'desc' }`, `where: { id: 1 }`)
|
- **Query style**: RQBv2 object syntax (`orderBy: { createdAt: 'desc' }`, `where: { id: 1 }`)
|
||||||
|
|
||||||
|
### Schema Definition
|
||||||
```typescript
|
```typescript
|
||||||
|
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||||
|
import { sql } from 'drizzle-orm'
|
||||||
|
|
||||||
export const myTable = pgTable('my_table', {
|
export const myTable = pgTable('my_table', {
|
||||||
id: uuid().primaryKey().default(sql`uuidv7()`),
|
id: uuid().primaryKey().default(sql`uuidv7()`),
|
||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
@@ -136,17 +159,64 @@ export const myTable = pgTable('my_table', {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Relations (RQBv2)
|
||||||
|
```typescript
|
||||||
|
// src/server/db/relations.ts
|
||||||
|
import { defineRelations } from 'drizzle-orm'
|
||||||
|
import * as schema from './schema'
|
||||||
|
|
||||||
|
export const relations = defineRelations(schema, (r) => ({
|
||||||
|
// Define relations here using r.one / r.many / r.through
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
### DB Instance
|
||||||
|
```typescript
|
||||||
|
// src/server/db/index.ts
|
||||||
|
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||||
|
import { relations } from '@/server/db/relations'
|
||||||
|
// In RQBv2, relations already contain schema info — no separate schema import needed
|
||||||
|
|
||||||
|
const db = drizzle({
|
||||||
|
connection: env.DATABASE_URL,
|
||||||
|
relations,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### RQBv2 Query Examples
|
||||||
|
```typescript
|
||||||
|
// Object-style orderBy (NOT callback style)
|
||||||
|
const todos = await db.query.todoTable.findMany({
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Object-style where
|
||||||
|
const todo = await db.query.todoTable.findFirst({
|
||||||
|
where: { id: someId },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
- Use `@t3-oss/env-core` with Zod validation in `src/env.ts`
|
- Use `@t3-oss/env-core` with Zod validation in `src/env.ts`
|
||||||
- Server vars: no prefix | Client vars: `VITE_` prefix required
|
- Server vars: no prefix | Client vars: `VITE_` prefix required
|
||||||
- Never commit `.env` files
|
- Never commit `.env` files
|
||||||
|
|
||||||
## Dependency Management
|
```typescript
|
||||||
|
// src/env.ts
|
||||||
|
import { createEnv } from '@t3-oss/env-core'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
- All versions centralized in root `package.json` `catalog` field
|
export const env = createEnv({
|
||||||
- Workspace packages use `"catalog:"` — never hardcode versions
|
server: {
|
||||||
- Internal packages use `"workspace:*"` references
|
DATABASE_URL: z.string().url(),
|
||||||
|
},
|
||||||
|
clientPrefix: 'VITE_',
|
||||||
|
client: {
|
||||||
|
VITE_API_URL: z.string().optional(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
## Development Principles
|
## Development Principles
|
||||||
|
|
||||||
@@ -162,7 +232,9 @@ export const myTable = pgTable('my_table', {
|
|||||||
- Run `bun run fix` before committing
|
- Run `bun run fix` before committing
|
||||||
- Use `@/*` path aliases (not relative imports)
|
- Use `@/*` path aliases (not relative imports)
|
||||||
- Include `createdAt`/`updatedAt` on all tables
|
- Include `createdAt`/`updatedAt` on all tables
|
||||||
- Use `catalog:` for dependency versions
|
- Use `ORPCError` with proper codes
|
||||||
|
- Use `drizzle-orm/zod` (NOT `drizzle-zod`) for schema validation
|
||||||
|
- Use RQBv2 object syntax for `orderBy` and `where`
|
||||||
- Update `AGENTS.md` and other docs whenever code patterns change
|
- Update `AGENTS.md` and other docs whenever code patterns change
|
||||||
|
|
||||||
**DON'T:**
|
**DON'T:**
|
||||||
@@ -171,7 +243,11 @@ export const myTable = pgTable('my_table', {
|
|||||||
- Use `as any`, `@ts-ignore`, `@ts-expect-error`
|
- Use `as any`, `@ts-ignore`, `@ts-expect-error`
|
||||||
- Commit `.env` files
|
- Commit `.env` files
|
||||||
- Use empty catch blocks `catch(e) {}`
|
- Use empty catch blocks `catch(e) {}`
|
||||||
- Hardcode dependency versions in workspace packages
|
- Import from `drizzle-zod` (use `drizzle-orm/zod` instead)
|
||||||
|
- Use RQBv1 callback-style `orderBy` / old `relations()` API
|
||||||
|
- Use `drizzle-orm/bun-sql` driver (use `drizzle-orm/postgres-js`)
|
||||||
|
- Pass `schema` to `drizzle()` constructor (only `relations` is needed in RQBv2)
|
||||||
|
- Import `os` from `@orpc/server` in middleware — use `@/server/api/server` (the local typed instance)
|
||||||
- Leave docs out of sync with code changes
|
- Leave docs out of sync with code changes
|
||||||
|
|
||||||
## Git Workflow
|
## Git Workflow
|
||||||
@@ -186,33 +262,39 @@ export const myTable = pgTable('my_table', {
|
|||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── apps/
|
├── src/
|
||||||
│ ├── server/ # TanStack Start fullstack app
|
│ ├── client/ # Client-side code
|
||||||
│ │ ├── src/
|
│ │ └── orpc.ts # ORPC client + TanStack Query utils (single entry point)
|
||||||
│ │ │ ├── client/ # ORPC client + TanStack Query utils
|
│ ├── components/ # React components
|
||||||
│ │ │ ├── components/
|
│ ├── routes/ # TanStack Router file routes
|
||||||
│ │ │ ├── routes/ # File-based routing
|
│ │ ├── __root.tsx # Root layout
|
||||||
│ │ │ └── server/ # API layer + database
|
│ │ ├── index.tsx # Home page
|
||||||
│ │ │ ├── api/ # ORPC contracts, routers, middlewares
|
│ │ └── api/
|
||||||
│ │ │ └── db/ # Drizzle schema
|
│ │ ├── $.ts # OpenAPI handler + Scalar docs
|
||||||
│ │ └── AGENTS.md
|
│ │ ├── health.ts # Health check endpoint
|
||||||
│ └── desktop/ # Electron desktop shell
|
│ │ └── rpc.$.ts # ORPC RPC handler
|
||||||
│ ├── src/
|
│ ├── server/ # Server-side code
|
||||||
│ │ ├── main/
|
│ │ ├── api/ # ORPC layer
|
||||||
│ │ │ └── index.ts # Main process entry
|
│ │ │ ├── contracts/ # Input/output schemas (Zod)
|
||||||
│ │ └── preload/
|
│ │ │ ├── middlewares/ # Middleware (db provider, auth)
|
||||||
│ │ └── index.ts # Preload script
|
│ │ │ ├── routers/ # Handler implementations
|
||||||
│ ├── electron.vite.config.ts
|
│ │ │ ├── interceptors.ts # Shared error interceptors
|
||||||
│ ├── electron-builder.yml # Packaging config
|
│ │ │ ├── context.ts # Request context
|
||||||
│ └── AGENTS.md
|
│ │ │ ├── server.ts # ORPC server instance
|
||||||
├── packages/
|
│ │ │ └── types.ts # Type exports
|
||||||
│ └── tsconfig/ # Shared TS configs
|
│ │ └── db/
|
||||||
├── biome.json # Linting/formatting config
|
│ │ ├── schema/ # Drizzle table definitions
|
||||||
├── turbo.json # Turbo task orchestration
|
│ │ ├── fields.ts # Shared field builders (id, createdAt, updatedAt)
|
||||||
└── package.json # Workspace root + dependency catalog
|
│ │ ├── relations.ts # Drizzle relations (defineRelations, RQBv2)
|
||||||
|
│ │ └── index.ts # Database instance (postgres-js driver)
|
||||||
|
│ ├── env.ts # Environment variable validation
|
||||||
|
│ ├── router.tsx # Router configuration
|
||||||
|
│ ├── routeTree.gen.ts # Auto-generated (DO NOT EDIT)
|
||||||
|
│ └── styles.css # Tailwind entry
|
||||||
|
├── biome.json # Linting/formatting config
|
||||||
|
├── compile.ts # Cross-platform binary compilation script
|
||||||
|
├── drizzle.config.ts # Drizzle Kit config
|
||||||
|
├── vite.config.ts # Vite + TanStack Start + Nitro config
|
||||||
|
├── tsconfig.json # TypeScript config
|
||||||
|
└── package.json # Dependencies and scripts
|
||||||
```
|
```
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- `apps/server/AGENTS.md` - Detailed TanStack Start / ORPC patterns
|
|
||||||
- `apps/desktop/AGENTS.md` - Electron desktop development guide
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# electron-vite build output
|
|
||||||
out/
|
|
||||||
dist/
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# AGENTS.md - Desktop App Guidelines
|
|
||||||
|
|
||||||
Thin Electron shell hosting the fullstack server app.
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
> **⚠️ This project uses Bun as the package manager. Runtime is Electron (Node.js). Always use `bun run <script>` (not `bun <script>`) to avoid conflicts with Bun built-in subcommands. Never use `npm`, `npx`, `yarn`, or `pnpm`.**
|
|
||||||
|
|
||||||
- **Type**: Electron desktop shell
|
|
||||||
- **Design**: Server-driven desktop (thin native window hosting web app)
|
|
||||||
- **Runtime**: Electron (Main/Renderer) + Sidecar server binary (Bun-compiled)
|
|
||||||
- **Build Tool**: electron-vite (Vite-based, handles main + preload builds)
|
|
||||||
- **Packager**: electron-builder (installers, signing, auto-update)
|
|
||||||
- **Orchestration**: Turborepo
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
- **Server-driven design**: The desktop app is a "thin" native shell. It does not contain UI or business logic; it opens a BrowserWindow pointing to the `apps/server` TanStack Start application.
|
|
||||||
- **Dev mode**: Opens a BrowserWindow pointing to `localhost:3000`. Requires `apps/server` to be running separately (Turbo handles this).
|
|
||||||
- **Production mode**: Spawns a compiled server binary (from `resources/`) as a sidecar process, waits for readiness, then loads its URL.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run dev # electron-vite dev (requires server dev running)
|
|
||||||
bun run build # electron-vite build (main + preload)
|
|
||||||
bun run dist # Build + package for current platform
|
|
||||||
bun run dist:linux # Build + package for Linux (x64 + arm64)
|
|
||||||
bun run dist:linux:x64 # Build + package for Linux x64
|
|
||||||
bun run dist:linux:arm64 # Build + package for Linux arm64
|
|
||||||
bun run dist:mac # Build + package for macOS (arm64 + x64)
|
|
||||||
bun run dist:mac:arm64 # Build + package for macOS arm64
|
|
||||||
bun run dist:mac:x64 # Build + package for macOS x64
|
|
||||||
bun run dist:win # Build + package for Windows x64
|
|
||||||
bun run fix # Biome auto-fix
|
|
||||||
bun run typecheck # TypeScript check
|
|
||||||
```
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── src/
|
|
||||||
│ ├── main/
|
|
||||||
│ │ └── index.ts # Main process (server lifecycle + BrowserWindow)
|
|
||||||
│ └── preload/
|
|
||||||
│ └── index.ts # Preload script (security isolation)
|
|
||||||
├── resources/ # Sidecar binaries (gitignored, copied from server build)
|
|
||||||
├── out/ # electron-vite build output (gitignored)
|
|
||||||
├── electron.vite.config.ts
|
|
||||||
├── electron-builder.yml # Packaging configuration
|
|
||||||
├── package.json
|
|
||||||
├── turbo.json
|
|
||||||
└── AGENTS.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
1. **Start server**: `bun run dev` in `apps/server` (or use root `bun run dev` via Turbo).
|
|
||||||
2. **Start desktop**: `bun run dev` in `apps/desktop`.
|
|
||||||
3. **Connection**: Main process polls `localhost:3000` until responsive, then opens BrowserWindow.
|
|
||||||
|
|
||||||
## Production Build Workflow
|
|
||||||
|
|
||||||
From monorepo root, run `bun run dist` to execute the full pipeline automatically (via Turbo task dependencies):
|
|
||||||
|
|
||||||
1. **Build server**: `apps/server` → `vite build` → `.output/`
|
|
||||||
2. **Compile server**: `apps/server` → `bun compile.ts --target ...` → `out/server-{os}-{arch}`
|
|
||||||
3. **Package desktop**: `apps/desktop` → `electron-vite build` + `electron-builder` → distributable
|
|
||||||
|
|
||||||
The `electron-builder.yml` `extraResources` config reads binaries directly from `../server/out/`, no manual copy needed.
|
|
||||||
|
|
||||||
To build for a specific platform explicitly, use `bun run dist:linux` / `bun run dist:mac` / `bun run dist:win` in `apps/desktop`.
|
|
||||||
For single-arch output, use `bun run dist:linux:x64`, `bun run dist:linux:arm64`, `bun run dist:mac:x64`, or `bun run dist:mac:arm64`.
|
|
||||||
|
|
||||||
## Development Principles
|
|
||||||
|
|
||||||
> **These principles apply to ALL code changes. Agents MUST follow them on every task.**
|
|
||||||
|
|
||||||
1. **No backward compatibility** — This project is in rapid iteration. Always use the latest API and patterns. Never keep deprecated code paths or old API fallbacks.
|
|
||||||
2. **Always sync documentation** — When code changes, immediately update all related documentation (`AGENTS.md`, `README.md`, inline code examples). Code and docs must never drift apart.
|
|
||||||
3. **Forward-only migration** — When upgrading dependencies, fully adopt the new API. Don't mix old and new patterns.
|
|
||||||
|
|
||||||
## Critical Rules
|
|
||||||
|
|
||||||
**DO:**
|
|
||||||
- Use arrow functions for all utility functions.
|
|
||||||
- Keep the desktop app as a thin shell — no UI or business logic.
|
|
||||||
- Use `catalog:` for all dependency versions in `package.json`.
|
|
||||||
|
|
||||||
**DON'T:**
|
|
||||||
- Use `npm`, `npx`, `yarn`, or `pnpm`. Use `bun` for package management.
|
|
||||||
- Include UI components or business logic in the desktop app.
|
|
||||||
- Use `as any` or `@ts-ignore`.
|
|
||||||
- Leave docs out of sync with code changes.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../node_modules/@biomejs/biome/configuration_schema.json",
|
|
||||||
"extends": "//",
|
|
||||||
"css": {
|
|
||||||
"parser": {
|
|
||||||
"tailwindDirectives": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB |
@@ -1,48 +0,0 @@
|
|||||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/electron-userland/electron-builder/refs/heads/master/packages/app-builder-lib/scheme.json
|
|
||||||
appId: com.furtherverse.desktop
|
|
||||||
productName: Furtherverse
|
|
||||||
executableName: furtherverse
|
|
||||||
|
|
||||||
npmRebuild: false
|
|
||||||
asarUnpack:
|
|
||||||
- resources/**
|
|
||||||
|
|
||||||
files:
|
|
||||||
- "!**/.vscode/*"
|
|
||||||
- "!src/*"
|
|
||||||
- "!electron.vite.config.{js,ts,mjs,cjs}"
|
|
||||||
- "!{.env,.env.*,bun.lock}"
|
|
||||||
- "!{tsconfig.json,tsconfig.node.json}"
|
|
||||||
- "!{AGENTS.md,README.md,CHANGELOG.md}"
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
mac:
|
|
||||||
target:
|
|
||||||
- dmg
|
|
||||||
category: public.app-category.productivity
|
|
||||||
extraResources:
|
|
||||||
- from: ../server/out/server-darwin-${arch}
|
|
||||||
to: server
|
|
||||||
dmg:
|
|
||||||
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
win:
|
|
||||||
target:
|
|
||||||
- portable
|
|
||||||
extraResources:
|
|
||||||
- from: ../server/out/server-windows-${arch}.exe
|
|
||||||
to: server.exe
|
|
||||||
portable:
|
|
||||||
artifactName: ${productName}-${version}-${os}-${arch}-Portable.${ext}
|
|
||||||
|
|
||||||
# Linux
|
|
||||||
linux:
|
|
||||||
target:
|
|
||||||
- AppImage
|
|
||||||
category: Utility
|
|
||||||
extraResources:
|
|
||||||
- from: ../server/out/server-linux-${arch}
|
|
||||||
to: server
|
|
||||||
appImage:
|
|
||||||
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import tailwindcss from '@tailwindcss/vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
import { defineConfig } from 'electron-vite'
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
main: {},
|
|
||||||
preload: {},
|
|
||||||
renderer: {
|
|
||||||
plugins: [react(), tailwindcss()],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@furtherverse/desktop",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"main": "out/main/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "electron-vite build",
|
|
||||||
"dev": "electron-vite dev --watch",
|
|
||||||
"dist": "electron-builder",
|
|
||||||
"dist:linux": "bun run dist:linux:x64 && bun run dist:linux:arm64",
|
|
||||||
"dist:linux:arm64": "electron-builder --linux --arm64",
|
|
||||||
"dist:linux:x64": "electron-builder --linux --x64",
|
|
||||||
"dist:mac": "bun run dist:mac:arm64 && bun run dist:mac:x64",
|
|
||||||
"dist:mac:arm64": "electron-builder --mac --arm64",
|
|
||||||
"dist:mac:x64": "electron-builder --mac --x64",
|
|
||||||
"dist:win": "electron-builder --win --x64",
|
|
||||||
"fix": "biome check --write",
|
|
||||||
"typecheck": "tsc -b"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"motion": "catalog:",
|
|
||||||
"react": "catalog:",
|
|
||||||
"react-dom": "catalog:",
|
|
||||||
"tree-kill": "catalog:"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@furtherverse/tsconfig": "workspace:*",
|
|
||||||
"@tailwindcss/vite": "catalog:",
|
|
||||||
"@types/node": "catalog:",
|
|
||||||
"@vitejs/plugin-react": "catalog:",
|
|
||||||
"electron": "catalog:",
|
|
||||||
"electron-builder": "catalog:",
|
|
||||||
"electron-vite": "catalog:",
|
|
||||||
"tailwindcss": "catalog:",
|
|
||||||
"vite": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { join } from 'node:path'
|
|
||||||
import { app, BrowserWindow, dialog, session, shell } from 'electron'
|
|
||||||
import { createSidecarRuntime } from './sidecar'
|
|
||||||
|
|
||||||
const DEV_SERVER_URL = 'http://localhost:3000'
|
|
||||||
const SAFE_EXTERNAL_PROTOCOLS = new Set(['https:', 'http:', 'mailto:'])
|
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
|
||||||
let windowCreationPromise: Promise<void> | null = null
|
|
||||||
let isQuitting = false
|
|
||||||
|
|
||||||
const showErrorAndQuit = (title: string, detail: string) => {
|
|
||||||
if (isQuitting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.showErrorBox(title, detail)
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidecar = createSidecarRuntime({
|
|
||||||
devServerUrl: DEV_SERVER_URL,
|
|
||||||
isPackaged: app.isPackaged,
|
|
||||||
resourcesPath: process.resourcesPath,
|
|
||||||
isQuitting: () => isQuitting,
|
|
||||||
onUnexpectedStop: (detail) => {
|
|
||||||
showErrorAndQuit('Service Stopped', detail)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const toErrorMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error))
|
|
||||||
|
|
||||||
const canOpenExternally = (url: string): boolean => {
|
|
||||||
try {
|
|
||||||
const parsed = new URL(url)
|
|
||||||
return SAFE_EXTERNAL_PROTOCOLS.has(parsed.protocol)
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadSplash = async (windowRef: BrowserWindow) => {
|
|
||||||
if (process.env.ELECTRON_RENDERER_URL) {
|
|
||||||
await windowRef.loadURL(process.env.ELECTRON_RENDERER_URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await windowRef.loadFile(join(__dirname, '../renderer/index.html'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const createWindow = async () => {
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.focus()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const windowRef = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
show: false,
|
|
||||||
webPreferences: {
|
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
|
||||||
sandbox: true,
|
|
||||||
contextIsolation: true,
|
|
||||||
nodeIntegration: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
mainWindow = windowRef
|
|
||||||
|
|
||||||
windowRef.webContents.setWindowOpenHandler(({ url }) => {
|
|
||||||
if (!canOpenExternally(url)) {
|
|
||||||
if (!app.isPackaged) {
|
|
||||||
console.warn(`Blocked external URL: ${url}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { action: 'deny' }
|
|
||||||
}
|
|
||||||
|
|
||||||
void shell.openExternal(url)
|
|
||||||
return { action: 'deny' }
|
|
||||||
})
|
|
||||||
|
|
||||||
windowRef.webContents.on('will-navigate', (event, url) => {
|
|
||||||
const allowed = [DEV_SERVER_URL, sidecar.lastResolvedUrl].filter((v): v is string => v != null)
|
|
||||||
const isAllowed = allowed.some((origin) => url.startsWith(origin))
|
|
||||||
|
|
||||||
if (!isAllowed) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
if (canOpenExternally(url)) {
|
|
||||||
void shell.openExternal(url)
|
|
||||||
} else if (!app.isPackaged) {
|
|
||||||
console.warn(`Blocked navigation to: ${url}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
windowRef.on('closed', () => {
|
|
||||||
if (mainWindow === windowRef) {
|
|
||||||
mainWindow = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
await loadSplash(windowRef)
|
|
||||||
} catch (error) {
|
|
||||||
if (mainWindow === windowRef) {
|
|
||||||
mainWindow = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!windowRef.isDestroyed()) {
|
|
||||||
windowRef.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!windowRef.isDestroyed()) {
|
|
||||||
windowRef.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetUrl = await sidecar.resolveUrl()
|
|
||||||
if (isQuitting || windowRef.isDestroyed()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await windowRef.loadURL(targetUrl)
|
|
||||||
} catch (error) {
|
|
||||||
if (mainWindow === windowRef) {
|
|
||||||
mainWindow = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!windowRef.isDestroyed()) {
|
|
||||||
windowRef.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensureWindow = async () => {
|
|
||||||
if (windowCreationPromise) {
|
|
||||||
return windowCreationPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
windowCreationPromise = createWindow().finally(() => {
|
|
||||||
windowCreationPromise = null
|
|
||||||
})
|
|
||||||
|
|
||||||
return windowCreationPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
const beginQuit = () => {
|
|
||||||
isQuitting = true
|
|
||||||
sidecar.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleWindowCreationError = (error: unknown, context: string) => {
|
|
||||||
console.error(`${context}:`, error)
|
|
||||||
showErrorAndQuit(
|
|
||||||
"App Couldn't Start",
|
|
||||||
app.isPackaged
|
|
||||||
? 'A required component failed to start. Please reinstall the app.'
|
|
||||||
: `${context}: ${toErrorMessage(error)}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
app
|
|
||||||
.whenReady()
|
|
||||||
.then(() => {
|
|
||||||
session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => {
|
|
||||||
callback(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
return ensureWindow()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
handleWindowCreationError(error, 'Failed to create window')
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (isQuitting || BrowserWindow.getAllWindows().length > 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureWindow().catch((error) => {
|
|
||||||
handleWindowCreationError(error, 'Failed to re-create window')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('before-quit', beginQuit)
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
import { type ChildProcess, spawn } from 'node:child_process'
|
|
||||||
import { existsSync } from 'node:fs'
|
|
||||||
import { createServer } from 'node:net'
|
|
||||||
import { join } from 'node:path'
|
|
||||||
import killProcessTree from 'tree-kill'
|
|
||||||
|
|
||||||
const SERVER_HOST = '127.0.0.1'
|
|
||||||
const SERVER_READY_TIMEOUT_MS = 10_000
|
|
||||||
const SERVER_REQUEST_TIMEOUT_MS = 1_500
|
|
||||||
const SERVER_POLL_INTERVAL_MS = 250
|
|
||||||
const SERVER_PROBE_PATHS = ['/api/health', '/']
|
|
||||||
|
|
||||||
type SidecarState = {
|
|
||||||
process: ChildProcess | null
|
|
||||||
startup: Promise<string> | null
|
|
||||||
url: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type SidecarRuntimeOptions = {
|
|
||||||
devServerUrl: string
|
|
||||||
isPackaged: boolean
|
|
||||||
resourcesPath: string
|
|
||||||
isQuitting: () => boolean
|
|
||||||
onUnexpectedStop: (detail: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
type SidecarRuntime = {
|
|
||||||
resolveUrl: () => Promise<string>
|
|
||||||
stop: () => void
|
|
||||||
lastResolvedUrl: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
|
|
||||||
|
|
||||||
const isProcessAlive = (processToCheck: ChildProcess | null): processToCheck is ChildProcess => {
|
|
||||||
if (!processToCheck || !processToCheck.pid) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return processToCheck.exitCode === null && !processToCheck.killed
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAvailablePort = (): Promise<number> =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const server = createServer()
|
|
||||||
server.listen(0, () => {
|
|
||||||
const addr = server.address()
|
|
||||||
if (!addr || typeof addr === 'string') {
|
|
||||||
server.close()
|
|
||||||
reject(new Error('Failed to resolve port'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
server.close(() => resolve(addr.port))
|
|
||||||
})
|
|
||||||
server.on('error', reject)
|
|
||||||
})
|
|
||||||
|
|
||||||
const isServerReady = async (url: string): Promise<boolean> => {
|
|
||||||
for (const probePath of SERVER_PROBE_PATHS) {
|
|
||||||
try {
|
|
||||||
const probeUrl = new URL(probePath, `${url}/`)
|
|
||||||
const response = await fetch(probeUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
cache: 'no-store',
|
|
||||||
signal: AbortSignal.timeout(SERVER_REQUEST_TIMEOUT_MS),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.status < 500) {
|
|
||||||
if (probePath === '/api/health' && response.status === 404) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Expected: probe request fails while server is still starting up
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const waitForServer = async (url: string, isQuitting: () => boolean, processRef?: ChildProcess): Promise<boolean> => {
|
|
||||||
const start = Date.now()
|
|
||||||
while (Date.now() - start < SERVER_READY_TIMEOUT_MS && !isQuitting()) {
|
|
||||||
if (processRef && processRef.exitCode !== null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await isServerReady(url)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(SERVER_POLL_INTERVAL_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveBinaryPath = (resourcesPath: string): string => {
|
|
||||||
const binaryName = process.platform === 'win32' ? 'server.exe' : 'server'
|
|
||||||
return join(resourcesPath, binaryName)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatUnexpectedStopMessage = (
|
|
||||||
isPackaged: boolean,
|
|
||||||
code: number | null,
|
|
||||||
signal: NodeJS.Signals | null,
|
|
||||||
): string => {
|
|
||||||
if (isPackaged) {
|
|
||||||
return 'The background service stopped unexpectedly. Please restart the app.'
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Server process exited unexpectedly (code ${code ?? 'unknown'}, signal ${signal ?? 'none'}).`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createSidecarRuntime = (options: SidecarRuntimeOptions): SidecarRuntime => {
|
|
||||||
const state: SidecarState = {
|
|
||||||
process: null,
|
|
||||||
startup: null,
|
|
||||||
url: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetState = (processRef?: ChildProcess) => {
|
|
||||||
if (processRef && state.process !== processRef) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
state.process = null
|
|
||||||
state.url = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const stop = () => {
|
|
||||||
const runningServer = state.process
|
|
||||||
resetState()
|
|
||||||
|
|
||||||
if (!runningServer?.pid || runningServer.exitCode !== null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
killProcessTree(runningServer.pid, 'SIGTERM', (error?: Error) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to stop server process:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachLifecycleHandlers = (processRef: ChildProcess) => {
|
|
||||||
processRef.on('error', (error) => {
|
|
||||||
if (state.process !== processRef) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const hadReadyServer = state.url !== null
|
|
||||||
resetState(processRef)
|
|
||||||
|
|
||||||
if (!options.isQuitting() && hadReadyServer) {
|
|
||||||
options.onUnexpectedStop('The background service crashed unexpectedly. Please restart the app.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Failed to start server process:', error)
|
|
||||||
})
|
|
||||||
|
|
||||||
processRef.on('exit', (code, signal) => {
|
|
||||||
if (state.process !== processRef) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const hadReadyServer = state.url !== null
|
|
||||||
resetState(processRef)
|
|
||||||
|
|
||||||
if (!options.isQuitting() && hadReadyServer) {
|
|
||||||
options.onUnexpectedStop(formatUnexpectedStopMessage(options.isPackaged, code, signal))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const startPackagedServer = async (): Promise<string> => {
|
|
||||||
if (state.url && isProcessAlive(state.process)) {
|
|
||||||
return state.url
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.startup) {
|
|
||||||
return state.startup
|
|
||||||
}
|
|
||||||
|
|
||||||
state.startup = (async () => {
|
|
||||||
const binaryPath = resolveBinaryPath(options.resourcesPath)
|
|
||||||
if (!existsSync(binaryPath)) {
|
|
||||||
throw new Error(`Sidecar server binary is missing: ${binaryPath}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.isQuitting()) {
|
|
||||||
throw new Error('Application is shutting down.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = await getAvailablePort()
|
|
||||||
const nextServerUrl = `http://${SERVER_HOST}:${port}`
|
|
||||||
const processRef = spawn(binaryPath, [], {
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
HOST: SERVER_HOST,
|
|
||||||
PORT: String(port),
|
|
||||||
},
|
|
||||||
stdio: 'ignore',
|
|
||||||
windowsHide: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
processRef.unref()
|
|
||||||
state.process = processRef
|
|
||||||
attachLifecycleHandlers(processRef)
|
|
||||||
|
|
||||||
const ready = await waitForServer(nextServerUrl, options.isQuitting, processRef)
|
|
||||||
if (ready && isProcessAlive(processRef)) {
|
|
||||||
state.url = nextServerUrl
|
|
||||||
return nextServerUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
const failureReason =
|
|
||||||
processRef.exitCode !== null
|
|
||||||
? `The service exited early (code ${processRef.exitCode}).`
|
|
||||||
: `The service did not respond at ${nextServerUrl} within 10 seconds.`
|
|
||||||
|
|
||||||
stop()
|
|
||||||
throw new Error(failureReason)
|
|
||||||
})().finally(() => {
|
|
||||||
state.startup = null
|
|
||||||
})
|
|
||||||
|
|
||||||
return state.startup
|
|
||||||
}
|
|
||||||
|
|
||||||
const resolveUrl = async (): Promise<string> => {
|
|
||||||
if (options.isPackaged) {
|
|
||||||
return startPackagedServer()
|
|
||||||
}
|
|
||||||
|
|
||||||
const ready = await waitForServer(options.devServerUrl, options.isQuitting)
|
|
||||||
if (!ready) {
|
|
||||||
throw new Error('Dev server not responding. Run `bun dev` in apps/server first.')
|
|
||||||
}
|
|
||||||
|
|
||||||
state.url = options.devServerUrl
|
|
||||||
return options.devServerUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
resolveUrl,
|
|
||||||
stop,
|
|
||||||
get lastResolvedUrl() {
|
|
||||||
return state.url
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export {}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 83 KiB |
@@ -1,33 +0,0 @@
|
|||||||
import { motion } from 'motion/react'
|
|
||||||
import logoImage from '../assets/logo.png'
|
|
||||||
|
|
||||||
export const SplashApp = () => {
|
|
||||||
return (
|
|
||||||
<main className="m-0 flex h-screen w-screen cursor-default select-none items-center justify-center overflow-hidden bg-white font-sans antialiased">
|
|
||||||
<motion.section
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="flex flex-col items-center gap-8"
|
|
||||||
initial={{ opacity: 0, y: 4 }}
|
|
||||||
transition={{
|
|
||||||
duration: 1,
|
|
||||||
ease: [0.16, 1, 0.3, 1],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img alt="Logo" className="h-20 w-auto object-contain" draggable={false} src={logoImage} />
|
|
||||||
|
|
||||||
<div className="relative h-[4px] w-36 overflow-hidden rounded-full bg-zinc-100">
|
|
||||||
<motion.div
|
|
||||||
animate={{ x: '100%' }}
|
|
||||||
className="h-full w-full bg-zinc-800"
|
|
||||||
initial={{ x: '-100%' }}
|
|
||||||
transition={{
|
|
||||||
duration: 2,
|
|
||||||
ease: [0.4, 0, 0.2, 1],
|
|
||||||
repeat: Infinity,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</motion.section>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Furtherverse</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="./main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { StrictMode } from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
import { SplashApp } from './components/SplashApp'
|
|
||||||
import './styles.css'
|
|
||||||
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: 一定存在
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
|
||||||
<StrictMode>
|
|
||||||
<SplashApp />
|
|
||||||
</StrictMode>,
|
|
||||||
)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@furtherverse/tsconfig/react.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"types": ["vite/client"]
|
|
||||||
},
|
|
||||||
"include": ["src/renderer/**/*"]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.app.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.node.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@furtherverse/tsconfig/base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"types": ["node"]
|
|
||||||
},
|
|
||||||
"include": ["src/main/**/*", "src/preload/**/*", "electron.vite.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../node_modules/turbo/schema.json",
|
|
||||||
"extends": ["//"],
|
|
||||||
"tasks": {
|
|
||||||
"build": {
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"dependsOn": ["build", "@furtherverse/server#compile"],
|
|
||||||
"outputs": ["dist/**"]
|
|
||||||
},
|
|
||||||
"dist:linux": {
|
|
||||||
"dependsOn": ["build", "@furtherverse/server#compile:linux:arm64", "@furtherverse/server#compile:linux:x64"],
|
|
||||||
"outputs": ["dist/**"]
|
|
||||||
},
|
|
||||||
"dist:linux:arm64": {
|
|
||||||
"dependsOn": ["build", "@furtherverse/server#compile:linux:arm64"],
|
|
||||||
"outputs": ["dist/**"]
|
|
||||||
},
|
|
||||||
"dist:linux:x64": {
|
|
||||||
"dependsOn": ["build", "@furtherverse/server#compile:linux:x64"],
|
|
||||||
"outputs": ["dist/**"]
|
|
||||||
},
|
|
||||||
"dist:mac": {
|
|
||||||
"dependsOn": ["build", "@furtherverse/server#compile:darwin:arm64", "@furtherverse/server#compile:darwin:x64"],
|
|
||||||
"outputs": ["dist/**"]
|
|
||||||
},
|
|
||||||
"dist:mac:arm64": {
|
|
||||||
"dependsOn": ["build", "@furtherverse/server#compile:darwin:arm64"],
|
|
||||||
"outputs": ["dist/**"]
|
|
||||||
},
|
|
||||||
"dist:mac:x64": {
|
|
||||||
"dependsOn": ["build", "@furtherverse/server#compile:darwin:x64"],
|
|
||||||
"outputs": ["dist/**"]
|
|
||||||
},
|
|
||||||
"dist:win": {
|
|
||||||
"dependsOn": ["build", "@furtherverse/server#compile:windows:x64"],
|
|
||||||
"outputs": ["dist/**"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
# AGENTS.md - Server App Guidelines
|
|
||||||
|
|
||||||
TanStack Start fullstack web app with ORPC (contract-first RPC).
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
> **⚠️ This project uses Bun — NOT Node.js / npm. All commands use `bun`. Always use `bun run <script>` (not `bun <script>`) to avoid conflicts with Bun built-in subcommands. Never use `npm`, `npx`, or `node`.**
|
|
||||||
|
|
||||||
- **Framework**: TanStack Start (React 19 SSR, file-based routing)
|
|
||||||
- **Runtime**: Bun — **NOT Node.js**
|
|
||||||
- **Package Manager**: Bun — **NOT npm / yarn / pnpm**
|
|
||||||
- **Language**: TypeScript (strict mode)
|
|
||||||
- **Styling**: Tailwind CSS v4
|
|
||||||
- **Database**: PostgreSQL + Drizzle ORM v1 beta (`drizzle-orm/postgres-js`, RQBv2)
|
|
||||||
- **State**: TanStack Query v5
|
|
||||||
- **RPC**: ORPC (contract-first, type-safe)
|
|
||||||
- **Build**: Vite + Nitro
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
bun run dev # Vite dev server (localhost:3000)
|
|
||||||
bun run db:studio # Drizzle Studio GUI
|
|
||||||
|
|
||||||
# Build
|
|
||||||
bun run build # Production build → .output/
|
|
||||||
bun run compile # Compile to standalone binary (current platform, depends on build)
|
|
||||||
bun run compile:darwin # Compile for macOS (arm64 + x64)
|
|
||||||
bun run compile:darwin:arm64 # Compile for macOS arm64
|
|
||||||
bun run compile:darwin:x64 # Compile for macOS x64
|
|
||||||
bun run compile:linux # Compile for Linux (x64 + arm64)
|
|
||||||
bun run compile:linux:arm64 # Compile for Linux arm64
|
|
||||||
bun run compile:linux:x64 # Compile for Linux x64
|
|
||||||
bun run compile:windows # Compile for Windows (default: x64)
|
|
||||||
bun run compile:windows:x64 # Compile for Windows x64
|
|
||||||
|
|
||||||
# Code Quality
|
|
||||||
bun run fix # Biome auto-fix
|
|
||||||
bun run typecheck # TypeScript check
|
|
||||||
|
|
||||||
# Database
|
|
||||||
bun run db:generate # Generate migrations from schema
|
|
||||||
bun run db:migrate # Run migrations
|
|
||||||
bun run 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.ts # ORPC client + TanStack Query utils (single entry point)
|
|
||||||
├── components/ # React components
|
|
||||||
├── routes/ # TanStack Router file routes
|
|
||||||
│ ├── __root.tsx # Root layout
|
|
||||||
│ ├── index.tsx # Home page
|
|
||||||
│ └── api/
|
|
||||||
│ ├── $.ts # OpenAPI handler + Scalar docs
|
|
||||||
│ ├── health.ts # Health check endpoint
|
|
||||||
│ └── rpc.$.ts # ORPC RPC handler
|
|
||||||
├── server/ # Server-side code
|
|
||||||
│ ├── api/ # ORPC layer
|
|
||||||
│ │ ├── contracts/ # Input/output schemas (Zod)
|
|
||||||
│ │ ├── middlewares/ # Middleware (db provider, auth)
|
|
||||||
│ │ ├── routers/ # Handler implementations
|
|
||||||
│ │ ├── interceptors.ts # Shared error interceptors
|
|
||||||
│ │ ├── context.ts # Request context
|
|
||||||
│ │ ├── server.ts # ORPC server instance
|
|
||||||
│ │ └── types.ts # Type exports
|
|
||||||
│ └── db/
|
|
||||||
│ ├── schema/ # Drizzle table definitions
|
|
||||||
│ ├── fields.ts # Shared field builders (id, createdAt, updatedAt)
|
|
||||||
│ ├── relations.ts # Drizzle relations (defineRelations, RQBv2)
|
|
||||||
│ └── index.ts # Database instance (postgres-js driver)
|
|
||||||
├── 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-orm/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({
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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'
|
|
||||||
|
|
||||||
const { data } = useSuspenseQuery(orpc.feature.list.queryOptions())
|
|
||||||
const mutation = useMutation(orpc.feature.create.mutationOptions())
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database (Drizzle ORM v1 beta)
|
|
||||||
|
|
||||||
- **Driver**: `drizzle-orm/postgres-js` (NOT `bun-sql`)
|
|
||||||
- **Validation**: `drizzle-orm/zod` (built-in, NOT separate `drizzle-zod` package)
|
|
||||||
- **Relations**: Defined via `defineRelations()` in `src/server/db/relations.ts`
|
|
||||||
- **Query**: RQBv2 — use `db.query.tableName.findMany()` with object-style `orderBy` and `where`
|
|
||||||
|
|
||||||
### Schema Definition
|
|
||||||
```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()),
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Relations (RQBv2)
|
|
||||||
```typescript
|
|
||||||
// src/server/db/relations.ts
|
|
||||||
import { defineRelations } from 'drizzle-orm'
|
|
||||||
import * as schema from './schema'
|
|
||||||
|
|
||||||
export const relations = defineRelations(schema, (r) => ({
|
|
||||||
// Define relations here using r.one / r.many / r.through
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
### DB Instance
|
|
||||||
```typescript
|
|
||||||
// src/server/db/index.ts
|
|
||||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
||||||
import { relations } from '@/server/db/relations'
|
|
||||||
// In RQBv2, relations already contain schema info — no separate schema import needed
|
|
||||||
|
|
||||||
const db = drizzle({
|
|
||||||
connection: env.DATABASE_URL,
|
|
||||||
relations,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### RQBv2 Query Examples
|
|
||||||
```typescript
|
|
||||||
// Object-style orderBy (NOT callback style)
|
|
||||||
const todos = await db.query.todoTable.findMany({
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Object-style where
|
|
||||||
const todo = await db.query.todoTable.findFirst({
|
|
||||||
where: { id: someId },
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
## 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(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Principles
|
|
||||||
|
|
||||||
> **These principles apply to ALL code changes. Agents MUST follow them on every task.**
|
|
||||||
|
|
||||||
1. **No backward compatibility** — This project is in rapid iteration. Always use the latest API and patterns. Never keep deprecated code paths or old API fallbacks.
|
|
||||||
2. **Always sync documentation** — When code changes, immediately update all related documentation (`AGENTS.md`, `README.md`, inline code examples). Code and docs must never drift apart.
|
|
||||||
3. **Forward-only migration** — When upgrading dependencies, fully adopt the new API. Don't mix old and new patterns.
|
|
||||||
|
|
||||||
## Critical Rules
|
|
||||||
|
|
||||||
**DO:**
|
|
||||||
- Run `bun run fix` before committing
|
|
||||||
- Use `@/*` path aliases
|
|
||||||
- Include `createdAt`/`updatedAt` on all tables
|
|
||||||
- Use `ORPCError` with proper codes
|
|
||||||
- Use `drizzle-orm/zod` (NOT `drizzle-zod`) for schema validation
|
|
||||||
- Use RQBv2 object syntax for `orderBy` and `where`
|
|
||||||
- Update `AGENTS.md` and other docs whenever code patterns change
|
|
||||||
|
|
||||||
**DON'T:**
|
|
||||||
- Use `npm`, `npx`, `node`, `yarn`, `pnpm` — always use `bun` / `bunx`
|
|
||||||
- Edit `src/routeTree.gen.ts` (auto-generated)
|
|
||||||
- Use `as any`, `@ts-ignore`, `@ts-expect-error`
|
|
||||||
- Commit `.env` files
|
|
||||||
- Use empty catch blocks
|
|
||||||
- Import from `drizzle-zod` (use `drizzle-orm/zod` instead)
|
|
||||||
- Use RQBv1 callback-style `orderBy` / old `relations()` API
|
|
||||||
- Use `drizzle-orm/bun-sql` driver (use `drizzle-orm/postgres-js`)
|
|
||||||
- Pass `schema` to `drizzle()` constructor (only `relations` is needed in RQBv2)
|
|
||||||
- Import `os` from `@orpc/server` in middleware — use `@/server/api/server` (the local typed instance)
|
|
||||||
- Leave docs out of sync with code changes
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../node_modules/@biomejs/biome/configuration_schema.json",
|
|
||||||
"extends": "//",
|
|
||||||
"files": {
|
|
||||||
"includes": ["**", "!**/routeTree.gen.ts"]
|
|
||||||
},
|
|
||||||
"css": {
|
|
||||||
"parser": {
|
|
||||||
"tailwindDirectives": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@furtherverse/server",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "bunx --bun vite build",
|
|
||||||
"compile": "bun compile.ts",
|
|
||||||
"compile:darwin": "bun run compile:darwin:arm64 && bun run compile:darwin:x64",
|
|
||||||
"compile:darwin:arm64": "bun compile.ts --target bun-darwin-arm64",
|
|
||||||
"compile:darwin:x64": "bun compile.ts --target bun-darwin-x64",
|
|
||||||
"compile:linux": "bun run compile:linux:x64 && bun run compile:linux:arm64",
|
|
||||||
"compile:linux:arm64": "bun compile.ts --target bun-linux-arm64",
|
|
||||||
"compile:linux:x64": "bun compile.ts --target bun-linux-x64",
|
|
||||||
"compile:windows": "bun run compile:windows:x64",
|
|
||||||
"compile:windows:x64": "bun compile.ts --target bun-windows-x64",
|
|
||||||
"db:generate": "drizzle-kit generate",
|
|
||||||
"db:migrate": "drizzle-kit migrate",
|
|
||||||
"db:push": "drizzle-kit push",
|
|
||||||
"db:studio": "drizzle-kit studio",
|
|
||||||
"dev": "bunx --bun vite dev",
|
|
||||||
"fix": "biome check --write",
|
|
||||||
"typecheck": "tsc --noEmit"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@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:",
|
|
||||||
"drizzle-orm": "catalog:",
|
|
||||||
"postgres": "catalog:",
|
|
||||||
"react": "catalog:",
|
|
||||||
"react-dom": "catalog:",
|
|
||||||
"uuid": "catalog:",
|
|
||||||
"zod": "catalog:"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@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:",
|
|
||||||
"drizzle-kit": "catalog:",
|
|
||||||
"nitro": "catalog:",
|
|
||||||
"tailwindcss": "catalog:",
|
|
||||||
"vite": "catalog:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "@furtherverse/tsconfig/react.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "../../node_modules/turbo/schema.json",
|
|
||||||
"extends": ["//"],
|
|
||||||
"tasks": {
|
|
||||||
"build": {
|
|
||||||
"env": ["NODE_ENV", "VITE_*"],
|
|
||||||
"inputs": ["src/**", "public/**", "package.json", "tsconfig.json", "vite.config.ts"],
|
|
||||||
"outputs": [".output/**"]
|
|
||||||
},
|
|
||||||
"compile": {
|
|
||||||
"dependsOn": ["build"],
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
},
|
|
||||||
"compile:darwin": {
|
|
||||||
"dependsOn": ["build"],
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
},
|
|
||||||
"compile:darwin:arm64": {
|
|
||||||
"dependsOn": ["build"],
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
},
|
|
||||||
"compile:darwin:x64": {
|
|
||||||
"dependsOn": ["build"],
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
},
|
|
||||||
"compile:linux": {
|
|
||||||
"dependsOn": ["build"],
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
},
|
|
||||||
"compile:linux:arm64": {
|
|
||||||
"dependsOn": ["build"],
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
},
|
|
||||||
"compile:linux:x64": {
|
|
||||||
"dependsOn": ["build"],
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
},
|
|
||||||
"compile:windows": {
|
|
||||||
"dependsOn": ["build"],
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
},
|
|
||||||
"compile:windows:x64": {
|
|
||||||
"dependsOn": ["build"],
|
|
||||||
"outputs": ["out/**"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+7
-1
@@ -6,7 +6,8 @@
|
|||||||
"useIgnoreFile": true
|
"useIgnoreFile": true
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"ignoreUnknown": false
|
"ignoreUnknown": false,
|
||||||
|
"includes": ["**", "!**/routeTree.gen.ts"]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -33,6 +34,11 @@
|
|||||||
"arrowParentheses": "always"
|
"arrowParentheses": "always"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"css": {
|
||||||
|
"parser": {
|
||||||
|
"tailwindDirectives": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"assist": {
|
"assist": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"actions": {
|
"actions": {
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
[install]
|
|
||||||
publicHoistPattern = ["@types/*", "bun-types", "nitro*"]
|
|
||||||
+33
-42
@@ -1,32 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "@furtherverse/monorepo",
|
"name": "fullstack-starter",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
|
||||||
"apps/*",
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "bunx --bun vite build",
|
||||||
"compile": "turbo run compile",
|
"compile": "bun compile.ts",
|
||||||
"compile:darwin": "turbo run compile:darwin",
|
"compile:darwin": "bun run compile:darwin:arm64 && bun run compile:darwin:x64",
|
||||||
"compile:linux": "turbo run compile:linux",
|
"compile:darwin:arm64": "bun compile.ts --target bun-darwin-arm64",
|
||||||
"compile:windows": "turbo run compile:windows",
|
"compile:darwin:x64": "bun compile.ts --target bun-darwin-x64",
|
||||||
"dev": "turbo run dev",
|
"compile:linux": "bun run compile:linux:x64 && bun run compile:linux:arm64",
|
||||||
"dist": "turbo run dist",
|
"compile:linux:arm64": "bun compile.ts --target bun-linux-arm64",
|
||||||
"dist:linux": "turbo run dist:linux",
|
"compile:linux:x64": "bun compile.ts --target bun-linux-x64",
|
||||||
"dist:mac": "turbo run dist:mac",
|
"compile:windows": "bun run compile:windows:x64",
|
||||||
"dist:win": "turbo run dist:win",
|
"compile:windows:x64": "bun compile.ts --target bun-windows-x64",
|
||||||
"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": "bunx --bun vite dev",
|
||||||
|
"fix": "biome check --write",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@biomejs/biome": "^2.4.9",
|
|
||||||
"turbo": "^2.8.20",
|
|
||||||
"typescript": "^6.0.2"
|
|
||||||
},
|
|
||||||
"catalog": {
|
|
||||||
"@orpc/client": "^1.13.11",
|
"@orpc/client": "^1.13.11",
|
||||||
"@orpc/contract": "^1.13.11",
|
"@orpc/contract": "^1.13.11",
|
||||||
"@orpc/openapi": "^1.13.11",
|
"@orpc/openapi": "^1.13.11",
|
||||||
@@ -34,35 +30,30 @@
|
|||||||
"@orpc/tanstack-query": "^1.13.11",
|
"@orpc/tanstack-query": "^1.13.11",
|
||||||
"@orpc/zod": "^1.13.11",
|
"@orpc/zod": "^1.13.11",
|
||||||
"@t3-oss/env-core": "^0.13.11",
|
"@t3-oss/env-core": "^0.13.11",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
|
||||||
"@tanstack/devtools-vite": "^0.6.0",
|
|
||||||
"@tanstack/react-devtools": "^0.10.0",
|
|
||||||
"@tanstack/react-query": "^5.95.2",
|
"@tanstack/react-query": "^5.95.2",
|
||||||
"@tanstack/react-query-devtools": "^5.95.2",
|
|
||||||
"@tanstack/react-router": "^1.168.3",
|
"@tanstack/react-router": "^1.168.3",
|
||||||
"@tanstack/react-router-devtools": "^1.166.11",
|
|
||||||
"@tanstack/react-router-ssr-query": "^1.166.10",
|
"@tanstack/react-router-ssr-query": "^1.166.10",
|
||||||
"@tanstack/react-start": "^1.167.6",
|
"@tanstack/react-start": "^1.167.6",
|
||||||
"@types/bun": "^1.3.11",
|
|
||||||
"@types/node": "^24.12.0",
|
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
|
||||||
"drizzle-kit": "1.0.0-beta.15-859cf75",
|
|
||||||
"drizzle-orm": "1.0.0-beta.15-859cf75",
|
"drizzle-orm": "1.0.0-beta.15-859cf75",
|
||||||
"electron": "^34.0.0",
|
|
||||||
"electron-builder": "^26.8.1",
|
|
||||||
"electron-vite": "^5.0.0",
|
|
||||||
"motion": "^12.38.0",
|
|
||||||
"nitro": "npm:nitro-nightly@3.0.1-20260324-103046-9ce219ca",
|
|
||||||
"postgres": "^3.4.8",
|
"postgres": "^3.4.8",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"tailwindcss": "^4.2.2",
|
|
||||||
"tree-kill": "^1.2.2",
|
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vite": "^8.0.2",
|
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"devDependencies": {
|
||||||
"@types/node": "catalog:"
|
"@biomejs/biome": "^2.4.9",
|
||||||
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
|
"@tanstack/devtools-vite": "^0.6.0",
|
||||||
|
"@tanstack/react-devtools": "^0.10.0",
|
||||||
|
"@tanstack/react-query-devtools": "^5.95.2",
|
||||||
|
"@tanstack/react-router-devtools": "^1.166.11",
|
||||||
|
"@types/bun": "^1.3.11",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"drizzle-kit": "1.0.0-beta.15-859cf75",
|
||||||
|
"nitro": "npm:nitro-nightly@3.0.1-20260324-103046-9ce219ca",
|
||||||
|
"tailwindcss": "^4.2.2",
|
||||||
|
"typescript": "^6.0.2",
|
||||||
|
"vite": "^8.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"display": "Base",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
"module": "preserve",
|
"module": "preserve",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
@@ -22,7 +21,12 @@
|
|||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
"types": ["bun"]
|
"jsx": "react-jsx",
|
||||||
|
"types": ["bun"],
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "./node_modules/turbo/schema.json",
|
|
||||||
"dangerouslyDisablePackageManagerCheck": true,
|
|
||||||
"globalDependencies": ["bun.lock"],
|
|
||||||
"tasks": {
|
|
||||||
"build": {
|
|
||||||
"dependsOn": ["^build"]
|
|
||||||
},
|
|
||||||
"dev": {
|
|
||||||
"cache": false,
|
|
||||||
"persistent": true
|
|
||||||
},
|
|
||||||
"fix": {
|
|
||||||
"cache": false
|
|
||||||
},
|
|
||||||
"typecheck": {
|
|
||||||
"inputs": ["package.json", "tsconfig.json", "tsconfig.*.json", "**/*.{ts,tsx,d.ts}"],
|
|
||||||
"outputs": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ui": "tui"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user