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/revert.ts

158 lines · 4.2 KB · TypeScript
import {
	createRun,
	getRun,
	getSnapshot,
	markSourceItemReverted,
	resumeRun,
	saveRun,
	summarizeItems,
	type RunItem,
} from './ledger.ts'
import { revertItem } from './process-package.ts'
import { shouldStopBeforeNextPackage } from './time-budget.ts'

export type RevertInput = {
	/** The prior `apply` run id whose applied packages should be restored. */
	revertOfRunId?: string
	runId?: string
	cursor?: string | null
	limit?: number
}

/**
 * Restores packages applied by a prior `apply` run to their pre-codemod
 * trees from the ledger snapshots. Skips packages republished since the
 * apply (drift) rather than overwriting newer work. Items that fail or are
 * skipped stay `applied`; start another revert of the same apply run to
 * retry them. Paged: keep calling with the returned runId + cursor until
 * nextCursor is null.
 */
export default async function revert(input: RevertInput = {}) {
	if (!input.revertOfRunId) {
		throw new Error('Pass revertOfRunId: the apply run id to revert.')
	}
	if (input.cursor != null && input.runId == null) {
		throw new Error('cursor requires runId to continue an existing run.')
	}
	const sourceRun = await getRun(input.revertOfRunId)
	if (!sourceRun) {
		throw new Error(`Run "${input.revertOfRunId}" was not found.`)
	}
	if (sourceRun.mode !== 'apply') {
		throw new Error(
			`Run "${input.revertOfRunId}" is a ${sourceRun.mode} run; only apply runs can be reverted.`,
		)
	}
	const run = input.runId
		? await resumeRun({
				runId: input.runId,
				mode: 'revert',
				codemod: null,
				revertOfRunId: input.revertOfRunId,
			})
		: await createRun({
				mode: 'revert',
				codemod: sourceRun.codemod,
				revertOfRunId: input.revertOfRunId,
			})

	const limit = Math.min(Math.max(input.limit ?? 1, 1), 2)
	const cursor = input.cursor ?? run.cursor
	const appliedItems = sourceRun.items
		.filter((item) => item.status === 'applied')
		.sort((left, right) =>
			left.packageId < right.packageId
				? -1
				: left.packageId > right.packageId
					? 1
					: 0,
		)
		.filter((item) => cursor == null || item.packageId > cursor)

	const startedAt = Date.now()
	const items: Array<RunItem> = []
	let processed = 0
	let stoppedEarlyAt: string | null = null
	let lastPackageMs = 0
	for (const sourceItem of appliedItems) {
		if (processed >= limit) {
			stoppedEarlyAt = items[items.length - 1]?.packageId ?? cursor ?? null
			break
		}
		if (
			shouldStopBeforeNextPackage({
				elapsedMs: Date.now() - startedAt,
				completedCount: items.length,
				lastPackageMs,
			})
		) {
			stoppedEarlyAt = items[items.length - 1]!.packageId
			break
		}
		const packageStarted = Date.now()
		const snapshot = await getSnapshot({
			runId: sourceRun.id,
			packageId: sourceItem.packageId,
		})
		if (!snapshot) {
			const failed = {
				...sourceItem,
				status: 'failed' as const,
				afterCommit: null,
				diff: null,
				error: 'No revert snapshot found for this package in the apply run.',
			}
			items.push(failed)
			run.items = [...run.items, failed]
			run.cursor = sourceItem.packageId
			run.status = 'running'
			await saveRun(run)
			lastPackageMs = Date.now() - packageStarted
			processed += 1
			continue
		}
		const item = await revertItem({
			revertRunId: run.id,
			sourceRunId: sourceRun.id,
			sourceCodemod: sourceRun.codemod,
			item: sourceItem,
			snapshot,
		})
		if (item.status === 'reverted') {
			await markSourceItemReverted({
				runId: sourceRun.id,
				packageId: sourceItem.packageId,
			})
		}
		items.push(item)
		run.items = [...run.items, item]
		run.cursor = sourceItem.packageId
		run.status = 'running'
		await saveRun(run)
		lastPackageMs = Date.now() - packageStarted
		processed += 1
	}

	const hasMore =
		stoppedEarlyAt != null ||
		appliedItems.length > processed
	const resolvedNextCursor = hasMore
		? (stoppedEarlyAt ?? items[items.length - 1]?.packageId ?? cursor)
		: null
	run.cursor = resolvedNextCursor
	run.status = resolvedNextCursor == null ? 'completed' : 'running'
	await saveRun(run)

	return {
		runId: run.id,
		mode: 'revert' as const,
		revertOfRunId: input.revertOfRunId,
		items,
		nextCursor: resolvedNextCursor,
		summary: summarizeItems(items),
		note:
			resolvedNextCursor == null
				? null
				: `Revert is not finished. Call again with runId "${run.id}" and cursor "${resolvedNextCursor}".`,
	}
}