From 2e48b728dd166c55cfca50a53922a3abe31c44c1 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 9 Apr 2026 20:53:13 +0000 Subject: [PATCH] Remove fs-extra and klaw (#882) These packages are obsoleted by the promises API to the built-in Node fs module. The fs-extra package is also causing problems with broken APIs that don't behave the same as Node's fs module, and throwing errors without code properties on them. --- clean.js | 10 +- package.json | 1 - packages/controller/controller.ts | 2 +- packages/controller/package.json | 32 ++-- packages/controller/src/BaseConnection.ts | 6 +- packages/controller/src/Controller.ts | 10 +- packages/controller/src/UserManager.ts | 2 +- packages/controller/src/routes.ts | 2 +- packages/create/create.js | 58 ++++--- packages/create/package.json | 1 - packages/create/template.js | 10 +- packages/ctl/ctl.ts | 2 +- packages/ctl/package.json | 6 +- packages/ctl/src/commands_controller.ts | 2 +- packages/ctl/src/commands_host.ts | 4 +- packages/ctl/src/commands_instance.ts | 4 +- packages/ctl/src/commands_mod.ts | 4 +- packages/ctl/src/commands_user.ts | 8 +- packages/host/host.ts | 8 +- packages/host/package.json | 2 - packages/host/src/Host.ts | 36 +++-- packages/host/src/Instance.ts | 19 ++- packages/host/src/export.ts | 2 +- packages/host/src/patch.ts | 6 +- packages/host/src/server.ts | 25 +-- packages/lib/build_mod.js | 64 +++++--- packages/lib/package.json | 17 +- packages/lib/scripts/compile_validator.js | 6 +- packages/lib/src/LockFile.ts | 4 +- packages/lib/src/ModStore.ts | 2 +- packages/lib/src/config/classes.ts | 4 +- packages/lib/src/data/ModInfo.ts | 2 +- packages/lib/src/database.ts | 2 +- packages/lib/src/datastore.ts | 2 +- packages/lib/src/file_ops.ts | 65 ++++---- packages/lib/src/hash.ts | 6 +- packages/lib/src/load_plugin_list.ts | 13 +- packages/lib/src/logging_utils.ts | 9 +- packages/lib/src/plugin_loader.ts | 2 +- packages/lib/src/stream.ts | 17 +- packages/lib/src/system_collectors.ts | 30 +--- packages/web_ui/webpack.common.js | 6 +- plugins/inventory_sync/controller.ts | 2 +- plugins/inventory_sync/package.json | 10 +- plugins/research_sync/controller.ts | 2 +- plugins/research_sync/package.json | 10 +- plugins/subspace_storage/controller.ts | 2 +- plugins/subspace_storage/package.json | 10 +- pnpm-workspace.yaml | 66 ++++---- publish.js | 4 +- test/controller/HttpCloser.js | 2 +- test/controller/UserManager.test.js | 17 +- test/host/patch.js | 4 +- test/host/server.js | 20 ++- test/integration/clusterio.js | 177 +++++++++++---------- test/integration/create.js | 14 +- test/integration/index.js | 23 +-- test/integration/instance.js | 10 +- test/integration/link.js | 2 +- test/integration/patch.js | 2 +- test/integration/server.js | 21 ++- test/lib/LockFile.test.js | 10 +- test/lib/ModStore.test.js | 25 +-- test/lib/config/classes.js | 31 ++-- test/lib/data/ModInfo.js | 2 +- test/lib/database.js | 2 +- test/lib/datastore.js | 13 +- test/lib/external/FactorioVersions.test.js | 2 +- test/lib/file_ops.js | 52 +++--- test/lib/hash.js | 4 +- test/lib/ini.js | 2 +- test/lib/logging_utils.js | 6 +- test/lib/plugin_loader.js | 33 ++-- test/lib/stream.js | 6 +- 74 files changed, 570 insertions(+), 529 deletions(-) diff --git a/clean.js b/clean.js index 1282c22f..f657c54f 100644 --- a/clean.js +++ b/clean.js @@ -8,7 +8,7 @@ "use strict"; const path = require("path"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const yargs = require("yargs"); const npmPackage = require("./package.json"); @@ -24,13 +24,15 @@ let DRY = false; * @param {string} filePath The path to remove. */ async function tryRemove(filePath) { - if (await fs.exists(filePath)) { - // eslint-disable-next-line no-console + try { + await fs.access(filePath); console.log(filePath); + } catch { + return; } if (!DRY) { - await fs.remove(filePath); + await fs.rm(filePath, { force: true, recursive: true, maxRetries: 10 }); } } diff --git a/package.json b/package.json index 023ee107..f5b45dcc 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "eslint-plugin-node": "^11.1.0", "express": "^5.1.0", "form-data": "^4.0.4", - "fs-extra": "^11.3.0", "globals": "^16.3.0", "jsonwebtoken": "^9.0.2", "jszip": "^3.10.1", diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index f541cbcd..d4db4cec 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -15,7 +15,7 @@ */ import path from "path"; -import fs from "fs-extra"; +import fs from "node:fs/promises"; import crypto from "crypto"; import setBlocking from "set-blocking"; diff --git a/packages/controller/package.json b/packages/controller/package.json index 81872cb8..d318b49e 100644 --- a/packages/controller/package.json +++ b/packages/controller/package.json @@ -22,40 +22,38 @@ "dependencies": { "@clusterio/lib": "workspace:*", "@sinclair/typebox": "catalog:", - "set-blocking": "catalog:", - "jsonwebtoken": "catalog:", - "express": "catalog:", - "fs-extra": "catalog:", - "ws": "catalog:", - "jszip": "catalog:", - "winston": "catalog:", - "winston-daily-rotate-file": "catalog:", "busboy": "^1.6.0", "compression": "^1.8.0", + "express": "catalog:", "finalhandler": "^1.3.1", + "jsonwebtoken": "catalog:", + "jszip": "catalog:", + "set-blocking": "catalog:", + "winston": "catalog:", + "winston-daily-rotate-file": "catalog:", + "ws": "catalog:", "yargs": "^17.7.2" }, "devDependencies": { "@clusterio/web_ui": "workspace:*", - "@types/jsonwebtoken": "catalog:", - "@types/set-blocking": "catalog:", - "@types/finalhandler": "catalog:", + "@swc/core": "catalog:", + "@types/busboy": "^1.5.0", + "@types/compression": "^1.7.2", "@types/express": "catalog:", - "@types/fs-extra": "catalog:", + "@types/finalhandler": "catalog:", + "@types/jsonwebtoken": "catalog:", "@types/node": "catalog:", + "@types/set-blocking": "catalog:", "@types/ws": "catalog:", "@types/yargs": "^17.0.33", - "@types/compression": "^1.7.2", - "@types/busboy": "^1.5.0", - "@swc/core": "catalog:", "react": "catalog:", "react-dom": "catalog:", "react-router-dom": "catalog:", "typescript": "catalog:", "webpack": "catalog:", "webpack-cli": "catalog:", - "webpack-merge": "catalog:", - "webpack-dev-middleware": "^6.1.1" + "webpack-dev-middleware": "^6.1.1", + "webpack-merge": "catalog:" }, "publishConfig": { "access": "public" diff --git a/packages/controller/src/BaseConnection.ts b/packages/controller/src/BaseConnection.ts index 07b753f8..5c1a2177 100644 --- a/packages/controller/src/BaseConnection.ts +++ b/packages/controller/src/BaseConnection.ts @@ -1,5 +1,5 @@ import path from "path"; -import fs from "fs-extra"; +import fs from "node:fs/promises"; import type Controller from "./Controller"; import type WsServerConnector from "./WsServerConnector"; @@ -94,10 +94,12 @@ export default class BaseConnection extends lib.Link { async handleModDownloadRequest(request: lib.ModDownloadRequest) { let mod = this.getMod(request); let modPath = path.join(this._controller.config.get("controller.mods_directory"), mod.filename); + // Open source file before creating the stream in case it throws. + let source = (await fs.open(modPath)).createReadStream(); let stream = await routes.createProxyStream(this._controller.app); stream.filename = mod.filename; - stream.source = fs.createReadStream(modPath); + stream.source = source; stream.mime = "application/zip"; stream.size = String(mod.size); diff --git a/packages/controller/src/Controller.ts b/packages/controller/src/Controller.ts index 122faa93..49a18ebe 100644 --- a/packages/controller/src/Controller.ts +++ b/packages/controller/src/Controller.ts @@ -7,7 +7,7 @@ import finalhandler from "finalhandler"; import compression from "compression"; import events from "events"; -import fs from "fs-extra"; +import fs from "node:fs/promises"; import http from "http"; import https from "https"; import jwt from "jsonwebtoken"; @@ -123,7 +123,7 @@ export default class Controller { static async bootstrap(config: lib.ControllerConfig) { let databaseDirectory = config.get("controller.database_directory"); - await fs.ensureDir(databaseDirectory); + await fs.mkdir(databaseDirectory, { recursive: true }); // Migrate combined user and role file from 2.0.0-alpha.22 await UserManager.attemptMigrateLegacyUsersFile( @@ -173,7 +173,7 @@ export default class Controller { ).bootstrap()); let modsDirectory = config.get("controller.mods_directory"); - await fs.ensureDir(modsDirectory); + await fs.mkdir(modsDirectory, { recursive: true }); const modStore = await lib.ModStore.fromDirectory(modsDirectory); // Add default mod packs @@ -602,7 +602,7 @@ export default class Controller { // First check the clusterio version const runningVersion = this.config.get("controller.version"); const packageJsonPath = require.resolve("@clusterio/controller/package.json"); - const packageJson = await fs.readJSON(packageJsonPath); + const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); if (runningVersion !== packageJson.version) { this.config.restartRequired = true; return true; @@ -619,7 +619,7 @@ export default class Controller { packageName = pluginInfo.npmPackage ?? pluginInfo.name; const pluginPackageJsonPath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json")); - const pluginPackageJson = await fs.readJSON(pluginPackageJsonPath); + const pluginPackageJson = JSON.parse(await fs.readFile(pluginPackageJsonPath, "utf8")); if (pluginInfo.version !== pluginPackageJson.version) { this.config.restartRequired = true; return true; diff --git a/packages/controller/src/UserManager.ts b/packages/controller/src/UserManager.ts index d1304d14..0b4861b8 100644 --- a/packages/controller/src/UserManager.ts +++ b/packages/controller/src/UserManager.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import jwt from "jsonwebtoken"; import * as lib from "@clusterio/lib"; diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index 2ecbb0c8..4622502b 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -4,7 +4,7 @@ import Controller from "./Controller"; import busboy from "busboy"; import crypto from "crypto"; import events from "events"; -import fs from "fs-extra"; +import fs from "node:fs/promises"; import JSZip from "jszip"; import jwt from "jsonwebtoken"; import path from "path"; diff --git a/packages/create/create.js b/packages/create/create.js index 9e1181c2..087cd1cc 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -2,7 +2,7 @@ "use strict"; const child_process = require("child_process"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const inquirer = require("inquirer"); const os = require("os"); const path = require("path"); @@ -97,11 +97,24 @@ class LineSplitter extends stream.Transform { class InstallError extends Error { } +async function pathExists(filePath) { + try { + await fs.access(filePath); + } catch (err) { + if (err.code === "ENOENT" || err.code === "ENOTDIR") { + return false; + } + } + return true; +} async function safeOutputFile(file, data, options={}) { let { dir, name, ext } = path.parse(file); let temporary = path.join(dir, `${name}.tmp${ext}`); - await fs.outputFile(temporary, data, options); + if (dir) { + await fs.mkdir(dir, { recursive: true }); + } + await fs.writeFile(temporary, data, { ...options, flush: true }); await fs.rename(temporary, file); } @@ -221,7 +234,7 @@ async function migrateRename(args) { } async function migrateConfig(source, destination) { - if (await fs.pathExists(source) && !await fs.pathExists(destination)) { + if (await pathExists(source) && !await pathExists(destination)) { await safeOutputFile(destination, JSON.stringify( renameConfig(JSON.parse(await fs.readFile(source))), null, "\t" )); @@ -233,7 +246,7 @@ async function migrateRename(args) { await migrateConfig("config-slave.json", "config-host.json"); let instancesFile = path.join("database", "instances.json"); - if (await fs.pathExists(instancesFile)) { + if (await pathExists(instancesFile)) { await safeOutputFile(instancesFile, JSON.stringify( JSON.parse(await fs.readFile(instancesFile)).map(renameConfig), null, "\t" )); @@ -241,7 +254,7 @@ async function migrateRename(args) { } let usersFile = path.join("database", "users.json"); - if (await fs.pathExists(usersFile)) { + if (await pathExists(usersFile)) { let users = JSON.parse(await fs.readFile(usersFile)); for (let role of users["roles"]) { role["permissions"] = role["permissions"].map(rename); @@ -258,7 +271,7 @@ async function migrateRename(args) { async function migrateLog(inputFile, outputFile) { let lineStream = new LineSplitter({ readableObjectMode: true }); - let fileStream = fs.createReadStream(inputFile); + let fileStream = (await fs.open(inputFile)).createReadStream(); fileStream.pipe(lineStream); let lines = []; @@ -278,7 +291,7 @@ async function migrateRename(args) { } async function migrateLogsDir(inputLogs, outputLogs) { - if (await fs.pathExists(inputLogs)) { + if (await pathExists(inputLogs)) { logger.info(`Migrating ${inputLogs} to ${outputLogs}`); for (let logFile of await fs.readdir(inputLogs)) { if (!logFile.endsWith(".log")) { @@ -286,7 +299,7 @@ async function migrateRename(args) { } let inputFile = path.join(inputLogs, logFile); let outputFile = path.join(outputLogs, rename(logFile)); - if (!await fs.pathExists(outputFile)) { + if (!await pathExists(outputFile)) { await migrateLog(inputFile, outputFile); logger.verbose(`Migrated ${inputFile} to ${outputFile}`); } @@ -297,16 +310,16 @@ async function migrateRename(args) { await migrateLogsDir(path.join("logs", "master"), path.join("logs", "controller")); await migrateLogsDir(path.join("logs", "slave"), path.join("logs", "host")); if ( - await fs.pathExists(path.join("logs", "cluster")) - && !await fs.pathExists(path.join("logs", "cluster-prerename")) + await pathExists(path.join("logs", "cluster")) + && !await pathExists(path.join("logs", "cluster-prerename")) ) { await fs.rename(path.join("logs", "cluster"), path.join("logs", "cluster-prerename")); await migrateLogsDir(path.join("logs", "cluster-prerename"), path.join("logs", "cluster")); } if ( - await fs.pathExists("sharedMods") - && !await fs.pathExists("mods") + await pathExists("sharedMods") + && !await pathExists("mods") ) { logger.info("Moving sharedMods/ to mods/"); await fs.rename("sharedMods", "mods"); @@ -328,8 +341,8 @@ async function migrateRename(args) { await execFile(`npm${scriptExt}`, ["update"]); } - const hasRunMaster = await fs.pathExists("run-master.sh") || await fs.pathExists("run-master.cmd"); - const hasRunSlave = await fs.pathExists("run-slave.sh") || await fs.pathExists("run-slave.cmd"); + const hasRunMaster = await pathExists("run-master.sh") || await pathExists("run-master.cmd"); + const hasRunSlave = await pathExists("run-slave.sh") || await pathExists("run-slave.cmd"); if (hasRunMaster || hasRunSlave) { logger.info("Writing run scripts"); let mode = "standalone"; @@ -354,7 +367,7 @@ async function migrateRename(args) { async function installClusterio(mode, plugins) { try { - await fs.outputFile("package.json", JSON.stringify({ + await fs.writeFile("package.json", JSON.stringify({ name: "clusterio-install", private: true, }, null, 2), { flag: "wx" }); @@ -643,7 +656,7 @@ async function inquirerMissingArgs(args) { let locations = factorioLocations[process.platform] || []; let foundLocations = []; for (let location of locations) { - if (await fs.pathExists(path.join(location, "data", "changelog.txt"))) { + if (await pathExists(path.join(location, "data", "changelog.txt"))) { foundLocations.push(location); } } @@ -687,7 +700,7 @@ async function inquirerMissingArgs(args) { }, ], answers); } else if (answers.factorioDir === "factorio") { - await fs.ensureDir("factorio"); + await fs.mkdir("factorio", { recursive: true }); } } @@ -772,20 +785,21 @@ async function downloadLinuxServer() { const factorioDir = `factorio/${version}/`; const tmpFactorioDir = tmpDir + version; - if (await fs.pathExists(factorioDir)) { + if (await pathExists(factorioDir)) { logger.warn(`setting downloadDir to ${factorioDir}, but not downloading because already existing`); } else { - await fs.emptyDir(tmpDir); + await fs.rm(tmpDir, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(tmpDir, { recursive: true }); logger.info("Downloading latest Factorio server release. This may take a while."); - const writeStream = fs.createWriteStream(tmpArchivePath); + const writeStream = (await fs.open(tmpArchivePath, "w")).createWriteStream(); stream.Readable.fromWeb(res.body).pipe(writeStream); await finished(writeStream); await fs.rename(tmpArchivePath, archivePath); try { - await fs.ensureDir(tmpFactorioDir); + await fs.mkdir(tmpFactorioDir, { recursive: true }); await execFile("tar", [ "xf", archivePath, "-C", tmpFactorioDir, "--strip-components", "1", ]); @@ -794,7 +808,7 @@ async function downloadLinuxServer() { throw e; } - await fs.ensureDir("factorio"); + await fs.mkdir("factorio", { recursive: true }); await fs.rename(tmpFactorioDir, factorioDir); await fs.unlink(archivePath); await fs.rm(tmpDir); diff --git a/packages/create/package.json b/packages/create/package.json index 4ea788f6..9ff05f9b 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -17,7 +17,6 @@ }, "dependencies": { "chalk": "catalog:", - "fs-extra": "catalog:", "inquirer": "^8.2.4", "yargs": "^17.7.2" }, diff --git a/packages/create/template.js b/packages/create/template.js index 43692e56..e8ded303 100644 --- a/packages/create/template.js +++ b/packages/create/template.js @@ -1,6 +1,6 @@ "use strict"; const { logger } = require("./logging"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); const _package = require("./package.json"); @@ -61,9 +61,15 @@ async function copyTemplateFile(src, dst, properties) { return include ? content : ""; }); + // Create folder for output file + const dir = path.parse(dst); + if (dir) { + await fs.mkdir(dir, { recursive: true }); + } + // Attempt to write the output file, warn if it already exists try { - await fs.outputFile(dst, outputData, { flag: "wx" }); + await fs.writeFile(dst, outputData, { flag: "wx" }); } catch (err) { if (err.code === "EEXIST") { logger.warn(`Could not create file ${dst} because it already exists`); diff --git a/packages/ctl/ctl.ts b/packages/ctl/ctl.ts index a65b6c9e..d230cabe 100755 --- a/packages/ctl/ctl.ts +++ b/packages/ctl/ctl.ts @@ -4,7 +4,7 @@ * Command line interface for controlling a Clusterio cluster * @module ctl/ctl */ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import yargs, { type Argv } from "yargs"; import path from "path"; import { version } from "./package.json"; diff --git a/packages/ctl/package.json b/packages/ctl/package.json index 07da9b6b..e9f3c469 100644 --- a/packages/ctl/package.json +++ b/packages/ctl/package.json @@ -21,17 +21,15 @@ }, "dependencies": { "@clusterio/lib": "workspace:*", - "set-blocking": "catalog:", - "fs-extra": "catalog:", - "winston": "catalog:", "as-table": "^1.0.55", + "set-blocking": "catalog:", + "winston": "catalog:", "yargs": "^17.7.2" }, "publishConfig": { "access": "public" }, "devDependencies": { - "@types/fs-extra": "catalog:", "@types/node": "catalog:", "@types/set-blocking": "catalog:", "@types/yargs": "^17.0.33", diff --git a/packages/ctl/src/commands_controller.ts b/packages/ctl/src/commands_controller.ts index bb17cbc8..fbdde7a4 100644 --- a/packages/ctl/src/commands_controller.ts +++ b/packages/ctl/src/commands_controller.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import asTableModule from "as-table"; import events from "events"; import os from "os"; diff --git a/packages/ctl/src/commands_host.ts b/packages/ctl/src/commands_host.ts index 16af4dfe..c44db2fe 100644 --- a/packages/ctl/src/commands_host.ts +++ b/packages/ctl/src/commands_host.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import asTableModule from "as-table"; import events from "events"; import os from "os"; @@ -87,7 +87,7 @@ hostCommands.add(new lib.Command({ } else { logger.info(`Writing ${args.output}`); try { - await fs.outputFile(args.output, content, { flag: "wx" }); + await fs.writeFile(args.output, content, { flag: "wx" }); } catch (err: any) { if (err.code === "EEXIST") { throw new lib.CommandError(`File ${args.output} already exists`); diff --git a/packages/ctl/src/commands_instance.ts b/packages/ctl/src/commands_instance.ts index b938ff97..fd880ba7 100644 --- a/packages/ctl/src/commands_instance.ts +++ b/packages/ctl/src/commands_instance.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import asTableModule from "as-table"; import events from "events"; import path from "path"; @@ -338,7 +338,7 @@ instanceSaveCommands.add(new lib.Command({ const response = await fetch(url, { method: "POST", - body: await fs.createReadStream(args.filepath), + body: (await fs.open(args.filepath)).createReadStream(), duplex: "half", headers: { "X-Access-Token": control.config.get("control.controller_token")!, diff --git a/packages/ctl/src/commands_mod.ts b/packages/ctl/src/commands_mod.ts index 5be260a4..a58a9f42 100644 --- a/packages/ctl/src/commands_mod.ts +++ b/packages/ctl/src/commands_mod.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import asTableModule from "as-table"; import events from "events"; import path from "path"; @@ -151,7 +151,7 @@ modCommands.add(new lib.Command({ let response = await fetch(url, { method: "POST", - body: fs.createReadStream(args.file), + body: (await fs.open(args.file)).createReadStream(), duplex: "half", headers: { "X-Access-Token": control.config.get("control.controller_token")!, diff --git a/packages/ctl/src/commands_user.ts b/packages/ctl/src/commands_user.ts index 656f1f44..37887013 100644 --- a/packages/ctl/src/commands_user.ts +++ b/packages/ctl/src/commands_user.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import asTableModule from "as-table"; import path from "path"; @@ -224,7 +224,7 @@ async function handleImportOrRestore(args: { print(`${restore ? "Restored" : "Imported"} ${importType} from ${args.filepath}`); if (restore && backup && !args.noBackup) { - await fs.writeJSON(`${importType}-backup.json`, backup, { spaces: 2 }); + await fs.writeFile(`${importType}-backup.json`, JSON.stringify(backup, undefined, "\t")); print(`Wrote backup to ${importType}-backup.json`); } } @@ -296,9 +296,9 @@ userCommands.add(new lib.Command({ exportType = "whitelist"; } - await fs.writeJSON(args.filepath, await control.send( + await fs.writeFile(args.filepath, JSON.stringify(await control.send( new lib.UserBulkExportRequest(exportType) - ), { spaces: 2 }); + ), undefined, "\t")); print(`Exported ${exportType} to ${args.filepath}`); }, })); diff --git a/packages/host/host.ts b/packages/host/host.ts index 485f2926..d9341372 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -12,7 +12,7 @@ * @example * npx clusteriohost run */ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import path from "path"; import yargs from "yargs"; import setBlocking from "set-blocking"; @@ -199,9 +199,9 @@ async function startHost() { await hostConfigLock.acquire(); // If we get here the command was run - await fs.ensureDir(hostConfig.get("host.instances_directory")); - await fs.ensureDir(hostConfig.get("host.mods_directory")); - await fs.ensureDir("modules"); + await fs.mkdir(hostConfig.get("host.instances_directory"), { recursive: true }); + await fs.mkdir(hostConfig.get("host.mods_directory"), { recursive: true }); + await fs.mkdir("modules", { recursive: true }); // Set the process title, shows up as the title of the CMD window on windows // and as the process name in ps/top on linux. diff --git a/packages/host/package.json b/packages/host/package.json index bae9248f..96424a1e 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -23,7 +23,6 @@ "dependencies": { "@clusterio/lib": "workspace:*", "@sinclair/typebox": "catalog:", - "fs-extra": "catalog:", "jimp": "^1.6.0", "jszip": "catalog:", "pidusage": "^3.0.2", @@ -38,7 +37,6 @@ "access": "public" }, "devDependencies": { - "@types/fs-extra": "catalog:", "@types/node": "catalog:", "@types/pidusage": "^2.0.2", "@types/semver": "^7.5.0", diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index 08ff05bb..0985edfd 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import path from "path"; import events from "events"; import pidusage from "pidusage"; @@ -294,7 +294,7 @@ export default class Host extends lib.Link { static async bootstrap(hostConfig: lib.HostConfig) { const modsDirectory = hostConfig.get("host.mods_directory"); - await fs.ensureDir(modsDirectory); + await fs.mkdir(modsDirectory, { recursive: true }); return [ await lib.ModStore.fromDirectory(modsDirectory), ] as const; @@ -847,7 +847,7 @@ export default class Host extends lib.Link { // First check the clusterio version const runningVersion = this.config.get("host.version"); const packageJsonPath = require.resolve("@clusterio/host/package.json"); - const packageJson = await fs.readJSON(packageJsonPath); + const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); if (runningVersion !== packageJson.version) { this.config.restartRequired = true; return true; @@ -864,7 +864,7 @@ export default class Host extends lib.Link { packageName = pluginInfo.npmPackage ?? pluginInfo.name; const pluginPackageJsonPath = require.resolve(path.posix.join(pluginInfo.requirePath, "package.json")); - const pluginPackageJson = await fs.readJSON(pluginPackageJsonPath); + const pluginPackageJson = JSON.parse(await fs.readFile(pluginPackageJsonPath, "utf8")); if (pluginInfo.version !== pluginPackageJson.version) { this.config.restartRequired = true; return true; @@ -931,12 +931,17 @@ export default class Host extends lib.Link { checkRequestSaveName(newName); let instance = this.getRequestInstance(instanceId); try { - await fs.move( + // Link save in the new location. + await fs.link( path.join(instance.path, "saves", oldName), path.join(instance.path, "saves", newName), - { overwrite: false }, ); + // Unlink save in old location. + await fs.unlink(path.join(instance.path, "saves", oldName)); } catch (err: any) { + if (err.code === "EEXIST") { + throw new lib.RequestError(`${newName} already exists`); + } if (err.code === "ENOENT") { throw new lib.RequestError(`${oldName} does not exist`); } @@ -951,10 +956,10 @@ export default class Host extends lib.Link { checkRequestSaveName(destination); let instance = this.getRequestInstance(instanceId); try { - await fs.copy( + await fs.copyFile( path.join(instance.path, "saves", source), path.join(instance.path, "saves", destination), - { overwrite: false, errorOnExist: true }, + fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE ); } catch (err: any) { if (err.code === "ENOENT") { @@ -980,16 +985,15 @@ export default class Host extends lib.Link { try { if (copy) { - await fs.copy( + await fs.copyFile( path.join(sourceInstance.path, "saves", sourceName), path.join(targetInstance.path, "saves", targetName), - { overwrite: true }, + fs.constants.COPYFILE_FICLONE ); } else { - await fs.move( + await fs.rename( path.join(sourceInstance.path, "saves", sourceName), path.join(targetInstance.path, "saves", targetName), - { overwrite: true }, ); } } catch (err: any) { @@ -1051,11 +1055,9 @@ export default class Host extends lib.Link { checkRequestSaveName(name); let instance = this.getRequestInstance(instanceId); - let content = fs.createReadStream(path.join(instance.path, "saves", name)); + let content; try { - // TODO: Due to a bug in fs-extra the ready event is not emitted here. Replace - // "open" with "ready" when fs-extra is replaced with node's fs module. - await events.once(content, "open"); + content = (await fs.open(path.join(instance.path, "saves", name))).createReadStream(); } catch (err: any) { if (err.code === "ENOENT") { throw new lib.RequestError(`${name} does not exist`); @@ -1087,7 +1089,7 @@ export default class Host extends lib.Link { this.discoveredInstances.delete(instanceId); this.assignedInstances.delete(instanceId); - await fs.remove(instance.path); + await fs.rm(instance.path, { recursive: true, maxRetries: 10 }); } async handleHostUpdateRequest(request: lib.HostUpdateRequest) { diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index fa228ee8..073abad1 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import path from "path"; import pidusage from "pidusage"; import util from "util"; @@ -755,8 +755,8 @@ end`.replace(/\r?\n/g, " "); * @param instanceDir - */ static async populate_folders(instanceDir: string) { - await fs.ensureDir(path.join(instanceDir, "script-output")); - await fs.ensureDir(path.join(instanceDir, "saves")); + await fs.mkdir(path.join(instanceDir, "script-output"), { recursive: true }); + await fs.mkdir(path.join(instanceDir, "saves"), { recursive: true }); } /** @@ -786,7 +786,7 @@ end`.replace(/\r?\n/g, " "); const mods = await this._host.fetchMods(modPack.mods.values()); - await fs.ensureDir(this.path("mods")); + await fs.mkdir(this.path("mods"), { recursive: true }); // Remove all files for (let entry of await fs.readdir(this.path("mods"), { withFileTypes: true })) { @@ -825,12 +825,12 @@ end`.replace(/\r?\n/g, " "); } // Write mod-list.json - await fs.outputFile(this.path("mods", "mod-list.json"), JSON.stringify({ + await fs.writeFile(this.path("mods", "mod-list.json"), JSON.stringify({ mods: [...this.activeModPack.mods.values()], }, null, "\t")); // Write mod-settings.dat - await fs.outputFile(this.path("mods", "mod-settings.dat"), this.activeModPack.toModSettingsDat()); + await fs.writeFile(this.path("mods", "mod-settings.dat"), this.activeModPack.toModSettingsDat()); } /** @@ -921,7 +921,12 @@ end`.replace(/\r?\n/g, " "); now.getUTCMinutes().toLocaleString("en", { minimumIntegerDigits: 2 }), saveName, ); - await fs.copy(this.path("saves", saveName), this.path("saves", newName)); + // This will overwrite the save if this runs twice in the same minute. + await fs.copyFile( + this.path("saves", saveName), + this.path("saves", newName), + fs.constants.COPYFILE_FICLONE, + ); saveName = newName; } diff --git a/packages/host/src/export.ts b/packages/host/src/export.ts index 0a401b42..198ffb0a 100644 --- a/packages/host/src/export.ts +++ b/packages/host/src/export.ts @@ -1,5 +1,5 @@ // Export of item icons and locale -import fs from "fs-extra"; +import fs from "node:fs/promises"; import path from "path"; import { Jimp, JimpMime, type JimpInstance } from "jimp"; import JSZip from "jszip"; diff --git a/packages/host/src/patch.ts b/packages/host/src/patch.ts index b4bb1dd0..639ce65f 100644 --- a/packages/host/src/patch.ts +++ b/packages/host/src/patch.ts @@ -1,7 +1,7 @@ // Library for patching Factorio saves with scenario code. import events from "events"; -import fs from "fs-extra"; +import fs from "node:fs/promises"; import JSZip from "jszip"; import path from "path"; import semver from "semver"; @@ -93,7 +93,9 @@ export class SaveModule { static async fromPlugin(plugin: BaseInstancePlugin) { let pluginPackagePath = require.resolve(path.posix.join(plugin.info.requirePath, "package.json")); let moduleDirectory = path.join(path.dirname(pluginPackagePath), "module"); - if (!await fs.pathExists(moduleDirectory)) { + try { + await fs.access(moduleDirectory); + } catch (err: any) { return null; } diff --git a/packages/host/src/server.ts b/packages/host/src/server.ts index 227d2319..c4ecfa05 100644 --- a/packages/host/src/server.ts +++ b/packages/host/src/server.ts @@ -1,5 +1,6 @@ // Factorio Server interface -import fs from "fs-extra"; +import fs from "node:fs/promises"; +import { type FSWatcher, watch as fsWatch, writeFileSync } from "node:fs"; import child_process from "child_process"; import path from "path"; import JSZip from "jszip"; @@ -116,7 +117,7 @@ async function downloadAndExtractZip(url: string, targetDir: string) { const zip = await JSZip.loadAsync(buffer); // Extract each file/folder - await fs.ensureDir(targetDir); + await fs.mkdir(targetDir, { recursive: true }); await Promise.all( Object.values(zip.files).map(async (entry) => { const parts = entry.name.split("/").filter(Boolean); @@ -127,9 +128,9 @@ async function downloadAndExtractZip(url: string, targetDir: string) { const entryPath = path.join(targetDir, strippedPath); if (entry.dir) { - await fs.ensureDir(entryPath); + await fs.mkdir(entryPath, { recursive: true }); } else { - await fs.ensureDir(path.dirname(entryPath)); + await fs.mkdir(path.dirname(entryPath), { recursive: true }); await fs.writeFile(entryPath, await entry.async("nodebuffer")); } }) @@ -153,7 +154,7 @@ async function downloadAndExtractTar(url: string, targetDir: string) { const tmpFilePath = `${targetDir}.tmp`; const buffer = Buffer.from(await res.arrayBuffer()); await fs.writeFile(tmpFilePath, buffer); - await fs.ensureDir(targetDir); + await fs.mkdir(targetDir, { recursive: true }); // Execute tar CLI to extract try { @@ -165,7 +166,7 @@ async function downloadAndExtractTar(url: string, targetDir: string) { throw err; } finally { // Clean up temp file - await fs.remove(tmpFilePath); + await fs.rm(tmpFilePath); } } @@ -631,7 +632,7 @@ export class FactorioServer extends events.EventEmitter { // Due to inconsistencies in the factorio api, we must manually watch the whitelist // https://forums.factorio.com/viewtopic.php?t=123673 - _whitelistWatcher: fs.FSWatcher | null = null; + _whitelistWatcher: FSWatcher | null = null; _whitelist = new Set(); _logger: lib.Logger; @@ -885,7 +886,7 @@ export class FactorioServer extends events.EventEmitter { const filePath = this.writePath("server-whitelist.json"); try { - this._whitelistWatcher = fs.watch(filePath); + this._whitelistWatcher = fsWatch(filePath); } catch (err: any) { this._logger.warn(`Unable to watch whitelist, bidirectional sync will not be available:\n${err}`); return; @@ -907,7 +908,7 @@ export class FactorioServer extends events.EventEmitter { let newWhitelistJson; try { - newWhitelistJson = await fs.readJSON(filePath); + newWhitelistJson = JSON.parse(await fs.readFile(filePath, "utf8")); } catch (err: any) { this._logger.error(`Unable to read whitelist, bidirectional sync will not be available:\n${err}`); return; @@ -1527,20 +1528,20 @@ export class FactorioServer extends events.EventEmitter { }); // Must be sync to allow process spawn without awaiting // eslint-disable-next-line node/no-sync - fs.writeFileSync(this.writePath("config.ini"), content); + writeFileSync(this.writePath("config.ini"), content); } _writeMapSettingsSync(mapGenSettings?: object, mapSettings?: object) { // Must be sync to allow process spawn without awaiting if (mapGenSettings) { // eslint-disable-next-line node/no-sync - fs.writeFileSync( + writeFileSync( this.writePath("map-gen-settings.json"), JSON.stringify(mapGenSettings, null, "\t") ); } if (mapSettings) { // eslint-disable-next-line node/no-sync - fs.writeFileSync( + writeFileSync( this.writePath("map-settings.json"), JSON.stringify(mapSettings, null, "\t") ); } diff --git a/packages/lib/build_mod.js b/packages/lib/build_mod.js index 30cc4326..1406eeda 100644 --- a/packages/lib/build_mod.js +++ b/packages/lib/build_mod.js @@ -1,9 +1,7 @@ /* eslint-disable no-console */ "use strict"; -const events = require("events"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const JSZip = require("jszip"); -const klaw = require("klaw"); const path = require("path"); const stream = require("stream"); const util = require("util"); @@ -14,30 +12,36 @@ const finished = util.promisify(stream.finished); async function buildMod(args, info, modName) { if (args.build) { - await fs.ensureDir(args.outputDir); + await fs.mkdir(args.outputDir, { recursive: true }); modName = args.modName ?? `${info.name}_${info.version}`; if (args.pack) { let zip = new JSZip(); - let walker = klaw(args.sourceDir) - .on("data", item => { - if (item.stats.isFile()) { - // On Windows the path created uses backslashes as the directory sepparator - // but the zip file needs to use forward slashes. We can't use the posix - // version of relative here as it doesn't work with Windows style paths. - let basePath = path.relative(args.sourceDir, item.path).replace(/\\/g, "/"); - zip.file(path.posix.join(modName, basePath), fs.createReadStream(item.path)); + for await (const dirent of await fs.opendir(args.sourceDir, { recursive: true })) { + if (dirent.isFile()) { + const itemPath = path.join(dirent.parentPath, dirent.name); + // On Windows the path created uses backslashes as the directory sepparator + // but the zip file needs to use forward slashes. We can't use the posix + // version of relative here as it doesn't work with Windows style paths. + let basePath = path.relative(args.sourceDir, itemPath).replace(/\\/g, "/"); + // eslint-disable-next-line max-depth + if (basePath === "info.json") { + // The info.json file is overridden later. + continue; } - }) - ; - await events.once(walker, "end"); + zip.file(path.posix.join(modName, basePath), (await fs.open(itemPath)).createReadStream()); + } + } for (let [fileName, pathParts] of Object.entries(info.additional_files || {})) { let filePath = path.join(args.sourceDir, ...pathParts); - if (!await fs.pathExists(filePath)) { - throw new Error(`Additional file ${filePath} does not exist`); + let fileStream; + try { + fileStream = (await fs.open(filePath)).createReadStream(); + } catch (err) { + throw new Error(`Error reading additional file ${filePath}`, { cause: err }); } - zip.file(path.posix.join(modName, fileName), fs.createReadStream(filePath)); + zip.file(path.posix.join(modName, fileName), fileStream); } delete info.additional_files; @@ -45,20 +49,28 @@ async function buildMod(args, info, modName) { let modPath = path.join(args.outputDir, `${modName}.zip`); console.log(`Writing ${modPath}`); - let writeStream = zip.generateNodeStream().pipe(fs.createWriteStream(modPath)); + let writeStream = zip.generateNodeStream().pipe( + (await fs.open(modPath, "w")).createWriteStream() + ); await finished(writeStream); } else { let modDir = path.join(args.outputDir, modName); - if (await fs.exists(modDir)) { - console.log(`Removing existing build ${modDir}`); - await fs.remove(modDir); - } + console.log(`Removing potentially existing build ${modDir}`); + await fs.rm(modDir, { force: true, recursive: true, maxRetries: 10 }); console.log(`Building ${modDir}`); - await fs.copy(args.sourceDir, modDir); + await fs.cp(args.sourceDir, modDir, { + errorOnExists: true, + mode: fs.constants.COPYFILE_FICLONE, + recursive: true, + }); for (let [fileName, pathParts] of Object.entries(info.additional_files) || []) { let filePath = path.join(...pathParts); - await fs.copy(filePath, path.join(modDir, fileName)); + await fs.copyFile( + filePath, + path.join(modDir, fileName), + fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE, + ); } delete info.additional_files; @@ -87,7 +99,7 @@ async function build(args) { if (name === info.name) { let modPath = path.join(args.outputDir, entry); console.log(`Removing ${modPath}`); - await fs.remove(modPath); + await fs.rm(modPath, { force: true, recursive: true, maxRetries: 10 }); } } } diff --git a/packages/lib/package.json b/packages/lib/package.json index 27c475b7..29da9318 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -19,28 +19,25 @@ }, "dependencies": { "@sinclair/typebox": "catalog:", - "set-blocking": "catalog:", - "fs-extra": "catalog:", - "jszip": "catalog:", + "ajv": "^8.17.1", "chalk": "catalog:", - "ws": "catalog:", + "fast-deep-equal": "^3.1.3", + "jszip": "catalog:", + "set-blocking": "catalog:", + "triple-beam": "^1.4.1", "winston": "catalog:", "winston-transport": "catalog:", - "fast-deep-equal": "^3.1.3", - "triple-beam": "^1.4.1", - "ajv": "^8.17.1", - "klaw": "^4.1.0", + "ws": "catalog:", "yargs": "^17.7.2" }, "publishConfig": { "access": "public" }, "devDependencies": { - "@types/fs-extra": "catalog:", "@types/node": "catalog:", - "@types/ws": "catalog:", "@types/set-blocking": "catalog:", "@types/triple-beam": "^1.3.2", + "@types/ws": "catalog:", "@types/yargs": "^17.0.33", "typescript": "catalog:" } diff --git a/packages/lib/scripts/compile_validator.js b/packages/lib/scripts/compile_validator.js index 9dd5fabd..47bcee1e 100644 --- a/packages/lib/scripts/compile_validator.js +++ b/packages/lib/scripts/compile_validator.js @@ -1,5 +1,5 @@ "use strict"; -const fs = require("fs-extra"); +const fs = require("node:fs"); const path = require("path"); const Ajv = require("ajv"); const standaloneCode = require("ajv/dist/standalone").default; @@ -7,7 +7,7 @@ const standaloneCode = require("ajv/dist/standalone").default; /* eslint-disable node/no-sync */ // Provide an empty file for message_core to load -fs.outputFileSync(path.join(__dirname, "../dist/node/src/data/message_validate.js"), ""); +fs.writeFileSync(path.join(__dirname, "../dist/node/src/data/message_validate.js"), ""); const { Message } = require("../dist/node/src/data/messages_core"); @@ -23,7 +23,7 @@ const ajv = new Ajv({ const validate = ajv.compile(Message.jsonSchema); let moduleCode = standaloneCode(ajv, validate); function write(output) { - fs.outputFileSync( + fs.writeFileSync( path.join(__dirname, output), `// generated by scripts/compile_validator.js\n// @ts-nocheck\n/* eslint-disable */\n${moduleCode}` ); diff --git a/packages/lib/src/LockFile.ts b/packages/lib/src/LockFile.ts index 5cb6ecce..afed36e1 100644 --- a/packages/lib/src/LockFile.ts +++ b/packages/lib/src/LockFile.ts @@ -1,5 +1,5 @@ -import { FileHandle, open, unlink, readFile } from "fs/promises"; -import { unlinkSync } from "fs"; +import { FileHandle, open, unlink, readFile } from "node:fs/promises"; +import { unlinkSync } from "node:fs"; import assert from "assert"; /** diff --git a/packages/lib/src/ModStore.ts b/packages/lib/src/ModStore.ts index ac36df4e..13e82716 100644 --- a/packages/lib/src/ModStore.ts +++ b/packages/lib/src/ModStore.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import path from "path"; import events from "events"; import { Static } from "@sinclair/typebox"; diff --git a/packages/lib/src/config/classes.ts b/packages/lib/src/config/classes.ts index b243cda2..767f6231 100644 --- a/packages/lib/src/config/classes.ts +++ b/packages/lib/src/config/classes.ts @@ -1,6 +1,6 @@ // Configuration classes import { Type, Static } from "@sinclair/typebox"; -import * as fs from "fs-extra"; +import * as fs from "node:fs/promises"; import events from "events"; import isDeepStrictEqual from "../is_deep_strict_equal"; @@ -446,7 +446,7 @@ export class Config< * @returns Instance of this config */ static async fromFile(location: ConfigLocation, filepath: string) { - const fields = await fs.readJson(filepath, { encoding: "utf8" }); + const fields = JSON.parse(await fs.readFile(filepath, { encoding: "utf8" })); return this.fromJSON(fields, location, filepath); } diff --git a/packages/lib/src/data/ModInfo.ts b/packages/lib/src/data/ModInfo.ts index 96ea90b0..83fdf384 100644 --- a/packages/lib/src/data/ModInfo.ts +++ b/packages/lib/src/data/ModInfo.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import JSZip from "jszip"; import { Type, Static } from "@sinclair/typebox"; diff --git a/packages/lib/src/database.ts b/packages/lib/src/database.ts index 1f903d4c..81ea71ac 100644 --- a/packages/lib/src/database.ts +++ b/packages/lib/src/database.ts @@ -4,7 +4,7 @@ * @module lib/database * @author Hornwitser */ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import * as libFileOps from "./file_ops"; import { basicType } from "./helpers"; diff --git a/packages/lib/src/datastore.ts b/packages/lib/src/datastore.ts index 48b9d15b..ed5113d9 100644 --- a/packages/lib/src/datastore.ts +++ b/packages/lib/src/datastore.ts @@ -1,5 +1,5 @@ import path from "path"; -import fs from "fs-extra"; +import fs from "node:fs/promises"; import { EventEmitter } from "events"; import { safeOutputFile } from "./file_ops"; diff --git a/packages/lib/src/file_ops.ts b/packages/lib/src/file_ops.ts index 228abc1a..d1dbf11f 100644 --- a/packages/lib/src/file_ops.ts +++ b/packages/lib/src/file_ops.ts @@ -3,7 +3,7 @@ */ import crypto from "crypto"; import events from "events"; -import fs from "fs-extra"; +import fs from "node:fs/promises"; import path from "path"; import stream from "stream"; @@ -46,7 +46,7 @@ export async function getNewestFile(directory: string, filter = (_name: string) * @returns The size in bytes of all files in the directory. */ export async function directorySize(directory: string) { - let dirEntries: fs.Dirent[]; + let dirEntries; try { dirEntries = await fs.readdir(directory, { withFileTypes: true }); } catch (err: any) { @@ -91,8 +91,12 @@ export async function findUnusedName(directory: string, name: string, extension } while (true) { - if (!await fs.pathExists(path.join(directory, `${name}${extension}`))) { - return `${name}${extension}`; + try { + await fs.access(path.join(directory, `${name}${extension}`)); + } catch (err: any) { + if (err.code === "ENOENT") { + return `${name}${extension}`; + } } let match = /^(.*?)(-(\d+))?$/.exec(name)!; @@ -125,12 +129,13 @@ export async function getTempFile(prefix = "tmp.", suffix = "", tmpdir = "./") { } /** - * Safely write data to a file + * Safely overwrite file data * - * Same as fs-extra.outputFile except the data is written to a temporary - * file that's renamed over the target file. The name of the temporary file - * is the same as the target file with the suffix `.tmp` added before the - * extension. + * Writes the passed data to a temporary file and rename it over the target + * file. If the operation fails because the parent directory does not exist, it + * attempts to create the directory, including any ancestors and then retries + * the operation. The name of the temporary file is the same as the target file + * with the suffix `.tmp` added before the extension. * * If the operation fails it may leave behind the temporary file. This * should not be too much of an issue as the next time the same file is @@ -138,26 +143,32 @@ export async function getTempFile(prefix = "tmp.", suffix = "", tmpdir = "./") { * * @param file - Path to file to write. * @param data - Content to write. - * @param options - see fs.writeFile, `flag` must not be set. + * @param options - see fs.writeFile, `flag` and `flush` may not be used. */ -export async function safeOutputFile(file: string, data: string | Buffer, options: fs.WriteFileOptions ={}) { +export async function safeOutputFile( + file: string, + data: string | Buffer, + options?: Omit< + Extract[2], { flush?: boolean }>, + "flag" | "flush" + >, +) { let { dir, name, ext } = path.parse(file); - if (dir && !await fs.pathExists(dir)) { - await fs.mkdirs(dir); - } let temporary = path.join(dir, `${name}.tmp${ext}`); - let fd = await fs.open(temporary, "w"); try { - await fs.writeFile(fd, data, options); - await fs.fsync(fd); - } finally { - await fs.close(fd); + await fs.writeFile(temporary, data, { ...options, flush: true }); + } catch (err: any) { + if (err.code === "ENOENT") { + // Try creating the folder and then retry the operation. + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(temporary, data, { ...options, flush: true }); + } else { + throw err; + } } - await fs.rename(temporary, file); } - // Reserved names by almost all filesystems const badNames = [".", ".."]; @@ -246,16 +257,14 @@ export function cleanFilename(name: string) { * @param filePath - The target file to create a temporary stream for. * @returns The path to the temporary file that was opened and the write stream to it. */ -export async function createTempWriteStream(filePath: string): Promise<[string, fs.WriteStream]> { +export async function createTempWriteStream(filePath: string) { const { dir, name: fileName, ext } = path.parse(filePath); let increment = 1; let writeStream; let tempFilePath = path.format({ dir, name: `${fileName}.tmp`, ext }); while (true) { try { - writeStream = fs.createWriteStream(tempFilePath, { flags: "wx", flush: true }); - // replace "open" event with "ready" after removing fs-extra - await events.once(writeStream, "open"); + writeStream = (await fs.open(tempFilePath, "wx")).createWriteStream({ flush: true }); break; } catch (err: any) { if (err.code === "EEXIST") { @@ -267,7 +276,7 @@ export async function createTempWriteStream(filePath: string): Promise<[string, } } - return [tempFilePath, writeStream]; + return [tempFilePath, writeStream] as const; } /** @@ -291,9 +300,7 @@ export async function downloadFile( let tempFilePath; if (overwriteMode === "error") { tempFilePath = filePath; - writeStream = fs.createWriteStream(tempFilePath, { flags: "wx", flush: true }); - // replace "open" event with "ready" after removing fs-extra - await events.once(writeStream, "open"); + writeStream = (await fs.open(tempFilePath, "wx")).createWriteStream({ flush: true }); } else { [tempFilePath, writeStream] = await createTempWriteStream(filePath); } diff --git a/packages/lib/src/hash.ts b/packages/lib/src/hash.ts index a2542b9b..d6fe18e6 100644 --- a/packages/lib/src/hash.ts +++ b/packages/lib/src/hash.ts @@ -3,7 +3,7 @@ * @module lib/hash */ import crypto from "crypto"; -import fs from "fs-extra"; +import fs from "node:fs/promises"; /** @@ -35,6 +35,6 @@ export function hashStream(stream: NodeJS.ReadableStream): Promise { * @param path - Path to the file to hash. * @returns hash of the file given. */ -export function hashFile(path: string) { - return hashStream(fs.createReadStream(path)); +export async function hashFile(path: string) { + return hashStream((await fs.open(path)).createReadStream()); } diff --git a/packages/lib/src/load_plugin_list.ts b/packages/lib/src/load_plugin_list.ts index 22d3fbaa..6cf09447 100644 --- a/packages/lib/src/load_plugin_list.ts +++ b/packages/lib/src/load_plugin_list.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import path from "path"; import { logger } from "./logging"; import * as libFileOps from "./file_ops"; @@ -174,12 +174,15 @@ export async function loadPluginList(pluginListPath: string, findLocal = true): */ async function checkPackageJson(pluginPath: string): Promise { const packageJsonPath = path.join(pluginPath, "package.json"); - if (!await fs.exists(packageJsonPath)) { - return false; - } // Read and parse package.json - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, { encoding: "utf8" })); + let content; + try { + content = await fs.readFile(packageJsonPath, { encoding: "utf8" }); + } catch (err: any) { + return false; + } + const packageJson = JSON.parse(content); // Check if package has the clusterio-plugin keyword return packageJson.keywords?.includes("clusterio-plugin"); diff --git a/packages/lib/src/logging_utils.ts b/packages/lib/src/logging_utils.ts index 47f4180e..eebb1c2e 100644 --- a/packages/lib/src/logging_utils.ts +++ b/packages/lib/src/logging_utils.ts @@ -4,7 +4,8 @@ * @author Hornwitser * @module lib/logging_utils */ -import fs from "fs-extra"; +import fs from "node:fs/promises"; +import type { ReadStream } from "node:fs"; import path from "path"; import stream from "stream"; import util from "util"; @@ -320,7 +321,7 @@ export class LogIndex { async indexFile(file: string) { let filePath = path.join(this.logDirectory, file); let lineStream = new libStream.LineSplitter({ readableObjectMode: true }); - let fileStream = fs.createReadStream(filePath); + let fileStream = (await fs.open(filePath)).createReadStream(); fileStream.pipe(lineStream); let entry = { levels: new Set() as Set, @@ -478,11 +479,11 @@ export async function queryLog(logDirectory: string, filter: QueryLogFilter, ind continue; } let filePath = path.join(logDirectory, file); - let fileStream: fs.ReadStream; + let fileStream: ReadStream; let lineStream: libStream.LineSplitter; if (filter.order === "asc") { lineStream = new libStream.LineSplitter({ readableObjectMode: true }); - fileStream = fs.createReadStream(filePath); + fileStream = (await fs.open(filePath)).createReadStream(); } else { lineStream = new libStream.ReverseLineSplitter({ readableObjectMode: true }); fileStream = await libStream.createReverseReadStream(filePath); diff --git a/packages/lib/src/plugin_loader.ts b/packages/lib/src/plugin_loader.ts index 37b48ca0..b56e0a95 100644 --- a/packages/lib/src/plugin_loader.ts +++ b/packages/lib/src/plugin_loader.ts @@ -4,7 +4,7 @@ * @module lib/plugin_loader */ import path from "path"; -import fs from "fs/promises"; +import fs from "node:fs/promises"; import * as libErrors from "./errors"; import * as libPlugin from "./plugin"; import { logger } from "./logging"; diff --git a/packages/lib/src/stream.ts b/packages/lib/src/stream.ts index 351cb89a..b9dc78da 100644 --- a/packages/lib/src/stream.ts +++ b/packages/lib/src/stream.ts @@ -1,6 +1,10 @@ import assert from "assert"; -import fs from "fs-extra"; +import fs from "node:fs"; // Need the non-promise api for reverse stream import stream from "stream"; +import util from "node:util"; + +const fsOpen = util.promisify(fs.open); +const fsFstat = util.promisify(fs.fstat); /** @@ -13,7 +17,7 @@ import stream from "stream"; * ```ts * let lineStream = new lib.LineSplitter({ readableObjectMode: true }); * lineStream.on("data", line => { console.log(line.toString("utf8")); }); - * let fileStream = fs.createReadStream(path); + * let fileStream = (await fs.open(path)).createReadStream(); * fileStream.pipe(lineStream); * ``` */ @@ -141,8 +145,8 @@ export async function createReverseReadStream(path: string, options?: ReadStream if (typeof options === "string") { options = { encoding: options }; } - const fileFd = await fs.open(path, "r"); - const fileSize = (await fs.fstat(fileFd)).size; + const fileFd = await fsOpen(path, "r"); + const fileSize = (await fsFstat(fileFd)).size; let filePosition = fileSize; const reverseFs = { read( @@ -162,8 +166,5 @@ export async function createReverseReadStream(path: string, options?: ReadStream open() { assert(false); }, close(fd: number, callback: () => void) { return fs.close(fd, callback); }, }; - return fs.createReadStream( - "", - { ...options, fd: fileFd, fs: reverseFs } as ReadStreamOptions, // type specification is missing fs prop. - ); + return fs.createReadStream("", { ...options, fd: fileFd, fs: reverseFs }); } diff --git a/packages/lib/src/system_collectors.ts b/packages/lib/src/system_collectors.ts index 436c57de..7369ecc4 100644 --- a/packages/lib/src/system_collectors.ts +++ b/packages/lib/src/system_collectors.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import os from "os"; import util from "util"; import { Gauge } from "./prometheus"; @@ -92,14 +92,7 @@ export const systemDiskCapacityBytes = new Gauge( "Size of the filesystem of the current working directory Node.js runs on", { callback: async function(collector) { - // statfs was added in Node.js v18.15.0 and may not be present. - // TODO: remove this check once minimum supported Node.js >= v18.15.0. - if (!fs.statfs) { - collector.set(0); - return; - } - const statFsAsync = util.promisify(fs.statfs); - const stats = await statFsAsync("."); + const stats = await fs.statfs("."); collector.set(stats.blocks * stats.bsize); }, }, @@ -110,13 +103,7 @@ export const systemDiskAvailableBytes = new Gauge( "Available space on the filesystem of the current working directory Node.js runs on", { callback: async function(collector) { - // TODO: remove this check once minimum supported Node.js >= v18.15.0. - if (!fs.statfs) { - collector.set(0); - return; - } - const statFsAsync = util.promisify(fs.statfs); - const stats = await statFsAsync("."); + const stats = await fs.statfs("."); collector.set(stats.bavail * stats.bsize); }, }, @@ -148,14 +135,9 @@ export async function gatherSystemInfo( const cpuUsage = minZip(deltaTotalCpuMs, deltaIdleCpuMs).map(([total, idle]) => (total - idle) / total); previousTotalCpuMs = currentTotalCpuMs; previousIdleCpuMs = currentIdleCpuMs; - let diskCapacity = 0; - let diskAvailable = 0; - if (fs.statfs) { // TODO: remove this check once minimum supported Node.js >= v18.15.0. - const statFsAsync = util.promisify(fs.statfs); - const stats = await statFsAsync("."); - diskCapacity = stats.blocks * stats.bsize; - diskAvailable = stats.bavail * stats.bsize; - } + const stats = await fs.statfs("."); + let diskCapacity = stats.blocks * stats.bsize; + let diskAvailable = stats.bavail * stats.bsize; const now = Date.now(); return new SystemInfo( diff --git a/packages/web_ui/webpack.common.js b/packages/web_ui/webpack.common.js index a74dc319..ed6b2328 100644 --- a/packages/web_ui/webpack.common.js +++ b/packages/web_ui/webpack.common.js @@ -45,10 +45,6 @@ module.exports = (env = {}, argv = {}) => ({ ], module: { rules: [ - { - test: /node_modules.fs-extra/, - use: require.resolve("null-loader"), - }, { test: /node_modules.winston.dist.winston.transports.(http|file)/, use: require.resolve("null-loader"), @@ -124,6 +120,8 @@ module.exports = (env = {}, argv = {}) => ({ extensions: [".js", ".ts", ".json", ".jsx", ".tsx"], fallback: { "crypto": false, + "fs": false, + "fs/promises": false, // Required for winston "util": require.resolve("util/"), diff --git a/plugins/inventory_sync/controller.ts b/plugins/inventory_sync/controller.ts index 203e5b48..19df1451 100644 --- a/plugins/inventory_sync/controller.ts +++ b/plugins/inventory_sync/controller.ts @@ -1,7 +1,7 @@ import { BaseControllerPlugin, type InstanceRecord } from "@clusterio/controller"; import type { IpcPlayerData } from "./messages"; -import fs from "fs-extra"; +import fs from "node:fs/promises"; import path from "path"; import * as lib from "@clusterio/lib"; import * as msg from "./messages"; diff --git a/plugins/inventory_sync/package.json b/plugins/inventory_sync/package.json index d8a165ad..071d5c45 100644 --- a/plugins/inventory_sync/package.json +++ b/plugins/inventory_sync/package.json @@ -24,20 +24,18 @@ }, "peerDependencies": { "@clusterio/controller": "workspace:*", - "@clusterio/web_ui": "workspace:*", "@clusterio/host": "workspace:*", - "@clusterio/lib": "workspace:*" + "@clusterio/lib": "workspace:*", + "@clusterio/web_ui": "workspace:*" }, "dependencies": { - "@sinclair/typebox": "catalog:", - "fs-extra": "catalog:" + "@sinclair/typebox": "catalog:" }, "devDependencies": { "@clusterio/controller": "workspace:*", - "@clusterio/web_ui": "workspace:*", "@clusterio/host": "workspace:*", "@clusterio/lib": "workspace:*", - "@types/fs-extra": "catalog:", + "@clusterio/web_ui": "workspace:*", "@types/node": "catalog:", "@types/react": "catalog:", "antd": "catalog:", diff --git a/plugins/research_sync/controller.ts b/plugins/research_sync/controller.ts index f1f3f12d..498e2a45 100644 --- a/plugins/research_sync/controller.ts +++ b/plugins/research_sync/controller.ts @@ -1,4 +1,4 @@ -import fs from "fs-extra"; +import fs from "node:fs/promises"; import path from "path"; import { BaseControllerPlugin } from "@clusterio/controller"; import { Static } from "@sinclair/typebox"; diff --git a/plugins/research_sync/package.json b/plugins/research_sync/package.json index 426c17eb..db9443ea 100644 --- a/plugins/research_sync/package.json +++ b/plugins/research_sync/package.json @@ -24,20 +24,18 @@ }, "peerDependencies": { "@clusterio/controller": "workspace:*", - "@clusterio/web_ui": "workspace:*", "@clusterio/host": "workspace:*", - "@clusterio/lib": "workspace:*" + "@clusterio/lib": "workspace:*", + "@clusterio/web_ui": "workspace:*" }, "dependencies": { - "@sinclair/typebox": "catalog:", - "fs-extra": "catalog:" + "@sinclair/typebox": "catalog:" }, "devDependencies": { "@clusterio/controller": "workspace:*", - "@clusterio/web_ui": "workspace:*", "@clusterio/host": "workspace:*", "@clusterio/lib": "workspace:*", - "@types/fs-extra": "catalog:", + "@clusterio/web_ui": "workspace:*", "@types/node": "catalog:", "@types/react": "catalog:", "typescript": "catalog:", diff --git a/plugins/subspace_storage/controller.ts b/plugins/subspace_storage/controller.ts index 3b8efdd1..4630d1a5 100644 --- a/plugins/subspace_storage/controller.ts +++ b/plugins/subspace_storage/controller.ts @@ -1,6 +1,6 @@ import { BaseControllerPlugin, type ControlConnection } from "@clusterio/controller"; -import fs from "fs-extra"; +import fs from "node:fs/promises"; import path from "path"; import * as lib from "@clusterio/lib"; diff --git a/plugins/subspace_storage/package.json b/plugins/subspace_storage/package.json index 9177e837..bc404d81 100644 --- a/plugins/subspace_storage/package.json +++ b/plugins/subspace_storage/package.json @@ -24,21 +24,19 @@ }, "peerDependencies": { "@clusterio/controller": "workspace:*", - "@clusterio/web_ui": "workspace:*", "@clusterio/host": "workspace:*", - "@clusterio/lib": "workspace:*" + "@clusterio/lib": "workspace:*", + "@clusterio/web_ui": "workspace:*" }, "dependencies": { - "@sinclair/typebox": "catalog:", - "fs-extra": "catalog:" + "@sinclair/typebox": "catalog:" }, "devDependencies": { "@clusterio/controller": "workspace:*", - "@clusterio/web_ui": "workspace:*", "@clusterio/host": "workspace:*", "@clusterio/lib": "workspace:*", + "@clusterio/web_ui": "workspace:*", "@types/express": "catalog:", - "@types/fs-extra": "catalog:", "@types/node": "catalog:", "@types/react": "catalog:", "antd": "catalog:", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fc8d08c6..2af79422 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,40 +1,40 @@ packages: - - "packages/*" - - "plugins/*" - - "external_plugins/*" - - "external_plugins/*/*" - - "test/" + - packages/* + - plugins/* + - external_plugins/* + - external_plugins/*/* + - test/ catalog: - "@sinclair/typebox": ^0.30.4 - "@swc/core": ^1.13.3 - "@types/express": ^4.17.17 - "@types/finalhandler": ^1.2.0 - "@types/fs-extra": ^11.0.1 - "@types/jsonwebtoken": ^9.0.9 - "@types/node": ^20.17.19 - "@types/react": ^18.3.18 - "@types/set-blocking": ^2.0.0 - "@types/ws": ^8.5.14 - "antd": ^5.24.2 - "chalk": ^4.1.2 - "express": ^5.1.0 - "fs-extra": ^8.1.0 - "jsonwebtoken": ^9.0.2 - "jszip": ^3.10.1 - "react": ^18.2.0 - "react-dom": ^18.2.0 - "react-router-dom": ^6.29.0 - "set-blocking": ^2.0.0 - "typescript": ^5.7.3 - "webpack": ^5.98.0 - "webpack-cli": ^5.1.4 - "webpack-merge": ^5.9.0 - "winston": ^3.17.0 - "winston-daily-rotate-file": ^4.7.1 - "winston-transport": ^4.9.0 - "ws": ^8.18.1 + '@sinclair/typebox': ^0.30.4 + '@swc/core': ^1.13.3 + '@types/express': ^4.17.17 + '@types/finalhandler': ^1.2.0 + '@types/jsonwebtoken': ^9.0.9 + '@types/node': ^20.17.19 + '@types/react': ^18.3.18 + '@types/set-blocking': ^2.0.0 + '@types/ws': ^8.5.14 + antd: ^5.24.2 + chalk: ^4.1.2 + express: ^5.1.0 + jsonwebtoken: ^9.0.2 + jszip: ^3.10.1 + react: ^18.2.0 + react-dom: ^18.2.0 + react-router-dom: ^6.29.0 + set-blocking: ^2.0.0 + typescript: ^5.7.3 + webpack: ^5.98.0 + webpack-cli: ^5.1.4 + webpack-merge: ^5.9.0 + winston: ^3.17.0 + winston-daily-rotate-file: ^4.7.1 + winston-transport: ^4.9.0 + ws: ^8.18.1 catalogMode: strict + linkWorkspacePackages: true + shellEmulator: true diff --git a/publish.js b/publish.js index 447a1610..21395c83 100644 --- a/publish.js +++ b/publish.js @@ -4,7 +4,7 @@ "use strict"; const FormData = require("form-data"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const yargs = require("yargs"); let DRY = false; @@ -62,7 +62,7 @@ async function uploadModRelease(file, name) { // Upload the new version of the mod const uploadUrl = (await initResponse.json()).upload_url; const uploadFormData = new FormData(); - uploadFormData.append("file", fs.createReadStream(file)); + uploadFormData.append("file", (await fs.open(file)).createReadStream(file)); const uploadResponse = await fetch(uploadUrl, { method: "POST", diff --git a/test/controller/HttpCloser.js b/test/controller/HttpCloser.js index d5e34122..d7bcadd4 100644 --- a/test/controller/HttpCloser.js +++ b/test/controller/HttpCloser.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const http = require("http"); const https = require("https"); const util = require("util"); diff --git a/test/controller/UserManager.test.js b/test/controller/UserManager.test.js index c010b9fe..8312c3b7 100644 --- a/test/controller/UserManager.test.js +++ b/test/controller/UserManager.test.js @@ -1,6 +1,6 @@ "use strict"; const path = require("path"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const assert = require("assert").strict; const jwt = require("jsonwebtoken"); const { Role, SubscribableDatastore, ControllerConfig } = require("@clusterio/lib"); @@ -26,7 +26,8 @@ describe("controller/UserManager", function () { "controller.auth_secret": Buffer.from("mysecret").toString("base64"), }); userManager = new UserManager(records, roles, config); - await fs.emptyDir(TEMP_DIR); + await fs.rm(TEMP_DIR, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(TEMP_DIR, { recursive: true }); }); describe(".has() / .get() / .getMutable() / .hasName() / .getByName() / .getByNameMutable()", function () { @@ -142,14 +143,14 @@ describe("controller/UserManager", function () { }); it("should do nothing if roles key missing", async function () { - await fs.writeJson(usersFile, { users: [] }); + await fs.writeFile(usersFile, JSON.stringify({ users: [] })); await UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile); - const content = await fs.readJson(usersFile); + const content = JSON.parse(await fs.readFile(usersFile)); assert.deepEqual(content, { users: [] }); }); it("should throw if users key missing in legacy file", async function () { - await fs.writeJson(usersFile, { roles: [] }); + await fs.writeFile(usersFile, JSON.stringify({ roles: [] })); await assert.rejects( () => UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile), /Error: Legacy users json does not contain users property/ @@ -158,12 +159,12 @@ describe("controller/UserManager", function () { it("should migrate valid legacy file", async function () { const legacy = { roles: [{ id: 1 }, { id: 2 }], users: [{ name: "Karl" }] }; - await fs.writeJson(usersFile, legacy); + await fs.writeFile(usersFile, JSON.stringify(legacy)); await UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile); - const writtenRoles = await fs.readJson(rolesFile); - const writtenUsers = await fs.readJson(usersFile); + const writtenRoles = JSON.parse(await fs.readFile(rolesFile)); + const writtenUsers = JSON.parse(await fs.readFile(usersFile)); assert.deepEqual(writtenRoles, [{ id: 1 }, { id: 2 }]); assert.deepEqual(writtenUsers, [{ name: "Karl" }]); diff --git a/test/host/patch.js b/test/host/patch.js index 8655aabd..3df1ca5e 100644 --- a/test/host/patch.js +++ b/test/host/patch.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const JSZip = require("jszip"); const path = require("path"); @@ -231,7 +231,7 @@ describe("host/patch", function() { let zip = new JSZip(); zip.file("world/control.lua", "-- unknown\n"); let zipPath = path.join("temp", "test", "patch.zip"); - await fs.outputFile(zipPath, await zip.generateAsync({ type: "nodebuffer" })); + await fs.writeFile(zipPath, await zip.generateAsync({ type: "nodebuffer" })); await assert.rejects( patch.patch(zipPath, []), diff --git a/test/host/server.js b/test/host/server.js index 13de4e75..ea19b133 100644 --- a/test/host/server.js +++ b/test/host/server.js @@ -1,7 +1,7 @@ "use strict"; const assert = require("assert").strict; const events = require("events"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); const hostServer = require("@clusterio/host/dist/node/src/server"); @@ -135,9 +135,10 @@ describe("host/server", function() { slowTest(this); const url = "https://github.com/clusterio/clusterio/archive/refs/tags/v2.0.0-alpha.22.zip"; const downloads = path.join("temp", "test", "downloads"); - await fs.emptyDir(downloads); + await fs.rm(downloads, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(downloads, { recursive: true }); await hostServer._downloadAndExtractZip(url, path.join(downloads, "zip")); - assert.ok(await fs.exists(path.join(downloads, "zip", "packages", "controller", "package.json"))); + await fs.access(path.join(downloads, "zip", "packages", "controller", "package.json")); }); it("errors and bad status", async function() { global.fetch = () => ({ ok: false, status: -1, statusText: "Fetch called" }); @@ -157,9 +158,10 @@ describe("host/server", function() { it("works", async function() { const url = "https://github.com/clusterio/clusterio/archive/refs/tags/v2.0.0-alpha.22.tar.gz"; const downloads = path.join("temp", "test", "downloads"); - await fs.emptyDir(downloads); + await fs.rm(downloads, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(downloads, { recursive: true }); await hostServer._downloadAndExtractTar(url, path.join(downloads, "tar")); - assert.ok(await fs.exists(path.join(downloads, "tar", "packages", "controller", "package.json"))); + await fs.access(path.join(downloads, "tar", "packages", "controller", "package.json")); }); it("errors and bad status", async function() { global.fetch = () => ({ ok: false, status: -1, statusText: "Fetch called" }); @@ -281,13 +283,14 @@ describe("host/server", function() { ); }); it("should load and delete json file", async function() { + await fs.mkdir(server.writePath("script-output"), { recursive: true }); let filePath = server.writePath("script-output", "data.json"); - await fs.outputFile(filePath, '{"data":"spam"}'); + await fs.writeFile(filePath, '{"data":"spam"}'); let waiter = events.once(server, "ipc-channel"); await server._handleIpc(Buffer.from("\f$ipc:channel?fdata.json")); let result = await waiter; assert.deepEqual(result[0], { "data": "spam" }); - assert(!await fs.pathExists(filePath), "File was not deleted"); + await assert.rejects(fs.access(filePath), "File was not deleted"); }); }); @@ -469,7 +472,8 @@ describe("host/server", function() { server._factorioDir = path.join("test", "file", "factorioDownload"); server._targetVersion = "latest"; global.fetch = _fetch; - await fs.emptyDir(server._factorioDir); + await fs.rm(server._factorioDir, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(server._factorioDir, { recursive: true }); await server.checkForUpdates([{ stable: true, version: "2.0.73", diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 6bc7e9b8..480f4b46 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const jwt = require("jsonwebtoken"); const path = require("path"); const events = require("events"); @@ -36,9 +36,9 @@ async function spawnAltHost(config = "alt-host-config.json") { async function startAltHost() { let config = "alt-host-config.json"; let configPath = path.join("temp", "test", config); - await fs.remove(configPath); - await fs.remove(path.join("temp", "test", "alt-instances")); - await fs.remove(path.join("temp", "test", "alt-mods")); + await fs.rm(configPath, { force: true }); + await fs.rm(path.join("temp", "test", "alt-instances"), { force: true, recursive: true, maxRetries: 10 }); + await fs.rm(path.join("temp", "test", "alt-mods"), { force: true, recursive: true, maxRetries: 10 }); await execCtl(`host create-config --id 5 --name alt-host --generate-token --output ${config}`); await execHost(`--config ${config} config set host.instances_directory alt-instances`); const dir = path.isAbsolute(factorioDir) ? factorioDir : path.join("..", "..", factorioDir); @@ -131,10 +131,10 @@ describe("Integration of Clusterio", function() { try { await execController("bootstrap create-admin BootstrapAdminTest2 --bypass-lock-file"); } finally { - await fs.move("temp.lock", lockFilePath); // Replace the lockfile after it is deleted + await fs.rename("temp.lock", lockFilePath); // Replace the lockfile after it is deleted } - const json = await fs.readJSON(path.join(databaseDir, "users.json")); + const json = JSON.parse(await fs.readFile(path.join(databaseDir, "users.json"), "utf8")); assert.equal(Object.values(json).some(user => user.name === "BootstrapAdminTest2"), true); }); }); @@ -154,10 +154,10 @@ describe("Integration of Clusterio", function() { try { await execController("config set controller.name ConfigEditTest2 --bypass-lock-file"); } finally { - await fs.move("temp.lock", lockFilePath); // Replace the lockfile after it is deleted + await fs.rename("temp.lock", lockFilePath); // Replace the lockfile after it is deleted } - const json = await fs.readJSON(path.join(controllerConfigPath)); + const json = JSON.parse(await fs.readFile(path.join(controllerConfigPath), "utf8")); assert.equal(json["controller.name"], "ConfigEditTest2"); }); }); @@ -197,17 +197,19 @@ describe("Integration of Clusterio", function() { } catch (err) { assert.equal(/Error: Server listening failed/.test(err.stderr), true); } - await fs.move("temp.lock", lockFilePath); // Replace the lockfile after it is deleted + await fs.rename("temp.lock", lockFilePath); // Replace the lockfile after it is deleted }); it("should refuse to start when no users are loaded", async function() { const dir = path.join("temp", "test", "empty_controller"); - await fs.emptyDir(dir); + await fs.rm(dir, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(dir, { recursive: true }); await assert.rejects(execController("run", { cwd: dir })); }); it("should start when no users are loaded if bypass option given", async function() { slowTest(this); const dir = path.join("temp", "test", "empty_controller"); - await fs.emptyDir(dir); + await fs.rm(dir, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(dir, { recursive: true }); const child = await spawnNode( "altController", "../../../packages/controller run --no-check-user-count", @@ -349,7 +351,7 @@ describe("Integration of Clusterio", function() { await stopAltHost(hostProcess); } - const json = await fs.readJSON(path.join("temp", "test", config)); + const json = JSON.parse(await fs.readFile(path.join("temp", "test", config), "utf8")); assert.equal(json["host.name"], "ConfigEditTest2"); }); }); @@ -615,7 +617,10 @@ describe("Integration of Clusterio", function() { describe("instance assign", function() { it("creates the instance files", async function() { await execCtl("instance assign test 4"); - assert(await fs.exists(path.join(instancesDir, "test")), "Instance directory was not created"); + await assert.doesNotReject( + fs.access(path.join(instancesDir, "test")), + "Instance directory was not created" + ); await checkInstanceStatus(44, "stopped"); }); }); @@ -652,15 +657,15 @@ describe("Integration of Clusterio", function() { slowTest(this); requiresFactorio(this); let exportPath = path.join("temp", "test", "static"); - await fs.remove(exportPath); + await fs.rm(exportPath, { force: true, recursive: true, maxRetries: 10 }); await execCtl("instance export-data test"); let modPack = await getControl().send(new lib.ModPackGetDefaultRequest()); let assets = modPack.exportManifest.assets; assert(Object.keys(assets).length > 1, "Export assets is empty"); for (let key of ["settings", "prototypes", "metadata", "spritesheet", "locale"]) { assert(assets[key], `Missing ${key} from assets`); - assert( - await fs.exists(path.join(exportPath, assets[key])), + await assert.doesNotReject( + fs.access(path.join(exportPath, assets[key])), `Manifest entry for ${key} was not written to filesystem` ); } @@ -733,7 +738,7 @@ describe("Integration of Clusterio", function() { slowTest(this); await execCtl("instance stop 44"); let savesDir = path.join("temp", "test", "instances", "test", "saves"); - await fs.copy(path.join(savesDir, "world.zip"), path.join(savesDir, "_autosave1.zip")); + await fs.copyFile(path.join(savesDir, "world.zip"), path.join(savesDir, "_autosave1.zip")); await prepareToStart(); await execCtl(`instance ${cmd} test`); let saves = await getControl().sendTo({ instanceId: 44 }, new lib.InstanceSaveDetailsListRequest()); @@ -1156,28 +1161,28 @@ describe("Integration of Clusterio", function() { describe("instance save upload", function() { it("should upload a zip file", async function() { - await fs.outputFile(path.join("temp", "test", "upload.zip"), "a test"); + await fs.writeFile(path.join("temp", "test", "upload.zip"), "a test"); await execCtl("instance save upload 44 upload.zip"); - assert( - await fs.pathExists(path.join("temp", "test", "instances", "test", "saves", "upload.zip")), - "file not uploaded to saves directory" + await assert.doesNotReject( + fs.access(path.join("temp", "test", "instances", "test", "saves", "upload.zip")), + "file not uploaded to saves directory", ); }); it("should reject non-zip files", async function() { - await fs.outputFile(path.join("temp", "test", "invalid"), "a test"); + await fs.writeFile(path.join("temp", "test", "invalid"), "a test"); await assert.rejects(execCtl("instance save upload 44 invalid")); }); it("should reject path traversal attacks", async function() { - await fs.outputFile(path.join("temp", "test", "upload.zip"), "a test"); + await fs.writeFile(path.join("temp", "test", "upload.zip"), "a test"); await assert.rejects(execCtl("instance save upload 44 upload.zip --name ../traversal.zip")); }); }); describe("instance save download", function() { it("should download a save", async function() { - await fs.remove(path.join("temp", "test", "upload.zip")); + await fs.rm(path.join("temp", "test", "upload.zip"), { force: true }); await execCtl("instance save download 44 upload.zip"); - assert(await fs.pathExists(path.join("temp", "test", "upload.zip"))); + await fs.access(path.join("temp", "test", "upload.zip")); }); it("should error if save does not exist", async function() { await assert.rejects(execCtl("instance save download 44 invalid")); @@ -1190,7 +1195,7 @@ describe("Integration of Clusterio", function() { describe("instance save copy", function() { it("copy a save file", async function() { await execCtl("instance save copy 44 upload.zip copy.zip"); - assert(await fs.pathExists(path.join("temp", "test", "instances", "test", "saves", "copy.zip"))); + await fs.access(path.join("temp", "test", "instances", "test", "saves", "copy.zip")); }); it("should error if destination exist", async function() { await assert.rejects(execCtl("instance save copy 44 upload.zip copy.zip")); @@ -1208,8 +1213,8 @@ describe("Integration of Clusterio", function() { describe("instance save rename", function() { it("rename a save file", async function() { await execCtl("instance save rename 44 copy.zip rename.zip"); - assert(!await fs.pathExists(path.join("temp", "test", "instances", "test", "saves", "copy.zip"))); - assert(await fs.pathExists(path.join("temp", "test", "instances", "test", "saves", "rename.zip"))); + await assert.rejects(fs.access(path.join("temp", "test", "instances", "test", "saves", "copy.zip"))); + await fs.access(path.join("temp", "test", "instances", "test", "saves", "rename.zip")); }); it("should error if new name exist", async function() { await assert.rejects(execCtl("instance save rename 44 upload.zip rename.zip")); @@ -1264,31 +1269,31 @@ describe("Integration of Clusterio", function() { it("should transfers a save", async function() { await uploadSave(pri, "transfer.zip", "transfer.zip"); await execCtlProcess(`instance save transfer ${pri} transfer.zip ${sec}`); - assert(!await fs.pathExists(path.join(priSaves, "transfer.zip")), "save still at pri"); - assert(await fs.pathExists(path.join(secSaves, "transfer.zip")), "save not at sec"); + await assert.rejects(fs.access(path.join(priSaves, "transfer.zip")), "save still at pri"); + await assert.doesNotReject(fs.access(path.join(secSaves, "transfer.zip")), "save not at sec"); await deleteSave(sec, "transfer.zip"); }); it("should support rename", async function() { await uploadSave(pri, "transfer.zip", "transfer.zip"); await execCtlProcess(`instance save transfer ${pri} transfer.zip ${sec} rename.zip`); - assert(!await fs.pathExists(path.join(priSaves, "transfer.zip")), "save still at pri"); - assert(await fs.pathExists(path.join(secSaves, "rename.zip")), "save not at sec"); + await assert.rejects(fs.access(path.join(priSaves, "transfer.zip")), "save still at pri"); + assert(await (path.join(secSaves, "rename.zip")), "save not at sec"); await deleteSave(sec, "rename.zip"); }); it("should auto-rename if target exists", async function() { await uploadSave(pri, "transfer.zip", "transfer.zip"); await uploadSave(sec, "transfer.zip", "transfer.zip"); await execCtlProcess(`instance save transfer ${pri} transfer.zip ${sec}`); - assert(!await fs.pathExists(path.join(priSaves, "transfer.zip")), "save still at pri"); - assert(await fs.pathExists(path.join(secSaves, "transfer-2.zip")), "save not at sec"); + await assert.rejects(fs.access(path.join(priSaves, "transfer.zip")), "save still at pri"); + await assert.doesNotReject(fs.access(path.join(secSaves, "transfer-2.zip")), "save not at sec"); await deleteSave(sec, "transfer.zip"); await deleteSave(sec, "transfer-2.zip"); }); it("should copy when using --copy", async function() { await uploadSave(pri, "transfer.zip", "transfer.zip"); await execCtlProcess(`instance save transfer ${pri} transfer.zip ${sec} --copy`); - assert(await fs.pathExists(path.join(priSaves, "transfer.zip")), "save not at pri"); - assert(await fs.pathExists(path.join(secSaves, "transfer.zip")), "save not at sec"); + await assert.doesNotReject(fs.access(path.join(priSaves, "transfer.zip")), "save not at pri"); + await assert.doesNotReject(fs.access(path.join(secSaves, "transfer.zip")), "save not at sec"); await deleteSave(pri, "transfer.zip"); await deleteSave(sec, "transfer.zip"); }); @@ -1344,16 +1349,16 @@ describe("Integration of Clusterio", function() { describe("instance save delete", function() { it("should delete a save", async function() { await execCtl("instance save delete 44 upload.zip"); - assert( - !await fs.pathExists(path.join("temp", "test", "instances", "test", "saves", "upload.zip")), - "file not deleted" + await assert.rejects( + fs.access(path.join("temp", "test", "instances", "test", "saves", "upload.zip")), + "file not deleted", ); }); it("should error if save does not exist", async function() { await assert.rejects(execCtl("instance save delete 44 upload.zip")); }); it("should error on path traversal attacks", async function() { - await fs.outputFile(path.join("temp", "test", "upload.zip"), "a test"); + await fs.writeFile(path.join("temp", "test", "upload.zip"), "a test"); await assert.rejects(execCtl("instance save delete 44 ../../upload.zip")); }); }); @@ -1363,7 +1368,7 @@ describe("Integration of Clusterio", function() { slowTest(this); getControl().saveUpdates = []; await execCtl("instance delete test"); - assert(!await fs.exists(path.join(instancesDir, "test")), "Instance files was not deleted"); + await assert.rejects(fs.access(path.join(instancesDir, "test")), "Instance files was not deleted"); let instances = await getInstances(); assert(!instances.has(44), "instance was not deleted from controller"); @@ -1497,9 +1502,9 @@ describe("Integration of Clusterio", function() { outputDir: path.join("temp", "test"), }); await execCtl("mod upload empty_mod_1.0.0.zip"); - assert( - await fs.pathExists(path.join("temp", "test", "mods", "empty_mod_1.0.0.zip")), - "mod not present in mods directory" + await assert.doesNotReject( + fs.access(path.join("temp", "test", "mods", "empty_mod_1.0.0.zip")), + "mod not present in mods directory", ); }); it("rejects path traversing mods", async function() { @@ -1510,11 +1515,11 @@ describe("Integration of Clusterio", function() { outputDir: path.join("temp", "test"), modName: "path_traversing_mod_1.0.0", }); - await fs.remove(path.join("temp", "test", "bad_path_mod_1.0.0.zip")); + await fs.rm(path.join("temp", "test", "bad_path_mod_1.0.0.zip"), { force: true }); await assert.rejects(execCtl("mod upload path_traversing_mod_1.0.0.zip")); - assert( - !await fs.pathExists(path.join("temp", "test", "bad_path_mod_1.0.0.zip")), - "mod executed a path traversal attack", + await assert.rejects( + fs.access(path.join("temp", "test", "bad_path_mod_1.0.0.zip")), + "mod executed a path traversal attack" ); }); }); @@ -1562,9 +1567,9 @@ describe("Integration of Clusterio", function() { it("downloads a mod", async function() { await fs.unlink(path.join("temp", "test", "empty_mod_1.0.0.zip")); await execCtl("mod download empty_mod 1.0.0"); - assert( - await fs.pathExists(path.join("temp", "test", "empty_mod_1.0.0.zip")), - "mod not downloaded to cwd" + await assert.doesNotReject( + fs.access(path.join("temp", "test", "empty_mod_1.0.0.zip")), + "mod not downloaded to cwd", ); }); }); @@ -1572,9 +1577,9 @@ describe("Integration of Clusterio", function() { describe("mod delete", function() { it("deletes a mod", async function() { await execCtl("mod delete empty_mod 1.0.0"); - assert( - !await fs.pathExists(path.join("temp", "test", "mods", "empty_mod_1.0.0.zip")), - "mod still present in mods dir" + await assert.rejects( + fs.access(path.join("temp", "test", "mods", "empty_mod_1.0.0.zip")), + "mod still present in mods dir", ); }); }); @@ -1761,7 +1766,7 @@ describe("Integration of Clusterio", function() { await execCtl("user set-whitelisted import_control --remove"); }); it("should import users", async function() { - await fs.writeJSON(path.join("temp", "test", "import-users.json"), { + await fs.writeFile(path.join("temp", "test", "import-users.json"), JSON.stringify({ export_version: "2.0.0.alpha20", users: [ { username: "import_user_admin", is_admin: true }, @@ -1771,7 +1776,7 @@ describe("Integration of Clusterio", function() { { username: "import_user_none" }, { username: "import_control", is_admin: true }, ], - }); + })); await execCtl("user import --users import-users.json"); @@ -1794,10 +1799,10 @@ describe("Integration of Clusterio", function() { assert.equal(userBanReason.banReason, "banned", "import_user_ban_reason had incorrect ban reason"); }); it("should import users by json format", async function() { - await fs.writeJSON(path.join("temp", "test", "import-users.json"), { + await fs.writeFile(path.join("temp", "test", "import-users.json"), JSON.stringify({ export_version: "2.0.0.alpha20", users: [{ username: "import_user", is_admin: true, is_whitelisted: true }], - }); + })); await execCtl("user import import-users.json"); const userFoo = (await getUsers()).get("import_user"); @@ -1805,11 +1810,11 @@ describe("Integration of Clusterio", function() { assert(userFoo.isWhitelisted, "import_user was not whitelisted"); }); it("should import bans", async function() { - await fs.writeJSON(path.join("temp", "test", "import-bans.json"), [ + await fs.writeFile(path.join("temp", "test", "import-bans.json"), JSON.stringify([ { username: "import_ban_reason", reason: "banned" }, "import_ban", "import_control", - ]); + ])); await execCtl("user import --bans import-bans.json"); @@ -1826,17 +1831,17 @@ describe("Integration of Clusterio", function() { assert(users.get("import_control").isBanned, "import_control was not banned"); }); it("should import bans by filename", async function() { - await fs.writeJSON(path.join("temp", "test", "import-bans.json"), [ + await fs.writeFile(path.join("temp", "test", "import-bans.json"), JSON.stringify([ "import_ban_filename", - ]); + ])); await execCtl("user import import-bans.json"); assert((await getUsers()).get("import_ban_filename").isBanned, "import_ban_filename was not banned"); }); it("should import admins", async function() { - await fs.writeJSON(path.join("temp", "test", "import-admins.json"), [ + await fs.writeFile(path.join("temp", "test", "import-admins.json"), JSON.stringify([ "import_admin", "import_control", - ]); + ])); await execCtl("user import --admins import-admins.json"); const users = await getUsers(); @@ -1844,17 +1849,17 @@ describe("Integration of Clusterio", function() { assert(users.get("import_control").isAdmin, "import_control was not admin"); }); it("should import admins by filename", async function() { - await fs.writeJSON(path.join("temp", "test", "import-admins.json"), [ + await fs.writeFile(path.join("temp", "test", "import-admins.json"), JSON.stringify([ "import_admin_filename", - ]); + ])); await execCtl("user import import-admins.json"); assert((await getUsers()).get("import_admin_filename").isAdmin, "import_admin_filename was not admin"); }); it("should import whitelist", async function() { - await fs.writeJSON(path.join("temp", "test", "import-whitelist.json"), [ + await fs.writeFile(path.join("temp", "test", "import-whitelist.json"), JSON.stringify([ "import_whitelist", "import_control", - ]); + ])); await execCtl("user import --whitelist import-whitelist.json"); const users = await getUsers(); @@ -1862,9 +1867,9 @@ describe("Integration of Clusterio", function() { assert(users.get("import_control").isWhitelisted, "import_control was not whitelisted"); }); it("should import whitelist by filename", async function() { - await fs.writeJSON(path.join("temp", "test", "import-whitelist.json"), [ + await fs.writeFile(path.join("temp", "test", "import-whitelist.json"), JSON.stringify([ "import_whitelist_filename", - ]); + ])); await execCtl("user import import-whitelist.json"); assert( @@ -1885,7 +1890,7 @@ describe("Integration of Clusterio", function() { await execCtl("user set-admin export_user --create"); await execCtl("user set-whitelisted export_user"); await execCtl("user export --users user-export.json"); - const data = await fs.readJson(path.join("temp", "test", "user-export.json")); + const data = JSON.parse(await fs.readFile(path.join("temp", "test", "user-export.json"))); assert(data.users, "Users array does not exist"); assert(!data.users.find(u => u.username === "export_control"), "Control user is present"); assert.deepEqual(data.users.find(u => u.username === "export_user"), { @@ -1896,7 +1901,7 @@ describe("Integration of Clusterio", function() { await execCtl("user set-banned export_ban --create"); await execCtl("user set-banned export_ban_reason --create --reason banned"); await execCtl("user export --bans ban-export.json"); - const data = await fs.readJson(path.join("temp", "test", "ban-export.json")); + const data = JSON.parse(await fs.readFile(path.join("temp", "test", "ban-export.json"))); assert.equal(data.indexOf("export_control"), -1, "Control user is present"); assert(data.indexOf("export_ban") >= 0, "export_ban is missing"); assert.deepEqual(data.find(u => typeof u === "object" && u.username === "export_ban_reason"), { @@ -1906,14 +1911,14 @@ describe("Integration of Clusterio", function() { it("should export admins", async function() { await execCtl("user set-admin export_admin --create"); await execCtl("user export --admins admin-export.json"); - const data = await fs.readJson(path.join("temp", "test", "admin-export.json")); + const data = JSON.parse(await fs.readFile(path.join("temp", "test", "admin-export.json"))); assert.equal(data.indexOf("export_control"), -1, "Control user is present"); assert(data.indexOf("export_admin") >= 0, "export_admin is missing"); }); it("should export whitelist", async function() { await execCtl("user set-whitelisted export_whitelist --create"); await execCtl("user export --whitelist whitelist-export.json"); - const data = await fs.readJson(path.join("temp", "test", "whitelist-export.json")); + const data = JSON.parse(await fs.readFile(path.join("temp", "test", "whitelist-export.json"))); assert.equal(data.indexOf("export_control"), -1, "Control user is present"); assert(data.indexOf("export_whitelist") >= 0, "export_whitelist is missing"); }); @@ -1927,13 +1932,13 @@ describe("Integration of Clusterio", function() { await execCtl("user set-whitelisted restore_control"); }); it("should restore users", async function() { - await fs.writeJSON(path.join("temp", "test", "restore-users.json"), { + await fs.writeFile(path.join("temp", "test", "restore-users.json"), JSON.stringify({ export_version: "2.0.0.alpha20", users: [ { username: "restore_user", is_admin: true, is_whitelisted: true }, { username: "restore_user_two", is_banned: true, ban_reason: "Test" }, ], - }); + })); await execCtl("user restore restore-users.json"); const users = await getUsers(); @@ -1951,17 +1956,17 @@ describe("Integration of Clusterio", function() { assert(!control.isBanned, "restore_control was banned"); assert(!control.isWhitelisted, "restore_control was whitelisted"); - const data = await fs.readJson(path.join("temp", "test", "users-backup.json")); + const data = JSON.parse(await fs.readFile(path.join("temp", "test", "users-backup.json"))); assert(data.users, "Users array does not exist"); assert.deepEqual(data.users.find(u => u.username === "restore_control"), { username: "restore_control", is_admin: true, is_banned: true, is_whitelisted: true, }); }); it("should restore bans", async function() { - await fs.writeJSON(path.join("temp", "test", "restore-bans.json"), [ + await fs.writeFile(path.join("temp", "test", "restore-bans.json"), JSON.stringify([ { username: "restore_ban_reason", reason: "banned" }, "restore_ban", - ]); + ])); await execCtl("user restore --bans restore-bans.json"); @@ -1977,13 +1982,13 @@ describe("Integration of Clusterio", function() { assert(!users.get("restore_control").isBanned, "restore_control was banned"); - const data = await fs.readJson(path.join("temp", "test", "bans-backup.json")); + const data = JSON.parse(await fs.readFile(path.join("temp", "test", "bans-backup.json"))); assert(data.indexOf("restore_control") >= 0, "restore_control is missing"); }); it("should restore admins", async function() { - await fs.writeJSON(path.join("temp", "test", "restore-admins.json"), [ + await fs.writeFile(path.join("temp", "test", "restore-admins.json"), JSON.stringify([ "restore_admin", - ]); + ])); await execCtl("user restore --admins restore-admins.json"); const users = await getUsers(); @@ -1991,13 +1996,13 @@ describe("Integration of Clusterio", function() { assert(users.get("restore_admin").isAdmin, "restore_admin was not admin"); assert(!users.get("restore_control").isAdmin, "restore_control was admin"); - const data = await fs.readJson(path.join("temp", "test", "admins-backup.json")); + const data = JSON.parse(await fs.readFile(path.join("temp", "test", "admins-backup.json"))); assert(data.indexOf("restore_control") >= 0, "restore_control is missing"); }); it("should restore whitelist", async function() { - await fs.writeJSON(path.join("temp", "test", "restore-whitelist.json"), [ + await fs.writeFile(path.join("temp", "test", "restore-whitelist.json"), JSON.stringify([ "restore_whitelist", - ]); + ])); await execCtl("user restore --whitelist restore-whitelist.json"); const users = await getUsers(); @@ -2005,7 +2010,7 @@ describe("Integration of Clusterio", function() { assert(users.get("restore_whitelist").isWhitelisted, "restore_whitelist was not whitelisted"); assert(!users.get("restore_control").isWhitelisted, "restore_control was whitelisted"); - const data = await fs.readJson(path.join("temp", "test", "whitelist-backup.json")); + const data = JSON.parse(await fs.readFile(path.join("temp", "test", "whitelist-backup.json"))); assert(data.indexOf("restore_control") >= 0, "restore_control is missing"); }); it("should reject multiple provided options", async function() { diff --git a/test/integration/create.js b/test/integration/create.js index c1dfcd9e..74784c11 100644 --- a/test/integration/create.js +++ b/test/integration/create.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const jwt = require("jsonwebtoken"); const path = require("path"); const events = require("events"); @@ -18,10 +18,10 @@ async function exec(command, options = {}) { describe("Integration of create tool", function () { beforeEach(async function () { - await fs.ensureDir(cwd); + await fs.mkdir(cwd, { recursive: true }); }); afterEach(async function () { - await fs.remove(cwd); + await fs.rm(cwd, { recursive: true, maxRetries: 10 }); }); it("should create a new standalone installation", async function () { this.skip(); @@ -37,10 +37,10 @@ describe("Integration of create tool", function () { ].join(" ")); // Check that the generated config is correct - const controllerConfig = await fs.readJson(path.join(cwd, "config-controller.json")); + const controllerConfig = JSON.parse(await fs.readFile(path.join(cwd, "config-controller.json"))); assert.equal(controllerConfig["controller.http_port"], 8099); - const hostConfig = await fs.readJson(path.join(cwd, "config-host.json")); + const hostConfig = JSON.parse(await fs.readFile(path.join(cwd, "config-host.json"))); assert.equal(hostConfig["host.name"], "local"); assert.equal(hostConfig["host.public_address"], "localhost"); assert.equal(hostConfig["host.factorio_directory"], "test/factorio"); @@ -58,9 +58,9 @@ describe("Integration of create tool", function () { ].join(" ")); // Check that the generated config is correct - const controllerConfig = await fs.readJson(path.join(cwd, "config-controller.json")); + const controllerConfig = JSON.parse(await fs.readFile(path.join(cwd, "config-controller.json"))); assert.equal(controllerConfig["controller.http_port"], 8099); - await assert.rejects(fs.readJson(path.join(cwd, "config-host.json"))); + await assert.rejects(fs.access(path.join(cwd, "config-host.json"))); }).timeout(60000); }); diff --git a/test/integration/index.js b/test/integration/index.js index 3a11620d..c9ea3d97 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -1,7 +1,8 @@ /* eslint-disable no-console */ "use strict"; const path = require("path"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); +const { readFileSync } = require("node:fs"); const child_process = require("child_process"); const jwt = require("jsonwebtoken"); const util = require("util"); @@ -160,7 +161,7 @@ async function get(urlPath) { function loadJSON(filePath) { try { // eslint-disable-next-line node/no-sync - return JSON.parse(fs.readFileSync(filePath, "utf8")); + return JSON.parse(readFileSync(filePath, "utf8")); } catch (err) { if (err.code === "ENOENT") { return undefined; @@ -319,19 +320,19 @@ before(async function() { console.log("FAST_TEST is present in env, slow tests will be skipped."); } - await fs.remove(instancesDir); - await fs.remove(modsDir); - await fs.remove(databaseDir); + await fs.rm(instancesDir, { force: true, recursive: true, maxRetries: 10 }); + await fs.rm(modsDir, { force: true, recursive: true, maxRetries: 10 }); + await fs.rm(databaseDir, { force: true, recursive: true, maxRetries: 10 }); - await fs.remove(pluginListPath); - await fs.remove(controllerConfigPath); - await fs.remove(hostConfigPath); - await fs.remove(controlConfigPath); + await fs.rm(pluginListPath, { force: true }); + await fs.rm(controllerConfigPath, { force: true }); + await fs.rm(hostConfigPath, { force: true }); + await fs.rm(controlConfigPath, { force: true }); - await fs.ensureDir(path.join("temp", "test")); + await fs.mkdir(path.join("temp", "test"), { recursive: true }); console.log("Building Mods"); - await fs.ensureDir(modsDir); + await fs.mkdir(modsDir, { recursive: true }); await lib.build({ build: true, pack: true, diff --git a/test/integration/instance.js b/test/integration/instance.js index 7cba7de8..ccd94d30 100644 --- a/test/integration/instance.js +++ b/test/integration/instance.js @@ -2,7 +2,7 @@ const path = require("path"); const assert = require("assert").strict; const lib = require("@clusterio/lib"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const { testMatrix } = require("../common"); const { @@ -82,16 +82,16 @@ describe("Clusterio Instance", function() { }); it("should always create a whitelist json even if unused", async function() { slowTest(this); - await fs.remove(whitelistPath); + await fs.rm(whitelistPath, { force: true }); await execCtl(`instance start ${instName}`); - const whitelist = await fs.readJSON(whitelistPath); + const whitelist = JSON.parse(await fs.readFile(whitelistPath, "utf8")); assert.deepEqual(whitelist, []); }); it("should not overwrite the existing whitelist if sync is disabled", async function() { slowTest(this); - await fs.writeJSON(whitelistPath, ["test_user"]); + await fs.writeFile(whitelistPath, JSON.stringify(["test_user"])); await execCtl(`instance start ${instName}`); - const whitelist = await fs.readJSON(whitelistPath); + const whitelist = JSON.parse(await fs.readFile(whitelistPath, "utf8")); assert.deepEqual(whitelist, ["test_user"]); }); }); diff --git a/test/integration/link.js b/test/integration/link.js index 3d75fbfd..01483b86 100644 --- a/test/integration/link.js +++ b/test/integration/link.js @@ -1,7 +1,7 @@ "use strict"; const assert = require("assert").strict; const events = require("events"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const jwt = require("jsonwebtoken"); const { TestControlConnector, TestHostConnector, TestControl, get, exec, url, slowTest } = require("./index"); diff --git a/test/integration/patch.js b/test/integration/patch.js index 3b2cc72d..a4323e79 100644 --- a/test/integration/patch.js +++ b/test/integration/patch.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const JSZip = require("jszip"); const path = require("path"); diff --git a/test/integration/server.js b/test/integration/server.js index 24e6635f..8c8a711d 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1,7 +1,7 @@ "use strict"; const assert = require("assert").strict; const events = require("events"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); const lib = require("@clusterio/lib"); @@ -44,12 +44,10 @@ describe("Integration of host/src/server", function() { before(async function() { // Delete result from previous run of these tests - if (await fs.pathExists(writePath)) { - await fs.remove(writePath); - } + await fs.rm(writePath, { force: true, recursive: true, maxRetries: 10 }); - await fs.ensureDir(writePath); - logFile = fs.createWriteStream(path.join(writePath, "log.txt"), "utf8"); + await fs.mkdir(writePath, { recursive: true }); + logFile = (await fs.open(path.join(writePath, "log.txt"), "w")).createWriteStream({ encoding: "utf8" }); server.on("output", function(output) { if (output.source === "stderr") { // Factorio doesn't print on stderr but sometimes on @@ -103,10 +101,10 @@ describe("Integration of host/src/server", function() { // Make sure the test is not fooled by previous data let mapPath = server.writePath("saves", "test.zip"); - assert(!await fs.exists(mapPath), "save exist before test"); + await assert.rejects(fs.access(mapPath), "save exist before test"); await server.create("test.zip", undefined, { width: 50, height: 50 }); - assert(await fs.exists(mapPath), "test did not create save"); + assert.doesNotReject(await fs.access(mapPath), "test did not create save"); }); }); @@ -119,11 +117,11 @@ describe("Integration of host/src/server", function() { let settings = await server.exampleSettings(); settings.visibility = { public: false, lan: true }; settings.require_user_verification = false; - await fs.outputFile(server.writePath("server-settings.json"), JSON.stringify(settings, null, "\t")); + await fs.writeFile(server.writePath("server-settings.json"), JSON.stringify(settings, null, "\t")); // Make sure the test does not fail due to create() failing. let mapPath = server.writePath("saves", "test.zip"); - assert(await fs.exists(mapPath), "save is missing"); + await assert.doesNotReject(fs.access(mapPath), "save is missing"); await server.start("test.zip"); }); @@ -171,7 +169,8 @@ describe("Integration of host/src/server", function() { describe(".startScenario()", function() { before("Write test_scenario", async function() { let content = "script.on_init(function() print('test_scenario init') end)\n"; - await fs.outputFile(server.writePath("scenarios", "test_scenario", "control.lua"), content); + await fs.mkdir(server.writePath("scenarios", "test_scenario"), { recursive: true }); + await fs.writeFile(server.writePath("scenarios", "test_scenario", "control.lua"), content); }); it("runs the given scenario", async function() { diff --git a/test/lib/LockFile.test.js b/test/lib/LockFile.test.js index 6bceeb06..f0903132 100644 --- a/test/lib/LockFile.test.js +++ b/test/lib/LockFile.test.js @@ -1,7 +1,7 @@ /* eslint-disable node/no-sync */ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); const { LockFile } = require("@clusterio/lib"); @@ -15,7 +15,7 @@ describe("class LockFile", function () { let lock; before(async function () { - await fs.ensureDir(baseDir); + await fs.mkdir(baseDir, { recursive: true }); }); beforeEach(function() { @@ -30,7 +30,7 @@ describe("class LockFile", function () { describe("acquire()", function () { it("should create the lock file when acquiring", async function () { await lock.acquire(); - assert.ok(fs.existsSync(filePath)); + await fs.access(filePath); }); it("should throw LockFileBusyError if called when not idle", async function () { @@ -41,7 +41,7 @@ describe("class LockFile", function () { it("should remove stale lock file and acquire it", async function () { await fs.writeFile(filePath, "999999\n"); // stale lock await lock.acquire(); - assert.ok(fs.existsSync(filePath)); + await fs.access(filePath); }); it("should throw if existing lock file is not stale", async function () { @@ -54,7 +54,7 @@ describe("class LockFile", function () { it("should remove the lock file when releasing", async function () { await lock.acquire(); await lock.release(); - assert.ok(!fs.existsSync(filePath)); + await assert.rejects(fs.access(filePath)); }); it("should throw LockFileBusyError if called when not acquired", async function () { diff --git a/test/lib/ModStore.test.js b/test/lib/ModStore.test.js index 6f188c8b..9c140b3a 100644 --- a/test/lib/ModStore.test.js +++ b/test/lib/ModStore.test.js @@ -1,7 +1,7 @@ "use strict"; const assert = require("assert").strict; const path = require("path"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const stream = require("node:stream"); const JSZip = require("jszip"); // Added for creating mock zips @@ -44,7 +44,7 @@ async function waitBriefly() { describe("lib/ModStore", function () { before(async function () { - await fs.ensureDir(MODS_DIR); + await fs.mkdir(MODS_DIR, { recursive: true }); }); after(function() { @@ -59,7 +59,8 @@ describe("lib/ModStore", function () { }); afterEach(async function () { - await fs.emptyDir(MODS_DIR); + await fs.rm(MODS_DIR, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(MODS_DIR, { recursive: true }); }); describe("constructor", function () { @@ -120,7 +121,7 @@ describe("lib/ModStore", function () { // Add other file types and directories await fs.writeFile(path.join(MODS_DIR, "some-text.txt"), "hello"); - await fs.ensureDir(path.join(MODS_DIR, "a-directory")); + await fs.mkdir(path.join(MODS_DIR, "a-directory"), { recursive: true }); await fs.writeFile(path.join(MODS_DIR, "archive.tar.gz"), "not a zip"); await createMockModZip(path.join(MODS_DIR, "temp_file.tmp.zip"), "temp", "1.0.0"); // Should ignore .tmp.zip @@ -139,7 +140,7 @@ describe("lib/ModStore", function () { await createMockModZip(modPath, modName, modVersion); let modStore = await ModStore.fromDirectory(MODS_DIR); assert.equal(modStore.files.size, 1); - assert(await fs.pathExists(CACHE_FILE), "Cache file should exist"); + await assert.doesNotReject(fs.access(CACHE_FILE), "Cache file should exist"); // Wait and recreate the file to update mtime await waitBriefly(); @@ -197,7 +198,9 @@ describe("lib/ModStore", function () { beforeEach(async function () { // Start with an empty store for download tests - await fs.emptyDir(MODS_DIR); // Clear any existing files in the temp dir + // Clear any existing files in the temp dir + await fs.rm(MODS_DIR, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(MODS_DIR, { recursive: true }); modStore = await ModStore.fromDirectory(MODS_DIR); // Initialize using fromDirectory // Restore fetch before each test in this suite to avoid pollution @@ -269,7 +272,7 @@ describe("lib/ModStore", function () { assert.equal(modStore.files.size, 1, "Should have 1 mod after download"); const filename = "download-mod-1_1.1.0.zip"; assert(modStore.files.has(filename), `Mod ${filename} should be in the store`); - assert(await fs.pathExists(path.join(MODS_DIR, filename)), `File ${filename} should exist`); + await assert.doesNotReject(fs.access(path.join(MODS_DIR, filename)), `File ${filename} should exist`); const modInfo = modStore.files.get(filename); assert(modInfo instanceof ModInfo); assert.equal(modInfo.name, "download-mod-1"); @@ -290,8 +293,8 @@ describe("lib/ModStore", function () { assert.equal(modStore.files.size, 2); assert(modStore.files.has("multi-mod-a_1.0.0.zip")); assert(modStore.files.has("multi-mod-b_2.0.0.zip")); - assert(await fs.pathExists(path.join(MODS_DIR, "multi-mod-a_1.0.0.zip"))); - assert(await fs.pathExists(path.join(MODS_DIR, "multi-mod-b_2.0.0.zip"))); + await fs.access(path.join(MODS_DIR, "multi-mod-a_1.0.0.zip")); + await fs.access(path.join(MODS_DIR, "multi-mod-b_2.0.0.zip")); }); it("should throw when no releases available", async function () { const modsToDownload = [{ name: "download-mod-1", version: ModVersionEquality.fromString("1.1.0") }]; @@ -803,12 +806,12 @@ describe("lib/ModStore", function () { describe("deleteFile", function () { it("should delete an existing file from store and filesystem", async function () { assert(modStore.files.has(testMod1Filename), "Mod should exist before deletion"); - assert(await fs.pathExists(testMod1Path), "Mod file should exist before deletion"); + await assert.doesNotReject(fs.access(testMod1Path), "Mod file should exist before deletion"); await modStore.deleteFile(testMod1Filename); assert(!modStore.files.has(testMod1Filename), "Mod should not exist in store after deletion"); - assert(!(await fs.pathExists(testMod1Path)), "Mod file should not exist after deletion"); + await assert.rejects(fs.access(testMod1Path), "Mod file should not exist after deletion"); }); it("should emit 'change' event with isDeleted flag when deleting", async function () { diff --git a/test/lib/config/classes.js b/test/lib/config/classes.js index 6db7014e..d4468d1d 100644 --- a/test/lib/config/classes.js +++ b/test/lib/config/classes.js @@ -1,7 +1,6 @@ "use strict"; const lib = require("@clusterio/lib"); -const { optional } = require("@clusterio/lib/dist/node/src/config/validators"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); const assert = require("assert").strict; const CA = lib.ConfigAccess; @@ -289,10 +288,10 @@ describe("lib/config/classes", function() { await assert.rejects(TestConfig.fromFile("local", filepath)); }); it("should load defaults for missing fields", async function() { - await fs.writeJSON(filepath, { + await fs.writeFile(filepath, JSON.stringify({ "alpha.foo": "a", "test.enum": "a", - }); + })); const testInstance = await TestConfig.fromFile("local", filepath); assert.equal(testInstance.get("alpha.foo"), "a"); assert.deepEqual(testInstance.get("beta.bar"), {}); @@ -302,14 +301,14 @@ describe("lib/config/classes", function() { assert.equal(testInstance.filepath, filepath); }); it("should load fields", async function() { - await fs.writeJSON(filepath, { + await fs.writeFile(filepath, JSON.stringify({ "test.enum": "c", "test.test": "blah", "test.func": 22, "test.bool": null, "test.json": { valid: true }, "test.priv": "bar", - }); + })); const testInstance = await TestConfig.fromFile("local", filepath); assert.equal(testInstance.get("test.enum"), "c"); assert.equal(testInstance.get("test.test"), "blah"); @@ -335,13 +334,13 @@ describe("lib/config/classes", function() { "test.beta": "decay", "test.gamma": 99, }; - await fs.writeJson(filepath, testFields); + await fs.writeFile(filepath, JSON.stringify(testFields)); const testInstance = await TestConfig.fromFile("local", filepath); assert.deepEqual(testInstance.toJSON(), testFields); assert.equal(testInstance.filepath, filepath); }); it("should ignore inaccessible fields", async function() { - await fs.writeJSON(filepath, { "test.priv": "bad" }); + await fs.writeFile(filepath, JSON.stringify({ "test.priv": "bad" })); const testInstance = await TestConfig.fromFile("remote", filepath); assert.equal(testInstance.fields["test.priv"], undefined); assert.equal(testInstance.fields["test.func"], 42); @@ -352,8 +351,8 @@ describe("lib/config/classes", function() { describe(".save()", function() { const filepath = path.join("temp", "test", "config_test.json"); beforeEach(async function() { - await fs.remove(filepath); - assert(!await fs.exists(filepath), "File was not removed"); + await fs.rm(filepath, { force: true }); + await assert.rejects(fs.access(filepath), "File was not removed"); }); it("should throw if there is no filepath", async function() { const testInstance = new TestConfig("local", { "alpha.foo": "a" }); @@ -366,24 +365,24 @@ describe("lib/config/classes", function() { it("should do nothing when not dirty", async function() { const testInstance = new TestConfig("local", { "alpha.foo": "a" }, filepath); await testInstance.save(); - assert(!await fs.exists(filepath), "File was created"); + await assert.rejects(fs.access(filepath), "File was created"); }); it("should save data to file", async function() { const testInstance = new TestConfig("local", { "alpha.foo": "a" }, filepath); testInstance.set("alpha.foo", "b"); // Sets the dirty flag await testInstance.save(); - const json = await fs.readJSON(filepath); + const json = JSON.parse(await fs.readFile(filepath, "utf8")); assert.deepEqual(json, testInstance.toJSON()); }); it("should clear the dirty flag after saving", async function() { const testInstance = new TestConfig("local", { "alpha.foo": "a" }, filepath); testInstance.set("alpha.foo", "b"); // Sets the dirty flag await testInstance.save(); - assert(await fs.exists(filepath), "File was not created"); - await fs.remove(filepath); - assert(!await fs.exists(filepath), "File was not removed"); + await assert.doesNotReject(fs.access(filepath), "File was not created"); + await fs.rm(filepath); + await assert.rejects(fs.access(filepath), "File was was not removed"); await testInstance.save(); - assert(!await fs.exists(filepath), "File was created"); + await assert.rejects(fs.access(filepath), "File was created"); }); it("should be round trip savable", async function() { const testInstance = new TestConfig("local", { diff --git a/test/lib/data/ModInfo.js b/test/lib/data/ModInfo.js index 8c918c17..590068cc 100644 --- a/test/lib/data/ModInfo.js +++ b/test/lib/data/ModInfo.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); const lib = require("@clusterio/lib"); diff --git a/test/lib/database.js b/test/lib/database.js index eccbe5be..4f01c942 100644 --- a/test/lib/database.js +++ b/test/lib/database.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); const lib = require("@clusterio/lib"); diff --git a/test/lib/datastore.js b/test/lib/datastore.js index ba2cf116..40a97ead 100644 --- a/test/lib/datastore.js +++ b/test/lib/datastore.js @@ -2,7 +2,7 @@ const events = require("events"); const assert = require("assert").strict; const lib = require("@clusterio/lib"); -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); class MockDatastoreProvider extends lib.DatastoreProvider { @@ -37,7 +37,8 @@ describe("lib/datastore", function() { const baseDir = path.join("temp", "test", "datastore"); const testFiles = path.join("test", "file"); before(async function() { - await fs.emptyDir(baseDir); + await fs.rm(baseDir, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(baseDir, { recursive: true }); }); describe("class DatastoreProvider", function() { @@ -107,7 +108,7 @@ describe("lib/datastore", function() { }); beforeEach(async function() { - await fs.copy(path.join(testFiles, "json", "load_map.json"), dataPath); + await fs.copyFile(path.join(testFiles, "json", "load_map.json"), dataPath); datastoreProvider = new lib.JsonDatastoreProvider(dataPath); }); @@ -170,7 +171,7 @@ describe("lib/datastore", function() { assert.equal(loadedData.size, 0); }); it("throws for invalid file contents", async function() { - await fs.copy(path.join(testFiles, "json", "invalid.json"), dataPath); + await fs.copyFile(path.join(testFiles, "json", "invalid.json"), dataPath); await assert.rejects(() => datastoreProvider.load(dataPath)); }); it("writes a backup if size changes after load", async function() { @@ -201,7 +202,7 @@ describe("lib/datastore", function() { }); beforeEach(async function() { - await fs.copy(path.join(testFiles, "json", "load_array_map.json"), dataPath); + await fs.copyFile(path.join(testFiles, "json", "load_array_map.json"), dataPath); datastoreProvider = new lib.JsonIdDatastoreProvider(dataPath); }); @@ -265,7 +266,7 @@ describe("lib/datastore", function() { assert.equal(loadedData.size, 0); }); it("throws for invalid file contents", async function() { - await fs.copy(path.join(testFiles, "json", "invalid.json"), dataPath); + await fs.copyFile(path.join(testFiles, "json", "invalid.json"), dataPath); await assert.rejects(() => datastoreProvider.load(dataPath)); }); it("writes a backup if size changes after load", async function() { diff --git a/test/lib/external/FactorioVersions.test.js b/test/lib/external/FactorioVersions.test.js index c2e4f49a..dee4b41c 100644 --- a/test/lib/external/FactorioVersions.test.js +++ b/test/lib/external/FactorioVersions.test.js @@ -1,5 +1,5 @@ const assert = require("assert").strict; -const { readFile } = require("fs-extra"); +const { readFile } = require("node:fs/promises"); const { fetchFactorioVersions } = require("@clusterio/lib"); const { slowTest } = require("../../integration"); diff --git a/test/lib/file_ops.js b/test/lib/file_ops.js index a4e9048d..7e503326 100644 --- a/test/lib/file_ops.js +++ b/test/lib/file_ops.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const http = require("node:http"); const path = require("path"); const util = require("node:util"); @@ -10,20 +10,21 @@ const lib = require("@clusterio/lib"); describe("lib/file_ops", function() { let baseDir = path.join("temp", "test", "file_ops"); async function setupTestingEnv() { - await fs.ensureDir(path.join(baseDir, "test", "folder")); - await fs.ensureDir(path.join(baseDir, "test", "another folder")); - await fs.remove(path.join(baseDir, "safe")); - await fs.ensureDir(path.join(baseDir, "safe")); + await fs.mkdir(path.join(baseDir, "test", "folder"), { recursive: true }); + await fs.mkdir(path.join(baseDir, "test", "another folder"), { recursive: true }); + await fs.rm(path.join(baseDir, "safe"), { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(path.join(baseDir, "safe"), { recursive: true }); - await fs.outputFile(path.join(baseDir, "test", "file.txt"), "contents"); - await fs.outputFile(path.join(baseDir, "test", "another file.txt"), "more contents"); + await fs.writeFile(path.join(baseDir, "test", "file.txt"), "contents"); + await fs.writeFile(path.join(baseDir, "test", "another file.txt"), "more contents"); - await fs.outputFile(path.join(baseDir, "find", "file"), "contents"); - await fs.outputFile(path.join(baseDir, "find", "file.txt"), "contents"); - await fs.outputFile(path.join(baseDir, "find", "foo-1"), "contents"); - await fs.outputFile(path.join(baseDir, "find", "foo-2"), "contents"); - await fs.outputFile(path.join(baseDir, "find", "bar-1.txt"), "contents"); - await fs.outputFile(path.join(baseDir, "find", "bar-2.txt"), "contents"); + await fs.mkdir(path.join(baseDir, "find"), { recursive: true }); + await fs.writeFile(path.join(baseDir, "find", "file"), "contents"); + await fs.writeFile(path.join(baseDir, "find", "file.txt"), "contents"); + await fs.writeFile(path.join(baseDir, "find", "foo-1"), "contents"); + await fs.writeFile(path.join(baseDir, "find", "foo-2"), "contents"); + await fs.writeFile(path.join(baseDir, "find", "bar-1.txt"), "contents"); + await fs.writeFile(path.join(baseDir, "find", "bar-2.txt"), "contents"); } before(setupTestingEnv); @@ -90,14 +91,14 @@ describe("lib/file_ops", function() { it("should write new target file", async function() { let target = path.join(baseDir, "safe", "simple.txt"); await lib.safeOutputFile(target, "a text file", "utf8"); - assert(!await fs.pathExists(target.replace(".txt", ".tmp.txt")), "temporary was left behind"); + await assert.rejects(fs.access(target.replace(".txt", ".tmp.txt")), "temporary was left behind"); assert.equal(await fs.readFile(target, "utf8"), "a text file"); }); it("should overwrite existing target file", async function() { let target = path.join(baseDir, "safe", "exists.txt"); - await fs.outputFile(target, "previous", "utf8"); + await fs.writeFile(target, "previous", "utf8"); await lib.safeOutputFile(target, "current", "utf8"); - assert(!await fs.pathExists(target.replace(".txt", ".tmp.txt")), "temporary was left behind"); + await assert.rejects(fs.access(target.replace(".txt", ".tmp.txt")), "temporary was left behind"); assert.equal(await fs.readFile(target, "utf8"), "current"); }); it("should handle creating file in current working directory", async function() { @@ -228,7 +229,8 @@ describe("lib/file_ops", function() { let server; before(async function () { - await fs.emptyDir(downloadDir); + await fs.rm(downloadDir, { force: true, recursive: true, maxRetries: 10 }); + await fs.mkdir(downloadDir, { recursive: true }); server = http.createServer(); server.unref(); server.on("request", (req, res) => { @@ -303,7 +305,7 @@ describe("lib/file_ops", function() { it("should throw if file exists and overwriteMode is error", async function () { const downloadPath = path.join(downloadDir, "exists.txt"); - await fs.outputFile(downloadPath, ""); + await fs.writeFile(downloadPath, ""); await assert.rejects( lib.downloadFile(new URL("/simple-file", baseUrl), downloadPath, "error"), { code: "EEXIST" }, @@ -322,7 +324,7 @@ describe("lib/file_ops", function() { it("should write a new file if it exists and overwriteMode is rename", async function () { const downloadPath = path.join(downloadDir, "exists.txt"); - await fs.outputFile(downloadPath, ""); + await fs.writeFile(downloadPath, ""); const newDownloadPath = await lib.downloadFile(new URL("/simple-file", baseUrl), downloadPath, "rename"); assert.equal(newDownloadPath, path.join(downloadDir, "exists-2.txt")); const writtenContent = await fs.readFile(newDownloadPath, "utf8"); @@ -336,9 +338,9 @@ describe("lib/file_ops", function() { path.join(downloadDir, "contended-3.txt"), path.join(downloadDir, "contended-4.txt"), ]; - await fs.outputFile(downloadPath, ""); + await fs.writeFile(downloadPath, ""); for (const additionalPath of additionalPaths) { - await fs.outputFile(additionalPath, ""); + await fs.writeFile(additionalPath, ""); } const newDownloadPath = await lib.downloadFile(new URL("/simple-file", baseUrl), downloadPath, "rename"); assert.equal(newDownloadPath, path.join(downloadDir, "contended-5.txt")); @@ -348,7 +350,7 @@ describe("lib/file_ops", function() { it("should overwrite existing file if overwriteMode is overwrite", async function () { const downloadPath = path.join(downloadDir, "overwrite.txt"); - await fs.outputFile(downloadPath, "original content"); + await fs.writeFile(downloadPath, "original content"); const newDownloadPath = await lib.downloadFile(new URL("/simple-file", baseUrl), downloadPath, "overwrite"); assert.equal(newDownloadPath, downloadPath); const writtenContent = await fs.readFile(newDownloadPath, "utf8"); @@ -357,7 +359,7 @@ describe("lib/file_ops", function() { it("should not overwrite existing temp files", async function () { const tempPath = path.join(downloadDir, "temp-exists.tmp.txt"); - await fs.outputFile(tempPath, "original content"); + await fs.writeFile(tempPath, "original content"); const downloadPath = path.join(downloadDir, "temp-exists.txt"); await lib.downloadFile(new URL("/simple-file", baseUrl), downloadPath, "overwrite"); const writtenContent = await fs.readFile(tempPath, "utf8"); @@ -377,7 +379,7 @@ describe("lib/file_ops", function() { const downloadSubdir = path.join(downloadDir, "subdir"); const downloadPath = path.join(downloadSubdir, "test.txt"); - await fs.ensureDir(downloadSubdir); + await fs.mkdir(downloadSubdir, { recursive: true }); const [stage1, waitForStage1] = stageWaiter(); const [stage2, waitForStage2] = stageWaiter(); @@ -391,7 +393,7 @@ describe("lib/file_ops", function() { download.catch(() => {}); await waitForStage1; - await fs.remove(downloadSubdir); + await fs.rm(downloadSubdir, { recursive: true, maxRetries: 10 }); stage2(); await assert.rejects( diff --git a/test/lib/hash.js b/test/lib/hash.js index adb9f642..726c45eb 100644 --- a/test/lib/hash.js +++ b/test/lib/hash.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs"); +const fs = require("node:fs/promises"); const path = require("path"); const lib = require("@clusterio/lib"); @@ -9,7 +9,7 @@ const lib = require("@clusterio/lib"); describe("lib/hash", function() { describe("hashStream()", function() { it("should hash a stream of bytes", async function() { - let stream = fs.createReadStream(path.join("test", "file", "hash.txt")); + let stream = (await fs.open(path.join("test", "file", "hash.txt"))).createReadStream(); let result = await lib.hashStream(stream); assert.equal(result, "be417768b5c3c5c1d9bcb2e7c119196dd76b5570"); }); diff --git a/test/lib/ini.js b/test/lib/ini.js index 6b0d9697..b600df7c 100644 --- a/test/lib/ini.js +++ b/test/lib/ini.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const lib = require("@clusterio/lib"); describe("lib/ini", function() { diff --git a/test/lib/logging_utils.js b/test/lib/logging_utils.js index 630691d0..919714e7 100644 --- a/test/lib/logging_utils.js +++ b/test/lib/logging_utils.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); const { testLines } = require("./factorio/lines"); @@ -47,7 +47,7 @@ describe("lib/logging_utils.js", function() { }); it("should handle loading broken index", async function() { let tempDir = path.join("temp", "test", "log"); - await fs.outputFile(path.join(tempDir, "index.json"), "Broken JSON"); + await fs.writeFile(path.join(tempDir, "index.json"), "Broken JSON"); let logIndex = await lib.LogIndex.load(tempDir); assert.deepEqual(logIndex.index, new Map()); }); @@ -60,7 +60,7 @@ describe("lib/logging_utils.js", function() { if (file === "excluded.txt") { continue; } - let fileStream = fs.createReadStream(path.join(logDir, file)); + let fileStream = (await fs.open(path.join(logDir, file))).createReadStream(); let lineStream = new lib.LineSplitter({ readableObjectMode: true }); fileStream.pipe(lineStream); for await (let line of lineStream) { diff --git a/test/lib/plugin_loader.js b/test/lib/plugin_loader.js index f8e275e7..da6768d7 100644 --- a/test/lib/plugin_loader.js +++ b/test/lib/plugin_loader.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); const lib = require("@clusterio/lib"); @@ -16,11 +16,12 @@ describe("lib/plugin_loader", function() { let invalidPlugin = path.join(baseDir, "invalid_plugin"); before(async function() { async function writePlugin(pluginPath, infoName) { - await fs.outputFile( + await fs.mkdir(pluginPath, { recursive: true }); + await fs.writeFile( path.join(pluginPath, "index.js"), `module.exports.plugin = { name: "${infoName}" };` ); - await fs.outputFile( + await fs.writeFile( path.join(pluginPath, "package.json"), '{ "version": "0.0.1" }' ); @@ -28,7 +29,7 @@ describe("lib/plugin_loader", function() { await writePlugin(testPlugin, "test"); await writePlugin(brokenPlugin, "broken"); - await fs.outputFile(path.join(brokenPlugin, "index.js"), "Syntax Error"); + await fs.writeFile(path.join(brokenPlugin, "index.js"), "Syntax Error"); await writePlugin(invalidPlugin, "wrong"); }); @@ -70,7 +71,8 @@ describe("lib/plugin_loader", function() { let wrongParentClass = path.join(baseDir, "wrong_parent_class_plugin"); before(async function() { async function writeEntrypoint(pluginPath, content) { - await fs.outputFile(path.join(pluginPath, "controller.js"), content); + await fs.mkdir(pluginPath, { recursive: true }); + await fs.writeFile(path.join(pluginPath, "controller.js"), content); } await writeEntrypoint(missingClass, ""); @@ -122,16 +124,18 @@ describe("lib/plugin_loader", function() { const npmPluginPath = path.join(baseDir, "node_modules", "npm-plugin"); const monorepoPluginPath = path.join(baseDir, "external_plugins", "monorepo", "monorepo-plugin"); const monorepoPluginPathAbs = path.resolve(monorepoPluginPath); + const notAPluginPath = path.join(baseDir, "node_modules", "not-a-plugin"); let pluginList; before(async function() { // Setup test plugins async function writePlugin(pluginPath, name) { - await fs.outputFile( + await fs.mkdir(pluginPath, { recursive: true }); + await fs.writeFile( path.join(pluginPath, "index.js"), `module.exports.plugin = { name: "${name}" };` ); - await fs.outputFile( + await fs.writeFile( path.join(pluginPath, "package.json"), JSON.stringify({ name: path.basename(pluginPath), @@ -150,7 +154,7 @@ describe("lib/plugin_loader", function() { // Write a monorepo plugin await writePlugin(monorepoPluginPath, "monorepo-plugin"); // Create root package.json - await fs.outputFile( + await fs.writeFile( path.join(baseDir, "package.json"), JSON.stringify({ dependencies: { @@ -160,8 +164,9 @@ describe("lib/plugin_loader", function() { }) ); // Create an npm module that is not a plugin - await fs.outputFile( - path.join(baseDir, "node_modules", "not-a-plugin", "package.json"), + await fs.mkdir(notAPluginPath, { recursive: true }); + await fs.writeFile( + path.join(notAPluginPath, "package.json"), JSON.stringify({ name: "not-a-plugin", version: "1.0.0" }) ); @@ -169,7 +174,7 @@ describe("lib/plugin_loader", function() { pluginList = await lib.loadPluginList(pluginListPath, true); }); beforeEach(async function() { - await fs.remove(pluginListPath); + await fs.rm(pluginListPath, { force: true }); }); it("should discover local plugins", async function() { @@ -188,7 +193,7 @@ describe("lib/plugin_loader", function() { it("should load existing plugin list", async function() { const existingList = new Map([["test", "/test/path"]]); - await fs.outputFile(pluginListPath, JSON.stringify([...existingList])); + await fs.writeFile(pluginListPath, JSON.stringify([...existingList])); const loadedPlugins = await lib.loadPluginList(pluginListPath, false); assert.ok(loadedPlugins.has("test")); assert.strictEqual(loadedPlugins.get("test"), "/test/path"); @@ -201,7 +206,7 @@ describe("lib/plugin_loader", function() { it("should not throw when package.json has no dependencies field", async function() { // Create a package.json without dependencies - await fs.outputFile( + await fs.writeFile( path.join("package.json"), JSON.stringify({ name: "test-package", @@ -217,7 +222,7 @@ describe("lib/plugin_loader", function() { after(async function() { process.chdir(old_cwd); - await fs.remove(baseDir); + await fs.rm(baseDir, { force: true, recursive: true, maxRetries: 10 }); }); }); }); diff --git a/test/lib/stream.js b/test/lib/stream.js index f1ec849b..144e7c06 100644 --- a/test/lib/stream.js +++ b/test/lib/stream.js @@ -1,6 +1,6 @@ "use strict"; const assert = require("assert").strict; -const fs = require("fs-extra"); +const fs = require("node:fs/promises"); const path = require("path"); const stream = require("stream"); const util = require("util"); @@ -109,7 +109,7 @@ describe("lib/stream", function() { it("should read chunks in a file in reverse", async function() { let content = ""; for (let i = 0; i < 10; i++) { content += String(i).repeat(10); } - await fs.outputFile(path.join("temp", "test", "reverse.txt"), content); + await fs.writeFile(path.join("temp", "test", "reverse.txt"), content); let reverseStream = await lib.createReverseReadStream( path.join("temp", "test", "reverse.txt"), { encoding: "utf8", highWaterMark: 10 } @@ -123,7 +123,7 @@ describe("lib/stream", function() { }); it("should reverse the lines of a file", async function() { - let ws = fs.createWriteStream(path.join("temp", "test", "reverse.txt")); + let ws = (await fs.open(path.join("temp", "test", "reverse.txt"), "w")).createWriteStream(); for (let i = 1; i < 100000; i++) { ws.write(Buffer.from(`${i}\n`)); }