Files
zn-ai/scripts/bundle-openclaw.mjs
duanshuwen ea1fd18e6f feat: enhance after-pack script to copy OpenClaw runtime dependencies
- Added a new script `bundle-openclaw.mjs` to bundle OpenClaw runtime dependencies.
- Updated `after-pack.cjs` to copy bundled OpenClaw runtime and its node_modules.
- Improved cleanup of unnecessary development files in node_modules.
- Adjusted paths for resources in the packaging process.

style: update loading indicator styles in ChatHistoryPanel

- Changed the border radius and padding for the loading indicator in ChatHistoryPanel.

fix: improve ProvidersSection to handle provider account syncing

- Added logic to sync model configuration to provider accounts.
- Introduced error handling and loading states during the sync process.
- Enhanced vendor resolution and account management logic.

fix: fallback session handling in chat store

- Implemented fallback session logic in loadSessions to ensure a valid session is always available on error.
2026-04-22 21:56:37 +08:00

305 lines
8.6 KiB
JavaScript

#!/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}`);