Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ npx @doist/react-compiler-tracker --overwrite

### `--stage-record-file <file1> <file2> ...`

Checks the provided files and updates the records. Exits with code 1 if errors increase (preventing the commit), otherwise updates the records file for the checked files. Reports when errors decrease, celebrating your progress. Deleted files are automatically removed from the records.
Checks the provided files and updates the records. Exits with code 1 if errors increase (preventing the commit), otherwise updates the records file for the checked files. Reports when errors decrease, celebrating your progress. Deleted files are automatically removed from the records (no need to pass their paths).

```bash
npx @doist/react-compiler-tracker --stage-record-file src/components/Button.tsx src/hooks/useData.ts
Expand Down Expand Up @@ -130,8 +130,9 @@ With lint-staged, the matched files are automatically passed as arguments to the

```bash
#!/bin/sh
# Get staged files (including deleted) and pass them to the tracker
FILES=$(git diff --diff-filter=ACMRD --cached --name-only -- '*.tsx' '*.ts' '*.jsx' '*.js' | tr '\n' ' ')
# Get staged files and pass them to the tracker
# (deleted files are auto-detected from records, no need to pass them)
FILES=$(git diff --diff-filter=ACMR --cached --name-only -- '*.tsx' '*.ts' '*.jsx' '*.js' | tr '\n' ' ')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing D (Deleted) from the diff-filter causes FILES to be empty for commits that only contain file deletions. In such cases, the script exits early (line 136) and the tracker never runs, so the records are not updated. You should keep D in the filter to ensure the tool is triggered; the new logic in runStageRecords handles non-existent input files gracefully.

Copy link
Member Author

@frankieyan frankieyan Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lint-staged by default also doesn't filter by deleted files, so for todoist-web, commits with only deletes also fail to run esplint.

Maybe asking users to always pass in deleted paths would be the better call (e.g. lint-staged --diff-filter=ACMRD), but not all of our tooling are able to handle it:

image

if [ -n "$FILES" ]; then
npx @doist/react-compiler-tracker --stage-record-file $FILES
fi
Expand Down
16 changes: 5 additions & 11 deletions src/index.integration.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('CLI', () => {
'🔍 Checking all 5 source files for React Compiler errors and recreating records…',
)
expect(output).toContain(
'✅ Records file completed. Found 4 total React Compiler issues across 2 files',
'✅ Records saved to .react-compiler.rec.json. Found 4 total React Compiler issues across 2 files',
)

const records = JSON.parse(readFileSync(recordsPath, 'utf8'))
Expand Down Expand Up @@ -145,20 +145,14 @@ describe('CLI', () => {
records = JSON.parse(readFileSync(recordsPath, 'utf8'))
expect(records.files['src/deleted-file.tsx']).toEqual({ CompileError: 2 })

// Now run --stage-record-file with the deleted file path
// The file src/deleted-file.tsx doesn't exist, simulating deletion
const output = runCLI([
'--stage-record-file',
'src/good-component.tsx',
'src/deleted-file.tsx',
])
// Run --stage-record-file WITHOUT the deleted file in arguments
// (simulating lint-staged which doesn't pass deleted files)
// The tool should auto-detect that src/deleted-file.tsx no longer exists
const output = runCLI(['--stage-record-file', 'src/good-component.tsx'])

// Should log the deleted file being removed
expect(output).toContain('🗑️ Removing 1 deleted file from records:')
expect(output).toContain('• src/deleted-file.tsx')
// Should check only existing files (1 file, not 2)
expect(output).toContain('🔍 Checking 1 file for React Compiler errors')
// Should not error on the deleted file
expect(output).not.toContain('File not found')

// Verify the deleted file was removed from records
Expand Down
45 changes: 25 additions & 20 deletions src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,9 @@ async function main() {
filePaths: sourceFiles.normalizeFilePaths(filePathParams),
globPattern: config.sourceGlob,
})
const { existing, deleted } = sourceFiles.partitionByExistence(filePaths)

return await runStageRecords({
existingFilePaths: existing,
allFilePaths: filePaths,
deletedFilePaths: deleted,
filePaths,
recordsFilePath: config.recordsFile,
})
}
Expand Down Expand Up @@ -136,40 +133,46 @@ async function runOverwriteRecords({

if (totalErrors > 0) {
console.log(
`✅ Records file completed. Found ${totalErrors} total React Compiler issues across ${compilerErrors.size} files`,
`✅ Records saved to ${recordsFilePath}. Found ${totalErrors} total React Compiler issues across ${compilerErrors.size} files`,
)
} else {
console.log('🎉 No React Compiler errors found')
console.log(`🎉 Records saved to ${recordsFilePath}. No React Compiler errors found`)
}
}

/**
* Handles the `--stage-record-file` flag by checking provided files and updating the records file.
*
* If errors have increased, the process will exit with code 1 and the records file will not be updated.
* Deleted files are automatically removed from the records.
* Deleted files are automatically detected by checking which recorded files no longer exist on disk.
*/
async function runStageRecords({
existingFilePaths,
allFilePaths,
deletedFilePaths,
filePaths,
recordsFilePath,
}: {
existingFilePaths: string[]
allFilePaths: string[]
deletedFilePaths: string[]
filePaths: string[]
recordsFilePath: string
}) {
const records = recordsFile.load(recordsFilePath)
const recordedFilePaths = records ? Object.keys(records.files) : []
const { deleted: deletedFromRecords } = sourceFiles.partitionByExistence(recordedFilePaths)

const { existing: existingFilePaths, deleted: deletedFromInput } =
sourceFiles.partitionByExistence(filePaths)

const allDeletedFilePaths = [...new Set([...deletedFromRecords, ...deletedFromInput])]
const allFilePaths = [...new Set([...filePaths, ...deletedFromRecords])]

if (!allFilePaths.length) {
console.log('✅ No files to check')
return
}

if (deletedFilePaths.length > 0) {
const deletedFileWord = pluralize(deletedFilePaths.length, 'file', 'files')
const fileList = deletedFilePaths.map((f) => ` • ${f}`).join('\n')
if (allDeletedFilePaths.length > 0) {
const deletedFileWord = pluralize(allDeletedFilePaths.length, 'file', 'files')
const fileList = allDeletedFilePaths.map((f) => ` • ${f}`).join('\n')
console.log(
`🗑️ Removing ${deletedFilePaths.length} deleted ${deletedFileWord} from records:\n${fileList}`,
`🗑️ Removing ${allDeletedFilePaths.length} deleted ${deletedFileWord} from records:\n${fileList}`,
)
}

Expand All @@ -191,7 +194,7 @@ async function runStageRecords({
customReactCompilerLogger: customReactCompilerLogger,
})

const records = checkErrorChanges({ filePaths: existingFilePaths, recordsFilePath })
checkErrorChanges({ filePaths: existingFilePaths, recordsFilePath, records })

//
// Update and stage records file (includes deleted files so they get removed from records)
Expand All @@ -212,7 +215,7 @@ async function runStageRecords({
exitWithWarning(`Failed to stage records file at ${recordsFileRelativePath}`)
}

console.log('✅ No new React Compiler errors')
console.log(`✅ Records saved to ${recordsFilePath}. No new React Compiler errors`)
}

/**
Expand Down Expand Up @@ -304,11 +307,13 @@ function getErrorCount() {
function checkErrorChanges({
filePaths,
recordsFilePath,
records: providedRecords,
}: {
filePaths: string[]
recordsFilePath: string
records?: recordsFile.Records | null
}) {
const records = recordsFile.load(recordsFilePath)
const records = providedRecords ?? recordsFile.load(recordsFilePath)
const { increases, decreases } = recordsFile.getErrorChanges({
filePaths,
existingRecords: records?.files ?? {},
Expand Down