Fix package issue (#952)
This commit is contained in:
59
electron/utils/runtime-package-resolution.ts
Normal file
59
electron/utils/runtime-package-resolution.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { createRequire } from 'node:module';
|
||||
import { join } from 'node:path';
|
||||
import { getOpenClawDir, getOpenClawResolvedDir } from './paths';
|
||||
|
||||
export type RuntimeModuleResolver = {
|
||||
label: string;
|
||||
resolve(specifier: string): string;
|
||||
};
|
||||
|
||||
export function resolveModulePathWithFallbacks(
|
||||
specifier: string,
|
||||
resolvers: RuntimeModuleResolver[],
|
||||
): string {
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const resolver of resolvers) {
|
||||
try {
|
||||
return resolver.resolve(specifier);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
errors.push(`${resolver.label}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Failed to resolve "${specifier}" from any runtime context. ${errors.join(' | ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
function getRuntimeModuleResolvers(): RuntimeModuleResolver[] {
|
||||
const candidates: Array<{ label: string; base: string | URL }> = [
|
||||
{ label: 'openclaw-resolved', base: join(getOpenClawResolvedDir(), 'package.json') },
|
||||
{ label: 'openclaw', base: join(getOpenClawDir(), 'package.json') },
|
||||
{ label: 'app', base: import.meta.url },
|
||||
];
|
||||
|
||||
const seen = new Set<string>();
|
||||
const resolvers: RuntimeModuleResolver[] = [];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const key = typeof candidate.base === 'string' ? candidate.base : candidate.base.toString();
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
|
||||
const runtimeRequire = createRequire(candidate.base);
|
||||
resolvers.push({
|
||||
label: candidate.label,
|
||||
resolve: runtimeRequire.resolve.bind(runtimeRequire),
|
||||
});
|
||||
}
|
||||
|
||||
return resolvers;
|
||||
}
|
||||
|
||||
export function resolveOpenClawRuntimeModulePath(specifier: string): string {
|
||||
return resolveModulePathWithFallbacks(specifier, getRuntimeModuleResolvers());
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { deflateSync } from 'node:zlib';
|
||||
import { normalizeOpenClawAccountId } from './channel-alias';
|
||||
import { getOpenClawResolvedDir } from './paths';
|
||||
import { resolveOpenClawRuntimeModulePath } from './runtime-package-resolution';
|
||||
|
||||
export const DEFAULT_WECHAT_BASE_URL = 'https://ilinkai.weixin.qq.com';
|
||||
const DEFAULT_ILINK_BOT_TYPE = '3';
|
||||
@@ -43,9 +43,10 @@ function getQrRenderDeps(): QrRenderDeps {
|
||||
return qrRenderDeps;
|
||||
}
|
||||
|
||||
const openclawRequire = createRequire(join(getOpenClawResolvedDir(), 'package.json'));
|
||||
const qrCodeModulePath = openclawRequire.resolve('qrcode-terminal/vendor/QRCode/index.js');
|
||||
const qrErrorCorrectLevelPath = openclawRequire.resolve('qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js');
|
||||
const qrCodeModulePath = resolveOpenClawRuntimeModulePath('qrcode-terminal/vendor/QRCode/index.js');
|
||||
const qrErrorCorrectLevelPath = resolveOpenClawRuntimeModulePath(
|
||||
'qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js',
|
||||
);
|
||||
qrRenderDeps = {
|
||||
QRCode: require(qrCodeModulePath),
|
||||
QRErrorCorrectLevel: require(qrErrorCorrectLevelPath),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { EventEmitter } from 'events';
|
||||
import { existsSync, mkdirSync, rmSync, readdirSync } from 'fs';
|
||||
import { deflateSync } from 'zlib';
|
||||
import { getOpenClawDir, getOpenClawResolvedDir } from './paths';
|
||||
import { resolveOpenClawRuntimeModulePath } from './runtime-package-resolution';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
@@ -40,8 +41,6 @@ function resolveOpenClawPackageJson(packageName: string): string {
|
||||
}
|
||||
|
||||
const baileysPath = dirname(resolveOpenClawPackageJson('@whiskeysockets/baileys'));
|
||||
const qrCodeModulePath = openclawRequire.resolve('qrcode-terminal/vendor/QRCode/index.js');
|
||||
const qrErrorCorrectLevelPath = openclawRequire.resolve('qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js');
|
||||
|
||||
// Load Baileys dependencies dynamically
|
||||
const {
|
||||
@@ -51,10 +50,6 @@ const {
|
||||
fetchLatestBaileysVersion
|
||||
} = require(baileysPath);
|
||||
|
||||
// Load QRCode dependencies dynamically
|
||||
const QRCodeModule = require(qrCodeModulePath);
|
||||
const QRErrorCorrectLevelModule = require(qrErrorCorrectLevelPath);
|
||||
|
||||
// Types from Baileys (approximate since we don't have types for dynamic require)
|
||||
interface BaileysError extends Error {
|
||||
output?: { statusCode?: number };
|
||||
@@ -68,12 +63,43 @@ type ConnectionState = {
|
||||
qr?: string;
|
||||
};
|
||||
|
||||
type QrCodeMatrix = {
|
||||
addData(input: string): void;
|
||||
make(): void;
|
||||
getModuleCount(): number;
|
||||
isDark(row: number, col: number): boolean;
|
||||
};
|
||||
type QrCodeConstructor = new (typeNumber: number, errorCorrectionLevel: unknown) => QrCodeMatrix;
|
||||
type QrErrorCorrectLevelModule = {
|
||||
L: unknown;
|
||||
};
|
||||
type QrRenderDeps = {
|
||||
QRCode: QrCodeConstructor;
|
||||
QRErrorCorrectLevel: QrErrorCorrectLevelModule;
|
||||
};
|
||||
|
||||
let qrRenderDeps: QrRenderDeps | null = null;
|
||||
|
||||
function getQrRenderDeps(): QrRenderDeps {
|
||||
if (qrRenderDeps) {
|
||||
return qrRenderDeps;
|
||||
}
|
||||
|
||||
const qrCodeModulePath = resolveOpenClawRuntimeModulePath('qrcode-terminal/vendor/QRCode/index.js');
|
||||
const qrErrorCorrectLevelPath = resolveOpenClawRuntimeModulePath(
|
||||
'qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js',
|
||||
);
|
||||
qrRenderDeps = {
|
||||
QRCode: require(qrCodeModulePath),
|
||||
QRErrorCorrectLevel: require(qrErrorCorrectLevelPath),
|
||||
};
|
||||
return qrRenderDeps;
|
||||
}
|
||||
|
||||
// --- QR Generation Logic (Adapted from OpenClaw) ---
|
||||
|
||||
const QRCode = QRCodeModule;
|
||||
const QRErrorCorrectLevel = QRErrorCorrectLevelModule;
|
||||
|
||||
function createQrMatrix(input: string) {
|
||||
const { QRCode, QRErrorCorrectLevel } = getQrRenderDeps();
|
||||
const qr = new QRCode(-1, QRErrorCorrectLevel.L);
|
||||
qr.addData(input);
|
||||
qr.make();
|
||||
|
||||
@@ -615,11 +615,12 @@ exports.default = async function afterPack(context) {
|
||||
const srcNM = join(buildExtDir, extEntry.name, 'node_modules');
|
||||
if (!existsSync(srcNM)) continue;
|
||||
|
||||
// Copy to extension's own node_modules (for direct requires from extension code)
|
||||
// Copy to extension's own node_modules (for direct requires from extension code).
|
||||
// electron-builder may leave behind an empty destination directory when it
|
||||
// skips node_modules content via .gitignore filtering, so always replace it.
|
||||
const destExtNM = join(packExtDir, extEntry.name, 'node_modules');
|
||||
if (!existsSync(destExtNM)) {
|
||||
cpSync(srcNM, destExtNM, { recursive: true });
|
||||
}
|
||||
rmSync(destExtNM, { recursive: true, force: true });
|
||||
cpSync(srcNM, destExtNM, { recursive: true });
|
||||
extNMCount++;
|
||||
|
||||
// Merge into top-level openclaw/node_modules/ (for shared chunks in dist/)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
import 'zx/globals';
|
||||
import { EXTRA_BUNDLED_PACKAGES } from './openclaw-bundle-config.mjs';
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const OUTPUT = path.join(ROOT, 'build', 'openclaw');
|
||||
@@ -41,6 +42,20 @@ if (!fs.existsSync(openclawLink)) {
|
||||
const openclawReal = fs.realpathSync(openclawLink);
|
||||
echo` openclaw resolved: ${openclawReal}`;
|
||||
|
||||
function shouldCopyOpenClawPackageEntry(src) {
|
||||
const rel = path.relative(openclawReal, src);
|
||||
if (!rel || rel.startsWith('..')) return true;
|
||||
const parts = rel.split(path.sep);
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i += 1) {
|
||||
if (parts[i] === 'node_modules' && parts[i + 1] === '.bin') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Clean and create output directory
|
||||
if (fs.existsSync(OUTPUT)) {
|
||||
fs.rmSync(OUTPUT, { recursive: true });
|
||||
@@ -49,7 +64,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
|
||||
//
|
||||
@@ -186,10 +205,6 @@ echo` Skipped ${skippedDevCount} dev-only package references`;
|
||||
//
|
||||
// For each package we resolve it from the workspace's own node_modules,
|
||||
// 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)
|
||||
];
|
||||
|
||||
let extraCount = 0;
|
||||
for (const pkgName of EXTRA_BUNDLED_PACKAGES) {
|
||||
const pkgLink = path.join(NODE_MODULES, ...pkgName.split('/'));
|
||||
@@ -248,7 +263,31 @@ const copiedNames = new Set(); // Track package names already copied
|
||||
let copiedCount = 0;
|
||||
let skippedDupes = 0;
|
||||
|
||||
for (const [realPath, pkgName] of collected) {
|
||||
const preferredBundledPackages = new Set(EXTRA_BUNDLED_PACKAGES);
|
||||
const preferredBundledPackageRealPaths = new Set();
|
||||
for (const pkgName of EXTRA_BUNDLED_PACKAGES) {
|
||||
const pkgLink = path.join(NODE_MODULES, ...pkgName.split('/'));
|
||||
if (!fs.existsSync(pkgLink)) continue;
|
||||
try {
|
||||
preferredBundledPackageRealPaths.add(fs.realpathSync(pkgLink));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
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 leftPreferred ? -1 : 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (const [realPath, pkgName] of collectedEntries) {
|
||||
if (copiedNames.has(pkgName)) {
|
||||
skippedDupes++;
|
||||
continue; // Keep the first version (closer to openclaw in dep tree)
|
||||
@@ -280,50 +319,106 @@ for (const [realPath, pkgName] of collected) {
|
||||
// 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');
|
||||
function readJsonSafe(filePath) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function listPackageDeps(pkgJson) {
|
||||
return Object.keys({
|
||||
...(pkgJson?.dependencies && typeof pkgJson.dependencies === 'object' ? pkgJson.dependencies : {}),
|
||||
...(pkgJson?.optionalDependencies && typeof pkgJson.optionalDependencies === 'object' ? pkgJson.optionalDependencies : {}),
|
||||
}).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
let mergedExtCount = 0;
|
||||
let mirroredExtRuntimeDeps = 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;
|
||||
const extRoot = path.join(extensionsDir, extEntry.name);
|
||||
const extNM = path.join(extRoot, 'node_modules');
|
||||
|
||||
for (const pkgEntry of fs.readdirSync(extNM, { withFileTypes: true })) {
|
||||
if (!pkgEntry.isDirectory() || pkgEntry.name === '.bin') continue;
|
||||
const srcPkg = path.join(extNM, pkgEntry.name);
|
||||
if (fs.existsSync(extNM)) {
|
||||
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);
|
||||
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.mkdirSync(normWin(path.dirname(destScoped)), { recursive: true });
|
||||
fs.cpSync(normWin(srcScoped), normWin(destScoped), { recursive: true, dereference: true });
|
||||
copiedNames.add(scopedName);
|
||||
fs.cpSync(normWin(srcPkg), normWin(destPkg), { recursive: true, dereference: true });
|
||||
copiedNames.add(pkgEntry.name);
|
||||
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 */ }
|
||||
}
|
||||
}
|
||||
|
||||
const extPkg = readJsonSafe(path.join(extRoot, 'package.json'));
|
||||
for (const depName of listPackageDeps(extPkg)) {
|
||||
const srcPkg = path.join(outputNodeModules, ...depName.split('/'));
|
||||
const destPkg = path.join(extNM, ...depName.split('/'));
|
||||
if (!fs.existsSync(srcPkg) || fs.existsSync(destPkg)) continue;
|
||||
try {
|
||||
fs.mkdirSync(normWin(path.dirname(destPkg)), { recursive: true });
|
||||
fs.cpSync(normWin(srcPkg), normWin(destPkg), { recursive: true, dereference: true });
|
||||
mirroredExtRuntimeDeps++;
|
||||
} catch { /* skip on copy error */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mergedExtCount > 0) {
|
||||
echo` Merged ${mergedExtCount} extension packages into top-level node_modules`;
|
||||
}
|
||||
if (mirroredExtRuntimeDeps > 0) {
|
||||
echo` Mirrored ${mirroredExtRuntimeDeps} extension runtime deps into dist/extensions/*/node_modules`;
|
||||
}
|
||||
|
||||
function patchBundledExtensionPackageJsons(extensionsRoot) {
|
||||
let patchedCount = 0;
|
||||
|
||||
const discordPkgPath = path.join(extensionsRoot, 'discord', 'package.json');
|
||||
if (fs.existsSync(discordPkgPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(discordPkgPath, 'utf8'));
|
||||
if (pkg?.dependencies?.opusscript === '^0.0.8') {
|
||||
pkg.dependencies.opusscript = '^0.1.1';
|
||||
fs.writeFileSync(discordPkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
||||
patchedCount++;
|
||||
echo` 🩹 Patched discord bundled runtime dep range: opusscript ^0.0.8 -> ^0.1.1`;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return patchedCount;
|
||||
}
|
||||
|
||||
patchBundledExtensionPackageJsons(extensionsDir);
|
||||
|
||||
// 6. Clean up the bundle to reduce package size
|
||||
//
|
||||
|
||||
23
scripts/openclaw-bundle-config.mjs
Normal file
23
scripts/openclaw-bundle-config.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
export const EXTRA_BUNDLED_PACKAGES = [
|
||||
'@whiskeysockets/baileys',
|
||||
|
||||
// Built-in channel/runtime extension deps that are not always pulled in by the
|
||||
// OpenClaw package's own transitive dependency graph, but are required in
|
||||
// packaged builds when dist/extensions/<channel>/*.js resolves bare imports
|
||||
// from resources/openclaw/node_modules.
|
||||
'@larksuiteoapi/node-sdk',
|
||||
'@grammyjs/runner',
|
||||
'@grammyjs/transformer-throttler',
|
||||
'grammy',
|
||||
'@buape/carbon',
|
||||
'@discordjs/voice',
|
||||
'discord-api-types',
|
||||
'opusscript',
|
||||
'@tencent-connect/qqbot-connector',
|
||||
'mpg123-decoder',
|
||||
'silk-wasm',
|
||||
|
||||
// Electron main process QR login flows resolve these files from the
|
||||
// bundled OpenClaw runtime context in packaged builds.
|
||||
'qrcode-terminal',
|
||||
];
|
||||
24
tests/unit/openclaw-bundle-config.test.ts
Normal file
24
tests/unit/openclaw-bundle-config.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('openclaw bundle config', () => {
|
||||
it('includes Electron runtime-only packages needed in packaged builds', async () => {
|
||||
const { EXTRA_BUNDLED_PACKAGES } = await import('../../scripts/openclaw-bundle-config.mjs');
|
||||
|
||||
expect(EXTRA_BUNDLED_PACKAGES).toEqual(expect.arrayContaining([
|
||||
'@whiskeysockets/baileys',
|
||||
'@larksuiteoapi/node-sdk',
|
||||
'@grammyjs/runner',
|
||||
'@grammyjs/transformer-throttler',
|
||||
'grammy',
|
||||
'@buape/carbon',
|
||||
'@discordjs/voice',
|
||||
'discord-api-types',
|
||||
'opusscript',
|
||||
'@tencent-connect/qqbot-connector',
|
||||
'mpg123-decoder',
|
||||
'silk-wasm',
|
||||
'qrcode-terminal',
|
||||
]));
|
||||
});
|
||||
});
|
||||
30
tests/unit/runtime-package-resolution.test.ts
Normal file
30
tests/unit/runtime-package-resolution.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { resolveModulePathWithFallbacks } from '@electron/utils/runtime-package-resolution';
|
||||
|
||||
describe('runtime package resolution', () => {
|
||||
it('returns the first successful resolver result', () => {
|
||||
const openclawResolve = vi.fn(() => {
|
||||
throw new Error('missing from openclaw');
|
||||
});
|
||||
const appResolve = vi.fn(() => '/tmp/app/node_modules/qrcode-terminal/vendor/QRCode/index.js');
|
||||
|
||||
const resolved = resolveModulePathWithFallbacks('qrcode-terminal/vendor/QRCode/index.js', [
|
||||
{ label: 'openclaw', resolve: openclawResolve },
|
||||
{ label: 'app', resolve: appResolve },
|
||||
]);
|
||||
|
||||
expect(resolved).toBe('/tmp/app/node_modules/qrcode-terminal/vendor/QRCode/index.js');
|
||||
expect(openclawResolve).toHaveBeenCalledOnce();
|
||||
expect(appResolve).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('surfaces all resolver failures in the final error', () => {
|
||||
expect(() => resolveModulePathWithFallbacks('qrcode-terminal/vendor/QRCode/index.js', [
|
||||
{ label: 'openclaw', resolve: () => { throw new Error('not bundled'); } },
|
||||
{ label: 'app', resolve: () => { throw new Error('not packaged'); } },
|
||||
])).toThrow(
|
||||
'openclaw: not bundled | app: not packaged',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user