mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
Fix local copied package installs honoring staged project .npmrc (#54543)
This commit is contained in:
@@ -21,6 +21,11 @@ async function listMatchingDirs(root: string, prefix: string): Promise<string[]>
|
||||
.map((entry) => entry.name);
|
||||
}
|
||||
|
||||
async function listMatchingEntries(root: string, prefix: string): Promise<string[]> {
|
||||
const entries = await fs.readdir(root, { withFileTypes: true });
|
||||
return entries.filter((entry) => entry.name.startsWith(prefix)).map((entry) => entry.name);
|
||||
}
|
||||
|
||||
function normalizeDarwinTmpPath(filePath: string): string {
|
||||
return process.platform === "darwin" && filePath.startsWith("/private/var/")
|
||||
? filePath.slice("/private".length)
|
||||
@@ -317,4 +322,59 @@ describe("installPackageDir", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("hides the staged project .npmrc while npm install runs and restores it afterward", async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-install-package-dir-"));
|
||||
const sourceDir = path.join(fixtureRoot, "source");
|
||||
const targetDir = path.join(fixtureRoot, "plugins", "demo");
|
||||
const npmrcContent = "git=calc.exe\n";
|
||||
await fs.mkdir(sourceDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "demo-plugin",
|
||||
version: "1.0.0",
|
||||
dependencies: {
|
||||
zod: "^4.0.0",
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(path.join(sourceDir, ".npmrc"), npmrcContent, "utf-8");
|
||||
|
||||
vi.mocked(runCommandWithTimeout).mockImplementation(async (_argv, optionsOrTimeout) => {
|
||||
const cwd = typeof optionsOrTimeout === "number" ? undefined : optionsOrTimeout.cwd;
|
||||
expect(cwd).toBeTruthy();
|
||||
await expect(fs.stat(path.join(cwd ?? "", ".npmrc"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(
|
||||
listMatchingEntries(cwd ?? "", ".openclaw-install-hidden-npmrc-"),
|
||||
).resolves.toHaveLength(1);
|
||||
return {
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
};
|
||||
});
|
||||
|
||||
const result = await installPackageDir({
|
||||
sourceDir,
|
||||
targetDir,
|
||||
mode: "install",
|
||||
timeoutMs: 1_000,
|
||||
copyErrorPrefix: "failed to copy plugin",
|
||||
hasDeps: true,
|
||||
depsLogMessage: "Installing deps…",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
await expect(fs.readFile(path.join(targetDir, ".npmrc"), "utf8")).resolves.toBe(npmrcContent);
|
||||
await expect(
|
||||
listMatchingEntries(targetDir, ".openclaw-install-hidden-npmrc-"),
|
||||
).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,14 @@ const INSTALL_BASE_CHANGED_ABORT_WARNING =
|
||||
"Install base directory changed during install; aborting staged publish.";
|
||||
const INSTALL_BASE_CHANGED_BACKUP_WARNING =
|
||||
"Install base directory changed before backup cleanup; leaving backup in place.";
|
||||
const STAGED_NPM_PROJECT_CONFIG_NAME = ".npmrc";
|
||||
const STAGED_NPM_PROJECT_CONFIG_PREFIX = ".openclaw-install-hidden-npmrc-";
|
||||
|
||||
type HiddenProjectConfigFile = {
|
||||
hiddenDir: string;
|
||||
originalPath: string;
|
||||
hiddenPath: string;
|
||||
} | null;
|
||||
|
||||
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
@@ -55,6 +63,35 @@ async function sanitizeManifestForNpmInstall(targetDir: string): Promise<void> {
|
||||
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
||||
}
|
||||
|
||||
async function hideProjectNpmConfigForInstall(targetDir: string): Promise<HiddenProjectConfigFile> {
|
||||
const originalPath = path.join(targetDir, STAGED_NPM_PROJECT_CONFIG_NAME);
|
||||
let hiddenDir = "";
|
||||
try {
|
||||
hiddenDir = await fs.mkdtemp(path.join(targetDir, STAGED_NPM_PROJECT_CONFIG_PREFIX));
|
||||
const hiddenPath = path.join(hiddenDir, STAGED_NPM_PROJECT_CONFIG_NAME);
|
||||
await fs.rename(originalPath, hiddenPath);
|
||||
return { hiddenDir, originalPath, hiddenPath };
|
||||
} catch (error) {
|
||||
if (hiddenDir) {
|
||||
await fs.rm(hiddenDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreProjectNpmConfigAfterInstall(
|
||||
hiddenConfig: HiddenProjectConfigFile,
|
||||
): Promise<void> {
|
||||
if (!hiddenConfig) {
|
||||
return;
|
||||
}
|
||||
await fs.rename(hiddenConfig.hiddenPath, hiddenConfig.originalPath);
|
||||
await fs.rm(hiddenConfig.hiddenDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function assertInstallBoundaryPaths(params: {
|
||||
installBaseDir: string;
|
||||
candidatePaths: string[];
|
||||
@@ -186,19 +223,30 @@ export async function installPackageDir(params: {
|
||||
}
|
||||
|
||||
if (params.hasDeps) {
|
||||
await sanitizeManifestForNpmInstall(stageDir);
|
||||
params.logger?.info?.(params.depsLogMessage);
|
||||
const npmRes = await runCommandWithTimeout(
|
||||
// Plugins install into isolated directories, so omitting peer deps can strip
|
||||
// runtime requirements that npm would otherwise materialize for the package.
|
||||
["npm", "install", "--omit=dev", "--silent", "--ignore-scripts"],
|
||||
{
|
||||
timeoutMs: Math.max(params.timeoutMs, 300_000),
|
||||
cwd: stageDir,
|
||||
},
|
||||
);
|
||||
if (npmRes.code !== 0) {
|
||||
return await fail(`npm install failed: ${npmRes.stderr.trim() || npmRes.stdout.trim()}`);
|
||||
try {
|
||||
await sanitizeManifestForNpmInstall(stageDir);
|
||||
const hiddenProjectNpmConfig = await hideProjectNpmConfigForInstall(stageDir);
|
||||
params.logger?.info?.(params.depsLogMessage);
|
||||
const npmRes = await (async () => {
|
||||
try {
|
||||
return await runCommandWithTimeout(
|
||||
// Plugins install into isolated directories, so omitting peer deps can strip
|
||||
// runtime requirements that npm would otherwise materialize for the package.
|
||||
["npm", "install", "--omit=dev", "--silent", "--ignore-scripts"],
|
||||
{
|
||||
timeoutMs: Math.max(params.timeoutMs, 300_000),
|
||||
cwd: stageDir,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await restoreProjectNpmConfigAfterInstall(hiddenProjectNpmConfig);
|
||||
}
|
||||
})();
|
||||
if (npmRes.code !== 0) {
|
||||
return await fail(`npm install failed: ${npmRes.stderr.trim() || npmRes.stdout.trim()}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return await fail(`npm install failed: ${String(error)}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user