From 9b44f423902599535cbc127aefd6d126bc4766f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 09:50:56 +0100 Subject: [PATCH 1/6] refactor(ext/node): consolidate node:fs (part 9) Inline `_fs_read.ts`, `_fs_readdir.ts`, `_fs_readFile.ts`, and `_fs_readlink.ts` into the main `fs.ts` module, following the same pattern as parts 1-8. Update imports in `promises.ts`, `_fs_glob.ts`, and `cp_sync.ts` to reference the consolidated location. Co-Authored-By: Claude Opus 4.6 --- ext/node/lib.rs | 4 - ext/node/polyfills/_fs/_fs_glob.ts | 5 +- ext/node/polyfills/_fs/_fs_read.ts | 267 ------- ext/node/polyfills/_fs/_fs_readFile.ts | 271 ------- ext/node/polyfills/_fs/_fs_readdir.ts | 186 ----- ext/node/polyfills/_fs/_fs_readlink.ts | 104 --- ext/node/polyfills/_fs/cp/cp_sync.ts | 2 +- ext/node/polyfills/fs.ts | 826 ++++++++++++++++++++- ext/node/polyfills/internal/fs/promises.ts | 4 +- 9 files changed, 823 insertions(+), 846 deletions(-) delete mode 100644 ext/node/polyfills/_fs/_fs_read.ts delete mode 100644 ext/node/polyfills/_fs/_fs_readFile.ts delete mode 100644 ext/node/polyfills/_fs/_fs_readdir.ts delete mode 100644 ext/node/polyfills/_fs/_fs_readlink.ts diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 4c3df2038104a1..6a18488a7066ad 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -386,10 +386,6 @@ deno_core::extension!(deno_node, "_fs/_fs_glob.ts", "_fs/_fs_lstat.ts", "_fs/_fs_lutimes.ts", - "_fs/_fs_read.ts", - "_fs/_fs_readdir.ts", - "_fs/_fs_readFile.ts", - "_fs/_fs_readlink.ts", "_next_tick.ts", "_process/exiting.ts", "_process/process.ts", diff --git a/ext/node/polyfills/_fs/_fs_glob.ts b/ext/node/polyfills/_fs/_fs_glob.ts index 8d71b294962777..39e00f00c76ed8 100644 --- a/ext/node/polyfills/_fs/_fs_glob.ts +++ b/ext/node/polyfills/_fs/_fs_glob.ts @@ -13,10 +13,7 @@ import { isMacOS, isWindows } from "ext:deno_node/_util/os.ts"; import { kEmptyObject } from "ext:deno_node/internal/util.mjs"; import process from "node:process"; -import { - readdirPromise as readdir, - readdirSync, -} from "ext:deno_node/_fs/_fs_readdir.ts"; +import { readdirPromise as readdir, readdirSync } from "node:fs"; import { lstatPromise as lstat, lstatSync, diff --git a/ext/node/polyfills/_fs/_fs_read.ts b/ext/node/polyfills/_fs/_fs_read.ts deleted file mode 100644 index e983213e12dd5b..00000000000000 --- a/ext/node/polyfills/_fs/_fs_read.ts +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright 2018-2026 the Deno authors. MIT license. - -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - -import { Buffer } from "node:buffer"; -import { ERR_INVALID_ARG_VALUE } from "ext:deno_node/internal/errors.ts"; -import * as io from "ext:deno_io/12_io.js"; -import { - arrayBufferViewToUint8Array, - getValidatedFd, - validateOffsetLengthRead, - validatePosition, -} from "ext:deno_node/internal/fs/utils.mjs"; -import { - validateBuffer, - validateFunction, - validateInteger, - validateObject, -} from "ext:deno_node/internal/validators.mjs"; -import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; -import { op_fs_seek_async, op_fs_seek_sync } from "ext:core/ops"; -import { primordials } from "ext:core/mod.js"; -import { - customPromisifyArgs, - kEmptyObject, -} from "ext:deno_node/internal/util.mjs"; -import * as process from "node:process"; -import type { ReadAsyncOptions, ReadSyncOptions } from "node:fs"; - -const { ObjectDefineProperty } = primordials; - -const validateOptionArgs = { __proto__: null, nullable: true }; - -type BinaryCallback = ( - err: Error | null, - bytesRead: number | null, - data?: ArrayBufferView, -) => void; -type Callback = BinaryCallback; - -export function read(fd: number, callback: Callback): void; -export function read( - fd: number, - options: ReadAsyncOptions, - callback: Callback, -): void; -export function read( - fd: number, - buffer: ArrayBufferView, - options: ReadSyncOptions, - callback: Callback, -): void; -export function read( - fd: number, - buffer: ArrayBufferView, - offset: number, - length: number, - position: number | null, - callback: Callback, -): void; -export function read( - fd: number, - optOrBufferOrCb?: - | ArrayBufferView - | ReadAsyncOptions - | Callback, - offsetOrOpt?: - | number - | ReadAsyncOptions - | Callback, - lengthOrCb?: number | Callback, - position?: number | null, - callback?: Callback, -) { - fd = getValidatedFd(fd); - - let offset = offsetOrOpt; - let buffer = optOrBufferOrCb; - let length = lengthOrCb; - let params = null; - if (arguments.length <= 4) { - if (arguments.length === 4) { - // This is fs.read(fd, buffer, options, callback) - validateObject(offsetOrOpt, "options", validateOptionArgs); - callback = length as Callback; - params = offsetOrOpt; - } else if (arguments.length === 3) { - // This is fs.read(fd, bufferOrParams, callback) - if (!isArrayBufferView(buffer)) { - // This is fs.read(fd, params, callback) - params = buffer; - ({ buffer = Buffer.alloc(16384) } = params ?? kEmptyObject); - } - callback = offsetOrOpt as Callback; - } else { - // This is fs.read(fd, callback) - callback = buffer as Callback; - buffer = Buffer.alloc(16384); - } - - if (params !== undefined) { - validateObject(params, "options", validateOptionArgs); - } - ({ - offset = 0, - length = buffer?.byteLength - (offset as number), - position = null, - } = params ?? kEmptyObject); - } - - validateBuffer(buffer); - validateFunction(callback, "cb"); - - if (offset == null) { - offset = 0; - } else { - validateInteger(offset, "offset", 0); - } - - (length as number) |= 0; - - if (length === 0) { - return process.nextTick(function tick() { - callback!(null, 0, buffer); - }); - } - - if (buffer.byteLength === 0) { - throw new ERR_INVALID_ARG_VALUE( - "buffer", - buffer, - "is empty and cannot be written", - ); - } - - validateOffsetLengthRead(offset, length, buffer.byteLength); - - if (position == null) { - position = -1; - } else { - validatePosition(position, "position", length as number); - } - - (async () => { - try { - let nread: number | null; - if (typeof position === "number" && position >= 0) { - const currentPosition = await op_fs_seek_async( - fd, - 0, - io.SeekMode.Current, - ); - // We use sync calls below to avoid being affected by others during - // these calls. - op_fs_seek_sync(fd, position, io.SeekMode.Start); - nread = io.readSync( - fd, - arrayBufferViewToUint8Array(buffer).subarray( - offset, - offset + (length as number), - ), - ); - op_fs_seek_sync(fd, currentPosition, io.SeekMode.Start); - } else { - nread = await io.read( - fd, - arrayBufferViewToUint8Array(buffer).subarray( - offset, - offset + (length as number), - ), - ); - } - callback!(null, nread ?? 0, buffer); - } catch (error) { - callback!(error as Error, null); - } - })(); -} - -ObjectDefineProperty(read, customPromisifyArgs, { - __proto__: null, - value: ["bytesRead", "buffer"], - enumerable: false, -}); - -export function readSync( - fd: number, - buffer: ArrayBufferView, - offset: number, - length: number, - position: number | null, -): number; -export function readSync( - fd: number, - buffer: ArrayBufferView, - opt: ReadSyncOptions, -): number; -export function readSync( - fd: number, - buffer: ArrayBufferView, - offsetOrOpt?: number | ReadSyncOptions, - length?: number, - position?: number | null, -): number { - fd = getValidatedFd(fd); - - validateBuffer(buffer); - - let offset = offsetOrOpt; - if (arguments.length <= 3 || typeof offsetOrOpt === "object") { - if (offsetOrOpt !== undefined) { - validateObject(offsetOrOpt, "options", validateOptionArgs); - } - - ({ - offset = 0, - length = buffer.byteLength - (offset as number), - position = null, - } = offsetOrOpt ?? kEmptyObject); - } - - if (offset === undefined) { - offset = 0; - } else { - validateInteger(offset, "offset", 0); - } - - length! |= 0; - - if (length === 0) { - return 0; - } - - if (buffer.byteLength === 0) { - throw new ERR_INVALID_ARG_VALUE( - "buffer", - buffer, - "is empty and cannot be written", - ); - } - - validateOffsetLengthRead(offset, length, buffer.byteLength); - - if (position == null) { - position = -1; - } else { - validatePosition(position, "position", length); - } - - let currentPosition = 0; - if (typeof position === "number" && position >= 0) { - currentPosition = op_fs_seek_sync(fd, 0, io.SeekMode.Current); - op_fs_seek_sync(fd, position, io.SeekMode.Start); - } - - const numberOfBytesRead = io.readSync( - fd, - arrayBufferViewToUint8Array(buffer).subarray(offset, offset + length!), - ); - - if (typeof position === "number" && position >= 0) { - op_fs_seek_sync(fd, currentPosition, io.SeekMode.Start); - } - - return numberOfBytesRead ?? 0; -} diff --git a/ext/node/polyfills/_fs/_fs_readFile.ts b/ext/node/polyfills/_fs/_fs_readFile.ts deleted file mode 100644 index 9cd5a480d7e757..00000000000000 --- a/ext/node/polyfills/_fs/_fs_readFile.ts +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright 2018-2026 the Deno authors. MIT license. - -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - -import { - BinaryOptionsArgument, - FileOptions, - FileOptionsArgument, - TextOptionsArgument, -} from "ext:deno_node/_fs/_fs_common.ts"; -import { Buffer } from "node:buffer"; -import { readAllSync } from "ext:deno_io/12_io.js"; -import { FileHandle } from "ext:deno_node/internal/fs/handle.ts"; -import { Encodings } from "ext:deno_node/_utils.ts"; -import { FsFile } from "ext:deno_fs/30_fs.js"; -import { - AbortError, - denoErrorToNodeError, - ERR_FS_FILE_TOO_LARGE, -} from "ext:deno_node/internal/errors.ts"; -import { - getOptions, - getValidatedPathToString, - stringToFlags, -} from "ext:deno_node/internal/fs/utils.mjs"; -import * as abortSignal from "ext:deno_web/03_abort_signal.js"; -import { op_fs_read_file_async, op_fs_read_file_sync } from "ext:core/ops"; -import { core, primordials } from "ext:core/mod.js"; -import { constants } from "ext:deno_node/internal/fs/utils.mjs"; -import { S_IFMT, S_IFREG } from "ext:deno_node/_fs/_fs_constants.ts"; - -const { - kIoMaxLength, - kReadFileBufferLength, - kReadFileUnknownBufferLength, -} = constants; - -const { - ArrayPrototypePush, - MathMin, - ObjectPrototypeIsPrototypeOf, - TypedArrayPrototypeGetByteLength, - TypedArrayPrototypeSet, - TypedArrayPrototypeSubarray, - Uint8Array, -} = primordials; - -const defaultOptions = { - __proto__: null, - flag: "r", -}; - -function maybeDecode(data: Uint8Array, encoding: Encodings): string; -function maybeDecode( - data: Uint8Array, - encoding: null | undefined, -): Buffer; -function maybeDecode( - data: Uint8Array, - encoding: Encodings | null | undefined, -): string | Buffer { - const buffer = Buffer.from(data.buffer, data.byteOffset, data.byteLength); - if (encoding) return buffer.toString(encoding); - return buffer; -} - -type TextCallback = (err: Error | null, data?: string) => void; -type BinaryCallback = (err: Error | null, data?: Buffer) => void; -type GenericCallback = (err: Error | null, data?: string | Buffer) => void; -type Callback = TextCallback | BinaryCallback | GenericCallback; -type Path = string | URL | FileHandle | number; - -async function readFileAsync( - path: string, - options: FileOptions | undefined, -): Promise { - let cancelRid: number | undefined; - let abortHandler: (rid: number) => void; - const flagsNumber = stringToFlags(options!.flag, "options.flag"); - if (options?.signal) { - options.signal.throwIfAborted(); - cancelRid = core.createCancelHandle(); - abortHandler = () => core.tryClose(cancelRid as number); - options.signal[abortSignal.add](abortHandler); - } - - try { - const read = await op_fs_read_file_async( - path, - cancelRid, - flagsNumber, - ); - return read; - } finally { - if (options?.signal) { - options.signal[abortSignal.remove](abortHandler); - - // always throw the abort error when aborted - options.signal.throwIfAborted(); - } - } -} - -function checkAborted(signal: AbortSignal | undefined) { - if (signal?.aborted) { - throw new AbortError(undefined, { cause: signal.reason }); - } -} - -function concatBuffers(buffers: Uint8Array[]): Uint8Array { - let totalLen = 0; - for (let i = 0; i < buffers.length; ++i) { - totalLen += TypedArrayPrototypeGetByteLength(buffers[i]); - } - - const contents = new Uint8Array(totalLen); - let n = 0; - for (let i = 0; i < buffers.length; ++i) { - const buf = buffers[i]; - TypedArrayPrototypeSet(contents, buf, n); - n += TypedArrayPrototypeGetByteLength(buf); - } - - return contents; -} - -async function fsFileReadAll(fsFile: FsFile, options?: FileOptions) { - const signal = options?.signal; - const encoding = options?.encoding; - checkAborted(signal); - - const statFields = await fsFile.stat(); - checkAborted(signal); - - let size = 0; - let length = 0; - if ((statFields.mode & S_IFMT) === S_IFREG) { - size = statFields.size; - length = encoding ? MathMin(size, kReadFileBufferLength) : size; - } - if (length === 0) { - length = kReadFileUnknownBufferLength; - } - - if (size > kIoMaxLength) { - throw new ERR_FS_FILE_TOO_LARGE(size); - } - - const buffer = new Uint8Array(length); - const buffers: Uint8Array[] = []; - - while (true) { - checkAborted(signal); - const read = await fsFile.read(buffer); - if (typeof read !== "number") { - break; - } - ArrayPrototypePush(buffers, TypedArrayPrototypeSubarray(buffer, 0, read)); - } - - return concatBuffers(buffers); -} - -export function readFile( - path: Path, - options: TextOptionsArgument, - callback: TextCallback, -): void; -export function readFile( - path: Path, - options: BinaryOptionsArgument, - callback: BinaryCallback, -): void; -export function readFile( - path: Path, - options: null | undefined | FileOptionsArgument, - callback: BinaryCallback, -): void; -export function readFile(path: string | URL, callback: BinaryCallback): void; -export function readFile( - pathOrRid: Path, - optOrCallback?: FileOptionsArgument | Callback | null | undefined, - callback?: Callback, -) { - if (ObjectPrototypeIsPrototypeOf(FileHandle.prototype, pathOrRid)) { - pathOrRid = (pathOrRid as FileHandle).fd; - } else if (typeof pathOrRid !== "number") { - pathOrRid = getValidatedPathToString(pathOrRid as string); - } - - let cb: Callback | undefined; - if (typeof optOrCallback === "function") { - cb = optOrCallback; - } else { - cb = callback; - } - - const options = getOptions(optOrCallback, defaultOptions); - - let p: Promise; - if (typeof pathOrRid === "string") { - p = readFileAsync(pathOrRid, options); - } else { - const fsFile = new FsFile(pathOrRid, Symbol.for("Deno.internal.FsFile")); - p = fsFileReadAll(fsFile, options); - } - - if (cb) { - p.then( - (data: Uint8Array) => { - const textOrBuffer = maybeDecode(data, options?.encoding); - (cb as BinaryCallback)(null, textOrBuffer); - }, - (err) => - cb && - cb( - denoErrorToNodeError(err, { - path: typeof pathOrRid === "string" ? pathOrRid : undefined, - syscall: "open", - }), - ), - ); - } -} - -export function readFilePromise( - path: Path, - options?: FileOptionsArgument | null | undefined, - // deno-lint-ignore no-explicit-any -): Promise { - return new Promise((resolve, reject) => { - readFile(path, options, (err, data) => { - if (err) reject(err); - else resolve(data); - }); - }); -} - -export function readFileSync( - path: string | URL | number, - opt: TextOptionsArgument, -): string; -export function readFileSync( - path: string | URL | number, - opt?: BinaryOptionsArgument, -): Buffer; -export function readFileSync( - path: string | URL | number, - opt?: FileOptionsArgument, -): string | Buffer { - const options = getOptions(opt, defaultOptions); - - let data; - if (typeof path === "number") { - const fsFile = new FsFile(path, Symbol.for("Deno.internal.FsFile")); - data = readAllSync(fsFile); - } else { - // Validate/convert path to string (throws on invalid types) - path = getValidatedPathToString(path as unknown as string); - - const flagsNumber = stringToFlags(options?.flag, "options.flag"); - try { - data = op_fs_read_file_sync(path, flagsNumber); - } catch (err) { - throw denoErrorToNodeError(err, { path, syscall: "open" }); - } - } - const textOrBuffer = maybeDecode(data, options?.encoding); - return textOrBuffer; -} diff --git a/ext/node/polyfills/_fs/_fs_readdir.ts b/ext/node/polyfills/_fs/_fs_readdir.ts deleted file mode 100644 index f2f07416861d1a..00000000000000 --- a/ext/node/polyfills/_fs/_fs_readdir.ts +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2018-2026 the Deno authors. MIT license. - -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - -import { TextDecoder, TextEncoder } from "ext:deno_web/08_text_encoding.js"; -import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts"; -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"; - -type readDirOptions = { - encoding?: string; - withFileTypes?: boolean; - recursive?: boolean; -}; - -type readDirCallback = (err: Error | null, files: string[]) => void; - -type readDirCallbackDirent = (err: Error | null, files: Dirent[]) => void; - -type readDirBoth = ( - ...args: [Error] | [null, string[] | Dirent[] | Array] -) => void; - -export function readdir( - path: string | Buffer | URL, - options: readDirOptions, - callback: readDirCallback, -): void; -export function readdir( - path: string | Buffer | URL, - options: readDirOptions, - callback: readDirCallbackDirent, -): void; -export function readdir(path: string | URL, callback: readDirCallback): void; -export function readdir( - path: string | Buffer | 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; - path = getValidatedPath(path).toString(); - - if (!callback) throw new Error("No callback function supplied"); - - if (options?.encoding) { - try { - new TextDecoder(options.encoding); - } catch { - throw new Error( - `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, - ); - } - } - - const result: Array = []; - const dirs = [path]; - let current: string | undefined; - (async () => { - while ((current = dirs.shift()) !== undefined) { - try { - const entries = await op_fs_read_dir_async(current); - - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (options?.recursive && entry.isDirectory) { - dirs.push(join(current, entry.name)); - } - - if (options?.withFileTypes) { - entry.parentPath = current; - result.push(direntFromDeno(entry)); - } else { - let name = decode(entry.name, options?.encoding); - if (options?.recursive) { - name = relative(path, join(current, name)); - } - result.push(name); - } - } - } catch (err) { - callback( - denoErrorToNodeError(err as Error, { - syscall: "readdir", - path: current, - }), - ); - return; - } - } - - callback(null, 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 const readdirPromise = promisify(readdir) as ( - & ((path: string | Buffer | URL, options: { - withFileTypes: true; - encoding?: string; - }) => Promise) - & ((path: string | Buffer | URL, options?: { - withFileTypes?: false; - encoding?: string; - }) => Promise) -); - -export function readdirSync( - path: string | Buffer | URL, - options: { withFileTypes: true; encoding?: string }, -): Dirent[]; -export function readdirSync( - path: string | Buffer | URL, - options?: { withFileTypes?: false; encoding?: string }, -): string[]; -export function readdirSync( - path: string | Buffer | URL, - options?: readDirOptions, -): Array { - path = getValidatedPath(path).toString(); - - if (options?.encoding) { - try { - new TextDecoder(options.encoding); - } catch { - throw new Error( - `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, - ); - } - } - - const result: Array = []; - const dirs = [path]; - let current: string | undefined; - while ((current = dirs.shift()) !== undefined) { - try { - const entries = op_fs_read_dir_sync(current); - - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (options?.recursive && entry.isDirectory) { - dirs.push(join(current, entry.name)); - } - - if (options?.withFileTypes) { - entry.parentPath = current; - result.push(direntFromDeno(entry)); - } else { - let name = decode(entry.name, options?.encoding); - if (options?.recursive) { - name = relative(path, join(current, name)); - } - result.push(name); - } - } - } catch (e) { - throw denoErrorToNodeError(e as Error, { - syscall: "readdir", - path: current, - }); - } - } - - return result; -} diff --git a/ext/node/polyfills/_fs/_fs_readlink.ts b/ext/node/polyfills/_fs/_fs_readlink.ts deleted file mode 100644 index af6d76dda9cc77..00000000000000 --- a/ext/node/polyfills/_fs/_fs_readlink.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2018-2026 the Deno authors. MIT license. - -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file prefer-primordials - -import { TextEncoder } from "ext:deno_web/08_text_encoding.js"; -import { MaybeEmpty, notImplemented } from "ext:deno_node/_utils.ts"; -import { promisify } from "ext:deno_node/internal/util.mjs"; -import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts"; -import { Buffer } from "node:buffer"; -import { getValidatedPathToString } from "ext:deno_node/internal/fs/utils.mjs"; -import { makeCallback } from "ext:deno_node/_fs/_fs_common.ts"; - -type ReadlinkCallback = ( - err: MaybeEmpty, - linkString: MaybeEmpty, -) => void; - -interface ReadlinkOptions { - encoding?: string | null; -} - -function maybeEncode( - data: string, - encoding: string | null, -): string | Uint8Array { - if (encoding === "buffer") { - return new TextEncoder().encode(data); - } - return data; -} - -function getEncoding( - optOrCallback?: ReadlinkOptions | ReadlinkCallback, -): string | null { - if (!optOrCallback || typeof optOrCallback === "function") { - return null; - } else { - if (optOrCallback.encoding) { - if ( - optOrCallback.encoding === "utf8" || - optOrCallback.encoding === "utf-8" - ) { - return "utf8"; - } else if (optOrCallback.encoding === "buffer") { - return "buffer"; - } else { - notImplemented(`fs.readlink encoding=${optOrCallback.encoding}`); - } - } - return null; - } -} - -export function readlink( - path: string | Buffer | URL, - optOrCallback: ReadlinkCallback | ReadlinkOptions, - callback?: ReadlinkCallback, -) { - path = getValidatedPathToString(path); - - let cb: ReadlinkCallback | undefined; - if (typeof optOrCallback === "function") { - cb = optOrCallback; - } else { - cb = callback; - } - cb = makeCallback(cb); - - const encoding = getEncoding(optOrCallback); - - Deno.readLink(path).then((data: string) => { - const res = maybeEncode(data, encoding); - if (cb) cb(null, res); - }, (err: Error) => { - if (cb) { - (cb as (e: Error) => void)(denoErrorToNodeError(err, { - syscall: "readlink", - path, - })); - } - }); -} - -export const readlinkPromise = promisify(readlink) as ( - path: string | Buffer | URL, - opt?: ReadlinkOptions, -) => Promise; - -export function readlinkSync( - path: string | Buffer | URL, - opt?: ReadlinkOptions, -): string | Uint8Array { - path = getValidatedPathToString(path); - - try { - return maybeEncode(Deno.readLinkSync(path), getEncoding(opt)); - } catch (error) { - throw denoErrorToNodeError(error, { - syscall: "readlink", - path, - }); - } -} diff --git a/ext/node/polyfills/_fs/cp/cp_sync.ts b/ext/node/polyfills/_fs/cp/cp_sync.ts index 07d271719dcc33..52a5a7ed7add39 100644 --- a/ext/node/polyfills/_fs/cp/cp_sync.ts +++ b/ext/node/polyfills/_fs/cp/cp_sync.ts @@ -5,7 +5,7 @@ import { dirname, isAbsolute, join, parse, resolve } from "node:path"; import { copyFileSync } from "ext:deno_node/_fs/_fs_copy.ts"; import { existsSync } from "ext:deno_node/_fs/_fs_exists.ts"; import { mkdirSync, opendirSync } from "node:fs"; -import { readlinkSync } from "ext:deno_node/_fs/_fs_readlink.ts"; +import { readlinkSync } from "node:fs"; import { symlinkSync, utimesSync } from "node:fs"; import { ERR_FS_CP_DIR_TO_NON_DIR, diff --git a/ext/node/polyfills/fs.ts b/ext/node/polyfills/fs.ts index d3b26d6762d178..25e741a221683f 100644 --- a/ext/node/polyfills/fs.ts +++ b/ext/node/polyfills/fs.ts @@ -2,12 +2,16 @@ import { fs as fsConstants } from "ext:deno_node/internal_binding/constants.ts"; import { codeMap } from "ext:deno_node/internal_binding/uv.ts"; import { + type BinaryOptionsArgument, type CallbackWithError, + type FileOptions, + type FileOptionsArgument, getValidatedEncoding, isFd, isFileOptions, makeCallback, maybeCallback, + type TextOptionsArgument, type WriteFileOptions, } from "ext:deno_node/_fs/_fs_common.ts"; import type { Encodings } from "ext:deno_node/_utils.ts"; @@ -15,6 +19,8 @@ import { AbortError, denoErrorToNodeError, denoWriteFileErrorToNodeError, + ERR_FS_FILE_TOO_LARGE, + ERR_INVALID_ARG_VALUE, } from "ext:deno_node/internal/errors.ts"; import * as constants from "ext:deno_node/_fs/_fs_constants.ts"; @@ -25,12 +31,8 @@ import { exists, existsSync } from "ext:deno_node/_fs/_fs_exists.ts"; import { fstat, fstatSync } from "ext:deno_node/_fs/_fs_fstat.ts"; import { lstat, lstatSync } from "ext:deno_node/_fs/_fs_lstat.ts"; import { lutimes, lutimesSync } from "ext:deno_node/_fs/_fs_lutimes.ts"; -import { read, readSync } from "ext:deno_node/_fs/_fs_read.ts"; -import { readdir, readdirSync } from "ext:deno_node/_fs/_fs_readdir.ts"; -import { readFile, readFileSync } from "ext:deno_node/_fs/_fs_readFile.ts"; -import { readlink, readlinkSync } from "ext:deno_node/_fs/_fs_readlink.ts"; import { EventEmitter } from "node:events"; -import { notImplemented } from "ext:deno_node/_utils.ts"; +import { type MaybeEmpty, notImplemented } from "ext:deno_node/_utils.ts"; import { promisify } from "node:util"; import { delay } from "ext:deno_node/_util/async.ts"; import promises from "ext:deno_node/internal/fs/promises.ts"; @@ -47,6 +49,7 @@ import { constants as fsUtilConstants, copyObject, Dirent, + direntFromDeno, emitRecursiveRmdirWarning, getOptions, getValidatedFd, @@ -59,7 +62,9 @@ import { stringToFlags, toUnixTimestamp as _toUnixTimestamp, validateBufferArray, + validateOffsetLengthRead, validateOffsetLengthWrite, + validatePosition, validateRmdirOptions, validateRmOptions, validateRmOptionsSync, @@ -70,6 +75,7 @@ import { glob, globSync } from "ext:deno_node/_fs/_fs_glob.ts"; import { parseFileMode, validateBoolean, + validateBuffer, validateEncoding, validateFunction, validateInt32, @@ -83,6 +89,8 @@ import process from "node:process"; import * as io from "ext:deno_io/12_io.js"; import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; import { pathFromURL } from "ext:deno_web/00_infra.js"; +import { TextDecoder, TextEncoder } from "ext:deno_web/08_text_encoding.js"; +import * as abortSignal from "ext:deno_web/03_abort_signal.js"; import { URLPrototype } from "ext:deno_web/00_url.js"; import { FileHandle } from "ext:deno_node/internal/fs/handle.ts"; import { isIterable } from "ext:deno_node/internal/streams/utils.js"; @@ -93,7 +101,10 @@ import { op_fs_fchmod_sync, op_fs_fchown_async, op_fs_fchown_sync, + op_fs_read_dir_async, + op_fs_read_dir_sync, op_fs_read_file_async, + op_fs_read_file_sync, op_fs_seek_async, op_fs_seek_sync, op_node_lchmod, @@ -122,7 +133,7 @@ import { kEmptyObject, normalizeEncoding, } from "ext:deno_node/internal/util.mjs"; -import { basename, resolve, toNamespacedPath } from "node:path"; +import { basename, join, relative, resolve, toNamespacedPath } from "node:path"; import * as pathModule from "node:path"; import type { Encoding } from "node:crypto"; import { core, primordials } from "ext:core/mod.js"; @@ -151,7 +162,10 @@ const { StringPrototypeToString, SymbolAsyncIterator, SymbolFor, + ArrayPrototypePush, TypedArrayPrototypeGetByteLength, + TypedArrayPrototypeSet, + TypedArrayPrototypeSubarray, Uint8Array, } = primordials; @@ -174,8 +188,16 @@ const { O_NONBLOCK, O_CREAT, O_EXCL, + S_IFMT, + S_IFREG, } = constants; +const { + kIoMaxLength, + kReadFileBufferLength, + kReadFileUnknownBufferLength, +} = fsUtilConstants; + // -- stat -- import { @@ -489,6 +511,795 @@ function readvPromise( }); } +// -- read -- + +interface ReadSyncOptions { + offset?: number | undefined; + length?: number | undefined; + position?: number | null | undefined; +} + +interface ReadAsyncOptions + extends ReadSyncOptions { + buffer?: TBuffer; +} + +const validateOptionArgs = { __proto__: null, nullable: true }; + +type ReadBinaryCallback = ( + err: Error | null, + bytesRead: number | null, + data?: ArrayBufferView, +) => void; +type ReadCallback = ReadBinaryCallback; + +function read(fd: number, callback: ReadCallback): void; +function read( + fd: number, + options: ReadAsyncOptions, + callback: ReadCallback, +): void; +function read( + fd: number, + buffer: ArrayBufferView, + options: ReadSyncOptions, + callback: ReadCallback, +): void; +function read( + fd: number, + buffer: ArrayBufferView, + offset: number, + length: number, + position: number | null, + callback: ReadCallback, +): void; +function read( + fd: number, + optOrBufferOrCb?: + | ArrayBufferView + | ReadAsyncOptions + | ReadCallback, + offsetOrOpt?: + | number + | ReadAsyncOptions + | ReadCallback, + lengthOrCb?: number | ReadCallback, + position?: number | null, + callback?: ReadCallback, +) { + fd = getValidatedFd(fd); + + let offset = offsetOrOpt; + let buffer = optOrBufferOrCb; + let length = lengthOrCb; + let params = null; + if (arguments.length <= 4) { + if (arguments.length === 4) { + // This is fs.read(fd, buffer, options, callback) + validateObject(offsetOrOpt, "options", validateOptionArgs); + callback = length as ReadCallback; + params = offsetOrOpt; + } else if (arguments.length === 3) { + // This is fs.read(fd, bufferOrParams, callback) + if (!isArrayBufferView(buffer)) { + // This is fs.read(fd, params, callback) + params = buffer; + ({ buffer = Buffer.alloc(16384) } = params ?? kEmptyObject); + } + callback = offsetOrOpt as ReadCallback; + } else { + // This is fs.read(fd, callback) + callback = buffer as ReadCallback; + buffer = Buffer.alloc(16384); + } + + if (params !== undefined) { + validateObject(params, "options", validateOptionArgs); + } + ({ + offset = 0, + // deno-lint-ignore prefer-primordials + length = buffer?.byteLength - (offset as number), + position = null, + } = params ?? kEmptyObject); + } + + validateBuffer(buffer); + validateFunction(callback, "cb"); + + if (offset == null) { + offset = 0; + } else { + validateInteger(offset, "offset", 0); + } + + (length as number) |= 0; + + if (length === 0) { + return process.nextTick(function tick() { + callback!(null, 0, buffer); + }); + } + + // deno-lint-ignore prefer-primordials + if (buffer.byteLength === 0) { + throw new ERR_INVALID_ARG_VALUE( + "buffer", + buffer, + "is empty and cannot be written", + ); + } + + // deno-lint-ignore prefer-primordials + validateOffsetLengthRead(offset, length, buffer.byteLength); + + if (position == null) { + position = -1; + } else { + validatePosition(position, "position", length as number); + } + + (async () => { + try { + let nread: number | null; + if (typeof position === "number" && position >= 0) { + const currentPosition = await op_fs_seek_async( + fd, + 0, + io.SeekMode.Current, + ); + // We use sync calls below to avoid being affected by others during + // these calls. + op_fs_seek_sync(fd, position, io.SeekMode.Start); + nread = io.readSync( + fd, + arrayBufferViewToUint8Array(buffer).subarray( + offset, + offset + (length as number), + ), + ); + op_fs_seek_sync(fd, currentPosition, io.SeekMode.Start); + } else { + nread = await io.read( + fd, + arrayBufferViewToUint8Array(buffer).subarray( + offset, + offset + (length as number), + ), + ); + } + callback!(null, nread ?? 0, buffer); + } catch (error) { + callback!(error as Error, null); + } + })(); +} + +ObjectDefineProperty(read, customPromisifyArgs, { + __proto__: null, + value: ["bytesRead", "buffer"], + enumerable: false, +}); + +function readSync( + fd: number, + buffer: ArrayBufferView, + offset: number, + length: number, + position: number | null, +): number; +function readSync( + fd: number, + buffer: ArrayBufferView, + opt: ReadSyncOptions, +): number; +function readSync( + fd: number, + buffer: ArrayBufferView, + offsetOrOpt?: number | ReadSyncOptions, + length?: number, + position?: number | null, +): number { + fd = getValidatedFd(fd); + + validateBuffer(buffer); + + let offset = offsetOrOpt; + if (arguments.length <= 3 || typeof offsetOrOpt === "object") { + if (offsetOrOpt !== undefined) { + validateObject(offsetOrOpt, "options", validateOptionArgs); + } + + ({ + offset = 0, + // deno-lint-ignore prefer-primordials + length = buffer.byteLength - (offset as number), + position = null, + } = offsetOrOpt ?? kEmptyObject); + } + + if (offset === undefined) { + offset = 0; + } else { + validateInteger(offset, "offset", 0); + } + + length! |= 0; + + if (length === 0) { + return 0; + } + + // deno-lint-ignore prefer-primordials + if (buffer.byteLength === 0) { + throw new ERR_INVALID_ARG_VALUE( + "buffer", + buffer, + "is empty and cannot be written", + ); + } + + // deno-lint-ignore prefer-primordials + validateOffsetLengthRead(offset, length, buffer.byteLength); + + if (position == null) { + position = -1; + } else { + validatePosition(position, "position", length); + } + + let currentPosition = 0; + if (typeof position === "number" && position >= 0) { + currentPosition = op_fs_seek_sync(fd, 0, io.SeekMode.Current); + op_fs_seek_sync(fd, position, io.SeekMode.Start); + } + + const numberOfBytesRead = io.readSync( + fd, + arrayBufferViewToUint8Array(buffer).subarray(offset, offset + length!), + ); + + if (typeof position === "number" && position >= 0) { + op_fs_seek_sync(fd, currentPosition, io.SeekMode.Start); + } + + return numberOfBytesRead ?? 0; +} + +// -- readdir -- + +type readDirOptions = { + encoding?: string; + withFileTypes?: boolean; + recursive?: boolean; +}; + +type readDirCallback = (err: Error | null, files: string[]) => void; + +type readDirCallbackDirent = (err: Error | null, files: Dirent[]) => void; + +type readDirBoth = ( + ...args: [Error] | [null, string[] | Dirent[] | Array] +) => void; + +function readdirDecode(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)); + } +} + +function readdir( + path: string | Buffer | URL, + options: readDirOptions, + callback: readDirCallback, +): void; +function readdir( + path: string | Buffer | URL, + options: readDirOptions, + callback: readDirCallbackDirent, +): void; +function readdir(path: string | URL, callback: readDirCallback): void; +function readdir( + path: string | Buffer | 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; + // deno-lint-ignore prefer-primordials + path = getValidatedPath(path).toString(); + + if (!callback) throw new Error("No callback function supplied"); + + if (options?.encoding) { + try { + new TextDecoder(options.encoding); + } catch { + throw new Error( + `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, + ); + } + } + + const result: Array = []; + const dirs = [path]; + let current: string | undefined; + (async () => { + // deno-lint-ignore prefer-primordials + while ((current = dirs.shift()) !== undefined) { + try { + const entries = await op_fs_read_dir_async(current); + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + if (options?.recursive && entry.isDirectory) { + // deno-lint-ignore prefer-primordials + dirs.push(join(current, entry.name)); + } + + if (options?.withFileTypes) { + entry.parentPath = current; + // deno-lint-ignore prefer-primordials + result.push(direntFromDeno(entry)); + } else { + let name = readdirDecode(entry.name, options?.encoding); + if (options?.recursive) { + name = relative(path, join(current, name)); + } + // deno-lint-ignore prefer-primordials + result.push(name); + } + } + } catch (err) { + callback( + denoErrorToNodeError(err as Error, { + syscall: "readdir", + path: current, + }), + ); + return; + } + } + + callback(null, result); + })(); +} + +const readdirPromise = promisify(readdir) as ( + & ((path: string | Buffer | URL, options: { + withFileTypes: true; + encoding?: string; + }) => Promise) + & ((path: string | Buffer | URL, options?: { + withFileTypes?: false; + encoding?: string; + }) => Promise) +); + +function readdirSync( + path: string | Buffer | URL, + options: { withFileTypes: true; encoding?: string }, +): Dirent[]; +function readdirSync( + path: string | Buffer | URL, + options?: { withFileTypes?: false; encoding?: string }, +): string[]; +function readdirSync( + path: string | Buffer | URL, + options?: readDirOptions, +): Array { + // deno-lint-ignore prefer-primordials + path = getValidatedPath(path).toString(); + + if (options?.encoding) { + try { + new TextDecoder(options.encoding); + } catch { + throw new Error( + `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, + ); + } + } + + const result: Array = []; + const dirs = [path]; + let current: string | undefined; + // deno-lint-ignore prefer-primordials + while ((current = dirs.shift()) !== undefined) { + try { + const entries = op_fs_read_dir_sync(current); + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + if (options?.recursive && entry.isDirectory) { + // deno-lint-ignore prefer-primordials + dirs.push(join(current, entry.name)); + } + + if (options?.withFileTypes) { + entry.parentPath = current; + // deno-lint-ignore prefer-primordials + result.push(direntFromDeno(entry)); + } else { + let name = readdirDecode(entry.name, options?.encoding); + if (options?.recursive) { + name = relative(path, join(current, name)); + } + // deno-lint-ignore prefer-primordials + result.push(name); + } + } + } catch (e) { + throw denoErrorToNodeError(e as Error, { + syscall: "readdir", + path: current, + }); + } + } + + return result; +} + +// -- readFile -- + +const readFileDefaultOptions = { + __proto__: null, + flag: "r", +}; + +function readFileMaybeDecode(data: Uint8Array, encoding: Encodings): string; +function readFileMaybeDecode( + data: Uint8Array, + encoding: null | undefined, +): Buffer; +function readFileMaybeDecode( + data: Uint8Array, + encoding: Encodings | null | undefined, +): string | Buffer { + // deno-lint-ignore prefer-primordials + const buffer = Buffer.from(data.buffer, data.byteOffset, data.byteLength); + // deno-lint-ignore prefer-primordials + if (encoding) return buffer.toString(encoding); + return buffer; +} + +type ReadFileTextCallback = (err: Error | null, data?: string) => void; +type ReadFileBinaryCallback = (err: Error | null, data?: Buffer) => void; +type ReadFileGenericCallback = ( + err: Error | null, + data?: string | Buffer, +) => void; +type ReadFileCallback = + | ReadFileTextCallback + | ReadFileBinaryCallback + | ReadFileGenericCallback; +type ReadFilePath = string | URL | FileHandle | number; + +async function readFileAsync( + path: string, + options: FileOptions | undefined, +): Promise { + let cancelRid: number | undefined; + let abortHandler: (rid: number) => void; + const flagsNumber = stringToFlags(options!.flag, "options.flag"); + if (options?.signal) { + options.signal.throwIfAborted(); + cancelRid = core.createCancelHandle(); + abortHandler = () => core.tryClose(cancelRid as number); + options.signal[abortSignal.add](abortHandler); + } + + try { + const data = await op_fs_read_file_async( + path, + cancelRid, + flagsNumber, + ); + return data; + } finally { + if (options?.signal) { + options.signal[abortSignal.remove](abortHandler); + + // always throw the abort error when aborted + options.signal.throwIfAborted(); + } + } +} + +function readFileCheckAborted(signal: AbortSignal | undefined) { + if (signal?.aborted) { + throw new AbortError(undefined, { cause: signal.reason }); + } +} + +function readFileConcatBuffers(buffers: Uint8Array[]): Uint8Array { + let totalLen = 0; + for (let i = 0; i < buffers.length; ++i) { + totalLen += TypedArrayPrototypeGetByteLength(buffers[i]); + } + + const contents = new Uint8Array(totalLen); + let n = 0; + for (let i = 0; i < buffers.length; ++i) { + const buf = buffers[i]; + TypedArrayPrototypeSet(contents, buf, n); + n += TypedArrayPrototypeGetByteLength(buf); + } + + return contents; +} + +async function fsFileReadAll(fsFile: FsFile, options?: FileOptions) { + const signal = options?.signal; + const encoding = options?.encoding; + readFileCheckAborted(signal); + + const statFields = await fsFile.stat(); + readFileCheckAborted(signal); + + let size = 0; + let length = 0; + if ((statFields.mode & S_IFMT) === S_IFREG) { + size = statFields.size; + length = encoding ? MathMin(size, kReadFileBufferLength) : size; + } + if (length === 0) { + length = kReadFileUnknownBufferLength; + } + + if (size > kIoMaxLength) { + throw new ERR_FS_FILE_TOO_LARGE(size); + } + + const buffer = new Uint8Array(length); + const buffers: Uint8Array[] = []; + + while (true) { + readFileCheckAborted(signal); + const nread = await fsFile.read(buffer); + if (typeof nread !== "number") { + break; + } + ArrayPrototypePush(buffers, TypedArrayPrototypeSubarray(buffer, 0, nread)); + } + + return readFileConcatBuffers(buffers); +} + +function readFile( + path: ReadFilePath, + options: TextOptionsArgument, + callback: ReadFileTextCallback, +): void; +function readFile( + path: ReadFilePath, + options: BinaryOptionsArgument, + callback: ReadFileBinaryCallback, +): void; +function readFile( + path: ReadFilePath, + options: null | undefined | FileOptionsArgument, + callback: ReadFileBinaryCallback, +): void; +function readFile( + path: string | URL, + callback: ReadFileBinaryCallback, +): void; +function readFile( + pathOrRid: ReadFilePath, + optOrCallback?: + | FileOptionsArgument + | ReadFileCallback + | null + | undefined, + callback?: ReadFileCallback, +) { + if (ObjectPrototypeIsPrototypeOf(FileHandle.prototype, pathOrRid)) { + pathOrRid = (pathOrRid as FileHandle).fd; + } else if (typeof pathOrRid !== "number") { + pathOrRid = getValidatedPathToString(pathOrRid as string); + } + + let cb: ReadFileCallback | undefined; + if (typeof optOrCallback === "function") { + cb = optOrCallback; + } else { + cb = callback; + } + + const options = getOptions( + optOrCallback, + readFileDefaultOptions, + ); + + let p: Promise; + if (typeof pathOrRid === "string") { + p = readFileAsync(pathOrRid, options); + } else { + const fsFile = new FsFile( + pathOrRid, + SymbolFor("Deno.internal.FsFile"), + ); + p = fsFileReadAll(fsFile, options); + } + + if (cb) { + PromisePrototypeThen( + p, + (data: Uint8Array) => { + const textOrBuffer = readFileMaybeDecode(data, options?.encoding); + (cb as ReadFileBinaryCallback)(null, textOrBuffer); + }, + (err) => + cb && + cb( + denoErrorToNodeError(err, { + path: typeof pathOrRid === "string" ? pathOrRid : undefined, + syscall: "open", + }), + ), + ); + } +} + +function readFilePromise( + path: ReadFilePath, + options?: FileOptionsArgument | null | undefined, + // deno-lint-ignore no-explicit-any +): Promise { + return new Promise((resolve, reject) => { + readFile(path, options, (err, data) => { + if (err) reject(err); + else resolve(data); + }); + }); +} + +function readFileSync( + path: string | URL | number, + opt: TextOptionsArgument, +): string; +function readFileSync( + path: string | URL | number, + opt?: BinaryOptionsArgument, +): Buffer; +function readFileSync( + path: string | URL | number, + opt?: FileOptionsArgument, +): string | Buffer { + const options = getOptions(opt, readFileDefaultOptions); + + let data; + if (typeof path === "number") { + const fsFile = new FsFile( + path, + SymbolFor("Deno.internal.FsFile"), + ); + data = io.readAllSync(fsFile); + } else { + // Validate/convert path to string (throws on invalid types) + path = getValidatedPathToString(path as unknown as string); + + const flagsNumber = stringToFlags(options?.flag, "options.flag"); + try { + data = op_fs_read_file_sync(path, flagsNumber); + } catch (err) { + throw denoErrorToNodeError(err, { path, syscall: "open" }); + } + } + const textOrBuffer = readFileMaybeDecode(data, options?.encoding); + return textOrBuffer; +} + +// -- readlink -- + +type ReadlinkCallback = ( + err: MaybeEmpty, + linkString: MaybeEmpty, +) => void; + +interface ReadlinkOptions { + encoding?: string | null; +} + +function readlinkMaybeEncode( + data: string, + encoding: string | null, +): string | Uint8Array { + if (encoding === "buffer") { + return new TextEncoder().encode(data); + } + return data; +} + +function readlinkGetEncoding( + optOrCallback?: ReadlinkOptions | ReadlinkCallback, +): string | null { + if (!optOrCallback || typeof optOrCallback === "function") { + return null; + } else { + if (optOrCallback.encoding) { + if ( + optOrCallback.encoding === "utf8" || + optOrCallback.encoding === "utf-8" + ) { + return "utf8"; + } else if (optOrCallback.encoding === "buffer") { + return "buffer"; + } else { + notImplemented(`fs.readlink encoding=${optOrCallback.encoding}`); + } + } + return null; + } +} + +function readlink( + path: string | Buffer | URL, + optOrCallback: ReadlinkCallback | ReadlinkOptions, + callback?: ReadlinkCallback, +) { + path = getValidatedPathToString(path); + + let cb: ReadlinkCallback | undefined; + if (typeof optOrCallback === "function") { + cb = optOrCallback; + } else { + cb = callback; + } + cb = makeCallback(cb); + + const encoding = readlinkGetEncoding(optOrCallback); + + PromisePrototypeThen( + Deno.readLink(path), + (data: string) => { + const res = readlinkMaybeEncode(data, encoding); + if (cb) cb(null, res); + }, + (err: Error) => { + if (cb) { + (cb as (e: Error) => void)(denoErrorToNodeError(err, { + syscall: "readlink", + path, + })); + } + }, + ); +} + +const readlinkPromise = promisify(readlink) as ( + path: string | Buffer | URL, + opt?: ReadlinkOptions, +) => Promise; + +function readlinkSync( + path: string | Buffer | URL, + opt?: ReadlinkOptions, +): string | Uint8Array { + path = getValidatedPathToString(path); + + try { + return readlinkMaybeEncode( + Deno.readLinkSync(path), + readlinkGetEncoding(opt), + ); + } catch (error) { + throw denoErrorToNodeError(error, { + syscall: "readlink", + path, + }); + } +} + // -- statfs -- type StatFsCallback = (err: Error | null, stats?: StatFs) => void; @@ -3221,10 +4032,13 @@ export { R_OK, read, readdir, + readdirPromise, readdirSync, readFile, + readFilePromise, readFileSync, readlink, + readlinkPromise, readlinkSync, ReadStream, readSync, diff --git a/ext/node/polyfills/internal/fs/promises.ts b/ext/node/polyfills/internal/fs/promises.ts index 4794031e9cf756..9b0701159562ab 100644 --- a/ext/node/polyfills/internal/fs/promises.ts +++ b/ext/node/polyfills/internal/fs/promises.ts @@ -7,9 +7,7 @@ import * as constants from "ext:deno_node/_fs/_fs_constants.ts"; import { copyFilePromise } from "ext:deno_node/_fs/_fs_copy.ts"; import { cpPromise } from "ext:deno_node/_fs/_fs_cp.ts"; import { lutimesPromise } from "ext:deno_node/_fs/_fs_lutimes.ts"; -import { readdirPromise } from "ext:deno_node/_fs/_fs_readdir.ts"; -import { readFilePromise } from "ext:deno_node/_fs/_fs_readFile.ts"; -import { readlinkPromise } from "ext:deno_node/_fs/_fs_readlink.ts"; +import { readdirPromise, readFilePromise, readlinkPromise } from "node:fs"; import { lstatPromise } from "ext:deno_node/_fs/_fs_lstat.ts"; import { access, From 5e0847d92dc41c0a1a8f25a60806138992195ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 18:16:25 +0100 Subject: [PATCH 2/6] refactor(ext/node): consolidate _fs_readFile.ts and _fs_readlink.ts into fs.ts Inline `_fs_readFile.ts` and `_fs_readlink.ts` into the main `fs.ts` module. Skip `_fs_read.ts` and `_fs_readdir.ts` for now as they cause circular dependency issues when imported from `_fs_glob.ts` and `promises.ts`. Co-Authored-By: Claude Opus 4.6 --- ext/node/lib.rs | 2 + ext/node/polyfills/_fs/_fs_glob.ts | 5 +- ext/node/polyfills/_fs/_fs_read.ts | 267 ++++++++++++ ext/node/polyfills/_fs/_fs_readdir.ts | 186 +++++++++ ext/node/polyfills/fs.ts | 455 +-------------------- ext/node/polyfills/internal/fs/promises.ts | 13 +- 6 files changed, 478 insertions(+), 450 deletions(-) create mode 100644 ext/node/polyfills/_fs/_fs_read.ts create mode 100644 ext/node/polyfills/_fs/_fs_readdir.ts diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 6a18488a7066ad..4d8b4f80c63c8b 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -386,6 +386,8 @@ deno_core::extension!(deno_node, "_fs/_fs_glob.ts", "_fs/_fs_lstat.ts", "_fs/_fs_lutimes.ts", + "_fs/_fs_read.ts", + "_fs/_fs_readdir.ts", "_next_tick.ts", "_process/exiting.ts", "_process/process.ts", diff --git a/ext/node/polyfills/_fs/_fs_glob.ts b/ext/node/polyfills/_fs/_fs_glob.ts index 39e00f00c76ed8..8d71b294962777 100644 --- a/ext/node/polyfills/_fs/_fs_glob.ts +++ b/ext/node/polyfills/_fs/_fs_glob.ts @@ -13,7 +13,10 @@ import { isMacOS, isWindows } from "ext:deno_node/_util/os.ts"; import { kEmptyObject } from "ext:deno_node/internal/util.mjs"; import process from "node:process"; -import { readdirPromise as readdir, readdirSync } from "node:fs"; +import { + readdirPromise as readdir, + readdirSync, +} from "ext:deno_node/_fs/_fs_readdir.ts"; import { lstatPromise as lstat, lstatSync, diff --git a/ext/node/polyfills/_fs/_fs_read.ts b/ext/node/polyfills/_fs/_fs_read.ts new file mode 100644 index 00000000000000..e983213e12dd5b --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_read.ts @@ -0,0 +1,267 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +// TODO(petamoriken): enable prefer-primordials for node polyfills +// deno-lint-ignore-file prefer-primordials + +import { Buffer } from "node:buffer"; +import { ERR_INVALID_ARG_VALUE } from "ext:deno_node/internal/errors.ts"; +import * as io from "ext:deno_io/12_io.js"; +import { + arrayBufferViewToUint8Array, + getValidatedFd, + validateOffsetLengthRead, + validatePosition, +} from "ext:deno_node/internal/fs/utils.mjs"; +import { + validateBuffer, + validateFunction, + validateInteger, + validateObject, +} from "ext:deno_node/internal/validators.mjs"; +import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; +import { op_fs_seek_async, op_fs_seek_sync } from "ext:core/ops"; +import { primordials } from "ext:core/mod.js"; +import { + customPromisifyArgs, + kEmptyObject, +} from "ext:deno_node/internal/util.mjs"; +import * as process from "node:process"; +import type { ReadAsyncOptions, ReadSyncOptions } from "node:fs"; + +const { ObjectDefineProperty } = primordials; + +const validateOptionArgs = { __proto__: null, nullable: true }; + +type BinaryCallback = ( + err: Error | null, + bytesRead: number | null, + data?: ArrayBufferView, +) => void; +type Callback = BinaryCallback; + +export function read(fd: number, callback: Callback): void; +export function read( + fd: number, + options: ReadAsyncOptions, + callback: Callback, +): void; +export function read( + fd: number, + buffer: ArrayBufferView, + options: ReadSyncOptions, + callback: Callback, +): void; +export function read( + fd: number, + buffer: ArrayBufferView, + offset: number, + length: number, + position: number | null, + callback: Callback, +): void; +export function read( + fd: number, + optOrBufferOrCb?: + | ArrayBufferView + | ReadAsyncOptions + | Callback, + offsetOrOpt?: + | number + | ReadAsyncOptions + | Callback, + lengthOrCb?: number | Callback, + position?: number | null, + callback?: Callback, +) { + fd = getValidatedFd(fd); + + let offset = offsetOrOpt; + let buffer = optOrBufferOrCb; + let length = lengthOrCb; + let params = null; + if (arguments.length <= 4) { + if (arguments.length === 4) { + // This is fs.read(fd, buffer, options, callback) + validateObject(offsetOrOpt, "options", validateOptionArgs); + callback = length as Callback; + params = offsetOrOpt; + } else if (arguments.length === 3) { + // This is fs.read(fd, bufferOrParams, callback) + if (!isArrayBufferView(buffer)) { + // This is fs.read(fd, params, callback) + params = buffer; + ({ buffer = Buffer.alloc(16384) } = params ?? kEmptyObject); + } + callback = offsetOrOpt as Callback; + } else { + // This is fs.read(fd, callback) + callback = buffer as Callback; + buffer = Buffer.alloc(16384); + } + + if (params !== undefined) { + validateObject(params, "options", validateOptionArgs); + } + ({ + offset = 0, + length = buffer?.byteLength - (offset as number), + position = null, + } = params ?? kEmptyObject); + } + + validateBuffer(buffer); + validateFunction(callback, "cb"); + + if (offset == null) { + offset = 0; + } else { + validateInteger(offset, "offset", 0); + } + + (length as number) |= 0; + + if (length === 0) { + return process.nextTick(function tick() { + callback!(null, 0, buffer); + }); + } + + if (buffer.byteLength === 0) { + throw new ERR_INVALID_ARG_VALUE( + "buffer", + buffer, + "is empty and cannot be written", + ); + } + + validateOffsetLengthRead(offset, length, buffer.byteLength); + + if (position == null) { + position = -1; + } else { + validatePosition(position, "position", length as number); + } + + (async () => { + try { + let nread: number | null; + if (typeof position === "number" && position >= 0) { + const currentPosition = await op_fs_seek_async( + fd, + 0, + io.SeekMode.Current, + ); + // We use sync calls below to avoid being affected by others during + // these calls. + op_fs_seek_sync(fd, position, io.SeekMode.Start); + nread = io.readSync( + fd, + arrayBufferViewToUint8Array(buffer).subarray( + offset, + offset + (length as number), + ), + ); + op_fs_seek_sync(fd, currentPosition, io.SeekMode.Start); + } else { + nread = await io.read( + fd, + arrayBufferViewToUint8Array(buffer).subarray( + offset, + offset + (length as number), + ), + ); + } + callback!(null, nread ?? 0, buffer); + } catch (error) { + callback!(error as Error, null); + } + })(); +} + +ObjectDefineProperty(read, customPromisifyArgs, { + __proto__: null, + value: ["bytesRead", "buffer"], + enumerable: false, +}); + +export function readSync( + fd: number, + buffer: ArrayBufferView, + offset: number, + length: number, + position: number | null, +): number; +export function readSync( + fd: number, + buffer: ArrayBufferView, + opt: ReadSyncOptions, +): number; +export function readSync( + fd: number, + buffer: ArrayBufferView, + offsetOrOpt?: number | ReadSyncOptions, + length?: number, + position?: number | null, +): number { + fd = getValidatedFd(fd); + + validateBuffer(buffer); + + let offset = offsetOrOpt; + if (arguments.length <= 3 || typeof offsetOrOpt === "object") { + if (offsetOrOpt !== undefined) { + validateObject(offsetOrOpt, "options", validateOptionArgs); + } + + ({ + offset = 0, + length = buffer.byteLength - (offset as number), + position = null, + } = offsetOrOpt ?? kEmptyObject); + } + + if (offset === undefined) { + offset = 0; + } else { + validateInteger(offset, "offset", 0); + } + + length! |= 0; + + if (length === 0) { + return 0; + } + + if (buffer.byteLength === 0) { + throw new ERR_INVALID_ARG_VALUE( + "buffer", + buffer, + "is empty and cannot be written", + ); + } + + validateOffsetLengthRead(offset, length, buffer.byteLength); + + if (position == null) { + position = -1; + } else { + validatePosition(position, "position", length); + } + + let currentPosition = 0; + if (typeof position === "number" && position >= 0) { + currentPosition = op_fs_seek_sync(fd, 0, io.SeekMode.Current); + op_fs_seek_sync(fd, position, io.SeekMode.Start); + } + + const numberOfBytesRead = io.readSync( + fd, + arrayBufferViewToUint8Array(buffer).subarray(offset, offset + length!), + ); + + if (typeof position === "number" && position >= 0) { + op_fs_seek_sync(fd, currentPosition, io.SeekMode.Start); + } + + return numberOfBytesRead ?? 0; +} diff --git a/ext/node/polyfills/_fs/_fs_readdir.ts b/ext/node/polyfills/_fs/_fs_readdir.ts new file mode 100644 index 00000000000000..f2f07416861d1a --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_readdir.ts @@ -0,0 +1,186 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +// TODO(petamoriken): enable prefer-primordials for node polyfills +// deno-lint-ignore-file prefer-primordials + +import { TextDecoder, TextEncoder } from "ext:deno_web/08_text_encoding.js"; +import { denoErrorToNodeError } from "ext:deno_node/internal/errors.ts"; +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"; + +type readDirOptions = { + encoding?: string; + withFileTypes?: boolean; + recursive?: boolean; +}; + +type readDirCallback = (err: Error | null, files: string[]) => void; + +type readDirCallbackDirent = (err: Error | null, files: Dirent[]) => void; + +type readDirBoth = ( + ...args: [Error] | [null, string[] | Dirent[] | Array] +) => void; + +export function readdir( + path: string | Buffer | URL, + options: readDirOptions, + callback: readDirCallback, +): void; +export function readdir( + path: string | Buffer | URL, + options: readDirOptions, + callback: readDirCallbackDirent, +): void; +export function readdir(path: string | URL, callback: readDirCallback): void; +export function readdir( + path: string | Buffer | 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; + path = getValidatedPath(path).toString(); + + if (!callback) throw new Error("No callback function supplied"); + + if (options?.encoding) { + try { + new TextDecoder(options.encoding); + } catch { + throw new Error( + `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, + ); + } + } + + const result: Array = []; + const dirs = [path]; + let current: string | undefined; + (async () => { + while ((current = dirs.shift()) !== undefined) { + try { + const entries = await op_fs_read_dir_async(current); + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + if (options?.recursive && entry.isDirectory) { + dirs.push(join(current, entry.name)); + } + + if (options?.withFileTypes) { + entry.parentPath = current; + result.push(direntFromDeno(entry)); + } else { + let name = decode(entry.name, options?.encoding); + if (options?.recursive) { + name = relative(path, join(current, name)); + } + result.push(name); + } + } + } catch (err) { + callback( + denoErrorToNodeError(err as Error, { + syscall: "readdir", + path: current, + }), + ); + return; + } + } + + callback(null, 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 const readdirPromise = promisify(readdir) as ( + & ((path: string | Buffer | URL, options: { + withFileTypes: true; + encoding?: string; + }) => Promise) + & ((path: string | Buffer | URL, options?: { + withFileTypes?: false; + encoding?: string; + }) => Promise) +); + +export function readdirSync( + path: string | Buffer | URL, + options: { withFileTypes: true; encoding?: string }, +): Dirent[]; +export function readdirSync( + path: string | Buffer | URL, + options?: { withFileTypes?: false; encoding?: string }, +): string[]; +export function readdirSync( + path: string | Buffer | URL, + options?: readDirOptions, +): Array { + path = getValidatedPath(path).toString(); + + if (options?.encoding) { + try { + new TextDecoder(options.encoding); + } catch { + throw new Error( + `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, + ); + } + } + + const result: Array = []; + const dirs = [path]; + let current: string | undefined; + while ((current = dirs.shift()) !== undefined) { + try { + const entries = op_fs_read_dir_sync(current); + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + if (options?.recursive && entry.isDirectory) { + dirs.push(join(current, entry.name)); + } + + if (options?.withFileTypes) { + entry.parentPath = current; + result.push(direntFromDeno(entry)); + } else { + let name = decode(entry.name, options?.encoding); + if (options?.recursive) { + name = relative(path, join(current, name)); + } + result.push(name); + } + } + } catch (e) { + throw denoErrorToNodeError(e as Error, { + syscall: "readdir", + path: current, + }); + } + } + + return result; +} diff --git a/ext/node/polyfills/fs.ts b/ext/node/polyfills/fs.ts index 25e741a221683f..a3adfbfa0ee6b3 100644 --- a/ext/node/polyfills/fs.ts +++ b/ext/node/polyfills/fs.ts @@ -20,7 +20,6 @@ import { denoErrorToNodeError, denoWriteFileErrorToNodeError, ERR_FS_FILE_TOO_LARGE, - ERR_INVALID_ARG_VALUE, } from "ext:deno_node/internal/errors.ts"; import * as constants from "ext:deno_node/_fs/_fs_constants.ts"; @@ -31,6 +30,8 @@ import { exists, existsSync } from "ext:deno_node/_fs/_fs_exists.ts"; import { fstat, fstatSync } from "ext:deno_node/_fs/_fs_fstat.ts"; import { lstat, lstatSync } from "ext:deno_node/_fs/_fs_lstat.ts"; import { lutimes, lutimesSync } from "ext:deno_node/_fs/_fs_lutimes.ts"; +import { read, readSync } from "ext:deno_node/_fs/_fs_read.ts"; +import { readdir, readdirSync } from "ext:deno_node/_fs/_fs_readdir.ts"; import { EventEmitter } from "node:events"; import { type MaybeEmpty, notImplemented } from "ext:deno_node/_utils.ts"; import { promisify } from "node:util"; @@ -49,7 +50,6 @@ import { constants as fsUtilConstants, copyObject, Dirent, - direntFromDeno, emitRecursiveRmdirWarning, getOptions, getValidatedFd, @@ -62,9 +62,7 @@ import { stringToFlags, toUnixTimestamp as _toUnixTimestamp, validateBufferArray, - validateOffsetLengthRead, validateOffsetLengthWrite, - validatePosition, validateRmdirOptions, validateRmOptions, validateRmOptionsSync, @@ -75,7 +73,6 @@ import { glob, globSync } from "ext:deno_node/_fs/_fs_glob.ts"; import { parseFileMode, validateBoolean, - validateBuffer, validateEncoding, validateFunction, validateInt32, @@ -88,9 +85,9 @@ import { Buffer } from "node:buffer"; import process from "node:process"; import * as io from "ext:deno_io/12_io.js"; import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; -import { pathFromURL } from "ext:deno_web/00_infra.js"; -import { TextDecoder, TextEncoder } from "ext:deno_web/08_text_encoding.js"; +import { TextEncoder } from "ext:deno_web/08_text_encoding.js"; import * as abortSignal from "ext:deno_web/03_abort_signal.js"; +import { pathFromURL } from "ext:deno_web/00_infra.js"; import { URLPrototype } from "ext:deno_web/00_url.js"; import { FileHandle } from "ext:deno_node/internal/fs/handle.ts"; import { isIterable } from "ext:deno_node/internal/streams/utils.js"; @@ -101,8 +98,6 @@ import { op_fs_fchmod_sync, op_fs_fchown_async, op_fs_fchown_sync, - op_fs_read_dir_async, - op_fs_read_dir_sync, op_fs_read_file_async, op_fs_read_file_sync, op_fs_seek_async, @@ -133,7 +128,7 @@ import { kEmptyObject, normalizeEncoding, } from "ext:deno_node/internal/util.mjs"; -import { basename, join, relative, resolve, toNamespacedPath } from "node:path"; +import { basename, resolve, toNamespacedPath } from "node:path"; import * as pathModule from "node:path"; import type { Encoding } from "node:crypto"; import { core, primordials } from "ext:core/mod.js"; @@ -511,443 +506,6 @@ function readvPromise( }); } -// -- read -- - -interface ReadSyncOptions { - offset?: number | undefined; - length?: number | undefined; - position?: number | null | undefined; -} - -interface ReadAsyncOptions - extends ReadSyncOptions { - buffer?: TBuffer; -} - -const validateOptionArgs = { __proto__: null, nullable: true }; - -type ReadBinaryCallback = ( - err: Error | null, - bytesRead: number | null, - data?: ArrayBufferView, -) => void; -type ReadCallback = ReadBinaryCallback; - -function read(fd: number, callback: ReadCallback): void; -function read( - fd: number, - options: ReadAsyncOptions, - callback: ReadCallback, -): void; -function read( - fd: number, - buffer: ArrayBufferView, - options: ReadSyncOptions, - callback: ReadCallback, -): void; -function read( - fd: number, - buffer: ArrayBufferView, - offset: number, - length: number, - position: number | null, - callback: ReadCallback, -): void; -function read( - fd: number, - optOrBufferOrCb?: - | ArrayBufferView - | ReadAsyncOptions - | ReadCallback, - offsetOrOpt?: - | number - | ReadAsyncOptions - | ReadCallback, - lengthOrCb?: number | ReadCallback, - position?: number | null, - callback?: ReadCallback, -) { - fd = getValidatedFd(fd); - - let offset = offsetOrOpt; - let buffer = optOrBufferOrCb; - let length = lengthOrCb; - let params = null; - if (arguments.length <= 4) { - if (arguments.length === 4) { - // This is fs.read(fd, buffer, options, callback) - validateObject(offsetOrOpt, "options", validateOptionArgs); - callback = length as ReadCallback; - params = offsetOrOpt; - } else if (arguments.length === 3) { - // This is fs.read(fd, bufferOrParams, callback) - if (!isArrayBufferView(buffer)) { - // This is fs.read(fd, params, callback) - params = buffer; - ({ buffer = Buffer.alloc(16384) } = params ?? kEmptyObject); - } - callback = offsetOrOpt as ReadCallback; - } else { - // This is fs.read(fd, callback) - callback = buffer as ReadCallback; - buffer = Buffer.alloc(16384); - } - - if (params !== undefined) { - validateObject(params, "options", validateOptionArgs); - } - ({ - offset = 0, - // deno-lint-ignore prefer-primordials - length = buffer?.byteLength - (offset as number), - position = null, - } = params ?? kEmptyObject); - } - - validateBuffer(buffer); - validateFunction(callback, "cb"); - - if (offset == null) { - offset = 0; - } else { - validateInteger(offset, "offset", 0); - } - - (length as number) |= 0; - - if (length === 0) { - return process.nextTick(function tick() { - callback!(null, 0, buffer); - }); - } - - // deno-lint-ignore prefer-primordials - if (buffer.byteLength === 0) { - throw new ERR_INVALID_ARG_VALUE( - "buffer", - buffer, - "is empty and cannot be written", - ); - } - - // deno-lint-ignore prefer-primordials - validateOffsetLengthRead(offset, length, buffer.byteLength); - - if (position == null) { - position = -1; - } else { - validatePosition(position, "position", length as number); - } - - (async () => { - try { - let nread: number | null; - if (typeof position === "number" && position >= 0) { - const currentPosition = await op_fs_seek_async( - fd, - 0, - io.SeekMode.Current, - ); - // We use sync calls below to avoid being affected by others during - // these calls. - op_fs_seek_sync(fd, position, io.SeekMode.Start); - nread = io.readSync( - fd, - arrayBufferViewToUint8Array(buffer).subarray( - offset, - offset + (length as number), - ), - ); - op_fs_seek_sync(fd, currentPosition, io.SeekMode.Start); - } else { - nread = await io.read( - fd, - arrayBufferViewToUint8Array(buffer).subarray( - offset, - offset + (length as number), - ), - ); - } - callback!(null, nread ?? 0, buffer); - } catch (error) { - callback!(error as Error, null); - } - })(); -} - -ObjectDefineProperty(read, customPromisifyArgs, { - __proto__: null, - value: ["bytesRead", "buffer"], - enumerable: false, -}); - -function readSync( - fd: number, - buffer: ArrayBufferView, - offset: number, - length: number, - position: number | null, -): number; -function readSync( - fd: number, - buffer: ArrayBufferView, - opt: ReadSyncOptions, -): number; -function readSync( - fd: number, - buffer: ArrayBufferView, - offsetOrOpt?: number | ReadSyncOptions, - length?: number, - position?: number | null, -): number { - fd = getValidatedFd(fd); - - validateBuffer(buffer); - - let offset = offsetOrOpt; - if (arguments.length <= 3 || typeof offsetOrOpt === "object") { - if (offsetOrOpt !== undefined) { - validateObject(offsetOrOpt, "options", validateOptionArgs); - } - - ({ - offset = 0, - // deno-lint-ignore prefer-primordials - length = buffer.byteLength - (offset as number), - position = null, - } = offsetOrOpt ?? kEmptyObject); - } - - if (offset === undefined) { - offset = 0; - } else { - validateInteger(offset, "offset", 0); - } - - length! |= 0; - - if (length === 0) { - return 0; - } - - // deno-lint-ignore prefer-primordials - if (buffer.byteLength === 0) { - throw new ERR_INVALID_ARG_VALUE( - "buffer", - buffer, - "is empty and cannot be written", - ); - } - - // deno-lint-ignore prefer-primordials - validateOffsetLengthRead(offset, length, buffer.byteLength); - - if (position == null) { - position = -1; - } else { - validatePosition(position, "position", length); - } - - let currentPosition = 0; - if (typeof position === "number" && position >= 0) { - currentPosition = op_fs_seek_sync(fd, 0, io.SeekMode.Current); - op_fs_seek_sync(fd, position, io.SeekMode.Start); - } - - const numberOfBytesRead = io.readSync( - fd, - arrayBufferViewToUint8Array(buffer).subarray(offset, offset + length!), - ); - - if (typeof position === "number" && position >= 0) { - op_fs_seek_sync(fd, currentPosition, io.SeekMode.Start); - } - - return numberOfBytesRead ?? 0; -} - -// -- readdir -- - -type readDirOptions = { - encoding?: string; - withFileTypes?: boolean; - recursive?: boolean; -}; - -type readDirCallback = (err: Error | null, files: string[]) => void; - -type readDirCallbackDirent = (err: Error | null, files: Dirent[]) => void; - -type readDirBoth = ( - ...args: [Error] | [null, string[] | Dirent[] | Array] -) => void; - -function readdirDecode(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)); - } -} - -function readdir( - path: string | Buffer | URL, - options: readDirOptions, - callback: readDirCallback, -): void; -function readdir( - path: string | Buffer | URL, - options: readDirOptions, - callback: readDirCallbackDirent, -): void; -function readdir(path: string | URL, callback: readDirCallback): void; -function readdir( - path: string | Buffer | 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; - // deno-lint-ignore prefer-primordials - path = getValidatedPath(path).toString(); - - if (!callback) throw new Error("No callback function supplied"); - - if (options?.encoding) { - try { - new TextDecoder(options.encoding); - } catch { - throw new Error( - `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, - ); - } - } - - const result: Array = []; - const dirs = [path]; - let current: string | undefined; - (async () => { - // deno-lint-ignore prefer-primordials - while ((current = dirs.shift()) !== undefined) { - try { - const entries = await op_fs_read_dir_async(current); - - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (options?.recursive && entry.isDirectory) { - // deno-lint-ignore prefer-primordials - dirs.push(join(current, entry.name)); - } - - if (options?.withFileTypes) { - entry.parentPath = current; - // deno-lint-ignore prefer-primordials - result.push(direntFromDeno(entry)); - } else { - let name = readdirDecode(entry.name, options?.encoding); - if (options?.recursive) { - name = relative(path, join(current, name)); - } - // deno-lint-ignore prefer-primordials - result.push(name); - } - } - } catch (err) { - callback( - denoErrorToNodeError(err as Error, { - syscall: "readdir", - path: current, - }), - ); - return; - } - } - - callback(null, result); - })(); -} - -const readdirPromise = promisify(readdir) as ( - & ((path: string | Buffer | URL, options: { - withFileTypes: true; - encoding?: string; - }) => Promise) - & ((path: string | Buffer | URL, options?: { - withFileTypes?: false; - encoding?: string; - }) => Promise) -); - -function readdirSync( - path: string | Buffer | URL, - options: { withFileTypes: true; encoding?: string }, -): Dirent[]; -function readdirSync( - path: string | Buffer | URL, - options?: { withFileTypes?: false; encoding?: string }, -): string[]; -function readdirSync( - path: string | Buffer | URL, - options?: readDirOptions, -): Array { - // deno-lint-ignore prefer-primordials - path = getValidatedPath(path).toString(); - - if (options?.encoding) { - try { - new TextDecoder(options.encoding); - } catch { - throw new Error( - `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, - ); - } - } - - const result: Array = []; - const dirs = [path]; - let current: string | undefined; - // deno-lint-ignore prefer-primordials - while ((current = dirs.shift()) !== undefined) { - try { - const entries = op_fs_read_dir_sync(current); - - for (let i = 0; i < entries.length; i++) { - const entry = entries[i]; - if (options?.recursive && entry.isDirectory) { - // deno-lint-ignore prefer-primordials - dirs.push(join(current, entry.name)); - } - - if (options?.withFileTypes) { - entry.parentPath = current; - // deno-lint-ignore prefer-primordials - result.push(direntFromDeno(entry)); - } else { - let name = readdirDecode(entry.name, options?.encoding); - if (options?.recursive) { - name = relative(path, join(current, name)); - } - // deno-lint-ignore prefer-primordials - result.push(name); - } - } - } catch (e) { - throw denoErrorToNodeError(e as Error, { - syscall: "readdir", - path: current, - }); - } - } - - return result; -} - // -- readFile -- const readFileDefaultOptions = { @@ -3904,8 +3462,10 @@ export default { readdir, readdirSync, readFile, + readFilePromise, readFileSync, readlink, + readlinkPromise, readlinkSync, ReadStream, realpath, @@ -4032,7 +3592,6 @@ export { R_OK, read, readdir, - readdirPromise, readdirSync, readFile, readFilePromise, diff --git a/ext/node/polyfills/internal/fs/promises.ts b/ext/node/polyfills/internal/fs/promises.ts index 9b0701159562ab..7e262cdc6db435 100644 --- a/ext/node/polyfills/internal/fs/promises.ts +++ b/ext/node/polyfills/internal/fs/promises.ts @@ -7,7 +7,7 @@ import * as constants from "ext:deno_node/_fs/_fs_constants.ts"; import { copyFilePromise } from "ext:deno_node/_fs/_fs_copy.ts"; import { cpPromise } from "ext:deno_node/_fs/_fs_cp.ts"; import { lutimesPromise } from "ext:deno_node/_fs/_fs_lutimes.ts"; -import { readdirPromise, readFilePromise, readlinkPromise } from "node:fs"; +import { readdirPromise } from "ext:deno_node/_fs/_fs_readdir.ts"; import { lstatPromise } from "ext:deno_node/_fs/_fs_lstat.ts"; import { access, @@ -20,6 +20,8 @@ import { mkdtemp, open, opendir, + readFile, + readlink, realpath, rename, rm, @@ -246,6 +248,15 @@ const statfsPromise = promisify(statfs) as ( options?: { bigint?: boolean }, ) => Promise; +// -- readFile / readlink -- + +const readFilePromise = promisify(readFile); + +const readlinkPromise = promisify(readlink) as ( + path: string | Buffer | URL, + opt?: { encoding?: string | null }, +) => Promise; + // -- promises object -- const promises = { From 861faac0b3fb5db971ae75fab1cc5b2ede696c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 19:03:54 +0100 Subject: [PATCH 3/6] update expected spec output --- tests/specs/node/error_stack_internal_frames/console_log.out | 2 +- tests/specs/node/error_stack_internal_frames/thrown.out | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/specs/node/error_stack_internal_frames/console_log.out b/tests/specs/node/error_stack_internal_frames/console_log.out index 61645be2ae7028..a074e32d3c75a5 100644 --- a/tests/specs/node/error_stack_internal_frames/console_log.out +++ b/tests/specs/node/error_stack_internal_frames/console_log.out @@ -1,5 +1,5 @@ Error: ENOENT: no such file or directory, open '/non/existent/file' - at readFileSync (ext:deno_node/_fs/_fs_readFile.ts:[WILDCARD]) + at readFileSync (node:fs:[WILDCARD]) at [WILDCARD]console_log.ts:6:5 at processTicksAndRejections (ext:core/01_core.js:[WILDCARD]) { errno: [WILDCARD], diff --git a/tests/specs/node/error_stack_internal_frames/thrown.out b/tests/specs/node/error_stack_internal_frames/thrown.out index 07504f4040c2c4..f2c07aaac26b8a 100644 --- a/tests/specs/node/error_stack_internal_frames/thrown.out +++ b/tests/specs/node/error_stack_internal_frames/thrown.out @@ -1,6 +1,6 @@ error: Uncaught Error: ENOENT: no such file or directory, open '/non/existent/file' readFileSync("/non/existent/file"); ^ - at readFileSync (ext:deno_node/_fs/_fs_readFile.ts:[WILDCARD]) + at readFileSync (node:fs:[WILDCARD]) at [WILDCARD]thrown.ts:5:3 at processTicksAndRejections (ext:core/01_core.js:[WILDCARD]) From b32260f3b167001e44c0161c5e155a119a72c9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 22:10:00 +0100 Subject: [PATCH 4/6] fix(core): skip node: frames when selecting error source snippet The source snippet frame selection logic skipped `ext:` frames but not `node:` frames, causing missing source snippets for errors thrown from node: builtins after the fs consolidation. Co-Authored-By: Claude Opus 4.6 --- libs/core/error.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/core/error.rs b/libs/core/error.rs index 298e378ae87cf8..8ba157ca675c4e 100644 --- a/libs/core/error.rs +++ b/libs/core/error.rs @@ -807,6 +807,7 @@ impl JsError { if let (Some(file_name), Some(line_number)) = (&frame.file_name, frame.line_number) && !file_name.trim_start_matches('[').starts_with("ext:") + && !file_name.starts_with("node:") { source_line = source_mapper.get_source_line(file_name, line_number); source_line_frame_index = Some(i); @@ -954,6 +955,7 @@ impl JsError { if let (Some(file_name), Some(line_number)) = (&frame.file_name, frame.line_number) && !file_name.trim_start_matches('[').starts_with("ext:") + && !file_name.starts_with("node:") { source_line = source_mapper.get_source_line(file_name, line_number); source_line_frame_index = Some(i); From 433df0b16b33ba04fd312d865a29c489bfa0cff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 12 Mar 2026 22:30:40 +0100 Subject: [PATCH 5/6] fix a spec test --- tests/specs/run/require_esm/main.out | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/specs/run/require_esm/main.out b/tests/specs/run/require_esm/main.out index 4890e1a492de05..14c80aa9544a05 100644 --- a/tests/specs/run/require_esm/main.out +++ b/tests/specs/run/require_esm/main.out @@ -1,4 +1,6 @@ [Module: null prototype] { sync_js: 1 } [Module: null prototype] { sync_mjs: 1 } error: Uncaught (in promise) Error: Top-level await is not allowed in synchronous evaluation +require("./async.js"); +^ at [WILDCARD] From 0415652d813d693d7182ce76d828005863543623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 13 Mar 2026 09:28:33 +0100 Subject: [PATCH 6/6] fix --- ext/node/polyfills/fs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/node/polyfills/fs.ts b/ext/node/polyfills/fs.ts index c28fef44212561..cc9f1ff080b6d7 100644 --- a/ext/node/polyfills/fs.ts +++ b/ext/node/polyfills/fs.ts @@ -561,7 +561,7 @@ async function fsFileReadAll(fsFile: FsFile, options?: FileOptions) { let size = 0; let length = 0; - if ((statFields.mode & S_IFMT) === S_IFREG) { + if ((statFields.mode & fsConstants.S_IFMT) === fsConstants.S_IFREG) { size = statFields.size; length = encoding ? MathMin(size, kReadFileBufferLength) : size; }