// packages/sdk/src/dev-vite.ts import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'; import { loadConfigFromFile, loadEnv } from 'vite'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { existsSync } from 'node:fs'; import type { IncomingMessage, ServerResponse } from 'node:http'; import { createDevModuleApp, createDevModuleDeps, closeDevDbPool } from './dev/index.js'; import type { ModulePluginFactory } from './platform/types.js'; import { formatDevApiLog, formatDevBanner } from './dev-log.js'; import { toFetchRequest, writeFetchResponse } from './dev-vite-request.js'; import { ensureDevModuleUserRequest } from './dev/ensure-dev-user.js'; import type { ModuleRequestUser } from './platform/module-user.js'; export interface StagoDevApiPluginOptions { /** * API 入口(Vite SSR 路径,如 `/src/handlers/index.ts`) * @default 'src/handlers/index.ts' */ apiEntry?: string; /** 是否打印请求日志,默认 true */ logRequests?: boolean; /** 是否打印 SQL 日志,默认 true(可用 DB_LOG_SQL=false 关闭) */ logSql?: boolean; /** * 仅在显式传入时,对未带 x-stago-user 的请求注入该用户。 * 默认不注入(curl / 无头请求的 handler.user 为 null)。 * 需要默认开发用户时可写:`devUser: DEFAULT_DEV_MODULE_REQUEST_USER`(从 `@stago/sdk/dev` 导入)。 */ devUser?: ModuleRequestUser; } interface ModuleMeta { moduleName: string; moduleVersion: string; mountPrefix: string; } function toSsrEntry(rootDir: string, apiEntry: string): string { const normalized = apiEntry.replace(/\\/g, '/'); if (normalized.startsWith('/')) return normalized; return `/${resolve(rootDir, normalized).slice(rootDir.length).replace(/\\/g, '/')}`; } async function loadModuleMeta(rootDir: string): Promise { let moduleName = 'unknown-module'; let moduleVersion = '0.0.0'; let mountPrefix = ''; const pkgPath = resolve(rootDir, 'package.json'); if (existsSync(pkgPath)) { try { const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version?: string }; moduleVersion = pkg.version || moduleVersion; } catch { // ignore } } const stagoConfigPath = resolve(rootDir, 'stago.config.ts'); if (existsSync(stagoConfigPath)) { try { const loaded = await loadConfigFromFile( { command: 'build', mode: 'development', isSsrBuild: false, isPreview: false }, stagoConfigPath, rootDir, ); if (loaded?.config) { const cfg = loaded.config as { name?: string; version?: string; api?: { prefix?: string }; }; moduleName = cfg.name || moduleName; moduleVersion = cfg.version || moduleVersion; mountPrefix = cfg.api?.prefix || mountPrefix; } } catch { // ignore } } return { moduleName, moduleVersion, mountPrefix }; } interface DevRuntimeOptions { meta: ModuleMeta; logRequests: boolean; logSql?: boolean; devUser?: ModuleRequestUser; /** 注入 deps.env 的业务配置(来自 loadEnv),使 dev 行为与生产对齐 */ env: Record; } function createApiMiddleware(server: ViteDevServer, ssrEntry: string, runtime: DevRuntimeOptions) { const { meta, logRequests, logSql, devUser, env } = runtime; const log = (msg: string) => server.config.logger.info(msg); return async (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => { const method = req.method ?? 'GET'; const path = req.url ?? '/'; try { const mod = (await server.ssrLoadModule(ssrEntry)) as { default: ModulePluginFactory }; const factory = mod.default; if (typeof factory !== 'function') { return next(); } const deps = createDevModuleDeps({ moduleName: meta.moduleName, moduleVersion: meta.moduleVersion, mountPrefix: meta.mountPrefix, logSql, log, env, }); const routeBase = deps.routeBase; const pathname = path.split('?')[0] ?? '/'; if (routeBase && !pathname.startsWith(routeBase)) { return next(); } const start = Date.now(); const app = createDevModuleApp(factory, deps); const rawRequest = await toFetchRequest(req, path); // 仅显式配置 devUser 时才补头;默认不注入,避免隐式假登录 const request = devUser ? ensureDevModuleUserRequest(rawRequest, devUser) : rawRequest; const response = await app.fetch(request); await writeFetchResponse(res, response); if (logRequests) { log( formatDevApiLog({ method, path, status: response.status, durationMs: Date.now() - start, }), ); } } catch (err) { const message = err instanceof Error ? (err.message || err.stack || 'Unknown error') : String(err); console.error('[stago-dev-api]', message); if (err instanceof Error && err.stack) { console.error(err.stack); } res.statusCode = 500; res.setHeader('Content-Type', 'application/json'); res.end( JSON.stringify({ success: false, error: { code: 'DEV_API_ERROR', message }, }), ); } }; } /** * Vite 插件:开发模式在 Vite Dev Server 内挂载 Elysia 模块 API(支持 HMR) */ export function stagoDevApiPlugin(options: StagoDevApiPluginOptions = {}): Plugin { const apiEntry = options.apiEntry ?? 'src/handlers/index.ts'; const logRequests = options.logRequests ?? true; const logSql = options.logSql; const { devUser } = options; let resolvedConfig: ResolvedConfig | null = null; let moduleMeta: ModuleMeta | null = null; let ssrEntry = ''; return { name: 'stago-dev-api', apply: 'serve', configResolved(config) { resolvedConfig = config; ssrEntry = toSsrEntry(config.root, apiEntry); }, async configureServer(server) { if (!resolvedConfig) return; const env = loadEnv(server.config.mode, server.config.root, ''); for (const [key, value] of Object.entries(env)) { if (value != null) process.env[key] = value; } await closeDevDbPool(); const apiPath = resolve(resolvedConfig.root, apiEntry.replace(/^\//, '')); if (!existsSync(apiPath)) { server.config.logger.warn(`[stago-dev-api] API entry not found: ${apiPath}, skipping`); return; } moduleMeta = await loadModuleMeta(resolvedConfig.root); const middleware = createApiMiddleware(server, ssrEntry, { meta: moduleMeta, logRequests, logSql, devUser, env, }); server.middlewares.use(middleware); console.log( formatDevBanner(`[stago-dev-api] Elysia module API enabled (entry: ${ssrEntry}, HMR via Vite)`), ); }, async closeWatcher() { await closeDevDbPool(); }, }; }