Skip to content

Commit d5b3d2c

Browse files
committed
feat: Add optional basePath argument for selective case-correction
Closes upstream barsh#2, re gatsbyjs/gatsby#15876
1 parent 998dc2a commit d5b3d2c

File tree

7 files changed

+135
-49
lines changed

7 files changed

+135
-49
lines changed

.prettierrc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"arrowParens": "always",
3+
"semi": false,
4+
"singleQuote": true
5+
}

.vscode/launch.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "node",
9+
"request": "launch",
10+
"name": "Test",
11+
"program": "${workspaceFolder}/test/index.js"
12+
}
13+
]
14+
}

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ const caseCorrectPath = trueCasePathSync(<fileSystemPath>)
1919

2020
> **NOTE**: If no matching path exists, an error with be thrown.
2121
22+
Optionally takes a second argument to use as the base path to begin case-correction from. This can be particularly useful within shared hosting environments since true-case-path relies on the ability to list a directory's contents in order to check the case and attempting to list the contents of `/` or `/home` will generally result in a permissions error.
23+
24+
```typescript
25+
const { trueCasePath } = require('true-case-path')
26+
27+
trueCasePath('code/my-app/sOmE-FiLe', '/home/casey')
28+
```
29+
30+
> **NOTE**: When specifying a basePath, the first argument is expected to be the file path _relative to that basePath_. If the first argument is absolute, every path segment will be checked. basePath defaults to `process.cwd()` if not specified and the first argument is relative.
31+
2232
## Platforms
2333

2434
Windows, OSX, and Linux

index.d.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1-
export function trueCasePath(filePath: string): Promise<string>
1+
export function trueCasePath(
2+
filePath: string,
3+
basePath?: string
4+
): Promise<string>
25

3-
export function trueCasePathSync(filePath: string): string
6+
export function trueCasePathSync(filePath: string, basePath?: string): string

index.js

Lines changed: 61 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,84 @@
11
'use strict'
22

3-
const fs = require('fs')
4-
const os = require('os')
5-
const path = require('path')
3+
const { readdir: _readdir, readdirSync } = require('fs')
4+
const { platform } = require('os')
5+
const { isAbsolute } = require('path')
66
const { promisify: pify } = require('util')
77

8-
const readdir = pify(fs.readdir)
9-
const isWindows = os.platform() === 'win32'
8+
const readdir = pify(_readdir)
9+
const isWindows = platform() === 'win32'
1010
const delimiter = isWindows ? '\\' : '/'
1111

1212
module.exports = {
13-
trueCasePath,
14-
trueCasePathSync
13+
trueCasePath: _trueCasePath({ sync: false }),
14+
trueCasePathSync: _trueCasePath({ sync: true })
1515
}
1616

17-
function getFilePathSegments(filePath) {
18-
return path.resolve(process.cwd(), filePath).split(delimiter).filter((s) => s !== '')
17+
function getRelevantFilePathSegments(filePath) {
18+
return filePath.split(delimiter).filter((s) => s !== '')
1919
}
2020

2121
function escapeString(str) {
2222
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
2323
}
2424

25+
function isDriveLetter(str) {
26+
return /[a-zA-Z]:/.test(str)
27+
}
28+
2529
function matchCaseInsensitive(fileOrDirectory, directoryContents, filePath) {
26-
const caseInsensitiveRegex = new RegExp(`^${escapeString(fileOrDirectory)}$`, 'i')
30+
const caseInsensitiveRegex = new RegExp(
31+
`^${escapeString(fileOrDirectory)}$`,
32+
'i'
33+
)
2734
for (const file of directoryContents) {
28-
if (caseInsensitiveRegex.test(file)) {
29-
return file
30-
}
35+
if (caseInsensitiveRegex.test(file)) return file
3136
}
32-
throw new Error(`[true-case-path]: Called with ${filePath}, but no matching file exists`)
37+
throw new Error(
38+
`[true-case-path]: Called with ${filePath}, but no matching file exists`
39+
)
3340
}
3441

35-
async function trueCasePath(filePath) {
36-
const segments = getFilePathSegments(filePath)
37-
let realPath = ''
38-
if (isWindows) {
39-
realPath += segments.shift().toUpperCase() // drive letter
40-
}
41-
for (const fileOrDirectory of segments) {
42-
const contents = await readdir(realPath + delimiter)
43-
const realCaseFileOrDirectory = matchCaseInsensitive(fileOrDirectory, contents, filePath)
44-
realPath += delimiter + realCaseFileOrDirectory
42+
function _trueCasePath({ sync }) {
43+
return (filePath, basePath) => {
44+
if (basePath && !isAbsolute(basePath)) {
45+
throw new Error(
46+
`[true-case-path]: basePath argument must be absolute. Received "${basePath}"`
47+
)
48+
}
49+
const segments = getRelevantFilePathSegments(filePath)
50+
if (!basePath) basePath = isAbsolute(filePath) ? '' : process.cwd()
51+
if (isDriveLetter(segments[0])) segments[0] = segments[0].toUpperCase()
52+
return sync
53+
? iterateSync(basePath, filePath, segments)
54+
: iterateAsync(basePath, filePath, segments)
4555
}
46-
return realPath
4756
}
4857

49-
function trueCasePathSync(filePath) {
50-
const segments = getFilePathSegments(filePath)
51-
let realPath = ''
52-
if (isWindows) {
53-
realPath += segments.shift().toUpperCase() // drive letter
54-
}
55-
for (const fileOrDirectory of segments) {
56-
const contents = fs.readdirSync(realPath + delimiter)
57-
const realCaseFileOrDirectory = matchCaseInsensitive(fileOrDirectory, contents, filePath)
58-
realPath += delimiter + realCaseFileOrDirectory
59-
}
60-
return realPath
61-
}
58+
function iterateSync(basePath, filePath, segments) {
59+
return segments.reduce(
60+
(realPath, fileOrDirectory) =>
61+
realPath +
62+
delimiter +
63+
matchCaseInsensitive(
64+
fileOrDirectory,
65+
readdirSync(realPath + delimiter),
66+
filePath
67+
),
68+
basePath
69+
)
70+
}
71+
72+
async function iterateAsync(basePath, filePath, segments) {
73+
return await segments.reduce(
74+
async (realPathPromise, fileOrDirectory) =>
75+
(await realPathPromise) +
76+
delimiter +
77+
matchCaseInsensitive(
78+
fileOrDirectory,
79+
await readdir((await realPathPromise) + delimiter),
80+
filePath
81+
),
82+
basePath
83+
)
84+
}

test/fixture/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
home

test/index.js

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
'use strict'
22

33
const assert = require('assert')
4+
const { exec: _exec } = require('child_process')
5+
const { platform } = require('os')
46
const path = require('path')
7+
const { promisify: pify } = require('util')
58

6-
const {
7-
trueCasePath,
8-
trueCasePathSync
9-
} = require('../')
9+
const exec = pify(_exec)
10+
11+
const { trueCasePath, trueCasePathSync } = require('../')
1012

1113
const expected = path.join(__dirname, 'fixture/fOoBaR/BAZ')
1214
const requested = expected.toLowerCase()
@@ -16,22 +18,50 @@ function testSync() {
1618
}
1719

1820
function testAsync() {
19-
return trueCasePath(requested).then((actual) => assert.equal(actual, expected, 'trueCasePath (async) works'))
21+
return trueCasePath(requested).then((actual) =>
22+
assert.equal(actual, expected, 'trueCasePath (async) works')
23+
)
2024
}
2125

2226
function testRelative() {
23-
assert.equal(trueCasePathSync('test/fixture/fOoBaR/BAZ'), expected, 'works with relative paths')
27+
assert.equal(
28+
trueCasePathSync(path.relative(process.cwd(), requested)),
29+
expected,
30+
'works with relative paths'
31+
)
2432
}
2533

2634
function testSpecialChars() {
27-
assert.equal(trueCasePathSync('test/fixture/F[U&N%K)Y'), path.join(__dirname, 'fixture/f[u&n%k)y'), 'works with file names w/ special chars')
35+
assert.equal(
36+
trueCasePathSync('test/fixture/F[U&N%K)Y'),
37+
path.join(__dirname, 'fixture/f[u&n%k)y'),
38+
'works with file names w/ special chars'
39+
)
40+
}
41+
42+
async function testSharedHostingWorkaround() {
43+
await exec('mkdir -p fixture/home/casey', { cwd: __dirname })
44+
await exec('touch fixture/home/casey/fOoBaR', { cwd: __dirname })
45+
await exec('chmod 100 fixture/home', { cwd: __dirname })
46+
47+
assert.throws(() => trueCasePathSync('fixture/home/casey/foobar', __dirname))
48+
49+
assert.equal(
50+
trueCasePathSync('foobar', path.join(__dirname, 'fixture/home/casey')),
51+
path.join(__dirname, 'fixture/home/casey/fOoBaR')
52+
)
53+
assert.equal(
54+
await trueCasePath('foobar', path.join(__dirname, 'fixture/home/casey')),
55+
path.join(__dirname, 'fixture/home/casey/fOoBaR')
56+
)
2857
}
2958

3059
Promise.all([
3160
testSync(),
3261
testRelative(),
3362
testAsync(),
34-
testSpecialChars()
63+
testSpecialChars(),
64+
platform() === 'linux' ? testSharedHostingWorkaround() : Promise.resolve()
3565
])
3666
.then(() => {
3767
console.log('All tests passed!')
@@ -40,4 +70,4 @@ Promise.all([
4070
console.log('Test failed!')
4171
console.error(err)
4272
process.exitCode = 1
43-
})
73+
})

0 commit comments

Comments
 (0)