Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/modules/manager/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import * as copier from './copier';
import * as cpanfile from './cpanfile';
import * as crossplane from './crossplane';
import * as crow from './crow';
import * as deno from './deno';
import * as depsEdn from './deps-edn';
import * as devbox from './devbox';
import * as devContainer from './devcontainer';
Expand Down Expand Up @@ -140,6 +141,7 @@ api.set('copier', copier);
api.set('cpanfile', cpanfile);
api.set('crossplane', crossplane);
api.set('crow', crow);
api.set('deno', deno);
api.set('deps-edn', depsEdn);
api.set('devbox', devbox);
api.set('devcontainer', devContainer);
Expand Down
186 changes: 186 additions & 0 deletions lib/modules/manager/deno/artifacts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import _fs from 'fs-extra';
import { GlobalConfig } from '../../../config/global';
import type { RepoGlobalConfig } from '../../../config/types';
import { TEMPORARY_ERROR } from '../../../constants/error-messages';
import { exec as _exec } from '../../../util/exec';
import { ExecError } from '../../../util/exec/exec-error';
import type { UpdateArtifact } from '../types';
import { updateArtifacts } from './artifacts';

vi.mock('../../../util/exec');
vi.mock('fs-extra');

const exec = vi.mocked(_exec);
const fs = vi.mocked(_fs);

const globalConfig: RepoGlobalConfig = {
localDir: '',
};

describe('modules/manager/deno/artifacts', () => {
describe('updateArtifacts()', () => {
let updateArtifact: UpdateArtifact;

beforeEach(() => {
GlobalConfig.set(globalConfig);
updateArtifact = {
config: {
constraints: { deno: '2.4.5' },
},
newPackageFileContent: '',
packageFileName: '',
updatedDeps: [],
};
});

it('skips if no updatedDeps and no lockFileMaintenance', async () => {
expect(await updateArtifacts(updateArtifact)).toBeNull();
});

it('skips if no lock file in config', async () => {
updateArtifact.updatedDeps = [{}];
expect(await updateArtifacts(updateArtifact)).toBeNull();
});

it('skips if cannot read lock file', async () => {
updateArtifact.updatedDeps = [
{ manager: 'deno', lockFiles: ['deno.lock'] },
];
expect(await updateArtifacts(updateArtifact)).toBeNull();
});

it('returns null if lock content unchanged', async () => {
updateArtifact.updatedDeps = [
{ manager: 'deno', lockFiles: ['deno.lock'] },
];
const oldLock = Buffer.from('old');
fs.readFile.mockResolvedValueOnce(oldLock as never);
fs.readFile.mockResolvedValueOnce(oldLock as never);
expect(await updateArtifacts(updateArtifact)).toBeNull();
});

it('returns updated lock content', async () => {
updateArtifact.updatedDeps = [
{ manager: 'deno', lockFiles: ['deno.lock'] },
];
const oldLock = Buffer.from('old');
fs.readFile.mockResolvedValueOnce(oldLock as never);
const newLock = Buffer.from('new');
fs.readFile.mockResolvedValueOnce(newLock as never);
expect(await updateArtifacts(updateArtifact)).toEqual([
{
file: {
path: 'deno.lock',
type: 'addition',
contents: newLock,
},
},
]);
});

it('supports lockFileMaintenance', async () => {
updateArtifact.updatedDeps = [
{ manager: 'deno', lockFiles: ['deno.lock'] },
];
updateArtifact.config.updateType = 'lockFileMaintenance';
const oldLock = Buffer.from('old');
fs.readFile.mockResolvedValueOnce(oldLock as never);
const newLock = Buffer.from('new');
fs.readFile.mockResolvedValueOnce(newLock as never);
expect(await updateArtifacts(updateArtifact)).toEqual([
{
file: {
path: 'deno.lock',
type: 'addition',
contents: newLock,
},
},
]);
});

it('handles temporary error', async () => {
const execError = new ExecError(TEMPORARY_ERROR, {
cmd: '',
stdout: '',
stderr: '',
options: { encoding: 'utf8' },
});
updateArtifact.updatedDeps = [
{ manager: 'deno', lockFiles: ['deno.lock'] },
];
const oldLock = Buffer.from('old');
fs.readFile.mockResolvedValueOnce(oldLock as never);
exec.mockRejectedValueOnce(execError);
await expect(updateArtifacts(updateArtifact)).rejects.toThrow(
TEMPORARY_ERROR,
);
});

it('handles full error', async () => {
const execError = new ExecError('nope', {
cmd: '',
stdout: '',
stderr: '',
options: { encoding: 'utf8' },
});
updateArtifact.updatedDeps = [
{ manager: 'deno', lockFiles: ['deno.lock'] },
];
const oldLock = Buffer.from('old');
fs.readFile.mockResolvedValueOnce(oldLock as never);
exec.mockRejectedValueOnce(execError);
expect(await updateArtifacts(updateArtifact)).toEqual([
{ artifactError: { lockFile: 'deno.lock', stderr: 'nope' } },
]);
});
});

it('depType tasks returns null', async () => {
GlobalConfig.set(globalConfig);
const updateArtifact: UpdateArtifact = {
config: {},
newPackageFileContent: '',
packageFileName: '',
updatedDeps: [
{ manager: 'deno', lockFiles: ['deno.lock'], depType: 'tasks' },
],
};
const oldLock = Buffer.from('old');
fs.readFile.mockResolvedValueOnce(oldLock as never);
const newLock = Buffer.from('new');
fs.readFile.mockResolvedValueOnce(newLock as never);

expect(await updateArtifacts(updateArtifact)).toBeNull();
});

it('deno command execution', async () => {
GlobalConfig.set(globalConfig);
const updateArtifact: UpdateArtifact = {
config: {},
newPackageFileContent: '',
packageFileName: '',
updatedDeps: [{ manager: 'deno', lockFiles: ['deno.lock'] }],
};

const oldLock = Buffer.from('old');
fs.readFile.mockResolvedValueOnce(oldLock as never);
const newLock = Buffer.from('new');
fs.readFile.mockResolvedValueOnce(newLock as never);

await updateArtifacts(updateArtifact);

expect(exec).toHaveBeenCalledWith('deno install', {
cwdFile: '',
docker: {},
toolConstraints: [
{
toolName: 'deno',
},
],
userConfiguredEnv: undefined,
});

exec.mockClear();
GlobalConfig.reset();
});
});
105 changes: 105 additions & 0 deletions lib/modules/manager/deno/artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import is from '@sindresorhus/is';
import { TEMPORARY_ERROR } from '../../../constants/error-messages';
import { logger } from '../../../logger';
import { exec } from '../../../util/exec';
import type { ExecOptions } from '../../../util/exec/types';
import {
deleteLocalFile,
readLocalFile,
writeLocalFile,
} from '../../../util/fs';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
import type { DenoManagerData } from './types';

export async function updateArtifacts(
updateArtifact: UpdateArtifact<DenoManagerData>,
): Promise<UpdateArtifactsResult[] | null> {
const { packageFileName, updatedDeps, newPackageFileContent, config } =
updateArtifact;
logger.debug(`deno.updateArtifacts(${packageFileName})`);
const isLockFileMaintenance = config.updateType === 'lockFileMaintenance';

if (is.emptyArray(updatedDeps) && !isLockFileMaintenance) {
logger.debug('No updated deno deps - returning null');
return null;
}

// Find the first deno dependency in order to handle mixed manager updates
const lockFileName = updatedDeps.find((dep) => dep.manager === 'deno')
?.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 null;
}

for (const updateDep of updatedDeps) {
if (updateDep.depType === 'tasks') {
logger.warn(
`depType "task" can't be updated with a lock file: ${updateDep.depName}`,
);
return null;
}
}

try {
await writeLocalFile(packageFileName, newPackageFileContent);
if (isLockFileMaintenance) {
await deleteLocalFile(lockFileName);
}

const execOptions: ExecOptions = {
cwdFile: packageFileName,
docker: {},
toolConstraints: [
{
toolName: 'deno',
constraint: updateArtifact?.config?.constraints?.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
// NOTE: Specifying individual packages e.g. "deno install <datasource>:<package>@<version>"
// could be better to reduce registry queries, but does not support the http import module
// https://docs.deno.com/runtime/reference/cli/install/#deno-install-%5Bpackages%5D
await exec('deno install', execOptions);

const newLockFileContent = await readLocalFile(lockFileName);
if (
!newLockFileContent ||
Buffer.compare(oldLockFileContent, newLockFileContent) === 0
) {
return null;
}
return [
{
file: {
type: 'addition',
path: lockFileName,
contents: newLockFileContent,
},
},
];
} catch (err) {
if (err.message === TEMPORARY_ERROR) {
throw err;
}
logger.warn({ lockfile: lockFileName, err }, `Failed to update lock file`);
return [
{
artifactError: {
lockFile: lockFileName,
stderr: err.message,
},
},
];
}
}
83 changes: 83 additions & 0 deletions lib/modules/manager/deno/compat.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { fs } from '../../../../test/util';
import { extractDenoCompatiblePackageJson } from './compat';

vi.mock('../../../util/fs');

describe('modules/manager/deno/compat', () => {
describe('extractDenoCompatiblePackageJson()', () => {
it('not supported remote datasource in package.json', async () => {
fs.getSiblingFileName.mockReturnValueOnce('package.json');
fs.readLocalFile.mockResolvedValueOnce(
JSON.stringify({
name: 'test',
version: '0.0.1',
dependencies: {
r: 'github:owner/r#semver:^1.0.0',
n: 'git+https://github.com/owner/n#v2.0.0',
},
}),
);
const result = await extractDenoCompatiblePackageJson('package.json');
expect(result).toEqual({
deps: [
{
currentRawValue: 'github:owner/r#semver:^1.0.0',
currentValue: '^1.0.0',
datasource: 'github-tags',
depName: 'r',
depType: 'dependencies',
gitRef: true,
packageName: 'owner/r',
pinDigests: false,
prettyDepType: 'dependency',
skipReason: 'unsupported-remote',
sourceUrl: 'https://github.com/owner/r',
versioning: 'npm',
},
{
currentRawValue: 'git+https://github.com/owner/n#v2.0.0',
currentValue: 'v2.0.0',
datasource: 'github-tags',
depName: 'n',
depType: 'dependencies',
gitRef: true,
packageName: 'owner/n',
pinDigests: false,
prettyDepType: 'dependency',
skipReason: 'unsupported-remote',
sourceUrl: 'https://github.com/owner/n',
versioning: 'npm',
},
],
extractedConstraints: {},
managerData: {
packageName: 'test',
workspaces: undefined,
},
packageFile: 'package.json',
packageFileVersion: '0.0.1',
});
});

it('invalid package.json', async () => {
fs.getSiblingFileName.mockReturnValueOnce('package.json');
fs.readLocalFile.mockResolvedValueOnce('invalid');
const result = await extractDenoCompatiblePackageJson('package.json');
expect(result).toBeNull();
});

it('handles null response', async () => {
fs.getSiblingFileName.mockReturnValueOnce('package.json');
fs.readLocalFile.mockResolvedValueOnce(
// This package.json returns null from the extractor
JSON.stringify({
_id: 1,
_args: 1,
_from: 1,
}),
);
const result = await extractDenoCompatiblePackageJson('package.json');
expect(result).toBeNull();
});
});
});
Loading
Loading