All files / src/plugin vitek-preview.ts

1.92% Statements 1/52
0% Branches 0/37
10% Functions 1/10
2% Lines 1/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133                                    27x                                                                                                                                                                                                                                    
/**
 * vitek:preview — configurePreviewServer, bundle loading
 */
 
import * as path from 'path';
import * as fs from 'fs';
import { pathToFileURL } from 'url';
import { createRequestHandler } from '../core/server/request-handler.js';
import { createSocketHandler } from '../core/socket/socket-handler.js';
import { getApiBundleFilename } from '../build/build-api-bundle.js';
import { getSocketsBundleFilename } from '../build/build-sockets-bundle.js';
import { API_BASE_PATH, getSocketBasePath } from '../shared/constants.js';
import type { Plugin } from 'vite';
import type { ApiClient, SocketEmitter, VitekApp } from '../core/shared/vitek-app.js';
import type { RequestHandlerOptions } from '../core/server/request-handler.js';
import type { PluginContext } from './context.js';
 
export function createPreviewPlugin(ctx: PluginContext): Plugin {
  return {
    name: 'vitek:preview',
 
    async configurePreviewServer(server) {
      if (!ctx.buildApi || !ctx.root || !ctx.buildOutDir) return;
      const bundlePath = path.join(ctx.buildOutDir, getApiBundleFilename());
      if (!fs.existsSync(bundlePath)) {
        server.config.logger.warn(
          '[vitek] API bundle not found; preview serving static assets only. Run `vite build` first.'
        );
        return;
      }
 
      const previewPort = server.config.preview?.port ?? 4173;
      const apiBaseUrl = `http://127.0.0.1:${previewPort}${API_BASE_PATH}`;
      const api: ApiClient = {
        async fetch(path: string, fetchOptions?: { method?: string; body?: unknown }) {
          const url = `${apiBaseUrl}/${path.replace(/^\//, '')}`;
          const res = await fetch(url, {
            method: fetchOptions?.method ?? 'GET',
            headers:
              fetchOptions?.body !== undefined
                ? { 'Content-Type': 'application/json' }
                : undefined,
            body:
              fetchOptions?.body !== undefined
                ? JSON.stringify(fetchOptions.body)
                : undefined,
          });
          const text = await res.text();
          if (!text) return undefined;
          try {
            return JSON.parse(text);
          } catch {
            return text;
          }
        },
      };
      const noopSockets: SocketEmitter = { emit() {} };
      const shared: VitekApp = { sockets: noopSockets, api };
 
      const bundleUrl = pathToFileURL(bundlePath).href;
      const bundleLoadPromise = import(bundleUrl) as Promise<{
        routes: RequestHandlerOptions['routes'];
        middlewares: RequestHandlerOptions['middlewares'];
      }>;
 
      let apiHandler: ReturnType<typeof createRequestHandler> | null = null;
 
      const apiMiddleware = (req: import('http').IncomingMessage, res: import('http').ServerResponse, next: () => void) => {
        const pathname = req.url?.split('?')[0] ?? '';
        if (pathname !== API_BASE_PATH && !pathname.startsWith(API_BASE_PATH + '/')) {
          return next();
        }
        bundleLoadPromise
          .then((mod) => {
            if (!apiHandler) {
              const beforeApiRequest = (ctx.options.plugins ?? [])
              .filter((p): p is typeof p & { beforeApiRequest: NonNullable<typeof p.beforeApiRequest> } => !!p.beforeApiRequest)
              .map((p) => (hookCtx: { req: import('http').IncomingMessage; res: import('http').ServerResponse; path: string; method: string }, next: () => void) =>
                p.beforeApiRequest!({ ...hookCtx, next })
              );
            apiHandler = createRequestHandler({
                routes: mod.routes,
                middlewares: mod.middlewares,
                beforeApiRequest,
                cors: ctx.options.cors,
                trustProxy: ctx.options.trustProxy,
                maxBodySize: ctx.options.maxBodySize,
                onError: ctx.options.onError,
                shared,
              });
              server.config.logger.info('[vitek] API middleware registered for preview');
            }
            apiHandler(req, res, next);
          })
          .catch((err) => {
            server.config.logger.error(
              `[vitek] Failed to load API bundle: ${err instanceof Error ? err.message : String(err)}`
            );
            res.statusCode = 500;
            res.setHeader('Content-Type', 'application/json');
            res.end(JSON.stringify({ error: 'Internal server error', message: 'Failed to load API bundle' }));
          });
      };
 
      server.middlewares.use(apiMiddleware);
 
      const socketsEnabled = ctx.options.sockets !== false;
      const socketBasePath = getSocketBasePath(
        ctx.options.apiBasePath,
        typeof ctx.options.sockets === 'object' ? ctx.options.sockets?.path : undefined
      );
      const socketsBundlePath = path.join(ctx.buildOutDir, getSocketsBundleFilename());
      if (socketsEnabled && fs.existsSync(socketsBundlePath)) {
        try {
          const socketsUrl = pathToFileURL(socketsBundlePath).href;
          const mod = await import(socketsUrl) as { sockets: Parameters<typeof createSocketHandler>[0]['sockets'] };
          const handler = createSocketHandler({
            sockets: mod.sockets,
            socketBasePath,
            shared,
          });
          server.httpServer?.on('upgrade', handler);
          server.config.logger.info('[vitek] WebSocket sockets registered for preview');
        } catch (err) {
          server.config.logger.warn(
            `[vitek] Failed to load sockets bundle: ${err instanceof Error ? err.message : String(err)}`
          );
        }
      }
    },
  };
}