From 47cd43c74480ad5ca5c94929edc1e27eb8d03fb2 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Fri, 17 May 2019 16:22:59 -0400 Subject: [PATCH 01/27] Add io lib --- packages/io/README.md | 49 ++ packages/io/__tests__/lib.test.ts | 1268 +++++++++++++++++++++++++++++ packages/io/package.json | 34 + packages/io/src/interfaces.ts | 9 + packages/io/src/internal.ts | 147 ++++ packages/io/src/lib.ts | 401 +++++++++ packages/io/tsconfig.json | 11 + 7 files changed, 1919 insertions(+) create mode 100644 packages/io/README.md create mode 100644 packages/io/__tests__/lib.test.ts create mode 100644 packages/io/package.json create mode 100644 packages/io/src/interfaces.ts create mode 100644 packages/io/src/internal.ts create mode 100644 packages/io/src/lib.ts create mode 100644 packages/io/tsconfig.json diff --git a/packages/io/README.md b/packages/io/README.md new file mode 100644 index 0000000000..e9b50d2f29 --- /dev/null +++ b/packages/io/README.md @@ -0,0 +1,49 @@ +# `@actions/io` + +> Core functions for cli filesystem scenarios + +## Usage + +``` +/** + * Copies a file or folder. + * + * @param source source path + * @param dest destination path + * @param options optional. See CopyOptions. + */ +export function cp(source: string, dest: string, options?: CopyOptions): Promise + +/** + * Remove a path recursively with force + * + * @param path path to remove + */ +export function rmRF(path: string): Promise + +/** + * Make a directory. Creates the full path with folders in between + * + * @param p path to create + * @returns Promise + */ +export function mkdirP(p: string): Promise + +/** + * Moves a path. + * + * @param source source path + * @param dest destination path + * @param options optional. See CopyOptions. + */ +export function mv(source: string, dest: string, options?: CopyOptions): Promise + +/** + * Returns path of a tool had the tool actually been invoked. Resolves via paths. + * + * @param tool name of the tool + * @param options optional. See WhichOptions. + * @returns Promise path to tool + */ +export function which(tool: string, options?: WhichOptions): Promise +``` \ No newline at end of file diff --git a/packages/io/__tests__/lib.test.ts b/packages/io/__tests__/lib.test.ts new file mode 100644 index 0000000000..b92f6582e5 --- /dev/null +++ b/packages/io/__tests__/lib.test.ts @@ -0,0 +1,1268 @@ +import child = require('child_process'); +import fs = require('fs'); +import path = require('path'); +import os = require('os'); + +import io = require('../src/lib'); + +describe('cp', () => { + it('copies file with no flags', async () => { + let root: string = path.join(getTestTemp(), 'cp_with_no_flags'); + let sourceFile: string = path.join(root, 'cp_source'); + let targetFile: string = path.join(root, 'cp_target'); + await io.mkdirP(root); + fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); + + await io.cp(sourceFile, targetFile); + + expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); + }); + + it('copies file using -f', async () => { + let root: string = path.join(path.join(__dirname, '_temp'), 'cp_with_-f'); + let sourceFile: string = path.join(root, 'cp_source'); + let targetFile: string = path.join(root, 'cp_target'); + await io.mkdirP(root); + fs.writeFileSync(sourceFile, 'test file content'); + + await io.cp(sourceFile, targetFile, {recursive: false, force: true}); + + expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); + }); + + it('try copying to existing file with -n', async () => { + let root: string = path.join(getTestTemp(), 'cp_to_existing'); + let sourceFile: string = path.join(root, 'cp_source'); + let targetFile: string = path.join(root, 'cp_target'); + await io.mkdirP(root); + fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); + fs.writeFileSync(targetFile, 'correct content', { encoding: 'utf8' }); + let failed = false + try { + await io.cp(sourceFile, targetFile, {recursive: false, force: false}); + } + catch { + failed = true; + } + expect(failed).toBe(true); + + expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('correct content'); + }); + + it('copies directory into existing destination with -r', async () => { + let root: string = path.join(getTestTemp(), 'cp_with_-r_existing_dest'); + let sourceFolder: string = path.join(root, 'cp_source'); + let sourceFile: string = path.join(sourceFolder, 'cp_source_file'); + + let targetFolder: string = path.join(root, 'cp_target'); + let targetFile: string = path.join(targetFolder, 'cp_source', 'cp_source_file'); + await io.mkdirP(sourceFolder); + fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); + await io.mkdirP(targetFolder); + await io.cp(sourceFolder, targetFolder, {recursive: true}); + + expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); + }); + + it('copies directory into non-existing destination with -r', async () => { + let root: string = path.join(getTestTemp(), 'cp_with_-r_nonexisting_dest'); + let sourceFolder: string = path.join(root, 'cp_source'); + let sourceFile: string = path.join(sourceFolder, 'cp_source_file'); + + let targetFolder: string = path.join(root, 'cp_target'); + let targetFile: string = path.join(targetFolder, 'cp_source_file'); + await io.mkdirP(sourceFolder); + fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); + await io.cp(sourceFolder, targetFolder, {recursive: true}); + + expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); + }); + + it('tries to copy directory without -r', async () => { + let root: string = path.join(getTestTemp(), 'cp_without_-r'); + let sourceFolder: string = path.join(root, 'cp_source'); + let sourceFile: string = path.join(sourceFolder, 'cp_source_file'); + + let targetFolder: string = path.join(root, 'cp_target'); + let targetFile: string = path.join(targetFolder, 'cp_source', 'cp_source_file'); + await io.mkdirP(sourceFolder); + fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); + + let thrown = false; + try { + await io.cp(sourceFolder, targetFolder); + } + catch (err) { + thrown = true; + } + expect(thrown).toBe(true); + expect(fs.existsSync(targetFile)).toBe(false); + }); +}); + +describe('mv', () => { + it('moves file with no flags', async () => { + let root: string = path.join(getTestTemp(), ' mv_with_no_flags'); + let sourceFile: string = path.join(root, ' mv_source'); + let targetFile: string = path.join(root, ' mv_target'); + await io.mkdirP(root); + fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); + + await io.mv(sourceFile, targetFile); + + expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); + expect(fs.existsSync(sourceFile)).toBe(false); + }); + + it('moves file using -f', async () => { + let root: string = path.join(path.join(__dirname, '_temp'), ' mv_with_-f'); + let sourceFile: string = path.join(root, ' mv_source'); + let targetFile: string = path.join(root, ' mv_target'); + await io.mkdirP(root); + fs.writeFileSync(sourceFile, 'test file content'); + + await io.mv(sourceFile, targetFile); + + expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); + expect(fs.existsSync(sourceFile)).toBe(false); + }); + + it('try moving to existing file with -n', async () => { + let root: string = path.join(getTestTemp(), ' mv_to_existing'); + let sourceFile: string = path.join(root, ' mv_source'); + let targetFile: string = path.join(root, ' mv_target'); + await io.mkdirP(root); + fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); + fs.writeFileSync(targetFile, 'correct content', { encoding: 'utf8' }); + let failed = false + try { + await io.mv(sourceFile, targetFile, {force: false}); + } + catch { + failed = true; + } + expect(failed).toBe(true); + + expect(fs.readFileSync(sourceFile, { encoding: 'utf8' })).toBe('test file content'); + expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('correct content'); + }); + + it('moves directory into existing destination with -r', async () => { + let root: string = path.join(getTestTemp(), ' mv_with_-r_existing_dest'); + let sourceFolder: string = path.join(root, ' mv_source'); + let sourceFile: string = path.join(sourceFolder, ' mv_source_file'); + + let targetFolder: string = path.join(root, ' mv_target'); + let targetFile: string = path.join(targetFolder, ' mv_source', ' mv_source_file'); + await io.mkdirP(sourceFolder); + fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); + await io.mkdirP(targetFolder); + await io.mv(sourceFolder, targetFolder, {recursive: true}); + + expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); + expect(fs.existsSync(sourceFile)).toBe(false); + }); + + it('moves directory into non-existing destination with -r', async () => { + let root: string = path.join(getTestTemp(), ' mv_with_-r_nonexisting_dest'); + let sourceFolder: string = path.join(root, ' mv_source'); + let sourceFile: string = path.join(sourceFolder, ' mv_source_file'); + + let targetFolder: string = path.join(root, ' mv_target'); + let targetFile: string = path.join(targetFolder, ' mv_source_file'); + await io.mkdirP(sourceFolder); + fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); + await io.mv(sourceFolder, targetFolder, {recursive: true}); + + expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); + expect(fs.existsSync(sourceFile)).toBe(false); + }); + + it('tries to move directory without -r', async () => { + let root: string = path.join(getTestTemp(), 'mv_without_-r'); + let sourceFolder: string = path.join(root, 'mv_source'); + let sourceFile: string = path.join(sourceFolder, 'mv_source_file'); + + let targetFolder: string = path.join(root, 'mv_target'); + let targetFile: string = path.join(targetFolder, 'mv_source', 'mv_source_file'); + await io.mkdirP(sourceFolder); + fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); + + let thrown = false; + try { + await io.mv(sourceFolder, targetFolder); + } + catch (err) { + thrown = true; + } + + expect(thrown).toBe(true); + expect(fs.existsSync(sourceFile)).toBe(true); + expect(fs.existsSync(targetFile)).toBe(false); + }); +}); + +describe('rmRF', () => { + it('removes single folder with rmRF', async () => { + var testPath = path.join(getTestTemp(), 'testFolder'); + + await io.mkdirP(testPath); + expect(fs.existsSync(testPath)).toBe(true); + + await io.rmRF(testPath); + expect(fs.existsSync(testPath)).toBe(false); + }); + + it('removes recursive folders with rmRF', async () => { + var testPath = path.join(getTestTemp(), 'testDir1'); + var testPath2 = path.join(testPath, 'testDir2'); + await io.mkdirP(testPath2); + + expect(fs.existsSync(testPath)).toBe(true); + expect(fs.existsSync(testPath2)).toBe(true); + + await io.rmRF(testPath); + expect(fs.existsSync(testPath)).toBe(false); + expect(fs.existsSync(testPath2)).toBe(false); + }); + + it('removes folder with locked file with rmRF', async () => { + var testPath = path.join(getTestTemp(), 'testFolder'); + await io.mkdirP(testPath); + expect(fs.existsSync(testPath)).toBe(true); + + // can't remove folder with locked file on windows + var filePath = path.join(testPath, 'file.txt'); + fs.appendFileSync(filePath, 'some data'); + expect(fs.existsSync(filePath)).toBe(true); + + var fd = fs.openSync(filePath, 'r'); + + var worked = false; + try { + await io.rmRF(testPath); + worked = true; + } + catch (err) { } + + if (os.platform() === 'win32') { + expect(worked).toBe(false); + expect(fs.existsSync(testPath)).toBe(true); + } + else { + expect(worked).toBe(true); + expect(fs.existsSync(testPath)).toBe(false); + } + + fs.closeSync(fd); + await io.rmRF(testPath); + expect(fs.existsSync(testPath)).toBe(false); + }); + + it('removes folder that doesnt exist with rmRF', async () => { + var testPath = path.join(getTestTemp(), 'testFolder'); + expect(fs.existsSync(testPath)).toBe(false); + + await io.rmRF(testPath); + expect(fs.existsSync(testPath)).toBe(false); + }); + + it('removes file with rmRF', async () => { + let file: string = path.join(getTestTemp(), 'rmRF_file'); + fs.writeFileSync(file, 'test file content'); + expect(fs.existsSync(file)).toBe(true); + await io.rmRF(file); + expect(fs.existsSync(file)).toBe(false); + }); + + it('removes hidden folder with rmRF', async () => { + let directory: string = path.join(getTestTemp(), '.rmRF_directory'); + await createHiddenDirectory(directory); + expect(fs.existsSync(directory)).toBe(true); + await io.rmRF(directory); + expect(fs.existsSync(directory)).toBe(false); + }); + + it('removes hidden file with rmRF', async () => { + let file: string = path.join(getTestTemp(), '.rmRF_file'); + fs.writeFileSync(file, 'test file content'); + expect(fs.existsSync(file)).toBe(true); + await io.rmRF(file); + expect(fs.existsSync(file)).toBe(false); + }); + + it('removes symlink folder with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // symlink_directory -> real_directory + let root: string = path.join(getTestTemp(), 'rmRF_sym_dir_test'); + let realDirectory: string = path.join(root, 'real_directory'); + let realFile: string = path.join(root, 'real_directory', 'real_file'); + let symlinkDirectory: string = path.join(root, 'symlink_directory'); + await io.mkdirP(realDirectory); + fs.writeFileSync(realFile, 'test file content'); + createSymlinkDir(realDirectory, symlinkDirectory); + expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true); + + await io.rmRF(symlinkDirectory); + expect(fs.existsSync(realDirectory)).toBe(true); + expect(fs.existsSync(realFile)).toBe(true); + expect(fs.existsSync(symlinkDirectory)).toBe(false); + }); + + // creating a symlink to a file on Windows requires elevated + if (os.platform() != 'win32') { + it('removes symlink file with rmRF', async () => { + // create the following layout: + // real_file + // symlink_file -> real_file + let root: string = path.join(getTestTemp(), 'rmRF_sym_file_test'); + let realFile: string = path.join(root, 'real_file'); + let symlinkFile: string = path.join(root, 'symlink_file'); + await io.mkdirP(root); + fs.writeFileSync(realFile, 'test file content'); + fs.symlinkSync(realFile, symlinkFile); + expect(fs.readFileSync(symlinkFile, { encoding: 'utf8' })).toBe('test file content'); + + await io.rmRF(symlinkFile); + expect(fs.existsSync(realFile)).toBe(true); + expect(fs.existsSync(symlinkFile)).toBe(false); + }); + + it('removes symlink file with missing source using rmRF', async () => { + // create the following layout: + // real_file + // symlink_file -> real_file + let root: string = path.join(getTestTemp(), 'rmRF_sym_file_missing_source_test'); + let realFile: string = path.join(root, 'real_file'); + let symlinkFile: string = path.join(root, 'symlink_file'); + await io.mkdirP(root); + fs.writeFileSync(realFile, 'test file content'); + fs.symlinkSync(realFile, symlinkFile); + expect(fs.readFileSync(symlinkFile, { encoding: 'utf8' })).toBe('test file content'); + + // remove the real file + fs.unlinkSync(realFile); + expect(fs.lstatSync(symlinkFile).isSymbolicLink()).toBe(true); + + // remove the symlink file + await io.rmRF(symlinkFile); + let errcode: string = ''; + try { + fs.lstatSync(symlinkFile); + } + catch (err) { + errcode = err.code; + } + + expect(errcode).toBe('ENOENT'); + }); + + it('removes symlink level 2 file with rmRF', async () => { + // create the following layout: + // real_file + // symlink_file -> real_file + // symlink_level_2_file -> symlink_file + let root: string = path.join(getTestTemp(), 'rmRF_sym_level_2_file_test'); + let realFile: string = path.join(root, 'real_file'); + let symlinkFile: string = path.join(root, 'symlink_file'); + let symlinkLevel2File: string = path.join(root, 'symlink_level_2_file'); + await io.mkdirP(root); + fs.writeFileSync(realFile, 'test file content'); + fs.symlinkSync(realFile, symlinkFile); + fs.symlinkSync(symlinkFile, symlinkLevel2File); + expect(fs.readFileSync(symlinkLevel2File, { encoding: 'utf8' })).toBe('test file content'); + + await io.rmRF(symlinkLevel2File); + expect(fs.existsSync(realFile)).toBe(true); + expect(fs.existsSync(symlinkFile)).toBe(true); + expect(fs.existsSync(symlinkLevel2File)).toBe(false); + }); + + it('removes nested symlink file with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // outer_directory + // outer_directory/symlink_file -> real_file + let root: string = path.join(getTestTemp(), 'rmRF_sym_nest_file_test'); + let realDirectory: string = path.join(root, 'real_directory'); + let realFile: string = path.join(root, 'real_directory', 'real_file'); + let outerDirectory: string = path.join(root, 'outer_directory'); + let symlinkFile: string = path.join(root, 'outer_directory', 'symlink_file'); + await io.mkdirP(realDirectory); + fs.writeFileSync(realFile, 'test file content'); + await io.mkdirP(outerDirectory); + fs.symlinkSync(realFile, symlinkFile); + expect(fs.readFileSync(symlinkFile, { encoding: 'utf8' })).toBe('test file content'); + + await io.rmRF(outerDirectory); + expect(fs.existsSync(realDirectory)).toBe(true); + expect(fs.existsSync(realFile)).toBe(true); + expect(fs.existsSync(symlinkFile)).toBe(false); + expect(fs.existsSync(outerDirectory)).toBe(false); + }); + + it('removes deeply nested symlink file with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // outer_directory + // outer_directory/nested_directory + // outer_directory/nested_directory/symlink_file -> real_file + let root: string = path.join(getTestTemp(), 'rmRF_sym_deep_nest_file_test'); + let realDirectory: string = path.join(root, 'real_directory'); + let realFile: string = path.join(root, 'real_directory', 'real_file'); + let outerDirectory: string = path.join(root, 'outer_directory'); + let nestedDirectory: string = path.join(root, 'outer_directory', 'nested_directory'); + let symlinkFile: string = path.join(root, 'outer_directory', 'nested_directory', 'symlink_file'); + await io.mkdirP(realDirectory); + fs.writeFileSync(realFile, 'test file content'); + await io.mkdirP(nestedDirectory); + fs.symlinkSync(realFile, symlinkFile); + expect(fs.readFileSync(symlinkFile, { encoding: 'utf8' })).toBe('test file content'); + + await io.rmRF(outerDirectory); + expect(fs.existsSync(realDirectory)).toBe(true); + expect(fs.existsSync(realFile)).toBe(true); + expect(fs.existsSync(symlinkFile)).toBe(false); + expect(fs.existsSync(outerDirectory)).toBe(false); + }); + } + + it('removes symlink folder with missing source using rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // symlink_directory -> real_directory + let root: string = path.join(getTestTemp(), 'rmRF_sym_dir_miss_src_test'); + let realDirectory: string = path.join(root, 'real_directory'); + let realFile: string = path.join(root, 'real_directory', 'real_file'); + let symlinkDirectory: string = path.join(root, 'symlink_directory'); + await io.mkdirP(realDirectory); + fs.writeFileSync(realFile, 'test file content'); + createSymlinkDir(realDirectory, symlinkDirectory); + expect(fs.existsSync(symlinkDirectory)).toBe(true); + + // remove the real directory + fs.unlinkSync(realFile); + fs.rmdirSync(realDirectory); + let errcode: string = ''; + try { + fs.statSync(symlinkDirectory); + } + catch (err){ + errcode = err.code; + } + + expect(errcode).toBe('ENOENT'); + + // lstat shouldn't throw + fs.lstatSync(symlinkDirectory); + + // remove the symlink directory + await io.rmRF(symlinkDirectory); + errcode = ''; + try { + fs.lstatSync(symlinkDirectory); + } + catch (err) { + errcode = err.code; + } + + expect(errcode).toBe('ENOENT'); + }); + + it('removes symlink level 2 folder with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // symlink_directory -> real_directory + // symlink_level_2_directory -> symlink_directory + let root: string = path.join(getTestTemp(), 'rmRF_sym_level_2_directory_test'); + let realDirectory: string = path.join(root, 'real_directory'); + let realFile: string = path.join(realDirectory, 'real_file'); + let symlinkDirectory: string = path.join(root, 'symlink_directory'); + let symlinkLevel2Directory: string = path.join(root, 'symlink_level_2_directory'); + await io.mkdirP(realDirectory); + fs.writeFileSync(realFile, 'test file content'); + createSymlinkDir(realDirectory, symlinkDirectory); + createSymlinkDir(symlinkDirectory, symlinkLevel2Directory); + expect(fs.readFileSync(path.join(symlinkDirectory, 'real_file'), { encoding: 'utf8' })).toBe('test file content'); + if (os.platform() == 'win32') { + expect(fs.readlinkSync(symlinkLevel2Directory)).toBe(symlinkDirectory + '\\'); + } + else { + expect(fs.readlinkSync(symlinkLevel2Directory)).toBe(symlinkDirectory); + } + + await io.rmRF(symlinkLevel2Directory); + expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true); + expect(fs.existsSync(symlinkLevel2Directory)).toBe(false); + }); + + it('removes nested symlink folder with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // outer_directory + // outer_directory/symlink_directory -> real_directory + let root: string = path.join(getTestTemp(), 'rmRF_sym_nest_dir_test'); + let realDirectory: string = path.join(root, 'real_directory'); + let realFile: string = path.join(root, 'real_directory', 'real_file'); + let outerDirectory: string = path.join(root, 'outer_directory'); + let symlinkDirectory: string = path.join(root, 'outer_directory', 'symlink_directory'); + await io.mkdirP(realDirectory); + fs.writeFileSync(realFile, 'test file content'); + await io.mkdirP(outerDirectory); + createSymlinkDir(realDirectory, symlinkDirectory); + expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true); + + await io.rmRF(outerDirectory); + expect(fs.existsSync(realDirectory)).toBe(true); + expect(fs.existsSync(realFile)).toBe(true); + expect(fs.existsSync(symlinkDirectory)).toBe(false); + expect(fs.existsSync(outerDirectory)).toBe(false); + }); + + it('removes deeply nested symlink folder with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // outer_directory + // outer_directory/nested_directory + // outer_directory/nested_directory/symlink_directory -> real_directory + let root: string = path.join(getTestTemp(), 'rmRF_sym_deep_nest_dir_test'); + let realDirectory: string = path.join(root, 'real_directory'); + let realFile: string = path.join(root, 'real_directory', 'real_file'); + let outerDirectory: string = path.join(root, 'outer_directory'); + let nestedDirectory: string = path.join(root, 'outer_directory', 'nested_directory'); + let symlinkDirectory: string = path.join(root, 'outer_directory', 'nested_directory', 'symlink_directory'); + await io.mkdirP(realDirectory); + fs.writeFileSync(realFile, 'test file content'); + await io.mkdirP(nestedDirectory); + createSymlinkDir(realDirectory, symlinkDirectory); + expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true); + + await io.rmRF(outerDirectory); + expect(fs.existsSync(realDirectory)).toBe(true); + expect(fs.existsSync(realFile)).toBe(true); + expect(fs.existsSync(symlinkDirectory)).toBe(false); + expect(fs.existsSync(outerDirectory)).toBe(false); + }); + + it('removes hidden file with rmRF', async () => { + let file: string = path.join(getTestTemp(), '.rmRF_file'); + await io.mkdirP(path.dirname(file)); + await createHiddenFile(file, 'test file content'); + expect(fs.existsSync(file)).toBe(true); + await io.rmRF(file); + expect(fs.existsSync(file)).toBe(false); + }); +}); + +describe('mkdirP', () => { + beforeAll(async () => { + await io.rmRF(getTestTemp()); + }); + + it('creates folder', async () => { + var testPath = path.join(getTestTemp(), 'mkdirTest'); + await io.mkdirP(testPath); + + expect(fs.existsSync(testPath)).toBe(true); + }); + + it('creates nested folders with mkdirP', async () => { + var testPath = path.join(getTestTemp(), 'mkdir1', 'mkdir2'); + await io.mkdirP(testPath); + + expect(fs.existsSync(testPath)).toBe(true); + }); + + it('fails if mkdirP with illegal chars', async () => { + var testPath = path.join(getTestTemp(), 'mkdir\0'); + var worked: boolean = false; + try { + await io.mkdirP(testPath); + worked = true; + } + catch (err) { + expect(fs.existsSync(testPath)).toBe(false); + } + + + expect(worked).toBe(false); + }); + + it('fails if mkdirP with empty path', async () => { + var worked: boolean = false; + try { + await io.mkdirP(''); + worked = true; + } + catch (err) { } + + + expect(worked).toBe(false); + }); + + it('fails if mkdirP with conflicting file path', async () => { + let testPath = path.join(getTestTemp(), 'mkdirP_conflicting_file_path'); + await io.mkdirP(getTestTemp()); + fs.writeFileSync(testPath, ''); + let worked: boolean = false; + try { + await io.mkdirP(testPath); + worked = true; + } + catch (err) { } + + expect(worked).toBe(false); + }); + + it('fails if mkdirP with conflicting parent file path', async () => { + let testPath = path.join(getTestTemp(), 'mkdirP_conflicting_parent_file_path', 'dir'); + await io.mkdirP(getTestTemp()); + fs.writeFileSync(path.dirname(testPath), ''); + let worked: boolean = false; + try { + await io.mkdirP(testPath); + worked = true; + } + catch (err) { } + + expect(worked).toBe(false); + }); + + it('no-ops if mkdirP directory exists', async () => { + let testPath = path.join(getTestTemp(), 'mkdirP_dir_exists'); + await io.mkdirP(testPath); + expect(fs.existsSync(testPath)).toBe(true); + + // Calling again shouldn't throw + await io.mkdirP(testPath); + expect(fs.existsSync(testPath)).toBe(true); + }); + + it('no-ops if mkdirP with symlink directory', async () => { + // create the following layout: + // real_dir + // real_dir/file.txt + // symlink_dir -> real_dir + let rootPath = path.join(getTestTemp(), 'mkdirP_symlink_dir'); + let realDirPath = path.join(rootPath, 'real_dir'); + let realFilePath = path.join(realDirPath, 'file.txt'); + let symlinkDirPath = path.join(rootPath, 'symlink_dir'); + await io.mkdirP(getTestTemp()); + fs.mkdirSync(rootPath); + fs.mkdirSync(realDirPath); + fs.writeFileSync(realFilePath, 'test real_dir/file.txt contet'); + createSymlinkDir(realDirPath, symlinkDirPath); + + await io.mkdirP(symlinkDirPath); + + // the file in the real directory should still be accessible via the symlink + expect(fs.lstatSync(symlinkDirPath).isSymbolicLink()).toBe(true); + expect(fs.statSync(path.join(symlinkDirPath, 'file.txt')).isFile()).toBe(true); + }); + + it('no-ops if mkdirP with parent symlink directory', async () => { + // create the following layout: + // real_dir + // real_dir/file.txt + // symlink_dir -> real_dir + let rootPath = path.join(getTestTemp(), 'mkdirP_parent_symlink_dir'); + let realDirPath = path.join(rootPath, 'real_dir'); + let realFilePath = path.join(realDirPath, 'file.txt'); + let symlinkDirPath = path.join(rootPath, 'symlink_dir'); + await io.mkdirP(getTestTemp()); + fs.mkdirSync(rootPath); + fs.mkdirSync(realDirPath); + fs.writeFileSync(realFilePath, 'test real_dir/file.txt contet'); + createSymlinkDir(realDirPath, symlinkDirPath); + + let subDirPath = path.join(symlinkDirPath, 'sub_dir'); + await io.mkdirP(subDirPath); + + // the subdirectory should be accessible via the real directory + expect(fs.lstatSync(path.join(realDirPath, 'sub_dir')).isDirectory()).toBe(true); + }); + + it('breaks if mkdirP loop out of control', async () => { + let testPath = path.join(getTestTemp(), 'mkdirP_failsafe', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'); + process.env['TEST_MKDIRP_FAILSAFE'] = '10'; + try { + await io.mkdirP(testPath); + throw new Error("directory should not have been created"); + } + catch (err) { + delete process.env['TEST_MKDIRP_FAILSAFE']; + + // ENOENT is expected, all other errors are not + expect(err.code).toBe('ENOENT'); + } + }); +}); + +describe('which', () => { + it('which() finds file name', async () => { + // create a executable file + let testPath = path.join(getTestTemp(), 'which-finds-file-name'); + await io.mkdirP(testPath); + let fileName = 'Which-Test-File'; + if (process.platform == 'win32') { + fileName += '.exe'; + } + + let filePath = path.join(testPath, fileName); + fs.writeFileSync(filePath, ''); + if (process.platform != 'win32') { + chmod(filePath, '+x'); + } + + let originalPath = process.env['PATH']; + try { + // update the PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; + + // exact file name + expect(await io.which(fileName)).toBe(filePath); + expect(await io.which(fileName, false)).toBe(filePath); + expect(await io.which(fileName, true)).toBe(filePath); + + if (process.platform == 'win32') { + // not case sensitive on windows + expect(await io.which('which-test-file.exe')).toBe(path.join(testPath, 'which-test-file.exe')); + expect(await io.which('WHICH-TEST-FILE.EXE')).toBe(path.join(testPath, 'WHICH-TEST-FILE.EXE')); + expect(await io.which('WHICH-TEST-FILE.EXE', false)).toBe(path.join(testPath, 'WHICH-TEST-FILE.EXE')); + expect(await io.which('WHICH-TEST-FILE.EXE', true)).toBe(path.join(testPath, 'WHICH-TEST-FILE.EXE')); + + // without extension + expect(await io.which('which-test-file')).toBe(filePath); + expect(await io.which('which-test-file', false)).toBe(filePath); + expect(await io.which('which-test-file', true)).toBe(filePath); + } + else if (process.platform == 'darwin') { + // not case sensitive on Mac + expect(await io.which(fileName.toUpperCase())).toBe(path.join(testPath, fileName.toUpperCase())); + expect(await io.which(fileName.toUpperCase(), false)).toBe(path.join(testPath, fileName.toUpperCase())); + expect(await io.which(fileName.toUpperCase(), true)).toBe(path.join(testPath, fileName.toUpperCase())); + } + else { + // case sensitive on Linux + expect(await io.which(fileName.toUpperCase()) || '').toBe(''); + } + } + finally { + process.env['PATH'] = originalPath; + } + }); + + it('which() not found', async () => { + expect(await io.which('which-test-no-such-file')).toBe(''); + expect(await io.which('which-test-no-such-file', false)).toBe(''); + try { + await io.which('which-test-no-such-file', true); + throw new Error('Should have thrown'); + } + catch (err) { + } + }); + + it('which() searches path in order', async () => { + // create a chcp.com/bash override file + let testPath = path.join(getTestTemp(), 'which-searches-path-in-order'); + await io.mkdirP(testPath); + let fileName; + if (process.platform == 'win32') { + fileName = 'chcp.com'; + } + else { + fileName = 'bash'; + } + + let filePath = path.join(testPath, fileName); + fs.writeFileSync(filePath, ''); + if (process.platform != 'win32') { + chmod(filePath, '+x'); + } + + let originalPath = process.env['PATH']; + try { + // sanity - regular chcp.com/bash should be found + let originalWhich = await io.which(fileName); + expect(!!(originalWhich || '')).toBe(true); + + // modify PATH + process.env['PATH'] = testPath + path.delimiter + process.env['PATH']; + + // override chcp.com/bash should be found + expect(await io.which(fileName)).toBe(filePath); + } + finally { + process.env['PATH'] = originalPath; + } + }); + + it('which() requires executable', async () => { + // create a non-executable file + // on Windows, should not end in valid PATHEXT + // on Mac/Linux should not have executable bit + let testPath = path.join(getTestTemp(), 'which-requires-executable'); + await io.mkdirP(testPath); + let fileName = 'Which-Test-File'; + if (process.platform == 'win32') { + fileName += '.abc'; // not a valid PATHEXT + } + + let filePath = path.join(testPath, fileName); + fs.writeFileSync(filePath, ''); + if (process.platform != 'win32') { + chmod(filePath, '-x'); + } + + let originalPath = process.env['PATH']; + try { + // modify PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; + + // should not be found + expect(await io.which(fileName) || '').toBe(''); + } + finally { + process.env['PATH'] = originalPath; + } + }); + + // which permissions tests + it('which() finds executable with different permissions', async () => { + await findsExecutableWithScopedPermissions('u=rwx,g=r,o=r'); + await findsExecutableWithScopedPermissions('u=rw,g=rx,o=r'); + await findsExecutableWithScopedPermissions('u=rw,g=r,o=rx'); + }); + + it('which() ignores directory match', async () => { + // create a directory + let testPath = path.join(getTestTemp(), 'which-ignores-directory-match'); + let dirPath = path.join(testPath, 'Which-Test-Dir'); + if (process.platform == 'win32') { + dirPath += '.exe'; + } + + await io.mkdirP(dirPath); + if (process.platform != 'win32') { + chmod(dirPath, '+x'); + } + + let originalPath = process.env['PATH']; + try { + // modify PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; + + // should not be found + expect(await io.which(path.basename(dirPath)) || '').toBe(''); + } + finally { + process.env['PATH'] = originalPath; + } + }); + + it('which() allows rooted path', async () => { + // create an executable file + let testPath = path.join(getTestTemp(), 'which-allows-rooted-path'); + await io.mkdirP(testPath); + let filePath = path.join(testPath, 'Which-Test-File'); + if (process.platform == 'win32') { + filePath += '.exe'; + } + + fs.writeFileSync(filePath, ''); + if (process.platform != 'win32') { + chmod(filePath, '+x'); + } + + // which the full path + expect(await io.which(filePath)).toBe(filePath); + expect(await io.which(filePath, false)).toBe(filePath); + expect(await io.which(filePath, true)).toBe(filePath); + }); + + it('which() requires rooted path to be executable', async () => { + // create a non-executable file + // on Windows, should not end in valid PATHEXT + // on Mac/Linux, should not have executable bit + let testPath = path.join(getTestTemp(), 'which-requires-rooted-path-to-be-executable'); + await io.mkdirP(testPath); + let filePath = path.join(testPath, 'Which-Test-File'); + if (process.platform == 'win32') { + filePath += '.abc'; // not a valid PATHEXT + } + + fs.writeFileSync(filePath, ''); + if (process.platform != 'win32') { + chmod(filePath, '-x'); + } + + // should not be found + expect(await io.which(filePath) || '').toBe(''); + expect(await io.which(filePath, false) || '').toBe(''); + let failed = false; + try { + await io.which(filePath, true); + } + catch (err) { + failed = true; + } + + expect(failed).toBe(true); + }); + + it('which() requires rooted path to be a file', async () => { + // create a dir + let testPath = path.join(getTestTemp(), 'which-requires-rooted-path-to-be-executable'); + let dirPath = path.join(testPath, 'Which-Test-Dir'); + if (process.platform == 'win32') { + dirPath += '.exe'; + } + + await io.mkdirP(dirPath); + if (process.platform != 'win32') { + chmod(dirPath, '+x'); + } + + // should not be found + expect(await io.which(dirPath) || '').toBe(''); + expect(await io.which(dirPath, false) || '').toBe(''); + let failed = false; + try { + await io.which(dirPath, true); + } + catch (err) { + failed = true; + } + + expect(failed).toBe(true); + }); + + it('which() requires rooted path to exist', async () => { + let filePath = path.join(__dirname, 'no-such-file'); + if (process.platform == 'win32') { + filePath += '.exe'; + } + + expect(await io.which(filePath) || '').toBe(''); + expect(await io.which(filePath, false) || '').toBe(''); + let failed = false; + try { + await io.which(filePath, true); + } + catch (err) { + failed = true; + } + + expect(failed).toBe(true); + }); + + it('which() does not allow separators', async () => { + // create an executable file + let testDirName = 'which-does-not-allow-separators'; + let testPath = path.join(getTestTemp(), testDirName); + await io.mkdirP(testPath); + let fileName = 'Which-Test-File'; + if (process.platform == 'win32') { + fileName += '.exe'; + } + + let filePath = path.join(testPath, fileName); + fs.writeFileSync(filePath, ''); + if (process.platform != 'win32') { + chmod(filePath, '+x'); + } + + let originalPath = process.env['PATH']; + try { + // modify PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; + + // which "dir/file", should not be found + expect(await io.which(testDirName + '/' + fileName) || '').toBe(''); + + // on Windows, also try "dir\file" + if (process.platform == 'win32') { + expect(await io.which(testDirName + '\\' + fileName) || '').toBe(''); + } + } + finally { + process.env['PATH'] = originalPath; + } + }); + + if (process.platform == 'win32') { + it('which() resolves actual case file name when extension is applied', async () => { + const comspec: string = process.env['ComSpec'] || ''; + expect(!!comspec).toBe(true); + expect(await io.which('CmD.eXe')).toBe(path.join(path.dirname(comspec), 'CmD.eXe')); + expect(await io.which('CmD')).toBe(comspec); + }); + + it('which() appends ext on windows', async () => { + // create executable files + let testPath = path.join(getTestTemp(), 'which-appends-ext-on-windows'); + await io.mkdirP(testPath); + // PATHEXT=.COM;.EXE;.BAT;.CMD... + let files: { [key:string]:string; } = { + "which-test-file-1": path.join(testPath, "which-test-file-1.com"), + "which-test-file-2": path.join(testPath, "which-test-file-2.exe"), + "which-test-file-3": path.join(testPath, "which-test-file-3.bat"), + "which-test-file-4": path.join(testPath, "which-test-file-4.cmd"), + "which-test-file-5.txt": path.join(testPath, "which-test-file-5.txt.com") + }; + for (let fileName of Object.keys(files)) { + fs.writeFileSync(files[fileName], ''); + } + + let originalPath = process.env['PATH']; + try { + // modify PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; + + // find each file + for (let fileName of Object.keys(files)) { + expect(await io.which(fileName)).toBe(files[fileName]); + } + } + finally { + process.env['PATH'] = originalPath; + } + }); + + it('which() appends ext on windows when rooted', async () => { + // create executable files + let testPath = path.join(getTestTemp(), 'which-appends-ext-on-windows-when-rooted'); + await io.mkdirP(testPath); + // PATHEXT=.COM;.EXE;.BAT;.CMD... + let files: { [key:string]:string; } = { }; + files[path.join(testPath, "which-test-file-1")] = path.join(testPath, "which-test-file-1.com"); + files[path.join(testPath, "which-test-file-2")] = path.join(testPath, "which-test-file-2.exe"); + files[path.join(testPath, "which-test-file-3")] = path.join(testPath, "which-test-file-3.bat"); + files[path.join(testPath, "which-test-file-4")] = path.join(testPath, "which-test-file-4.cmd"); + files[path.join(testPath, "which-test-file-5.txt")] = path.join(testPath, "which-test-file-5.txt.com"); + for (let fileName of Object.keys(files)) { + fs.writeFileSync(files[fileName], ''); + } + + // find each file + for (let fileName of Object.keys(files)) { + expect(await io.which(fileName)).toBe(files[fileName]); + } + }); + + it('which() prefer exact match on windows', async () => { + // create two executable files: + // which-test-file.bat + // which-test-file.bat.exe + // + // verify "which-test-file.bat" returns that file, and not "which-test-file.bat.exe" + // + // preference, within the same dir, should be given to the exact match (even though + // .EXE is defined with higher preference than .BAT in PATHEXT (PATHEXT=.COM;.EXE;.BAT;.CMD...) + let testPath = path.join(getTestTemp(), 'which-prefer-exact-match-on-windows'); + await io.mkdirP(testPath); + let fileName = 'which-test-file.bat'; + let expectedFilePath = path.join(testPath, fileName); + let notExpectedFilePath = path.join(testPath, fileName + '.exe'); + fs.writeFileSync(expectedFilePath, ''); + fs.writeFileSync(notExpectedFilePath, ''); + let originalPath = process.env['PATH']; + try { + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; + expect(await io.which(fileName)).toBe(expectedFilePath); + } + finally { + process.env['PATH'] = originalPath; + } + }); + + it('which() prefer exact match on windows when rooted', async () => { + // create two executable files: + // which-test-file.bat + // which-test-file.bat.exe + // + // verify "which-test-file.bat" returns that file, and not "which-test-file.bat.exe" + // + // preference, within the same dir, should be given to the exact match (even though + // .EXE is defined with higher preference than .BAT in PATHEXT (PATHEXT=.COM;.EXE;.BAT;.CMD...) + let testPath = path.join(getTestTemp(), 'which-prefer-exact-match-on-windows-when-rooted'); + await io.mkdirP(testPath); + let fileName = 'which-test-file.bat'; + let expectedFilePath = path.join(testPath, fileName); + let notExpectedFilePath = path.join(testPath, fileName + '.exe'); + fs.writeFileSync(expectedFilePath, ''); + fs.writeFileSync(notExpectedFilePath, ''); + expect(await io.which(path.join(testPath, fileName))).toBe(expectedFilePath); + }); + + it('which() searches ext in order', async () => { + let testPath = path.join(getTestTemp(), 'which-searches-ext-in-order'); + + // create a directory for testing .COM order preference + // PATHEXT=.COM;.EXE;.BAT;.CMD... + let fileNameWithoutExtension = 'which-test-file'; + let comTestPath = path.join(testPath, 'com-test'); + await io.mkdirP(comTestPath); + fs.writeFileSync(path.join(comTestPath, fileNameWithoutExtension + '.com'), ''); + fs.writeFileSync(path.join(comTestPath, fileNameWithoutExtension + '.exe'), ''); + fs.writeFileSync(path.join(comTestPath, fileNameWithoutExtension + '.bat'), ''); + fs.writeFileSync(path.join(comTestPath, fileNameWithoutExtension + '.cmd'), ''); + + // create a directory for testing .EXE order preference + // PATHEXT=.COM;.EXE;.BAT;.CMD... + let exeTestPath = path.join(testPath, 'exe-test'); + await io.mkdirP(exeTestPath); + fs.writeFileSync(path.join(exeTestPath, fileNameWithoutExtension + '.exe'), ''); + fs.writeFileSync(path.join(exeTestPath, fileNameWithoutExtension + '.bat'), ''); + fs.writeFileSync(path.join(exeTestPath, fileNameWithoutExtension + '.cmd'), ''); + + // create a directory for testing .BAT order preference + // PATHEXT=.COM;.EXE;.BAT;.CMD... + let batTestPath = path.join(testPath, 'bat-test'); + await io.mkdirP(batTestPath); + fs.writeFileSync(path.join(batTestPath, fileNameWithoutExtension + '.bat'), ''); + fs.writeFileSync(path.join(batTestPath, fileNameWithoutExtension + '.cmd'), ''); + + // create a directory for testing .CMD + let cmdTestPath = path.join(testPath, 'cmd-test'); + await io.mkdirP(cmdTestPath); + let cmdTest_cmdFilePath = path.join(cmdTestPath, fileNameWithoutExtension + '.cmd'); + fs.writeFileSync(cmdTest_cmdFilePath, ''); + + let originalPath = process.env['PATH']; + try { + // test .COM + process.env['PATH'] = comTestPath + path.delimiter + originalPath; + expect(await io.which(fileNameWithoutExtension)).toBe(path.join(comTestPath, fileNameWithoutExtension + '.com')); + + // test .EXE + process.env['PATH'] = exeTestPath + path.delimiter + originalPath; + expect(await io.which(fileNameWithoutExtension)).toBe(path.join(exeTestPath, fileNameWithoutExtension + '.exe')); + + // test .BAT + process.env['PATH'] = batTestPath + path.delimiter + originalPath; + expect(await io.which(fileNameWithoutExtension)).toBe(path.join(batTestPath, fileNameWithoutExtension + '.bat')); + + // test .CMD + process.env['PATH'] = cmdTestPath + path.delimiter + originalPath; + expect(await io.which(fileNameWithoutExtension)).toBe(path.join(cmdTestPath, fileNameWithoutExtension + '.cmd')); + } + finally { + process.env['PATH'] = originalPath; + } + }); + } +}); + +async function findsExecutableWithScopedPermissions(chmodOptions: string) { + // create a executable file + let testPath = path.join(getTestTemp(), 'which-finds-file-name'); + await io.mkdirP(testPath); + let fileName = 'Which-Test-File'; + if (process.platform == 'win32') { + return; + } + + let filePath = path.join(testPath, fileName); + fs.writeFileSync(filePath, ''); + chmod(filePath, chmodOptions); + + let originalPath = process.env['PATH']; + try { + // update the PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; + + // exact file name + expect(await io.which(fileName)).toBe(filePath); + expect(await io.which(fileName, false)).toBe(filePath); + expect(await io.which(fileName, true)).toBe(filePath); + + if (process.platform == 'darwin') { + // not case sensitive on Mac + expect(await io.which(fileName.toUpperCase())).toBe(path.join(testPath, fileName.toUpperCase())); + expect(await io.which(fileName.toUpperCase(), false)).toBe(path.join(testPath, fileName.toUpperCase())); + expect(await io.which(fileName.toUpperCase(), true)).toBe(path.join(testPath, fileName.toUpperCase())); + } + else { + // case sensitive on Linux + expect(await io.which(fileName.toUpperCase()) || '').toBe(''); + } + } + finally { + process.env['PATH'] = originalPath; + } + + return; +} + +function chmod(file: string, mode: string): void { + let result = child.spawnSync('chmod', [ mode, file ]); + if (result.status != 0) { + let message: string = (result.output || []).join(' ').trim(); + throw new Error(`Command failed: "chmod ${mode} ${file}". ${message}`); + } +} + +async function createHiddenDirectory(dir: string): Promise { + return new Promise(async (resolve, reject) => { + if (!path.basename(dir).match(/^\./)) { + reject(`Expected dir '${dir}' to start with '.'.`); + } + + await io.mkdirP(dir); + if (os.platform() == 'win32') { + let result = child.spawnSync('attrib.exe', [ '+H', dir ]); + if (result.status != 0) { + let message: string = (result.output || []).join(' ').trim(); + reject(`Failed to set hidden attribute for directory '${dir}'. ${message}`); + } + } + resolve() + }); +}; + +async function createHiddenFile(file: string, content: string): Promise { + return new Promise(async (resolve, reject) => { + if (!path.basename(file).match(/^\./)) { + reject(`Expected dir '${file}' to start with '.'.`); + } + + await io.mkdirP(path.dirname(file)); + fs.writeFileSync(file, content); + if (os.platform() == 'win32') { + let result = child.spawnSync('attrib.exe', [ '+H', file ]); + if (result.status != 0) { + let message: string = (result.output || []).join(' ').trim(); + reject(`Failed to set hidden attribute for file '${file}'. ${message}`); + } + } + + resolve(); + }); +}; + +function getTestTemp() { + return path.join(__dirname, '_temp'); +} + +/** + * Creates a symlink directory on OSX/Linux, and a junction point directory on Windows. + * A symlink directory is not created on Windows since it requires an elevated context. + */ +function createSymlinkDir(real: string, link: string): void { + if (os.platform() == 'win32') { + fs.symlinkSync(real, link, 'junction'); + } + else { + fs.symlinkSync(real, link); + } +}; \ No newline at end of file diff --git a/packages/io/package.json b/packages/io/package.json new file mode 100644 index 0000000000..4a739bca62 --- /dev/null +++ b/packages/io/package.json @@ -0,0 +1,34 @@ +{ + "name": "@actions/io", + "version": "1.0.0", + "description": "Actions io lib", + "keywords": [ + "io", + "actions" + ], + "author": "Danny McCormick ", + "homepage": "https://github.com/actions/toolkit/tree/master/packages/io", + "license": "MIT", + "main": "lib/lib.js", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/actions/toolkit.git" + }, + "scripts": { + "test": "echo \"Error: run tests from root\" && exit 1", + "tsc": "tsc" + }, + "bugs": { + "url": "https://github.com/actions/toolkit/issues" + } +} diff --git a/packages/io/src/interfaces.ts b/packages/io/src/interfaces.ts new file mode 100644 index 0000000000..909f9d518c --- /dev/null +++ b/packages/io/src/interfaces.ts @@ -0,0 +1,9 @@ +/** + * Interface for cp/mv options + */ +export interface CopyOptions { + /** Optional. Whether to recursively copy all subdirectories. Defaults to false */ + recursive?: boolean + /** Optional. Whether to overwrite existing files in the destination. Defaults to true */ + force?: boolean +} diff --git a/packages/io/src/internal.ts b/packages/io/src/internal.ts new file mode 100644 index 0000000000..8122a96f8d --- /dev/null +++ b/packages/io/src/internal.ts @@ -0,0 +1,147 @@ +import fs = require('fs') +import path = require('path') + +export function _removeDirectory(directoryPath: string) { + if (fs.existsSync(directoryPath)) { + fs.readdirSync(directoryPath).forEach(fileName => { + let file = path.join(directoryPath, fileName) + if (fs.lstatSync(file).isDirectory()) { + _removeDirectory(file) + } else { + fs.unlinkSync(file) + } + }) + } + fs.rmdirSync(directoryPath) +} + +/** + * On OSX/Linux, true if path starts with '/'. On Windows, true for paths like: + * \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases). + */ +export function _isRooted(p: string): boolean { + p = _normalizeSeparators(p) + if (!p) { + throw new Error('isRooted() parameter "p" cannot be empty') + } + + if (process.platform == 'win32') { + return ( + p.slice(0, '\\'.length) == '\\' || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello + ) // e.g. C: or C:\hello + } + + return p.slice(0, '/'.length) == '/' +} + +/** + * Best effort attempt to determine whether a file exists and is executable. + * @param filePath file path to check + * @param extensions additional file extensions to try + * @return if file exists and is executable, returns the file path. otherwise empty string. + */ +export function _tryGetExecutablePath( + filePath: string, + extensions: string[] +): string { + try { + // test file exists + let stats: fs.Stats = fs.statSync(filePath) + if (stats.isFile()) { + if (process.platform == 'win32') { + // on Windows, test for valid extension + let fileName = path.basename(filePath) + let dotIndex = fileName.lastIndexOf('.') + if (dotIndex >= 0) { + let upperExt = fileName.substr(dotIndex).toUpperCase() + if (extensions.some(validExt => validExt.toUpperCase() == upperExt)) { + return filePath + } + } + } else { + if (_isUnixExecutable(stats)) { + return filePath + } + } + } + } catch (err) { + if (err.code != 'ENOENT') { + console.log( + `Unexpected error attempting to determine if executable file exists '${filePath}': ${err}` + ) + } + } + + // try each extension + let originalFilePath = filePath + for (let extension of extensions) { + let found = false + let filePath = originalFilePath + extension + try { + let stats: fs.Stats = fs.statSync(filePath) + if (stats.isFile()) { + if (process.platform == 'win32') { + // preserve the case of the actual file (since an extension was appended) + try { + let directory = path.dirname(filePath) + let upperName = path.basename(filePath).toUpperCase() + for (let actualName of fs.readdirSync(directory)) { + if (upperName == actualName.toUpperCase()) { + filePath = path.join(directory, actualName) + break + } + } + } catch (err) { + console.log( + `Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}` + ) + } + + return filePath + } else { + if (_isUnixExecutable(stats)) { + return filePath + } + } + } + } catch (err) { + if (err.code != 'ENOENT') { + console.log( + `Unexpected error attempting to determine if executable file exists '${filePath}': ${err}` + ) + } + } + } + + return '' +} + +function _normalizeSeparators(p: string): string { + p = p || '' + if (process.platform == 'win32') { + // convert slashes on Windows + p = p.replace(/\//g, '\\') + + // remove redundant slashes + let isUnc = /^\\\\+[^\\]/.test(p) // e.g. \\hello + return (isUnc ? '\\' : '') + p.replace(/\\\\+/g, '\\') // preserve leading // for UNC + } + + // remove redundant slashes + return p.replace(/\/\/+/g, '/') +} + +function _startsWith(str: string, start: string): boolean { + return str.slice(0, start.length) == start +} + +// on Mac/Linux, test the execute bit +// R W X R W X R W X +// 256 128 64 32 16 8 4 2 1 +function _isUnixExecutable(stats: fs.Stats) { + return ( + (stats.mode & 1) > 0 || + ((stats.mode & 8) > 0 && stats.gid === process.getgid()) || + ((stats.mode & 64) > 0 && stats.uid === process.getuid()) + ) +} diff --git a/packages/io/src/lib.ts b/packages/io/src/lib.ts new file mode 100644 index 0000000000..1590ef1402 --- /dev/null +++ b/packages/io/src/lib.ts @@ -0,0 +1,401 @@ +import childProcess = require('child_process') +import fs = require('fs') +import path = require('path') +import intm = require('./internal') +import im = require('./interfaces') + +/** + * Copies a file or folder. + * + * @param source source path + * @param dest destination path + * @param options optional. See CopyOptions. + */ +export function cp( + source: string, + dest: string, + options?: im.CopyOptions +): Promise { + let force = true + let recursive = false + if (options) { + if (options.recursive) { + recursive = true + } + if (options.force === false) { + force = false + } + } + return new Promise(async (resolve, reject) => { + try { + if (fs.lstatSync(source).isDirectory()) { + if (!recursive) { + throw new Error(`non-recursive cp failed, ${source} is a directory`) + } + + // If directory exists, copy source inside it. Otherwise, create it and copy contents of source inside. + if (fs.existsSync(dest)) { + if (!fs.lstatSync(dest).isDirectory()) { + throw new Error(`${dest} is not a directory`) + } + + dest = path.join(dest, path.basename(source)) + } + + copyDirectoryContents(source, dest, force) + } else { + if (force) { + fs.copyFileSync(source, dest) + } else { + fs.copyFileSync(source, dest, fs.constants.COPYFILE_EXCL) + } + } + } catch (err) { + reject(err) + return + } + + resolve() + }) +} + +/** + * Moves a path. + * + * @param source source path + * @param dest destination path + * @param options optional. See CopyOptions. + */ +export function mv( + source: string, + dest: string, + options?: im.CopyOptions +): Promise { + let force = true + let recursive = false + if (options) { + if (options.recursive) { + recursive = true + } + if (options.force === false) { + force = false + } + } + return new Promise(async (resolve, reject) => { + try { + if (fs.lstatSync(source).isDirectory()) { + if (!recursive) { + throw new Error(`non-recursive cp failed, ${source} is a directory`) + } + + // If directory exists, move source inside it. Otherwise, create it and move contents of source inside. + if (fs.existsSync(dest)) { + if (!fs.lstatSync(dest).isDirectory()) { + throw new Error(`${dest} is not a directory`) + } + + dest = path.join(dest, path.basename(source)) + } + + copyDirectoryContents(source, dest, force, true) + } else { + if (force) { + fs.copyFileSync(source, dest) + } else { + fs.copyFileSync(source, dest, fs.constants.COPYFILE_EXCL) + } + + // Delete file after copying since this is mv. + fs.unlinkSync(source) + } + } catch (err) { + reject(err) + return + } + + resolve() + }) +} + +/** + * Remove a path recursively with force + * + * @param inputPath path to remove + */ +export function rmRF(inputPath: string): Promise { + return new Promise(async (resolve, reject) => { + if (process.platform === 'win32') { + // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another + // program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del. + try { + if (fs.statSync(inputPath).isDirectory()) { + childProcess.execSync(`rd /s /q "${inputPath}"`) + } else { + childProcess.execSync(`del /f /a "${inputPath}"`) + } + } catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code != 'ENOENT') { + reject(err) + return + } + } + + // Shelling out fails to remove a symlink folder with missing source, this unlink catches that + try { + fs.unlinkSync(inputPath) + } catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code != 'ENOENT') { + reject(err) + return + } + } + } else { + // get the lstats in order to workaround a bug in shelljs@0.3.0 where symlinks + // with missing targets are not handled correctly by "rm('-rf', path)" + let isDirectory = false + try { + isDirectory = fs.lstatSync(inputPath).isDirectory() + } catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code == 'ENOENT') { + resolve() + } else { + reject(err) + } + return + } + + if (isDirectory) { + intm._removeDirectory(inputPath) + resolve() + return + } else { + fs.unlinkSync(inputPath) + } + } + + resolve() + }) +} + +/** + * Make a directory. Creates the full path with folders in between + * Will throw if it fails + * + * @param p path to create + * @returns Promise + */ +export function mkdirP(p: string): Promise { + return new Promise(async (resolve, reject) => { + try { + if (!p) { + throw new Error('Parameter p is required') + } + + // build a stack of directories to create + let stack: string[] = [] + let testDir: string = p + while (true) { + // validate the loop is not out of control + if (stack.length >= (process.env['TEST_MKDIRP_FAILSAFE'] || 1000)) { + // let the framework throw + fs.mkdirSync(p) + resolve() + return + } + + let stats: fs.Stats + try { + stats = fs.statSync(testDir) + } catch (err) { + if (err.code == 'ENOENT') { + // validate the directory is not the drive root + let parentDir = path.dirname(testDir) + if (testDir == parentDir) { + throw new Error( + `Unable to create directory '${p}'. Root directory does not exist: '${testDir}'` + ) + } + + // push the dir and test the parent + stack.push(testDir) + testDir = parentDir + continue + } else if (err.code == 'UNKNOWN') { + throw new Error( + `Unable to create directory '${p}'. Unable to verify the directory exists: '${testDir}'. If directory is a file share, please verify the share name is correct, the share is online, and the current process has permission to access the share.` + ) + } else { + throw err + } + } + + if (!stats.isDirectory()) { + throw new Error( + `Unable to create directory '${p}'. Conflicting file exists: '${testDir}'` + ) + } + + // testDir exists + break + } + + // create each directory + while (stack.length) { + let dir = stack.pop()! // non-null because `stack.length` was truthy + fs.mkdirSync(dir) + } + } catch (err) { + reject(err) + return + } + + resolve() + }) +} + +/** + * Returns path of a tool had the tool actually been invoked. Resolves via paths. + * If you check and the tool does not exist, it will throw. + * + * @param tool name of the tool + * @param check whether to check if tool exists + * @returns Promise path to tool + */ +export function which(tool: string, check?: boolean): Promise { + return new Promise(async (resolve, reject) => { + if (!tool) { + reject("parameter 'tool' is required") + return + } + + // recursive when check=true + if (check) { + let result: string = await which(tool, false) + if (!result) { + if (process.platform == 'win32') { + reject( + `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.` + ) + } else { + reject( + `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.` + ) + } + return + } + } + + try { + // build the list of extensions to try + let extensions: string[] = [] + if (process.platform == 'win32' && process.env['PATHEXT']) { + for (let extension of process.env['PATHEXT']!.split(path.delimiter)) { + if (extension) { + extensions.push(extension) + } + } + } + + // if it's rooted, return it if exists. otherwise return empty. + if (intm._isRooted(tool)) { + let filePath: string = intm._tryGetExecutablePath(tool, extensions) + if (filePath) { + resolve(filePath) + return + } + + resolve('') + return + } + + // if any path separators, return empty + if ( + tool.indexOf('/') >= 0 || + (process.platform == 'win32' && tool.indexOf('\\') >= 0) + ) { + resolve('') + return + } + + // build the list of directories + // + // Note, technically "where" checks the current directory on Windows. From a task lib perspective, + // it feels like we should not do this. Checking the current directory seems like more of a use + // case of a shell, and the which() function exposed by the task lib should strive for consistency + // across platforms. + let directories: string[] = [] + if (process.env['PATH']) { + for (let p of process.env['PATH']!.split(path.delimiter)) { + if (p) { + directories.push(p) + } + } + } + + // return the first match + for (let directory of directories) { + let filePath = intm._tryGetExecutablePath( + directory + path.sep + tool, + extensions + ) + if (filePath) { + resolve(filePath) + return + } + } + + resolve('') + } catch (err) { + reject(`which failed with message ${err.message}`) + } + }) +} + +// Copies contents of source into dest, making any necessary folders along the way. +// Deletes the original copy if deleteOriginal is true +function copyDirectoryContents( + source: string, + dest: string, + force: boolean, + deleteOriginal = false +) { + if (fs.lstatSync(source).isDirectory()) { + if (fs.existsSync(dest)) { + if (!fs.lstatSync(dest).isDirectory()) { + throw new Error(`${dest} is not a directory`) + } + } else { + mkdirP(dest) + } + + // Copy all child files, and directories recursively + let sourceChildren: string[] = fs.readdirSync(source) + sourceChildren.forEach(newSource => { + let newDest = path.join(dest, path.basename(newSource)) + copyDirectoryContents( + path.resolve(source, newSource), + newDest, + force, + deleteOriginal + ) + }) + if (deleteOriginal) { + fs.rmdirSync(source) + } + } else { + if (force) { + fs.copyFileSync(source, dest) + } else { + fs.copyFileSync(source, dest, fs.constants.COPYFILE_EXCL) + } + if (deleteOriginal) { + fs.unlinkSync(source) + } + } +} diff --git a/packages/io/tsconfig.json b/packages/io/tsconfig.json new file mode 100644 index 0000000000..a8b812a6ff --- /dev/null +++ b/packages/io/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./lib", + "rootDir": "./src" + }, + "include": [ + "./src" + ] +} \ No newline at end of file From 908cec388cad7024181ce34c6f8610dd48f1afc7 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Tue, 21 May 2019 13:51:10 -0400 Subject: [PATCH 02/27] io cleanup --- .gitignore | 3 ++- .../io/__tests__/{lib.test.ts => io.test.ts} | 2 +- packages/io/package.json | 2 +- packages/io/src/interfaces.ts | 9 ------- packages/io/src/{lib.ts => io.ts} | 25 +++++++++++++------ packages/io/src/{internal.ts => ioUtil.ts} | 1 - 6 files changed, 21 insertions(+), 21 deletions(-) rename packages/io/__tests__/{lib.test.ts => io.test.ts} (99%) delete mode 100644 packages/io/src/interfaces.ts rename packages/io/src/{lib.ts => io.ts} (95%) rename packages/io/src/{internal.ts => ioUtil.ts} (99%) diff --git a/.gitignore b/.gitignore index ea4d4ad115..db6371d359 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ packages/*/node_modules/ -packages/*/lib/ \ No newline at end of file +packages/*/lib/ +packages/*/__tests__/_temp/ \ No newline at end of file diff --git a/packages/io/__tests__/lib.test.ts b/packages/io/__tests__/io.test.ts similarity index 99% rename from packages/io/__tests__/lib.test.ts rename to packages/io/__tests__/io.test.ts index b92f6582e5..6415da7826 100644 --- a/packages/io/__tests__/lib.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -3,7 +3,7 @@ import fs = require('fs'); import path = require('path'); import os = require('os'); -import io = require('../src/lib'); +import io = require('../src/io'); describe('cp', () => { it('copies file with no flags', async () => { diff --git a/packages/io/package.json b/packages/io/package.json index 4a739bca62..ddf6a9965c 100644 --- a/packages/io/package.json +++ b/packages/io/package.json @@ -9,7 +9,7 @@ "author": "Danny McCormick ", "homepage": "https://github.com/actions/toolkit/tree/master/packages/io", "license": "MIT", - "main": "lib/lib.js", + "main": "lib/io.js", "directories": { "lib": "lib", "test": "__tests__" diff --git a/packages/io/src/interfaces.ts b/packages/io/src/interfaces.ts deleted file mode 100644 index 909f9d518c..0000000000 --- a/packages/io/src/interfaces.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Interface for cp/mv options - */ -export interface CopyOptions { - /** Optional. Whether to recursively copy all subdirectories. Defaults to false */ - recursive?: boolean - /** Optional. Whether to overwrite existing files in the destination. Defaults to true */ - force?: boolean -} diff --git a/packages/io/src/lib.ts b/packages/io/src/io.ts similarity index 95% rename from packages/io/src/lib.ts rename to packages/io/src/io.ts index 1590ef1402..ac1331529e 100644 --- a/packages/io/src/lib.ts +++ b/packages/io/src/io.ts @@ -1,8 +1,17 @@ import childProcess = require('child_process') import fs = require('fs') import path = require('path') -import intm = require('./internal') -import im = require('./interfaces') +import util = require('./ioUtil') + +/** + * Interface for cp/mv options + */ +export interface CopyOptions { + /** Optional. Whether to recursively copy all subdirectories. Defaults to false */ + recursive?: boolean + /** Optional. Whether to overwrite existing files in the destination. Defaults to true */ + force?: boolean +} /** * Copies a file or folder. @@ -14,7 +23,7 @@ import im = require('./interfaces') export function cp( source: string, dest: string, - options?: im.CopyOptions + options?: CopyOptions ): Promise { let force = true let recursive = false @@ -69,7 +78,7 @@ export function cp( export function mv( source: string, dest: string, - options?: im.CopyOptions + options?: CopyOptions ): Promise { let force = true let recursive = false @@ -171,7 +180,7 @@ export function rmRF(inputPath: string): Promise { } if (isDirectory) { - intm._removeDirectory(inputPath) + util._removeDirectory(inputPath) resolve() return } else { @@ -303,8 +312,8 @@ export function which(tool: string, check?: boolean): Promise { } // if it's rooted, return it if exists. otherwise return empty. - if (intm._isRooted(tool)) { - let filePath: string = intm._tryGetExecutablePath(tool, extensions) + if (util._isRooted(tool)) { + let filePath: string = util._tryGetExecutablePath(tool, extensions) if (filePath) { resolve(filePath) return @@ -340,7 +349,7 @@ export function which(tool: string, check?: boolean): Promise { // return the first match for (let directory of directories) { - let filePath = intm._tryGetExecutablePath( + let filePath = util._tryGetExecutablePath( directory + path.sep + tool, extensions ) diff --git a/packages/io/src/internal.ts b/packages/io/src/ioUtil.ts similarity index 99% rename from packages/io/src/internal.ts rename to packages/io/src/ioUtil.ts index 8122a96f8d..f89ad033c3 100644 --- a/packages/io/src/internal.ts +++ b/packages/io/src/ioUtil.ts @@ -75,7 +75,6 @@ export function _tryGetExecutablePath( // try each extension let originalFilePath = filePath for (let extension of extensions) { - let found = false let filePath = originalFilePath + extension try { let stats: fs.Stats = fs.statSync(filePath) From b0a5f41df409b9a3eb485aa7743f2acce7ec5cc2 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 15:30:51 -0400 Subject: [PATCH 03/27] Run format script --- packages/io/__tests__/io.test.ts | 2534 ++++++++++++++++-------------- 1 file changed, 1354 insertions(+), 1180 deletions(-) diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index 6415da7826..0b0770006d 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -1,1257 +1,1432 @@ -import child = require('child_process'); -import fs = require('fs'); -import path = require('path'); -import os = require('os'); +import child = require('child_process') +import fs = require('fs') +import path = require('path') +import os = require('os') -import io = require('../src/io'); +import io = require('../src/io') describe('cp', () => { - it('copies file with no flags', async () => { - let root: string = path.join(getTestTemp(), 'cp_with_no_flags'); - let sourceFile: string = path.join(root, 'cp_source'); - let targetFile: string = path.join(root, 'cp_target'); - await io.mkdirP(root); - fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); - - await io.cp(sourceFile, targetFile); - - expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); - }); - - it('copies file using -f', async () => { - let root: string = path.join(path.join(__dirname, '_temp'), 'cp_with_-f'); - let sourceFile: string = path.join(root, 'cp_source'); - let targetFile: string = path.join(root, 'cp_target'); - await io.mkdirP(root); - fs.writeFileSync(sourceFile, 'test file content'); - - await io.cp(sourceFile, targetFile, {recursive: false, force: true}); - - expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); - }); - - it('try copying to existing file with -n', async () => { - let root: string = path.join(getTestTemp(), 'cp_to_existing'); - let sourceFile: string = path.join(root, 'cp_source'); - let targetFile: string = path.join(root, 'cp_target'); - await io.mkdirP(root); - fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); - fs.writeFileSync(targetFile, 'correct content', { encoding: 'utf8' }); - let failed = false - try { - await io.cp(sourceFile, targetFile, {recursive: false, force: false}); - } - catch { - failed = true; - } - expect(failed).toBe(true); - - expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('correct content'); - }); - - it('copies directory into existing destination with -r', async () => { - let root: string = path.join(getTestTemp(), 'cp_with_-r_existing_dest'); - let sourceFolder: string = path.join(root, 'cp_source'); - let sourceFile: string = path.join(sourceFolder, 'cp_source_file'); - - let targetFolder: string = path.join(root, 'cp_target'); - let targetFile: string = path.join(targetFolder, 'cp_source', 'cp_source_file'); - await io.mkdirP(sourceFolder); - fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); - await io.mkdirP(targetFolder); - await io.cp(sourceFolder, targetFolder, {recursive: true}); - - expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); - }); - - it('copies directory into non-existing destination with -r', async () => { - let root: string = path.join(getTestTemp(), 'cp_with_-r_nonexisting_dest'); - let sourceFolder: string = path.join(root, 'cp_source'); - let sourceFile: string = path.join(sourceFolder, 'cp_source_file'); - - let targetFolder: string = path.join(root, 'cp_target'); - let targetFile: string = path.join(targetFolder, 'cp_source_file'); - await io.mkdirP(sourceFolder); - fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); - await io.cp(sourceFolder, targetFolder, {recursive: true}); - - expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); - }); - - it('tries to copy directory without -r', async () => { - let root: string = path.join(getTestTemp(), 'cp_without_-r'); - let sourceFolder: string = path.join(root, 'cp_source'); - let sourceFile: string = path.join(sourceFolder, 'cp_source_file'); - - let targetFolder: string = path.join(root, 'cp_target'); - let targetFile: string = path.join(targetFolder, 'cp_source', 'cp_source_file'); - await io.mkdirP(sourceFolder); - fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); - - let thrown = false; - try { - await io.cp(sourceFolder, targetFolder); - } - catch (err) { - thrown = true; - } - expect(thrown).toBe(true); - expect(fs.existsSync(targetFile)).toBe(false); - }); -}); + it('copies file with no flags', async () => { + let root: string = path.join(getTestTemp(), 'cp_with_no_flags') + let sourceFile: string = path.join(root, 'cp_source') + let targetFile: string = path.join(root, 'cp_target') + await io.mkdirP(root) + fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + + await io.cp(sourceFile, targetFile) + + expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + }) + + it('copies file using -f', async () => { + let root: string = path.join(path.join(__dirname, '_temp'), 'cp_with_-f') + let sourceFile: string = path.join(root, 'cp_source') + let targetFile: string = path.join(root, 'cp_target') + await io.mkdirP(root) + fs.writeFileSync(sourceFile, 'test file content') + + await io.cp(sourceFile, targetFile, {recursive: false, force: true}) + + expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + }) + + it('try copying to existing file with -n', async () => { + let root: string = path.join(getTestTemp(), 'cp_to_existing') + let sourceFile: string = path.join(root, 'cp_source') + let targetFile: string = path.join(root, 'cp_target') + await io.mkdirP(root) + fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + fs.writeFileSync(targetFile, 'correct content', {encoding: 'utf8'}) + let failed = false + try { + await io.cp(sourceFile, targetFile, {recursive: false, force: false}) + } catch { + failed = true + } + expect(failed).toBe(true) + + expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + 'correct content' + ) + }) + + it('copies directory into existing destination with -r', async () => { + let root: string = path.join(getTestTemp(), 'cp_with_-r_existing_dest') + let sourceFolder: string = path.join(root, 'cp_source') + let sourceFile: string = path.join(sourceFolder, 'cp_source_file') + + let targetFolder: string = path.join(root, 'cp_target') + let targetFile: string = path.join( + targetFolder, + 'cp_source', + 'cp_source_file' + ) + await io.mkdirP(sourceFolder) + fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await io.mkdirP(targetFolder) + await io.cp(sourceFolder, targetFolder, {recursive: true}) + + expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + }) + + it('copies directory into non-existing destination with -r', async () => { + let root: string = path.join(getTestTemp(), 'cp_with_-r_nonexisting_dest') + let sourceFolder: string = path.join(root, 'cp_source') + let sourceFile: string = path.join(sourceFolder, 'cp_source_file') + + let targetFolder: string = path.join(root, 'cp_target') + let targetFile: string = path.join(targetFolder, 'cp_source_file') + await io.mkdirP(sourceFolder) + fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await io.cp(sourceFolder, targetFolder, {recursive: true}) + + expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + }) + + it('tries to copy directory without -r', async () => { + let root: string = path.join(getTestTemp(), 'cp_without_-r') + let sourceFolder: string = path.join(root, 'cp_source') + let sourceFile: string = path.join(sourceFolder, 'cp_source_file') + + let targetFolder: string = path.join(root, 'cp_target') + let targetFile: string = path.join( + targetFolder, + 'cp_source', + 'cp_source_file' + ) + await io.mkdirP(sourceFolder) + fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + + let thrown = false + try { + await io.cp(sourceFolder, targetFolder) + } catch (err) { + thrown = true + } + expect(thrown).toBe(true) + expect(fs.existsSync(targetFile)).toBe(false) + }) +}) describe('mv', () => { - it('moves file with no flags', async () => { - let root: string = path.join(getTestTemp(), ' mv_with_no_flags'); - let sourceFile: string = path.join(root, ' mv_source'); - let targetFile: string = path.join(root, ' mv_target'); - await io.mkdirP(root); - fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); - - await io.mv(sourceFile, targetFile); - - expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); - expect(fs.existsSync(sourceFile)).toBe(false); - }); - - it('moves file using -f', async () => { - let root: string = path.join(path.join(__dirname, '_temp'), ' mv_with_-f'); - let sourceFile: string = path.join(root, ' mv_source'); - let targetFile: string = path.join(root, ' mv_target'); - await io.mkdirP(root); - fs.writeFileSync(sourceFile, 'test file content'); - - await io.mv(sourceFile, targetFile); - - expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); - expect(fs.existsSync(sourceFile)).toBe(false); - }); - - it('try moving to existing file with -n', async () => { - let root: string = path.join(getTestTemp(), ' mv_to_existing'); - let sourceFile: string = path.join(root, ' mv_source'); - let targetFile: string = path.join(root, ' mv_target'); - await io.mkdirP(root); - fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); - fs.writeFileSync(targetFile, 'correct content', { encoding: 'utf8' }); - let failed = false - try { - await io.mv(sourceFile, targetFile, {force: false}); - } - catch { - failed = true; - } - expect(failed).toBe(true); - - expect(fs.readFileSync(sourceFile, { encoding: 'utf8' })).toBe('test file content'); - expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('correct content'); - }); - - it('moves directory into existing destination with -r', async () => { - let root: string = path.join(getTestTemp(), ' mv_with_-r_existing_dest'); - let sourceFolder: string = path.join(root, ' mv_source'); - let sourceFile: string = path.join(sourceFolder, ' mv_source_file'); - - let targetFolder: string = path.join(root, ' mv_target'); - let targetFile: string = path.join(targetFolder, ' mv_source', ' mv_source_file'); - await io.mkdirP(sourceFolder); - fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); - await io.mkdirP(targetFolder); - await io.mv(sourceFolder, targetFolder, {recursive: true}); - - expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); - expect(fs.existsSync(sourceFile)).toBe(false); - }); - - it('moves directory into non-existing destination with -r', async () => { - let root: string = path.join(getTestTemp(), ' mv_with_-r_nonexisting_dest'); - let sourceFolder: string = path.join(root, ' mv_source'); - let sourceFile: string = path.join(sourceFolder, ' mv_source_file'); - - let targetFolder: string = path.join(root, ' mv_target'); - let targetFile: string = path.join(targetFolder, ' mv_source_file'); - await io.mkdirP(sourceFolder); - fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); - await io.mv(sourceFolder, targetFolder, {recursive: true}); - - expect(fs.readFileSync(targetFile, { encoding: 'utf8' })).toBe('test file content'); - expect(fs.existsSync(sourceFile)).toBe(false); - }); - - it('tries to move directory without -r', async () => { - let root: string = path.join(getTestTemp(), 'mv_without_-r'); - let sourceFolder: string = path.join(root, 'mv_source'); - let sourceFile: string = path.join(sourceFolder, 'mv_source_file'); - - let targetFolder: string = path.join(root, 'mv_target'); - let targetFile: string = path.join(targetFolder, 'mv_source', 'mv_source_file'); - await io.mkdirP(sourceFolder); - fs.writeFileSync(sourceFile, 'test file content', { encoding: 'utf8' }); - - let thrown = false; - try { - await io.mv(sourceFolder, targetFolder); - } - catch (err) { - thrown = true; - } + it('moves file with no flags', async () => { + let root: string = path.join(getTestTemp(), ' mv_with_no_flags') + let sourceFile: string = path.join(root, ' mv_source') + let targetFile: string = path.join(root, ' mv_target') + await io.mkdirP(root) + fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + + await io.mv(sourceFile, targetFile) + + expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + expect(fs.existsSync(sourceFile)).toBe(false) + }) + + it('moves file using -f', async () => { + let root: string = path.join(path.join(__dirname, '_temp'), ' mv_with_-f') + let sourceFile: string = path.join(root, ' mv_source') + let targetFile: string = path.join(root, ' mv_target') + await io.mkdirP(root) + fs.writeFileSync(sourceFile, 'test file content') + + await io.mv(sourceFile, targetFile) + + expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + expect(fs.existsSync(sourceFile)).toBe(false) + }) + + it('try moving to existing file with -n', async () => { + let root: string = path.join(getTestTemp(), ' mv_to_existing') + let sourceFile: string = path.join(root, ' mv_source') + let targetFile: string = path.join(root, ' mv_target') + await io.mkdirP(root) + fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + fs.writeFileSync(targetFile, 'correct content', {encoding: 'utf8'}) + let failed = false + try { + await io.mv(sourceFile, targetFile, {force: false}) + } catch { + failed = true + } + expect(failed).toBe(true) + + expect(fs.readFileSync(sourceFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + 'correct content' + ) + }) + + it('moves directory into existing destination with -r', async () => { + let root: string = path.join(getTestTemp(), ' mv_with_-r_existing_dest') + let sourceFolder: string = path.join(root, ' mv_source') + let sourceFile: string = path.join(sourceFolder, ' mv_source_file') + + let targetFolder: string = path.join(root, ' mv_target') + let targetFile: string = path.join( + targetFolder, + ' mv_source', + ' mv_source_file' + ) + await io.mkdirP(sourceFolder) + fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await io.mkdirP(targetFolder) + await io.mv(sourceFolder, targetFolder, {recursive: true}) + + expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + expect(fs.existsSync(sourceFile)).toBe(false) + }) + + it('moves directory into non-existing destination with -r', async () => { + let root: string = path.join(getTestTemp(), ' mv_with_-r_nonexisting_dest') + let sourceFolder: string = path.join(root, ' mv_source') + let sourceFile: string = path.join(sourceFolder, ' mv_source_file') + + let targetFolder: string = path.join(root, ' mv_target') + let targetFile: string = path.join(targetFolder, ' mv_source_file') + await io.mkdirP(sourceFolder) + fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await io.mv(sourceFolder, targetFolder, {recursive: true}) + + expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + expect(fs.existsSync(sourceFile)).toBe(false) + }) + + it('tries to move directory without -r', async () => { + let root: string = path.join(getTestTemp(), 'mv_without_-r') + let sourceFolder: string = path.join(root, 'mv_source') + let sourceFile: string = path.join(sourceFolder, 'mv_source_file') + + let targetFolder: string = path.join(root, 'mv_target') + let targetFile: string = path.join( + targetFolder, + 'mv_source', + 'mv_source_file' + ) + await io.mkdirP(sourceFolder) + fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + + let thrown = false + try { + await io.mv(sourceFolder, targetFolder) + } catch (err) { + thrown = true + } - expect(thrown).toBe(true); - expect(fs.existsSync(sourceFile)).toBe(true); - expect(fs.existsSync(targetFile)).toBe(false); - }); -}); + expect(thrown).toBe(true) + expect(fs.existsSync(sourceFile)).toBe(true) + expect(fs.existsSync(targetFile)).toBe(false) + }) +}) describe('rmRF', () => { - it('removes single folder with rmRF', async () => { - var testPath = path.join(getTestTemp(), 'testFolder'); - - await io.mkdirP(testPath); - expect(fs.existsSync(testPath)).toBe(true); + it('removes single folder with rmRF', async () => { + var testPath = path.join(getTestTemp(), 'testFolder') - await io.rmRF(testPath); - expect(fs.existsSync(testPath)).toBe(false); - }); + await io.mkdirP(testPath) + expect(fs.existsSync(testPath)).toBe(true) - it('removes recursive folders with rmRF', async () => { - var testPath = path.join(getTestTemp(), 'testDir1'); - var testPath2 = path.join(testPath, 'testDir2'); - await io.mkdirP(testPath2); + await io.rmRF(testPath) + expect(fs.existsSync(testPath)).toBe(false) + }) - expect(fs.existsSync(testPath)).toBe(true); - expect(fs.existsSync(testPath2)).toBe(true); + it('removes recursive folders with rmRF', async () => { + var testPath = path.join(getTestTemp(), 'testDir1') + var testPath2 = path.join(testPath, 'testDir2') + await io.mkdirP(testPath2) - await io.rmRF(testPath); - expect(fs.existsSync(testPath)).toBe(false); - expect(fs.existsSync(testPath2)).toBe(false); - }); + expect(fs.existsSync(testPath)).toBe(true) + expect(fs.existsSync(testPath2)).toBe(true) - it('removes folder with locked file with rmRF', async () => { - var testPath = path.join(getTestTemp(), 'testFolder'); - await io.mkdirP(testPath); - expect(fs.existsSync(testPath)).toBe(true); + await io.rmRF(testPath) + expect(fs.existsSync(testPath)).toBe(false) + expect(fs.existsSync(testPath2)).toBe(false) + }) - // can't remove folder with locked file on windows - var filePath = path.join(testPath, 'file.txt'); - fs.appendFileSync(filePath, 'some data'); - expect(fs.existsSync(filePath)).toBe(true); + it('removes folder with locked file with rmRF', async () => { + var testPath = path.join(getTestTemp(), 'testFolder') + await io.mkdirP(testPath) + expect(fs.existsSync(testPath)).toBe(true) - var fd = fs.openSync(filePath, 'r'); - - var worked = false; - try { - await io.rmRF(testPath); - worked = true; - } - catch (err) { } + // can't remove folder with locked file on windows + var filePath = path.join(testPath, 'file.txt') + fs.appendFileSync(filePath, 'some data') + expect(fs.existsSync(filePath)).toBe(true) - if (os.platform() === 'win32') { - expect(worked).toBe(false); - expect(fs.existsSync(testPath)).toBe(true); - } - else { - expect(worked).toBe(true); - expect(fs.existsSync(testPath)).toBe(false); - } + var fd = fs.openSync(filePath, 'r') - fs.closeSync(fd); - await io.rmRF(testPath); - expect(fs.existsSync(testPath)).toBe(false); - }); - - it('removes folder that doesnt exist with rmRF', async () => { - var testPath = path.join(getTestTemp(), 'testFolder'); - expect(fs.existsSync(testPath)).toBe(false); - - await io.rmRF(testPath); - expect(fs.existsSync(testPath)).toBe(false); - }); - - it('removes file with rmRF', async () => { - let file: string = path.join(getTestTemp(), 'rmRF_file'); - fs.writeFileSync(file, 'test file content'); - expect(fs.existsSync(file)).toBe(true); - await io.rmRF(file); - expect(fs.existsSync(file)).toBe(false); - }); - - it('removes hidden folder with rmRF', async () => { - let directory: string = path.join(getTestTemp(), '.rmRF_directory'); - await createHiddenDirectory(directory); - expect(fs.existsSync(directory)).toBe(true); - await io.rmRF(directory); - expect(fs.existsSync(directory)).toBe(false); - }); - - it('removes hidden file with rmRF', async () => { - let file: string = path.join(getTestTemp(), '.rmRF_file'); - fs.writeFileSync(file, 'test file content'); - expect(fs.existsSync(file)).toBe(true); - await io.rmRF(file); - expect(fs.existsSync(file)).toBe(false); - }); - - it('removes symlink folder with rmRF', async () => { - // create the following layout: - // real_directory - // real_directory/real_file - // symlink_directory -> real_directory - let root: string = path.join(getTestTemp(), 'rmRF_sym_dir_test'); - let realDirectory: string = path.join(root, 'real_directory'); - let realFile: string = path.join(root, 'real_directory', 'real_file'); - let symlinkDirectory: string = path.join(root, 'symlink_directory'); - await io.mkdirP(realDirectory); - fs.writeFileSync(realFile, 'test file content'); - createSymlinkDir(realDirectory, symlinkDirectory); - expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true); - - await io.rmRF(symlinkDirectory); - expect(fs.existsSync(realDirectory)).toBe(true); - expect(fs.existsSync(realFile)).toBe(true); - expect(fs.existsSync(symlinkDirectory)).toBe(false); - }); - - // creating a symlink to a file on Windows requires elevated - if (os.platform() != 'win32') { - it('removes symlink file with rmRF', async () => { - // create the following layout: - // real_file - // symlink_file -> real_file - let root: string = path.join(getTestTemp(), 'rmRF_sym_file_test'); - let realFile: string = path.join(root, 'real_file'); - let symlinkFile: string = path.join(root, 'symlink_file'); - await io.mkdirP(root); - fs.writeFileSync(realFile, 'test file content'); - fs.symlinkSync(realFile, symlinkFile); - expect(fs.readFileSync(symlinkFile, { encoding: 'utf8' })).toBe('test file content'); - - await io.rmRF(symlinkFile); - expect(fs.existsSync(realFile)).toBe(true); - expect(fs.existsSync(symlinkFile)).toBe(false); - }); - - it('removes symlink file with missing source using rmRF', async () => { - // create the following layout: - // real_file - // symlink_file -> real_file - let root: string = path.join(getTestTemp(), 'rmRF_sym_file_missing_source_test'); - let realFile: string = path.join(root, 'real_file'); - let symlinkFile: string = path.join(root, 'symlink_file'); - await io.mkdirP(root); - fs.writeFileSync(realFile, 'test file content'); - fs.symlinkSync(realFile, symlinkFile); - expect(fs.readFileSync(symlinkFile, { encoding: 'utf8' })).toBe('test file content'); - - // remove the real file - fs.unlinkSync(realFile); - expect(fs.lstatSync(symlinkFile).isSymbolicLink()).toBe(true); - - // remove the symlink file - await io.rmRF(symlinkFile); - let errcode: string = ''; - try { - fs.lstatSync(symlinkFile); - } - catch (err) { - errcode = err.code; - } - - expect(errcode).toBe('ENOENT'); - }); - - it('removes symlink level 2 file with rmRF', async () => { - // create the following layout: - // real_file - // symlink_file -> real_file - // symlink_level_2_file -> symlink_file - let root: string = path.join(getTestTemp(), 'rmRF_sym_level_2_file_test'); - let realFile: string = path.join(root, 'real_file'); - let symlinkFile: string = path.join(root, 'symlink_file'); - let symlinkLevel2File: string = path.join(root, 'symlink_level_2_file'); - await io.mkdirP(root); - fs.writeFileSync(realFile, 'test file content'); - fs.symlinkSync(realFile, symlinkFile); - fs.symlinkSync(symlinkFile, symlinkLevel2File); - expect(fs.readFileSync(symlinkLevel2File, { encoding: 'utf8' })).toBe('test file content'); - - await io.rmRF(symlinkLevel2File); - expect(fs.existsSync(realFile)).toBe(true); - expect(fs.existsSync(symlinkFile)).toBe(true); - expect(fs.existsSync(symlinkLevel2File)).toBe(false); - }); - - it('removes nested symlink file with rmRF', async () => { - // create the following layout: - // real_directory - // real_directory/real_file - // outer_directory - // outer_directory/symlink_file -> real_file - let root: string = path.join(getTestTemp(), 'rmRF_sym_nest_file_test'); - let realDirectory: string = path.join(root, 'real_directory'); - let realFile: string = path.join(root, 'real_directory', 'real_file'); - let outerDirectory: string = path.join(root, 'outer_directory'); - let symlinkFile: string = path.join(root, 'outer_directory', 'symlink_file'); - await io.mkdirP(realDirectory); - fs.writeFileSync(realFile, 'test file content'); - await io.mkdirP(outerDirectory); - fs.symlinkSync(realFile, symlinkFile); - expect(fs.readFileSync(symlinkFile, { encoding: 'utf8' })).toBe('test file content'); - - await io.rmRF(outerDirectory); - expect(fs.existsSync(realDirectory)).toBe(true); - expect(fs.existsSync(realFile)).toBe(true); - expect(fs.existsSync(symlinkFile)).toBe(false); - expect(fs.existsSync(outerDirectory)).toBe(false); - }); - - it('removes deeply nested symlink file with rmRF', async () => { - // create the following layout: - // real_directory - // real_directory/real_file - // outer_directory - // outer_directory/nested_directory - // outer_directory/nested_directory/symlink_file -> real_file - let root: string = path.join(getTestTemp(), 'rmRF_sym_deep_nest_file_test'); - let realDirectory: string = path.join(root, 'real_directory'); - let realFile: string = path.join(root, 'real_directory', 'real_file'); - let outerDirectory: string = path.join(root, 'outer_directory'); - let nestedDirectory: string = path.join(root, 'outer_directory', 'nested_directory'); - let symlinkFile: string = path.join(root, 'outer_directory', 'nested_directory', 'symlink_file'); - await io.mkdirP(realDirectory); - fs.writeFileSync(realFile, 'test file content'); - await io.mkdirP(nestedDirectory); - fs.symlinkSync(realFile, symlinkFile); - expect(fs.readFileSync(symlinkFile, { encoding: 'utf8' })).toBe('test file content'); - - await io.rmRF(outerDirectory); - expect(fs.existsSync(realDirectory)).toBe(true); - expect(fs.existsSync(realFile)).toBe(true); - expect(fs.existsSync(symlinkFile)).toBe(false); - expect(fs.existsSync(outerDirectory)).toBe(false); - }); + var worked = false + try { + await io.rmRF(testPath) + worked = true + } catch (err) {} + + if (os.platform() === 'win32') { + expect(worked).toBe(false) + expect(fs.existsSync(testPath)).toBe(true) + } else { + expect(worked).toBe(true) + expect(fs.existsSync(testPath)).toBe(false) } - it('removes symlink folder with missing source using rmRF', async () => { - // create the following layout: - // real_directory - // real_directory/real_file - // symlink_directory -> real_directory - let root: string = path.join(getTestTemp(), 'rmRF_sym_dir_miss_src_test'); - let realDirectory: string = path.join(root, 'real_directory'); - let realFile: string = path.join(root, 'real_directory', 'real_file'); - let symlinkDirectory: string = path.join(root, 'symlink_directory'); - await io.mkdirP(realDirectory); - fs.writeFileSync(realFile, 'test file content'); - createSymlinkDir(realDirectory, symlinkDirectory); - expect(fs.existsSync(symlinkDirectory)).toBe(true); - - // remove the real directory - fs.unlinkSync(realFile); - fs.rmdirSync(realDirectory); - let errcode: string = ''; - try { - fs.statSync(symlinkDirectory); - } - catch (err){ - errcode = err.code; - } + fs.closeSync(fd) + await io.rmRF(testPath) + expect(fs.existsSync(testPath)).toBe(false) + }) + + it('removes folder that doesnt exist with rmRF', async () => { + var testPath = path.join(getTestTemp(), 'testFolder') + expect(fs.existsSync(testPath)).toBe(false) + + await io.rmRF(testPath) + expect(fs.existsSync(testPath)).toBe(false) + }) + + it('removes file with rmRF', async () => { + let file: string = path.join(getTestTemp(), 'rmRF_file') + fs.writeFileSync(file, 'test file content') + expect(fs.existsSync(file)).toBe(true) + await io.rmRF(file) + expect(fs.existsSync(file)).toBe(false) + }) + + it('removes hidden folder with rmRF', async () => { + let directory: string = path.join(getTestTemp(), '.rmRF_directory') + await createHiddenDirectory(directory) + expect(fs.existsSync(directory)).toBe(true) + await io.rmRF(directory) + expect(fs.existsSync(directory)).toBe(false) + }) + + it('removes hidden file with rmRF', async () => { + let file: string = path.join(getTestTemp(), '.rmRF_file') + fs.writeFileSync(file, 'test file content') + expect(fs.existsSync(file)).toBe(true) + await io.rmRF(file) + expect(fs.existsSync(file)).toBe(false) + }) + + it('removes symlink folder with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // symlink_directory -> real_directory + let root: string = path.join(getTestTemp(), 'rmRF_sym_dir_test') + let realDirectory: string = path.join(root, 'real_directory') + let realFile: string = path.join(root, 'real_directory', 'real_file') + let symlinkDirectory: string = path.join(root, 'symlink_directory') + await io.mkdirP(realDirectory) + fs.writeFileSync(realFile, 'test file content') + createSymlinkDir(realDirectory, symlinkDirectory) + expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true) + + await io.rmRF(symlinkDirectory) + expect(fs.existsSync(realDirectory)).toBe(true) + expect(fs.existsSync(realFile)).toBe(true) + expect(fs.existsSync(symlinkDirectory)).toBe(false) + }) + + // creating a symlink to a file on Windows requires elevated + if (os.platform() != 'win32') { + it('removes symlink file with rmRF', async () => { + // create the following layout: + // real_file + // symlink_file -> real_file + let root: string = path.join(getTestTemp(), 'rmRF_sym_file_test') + let realFile: string = path.join(root, 'real_file') + let symlinkFile: string = path.join(root, 'symlink_file') + await io.mkdirP(root) + fs.writeFileSync(realFile, 'test file content') + fs.symlinkSync(realFile, symlinkFile) + expect(fs.readFileSync(symlinkFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + + await io.rmRF(symlinkFile) + expect(fs.existsSync(realFile)).toBe(true) + expect(fs.existsSync(symlinkFile)).toBe(false) + }) + + it('removes symlink file with missing source using rmRF', async () => { + // create the following layout: + // real_file + // symlink_file -> real_file + let root: string = path.join( + getTestTemp(), + 'rmRF_sym_file_missing_source_test' + ) + let realFile: string = path.join(root, 'real_file') + let symlinkFile: string = path.join(root, 'symlink_file') + await io.mkdirP(root) + fs.writeFileSync(realFile, 'test file content') + fs.symlinkSync(realFile, symlinkFile) + expect(fs.readFileSync(symlinkFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + + // remove the real file + fs.unlinkSync(realFile) + expect(fs.lstatSync(symlinkFile).isSymbolicLink()).toBe(true) + + // remove the symlink file + await io.rmRF(symlinkFile) + let errcode: string = '' + try { + fs.lstatSync(symlinkFile) + } catch (err) { + errcode = err.code + } + + expect(errcode).toBe('ENOENT') + }) + + it('removes symlink level 2 file with rmRF', async () => { + // create the following layout: + // real_file + // symlink_file -> real_file + // symlink_level_2_file -> symlink_file + let root: string = path.join(getTestTemp(), 'rmRF_sym_level_2_file_test') + let realFile: string = path.join(root, 'real_file') + let symlinkFile: string = path.join(root, 'symlink_file') + let symlinkLevel2File: string = path.join(root, 'symlink_level_2_file') + await io.mkdirP(root) + fs.writeFileSync(realFile, 'test file content') + fs.symlinkSync(realFile, symlinkFile) + fs.symlinkSync(symlinkFile, symlinkLevel2File) + expect(fs.readFileSync(symlinkLevel2File, {encoding: 'utf8'})).toBe( + 'test file content' + ) + + await io.rmRF(symlinkLevel2File) + expect(fs.existsSync(realFile)).toBe(true) + expect(fs.existsSync(symlinkFile)).toBe(true) + expect(fs.existsSync(symlinkLevel2File)).toBe(false) + }) + + it('removes nested symlink file with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // outer_directory + // outer_directory/symlink_file -> real_file + let root: string = path.join(getTestTemp(), 'rmRF_sym_nest_file_test') + let realDirectory: string = path.join(root, 'real_directory') + let realFile: string = path.join(root, 'real_directory', 'real_file') + let outerDirectory: string = path.join(root, 'outer_directory') + let symlinkFile: string = path.join( + root, + 'outer_directory', + 'symlink_file' + ) + await io.mkdirP(realDirectory) + fs.writeFileSync(realFile, 'test file content') + await io.mkdirP(outerDirectory) + fs.symlinkSync(realFile, symlinkFile) + expect(fs.readFileSync(symlinkFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + + await io.rmRF(outerDirectory) + expect(fs.existsSync(realDirectory)).toBe(true) + expect(fs.existsSync(realFile)).toBe(true) + expect(fs.existsSync(symlinkFile)).toBe(false) + expect(fs.existsSync(outerDirectory)).toBe(false) + }) + + it('removes deeply nested symlink file with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // outer_directory + // outer_directory/nested_directory + // outer_directory/nested_directory/symlink_file -> real_file + let root: string = path.join( + getTestTemp(), + 'rmRF_sym_deep_nest_file_test' + ) + let realDirectory: string = path.join(root, 'real_directory') + let realFile: string = path.join(root, 'real_directory', 'real_file') + let outerDirectory: string = path.join(root, 'outer_directory') + let nestedDirectory: string = path.join( + root, + 'outer_directory', + 'nested_directory' + ) + let symlinkFile: string = path.join( + root, + 'outer_directory', + 'nested_directory', + 'symlink_file' + ) + await io.mkdirP(realDirectory) + fs.writeFileSync(realFile, 'test file content') + await io.mkdirP(nestedDirectory) + fs.symlinkSync(realFile, symlinkFile) + expect(fs.readFileSync(symlinkFile, {encoding: 'utf8'})).toBe( + 'test file content' + ) + + await io.rmRF(outerDirectory) + expect(fs.existsSync(realDirectory)).toBe(true) + expect(fs.existsSync(realFile)).toBe(true) + expect(fs.existsSync(symlinkFile)).toBe(false) + expect(fs.existsSync(outerDirectory)).toBe(false) + }) + } + + it('removes symlink folder with missing source using rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // symlink_directory -> real_directory + let root: string = path.join(getTestTemp(), 'rmRF_sym_dir_miss_src_test') + let realDirectory: string = path.join(root, 'real_directory') + let realFile: string = path.join(root, 'real_directory', 'real_file') + let symlinkDirectory: string = path.join(root, 'symlink_directory') + await io.mkdirP(realDirectory) + fs.writeFileSync(realFile, 'test file content') + createSymlinkDir(realDirectory, symlinkDirectory) + expect(fs.existsSync(symlinkDirectory)).toBe(true) + + // remove the real directory + fs.unlinkSync(realFile) + fs.rmdirSync(realDirectory) + let errcode: string = '' + try { + fs.statSync(symlinkDirectory) + } catch (err) { + errcode = err.code + } - expect(errcode).toBe('ENOENT'); + expect(errcode).toBe('ENOENT') - // lstat shouldn't throw - fs.lstatSync(symlinkDirectory); + // lstat shouldn't throw + fs.lstatSync(symlinkDirectory) - // remove the symlink directory - await io.rmRF(symlinkDirectory); - errcode = ''; - try { - fs.lstatSync(symlinkDirectory); - } - catch (err) { - errcode = err.code; - } + // remove the symlink directory + await io.rmRF(symlinkDirectory) + errcode = '' + try { + fs.lstatSync(symlinkDirectory) + } catch (err) { + errcode = err.code + } - expect(errcode).toBe('ENOENT'); - }); - - it('removes symlink level 2 folder with rmRF', async () => { - // create the following layout: - // real_directory - // real_directory/real_file - // symlink_directory -> real_directory - // symlink_level_2_directory -> symlink_directory - let root: string = path.join(getTestTemp(), 'rmRF_sym_level_2_directory_test'); - let realDirectory: string = path.join(root, 'real_directory'); - let realFile: string = path.join(realDirectory, 'real_file'); - let symlinkDirectory: string = path.join(root, 'symlink_directory'); - let symlinkLevel2Directory: string = path.join(root, 'symlink_level_2_directory'); - await io.mkdirP(realDirectory); - fs.writeFileSync(realFile, 'test file content'); - createSymlinkDir(realDirectory, symlinkDirectory); - createSymlinkDir(symlinkDirectory, symlinkLevel2Directory); - expect(fs.readFileSync(path.join(symlinkDirectory, 'real_file'), { encoding: 'utf8' })).toBe('test file content'); - if (os.platform() == 'win32') { - expect(fs.readlinkSync(symlinkLevel2Directory)).toBe(symlinkDirectory + '\\'); - } - else { - expect(fs.readlinkSync(symlinkLevel2Directory)).toBe(symlinkDirectory); - } + expect(errcode).toBe('ENOENT') + }) + + it('removes symlink level 2 folder with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // symlink_directory -> real_directory + // symlink_level_2_directory -> symlink_directory + let root: string = path.join( + getTestTemp(), + 'rmRF_sym_level_2_directory_test' + ) + let realDirectory: string = path.join(root, 'real_directory') + let realFile: string = path.join(realDirectory, 'real_file') + let symlinkDirectory: string = path.join(root, 'symlink_directory') + let symlinkLevel2Directory: string = path.join( + root, + 'symlink_level_2_directory' + ) + await io.mkdirP(realDirectory) + fs.writeFileSync(realFile, 'test file content') + createSymlinkDir(realDirectory, symlinkDirectory) + createSymlinkDir(symlinkDirectory, symlinkLevel2Directory) + expect( + fs.readFileSync(path.join(symlinkDirectory, 'real_file'), { + encoding: 'utf8' + }) + ).toBe('test file content') + if (os.platform() == 'win32') { + expect(fs.readlinkSync(symlinkLevel2Directory)).toBe( + symlinkDirectory + '\\' + ) + } else { + expect(fs.readlinkSync(symlinkLevel2Directory)).toBe(symlinkDirectory) + } - await io.rmRF(symlinkLevel2Directory); - expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true); - expect(fs.existsSync(symlinkLevel2Directory)).toBe(false); - }); - - it('removes nested symlink folder with rmRF', async () => { - // create the following layout: - // real_directory - // real_directory/real_file - // outer_directory - // outer_directory/symlink_directory -> real_directory - let root: string = path.join(getTestTemp(), 'rmRF_sym_nest_dir_test'); - let realDirectory: string = path.join(root, 'real_directory'); - let realFile: string = path.join(root, 'real_directory', 'real_file'); - let outerDirectory: string = path.join(root, 'outer_directory'); - let symlinkDirectory: string = path.join(root, 'outer_directory', 'symlink_directory'); - await io.mkdirP(realDirectory); - fs.writeFileSync(realFile, 'test file content'); - await io.mkdirP(outerDirectory); - createSymlinkDir(realDirectory, symlinkDirectory); - expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true); - - await io.rmRF(outerDirectory); - expect(fs.existsSync(realDirectory)).toBe(true); - expect(fs.existsSync(realFile)).toBe(true); - expect(fs.existsSync(symlinkDirectory)).toBe(false); - expect(fs.existsSync(outerDirectory)).toBe(false); - }); - - it('removes deeply nested symlink folder with rmRF', async () => { - // create the following layout: - // real_directory - // real_directory/real_file - // outer_directory - // outer_directory/nested_directory - // outer_directory/nested_directory/symlink_directory -> real_directory - let root: string = path.join(getTestTemp(), 'rmRF_sym_deep_nest_dir_test'); - let realDirectory: string = path.join(root, 'real_directory'); - let realFile: string = path.join(root, 'real_directory', 'real_file'); - let outerDirectory: string = path.join(root, 'outer_directory'); - let nestedDirectory: string = path.join(root, 'outer_directory', 'nested_directory'); - let symlinkDirectory: string = path.join(root, 'outer_directory', 'nested_directory', 'symlink_directory'); - await io.mkdirP(realDirectory); - fs.writeFileSync(realFile, 'test file content'); - await io.mkdirP(nestedDirectory); - createSymlinkDir(realDirectory, symlinkDirectory); - expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true); - - await io.rmRF(outerDirectory); - expect(fs.existsSync(realDirectory)).toBe(true); - expect(fs.existsSync(realFile)).toBe(true); - expect(fs.existsSync(symlinkDirectory)).toBe(false); - expect(fs.existsSync(outerDirectory)).toBe(false); - }); - - it('removes hidden file with rmRF', async () => { - let file: string = path.join(getTestTemp(), '.rmRF_file'); - await io.mkdirP(path.dirname(file)); - await createHiddenFile(file, 'test file content'); - expect(fs.existsSync(file)).toBe(true); - await io.rmRF(file); - expect(fs.existsSync(file)).toBe(false); - }); -}); + await io.rmRF(symlinkLevel2Directory) + expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true) + expect(fs.existsSync(symlinkLevel2Directory)).toBe(false) + }) + + it('removes nested symlink folder with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // outer_directory + // outer_directory/symlink_directory -> real_directory + let root: string = path.join(getTestTemp(), 'rmRF_sym_nest_dir_test') + let realDirectory: string = path.join(root, 'real_directory') + let realFile: string = path.join(root, 'real_directory', 'real_file') + let outerDirectory: string = path.join(root, 'outer_directory') + let symlinkDirectory: string = path.join( + root, + 'outer_directory', + 'symlink_directory' + ) + await io.mkdirP(realDirectory) + fs.writeFileSync(realFile, 'test file content') + await io.mkdirP(outerDirectory) + createSymlinkDir(realDirectory, symlinkDirectory) + expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true) + + await io.rmRF(outerDirectory) + expect(fs.existsSync(realDirectory)).toBe(true) + expect(fs.existsSync(realFile)).toBe(true) + expect(fs.existsSync(symlinkDirectory)).toBe(false) + expect(fs.existsSync(outerDirectory)).toBe(false) + }) + + it('removes deeply nested symlink folder with rmRF', async () => { + // create the following layout: + // real_directory + // real_directory/real_file + // outer_directory + // outer_directory/nested_directory + // outer_directory/nested_directory/symlink_directory -> real_directory + let root: string = path.join(getTestTemp(), 'rmRF_sym_deep_nest_dir_test') + let realDirectory: string = path.join(root, 'real_directory') + let realFile: string = path.join(root, 'real_directory', 'real_file') + let outerDirectory: string = path.join(root, 'outer_directory') + let nestedDirectory: string = path.join( + root, + 'outer_directory', + 'nested_directory' + ) + let symlinkDirectory: string = path.join( + root, + 'outer_directory', + 'nested_directory', + 'symlink_directory' + ) + await io.mkdirP(realDirectory) + fs.writeFileSync(realFile, 'test file content') + await io.mkdirP(nestedDirectory) + createSymlinkDir(realDirectory, symlinkDirectory) + expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true) + + await io.rmRF(outerDirectory) + expect(fs.existsSync(realDirectory)).toBe(true) + expect(fs.existsSync(realFile)).toBe(true) + expect(fs.existsSync(symlinkDirectory)).toBe(false) + expect(fs.existsSync(outerDirectory)).toBe(false) + }) + + it('removes hidden file with rmRF', async () => { + let file: string = path.join(getTestTemp(), '.rmRF_file') + await io.mkdirP(path.dirname(file)) + await createHiddenFile(file, 'test file content') + expect(fs.existsSync(file)).toBe(true) + await io.rmRF(file) + expect(fs.existsSync(file)).toBe(false) + }) +}) describe('mkdirP', () => { - beforeAll(async () => { - await io.rmRF(getTestTemp()); - }); - - it('creates folder', async () => { - var testPath = path.join(getTestTemp(), 'mkdirTest'); - await io.mkdirP(testPath); - - expect(fs.existsSync(testPath)).toBe(true); - }); - - it('creates nested folders with mkdirP', async () => { - var testPath = path.join(getTestTemp(), 'mkdir1', 'mkdir2'); - await io.mkdirP(testPath); - - expect(fs.existsSync(testPath)).toBe(true); - }); - - it('fails if mkdirP with illegal chars', async () => { - var testPath = path.join(getTestTemp(), 'mkdir\0'); - var worked: boolean = false; - try { - await io.mkdirP(testPath); - worked = true; - } - catch (err) { - expect(fs.existsSync(testPath)).toBe(false); - } - + beforeAll(async () => { + await io.rmRF(getTestTemp()) + }) - expect(worked).toBe(false); - }); + it('creates folder', async () => { + var testPath = path.join(getTestTemp(), 'mkdirTest') + await io.mkdirP(testPath) - it('fails if mkdirP with empty path', async () => { - var worked: boolean = false; - try { - await io.mkdirP(''); - worked = true; - } - catch (err) { } - - - expect(worked).toBe(false); - }); - - it('fails if mkdirP with conflicting file path', async () => { - let testPath = path.join(getTestTemp(), 'mkdirP_conflicting_file_path'); - await io.mkdirP(getTestTemp()); - fs.writeFileSync(testPath, ''); - let worked: boolean = false; - try { - await io.mkdirP(testPath); - worked = true; - } - catch (err) { } - - expect(worked).toBe(false); - }); - - it('fails if mkdirP with conflicting parent file path', async () => { - let testPath = path.join(getTestTemp(), 'mkdirP_conflicting_parent_file_path', 'dir'); - await io.mkdirP(getTestTemp()); - fs.writeFileSync(path.dirname(testPath), ''); - let worked: boolean = false; - try { - await io.mkdirP(testPath); - worked = true; - } - catch (err) { } - - expect(worked).toBe(false); - }); - - it('no-ops if mkdirP directory exists', async () => { - let testPath = path.join(getTestTemp(), 'mkdirP_dir_exists'); - await io.mkdirP(testPath); - expect(fs.existsSync(testPath)).toBe(true); - - // Calling again shouldn't throw - await io.mkdirP(testPath); - expect(fs.existsSync(testPath)).toBe(true); - }); - - it('no-ops if mkdirP with symlink directory', async () => { - // create the following layout: - // real_dir - // real_dir/file.txt - // symlink_dir -> real_dir - let rootPath = path.join(getTestTemp(), 'mkdirP_symlink_dir'); - let realDirPath = path.join(rootPath, 'real_dir'); - let realFilePath = path.join(realDirPath, 'file.txt'); - let symlinkDirPath = path.join(rootPath, 'symlink_dir'); - await io.mkdirP(getTestTemp()); - fs.mkdirSync(rootPath); - fs.mkdirSync(realDirPath); - fs.writeFileSync(realFilePath, 'test real_dir/file.txt contet'); - createSymlinkDir(realDirPath, symlinkDirPath); - - await io.mkdirP(symlinkDirPath); - - // the file in the real directory should still be accessible via the symlink - expect(fs.lstatSync(symlinkDirPath).isSymbolicLink()).toBe(true); - expect(fs.statSync(path.join(symlinkDirPath, 'file.txt')).isFile()).toBe(true); - }); - - it('no-ops if mkdirP with parent symlink directory', async () => { - // create the following layout: - // real_dir - // real_dir/file.txt - // symlink_dir -> real_dir - let rootPath = path.join(getTestTemp(), 'mkdirP_parent_symlink_dir'); - let realDirPath = path.join(rootPath, 'real_dir'); - let realFilePath = path.join(realDirPath, 'file.txt'); - let symlinkDirPath = path.join(rootPath, 'symlink_dir'); - await io.mkdirP(getTestTemp()); - fs.mkdirSync(rootPath); - fs.mkdirSync(realDirPath); - fs.writeFileSync(realFilePath, 'test real_dir/file.txt contet'); - createSymlinkDir(realDirPath, symlinkDirPath); - - let subDirPath = path.join(symlinkDirPath, 'sub_dir'); - await io.mkdirP(subDirPath); - - // the subdirectory should be accessible via the real directory - expect(fs.lstatSync(path.join(realDirPath, 'sub_dir')).isDirectory()).toBe(true); - }); - - it('breaks if mkdirP loop out of control', async () => { - let testPath = path.join(getTestTemp(), 'mkdirP_failsafe', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'); - process.env['TEST_MKDIRP_FAILSAFE'] = '10'; - try { - await io.mkdirP(testPath); - throw new Error("directory should not have been created"); - } - catch (err) { - delete process.env['TEST_MKDIRP_FAILSAFE']; + expect(fs.existsSync(testPath)).toBe(true) + }) - // ENOENT is expected, all other errors are not - expect(err.code).toBe('ENOENT'); - } - }); -}); + it('creates nested folders with mkdirP', async () => { + var testPath = path.join(getTestTemp(), 'mkdir1', 'mkdir2') + await io.mkdirP(testPath) -describe('which', () => { - it('which() finds file name', async () => { - // create a executable file - let testPath = path.join(getTestTemp(), 'which-finds-file-name'); - await io.mkdirP(testPath); - let fileName = 'Which-Test-File'; - if (process.platform == 'win32') { - fileName += '.exe'; - } + expect(fs.existsSync(testPath)).toBe(true) + }) - let filePath = path.join(testPath, fileName); - fs.writeFileSync(filePath, ''); - if (process.platform != 'win32') { - chmod(filePath, '+x'); - } + it('fails if mkdirP with illegal chars', async () => { + var testPath = path.join(getTestTemp(), 'mkdir\0') + var worked: boolean = false + try { + await io.mkdirP(testPath) + worked = true + } catch (err) { + expect(fs.existsSync(testPath)).toBe(false) + } - let originalPath = process.env['PATH']; - try { - // update the PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; - - // exact file name - expect(await io.which(fileName)).toBe(filePath); - expect(await io.which(fileName, false)).toBe(filePath); - expect(await io.which(fileName, true)).toBe(filePath); - - if (process.platform == 'win32') { - // not case sensitive on windows - expect(await io.which('which-test-file.exe')).toBe(path.join(testPath, 'which-test-file.exe')); - expect(await io.which('WHICH-TEST-FILE.EXE')).toBe(path.join(testPath, 'WHICH-TEST-FILE.EXE')); - expect(await io.which('WHICH-TEST-FILE.EXE', false)).toBe(path.join(testPath, 'WHICH-TEST-FILE.EXE')); - expect(await io.which('WHICH-TEST-FILE.EXE', true)).toBe(path.join(testPath, 'WHICH-TEST-FILE.EXE')); - - // without extension - expect(await io.which('which-test-file')).toBe(filePath); - expect(await io.which('which-test-file', false)).toBe(filePath); - expect(await io.which('which-test-file', true)).toBe(filePath); - } - else if (process.platform == 'darwin') { - // not case sensitive on Mac - expect(await io.which(fileName.toUpperCase())).toBe(path.join(testPath, fileName.toUpperCase())); - expect(await io.which(fileName.toUpperCase(), false)).toBe(path.join(testPath, fileName.toUpperCase())); - expect(await io.which(fileName.toUpperCase(), true)).toBe(path.join(testPath, fileName.toUpperCase())); - } - else { - // case sensitive on Linux - expect(await io.which(fileName.toUpperCase()) || '').toBe(''); - } - } - finally { - process.env['PATH'] = originalPath; - } - }); - - it('which() not found', async () => { - expect(await io.which('which-test-no-such-file')).toBe(''); - expect(await io.which('which-test-no-such-file', false)).toBe(''); - try { - await io.which('which-test-no-such-file', true); - throw new Error('Should have thrown'); - } - catch (err) { - } - }); - - it('which() searches path in order', async () => { - // create a chcp.com/bash override file - let testPath = path.join(getTestTemp(), 'which-searches-path-in-order'); - await io.mkdirP(testPath); - let fileName; - if (process.platform == 'win32') { - fileName = 'chcp.com'; - } - else { - fileName = 'bash'; - } + expect(worked).toBe(false) + }) - let filePath = path.join(testPath, fileName); - fs.writeFileSync(filePath, ''); - if (process.platform != 'win32') { - chmod(filePath, '+x'); - } + it('fails if mkdirP with empty path', async () => { + var worked: boolean = false + try { + await io.mkdirP('') + worked = true + } catch (err) {} + + expect(worked).toBe(false) + }) + + it('fails if mkdirP with conflicting file path', async () => { + let testPath = path.join(getTestTemp(), 'mkdirP_conflicting_file_path') + await io.mkdirP(getTestTemp()) + fs.writeFileSync(testPath, '') + let worked: boolean = false + try { + await io.mkdirP(testPath) + worked = true + } catch (err) {} + + expect(worked).toBe(false) + }) + + it('fails if mkdirP with conflicting parent file path', async () => { + let testPath = path.join( + getTestTemp(), + 'mkdirP_conflicting_parent_file_path', + 'dir' + ) + await io.mkdirP(getTestTemp()) + fs.writeFileSync(path.dirname(testPath), '') + let worked: boolean = false + try { + await io.mkdirP(testPath) + worked = true + } catch (err) {} + + expect(worked).toBe(false) + }) + + it('no-ops if mkdirP directory exists', async () => { + let testPath = path.join(getTestTemp(), 'mkdirP_dir_exists') + await io.mkdirP(testPath) + expect(fs.existsSync(testPath)).toBe(true) + + // Calling again shouldn't throw + await io.mkdirP(testPath) + expect(fs.existsSync(testPath)).toBe(true) + }) + + it('no-ops if mkdirP with symlink directory', async () => { + // create the following layout: + // real_dir + // real_dir/file.txt + // symlink_dir -> real_dir + let rootPath = path.join(getTestTemp(), 'mkdirP_symlink_dir') + let realDirPath = path.join(rootPath, 'real_dir') + let realFilePath = path.join(realDirPath, 'file.txt') + let symlinkDirPath = path.join(rootPath, 'symlink_dir') + await io.mkdirP(getTestTemp()) + fs.mkdirSync(rootPath) + fs.mkdirSync(realDirPath) + fs.writeFileSync(realFilePath, 'test real_dir/file.txt contet') + createSymlinkDir(realDirPath, symlinkDirPath) + + await io.mkdirP(symlinkDirPath) + + // the file in the real directory should still be accessible via the symlink + expect(fs.lstatSync(symlinkDirPath).isSymbolicLink()).toBe(true) + expect(fs.statSync(path.join(symlinkDirPath, 'file.txt')).isFile()).toBe( + true + ) + }) + + it('no-ops if mkdirP with parent symlink directory', async () => { + // create the following layout: + // real_dir + // real_dir/file.txt + // symlink_dir -> real_dir + let rootPath = path.join(getTestTemp(), 'mkdirP_parent_symlink_dir') + let realDirPath = path.join(rootPath, 'real_dir') + let realFilePath = path.join(realDirPath, 'file.txt') + let symlinkDirPath = path.join(rootPath, 'symlink_dir') + await io.mkdirP(getTestTemp()) + fs.mkdirSync(rootPath) + fs.mkdirSync(realDirPath) + fs.writeFileSync(realFilePath, 'test real_dir/file.txt contet') + createSymlinkDir(realDirPath, symlinkDirPath) + + let subDirPath = path.join(symlinkDirPath, 'sub_dir') + await io.mkdirP(subDirPath) + + // the subdirectory should be accessible via the real directory + expect(fs.lstatSync(path.join(realDirPath, 'sub_dir')).isDirectory()).toBe( + true + ) + }) + + it('breaks if mkdirP loop out of control', async () => { + let testPath = path.join( + getTestTemp(), + 'mkdirP_failsafe', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10' + ) + process.env['TEST_MKDIRP_FAILSAFE'] = '10' + try { + await io.mkdirP(testPath) + throw new Error('directory should not have been created') + } catch (err) { + delete process.env['TEST_MKDIRP_FAILSAFE'] - let originalPath = process.env['PATH']; - try { - // sanity - regular chcp.com/bash should be found - let originalWhich = await io.which(fileName); - expect(!!(originalWhich || '')).toBe(true); + // ENOENT is expected, all other errors are not + expect(err.code).toBe('ENOENT') + } + }) +}) - // modify PATH - process.env['PATH'] = testPath + path.delimiter + process.env['PATH']; +describe('which', () => { + it('which() finds file name', async () => { + // create a executable file + let testPath = path.join(getTestTemp(), 'which-finds-file-name') + await io.mkdirP(testPath) + let fileName = 'Which-Test-File' + if (process.platform == 'win32') { + fileName += '.exe' + } - // override chcp.com/bash should be found - expect(await io.which(fileName)).toBe(filePath); - } - finally { - process.env['PATH'] = originalPath; - } - }); - - it('which() requires executable', async () => { - // create a non-executable file - // on Windows, should not end in valid PATHEXT - // on Mac/Linux should not have executable bit - let testPath = path.join(getTestTemp(), 'which-requires-executable'); - await io.mkdirP(testPath); - let fileName = 'Which-Test-File'; - if (process.platform == 'win32') { - fileName += '.abc'; // not a valid PATHEXT - } + let filePath = path.join(testPath, fileName) + fs.writeFileSync(filePath, '') + if (process.platform != 'win32') { + chmod(filePath, '+x') + } - let filePath = path.join(testPath, fileName); - fs.writeFileSync(filePath, ''); - if (process.platform != 'win32') { - chmod(filePath, '-x'); - } + let originalPath = process.env['PATH'] + try { + // update the PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + + // exact file name + expect(await io.which(fileName)).toBe(filePath) + expect(await io.which(fileName, false)).toBe(filePath) + expect(await io.which(fileName, true)).toBe(filePath) + + if (process.platform == 'win32') { + // not case sensitive on windows + expect(await io.which('which-test-file.exe')).toBe( + path.join(testPath, 'which-test-file.exe') + ) + expect(await io.which('WHICH-TEST-FILE.EXE')).toBe( + path.join(testPath, 'WHICH-TEST-FILE.EXE') + ) + expect(await io.which('WHICH-TEST-FILE.EXE', false)).toBe( + path.join(testPath, 'WHICH-TEST-FILE.EXE') + ) + expect(await io.which('WHICH-TEST-FILE.EXE', true)).toBe( + path.join(testPath, 'WHICH-TEST-FILE.EXE') + ) + + // without extension + expect(await io.which('which-test-file')).toBe(filePath) + expect(await io.which('which-test-file', false)).toBe(filePath) + expect(await io.which('which-test-file', true)).toBe(filePath) + } else if (process.platform == 'darwin') { + // not case sensitive on Mac + expect(await io.which(fileName.toUpperCase())).toBe( + path.join(testPath, fileName.toUpperCase()) + ) + expect(await io.which(fileName.toUpperCase(), false)).toBe( + path.join(testPath, fileName.toUpperCase()) + ) + expect(await io.which(fileName.toUpperCase(), true)).toBe( + path.join(testPath, fileName.toUpperCase()) + ) + } else { + // case sensitive on Linux + expect((await io.which(fileName.toUpperCase())) || '').toBe('') + } + } finally { + process.env['PATH'] = originalPath + } + }) - let originalPath = process.env['PATH']; - try { - // modify PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; + it('which() not found', async () => { + expect(await io.which('which-test-no-such-file')).toBe('') + expect(await io.which('which-test-no-such-file', false)).toBe('') + try { + await io.which('which-test-no-such-file', true) + throw new Error('Should have thrown') + } catch (err) {} + }) + + it('which() searches path in order', async () => { + // create a chcp.com/bash override file + let testPath = path.join(getTestTemp(), 'which-searches-path-in-order') + await io.mkdirP(testPath) + let fileName + if (process.platform == 'win32') { + fileName = 'chcp.com' + } else { + fileName = 'bash' + } - // should not be found - expect(await io.which(fileName) || '').toBe(''); - } - finally { - process.env['PATH'] = originalPath; - } - }); - - // which permissions tests - it('which() finds executable with different permissions', async () => { - await findsExecutableWithScopedPermissions('u=rwx,g=r,o=r'); - await findsExecutableWithScopedPermissions('u=rw,g=rx,o=r'); - await findsExecutableWithScopedPermissions('u=rw,g=r,o=rx'); - }); - - it('which() ignores directory match', async () => { - // create a directory - let testPath = path.join(getTestTemp(), 'which-ignores-directory-match'); - let dirPath = path.join(testPath, 'Which-Test-Dir'); - if (process.platform == 'win32') { - dirPath += '.exe'; - } + let filePath = path.join(testPath, fileName) + fs.writeFileSync(filePath, '') + if (process.platform != 'win32') { + chmod(filePath, '+x') + } - await io.mkdirP(dirPath); - if (process.platform != 'win32') { - chmod(dirPath, '+x'); - } + let originalPath = process.env['PATH'] + try { + // sanity - regular chcp.com/bash should be found + let originalWhich = await io.which(fileName) + expect(!!(originalWhich || '')).toBe(true) - let originalPath = process.env['PATH']; - try { - // modify PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; + // modify PATH + process.env['PATH'] = testPath + path.delimiter + process.env['PATH'] - // should not be found - expect(await io.which(path.basename(dirPath)) || '').toBe(''); - } - finally { - process.env['PATH'] = originalPath; - } - }); - - it('which() allows rooted path', async () => { - // create an executable file - let testPath = path.join(getTestTemp(), 'which-allows-rooted-path'); - await io.mkdirP(testPath); - let filePath = path.join(testPath, 'Which-Test-File'); - if (process.platform == 'win32') { - filePath += '.exe'; - } + // override chcp.com/bash should be found + expect(await io.which(fileName)).toBe(filePath) + } finally { + process.env['PATH'] = originalPath + } + }) + + it('which() requires executable', async () => { + // create a non-executable file + // on Windows, should not end in valid PATHEXT + // on Mac/Linux should not have executable bit + let testPath = path.join(getTestTemp(), 'which-requires-executable') + await io.mkdirP(testPath) + let fileName = 'Which-Test-File' + if (process.platform == 'win32') { + fileName += '.abc' // not a valid PATHEXT + } - fs.writeFileSync(filePath, ''); - if (process.platform != 'win32') { - chmod(filePath, '+x'); - } + let filePath = path.join(testPath, fileName) + fs.writeFileSync(filePath, '') + if (process.platform != 'win32') { + chmod(filePath, '-x') + } - // which the full path - expect(await io.which(filePath)).toBe(filePath); - expect(await io.which(filePath, false)).toBe(filePath); - expect(await io.which(filePath, true)).toBe(filePath); - }); - - it('which() requires rooted path to be executable', async () => { - // create a non-executable file - // on Windows, should not end in valid PATHEXT - // on Mac/Linux, should not have executable bit - let testPath = path.join(getTestTemp(), 'which-requires-rooted-path-to-be-executable'); - await io.mkdirP(testPath); - let filePath = path.join(testPath, 'Which-Test-File'); - if (process.platform == 'win32') { - filePath += '.abc'; // not a valid PATHEXT - } + let originalPath = process.env['PATH'] + try { + // modify PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath - fs.writeFileSync(filePath, ''); - if (process.platform != 'win32') { - chmod(filePath, '-x'); - } + // should not be found + expect((await io.which(fileName)) || '').toBe('') + } finally { + process.env['PATH'] = originalPath + } + }) + + // which permissions tests + it('which() finds executable with different permissions', async () => { + await findsExecutableWithScopedPermissions('u=rwx,g=r,o=r') + await findsExecutableWithScopedPermissions('u=rw,g=rx,o=r') + await findsExecutableWithScopedPermissions('u=rw,g=r,o=rx') + }) + + it('which() ignores directory match', async () => { + // create a directory + let testPath = path.join(getTestTemp(), 'which-ignores-directory-match') + let dirPath = path.join(testPath, 'Which-Test-Dir') + if (process.platform == 'win32') { + dirPath += '.exe' + } - // should not be found - expect(await io.which(filePath) || '').toBe(''); - expect(await io.which(filePath, false) || '').toBe(''); - let failed = false; - try { - await io.which(filePath, true); - } - catch (err) { - failed = true; - } + await io.mkdirP(dirPath) + if (process.platform != 'win32') { + chmod(dirPath, '+x') + } - expect(failed).toBe(true); - }); - - it('which() requires rooted path to be a file', async () => { - // create a dir - let testPath = path.join(getTestTemp(), 'which-requires-rooted-path-to-be-executable'); - let dirPath = path.join(testPath, 'Which-Test-Dir'); - if (process.platform == 'win32') { - dirPath += '.exe'; - } + let originalPath = process.env['PATH'] + try { + // modify PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath - await io.mkdirP(dirPath); - if (process.platform != 'win32') { - chmod(dirPath, '+x'); - } + // should not be found + expect((await io.which(path.basename(dirPath))) || '').toBe('') + } finally { + process.env['PATH'] = originalPath + } + }) - // should not be found - expect(await io.which(dirPath) || '').toBe(''); - expect(await io.which(dirPath, false) || '').toBe(''); - let failed = false; - try { - await io.which(dirPath, true); - } - catch (err) { - failed = true; - } + it('which() allows rooted path', async () => { + // create an executable file + let testPath = path.join(getTestTemp(), 'which-allows-rooted-path') + await io.mkdirP(testPath) + let filePath = path.join(testPath, 'Which-Test-File') + if (process.platform == 'win32') { + filePath += '.exe' + } - expect(failed).toBe(true); - }); + fs.writeFileSync(filePath, '') + if (process.platform != 'win32') { + chmod(filePath, '+x') + } - it('which() requires rooted path to exist', async () => { - let filePath = path.join(__dirname, 'no-such-file'); - if (process.platform == 'win32') { - filePath += '.exe'; - } + // which the full path + expect(await io.which(filePath)).toBe(filePath) + expect(await io.which(filePath, false)).toBe(filePath) + expect(await io.which(filePath, true)).toBe(filePath) + }) + + it('which() requires rooted path to be executable', async () => { + // create a non-executable file + // on Windows, should not end in valid PATHEXT + // on Mac/Linux, should not have executable bit + let testPath = path.join( + getTestTemp(), + 'which-requires-rooted-path-to-be-executable' + ) + await io.mkdirP(testPath) + let filePath = path.join(testPath, 'Which-Test-File') + if (process.platform == 'win32') { + filePath += '.abc' // not a valid PATHEXT + } - expect(await io.which(filePath) || '').toBe(''); - expect(await io.which(filePath, false) || '').toBe(''); - let failed = false; - try { - await io.which(filePath, true); - } - catch (err) { - failed = true; - } - - expect(failed).toBe(true); - }); - - it('which() does not allow separators', async () => { - // create an executable file - let testDirName = 'which-does-not-allow-separators'; - let testPath = path.join(getTestTemp(), testDirName); - await io.mkdirP(testPath); - let fileName = 'Which-Test-File'; - if (process.platform == 'win32') { - fileName += '.exe'; - } + fs.writeFileSync(filePath, '') + if (process.platform != 'win32') { + chmod(filePath, '-x') + } - let filePath = path.join(testPath, fileName); - fs.writeFileSync(filePath, ''); - if (process.platform != 'win32') { - chmod(filePath, '+x'); - } + // should not be found + expect((await io.which(filePath)) || '').toBe('') + expect((await io.which(filePath, false)) || '').toBe('') + let failed = false + try { + await io.which(filePath, true) + } catch (err) { + failed = true + } - let originalPath = process.env['PATH']; - try { - // modify PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; + expect(failed).toBe(true) + }) - // which "dir/file", should not be found - expect(await io.which(testDirName + '/' + fileName) || '').toBe(''); + it('which() requires rooted path to be a file', async () => { + // create a dir + let testPath = path.join( + getTestTemp(), + 'which-requires-rooted-path-to-be-executable' + ) + let dirPath = path.join(testPath, 'Which-Test-Dir') + if (process.platform == 'win32') { + dirPath += '.exe' + } - // on Windows, also try "dir\file" - if (process.platform == 'win32') { - expect(await io.which(testDirName + '\\' + fileName) || '').toBe(''); - } - } - finally { - process.env['PATH'] = originalPath; - } - }); + await io.mkdirP(dirPath) + if (process.platform != 'win32') { + chmod(dirPath, '+x') + } + // should not be found + expect((await io.which(dirPath)) || '').toBe('') + expect((await io.which(dirPath, false)) || '').toBe('') + let failed = false + try { + await io.which(dirPath, true) + } catch (err) { + failed = true + } + + expect(failed).toBe(true) + }) + + it('which() requires rooted path to exist', async () => { + let filePath = path.join(__dirname, 'no-such-file') if (process.platform == 'win32') { - it('which() resolves actual case file name when extension is applied', async () => { - const comspec: string = process.env['ComSpec'] || ''; - expect(!!comspec).toBe(true); - expect(await io.which('CmD.eXe')).toBe(path.join(path.dirname(comspec), 'CmD.eXe')); - expect(await io.which('CmD')).toBe(comspec); - }); - - it('which() appends ext on windows', async () => { - // create executable files - let testPath = path.join(getTestTemp(), 'which-appends-ext-on-windows'); - await io.mkdirP(testPath); - // PATHEXT=.COM;.EXE;.BAT;.CMD... - let files: { [key:string]:string; } = { - "which-test-file-1": path.join(testPath, "which-test-file-1.com"), - "which-test-file-2": path.join(testPath, "which-test-file-2.exe"), - "which-test-file-3": path.join(testPath, "which-test-file-3.bat"), - "which-test-file-4": path.join(testPath, "which-test-file-4.cmd"), - "which-test-file-5.txt": path.join(testPath, "which-test-file-5.txt.com") - }; - for (let fileName of Object.keys(files)) { - fs.writeFileSync(files[fileName], ''); - } - - let originalPath = process.env['PATH']; - try { - // modify PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; - - // find each file - for (let fileName of Object.keys(files)) { - expect(await io.which(fileName)).toBe(files[fileName]); - } - } - finally { - process.env['PATH'] = originalPath; - } - }); - - it('which() appends ext on windows when rooted', async () => { - // create executable files - let testPath = path.join(getTestTemp(), 'which-appends-ext-on-windows-when-rooted'); - await io.mkdirP(testPath); - // PATHEXT=.COM;.EXE;.BAT;.CMD... - let files: { [key:string]:string; } = { }; - files[path.join(testPath, "which-test-file-1")] = path.join(testPath, "which-test-file-1.com"); - files[path.join(testPath, "which-test-file-2")] = path.join(testPath, "which-test-file-2.exe"); - files[path.join(testPath, "which-test-file-3")] = path.join(testPath, "which-test-file-3.bat"); - files[path.join(testPath, "which-test-file-4")] = path.join(testPath, "which-test-file-4.cmd"); - files[path.join(testPath, "which-test-file-5.txt")] = path.join(testPath, "which-test-file-5.txt.com"); - for (let fileName of Object.keys(files)) { - fs.writeFileSync(files[fileName], ''); - } - - // find each file - for (let fileName of Object.keys(files)) { - expect(await io.which(fileName)).toBe(files[fileName]); - } - }); - - it('which() prefer exact match on windows', async () => { - // create two executable files: - // which-test-file.bat - // which-test-file.bat.exe - // - // verify "which-test-file.bat" returns that file, and not "which-test-file.bat.exe" - // - // preference, within the same dir, should be given to the exact match (even though - // .EXE is defined with higher preference than .BAT in PATHEXT (PATHEXT=.COM;.EXE;.BAT;.CMD...) - let testPath = path.join(getTestTemp(), 'which-prefer-exact-match-on-windows'); - await io.mkdirP(testPath); - let fileName = 'which-test-file.bat'; - let expectedFilePath = path.join(testPath, fileName); - let notExpectedFilePath = path.join(testPath, fileName + '.exe'); - fs.writeFileSync(expectedFilePath, ''); - fs.writeFileSync(notExpectedFilePath, ''); - let originalPath = process.env['PATH']; - try { - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; - expect(await io.which(fileName)).toBe(expectedFilePath); - } - finally { - process.env['PATH'] = originalPath; - } - }); - - it('which() prefer exact match on windows when rooted', async () => { - // create two executable files: - // which-test-file.bat - // which-test-file.bat.exe - // - // verify "which-test-file.bat" returns that file, and not "which-test-file.bat.exe" - // - // preference, within the same dir, should be given to the exact match (even though - // .EXE is defined with higher preference than .BAT in PATHEXT (PATHEXT=.COM;.EXE;.BAT;.CMD...) - let testPath = path.join(getTestTemp(), 'which-prefer-exact-match-on-windows-when-rooted'); - await io.mkdirP(testPath); - let fileName = 'which-test-file.bat'; - let expectedFilePath = path.join(testPath, fileName); - let notExpectedFilePath = path.join(testPath, fileName + '.exe'); - fs.writeFileSync(expectedFilePath, ''); - fs.writeFileSync(notExpectedFilePath, ''); - expect(await io.which(path.join(testPath, fileName))).toBe(expectedFilePath); - }); - - it('which() searches ext in order', async () => { - let testPath = path.join(getTestTemp(), 'which-searches-ext-in-order'); - - // create a directory for testing .COM order preference - // PATHEXT=.COM;.EXE;.BAT;.CMD... - let fileNameWithoutExtension = 'which-test-file'; - let comTestPath = path.join(testPath, 'com-test'); - await io.mkdirP(comTestPath); - fs.writeFileSync(path.join(comTestPath, fileNameWithoutExtension + '.com'), ''); - fs.writeFileSync(path.join(comTestPath, fileNameWithoutExtension + '.exe'), ''); - fs.writeFileSync(path.join(comTestPath, fileNameWithoutExtension + '.bat'), ''); - fs.writeFileSync(path.join(comTestPath, fileNameWithoutExtension + '.cmd'), ''); - - // create a directory for testing .EXE order preference - // PATHEXT=.COM;.EXE;.BAT;.CMD... - let exeTestPath = path.join(testPath, 'exe-test'); - await io.mkdirP(exeTestPath); - fs.writeFileSync(path.join(exeTestPath, fileNameWithoutExtension + '.exe'), ''); - fs.writeFileSync(path.join(exeTestPath, fileNameWithoutExtension + '.bat'), ''); - fs.writeFileSync(path.join(exeTestPath, fileNameWithoutExtension + '.cmd'), ''); - - // create a directory for testing .BAT order preference - // PATHEXT=.COM;.EXE;.BAT;.CMD... - let batTestPath = path.join(testPath, 'bat-test'); - await io.mkdirP(batTestPath); - fs.writeFileSync(path.join(batTestPath, fileNameWithoutExtension + '.bat'), ''); - fs.writeFileSync(path.join(batTestPath, fileNameWithoutExtension + '.cmd'), ''); - - // create a directory for testing .CMD - let cmdTestPath = path.join(testPath, 'cmd-test'); - await io.mkdirP(cmdTestPath); - let cmdTest_cmdFilePath = path.join(cmdTestPath, fileNameWithoutExtension + '.cmd'); - fs.writeFileSync(cmdTest_cmdFilePath, ''); - - let originalPath = process.env['PATH']; - try { - // test .COM - process.env['PATH'] = comTestPath + path.delimiter + originalPath; - expect(await io.which(fileNameWithoutExtension)).toBe(path.join(comTestPath, fileNameWithoutExtension + '.com')); - - // test .EXE - process.env['PATH'] = exeTestPath + path.delimiter + originalPath; - expect(await io.which(fileNameWithoutExtension)).toBe(path.join(exeTestPath, fileNameWithoutExtension + '.exe')); - - // test .BAT - process.env['PATH'] = batTestPath + path.delimiter + originalPath; - expect(await io.which(fileNameWithoutExtension)).toBe(path.join(batTestPath, fileNameWithoutExtension + '.bat')); - - // test .CMD - process.env['PATH'] = cmdTestPath + path.delimiter + originalPath; - expect(await io.which(fileNameWithoutExtension)).toBe(path.join(cmdTestPath, fileNameWithoutExtension + '.cmd')); - } - finally { - process.env['PATH'] = originalPath; - } - }); + filePath += '.exe' } -}); -async function findsExecutableWithScopedPermissions(chmodOptions: string) { - // create a executable file - let testPath = path.join(getTestTemp(), 'which-finds-file-name'); - await io.mkdirP(testPath); - let fileName = 'Which-Test-File'; + expect((await io.which(filePath)) || '').toBe('') + expect((await io.which(filePath, false)) || '').toBe('') + let failed = false + try { + await io.which(filePath, true) + } catch (err) { + failed = true + } + + expect(failed).toBe(true) + }) + + it('which() does not allow separators', async () => { + // create an executable file + let testDirName = 'which-does-not-allow-separators' + let testPath = path.join(getTestTemp(), testDirName) + await io.mkdirP(testPath) + let fileName = 'Which-Test-File' if (process.platform == 'win32') { - return; + fileName += '.exe' } - let filePath = path.join(testPath, fileName); - fs.writeFileSync(filePath, ''); - chmod(filePath, chmodOptions); + let filePath = path.join(testPath, fileName) + fs.writeFileSync(filePath, '') + if (process.platform != 'win32') { + chmod(filePath, '+x') + } - let originalPath = process.env['PATH']; + let originalPath = process.env['PATH'] try { - // update the PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath; - - // exact file name - expect(await io.which(fileName)).toBe(filePath); - expect(await io.which(fileName, false)).toBe(filePath); - expect(await io.which(fileName, true)).toBe(filePath); - - if (process.platform == 'darwin') { - // not case sensitive on Mac - expect(await io.which(fileName.toUpperCase())).toBe(path.join(testPath, fileName.toUpperCase())); - expect(await io.which(fileName.toUpperCase(), false)).toBe(path.join(testPath, fileName.toUpperCase())); - expect(await io.which(fileName.toUpperCase(), true)).toBe(path.join(testPath, fileName.toUpperCase())); - } - else { - // case sensitive on Linux - expect(await io.which(fileName.toUpperCase()) || '').toBe(''); - } + // modify PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + + // which "dir/file", should not be found + expect((await io.which(testDirName + '/' + fileName)) || '').toBe('') + + // on Windows, also try "dir\file" + if (process.platform == 'win32') { + expect((await io.which(testDirName + '\\' + fileName)) || '').toBe('') + } + } finally { + process.env['PATH'] = originalPath } - finally { - process.env['PATH'] = originalPath; + }) + + if (process.platform == 'win32') { + it('which() resolves actual case file name when extension is applied', async () => { + const comspec: string = process.env['ComSpec'] || '' + expect(!!comspec).toBe(true) + expect(await io.which('CmD.eXe')).toBe( + path.join(path.dirname(comspec), 'CmD.eXe') + ) + expect(await io.which('CmD')).toBe(comspec) + }) + + it('which() appends ext on windows', async () => { + // create executable files + let testPath = path.join(getTestTemp(), 'which-appends-ext-on-windows') + await io.mkdirP(testPath) + // PATHEXT=.COM;.EXE;.BAT;.CMD... + let files: {[key: string]: string} = { + 'which-test-file-1': path.join(testPath, 'which-test-file-1.com'), + 'which-test-file-2': path.join(testPath, 'which-test-file-2.exe'), + 'which-test-file-3': path.join(testPath, 'which-test-file-3.bat'), + 'which-test-file-4': path.join(testPath, 'which-test-file-4.cmd'), + 'which-test-file-5.txt': path.join( + testPath, + 'which-test-file-5.txt.com' + ) + } + for (let fileName of Object.keys(files)) { + fs.writeFileSync(files[fileName], '') + } + + let originalPath = process.env['PATH'] + try { + // modify PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + + // find each file + for (let fileName of Object.keys(files)) { + expect(await io.which(fileName)).toBe(files[fileName]) + } + } finally { + process.env['PATH'] = originalPath + } + }) + + it('which() appends ext on windows when rooted', async () => { + // create executable files + let testPath = path.join( + getTestTemp(), + 'which-appends-ext-on-windows-when-rooted' + ) + await io.mkdirP(testPath) + // PATHEXT=.COM;.EXE;.BAT;.CMD... + let files: {[key: string]: string} = {} + files[path.join(testPath, 'which-test-file-1')] = path.join( + testPath, + 'which-test-file-1.com' + ) + files[path.join(testPath, 'which-test-file-2')] = path.join( + testPath, + 'which-test-file-2.exe' + ) + files[path.join(testPath, 'which-test-file-3')] = path.join( + testPath, + 'which-test-file-3.bat' + ) + files[path.join(testPath, 'which-test-file-4')] = path.join( + testPath, + 'which-test-file-4.cmd' + ) + files[path.join(testPath, 'which-test-file-5.txt')] = path.join( + testPath, + 'which-test-file-5.txt.com' + ) + for (let fileName of Object.keys(files)) { + fs.writeFileSync(files[fileName], '') + } + + // find each file + for (let fileName of Object.keys(files)) { + expect(await io.which(fileName)).toBe(files[fileName]) + } + }) + + it('which() prefer exact match on windows', async () => { + // create two executable files: + // which-test-file.bat + // which-test-file.bat.exe + // + // verify "which-test-file.bat" returns that file, and not "which-test-file.bat.exe" + // + // preference, within the same dir, should be given to the exact match (even though + // .EXE is defined with higher preference than .BAT in PATHEXT (PATHEXT=.COM;.EXE;.BAT;.CMD...) + let testPath = path.join( + getTestTemp(), + 'which-prefer-exact-match-on-windows' + ) + await io.mkdirP(testPath) + let fileName = 'which-test-file.bat' + let expectedFilePath = path.join(testPath, fileName) + let notExpectedFilePath = path.join(testPath, fileName + '.exe') + fs.writeFileSync(expectedFilePath, '') + fs.writeFileSync(notExpectedFilePath, '') + let originalPath = process.env['PATH'] + try { + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + expect(await io.which(fileName)).toBe(expectedFilePath) + } finally { + process.env['PATH'] = originalPath + } + }) + + it('which() prefer exact match on windows when rooted', async () => { + // create two executable files: + // which-test-file.bat + // which-test-file.bat.exe + // + // verify "which-test-file.bat" returns that file, and not "which-test-file.bat.exe" + // + // preference, within the same dir, should be given to the exact match (even though + // .EXE is defined with higher preference than .BAT in PATHEXT (PATHEXT=.COM;.EXE;.BAT;.CMD...) + let testPath = path.join( + getTestTemp(), + 'which-prefer-exact-match-on-windows-when-rooted' + ) + await io.mkdirP(testPath) + let fileName = 'which-test-file.bat' + let expectedFilePath = path.join(testPath, fileName) + let notExpectedFilePath = path.join(testPath, fileName + '.exe') + fs.writeFileSync(expectedFilePath, '') + fs.writeFileSync(notExpectedFilePath, '') + expect(await io.which(path.join(testPath, fileName))).toBe( + expectedFilePath + ) + }) + + it('which() searches ext in order', async () => { + let testPath = path.join(getTestTemp(), 'which-searches-ext-in-order') + + // create a directory for testing .COM order preference + // PATHEXT=.COM;.EXE;.BAT;.CMD... + let fileNameWithoutExtension = 'which-test-file' + let comTestPath = path.join(testPath, 'com-test') + await io.mkdirP(comTestPath) + fs.writeFileSync( + path.join(comTestPath, fileNameWithoutExtension + '.com'), + '' + ) + fs.writeFileSync( + path.join(comTestPath, fileNameWithoutExtension + '.exe'), + '' + ) + fs.writeFileSync( + path.join(comTestPath, fileNameWithoutExtension + '.bat'), + '' + ) + fs.writeFileSync( + path.join(comTestPath, fileNameWithoutExtension + '.cmd'), + '' + ) + + // create a directory for testing .EXE order preference + // PATHEXT=.COM;.EXE;.BAT;.CMD... + let exeTestPath = path.join(testPath, 'exe-test') + await io.mkdirP(exeTestPath) + fs.writeFileSync( + path.join(exeTestPath, fileNameWithoutExtension + '.exe'), + '' + ) + fs.writeFileSync( + path.join(exeTestPath, fileNameWithoutExtension + '.bat'), + '' + ) + fs.writeFileSync( + path.join(exeTestPath, fileNameWithoutExtension + '.cmd'), + '' + ) + + // create a directory for testing .BAT order preference + // PATHEXT=.COM;.EXE;.BAT;.CMD... + let batTestPath = path.join(testPath, 'bat-test') + await io.mkdirP(batTestPath) + fs.writeFileSync( + path.join(batTestPath, fileNameWithoutExtension + '.bat'), + '' + ) + fs.writeFileSync( + path.join(batTestPath, fileNameWithoutExtension + '.cmd'), + '' + ) + + // create a directory for testing .CMD + let cmdTestPath = path.join(testPath, 'cmd-test') + await io.mkdirP(cmdTestPath) + let cmdTest_cmdFilePath = path.join( + cmdTestPath, + fileNameWithoutExtension + '.cmd' + ) + fs.writeFileSync(cmdTest_cmdFilePath, '') + + let originalPath = process.env['PATH'] + try { + // test .COM + process.env['PATH'] = comTestPath + path.delimiter + originalPath + expect(await io.which(fileNameWithoutExtension)).toBe( + path.join(comTestPath, fileNameWithoutExtension + '.com') + ) + + // test .EXE + process.env['PATH'] = exeTestPath + path.delimiter + originalPath + expect(await io.which(fileNameWithoutExtension)).toBe( + path.join(exeTestPath, fileNameWithoutExtension + '.exe') + ) + + // test .BAT + process.env['PATH'] = batTestPath + path.delimiter + originalPath + expect(await io.which(fileNameWithoutExtension)).toBe( + path.join(batTestPath, fileNameWithoutExtension + '.bat') + ) + + // test .CMD + process.env['PATH'] = cmdTestPath + path.delimiter + originalPath + expect(await io.which(fileNameWithoutExtension)).toBe( + path.join(cmdTestPath, fileNameWithoutExtension + '.cmd') + ) + } finally { + process.env['PATH'] = originalPath + } + }) + } +}) + +async function findsExecutableWithScopedPermissions(chmodOptions: string) { + // create a executable file + let testPath = path.join(getTestTemp(), 'which-finds-file-name') + await io.mkdirP(testPath) + let fileName = 'Which-Test-File' + if (process.platform == 'win32') { + return + } + + let filePath = path.join(testPath, fileName) + fs.writeFileSync(filePath, '') + chmod(filePath, chmodOptions) + + let originalPath = process.env['PATH'] + try { + // update the PATH + process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + + // exact file name + expect(await io.which(fileName)).toBe(filePath) + expect(await io.which(fileName, false)).toBe(filePath) + expect(await io.which(fileName, true)).toBe(filePath) + + if (process.platform == 'darwin') { + // not case sensitive on Mac + expect(await io.which(fileName.toUpperCase())).toBe( + path.join(testPath, fileName.toUpperCase()) + ) + expect(await io.which(fileName.toUpperCase(), false)).toBe( + path.join(testPath, fileName.toUpperCase()) + ) + expect(await io.which(fileName.toUpperCase(), true)).toBe( + path.join(testPath, fileName.toUpperCase()) + ) + } else { + // case sensitive on Linux + expect((await io.which(fileName.toUpperCase())) || '').toBe('') } + } finally { + process.env['PATH'] = originalPath + } - return; + return } function chmod(file: string, mode: string): void { - let result = child.spawnSync('chmod', [ mode, file ]); - if (result.status != 0) { - let message: string = (result.output || []).join(' ').trim(); - throw new Error(`Command failed: "chmod ${mode} ${file}". ${message}`); - } + let result = child.spawnSync('chmod', [mode, file]) + if (result.status != 0) { + let message: string = (result.output || []).join(' ').trim() + throw new Error(`Command failed: "chmod ${mode} ${file}". ${message}`) + } } async function createHiddenDirectory(dir: string): Promise { - return new Promise(async (resolve, reject) => { - if (!path.basename(dir).match(/^\./)) { - reject(`Expected dir '${dir}' to start with '.'.`); - } + return new Promise(async (resolve, reject) => { + if (!path.basename(dir).match(/^\./)) { + reject(`Expected dir '${dir}' to start with '.'.`) + } - await io.mkdirP(dir); - if (os.platform() == 'win32') { - let result = child.spawnSync('attrib.exe', [ '+H', dir ]); - if (result.status != 0) { - let message: string = (result.output || []).join(' ').trim(); - reject(`Failed to set hidden attribute for directory '${dir}'. ${message}`); - } - } - resolve() - }); -}; + await io.mkdirP(dir) + if (os.platform() == 'win32') { + let result = child.spawnSync('attrib.exe', ['+H', dir]) + if (result.status != 0) { + let message: string = (result.output || []).join(' ').trim() + reject( + `Failed to set hidden attribute for directory '${dir}'. ${message}` + ) + } + } + resolve() + }) +} async function createHiddenFile(file: string, content: string): Promise { - return new Promise(async (resolve, reject) => { - if (!path.basename(file).match(/^\./)) { - reject(`Expected dir '${file}' to start with '.'.`); - } + return new Promise(async (resolve, reject) => { + if (!path.basename(file).match(/^\./)) { + reject(`Expected dir '${file}' to start with '.'.`) + } - await io.mkdirP(path.dirname(file)); - fs.writeFileSync(file, content); - if (os.platform() == 'win32') { - let result = child.spawnSync('attrib.exe', [ '+H', file ]); - if (result.status != 0) { - let message: string = (result.output || []).join(' ').trim(); - reject(`Failed to set hidden attribute for file '${file}'. ${message}`); - } - } + await io.mkdirP(path.dirname(file)) + fs.writeFileSync(file, content) + if (os.platform() == 'win32') { + let result = child.spawnSync('attrib.exe', ['+H', file]) + if (result.status != 0) { + let message: string = (result.output || []).join(' ').trim() + reject(`Failed to set hidden attribute for file '${file}'. ${message}`) + } + } - resolve(); - }); -}; + resolve() + }) +} function getTestTemp() { - return path.join(__dirname, '_temp'); + return path.join(__dirname, '_temp') } /** @@ -1259,10 +1434,9 @@ function getTestTemp() { * A symlink directory is not created on Windows since it requires an elevated context. */ function createSymlinkDir(real: string, link: string): void { - if (os.platform() == 'win32') { - fs.symlinkSync(real, link, 'junction'); - } - else { - fs.symlinkSync(real, link); - } -}; \ No newline at end of file + if (os.platform() == 'win32') { + fs.symlinkSync(real, link, 'junction') + } else { + fs.symlinkSync(real, link) + } +} From bc520a896defeccc24dbd303af39afdff884fbd5 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 15:33:53 -0400 Subject: [PATCH 04/27] Fix lint errors with autofix --- packages/io/__tests__/io.test.ts | 396 ++++++++++++++++--------------- packages/io/src/io.ts | 26 +- packages/io/src/ioUtil.ts | 24 +- 3 files changed, 226 insertions(+), 220 deletions(-) diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index 0b0770006d..4fd90181a6 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -7,9 +7,9 @@ import io = require('../src/io') describe('cp', () => { it('copies file with no flags', async () => { - let root: string = path.join(getTestTemp(), 'cp_with_no_flags') - let sourceFile: string = path.join(root, 'cp_source') - let targetFile: string = path.join(root, 'cp_target') + const root: string = path.join(getTestTemp(), 'cp_with_no_flags') + const sourceFile: string = path.join(root, 'cp_source') + const targetFile: string = path.join(root, 'cp_target') await io.mkdirP(root) fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) @@ -21,9 +21,9 @@ describe('cp', () => { }) it('copies file using -f', async () => { - let root: string = path.join(path.join(__dirname, '_temp'), 'cp_with_-f') - let sourceFile: string = path.join(root, 'cp_source') - let targetFile: string = path.join(root, 'cp_target') + const root: string = path.join(path.join(__dirname, '_temp'), 'cp_with_-f') + const sourceFile: string = path.join(root, 'cp_source') + const targetFile: string = path.join(root, 'cp_target') await io.mkdirP(root) fs.writeFileSync(sourceFile, 'test file content') @@ -35,9 +35,9 @@ describe('cp', () => { }) it('try copying to existing file with -n', async () => { - let root: string = path.join(getTestTemp(), 'cp_to_existing') - let sourceFile: string = path.join(root, 'cp_source') - let targetFile: string = path.join(root, 'cp_target') + const root: string = path.join(getTestTemp(), 'cp_to_existing') + const sourceFile: string = path.join(root, 'cp_source') + const targetFile: string = path.join(root, 'cp_target') await io.mkdirP(root) fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) fs.writeFileSync(targetFile, 'correct content', {encoding: 'utf8'}) @@ -55,12 +55,12 @@ describe('cp', () => { }) it('copies directory into existing destination with -r', async () => { - let root: string = path.join(getTestTemp(), 'cp_with_-r_existing_dest') - let sourceFolder: string = path.join(root, 'cp_source') - let sourceFile: string = path.join(sourceFolder, 'cp_source_file') + const root: string = path.join(getTestTemp(), 'cp_with_-r_existing_dest') + const sourceFolder: string = path.join(root, 'cp_source') + const sourceFile: string = path.join(sourceFolder, 'cp_source_file') - let targetFolder: string = path.join(root, 'cp_target') - let targetFile: string = path.join( + const targetFolder: string = path.join(root, 'cp_target') + const targetFile: string = path.join( targetFolder, 'cp_source', 'cp_source_file' @@ -76,12 +76,12 @@ describe('cp', () => { }) it('copies directory into non-existing destination with -r', async () => { - let root: string = path.join(getTestTemp(), 'cp_with_-r_nonexisting_dest') - let sourceFolder: string = path.join(root, 'cp_source') - let sourceFile: string = path.join(sourceFolder, 'cp_source_file') + const root: string = path.join(getTestTemp(), 'cp_with_-r_nonexisting_dest') + const sourceFolder: string = path.join(root, 'cp_source') + const sourceFile: string = path.join(sourceFolder, 'cp_source_file') - let targetFolder: string = path.join(root, 'cp_target') - let targetFile: string = path.join(targetFolder, 'cp_source_file') + const targetFolder: string = path.join(root, 'cp_target') + const targetFile: string = path.join(targetFolder, 'cp_source_file') await io.mkdirP(sourceFolder) fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) await io.cp(sourceFolder, targetFolder, {recursive: true}) @@ -92,12 +92,12 @@ describe('cp', () => { }) it('tries to copy directory without -r', async () => { - let root: string = path.join(getTestTemp(), 'cp_without_-r') - let sourceFolder: string = path.join(root, 'cp_source') - let sourceFile: string = path.join(sourceFolder, 'cp_source_file') + const root: string = path.join(getTestTemp(), 'cp_without_-r') + const sourceFolder: string = path.join(root, 'cp_source') + const sourceFile: string = path.join(sourceFolder, 'cp_source_file') - let targetFolder: string = path.join(root, 'cp_target') - let targetFile: string = path.join( + const targetFolder: string = path.join(root, 'cp_target') + const targetFile: string = path.join( targetFolder, 'cp_source', 'cp_source_file' @@ -118,9 +118,9 @@ describe('cp', () => { describe('mv', () => { it('moves file with no flags', async () => { - let root: string = path.join(getTestTemp(), ' mv_with_no_flags') - let sourceFile: string = path.join(root, ' mv_source') - let targetFile: string = path.join(root, ' mv_target') + const root: string = path.join(getTestTemp(), ' mv_with_no_flags') + const sourceFile: string = path.join(root, ' mv_source') + const targetFile: string = path.join(root, ' mv_target') await io.mkdirP(root) fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) @@ -133,9 +133,9 @@ describe('mv', () => { }) it('moves file using -f', async () => { - let root: string = path.join(path.join(__dirname, '_temp'), ' mv_with_-f') - let sourceFile: string = path.join(root, ' mv_source') - let targetFile: string = path.join(root, ' mv_target') + const root: string = path.join(path.join(__dirname, '_temp'), ' mv_with_-f') + const sourceFile: string = path.join(root, ' mv_source') + const targetFile: string = path.join(root, ' mv_target') await io.mkdirP(root) fs.writeFileSync(sourceFile, 'test file content') @@ -148,9 +148,9 @@ describe('mv', () => { }) it('try moving to existing file with -n', async () => { - let root: string = path.join(getTestTemp(), ' mv_to_existing') - let sourceFile: string = path.join(root, ' mv_source') - let targetFile: string = path.join(root, ' mv_target') + const root: string = path.join(getTestTemp(), ' mv_to_existing') + const sourceFile: string = path.join(root, ' mv_source') + const targetFile: string = path.join(root, ' mv_target') await io.mkdirP(root) fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) fs.writeFileSync(targetFile, 'correct content', {encoding: 'utf8'}) @@ -171,12 +171,12 @@ describe('mv', () => { }) it('moves directory into existing destination with -r', async () => { - let root: string = path.join(getTestTemp(), ' mv_with_-r_existing_dest') - let sourceFolder: string = path.join(root, ' mv_source') - let sourceFile: string = path.join(sourceFolder, ' mv_source_file') + const root: string = path.join(getTestTemp(), ' mv_with_-r_existing_dest') + const sourceFolder: string = path.join(root, ' mv_source') + const sourceFile: string = path.join(sourceFolder, ' mv_source_file') - let targetFolder: string = path.join(root, ' mv_target') - let targetFile: string = path.join( + const targetFolder: string = path.join(root, ' mv_target') + const targetFile: string = path.join( targetFolder, ' mv_source', ' mv_source_file' @@ -193,12 +193,15 @@ describe('mv', () => { }) it('moves directory into non-existing destination with -r', async () => { - let root: string = path.join(getTestTemp(), ' mv_with_-r_nonexisting_dest') - let sourceFolder: string = path.join(root, ' mv_source') - let sourceFile: string = path.join(sourceFolder, ' mv_source_file') + const root: string = path.join( + getTestTemp(), + ' mv_with_-r_nonexisting_dest' + ) + const sourceFolder: string = path.join(root, ' mv_source') + const sourceFile: string = path.join(sourceFolder, ' mv_source_file') - let targetFolder: string = path.join(root, ' mv_target') - let targetFile: string = path.join(targetFolder, ' mv_source_file') + const targetFolder: string = path.join(root, ' mv_target') + const targetFile: string = path.join(targetFolder, ' mv_source_file') await io.mkdirP(sourceFolder) fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) await io.mv(sourceFolder, targetFolder, {recursive: true}) @@ -210,12 +213,12 @@ describe('mv', () => { }) it('tries to move directory without -r', async () => { - let root: string = path.join(getTestTemp(), 'mv_without_-r') - let sourceFolder: string = path.join(root, 'mv_source') - let sourceFile: string = path.join(sourceFolder, 'mv_source_file') + const root: string = path.join(getTestTemp(), 'mv_without_-r') + const sourceFolder: string = path.join(root, 'mv_source') + const sourceFile: string = path.join(sourceFolder, 'mv_source_file') - let targetFolder: string = path.join(root, 'mv_target') - let targetFile: string = path.join( + const targetFolder: string = path.join(root, 'mv_target') + const targetFile: string = path.join( targetFolder, 'mv_source', 'mv_source_file' @@ -238,7 +241,7 @@ describe('mv', () => { describe('rmRF', () => { it('removes single folder with rmRF', async () => { - var testPath = path.join(getTestTemp(), 'testFolder') + const testPath = path.join(getTestTemp(), 'testFolder') await io.mkdirP(testPath) expect(fs.existsSync(testPath)).toBe(true) @@ -248,8 +251,8 @@ describe('rmRF', () => { }) it('removes recursive folders with rmRF', async () => { - var testPath = path.join(getTestTemp(), 'testDir1') - var testPath2 = path.join(testPath, 'testDir2') + const testPath = path.join(getTestTemp(), 'testDir1') + const testPath2 = path.join(testPath, 'testDir2') await io.mkdirP(testPath2) expect(fs.existsSync(testPath)).toBe(true) @@ -261,18 +264,18 @@ describe('rmRF', () => { }) it('removes folder with locked file with rmRF', async () => { - var testPath = path.join(getTestTemp(), 'testFolder') + const testPath = path.join(getTestTemp(), 'testFolder') await io.mkdirP(testPath) expect(fs.existsSync(testPath)).toBe(true) // can't remove folder with locked file on windows - var filePath = path.join(testPath, 'file.txt') + const filePath = path.join(testPath, 'file.txt') fs.appendFileSync(filePath, 'some data') expect(fs.existsSync(filePath)).toBe(true) - var fd = fs.openSync(filePath, 'r') + const fd = fs.openSync(filePath, 'r') - var worked = false + let worked = false try { await io.rmRF(testPath) worked = true @@ -292,7 +295,7 @@ describe('rmRF', () => { }) it('removes folder that doesnt exist with rmRF', async () => { - var testPath = path.join(getTestTemp(), 'testFolder') + const testPath = path.join(getTestTemp(), 'testFolder') expect(fs.existsSync(testPath)).toBe(false) await io.rmRF(testPath) @@ -300,7 +303,7 @@ describe('rmRF', () => { }) it('removes file with rmRF', async () => { - let file: string = path.join(getTestTemp(), 'rmRF_file') + const file: string = path.join(getTestTemp(), 'rmRF_file') fs.writeFileSync(file, 'test file content') expect(fs.existsSync(file)).toBe(true) await io.rmRF(file) @@ -308,7 +311,7 @@ describe('rmRF', () => { }) it('removes hidden folder with rmRF', async () => { - let directory: string = path.join(getTestTemp(), '.rmRF_directory') + const directory: string = path.join(getTestTemp(), '.rmRF_directory') await createHiddenDirectory(directory) expect(fs.existsSync(directory)).toBe(true) await io.rmRF(directory) @@ -316,7 +319,7 @@ describe('rmRF', () => { }) it('removes hidden file with rmRF', async () => { - let file: string = path.join(getTestTemp(), '.rmRF_file') + const file: string = path.join(getTestTemp(), '.rmRF_file') fs.writeFileSync(file, 'test file content') expect(fs.existsSync(file)).toBe(true) await io.rmRF(file) @@ -328,10 +331,10 @@ describe('rmRF', () => { // real_directory // real_directory/real_file // symlink_directory -> real_directory - let root: string = path.join(getTestTemp(), 'rmRF_sym_dir_test') - let realDirectory: string = path.join(root, 'real_directory') - let realFile: string = path.join(root, 'real_directory', 'real_file') - let symlinkDirectory: string = path.join(root, 'symlink_directory') + const root: string = path.join(getTestTemp(), 'rmRF_sym_dir_test') + const realDirectory: string = path.join(root, 'real_directory') + const realFile: string = path.join(root, 'real_directory', 'real_file') + const symlinkDirectory: string = path.join(root, 'symlink_directory') await io.mkdirP(realDirectory) fs.writeFileSync(realFile, 'test file content') createSymlinkDir(realDirectory, symlinkDirectory) @@ -349,9 +352,9 @@ describe('rmRF', () => { // create the following layout: // real_file // symlink_file -> real_file - let root: string = path.join(getTestTemp(), 'rmRF_sym_file_test') - let realFile: string = path.join(root, 'real_file') - let symlinkFile: string = path.join(root, 'symlink_file') + const root: string = path.join(getTestTemp(), 'rmRF_sym_file_test') + const realFile: string = path.join(root, 'real_file') + const symlinkFile: string = path.join(root, 'symlink_file') await io.mkdirP(root) fs.writeFileSync(realFile, 'test file content') fs.symlinkSync(realFile, symlinkFile) @@ -368,12 +371,12 @@ describe('rmRF', () => { // create the following layout: // real_file // symlink_file -> real_file - let root: string = path.join( + const root: string = path.join( getTestTemp(), 'rmRF_sym_file_missing_source_test' ) - let realFile: string = path.join(root, 'real_file') - let symlinkFile: string = path.join(root, 'symlink_file') + const realFile: string = path.join(root, 'real_file') + const symlinkFile: string = path.join(root, 'symlink_file') await io.mkdirP(root) fs.writeFileSync(realFile, 'test file content') fs.symlinkSync(realFile, symlinkFile) @@ -402,10 +405,13 @@ describe('rmRF', () => { // real_file // symlink_file -> real_file // symlink_level_2_file -> symlink_file - let root: string = path.join(getTestTemp(), 'rmRF_sym_level_2_file_test') - let realFile: string = path.join(root, 'real_file') - let symlinkFile: string = path.join(root, 'symlink_file') - let symlinkLevel2File: string = path.join(root, 'symlink_level_2_file') + const root: string = path.join( + getTestTemp(), + 'rmRF_sym_level_2_file_test' + ) + const realFile: string = path.join(root, 'real_file') + const symlinkFile: string = path.join(root, 'symlink_file') + const symlinkLevel2File: string = path.join(root, 'symlink_level_2_file') await io.mkdirP(root) fs.writeFileSync(realFile, 'test file content') fs.symlinkSync(realFile, symlinkFile) @@ -426,11 +432,11 @@ describe('rmRF', () => { // real_directory/real_file // outer_directory // outer_directory/symlink_file -> real_file - let root: string = path.join(getTestTemp(), 'rmRF_sym_nest_file_test') - let realDirectory: string = path.join(root, 'real_directory') - let realFile: string = path.join(root, 'real_directory', 'real_file') - let outerDirectory: string = path.join(root, 'outer_directory') - let symlinkFile: string = path.join( + const root: string = path.join(getTestTemp(), 'rmRF_sym_nest_file_test') + const realDirectory: string = path.join(root, 'real_directory') + const realFile: string = path.join(root, 'real_directory', 'real_file') + const outerDirectory: string = path.join(root, 'outer_directory') + const symlinkFile: string = path.join( root, 'outer_directory', 'symlink_file' @@ -457,19 +463,19 @@ describe('rmRF', () => { // outer_directory // outer_directory/nested_directory // outer_directory/nested_directory/symlink_file -> real_file - let root: string = path.join( + const root: string = path.join( getTestTemp(), 'rmRF_sym_deep_nest_file_test' ) - let realDirectory: string = path.join(root, 'real_directory') - let realFile: string = path.join(root, 'real_directory', 'real_file') - let outerDirectory: string = path.join(root, 'outer_directory') - let nestedDirectory: string = path.join( + const realDirectory: string = path.join(root, 'real_directory') + const realFile: string = path.join(root, 'real_directory', 'real_file') + const outerDirectory: string = path.join(root, 'outer_directory') + const nestedDirectory: string = path.join( root, 'outer_directory', 'nested_directory' ) - let symlinkFile: string = path.join( + const symlinkFile: string = path.join( root, 'outer_directory', 'nested_directory', @@ -496,10 +502,10 @@ describe('rmRF', () => { // real_directory // real_directory/real_file // symlink_directory -> real_directory - let root: string = path.join(getTestTemp(), 'rmRF_sym_dir_miss_src_test') - let realDirectory: string = path.join(root, 'real_directory') - let realFile: string = path.join(root, 'real_directory', 'real_file') - let symlinkDirectory: string = path.join(root, 'symlink_directory') + const root: string = path.join(getTestTemp(), 'rmRF_sym_dir_miss_src_test') + const realDirectory: string = path.join(root, 'real_directory') + const realFile: string = path.join(root, 'real_directory', 'real_file') + const symlinkDirectory: string = path.join(root, 'symlink_directory') await io.mkdirP(realDirectory) fs.writeFileSync(realFile, 'test file content') createSymlinkDir(realDirectory, symlinkDirectory) @@ -538,14 +544,14 @@ describe('rmRF', () => { // real_directory/real_file // symlink_directory -> real_directory // symlink_level_2_directory -> symlink_directory - let root: string = path.join( + const root: string = path.join( getTestTemp(), 'rmRF_sym_level_2_directory_test' ) - let realDirectory: string = path.join(root, 'real_directory') - let realFile: string = path.join(realDirectory, 'real_file') - let symlinkDirectory: string = path.join(root, 'symlink_directory') - let symlinkLevel2Directory: string = path.join( + const realDirectory: string = path.join(root, 'real_directory') + const realFile: string = path.join(realDirectory, 'real_file') + const symlinkDirectory: string = path.join(root, 'symlink_directory') + const symlinkLevel2Directory: string = path.join( root, 'symlink_level_2_directory' ) @@ -560,7 +566,7 @@ describe('rmRF', () => { ).toBe('test file content') if (os.platform() == 'win32') { expect(fs.readlinkSync(symlinkLevel2Directory)).toBe( - symlinkDirectory + '\\' + `${symlinkDirectory}\\` ) } else { expect(fs.readlinkSync(symlinkLevel2Directory)).toBe(symlinkDirectory) @@ -577,11 +583,11 @@ describe('rmRF', () => { // real_directory/real_file // outer_directory // outer_directory/symlink_directory -> real_directory - let root: string = path.join(getTestTemp(), 'rmRF_sym_nest_dir_test') - let realDirectory: string = path.join(root, 'real_directory') - let realFile: string = path.join(root, 'real_directory', 'real_file') - let outerDirectory: string = path.join(root, 'outer_directory') - let symlinkDirectory: string = path.join( + const root: string = path.join(getTestTemp(), 'rmRF_sym_nest_dir_test') + const realDirectory: string = path.join(root, 'real_directory') + const realFile: string = path.join(root, 'real_directory', 'real_file') + const outerDirectory: string = path.join(root, 'outer_directory') + const symlinkDirectory: string = path.join( root, 'outer_directory', 'symlink_directory' @@ -606,16 +612,16 @@ describe('rmRF', () => { // outer_directory // outer_directory/nested_directory // outer_directory/nested_directory/symlink_directory -> real_directory - let root: string = path.join(getTestTemp(), 'rmRF_sym_deep_nest_dir_test') - let realDirectory: string = path.join(root, 'real_directory') - let realFile: string = path.join(root, 'real_directory', 'real_file') - let outerDirectory: string = path.join(root, 'outer_directory') - let nestedDirectory: string = path.join( + const root: string = path.join(getTestTemp(), 'rmRF_sym_deep_nest_dir_test') + const realDirectory: string = path.join(root, 'real_directory') + const realFile: string = path.join(root, 'real_directory', 'real_file') + const outerDirectory: string = path.join(root, 'outer_directory') + const nestedDirectory: string = path.join( root, 'outer_directory', 'nested_directory' ) - let symlinkDirectory: string = path.join( + const symlinkDirectory: string = path.join( root, 'outer_directory', 'nested_directory', @@ -635,7 +641,7 @@ describe('rmRF', () => { }) it('removes hidden file with rmRF', async () => { - let file: string = path.join(getTestTemp(), '.rmRF_file') + const file: string = path.join(getTestTemp(), '.rmRF_file') await io.mkdirP(path.dirname(file)) await createHiddenFile(file, 'test file content') expect(fs.existsSync(file)).toBe(true) @@ -650,22 +656,22 @@ describe('mkdirP', () => { }) it('creates folder', async () => { - var testPath = path.join(getTestTemp(), 'mkdirTest') + const testPath = path.join(getTestTemp(), 'mkdirTest') await io.mkdirP(testPath) expect(fs.existsSync(testPath)).toBe(true) }) it('creates nested folders with mkdirP', async () => { - var testPath = path.join(getTestTemp(), 'mkdir1', 'mkdir2') + const testPath = path.join(getTestTemp(), 'mkdir1', 'mkdir2') await io.mkdirP(testPath) expect(fs.existsSync(testPath)).toBe(true) }) it('fails if mkdirP with illegal chars', async () => { - var testPath = path.join(getTestTemp(), 'mkdir\0') - var worked: boolean = false + const testPath = path.join(getTestTemp(), 'mkdir\0') + let worked: boolean = false try { await io.mkdirP(testPath) worked = true @@ -677,7 +683,7 @@ describe('mkdirP', () => { }) it('fails if mkdirP with empty path', async () => { - var worked: boolean = false + let worked: boolean = false try { await io.mkdirP('') worked = true @@ -687,7 +693,7 @@ describe('mkdirP', () => { }) it('fails if mkdirP with conflicting file path', async () => { - let testPath = path.join(getTestTemp(), 'mkdirP_conflicting_file_path') + const testPath = path.join(getTestTemp(), 'mkdirP_conflicting_file_path') await io.mkdirP(getTestTemp()) fs.writeFileSync(testPath, '') let worked: boolean = false @@ -700,7 +706,7 @@ describe('mkdirP', () => { }) it('fails if mkdirP with conflicting parent file path', async () => { - let testPath = path.join( + const testPath = path.join( getTestTemp(), 'mkdirP_conflicting_parent_file_path', 'dir' @@ -717,7 +723,7 @@ describe('mkdirP', () => { }) it('no-ops if mkdirP directory exists', async () => { - let testPath = path.join(getTestTemp(), 'mkdirP_dir_exists') + const testPath = path.join(getTestTemp(), 'mkdirP_dir_exists') await io.mkdirP(testPath) expect(fs.existsSync(testPath)).toBe(true) @@ -731,10 +737,10 @@ describe('mkdirP', () => { // real_dir // real_dir/file.txt // symlink_dir -> real_dir - let rootPath = path.join(getTestTemp(), 'mkdirP_symlink_dir') - let realDirPath = path.join(rootPath, 'real_dir') - let realFilePath = path.join(realDirPath, 'file.txt') - let symlinkDirPath = path.join(rootPath, 'symlink_dir') + const rootPath = path.join(getTestTemp(), 'mkdirP_symlink_dir') + const realDirPath = path.join(rootPath, 'real_dir') + const realFilePath = path.join(realDirPath, 'file.txt') + const symlinkDirPath = path.join(rootPath, 'symlink_dir') await io.mkdirP(getTestTemp()) fs.mkdirSync(rootPath) fs.mkdirSync(realDirPath) @@ -755,17 +761,17 @@ describe('mkdirP', () => { // real_dir // real_dir/file.txt // symlink_dir -> real_dir - let rootPath = path.join(getTestTemp(), 'mkdirP_parent_symlink_dir') - let realDirPath = path.join(rootPath, 'real_dir') - let realFilePath = path.join(realDirPath, 'file.txt') - let symlinkDirPath = path.join(rootPath, 'symlink_dir') + const rootPath = path.join(getTestTemp(), 'mkdirP_parent_symlink_dir') + const realDirPath = path.join(rootPath, 'real_dir') + const realFilePath = path.join(realDirPath, 'file.txt') + const symlinkDirPath = path.join(rootPath, 'symlink_dir') await io.mkdirP(getTestTemp()) fs.mkdirSync(rootPath) fs.mkdirSync(realDirPath) fs.writeFileSync(realFilePath, 'test real_dir/file.txt contet') createSymlinkDir(realDirPath, symlinkDirPath) - let subDirPath = path.join(symlinkDirPath, 'sub_dir') + const subDirPath = path.join(symlinkDirPath, 'sub_dir') await io.mkdirP(subDirPath) // the subdirectory should be accessible via the real directory @@ -775,7 +781,7 @@ describe('mkdirP', () => { }) it('breaks if mkdirP loop out of control', async () => { - let testPath = path.join( + const testPath = path.join( getTestTemp(), 'mkdirP_failsafe', '1', @@ -805,20 +811,20 @@ describe('mkdirP', () => { describe('which', () => { it('which() finds file name', async () => { // create a executable file - let testPath = path.join(getTestTemp(), 'which-finds-file-name') + const testPath = path.join(getTestTemp(), 'which-finds-file-name') await io.mkdirP(testPath) let fileName = 'Which-Test-File' if (process.platform == 'win32') { fileName += '.exe' } - let filePath = path.join(testPath, fileName) + const filePath = path.join(testPath, fileName) fs.writeFileSync(filePath, '') if (process.platform != 'win32') { chmod(filePath, '+x') } - let originalPath = process.env['PATH'] + const originalPath = process.env['PATH'] try { // update the PATH process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath @@ -878,7 +884,7 @@ describe('which', () => { it('which() searches path in order', async () => { // create a chcp.com/bash override file - let testPath = path.join(getTestTemp(), 'which-searches-path-in-order') + const testPath = path.join(getTestTemp(), 'which-searches-path-in-order') await io.mkdirP(testPath) let fileName if (process.platform == 'win32') { @@ -887,16 +893,16 @@ describe('which', () => { fileName = 'bash' } - let filePath = path.join(testPath, fileName) + const filePath = path.join(testPath, fileName) fs.writeFileSync(filePath, '') if (process.platform != 'win32') { chmod(filePath, '+x') } - let originalPath = process.env['PATH'] + const originalPath = process.env['PATH'] try { // sanity - regular chcp.com/bash should be found - let originalWhich = await io.which(fileName) + const originalWhich = await io.which(fileName) expect(!!(originalWhich || '')).toBe(true) // modify PATH @@ -913,20 +919,20 @@ describe('which', () => { // create a non-executable file // on Windows, should not end in valid PATHEXT // on Mac/Linux should not have executable bit - let testPath = path.join(getTestTemp(), 'which-requires-executable') + const testPath = path.join(getTestTemp(), 'which-requires-executable') await io.mkdirP(testPath) let fileName = 'Which-Test-File' if (process.platform == 'win32') { fileName += '.abc' // not a valid PATHEXT } - let filePath = path.join(testPath, fileName) + const filePath = path.join(testPath, fileName) fs.writeFileSync(filePath, '') if (process.platform != 'win32') { chmod(filePath, '-x') } - let originalPath = process.env['PATH'] + const originalPath = process.env['PATH'] try { // modify PATH process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath @@ -947,7 +953,7 @@ describe('which', () => { it('which() ignores directory match', async () => { // create a directory - let testPath = path.join(getTestTemp(), 'which-ignores-directory-match') + const testPath = path.join(getTestTemp(), 'which-ignores-directory-match') let dirPath = path.join(testPath, 'Which-Test-Dir') if (process.platform == 'win32') { dirPath += '.exe' @@ -958,7 +964,7 @@ describe('which', () => { chmod(dirPath, '+x') } - let originalPath = process.env['PATH'] + const originalPath = process.env['PATH'] try { // modify PATH process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath @@ -972,7 +978,7 @@ describe('which', () => { it('which() allows rooted path', async () => { // create an executable file - let testPath = path.join(getTestTemp(), 'which-allows-rooted-path') + const testPath = path.join(getTestTemp(), 'which-allows-rooted-path') await io.mkdirP(testPath) let filePath = path.join(testPath, 'Which-Test-File') if (process.platform == 'win32') { @@ -994,7 +1000,7 @@ describe('which', () => { // create a non-executable file // on Windows, should not end in valid PATHEXT // on Mac/Linux, should not have executable bit - let testPath = path.join( + const testPath = path.join( getTestTemp(), 'which-requires-rooted-path-to-be-executable' ) @@ -1024,7 +1030,7 @@ describe('which', () => { it('which() requires rooted path to be a file', async () => { // create a dir - let testPath = path.join( + const testPath = path.join( getTestTemp(), 'which-requires-rooted-path-to-be-executable' ) @@ -1071,31 +1077,31 @@ describe('which', () => { it('which() does not allow separators', async () => { // create an executable file - let testDirName = 'which-does-not-allow-separators' - let testPath = path.join(getTestTemp(), testDirName) + const testDirName = 'which-does-not-allow-separators' + const testPath = path.join(getTestTemp(), testDirName) await io.mkdirP(testPath) let fileName = 'Which-Test-File' if (process.platform == 'win32') { fileName += '.exe' } - let filePath = path.join(testPath, fileName) + const filePath = path.join(testPath, fileName) fs.writeFileSync(filePath, '') if (process.platform != 'win32') { chmod(filePath, '+x') } - let originalPath = process.env['PATH'] + const originalPath = process.env['PATH'] try { // modify PATH process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath // which "dir/file", should not be found - expect((await io.which(testDirName + '/' + fileName)) || '').toBe('') + expect((await io.which(`${testDirName}/${fileName}`)) || '').toBe('') // on Windows, also try "dir\file" if (process.platform == 'win32') { - expect((await io.which(testDirName + '\\' + fileName)) || '').toBe('') + expect((await io.which(`${testDirName}\\${fileName}`)) || '').toBe('') } } finally { process.env['PATH'] = originalPath @@ -1114,10 +1120,10 @@ describe('which', () => { it('which() appends ext on windows', async () => { // create executable files - let testPath = path.join(getTestTemp(), 'which-appends-ext-on-windows') + const testPath = path.join(getTestTemp(), 'which-appends-ext-on-windows') await io.mkdirP(testPath) // PATHEXT=.COM;.EXE;.BAT;.CMD... - let files: {[key: string]: string} = { + const files: {[key: string]: string} = { 'which-test-file-1': path.join(testPath, 'which-test-file-1.com'), 'which-test-file-2': path.join(testPath, 'which-test-file-2.exe'), 'which-test-file-3': path.join(testPath, 'which-test-file-3.bat'), @@ -1127,17 +1133,17 @@ describe('which', () => { 'which-test-file-5.txt.com' ) } - for (let fileName of Object.keys(files)) { + for (const fileName of Object.keys(files)) { fs.writeFileSync(files[fileName], '') } - let originalPath = process.env['PATH'] + const originalPath = process.env['PATH'] try { // modify PATH process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath // find each file - for (let fileName of Object.keys(files)) { + for (const fileName of Object.keys(files)) { expect(await io.which(fileName)).toBe(files[fileName]) } } finally { @@ -1147,13 +1153,13 @@ describe('which', () => { it('which() appends ext on windows when rooted', async () => { // create executable files - let testPath = path.join( + const testPath = path.join( getTestTemp(), 'which-appends-ext-on-windows-when-rooted' ) await io.mkdirP(testPath) // PATHEXT=.COM;.EXE;.BAT;.CMD... - let files: {[key: string]: string} = {} + const files: {[key: string]: string} = {} files[path.join(testPath, 'which-test-file-1')] = path.join( testPath, 'which-test-file-1.com' @@ -1174,12 +1180,12 @@ describe('which', () => { testPath, 'which-test-file-5.txt.com' ) - for (let fileName of Object.keys(files)) { + for (const fileName of Object.keys(files)) { fs.writeFileSync(files[fileName], '') } // find each file - for (let fileName of Object.keys(files)) { + for (const fileName of Object.keys(files)) { expect(await io.which(fileName)).toBe(files[fileName]) } }) @@ -1193,17 +1199,17 @@ describe('which', () => { // // preference, within the same dir, should be given to the exact match (even though // .EXE is defined with higher preference than .BAT in PATHEXT (PATHEXT=.COM;.EXE;.BAT;.CMD...) - let testPath = path.join( + const testPath = path.join( getTestTemp(), 'which-prefer-exact-match-on-windows' ) await io.mkdirP(testPath) - let fileName = 'which-test-file.bat' - let expectedFilePath = path.join(testPath, fileName) - let notExpectedFilePath = path.join(testPath, fileName + '.exe') + const fileName = 'which-test-file.bat' + const expectedFilePath = path.join(testPath, fileName) + const notExpectedFilePath = path.join(testPath, `${fileName}.exe`) fs.writeFileSync(expectedFilePath, '') fs.writeFileSync(notExpectedFilePath, '') - let originalPath = process.env['PATH'] + const originalPath = process.env['PATH'] try { process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath expect(await io.which(fileName)).toBe(expectedFilePath) @@ -1221,14 +1227,14 @@ describe('which', () => { // // preference, within the same dir, should be given to the exact match (even though // .EXE is defined with higher preference than .BAT in PATHEXT (PATHEXT=.COM;.EXE;.BAT;.CMD...) - let testPath = path.join( + const testPath = path.join( getTestTemp(), 'which-prefer-exact-match-on-windows-when-rooted' ) await io.mkdirP(testPath) - let fileName = 'which-test-file.bat' - let expectedFilePath = path.join(testPath, fileName) - let notExpectedFilePath = path.join(testPath, fileName + '.exe') + const fileName = 'which-test-file.bat' + const expectedFilePath = path.join(testPath, fileName) + const notExpectedFilePath = path.join(testPath, `${fileName}.exe`) fs.writeFileSync(expectedFilePath, '') fs.writeFileSync(notExpectedFilePath, '') expect(await io.which(path.join(testPath, fileName))).toBe( @@ -1237,93 +1243,93 @@ describe('which', () => { }) it('which() searches ext in order', async () => { - let testPath = path.join(getTestTemp(), 'which-searches-ext-in-order') + const testPath = path.join(getTestTemp(), 'which-searches-ext-in-order') // create a directory for testing .COM order preference // PATHEXT=.COM;.EXE;.BAT;.CMD... - let fileNameWithoutExtension = 'which-test-file' - let comTestPath = path.join(testPath, 'com-test') + const fileNameWithoutExtension = 'which-test-file' + const comTestPath = path.join(testPath, 'com-test') await io.mkdirP(comTestPath) fs.writeFileSync( - path.join(comTestPath, fileNameWithoutExtension + '.com'), + path.join(comTestPath, `${fileNameWithoutExtension}.com`), '' ) fs.writeFileSync( - path.join(comTestPath, fileNameWithoutExtension + '.exe'), + path.join(comTestPath, `${fileNameWithoutExtension}.exe`), '' ) fs.writeFileSync( - path.join(comTestPath, fileNameWithoutExtension + '.bat'), + path.join(comTestPath, `${fileNameWithoutExtension}.bat`), '' ) fs.writeFileSync( - path.join(comTestPath, fileNameWithoutExtension + '.cmd'), + path.join(comTestPath, `${fileNameWithoutExtension}.cmd`), '' ) // create a directory for testing .EXE order preference // PATHEXT=.COM;.EXE;.BAT;.CMD... - let exeTestPath = path.join(testPath, 'exe-test') + const exeTestPath = path.join(testPath, 'exe-test') await io.mkdirP(exeTestPath) fs.writeFileSync( - path.join(exeTestPath, fileNameWithoutExtension + '.exe'), + path.join(exeTestPath, `${fileNameWithoutExtension}.exe`), '' ) fs.writeFileSync( - path.join(exeTestPath, fileNameWithoutExtension + '.bat'), + path.join(exeTestPath, `${fileNameWithoutExtension}.bat`), '' ) fs.writeFileSync( - path.join(exeTestPath, fileNameWithoutExtension + '.cmd'), + path.join(exeTestPath, `${fileNameWithoutExtension}.cmd`), '' ) // create a directory for testing .BAT order preference // PATHEXT=.COM;.EXE;.BAT;.CMD... - let batTestPath = path.join(testPath, 'bat-test') + const batTestPath = path.join(testPath, 'bat-test') await io.mkdirP(batTestPath) fs.writeFileSync( - path.join(batTestPath, fileNameWithoutExtension + '.bat'), + path.join(batTestPath, `${fileNameWithoutExtension}.bat`), '' ) fs.writeFileSync( - path.join(batTestPath, fileNameWithoutExtension + '.cmd'), + path.join(batTestPath, `${fileNameWithoutExtension}.cmd`), '' ) // create a directory for testing .CMD - let cmdTestPath = path.join(testPath, 'cmd-test') + const cmdTestPath = path.join(testPath, 'cmd-test') await io.mkdirP(cmdTestPath) - let cmdTest_cmdFilePath = path.join( + const cmdTest_cmdFilePath = path.join( cmdTestPath, - fileNameWithoutExtension + '.cmd' + `${fileNameWithoutExtension}.cmd` ) fs.writeFileSync(cmdTest_cmdFilePath, '') - let originalPath = process.env['PATH'] + const originalPath = process.env['PATH'] try { // test .COM process.env['PATH'] = comTestPath + path.delimiter + originalPath expect(await io.which(fileNameWithoutExtension)).toBe( - path.join(comTestPath, fileNameWithoutExtension + '.com') + path.join(comTestPath, `${fileNameWithoutExtension}.com`) ) // test .EXE process.env['PATH'] = exeTestPath + path.delimiter + originalPath expect(await io.which(fileNameWithoutExtension)).toBe( - path.join(exeTestPath, fileNameWithoutExtension + '.exe') + path.join(exeTestPath, `${fileNameWithoutExtension}.exe`) ) // test .BAT process.env['PATH'] = batTestPath + path.delimiter + originalPath expect(await io.which(fileNameWithoutExtension)).toBe( - path.join(batTestPath, fileNameWithoutExtension + '.bat') + path.join(batTestPath, `${fileNameWithoutExtension}.bat`) ) // test .CMD process.env['PATH'] = cmdTestPath + path.delimiter + originalPath expect(await io.which(fileNameWithoutExtension)).toBe( - path.join(cmdTestPath, fileNameWithoutExtension + '.cmd') + path.join(cmdTestPath, `${fileNameWithoutExtension}.cmd`) ) } finally { process.env['PATH'] = originalPath @@ -1334,18 +1340,18 @@ describe('which', () => { async function findsExecutableWithScopedPermissions(chmodOptions: string) { // create a executable file - let testPath = path.join(getTestTemp(), 'which-finds-file-name') + const testPath = path.join(getTestTemp(), 'which-finds-file-name') await io.mkdirP(testPath) - let fileName = 'Which-Test-File' + const fileName = 'Which-Test-File' if (process.platform == 'win32') { return } - let filePath = path.join(testPath, fileName) + const filePath = path.join(testPath, fileName) fs.writeFileSync(filePath, '') chmod(filePath, chmodOptions) - let originalPath = process.env['PATH'] + const originalPath = process.env['PATH'] try { // update the PATH process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath @@ -1378,9 +1384,9 @@ async function findsExecutableWithScopedPermissions(chmodOptions: string) { } function chmod(file: string, mode: string): void { - let result = child.spawnSync('chmod', [mode, file]) + const result = child.spawnSync('chmod', [mode, file]) if (result.status != 0) { - let message: string = (result.output || []).join(' ').trim() + const message: string = (result.output || []).join(' ').trim() throw new Error(`Command failed: "chmod ${mode} ${file}". ${message}`) } } @@ -1393,9 +1399,9 @@ async function createHiddenDirectory(dir: string): Promise { await io.mkdirP(dir) if (os.platform() == 'win32') { - let result = child.spawnSync('attrib.exe', ['+H', dir]) + const result = child.spawnSync('attrib.exe', ['+H', dir]) if (result.status != 0) { - let message: string = (result.output || []).join(' ').trim() + const message: string = (result.output || []).join(' ').trim() reject( `Failed to set hidden attribute for directory '${dir}'. ${message}` ) @@ -1414,9 +1420,9 @@ async function createHiddenFile(file: string, content: string): Promise { await io.mkdirP(path.dirname(file)) fs.writeFileSync(file, content) if (os.platform() == 'win32') { - let result = child.spawnSync('attrib.exe', ['+H', file]) + const result = child.spawnSync('attrib.exe', ['+H', file]) if (result.status != 0) { - let message: string = (result.output || []).join(' ').trim() + const message: string = (result.output || []).join(' ').trim() reject(`Failed to set hidden attribute for file '${file}'. ${message}`) } } diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index ac1331529e..605019cd43 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -207,7 +207,7 @@ export function mkdirP(p: string): Promise { } // build a stack of directories to create - let stack: string[] = [] + const stack: string[] = [] let testDir: string = p while (true) { // validate the loop is not out of control @@ -224,7 +224,7 @@ export function mkdirP(p: string): Promise { } catch (err) { if (err.code == 'ENOENT') { // validate the directory is not the drive root - let parentDir = path.dirname(testDir) + const parentDir = path.dirname(testDir) if (testDir == parentDir) { throw new Error( `Unable to create directory '${p}'. Root directory does not exist: '${testDir}'` @@ -256,7 +256,7 @@ export function mkdirP(p: string): Promise { // create each directory while (stack.length) { - let dir = stack.pop()! // non-null because `stack.length` was truthy + const dir = stack.pop()! // non-null because `stack.length` was truthy fs.mkdirSync(dir) } } catch (err) { @@ -285,7 +285,7 @@ export function which(tool: string, check?: boolean): Promise { // recursive when check=true if (check) { - let result: string = await which(tool, false) + const result: string = await which(tool, false) if (!result) { if (process.platform == 'win32') { reject( @@ -302,9 +302,9 @@ export function which(tool: string, check?: boolean): Promise { try { // build the list of extensions to try - let extensions: string[] = [] + const extensions: string[] = [] if (process.platform == 'win32' && process.env['PATHEXT']) { - for (let extension of process.env['PATHEXT']!.split(path.delimiter)) { + for (const extension of process.env['PATHEXT']!.split(path.delimiter)) { if (extension) { extensions.push(extension) } @@ -313,7 +313,7 @@ export function which(tool: string, check?: boolean): Promise { // if it's rooted, return it if exists. otherwise return empty. if (util._isRooted(tool)) { - let filePath: string = util._tryGetExecutablePath(tool, extensions) + const filePath: string = util._tryGetExecutablePath(tool, extensions) if (filePath) { resolve(filePath) return @@ -338,9 +338,9 @@ export function which(tool: string, check?: boolean): Promise { // it feels like we should not do this. Checking the current directory seems like more of a use // case of a shell, and the which() function exposed by the task lib should strive for consistency // across platforms. - let directories: string[] = [] + const directories: string[] = [] if (process.env['PATH']) { - for (let p of process.env['PATH']!.split(path.delimiter)) { + for (const p of process.env['PATH']!.split(path.delimiter)) { if (p) { directories.push(p) } @@ -348,8 +348,8 @@ export function which(tool: string, check?: boolean): Promise { } // return the first match - for (let directory of directories) { - let filePath = util._tryGetExecutablePath( + for (const directory of directories) { + const filePath = util._tryGetExecutablePath( directory + path.sep + tool, extensions ) @@ -384,9 +384,9 @@ function copyDirectoryContents( } // Copy all child files, and directories recursively - let sourceChildren: string[] = fs.readdirSync(source) + const sourceChildren: string[] = fs.readdirSync(source) sourceChildren.forEach(newSource => { - let newDest = path.join(dest, path.basename(newSource)) + const newDest = path.join(dest, path.basename(newSource)) copyDirectoryContents( path.resolve(source, newSource), newDest, diff --git a/packages/io/src/ioUtil.ts b/packages/io/src/ioUtil.ts index f89ad033c3..1211a40511 100644 --- a/packages/io/src/ioUtil.ts +++ b/packages/io/src/ioUtil.ts @@ -4,7 +4,7 @@ import path = require('path') export function _removeDirectory(directoryPath: string) { if (fs.existsSync(directoryPath)) { fs.readdirSync(directoryPath).forEach(fileName => { - let file = path.join(directoryPath, fileName) + const file = path.join(directoryPath, fileName) if (fs.lstatSync(file).isDirectory()) { _removeDirectory(file) } else { @@ -46,14 +46,14 @@ export function _tryGetExecutablePath( ): string { try { // test file exists - let stats: fs.Stats = fs.statSync(filePath) + const stats: fs.Stats = fs.statSync(filePath) if (stats.isFile()) { if (process.platform == 'win32') { // on Windows, test for valid extension - let fileName = path.basename(filePath) - let dotIndex = fileName.lastIndexOf('.') + const fileName = path.basename(filePath) + const dotIndex = fileName.lastIndexOf('.') if (dotIndex >= 0) { - let upperExt = fileName.substr(dotIndex).toUpperCase() + const upperExt = fileName.substr(dotIndex).toUpperCase() if (extensions.some(validExt => validExt.toUpperCase() == upperExt)) { return filePath } @@ -73,18 +73,18 @@ export function _tryGetExecutablePath( } // try each extension - let originalFilePath = filePath - for (let extension of extensions) { + const originalFilePath = filePath + for (const extension of extensions) { let filePath = originalFilePath + extension try { - let stats: fs.Stats = fs.statSync(filePath) + const stats: fs.Stats = fs.statSync(filePath) if (stats.isFile()) { if (process.platform == 'win32') { // preserve the case of the actual file (since an extension was appended) try { - let directory = path.dirname(filePath) - let upperName = path.basename(filePath).toUpperCase() - for (let actualName of fs.readdirSync(directory)) { + const directory = path.dirname(filePath) + const upperName = path.basename(filePath).toUpperCase() + for (const actualName of fs.readdirSync(directory)) { if (upperName == actualName.toUpperCase()) { filePath = path.join(directory, actualName) break @@ -122,7 +122,7 @@ function _normalizeSeparators(p: string): string { p = p.replace(/\//g, '\\') // remove redundant slashes - let isUnc = /^\\\\+[^\\]/.test(p) // e.g. \\hello + const isUnc = /^\\\\+[^\\]/.test(p) // e.g. \\hello return (isUnc ? '\\' : '') + p.replace(/\\\\+/g, '\\') // preserve leading // for UNC } From dedef5ecff7a417d719ec47122b8891c6eec7691 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 15:39:32 -0400 Subject: [PATCH 05/27] Fix equality lint errors --- packages/io/__tests__/io.test.ts | 62 ++++++++++++++++---------------- packages/io/src/io.ts | 18 +++++----- packages/io/src/ioUtil.ts | 24 +++++++------ 3 files changed, 53 insertions(+), 51 deletions(-) diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index 4fd90181a6..dcfe3c5bb5 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -347,7 +347,7 @@ describe('rmRF', () => { }) // creating a symlink to a file on Windows requires elevated - if (os.platform() != 'win32') { + if (os.platform() !== 'win32') { it('removes symlink file with rmRF', async () => { // create the following layout: // real_file @@ -564,7 +564,7 @@ describe('rmRF', () => { encoding: 'utf8' }) ).toBe('test file content') - if (os.platform() == 'win32') { + if (os.platform() === 'win32') { expect(fs.readlinkSync(symlinkLevel2Directory)).toBe( `${symlinkDirectory}\\` ) @@ -814,13 +814,13 @@ describe('which', () => { const testPath = path.join(getTestTemp(), 'which-finds-file-name') await io.mkdirP(testPath) let fileName = 'Which-Test-File' - if (process.platform == 'win32') { + if (process.platform === 'win32') { fileName += '.exe' } const filePath = path.join(testPath, fileName) fs.writeFileSync(filePath, '') - if (process.platform != 'win32') { + if (process.platform !== 'win32') { chmod(filePath, '+x') } @@ -834,7 +834,7 @@ describe('which', () => { expect(await io.which(fileName, false)).toBe(filePath) expect(await io.which(fileName, true)).toBe(filePath) - if (process.platform == 'win32') { + if (process.platform === 'win32') { // not case sensitive on windows expect(await io.which('which-test-file.exe')).toBe( path.join(testPath, 'which-test-file.exe') @@ -853,7 +853,7 @@ describe('which', () => { expect(await io.which('which-test-file')).toBe(filePath) expect(await io.which('which-test-file', false)).toBe(filePath) expect(await io.which('which-test-file', true)).toBe(filePath) - } else if (process.platform == 'darwin') { + } else if (process.platform === 'darwin') { // not case sensitive on Mac expect(await io.which(fileName.toUpperCase())).toBe( path.join(testPath, fileName.toUpperCase()) @@ -887,7 +887,7 @@ describe('which', () => { const testPath = path.join(getTestTemp(), 'which-searches-path-in-order') await io.mkdirP(testPath) let fileName - if (process.platform == 'win32') { + if (process.platform === 'win32') { fileName = 'chcp.com' } else { fileName = 'bash' @@ -895,7 +895,7 @@ describe('which', () => { const filePath = path.join(testPath, fileName) fs.writeFileSync(filePath, '') - if (process.platform != 'win32') { + if (process.platform !== 'win32') { chmod(filePath, '+x') } @@ -922,13 +922,13 @@ describe('which', () => { const testPath = path.join(getTestTemp(), 'which-requires-executable') await io.mkdirP(testPath) let fileName = 'Which-Test-File' - if (process.platform == 'win32') { + if (process.platform === 'win32') { fileName += '.abc' // not a valid PATHEXT } const filePath = path.join(testPath, fileName) fs.writeFileSync(filePath, '') - if (process.platform != 'win32') { + if (process.platform !== 'win32') { chmod(filePath, '-x') } @@ -955,12 +955,12 @@ describe('which', () => { // create a directory const testPath = path.join(getTestTemp(), 'which-ignores-directory-match') let dirPath = path.join(testPath, 'Which-Test-Dir') - if (process.platform == 'win32') { + if (process.platform === 'win32') { dirPath += '.exe' } await io.mkdirP(dirPath) - if (process.platform != 'win32') { + if (process.platform !== 'win32') { chmod(dirPath, '+x') } @@ -981,12 +981,12 @@ describe('which', () => { const testPath = path.join(getTestTemp(), 'which-allows-rooted-path') await io.mkdirP(testPath) let filePath = path.join(testPath, 'Which-Test-File') - if (process.platform == 'win32') { + if (process.platform === 'win32') { filePath += '.exe' } fs.writeFileSync(filePath, '') - if (process.platform != 'win32') { + if (process.platform !== 'win32') { chmod(filePath, '+x') } @@ -1006,12 +1006,12 @@ describe('which', () => { ) await io.mkdirP(testPath) let filePath = path.join(testPath, 'Which-Test-File') - if (process.platform == 'win32') { + if (process.platform === 'win32') { filePath += '.abc' // not a valid PATHEXT } fs.writeFileSync(filePath, '') - if (process.platform != 'win32') { + if (process.platform !== 'win32') { chmod(filePath, '-x') } @@ -1035,12 +1035,12 @@ describe('which', () => { 'which-requires-rooted-path-to-be-executable' ) let dirPath = path.join(testPath, 'Which-Test-Dir') - if (process.platform == 'win32') { + if (process.platform === 'win32') { dirPath += '.exe' } await io.mkdirP(dirPath) - if (process.platform != 'win32') { + if (process.platform !== 'win32') { chmod(dirPath, '+x') } @@ -1059,7 +1059,7 @@ describe('which', () => { it('which() requires rooted path to exist', async () => { let filePath = path.join(__dirname, 'no-such-file') - if (process.platform == 'win32') { + if (process.platform === 'win32') { filePath += '.exe' } @@ -1081,13 +1081,13 @@ describe('which', () => { const testPath = path.join(getTestTemp(), testDirName) await io.mkdirP(testPath) let fileName = 'Which-Test-File' - if (process.platform == 'win32') { + if (process.platform === 'win32') { fileName += '.exe' } const filePath = path.join(testPath, fileName) fs.writeFileSync(filePath, '') - if (process.platform != 'win32') { + if (process.platform !== 'win32') { chmod(filePath, '+x') } @@ -1100,7 +1100,7 @@ describe('which', () => { expect((await io.which(`${testDirName}/${fileName}`)) || '').toBe('') // on Windows, also try "dir\file" - if (process.platform == 'win32') { + if (process.platform === 'win32') { expect((await io.which(`${testDirName}\\${fileName}`)) || '').toBe('') } } finally { @@ -1108,7 +1108,7 @@ describe('which', () => { } }) - if (process.platform == 'win32') { + if (process.platform === 'win32') { it('which() resolves actual case file name when extension is applied', async () => { const comspec: string = process.env['ComSpec'] || '' expect(!!comspec).toBe(true) @@ -1343,7 +1343,7 @@ async function findsExecutableWithScopedPermissions(chmodOptions: string) { const testPath = path.join(getTestTemp(), 'which-finds-file-name') await io.mkdirP(testPath) const fileName = 'Which-Test-File' - if (process.platform == 'win32') { + if (process.platform === 'win32') { return } @@ -1361,7 +1361,7 @@ async function findsExecutableWithScopedPermissions(chmodOptions: string) { expect(await io.which(fileName, false)).toBe(filePath) expect(await io.which(fileName, true)).toBe(filePath) - if (process.platform == 'darwin') { + if (process.platform === 'darwin') { // not case sensitive on Mac expect(await io.which(fileName.toUpperCase())).toBe( path.join(testPath, fileName.toUpperCase()) @@ -1385,7 +1385,7 @@ async function findsExecutableWithScopedPermissions(chmodOptions: string) { function chmod(file: string, mode: string): void { const result = child.spawnSync('chmod', [mode, file]) - if (result.status != 0) { + if (result.status !== 0) { const message: string = (result.output || []).join(' ').trim() throw new Error(`Command failed: "chmod ${mode} ${file}". ${message}`) } @@ -1398,9 +1398,9 @@ async function createHiddenDirectory(dir: string): Promise { } await io.mkdirP(dir) - if (os.platform() == 'win32') { + if (os.platform() === 'win32') { const result = child.spawnSync('attrib.exe', ['+H', dir]) - if (result.status != 0) { + if (result.status !== 0) { const message: string = (result.output || []).join(' ').trim() reject( `Failed to set hidden attribute for directory '${dir}'. ${message}` @@ -1419,9 +1419,9 @@ async function createHiddenFile(file: string, content: string): Promise { await io.mkdirP(path.dirname(file)) fs.writeFileSync(file, content) - if (os.platform() == 'win32') { + if (os.platform() === 'win32') { const result = child.spawnSync('attrib.exe', ['+H', file]) - if (result.status != 0) { + if (result.status !== 0) { const message: string = (result.output || []).join(' ').trim() reject(`Failed to set hidden attribute for file '${file}'. ${message}`) } @@ -1440,7 +1440,7 @@ function getTestTemp() { * A symlink directory is not created on Windows since it requires an elevated context. */ function createSymlinkDir(real: string, link: string): void { - if (os.platform() == 'win32') { + if (os.platform() === 'win32') { fs.symlinkSync(real, link, 'junction') } else { fs.symlinkSync(real, link) diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index 605019cd43..0e25f4dd81 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -145,7 +145,7 @@ export function rmRF(inputPath: string): Promise { } catch (err) { // if you try to delete a file that doesn't exist, desired result is achieved // other errors are valid - if (err.code != 'ENOENT') { + if (err.code !== 'ENOENT') { reject(err) return } @@ -157,7 +157,7 @@ export function rmRF(inputPath: string): Promise { } catch (err) { // if you try to delete a file that doesn't exist, desired result is achieved // other errors are valid - if (err.code != 'ENOENT') { + if (err.code !== 'ENOENT') { reject(err) return } @@ -171,7 +171,7 @@ export function rmRF(inputPath: string): Promise { } catch (err) { // if you try to delete a file that doesn't exist, desired result is achieved // other errors are valid - if (err.code == 'ENOENT') { + if (err.code === 'ENOENT') { resolve() } else { reject(err) @@ -222,10 +222,10 @@ export function mkdirP(p: string): Promise { try { stats = fs.statSync(testDir) } catch (err) { - if (err.code == 'ENOENT') { + if (err.code === 'ENOENT') { // validate the directory is not the drive root const parentDir = path.dirname(testDir) - if (testDir == parentDir) { + if (testDir === parentDir) { throw new Error( `Unable to create directory '${p}'. Root directory does not exist: '${testDir}'` ) @@ -235,7 +235,7 @@ export function mkdirP(p: string): Promise { stack.push(testDir) testDir = parentDir continue - } else if (err.code == 'UNKNOWN') { + } else if (err.code === 'UNKNOWN') { throw new Error( `Unable to create directory '${p}'. Unable to verify the directory exists: '${testDir}'. If directory is a file share, please verify the share name is correct, the share is online, and the current process has permission to access the share.` ) @@ -287,7 +287,7 @@ export function which(tool: string, check?: boolean): Promise { if (check) { const result: string = await which(tool, false) if (!result) { - if (process.platform == 'win32') { + if (process.platform === 'win32') { reject( `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.` ) @@ -303,7 +303,7 @@ export function which(tool: string, check?: boolean): Promise { try { // build the list of extensions to try const extensions: string[] = [] - if (process.platform == 'win32' && process.env['PATHEXT']) { + if (process.platform === 'win32' && process.env['PATHEXT']) { for (const extension of process.env['PATHEXT']!.split(path.delimiter)) { if (extension) { extensions.push(extension) @@ -326,7 +326,7 @@ export function which(tool: string, check?: boolean): Promise { // if any path separators, return empty if ( tool.indexOf('/') >= 0 || - (process.platform == 'win32' && tool.indexOf('\\') >= 0) + (process.platform === 'win32' && tool.indexOf('\\') >= 0) ) { resolve('') return diff --git a/packages/io/src/ioUtil.ts b/packages/io/src/ioUtil.ts index 1211a40511..5d45fd34a7 100644 --- a/packages/io/src/ioUtil.ts +++ b/packages/io/src/ioUtil.ts @@ -25,13 +25,13 @@ export function _isRooted(p: string): boolean { throw new Error('isRooted() parameter "p" cannot be empty') } - if (process.platform == 'win32') { + if (process.platform === 'win32') { return ( - p.slice(0, '\\'.length) == '\\' || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello + p.slice(0, '\\'.length) === '\\' || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello ) // e.g. C: or C:\hello } - return p.slice(0, '/'.length) == '/' + return p.slice(0, '/'.length) === '/' } /** @@ -48,13 +48,15 @@ export function _tryGetExecutablePath( // test file exists const stats: fs.Stats = fs.statSync(filePath) if (stats.isFile()) { - if (process.platform == 'win32') { + if (process.platform === 'win32') { // on Windows, test for valid extension const fileName = path.basename(filePath) const dotIndex = fileName.lastIndexOf('.') if (dotIndex >= 0) { const upperExt = fileName.substr(dotIndex).toUpperCase() - if (extensions.some(validExt => validExt.toUpperCase() == upperExt)) { + if ( + extensions.some(validExt => validExt.toUpperCase() === upperExt) + ) { return filePath } } @@ -65,7 +67,7 @@ export function _tryGetExecutablePath( } } } catch (err) { - if (err.code != 'ENOENT') { + if (err.code !== 'ENOENT') { console.log( `Unexpected error attempting to determine if executable file exists '${filePath}': ${err}` ) @@ -79,13 +81,13 @@ export function _tryGetExecutablePath( try { const stats: fs.Stats = fs.statSync(filePath) if (stats.isFile()) { - if (process.platform == 'win32') { + if (process.platform === 'win32') { // preserve the case of the actual file (since an extension was appended) try { const directory = path.dirname(filePath) const upperName = path.basename(filePath).toUpperCase() for (const actualName of fs.readdirSync(directory)) { - if (upperName == actualName.toUpperCase()) { + if (upperName === actualName.toUpperCase()) { filePath = path.join(directory, actualName) break } @@ -104,7 +106,7 @@ export function _tryGetExecutablePath( } } } catch (err) { - if (err.code != 'ENOENT') { + if (err.code !== 'ENOENT') { console.log( `Unexpected error attempting to determine if executable file exists '${filePath}': ${err}` ) @@ -117,7 +119,7 @@ export function _tryGetExecutablePath( function _normalizeSeparators(p: string): string { p = p || '' - if (process.platform == 'win32') { + if (process.platform === 'win32') { // convert slashes on Windows p = p.replace(/\//g, '\\') @@ -131,7 +133,7 @@ function _normalizeSeparators(p: string): string { } function _startsWith(str: string, start: string): boolean { - return str.slice(0, start.length) == start + return str.slice(0, start.length) === start } // on Mac/Linux, test the execute bit From 6b006bf76a4d4a65b192bd0837ef4e2e9050062b Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 16:18:37 -0400 Subject: [PATCH 06/27] Rename ioUtil to io-util --- packages/io/src/{ioUtil.ts => io-util.ts} | 0 packages/io/src/io.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/io/src/{ioUtil.ts => io-util.ts} (100%) diff --git a/packages/io/src/ioUtil.ts b/packages/io/src/io-util.ts similarity index 100% rename from packages/io/src/ioUtil.ts rename to packages/io/src/io-util.ts diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index 0e25f4dd81..ad97418369 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -1,7 +1,7 @@ import childProcess = require('child_process') import fs = require('fs') import path = require('path') -import util = require('./ioUtil') +import util = require('./io-util') /** * Interface for cp/mv options From 77b80f81bcff55a09b427646447d94a0aa5a78f7 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 16:21:00 -0400 Subject: [PATCH 07/27] Add no-import-requires --- .eslintrc.json | 3 ++- packages/io/__tests__/io.test.ts | 17 ++++++++--------- packages/io/src/io-util.ts | 4 ++-- packages/io/src/io.ts | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 241986aa87..d4f44dac2f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,7 +11,8 @@ "eslint-comments/no-use": "off", "import/no-namespace": "off", "@typescript-eslint/no-unused-vars": "error", - "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}] + "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], + "@typescript-eslint/no-require-imports": "error" }, "env": { "node": true, diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index dcfe3c5bb5..a669063dea 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -1,15 +1,14 @@ -import child = require('child_process') -import fs = require('fs') -import path = require('path') -import os = require('os') - -import io = require('../src/io') +import * as child from 'child_process' +import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' +import * as io from '../src/io' describe('cp', () => { it('copies file with no flags', async () => { - const root: string = path.join(getTestTemp(), 'cp_with_no_flags') - const sourceFile: string = path.join(root, 'cp_source') - const targetFile: string = path.join(root, 'cp_target') + const root = path.join(getTestTemp(), 'cp_with_no_flags') + const sourceFile = path.join(root, 'cp_source') + const targetFile = path.join(root, 'cp_target') await io.mkdirP(root) fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index 5d45fd34a7..fc7b691e4f 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -1,5 +1,5 @@ -import fs = require('fs') -import path = require('path') +import * as fs from 'fs' +import * as path from 'path' export function _removeDirectory(directoryPath: string) { if (fs.existsSync(directoryPath)) { diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index ad97418369..38cab8b04e 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -1,7 +1,7 @@ -import childProcess = require('child_process') -import fs = require('fs') -import path = require('path') -import util = require('./io-util') +import * as childProcess from 'child_process' +import * as fs from 'fs' +import * as path from 'path' +import * as util from './io-util' /** * Interface for cp/mv options From aaf90f9489fc4d8f6444b98c681dc85f23779dd7 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 17:10:22 -0400 Subject: [PATCH 08/27] Run auto-fix lint --- packages/io/__tests__/io.test.ts | 12 ++++++------ packages/io/src/io-util.ts | 6 +++--- packages/io/src/io.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index a669063dea..c81b81bbb4 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -389,7 +389,7 @@ describe('rmRF', () => { // remove the symlink file await io.rmRF(symlinkFile) - let errcode: string = '' + let errcode = '' try { fs.lstatSync(symlinkFile) } catch (err) { @@ -513,7 +513,7 @@ describe('rmRF', () => { // remove the real directory fs.unlinkSync(realFile) fs.rmdirSync(realDirectory) - let errcode: string = '' + let errcode = '' try { fs.statSync(symlinkDirectory) } catch (err) { @@ -670,7 +670,7 @@ describe('mkdirP', () => { it('fails if mkdirP with illegal chars', async () => { const testPath = path.join(getTestTemp(), 'mkdir\0') - let worked: boolean = false + let worked = false try { await io.mkdirP(testPath) worked = true @@ -682,7 +682,7 @@ describe('mkdirP', () => { }) it('fails if mkdirP with empty path', async () => { - let worked: boolean = false + let worked = false try { await io.mkdirP('') worked = true @@ -695,7 +695,7 @@ describe('mkdirP', () => { const testPath = path.join(getTestTemp(), 'mkdirP_conflicting_file_path') await io.mkdirP(getTestTemp()) fs.writeFileSync(testPath, '') - let worked: boolean = false + let worked = false try { await io.mkdirP(testPath) worked = true @@ -712,7 +712,7 @@ describe('mkdirP', () => { ) await io.mkdirP(getTestTemp()) fs.writeFileSync(path.dirname(testPath), '') - let worked: boolean = false + let worked = false try { await io.mkdirP(testPath) worked = true diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index fc7b691e4f..cf3e4bd124 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -27,11 +27,11 @@ export function _isRooted(p: string): boolean { if (process.platform === 'win32') { return ( - p.slice(0, '\\'.length) === '\\' || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello + p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello ) // e.g. C: or C:\hello } - return p.slice(0, '/'.length) === '/' + return p.startsWith('/') } /** @@ -133,7 +133,7 @@ function _normalizeSeparators(p: string): string { } function _startsWith(str: string, start: string): boolean { - return str.slice(0, start.length) === start + return str.startsWith(start) } // on Mac/Linux, test the execute bit diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index 38cab8b04e..559e13da12 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -325,7 +325,7 @@ export function which(tool: string, check?: boolean): Promise { // if any path separators, return empty if ( - tool.indexOf('/') >= 0 || + tool.includes('/') || (process.platform === 'win32' && tool.indexOf('\\') >= 0) ) { resolve('') From 0d2ce4f23416bc56189e32e2a2b473b8e41fbb5a Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 18:02:21 -0400 Subject: [PATCH 09/27] Remove lint errors --- .eslintrc.json | 2 - packages/io/__tests__/io.test.ts | 518 ++++++++++++++++--------------- packages/io/src/io-util.ts | 17 +- packages/io/src/io.ts | 48 +-- 4 files changed, 307 insertions(+), 278 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index a427f9e5d6..02c79de1a0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,8 +26,6 @@ "@typescript-eslint/no-array-constructor": "error", "@typescript-eslint/no-empty-interface": "error", "@typescript-eslint/no-explicit-any": "error", - "no-extra-parens": "off", - "@typescript-eslint/no-extra-parens": "error", "@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-for-in-array": "error", "@typescript-eslint/no-inferrable-types": "error", diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index c81b81bbb4..ddaedded65 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -1,5 +1,5 @@ import * as child from 'child_process' -import * as fs from 'fs' +import {promises as fs} from 'fs' import * as os from 'os' import * as path from 'path' import * as io from '../src/io' @@ -10,11 +10,11 @@ describe('cp', () => { const sourceFile = path.join(root, 'cp_source') const targetFile = path.join(root, 'cp_target') await io.mkdirP(root) - fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'}) await io.cp(sourceFile, targetFile) - expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe( 'test file content' ) }) @@ -24,11 +24,11 @@ describe('cp', () => { const sourceFile: string = path.join(root, 'cp_source') const targetFile: string = path.join(root, 'cp_target') await io.mkdirP(root) - fs.writeFileSync(sourceFile, 'test file content') + await fs.writeFile(sourceFile, 'test file content') await io.cp(sourceFile, targetFile, {recursive: false, force: true}) - expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe( 'test file content' ) }) @@ -38,8 +38,8 @@ describe('cp', () => { const sourceFile: string = path.join(root, 'cp_source') const targetFile: string = path.join(root, 'cp_target') await io.mkdirP(root) - fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) - fs.writeFileSync(targetFile, 'correct content', {encoding: 'utf8'}) + await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'}) + await fs.writeFile(targetFile, 'correct content', {encoding: 'utf8'}) let failed = false try { await io.cp(sourceFile, targetFile, {recursive: false, force: false}) @@ -48,7 +48,7 @@ describe('cp', () => { } expect(failed).toBe(true) - expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe( 'correct content' ) }) @@ -65,11 +65,11 @@ describe('cp', () => { 'cp_source_file' ) await io.mkdirP(sourceFolder) - fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'}) await io.mkdirP(targetFolder) await io.cp(sourceFolder, targetFolder, {recursive: true}) - expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe( 'test file content' ) }) @@ -82,10 +82,10 @@ describe('cp', () => { const targetFolder: string = path.join(root, 'cp_target') const targetFile: string = path.join(targetFolder, 'cp_source_file') await io.mkdirP(sourceFolder) - fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'}) await io.cp(sourceFolder, targetFolder, {recursive: true}) - expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe( 'test file content' ) }) @@ -102,7 +102,7 @@ describe('cp', () => { 'cp_source_file' ) await io.mkdirP(sourceFolder) - fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'}) let thrown = false try { @@ -111,24 +111,24 @@ describe('cp', () => { thrown = true } expect(thrown).toBe(true) - expect(fs.existsSync(targetFile)).toBe(false) + await assertNotExists(targetFile) }) }) describe('mv', () => { it('moves file with no flags', async () => { - const root: string = path.join(getTestTemp(), ' mv_with_no_flags') - const sourceFile: string = path.join(root, ' mv_source') - const targetFile: string = path.join(root, ' mv_target') + const root = path.join(getTestTemp(), ' mv_with_no_flags') + const sourceFile = path.join(root, ' mv_source') + const targetFile = path.join(root, ' mv_target') await io.mkdirP(root) - fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'}) await io.mv(sourceFile, targetFile) - expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe( 'test file content' ) - expect(fs.existsSync(sourceFile)).toBe(false) + await assertNotExists(sourceFile) }) it('moves file using -f', async () => { @@ -136,14 +136,15 @@ describe('mv', () => { const sourceFile: string = path.join(root, ' mv_source') const targetFile: string = path.join(root, ' mv_target') await io.mkdirP(root) - fs.writeFileSync(sourceFile, 'test file content') + await fs.writeFile(sourceFile, 'test file content') await io.mv(sourceFile, targetFile) - expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe( 'test file content' ) - expect(fs.existsSync(sourceFile)).toBe(false) + + await assertNotExists(sourceFile) }) it('try moving to existing file with -n', async () => { @@ -151,8 +152,8 @@ describe('mv', () => { const sourceFile: string = path.join(root, ' mv_source') const targetFile: string = path.join(root, ' mv_target') await io.mkdirP(root) - fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) - fs.writeFileSync(targetFile, 'correct content', {encoding: 'utf8'}) + await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'}) + await fs.writeFile(targetFile, 'correct content', {encoding: 'utf8'}) let failed = false try { await io.mv(sourceFile, targetFile, {force: false}) @@ -161,10 +162,10 @@ describe('mv', () => { } expect(failed).toBe(true) - expect(fs.readFileSync(sourceFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(sourceFile, {encoding: 'utf8'})).toBe( 'test file content' ) - expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe( 'correct content' ) }) @@ -181,14 +182,14 @@ describe('mv', () => { ' mv_source_file' ) await io.mkdirP(sourceFolder) - fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'}) await io.mkdirP(targetFolder) await io.mv(sourceFolder, targetFolder, {recursive: true}) - expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe( 'test file content' ) - expect(fs.existsSync(sourceFile)).toBe(false) + await assertNotExists(sourceFile) }) it('moves directory into non-existing destination with -r', async () => { @@ -202,13 +203,13 @@ describe('mv', () => { const targetFolder: string = path.join(root, ' mv_target') const targetFile: string = path.join(targetFolder, ' mv_source_file') await io.mkdirP(sourceFolder) - fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'}) await io.mv(sourceFolder, targetFolder, {recursive: true}) - expect(fs.readFileSync(targetFile, {encoding: 'utf8'})).toBe( + expect(await fs.readFile(targetFile, {encoding: 'utf8'})).toBe( 'test file content' ) - expect(fs.existsSync(sourceFile)).toBe(false) + await assertNotExists(sourceFile) }) it('tries to move directory without -r', async () => { @@ -223,7 +224,7 @@ describe('mv', () => { 'mv_source_file' ) await io.mkdirP(sourceFolder) - fs.writeFileSync(sourceFile, 'test file content', {encoding: 'utf8'}) + await fs.writeFile(sourceFile, 'test file content', {encoding: 'utf8'}) let thrown = false try { @@ -233,8 +234,8 @@ describe('mv', () => { } expect(thrown).toBe(true) - expect(fs.existsSync(sourceFile)).toBe(true) - expect(fs.existsSync(targetFile)).toBe(false) + await assertExists(sourceFile) + await assertNotExists(targetFile) }) }) @@ -243,10 +244,10 @@ describe('rmRF', () => { const testPath = path.join(getTestTemp(), 'testFolder') await io.mkdirP(testPath) - expect(fs.existsSync(testPath)).toBe(true) + await assertExists(testPath) await io.rmRF(testPath) - expect(fs.existsSync(testPath)).toBe(false) + await assertNotExists(testPath) }) it('removes recursive folders with rmRF', async () => { @@ -254,75 +255,78 @@ describe('rmRF', () => { const testPath2 = path.join(testPath, 'testDir2') await io.mkdirP(testPath2) - expect(fs.existsSync(testPath)).toBe(true) - expect(fs.existsSync(testPath2)).toBe(true) + await assertExists(testPath) + await assertExists(testPath2) await io.rmRF(testPath) - expect(fs.existsSync(testPath)).toBe(false) - expect(fs.existsSync(testPath2)).toBe(false) + await assertNotExists(testPath) + await assertNotExists(testPath2) }) it('removes folder with locked file with rmRF', async () => { const testPath = path.join(getTestTemp(), 'testFolder') await io.mkdirP(testPath) - expect(fs.existsSync(testPath)).toBe(true) + await assertExists(testPath) // can't remove folder with locked file on windows const filePath = path.join(testPath, 'file.txt') - fs.appendFileSync(filePath, 'some data') - expect(fs.existsSync(filePath)).toBe(true) + await fs.appendFile(filePath, 'some data') + await assertExists(filePath) - const fd = fs.openSync(filePath, 'r') + const fd = await fs.open(filePath, 'r') + + let worked: boolean - let worked = false try { await io.rmRF(testPath) worked = true - } catch (err) {} + } catch (err) { + worked = false + } if (os.platform() === 'win32') { expect(worked).toBe(false) - expect(fs.existsSync(testPath)).toBe(true) + await assertExists(testPath) } else { expect(worked).toBe(true) - expect(fs.existsSync(testPath)).toBe(false) + await assertNotExists(testPath) } - fs.closeSync(fd) + await fd.close() await io.rmRF(testPath) - expect(fs.existsSync(testPath)).toBe(false) + await assertNotExists(testPath) }) it('removes folder that doesnt exist with rmRF', async () => { const testPath = path.join(getTestTemp(), 'testFolder') - expect(fs.existsSync(testPath)).toBe(false) + await assertNotExists(testPath) await io.rmRF(testPath) - expect(fs.existsSync(testPath)).toBe(false) + await assertNotExists(testPath) }) it('removes file with rmRF', async () => { const file: string = path.join(getTestTemp(), 'rmRF_file') - fs.writeFileSync(file, 'test file content') - expect(fs.existsSync(file)).toBe(true) + await fs.writeFile(file, 'test file content') + await assertExists(file) await io.rmRF(file) - expect(fs.existsSync(file)).toBe(false) + await assertNotExists(file) }) it('removes hidden folder with rmRF', async () => { const directory: string = path.join(getTestTemp(), '.rmRF_directory') await createHiddenDirectory(directory) - expect(fs.existsSync(directory)).toBe(true) + await assertExists(directory) await io.rmRF(directory) - expect(fs.existsSync(directory)).toBe(false) + await assertNotExists(directory) }) it('removes hidden file with rmRF', async () => { const file: string = path.join(getTestTemp(), '.rmRF_file') - fs.writeFileSync(file, 'test file content') - expect(fs.existsSync(file)).toBe(true) + await fs.writeFile(file, 'test file content') + await assertExists(file) await io.rmRF(file) - expect(fs.existsSync(file)).toBe(false) + await assertNotExists(file) }) it('removes symlink folder with rmRF', async () => { @@ -335,14 +339,14 @@ describe('rmRF', () => { const realFile: string = path.join(root, 'real_directory', 'real_file') const symlinkDirectory: string = path.join(root, 'symlink_directory') await io.mkdirP(realDirectory) - fs.writeFileSync(realFile, 'test file content') - createSymlinkDir(realDirectory, symlinkDirectory) - expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true) + await fs.writeFile(realFile, 'test file content') + await createSymlinkDir(realDirectory, symlinkDirectory) + await assertExists(path.join(symlinkDirectory, 'real_file')) await io.rmRF(symlinkDirectory) - expect(fs.existsSync(realDirectory)).toBe(true) - expect(fs.existsSync(realFile)).toBe(true) - expect(fs.existsSync(symlinkDirectory)).toBe(false) + await assertExists(realDirectory) + await assertExists(realFile) + await assertNotExists(symlinkDirectory) }) // creating a symlink to a file on Windows requires elevated @@ -355,15 +359,15 @@ describe('rmRF', () => { const realFile: string = path.join(root, 'real_file') const symlinkFile: string = path.join(root, 'symlink_file') await io.mkdirP(root) - fs.writeFileSync(realFile, 'test file content') - fs.symlinkSync(realFile, symlinkFile) - expect(fs.readFileSync(symlinkFile, {encoding: 'utf8'})).toBe( + await fs.writeFile(realFile, 'test file content') + await fs.symlink(realFile, symlinkFile) + expect(await fs.readFile(symlinkFile, {encoding: 'utf8'})).toBe( 'test file content' ) await io.rmRF(symlinkFile) - expect(fs.existsSync(realFile)).toBe(true) - expect(fs.existsSync(symlinkFile)).toBe(false) + await assertExists(realFile) + await assertNotExists(symlinkFile) }) it('removes symlink file with missing source using rmRF', async () => { @@ -377,21 +381,21 @@ describe('rmRF', () => { const realFile: string = path.join(root, 'real_file') const symlinkFile: string = path.join(root, 'symlink_file') await io.mkdirP(root) - fs.writeFileSync(realFile, 'test file content') - fs.symlinkSync(realFile, symlinkFile) - expect(fs.readFileSync(symlinkFile, {encoding: 'utf8'})).toBe( + await fs.writeFile(realFile, 'test file content') + await fs.symlink(realFile, symlinkFile) + expect(await fs.readFile(symlinkFile, {encoding: 'utf8'})).toBe( 'test file content' ) // remove the real file - fs.unlinkSync(realFile) - expect(fs.lstatSync(symlinkFile).isSymbolicLink()).toBe(true) + await fs.unlink(realFile) + expect((await fs.lstat(symlinkFile)).isSymbolicLink()).toBe(true) // remove the symlink file await io.rmRF(symlinkFile) let errcode = '' try { - fs.lstatSync(symlinkFile) + await fs.lstat(symlinkFile) } catch (err) { errcode = err.code } @@ -412,17 +416,17 @@ describe('rmRF', () => { const symlinkFile: string = path.join(root, 'symlink_file') const symlinkLevel2File: string = path.join(root, 'symlink_level_2_file') await io.mkdirP(root) - fs.writeFileSync(realFile, 'test file content') - fs.symlinkSync(realFile, symlinkFile) - fs.symlinkSync(symlinkFile, symlinkLevel2File) - expect(fs.readFileSync(symlinkLevel2File, {encoding: 'utf8'})).toBe( + await fs.writeFile(realFile, 'test file content') + await fs.symlink(realFile, symlinkFile) + await fs.symlink(symlinkFile, symlinkLevel2File) + expect(await fs.readFile(symlinkLevel2File, {encoding: 'utf8'})).toBe( 'test file content' ) await io.rmRF(symlinkLevel2File) - expect(fs.existsSync(realFile)).toBe(true) - expect(fs.existsSync(symlinkFile)).toBe(true) - expect(fs.existsSync(symlinkLevel2File)).toBe(false) + await assertExists(realFile) + await assertExists(symlinkFile) + await assertNotExists(symlinkLevel2File) }) it('removes nested symlink file with rmRF', async () => { @@ -441,18 +445,18 @@ describe('rmRF', () => { 'symlink_file' ) await io.mkdirP(realDirectory) - fs.writeFileSync(realFile, 'test file content') + await fs.writeFile(realFile, 'test file content') await io.mkdirP(outerDirectory) - fs.symlinkSync(realFile, symlinkFile) - expect(fs.readFileSync(symlinkFile, {encoding: 'utf8'})).toBe( + await fs.symlink(realFile, symlinkFile) + expect(await fs.readFile(symlinkFile, {encoding: 'utf8'})).toBe( 'test file content' ) await io.rmRF(outerDirectory) - expect(fs.existsSync(realDirectory)).toBe(true) - expect(fs.existsSync(realFile)).toBe(true) - expect(fs.existsSync(symlinkFile)).toBe(false) - expect(fs.existsSync(outerDirectory)).toBe(false) + await assertExists(realDirectory) + await assertExists(realFile) + await assertNotExists(symlinkFile) + await assertNotExists(outerDirectory) }) it('removes deeply nested symlink file with rmRF', async () => { @@ -481,18 +485,18 @@ describe('rmRF', () => { 'symlink_file' ) await io.mkdirP(realDirectory) - fs.writeFileSync(realFile, 'test file content') + await fs.writeFile(realFile, 'test file content') await io.mkdirP(nestedDirectory) - fs.symlinkSync(realFile, symlinkFile) - expect(fs.readFileSync(symlinkFile, {encoding: 'utf8'})).toBe( + await fs.symlink(realFile, symlinkFile) + expect(await fs.readFile(symlinkFile, {encoding: 'utf8'})).toBe( 'test file content' ) await io.rmRF(outerDirectory) - expect(fs.existsSync(realDirectory)).toBe(true) - expect(fs.existsSync(realFile)).toBe(true) - expect(fs.existsSync(symlinkFile)).toBe(false) - expect(fs.existsSync(outerDirectory)).toBe(false) + await assertExists(realDirectory) + await assertExists(realFile) + await assertNotExists(symlinkFile) + await assertNotExists(outerDirectory) }) } @@ -506,16 +510,16 @@ describe('rmRF', () => { const realFile: string = path.join(root, 'real_directory', 'real_file') const symlinkDirectory: string = path.join(root, 'symlink_directory') await io.mkdirP(realDirectory) - fs.writeFileSync(realFile, 'test file content') - createSymlinkDir(realDirectory, symlinkDirectory) - expect(fs.existsSync(symlinkDirectory)).toBe(true) + await fs.writeFile(realFile, 'test file content') + await createSymlinkDir(realDirectory, symlinkDirectory) + await assertExists(symlinkDirectory) // remove the real directory - fs.unlinkSync(realFile) - fs.rmdirSync(realDirectory) + await fs.unlink(realFile) + await fs.rmdir(realDirectory) let errcode = '' try { - fs.statSync(symlinkDirectory) + await fs.stat(symlinkDirectory) } catch (err) { errcode = err.code } @@ -523,13 +527,13 @@ describe('rmRF', () => { expect(errcode).toBe('ENOENT') // lstat shouldn't throw - fs.lstatSync(symlinkDirectory) + await fs.lstat(symlinkDirectory) // remove the symlink directory await io.rmRF(symlinkDirectory) errcode = '' try { - fs.lstatSync(symlinkDirectory) + await fs.lstat(symlinkDirectory) } catch (err) { errcode = err.code } @@ -555,25 +559,25 @@ describe('rmRF', () => { 'symlink_level_2_directory' ) await io.mkdirP(realDirectory) - fs.writeFileSync(realFile, 'test file content') - createSymlinkDir(realDirectory, symlinkDirectory) - createSymlinkDir(symlinkDirectory, symlinkLevel2Directory) + await fs.writeFile(realFile, 'test file content') + await createSymlinkDir(realDirectory, symlinkDirectory) + await createSymlinkDir(symlinkDirectory, symlinkLevel2Directory) expect( - fs.readFileSync(path.join(symlinkDirectory, 'real_file'), { + await fs.readFile(path.join(symlinkDirectory, 'real_file'), { encoding: 'utf8' }) ).toBe('test file content') if (os.platform() === 'win32') { - expect(fs.readlinkSync(symlinkLevel2Directory)).toBe( + expect(await fs.readlink(symlinkLevel2Directory)).toBe( `${symlinkDirectory}\\` ) } else { - expect(fs.readlinkSync(symlinkLevel2Directory)).toBe(symlinkDirectory) + expect(await fs.readlink(symlinkLevel2Directory)).toBe(symlinkDirectory) } await io.rmRF(symlinkLevel2Directory) - expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true) - expect(fs.existsSync(symlinkLevel2Directory)).toBe(false) + await assertExists(path.join(symlinkDirectory, 'real_file')) + await assertNotExists(symlinkLevel2Directory) }) it('removes nested symlink folder with rmRF', async () => { @@ -592,16 +596,16 @@ describe('rmRF', () => { 'symlink_directory' ) await io.mkdirP(realDirectory) - fs.writeFileSync(realFile, 'test file content') + await fs.writeFile(realFile, 'test file content') await io.mkdirP(outerDirectory) - createSymlinkDir(realDirectory, symlinkDirectory) - expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true) + await createSymlinkDir(realDirectory, symlinkDirectory) + await assertExists(path.join(symlinkDirectory, 'real_file')) await io.rmRF(outerDirectory) - expect(fs.existsSync(realDirectory)).toBe(true) - expect(fs.existsSync(realFile)).toBe(true) - expect(fs.existsSync(symlinkDirectory)).toBe(false) - expect(fs.existsSync(outerDirectory)).toBe(false) + await assertExists(realDirectory) + await assertExists(realFile) + await assertNotExists(symlinkDirectory) + await assertNotExists(outerDirectory) }) it('removes deeply nested symlink folder with rmRF', async () => { @@ -627,25 +631,25 @@ describe('rmRF', () => { 'symlink_directory' ) await io.mkdirP(realDirectory) - fs.writeFileSync(realFile, 'test file content') + await fs.writeFile(realFile, 'test file content') await io.mkdirP(nestedDirectory) - createSymlinkDir(realDirectory, symlinkDirectory) - expect(fs.existsSync(path.join(symlinkDirectory, 'real_file'))).toBe(true) + await createSymlinkDir(realDirectory, symlinkDirectory) + await assertExists(path.join(symlinkDirectory, 'real_file')) await io.rmRF(outerDirectory) - expect(fs.existsSync(realDirectory)).toBe(true) - expect(fs.existsSync(realFile)).toBe(true) - expect(fs.existsSync(symlinkDirectory)).toBe(false) - expect(fs.existsSync(outerDirectory)).toBe(false) + await assertExists(realDirectory) + await assertExists(realFile) + await assertNotExists(symlinkDirectory) + await assertNotExists(outerDirectory) }) it('removes hidden file with rmRF', async () => { const file: string = path.join(getTestTemp(), '.rmRF_file') await io.mkdirP(path.dirname(file)) await createHiddenFile(file, 'test file content') - expect(fs.existsSync(file)).toBe(true) + await assertExists(file) await io.rmRF(file) - expect(fs.existsSync(file)).toBe(false) + await assertNotExists(file) }) }) @@ -658,14 +662,14 @@ describe('mkdirP', () => { const testPath = path.join(getTestTemp(), 'mkdirTest') await io.mkdirP(testPath) - expect(fs.existsSync(testPath)).toBe(true) + await assertExists(testPath) }) it('creates nested folders with mkdirP', async () => { const testPath = path.join(getTestTemp(), 'mkdir1', 'mkdir2') await io.mkdirP(testPath) - expect(fs.existsSync(testPath)).toBe(true) + await assertExists(testPath) }) it('fails if mkdirP with illegal chars', async () => { @@ -675,18 +679,23 @@ describe('mkdirP', () => { await io.mkdirP(testPath) worked = true } catch (err) { - expect(fs.existsSync(testPath)).toBe(false) + await expect(fs.stat(testPath)).rejects.toHaveProperty( + 'code', + 'ERR_INVALID_ARG_VALUE' + ) } expect(worked).toBe(false) }) it('fails if mkdirP with empty path', async () => { - let worked = false + let worked: boolean try { await io.mkdirP('') worked = true - } catch (err) {} + } catch (err) { + worked = false + } expect(worked).toBe(false) }) @@ -694,12 +703,14 @@ describe('mkdirP', () => { it('fails if mkdirP with conflicting file path', async () => { const testPath = path.join(getTestTemp(), 'mkdirP_conflicting_file_path') await io.mkdirP(getTestTemp()) - fs.writeFileSync(testPath, '') - let worked = false + await fs.writeFile(testPath, '') + let worked: boolean try { await io.mkdirP(testPath) worked = true - } catch (err) {} + } catch (err) { + worked = false + } expect(worked).toBe(false) }) @@ -711,12 +722,14 @@ describe('mkdirP', () => { 'dir' ) await io.mkdirP(getTestTemp()) - fs.writeFileSync(path.dirname(testPath), '') - let worked = false + await fs.writeFile(path.dirname(testPath), '') + let worked: boolean try { await io.mkdirP(testPath) worked = true - } catch (err) {} + } catch (err) { + worked = false + } expect(worked).toBe(false) }) @@ -724,11 +737,11 @@ describe('mkdirP', () => { it('no-ops if mkdirP directory exists', async () => { const testPath = path.join(getTestTemp(), 'mkdirP_dir_exists') await io.mkdirP(testPath) - expect(fs.existsSync(testPath)).toBe(true) + await assertExists(testPath) // Calling again shouldn't throw await io.mkdirP(testPath) - expect(fs.existsSync(testPath)).toBe(true) + await assertExists(testPath) }) it('no-ops if mkdirP with symlink directory', async () => { @@ -741,18 +754,18 @@ describe('mkdirP', () => { const realFilePath = path.join(realDirPath, 'file.txt') const symlinkDirPath = path.join(rootPath, 'symlink_dir') await io.mkdirP(getTestTemp()) - fs.mkdirSync(rootPath) - fs.mkdirSync(realDirPath) - fs.writeFileSync(realFilePath, 'test real_dir/file.txt contet') - createSymlinkDir(realDirPath, symlinkDirPath) + await fs.mkdir(rootPath) + await fs.mkdir(realDirPath) + await fs.writeFile(realFilePath, 'test real_dir/file.txt contet') + await createSymlinkDir(realDirPath, symlinkDirPath) await io.mkdirP(symlinkDirPath) // the file in the real directory should still be accessible via the symlink - expect(fs.lstatSync(symlinkDirPath).isSymbolicLink()).toBe(true) - expect(fs.statSync(path.join(symlinkDirPath, 'file.txt')).isFile()).toBe( - true - ) + expect((await fs.lstat(symlinkDirPath)).isSymbolicLink()).toBe(true) + expect( + (await fs.stat(path.join(symlinkDirPath, 'file.txt'))).isFile() + ).toBe(true) }) it('no-ops if mkdirP with parent symlink directory', async () => { @@ -765,18 +778,18 @@ describe('mkdirP', () => { const realFilePath = path.join(realDirPath, 'file.txt') const symlinkDirPath = path.join(rootPath, 'symlink_dir') await io.mkdirP(getTestTemp()) - fs.mkdirSync(rootPath) - fs.mkdirSync(realDirPath) - fs.writeFileSync(realFilePath, 'test real_dir/file.txt contet') - createSymlinkDir(realDirPath, symlinkDirPath) + await fs.mkdir(rootPath) + await fs.mkdir(realDirPath) + await fs.writeFile(realFilePath, 'test real_dir/file.txt contet') + await createSymlinkDir(realDirPath, symlinkDirPath) const subDirPath = path.join(symlinkDirPath, 'sub_dir') await io.mkdirP(subDirPath) // the subdirectory should be accessible via the real directory - expect(fs.lstatSync(path.join(realDirPath, 'sub_dir')).isDirectory()).toBe( - true - ) + expect( + (await fs.lstat(path.join(realDirPath, 'sub_dir'))).isDirectory() + ).toBe(true) }) it('breaks if mkdirP loop out of control', async () => { @@ -818,7 +831,7 @@ describe('which', () => { } const filePath = path.join(testPath, fileName) - fs.writeFileSync(filePath, '') + await fs.writeFile(filePath, '') if (process.platform !== 'win32') { chmod(filePath, '+x') } @@ -826,7 +839,7 @@ describe('which', () => { const originalPath = process.env['PATH'] try { // update the PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + process.env['PATH'] = `${process.env['PATH']}${path.delimiter}${testPath}` // exact file name expect(await io.which(fileName)).toBe(filePath) @@ -865,7 +878,7 @@ describe('which', () => { ) } else { // case sensitive on Linux - expect((await io.which(fileName.toUpperCase())) || '').toBe('') + expect(await io.which(fileName.toUpperCase())).toBe('') } } finally { process.env['PATH'] = originalPath @@ -875,10 +888,9 @@ describe('which', () => { it('which() not found', async () => { expect(await io.which('which-test-no-such-file')).toBe('') expect(await io.which('which-test-no-such-file', false)).toBe('') - try { - await io.which('which-test-no-such-file', true) - throw new Error('Should have thrown') - } catch (err) {} + await expect( + io.which('which-test-no-such-file', true) + ).rejects.toBeDefined() }) it('which() searches path in order', async () => { @@ -893,7 +905,7 @@ describe('which', () => { } const filePath = path.join(testPath, fileName) - fs.writeFileSync(filePath, '') + await fs.writeFile(filePath, '') if (process.platform !== 'win32') { chmod(filePath, '+x') } @@ -905,7 +917,7 @@ describe('which', () => { expect(!!(originalWhich || '')).toBe(true) // modify PATH - process.env['PATH'] = testPath + path.delimiter + process.env['PATH'] + process.env['PATH'] = [testPath, process.env.PATH].join(path.delimiter) // override chcp.com/bash should be found expect(await io.which(fileName)).toBe(filePath) @@ -926,7 +938,7 @@ describe('which', () => { } const filePath = path.join(testPath, fileName) - fs.writeFileSync(filePath, '') + await fs.writeFile(filePath, '') if (process.platform !== 'win32') { chmod(filePath, '-x') } @@ -934,10 +946,10 @@ describe('which', () => { const originalPath = process.env['PATH'] try { // modify PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + process.env['PATH'] = [process.env['PATH'], testPath].join(path.delimiter) // should not be found - expect((await io.which(fileName)) || '').toBe('') + expect(await io.which(fileName)).toBe('') } finally { process.env['PATH'] = originalPath } @@ -966,10 +978,10 @@ describe('which', () => { const originalPath = process.env['PATH'] try { // modify PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + process.env['PATH'] = [process.env['PATH'], testPath].join(path.delimiter) // should not be found - expect((await io.which(path.basename(dirPath))) || '').toBe('') + expect(await io.which(path.basename(dirPath))).toBe('') } finally { process.env['PATH'] = originalPath } @@ -984,7 +996,7 @@ describe('which', () => { filePath += '.exe' } - fs.writeFileSync(filePath, '') + await fs.writeFile(filePath, '') if (process.platform !== 'win32') { chmod(filePath, '+x') } @@ -1009,14 +1021,14 @@ describe('which', () => { filePath += '.abc' // not a valid PATHEXT } - fs.writeFileSync(filePath, '') + await fs.writeFile(filePath, '') if (process.platform !== 'win32') { chmod(filePath, '-x') } // should not be found - expect((await io.which(filePath)) || '').toBe('') - expect((await io.which(filePath, false)) || '').toBe('') + expect(await io.which(filePath)).toBe('') + expect(await io.which(filePath, false)).toBe('') let failed = false try { await io.which(filePath, true) @@ -1044,8 +1056,8 @@ describe('which', () => { } // should not be found - expect((await io.which(dirPath)) || '').toBe('') - expect((await io.which(dirPath, false)) || '').toBe('') + expect(await io.which(dirPath)).toBe('') + expect(await io.which(dirPath, false)).toBe('') let failed = false try { await io.which(dirPath, true) @@ -1062,8 +1074,8 @@ describe('which', () => { filePath += '.exe' } - expect((await io.which(filePath)) || '').toBe('') - expect((await io.which(filePath, false)) || '').toBe('') + expect(await io.which(filePath)).toBe('') + expect(await io.which(filePath, false)).toBe('') let failed = false try { await io.which(filePath, true) @@ -1085,7 +1097,7 @@ describe('which', () => { } const filePath = path.join(testPath, fileName) - fs.writeFileSync(filePath, '') + await fs.writeFile(filePath, '') if (process.platform !== 'win32') { chmod(filePath, '+x') } @@ -1093,14 +1105,14 @@ describe('which', () => { const originalPath = process.env['PATH'] try { // modify PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + process.env['PATH'] = [process.env['PATH'], testPath].join(path.delimiter) // which "dir/file", should not be found - expect((await io.which(`${testDirName}/${fileName}`)) || '').toBe('') + expect(await io.which(`${testDirName}/${fileName}`)).toBe('') // on Windows, also try "dir\file" if (process.platform === 'win32') { - expect((await io.which(`${testDirName}\\${fileName}`)) || '').toBe('') + expect(await io.which(`${testDirName}\\${fileName}`)).toBe('') } } finally { process.env['PATH'] = originalPath @@ -1133,13 +1145,15 @@ describe('which', () => { ) } for (const fileName of Object.keys(files)) { - fs.writeFileSync(files[fileName], '') + await fs.writeFile(files[fileName], '') } const originalPath = process.env['PATH'] try { // modify PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + process.env['PATH'] = `${process.env['PATH']}${ + path.delimiter + }${testPath}` // find each file for (const fileName of Object.keys(files)) { @@ -1180,7 +1194,7 @@ describe('which', () => { 'which-test-file-5.txt.com' ) for (const fileName of Object.keys(files)) { - fs.writeFileSync(files[fileName], '') + await fs.writeFile(files[fileName], '') } // find each file @@ -1206,11 +1220,13 @@ describe('which', () => { const fileName = 'which-test-file.bat' const expectedFilePath = path.join(testPath, fileName) const notExpectedFilePath = path.join(testPath, `${fileName}.exe`) - fs.writeFileSync(expectedFilePath, '') - fs.writeFileSync(notExpectedFilePath, '') + await fs.writeFile(expectedFilePath, '') + await fs.writeFile(notExpectedFilePath, '') const originalPath = process.env['PATH'] try { - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + process.env['PATH'] = `${process.env['PATH']}${ + path.delimiter + }${testPath}` expect(await io.which(fileName)).toBe(expectedFilePath) } finally { process.env['PATH'] = originalPath @@ -1234,8 +1250,8 @@ describe('which', () => { const fileName = 'which-test-file.bat' const expectedFilePath = path.join(testPath, fileName) const notExpectedFilePath = path.join(testPath, `${fileName}.exe`) - fs.writeFileSync(expectedFilePath, '') - fs.writeFileSync(notExpectedFilePath, '') + await fs.writeFile(expectedFilePath, '') + await fs.writeFile(notExpectedFilePath, '') expect(await io.which(path.join(testPath, fileName))).toBe( expectedFilePath ) @@ -1249,19 +1265,19 @@ describe('which', () => { const fileNameWithoutExtension = 'which-test-file' const comTestPath = path.join(testPath, 'com-test') await io.mkdirP(comTestPath) - fs.writeFileSync( + await fs.writeFile( path.join(comTestPath, `${fileNameWithoutExtension}.com`), '' ) - fs.writeFileSync( + await fs.writeFile( path.join(comTestPath, `${fileNameWithoutExtension}.exe`), '' ) - fs.writeFileSync( + await fs.writeFile( path.join(comTestPath, `${fileNameWithoutExtension}.bat`), '' ) - fs.writeFileSync( + await fs.writeFile( path.join(comTestPath, `${fileNameWithoutExtension}.cmd`), '' ) @@ -1270,15 +1286,15 @@ describe('which', () => { // PATHEXT=.COM;.EXE;.BAT;.CMD... const exeTestPath = path.join(testPath, 'exe-test') await io.mkdirP(exeTestPath) - fs.writeFileSync( + await fs.writeFile( path.join(exeTestPath, `${fileNameWithoutExtension}.exe`), '' ) - fs.writeFileSync( + await fs.writeFile( path.join(exeTestPath, `${fileNameWithoutExtension}.bat`), '' ) - fs.writeFileSync( + await fs.writeFile( path.join(exeTestPath, `${fileNameWithoutExtension}.cmd`), '' ) @@ -1287,11 +1303,11 @@ describe('which', () => { // PATHEXT=.COM;.EXE;.BAT;.CMD... const batTestPath = path.join(testPath, 'bat-test') await io.mkdirP(batTestPath) - fs.writeFileSync( + await fs.writeFile( path.join(batTestPath, `${fileNameWithoutExtension}.bat`), '' ) - fs.writeFileSync( + await fs.writeFile( path.join(batTestPath, `${fileNameWithoutExtension}.cmd`), '' ) @@ -1299,34 +1315,34 @@ describe('which', () => { // create a directory for testing .CMD const cmdTestPath = path.join(testPath, 'cmd-test') await io.mkdirP(cmdTestPath) - const cmdTest_cmdFilePath = path.join( + const cmdFilePath = path.join( cmdTestPath, `${fileNameWithoutExtension}.cmd` ) - fs.writeFileSync(cmdTest_cmdFilePath, '') + await fs.writeFile(cmdFilePath, '') const originalPath = process.env['PATH'] try { // test .COM - process.env['PATH'] = comTestPath + path.delimiter + originalPath + process.env['PATH'] = `${comTestPath}${path.delimiter}${originalPath}` expect(await io.which(fileNameWithoutExtension)).toBe( path.join(comTestPath, `${fileNameWithoutExtension}.com`) ) // test .EXE - process.env['PATH'] = exeTestPath + path.delimiter + originalPath + process.env['PATH'] = `${exeTestPath}${path.delimiter}${originalPath}` expect(await io.which(fileNameWithoutExtension)).toBe( path.join(exeTestPath, `${fileNameWithoutExtension}.exe`) ) // test .BAT - process.env['PATH'] = batTestPath + path.delimiter + originalPath + process.env['PATH'] = `${batTestPath}${path.delimiter}${originalPath}` expect(await io.which(fileNameWithoutExtension)).toBe( path.join(batTestPath, `${fileNameWithoutExtension}.bat`) ) // test .CMD - process.env['PATH'] = cmdTestPath + path.delimiter + originalPath + process.env['PATH'] = `${cmdTestPath}${path.delimiter}${originalPath}` expect(await io.which(fileNameWithoutExtension)).toBe( path.join(cmdTestPath, `${fileNameWithoutExtension}.cmd`) ) @@ -1337,7 +1353,9 @@ describe('which', () => { } }) -async function findsExecutableWithScopedPermissions(chmodOptions: string) { +async function findsExecutableWithScopedPermissions( + chmodOptions: string +): Promise { // create a executable file const testPath = path.join(getTestTemp(), 'which-finds-file-name') await io.mkdirP(testPath) @@ -1347,13 +1365,13 @@ async function findsExecutableWithScopedPermissions(chmodOptions: string) { } const filePath = path.join(testPath, fileName) - fs.writeFileSync(filePath, '') + await fs.writeFile(filePath, '') chmod(filePath, chmodOptions) const originalPath = process.env['PATH'] try { // update the PATH - process.env['PATH'] = process.env['PATH'] + path.delimiter + testPath + process.env['PATH'] = `${process.env['PATH']}${path.delimiter}${testPath}` // exact file name expect(await io.which(fileName)).toBe(filePath) @@ -1373,13 +1391,21 @@ async function findsExecutableWithScopedPermissions(chmodOptions: string) { ) } else { // case sensitive on Linux - expect((await io.which(fileName.toUpperCase())) || '').toBe('') + expect(await io.which(fileName.toUpperCase())).toBe('') } } finally { process.env['PATH'] = originalPath } +} - return +// Assert that a file exists +async function assertExists(filePath: string): Promise { + expect(await fs.stat(filePath)).toBeDefined() +} + +// Assert that reading a file raises an ENOENT error (does not exist) +async function assertNotExists(filePath: string): Promise { + await expect(fs.stat(filePath)).rejects.toHaveProperty('code', 'ENOENT') } function chmod(file: string, mode: string): void { @@ -1391,46 +1417,42 @@ function chmod(file: string, mode: string): void { } async function createHiddenDirectory(dir: string): Promise { - return new Promise(async (resolve, reject) => { - if (!path.basename(dir).match(/^\./)) { - reject(`Expected dir '${dir}' to start with '.'.`) - } + if (!path.basename(dir).match(/^\./)) { + throw new Error(`Expected dir '${dir}' to start with '.'.`) + } - await io.mkdirP(dir) - if (os.platform() === 'win32') { - const result = child.spawnSync('attrib.exe', ['+H', dir]) - if (result.status !== 0) { - const message: string = (result.output || []).join(' ').trim() - reject( - `Failed to set hidden attribute for directory '${dir}'. ${message}` - ) - } + await io.mkdirP(dir) + if (os.platform() === 'win32') { + const result = child.spawnSync('attrib.exe', ['+H', dir]) + if (result.status !== 0) { + const message: string = (result.output || []).join(' ').trim() + throw new Error( + `Failed to set hidden attribute for directory '${dir}'. ${message}` + ) } - resolve() - }) + } } async function createHiddenFile(file: string, content: string): Promise { - return new Promise(async (resolve, reject) => { - if (!path.basename(file).match(/^\./)) { - reject(`Expected dir '${file}' to start with '.'.`) - } + if (!path.basename(file).match(/^\./)) { + throw new Error(`Expected dir '${file}' to start with '.'.`) + } - await io.mkdirP(path.dirname(file)) - fs.writeFileSync(file, content) - if (os.platform() === 'win32') { - const result = child.spawnSync('attrib.exe', ['+H', file]) - if (result.status !== 0) { - const message: string = (result.output || []).join(' ').trim() - reject(`Failed to set hidden attribute for file '${file}'. ${message}`) - } - } + await io.mkdirP(path.dirname(file)) + await fs.writeFile(file, content) - resolve() - }) + if (os.platform() === 'win32') { + const result = child.spawnSync('attrib.exe', ['+H', file]) + if (result.status !== 0) { + const message: string = (result.output || []).join(' ').trim() + throw new Error( + `Failed to set hidden attribute for file '${file}'. ${message}` + ) + } + } } -function getTestTemp() { +function getTestTemp(): string { return path.join(__dirname, '_temp') } @@ -1438,10 +1460,10 @@ function getTestTemp() { * Creates a symlink directory on OSX/Linux, and a junction point directory on Windows. * A symlink directory is not created on Windows since it requires an elevated context. */ -function createSymlinkDir(real: string, link: string): void { +async function createSymlinkDir(real: string, link: string): Promise { if (os.platform() === 'win32') { - fs.symlinkSync(real, link, 'junction') + await fs.symlink(real, link, 'junction') } else { - fs.symlinkSync(real, link) + await fs.symlink(real, link) } } diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index cf3e4bd124..649407bd2a 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -1,16 +1,16 @@ import * as fs from 'fs' import * as path from 'path' -export function _removeDirectory(directoryPath: string) { +export function _removeDirectory(directoryPath: string): void { if (fs.existsSync(directoryPath)) { - fs.readdirSync(directoryPath).forEach(fileName => { + for (const fileName of fs.readdirSync(directoryPath)) { const file = path.join(directoryPath, fileName) if (fs.lstatSync(file).isDirectory()) { _removeDirectory(file) } else { fs.unlinkSync(file) } - }) + } } fs.rmdirSync(directoryPath) } @@ -68,6 +68,7 @@ export function _tryGetExecutablePath( } } catch (err) { if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console console.log( `Unexpected error attempting to determine if executable file exists '${filePath}': ${err}` ) @@ -77,7 +78,7 @@ export function _tryGetExecutablePath( // try each extension const originalFilePath = filePath for (const extension of extensions) { - let filePath = originalFilePath + extension + filePath = originalFilePath + extension try { const stats: fs.Stats = fs.statSync(filePath) if (stats.isFile()) { @@ -93,6 +94,7 @@ export function _tryGetExecutablePath( } } } catch (err) { + // eslint-disable-next-line no-console console.log( `Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}` ) @@ -107,6 +109,7 @@ export function _tryGetExecutablePath( } } catch (err) { if (err.code !== 'ENOENT') { + // eslint-disable-next-line no-console console.log( `Unexpected error attempting to determine if executable file exists '${filePath}': ${err}` ) @@ -132,14 +135,10 @@ function _normalizeSeparators(p: string): string { return p.replace(/\/\/+/g, '/') } -function _startsWith(str: string, start: string): boolean { - return str.startsWith(start) -} - // on Mac/Linux, test the execute bit // R W X R W X R W X // 256 128 64 32 16 8 4 2 1 -function _isUnixExecutable(stats: fs.Stats) { +function _isUnixExecutable(stats: fs.Stats): boolean { return ( (stats.mode & 1) > 0 || ((stats.mode & 8) > 0 && stats.gid === process.getgid()) || diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index 559e13da12..c12ed1e40f 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -20,7 +20,7 @@ export interface CopyOptions { * @param dest destination path * @param options optional. See CopyOptions. */ -export function cp( +export async function cp( source: string, dest: string, options?: CopyOptions @@ -75,7 +75,7 @@ export function cp( * @param dest destination path * @param options optional. See CopyOptions. */ -export function mv( +export async function mv( source: string, dest: string, options?: CopyOptions @@ -131,7 +131,7 @@ export function mv( * * @param inputPath path to remove */ -export function rmRF(inputPath: string): Promise { +export async function rmRF(inputPath: string): Promise { return new Promise(async (resolve, reject) => { if (process.platform === 'win32') { // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another @@ -199,7 +199,7 @@ export function rmRF(inputPath: string): Promise { * @param p path to create * @returns Promise */ -export function mkdirP(p: string): Promise { +export async function mkdirP(p: string): Promise { return new Promise(async (resolve, reject) => { try { if (!p) { @@ -209,6 +209,8 @@ export function mkdirP(p: string): Promise { // build a stack of directories to create const stack: string[] = [] let testDir: string = p + + // 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)) { @@ -255,9 +257,10 @@ export function mkdirP(p: string): Promise { } // create each directory - while (stack.length) { - const dir = stack.pop()! // non-null because `stack.length` was truthy + let dir = stack.pop() + while (dir != null) { fs.mkdirSync(dir) + dir = stack.pop() } } catch (err) { reject(err) @@ -276,10 +279,10 @@ export function mkdirP(p: string): Promise { * @param check whether to check if tool exists * @returns Promise path to tool */ -export function which(tool: string, check?: boolean): Promise { +export async function which(tool: string, check?: boolean): Promise { return new Promise(async (resolve, reject) => { if (!tool) { - reject("parameter 'tool' is required") + reject(new Error("parameter 'tool' is required")) return } @@ -289,11 +292,15 @@ export function which(tool: string, check?: boolean): Promise { if (!result) { if (process.platform === 'win32') { reject( - `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.` + new Error( + `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.` + ) ) } else { reject( - `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.` + new Error( + `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.` + ) ) } return @@ -303,8 +310,8 @@ export function which(tool: string, check?: boolean): Promise { try { // build the list of extensions to try const extensions: string[] = [] - if (process.platform === 'win32' && process.env['PATHEXT']) { - for (const extension of process.env['PATHEXT']!.split(path.delimiter)) { + if (process.platform === 'win32' && process.env.PATHEXT) { + for (const extension of process.env.PATHEXT.split(path.delimiter)) { if (extension) { extensions.push(extension) } @@ -326,7 +333,7 @@ export function which(tool: string, check?: boolean): Promise { // if any path separators, return empty if ( tool.includes('/') || - (process.platform === 'win32' && tool.indexOf('\\') >= 0) + (process.platform === 'win32' && tool.includes('\\')) ) { resolve('') return @@ -339,8 +346,9 @@ export function which(tool: string, check?: boolean): Promise { // case of a shell, and the which() function exposed by the task lib should strive for consistency // across platforms. const directories: string[] = [] - if (process.env['PATH']) { - for (const p of process.env['PATH']!.split(path.delimiter)) { + + if (process.env.PATH) { + for (const p of process.env.PATH.split(path.delimiter)) { if (p) { directories.push(p) } @@ -361,7 +369,7 @@ export function which(tool: string, check?: boolean): Promise { resolve('') } catch (err) { - reject(`which failed with message ${err.message}`) + reject(new Error(`which failed with message ${err.message}`)) } }) } @@ -373,7 +381,7 @@ function copyDirectoryContents( dest: string, force: boolean, deleteOriginal = false -) { +): void { if (fs.lstatSync(source).isDirectory()) { if (fs.existsSync(dest)) { if (!fs.lstatSync(dest).isDirectory()) { @@ -385,7 +393,8 @@ function copyDirectoryContents( // Copy all child files, and directories recursively const sourceChildren: string[] = fs.readdirSync(source) - sourceChildren.forEach(newSource => { + + for (const newSource of sourceChildren) { const newDest = path.join(dest, path.basename(newSource)) copyDirectoryContents( path.resolve(source, newSource), @@ -393,7 +402,8 @@ function copyDirectoryContents( force, deleteOriginal ) - }) + } + if (deleteOriginal) { fs.rmdirSync(source) } From 122e1dea19d239fdd17bcab0472c5055aa51ba07 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 18:03:46 -0400 Subject: [PATCH 10/27] Use Boolean() to convert options - `CopyOptions` on `cp` now defaults to empty - Setting option values is easier now --- packages/io/src/io.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index c12ed1e40f..74b91ff666 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -23,18 +23,11 @@ export interface CopyOptions { export async function cp( source: string, dest: string, - options?: CopyOptions + options: CopyOptions = {} ): Promise { - let force = true - let recursive = false - if (options) { - if (options.recursive) { - recursive = true - } - if (options.force === false) { - force = false - } - } + const force = Boolean(options.force) + const recursive = Boolean(options.recursive) + return new Promise(async (resolve, reject) => { try { if (fs.lstatSync(source).isDirectory()) { From 04cfb1e6d96cdc45d43a4b568d321a797c2ed71f Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 18:40:53 -0400 Subject: [PATCH 11/27] Rewrite packages/io to be fully async --- packages/io/src/io-util.ts | 57 +++-- packages/io/src/io.ts | 493 +++++++++++++++++-------------------- 2 files changed, 259 insertions(+), 291 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index 649407bd2a..bc0600da34 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -1,26 +1,45 @@ import * as fs from 'fs' import * as path from 'path' -export function _removeDirectory(directoryPath: string): void { - if (fs.existsSync(directoryPath)) { - for (const fileName of fs.readdirSync(directoryPath)) { +export async function exists(fsPath: string): Promise { + try { + await fs.promises.stat(fsPath) + } catch (err) { + if (err.code === 'ENOENT') { + return false + } + + throw err + } + + return true +} + +export async function isDirectory(fsPath: string): Promise { + const lstat = await fs.promises.lstat(fsPath) + return lstat.isDirectory() +} + +export async function removeDirectory(directoryPath: string): Promise { + if (await exists(directoryPath)) { + for (const fileName of await fs.promises.readdir(directoryPath)) { const file = path.join(directoryPath, fileName) - if (fs.lstatSync(file).isDirectory()) { - _removeDirectory(file) + if (await isDirectory(file)) { + await removeDirectory(file) } else { - fs.unlinkSync(file) + await fs.promises.unlink(file) } } } - fs.rmdirSync(directoryPath) + await fs.promises.rmdir(directoryPath) } /** * On OSX/Linux, true if path starts with '/'. On Windows, true for paths like: * \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases). */ -export function _isRooted(p: string): boolean { - p = _normalizeSeparators(p) +export function isRooted(p: string): boolean { + p = normalizeSeparators(p) if (!p) { throw new Error('isRooted() parameter "p" cannot be empty') } @@ -40,13 +59,14 @@ export function _isRooted(p: string): boolean { * @param extensions additional file extensions to try * @return if file exists and is executable, returns the file path. otherwise empty string. */ -export function _tryGetExecutablePath( +export async function tryGetExecutablePath( filePath: string, extensions: string[] -): string { +): Promise { try { // test file exists - const stats: fs.Stats = fs.statSync(filePath) + const stats = await fs.promises.stat(filePath) + if (stats.isFile()) { if (process.platform === 'win32') { // on Windows, test for valid extension @@ -61,7 +81,7 @@ export function _tryGetExecutablePath( } } } else { - if (_isUnixExecutable(stats)) { + if (isUnixExecutable(stats)) { return filePath } } @@ -80,14 +100,15 @@ export function _tryGetExecutablePath( for (const extension of extensions) { filePath = originalFilePath + extension try { - const stats: fs.Stats = fs.statSync(filePath) + const stats = await fs.promises.stat(filePath) + if (stats.isFile()) { if (process.platform === 'win32') { // preserve the case of the actual file (since an extension was appended) try { const directory = path.dirname(filePath) const upperName = path.basename(filePath).toUpperCase() - for (const actualName of fs.readdirSync(directory)) { + for (const actualName of await fs.promises.readdir(directory)) { if (upperName === actualName.toUpperCase()) { filePath = path.join(directory, actualName) break @@ -102,7 +123,7 @@ export function _tryGetExecutablePath( return filePath } else { - if (_isUnixExecutable(stats)) { + if (isUnixExecutable(stats)) { return filePath } } @@ -120,7 +141,7 @@ export function _tryGetExecutablePath( return '' } -function _normalizeSeparators(p: string): string { +function normalizeSeparators(p: string): string { p = p || '' if (process.platform === 'win32') { // convert slashes on Windows @@ -138,7 +159,7 @@ function _normalizeSeparators(p: string): string { // on Mac/Linux, test the execute bit // R W X R W X R W X // 256 128 64 32 16 8 4 2 1 -function _isUnixExecutable(stats: fs.Stats): boolean { +function isUnixExecutable(stats: fs.Stats): boolean { return ( (stats.mode & 1) > 0 || ((stats.mode & 8) > 0 && stats.gid === process.getgid()) || diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index 74b91ff666..2b2d47a0e0 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -1,7 +1,11 @@ import * as childProcess from 'child_process' import * as fs from 'fs' import * as path from 'path' -import * as util from './io-util' +import {promisify} from 'util' +import * as ioUtil from './io-util' + +const IS_WINDOWS = process.platform === 'win32' +const exec = promisify(childProcess.exec) /** * Interface for cp/mv options @@ -25,40 +29,30 @@ export async function cp( dest: string, options: CopyOptions = {} ): Promise { - const force = Boolean(options.force) - const recursive = Boolean(options.recursive) - - return new Promise(async (resolve, reject) => { - try { - if (fs.lstatSync(source).isDirectory()) { - if (!recursive) { - throw new Error(`non-recursive cp failed, ${source} is a directory`) - } - - // If directory exists, copy source inside it. Otherwise, create it and copy contents of source inside. - if (fs.existsSync(dest)) { - if (!fs.lstatSync(dest).isDirectory()) { - throw new Error(`${dest} is not a directory`) - } + const {force, recursive} = readCopyOptions(options) - dest = path.join(dest, path.basename(source)) - } + if (await ioUtil.isDirectory(source)) { + if (!recursive) { + throw new Error(`non-recursive cp failed, ${source} is a directory`) + } - copyDirectoryContents(source, dest, force) - } else { - if (force) { - fs.copyFileSync(source, dest) - } else { - fs.copyFileSync(source, dest, fs.constants.COPYFILE_EXCL) - } + // If directory exists, copy source inside it. Otherwise, create it and copy contents of source inside. + if (await ioUtil.exists(dest)) { + if (!(await ioUtil.isDirectory(dest))) { + throw new Error(`${dest} is not a directory`) } - } catch (err) { - reject(err) - return + + dest = path.join(dest, path.basename(source)) } - resolve() - }) + await copyDirectoryContents(source, dest, force) + } else { + if (force) { + await fs.promises.copyFile(source, dest) + } else { + await fs.promises.copyFile(source, dest, fs.constants.COPYFILE_EXCL) + } + } } /** @@ -71,197 +65,154 @@ export async function cp( export async function mv( source: string, dest: string, - options?: CopyOptions + options: CopyOptions = {} ): Promise { - let force = true - let recursive = false - if (options) { - if (options.recursive) { - recursive = true - } - if (options.force === false) { - force = false - } - } - return new Promise(async (resolve, reject) => { - try { - if (fs.lstatSync(source).isDirectory()) { - if (!recursive) { - throw new Error(`non-recursive cp failed, ${source} is a directory`) - } + const {force, recursive} = readCopyOptions(options) - // If directory exists, move source inside it. Otherwise, create it and move contents of source inside. - if (fs.existsSync(dest)) { - if (!fs.lstatSync(dest).isDirectory()) { - throw new Error(`${dest} is not a directory`) - } + if (await ioUtil.isDirectory(source)) { + if (!recursive) { + throw new Error(`non-recursive cp failed, ${source} is a directory`) + } - dest = path.join(dest, path.basename(source)) - } + // If directory exists, move source inside it. Otherwise, create it and move contents of source inside. + if (await ioUtil.exists(dest)) { + if (!(await ioUtil.isDirectory(dest))) { + throw new Error(`${dest} is not a directory`) + } - copyDirectoryContents(source, dest, force, true) - } else { - if (force) { - fs.copyFileSync(source, dest) - } else { - fs.copyFileSync(source, dest, fs.constants.COPYFILE_EXCL) - } + dest = path.join(dest, path.basename(source)) + } - // Delete file after copying since this is mv. - fs.unlinkSync(source) - } - } catch (err) { - reject(err) - return + await copyDirectoryContents(source, dest, force, true) + } else { + if (force) { + await fs.promises.copyFile(source, dest) + } else { + await fs.promises.copyFile(source, dest, fs.constants.COPYFILE_EXCL) } - resolve() - }) + // Delete file after copying since this is mv. + await fs.promises.unlink(source) + } } /** * Remove a path recursively with force * - * @param inputPath path to remove + * @param inputPath path to remove */ export async function rmRF(inputPath: string): Promise { - return new Promise(async (resolve, reject) => { - if (process.platform === 'win32') { - // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another - // program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del. - try { - if (fs.statSync(inputPath).isDirectory()) { - childProcess.execSync(`rd /s /q "${inputPath}"`) - } else { - childProcess.execSync(`del /f /a "${inputPath}"`) - } - } catch (err) { - // if you try to delete a file that doesn't exist, desired result is achieved - // other errors are valid - if (err.code !== 'ENOENT') { - reject(err) - return - } - } - - // Shelling out fails to remove a symlink folder with missing source, this unlink catches that - try { - fs.unlinkSync(inputPath) - } catch (err) { - // if you try to delete a file that doesn't exist, desired result is achieved - // other errors are valid - if (err.code !== 'ENOENT') { - reject(err) - return - } - } - } else { - // get the lstats in order to workaround a bug in shelljs@0.3.0 where symlinks - // with missing targets are not handled correctly by "rm('-rf', path)" - let isDirectory = false - try { - isDirectory = fs.lstatSync(inputPath).isDirectory() - } catch (err) { - // if you try to delete a file that doesn't exist, desired result is achieved - // other errors are valid - if (err.code === 'ENOENT') { - resolve() - } else { - reject(err) - } - return - } - - if (isDirectory) { - util._removeDirectory(inputPath) - resolve() - return + if (IS_WINDOWS) { + // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another + // program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del. + try { + if (await ioUtil.isDirectory(inputPath)) { + await exec(`rd /s /q "${inputPath}"`) } else { - fs.unlinkSync(inputPath) + await exec(`del /f /a "${inputPath}"`) } + } catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code !== 'ENOENT') throw err } - resolve() - }) + // Shelling out fails to remove a symlink folder with missing source, this unlink catches that + try { + await fs.promises.unlink(inputPath) + } catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code !== 'ENOENT') throw err + } + } else { + // get the lstats in order to workaround a bug in shelljs@0.3.0 where symlinks + // with missing targets are not handled correctly by "rm('-rf', path)" + let isDir = false + try { + isDir = await ioUtil.isDirectory(inputPath) + } catch (err) { + // if you try to delete a file that doesn't exist, desired result is achieved + // other errors are valid + if (err.code !== 'ENOENT') throw err + return + } + + if (isDir) { + await ioUtil.removeDirectory(inputPath) + } else { + await fs.promises.unlink(inputPath) + } + } } /** * Make a directory. Creates the full path with folders in between * Will throw if it fails * - * @param p path to create - * @returns Promise + * @param fsPath path to create + * @returns Promise */ -export async function mkdirP(p: string): Promise { - return new Promise(async (resolve, reject) => { - try { - if (!p) { - throw new Error('Parameter p is required') - } +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 = p - - // 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 - fs.mkdirSync(p) - resolve() - return - } + // build a stack of directories to create + const stack: string[] = [] + let testDir: string = fsPath - let stats: fs.Stats - try { - stats = fs.statSync(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 '${p}'. Root directory does not exist: '${testDir}'` - ) - } - - // push the dir and test the parent - stack.push(testDir) - testDir = parentDir - continue - } else if (err.code === 'UNKNOWN') { - throw new Error( - `Unable to create directory '${p}'. Unable to verify the directory exists: '${testDir}'. If directory is a file share, please verify the share name is correct, the share is online, and the current process has permission to access the share.` - ) - } else { - throw err - } - } + // 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 fs.promises.mkdir(fsPath) + return + } - if (!stats.isDirectory()) { + let stats: fs.Stats + try { + stats = await fs.promises.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 '${p}'. Conflicting file exists: '${testDir}'` + `Unable to create directory '${fsPath}'. Root directory does not exist: '${testDir}'` ) } - // testDir exists - break + // push the dir and test the parent + stack.push(testDir) + testDir = parentDir + continue + } else if (err.code === 'UNKNOWN') { + throw new Error( + `Unable to create directory '${fsPath}'. Unable to verify the directory exists: '${testDir}'. If directory is a file share, please verify the share name is correct, the share is online, and the current process has permission to access the share.` + ) + } else { + throw err } + } - // create each directory - let dir = stack.pop() - while (dir != null) { - fs.mkdirSync(dir) - dir = stack.pop() - } - } catch (err) { - reject(err) - return + if (!stats.isDirectory()) { + throw new Error( + `Unable to create directory '${fsPath}'. Conflicting file exists: '${testDir}'` + ) } - resolve() - }) + // testDir exists + break + } + + // create each directory + let dir = stack.pop() + while (dir != null) { + await fs.promises.mkdir(dir) + dir = stack.pop() + } } /** @@ -273,123 +224,113 @@ export async function mkdirP(p: string): Promise { * @returns Promise path to tool */ export async function which(tool: string, check?: boolean): Promise { - return new Promise(async (resolve, reject) => { - if (!tool) { - reject(new Error("parameter 'tool' is required")) - return - } + if (!tool) { + throw new Error("parameter 'tool' is required") + } - // recursive when check=true - if (check) { - const result: string = await which(tool, false) - if (!result) { - if (process.platform === 'win32') { - reject( - new Error( - `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.` - ) - ) - } else { - reject( - new Error( - `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.` - ) - ) - } - return + // recursive when check=true + if (check) { + const result: string = await which(tool, false) + + if (!result) { + if (IS_WINDOWS) { + throw new Error( + `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.` + ) + } else { + throw new Error( + `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.` + ) } } + } - try { - // build the list of extensions to try - const extensions: string[] = [] - if (process.platform === 'win32' && process.env.PATHEXT) { - for (const extension of process.env.PATHEXT.split(path.delimiter)) { - if (extension) { - extensions.push(extension) - } + try { + // build the list of extensions to try + const extensions: string[] = [] + if (IS_WINDOWS && process.env.PATHEXT) { + for (const extension of process.env.PATHEXT.split(path.delimiter)) { + if (extension) { + extensions.push(extension) } } + } - // if it's rooted, return it if exists. otherwise return empty. - if (util._isRooted(tool)) { - const filePath: string = util._tryGetExecutablePath(tool, extensions) - if (filePath) { - resolve(filePath) - return - } + // if it's rooted, return it if exists. otherwise return empty. + if (ioUtil.isRooted(tool)) { + const filePath: string = await ioUtil.tryGetExecutablePath( + tool, + extensions + ) - resolve('') - return + if (filePath) { + return filePath } - // if any path separators, return empty - if ( - tool.includes('/') || - (process.platform === 'win32' && tool.includes('\\')) - ) { - resolve('') - return - } + return '' + } - // build the list of directories - // - // Note, technically "where" checks the current directory on Windows. From a task lib perspective, - // it feels like we should not do this. Checking the current directory seems like more of a use - // case of a shell, and the which() function exposed by the task lib should strive for consistency - // across platforms. - const directories: string[] = [] - - if (process.env.PATH) { - for (const p of process.env.PATH.split(path.delimiter)) { - if (p) { - directories.push(p) - } - } - } + // if any path separators, return empty + if (tool.includes('/') || (IS_WINDOWS && tool.includes('\\'))) { + return '' + } - // return the first match - for (const directory of directories) { - const filePath = util._tryGetExecutablePath( - directory + path.sep + tool, - extensions - ) - if (filePath) { - resolve(filePath) - return + // build the list of directories + // + // Note, technically "where" checks the current directory on Windows. From a task lib perspective, + // it feels like we should not do this. Checking the current directory seems like more of a use + // case of a shell, and the which() function exposed by the task lib should strive for consistency + // across platforms. + const directories: string[] = [] + + if (process.env.PATH) { + for (const p of process.env.PATH.split(path.delimiter)) { + if (p) { + directories.push(p) } } + } - resolve('') - } catch (err) { - reject(new Error(`which failed with message ${err.message}`)) + // return the first match + for (const directory of directories) { + const filePath = await ioUtil.tryGetExecutablePath( + directory + path.sep + tool, + extensions + ) + if (filePath) { + return filePath + } } - }) + + return '' + } catch (err) { + throw new Error(`which failed with message ${err.message}`) + } } // Copies contents of source into dest, making any necessary folders along the way. // Deletes the original copy if deleteOriginal is true -function copyDirectoryContents( +async function copyDirectoryContents( source: string, dest: string, force: boolean, deleteOriginal = false -): void { - if (fs.lstatSync(source).isDirectory()) { - if (fs.existsSync(dest)) { - if (!fs.lstatSync(dest).isDirectory()) { +): Promise { + if (await ioUtil.isDirectory(source)) { + if (await ioUtil.exists(dest)) { + if (!(await ioUtil.isDirectory(dest))) { throw new Error(`${dest} is not a directory`) } } else { - mkdirP(dest) + await mkdirP(dest) } // Copy all child files, and directories recursively - const sourceChildren: string[] = fs.readdirSync(source) + const sourceChildren: string[] = await fs.promises.readdir(source) for (const newSource of sourceChildren) { const newDest = path.join(dest, path.basename(newSource)) - copyDirectoryContents( + await copyDirectoryContents( path.resolve(source, newSource), newDest, force, @@ -398,16 +339,22 @@ function copyDirectoryContents( } if (deleteOriginal) { - fs.rmdirSync(source) + await fs.promises.rmdir(source) } } else { if (force) { - fs.copyFileSync(source, dest) + await fs.promises.copyFile(source, dest) } else { - fs.copyFileSync(source, dest, fs.constants.COPYFILE_EXCL) + await fs.promises.copyFile(source, dest, fs.constants.COPYFILE_EXCL) } if (deleteOriginal) { - fs.unlinkSync(source) + await fs.promises.unlink(source) } } } + +function readCopyOptions(options: CopyOptions): Required { + const force = options.force == null ? true : options.force + const recursive = Boolean(options.recursive) + return {force, recursive} +} From e566c89aab6ddc59b553710f3ef9b3be238123c1 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 18:49:37 -0400 Subject: [PATCH 12/27] Move IS_WINDOWS into ioUtil --- packages/io/src/io-util.ts | 10 ++++++---- packages/io/src/io.ts | 9 ++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index bc0600da34..9e705337b4 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -1,6 +1,8 @@ import * as fs from 'fs' import * as path from 'path' +export const IS_WINDOWS = process.platform === 'win32' + export async function exists(fsPath: string): Promise { try { await fs.promises.stat(fsPath) @@ -44,7 +46,7 @@ export function isRooted(p: string): boolean { throw new Error('isRooted() parameter "p" cannot be empty') } - if (process.platform === 'win32') { + if (IS_WINDOWS) { return ( p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello ) // e.g. C: or C:\hello @@ -68,7 +70,7 @@ export async function tryGetExecutablePath( const stats = await fs.promises.stat(filePath) if (stats.isFile()) { - if (process.platform === 'win32') { + if (IS_WINDOWS) { // on Windows, test for valid extension const fileName = path.basename(filePath) const dotIndex = fileName.lastIndexOf('.') @@ -103,7 +105,7 @@ export async function tryGetExecutablePath( const stats = await fs.promises.stat(filePath) if (stats.isFile()) { - if (process.platform === 'win32') { + if (IS_WINDOWS) { // preserve the case of the actual file (since an extension was appended) try { const directory = path.dirname(filePath) @@ -143,7 +145,7 @@ export async function tryGetExecutablePath( function normalizeSeparators(p: string): string { p = p || '' - if (process.platform === 'win32') { + if (IS_WINDOWS) { // convert slashes on Windows p = p.replace(/\//g, '\\') diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index 2b2d47a0e0..256fb8f54b 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -4,7 +4,6 @@ import * as path from 'path' import {promisify} from 'util' import * as ioUtil from './io-util' -const IS_WINDOWS = process.platform === 'win32' const exec = promisify(childProcess.exec) /** @@ -102,7 +101,7 @@ export async function mv( * @param inputPath path to remove */ export async function rmRF(inputPath: string): Promise { - if (IS_WINDOWS) { + if (ioUtil.IS_WINDOWS) { // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another // program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del. try { @@ -233,7 +232,7 @@ export async function which(tool: string, check?: boolean): Promise { const result: string = await which(tool, false) if (!result) { - if (IS_WINDOWS) { + if (ioUtil.IS_WINDOWS) { throw new Error( `Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.` ) @@ -248,7 +247,7 @@ export async function which(tool: string, check?: boolean): Promise { try { // build the list of extensions to try const extensions: string[] = [] - if (IS_WINDOWS && process.env.PATHEXT) { + if (ioUtil.IS_WINDOWS && process.env.PATHEXT) { for (const extension of process.env.PATHEXT.split(path.delimiter)) { if (extension) { extensions.push(extension) @@ -271,7 +270,7 @@ export async function which(tool: string, check?: boolean): Promise { } // if any path separators, return empty - if (tool.includes('/') || (IS_WINDOWS && tool.includes('\\'))) { + if (tool.includes('/') || (ioUtil.IS_WINDOWS && tool.includes('\\'))) { return '' } From 04be602429f4a6c4752af987e1094bc574c1471f Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Tue, 21 May 2019 18:55:20 -0400 Subject: [PATCH 13/27] DRY up cp/mv by moving shared code into move function --- packages/io/src/io.ts | 89 ++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 51 deletions(-) diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index 256fb8f54b..61f7ec4b90 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -28,30 +28,7 @@ export async function cp( dest: string, options: CopyOptions = {} ): Promise { - const {force, recursive} = readCopyOptions(options) - - if (await ioUtil.isDirectory(source)) { - if (!recursive) { - throw new Error(`non-recursive cp failed, ${source} is a directory`) - } - - // If directory exists, copy source inside it. Otherwise, create it and copy contents of source inside. - if (await ioUtil.exists(dest)) { - if (!(await ioUtil.isDirectory(dest))) { - throw new Error(`${dest} is not a directory`) - } - - dest = path.join(dest, path.basename(source)) - } - - await copyDirectoryContents(source, dest, force) - } else { - if (force) { - await fs.promises.copyFile(source, dest) - } else { - await fs.promises.copyFile(source, dest, fs.constants.COPYFILE_EXCL) - } - } + await move(source, dest, options, {deleteOriginal: false}) } /** @@ -66,33 +43,7 @@ export async function mv( dest: string, options: CopyOptions = {} ): Promise { - const {force, recursive} = readCopyOptions(options) - - if (await ioUtil.isDirectory(source)) { - if (!recursive) { - throw new Error(`non-recursive cp failed, ${source} is a directory`) - } - - // If directory exists, move source inside it. Otherwise, create it and move contents of source inside. - if (await ioUtil.exists(dest)) { - if (!(await ioUtil.isDirectory(dest))) { - throw new Error(`${dest} is not a directory`) - } - - dest = path.join(dest, path.basename(source)) - } - - await copyDirectoryContents(source, dest, force, true) - } else { - if (force) { - await fs.promises.copyFile(source, dest) - } else { - await fs.promises.copyFile(source, dest, fs.constants.COPYFILE_EXCL) - } - - // Delete file after copying since this is mv. - await fs.promises.unlink(source) - } + await move(source, dest, options, {deleteOriginal: true}) } /** @@ -352,6 +303,42 @@ async function copyDirectoryContents( } } +async function move( + source: string, + dest: string, + options: CopyOptions = {}, + moveOptions: {deleteOriginal: boolean} +): Promise { + const {force, recursive} = readCopyOptions(options) + + if (await ioUtil.isDirectory(source)) { + if (!recursive) { + throw new Error(`non-recursive cp failed, ${source} is a directory`) + } + + // If directory exists, move source inside it. Otherwise, create it and move contents of source inside. + if (await ioUtil.exists(dest)) { + if (!(await ioUtil.isDirectory(dest))) { + throw new Error(`${dest} is not a directory`) + } + + dest = path.join(dest, path.basename(source)) + } + + await copyDirectoryContents(source, dest, force, moveOptions.deleteOriginal) + } else { + if (force) { + await fs.promises.copyFile(source, dest) + } else { + await fs.promises.copyFile(source, dest, fs.constants.COPYFILE_EXCL) + } + + if (moveOptions.deleteOriginal) { + await fs.promises.unlink(source) + } + } +} + function readCopyOptions(options: CopyOptions): Required { const force = options.force == null ? true : options.force const recursive = Boolean(options.recursive) From 42a06eec22a31ba5a2894b166ee865d8439ad061 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Wed, 22 May 2019 10:00:01 -0400 Subject: [PATCH 14/27] Remove unc support, change isDirectory call to stat --- packages/io/src/io-util.ts | 7 +++---- packages/io/src/io.ts | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index 9e705337b4..7071a2ff1a 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -18,8 +18,8 @@ export async function exists(fsPath: string): Promise { } export async function isDirectory(fsPath: string): Promise { - const lstat = await fs.promises.lstat(fsPath) - return lstat.isDirectory() + const stat = await fs.promises.stat(fsPath) + return stat.isDirectory() } export async function removeDirectory(directoryPath: string): Promise { @@ -150,8 +150,7 @@ function normalizeSeparators(p: string): string { p = p.replace(/\//g, '\\') // remove redundant slashes - const isUnc = /^\\\\+[^\\]/.test(p) // e.g. \\hello - return (isUnc ? '\\' : '') + p.replace(/\\\\+/g, '\\') // preserve leading // for UNC + return p.replace(/\\\\+/g, '\\') // preserve leading // for UNC } // remove redundant slashes diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index 61f7ec4b90..0cd7c7b2d8 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -76,8 +76,6 @@ export async function rmRF(inputPath: string): Promise { if (err.code !== 'ENOENT') throw err } } else { - // get the lstats in order to workaround a bug in shelljs@0.3.0 where symlinks - // with missing targets are not handled correctly by "rm('-rf', path)" let isDir = false try { isDir = await ioUtil.isDirectory(inputPath) From 34b510f27ac2250c503928058eeca07072b5a9f0 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Wed, 22 May 2019 10:05:33 -0400 Subject: [PATCH 15/27] Tighter try catches --- packages/io/src/io-util.ts | 98 +++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index 7071a2ff1a..e20467fc99 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -65,29 +65,10 @@ export async function tryGetExecutablePath( filePath: string, extensions: string[] ): Promise { + let stats: fs.Stats | undefined = undefined try { // test file exists - const stats = await fs.promises.stat(filePath) - - if (stats.isFile()) { - if (IS_WINDOWS) { - // on Windows, test for valid extension - const fileName = path.basename(filePath) - const dotIndex = fileName.lastIndexOf('.') - if (dotIndex >= 0) { - const upperExt = fileName.substr(dotIndex).toUpperCase() - if ( - extensions.some(validExt => validExt.toUpperCase() === upperExt) - ) { - return filePath - } - } - } else { - if (isUnixExecutable(stats)) { - return filePath - } - } - } + stats = await fs.promises.stat(filePath) } catch (err) { if (err.code !== 'ENOENT') { // eslint-disable-next-line no-console @@ -96,40 +77,32 @@ export async function tryGetExecutablePath( ) } } + if (stats && stats.isFile()) { + if (IS_WINDOWS) { + // on Windows, test for valid extension + const fileName = path.basename(filePath) + const dotIndex = fileName.lastIndexOf('.') + if (dotIndex >= 0) { + const upperExt = fileName.substr(dotIndex).toUpperCase() + if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) { + return filePath + } + } + } else { + if (isUnixExecutable(stats)) { + return filePath + } + } + } // try each extension const originalFilePath = filePath for (const extension of extensions) { filePath = originalFilePath + extension - try { - const stats = await fs.promises.stat(filePath) - - if (stats.isFile()) { - if (IS_WINDOWS) { - // preserve the case of the actual file (since an extension was appended) - try { - const directory = path.dirname(filePath) - const upperName = path.basename(filePath).toUpperCase() - for (const actualName of await fs.promises.readdir(directory)) { - if (upperName === actualName.toUpperCase()) { - filePath = path.join(directory, actualName) - break - } - } - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}` - ) - } - return filePath - } else { - if (isUnixExecutable(stats)) { - return filePath - } - } - } + stats = undefined + try { + stats = await fs.promises.stat(filePath) } catch (err) { if (err.code !== 'ENOENT') { // eslint-disable-next-line no-console @@ -138,6 +111,33 @@ export async function tryGetExecutablePath( ) } } + + if (stats && stats.isFile()) { + if (IS_WINDOWS) { + // preserve the case of the actual file (since an extension was appended) + try { + const directory = path.dirname(filePath) + const upperName = path.basename(filePath).toUpperCase() + for (const actualName of await fs.promises.readdir(directory)) { + if (upperName === actualName.toUpperCase()) { + filePath = path.join(directory, actualName) + break + } + } + } catch (err) { + // eslint-disable-next-line no-console + console.log( + `Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}` + ) + } + + return filePath + } else { + if (isUnixExecutable(stats)) { + return filePath + } + } + } } return '' From b9d5ac544137d7ff358cb1a3ae995206fb907c40 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Wed, 22 May 2019 10:10:30 -0400 Subject: [PATCH 16/27] more concise extensions search --- packages/io/src/io-util.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index e20467fc99..ee368f6b9c 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -80,13 +80,9 @@ export async function tryGetExecutablePath( if (stats && stats.isFile()) { if (IS_WINDOWS) { // on Windows, test for valid extension - const fileName = path.basename(filePath) - const dotIndex = fileName.lastIndexOf('.') - if (dotIndex >= 0) { - const upperExt = fileName.substr(dotIndex).toUpperCase() - if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) { - return filePath - } + const upperExt = path.extname(filePath).toUpperCase() + if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) { + return filePath } } else { if (isUnixExecutable(stats)) { From 3c3cc1e8acbda13c7013a0448fd253409a676dc2 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Wed, 22 May 2019 10:20:01 -0400 Subject: [PATCH 17/27] Allow isDirectory to be stat or lstat --- packages/io/src/io-util.ts | 4 ++-- packages/io/src/io.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index ee368f6b9c..bf489e555d 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -17,8 +17,8 @@ export async function exists(fsPath: string): Promise { return true } -export async function isDirectory(fsPath: string): Promise { - const stat = await fs.promises.stat(fsPath) +export async function isDirectory(fsPath: string, useStat: boolean = false): Promise { + const stat = useStat ? await fs.promises.stat(fsPath) : await fs.promises.lstat(fsPath) return stat.isDirectory() } diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index 0cd7c7b2d8..e4b598ed46 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -56,7 +56,7 @@ export async function rmRF(inputPath: string): Promise { // Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another // program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del. try { - if (await ioUtil.isDirectory(inputPath)) { + if (await ioUtil.isDirectory(inputPath, true)) { await exec(`rd /s /q "${inputPath}"`) } else { await exec(`del /f /a "${inputPath}"`) From a27177fcccf094341845eb25b88f45600084b078 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Wed, 22 May 2019 10:23:03 -0400 Subject: [PATCH 18/27] format --- packages/io/src/io-util.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index bf489e555d..6450482a05 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -17,8 +17,13 @@ export async function exists(fsPath: string): Promise { return true } -export async function isDirectory(fsPath: string, useStat: boolean = false): Promise { - const stat = useStat ? await fs.promises.stat(fsPath) : await fs.promises.lstat(fsPath) +export async function isDirectory( + fsPath: string, + useStat: boolean = false +): Promise { + const stat = useStat + ? await fs.promises.stat(fsPath) + : await fs.promises.lstat(fsPath) return stat.isDirectory() } From 8f69c22176878b29b32573878b8e663bbad21767 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Wed, 22 May 2019 10:39:55 -0400 Subject: [PATCH 19/27] Shell out to rm -rf --- packages/io/src/io-util.ts | 14 -------------- packages/io/src/io.ts | 2 +- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index 6450482a05..beb88b6711 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -27,20 +27,6 @@ export async function isDirectory( return stat.isDirectory() } -export async function removeDirectory(directoryPath: string): Promise { - if (await exists(directoryPath)) { - for (const fileName of await fs.promises.readdir(directoryPath)) { - const file = path.join(directoryPath, fileName) - if (await isDirectory(file)) { - await removeDirectory(file) - } else { - await fs.promises.unlink(file) - } - } - } - await fs.promises.rmdir(directoryPath) -} - /** * On OSX/Linux, true if path starts with '/'. On Windows, true for paths like: * \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases). diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index e4b598ed46..53b4ee22aa 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -87,7 +87,7 @@ export async function rmRF(inputPath: string): Promise { } if (isDir) { - await ioUtil.removeDirectory(inputPath) + await exec(`rm -rf "${inputPath}"`) } else { await fs.promises.unlink(inputPath) } From d8d83c9688caff0081586cde72cb176626773b35 Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Wed, 22 May 2019 11:02:34 -0400 Subject: [PATCH 20/27] Remove unc comment --- packages/io/src/io-util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index beb88b6711..acb7d398f1 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -137,7 +137,7 @@ function normalizeSeparators(p: string): string { p = p.replace(/\//g, '\\') // remove redundant slashes - return p.replace(/\\\\+/g, '\\') // preserve leading // for UNC + return p.replace(/\\\\+/g, '\\') } // remove redundant slashes From 2c1f4eb302cc5f27e5cf90cb55c8aa681b0e5bd6 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 11:25:05 -0400 Subject: [PATCH 21/27] Export fs.promises from io-util --- packages/io/src/io-util.ts | 24 ++++++++++++++++-------- packages/io/src/io.ts | 26 +++++++++++++------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index acb7d398f1..6aaa622f47 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -1,11 +1,21 @@ import * as fs from 'fs' import * as path from 'path' +export const { + copyFile, + lstat, + mkdir, + readdir, + rmdir, + stat, + unlink +} = fs.promises + export const IS_WINDOWS = process.platform === 'win32' export async function exists(fsPath: string): Promise { try { - await fs.promises.stat(fsPath) + await stat(fsPath) } catch (err) { if (err.code === 'ENOENT') { return false @@ -21,10 +31,8 @@ export async function isDirectory( fsPath: string, useStat: boolean = false ): Promise { - const stat = useStat - ? await fs.promises.stat(fsPath) - : await fs.promises.lstat(fsPath) - return stat.isDirectory() + const stats = useStat ? await stat(fsPath) : await lstat(fsPath) + return stats.isDirectory() } /** @@ -59,7 +67,7 @@ export async function tryGetExecutablePath( let stats: fs.Stats | undefined = undefined try { // test file exists - stats = await fs.promises.stat(filePath) + stats = await stat(filePath) } catch (err) { if (err.code !== 'ENOENT') { // eslint-disable-next-line no-console @@ -89,7 +97,7 @@ export async function tryGetExecutablePath( stats = undefined try { - stats = await fs.promises.stat(filePath) + stats = await stat(filePath) } catch (err) { if (err.code !== 'ENOENT') { // eslint-disable-next-line no-console @@ -105,7 +113,7 @@ export async function tryGetExecutablePath( try { const directory = path.dirname(filePath) const upperName = path.basename(filePath).toUpperCase() - for (const actualName of await fs.promises.readdir(directory)) { + for (const actualName of await readdir(directory)) { if (upperName === actualName.toUpperCase()) { filePath = path.join(directory, actualName) break diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index 53b4ee22aa..a1f44bea04 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -69,7 +69,7 @@ export async function rmRF(inputPath: string): Promise { // Shelling out fails to remove a symlink folder with missing source, this unlink catches that try { - await fs.promises.unlink(inputPath) + await ioUtil.unlink(inputPath) } catch (err) { // if you try to delete a file that doesn't exist, desired result is achieved // other errors are valid @@ -89,7 +89,7 @@ export async function rmRF(inputPath: string): Promise { if (isDir) { await exec(`rm -rf "${inputPath}"`) } else { - await fs.promises.unlink(inputPath) + await ioUtil.unlink(inputPath) } } } @@ -115,13 +115,13 @@ export async function mkdirP(fsPath: string): Promise { // validate the loop is not out of control if (stack.length >= (process.env['TEST_MKDIRP_FAILSAFE'] || 1000)) { // let the framework throw - await fs.promises.mkdir(fsPath) + await ioUtil.mkdir(fsPath) return } let stats: fs.Stats try { - stats = await fs.promises.stat(testDir) + stats = await ioUtil.stat(testDir) } catch (err) { if (err.code === 'ENOENT') { // validate the directory is not the drive root @@ -158,7 +158,7 @@ export async function mkdirP(fsPath: string): Promise { // create each directory let dir = stack.pop() while (dir != null) { - await fs.promises.mkdir(dir) + await ioUtil.mkdir(dir) dir = stack.pop() } } @@ -274,7 +274,7 @@ async function copyDirectoryContents( } // Copy all child files, and directories recursively - const sourceChildren: string[] = await fs.promises.readdir(source) + const sourceChildren: string[] = await ioUtil.readdir(source) for (const newSource of sourceChildren) { const newDest = path.join(dest, path.basename(newSource)) @@ -287,16 +287,16 @@ async function copyDirectoryContents( } if (deleteOriginal) { - await fs.promises.rmdir(source) + await ioUtil.rmdir(source) } } else { if (force) { - await fs.promises.copyFile(source, dest) + await ioUtil.copyFile(source, dest) } else { - await fs.promises.copyFile(source, dest, fs.constants.COPYFILE_EXCL) + await ioUtil.copyFile(source, dest, fs.constants.COPYFILE_EXCL) } if (deleteOriginal) { - await fs.promises.unlink(source) + await ioUtil.unlink(source) } } } @@ -326,13 +326,13 @@ async function move( await copyDirectoryContents(source, dest, force, moveOptions.deleteOriginal) } else { if (force) { - await fs.promises.copyFile(source, dest) + await ioUtil.copyFile(source, dest) } else { - await fs.promises.copyFile(source, dest, fs.constants.COPYFILE_EXCL) + await ioUtil.copyFile(source, dest, fs.constants.COPYFILE_EXCL) } if (moveOptions.deleteOriginal) { - await fs.promises.unlink(source) + await ioUtil.unlink(source) } } } From bf9650a972a6fce506fdc6027e6dc8c037986fcd Mon Sep 17 00:00:00 2001 From: Danny McCormick Date: Wed, 22 May 2019 12:35:57 -0400 Subject: [PATCH 22/27] Remove unknown error message --- packages/io/src/io.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index a1f44bea04..b6425e5076 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -136,10 +136,6 @@ export async function mkdirP(fsPath: string): Promise { stack.push(testDir) testDir = parentDir continue - } else if (err.code === 'UNKNOWN') { - throw new Error( - `Unable to create directory '${fsPath}'. Unable to verify the directory exists: '${testDir}'. If directory is a file share, please verify the share name is correct, the share is online, and the current process has permission to access the share.` - ) } else { throw err } From 8d79fd8851e815ccd38cac812a36dad9df37944a Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 13:22:50 -0400 Subject: [PATCH 23/27] Create an optimistic mkdirp --- packages/io/__tests__/io.test.ts | 23 +++--------- packages/io/src/io-util.ts | 43 +++++++++++++++++++++++ packages/io/src/io.ts | 60 +------------------------------- 3 files changed, 49 insertions(+), 77 deletions(-) diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index ddaedded65..8bbf0d931f 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 () => { @@ -688,18 +689,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 +796,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..d02a8a3881 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -54,6 +54,49 @@ 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 { + fsPath = path.resolve(fsPath) + + if (depth >= maxDepth) return mkdir(fsPath) + + try { + await mkdir(fsPath) + } catch (err) { + switch (err.code) { + case 'ENOENT': { + await mkdirP(path.dirname(fsPath), maxDepth, depth + 1) + await mkdirP(fsPath, maxDepth, depth + 1) + break + } + 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 a1f44bea04..25811854f5 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -102,65 +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 if (err.code === 'UNKNOWN') { - throw new Error( - `Unable to create directory '${fsPath}'. Unable to verify the directory exists: '${testDir}'. If directory is a file share, please verify the share name is correct, the share is online, and the current process has permission to access the share.` - ) - } 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) } /** From d19a10b72ef3ee7a2afe6c4b8f9519ea5dfd45a7 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 14:56:15 -0400 Subject: [PATCH 24/27] Update io-util.ts --- packages/io/src/io-util.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index d02a8a3881..d83cb6d3a7 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -74,13 +74,12 @@ export async function mkdirP( if (depth >= maxDepth) return mkdir(fsPath) try { - await mkdir(fsPath) + return mkdir(fsPath) } catch (err) { switch (err.code) { case 'ENOENT': { await mkdirP(path.dirname(fsPath), maxDepth, depth + 1) - await mkdirP(fsPath, maxDepth, depth + 1) - break + return mkdir(fsPath) } default: { let stats: fs.Stats From 9c3517ba7267e7d368ec2c434591e334f6929648 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 15:05:48 -0400 Subject: [PATCH 25/27] Update io-util.ts --- packages/io/src/io-util.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index d83cb6d3a7..e1c7b2e5d0 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' @@ -69,6 +70,8 @@ export async function mkdirP( 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) From d73dd7504e804333d4f791adf58981f029eb910c Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 15:13:24 -0400 Subject: [PATCH 26/27] Update io.test.ts --- packages/io/__tests__/io.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index 8bbf0d931f..4b18311da5 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -659,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) From acb4428a9da1363202c2b14a954894bf99cf9525 Mon Sep 17 00:00:00 2001 From: Jonathan Clem Date: Wed, 22 May 2019 15:51:12 -0400 Subject: [PATCH 27/27] Fix tests for mkdirP --- packages/io/__tests__/io.test.ts | 2 +- packages/io/src/io-util.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index 4b18311da5..9521825051 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -664,7 +664,7 @@ describe('mkdirP', () => { try { await io.mkdirP('') - } catch(err) { + } catch (err) { expect(err.message).toEqual('a path argument must be provided') } }) diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index e1c7b2e5d0..d5d4e6777a 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -77,12 +77,14 @@ export async function mkdirP( if (depth >= maxDepth) return mkdir(fsPath) try { - return mkdir(fsPath) + await mkdir(fsPath) + return } catch (err) { switch (err.code) { case 'ENOENT': { await mkdirP(path.dirname(fsPath), maxDepth, depth + 1) - return mkdir(fsPath) + await mkdir(fsPath) + return } default: { let stats: fs.Stats