diff --git a/config/knip.config.ts b/config/knip.config.ts index aec2d6b588cb..06f02ce4f507 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -211,6 +211,8 @@ const rootEntries = [ const bundledPluginEntries = [ "index.ts!", "setup-entry.ts!", + // Setup APIs may lazy-load this top-level package artifact by string specifier. + "setup-surface.ts!", // Core resolves these public plugin artifacts by basename rather than by a // static import from the plugin entry module. "*-api.ts!", diff --git a/extensions/zalo/setup-api.ts b/extensions/zalo/setup-api.ts index 525a93c2bde4..1de95a21f90d 100644 --- a/extensions/zalo/setup-api.ts +++ b/extensions/zalo/setup-api.ts @@ -1,7 +1,7 @@ // Zalo API module exposes the plugin public contract. import { loadBundledEntryExportSync } from "openclaw/plugin-sdk/channel-entry-contract"; -type SetupSurfaceModule = typeof import("./src/setup-surface.js"); +type SetupSurfaceModule = typeof import("./setup-surface.js"); function createLazyObjectValue(load: () => T): T { return new Proxy({} as T, { @@ -23,7 +23,7 @@ function createLazyObjectValue(load: () => T): T { function loadSetupSurfaceModule(): SetupSurfaceModule { return loadBundledEntryExportSync(import.meta.url, { - specifier: "./src/setup-surface.js", + specifier: "./setup-surface.js", }); } diff --git a/extensions/zalo/setup-surface.ts b/extensions/zalo/setup-surface.ts new file mode 100644 index 000000000000..438560968219 --- /dev/null +++ b/extensions/zalo/setup-surface.ts @@ -0,0 +1,2 @@ +// Zalo API module exposes the plugin public contract. +export { zaloSetupAdapter, zaloSetupWizard } from "./src/setup-surface.js"; diff --git a/test/plugin-npm-runtime-build.test.ts b/test/plugin-npm-runtime-build.test.ts index c770d4050d9d..17a50022a7f0 100644 --- a/test/plugin-npm-runtime-build.test.ts +++ b/test/plugin-npm-runtime-build.test.ts @@ -1,4 +1,5 @@ // Plugin npm runtime build tests validate plugin runtime package builds. +import { spawnSync } from "node:child_process"; import { existsSync, mkdirSync, @@ -9,12 +10,20 @@ import { } from "node:fs"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; +import { + resolvePluginNpmCommand, + withAugmentedPluginNpmManifestForPackage, +} from "../scripts/lib/plugin-npm-package-manifest.mts"; import { buildPluginNpmRuntime, listMissingPluginNpmRuntimeHostExports, listPublishablePluginPackageDirs, resolvePluginNpmRuntimeBuildPlan, } from "../scripts/lib/plugin-npm-runtime-build.mts"; +import { + createPluginModuleLoaderCache, + getCachedPluginSourceModuleLoader, +} from "../src/plugins/plugin-module-loader-cache.js"; import { useAutoCleanupTempDirTracker } from "./helpers/temp-dir.js"; const repoRoot = path.resolve(import.meta.dirname, ".."); @@ -220,6 +229,74 @@ describe("plugin npm runtime build planning", () => { expect(plan.runtimeBuildOutputs).toContain("./dist/setup-api.js"); }); + it("packs the Zalo public setup API with its lazy runtime surface", async () => { + const packageDir = path.join(repoRoot, "extensions", "zalo"); + const plan = expectPluginNpmRuntimeBuildPlan( + await buildPluginNpmRuntime({ + repoRoot, + packageDir, + logLevel: "silent", + }), + ); + const consumerDir = tempDirs.make("openclaw-zalo-packed-setup-"); + let packedFiles: string[] = []; + let setupApiPath = ""; + + withAugmentedPluginNpmManifestForPackage( + { repoRoot, packageDir, bundleDependencies: false }, + () => { + const invocation = resolvePluginNpmCommand([ + "pack", + "--json", + "--ignore-scripts", + "--pack-destination", + consumerDir, + ]); + const pack = spawnSync(invocation.command, invocation.args, { + cwd: packageDir, + encoding: "utf8", + ...(invocation.env ? { env: invocation.env } : {}), + ...(invocation.shell !== undefined ? { shell: invocation.shell } : {}), + stdio: ["ignore", "pipe", "pipe"], + ...(invocation.windowsVerbatimArguments !== undefined + ? { windowsVerbatimArguments: invocation.windowsVerbatimArguments } + : {}), + }); + expect(pack.status, pack.stderr).toBe(0); + const [packedPackage] = JSON.parse(pack.stdout) as [ + { filename: string; files: Array<{ path: string }> }, + ]; + packedFiles = packedPackage.files.map((file) => file.path); + const extract = spawnSync( + "tar", + ["-xzf", path.join(consumerDir, packedPackage.filename), "-C", consumerDir], + { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }, + ); + expect(extract.status, extract.stderr).toBe(0); + setupApiPath = path.join(consumerDir, "package", "dist", "setup-api.js"); + }, + ); + + expect(plan.runtimeBuildOutputs).toContain("./dist/setup-api.js"); + const loadSetupApi = getCachedPluginSourceModuleLoader({ + cache: createPluginModuleLoaderCache(), + modulePath: setupApiPath, + importerUrl: import.meta.url, + devSourceRoot: repoRoot, + }); + const setupApi = loadSetupApi(setupApiPath) as { + zaloSetupWizard: { channel: string }; + }; + expect(setupApi.zaloSetupWizard.channel).toBe("zalo"); + expect(plan.runtimeBuildOutputs).toContain("./dist/setup-surface.js"); + expect(plan.runtimeBuildOutputs).not.toContain("./dist/src/setup-surface.js"); + expect(packedFiles).toContain("dist/setup-surface.js"); + expect(packedFiles).not.toContain("dist/src/setup-surface.js"); + }); + it("keeps published Codex runtime imports resolvable from the host package", async () => { const result = await buildPluginNpmRuntime({ repoRoot,