Skip to content
← Public packages

@kentcdodds/codemod-runner

Run codemods over your own saved packages: scan, dry-run with real publish checks and diffs, apply, and revert.

src/candidates.ts

80 lines · 2.3 KB · TypeScript
import { kody, packageContext } from 'kody:runtime'
import { type CodemodRef } from './contract.ts'

export type CandidatePackage = {
	packageId: string
	kodyId: string
}

export type StepPaging = {
	cursor: string | null
	limit: number
}

/**
 * Lists the caller's own packages as codemod candidates, sorted by package
 * id with binary comparison so paging cursors are stable. The runner itself
 * and the codemod package are always excluded.
 */
export async function listCandidates(input: {
	codemod: CodemodRef | null
	packageIds?: Array<string>
	paging: StepPaging
}): Promise<{ candidates: Array<CandidatePackage>; nextCursor: string | null }> {
	if (input.packageIds != null) {
		const usable = input.packageIds.filter(
			(id) => typeof id === 'string' && id.trim().length > 0,
		)
		if (usable.length === 0) {
			throw new Error(
				'packageIds was provided but contains no usable ids. Omit it to run over all packages.',
			)
		}
	}
	const result = (await kody.packageList({})) as {
		packages: Array<{ package_id: string; kody_id: string }>
	}
	const selfPackageId = (packageContext as { packageId?: string } | null)
		?.packageId
	const filterSet =
		input.packageIds != null ? new Set(input.packageIds) : null
	const all = result.packages
		.filter((pkg) => pkg.package_id !== selfPackageId)
		.filter((pkg) => pkg.kody_id !== input.codemod?.kodyId)
		.filter(
			(pkg) =>
				filterSet === null ||
				filterSet.has(pkg.package_id) ||
				filterSet.has(pkg.kody_id),
		)
		.map((pkg) => ({ packageId: pkg.package_id, kodyId: pkg.kody_id }))
		.sort((left, right) =>
			left.packageId < right.packageId
				? -1
				: left.packageId > right.packageId
					? 1
					: 0,
		)
	const afterCursor =
		input.paging.cursor == null
			? all
			: all.filter((pkg) => pkg.packageId > (input.paging.cursor as string))
	const page = afterCursor.slice(0, input.paging.limit)
	const nextCursor =
		afterCursor.length > page.length && page.length > 0
			? page[page.length - 1]!.packageId
			: null
	return { candidates: page, nextCursor }
}

export function resolveLimit(input: {
	requested?: number
	fallback: number
	max: number
}): number {
	const requested = input.requested ?? input.fallback
	if (!Number.isInteger(requested) || requested < 1) {
		return input.fallback
	}
	return Math.min(requested, input.max)
}