diff --git a/package.json b/package.json index c6ef54673e0..db40952a030 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dist/", "!dist/**/*.map", "!dist/plugin-sdk/.tsbuildinfo", + "!dist/extensions/node_modules/**", "!dist/extensions/*/node_modules/**", "!dist/extensions/qa-channel/**", "dist/extensions/qa-channel/runtime-api.js", diff --git a/scripts/stage-bundled-plugin-runtime.mjs b/scripts/stage-bundled-plugin-runtime.mjs index fcf7cb750f5..65531b75fae 100644 --- a/scripts/stage-bundled-plugin-runtime.mjs +++ b/scripts/stage-bundled-plugin-runtime.mjs @@ -68,6 +68,57 @@ function symlinkPath(sourcePath, targetPath, type) { ensureSymlink(relativeSymlinkTarget(sourcePath, targetPath), targetPath, type, sourcePath); } +function writeJsonFile(targetPath, value) { + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +function removeStaleOpenClawSelfReference(sourcePluginNodeModulesDir, repoRoot) { + if (!fs.existsSync(sourcePluginNodeModulesDir)) { + return; + } + + const selfReferencePath = path.join(sourcePluginNodeModulesDir, "openclaw"); + try { + const existing = fs.lstatSync(selfReferencePath); + if (!existing.isSymbolicLink()) { + return; + } + if (fs.realpathSync(selfReferencePath) === fs.realpathSync(repoRoot)) { + removePathIfExists(selfReferencePath); + } + } catch (error) { + if (error?.code !== "ENOENT") { + throw error; + } + } +} + +function ensureOpenClawExtensionAlias(params) { + const pluginSdkDir = path.join(params.repoRoot, "dist", "plugin-sdk"); + if (!fs.existsSync(pluginSdkDir)) { + return; + } + + const aliasDir = path.join(params.distExtensionsRoot, "node_modules", "openclaw"); + const pluginSdkAliasPath = path.join(aliasDir, "plugin-sdk"); + fs.mkdirSync(aliasDir, { recursive: true }); + writeJsonFile(path.join(aliasDir, "package.json"), { + name: "openclaw", + type: "module", + exports: { + "./plugin-sdk": "./plugin-sdk/index.js", + "./plugin-sdk/*": "./plugin-sdk/*.js", + }, + }); + ensureSymlink( + relativeSymlinkTarget(pluginSdkDir, pluginSdkAliasPath), + pluginSdkAliasPath, + symlinkType(), + pluginSdkDir, + ); +} + function shouldWrapRuntimeJsFile(sourcePath) { return path.extname(sourcePath) === ".js"; } @@ -144,6 +195,7 @@ function linkPluginNodeModules(params) { if (!fs.existsSync(params.sourcePluginNodeModulesDir)) { return; } + removeStaleOpenClawSelfReference(params.sourcePluginNodeModulesDir, params.repoRoot); ensureSymlink( params.sourcePluginNodeModulesDir, runtimeNodeModulesDir, @@ -166,9 +218,10 @@ export function stageBundledPluginRuntime(params = {}) { removePathIfExists(runtimeRoot); fs.mkdirSync(runtimeExtensionsRoot, { recursive: true }); + ensureOpenClawExtensionAlias({ repoRoot, distExtensionsRoot }); for (const dirent of fs.readdirSync(distExtensionsRoot, { withFileTypes: true })) { - if (!dirent.isDirectory()) { + if (!dirent.isDirectory() || dirent.name === "node_modules") { continue; } const distPluginDir = path.join(distExtensionsRoot, dirent.name); @@ -177,6 +230,7 @@ export function stageBundledPluginRuntime(params = {}) { stagePluginRuntimeOverlay(distPluginDir, runtimePluginDir); linkPluginNodeModules({ + repoRoot, runtimePluginDir, sourcePluginNodeModulesDir: distPluginNodeModulesDir, }); diff --git a/src/plugins/stage-bundled-plugin-runtime.test.ts b/src/plugins/stage-bundled-plugin-runtime.test.ts index 6cfb034f3b9..ff6c7aa30ee 100644 --- a/src/plugins/stage-bundled-plugin-runtime.test.ts +++ b/src/plugins/stage-bundled-plugin-runtime.test.ts @@ -80,6 +80,7 @@ describe("stageBundledPluginRuntime", () => { const repoRoot = makeRepoRoot("openclaw-stage-bundled-runtime-"); const distPluginDir = createDistPluginDir(repoRoot, "diffs"); fs.mkdirSync(path.join(repoRoot, "dist"), { recursive: true }); + fs.mkdirSync(path.join(repoRoot, "dist", "plugin-sdk"), { recursive: true }); fs.mkdirSync(path.join(distPluginDir, "node_modules", "@pierre", "diffs"), { recursive: true, }); @@ -102,6 +103,31 @@ describe("stageBundledPluginRuntime", () => { fs.realpathSync(path.join(distPluginDir, "node_modules")), ); expect(fs.existsSync(path.join(distPluginDir, "node_modules"))).toBe(true); + expect( + fs + .lstatSync( + path.join(repoRoot, "dist", "extensions", "node_modules", "openclaw", "plugin-sdk"), + ) + .isSymbolicLink(), + ).toBe(true); + expect( + fs.readFileSync( + path.join(repoRoot, "dist", "extensions", "node_modules", "openclaw", "package.json"), + "utf8", + ), + ).toContain('"./plugin-sdk": "./plugin-sdk/index.js"'); + expect( + fs.readFileSync( + path.join(repoRoot, "dist", "extensions", "node_modules", "openclaw", "package.json"), + "utf8", + ), + ).toContain('"./plugin-sdk/*": "./plugin-sdk/*.js"'); + expect( + fs.realpathSync( + path.join(repoRoot, "dist", "extensions", "node_modules", "openclaw", "plugin-sdk"), + ), + ).toBe(fs.realpathSync(path.join(repoRoot, "dist", "plugin-sdk"))); + expect(fs.existsSync(path.join(runtimePluginDir, "node_modules", "openclaw"))).toBe(false); }); it("writes wrappers that forward plugin entry imports into canonical dist files", async () => {