← 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/step.ts
114 lines · 3.0 KB · TypeScriptimport { listCandidates, resolveLimit } from './candidates.ts'
import { validateCodemodRef, type CodemodRef } from './contract.ts'
import {
createRun,
resumeRun,
saveRun,
summarizeItems,
type ItemStatus,
type RunItem,
type RunMode,
} from './ledger.ts'
import { shouldStopBeforeNextPackage } from './time-budget.ts'
export type StepInput = {
codemod?: CodemodRef
packageIds?: Array<string>
runId?: string
cursor?: string | null
limit?: number
/**
* Optional JS regex. When set, scan treats zero repo_search hits as
* clean and only reads matching files before detect. Use only when
* detect findings require a match — absence-based detects must omit it.
*/
detectPattern?: string
}
export type StepResult = {
runId: string
mode: RunMode
items: Array<RunItem>
nextCursor: string | null
summary: Partial<Record<ItemStatus, number>>
note: string | null
}
export function requireCodemod(input: StepInput): CodemodRef {
return validateCodemodRef(input.codemod)
}
export async function runStep(input: {
mode: 'scan' | 'dry-run' | 'apply'
step: StepInput
defaults: { limit: number; max: number }
processPackage: (
run: { id: string },
pkg: { packageId: string; kodyId: string },
) => Promise<RunItem>
}): Promise<StepResult> {
const codemod = requireCodemod(input.step)
if (input.step.cursor != null && input.step.runId == null) {
throw new Error('cursor requires runId to continue an existing run.')
}
const run = input.step.runId
? await resumeRun({
runId: input.step.runId,
mode: input.mode,
codemod,
})
: await createRun({ mode: input.mode, codemod })
const limit = resolveLimit({
requested: input.step.limit,
fallback: input.defaults.limit,
max: input.defaults.max,
})
const { candidates, nextCursor } = await listCandidates({
codemod,
packageIds: input.step.packageIds,
paging: { cursor: input.step.cursor ?? run.cursor, limit },
})
const startedAt = Date.now()
const items: Array<RunItem> = []
let stoppedEarlyAt: string | null = null
let lastPackageMs = 0
for (const pkg of candidates) {
if (
shouldStopBeforeNextPackage({
elapsedMs: Date.now() - startedAt,
completedCount: items.length,
lastPackageMs,
})
) {
stoppedEarlyAt = items[items.length - 1]!.packageId
break
}
const packageStarted = Date.now()
const item = await input.processPackage({ id: run.id }, pkg)
lastPackageMs = Date.now() - packageStarted
items.push(item)
run.items = [...run.items, item]
run.cursor = pkg.packageId
run.status = 'running'
await saveRun(run)
}
const resolvedNextCursor = stoppedEarlyAt ?? nextCursor
run.cursor = resolvedNextCursor
run.status = resolvedNextCursor == null ? 'completed' : 'running'
await saveRun(run)
return {
runId: run.id,
mode: input.mode,
items,
nextCursor: resolvedNextCursor,
summary: summarizeItems(items),
note:
resolvedNextCursor == null
? null
: `Run is not finished. Call again with runId "${run.id}" and cursor "${resolvedNextCursor}".`,
}
}