#!/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", " Zoo Design Studio", " HiCAD" ); replaceSource( "index.html", ' ', ` ` ); replaceSource( "src/Router.tsx", " createBrowserRouter,\n createHashRouter,", " createHashRouter," ); replaceSource( "src/Router.tsx", "const createRouter = isDesktop() ? createHashRouter : createBrowserRouter", "const createRouter = createHashRouter" ); replaceSource( "src/Router.tsx", "import { isDesktop } from '@src/lib/isDesktop'\n", "" ); replaceSource( "src/components/OpenedProject.tsx", "import { HicadManufacturingPanel } from '@src/components/HicadManufacturingPanel'\n", "" ); replaceSource( "src/components/OpenedProject.tsx", " \n", "" ); replaceSource( "src/components/OpenedProject.tsx", "import { HicadManufacturingLauncher } from '@src/components/HicadManufacturingLauncher'\n", "" ); replaceSource( "src/components/OpenedProject.tsx", " \n", "" ); if (enableManufacturingExtension) { replaceSource( "src/components/OpenedProject.tsx", "import { AppHeader } from '@src/components/AppHeader'\n", "import { AppHeader } from '@src/components/AppHeader'\nimport { HicadManufacturingSlot } from '@src/components/HicadManufacturingSlot'\n" ); replaceSource( "src/components/OpenedProject.tsx", " \n \n", "" ); } replaceSource( "src/lib/socket.ts", ` ws.addEventListener('open', () => {\n ws.send(\n JSON.stringify({\n type: 'headers',\n headers: {\n Authorization: 'Bearer ' + token,\n },\n })\n )\n resolve(ws)\n })`, ` ws.addEventListener('open', () => {\n if (token !== '${proxyToken}') {\n ws.send(\n JSON.stringify({\n type: 'headers',\n headers: {\n Authorization: 'Bearer ' + token,\n },\n })\n )\n }\n resolve(ws)\n })` ); replaceSource( "src/lib/engineConnection/websocketConnection.ts", " if (token) {", ` if (token && token !== '${proxyToken}') {` ); replaceSource( "src/lib/withBaseURL.ts", `export function withKittycadWebSocketURL(qs: string): string {\n return mergeQueryString(env().VITE_KITTYCAD_WEBSOCKET_URL, qs)\n}\n\nexport function withMlephantWebSocketURL(qs: string): string {\n return mergeQueryString(env().VITE_MLEPHANT_WEBSOCKET_URL, qs)\n}`, `export function withKittycadWebSocketURL(qs: string): string {\n return mergeWebSocketQueryString(env().VITE_KITTYCAD_WEBSOCKET_URL, qs)\n}\n\nexport function withMlephantWebSocketURL(qs: string): string {\n return mergeWebSocketQueryString(env().VITE_MLEPHANT_WEBSOCKET_URL, qs)\n}\n\nfunction mergeWebSocketQueryString(\n url: string | undefined,\n qs: string\n): string {\n if (!url) return ''\n const absoluteUrl =\n typeof window !== 'undefined' && url.startsWith('/')\n ? \`\${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//\${window.location.host}\${url}\`\n : url\n return mergeQueryString(absoluteUrl, qs)\n}` ); replaceSource( "src/lang/wasmUtils.ts", `export const wasmUrl = () => {\n const wasmFile = '/kcl_wasm_lib_bg.wasm'\n // Check for test environment override\n if (typeof process !== 'undefined' && process.env?.VITE_WASM_BASE_URL) {\n return process.env.VITE_WASM_BASE_URL + wasmFile\n }\n\n // For when we're in electron (file based) or web server (network based)\n // For some reason relative paths don't work as expected. Otherwise we would\n // just do /wasm_lib_bg.wasm. In particular, the issue arises when the path\n // is used from within worker.ts.\n const fullUrl = document.location.protocol.includes('http')\n ? document.location.origin + '/kcl_wasm_lib_bg.wasm'\n : document.location.protocol +\n webSafeJoin(webSafePathSplit(document.location.pathname).slice(0, -1)) +\n wasmFile\n\n return fullUrl\n}`, `export const wasmUrl = () => {\n const wasmFile = 'kcl_wasm_lib_bg.wasm'\n if (typeof process !== 'undefined' && process.env?.VITE_WASM_BASE_URL) {\n return process.env.VITE_WASM_BASE_URL + '/' + wasmFile\n }\n\n return new URL(wasmFile, document.baseURI).toString()\n}` ); replaceSource( "src/lang/wasmUtils.ts", "import { webSafeJoin, webSafePathSplit } from '@src/lib/paths'\n", "" ); replaceSource( "src/components/UserSidebarMenu.tsx", " const displayedName = getDisplayName(user)\n", ` const isHiCadEmbedded = env().VITE_ZOO_API_TOKEN === '${proxyToken}'\n const displayedName = isHiCadEmbedded ? 'HiCAD' : getDisplayName(user)\n` ); replaceSource( "src/components/UserSidebarMenu.tsx", ` [\n {\n id: 'account',`, ` [\n {\n id: 'hicad-home',\n Element: 'button',\n children: 'Back to HiCAD',\n onClick: () => window.top?.location.assign('/'),\n className: isHiCadEmbedded ? '' : 'hidden',\n },\n {\n id: 'account',` ); replaceSource( "src/components/UserSidebarMenu.tsx", ` ),\n },\n {\n id: 'settings',`, ` ),\n className: isHiCadEmbedded ? 'hidden' : '',\n },\n {\n id: 'settings',` ); replaceSource( "src/components/UserSidebarMenu.tsx", " className: '',", " className: isHiCadEmbedded ? 'hidden' : ''," ); replaceSource( "src/components/UserSidebarMenu.tsx", " {user?.image && !imageLoadFailed ? (", " {!isHiCadEmbedded && user?.image && !imageLoadFailed ? (" ); replaceSource( "src/components/UserSidebarMenu.tsx", " {displayedName !== user.email && (", " {!isHiCadEmbedded && displayedName !== user.email && (" ); replaceSource( "src/components/AppHeader.tsx", "import { HicadAutosaveStatus } from '@src/components/HicadProjectExperience'\n", "" ); replaceSource( "src/components/AppHeader.tsx", "import { HicadAutosaveStatus, HicadEditorProjectActions } from '@src/components/HicadProjectExperience'\n", "" ); replaceSource( "src/components/AppHeader.tsx", "import ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'\n", "import { HicadAutosaveStatus, HicadEditorProjectActions } from '@src/components/HicadProjectExperience'\nimport ProjectSidebarMenu from '@src/components/ProjectSidebarMenu'\n" ); replaceSource( "src/components/AppHeader.tsx", " {project && }\n", "" ); replaceSource( "src/components/AppHeader.tsx", ` {project && ( { lsp.onProjectClose(file || null, project.path || null, false) kclManager.switchedFiles = true void navigate(PATHS.HOME) }} onCreateBlankProject={() => { lsp.onProjectClose(file || null, project.path || null, false) kclManager.switchedFiles = true void navigate(\`\${PATHS.HOME}?create=blank\`) }} /> )} `, "" ); replaceSource( "src/components/AppHeader.tsx", ` {project && ( { lsp.onProjectClose(file || null, project.path || null, false) kclManager.switchedFiles = true }} onCreateBlankProject={() => { lsp.onProjectClose(file || null, project.path || null, false) kclManager.switchedFiles = true }} /> )} `, "" ); replaceSource( "src/components/AppHeader.tsx", `
`, ` {project && ( { lsp.onProjectClose(file || null, project.path || null, false) kclManager.switchedFiles = true }} onCreateBlankProject={() => { lsp.onProjectClose(file || null, project.path || null, false) kclManager.switchedFiles = true }} /> )}
` ); replaceSource( "src/lib/routeLoaders.ts", " if (await webHomeRouteEnabled(app)) {\n", " if (true) {\n" ); replaceSource( "src/lib/routeLoaders.ts", " if (!window.electron && !(await webHomeRouteEnabled(app))) {\n", " if (false) {\n" ); replaceSource( "src/lib/app.ts", ` const enableProjectDirectoryCommands = typeof window !== 'undefined' && (Boolean(window.electron) || userFeaturesContextHas( this.userFeatures.actor.getSnapshot().context, OPFS_CLOUD_FEATURE_FLAG, false )) `, ` const enableProjectDirectoryCommands = typeof window !== 'undefined' ` ); replaceSource( "src/components/AppHeader.tsx", `
{children ||`, `
{project && } {children ||` ); replaceSource( "src/routes/Home.tsx", "import AppProjectCard from '@src/components/AppProjectCard/AppProjectCard'\n", "import AppProjectCard from '@src/components/AppProjectCard/AppProjectCard'\nimport { HicadProjectDashboardHeading } from '@src/components/HicadProjectExperience'\n" ); replaceSource( "src/routes/Home.tsx", ` projectSearchKeybinding={projectSearchKeybinding} className="col-start-2 -col-end-1"`, ` projectSearchKeybinding={projectSearchKeybinding} projectCount={scopedHomeProjectEntries.length} className="col-start-2 -col-end-1"` ); replaceSource( "src/routes/Home.tsx", ` projectSearchKeybinding?: string }`, ` projectSearchKeybinding?: string projectCount: number }` ); replaceSource( "src/routes/Home.tsx", ` projectSearchKeybinding, ...rest`, ` projectSearchKeybinding, projectCount, ...rest` ); replaceSource( "src/routes/Home.tsx", `
{library && showLibraryBackLink && ( All libraries )}

{title}

`, `
{library && showLibraryBackLink && ( 返回全部项目 )}
` ); replaceSource( "src/routes/Home.tsx", `
`, `
` ); replaceSource( "src/routes/Home.tsx", `
`, `
` ); replaceSource( "src/components/AppProjectCard/AppProjectCard.tsx", "import { ContextMenu, ContextMenuItem } from '@src/components/ContextMenu'\n", "import { ContextMenu, ContextMenuItem } from '@src/components/ContextMenu'\nimport { CustomIcon } from '@src/components/CustomIcon'\n" ); replaceSource( "src/components/AppProjectCard/AppProjectCard.tsx", ` { local: 'Local', 'cloud-only': 'Cloud-only', syncing: 'Syncing', synced: 'Synced', conflicted: 'Conflicted', }`, ` { local: '已保存', 'cloud-only': '云端项目', syncing: '保存中', synced: '已保存', conflicted: '需要处理', }` ); replaceSource( "src/components/AppProjectCard/AppProjectCard.tsx", `function getDisplayedTime(dateTimeMs: number) { const date = new Date(dateTimeMs) const startOfToday = new Date() startOfToday.setHours(0, 0, 0, 0) return date.getTime() < startOfToday.getTime() ? date.toLocaleDateString() : date.toLocaleTimeString() }`, `function getDisplayedTime(dateTimeMs: number) { const date = new Date(dateTimeMs) const now = new Date() const isToday = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() if (isToday) { return \`今天 \${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false, })}\` } return date.toLocaleDateString('zh-CN', { year: date.getFullYear() === now.getFullYear() ? undefined : 'numeric', month: 'short', day: 'numeric', }) }` ); replaceSource( "src/components/AppProjectCard/AppProjectCard.tsx", ` {project.kclFileCount !== undefined && ( {project.kclFileCount}{' '} file{project.kclFileCount === 1 ? '' : 's'}{' '} {(project.directoryCount ?? 0) > 0 && ( <> {'/ '} {project.directoryCount} {' '} folder{project.directoryCount === 1 ? '' : 's'} )} )} Edited{' '} {displayedProject.modified ? getDisplayedTime(displayedProject.modified) : 'never'} `, `
{project.kclFileCount !== undefined ? ( <> {project.kclFileCount}{' '} 个模型文件 ) : ( 'CAD 项目' )} 最近编辑{' '} {displayedProject.modified ? getDisplayedTime(displayedProject.modified) : '暂无记录'}
` ); replaceSource( "src/components/AppProjectCard/AppProjectCard.tsx", ` title="Delete Project"`, ` title="删除项目"` ); replaceSource( "src/components/AppProjectCard/AppProjectCard.tsx", `

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={
暂无模型预览
} badges={badges}` ); replaceSource( "src/components/AppProjectCard/AppProjectCard.tsx", ` Rename project`, ` 重命名` ); replaceSource( "src/components/AppProjectCard/AppProjectCard.tsx", ` Delete project`, ` 删除项目` ); replaceSource( "src/lib/commandBarConfigs/projectsCommandConfig.ts", ` displayName: \`Create project\`, description: 'Create a project',`, ` displayName: \`新建 CAD 项目\`, description: '输入项目名称即可开始,设计内容会自动保存。',` ); replaceSource( "src/lib/commandBarConfigs/projectsCommandConfig.ts", ` displayName: 'Title', required: true,`, ` displayName: '项目名称', description: '使用容易识别的名称,例如“五金连接安装板”。', required: true,` ); replaceSource( "src/lib/commandBarConfigs/projectsCommandConfig.ts", ` displayName: 'Library', required: () => createProjectLibraryOptions().length > 1,`, ` displayName: '保存位置', required: () => createProjectLibraryOptions().length > 1,` ); replaceSource( "src/lib/commandBarConfigs/projectsCommandConfig.ts", ` defaultValue: defaultProjectFolderNameSnapshot,`, ` defaultValue: () => '',` ); replaceSource( "src/components/CommandBar/CommandBarBasicInput.tsx", ` placeholder="Enter a value"`, ` placeholder={ commandBarState.context.selectedCommand?.name === 'Create project' ? '例如:五金连接安装板' : '请输入' }` ); replaceSource( "src/components/environment/Environment.tsx", " new URL(env().VITE_KITTYCAD_WEBSOCKET_URL ?? ''),", " new URL(env().VITE_KITTYCAD_WEBSOCKET_URL ?? '', window.location.href)," ); replaceSource( "src/components/environment/Environment.tsx", " new URL(env().VITE_MLEPHANT_WEBSOCKET_URL ?? '')", " new URL(env().VITE_MLEPHANT_WEBSOCKET_URL ?? '', window.location.href)" ); replaceSource( "src/components/ConnectionStream.tsx", ` className="cursor-pointer"\n id="freeze-frame"`, ` className="absolute inset-0 h-full w-full cursor-pointer"\n id="freeze-frame"` ); replaceSource( "src/components/OpenedProject.tsx", "import { getMlEphantProjectReloadBehavior } from '@src/components/openedProjectUtils'\n", "import { getMlEphantProjectReloadBehavior } from '@src/components/openedProjectUtils'\nimport env from '@src/env'\n" ); replaceSource( "src/components/OpenedProject.tsx", ` const needsOnboarded = !window.electron &&`, ` const needsOnboarded = env().VITE_ZOO_API_TOKEN !== '${proxyToken}' && !window.electron &&` ); replaceSource( "src/index.tsx", "import { markOnce } from '@src/lib/performance'\n", "import { installHiCadLocalization } from '@src/lib/hicadLocalization'\nimport { markOnce } from '@src/lib/performance'\n" ); replaceSource( "src/index.tsx", "function launchApp(app: App) {\n initSingletonBehavior(app)", "function launchApp(app: App) {\n installHiCadLocalization()\n initSingletonBehavior(app)" ); replaceSource( "src/registry/extensions/engineScene/viewExtensionControls.tsx", "import { CustomIcon } from '@src/components/CustomIcon'\n", "import { CustomIcon } from '@src/components/CustomIcon'\nimport { FitAllButton } from '@src/components/FitAllButton'\n" ); replaceSource( "src/registry/extensions/engineScene/viewExtensionControls.tsx", ` \n \n `, ` \n
\n \n \n
\n
` ); replaceSource( "src/lib/resetCameraPosition.ts", ` // We need a padding of 0.1 for zoom_to_fit for all E2E tests since they were originally\n // written with zoom_to_fit with padding 0.1\n const padding = 0.1\n if (isPlaywright()) {`, ` // Keep the official E2E framing stable while giving HiCAD users enough\n // breathing room to see the complete model after generation and file loads.\n const padding = isPlaywright() ? 0.1 : 0.35\n if (isPlaywright()) {` ); replaceSource( "src/lib/resetCameraPosition.ts", ` await engineViewIsometric({ engineCommandManager, padding, })`, ` await engineViewIsometric({ engineCommandManager, padding, }) // view_isometric changes the direction but can preserve the previous // close-up distance. Refit against every generated entity so newly // completed models open fully framed instead of showing a local detail. await engineStreamZoomToFit({ engineCommandManager, padding, })` ); replaceSource( "src/lib/commandBarConfigs/standardViewsConfig.ts", "engineStreamZoomToFit({ engineCommandManager, padding: 0.1 })", "engineStreamZoomToFit({ engineCommandManager, padding: 0.35 })" ); replaceSource( "src/lib/app.ts", " padding: 0.2,\n animated: false,", " padding: 0.35,\n animated: false," ); replaceSource( "src/clientSideScene/ClientSideSceneComp.tsx", "import { throttle, toSync } from '@src/lib/utils'", `import {\n engineStreamZoomToFit,\n throttle,\n toSync,\n} from '@src/lib/utils'` ); replaceSource( "src/clientSideScene/ClientSideSceneComp.tsx", ` const {\n kclManager: { sceneEntitiesManager, sceneInfra, engineCommandManager },\n } = useSingletons()`, ` const { kclManager } = useSingletons()\n const { sceneEntitiesManager, sceneInfra, engineCommandManager } = kclManager` ); replaceSource( "src/clientSideScene/ClientSideSceneComp.tsx", ` // Detect canvas size changes\n const observer = new ResizeObserver(() => {\n // This is called initially too, not just on resize\n sceneInfra.onCanvasResized()\n sceneInfra.camControls.onWindowResize()\n sceneEntitiesManager.onCamChange().catch(reportRejection)\n })\n observer.observe(container)`, ` // Preserve the full-model framing when panels or the browser resize.\n let fitAfterResizeTimer: ReturnType | undefined\n const observer = new ResizeObserver(() => {\n // This is called initially too, not just on resize\n sceneInfra.onCanvasResized()\n sceneInfra.camControls.onWindowResize()\n sceneEntitiesManager.onCamChange().catch(reportRejection)\n\n clearTimeout(fitAfterResizeTimer)\n fitAfterResizeTimer = setTimeout(() => {\n if (kclManager.artifactGraph.size === 0) return\n engineStreamZoomToFit({\n engineCommandManager,\n padding: 0.35,\n }).catch(reportRejection)\n }, 180)\n })\n observer.observe(container)` ); replaceSource( "src/clientSideScene/ClientSideSceneComp.tsx", ` observer.disconnect()\n media.removeEventListener('change', handleChange)`, ` observer.disconnect()\n clearTimeout(fitAfterResizeTimer)\n media.removeEventListener('change', handleChange)` ); replaceSource( "src/routes/Onboarding/utils.tsx", `

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