commit 592bf1a220094bfe610e2cd23b6333f1b65e7251 Author: Codex Bot Date: Tue Sep 15 19:00:10 2026 +0800 Initialize React SDK diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3c85c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.DS_Store +.env +.env.* +!.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..81a6c44 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# React SDK + +使用 TypeScript 开发的 React SDK,配套 `backend-server`。 + +## 开发 + +```sh +npm ci +npm run typecheck +npm run build +``` + +## 使用 + +本包尚未发布。先运行 `npm run build`,再在 React 项目中安装本地目录: + +```sh +npm install /absolute/path/to/react-sdk +``` + +```tsx +import { useHealth } from "@entroia/react-sdk"; + +export function ServerStatus() { + const { data, error, loading } = useHealth("http://localhost:8000"); + if (loading) return

加载中

; + if (error) return

{error.message}

; + return

{data?.status}

; +} +``` + +非组件场景可以使用 `createClient({ baseUrl }).health()`。 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a4944f8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,61 @@ +{ + "name": "@entroia/react-sdk", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@entroia/react-sdk", + "version": "0.1.0", + "devDependencies": { + "@types/react": "^19.0.0", + "react": "^19.0.0", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "react": ">=18 <20" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c2e65cc --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "@entroia/react-sdk", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": {".": {"types": "./dist/index.d.ts", "import": "./dist/index.js"}}, + "files": ["dist"], + "scripts": {"build": "tsc -p tsconfig.json", "typecheck": "tsc --noEmit"}, + "peerDependencies": {"react": ">=18 <20"}, + "devDependencies": {"@types/react": "^19.0.0", "react": "^19.0.0", "typescript": "^5.0.0"} +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2ce37b9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,48 @@ +import { useEffect, useState } from "react"; + +export interface HealthResponse { + status: "ok"; +} + +export interface ClientOptions { + baseUrl: string; +} + +export function createClient({ baseUrl }: ClientOptions) { + const url = baseUrl.replace(/\/+$/, ""); + return { + async health(signal?: AbortSignal): Promise { + const response = await fetch(`${url}/health`, { signal }); + if (!response.ok) { + throw new Error(`Health request failed: HTTP ${response.status}`); + } + const data: unknown = await response.json(); + if (typeof data !== "object" || data === null || !("status" in data) || data.status !== "ok") { + throw new Error("Invalid health response"); + } + return { status: "ok" }; + }, + }; +} + +export function useHealth(baseUrl: string) { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const controller = new AbortController(); + setLoading(true); + setData(null); + setError(null); + createClient({ baseUrl }).health(controller.signal) + .then((result) => { if (!controller.signal.aborted) setData(result); }) + .catch((cause: unknown) => { + if (!controller.signal.aborted) setError(cause instanceof Error ? cause : new Error(String(cause))); + }) + .finally(() => { if (!controller.signal.aborted) setLoading(false); }); + return () => controller.abort(); + }, [baseUrl]); + + return { data, error, loading }; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a367662 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM"], "strict": true, "declaration": true, + "outDir": "dist", "rootDir": "src", "skipLibCheck": true + }, + "include": ["src"] +}