From 5bed06fb94214db70a27cca8fa8eff717d537dba Mon Sep 17 00:00:00 2001
From: ali ahmed <48116123+AliBasicCoder@users.noreply.github.com>
Date: Wed, 14 Oct 2020 17:59:28 +0200
Subject: [PATCH] feat(std/fs/node): adding some functions (#7921)

---
 std/node/_fs/_fs_lstat.ts        |  59 +++++++
 std/node/_fs/_fs_lstat_test.ts   |  56 ++++++
 std/node/_fs/_fs_open.ts         | 103 +++++++++++
 std/node/_fs/_fs_open_test.ts    | 209 ++++++++++++++++++++++
 std/node/_fs/_fs_readdir.ts      | 117 +++++++++++++
 std/node/_fs/_fs_readdir_test.ts |  71 ++++++++
 std/node/_fs/_fs_rename.ts       |  23 +++
 std/node/_fs/_fs_rename_test.ts  |  38 ++++
 std/node/_fs/_fs_rmdir.ts        |  36 ++++
 std/node/_fs/_fs_rmdir_test.ts   |  88 ++++++++++
 std/node/_fs/_fs_stat.ts         | 289 +++++++++++++++++++++++++++++++
 std/node/_fs/_fs_stat_test.ts    | 107 ++++++++++++
 std/node/_fs/_fs_unlink.ts       |  10 ++
 std/node/_fs/_fs_unlink_test.ts  |  30 ++++
 std/node/_fs/_fs_watch.ts        | 111 ++++++++++++
 std/node/_fs/_fs_watch_test.ts   |  32 ++++
 std/node/fs.ts                   |  24 +++
 17 files changed, 1403 insertions(+)
 create mode 100644 std/node/_fs/_fs_lstat.ts
 create mode 100644 std/node/_fs/_fs_lstat_test.ts
 create mode 100644 std/node/_fs/_fs_open.ts
 create mode 100644 std/node/_fs/_fs_open_test.ts
 create mode 100644 std/node/_fs/_fs_readdir.ts
 create mode 100644 std/node/_fs/_fs_readdir_test.ts
 create mode 100644 std/node/_fs/_fs_rename.ts
 create mode 100644 std/node/_fs/_fs_rename_test.ts
 create mode 100644 std/node/_fs/_fs_rmdir.ts
 create mode 100644 std/node/_fs/_fs_rmdir_test.ts
 create mode 100644 std/node/_fs/_fs_stat.ts
 create mode 100644 std/node/_fs/_fs_stat_test.ts
 create mode 100644 std/node/_fs/_fs_unlink.ts
 create mode 100644 std/node/_fs/_fs_unlink_test.ts
 create mode 100644 std/node/_fs/_fs_watch.ts
 create mode 100644 std/node/_fs/_fs_watch_test.ts

diff --git a/std/node/_fs/_fs_lstat.ts b/std/node/_fs/_fs_lstat.ts
new file mode 100644
index 0000000000..0b79fb665d
--- /dev/null
+++ b/std/node/_fs/_fs_lstat.ts
@@ -0,0 +1,59 @@
+import {
+  BigIntStats,
+  CFISBIS,
+  statCallback,
+  statCallbackBigInt,
+  statOptions,
+  Stats,
+} from "./_fs_stat.ts";
+
+export function lstat(path: string | URL, callback: statCallback): void;
+export function lstat(
+  path: string | URL,
+  options: { bigint: false },
+  callback: statCallback,
+): void;
+export function lstat(
+  path: string | URL,
+  options: { bigint: true },
+  callback: statCallbackBigInt,
+): void;
+export function lstat(
+  path: string | URL,
+  optionsOrCallback: statCallback | statCallbackBigInt | statOptions,
+  maybeCallback?: statCallback | statCallbackBigInt,
+) {
+  const callback =
+    (typeof optionsOrCallback === "function"
+      ? optionsOrCallback
+      : maybeCallback) as (
+        err: Error | undefined,
+        stat: BigIntStats | Stats,
+      ) => void;
+  const options = typeof optionsOrCallback === "object"
+    ? optionsOrCallback
+    : { bigint: false };
+
+  if (!callback) throw new Error("No callback function supplied");
+
+  Deno.lstat(path)
+    .then((stat) => callback(undefined, CFISBIS(stat, options.bigint)))
+    .catch((err) => callback(err, err));
+}
+
+export function lstatSync(path: string | URL): Stats;
+export function lstatSync(
+  path: string | URL,
+  options: { bigint: false },
+): Stats;
+export function lstatSync(
+  path: string | URL,
+  options: { bigint: true },
+): BigIntStats;
+export function lstatSync(
+  path: string | URL,
+  options?: statOptions,
+): Stats | BigIntStats {
+  const origin = Deno.lstatSync(path);
+  return CFISBIS(origin, options?.bigint || false);
+}
diff --git a/std/node/_fs/_fs_lstat_test.ts b/std/node/_fs/_fs_lstat_test.ts
new file mode 100644
index 0000000000..1da0a562f4
--- /dev/null
+++ b/std/node/_fs/_fs_lstat_test.ts
@@ -0,0 +1,56 @@
+import { lstat, lstatSync } from "./_fs_lstat.ts";
+import { fail } from "../../testing/asserts.ts";
+import { assertStats, assertStatsBigInt } from "./_fs_stat_test.ts";
+import type { BigIntStats, Stats } from "./_fs_stat.ts";
+
+Deno.test({
+  name: "ASYNC: get a file Stats (lstat)",
+  async fn() {
+    const file = Deno.makeTempFileSync();
+    await new Promise<Stats>((resolve, reject) => {
+      lstat(file, (err, stat) => {
+        if (err) reject(err);
+        resolve(stat);
+      });
+    })
+      .then((stat) => {
+        assertStats(stat, Deno.lstatSync(file));
+      })
+      .catch(() => fail())
+      .finally(() => {
+        Deno.removeSync(file);
+      });
+  },
+});
+
+Deno.test({
+  name: "SYNC: get a file Stats (lstat)",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    assertStats(lstatSync(file), Deno.lstatSync(file));
+  },
+});
+
+Deno.test({
+  name: "ASYNC: get a file BigInt Stats (lstat)",
+  async fn() {
+    const file = Deno.makeTempFileSync();
+    await new Promise<BigIntStats>((resolve, reject) => {
+      lstat(file, { bigint: true }, (err, stat) => {
+        if (err) reject(err);
+        resolve(stat);
+      });
+    })
+      .then((stat) => assertStatsBigInt(stat, Deno.lstatSync(file)))
+      .catch(() => fail())
+      .finally(() => Deno.removeSync(file));
+  },
+});
+
+Deno.test({
+  name: "SYNC: BigInt Stats (lstat)",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    assertStatsBigInt(lstatSync(file, { bigint: true }), Deno.lstatSync(file));
+  },
+});
diff --git a/std/node/_fs/_fs_open.ts b/std/node/_fs/_fs_open.ts
new file mode 100644
index 0000000000..bf53115de6
--- /dev/null
+++ b/std/node/_fs/_fs_open.ts
@@ -0,0 +1,103 @@
+import { existsSync } from "../../fs/mod.ts";
+import { fromFileUrl } from "../path.ts";
+import { getOpenOptions } from "./_fs_common.ts";
+
+type openFlags =
+  | "a"
+  | "ax"
+  | "a+"
+  | "ax+"
+  | "as"
+  | "as+"
+  | "r"
+  | "r+"
+  | "rs+"
+  | "w"
+  | "wx"
+  | "w+"
+  | "wx+";
+
+type openCallback = (err: Error | undefined, fd: number) => void;
+
+function convertFlagAndModeToOptions(
+  flag?: openFlags,
+  mode?: number,
+): Deno.OpenOptions | undefined {
+  if (!flag && !mode) return undefined;
+  if (!flag && mode) return { mode };
+  return { ...getOpenOptions(flag), mode };
+}
+
+export function open(path: string | URL, callback: openCallback): void;
+export function open(
+  path: string | URL,
+  flags: openFlags,
+  callback: openCallback,
+): void;
+export function open(
+  path: string | URL,
+  flags: openFlags,
+  mode: number,
+  callback: openCallback,
+): void;
+export function open(
+  path: string | URL,
+  flagsOrCallback: openCallback | openFlags,
+  callbackOrMode?: openCallback | number,
+  maybeCallback?: openCallback,
+) {
+  const flags = typeof flagsOrCallback === "string"
+    ? flagsOrCallback
+    : undefined;
+  const callback = typeof flagsOrCallback === "function"
+    ? flagsOrCallback
+    : typeof callbackOrMode === "function"
+    ? callbackOrMode
+    : maybeCallback;
+  const mode = typeof callbackOrMode === "number" ? callbackOrMode : undefined;
+  path = path instanceof URL ? fromFileUrl(path) : path;
+
+  if (!callback) throw new Error("No callback function supplied");
+
+  if (["ax", "ax+", "wx", "wx+"].includes(flags || "") && existsSync(path)) {
+    const err = new Error(`EEXIST: file already exists, open '${path}'`);
+    callback(err, 0);
+  } else {
+    if (flags === "as" || flags === "as+") {
+      try {
+        const res = openSync(path, flags, mode);
+        callback(undefined, res);
+      } catch (error) {
+        callback(error, error);
+      }
+      return;
+    }
+    Deno.open(path, convertFlagAndModeToOptions(flags, mode))
+      .then((file) => callback(undefined, file.rid))
+      .catch((err) => callback(err, err));
+  }
+}
+
+export function openSync(path: string | URL): number;
+export function openSync(path: string | URL, flags?: openFlags): number;
+export function openSync(path: string | URL, mode?: number): number;
+export function openSync(
+  path: string | URL,
+  flags?: openFlags,
+  mode?: number,
+): number;
+export function openSync(
+  path: string | URL,
+  flagsOrMode?: openFlags | number,
+  maybeMode?: number,
+) {
+  const flags = typeof flagsOrMode === "string" ? flagsOrMode : undefined;
+  const mode = typeof flagsOrMode === "number" ? flagsOrMode : maybeMode;
+  path = path instanceof URL ? fromFileUrl(path) : path;
+
+  if (["ax", "ax+", "wx", "wx+"].includes(flags || "") && existsSync(path)) {
+    throw new Error(`EEXIST: file already exists, open '${path}'`);
+  }
+
+  return Deno.openSync(path, convertFlagAndModeToOptions(flags, mode)).rid;
+}
diff --git a/std/node/_fs/_fs_open_test.ts b/std/node/_fs/_fs_open_test.ts
new file mode 100644
index 0000000000..4cbd58d023
--- /dev/null
+++ b/std/node/_fs/_fs_open_test.ts
@@ -0,0 +1,209 @@
+import {
+  assert,
+  assertEquals,
+  assertThrows,
+  fail,
+} from "../../testing/asserts.ts";
+import { open, openSync } from "./_fs_open.ts";
+import { join, parse } from "../../path/mod.ts";
+import { existsSync } from "../../fs/mod.ts";
+import { closeSync } from "./_fs_close.ts";
+
+const temp_dir = parse(Deno.makeTempFileSync()).dir;
+
+Deno.test({
+  name: "ASYNC: open file",
+  async fn() {
+    const file = Deno.makeTempFileSync();
+    let fd1: number;
+    await new Promise<number>((resolve, reject) => {
+      open(file, (err, fd) => {
+        if (err) reject(err);
+        resolve(fd);
+      });
+    })
+      .then((fd) => {
+        fd1 = fd;
+        assert(Deno.resources()[fd], `${fd}`);
+      })
+      .catch(() => fail())
+      .finally(() => closeSync(fd1));
+  },
+});
+
+Deno.test({
+  name: "SYNC: open file",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    const fd = openSync(file);
+    assert(Deno.resources()[fd]);
+    closeSync(fd);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'a'",
+  fn() {
+    const file = join(temp_dir, "some_random_file");
+    const fd = openSync(file, "a");
+    assertEquals(typeof fd, "number");
+    assertEquals(existsSync(file), true);
+    assert(Deno.resources()[fd]);
+    closeSync(fd);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'ax'",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    assertThrows(
+      () => {
+        openSync(file, "ax");
+      },
+      Error,
+      `EEXIST: file already exists, open '${file}'`,
+    );
+    Deno.removeSync(file);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'a+'",
+  fn() {
+    const file = join(temp_dir, "some_random_file2");
+    const fd = openSync(file, "a+");
+    assertEquals(typeof fd, "number");
+    assertEquals(existsSync(file), true);
+    closeSync(fd);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'ax+'",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    assertThrows(
+      () => {
+        openSync(file, "ax+");
+      },
+      Error,
+      `EEXIST: file already exists, open '${file}'`,
+    );
+    Deno.removeSync(file);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'as'",
+  fn() {
+    const file = join(temp_dir, "some_random_file10");
+    const fd = openSync(file, "as");
+    assertEquals(existsSync(file), true);
+    assertEquals(typeof fd, "number");
+    closeSync(fd);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'as+'",
+  fn() {
+    const file = join(temp_dir, "some_random_file10");
+    const fd = openSync(file, "as+");
+    assertEquals(existsSync(file), true);
+    assertEquals(typeof fd, "number");
+    closeSync(fd);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'r'",
+  fn() {
+    const file = join(temp_dir, "some_random_file3");
+    assertThrows(() => {
+      openSync(file, "r");
+    }, Error);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'r+'",
+  fn() {
+    const file = join(temp_dir, "some_random_file4");
+    assertThrows(() => {
+      openSync(file, "r+");
+    }, Error);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'w'",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    Deno.writeTextFileSync(file, "hi there");
+    const fd = openSync(file, "w");
+    assertEquals(typeof fd, "number");
+    assertEquals(Deno.readTextFileSync(file), "");
+    closeSync(fd);
+
+    const file2 = join(temp_dir, "some_random_file5");
+    const fd2 = openSync(file2, "w");
+    assertEquals(typeof fd2, "number");
+    assertEquals(existsSync(file2), true);
+    closeSync(fd2);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'wx'",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    Deno.writeTextFileSync(file, "hi there");
+    const fd = openSync(file, "w");
+    assertEquals(typeof fd, "number");
+    assertEquals(Deno.readTextFileSync(file), "");
+    closeSync(fd);
+
+    const file2 = Deno.makeTempFileSync();
+    assertThrows(
+      () => {
+        openSync(file2, "wx");
+      },
+      Error,
+      `EEXIST: file already exists, open '${file2}'`,
+    );
+  },
+});
+
+Deno.test({
+  name: "open with flag 'w+'",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    Deno.writeTextFileSync(file, "hi there");
+    const fd = openSync(file, "w+");
+    assertEquals(typeof fd, "number");
+    assertEquals(Deno.readTextFileSync(file), "");
+    closeSync(fd);
+
+    const file2 = join(temp_dir, "some_random_file6");
+    const fd2 = openSync(file2, "w+");
+    assertEquals(typeof fd2, "number");
+    assertEquals(existsSync(file2), true);
+    closeSync(fd2);
+  },
+});
+
+Deno.test({
+  name: "open with flag 'wx+'",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    assertThrows(
+      () => {
+        openSync(file, "wx+");
+      },
+      Error,
+      `EEXIST: file already exists, open '${file}'`,
+    );
+    Deno.removeSync(file);
+  },
+});
diff --git a/std/node/_fs/_fs_readdir.ts b/std/node/_fs/_fs_readdir.ts
new file mode 100644
index 0000000000..9034eccf82
--- /dev/null
+++ b/std/node/_fs/_fs_readdir.ts
@@ -0,0 +1,117 @@
+import { asyncIterableToCallback } from "./_fs_watch.ts";
+import Dirent from "./_fs_dirent.ts";
+import { fromFileUrl } from "../path.ts";
+
+function toDirent(val: Deno.DirEntry): Dirent {
+  return new Dirent(val);
+}
+
+type readDirOptions = {
+  encoding?: string;
+  withFileTypes?: boolean;
+};
+
+type readDirCallback = (err: Error | undefined, files: string[]) => void;
+
+type readDirCallbackDirent = (err: Error | undefined, files: Dirent[]) => void;
+
+type readDirBoth = (
+  err: Error | undefined,
+  files: string[] | Dirent[] | Array<string | Dirent>,
+) => void;
+
+export function readdir(
+  path: string | URL,
+  options: { withFileTypes?: false; encoding?: string },
+  callback: readDirCallback,
+): void;
+export function readdir(
+  path: string | URL,
+  options: { withFileTypes: true; encoding?: string },
+  callback: readDirCallbackDirent,
+): void;
+export function readdir(path: string | URL, callback: readDirCallback): void;
+export function readdir(
+  path: string | URL,
+  optionsOrCallback: readDirOptions | readDirCallback | readDirCallbackDirent,
+  maybeCallback?: readDirCallback | readDirCallbackDirent,
+) {
+  const callback =
+    (typeof optionsOrCallback === "function"
+      ? optionsOrCallback
+      : maybeCallback) as readDirBoth | undefined;
+  const options = typeof optionsOrCallback === "object"
+    ? optionsOrCallback
+    : null;
+  const result: Array<string | Dirent> = [];
+  path = path instanceof URL ? fromFileUrl(path) : path;
+
+  if (!callback) throw new Error("No callback function supplied");
+
+  if (options?.encoding) {
+    try {
+      new TextDecoder(options.encoding);
+    } catch (error) {
+      throw new Error(
+        `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`,
+      );
+    }
+  }
+
+  try {
+    asyncIterableToCallback(Deno.readDir(path), (val, done) => {
+      if (typeof path !== "string") return;
+      if (done) {
+        callback(undefined, result);
+        return;
+      }
+      if (options?.withFileTypes) {
+        result.push(toDirent(val));
+      } else result.push(decode(val.name));
+    });
+  } catch (error) {
+    callback(error, result);
+  }
+}
+
+function decode(str: string, encoding?: string): string {
+  if (!encoding) return str;
+  else {
+    const decoder = new TextDecoder(encoding);
+    const encoder = new TextEncoder();
+    return decoder.decode(encoder.encode(str));
+  }
+}
+
+export function readdirSync(
+  path: string | URL,
+  options: { withFileTypes: true; encoding?: string },
+): Dirent[];
+export function readdirSync(
+  path: string | URL,
+  options?: { withFileTypes?: false; encoding?: string },
+): string[];
+export function readdirSync(
+  path: string | URL,
+  options?: readDirOptions,
+): Array<string | Dirent> {
+  const result = [];
+  path = path instanceof URL ? fromFileUrl(path) : path;
+
+  if (options?.encoding) {
+    try {
+      new TextDecoder(options.encoding);
+    } catch (error) {
+      throw new Error(
+        `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`,
+      );
+    }
+  }
+
+  for (const file of Deno.readDirSync(path)) {
+    if (options?.withFileTypes) {
+      result.push(toDirent(file));
+    } else result.push(decode(file.name));
+  }
+  return result;
+}
diff --git a/std/node/_fs/_fs_readdir_test.ts b/std/node/_fs/_fs_readdir_test.ts
new file mode 100644
index 0000000000..169e102455
--- /dev/null
+++ b/std/node/_fs/_fs_readdir_test.ts
@@ -0,0 +1,71 @@
+import { assertEquals, assertNotEquals, fail } from "../../testing/asserts.ts";
+import { readdir, readdirSync } from "./_fs_readdir.ts";
+import { join } from "../../path/mod.ts";
+
+Deno.test({
+  name: "ASYNC: reading empty directory",
+  async fn() {
+    const dir = Deno.makeTempDirSync();
+    await new Promise<string[]>((resolve, reject) => {
+      readdir(dir, (err, files) => {
+        if (err) reject(err);
+        resolve(files);
+      });
+    })
+      .then((files) => assertEquals(files, []))
+      .catch(() => fail())
+      .finally(() => Deno.removeSync(dir));
+  },
+});
+
+function assertEqualsArrayAnyOrder<T>(actual: T[], expected: T[]) {
+  assertEquals(actual.length, expected.length);
+  for (const item of expected) {
+    const index = actual.indexOf(item);
+    assertNotEquals(index, -1);
+    expected = expected.splice(index, 1);
+  }
+}
+
+Deno.test({
+  name: "ASYNC: reading non-empty directory",
+  async fn() {
+    const dir = Deno.makeTempDirSync();
+    Deno.writeTextFileSync(join(dir, "file1.txt"), "hi");
+    Deno.writeTextFileSync(join(dir, "file2.txt"), "hi");
+    Deno.mkdirSync(join(dir, "some_dir"));
+    await new Promise<string[]>((resolve, reject) => {
+      readdir(dir, (err, files) => {
+        if (err) reject(err);
+        resolve(files);
+      });
+    })
+      .then((files) =>
+        assertEqualsArrayAnyOrder(files, ["file1.txt", "some_dir", "file2.txt"])
+      )
+      .catch(() => fail())
+      .finally(() => Deno.removeSync(dir, { recursive: true }));
+  },
+});
+
+Deno.test({
+  name: "SYNC: reading empty the directory",
+  fn() {
+    const dir = Deno.makeTempDirSync();
+    assertEquals(readdirSync(dir), []);
+  },
+});
+
+Deno.test({
+  name: "SYNC: reading non-empty directory",
+  fn() {
+    const dir = Deno.makeTempDirSync();
+    Deno.writeTextFileSync(join(dir, "file1.txt"), "hi");
+    Deno.writeTextFileSync(join(dir, "file2.txt"), "hi");
+    Deno.mkdirSync(join(dir, "some_dir"));
+    assertEqualsArrayAnyOrder(
+      readdirSync(dir),
+      ["file1.txt", "some_dir", "file2.txt"],
+    );
+  },
+});
diff --git a/std/node/_fs/_fs_rename.ts b/std/node/_fs/_fs_rename.ts
new file mode 100644
index 0000000000..ee7c009770
--- /dev/null
+++ b/std/node/_fs/_fs_rename.ts
@@ -0,0 +1,23 @@
+import { fromFileUrl } from "../path.ts";
+
+export function rename(
+  oldPath: string | URL,
+  newPath: string | URL,
+  callback: (err?: Error) => void,
+) {
+  oldPath = oldPath instanceof URL ? fromFileUrl(oldPath) : oldPath;
+  newPath = newPath instanceof URL ? fromFileUrl(newPath) : newPath;
+
+  if (!callback) throw new Error("No callback function supplied");
+
+  Deno.rename(oldPath, newPath)
+    .then((_) => callback())
+    .catch(callback);
+}
+
+export function renameSync(oldPath: string | URL, newPath: string | URL) {
+  oldPath = oldPath instanceof URL ? fromFileUrl(oldPath) : oldPath;
+  newPath = newPath instanceof URL ? fromFileUrl(newPath) : newPath;
+
+  Deno.renameSync(oldPath, newPath);
+}
diff --git a/std/node/_fs/_fs_rename_test.ts b/std/node/_fs/_fs_rename_test.ts
new file mode 100644
index 0000000000..d0084d0d12
--- /dev/null
+++ b/std/node/_fs/_fs_rename_test.ts
@@ -0,0 +1,38 @@
+import { assertEquals, fail } from "../../testing/asserts.ts";
+import { rename, renameSync } from "./_fs_rename.ts";
+import { existsSync } from "../../fs/mod.ts";
+import { join, parse } from "../../path/mod.ts";
+
+Deno.test({
+  name: "ASYNC: renaming a file",
+  async fn() {
+    const file = Deno.makeTempFileSync();
+    const newPath = join(parse(file).dir, `${parse(file).base}_renamed`);
+    await new Promise((resolve, reject) => {
+      rename(file, newPath, (err) => {
+        if (err) reject(err);
+        resolve();
+      });
+    })
+      .then(() => {
+        assertEquals(existsSync(newPath), true);
+        assertEquals(existsSync(file), false);
+      })
+      .catch(() => fail())
+      .finally(() => {
+        if (existsSync(file)) Deno.removeSync(file);
+        if (existsSync(newPath)) Deno.removeSync(newPath);
+      });
+  },
+});
+
+Deno.test({
+  name: "SYNC: renaming a file",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    const newPath = join(parse(file).dir, `${parse(file).base}_renamed`);
+    renameSync(file, newPath);
+    assertEquals(existsSync(newPath), true);
+    assertEquals(existsSync(file), false);
+  },
+});
diff --git a/std/node/_fs/_fs_rmdir.ts b/std/node/_fs/_fs_rmdir.ts
new file mode 100644
index 0000000000..2035a1e717
--- /dev/null
+++ b/std/node/_fs/_fs_rmdir.ts
@@ -0,0 +1,36 @@
+type rmdirOptions = {
+  maxRetries?: number;
+  recursive?: boolean;
+  retryDelay?: number;
+};
+
+type rmdirCallback = (err?: Error) => void;
+
+export function rmdir(path: string | URL, callback: rmdirCallback): void;
+export function rmdir(
+  path: string | URL,
+  options: rmdirOptions,
+  callback: rmdirCallback,
+): void;
+export function rmdir(
+  path: string | URL,
+  optionsOrCallback: rmdirOptions | rmdirCallback,
+  maybeCallback?: rmdirCallback,
+) {
+  const callback = typeof optionsOrCallback === "function"
+    ? optionsOrCallback
+    : maybeCallback;
+  const options = typeof optionsOrCallback === "object"
+    ? optionsOrCallback
+    : undefined;
+
+  if (!callback) throw new Error("No callback function supplied");
+
+  Deno.remove(path, { recursive: options?.recursive })
+    .then((_) => callback())
+    .catch(callback);
+}
+
+export function rmdirSync(path: string | URL, options?: rmdirOptions) {
+  Deno.removeSync(path, { recursive: options?.recursive });
+}
diff --git a/std/node/_fs/_fs_rmdir_test.ts b/std/node/_fs/_fs_rmdir_test.ts
new file mode 100644
index 0000000000..884d4912ac
--- /dev/null
+++ b/std/node/_fs/_fs_rmdir_test.ts
@@ -0,0 +1,88 @@
+import { assertEquals, fail } from "../../testing/asserts.ts";
+import { rmdir, rmdirSync } from "./_fs_rmdir.ts";
+import { closeSync } from "./_fs_close.ts";
+import { existsSync } from "../../fs/mod.ts";
+import { join } from "../../path/mod.ts";
+
+Deno.test({
+  name: "ASYNC: removing empty folder",
+  async fn() {
+    const dir = Deno.makeTempDirSync();
+    await new Promise((resolve, reject) => {
+      rmdir(dir, (err) => {
+        if (err) reject(err);
+        resolve();
+      });
+    })
+      .then(() => assertEquals(existsSync(dir), false))
+      .catch(() => fail())
+      .finally(() => {
+        if (existsSync(dir)) Deno.removeSync(dir);
+      });
+  },
+});
+
+Deno.test({
+  name: "SYNC: removing empty folder",
+  fn() {
+    const dir = Deno.makeTempDirSync();
+    rmdirSync(dir);
+    assertEquals(existsSync(dir), false);
+  },
+});
+
+function closeRes(before: Deno.ResourceMap, after: Deno.ResourceMap) {
+  for (const key in after) {
+    if (!before[key]) {
+      try {
+        closeSync(Number(key));
+      } catch (error) {
+        return error;
+      }
+    }
+  }
+}
+
+Deno.test({
+  name: "ASYNC: removing non-empty folder",
+  async fn() {
+    const rBefore = Deno.resources();
+    const dir = Deno.makeTempDirSync();
+    Deno.createSync(join(dir, "file1.txt"));
+    Deno.createSync(join(dir, "file2.txt"));
+    Deno.mkdirSync(join(dir, "some_dir"));
+    Deno.createSync(join(dir, "some_dir", "file.txt"));
+    await new Promise((resolve, reject) => {
+      rmdir(dir, { recursive: true }, (err) => {
+        if (err) reject(err);
+        resolve();
+      });
+    })
+      .then(() => assertEquals(existsSync(dir), false))
+      .catch(() => fail())
+      .finally(() => {
+        if (existsSync(dir)) Deno.removeSync(dir, { recursive: true });
+        const rAfter = Deno.resources();
+        closeRes(rBefore, rAfter);
+      });
+  },
+  ignore: Deno.build.os === "windows",
+});
+
+Deno.test({
+  name: "SYNC: removing non-empty folder",
+  fn() {
+    const rBefore = Deno.resources();
+    const dir = Deno.makeTempDirSync();
+    Deno.createSync(join(dir, "file1.txt"));
+    Deno.createSync(join(dir, "file2.txt"));
+    Deno.mkdirSync(join(dir, "some_dir"));
+    Deno.createSync(join(dir, "some_dir", "file.txt"));
+    rmdirSync(dir, { recursive: true });
+    assertEquals(existsSync(dir), false);
+    // closing resources
+    const rAfter = Deno.resources();
+    closeRes(rBefore, rAfter);
+  },
+  ignore: Deno.build.os === "windows",
+});
diff --git a/std/node/_fs/_fs_stat.ts b/std/node/_fs/_fs_stat.ts
new file mode 100644
index 0000000000..d823f7ddbe
--- /dev/null
+++ b/std/node/_fs/_fs_stat.ts
@@ -0,0 +1,289 @@
+export type statOptions = {
+  bigint: boolean;
+};
+
+export type Stats = {
+  /** ID of the device containing the file.
+   *
+   * _Linux/Mac OS only._ */
+  dev: number | null;
+  /** Inode number.
+   *
+   * _Linux/Mac OS only._ */
+  ino: number | null;
+  /** **UNSTABLE**: Match behavior with Go on Windows for `mode`.
+   *
+   * The underlying raw `st_mode` bits that contain the standard Unix
+   * permissions for this file/directory. */
+  mode: number | null;
+  /** Number of hard links pointing to this file.
+   *
+   * _Linux/Mac OS only._ */
+  nlink: number | null;
+  /** User ID of the owner of this file.
+   *
+   * _Linux/Mac OS only._ */
+  uid: number | null;
+  /** Group ID of the owner of this file.
+   *
+   * _Linux/Mac OS only._ */
+  gid: number | null;
+  /** Device ID of this file.
+   *
+   * _Linux/Mac OS only._ */
+  rdev: number | null;
+  /** The size of the file, in bytes. */
+  size: number;
+  /** Blocksize for filesystem I/O.
+   *
+   * _Linux/Mac OS only._ */
+  blksize: number | null;
+  /** Number of blocks allocated to the file, in 512-byte units.
+   *
+   * _Linux/Mac OS only._ */
+  blocks: number | null;
+  /** The last modification time of the file. This corresponds to the `mtime`
+   * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This
+   * may not be available on all platforms. */
+  mtime: Date | null;
+  /** The last access time of the file. This corresponds to the `atime`
+   * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not
+   * be available on all platforms. */
+  atime: Date | null;
+  /** The creation time of the file. This corresponds to the `birthtime`
+   * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may
+   * not be available on all platforms. */
+  birthtime: Date | null;
+  /** change time */
+  ctime: Date | null;
+  /** atime in milliseconds */
+  atimeMs: number | null;
+  /** atime in milliseconds */
+  mtimeMs: number | null;
+  /** atime in milliseconds */
+  ctimeMs: number | null;
+  /** atime in milliseconds */
+  birthtimeMs: number | null;
+  isBlockDevice: () => boolean;
+  isCharacterDevice: () => boolean;
+  isDirectory: () => boolean;
+  isFIFO: () => boolean;
+  isFile: () => boolean;
+  isSocket: () => boolean;
+  isSymbolicLink: () => boolean;
+};
+
+export type BigIntStats = {
+  /** ID of the device containing the file.
+   *
+   * _Linux/Mac OS only._ */
+  dev: BigInt | null;
+  /** Inode number.
+   *
+   * _Linux/Mac OS only._ */
+  ino: BigInt | null;
+  /** **UNSTABLE**: Match behavior with Go on Windows for `mode`.
+   *
+   * The underlying raw `st_mode` bits that contain the standard Unix
+   * permissions for this file/directory. */
+  mode: BigInt | null;
+  /** Number of hard links pointing to this file.
+   *
+   * _Linux/Mac OS only._ */
+  nlink: BigInt | null;
+  /** User ID of the owner of this file.
+   *
+   * _Linux/Mac OS only._ */
+  uid: BigInt | null;
+  /** Group ID of the owner of this file.
+   *
+   * _Linux/Mac OS only._ */
+  gid: BigInt | null;
+  /** Device ID of this file.
+   *
+   * _Linux/Mac OS only._ */
+  rdev: BigInt | null;
+  /** The size of the file, in bytes. */
+  size: BigInt;
+  /** Blocksize for filesystem I/O.
+   *
+   * _Linux/Mac OS only._ */
+  blksize: BigInt | null;
+  /** Number of blocks allocated to the file, in 512-byte units.
+   *
+   * _Linux/Mac OS only._ */
+  blocks: BigInt | null;
+  /** The last modification time of the file. This corresponds to the `mtime`
+   * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This
+   * may not be available on all platforms. */
+  mtime: Date | null;
+  /** The last access time of the file. This corresponds to the `atime`
+   * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not
+   * be available on all platforms. */
+  atime: Date | null;
+  /** The creation time of the file. This corresponds to the `birthtime`
+   * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may
+   * not be available on all platforms. */
+  birthtime: Date | null;
+  /** change time */
+  ctime: Date | null;
+  /** atime in milliseconds */
+  atimeMs: BigInt | null;
+  /** atime in milliseconds */
+  mtimeMs: BigInt | null;
+  /** atime in milliseconds */
+  ctimeMs: BigInt | null;
+  /** atime in nanoseconds */
+  birthtimeMs: BigInt | null;
+  /** atime in nanoseconds */
+  atimeNs: BigInt | null;
+  /** atime in nanoseconds */
+  mtimeNs: BigInt | null;
+  /** atime in nanoseconds */
+  ctimeNs: BigInt | null;
+  /** atime in nanoseconds */
+  birthtimeNs: BigInt | null;
+  isBlockDevice: () => boolean;
+  isCharacterDevice: () => boolean;
+  isDirectory: () => boolean;
+  isFIFO: () => boolean;
+  isFile: () => boolean;
+  isSocket: () => boolean;
+  isSymbolicLink: () => boolean;
+};
+
+export function convertFileInfoToStats(origin: Deno.FileInfo): Stats {
+  return {
+    dev: origin.dev,
+    ino: origin.ino,
+    mode: origin.mode,
+    nlink: origin.nlink,
+    uid: origin.uid,
+    gid: origin.gid,
+    rdev: origin.rdev,
+    size: origin.size,
+    blksize: origin.blksize,
+    blocks: origin.blocks,
+    mtime: origin.mtime,
+    atime: origin.atime,
+    birthtime: origin.birthtime,
+    mtimeMs: origin.mtime?.getTime() || null,
+    atimeMs: origin.atime?.getTime() || null,
+    birthtimeMs: origin.birthtime?.getTime() || null,
+    isFile: () => origin.isFile,
+    isDirectory: () => origin.isDirectory,
+    isSymbolicLink: () => origin.isSymlink,
+    // not sure about those
+    isBlockDevice: () => false,
+    isFIFO: () => false,
+    isCharacterDevice: () => false,
+    isSocket: () => false,
+    ctime: origin.mtime,
+    ctimeMs: origin.mtime?.getTime() || null,
+  };
+}
+
+function to_BigInt(number?: number | null) {
+  if (number === null || number === undefined) return null;
+  return BigInt(number);
+}
+
+export function convertFileInfoToBigIntStats(
+  origin: Deno.FileInfo,
+): BigIntStats {
+  return {
+    dev: to_BigInt(origin.dev),
+    ino: to_BigInt(origin.ino),
+    mode: to_BigInt(origin.mode),
+    nlink: to_BigInt(origin.nlink),
+    uid: to_BigInt(origin.uid),
+    gid: to_BigInt(origin.gid),
+    rdev: to_BigInt(origin.rdev),
+    size: to_BigInt(origin.size) || 0n,
+    blksize: to_BigInt(origin.blksize),
+    blocks: to_BigInt(origin.blocks),
+    mtime: origin.mtime,
+    atime: origin.atime,
+    birthtime: origin.birthtime,
+    mtimeMs: origin.mtime ? BigInt(origin.mtime.getTime()) : null,
+    atimeMs: origin.atime ? BigInt(origin.atime.getTime()) : null,
+    birthtimeMs: origin.birthtime ? BigInt(origin.birthtime.getTime()) : null,
+    mtimeNs: origin.mtime ? BigInt(origin.mtime.getTime()) * 1000000n : null,
+    atimeNs: origin.atime ? BigInt(origin.atime.getTime()) * 1000000n : null,
+    birthtimeNs: origin.birthtime
+      ? BigInt(origin.birthtime.getTime()) * 1000000n
+      : null,
+    isFile: () => origin.isFile,
+    isDirectory: () => origin.isDirectory,
+    isSymbolicLink: () => origin.isSymlink,
+    // not sure about those
+    isBlockDevice: () => false,
+    isFIFO: () => false,
+    isCharacterDevice: () => false,
+    isSocket: () => false,
+    ctime: origin.mtime,
+    ctimeMs: origin.mtime ? BigInt(origin.mtime.getTime()) : null,
+    ctimeNs: origin.mtime ? BigInt(origin.mtime.getTime()) * 1000000n : null,
+  };
+}
+
+// shortcut for Convert File Info to Stats or BigIntStats
+export function CFISBIS(fileInfo: Deno.FileInfo, bigInt: boolean) {
+  if (bigInt) return convertFileInfoToBigIntStats(fileInfo);
+  return convertFileInfoToStats(fileInfo);
+}
+
+export type statCallbackBigInt = (
+  err: Error | undefined,
+  stat: BigIntStats,
+) => void;
+
+export type statCallback = (err: Error | undefined, stat: Stats) => void;
+
+export function stat(path: string | URL, callback: statCallback): void;
+export function stat(
+  path: string | URL,
+  options: { bigint: false },
+  callback: statCallback,
+): void;
+export function stat(
+  path: string | URL,
+  options: { bigint: true },
+  callback: statCallbackBigInt,
+): void;
+export function stat(
+  path: string | URL,
+  optionsOrCallback: statCallback | statCallbackBigInt | statOptions,
+  maybeCallback?: statCallback | statCallbackBigInt,
+) {
+  const callback =
+    (typeof optionsOrCallback === "function"
+      ? optionsOrCallback
+      : maybeCallback) as (
+        err: Error | undefined,
+        stat: BigIntStats | Stats,
+      ) => void;
+  const options = typeof optionsOrCallback === "object"
+    ? optionsOrCallback
+    : { bigint: false };
+
+  if (!callback) throw new Error("No callback function supplied");
+
+  Deno.stat(path)
+    .then((stat) => callback(undefined, CFISBIS(stat, options.bigint)))
+    .catch((err) => callback(err, err));
+}
+
+export function statSync(path: string | URL): Stats;
+export function statSync(path: string | URL, options: { bigint: false }): Stats;
+export function statSync(
+  path: string | URL,
+  options: { bigint: true },
+): BigIntStats;
+export function statSync(
+  path: string | URL,
+  options: statOptions = { bigint: false },
+): Stats | BigIntStats {
+  const origin = Deno.statSync(path);
+  return CFISBIS(origin, options.bigint);
+}
diff --git a/std/node/_fs/_fs_stat_test.ts b/std/node/_fs/_fs_stat_test.ts
new file mode 100644
index 0000000000..925a79be26
--- /dev/null
+++ b/std/node/_fs/_fs_stat_test.ts
@@ -0,0 +1,107 @@
+import { BigIntStats, stat, Stats, statSync } from "./_fs_stat.ts";
+import { assertEquals, fail } from "../../testing/asserts.ts";
+
+export function assertStats(actual: Stats, expected: Deno.FileInfo) {
+  assertEquals(actual.dev, expected.dev);
+  assertEquals(actual.gid, expected.gid);
+  assertEquals(actual.size, expected.size);
+  assertEquals(actual.blksize, expected.blksize);
+  assertEquals(actual.blocks, expected.blocks);
+  assertEquals(actual.ino, expected.ino);
+  assertEquals(actual.gid, expected.gid);
+  assertEquals(actual.mode, expected.mode);
+  assertEquals(actual.nlink, expected.nlink);
+  assertEquals(actual.rdev, expected.rdev);
+  assertEquals(actual.uid, expected.uid);
+  assertEquals(actual.atime?.getTime(), expected.atime?.getTime());
+  assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime());
+  assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime());
+  assertEquals(actual.atimeMs, expected.atime?.getTime());
+  assertEquals(actual.mtimeMs, expected.mtime?.getTime());
+  assertEquals(actual.birthtimeMs, expected.birthtime?.getTime());
+  assertEquals(actual.isFile(), expected.isFile);
+  assertEquals(actual.isDirectory(), expected.isDirectory);
+  assertEquals(actual.isSymbolicLink(), expected.isSymlink);
+}
+
+function to_BigInt(num?: number | null) {
+  if (num === undefined || num === null) return null;
+  return BigInt(num);
+}
+
+export function assertStatsBigInt(
+  actual: BigIntStats,
+  expected: Deno.FileInfo,
+) {
+  assertEquals(actual.dev, to_BigInt(expected.dev));
+  assertEquals(actual.gid, to_BigInt(expected.gid));
+  assertEquals(actual.size, to_BigInt(expected.size));
+  assertEquals(actual.blksize, to_BigInt(expected.blksize));
+  assertEquals(actual.blocks, to_BigInt(expected.blocks));
+  assertEquals(actual.ino, to_BigInt(expected.ino));
+  assertEquals(actual.gid, to_BigInt(expected.gid));
+  assertEquals(actual.mode, to_BigInt(expected.mode));
+  assertEquals(actual.nlink, to_BigInt(expected.nlink));
+  assertEquals(actual.rdev, to_BigInt(expected.rdev));
+  assertEquals(actual.uid, to_BigInt(expected.uid));
+  assertEquals(actual.atime?.getTime(), expected.atime?.getTime());
+  assertEquals(actual.mtime?.getTime(), expected.mtime?.getTime());
+  assertEquals(actual.birthtime?.getTime(), expected.birthtime?.getTime());
+  assertEquals(Number(actual.atimeMs), expected.atime?.getTime());
+  assertEquals(Number(actual.mtimeMs), expected.mtime?.getTime());
+  assertEquals(Number(actual.birthtimeMs), expected.birthtime?.getTime());
+  assertEquals(actual.atimeNs === null, actual.atime === null);
+  assertEquals(actual.mtimeNs === null, actual.mtime === null);
+  assertEquals(actual.birthtimeNs === null, actual.birthtime === null);
+  assertEquals(actual.isFile(), expected.isFile);
+  assertEquals(actual.isDirectory(), expected.isDirectory);
+  assertEquals(actual.isSymbolicLink(), expected.isSymlink);
+}
+
+Deno.test({
+  name: "ASYNC: get a file Stats",
+  async fn() {
+    const file = Deno.makeTempFileSync();
+    await new Promise<Stats>((resolve, reject) => {
+      stat(file, (err, stat) => {
+        if (err) reject(err);
+        resolve(stat);
+      });
+    })
+      .then((stat) => assertStats(stat, Deno.statSync(file)))
+      .catch(() => fail())
+      .finally(() => Deno.removeSync(file));
+  },
+});
+
+Deno.test({
+  name: "SYNC: get a file Stats",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    assertStats(statSync(file), Deno.statSync(file));
+  },
+});
+
+Deno.test({
+  name: "ASYNC: get a file BigInt Stats",
+  async fn() {
+    const file = Deno.makeTempFileSync();
+    await new Promise<BigIntStats>((resolve, reject) => {
+      stat(file, { bigint: true }, (err, stat) => {
+        if (err) reject(err);
+        resolve(stat);
+      });
+    })
+      .then((stat) => assertStatsBigInt(stat, Deno.statSync(file)))
+      .catch(() => fail())
+      .finally(() => Deno.removeSync(file));
+  },
+});
+
+Deno.test({
+  name: "SYNC: get a file BigInt Stats",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    assertStatsBigInt(statSync(file, { bigint: true }), Deno.statSync(file));
+  },
+});
diff --git a/std/node/_fs/_fs_unlink.ts b/std/node/_fs/_fs_unlink.ts
new file mode 100644
index 0000000000..aba734fe13
--- /dev/null
+++ b/std/node/_fs/_fs_unlink.ts
@@ -0,0 +1,10 @@
+export function unlink(path: string | URL, callback: (err?: Error) => void) {
+  if (!callback) throw new Error("No callback function supplied");
+  Deno.remove(path)
+    .then((_) => callback())
+    .catch(callback);
+}
+
+export function unlinkSync(path: string | URL) {
+  Deno.removeSync(path);
+}
diff --git a/std/node/_fs/_fs_unlink_test.ts b/std/node/_fs/_fs_unlink_test.ts
new file mode 100644
index 0000000000..922a1a703b
--- /dev/null
+++ b/std/node/_fs/_fs_unlink_test.ts
@@ -0,0 +1,30 @@
+import { assertEquals, fail } from "../../testing/asserts.ts";
+import { existsSync } from "../../fs/mod.ts";
+import { unlink, unlinkSync } from "./_fs_unlink.ts";
+
+Deno.test({
+  name: "ASYNC: deleting a file",
+  async fn() {
+    const file = Deno.makeTempFileSync();
+    await new Promise((resolve, reject) => {
+      unlink(file, (err) => {
+        if (err) reject(err);
+        resolve();
+      });
+    })
+      .then(() => assertEquals(existsSync(file), false))
+      .catch(() => fail())
+      .finally(() => {
+        if (existsSync(file)) Deno.removeSync(file);
+      });
+  },
+});
+
+Deno.test({
+  name: "SYNC: Test deleting a file",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    unlinkSync(file);
+    assertEquals(existsSync(file), false);
+  },
+});
diff --git a/std/node/_fs/_fs_watch.ts b/std/node/_fs/_fs_watch.ts
new file mode 100644
index 0000000000..a5f3bb9c17
--- /dev/null
+++ b/std/node/_fs/_fs_watch.ts
@@ -0,0 +1,111 @@
+import { fromFileUrl } from "../path.ts";
+import { EventEmitter } from "../events.ts";
+import { notImplemented } from "../_utils.ts";
+
+export function asyncIterableIteratorToCallback<T>(
+  iterator: AsyncIterableIterator<T>,
+  callback: (val: T, done?: boolean) => void,
+) {
+  function next() {
+    iterator.next().then((obj) => {
+      if (obj.done) {
+        callback(obj.value, true);
+        return;
+      }
+      callback(obj.value);
+      next();
+    });
+  }
+  next();
+}
+
+export function asyncIterableToCallback<T>(
+  iter: AsyncIterable<T>,
+  callback: (val: T, done?: boolean) => void,
+) {
+  const iterator = iter[Symbol.asyncIterator]();
+  function next() {
+    iterator.next().then((obj) => {
+      if (obj.done) {
+        callback(obj.value, true);
+        return;
+      }
+      callback(obj.value);
+      next();
+    });
+  }
+  next();
+}
+
+type watchOptions = {
+  persistent?: boolean;
+  recursive?: boolean;
+  encoding?: string;
+};
+
+type watchListener = (eventType: string, filename: string) => void;
+
+export function watch(
+  filename: string | URL,
+  options: watchOptions,
+  listener: watchListener,
+): FSWatcher;
+export function watch(
+  filename: string | URL,
+  listener: watchListener,
+): FSWatcher;
+export function watch(
+  filename: string | URL,
+  options: watchOptions,
+): FSWatcher;
+export function watch(filename: string | URL): FSWatcher;
+export function watch(
+  filename: string | URL,
+  optionsOrListener?: watchOptions | watchListener,
+  optionsOrListener2?: watchOptions | watchListener,
+) {
+  const listener = typeof optionsOrListener === "function"
+    ? optionsOrListener
+    : typeof optionsOrListener2 === "function"
+    ? optionsOrListener2
+    : undefined;
+  const options = typeof optionsOrListener === "object"
+    ? optionsOrListener
+    : typeof optionsOrListener2 === "object"
+    ? optionsOrListener2
+    : undefined;
+  filename = filename instanceof URL ? fromFileUrl(filename) : filename;
+
+  const iterator = Deno.watchFs(filename, {
+    recursive: options?.recursive || false,
+  });
+
+  if (!listener) throw new Error("No callback function supplied");
+
+  const fsWatcher = new FSWatcher(() => {
+    if (iterator.return) iterator.return();
+  });
+
+  fsWatcher.on("change", listener);
+
+  asyncIterableIteratorToCallback<Deno.FsEvent>(iterator, (val, done) => {
+    if (done) return;
+    fsWatcher.emit("change", val.kind, val.paths[0]);
+  });
+
+  return fsWatcher;
+}
+
+class FSWatcher extends EventEmitter {
+  close: () => void;
+  constructor(closer: () => void) {
+    super();
+    this.close = closer;
+  }
+  ref() {
+    notImplemented("FSWatcher.ref() is not implemented");
+  }
+  unref() {
+    notImplemented("FSWatcher.unref() is not implemented");
+  }
+}
diff --git a/std/node/_fs/_fs_watch_test.ts b/std/node/_fs/_fs_watch_test.ts
new file mode 100644
index 0000000000..e85b4c9bc1
--- /dev/null
+++ b/std/node/_fs/_fs_watch_test.ts
@@ -0,0 +1,32 @@
+import { watch } from "./_fs_watch.ts";
+import { assertEquals, fail } from "../../testing/asserts.ts";
+
+function wait(time: number) {
+  return new Promise((resolve) => {
+    setTimeout(resolve, time);
+  });
+}
+
+Deno.test({
+  name: "watching a file",
+  async fn() {
+    const file = Deno.makeTempFileSync();
+    const result: Array<[string, string]> = [];
+    await new Promise((resolve) => {
+      const watcher = watch(
+        file,
+        (eventType, filename) => result.push([eventType, filename]),
+      );
+      wait(100)
+        .then(() => Deno.writeTextFileSync(file, "something"))
+        .then(() => wait(100))
+        .then(() => watcher.close())
+        .then(() => wait(100))
+        .then(resolve);
+    })
+      .then(() => {
+        assertEquals(result.length >= 1, true);
+      })
+      .catch(() => fail());
+  },
+});
diff --git a/std/node/fs.ts b/std/node/fs.ts
index adb3a7c63c..91e24728bd 100644
--- a/std/node/fs.ts
+++ b/std/node/fs.ts
@@ -11,6 +11,15 @@ import { exists, existsSync } from "./_fs/_fs_exists.ts";
 import { mkdir, mkdirSync } from "./_fs/_fs_mkdir.ts";
 import { copyFile, copyFileSync } from "./_fs/_fs_copy.ts";
 import { writeFile, writeFileSync } from "./_fs/_fs_writeFile.ts";
+import { readdir, readdirSync } from "./_fs/_fs_readdir.ts";
+import { rename, renameSync } from "./_fs/_fs_rename.ts";
+import { rmdir, rmdirSync } from "./_fs/_fs_rmdir.ts";
+import { unlink, unlinkSync } from "./_fs/_fs_unlink.ts";
+import { watch } from "./_fs/_fs_watch.ts";
+import { open, openSync } from "./_fs/_fs_open.ts";
+import { stat, statSync } from "./_fs/_fs_stat.ts";
+import { lstat, lstatSync } from "./_fs/_fs_lstat.ts";
+
 import * as promises from "./_fs/promises/mod.ts";
 
 export {
@@ -29,13 +38,28 @@ export {
   copyFileSync,
   exists,
   existsSync,
+  lstat,
+  lstatSync,
   mkdir,
   mkdirSync,
+  open,
+  openSync,
   promises,
+  readdir,
+  readdirSync,
   readFile,
   readFileSync,
   readlink,
   readlinkSync,
+  rename,
+  renameSync,
+  rmdir,
+  rmdirSync,
+  stat,
+  statSync,
+  unlink,
+  unlinkSync,
+  watch,
   writeFile,
   writeFileSync,
 };