feat: 搭建前后端可执行程序示例
This commit is contained in:
423
.gitignore
vendored
Normal file
423
.gitignore
vendored
Normal file
@@ -0,0 +1,423 @@
|
||||
### Git.gitignore ###
|
||||
# Created by git for backups. To disable backups in Git:
|
||||
# $ git config --global mergetool.keepBackup false
|
||||
*.orig
|
||||
|
||||
# Created by git when using merge tools for conflicts
|
||||
*.BACKUP.*
|
||||
*.BASE.*
|
||||
*.LOCAL.*
|
||||
*.REMOTE.*
|
||||
*_BACKUP_*.txt
|
||||
*_BASE_*.txt
|
||||
*_LOCAL_*.txt
|
||||
*_REMOTE_*.txt
|
||||
|
||||
### Go.gitignore ###
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
### Go.patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
### JetBrains+all.gitignore ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### JetBrains+all.patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### Linux.gitignore ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### Node.gitignore ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Test
|
||||
playwright-report
|
||||
test-results
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
### VisualStudioCode.gitignore ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
### VisualStudioCode.patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
|
||||
### Windows.gitignore ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
### macOS.gitignore ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Python.gitignore ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Environments
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.python-version
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# Pyre
|
||||
.pyre/
|
||||
|
||||
# pytype
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# Custom
|
||||
.claude/*
|
||||
!.claude/settings.json
|
||||
.opencode
|
||||
.codex
|
||||
openspec/changes/archive
|
||||
temp
|
||||
.agents
|
||||
skills-lock.json
|
||||
.worktrees
|
||||
!scripts/build/
|
||||
backend/bin
|
||||
backend/server
|
||||
backend/desktop
|
||||
|
||||
# Embedfs generated
|
||||
embedfs/assets/
|
||||
embedfs/frontend-dist/
|
||||
backend/cmd/desktop/rsrc_windows_*.syso
|
||||
|
||||
# Bun
|
||||
.build/
|
||||
*.bun-build
|
||||
102
README.md
Normal file
102
README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Gateway Checker
|
||||
|
||||
基于 Bun + TypeScript 的前后端一体化 demo。开发期使用 Vite + React 提供前端 HMR,后端由 Bun 提供 API;生产期先构建前端静态资源,再将前端资源和 Bun 后端打包为单个 executable。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```text
|
||||
src/
|
||||
server/ Bun 后端运行时、API、静态资源 fallback
|
||||
shared/ 前后端共享 TypeScript 类型
|
||||
web/ Vite + React 前端 demo
|
||||
scripts/ 开发、构建和 smoke test 脚本
|
||||
tests/ Bun test 测试
|
||||
openspec/ OpenSpec 变更与规格文档
|
||||
```
|
||||
|
||||
## 开发命令
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
`bun run dev` 会同时启动:
|
||||
|
||||
- Bun 后端:默认 `http://127.0.0.1:3000`
|
||||
- Vite 前端:默认 `http://127.0.0.1:5173`
|
||||
|
||||
开发期请打开 Vite 前端地址。前端通过相对路径 `/api/demo` 调用后端,Vite 会把 `/api/*` 代理到 Bun 后端,因此浏览器不需要 CORS 配置。
|
||||
|
||||
也可以分别运行:
|
||||
|
||||
```bash
|
||||
bun run dev:server
|
||||
bun run dev:web
|
||||
```
|
||||
|
||||
## Demo 验证
|
||||
|
||||
开发期打开 `http://127.0.0.1:5173`,页面应显示 `/api/demo` 返回的后端 message、Bun 版本、平台和响应时间。
|
||||
|
||||
直接验证 API:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:3000/api/demo
|
||||
curl http://127.0.0.1:3000/health
|
||||
```
|
||||
|
||||
## 构建 executable
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
构建流程:
|
||||
|
||||
- 运行 `vite build`,输出前端资源到 `dist/web`
|
||||
- 生成临时 `.build/static-assets.ts`,用 Bun file import 嵌入 Vite 产物
|
||||
- 运行 `Bun.build({ compile })`,输出 `dist/gateway-checker`
|
||||
|
||||
运行 executable:
|
||||
|
||||
```bash
|
||||
./dist/gateway-checker
|
||||
```
|
||||
|
||||
生产期默认访问 `http://127.0.0.1:3000`。同一个 executable 会服务 `/api/demo`、`/health`、`/assets/*` 和前端 SPA fallback。
|
||||
|
||||
## 运行参数
|
||||
|
||||
默认配置:
|
||||
|
||||
- `HOST=127.0.0.1`
|
||||
- `PORT=3000`
|
||||
|
||||
可以通过环境变量或 CLI 参数覆盖:
|
||||
|
||||
```bash
|
||||
PORT=4000 ./dist/gateway-checker
|
||||
./dist/gateway-checker --host 0.0.0.0 --port 4000
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
bun run typecheck
|
||||
bun test
|
||||
bun run build
|
||||
bun run test:smoke
|
||||
```
|
||||
|
||||
`test:smoke` 会启动生成的 executable,并检查 `/api/demo`、`/health`、前端根路径、静态资源和 SPA fallback。
|
||||
|
||||
## 前后端边界
|
||||
|
||||
前端只通过 HTTP 调用后端,默认 API 路径为相对 `/api/*`。共享类型放在 `src/shared`,前端不得 import `src/server` 的运行时实现。
|
||||
|
||||
这保证了当前可以单文件部署,也保留未来将前端拆到 CDN 或独立静态站点的路径。
|
||||
|
||||
## 已知限制
|
||||
|
||||
当前 demo 不包含数据库、认证、SSR、React Router 或 UI 组件库。单 executable 是按目标平台构建的产物,不是一个文件同时覆盖 macOS、Linux 和 Windows。
|
||||
140
bun.lock
Normal file
140
bun.lock
Normal file
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "gateway-checker",
|
||||
"dependencies": {
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.13",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.11",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@emnapi/core": ["@emnapi/core@1.10.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.128.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.128.0.tgz", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", { "os": "android", "cpu": "arm64" }, "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "arm" }, "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "x64" }, "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", { "os": "linux", "cpu": "x64" }, "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", { "os": "none", "cpu": "arm64" }, "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", { "os": "win32", "cpu": "x64" }, "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.13", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.13.tgz", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
||||
|
||||
"@types/node": ["@types/node@25.6.2", "https://registry.npmmirror.com/@types/node/-/node-25.6.2.tgz", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.13", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.13.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.12", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"postcss": ["postcss@8.5.14", "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
|
||||
|
||||
"react": ["react@19.2.6", "https://registry.npmmirror.com/react/-/react-19.2.6.tgz", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.6", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.6.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-rc.18", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.18.tgz", { "dependencies": { "@oxc-project/types": "=0.128.0", "@rolldown/pluginutils": "1.0.0-rc.18" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-x64": "1.0.0-rc.18", "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.16", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@6.0.3", "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
||||
|
||||
"vite": ["vite@8.0.11", "https://registry.npmmirror.com/vite/-/vite-8.0.11.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0-rc.18", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.18", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", {}, "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw=="],
|
||||
}
|
||||
}
|
||||
21
openspec/config.yaml
Normal file
21
openspec/config.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
schema: spec-driven
|
||||
|
||||
context: |
|
||||
- 使用中文(注释、文档、交流),面向中文开发者
|
||||
- openspec文档的关键字按openspec规范使用,不要翻译为中文
|
||||
- **优先阅读README.md**获取项目结构与开发规范,所有代码风格、命名、注解、依赖、API等规范以README为准
|
||||
- 涉及模块结构、API、实体等变更时同步更新README.md
|
||||
- 新增代码优先复用已有组件、工具、依赖库,不引入新依赖
|
||||
- 新增的逻辑必须编写完善的测试,并保证测试的正确性,不允许跳过任何测试
|
||||
- Git提交: 仅中文; 格式"类型: 简短描述", 类型: feat/fix/refactor/docs/style/test/chore; 多行描述空行后写详细说明
|
||||
- 禁止创建git操作task
|
||||
- 积极使用subagents精心设计并行任务,节省上下文空间,加速任务执行
|
||||
- 优先使用提问工具对用户进行提问
|
||||
|
||||
rules:
|
||||
proposal:
|
||||
- 仔细审查每一个过往spec判断是否存在Modified Capabilities
|
||||
design:
|
||||
- 先前的讨论技术方案要尽可能体现在设计文档中,便于指导实现阶段不偏离已定的技术路线
|
||||
task:
|
||||
- 一行一个任务,严禁任务内容跨行
|
||||
67
openspec/specs/frontend-development-workflow/spec.md
Normal file
67
openspec/specs/frontend-development-workflow/spec.md
Normal file
@@ -0,0 +1,67 @@
|
||||
## Purpose
|
||||
|
||||
定义 Vite + React + TypeScript 前端开发工作流、开发期 API 代理、共享契约和端到端 demo 的行为要求。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Vite React 开发服务器
|
||||
系统 SHALL 提供基于 Vite + React + TypeScript 的前端开发工作流,并支持热模块替换。
|
||||
|
||||
#### Scenario: 启动前端开发服务器
|
||||
- **WHEN** 开发者启动前端开发命令
|
||||
- **THEN** 前端 SHALL 由 Vite 提供服务,并启用 React 热模块替换
|
||||
|
||||
#### Scenario: 构建前端静态资源
|
||||
- **WHEN** 开发者运行前端生产构建命令
|
||||
- **THEN** 系统 SHALL 产出可由 Bun 后端服务的前端静态资源
|
||||
|
||||
### Requirement: 前端开发期 API 代理
|
||||
前端开发服务器 SHALL 在本地开发期间将 `/api/*` 请求代理到 Bun 后端服务。
|
||||
|
||||
#### Scenario: 前端开发期调用 API
|
||||
- **WHEN** 浏览器从 Vite 开发源请求 `/api/demo`
|
||||
- **THEN** Vite SHALL 将请求转发到 Bun 后端服务,且不需要浏览器 CORS 配置
|
||||
|
||||
#### Scenario: 开发期访问非 API 前端路由
|
||||
- **WHEN** 浏览器从 Vite 开发源请求非 API 前端路由
|
||||
- **THEN** Vite SHALL 将该请求作为前端应用流量处理,而不是转发到后端
|
||||
|
||||
### Requirement: 前端使用相对 API 路径
|
||||
除非有文档化的部署配置覆盖该行为,前端代码 MUST 通过相对 `/api/*` URL 调用后端 API。
|
||||
|
||||
#### Scenario: 前端获取后端数据
|
||||
- **WHEN** 前端代码调用后端 API
|
||||
- **THEN** 请求 URL 默认 MUST 使用相对 `/api/*` 路径
|
||||
|
||||
#### Scenario: 运行环境变化
|
||||
- **WHEN** host 或 port 在开发环境和生产环境之间变化
|
||||
- **THEN** 前端 API 调用 SHALL 无需修改源码即可继续工作
|
||||
|
||||
### Requirement: 端到端开发 demo
|
||||
项目 SHALL 提供一个可见的开发 demo,用于证明 React 前端可以通过 Vite 代理调用 Bun 后端。
|
||||
|
||||
#### Scenario: Demo 页面展示后端响应
|
||||
- **WHEN** 开发者启动文档化的开发命令并打开前端 URL
|
||||
- **THEN** 页面 SHALL 调用 `/api/demo` 并展示 Bun 后端返回的数据
|
||||
|
||||
#### Scenario: 开发期后端不可用
|
||||
- **WHEN** 前端 demo 无法访问 `/api/demo`
|
||||
- **THEN** 页面 SHALL 展示清晰的错误状态,而不是静默显示为成功
|
||||
|
||||
### Requirement: 集成开发命令
|
||||
项目 SHALL 提供一个文档化命令,用于在 demo 开发期间同时运行前端和后端。
|
||||
|
||||
#### Scenario: 启动全栈开发
|
||||
- **WHEN** 开发者运行文档化的全栈开发命令
|
||||
- **THEN** 系统 SHALL 启动 Vite 前端开发服务器和 `/api/demo` 所需的 Bun 后端服务器
|
||||
|
||||
### Requirement: 共享 TypeScript 契约
|
||||
项目 SHALL 为前端和后端共同使用的请求与响应类型提供共享 TypeScript 边界。
|
||||
|
||||
#### Scenario: 定义 API 响应结构
|
||||
- **WHEN** 前端和后端都需要某个 API 响应类型
|
||||
- **THEN** 该类型 SHALL 定义在 shared 模块中,而不是在两端重复定义
|
||||
|
||||
#### Scenario: 前端导入共享类型
|
||||
- **WHEN** 前端代码导入共享 API 类型
|
||||
- **THEN** 该导入 SHALL 不要求将后端运行时实现打包进前端
|
||||
71
openspec/specs/fullstack-app-runtime/spec.md
Normal file
71
openspec/specs/fullstack-app-runtime/spec.md
Normal file
@@ -0,0 +1,71 @@
|
||||
## Purpose
|
||||
|
||||
定义 Bun 全栈应用运行时的 HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Bun HTTP 运行时
|
||||
系统 SHALL 运行一个 Bun HTTP server,由单个进程提供后端 API、健康检查、生产静态资源和 SPA fallback 行为。
|
||||
|
||||
#### Scenario: 启动运行时服务器
|
||||
- **WHEN** server 进程成功启动
|
||||
- **THEN** 它 SHALL 监听配置的 host 和 port,并记录实际 server URL
|
||||
|
||||
#### Scenario: 提供运行时配置
|
||||
- **WHEN** 通过支持的运行时配置提供 host 或 port
|
||||
- **THEN** server SHALL 使用该值,且不需要重新构建
|
||||
|
||||
### Requirement: API 路由命名空间
|
||||
系统 MUST 将 `/api/*` 保留给后端 API 路由。
|
||||
|
||||
#### Scenario: API 路由匹配
|
||||
- **WHEN** 请求匹配已注册的 `/api/*` 路由
|
||||
- **THEN** Bun server SHALL 返回 API handler 的响应
|
||||
|
||||
#### Scenario: API 路由未命中
|
||||
- **WHEN** 请求访问未注册的 `/api/*` 路由
|
||||
- **THEN** Bun server MUST 返回 JSON 404 响应,而不是前端 HTML 文档
|
||||
|
||||
### Requirement: Demo API 端点
|
||||
系统 SHALL 暴露 `/api/demo` 作为稳定 demo 端点,用于证明前后端集成可用。
|
||||
|
||||
#### Scenario: Demo API 成功响应
|
||||
- **WHEN** 客户端请求 `/api/demo`
|
||||
- **THEN** Bun server SHALL 返回包含可读 message 和 runtime metadata 的 JSON 响应
|
||||
|
||||
#### Scenario: Demo API 内容类型
|
||||
- **WHEN** 客户端请求 `/api/demo`
|
||||
- **THEN** Bun server SHALL 返回 JSON content type 的响应
|
||||
|
||||
### Requirement: 健康检查端点
|
||||
系统 SHALL 在前端 SPA fallback 之外暴露健康检查端点。
|
||||
|
||||
#### Scenario: 健康检查成功
|
||||
- **WHEN** 客户端请求 `/health`
|
||||
- **THEN** Bun server SHALL 返回成功的、机器可读的健康检查响应
|
||||
|
||||
### Requirement: 生产静态资源服务
|
||||
系统 SHALL 在生产模式下由 Bun runtime 服务 Vite 生产资源。
|
||||
|
||||
#### Scenario: 请求构建后的资源
|
||||
- **WHEN** 客户端请求构建后的前端资源,例如 `/assets/app.js`
|
||||
- **THEN** Bun server SHALL 返回该资源并带有适当的 content type
|
||||
|
||||
#### Scenario: 请求前端根路径
|
||||
- **WHEN** 客户端请求 `/`
|
||||
- **THEN** Bun server SHALL 返回前端入口 HTML 文档
|
||||
|
||||
#### Scenario: 生产 demo 页面调用 API
|
||||
- **WHEN** 客户端从生产 Bun runtime 打开前端页面
|
||||
- **THEN** demo 页面 SHALL 能够从同源调用 `/api/demo` 并展示后端响应
|
||||
|
||||
### Requirement: SPA fallback 行为
|
||||
系统 SHALL 在生产环境中为非 API、非静态资源的前端路由返回前端入口 HTML 文档。
|
||||
|
||||
#### Scenario: 刷新前端路由
|
||||
- **WHEN** 客户端请求前端路由,例如 `/dashboard`
|
||||
- **THEN** Bun server SHALL 返回前端入口 HTML 文档
|
||||
|
||||
#### Scenario: 保留 API 错误语义
|
||||
- **WHEN** 客户端请求未知的 `/api/*` 路由
|
||||
- **THEN** Bun server MUST NOT 返回前端入口 HTML 文档
|
||||
53
openspec/specs/single-executable-packaging/spec.md
Normal file
53
openspec/specs/single-executable-packaging/spec.md
Normal file
@@ -0,0 +1,53 @@
|
||||
## Purpose
|
||||
|
||||
定义将 Vite 前端资源与 Bun 后端打包为单个 standalone executable 的生产构建、运行配置和验证要求。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 生产构建顺序
|
||||
生产构建 MUST 在编译 Bun 后端 executable 之前先构建 Vite 前端。
|
||||
|
||||
#### Scenario: 运行生产构建
|
||||
- **WHEN** 开发者运行生产构建命令
|
||||
- **THEN** 系统 MUST 在调用 Bun standalone executable 编译之前生成前端静态资源
|
||||
|
||||
#### Scenario: 前端构建失败
|
||||
- **WHEN** 前端生产构建失败
|
||||
- **THEN** 系统 MUST 停止生产构建,且不能输出 stale executable
|
||||
|
||||
### Requirement: 单 executable 输出
|
||||
生产构建 SHALL 输出一个 standalone executable,其中包含 Bun 后端、必要 server 依赖和构建后的前端资源。
|
||||
|
||||
#### Scenario: 在目标机器运行 executable
|
||||
- **WHEN** 生成的 executable 在兼容目标平台上运行
|
||||
- **THEN** 它 SHALL 启动全栈应用,且不要求目标机器安装 Node.js、Bun、Vite 或 `node_modules`
|
||||
|
||||
#### Scenario: 服务嵌入的前端
|
||||
- **WHEN** executable 收到前端根路径请求
|
||||
- **THEN** 它 SHALL 从 executable 内包含的资源服务前端,且不需要外部 `dist/` 目录
|
||||
|
||||
#### Scenario: 服务嵌入 demo API 和页面
|
||||
- **WHEN** 生成的 executable 启动,且浏览器打开前端根路径
|
||||
- **THEN** 页面 SHALL 展示同一个 executable 进程中 `/api/demo` 返回的数据
|
||||
|
||||
### Requirement: 外部运行时配置
|
||||
executable MUST 将环境相关运行时配置保留在嵌入的前端和 server bundle 之外。
|
||||
|
||||
#### Scenario: 修改监听端口
|
||||
- **WHEN** 操作者修改受支持的 port 配置
|
||||
- **THEN** 同一个 executable SHALL 在不重新构建的情况下监听新端口
|
||||
|
||||
#### Scenario: 缺少可选配置
|
||||
- **WHEN** 可选运行时配置被省略
|
||||
- **THEN** executable SHALL 使用文档化的默认值
|
||||
|
||||
### Requirement: 构建验证
|
||||
项目 SHALL 提供验证,证明生产 executable 可以服务 API、健康检查、静态资源和 SPA fallback 路由。
|
||||
|
||||
#### Scenario: 验证 executable 路由
|
||||
- **WHEN** 构建验证针对生成的 executable 运行
|
||||
- **THEN** 它 SHALL 检查 `/api/demo`、`/health`、前端根路径、静态资源和前端 fallback 请求
|
||||
|
||||
#### Scenario: 验证失败
|
||||
- **WHEN** 任一代表性生产路由检查失败
|
||||
- **THEN** 验证 SHALL 使构建或测试命令失败
|
||||
29
package.json
Normal file
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "gateway-checker",
|
||||
"module": "src/server/dev.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun run scripts/dev.ts",
|
||||
"dev:server": "bun --watch src/server/dev.ts",
|
||||
"dev:web": "bunx --bun vite --host 127.0.0.1",
|
||||
"build:web": "bunx --bun vite build",
|
||||
"build": "bun run scripts/build.ts",
|
||||
"start": "bun src/server/dev.ts",
|
||||
"test": "bun test",
|
||||
"test:smoke": "bun run scripts/smoke.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.13",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.0.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6"
|
||||
}
|
||||
}
|
||||
122
scripts/build.ts
Normal file
122
scripts/build.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { $ } from "bun";
|
||||
|
||||
const rootDir = fileURLToPath(new URL("../", import.meta.url));
|
||||
const buildDir = fileURLToPath(new URL("../.build/", import.meta.url));
|
||||
const webDistDir = fileURLToPath(new URL("../dist/web/", import.meta.url));
|
||||
const executablePath = fileURLToPath(new URL("../dist/gateway-checker", import.meta.url));
|
||||
const generatedAssetsPath = fileURLToPath(new URL("../.build/static-assets.ts", import.meta.url));
|
||||
const generatedEntryPath = fileURLToPath(new URL("../.build/server-entry.ts", import.meta.url));
|
||||
|
||||
await rm(buildDir, { recursive: true, force: true });
|
||||
await rm(executablePath, { force: true });
|
||||
await mkdir(buildDir, { recursive: true });
|
||||
|
||||
await $`bunx --bun vite build`;
|
||||
|
||||
const files = await listFiles(webDistDir);
|
||||
const indexPath = files.find((file) => normalize(relative(webDistDir, file)) === "index.html");
|
||||
|
||||
if (!indexPath) {
|
||||
throw new Error("Vite build 未生成 dist/web/index.html");
|
||||
}
|
||||
|
||||
const assetFiles = files.filter((file) => file !== indexPath);
|
||||
await writeGeneratedAssets(indexPath, assetFiles);
|
||||
await writeGeneratedEntry();
|
||||
|
||||
const target = process.env.BUN_TARGET ?? process.env.BUILD_TARGET;
|
||||
const result = await Bun.build({
|
||||
entrypoints: [generatedEntryPath],
|
||||
compile: target
|
||||
? {
|
||||
target: target as Bun.Build.CompileTarget,
|
||||
outfile: executablePath,
|
||||
autoloadDotenv: true,
|
||||
autoloadBunfig: true,
|
||||
}
|
||||
: {
|
||||
outfile: executablePath,
|
||||
autoloadDotenv: true,
|
||||
autoloadBunfig: true,
|
||||
},
|
||||
minify: true,
|
||||
sourcemap: "linked",
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
await rm(executablePath, { force: true });
|
||||
throw new Error("Bun executable 构建失败");
|
||||
}
|
||||
|
||||
console.log(`Built executable: ${executablePath}`);
|
||||
|
||||
async function listFiles(directory: string): Promise<string[]> {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const files = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const path = `${directory.replace(/\/$/, "")}/${entry.name}`;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
return listFiles(path);
|
||||
}
|
||||
|
||||
return [path];
|
||||
}),
|
||||
);
|
||||
|
||||
return files.flat();
|
||||
}
|
||||
|
||||
async function writeGeneratedAssets(indexPath: string, assetFiles: string[]) {
|
||||
const imports = [
|
||||
`import type { StaticAssets } from "../src/server/app";`,
|
||||
`import indexPath from "${toImportPath(indexPath)}" with { type: "file" };`,
|
||||
...assetFiles.map(
|
||||
(file, index) => `import asset${index}Path from "${toImportPath(file)}" with { type: "file" };`,
|
||||
),
|
||||
];
|
||||
const assetEntries = assetFiles.map((file, index) => {
|
||||
const urlPath = `/${normalize(relative(webDistDir, file))}`;
|
||||
return ` ${JSON.stringify(urlPath)}: Bun.file(asset${index}Path),`;
|
||||
});
|
||||
const source = `${imports.join("\n")}
|
||||
|
||||
export const staticAssets: StaticAssets = {
|
||||
indexHtml: Bun.file(indexPath),
|
||||
files: {
|
||||
${assetEntries.join("\n")}
|
||||
},
|
||||
};
|
||||
`;
|
||||
|
||||
await mkdir(dirname(generatedAssetsPath), { recursive: true });
|
||||
await writeFile(generatedAssetsPath, source);
|
||||
}
|
||||
|
||||
async function writeGeneratedEntry() {
|
||||
await writeFile(
|
||||
generatedEntryPath,
|
||||
`import { readRuntimeConfig } from "../src/server/config";
|
||||
import { startServer } from "../src/server/server";
|
||||
import { staticAssets } from "./static-assets";
|
||||
|
||||
startServer({
|
||||
config: readRuntimeConfig(),
|
||||
mode: "production",
|
||||
staticAssets,
|
||||
});
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
function toImportPath(path: string): string {
|
||||
const rel = normalize(relative(buildDir, path));
|
||||
return rel.startsWith(".") ? rel : `./${rel}`;
|
||||
}
|
||||
|
||||
function normalize(path: string): string {
|
||||
return path.split(sep).join("/");
|
||||
}
|
||||
55
scripts/dev.ts
Normal file
55
scripts/dev.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
interface ChildProcessInfo {
|
||||
name: string;
|
||||
process: Bun.Subprocess;
|
||||
}
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
BACKEND_PORT: process.env.BACKEND_PORT ?? process.env.PORT ?? "3000",
|
||||
};
|
||||
|
||||
const children: ChildProcessInfo[] = [
|
||||
{
|
||||
name: "server",
|
||||
process: Bun.spawn(["bun", "run", "dev:server"], {
|
||||
env,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "web",
|
||||
process: Bun.spawn(["bun", "run", "dev:web"], {
|
||||
env,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
const stopChildren = () => {
|
||||
for (const child of children) {
|
||||
child.process.kill();
|
||||
}
|
||||
};
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
stopChildren();
|
||||
process.exit(130);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
stopChildren();
|
||||
process.exit(143);
|
||||
});
|
||||
|
||||
const firstExit = await Promise.race(
|
||||
children.map(async (child) => ({ name: child.name, code: await child.process.exited })),
|
||||
);
|
||||
|
||||
stopChildren();
|
||||
|
||||
if (firstExit.code !== 0) {
|
||||
console.error(`${firstExit.name} exited with code ${firstExit.code}`);
|
||||
process.exit(firstExit.code ?? 1);
|
||||
}
|
||||
130
scripts/smoke.ts
Normal file
130
scripts/smoke.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { access } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { DemoResponse, HealthResponse } from "../src/shared/api";
|
||||
|
||||
const executablePath = process.argv[2] ?? fileURLToPath(new URL("../dist/gateway-checker", import.meta.url));
|
||||
|
||||
await assertExecutableExists(executablePath);
|
||||
|
||||
const port = await getFreePort();
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
const app = Bun.spawn([executablePath, "--host", "127.0.0.1", "--port", String(port)], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: {
|
||||
...process.env,
|
||||
HOST: "127.0.0.1",
|
||||
PORT: String(port),
|
||||
},
|
||||
});
|
||||
const stdout = readStream(app.stdout);
|
||||
const stderr = readStream(app.stderr);
|
||||
|
||||
try {
|
||||
await waitForServer(`${baseUrl}/health`);
|
||||
|
||||
const health = await expectJson<HealthResponse>(`${baseUrl}/health`, 200);
|
||||
assert(health.ok === true, "健康检查响应缺少 ok=true");
|
||||
|
||||
const demo = await expectJson<DemoResponse>(`${baseUrl}/api/demo`, 200);
|
||||
assert(demo.message.includes("/api/demo"), "demo 响应未包含预期 message");
|
||||
|
||||
const missingApi = await fetch(`${baseUrl}/api/not-found`);
|
||||
assert(missingApi.status === 404, "未知 API 应返回 404");
|
||||
assert(
|
||||
missingApi.headers.get("content-type")?.includes("application/json") === true,
|
||||
"未知 API 应返回 JSON",
|
||||
);
|
||||
|
||||
const rootHtml = await expectText(`${baseUrl}/`, 200);
|
||||
assert(rootHtml.includes("Gateway Checker Demo"), "前端根页面缺少 demo 标题");
|
||||
|
||||
const fallbackHtml = await expectText(`${baseUrl}/dashboard`, 200);
|
||||
assert(fallbackHtml.includes("Gateway Checker Demo"), "SPA fallback 未返回前端入口页面");
|
||||
|
||||
const assetPath = rootHtml.match(/(?:src|href)="(\/assets\/[^"]+)"/)?.[1];
|
||||
assert(assetPath !== undefined, "前端入口页面未引用 /assets/* 资源");
|
||||
|
||||
const asset = await fetch(`${baseUrl}${assetPath}`);
|
||||
assert(asset.status === 200, `静态资源 ${assetPath} 未返回 200`);
|
||||
|
||||
console.log(`Smoke test passed: ${baseUrl}`);
|
||||
} catch (error) {
|
||||
app.kill();
|
||||
const [out, err] = await Promise.all([stdout, stderr]);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
throw new Error(`executable smoke test 失败: ${message}\nstdout:\n${out}\nstderr:\n${err}`);
|
||||
} finally {
|
||||
app.kill();
|
||||
}
|
||||
|
||||
async function assertExecutableExists(path: string) {
|
||||
try {
|
||||
await access(path);
|
||||
} catch {
|
||||
throw new Error(`找不到 executable: ${path},请先运行 bun run build`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getFreePort(): Promise<number> {
|
||||
const server = Bun.serve({
|
||||
hostname: "127.0.0.1",
|
||||
port: 0,
|
||||
fetch: () => new Response("ok"),
|
||||
});
|
||||
const port = server.port;
|
||||
|
||||
server.stop(true);
|
||||
|
||||
if (port === undefined) {
|
||||
throw new Error("无法分配 smoke test 端口");
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
async function waitForServer(url: string) {
|
||||
const deadline = Date.now() + 8_000;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.ok) return;
|
||||
} catch {
|
||||
await Bun.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`服务未在超时时间内启动: ${url}`);
|
||||
}
|
||||
|
||||
async function expectJson<T>(url: string, status: number): Promise<T> {
|
||||
const response = await fetch(url);
|
||||
|
||||
assert(response.status === status, `${url} 应返回 ${status},实际为 ${response.status}`);
|
||||
assert(response.headers.get("content-type")?.includes("application/json") === true, `${url} 应返回 JSON`);
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
async function expectText(url: string, status: number): Promise<string> {
|
||||
const response = await fetch(url);
|
||||
|
||||
assert(response.status === status, `${url} 应返回 ${status},实际为 ${response.status}`);
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
function assert(condition: boolean, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function readStream(stream: ReadableStream<Uint8Array> | null): Promise<string> {
|
||||
if (!stream) return "";
|
||||
|
||||
return new Response(stream).text();
|
||||
}
|
||||
111
src/server/app.ts
Normal file
111
src/server/app.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { ApiErrorResponse, DemoResponse, HealthResponse, RuntimeMode } from "../shared/api";
|
||||
|
||||
export interface StaticAssets {
|
||||
indexHtml: Blob;
|
||||
files: Record<string, Blob>;
|
||||
}
|
||||
|
||||
export interface AppOptions {
|
||||
mode: RuntimeMode;
|
||||
staticAssets?: StaticAssets;
|
||||
}
|
||||
|
||||
export function createFetchHandler(options: AppOptions) {
|
||||
return (request: Request): Response => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === "/health") {
|
||||
return Response.json(createHealthResponse());
|
||||
}
|
||||
|
||||
if (url.pathname === "/api/demo") {
|
||||
return Response.json(createDemoResponse(options.mode));
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
return Response.json(createApiError("API route not found", 404), { status: 404 });
|
||||
}
|
||||
|
||||
if (options.staticAssets) {
|
||||
return serveStaticAsset(url.pathname, options.staticAssets);
|
||||
}
|
||||
|
||||
return new Response("开发期请通过 Vite 前端地址访问页面。", {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createDemoResponse(mode: RuntimeMode): DemoResponse {
|
||||
return {
|
||||
message: "Bun 后端已通过 /api/demo 连接到 React 前端。",
|
||||
runtime: {
|
||||
mode,
|
||||
bunVersion: Bun.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createHealthResponse(): HealthResponse {
|
||||
return {
|
||||
ok: true,
|
||||
service: "gateway-checker",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function createApiError(error: string, status: number): ApiErrorResponse {
|
||||
return { error, status };
|
||||
}
|
||||
|
||||
function serveStaticAsset(pathname: string, staticAssets: StaticAssets): Response {
|
||||
if (pathname === "/") {
|
||||
return htmlResponse(staticAssets.indexHtml);
|
||||
}
|
||||
|
||||
const asset = staticAssets.files[pathname];
|
||||
|
||||
if (asset) {
|
||||
return new Response(asset, {
|
||||
headers: {
|
||||
"Content-Type": contentTypeFor(pathname),
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/assets/") || hasFileExtension(pathname)) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
return htmlResponse(staticAssets.indexHtml);
|
||||
}
|
||||
|
||||
function htmlResponse(indexHtml: Blob): Response {
|
||||
return new Response(indexHtml, {
|
||||
headers: {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function hasFileExtension(pathname: string): boolean {
|
||||
return /\/[^/]+\.[^/]+$/.test(pathname);
|
||||
}
|
||||
|
||||
function contentTypeFor(pathname: string): string {
|
||||
if (pathname.endsWith(".js") || pathname.endsWith(".mjs")) return "text/javascript; charset=utf-8";
|
||||
if (pathname.endsWith(".css")) return "text/css; charset=utf-8";
|
||||
if (pathname.endsWith(".svg")) return "image/svg+xml";
|
||||
if (pathname.endsWith(".json")) return "application/json; charset=utf-8";
|
||||
if (pathname.endsWith(".png")) return "image/png";
|
||||
if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) return "image/jpeg";
|
||||
if (pathname.endsWith(".ico")) return "image/x-icon";
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
39
src/server/config.ts
Normal file
39
src/server/config.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export interface RuntimeConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
const DEFAULT_HOST = "127.0.0.1";
|
||||
const DEFAULT_PORT = 3000;
|
||||
|
||||
export function readRuntimeConfig(
|
||||
argv: string[] = process.argv.slice(2),
|
||||
env: Record<string, string | undefined> = Bun.env,
|
||||
): RuntimeConfig {
|
||||
const host = readOption(argv, "host") ?? env.HOST ?? DEFAULT_HOST;
|
||||
const portValue = readOption(argv, "port") ?? env.PORT ?? String(DEFAULT_PORT);
|
||||
const port = Number(portValue);
|
||||
|
||||
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
||||
throw new Error(`无效端口: ${portValue}`);
|
||||
}
|
||||
|
||||
return { host, port };
|
||||
}
|
||||
|
||||
function readOption(argv: string[], name: string): string | undefined {
|
||||
const prefix = `--${name}=`;
|
||||
const inline = argv.find((value) => value.startsWith(prefix));
|
||||
|
||||
if (inline) {
|
||||
return inline.slice(prefix.length);
|
||||
}
|
||||
|
||||
const index = argv.indexOf(`--${name}`);
|
||||
|
||||
if (index >= 0) {
|
||||
return argv[index + 1];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
7
src/server/dev.ts
Normal file
7
src/server/dev.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { readRuntimeConfig } from "./config";
|
||||
import { startServer } from "./server";
|
||||
|
||||
startServer({
|
||||
config: readRuntimeConfig(),
|
||||
mode: "development",
|
||||
});
|
||||
25
src/server/server.ts
Normal file
25
src/server/server.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { RuntimeMode } from "../shared/api";
|
||||
import { createFetchHandler, type StaticAssets } from "./app";
|
||||
import { readRuntimeConfig, type RuntimeConfig } from "./config";
|
||||
|
||||
export interface StartServerOptions {
|
||||
config?: RuntimeConfig;
|
||||
mode: RuntimeMode;
|
||||
staticAssets?: StaticAssets;
|
||||
}
|
||||
|
||||
export function startServer(options: StartServerOptions) {
|
||||
const config = options.config ?? readRuntimeConfig();
|
||||
const server = Bun.serve({
|
||||
hostname: config.host,
|
||||
port: config.port,
|
||||
fetch: createFetchHandler({
|
||||
mode: options.mode,
|
||||
staticAssets: options.staticAssets,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log(`Gateway Checker listening on ${server.url}`);
|
||||
|
||||
return server;
|
||||
}
|
||||
23
src/shared/api.ts
Normal file
23
src/shared/api.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export type RuntimeMode = "development" | "production" | "test";
|
||||
|
||||
export interface DemoResponse {
|
||||
message: string;
|
||||
runtime: {
|
||||
mode: RuntimeMode;
|
||||
bunVersion: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
ok: true;
|
||||
service: "gateway-checker";
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface ApiErrorResponse {
|
||||
error: string;
|
||||
status: number;
|
||||
}
|
||||
91
src/web/app.tsx
Normal file
91
src/web/app.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { DemoResponse } from "../shared/api";
|
||||
|
||||
type DemoState =
|
||||
| { status: "loading" }
|
||||
| { status: "success"; data: DemoResponse }
|
||||
| { status: "error"; message: string };
|
||||
|
||||
export function App() {
|
||||
const [demoState, setDemoState] = useState<DemoState>({ status: "loading" });
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
async function loadDemo() {
|
||||
try {
|
||||
const response = await fetch("/api/demo", { signal: abortController.signal });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as DemoResponse;
|
||||
setDemoState({ status: "success", data });
|
||||
} catch (error) {
|
||||
if (abortController.signal.aborted) return;
|
||||
|
||||
setDemoState({
|
||||
status: "error",
|
||||
message: error instanceof Error ? error.message : "未知错误",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void loadDemo();
|
||||
|
||||
return () => abortController.abort();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="shell">
|
||||
<section className="hero" aria-labelledby="page-title">
|
||||
<p className="eyebrow">Vite + React + Bun</p>
|
||||
<h1 id="page-title">Gateway Checker Demo</h1>
|
||||
<p className="summary">这个页面用于验证前端开发、Bun 后端 API 和单可执行文件打包链路已经跑通。</p>
|
||||
</section>
|
||||
|
||||
<section className="card" aria-live="polite">
|
||||
<div className="card-header">
|
||||
<span className="status-dot" data-state={demoState.status} />
|
||||
<h2>后端连接状态</h2>
|
||||
</div>
|
||||
|
||||
{demoState.status === "loading" ? <p>正在请求 /api/demo...</p> : null}
|
||||
|
||||
{demoState.status === "error" ? (
|
||||
<div className="error">
|
||||
<strong>请求失败</strong>
|
||||
<p>{demoState.message}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{demoState.status === "success" ? (
|
||||
<div className="result">
|
||||
<p className="message">{demoState.data.message}</p>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>运行模式</dt>
|
||||
<dd>{demoState.data.runtime.mode}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Bun 版本</dt>
|
||||
<dd>{demoState.data.runtime.bunVersion}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>平台</dt>
|
||||
<dd>
|
||||
{demoState.data.runtime.platform}/{demoState.data.runtime.arch}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>响应时间</dt>
|
||||
<dd>{demoState.data.runtime.timestamp}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
13
src/web/index.html
Normal file
13
src/web/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Gateway Checker Vite React Bun executable demo" />
|
||||
<title>Gateway Checker Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
16
src/web/main.tsx
Normal file
16
src/web/main.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./app";
|
||||
import "./styles.css";
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
|
||||
if (!rootElement) {
|
||||
throw new Error("找不到前端挂载节点 #root");
|
||||
}
|
||||
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
169
src/web/styles.css
Normal file
169
src/web/styles.css
Normal file
@@ -0,0 +1,169 @@
|
||||
:root {
|
||||
color: #102033;
|
||||
background: #edf3f8;
|
||||
font-family:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 460px);
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 56px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(55, 125, 255, 0.18), transparent 34rem),
|
||||
linear-gradient(135deg, #f8fbff 0%, #e3edf7 100%);
|
||||
}
|
||||
|
||||
.hero,
|
||||
.card {
|
||||
border: 1px solid rgba(49, 83, 126, 0.14);
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
box-shadow: 0 24px 80px rgba(34, 57, 91, 0.16);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 18px;
|
||||
color: #356dd2;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 760px;
|
||||
margin-bottom: 20px;
|
||||
font-size: clamp(3rem, 8vw, 7rem);
|
||||
line-height: 0.9;
|
||||
letter-spacing: -0.08em;
|
||||
}
|
||||
|
||||
.summary {
|
||||
max-width: 620px;
|
||||
margin-bottom: 0;
|
||||
color: #42546c;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 999px;
|
||||
background: #f5a524;
|
||||
box-shadow: 0 0 0 8px rgba(245, 165, 36, 0.14);
|
||||
}
|
||||
|
||||
.status-dot[data-state="success"] {
|
||||
background: #1fbf75;
|
||||
box-shadow: 0 0 0 8px rgba(31, 191, 117, 0.14);
|
||||
}
|
||||
|
||||
.status-dot[data-state="error"] {
|
||||
background: #e5484d;
|
||||
box-shadow: 0 0 0 8px rgba(229, 72, 77, 0.14);
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(229, 72, 77, 0.25);
|
||||
border-radius: 18px;
|
||||
color: #9f2228;
|
||||
background: rgba(255, 240, 240, 0.8);
|
||||
}
|
||||
|
||||
.error p,
|
||||
.message {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.result {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: #1c3f73;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dl div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: rgba(236, 243, 252, 0.74);
|
||||
}
|
||||
|
||||
dt {
|
||||
color: #61728a;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.shell {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.card {
|
||||
padding: 28px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
}
|
||||
69
tests/server/app.test.ts
Normal file
69
tests/server/app.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createFetchHandler, type StaticAssets } from "../../src/server/app";
|
||||
|
||||
const staticAssets: StaticAssets = {
|
||||
indexHtml: new Blob(["<!doctype html><title>Gateway Checker Demo</title><div id=\"root\"></div>"], {
|
||||
type: "text/html",
|
||||
}),
|
||||
files: {
|
||||
"/assets/app.js": new Blob(["console.log('demo');"], { type: "text/javascript" }),
|
||||
},
|
||||
};
|
||||
|
||||
describe("Bun fullstack runtime", () => {
|
||||
const fetchHandler = createFetchHandler({ mode: "test", staticAssets });
|
||||
|
||||
test("/api/demo 返回 JSON demo 响应", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/api/demo"));
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("application/json");
|
||||
expect(body.message).toContain("/api/demo");
|
||||
expect(body.runtime.mode).toBe("test");
|
||||
});
|
||||
|
||||
test("/health 返回机器可读健康检查", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/health"));
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.ok).toBe(true);
|
||||
expect(body.service).toBe("gateway-checker");
|
||||
});
|
||||
|
||||
test("未知 /api/* 路由返回 JSON 404", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/api/missing"));
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.headers.get("content-type")).toContain("application/json");
|
||||
expect(body.status).toBe(404);
|
||||
});
|
||||
|
||||
test("生产根路径返回前端入口", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/"));
|
||||
const body = await response.text();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/html");
|
||||
expect(body).toContain("Gateway Checker Demo");
|
||||
});
|
||||
|
||||
test("生产静态资源返回正确内容类型", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/assets/app.js"));
|
||||
const body = await response.text();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/javascript");
|
||||
expect(body).toContain("demo");
|
||||
});
|
||||
|
||||
test("前端路由 fallback 到入口 HTML", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/dashboard"));
|
||||
const body = await response.text();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body).toContain("Gateway Checker Demo");
|
||||
});
|
||||
});
|
||||
26
tests/server/config.test.ts
Normal file
26
tests/server/config.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readRuntimeConfig } from "../../src/server/config";
|
||||
|
||||
describe("runtime config", () => {
|
||||
test("默认使用 127.0.0.1:3000", () => {
|
||||
expect(readRuntimeConfig([], {})).toEqual({ host: "127.0.0.1", port: 3000 });
|
||||
});
|
||||
|
||||
test("CLI 参数优先于环境变量", () => {
|
||||
expect(readRuntimeConfig(["--host", "0.0.0.0", "--port", "4001"], { HOST: "127.0.0.1", PORT: "3001" })).toEqual({
|
||||
host: "0.0.0.0",
|
||||
port: 4001,
|
||||
});
|
||||
});
|
||||
|
||||
test("支持 inline CLI 参数", () => {
|
||||
expect(readRuntimeConfig(["--host=localhost", "--port=4002"], {})).toEqual({
|
||||
host: "localhost",
|
||||
port: 4002,
|
||||
});
|
||||
});
|
||||
|
||||
test("拒绝无效端口", () => {
|
||||
expect(() => readRuntimeConfig(["--port", "invalid"], {})).toThrow("无效端口");
|
||||
});
|
||||
});
|
||||
30
tsconfig.json
Normal file
30
tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
"types": ["bun", "vite/client"],
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
25
vite.config.ts
Normal file
25
vite.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const backendPort = Number(process.env.BACKEND_PORT ?? process.env.PORT ?? 3000);
|
||||
|
||||
export default defineConfig({
|
||||
root: "src/web",
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: `http://127.0.0.1:${backendPort}`,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "../../dist/web",
|
||||
emptyOutDir: true,
|
||||
assetsDir: "assets",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user