Skip to content

Commit 44a4f6c

Browse files
committed
refactor: reimplement "normalize-path"
1 parent bc2d267 commit 44a4f6c

18 files changed

+180
-73
lines changed

lib/chunkFiles.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import path from 'node:path'
22

33
import debug from 'debug'
4-
import normalize from 'normalize-path'
4+
5+
import { normalizePath } from './normalizePath.js'
56

67
const debugLog = debug('lint-staged:chunkFiles')
78

@@ -35,7 +36,7 @@ const chunkArray = (arr, chunkCount) => {
3536
*/
3637
export const chunkFiles = ({ files, baseDir, maxArgLength = null, relative = false }) => {
3738
const normalizedFiles = files.map((file) =>
38-
normalize(relative || !baseDir ? file : path.resolve(baseDir, file))
39+
normalizePath(relative || !baseDir ? file : path.resolve(baseDir, file))
3940
)
4041

4142
if (!maxArgLength) {

lib/generateTasks.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import path from 'node:path'
22

33
import debug from 'debug'
44
import micromatch from 'micromatch'
5-
import normalize from 'normalize-path'
5+
6+
import { normalizePath } from './normalizePath.js'
67

78
const debugLog = debug('lint-staged:generateTasks')
89

@@ -19,7 +20,7 @@ const debugLog = debug('lint-staged:generateTasks')
1920
export const generateTasks = ({ config, cwd = process.cwd(), files, relative = false }) => {
2021
debugLog('Generating linter tasks')
2122

22-
const relativeFiles = files.map((file) => normalize(path.relative(cwd, file)))
23+
const relativeFiles = files.map((file) => normalizePath(path.relative(cwd, file)))
2324

2425
return Object.entries(config).map(([pattern, commands]) => {
2526
const isParentDirPattern = pattern.startsWith('../')
@@ -42,7 +43,7 @@ export const generateTasks = ({ config, cwd = process.cwd(), files, relative = f
4243
strictBrackets: true,
4344
})
4445

45-
const fileList = matches.map((file) => normalize(relative ? file : path.resolve(cwd, file)))
46+
const fileList = matches.map((file) => normalizePath(relative ? file : path.resolve(cwd, file)))
4647

4748
const task = { pattern, commands, fileList }
4849
debugLog('Generated task: \n%O', task)

lib/getStagedFiles.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import path from 'node:path'
22

3-
import normalize from 'normalize-path'
4-
53
import { execGit } from './execGit.js'
64
import { getDiffCommand } from './getDiffCommand.js'
5+
import { normalizePath } from './normalizePath.js'
76
import { parseGitZOutput } from './parseGitZOutput.js'
87

98
export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } = {}) => {
109
try {
1110
const lines = await execGit(getDiffCommand(diff, diffFilter), { cwd })
1211
if (!lines) return []
1312

14-
return parseGitZOutput(lines).map((file) => normalize(path.resolve(cwd, file)))
13+
return parseGitZOutput(lines).map((file) => normalizePath(path.resolve(cwd, file)))
1514
} catch {
1615
return null
1716
}

lib/normalizePath.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Reimplementation of "normalize-path"
3+
* @see https://github.com/jonschlinkert/normalize-path/blob/52c3a95ebebc2d98c1ad7606cbafa7e658656899/index.js
4+
*/
5+
6+
/*!
7+
* normalize-path <https://github.com/jonschlinkert/normalize-path>
8+
*
9+
* Copyright (c) 2014-2018, Jon Schlinkert.
10+
* Released under the MIT License.
11+
*/
12+
13+
import path from 'node:path'
14+
15+
/**
16+
* A file starting with \\?\
17+
* @see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
18+
*/
19+
const WIN32_FILE_NS = '\\\\?\\'
20+
21+
/**
22+
* A file starting with \\.\
23+
* @see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
24+
*/
25+
const WIN32_DEVICE_NS = '\\\\.\\'
26+
27+
/**
28+
* Normalize input file path to use POSIX separators
29+
* @param {String} input
30+
* @returns String
31+
*/
32+
export const normalizePath = (input) => {
33+
if (input === path.posix.sep || input === path.win32.sep) {
34+
return path.posix.sep
35+
}
36+
37+
let normalized = input.split(/[/\\]+/).join(path.posix.sep)
38+
39+
/** Handle win32 Namespaced paths by changing e.g. \\.\ to //./ */
40+
if (input.startsWith(WIN32_FILE_NS) || input.startsWith(WIN32_DEVICE_NS)) {
41+
normalized = normalized.replace(/^\/(\.|\?)/, '//$1')
42+
}
43+
44+
/** Remove trailing slash */
45+
if (normalized.endsWith(path.posix.sep)) {
46+
normalized = normalized.slice(0, -1)
47+
}
48+
49+
return normalized
50+
}

lib/resolveGitRepo.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import fs from 'node:fs/promises'
22
import path from 'node:path'
33

44
import debug from 'debug'
5-
import normalize from 'normalize-path'
65

76
import { execGit } from './execGit.js'
87
import { readFile } from './file.js'
8+
import { normalizePath } from './normalizePath.js'
99

1010
const debugLog = debug('lint-staged:resolveGitRepo')
1111

@@ -15,7 +15,7 @@ const debugLog = debug('lint-staged:resolveGitRepo')
1515
*/
1616
const resolveGitConfigDir = async (gitDir) => {
1717
// Get the real path in case it's a symlink
18-
const defaultDir = normalize(await fs.realpath(path.join(gitDir, '.git')))
18+
const defaultDir = normalizePath(await fs.realpath(path.join(gitDir, '.git')))
1919
const stats = await fs.lstat(defaultDir)
2020
// If .git is a directory, use it
2121
if (stats.isDirectory()) return defaultDir
@@ -33,10 +33,10 @@ export const determineGitDir = (cwd, relativeDir) => {
3333
}
3434
if (relativeDir) {
3535
// the current working dir is inside the git top-level directory
36-
return normalize(cwd.substring(0, cwd.lastIndexOf(relativeDir)))
36+
return normalizePath(cwd.substring(0, cwd.lastIndexOf(relativeDir)))
3737
} else {
3838
// the current working dir is the top-level git directory
39-
return normalize(cwd)
39+
return normalizePath(cwd)
4040
}
4141
}
4242

@@ -55,9 +55,9 @@ export const resolveGitRepo = async (cwd = process.cwd()) => {
5555

5656
// read the path of the current directory relative to the top-level directory
5757
// don't read the toplevel directly, it will lead to an posix conform path on non posix systems (cygwin)
58-
const gitRel = normalize(await execGit(['rev-parse', '--show-prefix'], { cwd }))
59-
const gitDir = determineGitDir(normalize(cwd), gitRel)
60-
const gitConfigDir = normalize(await resolveGitConfigDir(gitDir))
58+
const gitRel = normalizePath(await execGit(['rev-parse', '--show-prefix'], { cwd }))
59+
const gitDir = determineGitDir(normalizePath(cwd), gitRel)
60+
const gitConfigDir = normalizePath(await resolveGitConfigDir(gitDir))
6161

6262
debugLog('Resolved git directory to be `%s`', gitDir)
6363
debugLog('Resolved git config directory to be `%s`', gitConfigDir)

lib/runAll.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import path from 'node:path'
55
import chalk from 'chalk'
66
import debug from 'debug'
77
import { Listr } from 'listr2'
8-
import normalize from 'normalize-path'
98

109
import { chunkFiles } from './chunkFiles.js'
1110
import { execGit } from './execGit.js'
@@ -24,6 +23,7 @@ import {
2423
SKIPPED_GIT_ERROR,
2524
skippingBackup,
2625
} from './messages.js'
26+
import { normalizePath } from './normalizePath.js'
2727
import { resolveGitRepo } from './resolveGitRepo.js'
2828
import {
2929
applyModificationsSkipped,
@@ -160,7 +160,7 @@ export const runAll = async (
160160
const matchedFiles = new Set()
161161

162162
for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
163-
const configName = configPath ? normalize(path.relative(cwd, configPath)) : 'Config object'
163+
const configName = configPath ? normalizePath(path.relative(cwd, configPath)) : 'Config object'
164164

165165
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
166166

@@ -193,7 +193,7 @@ export const runAll = async (
193193
// relative filenames in the entire set.
194194
const normalizedFile = path.isAbsolute(file)
195195
? file
196-
: normalize(path.join(groupCwd, file))
196+
: normalizePath(path.join(groupCwd, file))
197197

198198
matchedFiles.add(normalizedFile)
199199
})

lib/searchConfigs.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import path from 'node:path'
44

55
import debug from 'debug'
6-
import normalize from 'normalize-path'
76

87
import { execGit } from './execGit.js'
98
import { loadConfig, searchPlaces } from './loadConfig.js'
9+
import { normalizePath } from './normalizePath.js'
1010
import { parseGitZOutput } from './parseGitZOutput.js'
1111
import { validateConfig } from './validateConfig.js'
1212

@@ -21,7 +21,7 @@ const numberOfLevels = (file) => file.split('/').length
2121

2222
const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 : 1)
2323

24-
const isInsideDirectory = (dir) => (file) => file.startsWith(normalize(dir))
24+
const isInsideDirectory = (dir) => (file) => file.startsWith(normalizePath(dir))
2525

2626
/**
2727
* Search all config files from the git repository, preferring those inside `cwd`.
@@ -68,7 +68,7 @@ export const searchConfigs = async (
6868

6969
/** Sort possible config files so that deepest is first */
7070
const possibleConfigFiles = [...cachedFiles, ...otherFiles]
71-
.map((file) => normalize(path.join(gitDir, file)))
71+
.map((file) => normalizePath(path.join(gitDir, file)))
7272
.filter(isInsideDirectory(cwd))
7373
.sort(sortDeepestParth)
7474

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
"lilconfig": "2.1.0",
4242
"listr2": "6.6.1",
4343
"micromatch": "4.0.5",
44-
"normalize-path": "3.0.0",
4544
"object-inspect": "1.12.3",
4645
"pidtree": "0.6.0",
4746
"string-argv": "0.3.2",

test/integration/__utils__/createTempDir.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises'
33
import os from 'node:os'
44
import path from 'node:path'
55

6-
import normalize from 'normalize-path'
6+
import { normalizePath } from '../../../lib/normalizePath.js'
77

88
/**
99
* Create temporary random directory and return its path
@@ -17,5 +17,5 @@ export const createTempDir = async () => {
1717
const tempDir = path.join(baseDir, 'lint-staged', crypto.randomUUID())
1818
await fs.mkdir(tempDir, { recursive: true })
1919

20-
return normalize(tempDir)
20+
return normalizePath(tempDir)
2121
}

test/integration/multiple-config-files.test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import './__mocks__/dynamicImport.js'
44
import path from 'node:path'
55

66
import { jest as jestGlobals } from '@jest/globals'
7-
import normalize from 'normalize-path'
7+
8+
import { normalizePath } from '../../lib/normalizePath.js'
89

910
import { withGitIntegration } from './__utils__/withGitIntegration.js'
1011

@@ -84,11 +85,11 @@ describe('lint-staged', () => {
8485
expect(await readFile('deeper/even/file.js')).toMatch('file.js')
8586

8687
// 'deeper/even/deeper/file.js' is relative to parent 'deeper/even/'
87-
expect(await readFile('deeper/even/deeper/file.js')).toMatch(normalize('deeper/file.js'))
88+
expect(await readFile('deeper/even/deeper/file.js')).toMatch(normalizePath('deeper/file.js'))
8889

8990
// 'a/very/deep/file/path/file.js' is relative to root '.'
9091
expect(await readFile('a/very/deep/file/path/file.js')).toMatch(
91-
normalize('a/very/deep/file/path/file.js')
92+
normalizePath('a/very/deep/file/path/file.js')
9293
)
9394
})
9495
)

test/unit/chunkFiles.spec.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import path from 'node:path'
22

3-
import normalize from 'normalize-path'
4-
53
import { chunkFiles } from '../../lib/chunkFiles.js'
4+
import { normalizePath } from '../../lib/normalizePath.js'
65

76
describe('chunkFiles', () => {
87
const files = ['example.js', 'foo.js', 'bar.js', 'foo/bar.js']
9-
const baseDir = normalize('/opt/git/example.git')
8+
const baseDir = normalizePath('/opt/git/example.git')
109

1110
it('should default to sane value', () => {
1211
const chunkedFiles = chunkFiles({ baseDir, files: ['foo.js'], relative: true })
@@ -20,7 +19,7 @@ describe('chunkFiles', () => {
2019

2120
it('should chunk too long argument string', () => {
2221
const chunkedFiles = chunkFiles({ baseDir, files, maxArgLength: 20, relative: false })
23-
expect(chunkedFiles).toEqual(files.map((file) => [normalize(path.resolve(baseDir, file))]))
22+
expect(chunkedFiles).toEqual(files.map((file) => [normalizePath(path.resolve(baseDir, file))]))
2423
})
2524

2625
it('should take into account relative setting', () => {
@@ -33,11 +32,11 @@ describe('chunkFiles', () => {
3332

3433
it('should resolve absolute paths by default', () => {
3534
const chunkedFiles = chunkFiles({ baseDir, files })
36-
expect(chunkedFiles).toEqual([files.map((file) => normalize(path.resolve(baseDir, file)))])
35+
expect(chunkedFiles).toEqual([files.map((file) => normalizePath(path.resolve(baseDir, file)))])
3736
})
3837

3938
it('should resolve absolute paths by default even when maxArgLength is set', () => {
4039
const chunkedFiles = chunkFiles({ baseDir, files, maxArgLength: 262144 })
41-
expect(chunkedFiles).toEqual([files.map((file) => normalize(path.resolve(baseDir, file)))])
40+
expect(chunkedFiles).toEqual([files.map((file) => normalizePath(path.resolve(baseDir, file)))])
4241
})
4342
})

0 commit comments

Comments
 (0)