Import CAD projects and cad-router v1
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
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,
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user