Reduce packaged app size (#1026)

This commit is contained in:
Lingxuan Zuo
2026-05-17 23:28:09 +08:00
committed by GitHub
parent 34bfae2851
commit 3edeb8cdfa
26 changed files with 760 additions and 116 deletions

View File

@@ -155,6 +155,7 @@ const PLATFORM_NATIVE_SCOPES = {
'@reflink': /^reflink-(darwin|linux|win32)-(x64|arm64|x64-gnu|x64-musl|arm64-gnu|arm64-musl|x64-msvc|arm64-msvc)/,
'@node-llama-cpp': /^(mac|linux|win)-(arm64|x64|armv7l)(-metal|-cuda|-cuda-ext|-vulkan)?$/,
'@esbuild': /^(darwin|linux|win32|android|freebsd|netbsd|openbsd|sunos|aix|openharmony)-(x64|arm64|arm|ia32|loong64|mips64el|ppc64|riscv64|s390x)/,
'@openai': /^codex-(darwin|linux|win32)-(x64|arm64)$/,
};
// Unscoped packages that follow a <name>-<platform>-<arch> convention.
@@ -173,6 +174,13 @@ function baseArch(rawArch) {
return dash > 0 ? rawArch.slice(0, dash) : rawArch;
}
function matchesTargetArch(pkgArch, targetArch) {
if (targetArch === 'universal') {
return pkgArch === 'x64' || pkgArch === 'arm64' || pkgArch === 'universal';
}
return pkgArch === targetArch || pkgArch === 'universal';
}
function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
let removed = 0;
@@ -190,7 +198,7 @@ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
const isMatch =
pkgPlatform === platform &&
(pkgArch === arch || pkgArch === 'universal');
matchesTargetArch(pkgArch, arch);
if (!isMatch) {
try {
@@ -215,7 +223,7 @@ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
const isMatch =
pkgPlatform === platform &&
(pkgArch === arch || pkgArch === 'universal');
matchesTargetArch(pkgArch, arch);
if (!isMatch) {
try {
@@ -229,6 +237,75 @@ function cleanupNativePlatformPackages(nodeModulesDir, platform, arch) {
return removed;
}
function cleanupNodeModulesRuntimeJunk(nodeModulesDir, platform, arch) {
let removed = 0;
const nodeWavDir = join(nodeModulesDir, 'node-wav');
for (const name of ['x.json', 'x.js', 'x.js~', 'file.wav']) {
try {
const target = join(nodeWavDir, name);
if (existsSync(target)) {
rmSync(target, { recursive: true, force: true });
removed++;
}
} catch { /* */ }
}
const treeSitterBashDir = join(nodeModulesDir, 'tree-sitter-bash');
const treeSitterSrc = join(treeSitterBashDir, 'src');
for (const name of ['parser.c', 'scanner.c', 'grammar.json', 'tree_sitter']) {
try {
const target = join(treeSitterSrc, name);
if (existsSync(target)) {
rmSync(target, { recursive: true, force: true });
removed++;
}
} catch { /* */ }
}
const prebuildsDir = join(treeSitterBashDir, 'prebuilds');
if (existsSync(prebuildsDir)) {
for (const entry of readdirSync(prebuildsDir)) {
const [entryPlatform, ...entryArchParts] = entry.split('-');
const entryArch = baseArch(entryArchParts.join('-'));
if (entryPlatform === platform && matchesTargetArch(entryArch, arch)) continue;
try {
rmSync(join(prebuildsDir, entry), { recursive: true, force: true });
removed++;
} catch { /* */ }
}
}
return removed;
}
function cleanupKnownRuntimeJunk(rootDir, platform, arch) {
let removed = 0;
const stack = [rootDir];
while (stack.length > 0) {
const dir = stack.pop();
let entries;
try { entries = readdirSync(normWin(dir), { withFileTypes: true }); } catch { continue; }
if (basename(dir) === 'node_modules') {
removed += cleanupNodeModulesRuntimeJunk(dir, platform, arch);
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
stack.push(join(dir, entry.name));
}
}
return removed;
}
exports.__test = {
cleanupNativePlatformPackages,
cleanupNodeModulesRuntimeJunk,
};
// ── Broken module patcher ─────────────────────────────────────────────────────
// Some bundled packages have transpiled CJS that sets `module.exports = exports.default`
// without ever assigning `exports.default`, leaving module.exports === undefined.
@@ -621,6 +698,10 @@ exports.default = async function afterPack(context) {
const pluginNM = join(pluginDestDir, 'node_modules');
cleanupUnnecessaryFiles(pluginDestDir);
if (existsSync(pluginNM)) {
const pluginJunkRemoved = cleanupKnownRuntimeJunk(pluginDestDir, platform, arch);
if (pluginJunkRemoved > 0) {
console.log(`[after-pack] ✅ ${pluginId}: removed ${pluginJunkRemoved} known runtime junk files/directories.`);
}
cleanupKoffi(pluginNM, platform, arch);
cleanupNativePlatformPackages(pluginNM, platform, arch);
}
@@ -761,7 +842,8 @@ exports.default = async function afterPack(context) {
// 2. General cleanup on the full openclaw directory (not just node_modules)
console.log('[after-pack] 🧹 Cleaning up unnecessary files ...');
const removedRoot = cleanupUnnecessaryFiles(openclawRoot);
console.log(`[after-pack] ✅ Removed ${removedRoot} unnecessary files/directories.`);
const removedKnownJunk = cleanupKnownRuntimeJunk(openclawRoot, platform, arch);
console.log(`[after-pack] ✅ Removed ${removedRoot + removedKnownJunk} unnecessary files/directories.`);
// 3. Platform-specific: strip koffi non-target platform binaries
const koffiRemoved = cleanupKoffi(dest, platform, arch);

View File

@@ -177,6 +177,10 @@ echo` Virtual store root: ${openclawVirtualNM}`;
queue.push({ nodeModulesDir: openclawVirtualNM, skipPkg: 'openclaw' });
const SKIP_PACKAGES = new Set([
// Extra bundled extensions such as @openclaw/codex can declare openclaw as a
// peer/optional dependency. The bundle already copies openclaw to OUTPUT root,
// so do not also copy a duplicate into OUTPUT/node_modules/openclaw.
'openclaw',
'typescript',
'@playwright/test',
// @discordjs/opus is a native .node addon compiled for the system Node.js
@@ -447,6 +451,25 @@ function patchBundledExtensionPackageJsons(extensionsRoot) {
patchBundledExtensionPackageJsons(extensionsDir);
function bundleExternalExtension(pkgName, extensionId) {
const pkgLink = path.join(NODE_MODULES, ...pkgName.split('/'));
if (!fs.existsSync(pkgLink)) {
throw new Error(`Missing extension package "${pkgName}". Run pnpm install first.`);
}
const realPath = fs.realpathSync(pkgLink);
const dest = path.join(extensionsDir, extensionId);
fs.rmSync(normWin(dest), { recursive: true, force: true });
fs.mkdirSync(normWin(path.dirname(dest)), { recursive: true });
fs.cpSync(normWin(realPath), normWin(dest), {
recursive: true,
dereference: true,
});
echo` Bundled external extension ${pkgName} -> dist/extensions/${extensionId}`;
}
bundleExternalExtension('@openclaw/codex', 'codex');
// 6. Clean up the bundle to reduce package size
//
// This removes platform-agnostic waste: dev artifacts, docs, source maps,
@@ -482,6 +505,47 @@ function rmSafe(target) {
} catch { return false; }
}
function cleanupNodeModulesRuntimeJunk(nodeModulesDir) {
let removedCount = 0;
const nodeWavDir = path.join(nodeModulesDir, 'node-wav');
for (const name of ['x.json', 'x.js', 'x.js~', 'file.wav']) {
if (rmSafe(path.join(nodeWavDir, name))) removedCount++;
}
// tree-sitter-bash ships C sources for rebuilding its native addon. Packaged
// builds use the prebuilt addon/wasm; keep node-types.json because the CJS
// entry exposes it as optional runtime metadata.
const treeSitterSrc = path.join(nodeModulesDir, 'tree-sitter-bash', 'src');
for (const name of ['parser.c', 'scanner.c', 'grammar.json', 'tree_sitter']) {
if (rmSafe(path.join(treeSitterSrc, name))) removedCount++;
}
return removedCount;
}
function cleanupKnownRuntimeJunk(rootDir) {
let removedCount = 0;
const stack = [rootDir];
while (stack.length > 0) {
const dir = stack.pop();
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
if (path.basename(dir) === 'node_modules') {
removedCount += cleanupNodeModulesRuntimeJunk(dir);
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
stack.push(path.join(dir, entry.name));
}
}
return removedCount;
}
function cleanupBundle(outputDir) {
let removedCount = 0;
const nm = path.join(outputDir, 'node_modules');
@@ -620,6 +684,8 @@ function cleanupBundle(outputDir) {
if (rmSafe(path.join(outputDir, rel))) removedCount++;
}
removedCount += cleanupKnownRuntimeJunk(outputDir);
return removedCount;
}

View File

@@ -28,6 +28,11 @@ export const EXTRA_BUNDLED_PACKAGES = [
// transitive dependency graph from the app bundle context.
'playwright-core',
// The Codex agent harness is published as an OpenClaw extension package.
// Bundle it with the runtime so openai-codex models can register the codex
// harness in packaged builds.
'@openclaw/codex',
// Electron main process QR login flows resolve these files from the
// bundled OpenClaw runtime context in packaged builds.
'qrcode-terminal',