NODEDC_LAUNCHER/vite.config.ts

188 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react";
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import type { ServerResponse } from "node:http";
import { dirname, extname, join } from "node:path";
import { fileURLToPath } from "node:url";
const projectRoot = dirname(fileURLToPath(import.meta.url));
const maxStorageJsonBodyBytes = 260 * 1024 * 1024;
function localStorageApiPlugin(): Plugin {
return {
name: "nodedc-local-storage-api",
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const pathname = req.url?.split("?")[0];
if (req.method === "POST" && pathname === "/api/storage/upload") {
try {
const payload = await readJsonBody(req);
const result = await saveUploadedFile(payload);
sendJson(res, 200, result);
} catch (error) {
sendJson(res, 500, { error: error instanceof Error ? error.message : "Upload failed" });
}
return;
}
if (req.method === "POST" && pathname === "/api/storage/data") {
try {
const payload = await readJsonBody(req);
await saveLauncherData(payload);
sendJson(res, 200, { ok: true, url: "/storage/launcher-data.json" });
} catch (error) {
sendJson(res, 500, { error: error instanceof Error ? error.message : "Data save failed" });
}
return;
}
next();
});
},
configurePreviewServer(server) {
server.middlewares.use(async (req, res, next) => {
const pathname = req.url?.split("?")[0];
if (req.method === "POST" && pathname === "/api/storage/upload") {
try {
const payload = await readJsonBody(req);
const result = await saveUploadedFile(payload);
sendJson(res, 200, result);
} catch (error) {
sendJson(res, 500, { error: error instanceof Error ? error.message : "Upload failed" });
}
return;
}
if (req.method === "POST" && pathname === "/api/storage/data") {
try {
const payload = await readJsonBody(req);
await saveLauncherData(payload);
sendJson(res, 200, { ok: true, url: "/storage/launcher-data.json" });
} catch (error) {
sendJson(res, 500, { error: error instanceof Error ? error.message : "Data save failed" });
}
return;
}
next();
});
},
};
}
async function readJsonBody(req: NodeJS.ReadableStream) {
const chunks: Buffer[] = [];
let totalBytes = 0;
for await (const chunk of req) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
totalBytes += buffer.byteLength;
if (totalBytes > maxStorageJsonBodyBytes) {
throw new Error("Файл слишком большой для локального mock-storage");
}
chunks.push(buffer);
}
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
}
async function saveUploadedFile(payload: unknown) {
if (!isUploadPayload(payload)) {
throw new Error("Некорректный payload загрузки");
}
const match = /^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/s.exec(payload.dataUrl);
if (!match) {
throw new Error("Файл должен прийти data-url с base64");
}
const mimeType = payload.mimeType || match[1] || "application/octet-stream";
const storedName = buildStoredFileName(payload.fileName, mimeType);
const fileBuffer = Buffer.from(match[2], "base64");
await Promise.all(
getWritableStorageRoots().map(async (storageRoot) => {
const uploadDir = join(storageRoot, "uploads");
await mkdir(uploadDir, { recursive: true });
await writeFile(join(uploadDir, storedName), fileBuffer);
})
);
return {
ok: true,
url: `/storage/uploads/${storedName}`,
fileName: storedName,
originalFileName: payload.fileName,
mimeType,
};
}
async function saveLauncherData(payload: unknown) {
await Promise.all(
getWritableStorageRoots().map(async (storageRoot) => {
await mkdir(storageRoot, { recursive: true });
await writeFile(join(storageRoot, "launcher-data.json"), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
})
);
}
function getWritableStorageRoots() {
const roots = [join(projectRoot, "public", "storage")];
const distRoot = join(projectRoot, "dist");
if (existsSync(distRoot)) {
roots.push(join(distRoot, "storage"));
}
return roots;
}
function buildStoredFileName(fileName: string, mimeType: string) {
const extension = extname(fileName) || extensionFromMimeType(mimeType);
const rawBase = fileName.slice(0, extension ? -extension.length : undefined);
const safeBase =
rawBase
.normalize("NFKD")
.replace(/[^\w.-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80) || "upload";
return `${Date.now()}-${randomUUID().slice(0, 8)}-${safeBase}${extension.toLowerCase()}`;
}
function extensionFromMimeType(mimeType: string) {
if (mimeType === "image/jpeg") return ".jpg";
if (mimeType === "image/png") return ".png";
if (mimeType === "image/gif") return ".gif";
if (mimeType === "image/webp") return ".webp";
if (mimeType === "video/mp4") return ".mp4";
if (mimeType === "video/webm") return ".webm";
if (mimeType === "video/quicktime") return ".mov";
return "";
}
function sendJson(res: ServerResponse, statusCode: number, payload: unknown) {
res.statusCode = statusCode;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(payload));
}
function isUploadPayload(payload: unknown): payload is { fileName: string; mimeType: string; dataUrl: string } {
if (!payload || typeof payload !== "object") return false;
const candidate = payload as { fileName?: unknown; mimeType?: unknown; dataUrl?: unknown };
return typeof candidate.fileName === "string" && typeof candidate.mimeType === "string" && typeof candidate.dataUrl === "string";
}
export default defineConfig({
plugins: [react(), localStorageApiPlugin()],
});