From ab53c62d59aee6f845fc8f072aef43439a784fed Mon Sep 17 00:00:00 2001 From: Linfang Wang Date: Sat, 18 Apr 2026 16:24:06 +0800 Subject: [PATCH] release: fix packaged release-check workflow --- docs/.generated/config-baseline.sha256 | 8 +-- .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- extensions/googlechat/src/config-schema.ts | 5 +- extensions/telegram/src/config-schema.ts | 5 +- extensions/x/channel-plugin-api.ts | 3 + extensions/x/index.test.ts | 11 ++++ extensions/x/index.ts | 28 +++++---- extensions/x/runtime-api.ts | 3 + package.json | 2 +- scripts/release-check.ts | 45 +++++++++++++- scripts/runtime-postbuild.mjs | 59 +++++++++++++++++++ src/plugin-sdk/root-alias.cjs | 23 ++++++-- .../contracts/plugin-sdk-root-alias.test.ts | 20 +++++++ src/plugins/sdk-alias.test.ts | 42 ++++++++++++- src/plugins/sdk-alias.ts | 30 ++++++++-- test/release-check.test.ts | 56 ++++++++++++++++++ test/scripts/runtime-postbuild.test.ts | 45 ++++++++++++++ 17 files changed, 357 insertions(+), 32 deletions(-) create mode 100644 extensions/x/channel-plugin-api.ts create mode 100644 extensions/x/index.test.ts create mode 100644 extensions/x/runtime-api.ts diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index 47d8446206974..3fbaf50ee95cd 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -900c26a9b060f1dfa712abfba877bd3bf9c7b0c9f2294faf9834038283ec24b6 config-baseline.json -d956a1d60f776bba712cb04374a4f5657cad95bb088b536c5e3e4e29d4a21328 config-baseline.core.json -ef83a06633fc001b5b2535566939186ecb49d05cd1a90b40e54cc58d3e6e44e3 config-baseline.channel.json -5f5d4e850df6e9854a85b5d008236854ce185c707fdbb566efcf00f8c08b36e3 config-baseline.plugin.json +a877055a70d2089a7c823caf909b51e3c1e589dc62f4a5f82ff0457caed9c916 config-baseline.json +a3f1abce6515f78ae7f4100bb1c9d32c85729687bf7cb0f4142bfe54c8d5d167 config-baseline.core.json +5a9d96d617732a6e93ef22259d0359c1e14c9b2f2624a6880639f7012fbedfcd config-baseline.channel.json +77639c2fa47bfa62452779cd9286a1ec6fa95ef4f975b71abce3e4aac34005f4 config-baseline.plugin.json diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 489e0cba74aa2..4e914269e67f1 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -73091009a0a45c72eded8003fdf9cf4c10e9470c4a055592a98ea00d55cd45d1 plugin-sdk-api-baseline.json -9c9d59ffc0b3b6677794cb8fd5afd0208dbc9f3cd1ad59b30ee627f6f6352929 plugin-sdk-api-baseline.jsonl +9e1ab526b7683e8790ce6edc3ea4d90b4b73e30505849f73da63c7dd7804eaa3 plugin-sdk-api-baseline.json +3d02b27553993312e83747bfe862db1d597aff8efea59352f5e5f84498c5d843 plugin-sdk-api-baseline.jsonl diff --git a/extensions/googlechat/src/config-schema.ts b/extensions/googlechat/src/config-schema.ts index 686d4a0b99c4c..21249d67e7344 100644 --- a/extensions/googlechat/src/config-schema.ts +++ b/extensions/googlechat/src/config-schema.ts @@ -1,3 +1,6 @@ -import { buildChannelConfigSchema, GoogleChatConfigSchema } from "openclaw/plugin-sdk/googlechat"; +import { + buildChannelConfigSchema, + GoogleChatConfigSchema, +} from "openclaw/plugin-sdk/channel-config-schema"; export const GoogleChatChannelConfigSchema = buildChannelConfigSchema(GoogleChatConfigSchema); diff --git a/extensions/telegram/src/config-schema.ts b/extensions/telegram/src/config-schema.ts index a4f3b2ad043dd..16849c4ee571a 100644 --- a/extensions/telegram/src/config-schema.ts +++ b/extensions/telegram/src/config-schema.ts @@ -1,4 +1,7 @@ -import { buildChannelConfigSchema, TelegramConfigSchema } from "../config-api.js"; +import { + buildChannelConfigSchema, + TelegramConfigSchema, +} from "openclaw/plugin-sdk/channel-config-schema"; import { telegramChannelConfigUiHints } from "./config-ui-hints.js"; export const TelegramChannelConfigSchema = buildChannelConfigSchema(TelegramConfigSchema, { diff --git a/extensions/x/channel-plugin-api.ts b/extensions/x/channel-plugin-api.ts new file mode 100644 index 0000000000000..ccd5e594d50df --- /dev/null +++ b/extensions/x/channel-plugin-api.ts @@ -0,0 +1,3 @@ +// Keep bundled channel entry imports narrow so package discovery does not load +// the wider X runtime and onboarding graph just to read the entry contract. +export { xPlugin } from "./src/plugin.js"; diff --git a/extensions/x/index.test.ts b/extensions/x/index.test.ts new file mode 100644 index 0000000000000..14ed152609805 --- /dev/null +++ b/extensions/x/index.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "vitest"; +import entry from "./index.js"; + +describe("x bundled entries", () => { + it("declares the channel entry without importing the broad api barrel", () => { + expect(entry.kind).toBe("bundled-channel-entry"); + expect(entry.id).toBe("x"); + expect(entry.name).toBe("X (Twitter)"); + expect(typeof entry.loadChannelPlugin).toBe("function"); + }); +}); diff --git a/extensions/x/index.ts b/extensions/x/index.ts index 11eedb63b7812..d68acbe620730 100644 --- a/extensions/x/index.ts +++ b/extensions/x/index.ts @@ -1,17 +1,19 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; -import { xPlugin } from "./src/plugin.js"; -import { setXRuntime } from "./src/runtime.js"; +import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema"; +import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract"; +import { XConfigSchema } from "./src/config-schema.js"; -const plugin = { +export default defineBundledChannelEntry({ id: "x", name: "X (Twitter)", - description: "X (Twitter) channel plugin - monitor mentions and reply to tweets", - configSchema: emptyPluginConfigSchema(), - register(api: OpenClawPluginApi) { - setXRuntime(api.runtime); - api.registerChannel({ plugin: xPlugin }); + description: "X (Twitter) channel plugin", + importMetaUrl: import.meta.url, + configSchema: buildChannelConfigSchema(XConfigSchema), + plugin: { + specifier: "./channel-plugin-api.js", + exportName: "xPlugin", }, -}; - -export default plugin; + runtime: { + specifier: "./runtime-api.js", + exportName: "setXRuntime", + }, +}); diff --git a/extensions/x/runtime-api.ts b/extensions/x/runtime-api.ts new file mode 100644 index 0000000000000..aeaad004c2dc1 --- /dev/null +++ b/extensions/x/runtime-api.ts @@ -0,0 +1,3 @@ +// Keep runtime activation narrow so the bundled channel entry only imports the +// X runtime setter when the host actually initializes the plugin. +export { setXRuntime } from "./src/runtime.js"; diff --git a/package.json b/package.json index 2ad28cc9c5da5..e054643b16146 100644 --- a/package.json +++ b/package.json @@ -1261,7 +1261,7 @@ "qa:lab:up:fast": "node --import tsx scripts/qa-lab-up.ts --use-prebuilt-image --bind-ui-dist --skip-ui-build", "qa:lab:watch": "vite build --watch --config extensions/qa-lab/web/vite.config.ts", "qverisbot": "node scripts/run-node.mjs", - "release:check": "pnpm check:base-config-schema && pnpm check:bundled-channel-config-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:api:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && pnpm ui:build && node --import tsx scripts/release-check.ts", + "release:check": "pnpm check:base-config-schema && pnpm check:bundled-channel-config-metadata && pnpm config:docs:check && pnpm plugin-sdk:check-exports && pnpm plugin-sdk:api:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && pnpm ui:build && node --import tsx scripts/release-check.ts", "release:npm": "bash scripts/release-npm.sh", "release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts", "release:openclaw:npm:verify-published": "node --import tsx scripts/openclaw-npm-postpublish-verify.ts", diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 35db18baa91c0..4a01fb0255f2c 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -36,6 +36,7 @@ export { type PackFile = { path: string }; type PackResult = { files?: PackFile[]; filename?: string; unpackedSize?: number }; +type RootPackageName = { name?: string }; const requiredPathGroups = [ ["dist/index.js", "dist/index.mjs"], @@ -72,6 +73,25 @@ export function listRequiredQaScenarioPackPaths(): string[] { .toSorted((left, right) => left.localeCompare(right)); } +export function createReleaseCheckNpmEnv(params?: { + env?: NodeJS.ProcessEnv; + scratchDir?: string; +}): NodeJS.ProcessEnv { + const env = { ...(params?.env ?? process.env) }; + const scratchDir = resolve( + params?.scratchDir ?? join(tmpdir(), "openclaw-release-check-npm-runtime"), + ); + const cacheDir = env.npm_config_cache?.trim() || join(scratchDir, "cache"); + const logsDir = env.npm_config_logs_dir?.trim() || join(scratchDir, "logs"); + + mkdirSync(cacheDir, { recursive: true }); + mkdirSync(logsDir, { recursive: true }); + + env.npm_config_cache = cacheDir; + env.npm_config_logs_dir = logsDir; + return env; +} + function collectBundledExtensions(): BundledExtension[] { const extensionsDir = resolve("extensions"); const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) => @@ -93,6 +113,22 @@ function collectBundledExtensions(): BundledExtension[] { }); } +export function packageNameToNodeModulesSegments(packageName: string): string[] { + return packageName + .split("/") + .map((segment) => segment.trim()) + .filter((segment) => segment.length > 0); +} + +function readRootPackageName(): string { + const rootPackage = JSON.parse(readFileSync(resolve("package.json"), "utf8")) as RootPackageName; + const packageName = rootPackage.name?.trim(); + if (!packageName) { + throw new Error("release-check: root package.json is missing a valid name."); + } + return packageName; +} + function checkBundledExtensionMetadata() { const extensions = collectBundledExtensions(); const manifestErrors = collectBundledExtensionManifestErrors(extensions); @@ -128,6 +164,7 @@ function checkBundledExtensionMetadata() { function runPackDry(): PackResult[] { const raw = execSync("npm pack --dry-run --json --ignore-scripts", { encoding: "utf8", + env: createReleaseCheckNpmEnv(), stdio: ["ignore", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 100, }); @@ -140,6 +177,7 @@ function runPack(packDestination: string): PackResult[] { ["pack", "--json", "--ignore-scripts", "--pack-destination", packDestination], { encoding: "utf8", + env: createReleaseCheckNpmEnv(), stdio: ["ignore", "pipe", "pipe"], maxBuffer: 1024 * 1024 * 100, }, @@ -175,6 +213,7 @@ function installPackedTarball(prefixDir: string, tarballPath: string, cwd: strin { cwd, encoding: "utf8", + env: createReleaseCheckNpmEnv(), stdio: "inherit", }, ); @@ -184,6 +223,7 @@ function resolveGlobalRoot(prefixDir: string, cwd: string): string { return execFileSync("npm", ["root", "-g", "--prefix", prefixDir], { cwd, encoding: "utf8", + env: createReleaseCheckNpmEnv(), stdio: ["ignore", "pipe", "pipe"], }).trim(); } @@ -199,7 +239,10 @@ function runPackedBundledChannelEntrySmoke(): void { const prefixDir = join(tmpRoot, "prefix"); installPackedTarball(prefixDir, tarballPath, tmpRoot); - const packageRoot = join(resolveGlobalRoot(prefixDir, tmpRoot), "openclaw"); + const packageRoot = join( + resolveGlobalRoot(prefixDir, tmpRoot), + ...packageNameToNodeModulesSegments(readRootPackageName()), + ); execFileSync( process.execPath, [ diff --git a/scripts/runtime-postbuild.mjs b/scripts/runtime-postbuild.mjs index 238f1f8360e3e..604d4f7378ca9 100644 --- a/scripts/runtime-postbuild.mjs +++ b/scripts/runtime-postbuild.mjs @@ -10,6 +10,12 @@ import { writeOfficialChannelCatalog } from "./write-official-channel-catalog.mj const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const ROOT_RUNTIME_ALIAS_PATTERN = /^(?.+\.(?:runtime|contract))-[A-Za-z0-9_-]+\.js$/u; +const PACKAGE_SELF_IMPORT_PATTERNS = [ + /(from\s*["'])openclaw(?=(?:\/|["']))/gu, + /(import\s*["'])openclaw(?=(?:\/|["']))/gu, + /(import\s*\(\s*["'])openclaw(?=(?:\/|["']))/gu, + /(require\s*\(\s*["'])openclaw(?=(?:\/|["']))/gu, +]; /** * Copy static (non-transpiled) runtime assets that are referenced by their @@ -57,6 +63,58 @@ export function copyStaticExtensionAssets(params = {}) { } } +function listFilesRecursive(dirPath, fsImpl) { + let entries = []; + try { + entries = fsImpl.readdirSync(dirPath, { withFileTypes: true }); + } catch { + return []; + } + + return entries.flatMap((entry) => { + const absolutePath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + return listFilesRecursive(absolutePath, fsImpl); + } + return [absolutePath]; + }); +} + +export function rewritePackageSelfImportsInSource(sourceText, packageName) { + if (!packageName || packageName === "openclaw") { + return sourceText; + } + return PACKAGE_SELF_IMPORT_PATTERNS.reduce( + (current, pattern) => current.replace(pattern, `$1${packageName}`), + sourceText, + ); +} + +export function rewriteBundledExtensionPackageSelfImports(params = {}) { + const rootDir = params.rootDir ?? ROOT; + const fsImpl = params.fs ?? fs; + const packageName = + params.packageName ?? + JSON.parse(fsImpl.readFileSync(path.join(rootDir, "package.json"), "utf8")).name; + const extensionsDir = path.join(rootDir, "dist", "extensions"); + let rewrittenFiles = 0; + + for (const filePath of listFilesRecursive(extensionsDir, fsImpl)) { + if (!filePath.endsWith(".js")) { + continue; + } + const current = fsImpl.readFileSync(filePath, "utf8"); + const next = rewritePackageSelfImportsInSource(current, packageName); + if (next === current) { + continue; + } + fsImpl.writeFileSync(filePath, next, "utf8"); + rewrittenFiles += 1; + } + + return rewrittenFiles; +} + export function writeStableRootRuntimeAliases(params = {}) { const rootDir = params.rootDir ?? ROOT; const distDir = path.join(rootDir, "dist"); @@ -89,6 +147,7 @@ export function runRuntimePostBuild(params = {}) { stageBundledPluginRuntime(params); writeStableRootRuntimeAliases(params); copyStaticExtensionAssets(params); + rewriteBundledExtensionPackageSelfImports(params); } if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { diff --git a/src/plugin-sdk/root-alias.cjs b/src/plugin-sdk/root-alias.cjs index 2c91cbdb61c48..b20a4a5e45a0a 100644 --- a/src/plugin-sdk/root-alias.cjs +++ b/src/plugin-sdk/root-alias.cjs @@ -7,7 +7,6 @@ let monolithicSdk = null; let diagnosticEventsModule = null; const jitiLoaders = new Map(); const pluginSdkSubpathsCache = new Map(); -const pluginSdkPackageNames = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"]; const pluginSdkSourceExtensions = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"]; const isDistRootAlias = __filename.includes( `${path.sep}dist${path.sep}plugin-sdk${path.sep}root-alias.cjs`, @@ -88,6 +87,22 @@ function getPackageRoot() { return path.resolve(__dirname, "..", ".."); } +function listPluginSdkPackageNames() { + const packageNames = new Set(["openclaw/plugin-sdk", "@openclaw/plugin-sdk"]); + try { + const packageJson = JSON.parse( + fs.readFileSync(path.join(getPackageRoot(), "package.json"), "utf8"), + ); + const rootPackageName = typeof packageJson?.name === "string" ? packageJson.name.trim() : ""; + if (rootPackageName) { + packageNames.add(`${rootPackageName}/plugin-sdk`); + } + } catch { + // Keep the canonical aliases even if package.json is unavailable. + } + return [...packageNames]; +} + function findDistChunkByPrefix(prefix) { const distRoot = path.join(getPackageRoot(), "dist"); try { @@ -133,14 +148,14 @@ function buildPluginSdkAliasMap(useDist) { const normalizeTarget = (target) => process.platform === "win32" ? target.replace(/\\/g, "/") : target; const aliasMap = Object.fromEntries( - pluginSdkPackageNames.map((packageName) => [packageName, normalizeTarget(__filename)]), + listPluginSdkPackageNames().map((packageName) => [packageName, normalizeTarget(__filename)]), ); for (const subpath of listPluginSdkExportedSubpaths()) { if (useDist) { const candidate = path.join(pluginSdkDir, `${subpath}.js`); if (fs.existsSync(candidate)) { - for (const packageName of pluginSdkPackageNames) { + for (const packageName of listPluginSdkPackageNames()) { aliasMap[`${packageName}/${subpath}`] = normalizeTarget(candidate); } } @@ -151,7 +166,7 @@ function buildPluginSdkAliasMap(useDist) { if (!fs.existsSync(candidate)) { continue; } - for (const packageName of pluginSdkPackageNames) { + for (const packageName of listPluginSdkPackageNames()) { aliasMap[`${packageName}/${subpath}`] = normalizeTarget(candidate); } break; diff --git a/src/plugins/contracts/plugin-sdk-root-alias.test.ts b/src/plugins/contracts/plugin-sdk-root-alias.test.ts index 599c373edf7d6..29237262834f5 100644 --- a/src/plugins/contracts/plugin-sdk-root-alias.test.ts +++ b/src/plugins/contracts/plugin-sdk-root-alias.test.ts @@ -26,6 +26,7 @@ function loadRootAliasWithStubs(options?: { env?: Record; monolithicExports?: Record; aliasPath?: string; + packageName?: string; packageExports?: Record; platform?: string; existingPaths?: string[]; @@ -63,6 +64,7 @@ function loadRootAliasWithStubs(options?: { return { readFileSync: () => JSON.stringify({ + name: options?.packageName, exports: { "./plugin-sdk/group-access": { default: "./dist/plugin-sdk/group-access.js" }, ...options?.packageExports, @@ -280,6 +282,24 @@ describe("plugin-sdk root alias", () => { }); }); + it("adds the current package name as a plugin-sdk alias when it differs from openclaw", () => { + const lazyModule = loadRootAliasWithStubs({ + distExists: true, + packageName: "@qverisai/qverisbot", + monolithicExports: { + slowHelper: (): string => "loaded", + }, + }); + + expect((lazyModule.moduleExports.slowHelper as () => string)()).toBe("loaded"); + expect(lazyModule.createJitiOptions.at(-1)?.alias).toMatchObject({ + "@qverisai/qverisbot/plugin-sdk": rootAliasPath, + "@qverisai/qverisbot/plugin-sdk/group-access": expect.stringContaining( + path.join("src", "plugin-sdk", "group-access.ts"), + ), + }); + }); + it("keeps bootstrap plugin-sdk aliases deterministic and ignores unsafe subpaths", () => { const lazyModule = loadRootAliasWithStubs({ distExists: true, diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index cba0071797cf7..f48503e6c7b81 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -58,6 +58,7 @@ function withCwd(cwd: string, run: () => T): T { } function createPluginSdkAliasFixture(params?: { + packageName?: string; srcFile?: string; distFile?: string; srcBody?: string; @@ -75,7 +76,7 @@ function createPluginSdkAliasFixture(params?: { params?.trustedRootIndicatorMode ?? (params?.trustedRootIndicators === false ? "none" : "bin+marker"); const packageJson: Record = { - name: "openclaw", + name: params?.packageName ?? "openclaw", type: "module", }; if (trustedRootIndicatorMode === "bin+marker") { @@ -869,6 +870,45 @@ describe("plugin sdk alias helpers", () => { ).toBe(false); }); + it("adds the current package name to plugin-sdk loader aliases", () => { + const { fixture, sourceRootAlias, sourceChannelRuntimePath } = + createPluginSdkAliasTargetFixture(); + fs.writeFileSync( + path.join(fixture.root, "package.json"), + JSON.stringify( + { + name: "@qverisai/qverisbot", + type: "module", + bin: { + openclaw: "openclaw.mjs", + }, + exports: { + "./plugin-sdk": { default: "./dist/plugin-sdk/index.js" }, + "./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" }, + }, + }, + null, + 2, + ), + "utf-8", + ); + const sourcePluginEntry = writePluginEntry( + fixture.root, + bundledPluginFile("discord", "index.ts"), + ); + + const aliases = withEnv({ NODE_ENV: undefined }, () => + buildPluginLoaderAliasMap(sourcePluginEntry), + ); + + expect(fs.realpathSync(aliases["@qverisai/qverisbot/plugin-sdk"] ?? "")).toBe( + fs.realpathSync(sourceRootAlias), + ); + expect(fs.realpathSync(aliases["@qverisai/qverisbot/plugin-sdk/channel-runtime"] ?? "")).toBe( + fs.realpathSync(sourceChannelRuntimePath), + ); + }); + it("normalizes Windows alias targets before handing them to Jiti", () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 20eefa05661ec..4b7e5feea7459 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -18,6 +18,7 @@ export type LoaderModuleResolveParams = { type PluginSdkPackageJson = { exports?: Record; bin?: string | Record; + name?: string; }; const STARTUP_ARGV1 = process.argv[1]; @@ -251,7 +252,7 @@ export function resolvePluginSdkAliasFile(params: { const cachedPluginSdkExportedSubpaths = new Map(); const cachedPluginSdkScopedAliasMaps = new Map>(); -const PLUGIN_SDK_PACKAGE_NAMES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const; +const cachedPluginSdkPackageNames = new Map(); const PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS = [ ".ts", ".mts", @@ -261,6 +262,24 @@ const PLUGIN_SDK_SOURCE_CANDIDATE_EXTENSIONS = [ ".cjs", ] as const; +function listPluginSdkPackageNames(packageRoot: string): string[] { + const cached = cachedPluginSdkPackageNames.get(packageRoot); + if (cached) { + return cached; + } + + const packageJson = readPluginSdkPackageJson(packageRoot); + const packageNames = new Set(["openclaw/plugin-sdk", "@openclaw/plugin-sdk"]); + const rootPackageName = typeof packageJson?.name === "string" ? packageJson.name.trim() : ""; + if (rootPackageName) { + packageNames.add(`${rootPackageName}/plugin-sdk`); + } + + const resolved = [...packageNames]; + cachedPluginSdkPackageNames.set(packageRoot, resolved); + return resolved; +} + export function listPluginSdkExportedSubpaths( params: { modulePath?: string; @@ -325,7 +344,7 @@ export function resolvePluginSdkScopedAliasMap( if (kind === "dist") { const candidate = path.join(packageRoot, "dist", "plugin-sdk", `${subpath}.js`); if (fs.existsSync(candidate)) { - for (const packageName of PLUGIN_SDK_PACKAGE_NAMES) { + for (const packageName of listPluginSdkPackageNames(packageRoot)) { aliasMap[`${packageName}/${subpath}`] = candidate; } break; @@ -337,7 +356,7 @@ export function resolvePluginSdkScopedAliasMap( if (!fs.existsSync(candidate)) { continue; } - for (const packageName of PLUGIN_SDK_PACKAGE_NAMES) { + for (const packageName of listPluginSdkPackageNames(packageRoot)) { aliasMap[`${packageName}/${subpath}`] = candidate; } break; @@ -391,6 +410,9 @@ export function buildPluginLoaderAliasMap( moduleUrl?: string, pluginSdkResolution: PluginSdkResolutionPreference = "auto", ): Record { + const packageRoot = + resolveLoaderPluginSdkPackageRoot({ modulePath, argv1, moduleUrl, pluginSdkResolution }) ?? + path.dirname(modulePath); const pluginSdkAlias = resolvePluginSdkAliasFile({ srcFile: "root-alias.cjs", distFile: "root-alias.cjs", @@ -406,7 +428,7 @@ export function buildPluginLoaderAliasMap( : {}), ...(pluginSdkAlias ? Object.fromEntries( - PLUGIN_SDK_PACKAGE_NAMES.map((packageName) => [ + listPluginSdkPackageNames(packageRoot).map((packageName) => [ packageName, normalizeJitiAliasTargetPath(pluginSdkAlias), ]), diff --git a/test/release-check.test.ts b/test/release-check.test.ts index 99ff7bf273517..cc6d031b97c2f 100644 --- a/test/release-check.test.ts +++ b/test/release-check.test.ts @@ -13,8 +13,10 @@ import { collectForbiddenPackPaths, collectMissingPackPaths, collectPackUnpackedSizeErrors, + createReleaseCheckNpmEnv, listRequiredQaScenarioPackPaths, packageNameFromSpecifier, + packageNameToNodeModulesSegments, } from "../scripts/release-check.ts"; import { bundledDistPluginFile, bundledPluginFile } from "./helpers/bundled-plugin-paths.js"; @@ -275,6 +277,19 @@ describe("bundled plugin root runtime mirrors", () => { }); }); +describe("packageNameToNodeModulesSegments", () => { + it("keeps unscoped package names as one path segment", () => { + expect(packageNameToNodeModulesSegments("openclaw")).toEqual(["openclaw"]); + }); + + it("splits scoped package names into node_modules path segments", () => { + expect(packageNameToNodeModulesSegments("@qverisai/qverisbot")).toEqual([ + "@qverisai", + "qverisbot", + ]); + }); +}); + describe("collectForbiddenPackPaths", () => { it("blocks all packaged node_modules payloads", () => { expect( @@ -418,3 +433,44 @@ describe("collectPackUnpackedSizeErrors", () => { ]); }); }); + +describe("createReleaseCheckNpmEnv", () => { + it("routes npm cache and logs to a writable scratch directory by default", () => { + const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-release-check-env-")); + + try { + const env = createReleaseCheckNpmEnv({ + env: { PATH: process.env.PATH }, + scratchDir: tempRoot, + }); + + expect(env.PATH).toBe(process.env.PATH); + expect(env.npm_config_cache).toBe(join(tempRoot, "cache")); + expect(env.npm_config_logs_dir).toBe(join(tempRoot, "logs")); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it("preserves explicit npm cache and log directories", () => { + const tempRoot = mkdtempSync(join(tmpdir(), "openclaw-release-check-explicit-")); + const explicitCache = join(tempRoot, "custom-cache"); + const explicitLogs = join(tempRoot, "custom-logs"); + + try { + const env = createReleaseCheckNpmEnv({ + env: { + PATH: process.env.PATH, + npm_config_cache: explicitCache, + npm_config_logs_dir: explicitLogs, + }, + scratchDir: join(tempRoot, "scratch"), + }); + + expect(env.npm_config_cache).toBe(explicitCache); + expect(env.npm_config_logs_dir).toBe(explicitLogs); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/test/scripts/runtime-postbuild.test.ts b/test/scripts/runtime-postbuild.test.ts index 7c10c779927bf..0774207218c74 100644 --- a/test/scripts/runtime-postbuild.test.ts +++ b/test/scripts/runtime-postbuild.test.ts @@ -4,6 +4,8 @@ import { describe, expect, it, vi } from "vitest"; import { copyStaticExtensionAssets, listStaticExtensionAssetOutputs, + rewriteBundledExtensionPackageSelfImports, + rewritePackageSelfImportsInSource, writeStableRootRuntimeAliases, } from "../../scripts/runtime-postbuild.mjs"; import { createScriptTestHarness } from "./test-helpers.js"; @@ -80,3 +82,46 @@ describe("runtime postbuild static assets", () => { await expect(fs.stat(path.join(distDir, "library.js"))).rejects.toThrow(); }); }); + +describe("runtime postbuild package self imports", () => { + it("rewrites bundled extension self imports to the published package name", () => { + const source = [ + 'import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";', + 'import "openclaw/plugin-sdk/runtime";', + 'const loadSetup = () => import("openclaw/plugin-sdk/setup-runtime");', + 'const compat = require("openclaw/plugin-sdk/compat");', + 'const docs = "https://github.com/openclaw/openclaw";', + ].join("\n"); + + expect(rewritePackageSelfImportsInSource(source, "@qverisai/qverisbot")).toBe( + [ + 'import { defineBundledChannelEntry } from "@qverisai/qverisbot/plugin-sdk/channel-entry-contract";', + 'import "@qverisai/qverisbot/plugin-sdk/runtime";', + 'const loadSetup = () => import("@qverisai/qverisbot/plugin-sdk/setup-runtime");', + 'const compat = require("@qverisai/qverisbot/plugin-sdk/compat");', + 'const docs = "https://github.com/openclaw/openclaw";', + ].join("\n"), + ); + }); + + it("rewrites dist extension entry files in place", async () => { + const rootDir = createTempDir("openclaw-runtime-postbuild-"); + const entryPath = path.join(rootDir, "dist", "extensions", "discord", "index.js"); + await fs.mkdir(path.dirname(entryPath), { recursive: true }); + await fs.writeFile( + path.join(rootDir, "package.json"), + JSON.stringify({ name: "@qverisai/qverisbot" }), + "utf8", + ); + await fs.writeFile( + entryPath, + 'import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";\n', + "utf8", + ); + + expect(rewriteBundledExtensionPackageSelfImports({ rootDir })).toBe(1); + expect(await fs.readFile(entryPath, "utf8")).toBe( + 'import { defineBundledChannelEntry } from "@qverisai/qverisbot/plugin-sdk/channel-entry-contract";\n', + ); + }); +});