From 0f1e9c88d40f4e4e885e234dc34fc27f44cd97fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:08:40 +0200 Subject: [PATCH] chore(deps): update dependency eslint-plugin-unicorn to v70 (#86) Co-authored-by: Daniel Dietzler --- apps/cloudflare-metrics/src/cloudflare-api.ts | 62 ++++++++++--------- apps/cloudflare-metrics/src/collector.test.ts | 2 +- apps/cloudflare-metrics/src/collector.ts | 2 +- apps/cloudflare-metrics/src/datasets.test.ts | 2 +- apps/cloudflare-metrics/src/flush-state.ts | 1 + .../src/graphql-builders.ts | 4 +- .../src/graphql-client.test.ts | 5 +- apps/cloudflare-metrics/src/graphql-client.ts | 18 +++--- apps/cloudflare-metrics/src/handler.test.ts | 16 +++-- .../src/handlers/scheduled.ts | 1 + .../src/integration.test.ts | 2 +- .../src/metric-providers.test.ts | 4 +- .../src/metric-providers.ts | 8 ++- apps/cloudflare-metrics/src/metrics.test.ts | 31 ++++------ apps/cloudflare-metrics/src/monitor.ts | 2 +- .../src/resource-cache.test.ts | 4 +- apps/cloudflare-metrics/src/resource-cache.ts | 42 ++++++++----- apps/cloudflare-metrics/src/test-helpers.ts | 4 +- apps/github-approval-check/src/approval.ts | 2 +- apps/github-approval-check/src/auth.ts | 2 +- apps/github-approval-check/src/dev-mode.ts | 4 +- apps/github-approval-check/src/webhook.ts | 4 +- apps/version/src/index.test.ts | 2 +- apps/version/src/index.ts | 4 +- apps/version/src/metrics.ts | 8 ++- apps/version/src/monitor.ts | 2 +- apps/version/src/version-service.ts | 2 +- apps/version/src/webhook.ts | 4 +- eslint.config.mjs | 6 +- package.json | 2 +- pnpm-lock.yaml | 15 ++--- 31 files changed, 142 insertions(+), 125 deletions(-) diff --git a/apps/cloudflare-metrics/src/cloudflare-api.ts b/apps/cloudflare-metrics/src/cloudflare-api.ts index 8d247f3..ddc2cfb 100644 --- a/apps/cloudflare-metrics/src/cloudflare-api.ts +++ b/apps/cloudflare-metrics/src/cloudflare-api.ts @@ -53,6 +53,37 @@ export class CloudflareRestClient implements ICloudflareRestClient { private readonly fetchImpl?: typeof fetch, ) {} + private async listPaginated(path: string): Promise { + const all: T[] = []; + let page = 1; + const separator = path.includes('?') ? '&' : '?'; + const doFetch = this.fetchImpl ?? globalThis.fetch; + for (;;) { + const url = `${this.baseUrl}${path}${separator}page=${page}&per_page=${DEFAULT_PAGE_SIZE}`; + const response = await doFetch(url, { + headers: { Authorization: `Bearer ${this.apiToken}` }, + }); + if (!response.ok) { + // eslint-disable-next-line unicorn/prefer-await + const body = await response.text().catch(() => ''); + throw new CloudflareRestError( + `Cloudflare REST error (${response.status}) for ${path}: ${body.slice(0, 200)}`, + response.status, + ); + } + const payload = (await response.json()) as CloudflareListResponse; + if (payload.result) { + all.push(...payload.result); + } + const info = payload.result_info; + if (!info || page >= (info.total_pages ?? 1)) { + break; + } + page++; + } + return all; + } + async listD1Databases(accountId: string): Promise { return this.listPaginated(`/accounts/${encodeURIComponent(accountId)}/d1/database`); } @@ -76,6 +107,7 @@ export class CloudflareRestClient implements ICloudflareRestClient { return null; } if (!response.ok) { + // eslint-disable-next-line unicorn/prefer-await const body = await response.text().catch(() => ''); throw new CloudflareRestError( `Cloudflare REST error (${response.status}) for /zones/${zoneId}: ${body.slice(0, 200)}`, @@ -85,34 +117,4 @@ export class CloudflareRestClient implements ICloudflareRestClient { const payload = (await response.json()) as { result: Zone | null }; return payload.result ?? null; } - - private async listPaginated(path: string): Promise { - const all: T[] = []; - let page = 1; - const separator = path.includes('?') ? '&' : '?'; - const doFetch = this.fetchImpl ?? globalThis.fetch; - for (;;) { - const url = `${this.baseUrl}${path}${separator}page=${page}&per_page=${DEFAULT_PAGE_SIZE}`; - const response = await doFetch(url, { - headers: { Authorization: `Bearer ${this.apiToken}` }, - }); - if (!response.ok) { - const body = await response.text().catch(() => ''); - throw new CloudflareRestError( - `Cloudflare REST error (${response.status}) for ${path}: ${body.slice(0, 200)}`, - response.status, - ); - } - const payload = (await response.json()) as CloudflareListResponse; - if (payload.result) { - all.push(...payload.result); - } - const info = payload.result_info; - if (!info || page >= (info.total_pages ?? 1)) { - break; - } - page++; - } - return all; - } } diff --git a/apps/cloudflare-metrics/src/collector.test.ts b/apps/cloudflare-metrics/src/collector.test.ts index f2cbbb5..a486f66 100644 --- a/apps/cloudflare-metrics/src/collector.test.ts +++ b/apps/cloudflare-metrics/src/collector.test.ts @@ -409,7 +409,7 @@ describe('CloudflareMetricsCollector', () => { expect(pages?.tags.get('zone_name')).toBe('pages.example.com'); // Only the uncached zone should have been looked up individually. - expect(restClient.getZoneCalls).toEqual(['zone-pages']); + expect(restClient.zoneCalls).toEqual(['zone-pages']); const lookup = provider.metrics.find( (m) => m.name === 'cloudflare_metrics_resource_lookup' && m.tags.get('resource') === 'zones_individual', diff --git a/apps/cloudflare-metrics/src/collector.ts b/apps/cloudflare-metrics/src/collector.ts index e1dd6fc..bfbce9c 100644 --- a/apps/cloudflare-metrics/src/collector.ts +++ b/apps/cloudflare-metrics/src/collector.ts @@ -268,12 +268,12 @@ export class CloudflareMetricsCollector { let zoneErrors = 0; for (let i = 0; i < zoneTags.length; i++) { const zoneTag = zoneTags[i]; - const zoneName = zoneNames[i]; if (batchResult.errors[zoneTag]) { zoneErrors++; console.error(`[collector] ${dataset.key} zone ${zoneTag} error:`, batchResult.errors[zoneTag]); continue; } + const zoneName = zoneNames[i]; const rows = batchResult.rows[zoneTag] ?? []; totalRows += rows.length; for (const row of rows) { diff --git a/apps/cloudflare-metrics/src/datasets.test.ts b/apps/cloudflare-metrics/src/datasets.test.ts index 7af6e44..1ca744f 100644 --- a/apps/cloudflare-metrics/src/datasets.test.ts +++ b/apps/cloudflare-metrics/src/datasets.test.ts @@ -6,7 +6,7 @@ describe('dataset registry invariants', () => { for (const dataset of ALL_DATASETS) { const fieldCount = Object.keys(dataset.fields).length; expect(fieldCount, `${dataset.key} must have fields`).toBeGreaterThan(0); - for (const [, spec] of Object.entries(dataset.fields)) { + for (const spec of Object.values(dataset.fields)) { expect(['int', 'float']).toContain(spec.type); expect(spec.source[0]).toMatch(/^(sum|avg|max|min|quantiles|uniq|_top)$/); } diff --git a/apps/cloudflare-metrics/src/flush-state.ts b/apps/cloudflare-metrics/src/flush-state.ts index fce3754..f0ffde8 100644 --- a/apps/cloudflare-metrics/src/flush-state.ts +++ b/apps/cloudflare-metrics/src/flush-state.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/no-top-level-assignment-in-function */ export const pendingFlushBuffers: string[] = []; // Cap retry buffer at 10 MB; eviction drops oldest-first. export const MAX_PENDING_FLUSH_BYTES = 10 * 1024 * 1024; diff --git a/apps/cloudflare-metrics/src/graphql-builders.ts b/apps/cloudflare-metrics/src/graphql-builders.ts index 22af2c9..4473ecc 100644 --- a/apps/cloudflare-metrics/src/graphql-builders.ts +++ b/apps/cloudflare-metrics/src/graphql-builders.ts @@ -43,10 +43,10 @@ const SCHEDULED_INVOCATIONS_SELECTION = `workers_scheduled: workersInvocationsSc export function buildBatchedAccountQuery( datasets: readonly DatasetQuery[], - includeScheduledInvocations = false, + shouldIncludeScheduledInvocations = false, ): string { const selections = datasets.map((d) => buildDatasetSelection(d, d.key)); - if (includeScheduledInvocations) { + if (shouldIncludeScheduledInvocations) { selections.push(SCHEDULED_INVOCATIONS_SELECTION); } return `query CloudflareMetricsAccountBatch($accountTag: String!, $filter: JSON!) { diff --git a/apps/cloudflare-metrics/src/graphql-client.test.ts b/apps/cloudflare-metrics/src/graphql-client.test.ts index 3d292db..c843f4d 100644 --- a/apps/cloudflare-metrics/src/graphql-client.test.ts +++ b/apps/cloudflare-metrics/src/graphql-client.test.ts @@ -136,7 +136,7 @@ describe('groupErrorsByAlias', () => { it('returns an empty map when errors is null or undefined', () => { expect(groupErrorsByAlias(null)).toEqual({}); - // eslint-disable-next-line unicorn/no-useless-undefined + expect(groupErrorsByAlias(undefined)).toEqual({}); }); }); @@ -249,6 +249,7 @@ describe('CloudflareGraphQLClient', () => { const fetchMock = vi.fn((_url: string, init: RequestInit) => { const body = JSON.parse(init.body as string); // Return the aliased fields that this chunk actually asked for. + // eslint-disable-next-line unicorn/prefer-iterator-to-array const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]); const account: Record = {}; for (const alias of aliases) { @@ -283,6 +284,7 @@ describe('CloudflareGraphQLClient', () => { })); const fetchMock = vi.fn((_url: string, init: RequestInit) => { const body = JSON.parse(init.body as string); + // eslint-disable-next-line unicorn/prefer-iterator-to-array const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]); const account: Record = {}; for (const alias of aliases) { @@ -393,6 +395,7 @@ describe('CloudflareGraphQLClient', () => { const fetchMock = vi.fn((_url: string, init: RequestInit) => { calls++; const body = JSON.parse(init.body as string); + // eslint-disable-next-line unicorn/prefer-iterator-to-array const aliases = [...body.query.matchAll(/(ds_\d+): workersInvocationsAdaptive/g)].map((m) => m[1]); const account: Record = {}; for (const alias of aliases) { diff --git a/apps/cloudflare-metrics/src/graphql-client.ts b/apps/cloudflare-metrics/src/graphql-client.ts index 221c870..533e2ed 100644 --- a/apps/cloudflare-metrics/src/graphql-client.ts +++ b/apps/cloudflare-metrics/src/graphql-client.ts @@ -96,11 +96,11 @@ export class CloudflareGraphQLClient implements ICloudflareGraphQLClient { const result: BatchedDatasetResult = { rows: {}, errors: {} }; for (const [i, chunk] of chunks.entries()) { - const includeScheduled = i === 0 && (options.includeScheduledInvocations ?? false); - const chunkResult = await this.fetchAccountBatchChunk(accountTag, chunk, range, includeScheduled); + const isIncludeScheduled = i === 0 && (options.includeScheduledInvocations ?? false); + const chunkResult = await this.fetchAccountBatchChunk(accountTag, chunk, range, isIncludeScheduled); Object.assign(result.rows, chunkResult.rows); Object.assign(result.errors, chunkResult.errors); - if (includeScheduled) { + if (isIncludeScheduled) { if (chunkResult.errors.workers_scheduled) { result.errors.workers_scheduled = chunkResult.errors.workers_scheduled; } else { @@ -116,10 +116,10 @@ export class CloudflareGraphQLClient implements ICloudflareGraphQLClient { accountTag: string, datasets: readonly DatasetQuery[], range: { start: Date; end: Date }, - includeScheduledInvocations: boolean, + shouldIncludeScheduledInvocations: boolean, attempt = 1, ): Promise { - const query = buildBatchedAccountQuery(datasets, includeScheduledInvocations); + const query = buildBatchedAccountQuery(datasets, shouldIncludeScheduledInvocations); const variables = { accountTag, filter: buildFilterObject(datasets[0] ?? null, range), @@ -138,7 +138,7 @@ export class CloudflareGraphQLClient implements ICloudflareGraphQLClient { result.rows[dataset.key] = rows ?? []; } - if (includeScheduledInvocations) { + if (shouldIncludeScheduledInvocations) { if (errorsByAlias.workers_scheduled) { result.errors.workers_scheduled = errorsByAlias.workers_scheduled; } else { @@ -153,14 +153,14 @@ export class CloudflareGraphQLClient implements ICloudflareGraphQLClient { for (const dataset of datasets) { result.errors[dataset.key] = message; } - if (includeScheduledInvocations) { + if (shouldIncludeScheduledInvocations) { result.errors.workers_scheduled = message; } } // Retry once when all fields errored — likely a transient Cloudflare // analytics backend issue that resolves on the next attempt. - const totalFields = datasets.length + (includeScheduledInvocations ? 1 : 0); + const totalFields = datasets.length + (shouldIncludeScheduledInvocations ? 1 : 0); if (attempt < 2 && totalFields > 0 && Object.keys(result.errors).length >= totalFields) { this._retryCount++; console.warn(`[graphql] all ${totalFields} fields errored, retrying chunk (attempt ${attempt + 1})`); @@ -172,7 +172,7 @@ export class CloudflareGraphQLClient implements ICloudflareGraphQLClient { accountTag, datasets, range, - includeScheduledInvocations, + shouldIncludeScheduledInvocations, attempt + 1, ); if (Object.keys(retryResult.errors).length < totalFields) { diff --git a/apps/cloudflare-metrics/src/handler.test.ts b/apps/cloudflare-metrics/src/handler.test.ts index fb70d38..701e718 100644 --- a/apps/cloudflare-metrics/src/handler.test.ts +++ b/apps/cloudflare-metrics/src/handler.test.ts @@ -1,6 +1,6 @@ import { createExecutionContext, createScheduledController, waitOnExecutionContext } from 'cloudflare:test'; import { env, exports } from 'cloudflare:workers'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { __resetFlushStateForTests } from './flush-state.js'; import worker from './index.js'; @@ -31,16 +31,15 @@ describe('scheduled handler', () => { it('emits cron_error{reason=missing_config} when API token or account ID are missing', async () => { // Collect metric flush bodies seen by the victoria-metrics POST so we // can assert on the cron_error line without touching internal state. - const originalFetch = globalThis.fetch; const flushBodies: string[] = []; - globalThis.fetch = ((url: RequestInfo | URL, init?: RequestInit) => { + vi.stubGlobal('fetch', (url: RequestInfo | URL, init?: RequestInit) => { const target = typeof url === 'string' ? url : url.toString(); if (target.includes('/write')) { flushBodies.push(init?.body as string); return Promise.resolve(new Response('', { status: 204 })); } return Promise.resolve(new Response('', { status: 200 })); - }) as typeof fetch; + }); const controller = createScheduledController(); const ctx = createExecutionContext(); @@ -51,7 +50,7 @@ describe('scheduled handler', () => { await worker.scheduled?.(controller, env as unknown as Env, ctx); await waitOnExecutionContext(ctx); } finally { - globalThis.fetch = originalFetch; + vi.unstubAllGlobals(); } const combined = flushBodies.join('\n'); @@ -63,16 +62,15 @@ describe('scheduled handler', () => { // First tick: a successful flush populates lastFlushStats. // Second tick: takeLastFlushStats() drains it and emits // cloudflare_metrics_flush_* lines to the second tick's flush body. - const originalFetch = globalThis.fetch; const flushBodies: string[] = []; - globalThis.fetch = ((url: RequestInfo | URL, init?: RequestInit) => { + vi.stubGlobal('fetch', (url: RequestInfo | URL, init?: RequestInit) => { const target = typeof url === 'string' ? url : url.toString(); if (target.includes('/write')) { flushBodies.push(init?.body as string); return Promise.resolve(new Response('', { status: 204 })); } return Promise.resolve(new Response('', { status: 200 })); - }) as typeof fetch; + }); try { const controller = createScheduledController(); @@ -86,7 +84,7 @@ describe('scheduled handler', () => { await worker.scheduled?.(controller, env as unknown as Env, ctx2); await waitOnExecutionContext(ctx2); } finally { - globalThis.fetch = originalFetch; + vi.unstubAllGlobals(); } expect(flushBodies.length).toBeGreaterThanOrEqual(2); diff --git a/apps/cloudflare-metrics/src/handlers/scheduled.ts b/apps/cloudflare-metrics/src/handlers/scheduled.ts index bd38582..64d2126 100644 --- a/apps/cloudflare-metrics/src/handlers/scheduled.ts +++ b/apps/cloudflare-metrics/src/handlers/scheduled.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/no-top-level-assignment-in-function */ import { CloudflareRestClient } from '../cloudflare-api.js'; import { CloudflareMetricsCollector } from '../collector.js'; import { ALL_DATASETS } from '../datasets.js'; diff --git a/apps/cloudflare-metrics/src/integration.test.ts b/apps/cloudflare-metrics/src/integration.test.ts index 1f2d998..19e3875 100644 --- a/apps/cloudflare-metrics/src/integration.test.ts +++ b/apps/cloudflare-metrics/src/integration.test.ts @@ -88,7 +88,7 @@ describe.skipIf(!hasCredentials)('Cloudflare GraphQL integration', () => { expect(row.dimensions).toBeDefined(); const timestampDim = dataset.timestampDimension ?? 'datetimeMinute'; expect(row.dimensions[timestampDim]).toBeDefined(); - for (const [, spec] of Object.entries(dataset.fields)) { + for (const spec of Object.values(dataset.fields)) { const [block, key] = spec.source; if (block === '_top') { continue; diff --git a/apps/cloudflare-metrics/src/metric-providers.test.ts b/apps/cloudflare-metrics/src/metric-providers.test.ts index 3162c8f..ec83979 100644 --- a/apps/cloudflare-metrics/src/metric-providers.test.ts +++ b/apps/cloudflare-metrics/src/metric-providers.test.ts @@ -68,7 +68,7 @@ describe('InfluxDB line protocol NaN/Infinity handling', () => { it('skips NaN float fields', () => { const provider = new InfluxMetricsProvider('', ''); - const metric = Metric.create('test').floatField('bad', Number.NaN).intField('good', 1); + const metric = Metric.create('test').floatField('bad', NaN).intField('good', 1); provider.pushMetric(metric); const [line] = getLines(provider); expect(line).not.toContain('bad'); @@ -95,7 +95,7 @@ describe('InfluxDB line protocol NaN/Infinity handling', () => { it('drops the entire line when all fields are NaN/Infinity', () => { const provider = new InfluxMetricsProvider('', ''); - const metric = Metric.create('test').floatField('a', Number.NaN).floatField('b', Infinity); + const metric = Metric.create('test').floatField('a', NaN).floatField('b', Infinity); provider.pushMetric(metric); expect(getLines(provider)).toHaveLength(0); }); diff --git a/apps/cloudflare-metrics/src/metric-providers.ts b/apps/cloudflare-metrics/src/metric-providers.ts index b559537..fd4b2ab 100644 --- a/apps/cloudflare-metrics/src/metric-providers.ts +++ b/apps/cloudflare-metrics/src/metric-providers.ts @@ -16,10 +16,12 @@ export class HeaderMetricsProvider implements IMetricsProviderRepository { pushMetric(metric: Metric) { for (const [label, { value, type }] of metric.fields) { - if (type === 'duration') { - const suffix = label === 'duration' ? '' : `_${label.replace('_duration', '')}`; - this._metrics.push(`${metric.name}${suffix};dur=${value}`); + if (type !== 'duration') { + continue; } + + const suffix = label === 'duration' ? '' : `_${label.replace('_duration', '')}`; + this._metrics.push(`${metric.name}${suffix};dur=${value}`); } } diff --git a/apps/cloudflare-metrics/src/metrics.test.ts b/apps/cloudflare-metrics/src/metrics.test.ts index e65f7fe..17e2bc5 100644 --- a/apps/cloudflare-metrics/src/metrics.test.ts +++ b/apps/cloudflare-metrics/src/metrics.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { __resetFlushStateForTests, takeLastFlushStats } from './flush-state.js'; import { InfluxMetricsProvider, type IMetricsProviderRepository } from './metric-providers.js'; import { Metric } from './metric.js'; @@ -74,8 +74,7 @@ describe('InfluxMetricsProvider flush self-telemetry', () => { }); it('records stats with status=ok after a successful flush', async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (() => Promise.resolve(new Response('', { status: 204 }))) as typeof fetch; + vi.stubGlobal('fetch', () => Promise.resolve(new Response('', { status: 204 }))); try { const provider = new InfluxMetricsProvider('token', 'prod'); provider.pushMetric(Metric.create('foo').intField('v', 1)); @@ -86,13 +85,12 @@ describe('InfluxMetricsProvider flush self-telemetry', () => { expect(stats?.pendingBuffers).toBe(0); expect(stats?.pendingBytes).toBe(0); } finally { - globalThis.fetch = originalFetch; + vi.unstubAllGlobals(); } }); it('records stats with status=error after a failed flush', async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (() => Promise.resolve(new Response('bad', { status: 502 }))) as typeof fetch; + vi.stubGlobal('fetch', () => Promise.resolve(new Response('bad', { status: 502 }))); try { const provider = new InfluxMetricsProvider('token', 'prod'); provider.pushMetric(Metric.create('foo').intField('v', 1)); @@ -104,13 +102,12 @@ describe('InfluxMetricsProvider flush self-telemetry', () => { expect(stats?.pendingBuffers).toBe(1); expect(stats?.pendingBytes).toBeGreaterThan(0); } finally { - globalThis.fetch = originalFetch; + vi.unstubAllGlobals(); } }); it('clears lastFlushStats after a single take', async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (() => Promise.resolve(new Response('', { status: 204 }))) as typeof fetch; + vi.stubGlobal('fetch', () => Promise.resolve(new Response('', { status: 204 }))); try { const provider = new InfluxMetricsProvider('token', 'prod'); provider.pushMetric(Metric.create('foo').intField('v', 1)); @@ -118,14 +115,13 @@ describe('InfluxMetricsProvider flush self-telemetry', () => { expect(takeLastFlushStats()).not.toBeNull(); expect(takeLastFlushStats()).toBeNull(); } finally { - globalThis.fetch = originalFetch; + vi.unstubAllGlobals(); } }); it('evicts the oldest stashed flush body when the total exceeds the 10 MB cap', async () => { - const originalFetch = globalThis.fetch; // Always fail so every flush stashes its body in the retry buffer. - globalThis.fetch = (() => Promise.resolve(new Response('', { status: 502 }))) as typeof fetch; + vi.stubGlobal('fetch', () => Promise.resolve(new Response('', { status: 502 }))); try { // Each payload is ~4 MB of raw line protocol; three of them land us // over the 10 MB cap and force an eviction. @@ -146,7 +142,7 @@ describe('InfluxMetricsProvider flush self-telemetry', () => { expect(stats?.pendingBuffers).toBe(2); expect(stats?.pendingBytes).toBeLessThan(10 * 1024 * 1024 + 4 * 1024 * 1024); } finally { - globalThis.fetch = originalFetch; + vi.unstubAllGlobals(); } }); }); @@ -185,12 +181,9 @@ describe('InfluxMetricsProvider line protocol', () => { }); it('stashes a failed flush body and resends it on the next successful flush', async () => { - // Use a fetch mock via globalThis so the provider picks it up through its - // real `fetch` call path. - const originalFetch = globalThis.fetch; let call = 0; const received: string[] = []; - globalThis.fetch = ((_url: RequestInfo | URL, init?: RequestInit) => { + vi.stubGlobal('fetch', (_url: RequestInfo | URL, init?: RequestInit) => { call++; received.push(init?.body as string); if (call === 1) { @@ -198,7 +191,7 @@ describe('InfluxMetricsProvider line protocol', () => { return Promise.resolve(new Response('bad gateway', { status: 502 })); } return Promise.resolve(new Response('', { status: 204 })); - }) as typeof fetch; + }); try { const provider = new InfluxMetricsProvider('token', 'prod'); @@ -216,7 +209,7 @@ describe('InfluxMetricsProvider line protocol', () => { expect(received[1]).toContain('first'); expect(received[1]).toContain('second'); } finally { - globalThis.fetch = originalFetch; + vi.unstubAllGlobals(); } }); }); diff --git a/apps/cloudflare-metrics/src/monitor.ts b/apps/cloudflare-metrics/src/monitor.ts index bd62f06..3bc2e80 100644 --- a/apps/cloudflare-metrics/src/monitor.ts +++ b/apps/cloudflare-metrics/src/monitor.ts @@ -26,7 +26,7 @@ export function monitorAsyncFunction( try { return await call(...args); } catch (error) { - if (!acceptedErrors.some((acceptedError) => error instanceof acceptedError)) { + if (acceptedErrors.every((acceptedError) => !(error instanceof acceptedError))) { console.error(error, `${operationName}_errors`); metric.intField('errors', 1); } diff --git a/apps/cloudflare-metrics/src/resource-cache.test.ts b/apps/cloudflare-metrics/src/resource-cache.test.ts index 683e908..d337e02 100644 --- a/apps/cloudflare-metrics/src/resource-cache.test.ts +++ b/apps/cloudflare-metrics/src/resource-cache.test.ts @@ -113,7 +113,7 @@ describe('ResourceCacheService', () => { const service = new ResourceCacheService('acct', metrics, now, restClient); await service.populate(); await service.resolveMissingZones(['zone-1']); - expect(restClient.getZoneCalls).toHaveLength(0); + expect(restClient.zoneCalls).toHaveLength(0); }); it('caps individual lookups per run', async () => { @@ -125,7 +125,7 @@ describe('ResourceCacheService', () => { const service = new ResourceCacheService('acct', metrics, now, restClient); await service.populate(); await service.resolveMissingZones(Object.keys(zones)); - expect(restClient.getZoneCalls).toHaveLength(20); + expect(restClient.zoneCalls).toHaveLength(20); }); it('does nothing when no rest client is provided', async () => { diff --git a/apps/cloudflare-metrics/src/resource-cache.ts b/apps/cloudflare-metrics/src/resource-cache.ts index 8a14ea7..9a88d35 100644 --- a/apps/cloudflare-metrics/src/resource-cache.ts +++ b/apps/cloudflare-metrics/src/resource-cache.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/no-top-level-assignment-in-function */ import type { ICloudflareRestClient } from './cloudflare-api.js'; import { Metric } from './metric.js'; import type { CloudflareMetricsRepository } from './metrics.js'; @@ -75,25 +76,28 @@ export class ResourceCacheService { const restClient = this.restClient; const now = this.now().getTime(); - const cacheFresh = (cache: CachedResourceLookup | null): cache is CachedResourceLookup => + const isCacheFresh = (cache: CachedResourceLookup | null): cache is CachedResourceLookup => cache !== null && now - cache.loadedAt < RESOURCE_CACHE_TTL_MS; const [d1Result, queuesResult, zonesResult] = await Promise.allSettled([ - cacheFresh(cachedD1Databases) + isCacheFresh(cachedD1Databases) ? Promise.resolve(cachedD1Databases.values) - : restClient.listD1Databases(this.accountTag).then((values) => { + : // eslint-disable-next-line unicorn/prefer-await + restClient.listD1Databases(this.accountTag).then((values) => { cachedD1Databases = { values, loadedAt: now }; return values; }), - cacheFresh(cachedQueues) + isCacheFresh(cachedQueues) ? Promise.resolve(cachedQueues.values) - : restClient.listQueues(this.accountTag).then((values) => { + : // eslint-disable-next-line unicorn/prefer-await + restClient.listQueues(this.accountTag).then((values) => { cachedQueues = { values, loadedAt: now }; return values; }), - cacheFresh(cachedBulkZones) + isCacheFresh(cachedBulkZones) ? Promise.resolve(cachedBulkZones.values) - : restClient.listZones(this.accountTag).then((values) => { + : // eslint-disable-next-line unicorn/prefer-await + restClient.listZones(this.accountTag).then((values) => { cachedBulkZones = { values, loadedAt: now }; return values; }), @@ -101,26 +105,32 @@ export class ResourceCacheService { this.recordResourceLookup('d1_databases', d1Result, (items) => { for (const db of items) { - if (db.uuid && db.name) { - this.cache.d1Databases.set(db.uuid, db.name); - globalD1NameCache.set(db.uuid, db.name); + if (!(db.uuid && db.name)) { + continue; } + + this.cache.d1Databases.set(db.uuid, db.name); + globalD1NameCache.set(db.uuid, db.name); } }); this.recordResourceLookup('queues', queuesResult, (items) => { for (const q of items) { - if (q.queue_id && q.queue_name) { - this.cache.queues.set(q.queue_id, q.queue_name); - globalQueueNameCache.set(q.queue_id, q.queue_name); + if (!(q.queue_id && q.queue_name)) { + continue; } + + this.cache.queues.set(q.queue_id, q.queue_name); + globalQueueNameCache.set(q.queue_id, q.queue_name); } }); this.recordResourceLookup('zones', zonesResult, (items) => { for (const z of items) { - if (z.id && z.name) { - this.cache.zones.set(z.id, z.name); - this.cache.bulkZoneTags.add(z.id); + if (!(z.id && z.name)) { + continue; } + + this.cache.zones.set(z.id, z.name); + this.cache.bulkZoneTags.add(z.id); } }); diff --git a/apps/cloudflare-metrics/src/test-helpers.ts b/apps/cloudflare-metrics/src/test-helpers.ts index af43a5c..5b4b5a5 100644 --- a/apps/cloudflare-metrics/src/test-helpers.ts +++ b/apps/cloudflare-metrics/src/test-helpers.ts @@ -109,7 +109,7 @@ export function asGraphQLClient(fake: FakeGraphQLClient): CloudflareGraphQLClien * pages-only zone exists outside the bulk list. */ export class FakeRestClient implements ICloudflareRestClient { - public getZoneCalls: string[] = []; + public zoneCalls: string[] = []; constructor( public readonly d1: Array<{ uuid: string; name: string }> = [], public readonly queues: Array<{ queue_id: string; queue_name: string }> = [], @@ -134,7 +134,7 @@ export class FakeRestClient implements ICloudflareRestClient { // eslint-disable-next-line @typescript-eslint/require-await async getZone(zoneId: string) { - this.getZoneCalls.push(zoneId); + this.zoneCalls.push(zoneId); if (zoneId in this.individualZones) { return this.individualZones[zoneId]; } diff --git a/apps/github-approval-check/src/approval.ts b/apps/github-approval-check/src/approval.ts index 798221e..c479f57 100644 --- a/apps/github-approval-check/src/approval.ts +++ b/apps/github-approval-check/src/approval.ts @@ -120,7 +120,7 @@ export class ApprovalValidator { console.log( `[approval] DEBUG: Reviews by valid users (after deduplication):`, JSON.stringify( - [...approvalsByUser.entries()].map(([userId, review]) => ({ + [...approvalsByUser].map(([userId, review]) => ({ userId, login: review.user.login, state: review.state, diff --git a/apps/github-approval-check/src/auth.ts b/apps/github-approval-check/src/auth.ts index 4e93315..e02dd02 100644 --- a/apps/github-approval-check/src/auth.ts +++ b/apps/github-approval-check/src/auth.ts @@ -74,7 +74,7 @@ export function createOctokitForInstallation(appId: string, privateKey: string, console.error('Invalid privateKey:', { hasPrivateKey: !!privateKey, type: typeof privateKey, - length: privateKey ? String(privateKey).length : 0, + length: privateKey ? privateKey.length : 0, }); throw new Error('Invalid GitHub App Private Key provided to createOctokitForInstallation'); } diff --git a/apps/github-approval-check/src/dev-mode.ts b/apps/github-approval-check/src/dev-mode.ts index 7be8c66..1c842aa 100644 --- a/apps/github-approval-check/src/dev-mode.ts +++ b/apps/github-approval-check/src/dev-mode.ts @@ -22,11 +22,11 @@ export function getDevModeConfig(env: Env): DevModeConfig { // Extract PR number from stage (e.g., '-pr-123' -> 123) let prNumber: number | undefined; if (env.DEV_PR_NUMBER) { - prNumber = Number.parseInt(env.DEV_PR_NUMBER, 10); + prNumber = Number(env.DEV_PR_NUMBER); } else if (isPRDeployment && env.STAGE) { const match = env.STAGE.match(/-pr-(\d+)/); if (match) { - prNumber = Number.parseInt(match[1], 10); + prNumber = Number(match[1]); } } diff --git a/apps/github-approval-check/src/webhook.ts b/apps/github-approval-check/src/webhook.ts index 0fe6b95..087d42d 100644 --- a/apps/github-approval-check/src/webhook.ts +++ b/apps/github-approval-check/src/webhook.ts @@ -30,13 +30,13 @@ export async function verifyWebhookSignature(body: string, signature: string, se const computedSignature = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join(''); // Constant-time comparison to prevent timing attacks - return safeCompare(computedSignature, providedSignature); + return isSafeCompare(computedSignature, providedSignature); } /** * Constant-time string comparison to prevent timing attacks */ -function safeCompare(a: string, b: string): boolean { +function isSafeCompare(a: string, b: string): boolean { if (a.length !== b.length) { return false; } diff --git a/apps/version/src/index.test.ts b/apps/version/src/index.test.ts index c314c65..2d58540 100644 --- a/apps/version/src/index.test.ts +++ b/apps/version/src/index.test.ts @@ -774,7 +774,7 @@ describe('GitHubRepository', () => { if (url.origin === 'https://api.github.com' && url.pathname === '/repos/immich-app/immich/releases') { return Promise.resolve(Response.json(githubReleases)); } - return Promise.reject(new Error(`unexpected fetch: ${url.toString()}`)); + return Promise.reject(new Error(`unexpected fetch: ${url.href}`)); }); const releases = await new GitHubRepository().fetchReleases(); diff --git a/apps/version/src/index.ts b/apps/version/src/index.ts index 90789b1..aee6c2c 100644 --- a/apps/version/src/index.ts +++ b/apps/version/src/index.ts @@ -111,7 +111,7 @@ export default { if (env.ENVIRONMENT) { const cache = caches.default; - const cacheKey = new Request(url.toString(), request); + const cacheKey = new Request(url.href, request); const cached = await cache.match(cacheKey); if (cached) { @@ -132,7 +132,7 @@ export default { const response = jsonResponse(changelog, 200, { 'Cache-Control': 'public, max-age=86400' }); if (env.ENVIRONMENT) { const cache = caches.default; - const cacheKey = new Request(url.toString(), request); + const cacheKey = new Request(url.href, request); ctx.waitUntil(cache.put(cacheKey, response.clone())); } return response; diff --git a/apps/version/src/metrics.ts b/apps/version/src/metrics.ts index 3475103..0ef404e 100644 --- a/apps/version/src/metrics.ts +++ b/apps/version/src/metrics.ts @@ -75,10 +75,12 @@ export class HeaderMetricsProvider implements IMetricsProviderRepository { pushMetric(metric: Metric) { for (const [label, { value, type }] of metric.fields) { - if (type === 'duration') { - const suffix = label === 'duration' ? '' : `_${label.replace('_duration', '')}`; - this._metrics.push(`${metric.name}${suffix};dur=${value}`); + if (type !== 'duration') { + continue; } + + const suffix = label === 'duration' ? '' : `_${label.replace('_duration', '')}`; + this._metrics.push(`${metric.name}${suffix};dur=${value}`); } } diff --git a/apps/version/src/monitor.ts b/apps/version/src/monitor.ts index 412c191..d68b00c 100644 --- a/apps/version/src/monitor.ts +++ b/apps/version/src/monitor.ts @@ -26,7 +26,7 @@ export function monitorAsyncFunction( try { return await call(...args); } catch (error) { - if (!acceptedErrors.some((acceptedError) => error instanceof acceptedError)) { + if (acceptedErrors.every((acceptedError) => !(error instanceof acceptedError))) { console.error(error, `${operationName}_errors`); metric.intField('errors', 1); } diff --git a/apps/version/src/version-service.ts b/apps/version/src/version-service.ts index 876f3b7..014ede5 100644 --- a/apps/version/src/version-service.ts +++ b/apps/version/src/version-service.ts @@ -55,7 +55,7 @@ export class VersionService { })(); const response = new Map( - [...latest.entries()] + [...latest] .filter(([_, release]) => release !== null) .map( ([channel, release]) => diff --git a/apps/version/src/webhook.ts b/apps/version/src/webhook.ts index a837316..7ca4c77 100644 --- a/apps/version/src/webhook.ts +++ b/apps/version/src/webhook.ts @@ -16,10 +16,10 @@ export async function verifyWebhookSignature(body: string, signature: string, se const mac = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(body)); const computedSignature = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, '0')).join(''); - return timingSafeEqual(computedSignature, providedSignature); + return isTimingSafeEqual(computedSignature, providedSignature); } -function timingSafeEqual(a: string, b: string): boolean { +function isTimingSafeEqual(a: string, b: string): boolean { if (a.length !== b.length) { return false; } diff --git a/eslint.config.mjs b/eslint.config.mjs index 1235edd..d2856be 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -39,7 +39,7 @@ export default typescriptEslint.config([ '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'error', - 'unicorn/prevent-abbreviations': 'off', + 'unicorn/name-replacements': 'off', 'unicorn/filename-case': 'off', 'unicorn/no-null': 'off', 'unicorn/prefer-top-level-await': 'off', @@ -48,6 +48,10 @@ export default typescriptEslint.config([ 'unicorn/import-style': 'off', 'unicorn/prefer-structured-clone': 'off', 'unicorn/no-for-loop': 'off', + 'unicorn/consistent-class-member-order': 'off', + 'unicorn/no-computed-property-existence-check': 'off', + 'unicorn/max-nested-calls': ['error', { max: 4 }], + 'unicorn/no-break-in-nested-loop': 'off', '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/no-misused-promises': 'error', 'require-await': 'off', diff --git a/package.json b/package.json index d9b1a93..d210f3d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "eslint": "^10.0.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.5.4", - "eslint-plugin-unicorn": "^65.0.0", + "eslint-plugin-unicorn": "^70.0.0", "globals": "^17.0.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3072804..0c3c7e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: ^5.5.4 version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0))(eslint@10.6.0)(prettier@3.9.4) eslint-plugin-unicorn: - specifier: ^65.0.0 - version: 65.0.1(eslint@10.6.0) + specifier: ^70.0.0 + version: 70.0.0(eslint@10.6.0) globals: specifier: ^17.0.0 version: 17.7.0 @@ -1032,11 +1032,11 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-unicorn@65.0.1: - resolution: {integrity: sha512-daCrQrgxOoOz2uMPWB3Y3vvv/5q+ncwICI8IjoebiwtW87CaY4tAN5EEiRXTYVnf7qi1v1BGBdHOSnZLV0rx6A==} - engines: {node: ^20.10.0 || >=21.0.0} + eslint-plugin-unicorn@70.0.0: + resolution: {integrity: sha512-uAF9xMcVvvhTfvusCgogJ1wh4To3q2KhVMw3i1Apf/ILTbxsCjscvraAZACsEurb7no2fdXblD3whcbVnjw5zg==} + engines: {node: '>=22'} peerDependencies: - eslint: '>=9.38.0' + eslint: '>=10.4' eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} @@ -2436,10 +2436,11 @@ snapshots: optionalDependencies: eslint-config-prettier: 10.1.8(eslint@10.6.0) - eslint-plugin-unicorn@65.0.1(eslint@10.6.0): + eslint-plugin-unicorn@70.0.0(eslint@10.6.0): dependencies: '@babel/helper-validator-identifier': 7.29.7 '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + browserslist: 4.28.4 change-case: 5.4.4 ci-info: 4.4.0 core-js-compat: 3.49.0