mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
* refactor(plugins): retain code and cleanup through retirement * fix(plugins): release cached publications when their owners retire Release idle prepared publications at registry and metadata-cache retirement while retaining admitted borrowers and surviving Gateways. Resolve late local imports from captured packages and align lifecycle fixtures with their owning contracts. * fix: preserve plugin loader startup ownership * fix: retain explicit SDK preference for Bun plugins * fix(plugins): preserve rebased startup and build contracts Prepare runtime SDK output for both startup corpora, preserve exact released update-bridge provenance, and share hosted runtime prerequisites within the existing serial budget. Remove the redundant packing pass and isolate the timing fixture from unrelated suite growth. * style: simplify foundation CI fixtures * fix(plugins): preserve SDK readiness and startup cleanup Recognize authorized built relative SDK imports before asynchronous linking so published CJS plugins can synchronously load the same host SDK graph. Bind dashboard preparation to Gateway drain and declare the package-runtime test build prerequisite. Validation: scoped P2 review clean; 51 loader tests and 602 package/planner tests; published 2026.9.3 upgrade-survivor replay passed; configured TypeScript upgrade, fresh TypeScript startup, and CLI-managed JS/CJS upgrade each passed two RPCs, health, exactly-once service cleanup, and natural exit with unchanged shutdown grace. Runtime/UI/metadata rebuilt; full hosted CI still required. * test(pr): model GitHub commit authors in merge fixtures Current main verifies human contributor credit through per-commit GitHub metadata. Return the synthetic repository commit author and unlinked account status, and include the requested PR author type, so the real merge workflow can exercise that contract. Validation: both original merge-ref failures reproduced before the fixture repair and passed afterward; all 16 selected tooling checks passed; focused P2 review is clean. No production code changes.
166 lines
6 KiB
TypeScript
166 lines
6 KiB
TypeScript
import path from "node:path";
|
|
import type { TsdownPlugin } from "tsdown";
|
|
|
|
const SDK = "openclaw/plugin-sdk/channel-entry-contract";
|
|
const HELPER = "defineBundledChannelSetupEntry";
|
|
const REFERENCE_SLOTS = [
|
|
"plugin",
|
|
"secrets",
|
|
"runtime",
|
|
"legacyStateMigrations",
|
|
"legacySessionSurface",
|
|
];
|
|
|
|
/** Give the SDK's lazy companion declarations real edges in the inventory build graph. */
|
|
export function createPluginInventoryModuleRefsPlugin(pluginRoot: string): TsdownPlugin {
|
|
const root = path.resolve(pluginRoot);
|
|
return {
|
|
name: "openclaw:inventory-module-refs",
|
|
async transform(code, id) {
|
|
if (!code.includes(SDK)) {
|
|
return undefined;
|
|
}
|
|
const program = this.parse(code, { lang: /\.[cm]?tsx?$/.test(id) ? "ts" : "js" });
|
|
const bindings = new Set<string>();
|
|
const namespaces = new Set<string>();
|
|
for (const statement of program.body) {
|
|
if (
|
|
statement.type !== "ImportDeclaration" ||
|
|
statement.source.value !== SDK ||
|
|
statement.importKind === "type"
|
|
) {
|
|
continue;
|
|
}
|
|
for (const specifier of statement.specifiers) {
|
|
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
namespaces.add(specifier.local.name);
|
|
} else if (
|
|
specifier.type === "ImportSpecifier" &&
|
|
specifier.importKind !== "type" &&
|
|
(specifier.imported.type === "Identifier"
|
|
? specifier.imported.name
|
|
: specifier.imported.value) === HELPER
|
|
) {
|
|
bindings.add(specifier.local.name);
|
|
}
|
|
}
|
|
}
|
|
// These shipped entries export the declaration directly. Restricting this to
|
|
// module scope prevents a shadowing parameter/local function from gaining edges.
|
|
const declaration = program.body.find((node) => node.type === "ExportDefaultDeclaration");
|
|
if (
|
|
declaration?.type !== "ExportDefaultDeclaration" ||
|
|
declaration.declaration.type !== "CallExpression"
|
|
) {
|
|
return undefined;
|
|
}
|
|
const call = declaration.declaration;
|
|
const callee = call.callee;
|
|
const ownsCall =
|
|
callee.type === "Identifier"
|
|
? bindings.has(callee.name)
|
|
: callee.type === "MemberExpression" &&
|
|
!callee.computed &&
|
|
callee.object.type === "Identifier" &&
|
|
namespaces.has(callee.object.name) &&
|
|
callee.property.type === "Identifier" &&
|
|
callee.property.name === HELPER;
|
|
const options = call.arguments[0];
|
|
if (!ownsCall || options?.type !== "ObjectExpression") {
|
|
return undefined;
|
|
}
|
|
const property = (object: { properties: typeof options.properties }, name: string) =>
|
|
object.properties.flatMap((entry) =>
|
|
entry.type === "Property" &&
|
|
!entry.computed &&
|
|
entry.kind === "init" &&
|
|
(entry.key.type === "Identifier"
|
|
? entry.key.name
|
|
: entry.key.type === "Literal"
|
|
? entry.key.value
|
|
: undefined) === name
|
|
? [entry.value]
|
|
: [],
|
|
)[0];
|
|
const identifiers = new Set<string>();
|
|
const visit = (node: unknown) => {
|
|
if (Array.isArray(node)) {
|
|
node.forEach(visit);
|
|
} else if (node && typeof node === "object") {
|
|
if (
|
|
"type" in node &&
|
|
node.type === "Identifier" &&
|
|
"name" in node &&
|
|
typeof node.name === "string"
|
|
) {
|
|
identifiers.add(node.name);
|
|
}
|
|
Object.values(node).forEach(visit);
|
|
}
|
|
};
|
|
visit(program);
|
|
let fileURLToPathBinding = "__openclawFileURLToPath";
|
|
while (identifiers.has(fileURLToPathBinding)) {
|
|
fileURLToPathBinding += "_";
|
|
}
|
|
const edits: Array<{ start: number; end: number; value: string }> = [];
|
|
for (const slot of REFERENCE_SLOTS) {
|
|
const reference = property(options, slot);
|
|
if (!reference) {
|
|
continue;
|
|
}
|
|
const specifier =
|
|
reference.type === "ObjectExpression" ? property(reference, "specifier") : undefined;
|
|
if (specifier?.type !== "Literal" || typeof specifier.value !== "string") {
|
|
this.error(
|
|
`Inventory ${slot} reference in ${id} must declare a literal module specifier`,
|
|
);
|
|
}
|
|
const resolved = await this.resolve(specifier.value, id);
|
|
if (!resolved || resolved.external) {
|
|
this.error(`Cannot resolve inventory ${slot} companion ${specifier.value} from ${id}`);
|
|
}
|
|
const relative = path.relative(root, resolved.id);
|
|
if (
|
|
!relative ||
|
|
relative === ".." ||
|
|
relative.startsWith(`..${path.sep}`) ||
|
|
path.isAbsolute(relative)
|
|
) {
|
|
this.error(`Inventory companion leaves its plugin package: ${resolved.id}`);
|
|
}
|
|
const emitted = this.emitFile({
|
|
type: "chunk",
|
|
id: resolved.id,
|
|
importer: id,
|
|
preserveSignature: "strict",
|
|
});
|
|
// Stable SDKs resolve filesystem specifiers; keep the URL nested for capture.
|
|
edits.push({
|
|
start: specifier.start,
|
|
end: specifier.end,
|
|
value: `${fileURLToPathBinding}(import.meta.ROLLUP_FILE_URL_${emitted})`,
|
|
});
|
|
}
|
|
if (edits.length) {
|
|
const start =
|
|
program.body.find(
|
|
(node) =>
|
|
node.type !== "ExpressionStatement" ||
|
|
node.expression.type !== "Literal" ||
|
|
typeof node.expression.value !== "string",
|
|
)?.start ?? code.length;
|
|
edits.push({
|
|
start,
|
|
end: start,
|
|
value: `import { fileURLToPath as ${fileURLToPathBinding} } from "node:url";\n`,
|
|
});
|
|
}
|
|
let transformed = code;
|
|
for (const edit of edits.toSorted((left, right) => right.start - left.start)) {
|
|
transformed = transformed.slice(0, edit.start) + edit.value + transformed.slice(edit.end);
|
|
}
|
|
return edits.length ? { code: transformed, map: null } : undefined;
|
|
},
|
|
};
|
|
}
|