Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 6.2

- Handle `''`, `'..'` and `'.'` paths the same as Node's intended
behavior.
- Add support for `file:` URL objects and Buffers. (Glob patterns
still have to be normal `string` types.)

# 6.1

- Move to native `fs/promises` usage instead of promisifying
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ something was omitted from the removal via a `filter` option.
This first parameter is a path or array of paths. The second
argument is an options object.

If interpreted as `glob` patterns, then the paths must be
normal `string` values. If not glob pattern matching, then you
may also pass in `file:` URL objects, or `Buffer` objects, which
will be turned into string paths in the normal ways.

Options:

- `preserveRoot`: If set to boolean `false`, then allow the
Expand Down
49 changes: 28 additions & 21 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { glob, globSync } from 'glob'
import {
optArg,
optArgSync,
RimrafAsyncOptions,
RimrafSyncOptions,
} from './opt-arg.js'
import pathArg from './path-arg.js'
import { optArg, optArgSync } from './opt-arg.js'
import type { RimrafAsyncOptions, RimrafSyncOptions } from './opt-arg.js'
import { pathArg } from './path-arg.js'
import type { PathLike } from './path-arg.js'
import { rimrafManual, rimrafManualSync } from './rimraf-manual.js'
import {
rimrafMoveRemove,
Expand All @@ -15,23 +12,27 @@ import { rimrafNative, rimrafNativeSync } from './rimraf-native.js'
import { rimrafPosix, rimrafPosixSync } from './rimraf-posix.js'
import { rimrafWindows, rimrafWindowsSync } from './rimraf-windows.js'
import { useNative, useNativeSync } from './use-native.js'
import { isStrings } from './is-strings.js'

export {
assertRimrafOptions,
isRimrafOptions,
type RimrafAsyncOptions,
type RimrafOptions,
type RimrafSyncOptions,
export type { PathLike } from './path-arg.js'

export type {
RimrafAsyncOptions,
RimrafOptions,
RimrafSyncOptions,
} from './opt-arg.js'

const wrap =
(fn: (p: string, o: RimrafAsyncOptions) => Promise<boolean>) =>
async (
export { assertRimrafOptions, isRimrafOptions } from './opt-arg.js'

const wrap = (
fn: (p: string, o: RimrafAsyncOptions) => Promise<boolean>,
) => {
const rimraf = async (
path: string | string[],
opt?: RimrafAsyncOptions,
): Promise<boolean> => {
const options = optArg(opt)
if (options.glob) {
if (options.glob && isStrings(path)) {
path = await glob(path, options.glob)
}
if (Array.isArray(path)) {
Expand All @@ -42,12 +43,16 @@ const wrap =
return !!(await fn(pathArg(path, options), options))
}
}
return rimraf
}

const wrapSync =
(fn: (p: string, o: RimrafSyncOptions) => boolean) =>
(path: string | string[], opt?: RimrafSyncOptions): boolean => {
const wrapSync = (fn: (p: string, o: RimrafSyncOptions) => boolean) => {
const rimraf = (
path: PathLike | PathLike[],
opt?: RimrafSyncOptions,
): boolean => {
const options = optArgSync(opt)
if (options.glob) {
if (options.glob && isStrings(path)) {
path = globSync(path, options.glob)
}
if (Array.isArray(path)) {
Expand All @@ -58,6 +63,8 @@ const wrapSync =
return !!fn(pathArg(path, options), options)
}
}
return rimraf
}

export const nativeSync = wrapSync(rimrafNativeSync)
export const native = Object.assign(wrap(rimrafNative), {
Expand Down
14 changes: 14 additions & 0 deletions src/is-strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PathLike } from './path-arg.js'

export const isStrings = (
p: PathLike | PathLike[],
): p is string | string[] => {
if (typeof p === 'string') return true
if (!Array.isArray(p)) return false
for (const s of p) {
if (typeof s !== 'string') {
return false
}
}
return true
}
55 changes: 42 additions & 13 deletions src/path-arg.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
import { parse, resolve } from 'path'
import { parse, resolve, normalize } from 'path'
import { inspect } from 'util'
import { RimrafAsyncOptions } from './index.js'
import { fileURLToPath } from 'url'

const pathArg = (path: string, opt: RimrafAsyncOptions = {}) => {
const type = typeof path
if (type !== 'string') {
const dotPattern = /(?:^|\\|\/)\.\.?(?:$|\\|\/)/
const BufferToString = (b: ArrayBufferView) =>
Buffer.prototype.toString.call(b, 'utf8')

export type PathLike = string | URL | ArrayBufferLike | Buffer

export function pathArg(
path: PathLike,
opt: RimrafAsyncOptions = {},
): string {
if (ArrayBuffer.isView(path)) {
path = BufferToString(path)
} else if (path instanceof URL && path.protocol === 'file:') {
path = fileURLToPath(path)
}
if (typeof path !== 'string') {
const type = typeof path
const ctor = path && type === 'object' && path.constructor
const received =
ctor && ctor.name ? `an instance of ${ctor.name}`
path instanceof URL ? `"${path.protocol}" URL object`
: ctor && ctor.name ? `an instance of ${ctor.name}`
: type === 'object' ? inspect(path)
: `type ${type} ${path}`
const msg =
'The "path" argument must be of type string. ' +
'The "path" argument must be of type string, Buffer, or "file:" URL. ' +
`Received ${received}`
throw Object.assign(new TypeError(msg), {
path,
code: 'ERR_INVALID_ARG_TYPE',
})
}
if (dotPattern.test(path)) {
path = normalize(path)
}
if (path === '.') path = process.cwd()

if (/\0/.test(path)) {
// simulate same failure that node raises
Expand All @@ -28,10 +48,22 @@ const pathArg = (path: string, opt: RimrafAsyncOptions = {}) => {
})
}

path = resolve(path)
const { root } = parse(path)
if (path === '') {
throw Object.assign(
new Error("'ENOENT: no such file or directory, lstat ''"),
{
errno: -2,
code: 'ENOENT',
syscall: 'lstat',
path: '',
},
)
}

const rpath = resolve(path)
const { root } = parse(rpath)

if (path === root && opt.preserveRoot !== false) {
if (rpath === root && opt.preserveRoot !== false) {
const msg =
'refusing to remove root directory without preserveRoot:false'
throw Object.assign(new Error(msg), {
Expand All @@ -42,8 +74,7 @@ const pathArg = (path: string, opt: RimrafAsyncOptions = {}) => {

if (process.platform === 'win32') {
const badWinChars = /[*|"<>?:]/
const { root } = parse(path)
if (badWinChars.test(path.substring(root.length))) {
if (badWinChars.test(rpath.substring(root.length))) {
throw Object.assign(new Error('Illegal characters in path.'), {
path,
code: 'EINVAL',
Expand All @@ -53,5 +84,3 @@ const pathArg = (path: string, opt: RimrafAsyncOptions = {}) => {

return path
}

export default pathArg
2 changes: 2 additions & 0 deletions src/rimraf-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { RimrafAsyncOptions, RimrafSyncOptions } from './index.js'
import { promises, rmSync } from './fs.js'
const { rm } = promises

// NB: node will raise the "no rm cwd" error for us

export const rimrafNative = async (
path: string,
opt: RimrafAsyncOptions,
Expand Down
33 changes: 17 additions & 16 deletions test/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { statSync } from 'fs'
import { resolve } from 'path'
import t from 'tap'
import {
rimraf,
Expand All @@ -26,9 +25,11 @@ t.test('mocky unit tests to select the correct function', async t => {
return USE_NATIVE
},
},
'../dist/esm/path-arg.js': (path: string) => {
CALLS.push(['pathArg', path])
return path
'../dist/esm/path-arg.js': {
pathArg: (path: string) => {
CALLS.push(['pathArg', path])
return path
},
},
'../dist/esm/opt-arg.js': {
...OPTARG,
Expand Down Expand Up @@ -181,12 +182,12 @@ t.test('accept array of paths as first arg', async t => {
true,
)
t.same(ASYNC_CALLS, [
[resolve('a'), {}],
[resolve('b'), {}],
[resolve('c'), {}],
[resolve('i'), { x: 'ya' }],
[resolve('j'), { x: 'ya' }],
[resolve('k'), { x: 'ya' }],
['a', {}],
['b', {}],
['c', {}],
['i', { x: 'ya' }],
['j', { x: 'ya' }],
['k', { x: 'ya' }],
])

t.equal(rimrafSync(['x', 'y', 'z']), true)
Expand All @@ -197,12 +198,12 @@ t.test('accept array of paths as first arg', async t => {
true,
)
t.same(SYNC_CALLS, [
[resolve('x'), {}],
[resolve('y'), {}],
[resolve('z'), {}],
[resolve('m'), { cat: 'chai' }],
[resolve('n'), { cat: 'chai' }],
[resolve('o'), { cat: 'chai' }],
['x', {}],
['y', {}],
['z', {}],
['m', { cat: 'chai' }],
['n', { cat: 'chai' }],
['o', { cat: 'chai' }],
])
})

Expand Down
11 changes: 11 additions & 0 deletions test/is-strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import t from 'tap'

import { isStrings } from '../src/is-strings.js'

t.equal(isStrings('asdf'), true)
t.equal(isStrings('asdf'.split('')), true)
t.equal(isStrings([]), true)
//@ts-expect-error
t.equal(isStrings([{x:1}]), false)
//@ts-expect-error
t.equal(isStrings({x:1}), false)
41 changes: 29 additions & 12 deletions test/path-arg.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import * as PATH from 'path'
import t from 'tap'
import { pathToFileURL } from 'url'
import { inspect } from 'util'

for (const platform of ['win32', 'posix'] as const) {
t.test(platform, async t => {
t.intercept(process, 'platform', { value: platform })
const path = PATH[platform] || PATH
const { default: pathArg } = (await t.mockImport(
'../src/path-arg.js',
{
path,
},
)) as typeof import('../src/path-arg.js')
const sep = path.sep
const { pathArg } = (await t.mockImport('../src/path-arg.js', {
path,
})) as typeof import('../src/path-arg.js')

t.equal(pathArg('a/b/c'), path.resolve('a/b/c'))
t.equal(pathArg('a/b/c'), 'a/b/c')
t.throws(
() => pathArg('a\0b'),
Error('path must be a string without null bytes'),
Expand Down Expand Up @@ -42,23 +41,41 @@ for (const platform of ['win32', 'posix'] as const) {
t.throws(() => pathArg('/', { preserveRoot: undefined }), {
code: 'ERR_PRESERVE_ROOT',
})
t.equal(pathArg('/', { preserveRoot: false }), path.resolve('/'))
t.equal(pathArg('/', { preserveRoot: false }), '/')

//@ts-expect-error
t.throws(() => pathArg({}), {
code: 'ERR_INVALID_ARG_TYPE',
path: {},
message:
'The "path" argument must be of type string. ' +
'The "path" argument must be of type string, Buffer, or "file:" URL. ' +
'Received an instance of Object',
name: 'TypeError',
})
t.equal(pathArg(pathToFileURL(process.cwd())), process.cwd())
t.equal(pathArg('.'), process.cwd())
t.equal(pathArg(Buffer.from('a/b/c/../.')), `a${sep}b`)
t.throws(() => pathArg(''), {
message: "'ENOENT: no such file or directory, lstat ''",
errno: -2,
code: 'ENOENT',
syscall: 'lstat',
path: '',
})
t.throws(() => pathArg(new URL('https://example.com/')), {
code: 'ERR_INVALID_ARG_TYPE',
path: {},
message:
'The "path" argument must be of type string, Buffer, or "file:" URL. ' +
`Received "https:" URL`,
name: 'TypeError',
})
//@ts-expect-error
t.throws(() => pathArg([]), {
code: 'ERR_INVALID_ARG_TYPE',
path: [],
message:
'The "path" argument must be of type string. ' +
'The "path" argument must be of type string, Buffer, or "file:" URL. ' +
'Received an instance of Array',
name: 'TypeError',
})
Expand All @@ -67,7 +84,7 @@ for (const platform of ['win32', 'posix'] as const) {
code: 'ERR_INVALID_ARG_TYPE',
path: Object.create(null) as object,
message:
'The "path" argument must be of type string. ' +
'The "path" argument must be of type string, Buffer, or "file:" URL. ' +
`Received ${inspect(Object.create(null))}`,
name: 'TypeError',
})
Expand All @@ -76,7 +93,7 @@ for (const platform of ['win32', 'posix'] as const) {
code: 'ERR_INVALID_ARG_TYPE',
path: true,
message:
'The "path" argument must be of type string. ' +
'The "path" argument must be of type string, Buffer, or "file:" URL. ' +
`Received type boolean true`,
name: 'TypeError',
})
Expand Down
Loading