velund
Version:
Модульная UI-система на Vite для полиглотных бэкендов
148 lines (145 loc) • 5.75 kB
JavaScript
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 };