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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/js/node/fs.promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,21 @@ const exports = {
lstat: asyncWrap(fs.lstat, "lstat"),
mkdir: asyncWrap(fs.mkdir, "mkdir"),
mkdtemp: asyncWrap(fs.mkdtemp, "mkdtemp"),
mkdtempDisposable: async function mkdtempDisposable(prefix, options) {
// Capture cwd at creation time so process.chdir() between creation and
// disposal cannot misdirect the recursive rm.
const cwd = process.cwd();
const folder = await fs.mkdtemp(prefix, options);
const fullPath = require("node:path").resolve(cwd, folder);
const remove = () => fs.rm(fullPath, { recursive: true, force: true });
return {
path: folder,
remove,
async [Symbol.asyncDispose]() {
await remove();
},
};
},
statfs: asyncWrap(fs.statfs, "statfs"),
open: async (path, flags = "r", mode = 0o666) => {
return new private_symbols.FileHandle(await fs.open(path, flags, mode), flags);
Expand Down
18 changes: 18 additions & 0 deletions src/js/node/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,22 @@ var access = function access(path, mode, callback) {
lstatSync = fs.lstatSync.bind(fs) as unknown as typeof import("node:fs").lstatSync,
mkdirSync = fs.mkdirSync.bind(fs) as unknown as typeof import("node:fs").mkdirSync,
mkdtempSync = fs.mkdtempSync.bind(fs) as unknown as typeof import("node:fs").mkdtempSync,
mkdtempDisposableSync = function mkdtempDisposableSync(prefix, options) {
const folder = fs.mkdtempSync(prefix, options);
// Capture cwd at creation time so process.chdir() between creation and
// disposal cannot misdirect the recursive rm.
const fullPath = require("node:path").resolve(process.cwd(), folder);
const remove = () => {
rmSync(fullPath, { recursive: true, force: true });
};
return {
path: folder,
remove,
[Symbol.dispose]() {
remove();
},
};
},
openSync = fs.openSync.bind(fs) as unknown as typeof import("node:fs").openSync,
readSync = function readSync(fd, buffer, offsetOrOptions, length, position) {
let offset = offsetOrOptions;
Expand Down Expand Up @@ -1199,6 +1215,7 @@ var exports = {
mkdir,
mkdirSync,
mkdtemp,
mkdtempDisposableSync,
mkdtempSync,
open,
openSync,
Expand Down Expand Up @@ -1349,6 +1366,7 @@ setName(lutimesSync, "lutimesSync");
setName(mkdir, "mkdir");
setName(mkdirSync, "mkdirSync");
setName(mkdtemp, "mkdtemp");
setName(mkdtempDisposableSync, "mkdtempDisposableSync");
setName(mkdtempSync, "mkdtempSync");
setName(open, "open");
setName(openSync, "openSync");
Expand Down
99 changes: 99 additions & 0 deletions test/js/node/fs/fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import fs, {
lstatSync,
mkdirSync,
mkdtemp,
mkdtempDisposableSync,
mkdtempSync,
openAsBlob,
openSync,
Expand Down Expand Up @@ -4043,3 +4044,101 @@ describe.skipIf(isWindows)("readFileSync on a FIFO larger than the stat size", (
expect(exitCode).toBe(0);
});
});

describe("mkdtempDisposableSync", () => {
it("creates a directory and removes it via Symbol.dispose", () => {
const result = mkdtempDisposableSync(join(tmpdirSync(), "disposable-"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace tmpdirSync() with tempDir(...) in these new tests.

These new cases create temp roots with tmpdirSync(), which conflicts with the test harness rule and loses disposable cleanup ergonomics.

Suggested pattern
- const result = mkdtempDisposableSync(join(tmpdirSync(), "disposable-"));
+ using base = tempDir("mkdtemp-disposable-sync", {});
+ const result = mkdtempDisposableSync(join(String(base), "disposable-"));
- const result = await _promises.mkdtempDisposable(join(tmpdirSync(), "disposable-"));
+ using base = tempDir("mkdtemp-disposable-async", {});
+ const result = await _promises.mkdtempDisposable(join(String(base), "disposable-"));

As per coding guidelines: "Use tempDir from 'harness' to create temporary directories - do not use tmpdirSync or fs.mkdtempSync."

Also applies to: 4046-4046, 4053-4053, 4061-4061, 4069-4069, 4079-4079, 4096-4096, 4106-4106, 4114-4114, 4121-4121

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/node/fs/fs.test.ts` at line 4038, Replace uses of tmpdirSync() with
the harness helper tempDir(...) in these tests so the created temp roots are
tracked and auto-cleaned; specifically update calls like
mkdtempDisposableSync(join(tmpdirSync(), "disposable-")) to instead call
tempDir("disposable-") (or wrap tempDir().path as needed) and ensure you import
tempDir from the test harness if not present. Apply the same replacement for all
similar occurrences (references to tmpdirSync in the block around
mkdtempDisposableSync and other mkdtemp* usages) so temp directories are created
via tempDir and the disposable cleanup semantics are preserved.

expect(typeof result.path).toBe("string");
expect(existsSync(result.path)).toBe(true);
result[Symbol.dispose]();
expect(existsSync(result.path)).toBe(false);
});

it("calling dispose twice is safe", () => {
const result = mkdtempDisposableSync(join(tmpdirSync(), "disposable-"));
result[Symbol.dispose]();
result[Symbol.dispose]();
expect(existsSync(result.path)).toBe(false);
});

it("remove() works as a manual alternative to dispose", () => {
const result = mkdtempDisposableSync(join(tmpdirSync(), "disposable-"));
result.remove();
expect(existsSync(result.path)).toBe(false);
});

it("works with `using` syntax", () => {
let savedPath: string;
{
using temp = mkdtempDisposableSync(join(tmpdirSync(), "disposable-"));
savedPath = temp.path;
expect(existsSync(temp.path)).toBe(true);
}
expect(existsSync(savedPath!)).toBe(false);
});

it("removes a non-empty directory recursively", () => {
const result = mkdtempDisposableSync(join(tmpdirSync(), "disposable-"));
writeFileSync(join(result.path, "nested.txt"), "hello");
mkdirSync(join(result.path, "sub"));
writeFileSync(join(result.path, "sub", "deep.txt"), "world");
result[Symbol.dispose]();
expect(existsSync(result.path)).toBe(false);
});

it("survives process.chdir between create and dispose", () => {
const originalCwd = process.cwd();
const base = tmpdirSync();
process.chdir(base);
try {
const result = mkdtempDisposableSync("./disposable-");
const absolute = path.resolve(base, result.path);
expect(existsSync(absolute)).toBe(true);
process.chdir(os.tmpdir());
result[Symbol.dispose]();
expect(existsSync(absolute)).toBe(false);
} finally {
process.chdir(originalCwd);
}
});
});

describe("fs.promises.mkdtempDisposable", () => {
it("creates a directory and removes it via Symbol.asyncDispose", async () => {
const result = await _promises.mkdtempDisposable(join(tmpdirSync(), "disposable-"));
expect(typeof result.path).toBe("string");
expect(existsSync(result.path)).toBe(true);
await result[Symbol.asyncDispose]();
expect(existsSync(result.path)).toBe(false);
});

it("works with `await using` syntax", async () => {
let savedPath: string;
{
await using temp = await _promises.mkdtempDisposable(join(tmpdirSync(), "disposable-"));
savedPath = temp.path;
expect(existsSync(temp.path)).toBe(true);
}
expect(existsSync(savedPath!)).toBe(false);
});

it("calling asyncDispose twice is safe", async () => {
const result = await _promises.mkdtempDisposable(join(tmpdirSync(), "disposable-"));
await result[Symbol.asyncDispose]();
await result[Symbol.asyncDispose]();
expect(existsSync(result.path)).toBe(false);
});

it("remove() returns a Promise that resolves", async () => {
const result = await _promises.mkdtempDisposable(join(tmpdirSync(), "disposable-"));
await result.remove();
expect(existsSync(result.path)).toBe(false);
});

it("is also reachable via the node:fs `promises` namespace", async () => {
const result = await promises.mkdtempDisposable(join(tmpdirSync(), "disposable-"));
expect(existsSync(result.path)).toBe(true);
await result[Symbol.asyncDispose]();
expect(existsSync(result.path)).toBe(false);
});
});
Comment on lines +4106 to +4144

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a process.chdir() regression test for the async variant too.

The sync suite verifies cwd drift safety, but the async suite doesn’t. Add one equivalent async case to lock in the captured-cwd cleanup behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/node/fs/fs.test.ts` around lines 4094 - 4132, Add an async regression
test to ensure mkdtempDisposable does not suffer cwd drift by using
process.chdir around the async disposable - inside the
"fs.promises.mkdtempDisposable" describe add a test similar to the sync case
that calls process.chdir(...) to a different directory, then await
_promises.mkdtempDisposable(...) (or promises.mkdtempDisposable(...)), capture
the returned result.path, change the cwd back, call await
result[Symbol.asyncDispose]() (or result.remove()), and assert that the
process.cwd() is unchanged and the temporary directory has been removed
(existsSync(result.path) is false); reference _promises.mkdtempDisposable,
promises.mkdtempDisposable, Symbol.asyncDispose and remove() to locate the code.