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.
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
* It handles:
|
||||
* 1. Copying and bundling electron/scripts/ directory
|
||||
* 2. Ensuring required dependencies (playwright, chromium-bidi, bytenode) are included
|
||||
* 3. Cleaning up unnecessary development files to reduce package size
|
||||
* 3. Copying bundled OpenClaw runtime dependencies that electron-builder may skip
|
||||
* 4. Cleaning up unnecessary development files to reduce package size
|
||||
*/
|
||||
|
||||
const fs = require('fs-extra');
|
||||
@@ -75,15 +76,65 @@ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function resolveResourcesDir(context) {
|
||||
if (context.electronPlatformName === 'darwin') {
|
||||
if (context.appOutDir.endsWith('.app')) {
|
||||
return path.join(context.appOutDir, 'Contents', 'Resources');
|
||||
}
|
||||
|
||||
const appName = context.packager?.appInfo?.productFilename || 'NIANXX';
|
||||
return path.join(context.appOutDir, `${appName}.app`, 'Contents', 'Resources');
|
||||
}
|
||||
|
||||
return path.join(context.appOutDir, 'resources');
|
||||
}
|
||||
|
||||
async function copyBundledOpenClaw(context) {
|
||||
const srcRoot = path.join(__dirname, '..', 'build', 'openclaw');
|
||||
if (!(await fs.pathExists(srcRoot))) {
|
||||
console.warn('[after-pack] build/openclaw not found. Run bundle-openclaw first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const resourcesDir = resolveResourcesDir(context);
|
||||
const destRoot = path.join(resourcesDir, 'openclaw');
|
||||
const srcNodeModules = path.join(srcRoot, 'node_modules');
|
||||
const destNodeModules = path.join(destRoot, 'node_modules');
|
||||
|
||||
await fs.ensureDir(resourcesDir);
|
||||
|
||||
if (!(await fs.pathExists(destRoot))) {
|
||||
await fs.copy(srcRoot, destRoot, {
|
||||
filter: (src) => {
|
||||
const relative = path.relative(srcRoot, src);
|
||||
if (!relative) return true;
|
||||
return relative.split(path.sep)[0] !== 'node_modules';
|
||||
},
|
||||
});
|
||||
console.log('[after-pack] Copied bundled OpenClaw runtime root.');
|
||||
}
|
||||
|
||||
if (!(await fs.pathExists(srcNodeModules))) {
|
||||
console.warn('[after-pack] build/openclaw/node_modules not found. OpenClaw runtime will be incomplete.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await fs.pathExists(destNodeModules))) {
|
||||
await fs.copy(srcNodeModules, destNodeModules);
|
||||
console.log('[after-pack] Copied bundled OpenClaw node_modules.');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async function afterPack(context) {
|
||||
const { appOutDir, electronPlatformName: platform, arch } = context;
|
||||
const resourcesDir = resolveResourcesDir(context);
|
||||
|
||||
console.log(`Running afterPack hook for ${platform}-${arch}`);
|
||||
console.log(`App output directory: ${appOutDir}`);
|
||||
|
||||
// 1. Handle electron/scripts/ directory
|
||||
const scriptsSrc = path.join(__dirname, '..', 'electron/scripts');
|
||||
const scriptsDest = path.join(appOutDir, 'resources', 'scripts');
|
||||
const scriptsDest = path.join(resourcesDir, 'scripts');
|
||||
|
||||
if (await fs.pathExists(scriptsSrc)) {
|
||||
await fs.ensureDir(scriptsDest);
|
||||
@@ -118,13 +169,15 @@ module.exports = async function afterPack(context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await copyBundledOpenClaw(context);
|
||||
|
||||
// 2. Ensure required dependencies are included
|
||||
// electron-builder may exclude some dependencies due to .gitignore rules
|
||||
|
||||
const ensureDependency = async (depName) => {
|
||||
const src = path.join(__dirname, '..', 'node_modules', depName);
|
||||
const dest = path.join(appOutDir, 'node_modules', depName);
|
||||
const dest = path.join(resourcesDir, 'node_modules', depName);
|
||||
|
||||
if (await fs.pathExists(src)) {
|
||||
await fs.ensureDir(path.dirname(dest));
|
||||
@@ -144,7 +197,7 @@ module.exports = async function afterPack(context) {
|
||||
|
||||
// 3. Clean up unnecessary development files from node_modules (skip if SKIP_AFTERPACK_CLEANUP is set)
|
||||
if (!process.env.SKIP_AFTERPACK_CLEANUP) {
|
||||
const nodeModulesDest = path.join(appOutDir, 'node_modules');
|
||||
const nodeModulesDest = path.join(resourcesDir, 'node_modules');
|
||||
if (await fs.pathExists(nodeModulesDest)) {
|
||||
console.log('Cleaning up development files in node_modules...');
|
||||
const removed = cleanupUnnecessaryFiles(nodeModulesDest);
|
||||
@@ -165,4 +218,4 @@ module.exports = async function afterPack(context) {
|
||||
// cleanupNativePlatformPackages(nodeModulesDest, platform, arch);
|
||||
|
||||
console.log('afterPack hook completed successfully');
|
||||
};
|
||||
};
|
||||
|
||||
304
scripts/bundle-openclaw.mjs
Normal file
304
scripts/bundle-openclaw.mjs
Normal file
@@ -0,0 +1,304 @@
|
||||
#!/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}`);
|
||||
Reference in New Issue
Block a user