UNPKG

velund

Version:

Модульная UI-система на Vite для полиглотных бэкендов

148 lines (145 loc) 5.75 kB
import { TypeCompiler } from '@sinclair/typebox/compiler'; import chokidar from 'chokidar'; import url from 'url'; import { match } from 'path-to-regexp'; import genHomePage from './pages/home.js'; import gen404Page from './pages/404.js'; import genErrorPage from './pages/error.js'; function devServer(options, renderer) { let entry; let app; let registeredComponents = /* @__PURE__ */ new Map(); const updateTemplates = async (server) => { const input = entry || "src/main.ts"; const entryModule = await server.ssrLoadModule(input); const newComponentNames = []; const newComponents = []; registeredComponents = /* @__PURE__ */ new Map(); if (entryModule?.default?.components) { app = entryModule.default; } app?.components?.forEach((comp) => { if (!comp) return; newComponents.push(comp); newComponentNames.push(comp.name); registeredComponents.set(comp.name, comp); }); renderer.setComponents(newComponents); }; function formatValidationErrors(errors) { const errorsByPath = {}; for (const error of errors) { const dotPath = error.path.replace(/^\//, "").replace(/\//g, "."); if (!errorsByPath[dotPath]) { errorsByPath[dotPath] = []; } errorsByPath[dotPath].push(error); } const formattedErrors = []; for (const [path, pathErrors] of Object.entries(errorsByPath)) { const messages = pathErrors.map((error) => error.message); const uniqueMessages = [...new Set(messages)]; const fieldName = path || "root"; formattedErrors.push(`• ${fieldName}: ${uniqueMessages.join("; ")}`); } return formattedErrors.join("\n"); } return { configResolved(resolvedConfig) { const config = resolvedConfig; entry = (typeof config.build === "object" ? Array.isArray(config.build.rollupOptions.input) ? config.build.rollupOptions.input[0] : typeof config.build.rollupOptions.input === "object" ? config.build.rollupOptions.input[Object.keys(config.build.rollupOptions.input)[0]] : config.build.rollupOptions.input : "") || ""; }, configureServer(server) { const watcher = chokidar.watch("./src", { ignoreInitial: true }); watcher.on("all", async () => { await updateTemplates(server); server.ws.send({ type: "full-reload", path: "*" }); }); server.middlewares.use(async (req, res, next) => { try { if (!req.url || req.url.startsWith("/src") || req.url.startsWith("/@") || req.url.includes("node_modules") || req.url.includes("vite")) { return next(); } const parsedUrl = url.parse(req.url); await updateTemplates(server); let isMetaRender = false; let component; let context = {}; if (app) { for (const route of app.routes) { const matched = match(route.path, { decode: decodeURIComponent })( req.url ); if (matched) { component = route.component; context = matched.params; break; } } } if (!component) { if (["", "/"].includes(parsedUrl.pathname || "")) { res.writeHead(302, { Location: "/__velund" }); res.end(); return; } if (parsedUrl.pathname?.replace(/\/+$/, "") == "/__velund") { res.setHeader("Content-Type", `text/html; charset=utf-8`); res.end(genHomePage(Array.from(registeredComponents.values()))); return; } isMetaRender = parsedUrl.pathname?.toString().replace(/\/+$/, "").startsWith(options.renderUrl) || false; const query = Object.fromEntries( new URLSearchParams(parsedUrl.query || "") ); const componentName = isMetaRender ? query.component : parsedUrl.pathname?.slice(1).replace(/\/+$/, ""); context = { ...context, ...isMetaRender ? JSON.parse(query?.context || "{}") : query?.props || query }; component = registeredComponents.get(componentName || ""); } if (component) { const validateSchema = component?.prepare ? component?.propsSchema || null : component?.contextSchema || null; if (validateSchema) { const cSchema = TypeCompiler.Compile(validateSchema); if (!cSchema.Check(context || {})) { console.warn( `[WARN]: Invalid context data for "${component.name}":` ); console.warn( formatValidationErrors([...cSchema.Errors(context || {})]) ); } } const renderResult = await renderer.render( component.name, context || {}, true ); renderResult.html = renderResult.html.replace(/@\//g, "/src/"); res.setHeader( "Content-Type", `${isMetaRender ? "application/json" : "text/html"}; charset=utf-8` ); res.end( isMetaRender ? JSON.stringify(renderResult, null, " ") : renderResult.html + '\n<script type="module" src="/@vite/client"><\/script>' ); return; } else { res.statusCode = 404; res.end(gen404Page()); return; } } catch (err) { console.error("[ERROR] Velund render error:", err); res.statusCode = 500; res.end(genErrorPage(err)); } }); } }; } export { devServer as default };