Skip to content

Commit 87978e0

Browse files
frankieyanclaude
andcommitted
refactor: Consolidate path normalization into source-files.mts
- Move getGitPrefix() and normalizeFilePaths() from git-utils.mts - Add absolute path handling to normalizeFilePaths() - Delete git-utils.mts and git-utils.test.mts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bf4a673 commit 87978e0

5 files changed

Lines changed: 150 additions & 124 deletions

File tree

src/git-utils.mts

Lines changed: 0 additions & 49 deletions
This file was deleted.

src/git-utils.test.mts

Lines changed: 0 additions & 70 deletions
This file was deleted.

src/index.mts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { join, relative } from 'node:path'
44
import type { Logger as ReactCompilerLogger } from 'babel-plugin-react-compiler'
55
import * as babel from './babel.mjs'
66
import { loadConfig } from './config.mjs'
7-
import { normalizeFilePaths } from './git-utils.mjs'
87
import type { FileErrors } from './records-file.mjs'
98
import * as recordsFile from './records-file.mjs'
109
import * as sourceFiles from './source-files.mjs'
@@ -44,7 +43,7 @@ async function main() {
4443
switch (flag) {
4544
case STAGE_RECORD_FILE_FLAG: {
4645
const filePathParams = process.argv.slice(3)
47-
const filePaths = normalizeFilePaths(filePathParams)
46+
const filePaths = sourceFiles.normalizeFilePaths(filePathParams)
4847

4948
return await runStageRecords({
5049
filePaths,
@@ -59,7 +58,7 @@ async function main() {
5958
}
6059
case CHECK_FILES_FLAG: {
6160
const filePathParams = process.argv.slice(3)
62-
const filePaths = normalizeFilePaths(filePathParams)
61+
const filePaths = sourceFiles.normalizeFilePaths(filePathParams)
6362

6463
return await runCheckFiles({ filePaths, recordsFilePath: config.recordsFile })
6564
}

src/source-files.mts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { execSync } from 'node:child_process'
2+
import { relative } from 'node:path'
13
import { glob } from 'glob'
24

35
function getAll({ globPattern }: { globPattern: string }) {
@@ -7,4 +9,58 @@ function getAll({ globPattern }: { globPattern: string }) {
79
})
810
}
911

10-
export { getAll }
12+
/**
13+
* Gets the current working directory relative to the git repository root.
14+
*
15+
* @returns The cwd relative to git root (e.g., "apps/frontend/"), empty string if at git root, or null if not in a git repo
16+
*/
17+
function getGitPrefix() {
18+
try {
19+
const result = execSync('git rev-parse --show-prefix', {
20+
encoding: 'utf8',
21+
stdio: ['pipe', 'pipe', 'pipe'],
22+
})
23+
return result.trim()
24+
} catch {
25+
return null
26+
}
27+
}
28+
29+
/**
30+
* Normalizes file paths by converting absolute paths to cwd-relative
31+
* and stripping the git prefix when present.
32+
*
33+
* When running from a package subdirectory in a monorepo, file paths from git
34+
* (e.g., lint-staged) are relative to the repo root. This function converts
35+
* those paths to cwd-relative paths.
36+
*
37+
* @example
38+
* // When cwd is apps/frontend/ (prefix is "apps/frontend/")
39+
* normalizeFilePaths(["apps/frontend/src/file.tsx"]) // => ["src/file.tsx"]
40+
*
41+
* // Absolute paths are converted to cwd-relative
42+
* normalizeFilePaths(["/Users/frankie/project/src/file.tsx"]) // => ["src/file.tsx"]
43+
*
44+
* // Paths that don't start with prefix are unchanged
45+
* normalizeFilePaths(["src/file.tsx"]) // => ["src/file.tsx"]
46+
*/
47+
function normalizeFilePaths(filePaths: string[]) {
48+
const prefix = getGitPrefix()
49+
const cwd = process.cwd()
50+
51+
return filePaths.map((filePath) => {
52+
// Handle absolute paths by converting to cwd-relative
53+
if (filePath.startsWith('/')) {
54+
return relative(cwd, filePath)
55+
}
56+
57+
// Handle monorepo prefix stripping
58+
if (prefix && filePath.startsWith(prefix)) {
59+
return filePath.slice(prefix.length)
60+
}
61+
62+
return filePath
63+
})
64+
}
65+
66+
export { getAll, normalizeFilePaths }

src/source-files.test.mts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import { execSync } from 'node:child_process'
12
import { glob } from 'glob'
23
import { afterEach, describe, expect, it, vi } from 'vitest'
3-
import { getAll } from './source-files.mjs'
4+
import { getAll, normalizeFilePaths } from './source-files.mjs'
5+
6+
vi.mock('node:child_process', () => ({
7+
execSync: vi.fn(),
8+
}))
49

510
vi.mock('glob', () => ({
611
glob: {
@@ -33,3 +38,88 @@ describe('getAll', () => {
3338
expect(result).toEqual([])
3439
})
3540
})
41+
42+
describe('normalizeFilePaths', () => {
43+
it('strips prefix from matching paths', () => {
44+
vi.mocked(execSync).mockReturnValue('apps/frontend/\n')
45+
46+
const result = normalizeFilePaths([
47+
'apps/frontend/src/App.tsx',
48+
'apps/frontend/src/utils/helper.ts',
49+
])
50+
51+
expect(result).toEqual(['src/App.tsx', 'src/utils/helper.ts'])
52+
})
53+
54+
it('leaves non-matching paths unchanged', () => {
55+
vi.mocked(execSync).mockReturnValue('apps/frontend/\n')
56+
57+
const result = normalizeFilePaths(['src/App.tsx', 'src/utils/helper.ts'])
58+
59+
expect(result).toEqual(['src/App.tsx', 'src/utils/helper.ts'])
60+
})
61+
62+
it('handles mixed paths (some matching, some not)', () => {
63+
vi.mocked(execSync).mockReturnValue('apps/frontend/\n')
64+
65+
const result = normalizeFilePaths([
66+
'apps/frontend/src/App.tsx',
67+
'src/local.ts',
68+
'apps/frontend/index.ts',
69+
])
70+
71+
expect(result).toEqual(['src/App.tsx', 'src/local.ts', 'index.ts'])
72+
})
73+
74+
it('returns paths unchanged when at git root', () => {
75+
vi.mocked(execSync).mockReturnValue('\n')
76+
77+
const result = normalizeFilePaths(['src/App.tsx', 'src/utils/helper.ts'])
78+
79+
expect(result).toEqual(['src/App.tsx', 'src/utils/helper.ts'])
80+
})
81+
82+
it('returns paths unchanged when not in a git repo', () => {
83+
vi.mocked(execSync).mockImplementation(() => {
84+
throw new Error('fatal: not a git repository')
85+
})
86+
87+
const result = normalizeFilePaths(['src/App.tsx', 'src/utils/helper.ts'])
88+
89+
expect(result).toEqual(['src/App.tsx', 'src/utils/helper.ts'])
90+
})
91+
92+
it('handles empty file list', () => {
93+
vi.mocked(execSync).mockReturnValue('apps/frontend/\n')
94+
95+
const result = normalizeFilePaths([])
96+
97+
expect(result).toEqual([])
98+
})
99+
100+
it('converts absolute paths to cwd-relative paths', () => {
101+
vi.mocked(execSync).mockReturnValue('\n')
102+
const cwd = process.cwd()
103+
104+
const result = normalizeFilePaths([`${cwd}/src/App.tsx`, `${cwd}/src/utils/helper.ts`])
105+
106+
expect(result).toEqual(['src/App.tsx', 'src/utils/helper.ts'])
107+
})
108+
109+
it('handles absolute paths outside cwd with ../', () => {
110+
vi.mocked(execSync).mockReturnValue('\n')
111+
112+
const result = normalizeFilePaths(['/other/project/file.tsx'])
113+
114+
expect(result[0]).toMatch(/^\.\.\//)
115+
})
116+
117+
it('handles mixed absolute and relative paths', () => {
118+
vi.mocked(execSync).mockReturnValue('\n')
119+
const cwd = process.cwd()
120+
121+
const result = normalizeFilePaths([`${cwd}/src/App.tsx`, 'src/local.ts'])
122+
123+
expect(result).toEqual(['src/App.tsx', 'src/local.ts'])
124+
})
125+
})

0 commit comments

Comments
 (0)