Add Express API server for Claude API proxy
- Add Express server with vite-express for combined frontend/API serving - Create modular API route structure (server/api/) - Implement Claude API proxy with proper header injection - Support split deployment via API_ONLY and ALLOWED_ORIGINS env vars - Remove Claude proxy from Vite config (now handled by Express) - Add migration plan documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1ccdab2780
commit
1152ab0d0c
403
EXPRESS_API_PLAN.md
Normal file
403
EXPRESS_API_PLAN.md
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
# Express.js API Server Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Add an Express.js backend server to handle API routes (starting with Claude API), with support for either combined or split deployment.
|
||||||
|
|
||||||
|
## Advantages Over Next.js Migration
|
||||||
|
- **Minimal frontend changes** - only API URL configuration
|
||||||
|
- **No routing changes** - keep react-router-dom as-is
|
||||||
|
- **Flexible deployment** - combined or split frontend/backend
|
||||||
|
- **Already partially exists** - `server.js` in root has Express + vite-express scaffolding
|
||||||
|
|
||||||
|
## Deployment Options
|
||||||
|
|
||||||
|
### Option A: Combined (Single Server)
|
||||||
|
```
|
||||||
|
Express Server (vite-express)
|
||||||
|
├── Serves static files from dist/
|
||||||
|
└── Handles /api/* routes
|
||||||
|
```
|
||||||
|
- Simpler setup, one deployment
|
||||||
|
- Good for: VPS, Railway, Fly.io, DigitalOcean App Platform
|
||||||
|
|
||||||
|
### Option B: Split (Separate Hosts)
|
||||||
|
```
|
||||||
|
Static Host (CDN) API Server (Node.js)
|
||||||
|
├── Cloudflare Pages ├── Railway
|
||||||
|
├── Netlify ├── Fly.io
|
||||||
|
├── Vercel ├── AWS Lambda
|
||||||
|
└── S3 + CloudFront └── Any VPS
|
||||||
|
|
||||||
|
Serves dist/ Handles /api/*
|
||||||
|
```
|
||||||
|
- Better scalability, cheaper static hosting
|
||||||
|
- Good for: High traffic, global CDN distribution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
### Existing `server.js` (incomplete)
|
||||||
|
```javascript
|
||||||
|
import express from "express";
|
||||||
|
import ViteExpress from "vite-express";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import expressProxy from "express-http-proxy";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
const app = express();
|
||||||
|
app.use("/api", expressProxy("local.immersiveidea.com"));
|
||||||
|
ViteExpress.listen(app, process.env.PORT || 3001, () => console.log("Server is listening..."));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing Dependencies
|
||||||
|
The following packages are imported but not in package.json:
|
||||||
|
- `express`
|
||||||
|
- `vite-express`
|
||||||
|
- `express-http-proxy`
|
||||||
|
- `dotenv`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install express vite-express dotenv cors
|
||||||
|
```
|
||||||
|
|
||||||
|
- `express` - Web framework
|
||||||
|
- `vite-express` - Vite integration for combined deployment
|
||||||
|
- `dotenv` - Environment variable loading
|
||||||
|
- `cors` - Cross-origin support for split deployment
|
||||||
|
|
||||||
|
### Phase 2: Create API Routes Structure
|
||||||
|
|
||||||
|
Create a modular API structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
server/
|
||||||
|
├── server.js # Existing WebSocket server (keep as-is)
|
||||||
|
├── api/
|
||||||
|
│ ├── index.js # Main API router
|
||||||
|
│ └── claude.js # Claude API proxy route
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Update Root `server.js`
|
||||||
|
|
||||||
|
Replace the current incomplete server.js with:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import express from "express";
|
||||||
|
import ViteExpress from "vite-express";
|
||||||
|
import cors from "cors";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import apiRoutes from "./server/api/index.js";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// CORS configuration for split deployment
|
||||||
|
// In combined mode, same-origin requests don't need CORS
|
||||||
|
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [];
|
||||||
|
if (allowedOrigins.length > 0) {
|
||||||
|
app.use(cors({
|
||||||
|
origin: allowedOrigins,
|
||||||
|
credentials: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
app.use("/api", apiRoutes);
|
||||||
|
|
||||||
|
// Check if running in API-only mode (split deployment)
|
||||||
|
const apiOnly = process.env.API_ONLY === "true";
|
||||||
|
|
||||||
|
if (apiOnly) {
|
||||||
|
// API-only mode: no static file serving
|
||||||
|
app.listen(process.env.PORT || 3000, () => {
|
||||||
|
console.log(`API server running on port ${process.env.PORT || 3000}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Combined mode: Vite handles static files + SPA
|
||||||
|
ViteExpress.listen(app, process.env.PORT || 3001, () => {
|
||||||
|
console.log(`Server running on port ${process.env.PORT || 3001}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Create API Router
|
||||||
|
|
||||||
|
**`server/api/index.js`**:
|
||||||
|
```javascript
|
||||||
|
import { Router } from "express";
|
||||||
|
import claudeRouter from "./claude.js";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Claude API proxy
|
||||||
|
router.use("/claude", claudeRouter);
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
router.get("/health", (req, res) => {
|
||||||
|
res.json({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
```
|
||||||
|
|
||||||
|
**`server/api/claude.js`**:
|
||||||
|
```javascript
|
||||||
|
import { Router } from "express";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const ANTHROPIC_API_URL = "https://api.anthropic.com";
|
||||||
|
|
||||||
|
router.post("/*", async (req, res) => {
|
||||||
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return res.status(500).json({ error: "API key not configured" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the path after /api/claude (e.g., /v1/messages)
|
||||||
|
const path = req.params[0] || req.path;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${ANTHROPIC_API_URL}${path}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": apiKey,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(req.body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
res.status(response.status).json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Claude API error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to proxy request to Claude API" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 5: Update Vite Config
|
||||||
|
|
||||||
|
Remove the Claude proxy from `vite.config.ts` since Express handles it now.
|
||||||
|
|
||||||
|
**Before** (lines 41-56):
|
||||||
|
```javascript
|
||||||
|
'^/api/claude': {
|
||||||
|
target: 'https://api.anthropic.com',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api\/claude/, ''),
|
||||||
|
configure: (proxy) => {
|
||||||
|
proxy.on('proxyReq', (proxyReq) => {
|
||||||
|
const apiKey = env.ANTHROPIC_API_KEY;
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After**: Remove this block entirely. The Express server handles `/api/claude/*`.
|
||||||
|
|
||||||
|
Keep the other proxies (`/sync/*`, `/create-db`, `/api/images`) - they still proxy to deepdiagram.com in dev mode.
|
||||||
|
|
||||||
|
### Phase 6: Add API URL Configuration (for Split Deployment)
|
||||||
|
|
||||||
|
Create a utility to get the API base URL:
|
||||||
|
|
||||||
|
**`src/util/apiConfig.ts`**:
|
||||||
|
```typescript
|
||||||
|
// API base URL - empty string for same-origin (combined deployment)
|
||||||
|
// Set VITE_API_URL for split deployment (e.g., "https://api.yourdomain.com")
|
||||||
|
export const API_BASE_URL = import.meta.env.VITE_API_URL || '';
|
||||||
|
|
||||||
|
export function apiUrl(path: string): string {
|
||||||
|
return `${API_BASE_URL}${path}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update `src/react/services/diagramAI.ts`**:
|
||||||
|
```typescript
|
||||||
|
import { apiUrl } from '../../util/apiConfig';
|
||||||
|
|
||||||
|
// Change from:
|
||||||
|
const response = await fetch('/api/claude/v1/messages', { ... });
|
||||||
|
|
||||||
|
// To:
|
||||||
|
const response = await fetch(apiUrl('/api/claude/v1/messages'), { ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
This change is backward-compatible:
|
||||||
|
- **Combined deployment**: `VITE_API_URL` is empty, calls go to same origin
|
||||||
|
- **Split deployment**: `VITE_API_URL=https://api.example.com`, calls go to API server
|
||||||
|
|
||||||
|
### Phase 7: Update package.json Scripts
|
||||||
|
|
||||||
|
```json
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node server.js",
|
||||||
|
"build": "node versionBump.js && vite build",
|
||||||
|
"start": "NODE_ENV=production node server.js",
|
||||||
|
"start:api": "API_ONLY=true node server.js",
|
||||||
|
"test": "vitest",
|
||||||
|
"socket": "node server/server.js",
|
||||||
|
"serverBuild": "cd server && tsc"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- `dev`: Runs Express + vite-express (serves Vite in dev mode)
|
||||||
|
- `start`: Combined mode - serves dist/ + API
|
||||||
|
- `start:api`: API-only mode for split deployment
|
||||||
|
- Removed `preview` (use `start` instead)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Changes Summary
|
||||||
|
|
||||||
|
| Action | File | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| Modify | `package.json` | Add dependencies, update scripts |
|
||||||
|
| Modify | `server.js` | Full Express server with CORS + API routes |
|
||||||
|
| Create | `server/api/index.js` | Main API router |
|
||||||
|
| Create | `server/api/claude.js` | Claude API proxy endpoint |
|
||||||
|
| Create | `src/util/apiConfig.ts` | API URL configuration utility |
|
||||||
|
| Modify | `src/react/services/diagramAI.ts` | Use apiUrl() for API calls |
|
||||||
|
| Modify | `vite.config.ts` | Remove `/api/claude` proxy block |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How vite-express Works
|
||||||
|
|
||||||
|
`vite-express` is a simple integration that:
|
||||||
|
|
||||||
|
1. **Development**: Runs Vite's dev server as middleware, providing HMR
|
||||||
|
2. **Production**: Serves the built `dist/` folder as static files
|
||||||
|
|
||||||
|
This means:
|
||||||
|
- One server handles both API and frontend
|
||||||
|
- No CORS issues (same origin)
|
||||||
|
- HMR works in development
|
||||||
|
- Production-ready with `vite build`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Option A: Combined Deployment
|
||||||
|
|
||||||
|
Single server handles both frontend and API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build frontend
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Start combined server (serves dist/ + API)
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment variables (.env)**:
|
||||||
|
```bash
|
||||||
|
PORT=3001
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
```
|
||||||
|
|
||||||
|
The Express server will:
|
||||||
|
1. Handle `/api/*` routes directly
|
||||||
|
2. Serve static files from `dist/`
|
||||||
|
3. Fall back to `dist/index.html` for SPA routing
|
||||||
|
|
||||||
|
### Option B: Split Deployment
|
||||||
|
|
||||||
|
Separate hosting for frontend (CDN) and API (Node server):
|
||||||
|
|
||||||
|
**API Server:**
|
||||||
|
```bash
|
||||||
|
# Start API-only server
|
||||||
|
npm run start:api
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment variables (.env for API server)**:
|
||||||
|
```bash
|
||||||
|
PORT=3000
|
||||||
|
API_ONLY=true
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-...
|
||||||
|
ALLOWED_ORIGINS=https://your-frontend.com,https://www.your-frontend.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend (Static Host):**
|
||||||
|
```bash
|
||||||
|
# Build with API URL configured
|
||||||
|
VITE_API_URL=https://api.yourdomain.com npm run build
|
||||||
|
|
||||||
|
# Deploy dist/ to your static host (Cloudflare Pages, Netlify, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment variables (.env.production for frontend build)**:
|
||||||
|
```bash
|
||||||
|
VITE_API_URL=https://api.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment Examples
|
||||||
|
|
||||||
|
| Deployment | Frontend | API Server | Cost |
|
||||||
|
|------------|----------|------------|------|
|
||||||
|
| Combined | Railway | (same) | ~$5/mo |
|
||||||
|
| Combined | Fly.io | (same) | Free tier |
|
||||||
|
| Split | Cloudflare Pages (free) | Railway ($5/mo) | ~$5/mo |
|
||||||
|
| Split | Netlify (free) | Fly.io (free) | Free |
|
||||||
|
| Split | Vercel (free) | AWS Lambda | Pay-per-use |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future API Routes
|
||||||
|
|
||||||
|
To add more API routes, create new files in `server/api/`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// server/api/index.js
|
||||||
|
import claudeRouter from "./claude.js";
|
||||||
|
import imagesRouter from "./images.js"; // future
|
||||||
|
import authRouter from "./auth.js"; // future
|
||||||
|
|
||||||
|
router.use("/claude", claudeRouter);
|
||||||
|
router.use("/images", imagesRouter);
|
||||||
|
router.use("/auth", authRouter);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Order
|
||||||
|
|
||||||
|
1. `npm install express vite-express dotenv cors`
|
||||||
|
2. Create `server/api/index.js`
|
||||||
|
3. Create `server/api/claude.js`
|
||||||
|
4. Create `src/util/apiConfig.ts`
|
||||||
|
5. Update `src/react/services/diagramAI.ts` to use `apiUrl()`
|
||||||
|
6. Update `server.js` (root) with full Express + CORS setup
|
||||||
|
7. Remove `/api/claude` proxy from `vite.config.ts`
|
||||||
|
8. Update `package.json` scripts
|
||||||
|
9. Test combined: `npm run dev` and verify Claude API works
|
||||||
|
10. (Optional) Test split: Set `VITE_API_URL` and `API_ONLY=true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **WebSocket server unchanged**: `server/server.js` (port 8080) runs separately
|
||||||
|
- **Minimal frontend changes**: Only `diagramAI.ts` updated to use `apiUrl()`
|
||||||
|
- **Environment variables**: `ANTHROPIC_API_KEY` already in `.env.local`
|
||||||
|
- **Node version**: Requires Node 18+ for native `fetch`
|
||||||
|
- **CORS**: Only enabled when `ALLOWED_ORIGINS` is set (split deployment)
|
||||||
|
- **Backward compatible**: Works as combined deployment by default
|
||||||
167
NEXT_MIGRATION_PLAN.md
Normal file
167
NEXT_MIGRATION_PLAN.md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# Vite to Next.js Migration Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Migrate from Vite to Next.js App Router to get proper API route support, with minimal changes to existing code.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
- **Router**: App Router with `'use client'` on all pages
|
||||||
|
- **Rendering**: CSR only (no SSR) - simplifies migration since BabylonJS can't SSR
|
||||||
|
- **API Routes**: Claude API now, structured for future expansion
|
||||||
|
- **External Proxies**: Keep sync/create-db/images as Next.js rewrites to deepdiagram.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Setup (No Breaking Changes)
|
||||||
|
|
||||||
|
### 1.1 Install Next.js
|
||||||
|
```bash
|
||||||
|
npm install next
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Create `next.config.js`
|
||||||
|
```javascript
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{ source: '/sync/:path*', destination: 'https://www.deepdiagram.com/sync/:path*' },
|
||||||
|
{ source: '/create-db', destination: 'https://www.deepdiagram.com/create-db' },
|
||||||
|
{ source: '/api/images', destination: 'https://www.deepdiagram.com/api/images' },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
webpack: (config, { isServer }) => {
|
||||||
|
config.experiments = { ...config.experiments, asyncWebAssembly: true };
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
module.exports = nextConfig;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 Update `tsconfig.json`
|
||||||
|
Add path alias:
|
||||||
|
```json
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": { "@/*": ["./*"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Create New Files
|
||||||
|
|
||||||
|
### 2.1 `src/react/providers.tsx` (extract from webApp.tsx)
|
||||||
|
- Move Auth0Provider and FeatureProvider wrapping here
|
||||||
|
- Add `'use client'` directive
|
||||||
|
- Handle window/document checks for SSR safety
|
||||||
|
|
||||||
|
### 2.2 `app/layout.tsx`
|
||||||
|
- Root layout with html/body tags
|
||||||
|
- Metadata (title, favicon from current index.html)
|
||||||
|
- Import global CSS
|
||||||
|
|
||||||
|
### 2.3 `app/globals.css`
|
||||||
|
```css
|
||||||
|
@import '../src/react/styles.css';
|
||||||
|
@import '@mantine/core/styles.css';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 `app/api/claude/[...path]/route.ts`
|
||||||
|
- POST handler that proxies to api.anthropic.com
|
||||||
|
- Injects `ANTHROPIC_API_KEY` from env
|
||||||
|
- Adds `x-api-key` and `anthropic-version` headers
|
||||||
|
|
||||||
|
### 2.5 Page files (all with `'use client'`)
|
||||||
|
| Route | File | Component |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| `/` | `app/page.tsx` | About |
|
||||||
|
| `/documentation` | `app/documentation/page.tsx` | Documentation |
|
||||||
|
| `/examples` | `app/examples/page.tsx` | Examples |
|
||||||
|
| `/pricing` | `app/pricing/page.tsx` | Pricing |
|
||||||
|
| `/db/[visibility]/[db]` | `app/db/[visibility]/[db]/page.tsx` | VrExperience |
|
||||||
|
| 404 | `app/not-found.tsx` | NotFound |
|
||||||
|
|
||||||
|
### 2.6 `src/react/components/ProtectedPage.tsx`
|
||||||
|
- Next.js version of route protection
|
||||||
|
- Uses `useRouter` from `next/navigation` for redirects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Modify Existing Files
|
||||||
|
|
||||||
|
### 3.1 `src/react/pages/vrExperience.tsx`
|
||||||
|
**Changes:**
|
||||||
|
- Remove `useParams()` from react-router-dom
|
||||||
|
- Accept `visibility` and `db` as props instead
|
||||||
|
- Replace `useNavigate()` with `useRouter()` from `next/navigation`
|
||||||
|
|
||||||
|
### 3.2 `src/react/pageHeader.tsx`
|
||||||
|
**Changes:**
|
||||||
|
- Replace `import {Link} from "react-router-dom"` with `import Link from "next/link"`
|
||||||
|
- Change `to={item.href}` to `href={item.href}` on Link components
|
||||||
|
|
||||||
|
### 3.3 `src/react/marketing/about.tsx`
|
||||||
|
**Changes:**
|
||||||
|
- Replace `useNavigate()` with `useRouter()` from `next/navigation`
|
||||||
|
- Change `navigate('/path')` to `router.push('/path')`
|
||||||
|
|
||||||
|
### 3.4 `package.json`
|
||||||
|
```json
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 3001",
|
||||||
|
"build": "node versionBump.js && next build",
|
||||||
|
"start": "next start -p 3001",
|
||||||
|
"test": "vitest",
|
||||||
|
"socket": "node server/server.js",
|
||||||
|
"serverBuild": "cd server && tsc"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Delete Old Files
|
||||||
|
|
||||||
|
| File | Reason |
|
||||||
|
|------|--------|
|
||||||
|
| `vite.config.ts` | Replaced by next.config.js |
|
||||||
|
| `index.html` | Next.js generates HTML |
|
||||||
|
| `src/webApp.ts` | Entry point no longer needed |
|
||||||
|
| `src/react/webRouter.tsx` | Replaced by app/ routing |
|
||||||
|
| `src/react/webApp.tsx` | Logic moved to providers.tsx |
|
||||||
|
| `src/react/components/ProtectedRoute.tsx` | Replaced by ProtectedPage.tsx |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Files to Modify
|
||||||
|
|
||||||
|
- `src/react/pages/vrExperience.tsx` - useParams -> props
|
||||||
|
- `src/react/pageHeader.tsx` - react-router Link -> Next.js Link
|
||||||
|
- `src/react/marketing/about.tsx` - useNavigate -> useRouter
|
||||||
|
- `src/react/webApp.tsx` - extract to providers.tsx
|
||||||
|
- `package.json` - scripts update
|
||||||
|
- `tsconfig.json` - path aliases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Order
|
||||||
|
|
||||||
|
1. Install next, create next.config.js
|
||||||
|
2. Update tsconfig.json
|
||||||
|
3. Create app/globals.css
|
||||||
|
4. Create src/react/providers.tsx
|
||||||
|
5. Create app/layout.tsx
|
||||||
|
6. Create app/api/claude/[...path]/route.ts
|
||||||
|
7. Create src/react/components/ProtectedPage.tsx
|
||||||
|
8. Modify vrExperience.tsx (accept props)
|
||||||
|
9. Create all app/*/page.tsx files
|
||||||
|
10. Modify pageHeader.tsx (Next.js Link)
|
||||||
|
11. Modify about.tsx (useRouter)
|
||||||
|
12. Update package.json scripts
|
||||||
|
13. Delete old files (vite.config.ts, index.html, webApp.ts, webRouter.tsx, webApp.tsx)
|
||||||
|
14. Test all routes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- **Havok WASM**: Move `HavokPhysics.wasm` to `public/` folder
|
||||||
|
- **react-router-dom**: Can be removed from dependencies after migration
|
||||||
|
- **vite devDependencies**: Can be removed (vite, vite-plugin-cp)
|
||||||
1006
package-lock.json
generated
1006
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -8,12 +8,12 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "node server.js",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"build": "node versionBump.js && vite build",
|
"build": "node versionBump.js && vite build",
|
||||||
"preview": "vite preview",
|
"start": "NODE_ENV=production node server.js",
|
||||||
|
"start:api": "API_ONLY=true node server.js",
|
||||||
"socket": "node server/server.js",
|
"socket": "node server/server.js",
|
||||||
"serve": "node server.js",
|
|
||||||
"serverBuild": "cd server && tsc",
|
"serverBuild": "cd server && tsc",
|
||||||
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps"
|
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps"
|
||||||
},
|
},
|
||||||
@ -27,38 +27,42 @@
|
|||||||
"@babylonjs/materials": "^8.16.2",
|
"@babylonjs/materials": "^8.16.2",
|
||||||
"@babylonjs/serializers": "^8.16.2",
|
"@babylonjs/serializers": "^8.16.2",
|
||||||
"@emotion/react": "^11.13.0",
|
"@emotion/react": "^11.13.0",
|
||||||
|
"@giphy/js-fetch-api": "^5.6.0",
|
||||||
|
"@giphy/react-components": "^9.6.0",
|
||||||
"@mantine/core": "^7.17.8",
|
"@mantine/core": "^7.17.8",
|
||||||
"@mantine/form": "^7.17.8",
|
"@mantine/form": "^7.17.8",
|
||||||
"@mantine/hooks": "^7.17.8",
|
"@mantine/hooks": "^7.17.8",
|
||||||
"@giphy/react-components": "^9.6.0",
|
|
||||||
"@giphy/js-fetch-api": "^5.6.0",
|
|
||||||
"@maptiler/client": "1.8.1",
|
"@maptiler/client": "1.8.1",
|
||||||
"@picovoice/cobra-web": "^2.0.3",
|
"@picovoice/cobra-web": "^2.0.3",
|
||||||
"@picovoice/eagle-web": "^1.0.0",
|
"@picovoice/eagle-web": "^1.0.0",
|
||||||
"@picovoice/web-voice-processor": "^4.0.9",
|
"@picovoice/web-voice-processor": "^4.0.9",
|
||||||
|
"@tabler/icons-react": "^3.14.0",
|
||||||
"@types/node": "^18.14.0",
|
"@types/node": "^18.14.0",
|
||||||
"@types/react": "^18.2.72",
|
"@types/react": "^18.2.72",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"canvas-hypertxt": "1.0.3",
|
"canvas-hypertxt": "1.0.3",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
|
"express": "^5.2.1",
|
||||||
"hash-wasm": "4.11.0",
|
"hash-wasm": "4.11.0",
|
||||||
"hls.js": "^1.1.4",
|
"hls.js": "^1.1.4",
|
||||||
"js-crypto-aes": "1.0.6",
|
"js-crypto-aes": "1.0.6",
|
||||||
"loglevel": "^1.9.1",
|
"loglevel": "^1.9.1",
|
||||||
"meaningful-string": "^1.4.0",
|
"meaningful-string": "^1.4.0",
|
||||||
"peer-lite": "2.0.2",
|
"peer-lite": "2.0.2",
|
||||||
"use-pouchdb": "^2.0.2",
|
|
||||||
"pouchdb": "^8.0.1",
|
"pouchdb": "^8.0.1",
|
||||||
"pouchdb-find": "^8.0.1",
|
"pouchdb-find": "^8.0.1",
|
||||||
"query-string": "^8.1.0",
|
"query-string": "^8.1.0",
|
||||||
"react-router-dom": "^6.26.1",
|
"react-router-dom": "^6.26.1",
|
||||||
"@tabler/icons-react": "^3.14.0",
|
|
||||||
"recordrtc": "^5.6.0",
|
"recordrtc": "^5.6.0",
|
||||||
"rfc4648": "^1.5.3",
|
"rfc4648": "^1.5.3",
|
||||||
"round": "^2.0.1",
|
"round": "^2.0.1",
|
||||||
"uint8-to-b64": "^1.0.2",
|
"uint8-to-b64": "^1.0.2",
|
||||||
|
"use-pouchdb": "^2.0.2",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
|
"vite-express": "^0.21.1",
|
||||||
"websocket": "^1.0.34",
|
"websocket": "^1.0.34",
|
||||||
"websocket-ts": "^2.1.5"
|
"websocket-ts": "^2.1.5"
|
||||||
},
|
},
|
||||||
|
|||||||
38
server.js
38
server.js
@ -1,13 +1,41 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import ViteExpress from "vite-express";
|
import ViteExpress from "vite-express";
|
||||||
|
import cors from "cors";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import expressProxy from "express-http-proxy";
|
import apiRoutes from "./server/api/index.js";
|
||||||
|
|
||||||
|
// Load .env.local first, then fall back to .env
|
||||||
|
dotenv.config({ path: '.env.local' });
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use("/api", expressProxy("local.immersiveidea.com"));
|
|
||||||
|
|
||||||
ViteExpress.listen(app, process.env.PORT || 3001, () => console.log("Server is listening..."));
|
// CORS configuration for split deployment
|
||||||
|
// In combined mode, same-origin requests don't need CORS
|
||||||
|
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [];
|
||||||
|
if (allowedOrigins.length > 0) {
|
||||||
|
app.use(cors({
|
||||||
|
origin: allowedOrigins,
|
||||||
|
credentials: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
app.use("/api", apiRoutes);
|
||||||
|
|
||||||
|
// Check if running in API-only mode (split deployment)
|
||||||
|
const apiOnly = process.env.API_ONLY === "true";
|
||||||
|
|
||||||
|
if (apiOnly) {
|
||||||
|
// API-only mode: no static file serving
|
||||||
|
app.listen(process.env.PORT || 3000, () => {
|
||||||
|
console.log(`API server running on port ${process.env.PORT || 3000}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Combined mode: Vite handles static files + SPA
|
||||||
|
ViteExpress.listen(app, process.env.PORT || 3001, () => {
|
||||||
|
console.log(`Server running on port ${process.env.PORT || 3001}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
38
server/api/claude.js
Normal file
38
server/api/claude.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const ANTHROPIC_API_URL = "https://api.anthropic.com";
|
||||||
|
|
||||||
|
// Express 5 uses named parameters for wildcards
|
||||||
|
router.post("/*path", async (req, res) => {
|
||||||
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return res.status(500).json({ error: "API key not configured" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the path after /api/claude (e.g., /v1/messages)
|
||||||
|
// Express 5 returns path segments as an array
|
||||||
|
const pathParam = req.params.path;
|
||||||
|
const path = "/" + (Array.isArray(pathParam) ? pathParam.join("/") : pathParam || "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${ANTHROPIC_API_URL}${path}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": apiKey,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(req.body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
res.status(response.status).json(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Claude API error:", error.message);
|
||||||
|
res.status(500).json({ error: "Failed to proxy request to Claude API" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
14
server/api/index.js
Normal file
14
server/api/index.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import claudeRouter from "./claude.js";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Claude API proxy
|
||||||
|
router.use("/claude", claudeRouter);
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
router.get("/health", (req, res) => {
|
||||||
|
res.json({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -39,23 +39,8 @@ export default defineConfig(({mode}) => {
|
|||||||
'^/api/images': {
|
'^/api/images': {
|
||||||
target: 'https://www.deepdiagram.com/',
|
target: 'https://www.deepdiagram.com/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
|
||||||
'^/api/claude': {
|
|
||||||
target: 'https://api.anthropic.com',
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/api\/claude/, ''),
|
|
||||||
configure: (proxy) => {
|
|
||||||
proxy.on('proxyReq', (proxyReq) => {
|
|
||||||
|
|
||||||
const apiKey = env.ANTHROPIC_API_KEY;
|
|
||||||
console.log(` API KEY: ${apiKey}`);
|
|
||||||
if (apiKey) {
|
|
||||||
proxyReq.setHeader('x-api-key', apiKey);
|
|
||||||
proxyReq.setHeader('anthropic-version', '2023-06-01');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// /api/claude is now handled by Express server
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -73,22 +58,8 @@ export default defineConfig(({mode}) => {
|
|||||||
'^/api/images': {
|
'^/api/images': {
|
||||||
target: 'https://www.deepdiagram.com/',
|
target: 'https://www.deepdiagram.com/',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
|
||||||
'^/api/claude': {
|
|
||||||
target: 'https://api.anthropic.com',
|
|
||||||
changeOrigin: true,
|
|
||||||
rewrite: (path) => path.replace(/^\/api\/claude/, ''),
|
|
||||||
configure: (proxy) => {
|
|
||||||
proxy.on('proxyReq', (proxyReq) => {
|
|
||||||
const apiKey = env.ANTHROPIC_API_KEY;
|
|
||||||
console.log(` API KEY: ${apiKey}`);
|
|
||||||
if (apiKey) {
|
|
||||||
proxyReq.setHeader('x-api-key', apiKey);
|
|
||||||
proxyReq.setHeader('anthropic-version', '2023-06-01');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// /api/claude is now handled by Express server
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
base: "/"
|
base: "/"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user