Skip to content

Commit 9a14e92

Browse files
authored
fix: use config directory as cwd, when multiple configs present (#1091)
1 parent 3a897ff commit 9a14e92

File tree

3 files changed

+133
-6
lines changed

3 files changed

+133
-6
lines changed

lib/runAll.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ export const runAll = async (
8080
debugLog('Running all linter scripts...')
8181

8282
// Resolve relative CWD option
83-
cwd = cwd ? path.resolve(cwd) : process.cwd()
83+
const hasExplicitCwd = !!cwd
84+
cwd = hasExplicitCwd ? path.resolve(cwd) : process.cwd()
8485
debugLog('Using working directory `%s`', cwd)
8586

8687
const ctx = getInitialState({ quiet })
@@ -120,6 +121,8 @@ export const runAll = async (
120121

121122
const configGroups = await getConfigGroups({ configObject, configPath, cwd, files }, logger)
122123

124+
const hasMultipleConfigs = Object.keys(configGroups).length > 1
125+
123126
// lint-staged 10 will automatically add modifications to index
124127
// Warn user when their command includes `git add`
125128
let hasDeprecatedGitAdd = false
@@ -138,21 +141,25 @@ export const runAll = async (
138141
const matchedFiles = new Set()
139142

140143
for (const [configPath, { config, files }] of Object.entries(configGroups)) {
144+
const relativeConfig = normalize(path.relative(cwd, configPath))
141145
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
142146

147+
// Use actual cwd if it's specified, or there's only a single config file.
148+
// Otherwise use the directory of the config file for each config group,
149+
// to make sure tasks are separated from each other.
150+
const groupCwd = hasMultipleConfigs && !hasExplicitCwd ? path.dirname(configPath) : cwd
151+
143152
const chunkCount = stagedFileChunks.length
144153
if (chunkCount > 1) {
145154
debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount)
146155
}
147156

148157
for (const [index, files] of stagedFileChunks.entries()) {
149-
const relativeConfig = normalize(path.relative(cwd, configPath))
150-
151158
const chunkListrTasks = await Promise.all(
152-
generateTasks({ config, cwd, files, relative }).map((task) =>
159+
generateTasks({ config, cwd: groupCwd, files, relative }).map((task) =>
153160
makeCmdTasks({
154161
commands: task.commands,
155-
cwd,
162+
cwd: groupCwd,
156163
files: task.fileList,
157164
gitDir,
158165
renderer: listrOptions.renderer,
@@ -161,7 +168,14 @@ export const runAll = async (
161168
}).then((subTasks) => {
162169
// Add files from task to match set
163170
task.fileList.forEach((file) => {
164-
matchedFiles.add(file)
171+
// Make sure relative files are normalized to the
172+
// group cwd, because other there might be identical
173+
// relative filenames in the entire set.
174+
const normalizedFile = path.isAbsolute(file)
175+
? file
176+
: normalize(path.join(groupCwd, file))
177+
178+
matchedFiles.add(normalizedFile)
165179
})
166180

167181
hasDeprecatedGitAdd =

test/integration.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,44 @@ describe('lint-staged', () => {
11491149
expect(await readFile('a/very/deep/file/path/file.js')).toMatch('level-0')
11501150
})
11511151

1152+
it('should support multiple configuration files with --relative', async () => {
1153+
// Add some empty files
1154+
await writeFile('file.js', '')
1155+
await writeFile('deeper/file.js', '')
1156+
await writeFile('deeper/even/file.js', '')
1157+
await writeFile('deeper/even/deeper/file.js', '')
1158+
await writeFile('a/very/deep/file/path/file.js', '')
1159+
1160+
const echoJSConfig = `module.exports = { '*.js': (files) => files.map((f) => \`echo \${f} > \${f}\`) }`
1161+
1162+
await writeFile('.lintstagedrc.js', echoJSConfig)
1163+
await writeFile('deeper/.lintstagedrc.js', echoJSConfig)
1164+
await writeFile('deeper/even/.lintstagedrc.cjs', echoJSConfig)
1165+
1166+
// Stage all files
1167+
await execGit(['add', '.'])
1168+
1169+
// Run lint-staged with `--shell` so that tasks do their thing
1170+
await gitCommit({ relative: true, shell: true })
1171+
1172+
// 'file.js' is relative to '.'
1173+
expect(await readFile('file.js')).toMatch('file.js')
1174+
1175+
// 'deeper/file.js' is relative to 'deeper/'
1176+
expect(await readFile('deeper/file.js')).toMatch('file.js')
1177+
1178+
// 'deeper/even/file.js' is relative to 'deeper/even/'
1179+
expect(await readFile('deeper/even/file.js')).toMatch('file.js')
1180+
1181+
// 'deeper/even/deeper/file.js' is relative to parent 'deeper/even/'
1182+
expect(await readFile('deeper/even/deeper/file.js')).toMatch(normalize('deeper/file.js'))
1183+
1184+
// 'a/very/deep/file/path/file.js' is relative to root '.'
1185+
expect(await readFile('a/very/deep/file/path/file.js')).toMatch(
1186+
normalize('a/very/deep/file/path/file.js')
1187+
)
1188+
})
1189+
11521190
it('should not care about staged file outside current cwd with another staged file', async () => {
11531191
await writeFile('file.js', testJsFileUgly)
11541192
await writeFile('deeper/file.js', testJsFileUgly)

test/runAll.spec.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { GitWorkflow } from '../lib/gitWorkflow'
99
import { resolveGitRepo } from '../lib/resolveGitRepo'
1010
import { runAll } from '../lib/runAll'
1111
import { GitError } from '../lib/symbols'
12+
import * as getConfigGroupsNS from '../lib/getConfigGroups'
1213

1314
jest.mock('../lib/file')
1415
jest.mock('../lib/getStagedFiles')
@@ -26,6 +27,8 @@ jest.mock('../lib/resolveConfig', () => ({
2627
},
2728
}))
2829

30+
const getConfigGroups = jest.spyOn(getConfigGroupsNS, 'getConfigGroups')
31+
2932
getStagedFiles.mockImplementation(async () => [])
3033

3134
resolveGitRepo.mockImplementation(async () => {
@@ -293,4 +296,76 @@ describe('runAll', () => {
293296
expect(mockConstructor).toHaveBeenCalledTimes(1)
294297
expect(expected).toEqual([[normalize(path.join(cwd, 'test/foo.js'))]])
295298
})
299+
300+
it('should resolve matched files to config locations with multiple configs', async () => {
301+
getStagedFiles.mockImplementationOnce(async () => ['foo.js', 'test/foo.js'])
302+
303+
const mockTask = jest.fn(() => ['echo "sample"'])
304+
305+
getConfigGroups.mockResolvedValueOnce({
306+
'.lintstagedrc.json': {
307+
config: { '*.js': mockTask },
308+
files: ['foo.js'],
309+
},
310+
'test/.lintstagedrc.json': {
311+
config: { '*.js': mockTask },
312+
files: ['test/foo.js'],
313+
},
314+
})
315+
316+
// We are only interested in the `matchedFileChunks` generation
317+
let expected
318+
const mockConstructor = jest.fn(({ matchedFileChunks }) => (expected = matchedFileChunks))
319+
GitWorkflow.mockImplementationOnce(mockConstructor)
320+
321+
try {
322+
await runAll({
323+
stash: false,
324+
relative: true,
325+
})
326+
} catch {} // eslint-disable-line no-empty
327+
328+
// task received relative `foo.js` from both directories
329+
expect(mockTask).toHaveBeenCalledTimes(2)
330+
expect(mockTask).toHaveBeenNthCalledWith(1, ['foo.js'])
331+
expect(mockTask).toHaveBeenNthCalledWith(2, ['foo.js'])
332+
// GitWorkflow received absolute paths `foo.js` and `test/foo.js`
333+
expect(mockConstructor).toHaveBeenCalledTimes(1)
334+
expect(expected).toEqual([
335+
[
336+
normalize(path.join(process.cwd(), 'foo.js')),
337+
normalize(path.join(process.cwd(), 'test/foo.js')),
338+
],
339+
])
340+
})
341+
342+
it('should resolve matched files to explicit cwd with multiple configs', async () => {
343+
getStagedFiles.mockImplementationOnce(async () => ['foo.js', 'test/foo.js'])
344+
345+
const mockTask = jest.fn(() => ['echo "sample"'])
346+
347+
getConfigGroups.mockResolvedValueOnce({
348+
'.lintstagedrc.json': {
349+
config: { '*.js': mockTask },
350+
files: ['foo.js'],
351+
},
352+
'test/.lintstagedrc.json': {
353+
config: { '*.js': mockTask },
354+
files: ['test/foo.js'],
355+
},
356+
})
357+
358+
try {
359+
await runAll({
360+
cwd: '.',
361+
stash: false,
362+
relative: true,
363+
})
364+
} catch {} // eslint-disable-line no-empty
365+
366+
expect(mockTask).toHaveBeenCalledTimes(2)
367+
expect(mockTask).toHaveBeenNthCalledWith(1, ['foo.js'])
368+
// This is now relative to "." instead of "test/"
369+
expect(mockTask).toHaveBeenNthCalledWith(2, ['test/foo.js'])
370+
})
296371
})

0 commit comments

Comments
 (0)