Files
renovate/lib/modules/manager/deno/artifacts.ts
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

148 lines
5.0 KiB
TypeScript

import { isEmptyArray } from '@sindresorhus/is';
import { quote } from 'shlex';
import upath from 'upath';
import { TEMPORARY_ERROR } from '../../../constants/error-messages.ts';
import { logger } from '../../../logger/index.ts';
import { exec } from '../../../util/exec/index.ts';
import type { ExecOptions } from '../../../util/exec/types.ts';
import { readLocalFile } from '../../../util/fs/index.ts';
import * as hostRules from '../../../util/host-rules.ts';
import { processHostRules } from '../npm/post-update/rules.ts';
import { withNpmrcHostRules } from '../npm/utils.ts';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types.ts';
import {
artifactError,
artifactErrorResult,
resolveToolConstraint,
updateLockFile,
} from '../util.ts';
import type { DenoManagerData } from './types.ts';
export async function updateArtifacts(
updateArtifact: UpdateArtifact<DenoManagerData>,
): Promise<UpdateArtifactsResult[] | null> {
const { packageFileName, updatedDeps, newPackageFileContent, config } =
updateArtifact;
logger.debug(`deno.updateArtifacts(${packageFileName})`);
const { isLockFileMaintenance } = config;
if (isEmptyArray(updatedDeps) && !isLockFileMaintenance) {
logger.debug('No updated deno deps - returning null');
return null;
}
// falling back for lockFileMaintenance
const lockFileName = updatedDeps[0]?.lockFiles?.[0] ?? config.lockFiles?.[0];
if (!lockFileName) {
logger.debug('No lock file found. Skipping artifact update.');
return null;
}
const oldLockFileContent = await readLocalFile(lockFileName);
if (!oldLockFileContent) {
logger.debug(`Failed to read ${lockFileName}. Skipping artifact update.`);
return [artifactError(lockFileName, `Failed to read "${lockFileName}"`)];
}
for (const updateDep of updatedDeps) {
if (
updateDep.depType === 'tasks' ||
updateDep.depType === 'tasks.command'
) {
logger.warn(
{
depType: updateDep.depType,
depName: updateDep.depName,
lockFileName,
},
"Dependency can't be updated with a lock file",
);
return [
artifactError(
lockFileName,
`depType: "${updateDep.depType}", depName: "${updateDep.depName}" can't be updated with a lock file: "${lockFileName}"`,
),
];
}
}
const pkgFileDir = upath.dirname(packageFileName);
const { additionalNpmrcContent } = processHostRules();
try {
return await withNpmrcHostRules(
pkgFileDir,
additionalNpmrcContent,
async () => {
// run from its referred deno.json/deno.jsonc location if import map is used
const importMapReferrerDep = updatedDeps.find(
(dep) => dep.managerData?.importMapReferrer,
);
const cwdFile =
importMapReferrerDep?.managerData?.importMapReferrer ??
packageFileName;
const execOptions: ExecOptions = {
cwdFile,
docker: {},
toolConstraints: [
{
toolName: 'deno',
constraint: await resolveToolConstraint(config, 'deno'),
},
],
};
// "deno install" don't execute lifecycle scripts of package.json by default
// https://docs.deno.com/runtime/reference/cli/install/#native-node.js-addons
// deno.json(c) could have the `lock.frozen` field
// we should always override the `frozen` flag due to if it would be specified true
let command = 'deno install --frozen=false';
// defaults as per https://docs.deno.com/runtime/fundamentals/security/#importing-from-the-web
const defaultImportHosts = [
'deno.land:443',
'esm.sh:443',
'jsr.io:443',
'cdn.jsdelivr.net:443',
'raw.githubusercontent.com:443',
'gist.githubusercontent.com:443',
];
const additionalImportHosts = hostRules
.findAll({ hostType: 'npm' })
.filter((rule) => rule.resolvedHost)
.map((rule) => rule.resolvedHost);
if (additionalImportHosts.length > 0) {
// combine default and additional import hosts, removing duplicates
const importHosts = [
...new Set([...defaultImportHosts, ...additionalImportHosts]),
].join(',');
command += ` --allow-import=${quote(importHosts)}`;
}
// TODO: appending `--lockfile-only` is better to reduce disk usage
// https://docs.deno.com/runtime/reference/cli/install/#options-lockfile-only
return await updateLockFile({
lockFileName,
existingLockFileContent: oldLockFileContent,
packageFile: {
path: packageFileName,
contents: newPackageFileContent,
},
deleteLockFile: isLockFileMaintenance,
run: () => exec(command, execOptions),
});
},
);
} catch (err) {
if (err.message === TEMPORARY_ERROR) {
throw err;
}
logger.warn({ lockfile: lockFileName, err }, `Failed to update lock file`);
return artifactErrorResult(lockFileName, err);
}
}