mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
fix: prepare public artifact runtime deps
This commit is contained in:
@@ -4,10 +4,7 @@ import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
|
||||
import {
|
||||
isBuiltBundledPluginRuntimeRoot,
|
||||
prepareBundledPluginRuntimeRoot,
|
||||
} from "../plugins/bundled-runtime-root.js";
|
||||
import { prepareBuiltBundledPluginPublicSurfaceLocation } from "../plugins/bundled-public-surface-runtime-root.js";
|
||||
import {
|
||||
getCachedPluginJitiLoader,
|
||||
type PluginJitiLoaderCache,
|
||||
@@ -174,30 +171,6 @@ export type FacadeModuleLocation = {
|
||||
boundaryRoot: string;
|
||||
};
|
||||
|
||||
function resolveBuiltBundledPluginRoot(params: {
|
||||
modulePath: string;
|
||||
pluginId: string;
|
||||
}): string | null {
|
||||
const resolvedModulePath = path.resolve(params.modulePath);
|
||||
let currentDir = path.dirname(resolvedModulePath);
|
||||
while (true) {
|
||||
if (
|
||||
path.basename(currentDir) === params.pluginId &&
|
||||
isBuiltBundledPluginRuntimeRoot(currentDir)
|
||||
) {
|
||||
const relativePath = path.relative(currentDir, resolvedModulePath);
|
||||
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
||||
return currentDir;
|
||||
}
|
||||
}
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) {
|
||||
return null;
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareFacadeLocationForBundledRuntimeDeps(params: {
|
||||
location: FacadeModuleLocation;
|
||||
runtimeDeps?: {
|
||||
@@ -208,23 +181,11 @@ function prepareFacadeLocationForBundledRuntimeDeps(params: {
|
||||
if (!params.runtimeDeps) {
|
||||
return params.location;
|
||||
}
|
||||
const pluginRoot = resolveBuiltBundledPluginRoot({
|
||||
modulePath: params.location.modulePath,
|
||||
return prepareBuiltBundledPluginPublicSurfaceLocation({
|
||||
location: params.location,
|
||||
pluginId: params.runtimeDeps.pluginId,
|
||||
});
|
||||
if (!pluginRoot) {
|
||||
return params.location;
|
||||
}
|
||||
const prepared = prepareBundledPluginRuntimeRoot({
|
||||
pluginId: params.runtimeDeps.pluginId,
|
||||
pluginRoot,
|
||||
modulePath: params.location.modulePath,
|
||||
...(params.runtimeDeps.env ? { env: params.runtimeDeps.env } : {}),
|
||||
});
|
||||
return {
|
||||
modulePath: prepared.modulePath,
|
||||
boundaryRoot: prepared.pluginRoot,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFacadeModuleAtLocationSync<T extends object>(params: {
|
||||
|
||||
58
src/plugins/bundled-public-surface-runtime-root.ts
Normal file
58
src/plugins/bundled-public-surface-runtime-root.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import path from "node:path";
|
||||
import {
|
||||
isBuiltBundledPluginRuntimeRoot,
|
||||
prepareBundledPluginRuntimeRoot,
|
||||
} from "./bundled-runtime-root.js";
|
||||
|
||||
export type BundledPublicSurfaceLocation = {
|
||||
modulePath: string;
|
||||
boundaryRoot: string;
|
||||
};
|
||||
|
||||
export function resolveBuiltBundledPluginRootFromModulePath(params: {
|
||||
modulePath: string;
|
||||
pluginId: string;
|
||||
}): string | null {
|
||||
const resolvedModulePath = path.resolve(params.modulePath);
|
||||
let currentDir = path.dirname(resolvedModulePath);
|
||||
while (true) {
|
||||
if (
|
||||
path.basename(currentDir) === params.pluginId &&
|
||||
isBuiltBundledPluginRuntimeRoot(currentDir)
|
||||
) {
|
||||
const relativePath = path.relative(currentDir, resolvedModulePath);
|
||||
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
||||
return currentDir;
|
||||
}
|
||||
}
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) {
|
||||
return null;
|
||||
}
|
||||
currentDir = parentDir;
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareBuiltBundledPluginPublicSurfaceLocation(params: {
|
||||
location: BundledPublicSurfaceLocation;
|
||||
pluginId: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): BundledPublicSurfaceLocation {
|
||||
const pluginRoot = resolveBuiltBundledPluginRootFromModulePath({
|
||||
modulePath: params.location.modulePath,
|
||||
pluginId: params.pluginId,
|
||||
});
|
||||
if (!pluginRoot) {
|
||||
return params.location;
|
||||
}
|
||||
const prepared = prepareBundledPluginRuntimeRoot({
|
||||
pluginId: params.pluginId,
|
||||
pluginRoot,
|
||||
modulePath: params.location.modulePath,
|
||||
...(params.env ? { env: params.env } : {}),
|
||||
});
|
||||
return {
|
||||
modulePath: prepared.modulePath,
|
||||
boundaryRoot: prepared.pluginRoot,
|
||||
};
|
||||
}
|
||||
@@ -3,9 +3,14 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearBundledRuntimeDependencyNodePaths,
|
||||
resolveBundledRuntimeDependencyInstallRoot,
|
||||
} from "./bundled-runtime-deps.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const originalPluginStageDir = process.env.OPENCLAW_PLUGIN_STAGE_DIR;
|
||||
|
||||
function createTempDir(): string {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-public-surface-loader-"));
|
||||
@@ -13,6 +18,77 @@ function createTempDir(): string {
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
function createPackagedPublicArtifactWithStagedRuntimeDep(): {
|
||||
bundledPluginsDir: string;
|
||||
pluginRoot: string;
|
||||
stageRoot: string;
|
||||
} {
|
||||
const packageRoot = createTempDir();
|
||||
const pluginRoot = path.join(packageRoot, "dist", "extensions", "demo");
|
||||
const stageRoot = path.join(packageRoot, "stage");
|
||||
fs.mkdirSync(pluginRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "0.0.0", type: "module" }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/plugin-demo",
|
||||
version: "0.0.0",
|
||||
type: "module",
|
||||
dependencies: {
|
||||
"public-artifact-runtime-dep": "1.0.0",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginRoot, "provider-policy-api.js"),
|
||||
[
|
||||
'import { marker as depMarker } from "public-artifact-runtime-dep";',
|
||||
"export const marker = `artifact:${depMarker}`;",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, {
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_PLUGIN_STAGE_DIR: stageRoot,
|
||||
},
|
||||
});
|
||||
const depRoot = path.join(installRoot, "node_modules", "public-artifact-runtime-dep");
|
||||
fs.mkdirSync(depRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(depRoot, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "public-artifact-runtime-dep",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
exports: "./index.js",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(depRoot, "index.js"), 'export const marker = "staged";\n', "utf8");
|
||||
|
||||
return {
|
||||
bundledPluginsDir: path.join(packageRoot, "dist", "extensions"),
|
||||
pluginRoot,
|
||||
stageRoot,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const tempDir of tempDirs.splice(0)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
@@ -20,11 +96,18 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.resetModules();
|
||||
vi.doUnmock("jiti");
|
||||
vi.doUnmock("node:module");
|
||||
clearBundledRuntimeDependencyNodePaths();
|
||||
if (originalBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
|
||||
}
|
||||
if (originalPluginStageDir === undefined) {
|
||||
delete process.env.OPENCLAW_PLUGIN_STAGE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_PLUGIN_STAGE_DIR = originalPluginStageDir;
|
||||
}
|
||||
});
|
||||
|
||||
describe("bundled plugin public surface loader", () => {
|
||||
@@ -140,6 +223,25 @@ describe("bundled plugin public surface loader", () => {
|
||||
expect(createJiti).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("loads built public artifacts through staged runtime deps", async () => {
|
||||
const publicSurfaceLoader = await importFreshModule<
|
||||
typeof import("./public-surface-loader.js")
|
||||
>(import.meta.url, "./public-surface-loader.js?scope=runtime-deps");
|
||||
const fixture = createPackagedPublicArtifactWithStagedRuntimeDep();
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = fixture.bundledPluginsDir;
|
||||
process.env.OPENCLAW_PLUGIN_STAGE_DIR = fixture.stageRoot;
|
||||
|
||||
const loaded = publicSurfaceLoader.loadBundledPluginPublicArtifactModuleSync<{
|
||||
marker: string;
|
||||
}>({
|
||||
dirName: "demo",
|
||||
artifactBasename: "provider-policy-api.js",
|
||||
});
|
||||
|
||||
expect(loaded.marker).toBe("artifact:staged");
|
||||
expect(fs.existsSync(path.join(fixture.pluginRoot, "node_modules"))).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects public artifacts that change after boundary validation", async () => {
|
||||
const createJiti = vi.fn(() => vi.fn(() => ({ marker: "should-not-load" })));
|
||||
vi.doMock("jiti", () => ({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { sameFileIdentity } from "../infra/file-identity.js";
|
||||
import { resolveBundledPluginsDir } from "./bundled-dir.js";
|
||||
import { prepareBuiltBundledPluginPublicSurfaceLocation } from "./bundled-public-surface-runtime-root.js";
|
||||
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
|
||||
import { resolveBundledPluginPublicSurfacePath } from "./public-surface-runtime.js";
|
||||
import {
|
||||
@@ -150,18 +151,24 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
|
||||
`Unable to resolve bundled plugin public surface ${params.dirName}/${params.artifactBasename}`,
|
||||
);
|
||||
}
|
||||
const cached = loadedPublicSurfaceModules.get(location.modulePath);
|
||||
const preparedLocation = prepareBuiltBundledPluginPublicSurfaceLocation({
|
||||
location,
|
||||
pluginId: params.dirName,
|
||||
});
|
||||
const cached =
|
||||
loadedPublicSurfaceModules.get(location.modulePath) ??
|
||||
loadedPublicSurfaceModules.get(preparedLocation.modulePath);
|
||||
if (cached) {
|
||||
return cached as T;
|
||||
}
|
||||
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath: location.modulePath,
|
||||
rootPath: location.boundaryRoot,
|
||||
absolutePath: preparedLocation.modulePath,
|
||||
rootPath: preparedLocation.boundaryRoot,
|
||||
boundaryLabel:
|
||||
location.boundaryRoot === OPENCLAW_PACKAGE_ROOT
|
||||
preparedLocation.boundaryRoot === OPENCLAW_PACKAGE_ROOT
|
||||
? "OpenClaw package root"
|
||||
: "bundled plugin directory",
|
||||
: "plugin root",
|
||||
rejectHardlinks: true,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
@@ -183,6 +190,7 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
|
||||
|
||||
const sentinel = {} as T;
|
||||
loadedPublicSurfaceModules.set(location.modulePath, sentinel);
|
||||
loadedPublicSurfaceModules.set(preparedLocation.modulePath, sentinel);
|
||||
loadedPublicSurfaceModules.set(validatedPath, sentinel);
|
||||
try {
|
||||
const loaded = loadPublicSurfaceModule(validatedPath) as T;
|
||||
@@ -190,6 +198,7 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
|
||||
return sentinel;
|
||||
} catch (error) {
|
||||
loadedPublicSurfaceModules.delete(location.modulePath);
|
||||
loadedPublicSurfaceModules.delete(preparedLocation.modulePath);
|
||||
loadedPublicSurfaceModules.delete(validatedPath);
|
||||
throw error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user