chore: 移除桌面端相关代码和配置
This commit is contained in:
@@ -11,7 +11,6 @@ Guidelines for AI agents working in this Bun monorepo.
|
|||||||
- **Package Manager**: Bun — **NOT npm / yarn / pnpm**
|
- **Package Manager**: Bun — **NOT npm / yarn / pnpm**
|
||||||
- **Apps**:
|
- **Apps**:
|
||||||
- `apps/server` - TanStack Start fullstack web app (see `apps/server/AGENTS.md`)
|
- `apps/server` - TanStack Start fullstack web app (see `apps/server/AGENTS.md`)
|
||||||
- `apps/desktop` - Electron desktop shell, sidecar server pattern (see `apps/desktop/AGENTS.md`)
|
|
||||||
- **Packages**: `packages/tsconfig` (shared TS configs)
|
- **Packages**: `packages/tsconfig` (shared TS configs)
|
||||||
|
|
||||||
## Build / Lint / Test Commands
|
## Build / Lint / Test Commands
|
||||||
@@ -24,10 +23,6 @@ bun run compile # Compile server to standalone binary (current platform
|
|||||||
bun run compile:darwin # Compile server for macOS (arm64 + x64)
|
bun run compile:darwin # Compile server for macOS (arm64 + x64)
|
||||||
bun run compile:linux # Compile server for Linux (x64 + arm64)
|
bun run compile:linux # Compile server for Linux (x64 + arm64)
|
||||||
bun run compile:windows # Compile server for Windows x64
|
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 fix # Lint + format (Biome auto-fix)
|
||||||
bun run typecheck # TypeScript check across monorepo
|
bun run typecheck # TypeScript check across monorepo
|
||||||
```
|
```
|
||||||
@@ -55,22 +50,6 @@ bun run db:push # Push schema (dev only)
|
|||||||
bun run db:studio # Open Drizzle Studio
|
bun run db:studio # Open Drizzle Studio
|
||||||
```
|
```
|
||||||
|
|
||||||
### Desktop App (`apps/desktop`)
|
|
||||||
```bash
|
|
||||||
bun run dev # electron-vite dev mode (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
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
No test framework configured yet. When adding tests:
|
No test framework configured yet. When adding tests:
|
||||||
```bash
|
```bash
|
||||||
@@ -197,15 +176,6 @@ export const myTable = pgTable('my_table', {
|
|||||||
│ │ │ ├── api/ # ORPC contracts, routers, middlewares
|
│ │ │ ├── api/ # ORPC contracts, routers, middlewares
|
||||||
│ │ │ └── db/ # Drizzle schema
|
│ │ │ └── db/ # Drizzle schema
|
||||||
│ │ └── AGENTS.md
|
│ │ └── AGENTS.md
|
||||||
│ └── desktop/ # Electron desktop shell
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── main/
|
|
||||||
│ │ │ └── index.ts # Main process entry
|
|
||||||
│ │ └── preload/
|
|
||||||
│ │ └── index.ts # Preload script
|
|
||||||
│ ├── electron.vite.config.ts
|
|
||||||
│ ├── electron-builder.yml # Packaging config
|
|
||||||
│ └── AGENTS.md
|
|
||||||
├── packages/
|
├── packages/
|
||||||
│ └── tsconfig/ # Shared TS configs
|
│ └── tsconfig/ # Shared TS configs
|
||||||
├── biome.json # Linting/formatting config
|
├── biome.json # Linting/formatting config
|
||||||
@@ -216,4 +186,3 @@ export const myTable = pgTable('my_table', {
|
|||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- `apps/server/AGENTS.md` - Detailed TanStack Start / ORPC patterns
|
- `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 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@@ -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/**"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,10 +14,6 @@
|
|||||||
"compile:linux": "turbo run compile:linux",
|
"compile:linux": "turbo run compile:linux",
|
||||||
"compile:windows": "turbo run compile:windows",
|
"compile:windows": "turbo run compile:windows",
|
||||||
"dev": "turbo run dev",
|
"dev": "turbo run dev",
|
||||||
"dist": "turbo run dist",
|
|
||||||
"dist:linux": "turbo run dist:linux",
|
|
||||||
"dist:mac": "turbo run dist:mac",
|
|
||||||
"dist:win": "turbo run dist:win",
|
|
||||||
"fix": "turbo run fix",
|
"fix": "turbo run fix",
|
||||||
"typecheck": "turbo run typecheck"
|
"typecheck": "turbo run typecheck"
|
||||||
},
|
},
|
||||||
@@ -45,25 +41,16 @@
|
|||||||
"@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/bun": "^1.3.11",
|
||||||
"@types/node": "^24.12.0",
|
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"drizzle-kit": "1.0.0-beta.15-859cf75",
|
"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",
|
"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",
|
"tailwindcss": "^4.2.2",
|
||||||
"tree-kill": "^1.2.2",
|
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vite": "^8.0.2",
|
"vite": "^8.0.2",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"@types/node": "catalog:"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user