Files
Sebastian Poxhofer 4220c0fe36 feat(manager): share the .npmrc host-rule wrapper between bun, deno and npm (#46131)
* refactor(manager): share the .npmrc host-rule wrapper between bun, deno and npm

Adds `withNpmrcHostRules()` to the npm manager utils: it injects the host-rule lines into a directory's `.npmrc`, runs the callback, and restores the original `.npmrc` from a `finally` block. bun, deno, the corepack `updateArtifacts()` and the three lock file loops in `post-update/index.ts` now share it instead of repeating getNpmrcContent / updateNpmrcContent / resetNpmrcContent.

Adds `readUpdatedBinaryLockFile()` to the shared manager utils for the `Buffer.compare` plus file-addition block that bun and deno both carried.

Behaviour changes:

- bun and deno now restore `.npmrc` when `exec()` throws. The reset previously ran only after a successful install, so a failed lock file update left the injected registry credentials in the working tree. Covered by "restores .npmrc when the install fails" in `bun/artifacts.spec.ts` and `deno/artifacts.spec.ts`.
- The npm, yarn and pnpm loops in `post-update/index.ts` now restore `.npmrc` when the loop body throws an `ExternalHostError`, for the same reason.
- deno reads `config.isLockFileMaintenance` instead of deriving the flag from `config.updateType === 'lockFileMaintenance'`. Grouped branches overwrite `updateType` while `isLockFileMaintenance` is set explicitly, so the flag is the reliable source. The deno specs were updated to set it.

* refactor(manager): let updateLockFile() compare binary lock files

A Buffer as the existing content makes the skeleton read, compare and
return the new lock file as bytes, so bun and deno use it instead of a
separate readUpdatedBinaryLockFile() helper.

* refactor(manager/devbox): update the lock file through updateLockFile()

devbox kept the same Buffer compare as bun and deno; the trace log before
returning the lock file goes away with it.

* fix(manager/devbox): type the command list explicitly

The list is now only read inside the run() closure, where TypeScript
cannot resolve an evolving array type, so tsc reported an implicit any.
2026-09-18 09:30:27 +00:00

115 lines
3.4 KiB
TypeScript

import detectIndent from 'detect-indent';
import upath from 'upath';
import { logger } from '../../../logger/index.ts';
import {
deleteLocalFile,
readLocalFile,
writeLocalFile,
} from '../../../util/fs/index.ts';
import { PackageJson } from './schema.ts';
import type { LockFile, ParseLockFileResult } from './types.ts';
export function parseLockFile(lockFile: string): ParseLockFileResult {
const detectedIndent = detectIndent(lockFile).indent || ' ';
let lockFileParsed: LockFile | undefined;
try {
lockFileParsed = JSON.parse(lockFile);
} catch (err) {
logger.warn({ err }, 'Error parsing npm lock file');
}
return { detectedIndent, lockFileParsed };
}
export function composeLockFile(lockFile: LockFile, indent: string): string {
return `${JSON.stringify(lockFile, null, indent)}\n`;
}
export async function getNpmrcContent(dir: string): Promise<string | null> {
const npmrcFilePath = upath.join(dir, '.npmrc');
let originalNpmrcContent: string | null = null;
try {
originalNpmrcContent = await readLocalFile(npmrcFilePath, 'utf8');
} catch {
/* v8 ignore next -- needs test */
originalNpmrcContent = null;
}
if (originalNpmrcContent) {
logger.debug(`npmrc file ${npmrcFilePath} found in repository`);
}
return originalNpmrcContent;
}
export async function updateNpmrcContent(
dir: string,
originalContent: string | null,
additionalLines: string[],
): Promise<void> {
const npmrcFilePath = upath.join(dir, '.npmrc');
const newNpmrc = originalContent
? [originalContent, ...additionalLines]
: additionalLines;
try {
const newContent = newNpmrc.length ? newNpmrc.join('\n') : null;
if (newContent !== originalContent) {
logger.debug(`Writing updated .npmrc file to ${npmrcFilePath}`);
await writeLocalFile(npmrcFilePath, `${newContent}\n`);
}
} catch /* v8 ignore next -- TODO: add test #40625 */ {
logger.warn('Unable to write custom npmrc file');
}
}
export async function resetNpmrcContent(
dir: string,
originalContent: string | null,
): Promise<void> {
const npmrcFilePath = upath.join(dir, '.npmrc');
if (originalContent) {
try {
await writeLocalFile(npmrcFilePath, originalContent);
} catch /* v8 ignore next -- TODO: add test #40625 */ {
logger.warn('Unable to reset npmrc to original contents');
}
} else {
try {
await deleteLocalFile(npmrcFilePath);
} catch /* v8 ignore next -- TODO: add test #40625 */ {
logger.warn('Unable to delete custom npmrc');
}
}
}
/**
* Add the host-rule lines to the `.npmrc` in `dir`, run `fn`, and restore the original `.npmrc` afterwards.
*
* The restore runs in a `finally`, so a package manager that throws cannot leave the injected credentials behind in the working tree.
*/
export async function withNpmrcHostRules<T>(
dir: string,
additionalLines: string[],
fn: (originalNpmrcContent: string | null) => Promise<T>,
): Promise<T> {
const originalContent = await getNpmrcContent(dir);
await updateNpmrcContent(dir, originalContent, additionalLines);
try {
return await fn(originalContent);
} finally {
await resetNpmrcContent(dir, originalContent);
}
}
export async function loadPackageJson(parentDir: string): Promise<PackageJson> {
const json = await readLocalFile(
upath.join(parentDir, 'package.json'),
'utf8',
);
const res = PackageJson.safeParse(json);
if (res.success) {
return res.data;
}
return {};
}