diff --git a/.changeset/yellow-cows-sniff.md b/.changeset/yellow-cows-sniff.md new file mode 100644 index 00000000000..7b28a1f4989 --- /dev/null +++ b/.changeset/yellow-cows-sniff.md @@ -0,0 +1,5 @@ +--- +"@smithy/shared-ini-file-loader": minor +--- + +export readFile from shared-ini-file-loader diff --git a/packages/shared-ini-file-loader/package.json b/packages/shared-ini-file-loader/package.json index e234a49ecf1..868b8319684 100644 --- a/packages/shared-ini-file-loader/package.json +++ b/packages/shared-ini-file-loader/package.json @@ -37,13 +37,13 @@ "types": "./dist-types/index.d.ts", "browser": { "./dist-es/getSSOTokenFromFile": false, - "./dist-es/slurpFile": false + "./dist-es/readFile": false }, "react-native": { "./dist-cjs/getSSOTokenFromFile": false, - "./dist-cjs/slurpFile": false, + "./dist-cjs/readFile": false, "./dist-es/getSSOTokenFromFile": false, - "./dist-es/slurpFile": false + "./dist-es/readFile": false }, "engines": { "node": ">=18.0.0" diff --git a/packages/shared-ini-file-loader/src/externalDataInterceptor.spec.ts b/packages/shared-ini-file-loader/src/externalDataInterceptor.spec.ts index fb559a5ebf5..6acbf86de81 100644 --- a/packages/shared-ini-file-loader/src/externalDataInterceptor.spec.ts +++ b/packages/shared-ini-file-loader/src/externalDataInterceptor.spec.ts @@ -2,13 +2,13 @@ import { describe, expect, test as it } from "vitest"; import { externalDataInterceptor } from "./externalDataInterceptor"; import { getSSOTokenFromFile } from "./getSSOTokenFromFile"; -import { slurpFile } from "./slurpFile"; +import { readFile } from "./readFile"; describe("fileMockController", () => { - it("intercepts slurpFile", async () => { + it("intercepts readFile", async () => { externalDataInterceptor.interceptFile("abcd", "contents"); - expect(await slurpFile("abcd")).toEqual("contents"); + expect(await readFile("abcd")).toEqual("contents"); expect(externalDataInterceptor.getFileRecord()).toEqual({ abcd: Promise.resolve("contents"), }); diff --git a/packages/shared-ini-file-loader/src/externalDataInterceptor.ts b/packages/shared-ini-file-loader/src/externalDataInterceptor.ts index 3aa898338b6..fb0998db483 100644 --- a/packages/shared-ini-file-loader/src/externalDataInterceptor.ts +++ b/packages/shared-ini-file-loader/src/externalDataInterceptor.ts @@ -1,5 +1,5 @@ import { tokenIntercept } from "./getSSOTokenFromFile"; -import { fileIntercept } from "./slurpFile"; +import { fileIntercept } from "./readFile"; /** * @internal diff --git a/packages/shared-ini-file-loader/src/getSSOTokenFromFile.spec.ts b/packages/shared-ini-file-loader/src/getSSOTokenFromFile.spec.ts index 8c5f9f1a50f..821cdd8b5c1 100644 --- a/packages/shared-ini-file-loader/src/getSSOTokenFromFile.spec.ts +++ b/packages/shared-ini-file-loader/src/getSSOTokenFromFile.spec.ts @@ -1,11 +1,10 @@ -// ToDo: Change to "fs/promises" when supporting nodejs>=14 -import { promises } from "fs"; +import * as promises from "node:fs/promises"; import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest"; import { getSSOTokenFilepath } from "./getSSOTokenFilepath"; import { getSSOTokenFromFile } from "./getSSOTokenFromFile"; -vi.mock("fs", () => ({ promises: { readFile: vi.fn() } })); +vi.mock("node:fs/promises", () => ({ readFile: vi.fn() })); vi.mock("./getSSOTokenFilepath"); describe(getSSOTokenFromFile.name, () => { diff --git a/packages/shared-ini-file-loader/src/getSSOTokenFromFile.ts b/packages/shared-ini-file-loader/src/getSSOTokenFromFile.ts index e0341d49322..c391bbe06f5 100644 --- a/packages/shared-ini-file-loader/src/getSSOTokenFromFile.ts +++ b/packages/shared-ini-file-loader/src/getSSOTokenFromFile.ts @@ -1,10 +1,7 @@ -// ToDo: Change to "fs/promises" when supporting nodejs>=14 -import { promises as fsPromises } from "fs"; +import { readFile } from "fs/promises"; import { getSSOTokenFilepath } from "./getSSOTokenFilepath"; -const { readFile } = fsPromises; - /** * Cached SSO token retrieved from SSO login flow. * @public @@ -54,7 +51,9 @@ export interface SSOToken { } /** + * For testing only. * @internal + * @deprecated minimize use in application code. */ export const tokenIntercept = {} as Record; diff --git a/packages/shared-ini-file-loader/src/index.ts b/packages/shared-ini-file-loader/src/index.ts index 33cd617c627..29869da9f6f 100644 --- a/packages/shared-ini-file-loader/src/index.ts +++ b/packages/shared-ini-file-loader/src/index.ts @@ -7,3 +7,4 @@ export * from "./loadSsoSessionData"; export * from "./parseKnownFiles"; export { externalDataInterceptor } from "./externalDataInterceptor"; export * from "./types"; +export { type ReadFileOptions, readFile } from "./readFile"; diff --git a/packages/shared-ini-file-loader/src/loadSharedConfigFiles.spec.ts b/packages/shared-ini-file-loader/src/loadSharedConfigFiles.spec.ts index da823ed75fd..3a1b99d4803 100644 --- a/packages/shared-ini-file-loader/src/loadSharedConfigFiles.spec.ts +++ b/packages/shared-ini-file-loader/src/loadSharedConfigFiles.spec.ts @@ -6,13 +6,13 @@ import { getCredentialsFilepath } from "./getCredentialsFilepath"; import { getHomeDir } from "./getHomeDir"; import { loadSharedConfigFiles } from "./loadSharedConfigFiles"; import { parseIni } from "./parseIni"; -import { slurpFile } from "./slurpFile"; +import { readFile } from "./readFile"; vi.mock("./getConfigData"); vi.mock("./getConfigFilepath"); vi.mock("./getCredentialsFilepath"); vi.mock("./parseIni"); -vi.mock("./slurpFile"); +vi.mock("./readFile"); vi.mock("./getHomeDir"); describe("loadSharedConfigFiles", () => { @@ -29,7 +29,7 @@ describe("loadSharedConfigFiles", () => { vi.mocked(getCredentialsFilepath).mockReturnValue(mockCredsFilepath); vi.mocked(parseIni).mockImplementation((args: any) => args); vi.mocked(getConfigData).mockImplementation((args) => args); - vi.mocked(slurpFile).mockImplementation((path) => Promise.resolve(path)); + vi.mocked(readFile).mockImplementation((path) => Promise.resolve(path)); vi.mocked(getHomeDir).mockReturnValue(mockHomeDir); }); @@ -71,7 +71,7 @@ describe("loadSharedConfigFiles", () => { describe("swallows error and returns empty configuration", () => { it("when readFile throws error", async () => { - vi.mocked(slurpFile).mockRejectedValue("error"); + vi.mocked(readFile).mockRejectedValue("error"); const sharedConfigFiles = await loadSharedConfigFiles(); expect(sharedConfigFiles).toStrictEqual({ configFile: {}, credentialsFile: {} }); }); diff --git a/packages/shared-ini-file-loader/src/loadSharedConfigFiles.ts b/packages/shared-ini-file-loader/src/loadSharedConfigFiles.ts index 79f854e7145..7a6941a6d9c 100644 --- a/packages/shared-ini-file-loader/src/loadSharedConfigFiles.ts +++ b/packages/shared-ini-file-loader/src/loadSharedConfigFiles.ts @@ -6,7 +6,7 @@ import { getConfigFilepath } from "./getConfigFilepath"; import { getCredentialsFilepath } from "./getCredentialsFilepath"; import { getHomeDir } from "./getHomeDir"; import { parseIni } from "./parseIni"; -import { slurpFile } from "./slurpFile"; +import { readFile } from "./readFile"; /** * @public @@ -62,13 +62,13 @@ export const loadSharedConfigFiles = async (init: SharedConfigInit = {}): Promis } const parsedFiles = await Promise.all([ - slurpFile(resolvedConfigFilepath, { + readFile(resolvedConfigFilepath, { ignoreCache: init.ignoreCache, }) .then(parseIni) .then(getConfigData) .catch(swallowError), - slurpFile(resolvedFilepath, { + readFile(resolvedFilepath, { ignoreCache: init.ignoreCache, }) .then(parseIni) diff --git a/packages/shared-ini-file-loader/src/loadSsoSessionData.spec.ts b/packages/shared-ini-file-loader/src/loadSsoSessionData.spec.ts index baa0e965454..f11f46c3b00 100644 --- a/packages/shared-ini-file-loader/src/loadSsoSessionData.spec.ts +++ b/packages/shared-ini-file-loader/src/loadSsoSessionData.spec.ts @@ -4,12 +4,12 @@ import { getConfigFilepath } from "./getConfigFilepath"; import { getSsoSessionData } from "./getSsoSessionData"; import { loadSsoSessionData } from "./loadSsoSessionData"; import { parseIni } from "./parseIni"; -import { slurpFile } from "./slurpFile"; +import { readFile } from "./readFile"; vi.mock("./getConfigFilepath"); vi.mock("./getSsoSessionData"); vi.mock("./parseIni"); -vi.mock("./slurpFile"); +vi.mock("./readFile"); describe(loadSsoSessionData.name, () => { const mockConfigFilepath = "/mock/file/path/config"; @@ -19,7 +19,7 @@ describe(loadSsoSessionData.name, () => { vi.mocked(getConfigFilepath).mockReturnValue(mockConfigFilepath); vi.mocked(parseIni).mockImplementation((args: any) => args); vi.mocked(getSsoSessionData).mockReturnValue(mockSsoSessionData); - vi.mocked(slurpFile).mockImplementation((path) => Promise.resolve(path)); + vi.mocked(readFile).mockImplementation((path) => Promise.resolve(path)); }); afterEach(() => { @@ -42,7 +42,7 @@ describe(loadSsoSessionData.name, () => { describe("swallows error and returns empty configuration", () => { it("when readFile throws error", async () => { - vi.mocked(slurpFile).mockRejectedValue("error"); + vi.mocked(readFile).mockRejectedValue("error"); const ssoSessionData = await loadSsoSessionData(); expect(ssoSessionData).toStrictEqual({}); }); diff --git a/packages/shared-ini-file-loader/src/loadSsoSessionData.ts b/packages/shared-ini-file-loader/src/loadSsoSessionData.ts index 029ebd04b06..c709e949a9d 100644 --- a/packages/shared-ini-file-loader/src/loadSsoSessionData.ts +++ b/packages/shared-ini-file-loader/src/loadSsoSessionData.ts @@ -3,7 +3,7 @@ import type { ParsedIniData } from "@smithy/types"; import { getConfigFilepath } from "./getConfigFilepath"; import { getSsoSessionData } from "./getSsoSessionData"; import { parseIni } from "./parseIni"; -import { slurpFile } from "./slurpFile"; +import { readFile } from "./readFile"; /** * Subset of {@link SharedConfigInit}. @@ -24,7 +24,7 @@ const swallowError = () => ({}); * @internal */ export const loadSsoSessionData = async (init: SsoSessionInit = {}): Promise => - slurpFile(init.configFilepath ?? getConfigFilepath()) + readFile(init.configFilepath ?? getConfigFilepath()) .then(parseIni) .then(getSsoSessionData) .catch(swallowError); diff --git a/packages/shared-ini-file-loader/src/slurpFile.spec.ts b/packages/shared-ini-file-loader/src/readFile.spec.ts similarity index 63% rename from packages/shared-ini-file-loader/src/slurpFile.spec.ts rename to packages/shared-ini-file-loader/src/readFile.spec.ts index 3b84db07eca..cef412133ee 100644 --- a/packages/shared-ini-file-loader/src/slurpFile.spec.ts +++ b/packages/shared-ini-file-loader/src/readFile.spec.ts @@ -1,10 +1,9 @@ -// ToDo: Change to "fs/promises" when supporting nodejs>=14 -import { promises } from "fs"; +import * as promises from "node:fs/promises"; import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest"; -vi.mock("fs", () => ({ promises: { readFile: vi.fn() } })); +vi.mock("node:fs/promises", () => ({ readFile: vi.fn() })); -describe("slurpFile", () => { +describe("readFile", () => { const UTF8 = "utf8"; const getMockFileContents = (path: string, options = UTF8) => JSON.stringify({ path, options }); @@ -20,44 +19,44 @@ describe("slurpFile", () => { vi.resetModules(); }); - describe("makes one readFile call for a filepath irrespective of slurpFile calls", () => { + describe("makes one fs.readFile call for a filepath irrespective of smithy.readFile calls", () => { it.each([10, 100, 1000, 10000])("parallel calls: %d ", async (num: number) => { - const { slurpFile } = await import("./slurpFile"); + const { readFile } = await import("./readFile"); const mockPath = "/mock/path"; const mockPathContent = getMockFileContents(mockPath); expect(promises.readFile).not.toHaveBeenCalled(); - const fileContentArr = await Promise.all(Array(num).fill(slurpFile(mockPath))); + const fileContentArr = await Promise.all(Array(num).fill(readFile(mockPath))); expect(fileContentArr).toStrictEqual(Array(num).fill(mockPathContent)); - // There is one readFile call even through slurpFile is called in parallel num times. + // There is one fs.readFile call even through smithy.readFile is called in parallel num times. expect(promises.readFile).toHaveBeenCalledTimes(1); expect(promises.readFile).toHaveBeenCalledWith(mockPath, UTF8); }); it("two parallel calls and one sequential call", async () => { - const { slurpFile } = await import("./slurpFile"); + const { readFile } = await import("./readFile"); const mockPath = "/mock/path"; const mockPathContent = getMockFileContents(mockPath); expect(promises.readFile).not.toHaveBeenCalled(); - const fileContentArr = await Promise.all([slurpFile(mockPath), slurpFile(mockPath)]); + const fileContentArr = await Promise.all([readFile(mockPath), readFile(mockPath)]); expect(fileContentArr).toStrictEqual([mockPathContent, mockPathContent]); - // There is one readFile call even through slurpFile is called in parallel twice. + // There is one fs.readFile call even though smithy.readFile is called in parallel twice. expect(promises.readFile).toHaveBeenCalledTimes(1); expect(promises.readFile).toHaveBeenCalledWith(mockPath, UTF8); - const fileContent = await slurpFile(mockPath); + const fileContent = await readFile(mockPath); expect(fileContent).toStrictEqual(mockPathContent); - // There is one readFile call even through slurpFile is called for the third time. + // There is one fs.readFile call even though smithy.readFile is called for the third time. expect(promises.readFile).toHaveBeenCalledTimes(1); }); }); it("makes multiple readFile calls with based on filepaths", async () => { - const { slurpFile } = await import("./slurpFile"); + const { readFile } = await import("./readFile"); const mockPath1 = "/mock/path/1"; const mockPathContent1 = getMockFileContents(mockPath1); @@ -66,45 +65,45 @@ describe("slurpFile", () => { const mockPathContent2 = getMockFileContents(mockPath2); expect(promises.readFile).not.toHaveBeenCalled(); - const fileContentArr = await Promise.all([slurpFile(mockPath1), slurpFile(mockPath2)]); + const fileContentArr = await Promise.all([readFile(mockPath1), readFile(mockPath2)]); expect(fileContentArr).toStrictEqual([mockPathContent1, mockPathContent2]); - // There are two readFile calls as slurpFile is called in parallel with different filepaths. + // There are two fs.readFile calls as smithy.readFile is called in parallel with different file paths. expect(promises.readFile).toHaveBeenCalledTimes(2); expect(promises.readFile).toHaveBeenNthCalledWith(1, mockPath1, UTF8); expect(promises.readFile).toHaveBeenNthCalledWith(2, mockPath2, UTF8); - const fileContent1 = await slurpFile(mockPath1); + const fileContent1 = await readFile(mockPath1); expect(fileContent1).toStrictEqual(mockPathContent1); - const fileContent2 = await slurpFile(mockPath2); + const fileContent2 = await readFile(mockPath2); expect(fileContent2).toStrictEqual(mockPathContent2); - // There is one readFile call even through slurpFile is called for the third time. + // There is one fs.readFile call even though smithy.readFile is called for the third time. expect(promises.readFile).toHaveBeenCalledTimes(2); }); it("makes multiple readFile calls when called with ignoreCache option", async () => { - const { slurpFile } = await import("./slurpFile"); + const { readFile } = await import("./readFile"); const mockPath1 = "/mock/path/1"; const mockPathContent1 = getMockFileContents(mockPath1); expect(promises.readFile).not.toHaveBeenCalled(); const fileContentArr = await Promise.all([ - slurpFile(mockPath1, { ignoreCache: true }), - slurpFile(mockPath1, { ignoreCache: true }), + readFile(mockPath1, { ignoreCache: true }), + readFile(mockPath1, { ignoreCache: true }), ]); expect(fileContentArr).toStrictEqual([mockPathContent1, mockPathContent1]); - // There are two readFile calls as slurpFile is called in parallel with the same filepath. + // There are two fs.readFile calls as smithy.readFile is called in parallel with the same filepath. expect(promises.readFile).toHaveBeenCalledTimes(2); expect(promises.readFile).toHaveBeenNthCalledWith(1, mockPath1, UTF8); expect(promises.readFile).toHaveBeenNthCalledWith(2, mockPath1, UTF8); - const fileContent1 = await slurpFile(mockPath1); + const fileContent1 = await readFile(mockPath1); expect(fileContent1).toStrictEqual(mockPathContent1); - // There is no readFile call since slurpFile is now called without refresh. + // There is no fs.readFile call since smithy.readFile is now called without refresh. expect(promises.readFile).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/shared-ini-file-loader/src/readFile.ts b/packages/shared-ini-file-loader/src/readFile.ts new file mode 100644 index 00000000000..ad5e6ac4683 --- /dev/null +++ b/packages/shared-ini-file-loader/src/readFile.ts @@ -0,0 +1,34 @@ +import { readFile as fsReadFile } from "node:fs/promises"; + +/** + * Runtime file cache. + * @internal + */ +export const filePromises: Record> = {}; + +/** + * For testing only. + * @internal + * @deprecated minimize use in application code. + */ +export const fileIntercept: Record> = {}; + +/** + * @internal + */ +export interface ReadFileOptions { + ignoreCache?: boolean; +} + +/** + * @internal + */ +export const readFile = (path: string, options?: ReadFileOptions) => { + if (fileIntercept[path] !== undefined) { + return fileIntercept[path]; + } + if (!filePromises[path] || options?.ignoreCache) { + filePromises[path] = fsReadFile(path, "utf8"); + } + return filePromises[path]; +}; diff --git a/packages/shared-ini-file-loader/src/slurpFile.ts b/packages/shared-ini-file-loader/src/slurpFile.ts deleted file mode 100644 index 7f63e860065..00000000000 --- a/packages/shared-ini-file-loader/src/slurpFile.ts +++ /dev/null @@ -1,21 +0,0 @@ -// ToDo: Change to "fs/promises" when supporting nodejs>=14 -import { promises as fsPromises } from "fs"; - -const { readFile } = fsPromises; - -export const filePromisesHash: Record> = {}; -export const fileIntercept: Record> = {}; - -interface SlurpFileOptions { - ignoreCache?: boolean; -} - -export const slurpFile = (path: string, options?: SlurpFileOptions) => { - if (fileIntercept[path] !== undefined) { - return fileIntercept[path]; - } - if (!filePromisesHash[path] || options?.ignoreCache) { - filePromisesHash[path] = readFile(path, "utf8"); - } - return filePromisesHash[path]; -};