From 4e7821d4e3b3eb8d3711d3da78028884af79dd2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 04:28:22 +0000 Subject: [PATCH 1/7] Add createReadStream/createWriteStream to FileSystem in node-core-library and update consumers to use FileSystem instead of fs directly Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/5ad20d4d-c9a4-4855-bb13-8dd9e2c1350b Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com> --- ...ntry-for-http-plugin_2026-04-05-04-27.json | 11 +++++ common/reviews/api/node-core-library.api.md | 8 ++++ libraries/node-core-library/src/FileSystem.ts | 40 +++++++++++++++++++ libraries/node-core-library/src/index.ts | 2 + 4 files changed, 61 insertions(+) create mode 100644 common/changes/@rushstack/node-core-library/copilot-stream-cache-entry-for-http-plugin_2026-04-05-04-27.json diff --git a/common/changes/@rushstack/node-core-library/copilot-stream-cache-entry-for-http-plugin_2026-04-05-04-27.json b/common/changes/@rushstack/node-core-library/copilot-stream-cache-entry-for-http-plugin_2026-04-05-04-27.json new file mode 100644 index 00000000000..50836a0011e --- /dev/null +++ b/common/changes/@rushstack/node-core-library/copilot-stream-cache-entry-for-http-plugin_2026-04-05-04-27.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Add createReadStream() and createWriteStream() APIs, along with FileSystemReadStream and FileSystemWriteStream type aliases", + "type": "minor", + "packageName": "@rushstack/node-core-library" + } + ], + "packageName": "@rushstack/node-core-library", + "email": "198982749+Copilot@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index c5b2ebe4f9b..6505856a295 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -166,12 +166,14 @@ export class FileSystem { static copyFilesAsync(options: IFileSystemCopyFilesAsyncOptions): Promise; static createHardLink(options: IFileSystemCreateLinkOptions): void; static createHardLinkAsync(options: IFileSystemCreateLinkOptions): Promise; + static createReadStream(filePath: string): FileSystemReadStream; static createSymbolicLinkFile(options: IFileSystemCreateLinkOptions): void; static createSymbolicLinkFileAsync(options: IFileSystemCreateLinkOptions): Promise; static createSymbolicLinkFolder(options: IFileSystemCreateLinkOptions): void; static createSymbolicLinkFolderAsync(options: IFileSystemCreateLinkOptions): Promise; static createSymbolicLinkJunction(options: IFileSystemCreateLinkOptions): void; static createSymbolicLinkJunctionAsync(options: IFileSystemCreateLinkOptions): Promise; + static createWriteStream(filePath: string): FileSystemWriteStream; static deleteFile(filePath: string, options?: IFileSystemDeleteFileOptions): void; static deleteFileAsync(filePath: string, options?: IFileSystemDeleteFileOptions): Promise; static deleteFolder(folderPath: string): void; @@ -225,9 +227,15 @@ export type FileSystemCopyFilesAsyncFilter = (sourcePath: string, destinationPat // @public export type FileSystemCopyFilesFilter = (sourcePath: string, destinationPath: string) => boolean; +// @public +export type FileSystemReadStream = fs.ReadStream; + // @public export type FileSystemStats = fs.Stats; +// @public +export type FileSystemWriteStream = fs.WriteStream; + // @public export class FileWriter { close(): void; diff --git a/libraries/node-core-library/src/FileSystem.ts b/libraries/node-core-library/src/FileSystem.ts index a2ff0b74d2f..77321a4b834 100644 --- a/libraries/node-core-library/src/FileSystem.ts +++ b/libraries/node-core-library/src/FileSystem.ts @@ -28,6 +28,24 @@ export type FileSystemStats = fs.Stats; */ export type FolderItem = fs.Dirent; +/** + * An alias for the Node.js `fs.ReadStream` object. + * + * @remarks + * This avoids the need to import the `fs` package when using the {@link FileSystem} API. + * @public + */ +export type FileSystemReadStream = fs.ReadStream; + +/** + * An alias for the Node.js `fs.WriteStream` object. + * + * @remarks + * This avoids the need to import the `fs` package when using the {@link FileSystem} API. + * @public + */ +export type FileSystemWriteStream = fs.WriteStream; + // The PosixModeBits are intended to be used with bitwise operations. /* eslint-disable no-bitwise */ @@ -1237,6 +1255,28 @@ export class FileSystem { }); } + /** + * Creates a readable stream for an existing file. + * Behind the scenes it uses `fs.createReadStream()`. + * + * @param filePath - The path to the file. The path may be absolute or relative. + * @returns A new readable stream for the file. + */ + public static createReadStream(filePath: string): FileSystemReadStream { + return fs.createReadStream(filePath); + } + + /** + * Creates a writable stream for writing to a file. + * Behind the scenes it uses `fs.createWriteStream()`. + * + * @param filePath - The path to the file. The path may be absolute or relative. + * @returns A new writable stream for the file. + */ + public static createWriteStream(filePath: string): FileSystemWriteStream { + return fs.createWriteStream(filePath); + } + // =============== // LINK OPERATIONS // =============== diff --git a/libraries/node-core-library/src/index.ts b/libraries/node-core-library/src/index.ts index c48a9c950a3..84afe68fe78 100644 --- a/libraries/node-core-library/src/index.ts +++ b/libraries/node-core-library/src/index.ts @@ -52,6 +52,8 @@ export { FileSystem, type FileSystemCopyFilesAsyncFilter, type FileSystemCopyFilesFilter, + type FileSystemReadStream, + type FileSystemWriteStream, type FolderItem, type FileSystemStats, type IFileSystemCopyFileBaseOptions, From 342a83b29345c60591339d81183d30d3dc80c298 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 05:23:28 +0000 Subject: [PATCH 2/7] Add ensureFolderExists option to FileSystem.createWriteStream Agent-Logs-Url: https://github.com/microsoft/rushstack/sessions/199d4b3e-1f3f-44e1-9fc6-7b4a0e027c7e Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com> --- common/reviews/api/node-core-library.api.md | 7 +++++- libraries/node-core-library/src/FileSystem.ts | 25 ++++++++++++++++++- libraries/node-core-library/src/index.ts | 1 + 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index 6505856a295..7e8118d5e83 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -173,7 +173,7 @@ export class FileSystem { static createSymbolicLinkFolderAsync(options: IFileSystemCreateLinkOptions): Promise; static createSymbolicLinkJunction(options: IFileSystemCreateLinkOptions): void; static createSymbolicLinkJunctionAsync(options: IFileSystemCreateLinkOptions): Promise; - static createWriteStream(filePath: string): FileSystemWriteStream; + static createWriteStream(filePath: string, options?: IFileSystemCreateWriteStreamOptions): FileSystemWriteStream; static deleteFile(filePath: string, options?: IFileSystemDeleteFileOptions): void; static deleteFileAsync(filePath: string, options?: IFileSystemDeleteFileOptions): Promise; static deleteFolder(folderPath: string): void; @@ -344,6 +344,11 @@ export interface IFileSystemCreateLinkOptions { newLinkPath: string; } +// @public +export interface IFileSystemCreateWriteStreamOptions { + ensureFolderExists?: boolean; +} + // @public export interface IFileSystemDeleteFileOptions { throwIfNotExists?: boolean; diff --git a/libraries/node-core-library/src/FileSystem.ts b/libraries/node-core-library/src/FileSystem.ts index 77321a4b834..3a0445e94c6 100644 --- a/libraries/node-core-library/src/FileSystem.ts +++ b/libraries/node-core-library/src/FileSystem.ts @@ -276,6 +276,18 @@ export interface IFileSystemCopyFilesOptions extends IFileSystemCopyFilesAsyncOp filter?: FileSystemCopyFilesFilter; // narrow the type to exclude FileSystemCopyFilesAsyncFilter } +/** + * The options for {@link FileSystem.createWriteStream} + * @public + */ +export interface IFileSystemCreateWriteStreamOptions { + /** + * If true, will ensure the folder is created before writing the file. + * @defaultValue false + */ + ensureFolderExists?: boolean; +} + /** * The options for {@link FileSystem.deleteFile} * @public @@ -1270,10 +1282,21 @@ export class FileSystem { * Creates a writable stream for writing to a file. * Behind the scenes it uses `fs.createWriteStream()`. * + * @remarks + * Throws an error if the folder doesn't exist, unless {@link IFileSystemCreateWriteStreamOptions.ensureFolderExists} + * is set to `true`. * @param filePath - The path to the file. The path may be absolute or relative. + * @param options - Optional settings that can change the behavior. * @returns A new writable stream for the file. */ - public static createWriteStream(filePath: string): FileSystemWriteStream { + public static createWriteStream( + filePath: string, + options?: IFileSystemCreateWriteStreamOptions + ): FileSystemWriteStream { + if (options?.ensureFolderExists) { + const folderPath: string = nodeJsPath.dirname(filePath); + FileSystem.ensureFolder(folderPath); + } return fs.createWriteStream(filePath); } diff --git a/libraries/node-core-library/src/index.ts b/libraries/node-core-library/src/index.ts index 84afe68fe78..53101fda7a6 100644 --- a/libraries/node-core-library/src/index.ts +++ b/libraries/node-core-library/src/index.ts @@ -61,6 +61,7 @@ export { type IFileSystemCopyFilesAsyncOptions, type IFileSystemCopyFilesOptions, type IFileSystemCreateLinkOptions, + type IFileSystemCreateWriteStreamOptions, type IFileSystemDeleteFileOptions, type IFileSystemMoveOptions, type IFileSystemReadFileOptions, From c774a67f8345d2b9387efbc71c3c7eaa8272bc06 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Sun, 5 Apr 2026 13:24:20 -0700 Subject: [PATCH 3/7] Expand FileSystem.createWriteStream. --- common/reviews/api/node-core-library.api.md | 15 +++-- libraries/node-core-library/src/FileSystem.ts | 56 +++++++++++-------- libraries/node-core-library/src/index.ts | 1 + 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/common/reviews/api/node-core-library.api.md b/common/reviews/api/node-core-library.api.md index 7e8118d5e83..2a557ba9e75 100644 --- a/common/reviews/api/node-core-library.api.md +++ b/common/reviews/api/node-core-library.api.md @@ -174,6 +174,7 @@ export class FileSystem { static createSymbolicLinkJunction(options: IFileSystemCreateLinkOptions): void; static createSymbolicLinkJunctionAsync(options: IFileSystemCreateLinkOptions): Promise; static createWriteStream(filePath: string, options?: IFileSystemCreateWriteStreamOptions): FileSystemWriteStream; + static createWriteStreamAsync(filePath: string, options?: IFileSystemCreateWriteStreamOptions): Promise; static deleteFile(filePath: string, options?: IFileSystemDeleteFileOptions): void; static deleteFileAsync(filePath: string, options?: IFileSystemDeleteFileOptions): Promise; static deleteFolder(folderPath: string): void; @@ -345,8 +346,7 @@ export interface IFileSystemCreateLinkOptions { } // @public -export interface IFileSystemCreateWriteStreamOptions { - ensureFolderExists?: boolean; +export interface IFileSystemCreateWriteStreamOptions extends IFileSystemWriteFileOptionsBase { } // @public @@ -355,9 +355,8 @@ export interface IFileSystemDeleteFileOptions { } // @public -export interface IFileSystemMoveOptions { +export interface IFileSystemMoveOptions extends IFileSystemWriteFileOptionsBase { destinationPath: string; - ensureFolderExists?: boolean; overwrite?: boolean; sourcePath: string; } @@ -380,8 +379,7 @@ export interface IFileSystemUpdateTimeParameters { } // @public -export interface IFileSystemWriteBinaryFileOptions { - ensureFolderExists?: boolean; +export interface IFileSystemWriteBinaryFileOptions extends IFileSystemWriteFileOptionsBase { } // @public @@ -390,6 +388,11 @@ export interface IFileSystemWriteFileOptions extends IFileSystemWriteBinaryFileO encoding?: Encoding; } +// @public (undocumented) +export interface IFileSystemWriteFileOptionsBase { + ensureFolderExists?: boolean; +} + // @public export interface IFileWriterFlags { append?: boolean; diff --git a/libraries/node-core-library/src/FileSystem.ts b/libraries/node-core-library/src/FileSystem.ts index 3a0445e94c6..e9557139e2e 100644 --- a/libraries/node-core-library/src/FileSystem.ts +++ b/libraries/node-core-library/src/FileSystem.ts @@ -62,10 +62,9 @@ export interface IFileSystemReadFolderOptions { } /** - * The options for {@link FileSystem.writeBuffersToFile} * @public */ -export interface IFileSystemWriteBinaryFileOptions { +export interface IFileSystemWriteFileOptionsBase { /** * If true, will ensure the folder is created before writing the file. * @defaultValue false @@ -73,6 +72,12 @@ export interface IFileSystemWriteBinaryFileOptions { ensureFolderExists?: boolean; } +/** + * The options for {@link FileSystem.writeBuffersToFile} + * @public + */ +export interface IFileSystemWriteBinaryFileOptions extends IFileSystemWriteFileOptionsBase {} + /** * The options for {@link FileSystem.writeFile} * @public @@ -113,7 +118,7 @@ export interface IFileSystemReadFileOptions { * The options for {@link FileSystem.move} * @public */ -export interface IFileSystemMoveOptions { +export interface IFileSystemMoveOptions extends IFileSystemWriteFileOptionsBase { /** * The path of the existing object to be moved. * The path may be absolute or relative. @@ -131,12 +136,6 @@ export interface IFileSystemMoveOptions { * @defaultValue true */ overwrite?: boolean; - - /** - * If true, will ensure the folder is created before writing the file. - * @defaultValue false - */ - ensureFolderExists?: boolean; } /** @@ -280,13 +279,7 @@ export interface IFileSystemCopyFilesOptions extends IFileSystemCopyFilesAsyncOp * The options for {@link FileSystem.createWriteStream} * @public */ -export interface IFileSystemCreateWriteStreamOptions { - /** - * If true, will ensure the folder is created before writing the file. - * @defaultValue false - */ - ensureFolderExists?: boolean; -} +export interface IFileSystemCreateWriteStreamOptions extends IFileSystemWriteFileOptionsBase {} /** * The options for {@link FileSystem.deleteFile} @@ -780,10 +773,11 @@ export class FileSystem { * Writes a text string to a file on disk, overwriting the file if it already exists. * Behind the scenes it uses `fs.writeFileSync()`. * @remarks - * Throws an error if the folder doesn't exist, unless ensureFolder=true. + * Throws an error if the folder doesn't exist, unless {@link IFileSystemWriteFileOptionsBase.ensureFolderExists} + * is set to `true`. * @param filePath - The absolute or relative path of the file. * @param contents - The text that should be written to the file. - * @param options - Optional settings that can change the behavior. Type: `IWriteFileOptions` + * @param options - Optional settings that can change the behavior. */ public static writeFile( filePath: string, @@ -826,7 +820,8 @@ export class FileSystem { * multiple sources. * * @remarks - * Throws an error if the folder doesn't exist, unless ensureFolder=true. + * Throws an error if the folder doesn't exist, unless {@link IFileSystemWriteFileOptionsBase.ensureFolderExists} + * is set to `true`. * @param filePath - The absolute or relative path of the file. * @param contents - The content that should be written to the file. * @param options - Optional settings that can change the behavior. @@ -986,10 +981,11 @@ export class FileSystem { * Writes a text string to a file on disk, appending to the file if it already exists. * Behind the scenes it uses `fs.appendFileSync()`. * @remarks - * Throws an error if the folder doesn't exist, unless ensureFolder=true. + * Throws an error if the folder doesn't exist, unless {@link IFileSystemWriteFileOptionsBase.ensureFolderExists} + * is set to `true`. * @param filePath - The absolute or relative path of the file. * @param contents - The text that should be written to the file. - * @param options - Optional settings that can change the behavior. Type: `IWriteFileOptions` + * @param options - Optional settings that can change the behavior. */ public static appendToFile( filePath: string, @@ -1283,7 +1279,7 @@ export class FileSystem { * Behind the scenes it uses `fs.createWriteStream()`. * * @remarks - * Throws an error if the folder doesn't exist, unless {@link IFileSystemCreateWriteStreamOptions.ensureFolderExists} + * Throws an error if the folder doesn't exist, unless {@link IFileSystemWriteFileOptionsBase.ensureFolderExists} * is set to `true`. * @param filePath - The path to the file. The path may be absolute or relative. * @param options - Optional settings that can change the behavior. @@ -1297,6 +1293,22 @@ export class FileSystem { const folderPath: string = nodeJsPath.dirname(filePath); FileSystem.ensureFolder(folderPath); } + + return fs.createWriteStream(filePath); + } + + /** + * An async version of {@link FileSystem.createWriteStream}. + */ + public static async createWriteStreamAsync( + filePath: string, + options?: IFileSystemCreateWriteStreamOptions + ): Promise { + if (options?.ensureFolderExists) { + const folderPath: string = nodeJsPath.dirname(filePath); + await FileSystem.ensureFolderAsync(folderPath); + } + return fs.createWriteStream(filePath); } diff --git a/libraries/node-core-library/src/index.ts b/libraries/node-core-library/src/index.ts index 53101fda7a6..3f4592cf6b1 100644 --- a/libraries/node-core-library/src/index.ts +++ b/libraries/node-core-library/src/index.ts @@ -50,6 +50,7 @@ export { type IFileErrorOptions, type IFileErrorFormattingOptions, FileError } f export { AlreadyExistsBehavior, FileSystem, + type IFileSystemWriteFileOptionsBase, type FileSystemCopyFilesAsyncFilter, type FileSystemCopyFilesFilter, type FileSystemReadStream, From 8da70ce404755db1e416c20e47f09898f04e90d1 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Tue, 7 Apr 2026 01:03:02 -0700 Subject: [PATCH 4/7] fixup! Add createReadStream/createWriteStream to FileSystem in node-core-library and update consumers to use FileSystem instead of fs directly Wrap FileSystem stream methods in _wrapException for consistent error handling --- libraries/node-core-library/src/FileSystem.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/libraries/node-core-library/src/FileSystem.ts b/libraries/node-core-library/src/FileSystem.ts index e9557139e2e..0975847f35d 100644 --- a/libraries/node-core-library/src/FileSystem.ts +++ b/libraries/node-core-library/src/FileSystem.ts @@ -1271,7 +1271,9 @@ export class FileSystem { * @returns A new readable stream for the file. */ public static createReadStream(filePath: string): FileSystemReadStream { - return fs.createReadStream(filePath); + return FileSystem._wrapException(() => { + return fs.createReadStream(filePath); + }); } /** @@ -1289,12 +1291,14 @@ export class FileSystem { filePath: string, options?: IFileSystemCreateWriteStreamOptions ): FileSystemWriteStream { - if (options?.ensureFolderExists) { - const folderPath: string = nodeJsPath.dirname(filePath); - FileSystem.ensureFolder(folderPath); - } + return FileSystem._wrapException(() => { + if (options?.ensureFolderExists) { + const folderPath: string = nodeJsPath.dirname(filePath); + FileSystem.ensureFolder(folderPath); + } - return fs.createWriteStream(filePath); + return fs.createWriteStream(filePath); + }); } /** @@ -1304,12 +1308,14 @@ export class FileSystem { filePath: string, options?: IFileSystemCreateWriteStreamOptions ): Promise { - if (options?.ensureFolderExists) { - const folderPath: string = nodeJsPath.dirname(filePath); - await FileSystem.ensureFolderAsync(folderPath); - } + return await FileSystem._wrapExceptionAsync(async () => { + if (options?.ensureFolderExists) { + const folderPath: string = nodeJsPath.dirname(filePath); + await FileSystem.ensureFolderAsync(folderPath); + } - return fs.createWriteStream(filePath); + return fs.createWriteStream(filePath); + }); } // =============== From 67c5bb8cc595676b7d7b9df54439e1ab085b80c0 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 8 Apr 2026 12:38:50 -0700 Subject: [PATCH 5/7] fixup! Add createReadStream/createWriteStream to FileSystem in node-core-library and update consumers to use FileSystem instead of fs directly --- ...lot-stream-cache-entry-for-http-plugin_2026-04-05-04-27.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@rushstack/node-core-library/copilot-stream-cache-entry-for-http-plugin_2026-04-05-04-27.json b/common/changes/@rushstack/node-core-library/copilot-stream-cache-entry-for-http-plugin_2026-04-05-04-27.json index 50836a0011e..7c7d4cd77b9 100644 --- a/common/changes/@rushstack/node-core-library/copilot-stream-cache-entry-for-http-plugin_2026-04-05-04-27.json +++ b/common/changes/@rushstack/node-core-library/copilot-stream-cache-entry-for-http-plugin_2026-04-05-04-27.json @@ -1,7 +1,7 @@ { "changes": [ { - "comment": "Add createReadStream() and createWriteStream() APIs, along with FileSystemReadStream and FileSystemWriteStream type aliases", + "comment": "Add `FileSystem.createReadStream`, `FileSystem.createWriteStream`, and `FileSystem.createWriteStreamAsync` APIs for creating read and write filesystem streams.", "type": "minor", "packageName": "@rushstack/node-core-library" } From 601df735ab6d32b73e5f549c253b7d473ff68bd3 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 8 Apr 2026 12:44:59 -0700 Subject: [PATCH 6/7] Add unit tests for FileSystem.createReadStream/createWriteStream Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/test/FileSystem.test.ts | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/libraries/node-core-library/src/test/FileSystem.test.ts b/libraries/node-core-library/src/test/FileSystem.test.ts index 05e257d4917..1ef3addc76a 100644 --- a/libraries/node-core-library/src/test/FileSystem.test.ts +++ b/libraries/node-core-library/src/test/FileSystem.test.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import * as nodeJsOs from 'node:os'; import fs from 'node:fs'; import { FileSystem } from '../FileSystem'; @@ -51,4 +52,141 @@ describe(FileSystem.name, () => { } }); }); + + describe(FileSystem.createReadStream.name, () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(`${nodeJsOs.tmpdir()}/filesystem-test-`); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test('returns a readable stream for an existing file', (done) => { + const filePath: string = `${tempDir}/test.txt`; + fs.writeFileSync(filePath, 'hello world'); + + const stream: fs.ReadStream = FileSystem.createReadStream(filePath); + const chunks: Buffer[] = []; + + stream.on('data', (chunk: string | Buffer) => { + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); + }); + + stream.on('end', () => { + const result: string = Buffer.concat(chunks).toString(); + expect(result).toBe('hello world'); + done(); + }); + + stream.on('error', done); + }); + + test('stream emits an error for a nonexistent file', (done) => { + const filePath: string = `${tempDir}/nonexistent.txt`; + const stream: fs.ReadStream = FileSystem.createReadStream(filePath); + + stream.on('error', (error: NodeJS.ErrnoException) => { + expect(error.code).toBe('ENOENT'); + done(); + }); + }); + }); + + describe(FileSystem.createWriteStream.name, () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(`${nodeJsOs.tmpdir()}/filesystem-test-`); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test('creates a writable stream that writes data to a file', (done) => { + const filePath: string = `${tempDir}/output.txt`; + const stream: fs.WriteStream = FileSystem.createWriteStream(filePath); + + stream.write('hello '); + stream.write('world'); + stream.end(() => { + const result: string = fs.readFileSync(filePath, 'utf-8'); + expect(result).toBe('hello world'); + done(); + }); + + stream.on('error', done); + }); + + test('emits an error when the parent folder does not exist and ensureFolderExists is not set', (done) => { + const filePath: string = `${tempDir}/nonexistent-folder/output.txt`; + const stream: fs.WriteStream = FileSystem.createWriteStream(filePath); + + stream.on('error', (error: NodeJS.ErrnoException) => { + expect(error.code).toBe('ENOENT'); + done(); + }); + }); + + test('creates the parent folder when ensureFolderExists is true', (done) => { + const filePath: string = `${tempDir}/new-folder/output.txt`; + const stream: fs.WriteStream = FileSystem.createWriteStream(filePath, { + ensureFolderExists: true + }); + + stream.write('test data'); + stream.end(() => { + const result: string = fs.readFileSync(filePath, 'utf-8'); + expect(result).toBe('test data'); + done(); + }); + + stream.on('error', done); + }); + }); + + describe('createWriteStreamAsync', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = fs.mkdtempSync(`${nodeJsOs.tmpdir()}/filesystem-test-`); + }); + + afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + + test('creates a writable stream that writes data to a file', async () => { + const filePath: string = `${tempDir}/output.txt`; + const stream: fs.WriteStream = await FileSystem.createWriteStreamAsync(filePath); + + await new Promise((resolve, reject) => { + stream.on('error', reject); + stream.write('async hello'); + stream.end(() => resolve()); + }); + + const result: string = fs.readFileSync(filePath, 'utf-8'); + expect(result).toBe('async hello'); + }); + + test('creates the parent folder when ensureFolderExists is true', async () => { + const filePath: string = `${tempDir}/new-folder/output.txt`; + const stream: fs.WriteStream = await FileSystem.createWriteStreamAsync(filePath, { + ensureFolderExists: true + }); + + await new Promise((resolve, reject) => { + stream.on('error', reject); + stream.write('async test data'); + stream.end(() => resolve()); + }); + + const result: string = fs.readFileSync(filePath, 'utf-8'); + expect(result).toBe('async test data'); + }); + }); }); From 489eeee4ca266c24c89bf0fff616dba30c141768 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Wed, 8 Apr 2026 13:27:01 -0700 Subject: [PATCH 7/7] Address PR feedback: use async APIs, deterministic paths, and async iteration Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/test/FileSystem.test.ts | 111 +++++++++--------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/libraries/node-core-library/src/test/FileSystem.test.ts b/libraries/node-core-library/src/test/FileSystem.test.ts index 1ef3addc76a..8aa3432f6c3 100644 --- a/libraries/node-core-library/src/test/FileSystem.test.ts +++ b/libraries/node-core-library/src/test/FileSystem.test.ts @@ -1,12 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as nodeJsOs from 'node:os'; import fs from 'node:fs'; import { FileSystem } from '../FileSystem'; import { PosixModeBits } from '../PosixModeBits'; +// Use a deterministic path inside the project's output folder for temp files +const testTempFolder: string = `${__dirname}/temp`; + describe(FileSystem.name, () => { test(FileSystem.formatPosixModeBits.name, () => { // The PosixModeBits are intended to be used with bitwise operations. @@ -54,109 +56,108 @@ describe(FileSystem.name, () => { }); describe(FileSystem.createReadStream.name, () => { - let tempDir: string; + const tempDir: string = `${testTempFolder}/createReadStream`; - beforeEach(() => { - tempDir = fs.mkdtempSync(`${nodeJsOs.tmpdir()}/filesystem-test-`); + beforeEach(async () => { + await FileSystem.ensureFolderAsync(tempDir); }); - afterEach(() => { - fs.rmSync(tempDir, { recursive: true, force: true }); + afterEach(async () => { + await FileSystem.deleteFolderAsync(tempDir); }); - test('returns a readable stream for an existing file', (done) => { + test('returns a readable stream for an existing file', async () => { const filePath: string = `${tempDir}/test.txt`; - fs.writeFileSync(filePath, 'hello world'); + await FileSystem.writeFileAsync(filePath, 'hello world'); const stream: fs.ReadStream = FileSystem.createReadStream(filePath); const chunks: Buffer[] = []; - stream.on('data', (chunk: string | Buffer) => { + for await (const chunk of stream) { chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); - }); - - stream.on('end', () => { - const result: string = Buffer.concat(chunks).toString(); - expect(result).toBe('hello world'); - done(); - }); + } - stream.on('error', done); + const result: string = Buffer.concat(chunks).toString(); + expect(result).toBe('hello world'); }); - test('stream emits an error for a nonexistent file', (done) => { + test('stream emits an error for a nonexistent file', async () => { const filePath: string = `${tempDir}/nonexistent.txt`; const stream: fs.ReadStream = FileSystem.createReadStream(filePath); - stream.on('error', (error: NodeJS.ErrnoException) => { - expect(error.code).toBe('ENOENT'); - done(); - }); + await expect(async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _chunk of stream) { + fail(); + } + }).rejects.toThrow(/ENOENT/); }); }); describe(FileSystem.createWriteStream.name, () => { - let tempDir: string; + const tempDir: string = `${testTempFolder}/createWriteStream`; - beforeEach(() => { - tempDir = fs.mkdtempSync(`${nodeJsOs.tmpdir()}/filesystem-test-`); + beforeEach(async () => { + await FileSystem.ensureFolderAsync(tempDir); }); - afterEach(() => { - fs.rmSync(tempDir, { recursive: true, force: true }); + afterEach(async () => { + await FileSystem.deleteFolderAsync(tempDir); }); - test('creates a writable stream that writes data to a file', (done) => { + test('creates a writable stream that writes data to a file', async () => { const filePath: string = `${tempDir}/output.txt`; const stream: fs.WriteStream = FileSystem.createWriteStream(filePath); - stream.write('hello '); - stream.write('world'); - stream.end(() => { - const result: string = fs.readFileSync(filePath, 'utf-8'); - expect(result).toBe('hello world'); - done(); + await new Promise((resolve, reject) => { + stream.on('error', reject); + stream.write('hello '); + stream.write('world'); + stream.end(() => resolve()); }); - stream.on('error', done); + const result: string = await FileSystem.readFileAsync(filePath); + expect(result).toBe('hello world'); }); - test('emits an error when the parent folder does not exist and ensureFolderExists is not set', (done) => { + test('emits an error when the parent folder does not exist and ensureFolderExists is not set', async () => { const filePath: string = `${tempDir}/nonexistent-folder/output.txt`; const stream: fs.WriteStream = FileSystem.createWriteStream(filePath); - stream.on('error', (error: NodeJS.ErrnoException) => { - expect(error.code).toBe('ENOENT'); - done(); - }); + await expect( + new Promise((resolve, reject) => { + stream.on('error', reject); + stream.on('open', () => resolve()); + }) + ).rejects.toThrow(/ENOENT/); }); - test('creates the parent folder when ensureFolderExists is true', (done) => { + test('creates the parent folder when ensureFolderExists is true', async () => { const filePath: string = `${tempDir}/new-folder/output.txt`; const stream: fs.WriteStream = FileSystem.createWriteStream(filePath, { ensureFolderExists: true }); - stream.write('test data'); - stream.end(() => { - const result: string = fs.readFileSync(filePath, 'utf-8'); - expect(result).toBe('test data'); - done(); + await new Promise((resolve, reject) => { + stream.on('error', reject); + stream.write('test data'); + stream.end(() => resolve()); }); - stream.on('error', done); + const result: string = await FileSystem.readFileAsync(filePath); + expect(result).toBe('test data'); }); }); - describe('createWriteStreamAsync', () => { - let tempDir: string; + describe(FileSystem.createWriteStreamAsync.name, () => { + const tempDir: string = `${testTempFolder}/createWriteStreamAsync`; - beforeEach(() => { - tempDir = fs.mkdtempSync(`${nodeJsOs.tmpdir()}/filesystem-test-`); + beforeEach(async () => { + await FileSystem.ensureFolderAsync(tempDir); }); - afterEach(() => { - fs.rmSync(tempDir, { recursive: true, force: true }); + afterEach(async () => { + await FileSystem.deleteFolderAsync(tempDir); }); test('creates a writable stream that writes data to a file', async () => { @@ -169,7 +170,7 @@ describe(FileSystem.name, () => { stream.end(() => resolve()); }); - const result: string = fs.readFileSync(filePath, 'utf-8'); + const result: string = await FileSystem.readFileAsync(filePath); expect(result).toBe('async hello'); }); @@ -185,7 +186,7 @@ describe(FileSystem.name, () => { stream.end(() => resolve()); }); - const result: string = fs.readFileSync(filePath, 'utf-8'); + const result: string = await FileSystem.readFileAsync(filePath); expect(result).toBe('async test data'); }); });