diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index ddaedded65..9521825051 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -3,6 +3,7 @@ import {promises as fs} from 'fs' import * as os from 'os' import * as path from 'path' import * as io from '../src/io' +import * as ioUtil from '../src/io-util' describe('cp', () => { it('copies file with no flags', async () => { @@ -658,6 +659,16 @@ describe('mkdirP', () => { await io.rmRF(getTestTemp()) }) + it('fails when called with an empty path', async () => { + expect.assertions(1) + + try { + await io.mkdirP('') + } catch (err) { + expect(err.message).toEqual('a path argument must be provided') + } + }) + it('creates folder', async () => { const testPath = path.join(getTestTemp(), 'mkdirTest') await io.mkdirP(testPath) @@ -688,18 +699,6 @@ describe('mkdirP', () => { expect(worked).toBe(false) }) - it('fails if mkdirP with empty path', async () => { - let worked: boolean - try { - await io.mkdirP('') - worked = true - } catch (err) { - worked = false - } - - expect(worked).toBe(false) - }) - it('fails if mkdirP with conflicting file path', async () => { const testPath = path.join(getTestTemp(), 'mkdirP_conflicting_file_path') await io.mkdirP(getTestTemp()) @@ -807,14 +806,12 @@ describe('mkdirP', () => { '9', '10' ) - process.env['TEST_MKDIRP_FAILSAFE'] = '10' + + expect.assertions(1) + try { - await io.mkdirP(testPath) - throw new Error('directory should not have been created') + await ioUtil.mkdirP(testPath, 10) } catch (err) { - delete process.env['TEST_MKDIRP_FAILSAFE'] - - // ENOENT is expected, all other errors are not expect(err.code).toBe('ENOENT') } }) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index 6aaa622f47..d5d4e6777a 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -1,3 +1,4 @@ +import {ok} from 'assert' import * as fs from 'fs' import * as path from 'path' @@ -54,6 +55,52 @@ export function isRooted(p: string): boolean { return p.startsWith('/') } +/** + * Recursively create a directory at `fsPath`. + * + * This implementation is optimistic, meaning it attempts to create the full + * path first, and backs up the path stack from there. + * + * @param fsPath The path to create + * @param maxDepth The maximum recursion depth + * @param depth The current recursion depth + */ +export async function mkdirP( + fsPath: string, + maxDepth: number = 1000, + depth: number = 1 +): Promise { + ok(fsPath, 'a path argument must be provided') + + fsPath = path.resolve(fsPath) + + if (depth >= maxDepth) return mkdir(fsPath) + + try { + await mkdir(fsPath) + return + } catch (err) { + switch (err.code) { + case 'ENOENT': { + await mkdirP(path.dirname(fsPath), maxDepth, depth + 1) + await mkdir(fsPath) + return + } + default: { + let stats: fs.Stats + + try { + stats = await stat(fsPath) + } catch (err2) { + throw err + } + + if (!stats.isDirectory()) throw err + } + } + } +} + /** * Best effort attempt to determine whether a file exists and is executable. * @param filePath file path to check diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index b6425e5076..25811854f5 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -102,61 +102,7 @@ export async function rmRF(inputPath: string): Promise { * @returns Promise */ export async function mkdirP(fsPath: string): Promise { - if (!fsPath) { - throw new Error('Parameter p is required') - } - - // build a stack of directories to create - const stack: string[] = [] - let testDir: string = fsPath - - // eslint-disable-next-line no-constant-condition - while (true) { - // validate the loop is not out of control - if (stack.length >= (process.env['TEST_MKDIRP_FAILSAFE'] || 1000)) { - // let the framework throw - await ioUtil.mkdir(fsPath) - return - } - - let stats: fs.Stats - try { - stats = await ioUtil.stat(testDir) - } catch (err) { - if (err.code === 'ENOENT') { - // validate the directory is not the drive root - const parentDir = path.dirname(testDir) - if (testDir === parentDir) { - throw new Error( - `Unable to create directory '${fsPath}'. Root directory does not exist: '${testDir}'` - ) - } - - // push the dir and test the parent - stack.push(testDir) - testDir = parentDir - continue - } else { - throw err - } - } - - if (!stats.isDirectory()) { - throw new Error( - `Unable to create directory '${fsPath}'. Conflicting file exists: '${testDir}'` - ) - } - - // testDir exists - break - } - - // create each directory - let dir = stack.pop() - while (dir != null) { - await ioUtil.mkdir(dir) - dir = stack.pop() - } + await ioUtil.mkdirP(fsPath) } /**