Files
renovate/lib/modules/manager/deno/update.spec.ts
Sebastian Poxhofer 58771bb183 refactor(linters): enabled oxlints func-style (#44691)
* refactor(oxlint): enable func-style

Enable the built-in func-style rule as ["warn", "declaration"] so that
named functions are written as function declarations instead of arrow or
function expressions assigned to variables, matching the convention in
docs/development/best-practices.md (Functions section). Inline arrow
callbacks are unaffected; prefer-arrow-callback continues to cover the
callback direction.

Severity is "warn" instead of "error" as a fallback: a repo-wide run
found 196 pre-existing violations (70 in spec files, 126 in production
code), exceeding the 80-violation fix threshold for this change, so no
violations were fixed here. Follow-up work can convert the offenders
(largest clusters: lib/modules/versioning/ruby, lib/util/cache/package,
lib/modules/versioning/maven, lib/modules/versioning/gradle) and then
raise the rule to "error". Note oxlint 1.72 also supports an
allowTypeAnnotation option if typed consts should remain arrows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(linter): enforce func-style at error

* refactor(func-style): convert const-arrow functions to declarations in with-cache spec and ruby versioning

* refactor(func-style): convert const-arrow functions to declarations in maven/gradle modules

* refactor(func-style): convert const-arrow functions to declarations in remaining versioning modules

* refactor(func-style): convert const-arrow functions to declarations in platform modules

Discriminated-union closures (ensureCommentRemoval byTopic/byContent
helpers in github, gitlab, bitbucket, bitbucket-server) needed the
narrowed field passed explicitly as a parameter, since function
declarations are hoisted and lose the outer if-narrowing that inline
arrow-function closures previously relied on.

* refactor(func-style): convert const-arrow functions to declarations across util/manager/datasource modules

One case (logger/renovate-logger.ts logOnceFn) is kept as an arrow
function with a documented oxlint-disable: it is stored on
this.logger.once[level] and relies on lexically-bound this, which a
hoisted function declaration would not preserve.

* refactor(func-style): convert remaining const-arrow test helpers to declarations

get-updated.spec.ts: hoisted expectPipCompilePackageAndLockFile to
module scope (it only closes over the module-level pipCompile mock)
and added a documented oxlint-disable for vitest/expect-expect on its
caller test, since oxlint's expect-expect check does not trace
assertions through an externally-defined helper function.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:14:22 +00:00

1019 lines
26 KiB
TypeScript

import { Fixtures } from '~test/fixtures.ts';
import type { Upgrade } from '../types.ts';
import type { DenoManagerData } from './types.ts';
import { updateDependency } from './update.ts';
describe('modules/manager/deno/update', () => {
describe('updateDependency', () => {
describe('deno.json/jsonc', () => {
it('updates dependency in imports', () => {
const fileContent = JSON.stringify(
{
imports: {
fs: 'https://deno.land/std@0.223.0/fs/mod.ts',
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'https://deno.land/std',
depType: 'imports',
currentValue: '0.223.0',
newValue: '0.224.0',
datasource: 'deno',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(result!);
expect(parsed.imports.fs).toBe(
'https://deno.land/std@0.224.0/fs/mod.ts',
);
});
it('throws when multiple imports require more than one replacement', () => {
const fileContent = JSON.stringify(
{
imports: {
fs: 'https://deno.land/std@0.223.0/fs/mod.ts',
path: 'https://deno.land/std@0.223.0/path/mod.ts',
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'https://deno.land/std',
depType: 'imports',
currentValue: '0.223.0',
newValue: '0.224.0',
datasource: 'deno',
};
expect(() =>
updateDependency({ fileContent, packageFile: 'deno.json', upgrade }),
).toThrow(Error);
});
it('updates dependency in scopes', () => {
const fileContent = JSON.stringify(
{
scopes: {
'https://deno.land/x/': {
dep2: 'jsr:@scope/dep1@latest',
},
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: '@scope/dep1',
depType: 'scopes',
currentValue: 'latest',
newValue: '2.0.0',
datasource: 'jsr',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).not.toBeNull();
const parsed = JSON.parse(result!);
expect(parsed.scopes['https://deno.land/x/'].dep2).toBe(
'jsr:@scope/dep1@2.0.0',
);
});
it('returns null when scopes element not found', () => {
const fileContent = JSON.stringify(
{
scopes: {
'https://deno.land/x/': {
dep2: 'jsr:@scope/dep2@latest',
},
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: '@scope/dep1',
depType: 'scopes',
currentValue: 'latest',
newValue: '2.0.0',
datasource: 'jsr',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('updates dependency in tasks', () => {
const fileContent = JSON.stringify(
{
tasks: {
build: 'deno run -A npm:dep1@4.0.0',
dev: {
command: 'deno run --allow-net npm:dep2@14.0.1',
},
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'tasks',
currentValue: '4.0.0',
newValue: '4.1.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(result!);
expect(parsed.tasks.build).toBe('deno run -A npm:dep1@4.1.0');
});
it('updates dependency in tasks.command', () => {
const fileContent = JSON.stringify(
{
tasks: {
build: 'deno run -A npm:dep1@4.0.0',
dev: {
command: 'deno run --allow-net npm:dep2@14.0.1',
},
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'dep2',
depType: 'tasks.command',
currentValue: '14.0.1',
newValue: '16.0.0',
datasource: 'npm',
};
const resul = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(resul!);
expect(parsed.tasks.dev.command).toBe(
'deno run --allow-net npm:dep2@16.0.0',
);
});
it('returns null when tasks element not found', () => {
const fileContent = JSON.stringify(
{
tasks: {
build: 'deno run -A npm:dep2@4.0.0',
dev: {
command: 'deno run --allow-net npm:dep2@14.0.1',
},
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'tasks',
currentValue: '4.0.0',
newValue: '4.1.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('returns null when tasks.command element not found', () => {
const fileContent = JSON.stringify(
{
tasks: {
build: 'deno run -A npm:dep1@4.0.0',
dev: {
command: 'deno run --allow-net npm:dep2@14.0.1',
},
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'tasks.command',
currentValue: '14.0.1',
newValue: '16.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('updates dependency in compilerOptions.types', () => {
const fileContent = JSON.stringify(
{
compilerOptions: {
types: ['npm:@types/dep2@18.0.0'],
jsxImportSource: 'https://deno.land/x/dep2@18.0.0',
jsxImportSourceTypes: 'npm:@types/dep2@18.0.0',
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: '@types/dep2',
depType: 'compilerOptions.types',
currentValue: '18.0.0',
newValue: '19.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(result!);
expect(parsed.compilerOptions.types[0]).toBe('npm:@types/dep2@19.0.0');
});
it('returns null when compilerOptions.types is empty array', () => {
const fileContent = JSON.stringify(
{
compilerOptions: {
types: [],
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: '@types/dep2',
depType: 'compilerOptions.types',
currentValue: '18.0.0',
newValue: '19.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('returns null when compilerOptions.types element not found', () => {
const fileContent = JSON.stringify(
{
compilerOptions: {
types: ['npm:@types/other@18.0.0'],
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: '@types/dep2',
depType: 'compilerOptions.types',
currentValue: '18.0.0',
newValue: '19.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('updates dependency in compilerOptions.jsxImportSource', () => {
const fileContent = JSON.stringify(
{
compilerOptions: {
types: ['npm:@types/dep2@18.0.0'],
jsxImportSource: 'https://deno.land/x/dep2@18.0.0',
jsxImportSourceTypes: 'npm:@types/dep2@18.0.0',
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'https://deno.land/x/dep2',
depType: 'compilerOptions.jsxImportSource',
currentValue: '18.0.0',
newValue: '19.0.0',
datasource: 'deno',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(result!);
expect(parsed.compilerOptions.jsxImportSource).toBe(
'https://deno.land/x/dep2@19.0.0',
);
});
it('returns null when compilerOptions.jsxImportSource does not exist', () => {
const fileContent = JSON.stringify(
{
compilerOptions: {
types: ['npm:@types/dep2@18.0.0'],
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'https://deno.land/x/dep2',
depType: 'compilerOptions.jsxImportSource',
currentValue: '18.0.0',
newValue: '19.0.0',
datasource: 'deno',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('returns null when compilerOptions.jsxImportSourceTypes does not exist', () => {
const fileContent = JSON.stringify(
{
compilerOptions: {
types: ['npm:@types/dep2@18.0.0'],
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: '@types/dep2',
depType: 'compilerOptions.jsxImportSourceTypes',
currentValue: '18.0.0',
newValue: '19.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('updates dependency in compilerOptions.jsxImportSourceTypes', () => {
const fileContent = JSON.stringify(
{
compilerOptions: {
types: ['npm:@types/dep2@18.0.0'],
jsxImportSource: 'https://deno.land/x/dep2@18.0.0',
jsxImportSourceTypes: 'npm:@types/dep2@18.0.0',
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: '@types/dep2',
depType: 'compilerOptions.jsxImportSourceTypes',
currentValue: '18.0.0',
newValue: '19.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(result!);
expect(parsed.compilerOptions.jsxImportSourceTypes).toBe(
'npm:@types/dep2@19.0.0',
);
});
it('updates dependency in lint plugins', () => {
const fileContent = JSON.stringify(
{
lint: {
plugins: ['npm:dep1@5.0.0'],
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'lint.plugins',
currentValue: '5.0.0',
newValue: '6.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(result!);
expect(parsed.lint.plugins).toContain('npm:dep1@6.0.0');
});
it('returns null when lint.plugins element not found', () => {
const fileContent = JSON.stringify(
{
lint: {
plugins: ['npm:dep2@5.0.0'],
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'lint.plugins',
currentValue: '5.0.0',
newValue: '6.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('returns null when lint.plugins is empty array', () => {
const fileContent = JSON.stringify(
{
lint: {
plugins: [],
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'lint.plugins',
currentValue: '5.0.0',
newValue: '6.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('handles dependency without version', () => {
const fileContent = JSON.stringify(
{
imports: {
dep1: 'npm:dep1',
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'imports',
currentValue: undefined,
newValue: '1.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(result!);
expect(parsed.imports.dep1).toBe('npm:dep1@1.0.0');
});
it('returns null if packageFile is not defined', () => {
const fileContent = JSON.stringify({}, null, 2);
const upgrade: Upgrade = {};
const result = updateDependency({
fileContent,
packageFile: '',
upgrade,
});
expect(result).toBeNull();
});
it('returns null for not supported datasource', () => {
const fileContent = JSON.stringify(
{
imports: {
dep1: 'unknown:dep1@1.0.0',
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'imports',
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'unknown',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('currentValue is not defined when deno datasource', () => {
const fileContent = JSON.stringify(
{
imports: {
fs: 'https://deno.land/std/fs',
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'https://deno.land/std/fs',
depType: 'imports',
newValue: '2.0.0',
datasource: 'deno',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(result!);
expect(parsed.imports.fs).toBe('https://deno.land/std/fs@2.0.0');
});
it('returns null for missing required values', () => {
const fileContent = '{}';
const upgrade: Upgrade = {
depName: undefined,
depType: undefined,
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('handles complex JSON with nested structures', () => {
const fileContent = JSON.stringify(
{
name: 'my-deno-app',
version: '1.0.0',
imports: {
dep1: 'npm:dep1@1.0.0',
dep2: 'npm:dep2@1.0.0',
},
tasks: {
build: 'deno run -A npm:dep1@2.0.0 build',
dev: {
command: 'deno run -A --watch npm:dep1@3.0.0 dev',
},
},
compilerOptions: {
jsx: 'dep1',
jsxImportSource: 'npm:dep1@1.0.0',
},
},
null,
2,
);
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'imports',
currentValue: '1.0.0',
newValue: '1.1.0',
datasource: 'npm',
};
const result1 = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(result1!);
expect(parsed.imports.dep1).toBe('npm:dep1@1.1.0');
});
it('handles the case where the desired version is already supported', () => {
const fileContent = JSON.stringify({
imports: {
dep1: 'npm:dep1@1.0.0',
},
});
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'imports',
currentValue: '1.0.0',
newValue: '1.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toEqual(fileContent);
});
it('returns null if empty file', () => {
const fileContent = '{}';
const upgrade: Upgrade = {
depName: undefined,
depType: undefined,
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('handles error during update gracefully', () => {
const fileContent = 'invalid json';
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'imports',
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('depName is not defined', () => {
const fileContent = JSON.stringify({
imports: {
dep1: 'invalid:dep1@1.0.0',
},
});
const upgrade: Upgrade = {
depName: undefined,
depType: 'imports',
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'invalid',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('unsupported packageFile', () => {
const fileContent = JSON.stringify({});
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'imports',
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'unknown.yaml',
upgrade,
});
expect(result).toBeNull();
});
it('replaces only exact matches', () => {
const fileContent = JSON.stringify({
imports: {
dep1: 'npm:dep1@1.0.0',
dep11: 'npm:dep11@1.0.0',
},
});
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'imports',
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'npm',
packageFile: 'deno.json',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
const parsed = JSON.parse(result!);
expect(parsed.imports.dep1).toContain('npm:dep1@2.0.0');
});
it('returns null when depType is not handled', () => {
const fileContent = JSON.stringify({
imports: {
dep1: 'npm:dep1@1.0.0',
},
});
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'unknown.type',
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('returns null when compilerOptions.types does not exist', () => {
const fileContent = JSON.stringify({
compilerOptions: {
jsx: 'react',
},
});
const upgrade: Upgrade = {
depName: '@types/dep2',
depType: 'compilerOptions.types',
currentValue: '18.0.0',
newValue: '19.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
it('returns null when lint.plugins does not exist', () => {
const fileContent = JSON.stringify({
lint: {
rules: {},
},
});
const upgrade: Upgrade = {
depName: 'dep1',
depType: 'lint.plugins',
currentValue: '5.0.0',
newValue: '6.0.0',
datasource: 'npm',
};
const result = updateDependency({
fileContent,
packageFile: 'deno.json',
upgrade,
});
expect(result).toBeNull();
});
});
describe('<importMap>.json', () => {
it('updates dependency in imports', () => {
const fileContent = JSON.stringify({
imports: {
dep1: 'jsr:@scope/dep1@1.0.0',
},
});
const upgrade: Upgrade<DenoManagerData> = {
depName: '@scope/dep1',
depType: 'imports',
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'jsr',
managerData: {
importMapReferrer: 'deno.json',
},
};
const result = updateDependency({
fileContent,
packageFile: 'import_map.json',
upgrade,
});
const parsed = JSON.parse(result!);
expect(parsed.imports.dep1).toContain('jsr:@scope/dep1@2.0.0');
});
it('handles error during update gracefully', () => {
const fileContent = 'invalid json';
const upgrade: Upgrade<DenoManagerData> = {
depName: 'dep1',
depType: 'imports',
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'npm',
managerData: {
importMapReferrer: 'deno.json',
},
};
const result = updateDependency({
fileContent,
packageFile: 'import_map.json',
upgrade,
});
expect(result).toBeNull();
});
it('returns null for not supported datasource', () => {
const fileContent = JSON.stringify(
{
imports: {
dep1: 'unknown:dep1@1.0.0',
},
},
null,
2,
);
const upgrade: Upgrade<DenoManagerData> = {
depName: 'dep1',
depType: 'imports',
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'unknown',
managerData: {
importMapReferrer: 'deno.json',
},
};
const result = updateDependency({
fileContent,
packageFile: 'import_map.json',
upgrade,
});
expect(result).toBeNull();
});
it('depName is not defined', () => {
const fileContent = JSON.stringify({
imports: {
dep1: 'invalid:dep1@1.0.0',
},
});
const upgrade: Upgrade<DenoManagerData> = {
depName: undefined,
depType: 'imports',
currentValue: '1.0.0',
newValue: '2.0.0',
datasource: 'invalid',
managerData: {
importMapReferrer: 'deno.json',
},
};
const result = updateDependency({
fileContent,
packageFile: 'import_map.json',
upgrade,
});
expect(result).toBeNull();
});
});
describe('package.json', () => {
// ported from lib/modules/manager/npm/update/dependency/index.spec.ts
it('replaces a dependency value', () => {
function readFixture(x: string): string {
return Fixtures.get(x, '../npm');
}
const input01Content = readFixture('inputs/01.json');
const upgrade: Upgrade = {
depType: 'dependencies',
depName: 'cheerio',
newValue: '0.22.1',
};
const outputContent = readFixture('outputs/011.json');
const testContent = updateDependency({
fileContent: input01Content,
packageFile: 'package.json',
upgrade,
});
expect(testContent).toEqual(outputContent);
});
});
});
});