From c8db94649c4caae1c0a6199a6a2e96699a8b522c Mon Sep 17 00:00:00 2001 From: Himanshu Jha <07himanshuraj@gmail.com> Date: Thu, 12 Nov 2026 02:39:38 +0530 Subject: [PATCH] fs: make mkstemp disposable Implement Symbol.dispose and Symbol.asyncDispose on objects returned by fs.mkstempSync(), fs.mkstemp(), and fs.promises.mkstemp(). When disposed or when calling .remove(), the file descriptor or FileHandle is closed and the created file is removed. Repeated removal calls are idempotent, and manually closing the descriptor or handle prior to disposal does not throw. Refs: https://github.com/nodejs/node/issues/67360 Signed-off-by: Himanshu Jha <07himanshuraj@gmail.com> --- doc/api/fs.md | 66 +++-- lib/fs.js | 71 +++++- lib/internal/fs/promises.js | 37 ++- lib/internal/vfs/file_system.js | 49 +++- lib/internal/vfs/setup.js | 27 ++- test/parallel/test-fs-mkstemp-disposable.js | 251 ++++++++++++++++++++ test/parallel/test-vfs-mkstemp.js | 35 +++ 7 files changed, 502 insertions(+), 34 deletions(-) create mode 100644 test/parallel/test-fs-mkstemp-disposable.js diff --git a/doc/api/fs.md b/doc/api/fs.md index 93e6e7c68a1..d6fe8837d0b 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -1743,9 +1743,11 @@ added: v27.0.0-alpha.0.0.0 * `prefix` {string|Buffer|URL} * `options` {string|Object} * `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`) -* Returns: {Promise} Fulfills with an {Object}: +* Returns: {Promise} Fulfills with an async-disposable {Object}: * `path` {string|Buffer} The path of the created file. * `handle` {FileHandle} The created file, opened for reading and writing. + * `remove` {AsyncFunction} A function which closes the handle and removes the created file. + * `[Symbol.asyncDispose]` {AsyncFunction} The same as `remove`. Creates and opens a unique temporary file. A unique file name is generated by appending six random characters to the end of the provided `prefix`. Due to @@ -1757,6 +1759,18 @@ The file is created and opened in a single operation, so another process cannot create a file with the same name in between. On POSIX systems, the file is only readable and writable by its owner. +When the returned object is disposed, its file handle will be closed and the +file will be removed asynchronously if it still exists. If the file cannot be +deleted, disposal will throw an error. The object has an async `remove()` method +which will perform the same task. + +Both this function and the disposal function on the resulting object are async, +so it can be used with `await` + [`await using`][] as in +`await using file = await fsPromises.mkstemp('prefix')`. + +See the [MDN documentation on `using` statements][`using`] for more information about +explicit resource management. + The optional `options` argument can be a string specifying an encoding, or an object with an `encoding` property specifying the character encoding to use for the returned `path`. @@ -1766,18 +1780,16 @@ import { mkstemp } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; -const { path, handle } = await mkstemp(join(tmpdir(), 'foo-')); -try { - await handle.writeFile('some data'); - console.log(path); - // Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2 -} finally { - await handle.close(); -} +await using file = await mkstemp(join(tmpdir(), 'foo-')); +await file.handle.writeFile('some data'); +console.log(file.path); +// Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2 +// When the scope exits, the file handle is closed and the file is deleted. ``` -The file is not removed automatically. It is the caller's responsibility to -close the {FileHandle} and to remove the file when it is no longer needed. +When not using `await using`, the file can be removed manually by calling +`await file.remove()`, or the handle can be closed manually with +`await file.handle.close()`. As with [`fsPromises.mkdtemp()`][], the random characters are appended directly to `prefix`. To create a file _within_ a directory, `prefix` must end with a @@ -4164,10 +4176,12 @@ added: v27.0.0-alpha.0.0.0 * `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`) * `callback` {Function} * `err` {Error} - * `file` {Object} + * `file` {Object} A disposable object: * `path` {string|Buffer} The path of the created file. * `fd` {integer} A file descriptor for the created file, opened for reading and writing. + * `remove` {Function} A function which closes the file descriptor and removes the created file. + * `[Symbol.dispose]` {Function} The same as `remove`. Creates and opens a unique temporary file. @@ -4181,6 +4195,11 @@ The file is created and opened in a single operation, so another process cannot create a file with the same name in between. On POSIX systems, the file is only readable and writable by its owner. +When the `file` object is disposed, its file descriptor will be closed and the +file will be removed if it still exists. If the file cannot be deleted, disposal +will throw an error. The object has a `remove()` method which will perform the +same task. + The optional `options` argument can be a string specifying an encoding, or an object with an `encoding` property specifying the character encoding to use for the `path` passed to the callback. @@ -4196,15 +4215,13 @@ mkstemp(join(tmpdir(), 'foo-'), (err, file) => { // Prints: /tmp/foo-itXde2 or C:\Users\...\AppData\Local\Temp\foo-itXde2 write(file.fd, 'some data', (err) => { if (err) throw err; - close(file.fd, (err) => { - if (err) throw err; - }); + file.remove(); }); }); ``` -The file is not removed automatically. It is the caller's responsibility to -close the file descriptor and to remove the file when it is no longer needed. +When not using `file.remove()` or `file[Symbol.dispose]()`, the file descriptor +can be closed manually with `close()` or `closeSync()`. As with [`fs.mkdtemp()`][], the random characters are appended directly to `prefix`. To create a file _within_ a directory, `prefix` must end with a @@ -6706,13 +6723,26 @@ added: v27.0.0-alpha.0.0.0 * `prefix` {string|Buffer|URL} * `options` {string|Object} * `encoding` {string} **Default:** `'utf8'` (or `'buffer'` if `prefix` is a `Buffer`) -* Returns: {Object} +* Returns: {Object} A disposable object: * `path` {string|Buffer} The path of the created file. * `fd` {integer} A file descriptor for the created file, opened for reading and writing. + * `remove` {Function} A function which closes the file descriptor and removes the created file. + * `[Symbol.dispose]` {Function} The same as `remove`. Synchronously creates and opens a unique temporary file. +When the returned object is disposed, its file descriptor will be closed and the +file will be removed if it still exists. If the file cannot be deleted, disposal +will throw an error. The object has a `remove()` method which will perform the +same task. + +This enables the returned object to be used with the [`using`][] syntax as in +`using file = fs.mkstempSync('prefix')`. + +See the [MDN documentation on `using` statements][`using`] for more information about +explicit resource management. + For detailed information, see the documentation of the asynchronous version of this API: [`fs.mkstemp()`][]. diff --git a/lib/fs.js b/lib/fs.js index a5fc9a7571c..294bd25de6c 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -3835,9 +3835,16 @@ function mkdtempDisposableSync(prefix, options) { /** * Creates and opens a unique temporary file. + * The returned value is an object which removes the file and closes its file + * descriptor when disposed. * @param {string | Buffer | URL} prefix * @param {string | { encoding?: string; }} [options] - * @param {(err?: Error, file?: { path: string | Buffer, fd: number }) => any} callback + * @param {(err?: Error, file?: { + * path: string | Buffer, + * fd: number, + * remove: () => void, + * [Symbol.dispose]: () => void, + * }) => any} callback * @returns {void} */ function mkstemp(prefix, options, callback) { @@ -3853,19 +3860,51 @@ function mkstemp(prefix, options, callback) { const h = vfsState.handlers; if (h !== null && vfsResult(h.mkstemp(prefix, options), callback)) return; + const cwd = process.cwd(); const req = new FSReqCallback(); req.oncomplete = (err, result) => { if (err) return callback(err); - callback(null, { path: result[0], fd: result[1] }); + const path = result[0]; + const fd = result[1]; + const fullPath = pathModule.resolve(cwd, BufferIsBuffer(path) ? BufferToString(path) : path); + + const remove = () => { + try { + binding.close(fd); + } catch (err) { + if (err.code !== 'EBADF') throw err; + } + try { + binding.unlink(fullPath); + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } + }; + + callback(null, { + path, + fd, + remove, + [SymbolDispose]() { + remove(); + }, + }); }; binding.mkstemp(prefix, options.encoding, req); } /** * Synchronously creates and opens a unique temporary file. + * The returned value is an object which removes the file and closes its file + * descriptor when disposed. * @param {string | Buffer | URL} prefix * @param {string | { encoding?: string; }} [options] - * @returns {{ path: string | Buffer, fd: number }} + * @returns {{ + * path: string | Buffer, + * fd: number, + * remove: () => void, + * [Symbol.dispose]: () => void, + * }} */ function mkstempSync(prefix, options) { options = getOptions(options); @@ -3882,7 +3921,31 @@ function mkstempSync(prefix, options) { } const result = binding.mkstemp(prefix, options.encoding); - return { path: result[0], fd: result[1] }; + const path = result[0]; + const fd = result[1]; + const fullPath = pathModule.resolve(process.cwd(), BufferIsBuffer(path) ? BufferToString(path) : path); + + const remove = () => { + try { + binding.close(fd); + } catch (err) { + if (err.code !== 'EBADF') throw err; + } + try { + binding.unlink(fullPath); + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } + }; + + return { + path, + fd, + remove, + [SymbolDispose]() { + remove(); + }, + }; } /** diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index b79e807fe6c..a5207f4134d 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -2136,6 +2136,19 @@ async function mkdtemp(prefix, options) { ); } +/** + * Creates and opens a unique temporary file asynchronously. + * The returned value is an async-disposable object which removes the file and + * closes its file handle when disposed. + * @param {string | Buffer | URL} prefix + * @param {string | { encoding?: string; }} [options] + * @returns {Promise<{ + * path: string | Buffer, + * handle: FileHandle, + * remove: () => Promise, + * [Symbol.asyncDispose]: () => Promise, + * }>} + */ async function mkstemp(prefix, options) { options = getOptions(options); if (BufferIsBuffer(prefix)) { @@ -2150,12 +2163,34 @@ async function mkstemp(prefix, options) { if (promise !== undefined) return await promise; } + const cwd = process.cwd(); const result = await PromisePrototypeThen( binding.mkstemp(prefix, options.encoding, kUsePromises), undefined, handleErrorFromBinding, ); - return { __proto__: null, path: result[0], handle: new FileHandle(result[1]) }; + const path = result[0]; + const handle = new FileHandle(result[1]); + const fullPath = pathModule.resolve(cwd, BufferIsBuffer(path) ? BufferToString(path) : path); + + const remove = async () => { + await handle.close(); + try { + await unlink(fullPath); + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } + }; + + return { + __proto__: null, + path, + handle, + remove, + async [SymbolAsyncDispose]() { + await remove(); + }, + }; } async function mkdtempDisposable(prefix, options) { diff --git a/lib/internal/vfs/file_system.js b/lib/internal/vfs/file_system.js index 4982542239e..f0061d0a7a2 100644 --- a/lib/internal/vfs/file_system.js +++ b/lib/internal/vfs/file_system.js @@ -7,6 +7,7 @@ const { StringPrototypeSlice, StringPrototypeStartsWith, Symbol, + SymbolAsyncDispose, SymbolDispose, } = primordials; @@ -620,12 +621,38 @@ class VirtualFileSystem { /** * Creates and opens a unique temporary file synchronously. * @param {string} prefix The prefix for the temp file - * @returns {{ path: string, fd: number }} The full path and a file descriptor + * @returns {{ + * path: string, + * fd: number, + * remove: () => void, + * [Symbol.dispose]: () => void, + * }} The full path and a file descriptor */ mkstempSync(prefix) { const filePath = this.#toProviderPrefix(prefix) + randomSuffix(); const handle = this[kProvider].openSync(filePath, 'wx+', 0o600); - return { path: this.#toMountedPath(filePath), fd: openVirtualFd(handle) }; + const mountedPath = this.#toMountedPath(filePath); + const fd = openVirtualFd(handle); + const remove = () => { + try { + this.closeSync(fd); + } catch (err) { + if (err.code !== 'EBADF') throw err; + } + try { + this.unlinkSync(mountedPath); + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } + }; + return { + path: mountedPath, + fd, + remove, + [SymbolDispose]() { + remove(); + }, + }; } /** @@ -1349,10 +1376,24 @@ class VirtualFileSystem { async mkstemp(prefix) { const filePath = toProviderPrefix(prefix) + randomSuffix(); const handle = provider.openSync(filePath, 'wx+', 0o600); + const mountedPath = toMountedPath(filePath); + const fd = openVirtualFd(handle); + const remove = async () => { + closeVirtualFd(fd); + try { + await this.unlink(mountedPath); + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } + }; return { __proto__: null, - path: toMountedPath(filePath), - fd: openVirtualFd(handle), + path: mountedPath, + fd, + remove, + async [SymbolAsyncDispose]() { + await remove(); + }, }; }, diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index 1ed74f5d566..01cb684c89b 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -9,6 +9,7 @@ const { String, StringPrototypeEndsWith, StringPrototypeStartsWith, + SymbolAsyncDispose, } = primordials; const { Buffer } = require('buffer'); @@ -891,21 +892,33 @@ function createVfsHandlers() { // Wrapped in async functions so provider throws become rejections. mkstemp(prefix, options) { return vfsOp(prefix, async (vfs, n) => { - const { path, fd } = vfs.mkstempSync(n); - return { - __proto__: null, - path: options?.encoding === 'buffer' ? Buffer.from(path) : path, - fd, - }; + const file = vfs.mkstempSync(n); + if (options?.encoding === 'buffer') { + file.path = Buffer.from(file.path); + } + return file; }); }, promisesMkstemp(prefix, options) { return vfsOp(prefix, async (vfs, n) => { const { path, fd } = vfs.mkstempSync(n); + const handle = createVfsFileHandle(getVirtualFd(fd)); + const remove = async () => { + await handle.close(); + try { + await vfs.promises.unlink(path); + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } + }; return { __proto__: null, path: options?.encoding === 'buffer' ? Buffer.from(path) : path, - handle: createVfsFileHandle(getVirtualFd(fd)), + handle, + remove, + async [SymbolAsyncDispose]() { + await remove(); + }, }; }); }, diff --git a/test/parallel/test-fs-mkstemp-disposable.js b/test/parallel/test-fs-mkstemp-disposable.js new file mode 100644 index 00000000000..bf92035cb0e --- /dev/null +++ b/test/parallel/test-fs-mkstemp-disposable.js @@ -0,0 +1,251 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const fsPromises = require('fs/promises'); +const path = require('path'); +const { isMainThread } = require('worker_threads'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +// Sync basic usage +{ + const result = fs.mkstempSync(tmpdir.resolve('sync-remove.')); + + assert.strictEqual(path.basename(result.path).length, 'sync-remove.XXXXXX'.length); + assert.strictEqual(path.dirname(result.path), tmpdir.path); + assert.strictEqual(typeof result.fd, 'number'); + assert.strictEqual(typeof result.remove, 'function'); + assert.strictEqual(typeof result[Symbol.dispose], 'function'); + assert(fs.existsSync(result.path)); + + fs.writeSync(result.fd, 'hello'); + result.remove(); + + assert(!fs.existsSync(result.path)); + + // Second removal does not throw error (idempotent) + result.remove(); +} + +// Sync usage with [Symbol.dispose]() +{ + const result = fs.mkstempSync(tmpdir.resolve('sync-dispose.')); + + assert(fs.existsSync(result.path)); + + fs.writeSync(result.fd, 'hello'); + result[Symbol.dispose](); + + assert(!fs.existsSync(result.path)); + + // Second removal does not throw error (idempotent) + result[Symbol.dispose](); +} + +// Sync when fd is closed manually prior to disposal +{ + const result = fs.mkstempSync(tmpdir.resolve('sync-manual-close.')); + + assert(fs.existsSync(result.path)); + fs.closeSync(result.fd); + + result.remove(); + assert(!fs.existsSync(result.path)); +} + +// Sync with Buffer prefix +{ + const result = fs.mkstempSync(Buffer.from(tmpdir.resolve('sync-buffer.'))); + + assert(Buffer.isBuffer(result.path)); + assert(fs.existsSync(result.path.toString())); + + result.remove(); + assert(!fs.existsSync(result.path.toString())); +} + +// Sync `chdir` does not affect removal +if (isMainThread) { + const originalCwd = process.cwd(); + const otherDir = tmpdir.resolve('other-dir-sync'); + fs.mkdirSync(otherDir); + + process.chdir(tmpdir.path); + const first = fs.mkstempSync('first.'); + const second = fs.mkstempSync('second.'); + + const fullFirstPath = path.join(tmpdir.path, first.path); + const fullSecondPath = path.join(tmpdir.path, second.path); + + assert(fs.existsSync(fullFirstPath)); + assert(fs.existsSync(fullSecondPath)); + + process.chdir(otherDir); + second.remove(); + + assert(!fs.existsSync(fullSecondPath)); + + process.chdir(tmpdir.path); + first.remove(); + assert(!fs.existsSync(fullFirstPath)); + + process.chdir(originalCwd); +} + +// Callback basic usage +{ + fs.mkstemp(tmpdir.resolve('callback-remove.'), common.mustSucceed((result) => { + assert.strictEqual(typeof result.fd, 'number'); + assert.strictEqual(typeof result.remove, 'function'); + assert.strictEqual(typeof result[Symbol.dispose], 'function'); + assert(fs.existsSync(result.path)); + + fs.writeSync(result.fd, 'hello'); + result.remove(); + + assert(!fs.existsSync(result.path)); + + // Second removal does not throw error (idempotent) + result.remove(); + })); + + fs.mkstemp(tmpdir.resolve('callback-dispose.'), common.mustSucceed((result) => { + assert(fs.existsSync(result.path)); + + fs.writeSync(result.fd, 'hello'); + result[Symbol.dispose](); + + assert(!fs.existsSync(result.path)); + + result[Symbol.dispose](); + })); + + fs.mkstemp(tmpdir.resolve('callback-manual-close.'), common.mustSucceed((result) => { + assert(fs.existsSync(result.path)); + fs.closeSync(result.fd); + + result.remove(); + assert(!fs.existsSync(result.path)); + })); +} + +// Promises basic usage +async function promisesBasicUsage() { + const result = await fsPromises.mkstemp(tmpdir.resolve('promise-remove.')); + + assert.strictEqual(path.basename(result.path).length, 'promise-remove.XXXXXX'.length); + assert.strictEqual(path.dirname(result.path), tmpdir.path); + assert.ok(result.handle); + assert.strictEqual(typeof result.remove, 'function'); + assert.strictEqual(typeof result[Symbol.asyncDispose], 'function'); + assert(fs.existsSync(result.path)); + + await result.handle.writeFile('hello'); + await result.remove(); + + assert(!fs.existsSync(result.path)); + + // Second removal does not throw error (idempotent) + await result.remove(); +} + +// Promises usage with [Symbol.asyncDispose]() +async function promisesSymbolAsyncDispose() { + const result = await fsPromises.mkstemp(tmpdir.resolve('promise-dispose.')); + + assert(fs.existsSync(result.path)); + + await result.handle.writeFile('hello'); + await result[Symbol.asyncDispose](); + + assert(!fs.existsSync(result.path)); + + // Second removal does not throw error (idempotent) + await result[Symbol.asyncDispose](); +} + +// Promises when handle is closed manually prior to disposal +async function promisesManualClose() { + const result = await fsPromises.mkstemp(tmpdir.resolve('promise-manual-close.')); + + assert(fs.existsSync(result.path)); + await result.handle.close(); + + await result.remove(); + assert(!fs.existsSync(result.path)); +} + +// Promises with Buffer prefix +async function promisesBufferPrefix() { + const result = await fsPromises.mkstemp(Buffer.from(tmpdir.resolve('promise-buffer.'))); + + assert(Buffer.isBuffer(result.path)); + assert(fs.existsSync(result.path.toString())); + + await result.remove(); + assert(!fs.existsSync(result.path.toString())); +} + +// Promises `chdir` does not affect removal +async function promisesChdirDoesNotAffectRemoval() { + if (!isMainThread) return; + + const originalCwd = process.cwd(); + const otherDir = tmpdir.resolve('other-dir-promise'); + fs.mkdirSync(otherDir); + + process.chdir(tmpdir.path); + const first = await fsPromises.mkstemp('first-p.'); + const second = await fsPromises.mkstemp('second-p.'); + + const fullFirstPath = path.join(tmpdir.path, first.path); + const fullSecondPath = path.join(tmpdir.path, second.path); + + assert(fs.existsSync(fullFirstPath)); + assert(fs.existsSync(fullSecondPath)); + + process.chdir(otherDir); + await second.remove(); + + assert(!fs.existsSync(fullSecondPath)); + + process.chdir(tmpdir.path); + await first.remove(); + assert(!fs.existsSync(fullFirstPath)); + + process.chdir(originalCwd); +} + +// Errors from cleanup are thrown +// On Unix we can prevent removal by making the parent directory read-only +async function errorsAreReThrown() { + if (common.isWindows || process.getuid() === 0) return; + + const subDir = tmpdir.resolve('readonly-dir'); + fs.mkdirSync(subDir); + + const file = fs.mkstempSync(path.join(subDir, 'child.')); + fs.chmodSync(subDir, 0o444); + + assert.throws(() => { + file.remove(); + }, /EACCES|EPERM/); + + fs.chmodSync(subDir, 0o777); + file.remove(); + assert(!fs.existsSync(file.path)); + + fs.rmdirSync(subDir); +} + +(async () => { + await promisesBasicUsage(); + await promisesSymbolAsyncDispose(); + await promisesManualClose(); + await promisesBufferPrefix(); + await promisesChdirDoesNotAffectRemoval(); + await errorsAreReThrown(); +})().then(common.mustCall()); diff --git a/test/parallel/test-vfs-mkstemp.js b/test/parallel/test-vfs-mkstemp.js index 5dd805e3643..41719c40429 100644 --- a/test/parallel/test-vfs-mkstemp.js +++ b/test/parallel/test-vfs-mkstemp.js @@ -34,6 +34,17 @@ const vfs = require('node:vfs'); })); assert.rejects(myVfs.promises.mkstemp('/missing/prefix-'), { code: 'ENOENT' }) .then(common.mustCall()); + + // Test disposal on VirtualFileSystem + { + const file = myVfs.mkstempSync('/tmp-disp-'); + assert.strictEqual(typeof file.remove, 'function'); + assert.strictEqual(typeof file[Symbol.dispose], 'function'); + assert.ok(myVfs.statSync(file.path).isFile()); + file.remove(); + assert.throws(() => myVfs.statSync(file.path), { code: 'ENOENT' }); + file.remove(); // idempotent + } } // node:fs on a mounted path @@ -84,8 +95,32 @@ const vfs = require('node:vfs'); const result = await fs.promises.mkstemp(Buffer.from(prefix)); assert.ok(Buffer.isBuffer(result.path)); await result.handle.close(); + + const disposable = await fs.promises.mkstemp(prefix); + assert.strictEqual(typeof disposable.remove, 'function'); + assert.strictEqual(typeof disposable[Symbol.asyncDispose], 'function'); + assert.ok(fs.existsSync(disposable.path)); + await disposable.remove(); + assert.ok(!fs.existsSync(disposable.path)); + await disposable.remove(); // idempotent })().then(common.mustCall()); + { + const file = fs.mkstempSync(prefix); + assert.strictEqual(typeof file.remove, 'function'); + assert.strictEqual(typeof file[Symbol.dispose], 'function'); + assert.ok(fs.existsSync(file.path)); + file.remove(); + assert.ok(!fs.existsSync(file.path)); + file.remove(); // idempotent + } + + { + const file = fs.mkstempSync(prefix); + file[Symbol.dispose](); + assert.ok(!fs.existsSync(file.path)); + } + const missing = path.join(dir, 'missing', 'f-'); assert.throws(() => fs.mkstempSync(missing), { code: 'ENOENT' }); fs.mkstemp(missing, common.expectsError({ code: 'ENOENT' }));