422 lines
13 KiB
JavaScript
422 lines
13 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { defineConfig } from "vite";
|
|
import react from "@vitejs/plugin-react";
|
|
import {
|
|
isCatalogRelevantPath,
|
|
} from "./src/server/catalog/cadDirectoryScanner.mjs";
|
|
import {
|
|
normalizeViewerDefaultFile,
|
|
normalizeViewerGithubUrl,
|
|
} from "./src/shared/viewerConfig.mjs";
|
|
import {
|
|
DEFAULT_VIEWER_PORT,
|
|
buildViewerServerInfo,
|
|
} from "./src/server/viewerServerInfo.mjs";
|
|
import {
|
|
removeViewerServerRegistryEntry,
|
|
writeViewerServerRegistry,
|
|
} from "./src/server/viewerServerRegistry.mjs";
|
|
import {
|
|
pathIsInside,
|
|
} from "cadjs/lib/pathUtils.mjs";
|
|
import { resolveDirectoryRoot as resolveViewerDirectoryRoot } from "./src/server/directoryRoot.mjs";
|
|
import { createLocalAssetBackend } from "./src/server/localAssetBackend.mjs";
|
|
import {
|
|
createCadViewerApiMiddleware,
|
|
createLocalAssetMiddleware,
|
|
} from "./src/server/httpHandlers.mjs";
|
|
import {
|
|
assertNoDeprecatedLocalRootEnv,
|
|
normalizeViewerAssetBackend,
|
|
} from "./src/server/viewerEnv.mjs";
|
|
import {
|
|
normalizeServerLifetimeMs,
|
|
scheduleProcessShutdown,
|
|
} from "./src/server/serverLifetime.mjs";
|
|
|
|
const viewerAppRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
const viewerClientRoot = path.join(viewerAppRoot, "src", "client");
|
|
const cadJsPackageRoot = resolveCadJsPackageRoot();
|
|
const viewerNodeModulesRoot = path.join(viewerAppRoot, "node_modules");
|
|
const defaultDirectoryRoot = path.resolve(viewerAppRoot, "..");
|
|
const directoryRoot = resolveDirectoryRoot();
|
|
const repoRoot = directoryRoot;
|
|
normalizeViewerAssetBackend(process.env.VIEWER_ASSET_BACKEND);
|
|
const buildViewerDefaultFile = normalizeViewerDefaultFile(process.env.VIEWER_DEFAULT_FILE ?? "");
|
|
const buildViewerGithubUrl = normalizeViewerGithubUrl(process.env.VIEWER_GITHUB_URL ?? "");
|
|
const buildViewerDefaultDir = String(process.env.VIEWER_DEFAULT_DIR || "").trim();
|
|
const viewerAllowedHosts = normalizeViewerAllowedHosts(process.env.VIEWER_ALLOWED_HOSTS ?? "");
|
|
const viewerServerLifetimeMs = normalizeServerLifetimeMs(process.env.VIEWER_SERVER_LIFETIME_MS);
|
|
assertNoDeprecatedLocalRootEnv(process.env);
|
|
const viewerVersion = readViewerPackageVersion(viewerAppRoot);
|
|
const viewerServerMode = String(process.env.VIEWER_AGENT_START_MODE || "dev").trim() || "dev";
|
|
const viewerGit = String(process.env.VIEWER_GIT || "").trim();
|
|
const localServerFeatures = [
|
|
"dynamic-root",
|
|
"relative-dir-query",
|
|
"default-dir",
|
|
"directory-activation",
|
|
];
|
|
const localAssetBackend = createLocalAssetBackend({
|
|
directoryRoot,
|
|
rootDir: buildViewerDefaultDir,
|
|
defaultFile: buildViewerDefaultFile,
|
|
githubUrl: buildViewerGithubUrl,
|
|
});
|
|
|
|
function normalizeViewerAllowedHosts(value) {
|
|
return String(value || "")
|
|
.split(",")
|
|
.map((host) => host.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function readViewerPackageVersion(appRoot) {
|
|
try {
|
|
const packageJson = JSON.parse(fs.readFileSync(path.join(appRoot, "package.json"), "utf8"));
|
|
return String(packageJson.version || "");
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function findRootPackageSrc(packageDirName) {
|
|
let current = viewerAppRoot;
|
|
for (;;) {
|
|
const candidate = path.join(current, "packages", packageDirName, "src");
|
|
if (
|
|
fs.existsSync(candidate) &&
|
|
fs.existsSync(path.join(current, "packages", packageDirName, "package.json"))
|
|
) {
|
|
return candidate;
|
|
}
|
|
const next = path.dirname(current);
|
|
if (next === current) {
|
|
return "";
|
|
}
|
|
current = next;
|
|
}
|
|
}
|
|
|
|
function resolveCadJsPackageRoot() {
|
|
const bundledPackageSrc = path.join(viewerAppRoot, "packages", "cadjs", "src");
|
|
if (fs.existsSync(bundledPackageSrc)) {
|
|
return bundledPackageSrc;
|
|
}
|
|
const rootPackageSrc = findRootPackageSrc("cadjs");
|
|
if (rootPackageSrc) {
|
|
return rootPackageSrc;
|
|
}
|
|
const installedPackageSrc = path.join(viewerAppRoot, "node_modules", "cadjs", "src");
|
|
if (fs.existsSync(installedPackageSrc)) {
|
|
return installedPackageSrc;
|
|
}
|
|
return path.resolve(viewerAppRoot, "../packages/cadjs/src");
|
|
}
|
|
|
|
function resolveDirectoryRoot() {
|
|
return resolveViewerDirectoryRoot({
|
|
env: process.env,
|
|
cwd: process.cwd(),
|
|
appRoot: viewerAppRoot,
|
|
defaultDirectoryRoot,
|
|
});
|
|
}
|
|
|
|
function viteServerPort(server) {
|
|
const address = server?.httpServer?.address?.();
|
|
return address && typeof address === "object" && Number.isInteger(address.port)
|
|
? address.port
|
|
: DEFAULT_VIEWER_PORT;
|
|
}
|
|
|
|
function isGenerationStatusFilePath(filePath) {
|
|
const name = path.basename(String(filePath || ""));
|
|
return name.startsWith(".") && name.endsWith(".generation.lock.json");
|
|
}
|
|
|
|
function cadCatalogPlugin({ enableStepArtifactBackend = false } = {}) {
|
|
const activeDirectories = new Map();
|
|
const refreshTimers = new Map();
|
|
const pendingRefreshes = new Map();
|
|
|
|
function activateDirectory(server, resolvedRoot) {
|
|
const resolved = resolvedRoot && typeof resolvedRoot === "object"
|
|
? resolvedRoot
|
|
: localAssetBackend.resolveRoot(resolvedRoot);
|
|
const wasActive = activeDirectories.has(resolved.rootPath);
|
|
activeDirectories.set(resolved.rootPath, {
|
|
dir: resolved.dir || "",
|
|
});
|
|
if (!wasActive) {
|
|
server.watcher.add(resolved.rootPath);
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
function activeDirectoryOptions({ rootDir = "" } = {}) {
|
|
const options = [];
|
|
const addOption = (option = {}) => {
|
|
const dir = String(option.dir || "").trim();
|
|
if (!dir) {
|
|
return;
|
|
}
|
|
const rootPath = String(option.rootPath || "").trim();
|
|
if (options.some((current) => current.dir === dir || (rootPath && current.rootPath === rootPath))) {
|
|
return;
|
|
}
|
|
options.push({
|
|
dir,
|
|
rootPath,
|
|
rootName: String(option.rootName || (rootPath ? path.basename(rootPath) : "") || dir),
|
|
});
|
|
};
|
|
for (const [rootPath, activeRoot] of activeDirectories.entries()) {
|
|
addOption({
|
|
dir: activeRoot.dir,
|
|
rootPath,
|
|
rootName: path.basename(rootPath),
|
|
});
|
|
}
|
|
addOption({ dir: rootDir });
|
|
return options;
|
|
}
|
|
|
|
function scheduleCatalogRefresh(server, rootPath, activeRoot = {}, changedPath = "") {
|
|
const rootState = typeof activeRoot === "string"
|
|
? { dir: activeRoot }
|
|
: {
|
|
dir: String(activeRoot?.dir || ""),
|
|
};
|
|
if (refreshTimers.has(rootPath)) {
|
|
clearTimeout(refreshTimers.get(rootPath));
|
|
}
|
|
const pending = pendingRefreshes.get(rootPath) || {
|
|
dir: rootState.dir,
|
|
paths: new Set(),
|
|
full: false,
|
|
};
|
|
pending.dir = rootState.dir;
|
|
if (changedPath) {
|
|
pending.paths.add(path.resolve(changedPath));
|
|
} else {
|
|
pending.full = true;
|
|
}
|
|
pendingRefreshes.set(rootPath, pending);
|
|
refreshTimers.set(rootPath, setTimeout(() => {
|
|
refreshTimers.delete(rootPath);
|
|
const nextRefresh = pendingRefreshes.get(rootPath) || {
|
|
dir: rootState.dir,
|
|
paths: new Set(),
|
|
full: true,
|
|
};
|
|
pendingRefreshes.delete(rootPath);
|
|
try {
|
|
if (nextRefresh.full || typeof localAssetBackend.refreshCatalogForPath !== "function") {
|
|
localAssetBackend.refreshCatalog({ rootDir: nextRefresh.dir });
|
|
} else {
|
|
for (const filePath of nextRefresh.paths) {
|
|
localAssetBackend.refreshCatalogForPath({
|
|
rootDir: nextRefresh.dir,
|
|
filePath,
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn("Failed to refresh CAD catalog", error);
|
|
}
|
|
server.ws.send({
|
|
type: "custom",
|
|
event: "cad-catalog:changed",
|
|
data: { dir: nextRefresh.dir },
|
|
});
|
|
}, 150));
|
|
}
|
|
|
|
function notifyChangedPath(server, changedPath) {
|
|
const resolvedChangedPath = path.resolve(changedPath);
|
|
for (const [rootPath, activeRoot] of activeDirectories.entries()) {
|
|
if (resolvedChangedPath === rootPath || pathIsInside(resolvedChangedPath, rootPath)) {
|
|
if (isGenerationStatusFilePath(resolvedChangedPath)) {
|
|
server.ws.send({
|
|
type: "custom",
|
|
event: "cad-generation-status:changed",
|
|
data: { dir: activeRoot.dir },
|
|
});
|
|
continue;
|
|
}
|
|
if (isCatalogRelevantPath(resolvedChangedPath)) {
|
|
scheduleCatalogRefresh(server, rootPath, activeRoot, resolvedChangedPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
name: "cad-catalog",
|
|
configureServer(server) {
|
|
let activeServerInfo = null;
|
|
try {
|
|
activateDirectory(server, localAssetBackend.resolveRoot(""));
|
|
} catch (error) {
|
|
console.warn("Failed to activate default CAD Viewer directory", error);
|
|
}
|
|
const currentServerInfo = ({ rootDir = "" } = {}) => {
|
|
const infoRootDir = rootDir || "";
|
|
activeServerInfo = buildViewerServerInfo({
|
|
directoryRoot: repoRoot,
|
|
rootDir: infoRootDir,
|
|
port: viteServerPort(server),
|
|
pid: process.pid,
|
|
backend: "local-fs",
|
|
dynamicRoot: true,
|
|
stepArtifactGenerationAvailable: enableStepArtifactBackend,
|
|
viewerVersion,
|
|
serverMode: viewerServerMode,
|
|
git: viewerGit,
|
|
serverFeatures: localServerFeatures,
|
|
activeDirectories: activeDirectoryOptions({ rootDir: infoRootDir }),
|
|
});
|
|
return activeServerInfo;
|
|
};
|
|
const registerServer = () => {
|
|
writeViewerServerRegistry(currentServerInfo());
|
|
};
|
|
if (server.httpServer?.listening) {
|
|
registerServer();
|
|
} else {
|
|
server.httpServer?.once("listening", registerServer);
|
|
}
|
|
server.httpServer?.once("close", () => {
|
|
removeViewerServerRegistryEntry(activeServerInfo || currentServerInfo());
|
|
});
|
|
server.middlewares.use(createCadViewerApiMiddleware({
|
|
backend: localAssetBackend,
|
|
enableStepArtifactBackend,
|
|
serverInfo: currentServerInfo,
|
|
onCatalogActivated: (resolvedRoot) => {
|
|
activateDirectory(server, resolvedRoot);
|
|
},
|
|
onDirectoryActivated: (resolvedRoot) => {
|
|
activateDirectory(server, resolvedRoot);
|
|
},
|
|
onCatalogChanged: (resolvedRoot) => {
|
|
scheduleCatalogRefresh(server, resolvedRoot.rootPath, { dir: resolvedRoot.dir });
|
|
},
|
|
}));
|
|
server.middlewares.use(createLocalAssetMiddleware({
|
|
backend: localAssetBackend,
|
|
}));
|
|
for (const eventName of ["add", "change", "unlink"]) {
|
|
server.watcher.on(eventName, (changedPath) => notifyChangedPath(server, changedPath));
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function serverLifetimePlugin() {
|
|
return {
|
|
name: "cad-viewer-server-lifetime",
|
|
configureServer(server) {
|
|
if (viewerServerLifetimeMs === null) {
|
|
return;
|
|
}
|
|
let shutdownTimer = null;
|
|
const scheduleShutdown = () => {
|
|
shutdownTimer = scheduleProcessShutdown({
|
|
lifetimeMs: viewerServerLifetimeMs,
|
|
label: "CAD Viewer dev server",
|
|
close: () => server.close(),
|
|
});
|
|
};
|
|
if (server.httpServer?.listening) {
|
|
scheduleShutdown();
|
|
} else {
|
|
server.httpServer?.once("listening", scheduleShutdown);
|
|
}
|
|
server.httpServer?.once("close", () => {
|
|
if (shutdownTimer) {
|
|
clearTimeout(shutdownTimer);
|
|
}
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineConfig(({ command }) => ({
|
|
root: viewerAppRoot,
|
|
envPrefix: "VIEWER_",
|
|
plugins: [
|
|
react(),
|
|
cadCatalogPlugin({ enableStepArtifactBackend: command === "serve" }),
|
|
serverLifetimePlugin(),
|
|
],
|
|
resolve: {
|
|
alias: {
|
|
"@": viewerClientRoot,
|
|
"cadjs": cadJsPackageRoot,
|
|
"clsx": path.join(viewerNodeModulesRoot, "clsx"),
|
|
"gifenc": path.join(viewerNodeModulesRoot, "gifenc", "dist", "gifenc.esm.js"),
|
|
"tailwind-merge": path.join(viewerNodeModulesRoot, "tailwind-merge"),
|
|
"three": path.join(viewerNodeModulesRoot, "three"),
|
|
"three/examples": path.join(viewerNodeModulesRoot, "three", "examples"),
|
|
},
|
|
},
|
|
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;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
worker: {
|
|
format: "es",
|
|
},
|
|
server: {
|
|
host: "127.0.0.1",
|
|
allowedHosts: viewerAllowedHosts,
|
|
fs: {
|
|
allow: [
|
|
viewerAppRoot,
|
|
cadJsPackageRoot,
|
|
],
|
|
},
|
|
},
|
|
preview: {
|
|
host: "127.0.0.1",
|
|
allowedHosts: viewerAllowedHosts,
|
|
},
|
|
}));
|