Skip to content

Commit 003aaaf

Browse files
authored
fix(fs,internal,path): unify isWindows implementations (#6744)
1 parent 80f7aee commit 003aaaf

33 files changed

+286
-91
lines changed

_tools/node_test_runner/tsconfig_for_bun.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@std/internal/diff": ["./internal/diff.ts"],
88
"@std/internal/diff-str": ["./internal/diff_str.ts"],
99
"@std/internal/format": ["./internal/format.ts"],
10+
"@std/internal/os": ["./internal/os.ts"],
1011
"@std/internal/styles": ["./internal/styles.ts"],
1112
"@std/path": ["./path/mod.ts"],
1213
"@std/semver": ["./semver/mod.ts"]

fs/_to_file_info.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

33
import type { FileInfo } from "./unstable_types.ts";
4-
import { isWindows } from "./_utils.ts";
4+
import { isWindows } from "@std/internal/os";
55

66
export function toFileInfo(s: import("node:fs").Stats): FileInfo {
77
return {

fs/_utils.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,6 @@
66
*/
77
export const isDeno = navigator.userAgent?.includes("Deno");
88

9-
/** True if the platform is windows, false otherwise */
10-
export const isWindows = checkWindows();
11-
12-
/**
13-
* @returns true if the platform is Windows, false otherwise.
14-
*/
15-
function checkWindows(): boolean {
16-
if (typeof navigator !== "undefined" && (navigator as any).platform) {
17-
return (navigator as any).platform.startsWith("Win");
18-
} else if (typeof (globalThis as any).process !== "undefined") {
19-
return (globalThis as any).platform === "win32";
20-
}
21-
return false;
22-
}
23-
249
/**
2510
* @returns The Node.js `fs` module.
2611
*/

fs/copy.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import { ensureDir, ensureDirSync } from "./ensure_dir.ts";
77
import { getFileInfoType } from "./_get_file_info_type.ts";
88
import { toPathString } from "./_to_path_string.ts";
99
import { isSubdir } from "./_is_subdir.ts";
10-
11-
// deno-lint-ignore no-explicit-any
12-
const isWindows = (globalThis as any).Deno?.build.os === "windows";
10+
import { isWindows } from "@std/internal/os";
1311

1412
/** Options for {@linkcode copy} and {@linkcode copySync}. */
1513
export interface CopyOptions {

fs/ensure_symlink.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import { resolve } from "@std/path/resolve";
44
import { ensureDir, ensureDirSync } from "./ensure_dir.ts";
55
import { getFileInfoType, type PathType } from "./_get_file_info_type.ts";
66
import { toPathString } from "./_to_path_string.ts";
7-
8-
// deno-lint-ignore no-explicit-any
9-
const isWindows = (globalThis as any).Deno?.build.os === "windows";
7+
import { isWindows } from "@std/internal/os";
108

119
function resolveSymlinkTarget(target: string | URL, linkName: string | URL) {
1210
if (typeof target !== "string") return target; // URL is always absolute path

fs/expand_glob.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@ import {
1212
createWalkEntrySync,
1313
type WalkEntry,
1414
} from "./_create_walk_entry.ts";
15+
import { isWindows } from "@std/internal/os";
1516

1617
export type { GlobOptions, WalkEntry };
1718

18-
// deno-lint-ignore no-explicit-any
19-
const isWindows = (globalThis as any).Deno?.build.os === "windows";
20-
2119
/** Options for {@linkcode expandGlob} and {@linkcode expandGlobSync}. */
2220
export interface ExpandGlobOptions extends Omit<GlobOptions, "os"> {
2321
/**

internal/_os.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
export function checkWindows(): boolean {
4+
// deno-lint-ignore no-explicit-any
5+
const global = globalThis as any;
6+
const os = global.Deno?.build?.os;
7+
8+
// Check Deno, then the remaining runtimes (e.g. Node, Bun and the browser)
9+
return typeof os === "string"
10+
? os === "windows"
11+
: global.navigator?.platform?.startsWith("Win") ??
12+
global.process?.platform?.startsWith("win") ?? false;
13+
}

internal/_os_test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
import { assertEquals } from "@std/assert/equals";
3+
import { checkWindows } from "./_os.ts";
4+
import { disposableStack, stubProperty } from "./_testing.ts";
5+
6+
// deno-lint-ignore no-explicit-any
7+
const global = globalThis as any;
8+
9+
Deno.test("checkWindows() returns `true` on Windows on various runtimes", async (t) => {
10+
await t.step("Deno", () => {
11+
using stack = disposableStack();
12+
stack.use(stubProperty(global, "Deno", { build: { os: "windows" } }));
13+
stack.use(stubProperty(global, "navigator", undefined));
14+
stack.use(stubProperty(global, "process", undefined));
15+
16+
assertEquals(checkWindows(), true);
17+
});
18+
19+
await t.step("Browser etc.", () => {
20+
using stack = disposableStack();
21+
stack.use(stubProperty(global, "Deno", undefined));
22+
stack.use(stubProperty(global, "navigator", { platform: "Win32" }));
23+
stack.use(stubProperty(global, "process", undefined));
24+
25+
assertEquals(checkWindows(), true);
26+
});
27+
28+
await t.step("NodeJS etc.", () => {
29+
using stack = disposableStack();
30+
stack.use(stubProperty(global, "Deno", undefined));
31+
stack.use(stubProperty(global, "navigator", undefined));
32+
stack.use(stubProperty(global, "process", { platform: "win32" }));
33+
34+
assertEquals(checkWindows(), true);
35+
});
36+
});
37+
38+
Deno.test("checkWindows() returns `false` on Linux on various runtimes", async (t) => {
39+
await t.step("Deno", () => {
40+
using stack = disposableStack();
41+
stack.use(stubProperty(global, "Deno", { build: { os: "linux" } }));
42+
stack.use(stubProperty(global, "navigator", undefined));
43+
stack.use(stubProperty(global, "process", undefined));
44+
45+
assertEquals(checkWindows(), false);
46+
});
47+
48+
await t.step("Browser etc.", () => {
49+
using stack = disposableStack();
50+
stack.use(stubProperty(global, "Deno", undefined));
51+
stack.use(stubProperty(global, "navigator", { platform: "Linux x86_64" }));
52+
stack.use(stubProperty(global, "process", undefined));
53+
54+
assertEquals(checkWindows(), false);
55+
});
56+
57+
await t.step("NodeJS etc.", () => {
58+
using stack = disposableStack();
59+
stack.use(stubProperty(global, "Deno", undefined));
60+
stack.use(stubProperty(global, "navigator", undefined));
61+
stack.use(stubProperty(global, "process", { platform: "linux" }));
62+
63+
assertEquals(checkWindows(), false);
64+
});
65+
});

internal/_testing.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
3+
/**
4+
* Stubs a property on an object, retaining the attributes of the original property descriptor as far as possible.
5+
*
6+
* @typeParam Self The type of the object to stub a property of.
7+
* @typeParam Prop The property of the instance to stub.
8+
* @param self The object to stub the property on.
9+
* @param property The property to stub.
10+
* @param value The value to stub the property with.
11+
* @returns A disposable that restores the original property when disposed.
12+
*/
13+
export function stubProperty<Self, Prop extends keyof Self>(
14+
self: Self,
15+
property: Prop,
16+
value: Self[Prop],
17+
): Disposable {
18+
const descriptor = Object.getOwnPropertyDescriptor(self, property);
19+
20+
if (descriptor == null) {
21+
Object.defineProperty(self, property, { value, configurable: true });
22+
return {
23+
[Symbol.dispose]() {
24+
delete self[property];
25+
},
26+
};
27+
}
28+
29+
if (!descriptor.configurable && !descriptor.writable) {
30+
throw new TypeError(
31+
`Cannot stub property "${
32+
String(property)
33+
}" because it is not configurable or writable.`,
34+
);
35+
}
36+
37+
Object.defineProperty(self, property, {
38+
...descriptor,
39+
...(Object.hasOwn(descriptor, "get") || Object.hasOwn(descriptor, "set")
40+
? { get: () => value }
41+
: { value }),
42+
});
43+
44+
return {
45+
[Symbol.dispose]() {
46+
Object.defineProperty(self, property, descriptor);
47+
},
48+
};
49+
}
50+
51+
// partial `DisposableStack` polyfill
52+
// https://github.com/tc39/proposal-explicit-resource-management
53+
export function disposableStack() {
54+
return {
55+
disposables: [] as Disposable[],
56+
defer(fn: () => void) {
57+
this.disposables.push({ [Symbol.dispose]: fn });
58+
},
59+
use(val: Disposable) {
60+
this.disposables.push(val);
61+
},
62+
[Symbol.dispose]() {
63+
for (let i = this.disposables.length - 1; i >= 0; --i) {
64+
this.disposables[i]![Symbol.dispose]();
65+
}
66+
},
67+
};
68+
}

internal/_testing_test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
// deno-lint-ignore-file deno-style-guide/naming-convention
3+
import { assertEquals } from "@std/assert/equals";
4+
import { stubProperty } from "./_testing.ts";
5+
import { assertThrows } from "@std/assert/throws";
6+
7+
const PROP = "foo";
8+
const ORIGINAL_VALUE = "bar";
9+
const STUBBED_VALUE = "baz";
10+
11+
type Obj = { [PROP]?: string };
12+
13+
Deno.test("stubProperty() stubs properties and returns them to their original values", () => {
14+
const descriptors: (TypedPropertyDescriptor<string> | undefined)[] = [
15+
undefined,
16+
{
17+
get: () => ORIGINAL_VALUE,
18+
configurable: true,
19+
},
20+
{
21+
set: () => {},
22+
configurable: true,
23+
},
24+
{ value: ORIGINAL_VALUE, writable: true, configurable: true },
25+
{ value: ORIGINAL_VALUE, writable: true, configurable: false },
26+
{ value: ORIGINAL_VALUE, writable: false, configurable: true },
27+
];
28+
29+
const Empty = () => class Empty {};
30+
const Val = () =>
31+
class Val {
32+
[PROP] = ORIGINAL_VALUE;
33+
};
34+
const Get = () =>
35+
class Get {
36+
get [PROP]() {
37+
return ORIGINAL_VALUE;
38+
}
39+
};
40+
41+
const objCreators: (() => Obj)[] = [
42+
() => ({}),
43+
() => ({ [PROP]: ORIGINAL_VALUE }),
44+
() => {
45+
const o: Obj = {};
46+
o[PROP] = ORIGINAL_VALUE;
47+
return o;
48+
},
49+
() => new (Val())(),
50+
() => new (Get())(),
51+
() => new (Empty())(),
52+
() => Val().prototype,
53+
() => Get().prototype,
54+
() => Empty().prototype,
55+
() => Object.create({ [PROP]: ORIGINAL_VALUE }),
56+
];
57+
58+
for (const getObj of objCreators) {
59+
for (const descriptor of descriptors) {
60+
const obj = getObj();
61+
if (descriptor != null) Object.defineProperty(obj, PROP, descriptor);
62+
63+
const initialDescriptor = Object.getOwnPropertyDescriptor(obj, PROP);
64+
const initialValue = obj[PROP];
65+
66+
{
67+
using _ = stubProperty(obj, PROP, STUBBED_VALUE);
68+
assertEquals(obj[PROP], STUBBED_VALUE);
69+
}
70+
71+
assertEquals(obj[PROP], initialValue);
72+
assertEquals(
73+
Object.getOwnPropertyDescriptor(obj, PROP),
74+
initialDescriptor,
75+
);
76+
}
77+
}
78+
});
79+
80+
Deno.test("stubProperty() throws if property is not configurable or writable", () => {
81+
assertThrows(
82+
() =>
83+
stubProperty(
84+
Object.defineProperty(
85+
{} as Obj,
86+
PROP,
87+
{ value: ORIGINAL_VALUE },
88+
),
89+
PROP,
90+
STUBBED_VALUE,
91+
),
92+
TypeError,
93+
'Cannot stub property "foo" because it is not configurable or writable.',
94+
);
95+
96+
assertThrows(
97+
() =>
98+
stubProperty(
99+
Object.freeze({ [PROP]: ORIGINAL_VALUE } as Obj),
100+
PROP,
101+
STUBBED_VALUE,
102+
),
103+
TypeError,
104+
'Cannot stub property "foo" because it is not configurable or writable.',
105+
);
106+
});

0 commit comments

Comments
 (0)