mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:20:43 +00:00
refactor(plugins): remove extension jiti test hooks
This commit is contained in:
@@ -21,7 +21,7 @@ describe("thread binding manager state", () => {
|
||||
});
|
||||
|
||||
it("shares managers between ESM and alternate-loaded module instances", async () => {
|
||||
const viaJiti = await loadThreadBindingsViaAlternateLoader();
|
||||
const viaAlternateLoader = await loadThreadBindingsViaAlternateLoader();
|
||||
|
||||
createThreadBindingManager({
|
||||
cfg: EMPTY_DISCORD_TEST_CONFIG,
|
||||
@@ -31,6 +31,6 @@ describe("thread binding manager state", () => {
|
||||
});
|
||||
|
||||
expect(getThreadBindingManager("work")).not.toBeNull();
|
||||
expect(viaJiti.getThreadBindingManager("work")).not.toBeNull();
|
||||
expect(viaAlternateLoader.getThreadBindingManager("work")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,8 +32,9 @@ type ThreadBindingsGlobalState = {
|
||||
lastPersistedAtMs: number;
|
||||
};
|
||||
|
||||
// Plugin hooks can load this module via Jiti while core imports it via ESM.
|
||||
// Store mutable state on globalThis so both loader paths share one registry.
|
||||
// Plugin hooks can load this module through a separate runtime path while core
|
||||
// imports it via ESM. Store mutable state on globalThis so both paths share one
|
||||
// registry.
|
||||
const THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.discordThreadBindingsState");
|
||||
let threadBindingsState: ThreadBindingsGlobalState | undefined;
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ collapsed here.
|
||||
### Fixes
|
||||
|
||||
- Plugins/bundled runtimes: ship bundled plugin runtime sidecars like WhatsApp `light-runtime-api.js`, Matrix `runtime-api.js`, and other plugin runtime entry files in the npm package again, so global installs stop failing on missing bundled plugin runtime surfaces.
|
||||
- Plugins/Matrix: avoid duplicate `resolveMatrixAccountStringValues` runtime-api exports under Jiti so bundled Matrix installs no longer crash at startup with `Cannot redefine property: resolveMatrixAccountStringValues`. Fixes #52909 and #52891. Thanks @vincentkoc.
|
||||
- Plugins/Matrix: avoid duplicate `resolveMatrixAccountStringValues` runtime-api exports under source loaders so bundled Matrix installs no longer crash at startup with `Cannot redefine property: resolveMatrixAccountStringValues`. Fixes #52909 and #52891. Thanks @vincentkoc.
|
||||
|
||||
## 2026.3.22
|
||||
|
||||
|
||||
@@ -123,16 +123,11 @@ it("loads the packaged runtime wrapper without recursing through the stable root
|
||||
);
|
||||
}, 240_000);
|
||||
|
||||
it("does not use Jiti when only a TypeScript Matrix runtime shim exists", async () => {
|
||||
it("does not load when only a TypeScript Matrix runtime shim exists", async () => {
|
||||
const fixtureRoot = makeFixtureRoot(".tmp-matrix-runtime-ts-only-");
|
||||
|
||||
writeOpenClawPackageFixture(fixtureRoot);
|
||||
writeSourceRuntimeWrapperFixture(fixtureRoot, { runtimeExtension: ".ts" });
|
||||
writeFixtureFile(
|
||||
fixtureRoot,
|
||||
"node_modules/jiti/index.js",
|
||||
"throw new Error('matrix wrapper must not require jiti');\n",
|
||||
);
|
||||
|
||||
await expect(
|
||||
importFixtureModule(fixtureRoot, "extensions/matrix/src/plugin-entry.runtime.js"),
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import path from "node:path";
|
||||
import { loadRuntimeApiExportTypesViaJiti } from "openclaw/plugin-sdk/plugin-test-contracts";
|
||||
import { runDirectImportSmoke } from "openclaw/plugin-sdk/plugin-test-contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("zalo runtime api", () => {
|
||||
it("loads the narrow runtime api without reentering setup surfaces", () => {
|
||||
const runtimeApiPath = path.join(process.cwd(), "extensions", "zalo", "runtime-api.ts");
|
||||
it("loads the narrow runtime api without reentering setup surfaces", async () => {
|
||||
const stdout = await runDirectImportSmoke(
|
||||
`const runtime = await import("./extensions/zalo/runtime-api.ts");
|
||||
process.stdout.write(JSON.stringify({
|
||||
hasZaloPlugin: Object.hasOwn(runtime, "zaloPlugin"),
|
||||
hasZaloSetupWizard: Object.hasOwn(runtime, "zaloSetupWizard"),
|
||||
type: typeof runtime.setZaloRuntime,
|
||||
}));`,
|
||||
);
|
||||
|
||||
expect(
|
||||
loadRuntimeApiExportTypesViaJiti({
|
||||
modulePath: runtimeApiPath,
|
||||
exportNames: ["setZaloRuntime"],
|
||||
realPluginSdkSpecifiers: ["openclaw/plugin-sdk/runtime-store"],
|
||||
}),
|
||||
).toEqual({
|
||||
setZaloRuntime: "function",
|
||||
});
|
||||
});
|
||||
expect(stdout).toBe('{"hasZaloPlugin":false,"hasZaloSetupWizard":false,"type":"function"}');
|
||||
}, 45_000);
|
||||
});
|
||||
|
||||
@@ -100,7 +100,6 @@ const RETIRED_EXTENSION_TEST_HELPER_BRIDGE_FILES = [
|
||||
"test/helpers/plugins/contracts-testkit.ts",
|
||||
"test/helpers/plugins/direct-smoke.ts",
|
||||
"test/helpers/plugins/directory.ts",
|
||||
"test/helpers/plugins/jiti-runtime-api.ts",
|
||||
"test/helpers/plugins/onboard-config.ts",
|
||||
"test/helpers/plugins/outbound-delivery.ts",
|
||||
"test/helpers/plugins/package-manifest-contract.ts",
|
||||
|
||||
@@ -8,7 +8,6 @@ export {
|
||||
uniqueSortedStrings,
|
||||
} from "./test-helpers/contracts-testkit.js";
|
||||
export { runDirectImportSmoke } from "./test-helpers/direct-smoke.js";
|
||||
export { loadRuntimeApiExportTypesViaJiti } from "./test-helpers/jiti-runtime-api.js";
|
||||
export { describePackageManifestContract } from "./test-helpers/package-manifest-contract.js";
|
||||
export { pluginRegistrationContractCases } from "./test-helpers/plugin-registration-contract-cases.js";
|
||||
export { describePluginRegistrationContract } from "./test-helpers/plugin-registration-contract.js";
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
|
||||
function loadTypeScript(): typeof import("typescript") {
|
||||
return nodeRequire("typescript") as typeof import("typescript");
|
||||
}
|
||||
|
||||
const JITI_EXTENSIONS = [
|
||||
".ts",
|
||||
".tsx",
|
||||
".mts",
|
||||
".cts",
|
||||
".mtsx",
|
||||
".ctsx",
|
||||
".js",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".json",
|
||||
] as const;
|
||||
|
||||
const PLUGIN_SDK_SPECIFIER_PREFIX = "openclaw/plugin-sdk/";
|
||||
const SOURCE_MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"] as const;
|
||||
|
||||
type SourceModuleRef = {
|
||||
specifier: string;
|
||||
typeOnly: boolean;
|
||||
};
|
||||
|
||||
function listPluginSdkExportedSubpaths(root: string): string[] {
|
||||
const packageJsonPath = path.join(root, "package.json");
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
||||
exports?: Record<string, unknown>;
|
||||
};
|
||||
return Object.keys(packageJson.exports ?? {})
|
||||
.filter((key) => key.startsWith("./plugin-sdk/"))
|
||||
.map((key) => key.slice("./plugin-sdk/".length));
|
||||
}
|
||||
|
||||
function resolvePluginSdkAliasTarget(root: string, subpath: string): string | null {
|
||||
const distCandidate = path.join(root, "dist", "plugin-sdk", `${subpath}.js`);
|
||||
if (existsSync(distCandidate)) {
|
||||
return distCandidate;
|
||||
}
|
||||
|
||||
for (const ext of SOURCE_MODULE_EXTENSIONS) {
|
||||
const srcCandidate = path.join(root, "src", "plugin-sdk", `${subpath}${ext}`);
|
||||
if (existsSync(srcCandidate)) {
|
||||
return srcCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveLocalModulePath(filePath: string, specifier: string): string | null {
|
||||
const basePath = path.resolve(path.dirname(filePath), specifier);
|
||||
const candidates = new Set<string>([basePath]);
|
||||
|
||||
for (const ext of SOURCE_MODULE_EXTENSIONS) {
|
||||
candidates.add(`${basePath}${ext}`);
|
||||
}
|
||||
|
||||
if (/\.[cm]?[jt]sx?$/u.test(basePath)) {
|
||||
const withoutExt = basePath.replace(/\.[cm]?[jt]sx?$/u, "");
|
||||
for (const ext of SOURCE_MODULE_EXTENSIONS) {
|
||||
candidates.add(`${withoutExt}${ext}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const ext of SOURCE_MODULE_EXTENSIONS) {
|
||||
candidates.add(path.join(basePath, `index${ext}`));
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectSourceModuleRefs(filePath: string): SourceModuleRef[] {
|
||||
const ts = loadTypeScript();
|
||||
const sourceText = readFileSync(filePath, "utf8");
|
||||
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
|
||||
const refs: SourceModuleRef[] = [];
|
||||
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (ts.isImportDeclaration(statement)) {
|
||||
const specifier =
|
||||
statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)
|
||||
? statement.moduleSpecifier.text
|
||||
: undefined;
|
||||
if (specifier) {
|
||||
refs.push({
|
||||
specifier,
|
||||
typeOnly: Boolean(statement.importClause?.isTypeOnly),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ts.isExportDeclaration(statement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const specifier =
|
||||
statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)
|
||||
? statement.moduleSpecifier.text
|
||||
: undefined;
|
||||
if (!specifier) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const typeOnly = Boolean(
|
||||
statement.isTypeOnly ||
|
||||
(statement.exportClause &&
|
||||
ts.isNamedExports(statement.exportClause) &&
|
||||
statement.exportClause.elements.length > 0 &&
|
||||
statement.exportClause.elements.every((element) => element.isTypeOnly)),
|
||||
);
|
||||
|
||||
refs.push({ specifier, typeOnly });
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
function collectPluginSdkAliases(params: {
|
||||
modulePath: string;
|
||||
root: string;
|
||||
realPluginSdkSpecifiers?: readonly string[];
|
||||
}): Record<string, string> {
|
||||
const realSpecifiers = new Set<string>();
|
||||
const stubSpecifiers = new Set<string>();
|
||||
const visitedFiles = new Set<string>();
|
||||
const stubPath = path.join(params.root, "test", "helpers", "plugins", "plugin-sdk-stub.cjs");
|
||||
const explicitRealSpecifiers = new Set(params.realPluginSdkSpecifiers ?? []);
|
||||
|
||||
function visitModule(filePath: string, rootModule: boolean): void {
|
||||
if (visitedFiles.has(filePath)) {
|
||||
return;
|
||||
}
|
||||
visitedFiles.add(filePath);
|
||||
|
||||
for (const ref of collectSourceModuleRefs(filePath)) {
|
||||
if (ref.specifier.startsWith(PLUGIN_SDK_SPECIFIER_PREFIX)) {
|
||||
const shouldKeepReal =
|
||||
rootModule &&
|
||||
!ref.typeOnly &&
|
||||
(explicitRealSpecifiers.size === 0 || explicitRealSpecifiers.has(ref.specifier));
|
||||
if (shouldKeepReal) {
|
||||
realSpecifiers.add(ref.specifier);
|
||||
const subpath = ref.specifier.slice(PLUGIN_SDK_SPECIFIER_PREFIX.length);
|
||||
const target = resolvePluginSdkAliasTarget(params.root, subpath);
|
||||
if (target?.endsWith(".ts")) {
|
||||
visitModule(target, false);
|
||||
}
|
||||
} else {
|
||||
stubSpecifiers.add(ref.specifier);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ref.specifier.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolved = resolveLocalModulePath(filePath, ref.specifier);
|
||||
if (resolved) {
|
||||
visitModule(resolved, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitModule(params.modulePath, true);
|
||||
|
||||
const aliasEntries = new Map<string, string>();
|
||||
for (const specifier of listPluginSdkExportedSubpaths(params.root).map(
|
||||
(subpath) => `${PLUGIN_SDK_SPECIFIER_PREFIX}${subpath}`,
|
||||
)) {
|
||||
if (realSpecifiers.has(specifier)) {
|
||||
const subpath = specifier.slice(PLUGIN_SDK_SPECIFIER_PREFIX.length);
|
||||
aliasEntries.set(specifier, resolvePluginSdkAliasTarget(params.root, subpath) ?? stubPath);
|
||||
continue;
|
||||
}
|
||||
if (stubSpecifiers.has(specifier)) {
|
||||
aliasEntries.set(specifier, stubPath);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(aliasEntries);
|
||||
}
|
||||
|
||||
export function loadRuntimeApiExportTypesViaJiti(params: {
|
||||
modulePath: string;
|
||||
exportNames: readonly string[];
|
||||
additionalAliases?: Record<string, string>;
|
||||
realPluginSdkSpecifiers?: readonly string[];
|
||||
}): Record<string, string> {
|
||||
const root = process.cwd();
|
||||
const alias = {
|
||||
...collectPluginSdkAliases({
|
||||
modulePath: params.modulePath,
|
||||
root,
|
||||
realPluginSdkSpecifiers: params.realPluginSdkSpecifiers,
|
||||
}),
|
||||
...params.additionalAliases,
|
||||
};
|
||||
|
||||
const script = `
|
||||
import path from "node:path";
|
||||
import { createJiti } from "jiti";
|
||||
|
||||
const modulePath = ${JSON.stringify(params.modulePath)};
|
||||
const exportNames = ${JSON.stringify(params.exportNames)};
|
||||
const alias = ${JSON.stringify(alias)};
|
||||
const jiti = createJiti(path.join(${JSON.stringify(root)}, "openclaw.mjs"), {
|
||||
interopDefault: true,
|
||||
tryNative: false,
|
||||
fsCache: false,
|
||||
moduleCache: false,
|
||||
extensions: ${JSON.stringify(JITI_EXTENSIONS)},
|
||||
alias,
|
||||
});
|
||||
const mod = jiti(modulePath);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
Object.fromEntries(exportNames.map((name) => [name, typeof mod[name]])),
|
||||
),
|
||||
);
|
||||
`;
|
||||
|
||||
const raw = execFileSync(process.execPath, ["--input-type=module", "--eval", script], {
|
||||
cwd: root,
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
return JSON.parse(raw) as Record<string, string>;
|
||||
}
|
||||
Reference in New Issue
Block a user