import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; import path from 'path'; import {defineConfig, loadEnv} from 'vite'; import { getBcConfig, getBCToken, findItem, patchItemCpnpNo, fetchAllItems, buildWorkbook } from './bc-runtime.js'; import * as XLSX from 'xlsx'; // @ts-ignore import dropboxProxyHandler from './api/dropbox-proxy.js'; // @ts-ignore import dropboxSyncHandler from './api/dropbox-sync.js'; // @ts-ignore import backupHandler from './api/backup.js'; // @ts-ignore import usersAdminHandler from './api/users-admin.js'; async function readJsonBody(req: any): Promise { return await new Promise((resolve, reject) => { let raw = ''; req.on('data', (chunk: Buffer) => { raw += chunk.toString('utf8'); }); req.on('end', () => { if (!raw.trim()) return resolve({}); try { resolve(JSON.parse(raw)); } catch (err) { reject(err); } }); req.on('error', reject); }); } function createLocalApiMiddleware(env: Record, prodEnv: Record) { return async (req: any, res: any, next: any) => { try { const url = new URL(req.url || '/', 'http://localhost'); const config = getBcConfig(env); // Populate process.env with loaded env variables for serverless handlers. // The Dropbox proxy reads env at request time so preview/dev/production // can share the same handler implementation. Object.assign(process.env, prodEnv, env); // Helper to adapt Node.js req/res to Vercel signature. const adaptVercelHandler = async (handler: any) => { (req as any).query = Object.fromEntries(url.searchParams.entries()); try { (req as any).body = await readJsonBody(req); } catch { (req as any).body = {}; } (res as any).status = (code: number) => { res.statusCode = code; return res; }; (res as any).json = (data: any) => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(data)); }; (res as any).send = (data: any) => { if (data instanceof Buffer) { res.end(data); } else if (typeof data === 'object') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(data)); } else { res.end(data); } }; await handler(req, res); }; if (url.pathname === '/api/dropbox-proxy') { await adaptVercelHandler(dropboxProxyHandler); return; } if (url.pathname === '/api/dropbox-sync') { await adaptVercelHandler(dropboxSyncHandler); return; } if (url.pathname === '/api/backup') { await adaptVercelHandler(backupHandler); return; } if (url.pathname === '/api/users-admin') { await adaptVercelHandler(usersAdminHandler); return; } if (url.pathname === '/api/bc-proxy') { if (req.method === 'OPTIONS') { res.statusCode = 204; res.end(); return; } if (req.method !== 'POST') { res.statusCode = 405; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Method not allowed' })); return; } const body = await readJsonBody(req); const { articleNo, cpnpNo } = body || {}; if (!articleNo || cpnpNo === undefined) { res.statusCode = 400; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Missing articleNo or cpnpNo' })); return; } const token = await getBCToken(config); const item = await findItem(config, token, articleNo); await patchItemCpnpNo(config, token, item, String(cpnpNo)); res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ success: true, articleNo, cpnpNo })); return; } if (url.pathname === '/api/bc-export') { if (req.method === 'OPTIONS') { res.statusCode = 204; res.end(); return; } if (req.method !== 'GET') { res.statusCode = 405; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Method not allowed' })); return; } const token = await getBCToken(config); const items = await fetchAllItems(config, token); if (url.searchParams.get('format') === 'json') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ success: true, count: items.length, items })); return; } const workbook = buildWorkbook(items); const buffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' }); const dateStr = new Date().toISOString().split('T')[0]; res.statusCode = 200; res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', `attachment; filename="BusinessCentral_Items_${dateStr}.xlsx"`); res.setHeader('Cache-Control', 'no-store'); res.end(buffer); return; } } catch (err: any) { const message = err?.message || String(err); if ((req.url || '').startsWith('/api/bc-')) { res.statusCode = 500; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ success: false, error: message })); return; } console.error('[bc-local-api]', message); } next(); }; } export default defineConfig(({mode}) => { const env = loadEnv(mode, '.', ''); const prodEnv = loadEnv('production', '.', ''); const localApiMiddleware = createLocalApiMiddleware(env, prodEnv); return { plugins: [ react(), tailwindcss(), { name: 'bc-local-api', configureServer(server) { server.middlewares.use(localApiMiddleware); }, configurePreviewServer(server) { server.middlewares.use(localApiMiddleware); }, }, ], define: { 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY), }, resolve: { alias: { '@': path.resolve(__dirname, '.'), }, }, server: { // HMR is disabled in AI Studio via DISABLE_HMR env var. // Do not modify - file watching is disabled to prevent flickering during agent edits. hmr: process.env.DISABLE_HMR !== 'true', proxy: { '/dropbox-file': { target: 'https://www.dropbox.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/dropbox-file/, ''), }, }, }, }; });