Files
cadSet/text-to-cad/viewer/vite.config.mjs
2026-07-22 19:38:36 +08:00

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,
},
}));