#!/usr/bin/env node import { spawnSync } from "node:child_process"; import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { cp } from "node:fs/promises"; import path from "node:path"; import { gzipSync } from "node:zlib"; import { cacheBustZooModelingAppAssets } from "./cache-bust-zoo-modeling-app-assets.mjs"; const repoUrl = "https://github.com/KittyCAD/modeling-app.git"; const repoRef = process.env.ZOO_MODELING_APP_REF || "f2b345dbb56a851e10d8a742732ab73ee139f025"; const repoTarballUrls = [ `https://ghfast.top/https://github.com/KittyCAD/modeling-app/archive/${repoRef}.tar.gz`, `https://github.com/KittyCAD/modeling-app/archive/${repoRef}.tar.gz`, ]; const root = process.cwd(); const workspaceDir = path.join(root, ".zoo-modeling-app"); const sourceDir = path.join(workspaceDir, "modeling-app"); const sourceRefFile = path.join(sourceDir, ".hicad-source-ref"); const outputDir = path.join(root, "public/vendor/zoo-modeling-app"); const customizationDir = path.join(root, "vendor/zoo-modeling-app"); const proxyToken = "hicad-session"; const brandedFaviconUrl = "/api/favicon?surface=hicad"; const enableManufacturingExtension = process.env.HICAD_MANUFACTURING_EXTENSION === "1"; function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: options.cwd || root, env: { ...process.env, ...(options.env || {}) }, shell: false, stdio: "inherit", }); if (result.status !== 0) { throw new Error(`${command} ${args.join(" ")} failed`); } } function tryRun(command, args, options = {}) { const result = spawnSync(command, args, { cwd: options.cwd || root, env: { ...process.env, ...(options.env || {}) }, shell: false, stdio: "inherit", }); return result.status === 0; } function downloadOfficialSource() { mkdirSync(workspaceDir, { recursive: true }); const archivePath = path.join(workspaceDir, "modeling-app-main.tar.gz"); const extractedDir = path.join(workspaceDir, `modeling-app-${repoRef}`); rmSync(archivePath, { force: true }); rmSync(extractedDir, { force: true, recursive: true }); const downloaded = repoTarballUrls.some((url) => tryRun("curl", [ "--http1.1", "--location", "--fail", "--retry", "3", url, "--output", archivePath, ]) ); if (!downloaded) { throw new Error("Unable to download KittyCAD/modeling-app"); } run("tar", ["-xzf", archivePath, "-C", workspaceDir]); rmSync(sourceDir, { force: true, recursive: true }); run("mv", [extractedDir, sourceDir]); writeFileSync(sourceRefFile, `${repoRef}\n`); } function ensureOfficialSource() { if (existsSync(path.join(sourceDir, "package.json"))) { if (!existsSync(sourceRefFile)) { writeFileSync(sourceRefFile, `${repoRef}\n`); } const currentRef = readFileSync(sourceRefFile, "utf8").trim(); if (currentRef !== repoRef) { downloadOfficialSource(); } return; } downloadOfficialSource(); } function replaceSource(relativePath, before, after) { const filePath = path.join(sourceDir, relativePath); const source = readFileSync(filePath, "utf8"); if (after && source.includes(after)) { return; } if (!source.includes(before)) { if (!after) { return; } throw new Error( `Official source changed; patch failed for ${relativePath}; expected ${JSON.stringify( before.slice(0, 160) )}` ); } writeFileSync(filePath, source.replace(before, after)); } function patchOfficialManifest() { const manifestPath = path.join(sourceDir, "public/manifest.json"); const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); manifest.short_name = "HiCAD"; manifest.name = "HiCAD"; manifest.icons = [ { src: brandedFaviconUrl, sizes: "any", }, ]; writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); } function patchOfficialSource() { copyFileSync( path.join(customizationDir, "Logo.tsx"), path.join(sourceDir, "src/components/Logo.tsx") ); if (enableManufacturingExtension) { copyFileSync( path.join(customizationDir, "manufacturing.ts"), path.join(sourceDir, "src/components/manufacturing.ts") ); copyFileSync( path.join(customizationDir, "HicadManufacturingPanel.tsx.template"), path.join(sourceDir, "src/components/HicadManufacturingPanel.tsx") ); copyFileSync( path.join(customizationDir, "HicadManufacturingLauncher.tsx.template"), path.join(sourceDir, "src/components/HicadManufacturingLauncher.tsx") ); copyFileSync( path.join(customizationDir, "HicadManufacturingSlot.tsx.template"), path.join(sourceDir, "src/components/HicadManufacturingSlot.tsx") ); } copyFileSync( path.join(customizationDir, "localization.ts"), path.join(sourceDir, "src/lib/hicadLocalization.ts") ); copyFileSync( path.join(customizationDir, "FitAllButton.tsx.template"), path.join(sourceDir, "src/components/FitAllButton.tsx") ); copyFileSync( path.join(customizationDir, "HicadProjectExperience.tsx.template"), path.join(sourceDir, "src/components/HicadProjectExperience.tsx") ); replaceSource( "index.html", ' ', ' ' ); replaceSource( "index.html", ' ', ` ` ); replaceSource( "index.html", ' ', ` ` ); patchOfficialManifest(); replaceSource( "index.html", ' \n', "" ); replaceSource( "index.html", "
This will permanently delete "{projectName || 'this file'} ".
Are you sure you want to delete "{projectName || 'this file'} "? This action cannot be undone.
`, `确定删除“{projectName || '此项目'}”吗?
项目及其全部模型文件将被永久删除,此操作无法撤销。
` ); replaceSource( "src/components/AppProjectCard/AppProjectCard.tsx", ` title={projectName} titleText={projectName}`, ` title={ {projectName} {canOpen && ( 继续设计 )} } titleText={projectName}` ); replaceSource( "src/components/AppProjectCard/AppProjectCard.tsx", ` thumbnailUrl={imageUrl} badges={badges}`, ` thumbnailUrl={imageUrl} thumbnailAlt={projectName ? \`\${projectName} 的模型预览\` : 'CAD 项目预览'} thumbnailFallback={To view your account, manage payment methods, or change tiers, simply{' '}`, `
To view your account, manage payment methods, or change tiers, simply{' '}` ); } function ensureDependencies() { if (existsSync(path.join(sourceDir, "node_modules/.package-lock.json"))) { return; } run("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], { cwd: sourceDir, }); } function ensureWasm() { const wasmJs = path.join( sourceDir, "rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js" ); const wasmBinary = path.join( sourceDir, "rust/kcl-wasm-lib/pkg/kcl_wasm_lib_bg.wasm" ); if (!(existsSync(wasmJs) && existsSync(wasmBinary))) { const cargoEnv = { CARGO_HTTP_MULTIPLEXING: "false", CARGO_REGISTRIES_CRATES_IO_INDEX: process.env.CARGO_REGISTRIES_CRATES_IO_INDEX || "sparse+https://rsproxy.cn/index/", CARGO_REGISTRIES_CRATES_IO_PROTOCOL: "sparse", }; run( "wasm-pack", [ "build", "kcl-wasm-lib", "--release", "--target", "web", "--out-dir", "pkg", "--scope", "kittycad", "--no-opt", ], { cwd: path.join(sourceDir, "rust"), env: cargoEnv } ); run( "cargo", [ "test", "-p", "kcl-lib", "--features", "artifact-graph", "export_bindings", ], { cwd: path.join(sourceDir, "rust"), env: cargoEnv } ); } copyFileSync( path.join(sourceDir, "rust/kcl-wasm-lib/README.md"), path.join(sourceDir, "rust/kcl-wasm-lib/pkg/README.md") ); copyFileSync(wasmBinary, path.join(sourceDir, "public/kcl_wasm_lib_bg.wasm")); } function precompressVersionedAssets(assetsDir) { for (const entry of readdirSync(assetsDir, { withFileTypes: true })) { if (!entry.isFile() || !/\.(?:css|js|wasm)$/.test(entry.name)) { continue; } const assetPath = path.join(assetsDir, entry.name); writeFileSync(`${assetPath}.gz`, gzipSync(readFileSync(assetPath), { level: 9 })); } } function precompressRootWasm(buildDir) { const wasmPath = path.join(buildDir, "kcl_wasm_lib_bg.wasm"); if (!existsSync(wasmPath)) return; writeFileSync( `${wasmPath}.gz`, gzipSync(readFileSync(wasmPath), { level: 9 }) ); } async function main() { ensureOfficialSource(); ensureDependencies(); ensureWasm(); patchOfficialSource(); const buildEnv = { VITE_KITTYCAD_WEBSOCKET_URL: process.env.HICAD_ZOO_MODELING_WEBSOCKET_URL || "/api/ai-cad/zoo-ws/modeling/commands", VITE_MLEPHANT_WEBSOCKET_URL: process.env.HICAD_ZOO_AGENT_WEBSOCKET_URL || "/api/ai-cad/zoo-ws/ml/copilot", VITE_ZOO_API_BASE_URL: "/api/ai-cad/zoo", VITE_ZOO_API_TOKEN: proxyToken, VITE_ZOO_BASE_DOMAIN: "zoo.dev", }; rmSync(path.join(sourceDir, "build"), { force: true, recursive: true }); run("npx", ["vite", "build", "--base=./", "--mode=production"], { cwd: sourceDir, env: buildEnv, }); const buildDir = path.join(sourceDir, "build"); if (!existsSync(path.join(buildDir, "index.html"))) { throw new Error("Zoo Design Studio build did not produce index.html"); } precompressVersionedAssets(path.join(buildDir, "assets")); precompressRootWasm(buildDir); cacheBustZooModelingAppAssets(buildDir); rmSync(outputDir, { force: true, recursive: true }); mkdirSync(path.dirname(outputDir), { recursive: true }); await cp(buildDir, outputDir, { recursive: true }); const license = path.join(sourceDir, "LICENSE"); if (existsSync(license)) { await cp(license, path.join(outputDir, "LICENSE")); } writeFileSync( path.join(outputDir, "SOURCE.txt"), [ "HiCAD web frontend based on Zoo Design Studio", `Source: ${repoUrl}`, `Source commit: ${repoRef}`, "License: MIT", "Authentication: HiCAD session with server-side Zoo proxy", "", ].join("\n") ); console.log(`HiCAD official frontend copied to ${outputDir}`); } main().catch((error) => { console.error(error instanceof Error ? error.message : error); process.exit(1); });