fix: resolve extension dependency missing in shared chunks (#765) (#771)

This commit is contained in:
paisley
2026-04-07 10:04:03 +08:00
committed by GitHub
Unverified
parent c866205eac
commit 0cdd12cd40
2 changed files with 132 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
import { app } from 'electron';
import path from 'path';
import { existsSync, readFileSync, mkdirSync, rmSync } from 'fs';
import { existsSync, readFileSync, mkdirSync, readdirSync, rmSync, symlinkSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
@@ -164,6 +164,73 @@ function ensureConfiguredPluginsUpgraded(configuredChannels: string[]): void {
}
}
/**
* Ensure extension-specific packages are resolvable from shared dist/ chunks.
*
* OpenClaw's Rollup bundler creates shared chunks in dist/ (e.g.
* sticker-cache-*.js) that eagerly `import "grammy"`. ESM bare specifier
* resolution walks from the importing file's directory upward:
* dist/node_modules/ → openclaw/node_modules/ → …
* It does NOT search `dist/extensions/telegram/node_modules/`.
*
* NODE_PATH only works for CJS require(), NOT for ESM import statements.
*
* Fix: create symlinks in openclaw/node_modules/ pointing to packages in
* dist/extensions/<ext>/node_modules/. This makes the standard ESM
* resolution algorithm find them. Skip-if-exists avoids overwriting
* openclaw's own deps (they take priority).
*/
function ensureExtensionDepsResolvable(openclawDir: string): void {
const extDir = join(openclawDir, 'dist', 'extensions');
const topNM = join(openclawDir, 'node_modules');
let linkedCount = 0;
try {
if (!existsSync(extDir)) return;
for (const ext of readdirSync(extDir, { withFileTypes: true })) {
if (!ext.isDirectory()) continue;
const extNM = join(extDir, ext.name, 'node_modules');
if (!existsSync(extNM)) continue;
for (const pkg of readdirSync(extNM, { withFileTypes: true })) {
if (pkg.name === '.bin') continue;
if (pkg.name.startsWith('@')) {
// Scoped package — iterate sub-entries
const scopeDir = join(extNM, pkg.name);
let scopeEntries;
try { scopeEntries = readdirSync(scopeDir, { withFileTypes: true }); } catch { continue; }
for (const sub of scopeEntries) {
if (!sub.isDirectory()) continue;
const dest = join(topNM, pkg.name, sub.name);
if (existsSync(dest)) continue;
try {
mkdirSync(join(topNM, pkg.name), { recursive: true });
symlinkSync(join(scopeDir, sub.name), dest);
linkedCount++;
} catch { /* skip on error — non-fatal */ }
}
} else {
const dest = join(topNM, pkg.name);
if (existsSync(dest)) continue;
try {
mkdirSync(topNM, { recursive: true });
symlinkSync(join(extNM, pkg.name), dest);
linkedCount++;
} catch { /* skip on error — non-fatal */ }
}
}
}
} catch {
// extensions dir may not exist or be unreadable — non-fatal
}
if (linkedCount > 0) {
logger.info(`[extension-deps] Linked ${linkedCount} extension packages into ${topNM}`);
}
}
// ── Pre-launch sync ──────────────────────────────────────────────
export async function syncGatewayConfigBeforeLaunch(
@@ -365,6 +432,11 @@ export async function prepareGatewayLaunchContext(port: number): Promise<Gateway
OPENCLAW_NO_RESPAWN: '1',
};
// Ensure extension-specific packages (e.g. grammy from the telegram
// extension) are resolvable by shared dist/ chunks via symlinks in
// openclaw/node_modules/. NODE_PATH does NOT work for ESM imports.
ensureExtensionDepsResolvable(openclawDir);
return {
appSettings,
openclawDir,

View File

@@ -266,6 +266,65 @@ for (const [realPath, pkgName] of collected) {
}
}
// 5b. Merge built-in extension node_modules into top-level node_modules
//
// OpenClaw 3.31+ ships built-in extensions (telegram, discord, etc.) under
// dist/extensions/<ext>/node_modules/. The Rollup bundler creates shared
// chunks at dist/ root (e.g. sticker-cache-*.js) that eagerly import
// extension-specific packages like "grammy". Node.js resolves bare
// specifiers from the importing file's directory upward:
// dist/ → openclaw/ → openclaw/node_modules/
// It does NOT search dist/extensions/telegram/node_modules/.
//
// 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 })) {
if (!extEntry.isDirectory()) continue;
const extNM = path.join(extensionsDir, extEntry.name, 'node_modules');
if (!fs.existsSync(extNM)) continue;
for (const pkgEntry of fs.readdirSync(extNM, { withFileTypes: true })) {
if (!pkgEntry.isDirectory() || pkgEntry.name === '.bin') continue;
const srcPkg = path.join(extNM, pkgEntry.name);
if (pkgEntry.name.startsWith('@')) {
// Scoped package — iterate sub-entries
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);
mergedExtCount++;
} catch { /* skip on copy error */ }
}
} else {
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);
mergedExtCount++;
} catch { /* skip on copy error */ }
}
}
}
}
if (mergedExtCount > 0) {
echo` Merged ${mergedExtCount} extension packages into top-level node_modules`;
}
// 6. Clean up the bundle to reduce package size
//
// This removes platform-agnostic waste: dev artifacts, docs, source maps,