Skip to content

Commit

Permalink
feat(lib): resolve
Browse files Browse the repository at this point in the history
Signed-off-by: Lexus Drumgold <[email protected]>
  • Loading branch information
unicornware committed Dec 12, 2022
1 parent 582e287 commit e3aeab3
Show file tree
Hide file tree
Showing 10 changed files with 425 additions and 79 deletions.
98 changes: 48 additions & 50 deletions src/internal/__tests__/normalize-string.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,69 +9,67 @@ describe('unit:internal/normalizeString', () => {
it('should return normalized string', () => {
// Arrange
const cases: [...Parameters<typeof testSubject>, string][] = [
['', ''],
[' ', ' '],
['../.../.././.../../../bar', '../../bar'],
['../.../../foobar/../../../bar/../../baz', '../../../../baz'],
['../../../foo/../../../bar', '../../../../../bar'],
['../../../foo/../../../bar/../../', '../../../../../..'],
['../foo../../../bar', '../../bar'],
['../foobar/barfoo/foo/../../../bar/../../', '../..'],
['./fixtures///b/../b/c.js', 'fixtures/b/c.js'],
['///..//./foo/.//bar', 'foo/bar'],
['/a/b/c/../../../x/y/z', 'x/y/z'],
['/foo/../../../bar', 'bar'],
['a//b//.', 'a/b'],
['a//b//../b', 'a/b'],
['a//b//./c', 'a/b/c'],
['bar/foo..', 'bar/foo..'],
['bar/foo../..', 'bar'],
['bar/foo../../', 'bar'],
['bar/foo../../baz', 'bar/baz'],
['foo', 'foo'],
['foo/bar/foo/bar/foo/../../../bar/../../', 'foo']
['', undefined, ''],
[' ', undefined, ' '],
['../.../.././.../../../bar', undefined, '../../bar'],
['../.../../foobar/../../../bar/../../baz', undefined, '../../../../baz'],
['../../../foo/../../../bar', undefined, '../../../../../bar'],
['../../../foo/../../../bar/../../', undefined, '../../../../../..'],
['../foo../../../bar', undefined, '../../bar'],
['../foobar/barfoo/foo/../../../bar/../../', undefined, '../..'],
['./fixtures///b/../b/c.js', undefined, 'fixtures/b/c.js'],
['///..//./foo/.//bar', undefined, 'foo/bar'],
['/a/b/c/../../../x/y/z', undefined, 'x/y/z'],
['/foo/../../../bar', undefined, 'bar'],
['a//b//.', undefined, 'a/b'],
['a//b//../b', undefined, 'a/b'],
['a//b//./c', undefined, 'a/b/c'],
['bar/foo..', undefined, 'bar/foo..'],
['bar/foo../..', undefined, 'bar'],
['bar/foo../../', undefined, 'bar'],
['bar/foo../../baz', undefined, 'bar/baz'],
['foo', undefined, 'foo'],
['foo/bar/foo/bar/foo/../../../bar/../../', undefined, 'foo']
]

// Act + Expect
cases.forEach(([path, expected]) => {
expect(testSubject(path)).to.equal(expected)
cases.forEach(([path, allow_above_root, expected]) => {
expect(testSubject(path, allow_above_root)).to.equal(expected)
})
})

describe('windows', () => {
it('should return normalized string', () => {
// Arrange
const cases: [...Parameters<typeof testSubject>, string][] = [
['', ''],
[' ', ' '],
['..\\...\\..\\.\\...\\..\\..\\bar', '../../bar'],
['..\\..\\..\\foo\\..\\..\\..\\bar', '../../../../../bar'],
['..\\..\\..\\foo\\..\\..\\..\\bar\\..\\..\\', '../../../../../..'],
['..\\foo..\\..\\..\\bar', '../../bar'],
['..\\foobar\\barfoo\\foo\\..\\..\\..\\bar\\..\\..\\', '../..'],
['.\\fixtures\\\\\\b\\..\\b\\c.js', 'fixtures/b/c.js'],
['C:', 'C:'],
['C:..\\..\\abc\\..\\def', 'def'],
['C:..\\abc', 'C:../abc'],
['C:\\.', 'C:'],
['\\\\server\\share\\dir\\file.ext', 'server/share/dir/file.ext'],
['\\a\\b\\c\\..\\..\\..\\x\\y\\z', 'x/y/z'],
['\\foo\\..\\..\\..\\bar', 'bar'],
['a\\\\b\\\\.', 'a/b'],
['a\\\\b\\\\..\\b', 'a/b'],
['a\\\\b\\\\.\\c', 'a/b/c'],
['bar\\foo..', 'bar/foo..'],
['bar\\foo..\\', 'bar/foo..'],
['bar\\foo..\\..', 'bar'],
['bar\\foo..\\..\\', 'bar'],
['bar\\foo..\\..\\baz', 'bar/baz'],
['file:stream', 'file:stream'],
['foo\\bar\\baz', 'foo/bar/baz']
['', undefined, ''],
[' ', undefined, ' '],
['..\\...\\..\\.\\...\\..\\..\\bar', undefined, '../../bar'],
['..\\..\\..\\foo\\..\\..\\..\\bar', undefined, '../../../../../bar'],
['..\\foo..\\..\\..\\bar', undefined, '../../bar'],
['.\\fixtures\\\\\\b\\..\\b\\c.js', undefined, 'fixtures/b/c.js'],
['C:', undefined, 'C:'],
['C:..\\..\\abc\\..\\def', undefined, 'def'],
['C:..\\abc', undefined, 'C:../abc'],
['C:\\.', undefined, 'C:'],
['\\\\host\\share\\dir\\foo.txt', undefined, 'host/share/dir/foo.txt'],
['\\a\\b\\c\\..\\..\\..\\x\\y\\z', undefined, 'x/y/z'],
['\\foo\\..\\..\\..\\bar', undefined, 'bar'],
['a\\\\b\\\\.', undefined, 'a/b'],
['a\\\\b\\\\..\\b', undefined, 'a/b'],
['a\\\\b\\\\.\\c', undefined, 'a/b/c'],
['bar\\foo..', undefined, 'bar/foo..'],
['bar\\foo..\\', undefined, 'bar/foo..'],
['bar\\foo..\\..', undefined, 'bar'],
['bar\\foo..\\..\\', undefined, 'bar'],
['bar\\foo..\\..\\baz', undefined, 'bar/baz'],
['file:stream', undefined, 'file:stream'],
['foo\\bar\\baz', undefined, 'foo/bar/baz']
]

// Act + Expect
cases.forEach(([path, expected]) => {
expect(testSubject(path)).to.equal(expected)
cases.forEach(([path, allow_above_root, expected]) => {
expect(testSubject(path, allow_above_root)).to.equal(expected)
})
})
})
Expand Down
12 changes: 0 additions & 12 deletions src/internal/__tests__/validate-string.spec-d.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/internal/__tests__/validate-string.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ describe('unit:internal/validateString', () => {
name = 'path'
})

it('should return true if value is typeof string', () => {
expect(testSubject(faker.datatype.string(13), name)).to.be.true
it('should return value if value is typeof string', () => {
expect(testSubject(faker.datatype.string(13), name)).to.be.a('string')
})

it('should throw if value is not typeof string', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/internal/is-drive-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import validateString from './validate-string'
*
* @param {string} path - Path to evaluate
* @return {boolean} `true` if path starts with [drive letter][1]
* @throws {TypeError} If `path` is not a string
*/
const isDrivePath = (path: string): boolean => {
return validateString(path, 'path') && DRIVE_PATH_REGEX.test(path)
validateString(path, 'path')
return DRIVE_PATH_REGEX.test(path)
}

export default isDrivePath
4 changes: 3 additions & 1 deletion src/internal/is-unc-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import validateString from './validate-string'
* @param {string} path - Path to evaluate
* @param {boolean} [exact=true] - Check for exactly two leading separators
* @return {boolean} `true` if path is UNC path
* @throws {TypeError} If `path` is not a string
*/
const isUncPath = (path: string, exact: boolean = true): boolean => {
validateString(path, 'path')
exact = exact ? /^[/\\]{2}[^/\\]/.test(path) : true
return validateString(path, 'path') && UNC_PATH_REGEX.test(path) && exact
return UNC_PATH_REGEX.test(path) && exact
}

export default isUncPath
17 changes: 7 additions & 10 deletions src/internal/normalize-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ import validateString from './validate-string'
* [1]: {@link ../lib/sep.ts}
*
* @param {string} path - Path to normalize
* @param {boolean} [allow_above_root=!isAbsolute(path)] - Normalize past root
* @return {string} Normalized `path`
* @throws {TypeError} If `path` is not a string
*/
const normalizeString = (path: string): string => {
const normalizeString = (
path: string,
allow_above_root: boolean = !isAbsolute(path)
): string => {
validateString(path, 'path')

// exit early if path is empty string
Expand All @@ -38,13 +42,6 @@ const normalizeString = (path: string): string => {
// exit early if path does not contain dot characters or separators
if (!path.includes(DOT) && !path.includes(sep)) return path

/**
* Absolute path check.
*
* @const {boolean} absolute
*/
const absolute: boolean = isAbsolute(path)

/**
* Current character in {@link path} being processed.
*
Expand Down Expand Up @@ -134,8 +131,8 @@ const normalizeString = (path: string): string => {
}
}

// resolve past root if path is not absolute
if (!absolute) {
// resolve past root
if (allow_above_root) {
res += `${!res ? '' : sep}${DOT.repeat(2)}`
seglen = 2
}
Expand Down
6 changes: 3 additions & 3 deletions src/internal/validate-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import ERR_INVALID_ARG_TYPE from './err-invalid-arg-type'
*
* @param {unknown} value - Possible string value
* @param {string} name - `value` label
* @return {boolean} `true` if `value` is a string
* @return {string} `value` if `value` is a string
* @throws {ERR_INVALID_ARG_TYPE}
*/
const validateString = (value: unknown, name: string): value is string => {
if (typeof value === 'string') return true
const validateString = (value: unknown, name: string): string => {
if (typeof value === 'string') return value
throw new ERR_INVALID_ARG_TYPE(name, 'string', value)
}

Expand Down
120 changes: 120 additions & 0 deletions src/lib/__tests__/resolve.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* @file Unit Tests - resolve
* @module pathe/lib/tests/unit/resolve
* @see https://github.com/nodejs/node/blob/main/test/parallel/test-path-resolve.js
*/

import { DOT } from '#src/internal/constants'
import sep from '#src/lib/sep'
import { posix, win32 } from 'node:path'
import testSubject from '../resolve'

describe('unit:lib/resolve', () => {
it('should return "." on process.cwd() failure', () => {
// Arrange
process.cwd = () => ''
assert.strictEqual(process.cwd(), '')

// Act + Expect
expect(testSubject()).to.equal(posix.resolve()).to.equal(DOT)
})

it('should return absolute path', () => {
// Arrange
const cases: Parameters<typeof testSubject>[] = [
[],
[''],
['', ''],
['/foo/bar', './baz', ''],
['/foo/bar', './baz'],
['/foo/bar', '/tmp/file/'],
['/foo/bar', DOT.repeat(2), DOT, './baz'],
['/foo/tmp.3/', '../tmp.3/cycles/root.js'],
['/some/dir', DOT, '/absolute/'],
['/var/lib', '../', 'file/'],
['/var/lib', '/../', 'file/'],
['a/b/c/', '../../..'],
['package.json'],
[DOT],
[import.meta.url],
[posix.sep],
[posix.sep, '/path'],
[posix.sep, '', '', '/path']
]

// Act + Expect
cases.forEach(paths => {
expect(testSubject(...paths)).to.equal(posix.resolve(...paths))
})
})

describe('windows', () => {
beforeAll(() => {
Object.assign(process.env, {
'=P:': 'P:' + process.cwd(),
'=Z:': 'A:' + process.cwd()
})
})

/**
* Converts Windows-style path separators (`\`) to POSIX (`/`).
*
* @param {string} path - Path to normalize
* @return {string} `path` normalized
*/
const ensurePosix = (path: string): string => path.replace(/\\/g, sep)

it('should return "." on process.cwd() failure', () => {
// Arrange
process.cwd = () => ''
assert.strictEqual(process.cwd(), '')

// Act + Expect
expect(testSubject()).to.equal(win32.resolve()).to.equal(DOT)
})

it('should return absolute path', () => {
// Arrange
const cases: Parameters<typeof testSubject>[] = [
[],
[''],
['', ''],
['C:'],
['C:\\'],
['C:\\Windows\\long\\path/mixed', '../..', '..\\..\\reports'],
['C:\\Windows\\path\\only', '..\\..\\reports'],
['C:\\foo\\bar', '.\\baz'],
['C:\\foo\\tmp.3\\', '..\\tmp.3\\cycles\\root.js'],
['P:'],
['P:\\'],
['Z:'],
['Z:\\'],
['\\\\host\\share\\dir\\file.txt'],
['\\\\server\\share', '..', 'relative\\'],
['\\foo\\bar', '', '\\tmp\\file\\'],
['\\foo\\bar', '.\\baz'],
['\\foo\\bar', '\\tmp\\file\\'],
['\\foo\\bar', DOT.repeat(2), DOT, '.\\baz'],
['c:\\', '\\\\'],
['c:\\', '\\\\\\some\\\\dir'],
['c:\\', '\\\\dir'],
['c:\\', '\\\\server\\\\share'],
['c:\\', '\\\\server\\share'],
['c:\\blah\\blah', 'd:\\games', 'c:..\\a'],
['c:\\ignore', 'c:\\some\\file'],
['c:\\ignore', 'd:\\a\\b\\c\\d', '\\e.exe'],
['d:', 'file.txt'],
['d:\\ignore', 'd:some\\dir\\\\'],
['foo', '\\\\host\\share\\dir\\file.txt'],
['package.json'],
[DOT],
[import.meta.url]
]

// Act + Expect
cases.forEach(p => {
expect(testSubject(...p)).to.equal(ensurePosix(win32.resolve(...p)))
})
})
})
})
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export { default as isAbsolute } from './is-absolute'
export { default as join } from './join'
export { default as normalize } from './normalize'
export { default as parse } from './parse'
export { default as resolve } from './resolve'
export { default as sep } from './sep'
Loading

0 comments on commit e3aeab3

Please sign in to comment.