From 9d33cf2942776cf81faef81b7c0b93c154c36050 Mon Sep 17 00:00:00 2001
From: James Bronder <36022278+jbronder@users.noreply.github.com>
Date: Sun, 19 Jan 2025 22:43:17 -0800
Subject: [PATCH 1/4] feat(fs/unstable): added import references for symlink

---
 _tools/node_test_runner/run_test.mjs | 1 +
 fs/deno.json                         | 1 +
 2 files changed, 2 insertions(+)

diff --git a/_tools/node_test_runner/run_test.mjs b/_tools/node_test_runner/run_test.mjs
index a3ad0260b9b6..cd1cb999ff43 100644
--- a/_tools/node_test_runner/run_test.mjs
+++ b/_tools/node_test_runner/run_test.mjs
@@ -51,6 +51,7 @@ import "../../collections/without_all_test.ts";
 import "../../collections/zip_test.ts";
 import "../../fs/unstable_read_dir_test.ts";
 import "../../fs/unstable_stat_test.ts";
+import "../../fs/unstable_symlink_test.ts";
 import "../../fs/unstable_lstat_test.ts";
 import "../../fs/unstable_chmod_test.ts";
 
diff --git a/fs/deno.json b/fs/deno.json
index 809654b85731..cd2c9bd9732c 100644
--- a/fs/deno.json
+++ b/fs/deno.json
@@ -17,6 +17,7 @@
     "./unstable-lstat": "./unstable_lstat.ts",
     "./unstable-read-dir": "./unstable_read_dir.ts",
     "./unstable-stat": "./unstable_stat.ts",
+    "./unstable-symlink": "./unstable_symlink.ts",
     "./unstable-types": "./unstable_types.ts",
     "./walk": "./walk.ts"
   }

From f13ff189a9fcfbdf9c991d1bb64db3422400295d Mon Sep 17 00:00:00 2001
From: James Bronder <36022278+jbronder@users.noreply.github.com>
Date: Sun, 19 Jan 2025 22:44:18 -0800
Subject: [PATCH 2/4] feat(fs/unstable): added SymlinkOptions type

---
 fs/unstable_types.ts | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/fs/unstable_types.ts b/fs/unstable_types.ts
index b86cba1c0123..60d61e8a8a3e 100644
--- a/fs/unstable_types.ts
+++ b/fs/unstable_types.ts
@@ -107,3 +107,13 @@ export interface DirEntry {
    * `FileInfo.isFile` and `FileInfo.isDirectory`. */
   isSymlink: boolean;
 }
+
+/**
+ * Options that can be used with {@linkcode symlink} and
+ * {@linkcode symlinkSync}.
+ */
+export interface SymlinkOptions {
+  /** Specify the symbolic link type as file, directory or NTFS junction. This
+   * option only applies to Windows and is ignored on other operating systems. */
+  type: "file" | "dir" | "junction";
+}

From 9785a08d7d615ea8b15654d21b6fbe940db9b15b Mon Sep 17 00:00:00 2001
From: James Bronder <36022278+jbronder@users.noreply.github.com>
Date: Sun, 19 Jan 2025 22:46:00 -0800
Subject: [PATCH 3/4] feat(fs/unstable): added symlink and symlinkSync
 functions with tests

---
 fs/unstable_symlink.ts      |  79 ++++++++++++++++++++++++
 fs/unstable_symlink_test.ts | 118 ++++++++++++++++++++++++++++++++++++
 2 files changed, 197 insertions(+)
 create mode 100644 fs/unstable_symlink.ts
 create mode 100644 fs/unstable_symlink_test.ts

diff --git a/fs/unstable_symlink.ts b/fs/unstable_symlink.ts
new file mode 100644
index 000000000000..97c5ba21a7d8
--- /dev/null
+++ b/fs/unstable_symlink.ts
@@ -0,0 +1,79 @@
+// Copyright 2018-2025 the Deno authors. MIT license.
+
+import { getNodeFs, isDeno } from "./_utils.ts";
+import { mapError } from "./_map_error.ts";
+import type { SymlinkOptions } from "./unstable_types.ts";
+
+/**
+ * Creates `newpath` as a symbolic link to `oldpath`.
+ *
+ * The `options.type` parameter can be set to `"file"`, `"dir"` or `"junction"`.
+ * This argument is only available on Windows and ignored on other platforms.
+ *
+ * Requires full `allow-read` and `allow-write` permissions.
+ *
+ * @example Usage
+ * ```ts
+ * import { symlink } from "@std/fs/unstable-symlink";
+ * await symlink("README.md", "README.md.link");
+ * ```
+ *
+ * @tags allow-read, allow-write
+ *
+ * @param oldpath The path of the resource pointed by the symbolic link.
+ * @param newpath The path of the symbolic link.
+ */
+export async function symlink(
+  oldpath: string | URL,
+  newpath: string | URL,
+  options?: SymlinkOptions,
+): Promise<void> {
+  if (isDeno) {
+    return Deno.symlink(oldpath, newpath, options);
+  } else {
+    try {
+      return await getNodeFs().promises.symlink(
+        oldpath,
+        newpath,
+        options?.type,
+      );
+    } catch (error) {
+      throw mapError(error);
+    }
+  }
+}
+
+/**
+ * Creates `newpath` as a symbolic link to `oldpath`.
+ *
+ * The `options.type` parameter can be set to `"file"`, `"dir"` or `"junction"`.
+ * This argument is only available on Windows and ignored on other platforms.
+ *
+ * Requires full `allow-read` and `allow-write` permissions.
+ *
+ * @example Usage
+ * ```ts
+ * import { symlinkSync } from "@std/fs/unstable-symlink";
+ * symlinkSync("README.md", "README.md.link2");
+ * ```
+ *
+ * @tags allow-read, allow-write
+ *
+ * @param oldpath The path of the resource pointed by the symbolic link.
+ * @param newpath The path of the symbolic link.
+ */
+export function symlinkSync(
+  oldpath: string | URL,
+  newpath: string | URL,
+  options?: SymlinkOptions,
+): void {
+  if (isDeno) {
+    return Deno.symlinkSync(oldpath, newpath, options);
+  } else {
+    try {
+      return getNodeFs().symlinkSync(oldpath, newpath, options?.type);
+    } catch (error) {
+      throw mapError(error);
+    }
+  }
+}
diff --git a/fs/unstable_symlink_test.ts b/fs/unstable_symlink_test.ts
new file mode 100644
index 000000000000..7232ff178b91
--- /dev/null
+++ b/fs/unstable_symlink_test.ts
@@ -0,0 +1,118 @@
+// Copyright 2018-2025 the Deno authors. MIT license.
+
+import { assert, assertRejects, assertThrows } from "@std/assert";
+import { symlink, symlinkSync } from "./unstable_symlink.ts";
+import { AlreadyExists } from "./unstable_errors.js";
+import { lstat, mkdir, mkdtemp, open, rm, stat } from "node:fs/promises";
+import {
+  closeSync,
+  lstatSync,
+  mkdirSync,
+  mkdtempSync,
+  openSync,
+  rmSync,
+  statSync,
+} from "node:fs";
+import { tmpdir } from "node:os";
+import { dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const moduleDir = dirname(fileURLToPath(import.meta.url));
+const testdataDir = resolve(moduleDir, "testdata");
+
+Deno.test("symlink() creates a link to a regular file", async () => {
+  const tempDirPath = await mkdtemp(resolve(tmpdir(), "symlink_"));
+  const testFile = join(tempDirPath, "testFile.txt");
+  const symlinkPath = join(tempDirPath, "testFile.txt.link");
+
+  const tempFh = await open(testFile, "w");
+  await symlink(testFile, symlinkPath);
+
+  const symlinkLstat = await lstat(symlinkPath);
+  const fileStat = await stat(testFile);
+
+  assert(symlinkLstat.isSymbolicLink);
+  assert(fileStat.isFile);
+
+  await tempFh.close();
+  await rm(tempDirPath, { recursive: true, force: true });
+});
+
+Deno.test("symlink() creates a link to a directory", async () => {
+  const tempDirPath = await mkdtemp(resolve(tmpdir(), "symlink_"));
+  const testDir = join(tempDirPath, "testDir");
+  const symlinkPath = join(tempDirPath, "testDir.link");
+
+  await mkdir(testDir);
+  await symlink(testDir, symlinkPath);
+
+  const symlinkLstat = await lstat(symlinkPath);
+  const dirStat = await stat(testDir);
+
+  assert(symlinkLstat.isSymbolicLink);
+  assert(dirStat.isDirectory);
+
+  await rm(tempDirPath, { recursive: true, force: true });
+});
+
+Deno.test(
+  "symlink() rejects with AlreadyExists for creating the same link path to the same file path",
+  async () => {
+    const existingFile = join(testdataDir, "0.ts");
+    const existingSymlink = join(testdataDir, "0-link");
+
+    await assertRejects(async () => {
+      await symlink(existingFile, existingSymlink);
+    }, AlreadyExists);
+  },
+);
+
+Deno.test(
+  "symlinkSync() creates a link to a regular file",
+  () => {
+    const tempDirPath = mkdtempSync(resolve(tmpdir(), "symlinkSync_"));
+    const filePath = join(tempDirPath, "testFile.txt");
+    const symlinkPath = join(tempDirPath, "testFile.txt.link");
+
+    const tempFd = openSync(filePath, "w");
+    symlinkSync(filePath, symlinkPath);
+
+    const symlinkLstat = lstatSync(symlinkPath);
+    const fileStat = statSync(filePath);
+
+    assert(symlinkLstat.isSymbolicLink);
+    assert(fileStat.isFile);
+
+    closeSync(tempFd);
+    rmSync(tempDirPath, { recursive: true, force: true });
+  },
+);
+
+Deno.test("symlinkSync() creates a link to a directory", () => {
+  const tempDirPath = mkdtempSync(resolve(tmpdir(), "symlinkSync_"));
+  const testDir = join(tempDirPath, "testDir");
+  const symlinkPath = join(tempDirPath, "testDir.link");
+
+  mkdirSync(testDir);
+  symlinkSync(testDir, symlinkPath);
+
+  const symlinkLstat = lstatSync(symlinkPath);
+  const dirStat = statSync(testDir);
+
+  assert(symlinkLstat.isSymbolicLink);
+  assert(dirStat.isDirectory);
+
+  rmSync(tempDirPath, { recursive: true, force: true });
+});
+
+Deno.test(
+  "symlinkSync() throws with AlreadyExists for creating the same link path to the same file path",
+  () => {
+    const existingFile = join(testdataDir, "0.ts");
+    const existingSymlink = join(testdataDir, "0-link");
+
+    assertThrows(() => {
+      symlinkSync(existingFile, existingSymlink);
+    }, AlreadyExists);
+  },
+);

From 0d53a6d3b246329fa8b456e9012f448023d97232 Mon Sep 17 00:00:00 2001
From: James Bronder <36022278+jbronder@users.noreply.github.com>
Date: Sun, 19 Jan 2025 23:17:32 -0800
Subject: [PATCH 4/4] fix: add `ignore` to TS code snippet to prevent add of
 link to library

---
 fs/unstable_symlink.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/fs/unstable_symlink.ts b/fs/unstable_symlink.ts
index 97c5ba21a7d8..fbaa201354e8 100644
--- a/fs/unstable_symlink.ts
+++ b/fs/unstable_symlink.ts
@@ -13,7 +13,7 @@ import type { SymlinkOptions } from "./unstable_types.ts";
  * Requires full `allow-read` and `allow-write` permissions.
  *
  * @example Usage
- * ```ts
+ * ```ts ignore
  * import { symlink } from "@std/fs/unstable-symlink";
  * await symlink("README.md", "README.md.link");
  * ```
@@ -52,9 +52,9 @@ export async function symlink(
  * Requires full `allow-read` and `allow-write` permissions.
  *
  * @example Usage
- * ```ts
+ * ```ts ignore
  * import { symlinkSync } from "@std/fs/unstable-symlink";
- * symlinkSync("README.md", "README.md.link2");
+ * symlinkSync("README.md", "README.md.link");
  * ```
  *
  * @tags allow-read, allow-write