Skip to content

Commit

Permalink
feat: Add optional basePath argument for selective case-correction
Browse files Browse the repository at this point in the history
Closes upstream barsh#2, re gatsbyjs/gatsby#15876
  • Loading branch information
caseyWebb committed Jul 24, 2019
1 parent 998dc2a commit d5b3d2c
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 49 deletions.
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"arrowParens": "always",
"semi": false,
"singleQuote": true
}
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Test",
"program": "${workspaceFolder}/test/index.js"
}
]
}
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ const caseCorrectPath = trueCasePathSync(<fileSystemPath>)

> **NOTE**: If no matching path exists, an error with be thrown.
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.

```typescript
const { trueCasePath } = require('true-case-path')

trueCasePath('code/my-app/sOmE-FiLe', '/home/casey')
```

> **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.
## Platforms

Windows, OSX, and Linux
Expand Down
7 changes: 5 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export function trueCasePath(filePath: string): Promise<string>
export function trueCasePath(
filePath: string,
basePath?: string
): Promise<string>

export function trueCasePathSync(filePath: string): string
export function trueCasePathSync(filePath: string, basePath?: string): string
99 changes: 61 additions & 38 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,84 @@
'use strict'

const fs = require('fs')
const os = require('os')
const path = require('path')
const { readdir: _readdir, readdirSync } = require('fs')
const { platform } = require('os')
const { isAbsolute } = require('path')
const { promisify: pify } = require('util')

const readdir = pify(fs.readdir)
const isWindows = os.platform() === 'win32'
const readdir = pify(_readdir)
const isWindows = platform() === 'win32'
const delimiter = isWindows ? '\\' : '/'

module.exports = {
trueCasePath,
trueCasePathSync
trueCasePath: _trueCasePath({ sync: false }),
trueCasePathSync: _trueCasePath({ sync: true })
}

function getFilePathSegments(filePath) {
return path.resolve(process.cwd(), filePath).split(delimiter).filter((s) => s !== '')
function getRelevantFilePathSegments(filePath) {
return filePath.split(delimiter).filter((s) => s !== '')
}

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

function isDriveLetter(str) {
return /[a-zA-Z]:/.test(str)
}

function matchCaseInsensitive(fileOrDirectory, directoryContents, filePath) {
const caseInsensitiveRegex = new RegExp(`^${escapeString(fileOrDirectory)}$`, 'i')
const caseInsensitiveRegex = new RegExp(
`^${escapeString(fileOrDirectory)}$`,
'i'
)
for (const file of directoryContents) {
if (caseInsensitiveRegex.test(file)) {
return file
}
if (caseInsensitiveRegex.test(file)) return file
}
throw new Error(`[true-case-path]: Called with ${filePath}, but no matching file exists`)
throw new Error(
`[true-case-path]: Called with ${filePath}, but no matching file exists`
)
}

async function trueCasePath(filePath) {
const segments = getFilePathSegments(filePath)
let realPath = ''
if (isWindows) {
realPath += segments.shift().toUpperCase() // drive letter
}
for (const fileOrDirectory of segments) {
const contents = await readdir(realPath + delimiter)
const realCaseFileOrDirectory = matchCaseInsensitive(fileOrDirectory, contents, filePath)
realPath += delimiter + realCaseFileOrDirectory
function _trueCasePath({ sync }) {
return (filePath, basePath) => {
if (basePath && !isAbsolute(basePath)) {
throw new Error(
`[true-case-path]: basePath argument must be absolute. Received "${basePath}"`
)
}
const segments = getRelevantFilePathSegments(filePath)
if (!basePath) basePath = isAbsolute(filePath) ? '' : process.cwd()
if (isDriveLetter(segments[0])) segments[0] = segments[0].toUpperCase()
return sync
? iterateSync(basePath, filePath, segments)
: iterateAsync(basePath, filePath, segments)
}
return realPath
}

function trueCasePathSync(filePath) {
const segments = getFilePathSegments(filePath)
let realPath = ''
if (isWindows) {
realPath += segments.shift().toUpperCase() // drive letter
}
for (const fileOrDirectory of segments) {
const contents = fs.readdirSync(realPath + delimiter)
const realCaseFileOrDirectory = matchCaseInsensitive(fileOrDirectory, contents, filePath)
realPath += delimiter + realCaseFileOrDirectory
}
return realPath
}
function iterateSync(basePath, filePath, segments) {
return segments.reduce(
(realPath, fileOrDirectory) =>
realPath +
delimiter +
matchCaseInsensitive(
fileOrDirectory,
readdirSync(realPath + delimiter),
filePath
),
basePath
)
}

async function iterateAsync(basePath, filePath, segments) {
return await segments.reduce(
async (realPathPromise, fileOrDirectory) =>
(await realPathPromise) +
delimiter +
matchCaseInsensitive(
fileOrDirectory,
await readdir((await realPathPromise) + delimiter),
filePath
),
basePath
)
}
1 change: 1 addition & 0 deletions test/fixture/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
home
48 changes: 39 additions & 9 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
'use strict'

const assert = require('assert')
const { exec: _exec } = require('child_process')
const { platform } = require('os')
const path = require('path')
const { promisify: pify } = require('util')

const {
trueCasePath,
trueCasePathSync
} = require('../')
const exec = pify(_exec)

const { trueCasePath, trueCasePathSync } = require('../')

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

function testAsync() {
return trueCasePath(requested).then((actual) => assert.equal(actual, expected, 'trueCasePath (async) works'))
return trueCasePath(requested).then((actual) =>
assert.equal(actual, expected, 'trueCasePath (async) works')
)
}

function testRelative() {
assert.equal(trueCasePathSync('test/fixture/fOoBaR/BAZ'), expected, 'works with relative paths')
assert.equal(
trueCasePathSync(path.relative(process.cwd(), requested)),
expected,
'works with relative paths'
)
}

function testSpecialChars() {
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')
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'
)
}

async function testSharedHostingWorkaround() {
await exec('mkdir -p fixture/home/casey', { cwd: __dirname })
await exec('touch fixture/home/casey/fOoBaR', { cwd: __dirname })
await exec('chmod 100 fixture/home', { cwd: __dirname })

assert.throws(() => trueCasePathSync('fixture/home/casey/foobar', __dirname))

assert.equal(
trueCasePathSync('foobar', path.join(__dirname, 'fixture/home/casey')),
path.join(__dirname, 'fixture/home/casey/fOoBaR')
)
assert.equal(
await trueCasePath('foobar', path.join(__dirname, 'fixture/home/casey')),
path.join(__dirname, 'fixture/home/casey/fOoBaR')
)
}

Promise.all([
testSync(),
testRelative(),
testAsync(),
testSpecialChars()
testSpecialChars(),
platform() === 'linux' ? testSharedHostingWorkaround() : Promise.resolve()
])
.then(() => {
console.log('All tests passed!')
Expand All @@ -40,4 +70,4 @@ Promise.all([
console.log('Test failed!')
console.error(err)
process.exitCode = 1
})
})

0 comments on commit d5b3d2c

Please sign in to comment.