mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
272 lines
7.8 KiB
JavaScript
272 lines
7.8 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { defineConfig } from "vite";
|
|
import react from "@vitejs/plugin-react";
|
|
import {
|
|
DEFAULT_CAD_DIRECTORY,
|
|
isCatalogRelevantPath,
|
|
isServedCadAsset,
|
|
normalizeCadDirectory,
|
|
repoRelativePath,
|
|
resolveCadDirectory,
|
|
scanCadDirectory,
|
|
} from "./lib/cadDirectoryScanner.mjs";
|
|
|
|
const DEFAULT_VIEWER_PORT = 4178;
|
|
const resolvedPort = Number.parseInt(process.env.VIEWER_PORT || process.env.GUI_PORT || process.env.PORT || "", 10);
|
|
const viewerPort = Number.isFinite(resolvedPort) ? resolvedPort : DEFAULT_VIEWER_PORT;
|
|
const viewerRoot = process.cwd();
|
|
const repoRoot = path.resolve(viewerRoot, "..");
|
|
const buildCadDir = normalizeCadDirectory(process.env.CAD_DIR || DEFAULT_CAD_DIRECTORY);
|
|
|
|
function emptyCatalog(dir = DEFAULT_CAD_DIRECTORY) {
|
|
const normalizedDir = normalizeCadDirectory(dir);
|
|
return {
|
|
schemaVersion: 3,
|
|
root: {
|
|
dir: normalizedDir,
|
|
name: path.basename(normalizedDir) || normalizedDir,
|
|
path: normalizedDir,
|
|
},
|
|
entries: [],
|
|
};
|
|
}
|
|
|
|
function readCadCatalog(dir = buildCadDir) {
|
|
try {
|
|
return scanCadDirectory({ repoRoot, dir });
|
|
} catch {
|
|
return emptyCatalog(dir);
|
|
}
|
|
}
|
|
|
|
function pathIsInside(childPath, parentPath) {
|
|
const relativePath = path.relative(path.resolve(parentPath), path.resolve(childPath));
|
|
return Boolean(relativePath) && !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
|
|
}
|
|
|
|
function serveStaticFile(root, requestUrl, res, next, { allow } = {}) {
|
|
const requestPath = String(requestUrl || "").replace(/\?.*$/, "");
|
|
let decodedRequestPath = "";
|
|
try {
|
|
decodedRequestPath = decodeURIComponent(requestPath);
|
|
} catch {
|
|
res.statusCode = 400;
|
|
res.end("Bad request");
|
|
return true;
|
|
}
|
|
const filePath = path.resolve(root, decodedRequestPath.replace(/^\/+/, ""));
|
|
if (
|
|
!(filePath === path.resolve(root) || pathIsInside(filePath, root))
|
|
|| (typeof allow === "function" && !allow(filePath))
|
|
) {
|
|
res.statusCode = 403;
|
|
res.end("Forbidden");
|
|
return true;
|
|
}
|
|
fs.stat(filePath, (error, stats) => {
|
|
if (error || !stats.isFile()) {
|
|
next();
|
|
return;
|
|
}
|
|
fs.createReadStream(filePath).pipe(res);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
function copyRecursiveFiltered(sourceRoot, destinationRoot, predicate) {
|
|
if (!fs.existsSync(sourceRoot)) {
|
|
return;
|
|
}
|
|
for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
|
|
const sourcePath = path.join(sourceRoot, entry.name);
|
|
const destinationPath = path.join(destinationRoot, entry.name);
|
|
if (entry.isDirectory()) {
|
|
copyRecursiveFiltered(sourcePath, destinationPath, predicate);
|
|
continue;
|
|
}
|
|
if (!predicate(sourcePath)) {
|
|
continue;
|
|
}
|
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
fs.copyFileSync(sourcePath, destinationPath);
|
|
}
|
|
}
|
|
|
|
function sendJson(res, statusCode, payload) {
|
|
res.statusCode = statusCode;
|
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
res.setHeader("cache-control", "no-store");
|
|
res.end(JSON.stringify(payload));
|
|
}
|
|
|
|
function cadCatalogPlugin() {
|
|
const virtualId = "virtual:cad-catalog";
|
|
const resolvedVirtualId = `\0${virtualId}`;
|
|
let resolvedConfig = null;
|
|
const activeDirectories = new Map();
|
|
const refreshTimers = new Map();
|
|
|
|
function activateDirectory(server, dir) {
|
|
const resolved = resolveCadDirectory(repoRoot, dir);
|
|
activeDirectories.set(resolved.rootPath, resolved.dir);
|
|
server.watcher.add(resolved.rootPath);
|
|
return resolved;
|
|
}
|
|
|
|
function scheduleCatalogRefresh(server, rootPath, dir) {
|
|
if (refreshTimers.has(rootPath)) {
|
|
clearTimeout(refreshTimers.get(rootPath));
|
|
}
|
|
refreshTimers.set(rootPath, setTimeout(() => {
|
|
refreshTimers.delete(rootPath);
|
|
server.ws.send({
|
|
type: "custom",
|
|
event: "cad-catalog:changed",
|
|
data: { dir },
|
|
});
|
|
}, 150));
|
|
}
|
|
|
|
function notifyChangedPath(server, changedPath) {
|
|
const resolvedChangedPath = path.resolve(changedPath);
|
|
if (!isCatalogRelevantPath(resolvedChangedPath)) {
|
|
return;
|
|
}
|
|
for (const [rootPath, dir] of activeDirectories.entries()) {
|
|
if (resolvedChangedPath === rootPath || pathIsInside(resolvedChangedPath, rootPath)) {
|
|
scheduleCatalogRefresh(server, rootPath, dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
name: "cad-catalog",
|
|
configResolved(config) {
|
|
resolvedConfig = config;
|
|
},
|
|
resolveId(id) {
|
|
if (id === virtualId) {
|
|
return resolvedVirtualId;
|
|
}
|
|
return null;
|
|
},
|
|
load(id) {
|
|
if (id !== resolvedVirtualId) {
|
|
return null;
|
|
}
|
|
const catalog = readCadCatalog(buildCadDir);
|
|
return `export default ${JSON.stringify(catalog)};`;
|
|
},
|
|
configureServer(server) {
|
|
activateDirectory(server, buildCadDir);
|
|
server.middlewares.use((req, res, next) => {
|
|
const requestUrl = new URL(req.url || "/", "http://localhost");
|
|
if (requestUrl.pathname !== "/__cad/catalog") {
|
|
next();
|
|
return;
|
|
}
|
|
const dir = requestUrl.searchParams.get("dir") || DEFAULT_CAD_DIRECTORY;
|
|
let catalog;
|
|
try {
|
|
const resolved = activateDirectory(server, dir);
|
|
catalog = scanCadDirectory({ repoRoot, dir: resolved.dir });
|
|
} catch (error) {
|
|
sendJson(res, 400, {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
return;
|
|
}
|
|
sendJson(res, 200, catalog);
|
|
});
|
|
server.middlewares.use((req, res, next) => {
|
|
const requestPath = String(req.url || "").replace(/\?.*$/, "");
|
|
let decodedRequestPath = "";
|
|
try {
|
|
decodedRequestPath = decodeURIComponent(requestPath);
|
|
} catch {
|
|
next();
|
|
return;
|
|
}
|
|
const candidatePath = path.resolve(repoRoot, decodedRequestPath.replace(/^\/+/, ""));
|
|
if (!isServedCadAsset(candidatePath)) {
|
|
next();
|
|
return;
|
|
}
|
|
serveStaticFile(repoRoot, req.url, res, next, {
|
|
allow: (filePath) => (
|
|
isServedCadAsset(filePath) &&
|
|
(filePath === repoRoot || pathIsInside(filePath, repoRoot))
|
|
),
|
|
});
|
|
});
|
|
for (const eventName of ["add", "change", "unlink"]) {
|
|
server.watcher.on(eventName, (changedPath) => notifyChangedPath(server, changedPath));
|
|
}
|
|
},
|
|
writeBundle() {
|
|
const outDir = resolvedConfig?.build?.outDir || "dist";
|
|
const resolved = resolveCadDirectory(repoRoot, buildCadDir);
|
|
const cadDestinationRoot = path.resolve(viewerRoot, outDir, repoRelativePath(repoRoot, resolved.rootPath));
|
|
copyRecursiveFiltered(resolved.rootPath, cadDestinationRoot, (filePath) => {
|
|
return isServedCadAsset(filePath);
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineConfig({
|
|
plugins: [react(), cadCatalogPlugin()],
|
|
resolve: {
|
|
alias: {
|
|
"@": viewerRoot,
|
|
},
|
|
},
|
|
esbuild: {
|
|
loader: "jsx",
|
|
include: /.*\.[jt]sx?$/,
|
|
exclude: [],
|
|
},
|
|
optimizeDeps: {
|
|
esbuildOptions: {
|
|
loader: {
|
|
".js": "jsx",
|
|
},
|
|
},
|
|
},
|
|
build: {
|
|
chunkSizeWarningLimit: 800,
|
|
rollupOptions: {
|
|
output: {
|
|
manualChunks(id) {
|
|
if (!id.includes("node_modules")) {
|
|
return undefined;
|
|
}
|
|
if (id.includes("/three/")) {
|
|
return "vendor-three";
|
|
}
|
|
if (id.includes("/react/") || id.includes("/react-dom/")) {
|
|
return "vendor-react";
|
|
}
|
|
if (id.includes("/radix-ui/") || id.includes("/@radix-ui/")) {
|
|
return "vendor-ui";
|
|
}
|
|
if (id.includes("/lucide-react/")) {
|
|
return "vendor-icons";
|
|
}
|
|
return undefined;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
server: {
|
|
host: "127.0.0.1",
|
|
port: viewerPort,
|
|
strictPort: true,
|
|
},
|
|
preview: {
|
|
host: "127.0.0.1",
|
|
port: viewerPort,
|
|
strictPort: true,
|
|
},
|
|
});
|