// @stago/sdk - API Plugin Build // Bundles module Elysia plugin factory into dist/api.js for dynamic loading by stago import type { Plugin, ResolvedConfig } from 'vite'; import { loadConfigFromFile } from 'vite'; import { resolve, join, dirname } from 'path'; import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { build } from 'esbuild'; export interface StagoApiPluginOptions { /** * Path to Stago module config file, relative to Vite root. * @default "stago.config.ts" */ configFile?: string; /** * Output file name. * @default "api.js" */ outputFileName?: string; } /** * Vite plugin that bundles API handlers into dist/api.js * * @description Reads api.handlers path from stago.config.ts, * bundles the handlers using esbuild, and outputs to dist/api.js. * This enables stago to dynamically load and execute module APIs. * * @example * ```typescript * // vite.config.ts * import { stagoConfigPlugin, stagoApiPlugin } from '@stago/sdk/vite' * * export default defineConfig({ * plugins: [ * stagoConfigPlugin(), * stagoApiPlugin() // Bundle handlers to dist/api.js * ] * }) * ``` */ export function stagoApiPlugin(options: StagoApiPluginOptions = {}): Plugin { const configFileName = options.configFile || 'stago.config.ts'; const outputFileName = options.outputFileName || 'api.js'; let resolvedConfig: ResolvedConfig | null = null; return { name: 'stago-api-plugin', apply: 'build', configResolved(config: ResolvedConfig) { resolvedConfig = config; }, async closeBundle() { if (!resolvedConfig) return; const rootDir = resolvedConfig.root || process.cwd(); const outDir = resolve(rootDir, resolvedConfig.build.outDir); const configPath = resolve(rootDir, configFileName); // Check if config exists if (!existsSync(configPath)) { console.log('[stago-api] No stago.config.ts found, skipping API bundle'); return; } // Load config try { const loaded = await loadConfigFromFile( { command: 'build', mode: resolvedConfig.mode || 'production', isSsrBuild: false, isPreview: false, }, configPath, rootDir, ); const stagoConfig = loaded?.config as { name?: string; api?: { handlers?: string; prefix?: string; }; } | undefined; if (!stagoConfig?.api?.handlers) { console.log('[stago-api] No api.handlers configured, skipping API bundle'); return; } const handlersPath = resolve(rootDir, stagoConfig.api.handlers); // Check if handlers directory/file exists if (!existsSync(handlersPath) && !existsSync(dirname(handlersPath))) { console.warn('[stago-api] Handlers path not found:', handlersPath); return; } // Determine entry point // handlers can be a directory (e.g., './src/handlers') or file (e.g., './src/handlers/index.ts') let entryPoint = handlersPath; if (!handlersPath.endsWith('.ts') && !handlersPath.endsWith('.js')) { // It's a directory, look for index.ts const indexPath = join(handlersPath, 'index.ts'); if (existsSync(indexPath)) { entryPoint = indexPath; } else { console.warn('[stago-api] No index.ts found in handlers directory:', handlersPath); return; } } // Bundle with esbuild const outputPath = join(outDir, outputFileName); console.log('[stago-api] Bundling handlers:', entryPoint); console.log('[stago-api] Output:', outputPath); await build({ entryPoints: [entryPoint], bundle: true, outfile: outputPath, format: 'esm', platform: 'node', target: 'node18', external: [ // @stago/sdk is provided by @stago/api at runtime // Node.js will resolve it from api's node_modules via NODE_PATH '@stago/sdk', 'pg', 'elysia', // Exclude node built-ins 'fs', 'path', 'crypto', 'stream', 'util', 'events', ], minify: false, // Keep readable for debugging sourcemap: false, }); console.log('[stago-api] Successfully bundled API handlers'); } catch (err) { console.error('[stago-api] Failed to bundle API handlers:', err); // Don't throw - allow build to continue even if API bundling fails } }, }; }