Skip to content

Commit a2a6305

Browse files
authored
feat(fs/unstable): add symlink and symlinkSync (#6352)
1 parent 5d41054 commit a2a6305

File tree

5 files changed

+209
-0
lines changed

5 files changed

+209
-0
lines changed

_tools/node_test_runner/run_test.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import "../../collections/without_all_test.ts";
5151
import "../../collections/zip_test.ts";
5252
import "../../fs/unstable_read_dir_test.ts";
5353
import "../../fs/unstable_stat_test.ts";
54+
import "../../fs/unstable_symlink_test.ts";
5455
import "../../fs/unstable_lstat_test.ts";
5556
import "../../fs/unstable_chmod_test.ts";
5657

fs/deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"./unstable-lstat": "./unstable_lstat.ts",
1818
"./unstable-read-dir": "./unstable_read_dir.ts",
1919
"./unstable-stat": "./unstable_stat.ts",
20+
"./unstable-symlink": "./unstable_symlink.ts",
2021
"./unstable-types": "./unstable_types.ts",
2122
"./walk": "./walk.ts"
2223
}

fs/unstable_symlink.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
import { getNodeFs, isDeno } from "./_utils.ts";
4+
import { mapError } from "./_map_error.ts";
5+
import type { SymlinkOptions } from "./unstable_types.ts";
6+
7+
/**
8+
* Creates `newpath` as a symbolic link to `oldpath`.
9+
*
10+
* The `options.type` parameter can be set to `"file"`, `"dir"` or `"junction"`.
11+
* This argument is only available on Windows and ignored on other platforms.
12+
*
13+
* Requires full `allow-read` and `allow-write` permissions.
14+
*
15+
* @example Usage
16+
* ```ts ignore
17+
* import { symlink } from "@std/fs/unstable-symlink";
18+
* await symlink("README.md", "README.md.link");
19+
* ```
20+
*
21+
* @tags allow-read, allow-write
22+
*
23+
* @param oldpath The path of the resource pointed by the symbolic link.
24+
* @param newpath The path of the symbolic link.
25+
*/
26+
export async function symlink(
27+
oldpath: string | URL,
28+
newpath: string | URL,
29+
options?: SymlinkOptions,
30+
): Promise<void> {
31+
if (isDeno) {
32+
return Deno.symlink(oldpath, newpath, options);
33+
} else {
34+
try {
35+
return await getNodeFs().promises.symlink(
36+
oldpath,
37+
newpath,
38+
options?.type,
39+
);
40+
} catch (error) {
41+
throw mapError(error);
42+
}
43+
}
44+
}
45+
46+
/**
47+
* Creates `newpath` as a symbolic link to `oldpath`.
48+
*
49+
* The `options.type` parameter can be set to `"file"`, `"dir"` or `"junction"`.
50+
* This argument is only available on Windows and ignored on other platforms.
51+
*
52+
* Requires full `allow-read` and `allow-write` permissions.
53+
*
54+
* @example Usage
55+
* ```ts ignore
56+
* import { symlinkSync } from "@std/fs/unstable-symlink";
57+
* symlinkSync("README.md", "README.md.link");
58+
* ```
59+
*
60+
* @tags allow-read, allow-write
61+
*
62+
* @param oldpath The path of the resource pointed by the symbolic link.
63+
* @param newpath The path of the symbolic link.
64+
*/
65+
export function symlinkSync(
66+
oldpath: string | URL,
67+
newpath: string | URL,
68+
options?: SymlinkOptions,
69+
): void {
70+
if (isDeno) {
71+
return Deno.symlinkSync(oldpath, newpath, options);
72+
} else {
73+
try {
74+
return getNodeFs().symlinkSync(oldpath, newpath, options?.type);
75+
} catch (error) {
76+
throw mapError(error);
77+
}
78+
}
79+
}

fs/unstable_symlink_test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
import { assert, assertRejects, assertThrows } from "@std/assert";
4+
import { symlink, symlinkSync } from "./unstable_symlink.ts";
5+
import { AlreadyExists } from "./unstable_errors.js";
6+
import { lstat, mkdir, mkdtemp, open, rm, stat } from "node:fs/promises";
7+
import {
8+
closeSync,
9+
lstatSync,
10+
mkdirSync,
11+
mkdtempSync,
12+
openSync,
13+
rmSync,
14+
statSync,
15+
} from "node:fs";
16+
import { tmpdir } from "node:os";
17+
import { dirname, join, resolve } from "node:path";
18+
import { fileURLToPath } from "node:url";
19+
20+
const moduleDir = dirname(fileURLToPath(import.meta.url));
21+
const testdataDir = resolve(moduleDir, "testdata");
22+
23+
Deno.test("symlink() creates a link to a regular file", async () => {
24+
const tempDirPath = await mkdtemp(resolve(tmpdir(), "symlink_"));
25+
const testFile = join(tempDirPath, "testFile.txt");
26+
const symlinkPath = join(tempDirPath, "testFile.txt.link");
27+
28+
const tempFh = await open(testFile, "w");
29+
await symlink(testFile, symlinkPath);
30+
31+
const symlinkLstat = await lstat(symlinkPath);
32+
const fileStat = await stat(testFile);
33+
34+
assert(symlinkLstat.isSymbolicLink);
35+
assert(fileStat.isFile);
36+
37+
await tempFh.close();
38+
await rm(tempDirPath, { recursive: true, force: true });
39+
});
40+
41+
Deno.test("symlink() creates a link to a directory", async () => {
42+
const tempDirPath = await mkdtemp(resolve(tmpdir(), "symlink_"));
43+
const testDir = join(tempDirPath, "testDir");
44+
const symlinkPath = join(tempDirPath, "testDir.link");
45+
46+
await mkdir(testDir);
47+
await symlink(testDir, symlinkPath);
48+
49+
const symlinkLstat = await lstat(symlinkPath);
50+
const dirStat = await stat(testDir);
51+
52+
assert(symlinkLstat.isSymbolicLink);
53+
assert(dirStat.isDirectory);
54+
55+
await rm(tempDirPath, { recursive: true, force: true });
56+
});
57+
58+
Deno.test(
59+
"symlink() rejects with AlreadyExists for creating the same link path to the same file path",
60+
async () => {
61+
const existingFile = join(testdataDir, "0.ts");
62+
const existingSymlink = join(testdataDir, "0-link");
63+
64+
await assertRejects(async () => {
65+
await symlink(existingFile, existingSymlink);
66+
}, AlreadyExists);
67+
},
68+
);
69+
70+
Deno.test(
71+
"symlinkSync() creates a link to a regular file",
72+
() => {
73+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "symlinkSync_"));
74+
const filePath = join(tempDirPath, "testFile.txt");
75+
const symlinkPath = join(tempDirPath, "testFile.txt.link");
76+
77+
const tempFd = openSync(filePath, "w");
78+
symlinkSync(filePath, symlinkPath);
79+
80+
const symlinkLstat = lstatSync(symlinkPath);
81+
const fileStat = statSync(filePath);
82+
83+
assert(symlinkLstat.isSymbolicLink);
84+
assert(fileStat.isFile);
85+
86+
closeSync(tempFd);
87+
rmSync(tempDirPath, { recursive: true, force: true });
88+
},
89+
);
90+
91+
Deno.test("symlinkSync() creates a link to a directory", () => {
92+
const tempDirPath = mkdtempSync(resolve(tmpdir(), "symlinkSync_"));
93+
const testDir = join(tempDirPath, "testDir");
94+
const symlinkPath = join(tempDirPath, "testDir.link");
95+
96+
mkdirSync(testDir);
97+
symlinkSync(testDir, symlinkPath);
98+
99+
const symlinkLstat = lstatSync(symlinkPath);
100+
const dirStat = statSync(testDir);
101+
102+
assert(symlinkLstat.isSymbolicLink);
103+
assert(dirStat.isDirectory);
104+
105+
rmSync(tempDirPath, { recursive: true, force: true });
106+
});
107+
108+
Deno.test(
109+
"symlinkSync() throws with AlreadyExists for creating the same link path to the same file path",
110+
() => {
111+
const existingFile = join(testdataDir, "0.ts");
112+
const existingSymlink = join(testdataDir, "0-link");
113+
114+
assertThrows(() => {
115+
symlinkSync(existingFile, existingSymlink);
116+
}, AlreadyExists);
117+
},
118+
);

fs/unstable_types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,13 @@ export interface DirEntry {
107107
* `FileInfo.isFile` and `FileInfo.isDirectory`. */
108108
isSymlink: boolean;
109109
}
110+
111+
/**
112+
* Options that can be used with {@linkcode symlink} and
113+
* {@linkcode symlinkSync}.
114+
*/
115+
export interface SymlinkOptions {
116+
/** Specify the symbolic link type as file, directory or NTFS junction. This
117+
* option only applies to Windows and is ignored on other operating systems. */
118+
type: "file" | "dir" | "junction";
119+
}

0 commit comments

Comments
 (0)