feat(node API): add fs.glob, fs.globSync, fs.promises.glob (#28972)
Some checks are pending
ci / pre-build (push) Waiting to run
ci / test debug linux-aarch64 (push) Blocked by required conditions
ci / test release linux-aarch64 (push) Blocked by required conditions
ci / test debug macos-aarch64 (push) Blocked by required conditions
ci / test release macos-aarch64 (push) Blocked by required conditions
ci / bench release linux-x86_64 (push) Blocked by required conditions
ci / lint debug linux-x86_64 (push) Blocked by required conditions
ci / lint debug macos-x86_64 (push) Blocked by required conditions
ci / lint debug windows-x86_64 (push) Blocked by required conditions
ci / test debug linux-x86_64 (push) Blocked by required conditions
ci / test release linux-x86_64 (push) Blocked by required conditions
ci / test debug macos-x86_64 (push) Blocked by required conditions
ci / test release macos-x86_64 (push) Blocked by required conditions
ci / test debug windows-x86_64 (push) Blocked by required conditions
ci / test release windows-x86_64 (push) Blocked by required conditions
ci / build libs (push) Blocked by required conditions
ci / publish canary (push) Blocked by required conditions

This commit is contained in:
Jeff Hykin 2025-07-01 04:35:45 -05:00 committed by GitHub
parent 033da85781
commit 2f72884425
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 2730 additions and 178 deletions

View file

@ -33,6 +33,7 @@
"cli/tsc/dts/typescript.d.ts",
"cli/tools/doc/prism.css",
"cli/tools/doc/prism.js",
"ext/node/polyfills/deps",
"ext/websocket/autobahn/reports",
"gh-pages",
"libs/config/testdata",

View file

@ -533,7 +533,6 @@ deno_core::extension!(deno_node,
"_fs/_fs_copy.ts",
"_fs/_fs_cp.js",
"_fs/_fs_dir.ts",
"_fs/_fs_dirent.ts",
"_fs/_fs_exists.ts",
"_fs/_fs_fchmod.ts",
"_fs/_fs_fchown.ts",
@ -542,6 +541,7 @@ deno_core::extension!(deno_node,
"_fs/_fs_fsync.ts",
"_fs/_fs_ftruncate.ts",
"_fs/_fs_futimes.ts",
"_fs/_fs_glob.ts",
"_fs/_fs_lchmod.ts",
"_fs/_fs_lchown.ts",
"_fs/_fs_link.ts",
@ -773,6 +773,10 @@ deno_core::extension!(deno_node,
"node:worker_threads" = "worker_threads.ts",
"node:zlib" = "zlib.ts",
],
lazy_loaded_esm = [
dir "polyfills",
"deps/minimatch.js",
],
options = {
maybe_init: Option<NodeExtInitServices<TInNpmPackageChecker, TNpmPackageFolderResolver, TSys>>,
fs: deno_fs::FileSystemRc,

View file

@ -1,7 +1,10 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { primordials } from "ext:core/mod.js";
import Dirent from "ext:deno_node/_fs/_fs_dirent.ts";
import {
type Dirent,
direntFromDeno,
} from "ext:deno_node/internal/fs/utils.mjs";
import { assert } from "ext:deno_node/_util/asserts.ts";
import { ERR_MISSING_ARGS } from "ext:deno_node/internal/errors.ts";
import { TextDecoder } from "ext:deno_web/08_text_encoding.js";
@ -47,12 +50,12 @@ export default class Dir {
AsyncGeneratorPrototypeNext(this.#asyncIterator),
(iteratorResult) => {
resolve(
iteratorResult.done ? null : new Dirent(iteratorResult.value),
iteratorResult.done ? null : direntFromDeno(iteratorResult.value),
);
if (callback) {
callback(
null,
iteratorResult.done ? null : new Dirent(iteratorResult.value),
iteratorResult.done ? null : direntFromDeno(iteratorResult.value),
);
}
},
@ -75,7 +78,7 @@ export default class Dir {
if (iteratorResult.done) {
return null;
} else {
return new Dirent(iteratorResult.value);
return direntFromDeno(iteratorResult.value);
}
}

View file

@ -1,55 +0,0 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { notImplemented } from "ext:deno_node/_utils.ts";
export default class Dirent {
constructor(private entry: Deno.DirEntry & { parentPath: string }) {}
isBlockDevice(): boolean {
notImplemented("Deno does not yet support identification of block devices");
return false;
}
isCharacterDevice(): boolean {
notImplemented(
"Deno does not yet support identification of character devices",
);
return false;
}
isDirectory(): boolean {
return this.entry.isDirectory;
}
isFIFO(): boolean {
notImplemented(
"Deno does not yet support identification of FIFO named pipes",
);
return false;
}
isFile(): boolean {
return this.entry.isFile;
}
isSocket(): boolean {
notImplemented("Deno does not yet support identification of sockets");
return false;
}
isSymbolicLink(): boolean {
return this.entry.isSymlink;
}
get name(): string | null {
return this.entry.name;
}
get parentPath(): string {
return this.entry.parentPath;
}
/** @deprecated */
get path(): string {
return this.parentPath;
}
}

File diff suppressed because it is too large Load diff

View file

@ -4,18 +4,17 @@
// deno-lint-ignore-file prefer-primordials
import { TextDecoder, TextEncoder } from "ext:deno_web/08_text_encoding.js";
import Dirent from "ext:deno_node/_fs/_fs_dirent.ts";
import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts";
import { getValidatedPath } from "ext:deno_node/internal/fs/utils.mjs";
import {
type Dirent,
direntFromDeno,
getValidatedPath,
} from "ext:deno_node/internal/fs/utils.mjs";
import { Buffer } from "node:buffer";
import { promisify } from "ext:deno_node/internal/util.mjs";
import { op_fs_read_dir_async, op_fs_read_dir_sync } from "ext:core/ops";
import { join, relative } from "node:path";
function toDirent(val: Deno.DirEntry & { parentPath: string }): Dirent {
return new Dirent(val);
}
type readDirOptions = {
encoding?: string;
withFileTypes?: boolean;
@ -83,7 +82,7 @@ export function readdir(
if (options?.withFileTypes) {
entry.parentPath = current;
result.push(toDirent(entry));
result.push(direntFromDeno(entry));
} else {
let name = decode(entry.name, options?.encoding);
if (options?.recursive) {
@ -166,7 +165,7 @@ export function readdirSync(
if (options?.withFileTypes) {
entry.parentPath = current;
result.push(toDirent(entry));
result.push(direntFromDeno(entry));
} else {
let name = decode(entry.name, options?.encoding);
if (options?.recursive) {

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,6 @@ import {
} from "ext:deno_node/_fs/_fs_copy.ts";
import { cp, cpPromise, cpSync } from "ext:deno_node/_fs/_fs_cp.js";
import Dir from "ext:deno_node/_fs/_fs_dir.ts";
import Dirent from "ext:deno_node/_fs/_fs_dirent.ts";
import { exists, existsSync } from "ext:deno_node/_fs/_fs_exists.ts";
import { fchmod, fchmodSync } from "ext:deno_node/_fs/_fs_fchmod.ts";
import { fchown, fchownSync } from "ext:deno_node/_fs/_fs_fchown.ts";
@ -140,7 +139,11 @@ import {
ReadStream,
WriteStream,
} from "ext:deno_node/internal/fs/streams.mjs";
import { toUnixTimestamp as _toUnixTimestamp } from "ext:deno_node/internal/fs/utils.mjs";
import {
Dirent,
toUnixTimestamp as _toUnixTimestamp,
} from "ext:deno_node/internal/fs/utils.mjs";
import { glob, globPromise, globSync } from "ext:deno_node/_fs/_fs_glob.ts";
const {
F_OK,
@ -168,6 +171,7 @@ const promises = {
constants,
copyFile: copyFilePromise,
cp: cpPromise,
glob: globPromise,
open: openPromise,
opendir: opendirPromise,
rename: renamePromise,
@ -235,6 +239,8 @@ export default {
ftruncateSync,
futimes,
futimesSync,
glob,
globSync,
lchmod,
lchmodSync,
lchown,
@ -356,6 +362,8 @@ export {
ftruncateSync,
futimes,
futimesSync,
glob,
globSync,
lchmod,
lchmodSync,
link,

View file

@ -32,5 +32,6 @@ export const appendFile = fsPromises.appendFile;
export const readFile = fsPromises.readFile;
export const watch = fsPromises.watch;
export const cp = fsPromises.cp;
export const glob = fsPromises.glob;
export default fsPromises;

View file

@ -159,8 +159,9 @@ export function assertEncoding(encoding) {
}
export class Dirent {
constructor(name, type) {
constructor(name, type, path) {
this.name = name;
this.parentPath = path;
this[kType] = type;
}
@ -193,9 +194,23 @@ export class Dirent {
}
}
class DirentFromStats extends Dirent {
constructor(name, stats) {
super(name, null);
export function direntFromDeno(entry) {
let type;
if (entry.isDirectory) {
type = UV_DIRENT_DIR;
} else if (entry.isFile) {
type = UV_DIRENT_FILE;
} else if (entry.isSymlink) {
type = UV_DIRENT_LINK;
}
return new Dirent(entry.name, type, entry.parentPath);
}
export class DirentFromStats extends Dirent {
constructor(name, stats, path) {
super(name, null, path);
this[kStats] = stats;
}
}
@ -277,13 +292,13 @@ export function getDirents(path, { 0: names, 1: types }, callback) {
callback(err);
return;
}
names[idx] = new DirentFromStats(name, stats);
names[idx] = new DirentFromStats(name, stats, path);
if (--toFinish === 0) {
callback(null, names);
}
});
} else {
names[i] = new Dirent(names[i], types[i]);
names[i] = new Dirent(names[i], types[i], path);
}
}
if (toFinish === 0) {
@ -313,16 +328,16 @@ export function getDirent(path, name, type, callback) {
callback(err);
return;
}
callback(null, new DirentFromStats(name, stats));
callback(null, new DirentFromStats(name, stats, path));
});
} else {
callback(null, new Dirent(name, type));
callback(null, new Dirent(name, type, path));
}
} else if (type === UV_DIRENT_UNKNOWN) {
const stats = lstatSync(join(path, name));
return new DirentFromStats(name, stats);
return new DirentFromStats(name, stats, path);
} else {
return new Dirent(name, type);
return new Dirent(name, type, path);
}
}

View file

@ -22,7 +22,6 @@ util::unit_test_factory!(
_fs_close_test = _fs / _fs_close_test,
_fs_copy_test = _fs / _fs_copy_test,
_fs_dir_test = _fs / _fs_dir_test,
_fs_dirent_test = _fs / _fs_dirent_test,
_fs_open_test = _fs / _fs_open_test,
_fs_read_test = _fs / _fs_read_test,
_fs_exists_test = _fs / _fs_exists_test,

View file

@ -1354,7 +1354,8 @@
"sequential/test-debugger-pid.js" = {}
"sequential/test-diagnostic-dir-cpu-prof.js" = {}
"sequential/test-diagnostic-dir-heap-prof.js" = {}
"sequential/test-fs-readdir-recursive.js" = {}
# TODO(kt3k): Enable this when node_test is updated to v24.x
# "sequential/test-fs-readdir-recursive.js" = {}
"sequential/test-fs-stat-sync-overflow.js" = {}
"sequential/test-http-server-keep-alive-timeout-slow-server.js" = {}
"sequential/test-inspector-open-dispose.mjs" = {}

View file

@ -1,95 +0,0 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { assert, assertEquals, assertThrows } from "@std/assert";
import { Dirent as Dirent_ } from "node:fs";
// deno-lint-ignore no-explicit-any
const Dirent = Dirent_ as any;
class DirEntryMock implements Deno.DirEntry {
parentPath = "";
name = "";
isFile = false;
isDirectory = false;
isSymlink = false;
}
Deno.test({
name: "Directories are correctly identified",
fn() {
const entry: DirEntryMock = new DirEntryMock();
entry.isDirectory = true;
entry.isFile = false;
entry.isSymlink = false;
assert(new Dirent(entry).isDirectory());
assert(!new Dirent(entry).isFile());
assert(!new Dirent(entry).isSymbolicLink());
},
});
Deno.test({
name: "Files are correctly identified",
fn() {
const entry: DirEntryMock = new DirEntryMock();
entry.isDirectory = false;
entry.isFile = true;
entry.isSymlink = false;
assert(!new Dirent(entry).isDirectory());
assert(new Dirent(entry).isFile());
assert(!new Dirent(entry).isSymbolicLink());
},
});
Deno.test({
name: "Symlinks are correctly identified",
fn() {
const entry: DirEntryMock = new DirEntryMock();
entry.isDirectory = false;
entry.isFile = false;
entry.isSymlink = true;
assert(!new Dirent(entry).isDirectory());
assert(!new Dirent(entry).isFile());
assert(new Dirent(entry).isSymbolicLink());
},
});
Deno.test({
name: "File name is correct",
fn() {
const entry: DirEntryMock = new DirEntryMock();
entry.name = "my_file";
assertEquals(new Dirent(entry).name, "my_file");
},
});
Deno.test({
name: "Socket and FIFO pipes aren't yet available",
fn() {
const entry: DirEntryMock = new DirEntryMock();
assertThrows(
() => {
new Dirent(entry).isFIFO();
},
Error,
"does not yet support",
);
assertThrows(
() => {
new Dirent(entry).isSocket();
},
Error,
"does not yet support",
);
},
});
Deno.test({
name: "Path and parent path is correct",
fn() {
const entry: DirEntryMock = new DirEntryMock();
entry.name = "my_file";
entry.parentPath = "/home/user";
assertEquals(new Dirent(entry).name, "my_file");
assertEquals(new Dirent(entry).path, "/home/user");
assertEquals(new Dirent(entry).parentPath, "/home/user");
},
});

View file

@ -44,6 +44,7 @@ export async function checkCopyright() {
":!:tests/unit_node/testdata/**",
":!:tests/wpt/suite/**",
":!:libs/config/testdata/**",
":!:ext/node/polyfills/deps/**",
// rust
"*.rs",

View file

@ -28,7 +28,6 @@
"ext:deno_node/_fs/_fs_common.ts": "../ext/node/polyfills/_fs/_fs_common.ts",
"ext:deno_node/_fs/_fs_constants.ts": "../ext/node/polyfills/_fs/_fs_constants.ts",
"ext:deno_node/_fs/_fs_dir.ts": "../ext/node/polyfills/_fs/_fs_dir.ts",
"ext:deno_node/_fs/_fs_dirent.ts": "../ext/node/polyfills/_fs/_fs_dirent.ts",
"ext:deno_node/_fs/_fs_exists.ts": "../ext/node/polyfills/_fs/_fs_exists.ts",
"ext:deno_node/_fs/_fs_lstat.ts": "../ext/node/polyfills/_fs/_fs_lstat.ts",
"ext:deno_node/_fs/_fs_mkdir.ts": "../ext/node/polyfills/_fs/_fs_mkdir.ts",

26
tools/generate_minimatch_dep.js Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env -S deno run --allow-write --allow-run --allow-env --allow-read
// Copyright 2018-2025 the Deno authors. MIT license.
import { join } from "./util.js";
const dir = await Deno.makeTempDir();
const installCommand = new Deno.Command(Deno.execPath(), {
cwd: dir,
args: ["install", "--node-modules-dir=auto", "npm:esbuild", "npm:minimatch"],
});
await installCommand.output();
const bundleCommand = new Deno.Command(
join(dir, "./node_modules/.bin/esbuild"),
{
cwd: dir,
args: [
"./node_modules/minimatch/dist/commonjs/index.js",
"--bundle",
"--format=esm",
],
},
);
const output = await bundleCommand.output();
await Deno.writeFile("./ext/node/polyfills/deps/minimatch.js", output.stdout);

View file

@ -66,6 +66,7 @@ async function dlint() {
":!:cli/tsc/dts/**",
":!:cli/tsc/*typescript.js",
":!:cli/tsc/compiler.d.ts",
":!:ext/node/polyfills/deps/**",
":!:runtime/examples/",
":!:target/",
":!:tests/ffi/tests/test.js",
@ -121,6 +122,7 @@ async function dlintPreferPrimordials() {
"ext/**/*.ts",
":!:ext/**/*.d.ts",
"ext/node/polyfills/*.mjs",
":!:ext/node/polyfills/deps/**",
]);
if (!sourceFiles.length) {