diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index c05be13f..4d7f1af0 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -292,6 +292,34 @@ describe('mv', () => { ) await assertNotExists(sourceFile) }) + + describe('ioUtil.rename', () => { + beforeAll(() => { + jest.spyOn(fs, 'rename').mockImplementation(() => { + throw Object.assign(new Error('cross-device link not permitted'), { + code: 'EXDEV' + }) + }) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('fallbacks to `fs.cp` and `fs.rm` on `EXDEV` exception', async () => { + const root: string = path.join(getTestTemp(), 'rename_fallback_test') + const source: string = path.join(root, 'realfile1') + const dest: string = path.join(root, 'realfile2') + + await io.mkdirP(root) + await fs.writeFile(source, 'test file content', {encoding: 'utf8'}) + await io.mv(source, dest) + + expect(await fs.readFile(dest, {encoding: 'utf8'})).toBe( + 'test file content' + ) + }) + }) }) describe('rmRF', () => { diff --git a/packages/io/src/io-util.ts b/packages/io/src/io-util.ts index 89709f38..48edf05e 100644 --- a/packages/io/src/io-util.ts +++ b/packages/io/src/io-util.ts @@ -9,13 +9,34 @@ export const { open, readdir, readlink, - rename, rm, rmdir, stat, symlink, unlink } = fs.promises + +export async function rename( + src: fs.PathLike, + dest: fs.PathLike +): Promise { + try { + await fs.promises.rename(src, dest) + } catch (err) { + if (err.code === 'EXDEV') { + await fs.promises.cp( + src instanceof Buffer ? src.toString('utf8') : src, + dest instanceof Buffer ? dest.toString('utf8') : dest, + {recursive: true} + ) + await fs.promises.rm(src, {recursive: true}) + return + } + + throw err + } +} + // export const {open} = 'fs' export const IS_WINDOWS = process.platform === 'win32' // See https://github.com/nodejs/node/blob/d0153aee367422d0858105abec186da4dff0a0c5/deps/uv/include/uv/win.h#L691