diff --git a/README.md b/README.md index b57a299f..8aa06ae9 100644 --- a/README.md +++ b/README.md @@ -139,20 +139,17 @@ db.set("key", "value", false); // Don't update the timestamp ### Import / Export -Importing JSON files can be done this way: +Importing structured data into the database can be done the following way: ```ts -// pass a filename, the import will be asynchronous -await db.importJson(filename); // pass the object directly, the import will be synchronous -db.importJson({key: "value"}); +db.importJSON({key: "value"}); ``` -In both cases, existing entries in the DB will not be deleted but will be overwritten if they exist. +Existing entries in the DB will not be deleted but will be overwritten if they exist. -Exporting JSON files is also possible: +Exporting the data as a JSON object is also possible: ```ts -await db.exportJson(filename[, options]); +const json = db.toJSON(); ``` -The file will be overwritten if it exists. The 2nd options argument can be used to control the file formatting. Since `fs-extra`'s `writeJson` is used under the hood, take a look at that [method documentation](https://github.com/jprichardson/node-fs-extra/blob/master/docs/writeJson.md) for details on the options object. ## Changelog @@ -160,6 +157,13 @@ The file will be overwritten if it exists. The 2nd options argument can be used Placeholder for next release: ### __WORK IN PROGRESS__ --> +### __WORK IN PROGRESS__ +* BREAKING: Drop support for Node.js versions older than v22 (#529) +* BREAKING: Removed `exportJson` method in favor of `toJSON` +* BREAKING: Renamed `importJson` method to `importJSON`. Importing from a file is no longer supported. Read the file yourself if you need this functionality. +* Filesystem access is now done using the native `fs.promises` instead of `fs-extra` +* The package is now a hybrid ESM/CJS package (#529) + ### 3.1.1 (2024-01-25) * Reduced CPU load while idle (#475) diff --git a/package-lock.json b/package-lock.json index 4d584e10..36282a0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,7 @@ "license": "MIT", "dependencies": { "@alcalzone/proper-lockfile": "^4.1.3-0", - "alcalzone-shared": "^5.0.0", - "fs-extra": "^10.1.0" + "alcalzone-shared": "^5.0.0" }, "devDependencies": { "@alcalzone/esm2cjs": "^1.4.1", @@ -20,7 +19,6 @@ "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", "@tsconfig/node22": "^22.0.2", - "@types/fs-extra": "^9.0.13", "@types/node": "^22.18.8", "@types/proper-lockfile": "^4.1.2", "@vitest/coverage-istanbul": "^3.2.4", @@ -2731,15 +2729,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4774,6 +4763,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -5667,6 +5657,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -7565,6 +7556,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, "engines": { "node": ">= 10.0.0" } @@ -9687,15 +9679,6 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, - "@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -11065,6 +11048,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -11685,6 +11669,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "requires": { "graceful-fs": "^4.1.6", "universalify": "^2.0.0" @@ -12966,7 +12951,8 @@ "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true }, "update-browserslist-db": { "version": "1.1.3", diff --git a/package.json b/package.json index e722ffa4..e528f672 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", "@tsconfig/node22": "^22.0.2", - "@types/fs-extra": "^9.0.13", "@types/node": "^22.18.8", "@types/proper-lockfile": "^4.1.2", "@vitest/coverage-istanbul": "^3.2.4", @@ -64,8 +63,7 @@ }, "dependencies": { "@alcalzone/proper-lockfile": "^4.1.3-0", - "alcalzone-shared": "^5.0.0", - "fs-extra": "^10.1.0" + "alcalzone-shared": "^5.0.0" }, "overrides": { "minimist@1.2.5": "^1.2.6" diff --git a/src/index.ts b/src/index.ts index 6e12f0b6..e7b791a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -export { FsWriteOptions, JsonlDB, JsonlDBOptions } from "./lib/db.js"; +export { JsonlDB, JsonlDBOptions } from "./lib/db.js"; diff --git a/src/lib/db.ts b/src/lib/db.ts index a0a0f863..4affc4a0 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -4,11 +4,20 @@ import { createDeferredPromise, DeferredPromise, } from "alcalzone-shared/deferred-promise"; -import * as fs from "fs-extra"; +import type { FileHandle } from "node:fs/promises"; +import * as fs from "node:fs/promises"; import * as path from "path"; import * as readline from "readline"; import { Signal } from "./signal.js"; +async function ensureDir(dirPath: string): Promise { + await fs.mkdir(dirPath, { recursive: true }); +} + +async function remove(filePath: string): Promise { + await fs.rm(filePath, { recursive: true, force: true }); +} + export interface JsonlDBOptions { /** * Whether errors reading the db file (e.g. invalid JSON) should silently be ignored. @@ -101,17 +110,6 @@ export interface JsonlDBOptions { lockfileDirectory?: string; } -/** This is the same as `fs-extra`'s WriteOptions */ -export interface FsWriteOptions { - encoding?: string | null; - flag?: string; - mode?: number; - fs?: object; - replacer?: any; - spaces?: number | string; - EOL?: string; -} - enum Operation { Clear = 0, Write = 1, @@ -163,8 +161,8 @@ async function fsyncDir(dirname: string): Promise { if (process.platform === "win32") return; const fd = await fs.open(dirname, "r"); - await fs.fsync(fd); - await fs.close(fd); + await fd.sync(); + await fd.close(); } function getCurrentErrorStack(): string { @@ -355,7 +353,7 @@ export class JsonlDB { private _persistenceTaskSignal = new Signal(); private _journal: LazyEntry[] = []; - private _fd: number | undefined; + private _handle: FileHandle | undefined; private drainJournal(): LazyEntry[] { return this._journal.splice(0, this._journal.length); @@ -370,7 +368,7 @@ export class JsonlDB { // /** Opens the database file or creates it if it doesn't exist */ public async open(): Promise { // Open the file for appending and reading - await fs.ensureDir(path.dirname(this.filename)); + await ensureDir(path.dirname(this.filename)); let retryOptions: lockfile.LockOptions["retries"]; if (this.options.lockfile?.retries) { @@ -384,7 +382,7 @@ export class JsonlDB { } try { - await fs.ensureDir(path.dirname(this.lockfileName)); + await ensureDir(path.dirname(this.lockfileName)); await lockfile.lock(this.lockfileName, { // We cannot be sure that the file exists before acquiring the lock realpath: false, @@ -409,10 +407,9 @@ export class JsonlDB { // If the application crashed previously, try to recover from it await this.tryRecoverDBFiles(); - this._fd = await fs.open(this.filename, "a+"); - const readStream = fs.createReadStream(this.filename, { + this._handle = await fs.open(this.filename, "a+"); + const readStream = this._handle.createReadStream({ encoding: "utf8", - fd: this._fd, autoClose: false, }); const rl = readline.createInterface(readStream); @@ -470,8 +467,8 @@ export class JsonlDB { } finally { // Close the file again to avoid EBADF rl.close(); - await fs.close(this._fd); - this._fd = undefined; + await this._handle.close(); + this._handle = undefined; } this.updateCompressBySizeThreshold(); @@ -509,12 +506,12 @@ export class JsonlDB { // Prefer the DB file if it exists, remove the others in case they exist if (dbFileIsOK) { try { - await fs.remove(this.backupFilename); + await remove(this.backupFilename); } catch { // ignore } try { - await fs.remove(this.dumpFilename); + await remove(this.dumpFilename); } catch { // ignore } @@ -533,11 +530,9 @@ export class JsonlDB { if (bakFileIsOK) { // Overwrite the broken db file with it and delete the dump file try { - await fs.move(this.backupFilename, this.filename, { - overwrite: true, - }); + await fs.rename(this.backupFilename, this.filename); try { - await fs.remove(this.dumpFilename); + await remove(this.dumpFilename); } catch { // ignore } @@ -558,11 +553,9 @@ export class JsonlDB { if (dumpFileIsOK) { try { // Overwrite the broken db file with the dump file and delete the backup file - await fs.move(this.dumpFilename, this.filename, { - overwrite: true, - }); + await fs.rename(this.dumpFilename, this.filename); try { - await fs.remove(this.backupFilename); + await remove(this.backupFilename); } catch { // ignore } @@ -661,28 +654,12 @@ export class JsonlDB { return this; } - private async importJsonFile(filename: string): Promise { - const json = await fs.readJSON(filename); - return this.importJson(json); - } - - public importJson(filename: string): Promise; - public importJson(json: Record): void; - public importJson( - jsonOrFile: Record | string, - ): void | Promise { - if (typeof jsonOrFile === "string") { - if (!this._isOpen) { - return Promise.reject(new Error("The database is not open!")); - } - return this.importJsonFile(jsonOrFile); - } else { - if (!this._isOpen) { - throw new Error("The database is not open!"); - } + public importJSON(json: Record): void { + if (!this._isOpen) { + throw new Error("The database is not open!"); } - for (const [key, value] of Object.entries(jsonOrFile)) { + for (const [key, value] of Object.entries(json)) { // Importing JSON does not have timestamp information this._db.set(key, value); this._journal.push(this.makeLazyWrite(key, value)); @@ -692,18 +669,11 @@ export class JsonlDB { this.triggerJournalFlush(); } - public async exportJson( - filename: string, - options?: FsWriteOptions, - ): Promise { + public toJSON(): Record { if (!this._isOpen) { - return Promise.reject(new Error("The database is not open!")); + throw new Error("The database is not open!"); } - return fs.writeJSON( - filename, - Object.fromEntries([...this._db]), - options, - ); + return Object.fromEntries([...this._db]); } private entryToLine(key: string, value?: V, timestamp?: number): string { @@ -797,7 +767,7 @@ export class JsonlDB { serialized += this.entryToLine(key, value) + "\n"; } } - await fs.appendFile(fd, serialized); + await fd.appendFile(serialized); // In case there is any new data in the journal, persist that too let journal = drainJournal @@ -806,7 +776,7 @@ export class JsonlDB { journal = journal.slice(journalLength); await this.writeJournalToFile(fd, journal, false); - await fs.close(fd); + await fd.close(); } /** @@ -873,7 +843,7 @@ export class JsonlDB { let lastCompress = Date.now(); // Open the file for appending and reading - this._fd = await fs.open(this.filename, "a+"); + this._handle = await fs.open(this.filename, "a+"); this._openPromise?.resolve(); while (true) { @@ -976,16 +946,16 @@ export class JsonlDB { if (shouldWrite) { // Drain the journal const journal = this.drainJournal(); - this._fd = await this.writeJournalToFile( - this._fd, + this._handle = await this.writeJournalToFile( + this._handle, journal, ); lastWrite = Date.now(); } if (isStopCmd) { - await fs.close(this._fd); - this._fd = undefined; + await this._handle.close(); + this._handle = undefined; return; } else { // Since we wrote something, the uncompressed size may have changed @@ -1027,10 +997,10 @@ export class JsonlDB { /** Writes the given journal to the given file descriptor. Returns the new file descriptor if the file was re-opened during the process */ private async writeJournalToFile( - fd: number, + fd: FileHandle, journal: LazyEntry[], updateStatistics: boolean = true, - ): Promise { + ): Promise { // The chunk map is used to buffer all entries that are currently waiting in line // so we avoid serializing redundant entries. When the writing is throttled, // the chunk map will only be used for a short time. @@ -1052,7 +1022,7 @@ export class JsonlDB { if (truncate) { // Since we opened the file in append mode, we cannot truncate // therefore close and open in write mode again - await fs.close(fd); + await fd.close(); fd = await fs.open(this.filename, "w+"); truncate = false; if (updateStatistics) { @@ -1070,8 +1040,8 @@ export class JsonlDB { } } // and write once, making sure everything is written - await fs.appendFile(fd, serialized); - await fs.fsync(fd); + await fd.appendFile(serialized); + await fd.sync(); // Reset the signals related to writing the journal this._journalFlushable.reset(); @@ -1088,9 +1058,9 @@ export class JsonlDB { private async doCompress(): Promise { // 1. Ensure the backup contains everything in the DB and journal const journal = this.drainJournal(); - this._fd = await this.writeJournalToFile(this._fd!, journal); - await fs.close(this._fd); - this._fd = undefined; + this._handle = await this.writeJournalToFile(this._handle!, journal); + await this._handle.close(); + this._handle = undefined; // 2. Create a dump, draining the journal to avoid duplicate writes await this.dumpInternal(this.dumpFilename, true); @@ -1105,17 +1075,15 @@ export class JsonlDB { await fsyncDir(path.dirname(this.filename)); // 4. Swap files around, then ensure the directory entries are written to disk - await fs.move(this.filename, this.backupFilename, { - overwrite: true, - }); - await fs.move(this.dumpFilename, this.filename, { overwrite: true }); + await fs.rename(this.filename, this.backupFilename); + await fs.rename(this.dumpFilename, this.filename); await fsyncDir(path.dirname(this.filename)); // 5. Delete backup await fs.unlink(this.backupFilename); // 6. open the main DB file again in append mode - this._fd = await fs.open(this.filename, "a+"); + this._handle = await fs.open(this.filename, "a+"); // Reset the signals related to compressing the DB this._compressBySizeNeeded.reset(); diff --git a/test/db.test.ts b/test/db.test.ts index 52c9ea4c..13cabcaf 100644 --- a/test/db.test.ts +++ b/test/db.test.ts @@ -1,10 +1,19 @@ import { wait } from "alcalzone-shared/async"; -import * as fs from "fs-extra"; +import * as fs from "node:fs/promises"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { JsonlDB } from "../src/lib/db.js"; import { TestFS } from "./testFs.js"; +async function pathExists(path: string): Promise { + try { + await fs.access(path); + return true; + } catch { + return false; + } +} + let mockAppendFileThrottle = 0; let mockMoveFileThrottle = 0; @@ -21,33 +30,57 @@ async function retry(times: number, test: () => Promise): Promise { } } -vi.mock("fs-extra", async () => { - const originalFS = (await vi.importActual("fs-extra")).default; +vi.mock("node:fs/promises", async () => { + const originalFS = await vi.importActual("node:fs/promises"); // eslint-disable-next-line @typescript-eslint/no-var-requires const wait = require("alcalzone-shared/async").wait; + + // Wrap the FileHandle methods to add throttling + const wrapFileHandle = (fh: any) => { + return new Proxy(fh, { + get(target: any, prop: string) { + if (prop === "appendFile") { + return async (...args: any[]) => { + if (mockAppendFileThrottle > 0) { + await wait(mockAppendFileThrottle); + } + return target[prop](...args); + }; + } + return target[prop]; + }, + }); + }; + return { - __esModule: true, // Use it when dealing with esModules + __esModule: true, ...originalFS, - appendFile: vi.fn().mockImplementation(async (...args: any[]) => { - if (mockAppendFileThrottle > 0) { - await wait(mockAppendFileThrottle); - } - return originalFS.appendFile(...args); + open: vi.fn().mockImplementation(async (...args: any[]) => { + const fh = await originalFS.open(...args); + return wrapFileHandle(fh); }), - move: vi.fn().mockImplementation(async (...args: any[]) => { + rename: vi.fn().mockImplementation(async (...args: any[]) => { if (mockMoveFileThrottle > 0) { await wait(mockMoveFileThrottle); } - return originalFS.move(...args); + return originalFS.rename(...args); }), + }; +}); + +vi.mock("node:fs", async () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const originalFsSync = await vi.importActual("node:fs"); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const fsSync = require("node:fs"); + return { + __esModule: true, + ...originalFsSync, createReadStream: vi.fn().mockImplementation((path, options) => { // eslint-disable-next-line @typescript-eslint/no-var-requires const { PassThrough } = require("stream"); const { /*fd,*/ encoding } = options; - // const file = fd - // ? originalFS.readFileSync(fd, encoding) - // : originalFS.readFileSync(path, encoding); - const file = originalFS.readFileSync(path, encoding); + const file = fsSync.readFileSync(path, encoding); const ret = new PassThrough(); ret.write(file, encoding, () => { ret.end(); @@ -298,7 +331,7 @@ describe("lib/db", () => { await db.open(); await db.close(); - await expect(fs.pathExists(lockfileDirectory)).resolves.toBe(true); + await expect(pathExists(lockfileDirectory)).resolves.toBe(true); }); it("reads the file if it exists", async () => { @@ -606,14 +639,13 @@ describe("lib/db", () => { await testFS.remove(); }); - it("both versions throw when the DB is not open", async () => { + it("throws when the DB is not open", async () => { await db.close(); - expect(() => db.importJson({})).toThrowError("not open"); - await expect(db.importJson("")).rejects.toThrow(); + expect(() => db.importJSON({})).toThrowError("not open"); }); - it("the object version adds all keys and values to the database", async () => { - db.importJson({ + it("adds all keys and values to the database", async () => { + db.importJSON({ foo: "bar", baz: "inga", "1": 1, @@ -628,21 +660,6 @@ describe("lib/db", () => { {"k":"1","v":1} {"k":"foo","v":"bar"} {"k":"baz","v":"inga"} -`, - ); - }); - - it("the file version asynchronously adds all keys and values to the database", async () => { - await db.importJson(jsonFilenameFull); - // Force the stream to be flushed - await db.close(); - - // The order changes because Object.entries reads the entries in a different order - await expect(fs.readFile(testFilenameFull, "utf8")).resolves.toBe( - `{"k":"key1","v":1} -{"k":"key2","v":"2"} -{"k":"key3","v":1} -{"k":"key4","v":true} `, ); }); @@ -675,24 +692,15 @@ describe("lib/db", () => { it("throws when the DB is not open", async () => { await db.close(); - await expect(db.exportJson(jsonFilenameFull)).rejects.toThrow(); - }); - - it("overwrites the given file with the DB contents as valid JSON", async () => { - await db.exportJson(jsonFilenameFull); - await expect(fs.readFile(jsonFilenameFull, "utf8")).resolves.toBe( - `{"key1":1,"key2":"2"}\n`, - ); + expect(() => db.toJSON()).toThrowError("not open"); }); - it("honors the JSON formatting options", async () => { - await db.exportJson(jsonFilenameFull, { spaces: "\t" }); - await expect(fs.readFile(jsonFilenameFull, "utf8")).resolves.toBe( - `{ - "key1": 1, - "key2": "2" -}\n`, - ); + it("returns the DB contents as valid JSON", async () => { + const actual = db.toJSON(); + expect(actual).toEqual({ + key1: 1, + key2: "2", + }); }); }); @@ -850,12 +858,12 @@ describe("lib/db", () => { await expect(fs.readFile(testFilenameFull, "utf8")).resolves.toBe( '{"k":"key1","v":1}\n{"k":"key3","v":3.5}\n', ); - await expect( - fs.pathExists(testFilenameFull + ".dump"), - ).resolves.toBe(false); - await expect( - fs.pathExists(testFilenameFull + ".bak"), - ).resolves.toBe(false); + await expect(pathExists(testFilenameFull + ".dump")).resolves.toBe( + false, + ); + await expect(pathExists(testFilenameFull + ".bak")).resolves.toBe( + false, + ); }); it("after compresing, writing works as usual", async () => { @@ -1553,13 +1561,13 @@ describe("lib/db", () => { async function assertCleanedUp() { // The other files should have been cleaned up - await expect(fs.pathExists(testFilenameFull)).resolves.toBe(true); - await expect( - fs.pathExists(testFilenameFull + ".bak"), - ).resolves.toBe(false); - await expect( - fs.pathExists(testFilenameFull + ".dump"), - ).resolves.toBe(false); + await expect(pathExists(testFilenameFull)).resolves.toBe(true); + await expect(pathExists(testFilenameFull + ".bak")).resolves.toBe( + false, + ); + await expect(pathExists(testFilenameFull + ".dump")).resolves.toBe( + false, + ); } it("db truncated, .bak ok -> use .bak", async () => { diff --git a/test/perf.ts b/test/perf.ts index e6fb7f43..44c03637 100644 --- a/test/perf.ts +++ b/test/perf.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import * as fs from "node:fs/promises"; import { JsonlDB } from "../src/index.js"; process.on("unhandledRejection", (_r) => { @@ -44,7 +44,7 @@ async function testMedium(enableTimestamps: boolean = false) { ); for (let pass = 1; pass <= NUM_PASSES; pass++) { - await fs.remove("test.jsonl"); + await fs.rm("test.jsonl", { force: true, recursive: true }); await testDB.open(); @@ -63,7 +63,7 @@ async function testMedium(enableTimestamps: boolean = false) { process.stdout.write("."); } - await fs.remove("test.jsonl"); + await fs.rm("test.jsonl", { force: true, recursive: true }); process.stdout.write("\n\n"); @@ -100,7 +100,7 @@ async function testSmall(enableTimestamps: boolean = false) { ); for (let pass = 1; pass <= NUM_PASSES; pass++) { - await fs.remove("test.jsonl"); + await fs.rm("test.jsonl", { force: true, recursive: true }); await testDB.open(); @@ -123,7 +123,7 @@ async function testSmall(enableTimestamps: boolean = false) { process.stdout.write("."); } - await fs.remove("test.jsonl"); + await fs.rm("test.jsonl", { force: true, recursive: true }); process.stdout.write("\n\n"); @@ -177,7 +177,7 @@ async function testDelete(enableTimestamps: boolean = false) { ); for (let pass = 1; pass <= NUM_PASSES; pass++) { - await fs.remove("test.jsonl"); + await fs.rm("test.jsonl", { force: true, recursive: true }); await testDB.open(); @@ -201,7 +201,7 @@ async function testDelete(enableTimestamps: boolean = false) { process.stdout.write("."); } - await fs.remove("test.jsonl"); + await fs.rm("test.jsonl", { force: true, recursive: true }); process.stdout.write("\n\n"); @@ -224,7 +224,7 @@ testSmall(false) .then(() => testDelete(false)) .then(() => testDelete(true)) .catch(console.error) - .finally(() => fs.remove("test.jsonl")) + .finally(() => fs.rm("test.jsonl", { force: true, recursive: true })) .catch(() => { /* ignore */ }); diff --git a/test/testFs.ts b/test/testFs.ts index d4f26be3..882c0e83 100644 --- a/test/testFs.ts +++ b/test/testFs.ts @@ -4,10 +4,10 @@ import { vi } from "vitest"; /** Class to manage an isolated test "filesystem" for unit tests */ export class TestFS { - private _fs: undefined | typeof import("fs-extra"); - private async fs(): Promise { + private _fs: undefined | typeof import("node:fs/promises"); + private async fs(): Promise { if (!this._fs) { - this._fs = await vi.importActual("fs-extra"); + this._fs = await vi.importActual("node:fs/promises"); } return this._fs!; } @@ -35,15 +35,18 @@ export class TestFS { async create(structure: Record = {}): Promise { const root = await this.getRoot(); const fs = await this.fs(); - await fs.emptyDir(root); + await fs.rm(root, { recursive: true, force: true }).catch(() => {}); + await fs.mkdir(root, { recursive: true }); for (const [filename, content] of Object.entries(structure)) { const normalizedFilename = this.normalizePath(root, filename); if (content === null) { // this is a directory - await fs.ensureDir(normalizedFilename); + await fs.mkdir(normalizedFilename, { recursive: true }); } else { // this is a file - await fs.ensureDir(path.dirname(normalizedFilename)); + await fs.mkdir(path.dirname(normalizedFilename), { + recursive: true, + }); await fs.writeFile(normalizedFilename, content, "utf8"); } } @@ -53,6 +56,6 @@ export class TestFS { async remove(): Promise { if (!this.testFsRoot) return; const fs = await this.fs(); - await fs.remove(this.testFsRoot); + await fs.rm(this.testFsRoot, { recursive: true, force: true }); } }