feat: upgrade openclaw to 4.26 (#899)
Co-authored-by: paisley <8197966+su8su@users.noreply.github.com>
This commit is contained in:
@@ -19,9 +19,13 @@
|
||||
* @mariozechner/clipboard).
|
||||
*/
|
||||
|
||||
const { cpSync, existsSync, readdirSync, rmSync, statSync, mkdirSync, realpathSync } = require('fs');
|
||||
const { createHash } = require('crypto');
|
||||
const { cpSync, existsSync, readdirSync, rmSync, statSync, mkdirSync, realpathSync, readFileSync, renameSync, writeFileSync } = require('fs');
|
||||
const { join, dirname, basename, relative } = require('path');
|
||||
|
||||
const RUNTIME_DEPS_MANIFEST = 'clawx-runtime-deps.json';
|
||||
const OPENCLAW_RUNTIME_READY_MARKER = '.clawx-runtime-ready.json';
|
||||
|
||||
// On Windows, paths in pnpm's virtual store can exceed the default MAX_PATH
|
||||
// limit (260 chars). Node.js 18.17+ respects the system LongPathsEnabled
|
||||
// registry key, but as a safety net we normalize paths to use the \\?\ prefix
|
||||
@@ -459,10 +463,19 @@ function bundlePlugin(nodeModulesRoot, npmName, destDir) {
|
||||
let realPluginPath;
|
||||
try { realPluginPath = realpathSync(pkgPath); } catch { realPluginPath = pkgPath; }
|
||||
|
||||
function shouldCopyNodePackageEntry(src) {
|
||||
const base = basename(src);
|
||||
return base !== '.vscode' && base !== '.idea';
|
||||
}
|
||||
|
||||
// Copy plugin package itself
|
||||
if (existsSync(normWin(destDir))) rmSync(normWin(destDir), { recursive: true, force: true });
|
||||
mkdirSync(normWin(destDir), { recursive: true });
|
||||
cpSync(normWin(realPluginPath), normWin(destDir), { recursive: true, dereference: true });
|
||||
cpSync(normWin(realPluginPath), normWin(destDir), {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: shouldCopyNodePackageEntry,
|
||||
});
|
||||
|
||||
// Collect transitive deps via pnpm virtual store BFS
|
||||
const collected = new Map();
|
||||
@@ -515,7 +528,11 @@ function bundlePlugin(nodeModulesRoot, npmName, destDir) {
|
||||
const d = join(destNM, pkgName);
|
||||
try {
|
||||
mkdirSync(normWin(dirname(d)), { recursive: true });
|
||||
cpSync(normWin(rp), normWin(d), { recursive: true, dereference: true });
|
||||
cpSync(normWin(rp), normWin(d), {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: shouldCopyNodePackageEntry,
|
||||
});
|
||||
count++;
|
||||
} catch (e) {
|
||||
console.warn(`[after-pack] Skipped dep ${pkgName}: ${e.message}`);
|
||||
@@ -525,6 +542,132 @@ function bundlePlugin(nodeModulesRoot, npmName, destDir) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function hasPluginRuntimeDeps(pluginDestDir) {
|
||||
const pluginNM = join(pluginDestDir, 'node_modules');
|
||||
if (!existsSync(normWin(pluginNM))) return false;
|
||||
try {
|
||||
return readdirSync(normWin(pluginNM)).some((entry) => entry !== '.bin');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function collectPluginRuntimeDeps(pluginDestDir) {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(normWin(join(pluginDestDir, 'package.json')), 'utf8'));
|
||||
return Object.keys({
|
||||
...pkg.dependencies,
|
||||
...pkg.optionalDependencies,
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function missingPluginRuntimeDeps(pluginDestDir) {
|
||||
return collectPluginRuntimeDeps(pluginDestDir).filter((depName) => {
|
||||
const depPackageJson = join(pluginDestDir, 'node_modules', ...depName.split('/'), 'package.json');
|
||||
return !existsSync(normWin(depPackageJson));
|
||||
});
|
||||
}
|
||||
|
||||
function preparePackagedPluginMirror(nodeModulesRoot, npmName, pluginId, pluginDestDir, platform, arch) {
|
||||
const manifestPath = join(pluginDestDir, 'openclaw.plugin.json');
|
||||
if (!existsSync(normWin(manifestPath)) || !hasPluginRuntimeDeps(pluginDestDir) || missingPluginRuntimeDeps(pluginDestDir).length > 0) {
|
||||
console.log(`[after-pack] Bundling plugin ${npmName} -> ${pluginDestDir}`);
|
||||
const ok = bundlePlugin(nodeModulesRoot, npmName, pluginDestDir);
|
||||
if (!ok) return;
|
||||
} else {
|
||||
console.log(`[after-pack] ✅ Plugin mirror already present: ${pluginDestDir}`);
|
||||
}
|
||||
|
||||
const pluginNM = join(pluginDestDir, 'node_modules');
|
||||
cleanupUnnecessaryFiles(pluginDestDir);
|
||||
if (existsSync(normWin(pluginNM))) {
|
||||
cleanupKoffi(pluginNM, platform, arch);
|
||||
cleanupNativePlatformPackages(pluginNM, platform, arch);
|
||||
}
|
||||
patchPluginIds(pluginDestDir, pluginId);
|
||||
}
|
||||
|
||||
function validateRuntimeDepsManifest(openclawRoot) {
|
||||
const manifestPath = join(openclawRoot, RUNTIME_DEPS_MANIFEST);
|
||||
if (!existsSync(normWin(manifestPath))) {
|
||||
throw new Error(`[after-pack] Missing ${RUNTIME_DEPS_MANIFEST} in packaged OpenClaw resources`);
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(readFileSync(normWin(manifestPath), 'utf8'));
|
||||
const plugins = manifest && typeof manifest === 'object' ? manifest.plugins : null;
|
||||
if (!plugins || typeof plugins !== 'object' || Array.isArray(plugins)) {
|
||||
throw new Error(`[after-pack] Invalid ${RUNTIME_DEPS_MANIFEST}: missing plugins object`);
|
||||
}
|
||||
|
||||
const missing = [];
|
||||
for (const [pluginId, deps] of Object.entries(plugins)) {
|
||||
if (!Array.isArray(deps)) continue;
|
||||
for (const dep of deps) {
|
||||
if (!dep || typeof dep.name !== 'string') continue;
|
||||
const depPackageJson = join(openclawRoot, 'node_modules', ...dep.name.split('/'), 'package.json');
|
||||
if (!existsSync(normWin(depPackageJson))) {
|
||||
missing.push(`${pluginId}:${dep.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`[after-pack] Missing packaged OpenClaw runtime deps: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(`[after-pack] ✅ Verified ${RUNTIME_DEPS_MANIFEST}.`);
|
||||
}
|
||||
|
||||
function hashOpenClawRuntime(openclawRoot) {
|
||||
const packageJsonPath = join(openclawRoot, 'package.json');
|
||||
const manifestPath = join(openclawRoot, RUNTIME_DEPS_MANIFEST);
|
||||
const packageJsonRaw = readFileSync(normWin(packageJsonPath), 'utf8');
|
||||
const manifestRaw = existsSync(normWin(manifestPath)) ? readFileSync(normWin(manifestPath), 'utf8') : '';
|
||||
const packageJson = JSON.parse(packageJsonRaw);
|
||||
const version = packageJson.version || 'unknown';
|
||||
const manifestHash = createHash('sha256')
|
||||
.update(packageJsonRaw)
|
||||
.update('\0')
|
||||
.update(manifestRaw)
|
||||
.digest('hex')
|
||||
.slice(0, 12);
|
||||
const safeVersion = String(version).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
return {
|
||||
key: `openclaw-${safeVersion}-${manifestHash}`,
|
||||
version,
|
||||
manifestHash,
|
||||
};
|
||||
}
|
||||
|
||||
function stageOpenClawRuntimeForPackagedApp(resourcesDir, openclawRoot) {
|
||||
if (!existsSync(normWin(openclawRoot))) {
|
||||
throw new Error(`[after-pack] Cannot stage OpenClaw runtime; missing ${openclawRoot}`);
|
||||
}
|
||||
|
||||
const { key, version, manifestHash } = hashOpenClawRuntime(openclawRoot);
|
||||
const runtimeRoot = join(resourcesDir, 'openclaw-runtime');
|
||||
const targetDir = join(runtimeRoot, key);
|
||||
mkdirSync(normWin(runtimeRoot), { recursive: true });
|
||||
rmSync(normWin(targetDir), { recursive: true, force: true });
|
||||
renameSync(normWin(openclawRoot), normWin(targetDir));
|
||||
writeFileSync(
|
||||
normWin(join(targetDir, OPENCLAW_RUNTIME_READY_MARKER)),
|
||||
`${JSON.stringify({
|
||||
version,
|
||||
manifestHash,
|
||||
source: 'after-pack',
|
||||
createdAt: new Date().toISOString(),
|
||||
}, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
validateRuntimeDepsManifest(targetDir);
|
||||
console.log(`[after-pack] ✅ Staged OpenClaw runtime at ${targetDir}`);
|
||||
return targetDir;
|
||||
}
|
||||
|
||||
// ── Main hook ────────────────────────────────────────────────────────────────
|
||||
|
||||
exports.default = async function afterPack(context) {
|
||||
@@ -562,6 +705,7 @@ exports.default = async function afterPack(context) {
|
||||
console.log(`[after-pack] Copying ${depCount} openclaw dependencies to ${dest} ...`);
|
||||
cpSync(src, dest, { recursive: true });
|
||||
console.log('[after-pack] ✅ openclaw node_modules copied.');
|
||||
validateRuntimeDepsManifest(openclawRoot);
|
||||
|
||||
// Patch broken modules whose CJS transpiled output sets module.exports = undefined,
|
||||
// causing TypeError in Node.js 22+ ESM interop.
|
||||
@@ -582,29 +726,22 @@ exports.default = async function afterPack(context) {
|
||||
mkdirSync(pluginsDestRoot, { recursive: true });
|
||||
for (const { npmName, pluginId } of BUNDLED_PLUGINS) {
|
||||
const pluginDestDir = join(pluginsDestRoot, pluginId);
|
||||
console.log(`[after-pack] Bundling plugin ${npmName} -> ${pluginDestDir}`);
|
||||
const ok = bundlePlugin(nodeModulesRoot, npmName, pluginDestDir);
|
||||
if (ok) {
|
||||
const pluginNM = join(pluginDestDir, 'node_modules');
|
||||
cleanupUnnecessaryFiles(pluginDestDir);
|
||||
if (existsSync(pluginNM)) {
|
||||
cleanupKoffi(pluginNM, platform, arch);
|
||||
cleanupNativePlatformPackages(pluginNM, platform, arch);
|
||||
}
|
||||
// Fix hardcoded plugin ID mismatches in compiled JS
|
||||
patchPluginIds(pluginDestDir, pluginId);
|
||||
preparePackagedPluginMirror(nodeModulesRoot, npmName, pluginId, pluginDestDir, platform, arch);
|
||||
const manifestPath = join(pluginDestDir, 'openclaw.plugin.json');
|
||||
if (!existsSync(normWin(manifestPath))) {
|
||||
throw new Error(`[after-pack] Missing packaged plugin mirror: ${pluginId} (${npmName})`);
|
||||
}
|
||||
const missingRuntimeDeps = missingPluginRuntimeDeps(pluginDestDir);
|
||||
if (missingRuntimeDeps.length > 0) {
|
||||
throw new Error(`[after-pack] Missing packaged plugin runtime deps for ${pluginId}: ${missingRuntimeDeps.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 1.2 Copy built-in extension node_modules that electron-builder skipped.
|
||||
// OpenClaw 3.31+ ships built-in extensions (discord, qqbot, etc.) under
|
||||
// dist/extensions/<ext>/node_modules/. These are skipped by extraResources
|
||||
// because .gitignore contains "node_modules/".
|
||||
//
|
||||
// Extension code is loaded via shared chunks in dist/ (e.g. outbound-*.js)
|
||||
// which resolve modules from the top-level openclaw/node_modules/, NOT from
|
||||
// the extension's own node_modules/. So we must merge extension deps into
|
||||
// the top-level node_modules/ as well.
|
||||
// 1.2 Legacy safety net for build/openclaw bundles that still contain nested
|
||||
// built-in extension node_modules. The current bundle-openclaw.mjs skips
|
||||
// these nested directories and merges their packages into the top-level
|
||||
// OpenClaw node_modules instead, which is where shared dist chunks resolve
|
||||
// bare imports from at runtime.
|
||||
const buildExtDir = join(__dirname, '..', 'build', 'openclaw', 'dist', 'extensions');
|
||||
const packExtDir = join(openclawRoot, 'dist', 'extensions');
|
||||
if (existsSync(buildExtDir)) {
|
||||
@@ -670,6 +807,15 @@ exports.default = async function afterPack(context) {
|
||||
console.log(`[after-pack] ✅ Removed ${nativeRemoved} non-target native platform packages.`);
|
||||
}
|
||||
|
||||
// 4.1 Move the finalized OpenClaw tree into the shape OpenClaw already
|
||||
// recognizes as an external runtime root:
|
||||
// resources/openclaw-runtime/openclaw-<version>-<hash>
|
||||
//
|
||||
// At runtime ClawX sets OPENCLAW_PLUGIN_STAGE_DIR to resources/openclaw-runtime
|
||||
// and launches from the staged openclaw-* directory. This avoids a first-run
|
||||
// copy while still preventing OpenClaw from running npm install.
|
||||
stageOpenClawRuntimeForPackagedApp(resourcesDir, openclawRoot);
|
||||
|
||||
// 5. Patch lru-cache in app.asar.unpacked
|
||||
//
|
||||
// Production dependencies (electron-updater → semver → lru-cache@6,
|
||||
|
||||
@@ -93,13 +93,22 @@ function bundleOnePlugin({ npmName, pluginId }) {
|
||||
|
||||
echo`📦 Bundling plugin ${npmName} -> ${outputDir}`;
|
||||
|
||||
function shouldCopyNodePackageEntry(src) {
|
||||
const base = path.basename(src);
|
||||
return base !== '.vscode' && base !== '.idea';
|
||||
}
|
||||
|
||||
if (fs.existsSync(outputDir)) {
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
// 1) Copy plugin package itself
|
||||
fs.cpSync(realPluginPath, outputDir, { recursive: true, dereference: true });
|
||||
fs.cpSync(realPluginPath, outputDir, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: shouldCopyNodePackageEntry,
|
||||
});
|
||||
|
||||
// 2) Collect transitive deps from pnpm virtual store
|
||||
const collected = new Map();
|
||||
@@ -160,7 +169,11 @@ function bundleOnePlugin({ npmName, pluginId }) {
|
||||
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 });
|
||||
fs.cpSync(normWin(realPath), normWin(dest), {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: shouldCopyNodePackageEntry,
|
||||
});
|
||||
copiedCount++;
|
||||
} catch (err) {
|
||||
echo` ⚠️ Skipped ${pkgName}: ${err.message}`;
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
*/
|
||||
|
||||
import 'zx/globals';
|
||||
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');
|
||||
const RUNTIME_DEPS_MANIFEST = 'clawx-runtime-deps.json';
|
||||
|
||||
// On Windows, pnpm virtual store paths can exceed MAX_PATH (260 chars).
|
||||
function normWin(p) {
|
||||
@@ -40,6 +43,27 @@ if (!fs.existsSync(openclawLink)) {
|
||||
|
||||
const openclawReal = fs.realpathSync(openclawLink);
|
||||
echo` openclaw resolved: ${openclawReal}`;
|
||||
const extensionsDir = path.join(openclawReal, 'dist', 'extensions');
|
||||
|
||||
function shouldCopyOpenClawPackageEntry(src) {
|
||||
const rel = path.relative(openclawReal, src);
|
||||
if (!rel || rel.startsWith('..')) return true;
|
||||
const parts = rel.split(path.sep);
|
||||
|
||||
if (parts[0] === 'dist' && parts[1] === 'extensions') {
|
||||
const nodeModulesIndex = parts.indexOf('node_modules');
|
||||
if (nodeModulesIndex >= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (parts[i] === 'node_modules' && parts[i + 1] === '.bin') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Clean and create output directory
|
||||
if (fs.existsSync(OUTPUT)) {
|
||||
@@ -49,7 +73,11 @@ fs.mkdirSync(OUTPUT, { recursive: true });
|
||||
|
||||
// 3. Copy openclaw package itself to OUTPUT root
|
||||
echo` Copying openclaw package...`;
|
||||
fs.cpSync(openclawReal, OUTPUT, { recursive: true, dereference: true });
|
||||
fs.cpSync(openclawReal, OUTPUT, {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: shouldCopyOpenClawPackageEntry,
|
||||
});
|
||||
|
||||
// 4. Recursively collect ALL transitive dependencies via pnpm virtual store BFS
|
||||
//
|
||||
@@ -188,9 +216,109 @@ echo` Skipped ${skippedDevCount} dev-only package references`;
|
||||
// then BFS its transitive deps exactly like we did for openclaw above.
|
||||
const EXTRA_BUNDLED_PACKAGES = [
|
||||
'@whiskeysockets/baileys', // WhatsApp channel (was a dep of old clawdbot, not openclaw)
|
||||
'@larksuiteoapi/node-sdk', // Fallback for Feishu plugin setup/doctor module resolution
|
||||
'qrcode-terminal', // QR rendering used by WhatsApp/WeChat login helpers
|
||||
];
|
||||
|
||||
const BUNDLED_EXTENSION_RUNTIME_DEP_PLUGIN_IDS = [
|
||||
'acpx',
|
||||
'bonjour',
|
||||
'browser',
|
||||
'discord',
|
||||
'qqbot',
|
||||
'telegram',
|
||||
];
|
||||
|
||||
function readJsonFile(filePath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function collectBundledExtensionRuntimeDeps(extensionsRoot) {
|
||||
const depsByPlugin = {};
|
||||
for (const pluginId of BUNDLED_EXTENSION_RUNTIME_DEP_PLUGIN_IDS) {
|
||||
const packageJson = readJsonFile(path.join(extensionsRoot, pluginId, 'package.json'));
|
||||
if (!packageJson || typeof packageJson !== 'object') {
|
||||
echo`❌ Bundled extension package.json not found for ${pluginId}`;
|
||||
process.exit(1);
|
||||
}
|
||||
const depsByName = {};
|
||||
for (const deps of [packageJson.dependencies, packageJson.optionalDependencies]) {
|
||||
if (!deps || typeof deps !== 'object' || Array.isArray(deps)) continue;
|
||||
for (const [name, version] of Object.entries(deps)) {
|
||||
depsByName[name] = String(version);
|
||||
}
|
||||
}
|
||||
depsByPlugin[pluginId] = Object.entries(depsByName)
|
||||
.map(([name, version]) => ({ name, version }))
|
||||
.sort((left, right) => left.name.localeCompare(right.name));
|
||||
}
|
||||
return depsByPlugin;
|
||||
}
|
||||
|
||||
function flattenRuntimeDeps(depsByPlugin) {
|
||||
return [...new Set(Object.values(depsByPlugin).flat().map(dep => dep.name))]
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function readBundledPackageVersion(nodeModulesDir, pkgName) {
|
||||
const packageJson = readJsonFile(path.join(nodeModulesDir, ...pkgName.split('/'), 'package.json'));
|
||||
return typeof packageJson?.version === 'string' ? packageJson.version : null;
|
||||
}
|
||||
|
||||
function writeRuntimeDepsManifest(outputDir, depsByPlugin) {
|
||||
const nodeModulesDir = path.join(outputDir, 'node_modules');
|
||||
const manifest = {
|
||||
generatedBy: 'scripts/bundle-openclaw.mjs',
|
||||
packageRoot: '.',
|
||||
nodeModulesRoot: 'node_modules',
|
||||
plugins: {},
|
||||
};
|
||||
const missing = [];
|
||||
|
||||
for (const [pluginId, deps] of Object.entries(depsByPlugin)) {
|
||||
manifest.plugins[pluginId] = deps.map((dep) => {
|
||||
const installedVersion = readBundledPackageVersion(nodeModulesDir, dep.name);
|
||||
const present = Boolean(installedVersion);
|
||||
if (!present) {
|
||||
missing.push(`${pluginId}:${dep.name}@${dep.version}`);
|
||||
}
|
||||
return {
|
||||
name: dep.name,
|
||||
version: dep.version,
|
||||
installedVersion,
|
||||
present,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
echo`❌ Missing bundled extension runtime deps: ${missing.join(', ')}`;
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(outputDir, RUNTIME_DEPS_MANIFEST),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
echo` Wrote ${RUNTIME_DEPS_MANIFEST} for ${Object.keys(manifest.plugins).length} bundled extension(s)`;
|
||||
}
|
||||
|
||||
const bundledExtensionRuntimeDeps = collectBundledExtensionRuntimeDeps(extensionsDir);
|
||||
const bundledExtensionRuntimePackages = flattenRuntimeDeps(bundledExtensionRuntimeDeps);
|
||||
for (const pkgName of bundledExtensionRuntimePackages) {
|
||||
if (!EXTRA_BUNDLED_PACKAGES.includes(pkgName)) {
|
||||
EXTRA_BUNDLED_PACKAGES.push(pkgName);
|
||||
}
|
||||
}
|
||||
const preferredBundledPackages = new Set(EXTRA_BUNDLED_PACKAGES);
|
||||
|
||||
let extraCount = 0;
|
||||
const preferredBundledPackageRealPaths = new Set();
|
||||
for (const pkgName of EXTRA_BUNDLED_PACKAGES) {
|
||||
const pkgLink = path.join(NODE_MODULES, ...pkgName.split('/'));
|
||||
if (!fs.existsSync(pkgLink)) {
|
||||
@@ -200,6 +328,7 @@ for (const pkgName of EXTRA_BUNDLED_PACKAGES) {
|
||||
|
||||
let pkgReal;
|
||||
try { pkgReal = fs.realpathSync(pkgLink); } catch { continue; }
|
||||
preferredBundledPackageRealPaths.add(pkgReal);
|
||||
|
||||
if (!collected.has(pkgReal)) {
|
||||
collected.set(pkgReal, pkgName);
|
||||
@@ -247,8 +376,22 @@ fs.mkdirSync(outputNodeModules, { recursive: true });
|
||||
const copiedNames = new Set(); // Track package names already copied
|
||||
let copiedCount = 0;
|
||||
let skippedDupes = 0;
|
||||
const collectedEntries = [...collected].sort(([leftRealPath, leftName], [rightRealPath, rightName]) => {
|
||||
const leftPreferredRealPath = preferredBundledPackageRealPaths.has(leftRealPath);
|
||||
const rightPreferredRealPath = preferredBundledPackageRealPaths.has(rightRealPath);
|
||||
if (leftPreferredRealPath !== rightPreferredRealPath) return leftPreferredRealPath ? -1 : 1;
|
||||
const leftPreferred = preferredBundledPackages.has(leftName);
|
||||
const rightPreferred = preferredBundledPackages.has(rightName);
|
||||
if (leftPreferred === rightPreferred) return 0;
|
||||
return leftPreferred ? -1 : 1;
|
||||
});
|
||||
|
||||
for (const [realPath, pkgName] of collected) {
|
||||
function shouldCopyNodePackageEntry(src) {
|
||||
const base = path.basename(src);
|
||||
return base !== '.vscode' && base !== '.idea';
|
||||
}
|
||||
|
||||
for (const [realPath, pkgName] of collectedEntries) {
|
||||
if (copiedNames.has(pkgName)) {
|
||||
skippedDupes++;
|
||||
continue; // Keep the first version (closer to openclaw in dep tree)
|
||||
@@ -259,7 +402,11 @@ for (const [realPath, pkgName] of collected) {
|
||||
|
||||
try {
|
||||
fs.mkdirSync(normWin(path.dirname(dest)), { recursive: true });
|
||||
fs.cpSync(normWin(realPath), normWin(dest), { recursive: true, dereference: true });
|
||||
fs.cpSync(normWin(realPath), normWin(dest), {
|
||||
recursive: true,
|
||||
dereference: true,
|
||||
filter: shouldCopyNodePackageEntry,
|
||||
});
|
||||
copiedCount++;
|
||||
} catch (err) {
|
||||
echo` ⚠️ Skipped ${pkgName}: ${err.message}`;
|
||||
@@ -279,7 +426,6 @@ for (const [realPath, pkgName] of collected) {
|
||||
// Fix: copy extension deps into the top-level node_modules/ so they are
|
||||
// resolvable from shared chunks. Skip-if-exists preserves version priority
|
||||
// (openclaw's own deps take precedence over extension deps).
|
||||
const extensionsDir = path.join(OUTPUT, 'dist', 'extensions');
|
||||
let mergedExtCount = 0;
|
||||
if (fs.existsSync(extensionsDir)) {
|
||||
for (const extEntry of fs.readdirSync(extensionsDir, { withFileTypes: true })) {
|
||||
@@ -325,6 +471,8 @@ if (mergedExtCount > 0) {
|
||||
echo` Merged ${mergedExtCount} extension packages into top-level node_modules`;
|
||||
}
|
||||
|
||||
writeRuntimeDepsManifest(OUTPUT, bundledExtensionRuntimeDeps);
|
||||
|
||||
// 6. Clean up the bundle to reduce package size
|
||||
//
|
||||
// This removes platform-agnostic waste: dev artifacts, docs, source maps,
|
||||
@@ -470,7 +618,7 @@ function cleanupBundle(outputDir) {
|
||||
'node_modules/koffi/src',
|
||||
'node_modules/koffi/vendor',
|
||||
'node_modules/koffi/doc',
|
||||
'extensions/feishu', // Removed in favor of official @larksuite/openclaw-lark plugin
|
||||
'dist/extensions/feishu', // Removed in favor of official @larksuite/openclaw-lark plugin
|
||||
];
|
||||
for (const rel of LARGE_REMOVALS) {
|
||||
if (rmSafe(path.join(outputDir, rel))) removedCount++;
|
||||
@@ -712,12 +860,20 @@ function patchBundledRuntime(outputDir) {
|
||||
for (const patch of replacePatches) {
|
||||
const target = patch.target();
|
||||
if (!target || !fs.existsSync(target)) {
|
||||
if (patch.required) {
|
||||
echo`❌ Required patch failed for ${patch.label}: target file not found`;
|
||||
process.exit(1);
|
||||
}
|
||||
echo` ⚠️ Skipped patch for ${patch.label}: target file not found`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const current = fs.readFileSync(target, 'utf8');
|
||||
if (!current.includes(patch.search)) {
|
||||
if (patch.required) {
|
||||
echo`❌ Required patch failed for ${patch.label}: expected source snippet not found`;
|
||||
process.exit(1);
|
||||
}
|
||||
echo` ⚠️ Skipped patch for ${patch.label}: expected source snippet not found`;
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user