Reduce packaged app size (#1026)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user