#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); const OUTPUT = path.join(ROOT, 'build', 'openclaw'); const NODE_MODULES = path.join(ROOT, 'node_modules'); function log(message) { console.log(`[bundle-openclaw] ${message}`); } function normWin(filePath) { if (process.platform !== 'win32') return filePath; if (filePath.startsWith('\\\\?\\')) return filePath; return `\\\\?\\${filePath.replace(/\//g, '\\')}`; } function getVirtualStoreNodeModules(realPkgPath) { let current = realPkgPath; while (current !== path.dirname(current)) { if (path.basename(current) === 'node_modules') { return current; } current = path.dirname(current); } return null; } function listPackages(nodeModulesDir) { const results = []; const targetDir = normWin(nodeModulesDir); if (!fs.existsSync(targetDir)) return results; for (const entry of fs.readdirSync(targetDir)) { if (entry === '.bin') continue; const entryPath = path.join(nodeModulesDir, entry); if (entry.startsWith('@')) { let scopedEntries = []; try { scopedEntries = fs.readdirSync(normWin(entryPath)); } catch { continue; } for (const scoped of scopedEntries) { results.push({ name: `${entry}/${scoped}`, fullPath: path.join(entryPath, scoped), }); } continue; } results.push({ name: entry, fullPath: entryPath }); } return results; } function rmSafe(target) { try { const stat = fs.lstatSync(target); if (stat.isDirectory()) { fs.rmSync(target, { recursive: true, force: true }); } else { fs.rmSync(target, { force: true }); } return true; } catch { return false; } } function cleanupNodeModules(dir) { let removedCount = 0; const REMOVE_DIRS = new Set([ 'test', 'tests', '__tests__', '.github', 'docs', 'examples', 'example', ]); const REMOVE_FILE_EXTS = ['.d.ts', '.d.ts.map', '.js.map', '.mjs.map', '.ts.map', '.markdown']; const REMOVE_FILE_NAMES = new Set([ '.DS_Store', 'README.md', 'CHANGELOG.md', 'LICENSE.md', 'CONTRIBUTING.md', 'tsconfig.json', '.npmignore', '.eslintrc', '.prettierrc', '.editorconfig', ]); function walk(currentDir) { let entries = []; try { entries = fs.readdirSync(currentDir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { if (REMOVE_DIRS.has(entry.name)) { if (rmSafe(fullPath)) removedCount++; continue; } walk(fullPath); continue; } if ( REMOVE_FILE_NAMES.has(entry.name) || REMOVE_FILE_EXTS.some((ext) => entry.name.endsWith(ext)) ) { if (rmSafe(fullPath)) removedCount++; } } } if (fs.existsSync(dir)) { walk(dir); } return removedCount; } function cleanupBundle(outputDir) { let removedCount = 0; for (const name of ['CHANGELOG.md', 'README.md']) { if (rmSafe(path.join(outputDir, name))) removedCount++; } removedCount += cleanupNodeModules(path.join(outputDir, 'node_modules')); const extensionsDir = path.join(outputDir, 'dist', 'extensions'); if (fs.existsSync(extensionsDir)) { for (const entry of fs.readdirSync(extensionsDir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; removedCount += cleanupNodeModules(path.join(extensionsDir, entry.name, 'node_modules')); } } return removedCount; } const openclawLink = path.join(NODE_MODULES, 'openclaw'); if (!fs.existsSync(openclawLink)) { console.error('[bundle-openclaw] node_modules/openclaw not found. Run pnpm install first.'); process.exit(1); } const openclawReal = fs.realpathSync(openclawLink); log(`resolved openclaw package: ${openclawReal}`); if (fs.existsSync(OUTPUT)) { fs.rmSync(OUTPUT, { recursive: true, force: true }); } fs.mkdirSync(OUTPUT, { recursive: true }); log('copying openclaw package root...'); fs.cpSync(normWin(openclawReal), normWin(OUTPUT), { recursive: true, dereference: true }); const openclawVirtualNodeModules = getVirtualStoreNodeModules(openclawReal); if (!openclawVirtualNodeModules) { console.error('[bundle-openclaw] could not determine pnpm virtual store for openclaw.'); process.exit(1); } const collected = new Map(); const queue = [{ nodeModulesDir: openclawVirtualNodeModules, skipPkg: 'openclaw' }]; const SKIP_PACKAGES = new Set([ 'typescript', '@playwright/test', '@discordjs/opus', ]); const SKIP_SCOPES = ['@cloudflare/', '@types/']; while (queue.length > 0) { const current = queue.shift(); if (!current) break; for (const { name, fullPath } of listPackages(current.nodeModulesDir)) { if (name === current.skipPkg) continue; if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some((scope) => name.startsWith(scope))) { continue; } let realPath; try { realPath = fs.realpathSync(fullPath); } catch { continue; } if (collected.has(realPath)) continue; collected.set(realPath, name); const depVirtualNodeModules = getVirtualStoreNodeModules(realPath); if (depVirtualNodeModules && depVirtualNodeModules !== current.nodeModulesDir) { queue.push({ nodeModulesDir: depVirtualNodeModules, skipPkg: name }); } } } log(`resolved ${collected.size} transitive packages.`); const outputNodeModules = path.join(OUTPUT, 'node_modules'); fs.mkdirSync(outputNodeModules, { recursive: true }); const copiedNames = new Set(); let copiedCount = 0; let skippedDuplicates = 0; for (const [realPath, pkgName] of collected) { if (copiedNames.has(pkgName)) { skippedDuplicates++; continue; } copiedNames.add(pkgName); const dest = path.join(outputNodeModules, pkgName); try { fs.mkdirSync(normWin(path.dirname(dest)), { recursive: true }); fs.cpSync(normWin(realPath), normWin(dest), { recursive: true, dereference: true }); copiedCount++; } catch (error) { const message = error instanceof Error ? error.message : String(error); log(`skipped ${pkgName}: ${message}`); } } log(`copied ${copiedCount} packages to bundled node_modules (${skippedDuplicates} duplicates skipped).`); const extensionsDir = path.join(OUTPUT, 'dist', 'extensions'); let mergedExtensionPackages = 0; if (fs.existsSync(extensionsDir)) { for (const extEntry of fs.readdirSync(extensionsDir, { withFileTypes: true })) { if (!extEntry.isDirectory()) continue; const extNodeModules = path.join(extensionsDir, extEntry.name, 'node_modules'); if (!fs.existsSync(extNodeModules)) continue; for (const pkgEntry of fs.readdirSync(extNodeModules, { withFileTypes: true })) { if (!pkgEntry.isDirectory() || pkgEntry.name === '.bin') continue; const srcPkg = path.join(extNodeModules, pkgEntry.name); if (pkgEntry.name.startsWith('@')) { let scopeEntries = []; try { scopeEntries = fs.readdirSync(srcPkg, { withFileTypes: true }); } catch { continue; } for (const scopeEntry of scopeEntries) { if (!scopeEntry.isDirectory()) continue; const scopedName = `${pkgEntry.name}/${scopeEntry.name}`; if (copiedNames.has(scopedName)) continue; const srcScoped = path.join(srcPkg, scopeEntry.name); const destScoped = path.join(outputNodeModules, pkgEntry.name, scopeEntry.name); try { fs.mkdirSync(normWin(path.dirname(destScoped)), { recursive: true }); fs.cpSync(normWin(srcScoped), normWin(destScoped), { recursive: true, dereference: true }); copiedNames.add(scopedName); mergedExtensionPackages++; } catch { // Ignore extension copy failures and continue building the rest. } } continue; } if (copiedNames.has(pkgEntry.name)) continue; const destPkg = path.join(outputNodeModules, pkgEntry.name); try { fs.cpSync(normWin(srcPkg), normWin(destPkg), { recursive: true, dereference: true }); copiedNames.add(pkgEntry.name); mergedExtensionPackages++; } catch { // Ignore extension copy failures and continue building the rest. } } } } if (mergedExtensionPackages > 0) { log(`merged ${mergedExtensionPackages} built-in extension packages into top-level node_modules.`); } const removedCount = cleanupBundle(OUTPUT); log(`removed ${removedCount} development-only files from bundled runtime.`); log(`OpenClaw bundle ready at ${OUTPUT}`);