From d5b3d2ce4927cdae0fe04cb1616251b37d57443f Mon Sep 17 00:00:00 2001 From: Casey Webb Date: Wed, 24 Jul 2019 15:35:46 -0500 Subject: [PATCH] feat: Add optional basePath argument for selective case-correction Closes upstream barsh/true-case-path#2, re gatsbyjs/gatsby#15876 --- .prettierrc | 5 +++ .vscode/launch.json | 14 ++++++ README.md | 10 +++++ index.d.ts | 7 ++- index.js | 99 +++++++++++++++++++++++++---------------- test/fixture/.gitignore | 1 + test/index.js | 48 ++++++++++++++++---- 7 files changed, 135 insertions(+), 49 deletions(-) create mode 100644 .prettierrc create mode 100644 .vscode/launch.json create mode 100644 test/fixture/.gitignore diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..cf9edfd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "arrowParens": "always", + "semi": false, + "singleQuote": true +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..bcbdb18 --- /dev/null +++ b/.vscode/launch.json @@ -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" + } + ] +} diff --git a/README.md b/README.md index 46f56c5..3d88b16 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,16 @@ const caseCorrectPath = trueCasePathSync() > **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 diff --git a/index.d.ts b/index.d.ts index c9902a3..4568987 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,3 +1,6 @@ -export function trueCasePath(filePath: string): Promise +export function trueCasePath( + filePath: string, + basePath?: string +): Promise -export function trueCasePathSync(filePath: string): string \ No newline at end of file +export function trueCasePathSync(filePath: string, basePath?: string): string diff --git a/index.js b/index.js index b1ce781..8ec2b0e 100644 --- a/index.js +++ b/index.js @@ -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 -} \ No newline at end of file +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 + ) +} diff --git a/test/fixture/.gitignore b/test/fixture/.gitignore new file mode 100644 index 0000000..0247178 --- /dev/null +++ b/test/fixture/.gitignore @@ -0,0 +1 @@ +home \ No newline at end of file diff --git a/test/index.js b/test/index.js index 22efd0d..10529f8 100644 --- a/test/index.js +++ b/test/index.js @@ -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() @@ -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!') @@ -40,4 +70,4 @@ Promise.all([ console.log('Test failed!') console.error(err) process.exitCode = 1 - }) \ No newline at end of file + })