mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
refactor: prune legacy plugin dependency debris on postinstall
This commit is contained in:
@@ -164,12 +164,6 @@ function assertSafeInstalledDistPath(relativePath, params) {
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
function isStagedRuntimeDependencyPath(relativePath) {
|
||||
return /^dist\/extensions\/[^/]+\/(?:node_modules|\.openclaw-install-stage(?:-[^/]+)?)(?:\/|$)/u.test(
|
||||
normalizeRelativePath(relativePath),
|
||||
);
|
||||
}
|
||||
|
||||
function listInstalledDistFiles(params = {}) {
|
||||
const readDir = params.readdirSync ?? readdirSync;
|
||||
const distRoot = resolveInstalledDistRoot(params);
|
||||
@@ -184,10 +178,6 @@ function listInstalledDistFiles(params = {}) {
|
||||
if (!currentDir) {
|
||||
continue;
|
||||
}
|
||||
const relativeCurrentDir = normalizeRelativePath(relative(packageRoot, currentDir));
|
||||
if (isStagedRuntimeDependencyPath(relativeCurrentDir)) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of readDir(currentDir, { withFileTypes: true })) {
|
||||
const entryPath = join(currentDir, entry.name);
|
||||
if (entry.isSymbolicLink()) {
|
||||
@@ -223,10 +213,6 @@ function pruneEmptyDistDirectories(params = {}) {
|
||||
const pathLstat = params.lstatSync ?? lstatSync;
|
||||
|
||||
function prune(currentDir) {
|
||||
const relativeCurrentDir = normalizeRelativePath(relative(packageRoot, currentDir));
|
||||
if (isStagedRuntimeDependencyPath(relativeCurrentDir)) {
|
||||
return;
|
||||
}
|
||||
for (const entry of readDir(currentDir, { withFileTypes: true })) {
|
||||
if (entry.isSymbolicLink()) {
|
||||
throw new Error(
|
||||
@@ -261,6 +247,57 @@ function pruneEmptyDistDirectories(params = {}) {
|
||||
prune(distRoot.distDir);
|
||||
}
|
||||
|
||||
function isLegacyInstalledPluginDependencyDirName(name) {
|
||||
return name === "node_modules" || /^\.openclaw-install-stage(?:-[^/]+)?$/iu.test(name);
|
||||
}
|
||||
|
||||
function pruneLegacyInstalledPluginDependencyDirs(params) {
|
||||
const readDir = params.readdirSync ?? readdirSync;
|
||||
const removePath = params.rmSync ?? rmSync;
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
const extensionsDir = join(packageRoot, "dist", "extensions");
|
||||
const removed = [];
|
||||
let pluginEntries;
|
||||
try {
|
||||
pluginEntries = readDir(extensionsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return removed;
|
||||
}
|
||||
|
||||
for (const pluginEntry of pluginEntries) {
|
||||
if (!pluginEntry.isDirectory() || pluginEntry.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
const pluginDir = join(extensionsDir, pluginEntry.name);
|
||||
let pluginChildren;
|
||||
try {
|
||||
pluginChildren = readDir(pluginDir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const childEntry of pluginChildren) {
|
||||
if (!isLegacyInstalledPluginDependencyDirName(childEntry.name)) {
|
||||
continue;
|
||||
}
|
||||
const safePluginDir = assertSafeInstalledDistPath(
|
||||
normalizeRelativePath(relative(packageRoot, pluginDir)),
|
||||
{
|
||||
packageRoot,
|
||||
distDirReal: params.distDirReal,
|
||||
realpathSync: params.realpathSync,
|
||||
},
|
||||
);
|
||||
const relativePath = normalizeRelativePath(
|
||||
relative(packageRoot, join(pluginDir, childEntry.name)),
|
||||
);
|
||||
removePath(join(safePluginDir, childEntry.name), { recursive: true, force: true });
|
||||
removed.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
const JS_DIST_FILE_RE = /^dist\/.*\.(?:cjs|js|mjs)$/u;
|
||||
|
||||
function stripSpecifierSuffix(value) {
|
||||
@@ -400,6 +437,13 @@ export function pruneInstalledPackageDist(params = {}) {
|
||||
if (distRoot === null) {
|
||||
return [];
|
||||
}
|
||||
const removedLegacyDependencyDirs = pruneLegacyInstalledPluginDependencyDirs({
|
||||
packageRoot,
|
||||
distDirReal: distRoot.distDirReal,
|
||||
realpathSync: params.realpathSync,
|
||||
readdirSync: params.readdirSync,
|
||||
rmSync: params.rmSync,
|
||||
});
|
||||
let expectedFiles = params.expectedFiles ?? null;
|
||||
if (expectedFiles === null) {
|
||||
try {
|
||||
@@ -444,6 +488,11 @@ export function pruneInstalledPackageDist(params = {}) {
|
||||
if (removed.length > 0) {
|
||||
log.log(`[postinstall] pruned stale dist files: ${removed.join(", ")}`);
|
||||
}
|
||||
if (removedLegacyDependencyDirs.length > 0) {
|
||||
log.log(
|
||||
`[postinstall] pruned legacy plugin dependency dirs: ${removedLegacyDependencyDirs.join(", ")}`,
|
||||
);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
|
||||
@@ -739,25 +739,6 @@ describe("bundled channel entry shape guards", () => {
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps setup-only plugin barrels off legacy staged runtime-dependency metadata", () => {
|
||||
const offenders: string[] = [];
|
||||
|
||||
for (const extensionDir of bundledPluginRoots) {
|
||||
const packageJsonPath = path.join(extensionDir, "package.json");
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
continue;
|
||||
}
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
||||
openclaw?: { bundle?: Record<string, unknown> };
|
||||
};
|
||||
if (packageJson.openclaw?.bundle?.stageRuntimeDependencies === true) {
|
||||
offenders.push(path.relative(process.cwd(), packageJsonPath));
|
||||
}
|
||||
}
|
||||
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps bundled channel entrypoints free of static src imports", () => {
|
||||
const offenders = collectBundledChannelEntrypointOffenders(bundledPluginRoots, (source) =>
|
||||
/^(?:import|export)\s.+["']\.\/src\//mu.test(source),
|
||||
|
||||
@@ -61,27 +61,6 @@ describe("bundled plugin postinstall", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not install bundled plugin package deps outside of source checkouts by default", async () => {
|
||||
const extensionsDir = await createExtensionsDir();
|
||||
const packageRoot = path.dirname(path.dirname(extensionsDir));
|
||||
await writePluginPackage(extensionsDir, "acpx", {
|
||||
dependencies: {
|
||||
acpx: "0.4.1",
|
||||
},
|
||||
});
|
||||
const spawnSync = vi.fn();
|
||||
|
||||
runBundledPluginPostinstall({
|
||||
env: { HOME: "/tmp/home" },
|
||||
extensionsDir,
|
||||
packageRoot,
|
||||
spawnSync,
|
||||
log: { log: vi.fn(), warn: vi.fn() },
|
||||
});
|
||||
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prunes Node versioned compile cache dirs during package postinstall", () => {
|
||||
const configuredBase = path.join("/tmp", "openclaw-cache");
|
||||
const defaultBase = path.join(tmpdir(), "node-compile-cache");
|
||||
@@ -173,19 +152,15 @@ describe("bundled plugin postinstall", () => {
|
||||
path.join(extensionsDir, "acpx", "node_modules", "acpx", "package.json"),
|
||||
JSON.stringify({ name: "acpx", version: "0.4.1" }),
|
||||
);
|
||||
const spawnSync = vi.fn();
|
||||
|
||||
runBundledPluginPostinstall({
|
||||
env: { HOME: "/tmp/home" },
|
||||
packageRoot,
|
||||
spawnSync,
|
||||
log: { log: vi.fn(), warn: vi.fn() },
|
||||
});
|
||||
|
||||
await expect(fs.stat(path.join(extensionsDir, "acpx", "node_modules"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps source-checkout prune non-fatal", async () => {
|
||||
@@ -419,7 +394,6 @@ describe("bundled plugin postinstall", () => {
|
||||
|
||||
runBundledPluginPostinstall({
|
||||
packageRoot,
|
||||
spawnSync: vi.fn(),
|
||||
log: { log: vi.fn(), warn: vi.fn() },
|
||||
});
|
||||
|
||||
@@ -529,11 +503,20 @@ describe("bundled plugin postinstall", () => {
|
||||
).toThrow("unsafe dist entry: dist/escape");
|
||||
});
|
||||
|
||||
it("ignores staged bundled plugin node_modules when pruning packaged dist", async () => {
|
||||
it("prunes stale bundled plugin dependency debris from packaged dist", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-packaged-install-dist-prune-");
|
||||
const staleFile = path.join(packageRoot, "dist", "stale-runtime.js");
|
||||
const packageJson = path.join(packageRoot, "dist", "extensions", "slack", "package.json");
|
||||
const binDir = path.join(packageRoot, "dist", "extensions", "slack", "node_modules", ".bin");
|
||||
const dependencyFile = path.join(
|
||||
packageRoot,
|
||||
"dist",
|
||||
"extensions",
|
||||
"slack",
|
||||
"node_modules",
|
||||
"typebox",
|
||||
"package.json",
|
||||
);
|
||||
const installStageFile = path.join(
|
||||
packageRoot,
|
||||
"dist",
|
||||
@@ -561,10 +544,12 @@ describe("bundled plugin postinstall", () => {
|
||||
await fs.mkdir(path.dirname(staleFile), { recursive: true });
|
||||
await fs.mkdir(path.dirname(packageJson), { recursive: true });
|
||||
await fs.mkdir(binDir, { recursive: true });
|
||||
await fs.mkdir(path.dirname(dependencyFile), { recursive: true });
|
||||
await fs.mkdir(path.dirname(installStageFile), { recursive: true });
|
||||
await fs.mkdir(path.dirname(retryInstallStageFile), { recursive: true });
|
||||
await fs.writeFile(staleFile, "export {};\n");
|
||||
await fs.writeFile(packageJson, "{}\n");
|
||||
await fs.writeFile(dependencyFile, "{}\n");
|
||||
await fs.writeFile(installStageFile, "export {};\n");
|
||||
await fs.writeFile(retryInstallStageFile, "export {};\n");
|
||||
await fs.symlink("../fxparser/bin.js", path.join(binDir, "fxparser"));
|
||||
@@ -576,8 +561,15 @@ describe("bundled plugin postinstall", () => {
|
||||
log: { log: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).toEqual(["dist/stale-runtime.js"]);
|
||||
await expect(fs.stat(installStageFile)).resolves.toBeDefined();
|
||||
await expect(fs.stat(retryInstallStageFile)).resolves.toBeDefined();
|
||||
await expect(
|
||||
fs.stat(path.join(packageRoot, "dist", "extensions", "slack", "node_modules")),
|
||||
).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(fs.stat(path.dirname(installStageFile))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(fs.stat(path.dirname(retryInstallStageFile))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
});
|
||||
|
||||
it("unlinks stale files instead of recursive pruning them", () => {
|
||||
@@ -614,32 +606,6 @@ describe("bundled plugin postinstall", () => {
|
||||
expect(unlinkSync).toHaveBeenCalledWith("/pkg/dist/stale.js");
|
||||
});
|
||||
|
||||
it("skips reinstall when the bundled sentinel package already exists", async () => {
|
||||
const extensionsDir = await createExtensionsDir();
|
||||
const packageRoot = path.dirname(path.dirname(extensionsDir));
|
||||
await writePluginPackage(extensionsDir, "acpx", {
|
||||
dependencies: {
|
||||
acpx: "0.4.1",
|
||||
},
|
||||
});
|
||||
await fs.mkdir(path.join(packageRoot, "node_modules", "acpx"), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(packageRoot, "node_modules", "acpx", "package.json"),
|
||||
"{}\n",
|
||||
"utf8",
|
||||
);
|
||||
const spawnSync = vi.fn();
|
||||
|
||||
runBundledPluginPostinstall({
|
||||
env: { npm_config_global: "true" },
|
||||
extensionsDir,
|
||||
packageRoot,
|
||||
spawnSync,
|
||||
});
|
||||
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prunes only bundled plugin package node_modules in source checkouts", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-source-prune-");
|
||||
const extensionsDir = path.join(packageRoot, "extensions");
|
||||
|
||||
Reference in New Issue
Block a user