@metamask/utils
Version:
Various JavaScript/TypeScript utilities of wide relevance to the MetaMask codebase
210 lines • 7.59 kB
JavaScript
// This file is intended to be used only in a Node.js context.
/* eslint-disable import/no-nodejs-modules */
import fs from "fs";
import os from "os";
import path from "path";
import * as uuid from "uuid";
import { isErrorWithCode, wrapError } from "./errors.mjs";
/**
* Read the file at the given path, assuming its content is encoded as UTF-8.
*
* @param filePath - The path to the file.
* @returns The content of the file.
* @throws An error with a stack trace if reading fails in any way.
*/
export async function readFile(filePath) {
try {
return await fs.promises.readFile(filePath, 'utf8');
}
catch (error) {
throw wrapError(error, `Could not read file '${filePath}'`);
}
}
/**
* Write content to the file at the given path, creating the directory structure
* for the file automatically if necessary.
*
* @param filePath - The path to the file.
* @param content - The new content of the file.
* @throws An error with a stack trace if writing fails in any way.
*/
export async function writeFile(filePath, content) {
try {
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
await fs.promises.writeFile(filePath, content);
}
catch (error) {
throw wrapError(error, `Could not write file '${filePath}'`);
}
}
/**
* Read the assumed JSON file at the given path, attempts to parse it, and
* get the resulting object. Supports a custom parser (in case you want to
* use the [JSON5](https://www.npmjs.com/package/json5) package instead).
*
* @param filePath - The path segments pointing to the JSON file. Will be passed
* to path.join().
* @param options - Options to this function.
* @param options.parser - The parser object to use. Defaults to `JSON`.
* @param options.parser.parse - A function that parses JSON data.
* @returns The object corresponding to the parsed JSON file, typed against the
* struct.
* @throws An error with a stack trace if reading fails in any way, or if the
* parsed value is not a plain object.
*/
export async function readJsonFile(filePath, { parser = JSON, } = {}) {
try {
const content = await fs.promises.readFile(filePath, 'utf8');
return parser.parse(content);
}
catch (error) {
throw wrapError(error, `Could not read JSON file '${filePath}'`);
}
}
/**
* Attempt to write the given JSON-like value to the file at the given path,
* creating the directory structure for the file automatically if necessary.
* Adds a newline to the end of the file. Supports a custom parser (in case you
* want to use the [JSON5](https://www.npmjs.com/package/json5) package
* instead).
*
* @param filePath - The path to write the JSON file to, including the file
* itself.
* @param jsonValue - The JSON-like value to write to the file. Make sure that
* JSON.stringify can handle it.
* @param options - The options to this function.
* @param options.prettify - Whether to format the JSON as it is turned into a
* string such that it is broken up into separate lines (using 2 spaces as
* indentation).
* @param options.stringifier - The stringifier to use. Defaults to `JSON`.
* @param options.stringifier.stringify - A function that stringifies JSON.
* @returns The object corresponding to the parsed JSON file, typed against the
* struct.
* @throws An error with a stack trace if writing fails in any way.
*/
export async function writeJsonFile(filePath, jsonValue, { stringifier = JSON, prettify = false, } = {}) {
try {
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
const json = prettify
? stringifier.stringify(jsonValue, null, ' ')
: stringifier.stringify(jsonValue);
await fs.promises.writeFile(filePath, json);
}
catch (error) {
throw wrapError(error, `Could not write JSON file '${filePath}'`);
}
}
/**
* Test the given path to determine whether it represents a file.
*
* @param filePath - The path to a (supposed) file on the filesystem.
* @returns A promise for true if the file exists or false otherwise.
* @throws An error with a stack trace if reading fails in any way.
*/
export async function fileExists(filePath) {
try {
const stats = await fs.promises.stat(filePath);
return stats.isFile();
}
catch (error) {
if (isErrorWithCode(error) && error.code === 'ENOENT') {
return false;
}
throw wrapError(error, `Could not determine if file exists '${filePath}'`);
}
}
/**
* Test the given path to determine whether it represents a directory.
*
* @param directoryPath - The path to a (supposed) directory on the filesystem.
* @returns A promise for true if the file exists or false otherwise.
* @throws An error with a stack trace if reading fails in any way.
*/
export async function directoryExists(directoryPath) {
try {
const stats = await fs.promises.stat(directoryPath);
return stats.isDirectory();
}
catch (error) {
if (isErrorWithCode(error) && error.code === 'ENOENT') {
return false;
}
throw wrapError(error, `Could not determine if directory exists '${directoryPath}'`);
}
}
/**
* Create the given directory along with any directories leading up to the
* directory, or do nothing if the directory already exists.
*
* @param directoryPath - The path to the desired directory.
* @throws An error with a stack trace if reading fails in any way.
*/
export async function ensureDirectoryStructureExists(directoryPath) {
try {
await fs.promises.mkdir(directoryPath, { recursive: true });
}
catch (error) {
throw wrapError(error, `Could not create directory structure '${directoryPath}'`);
}
}
/**
* Remove the given file or directory if it exists, or do nothing if it does
* not.
*
* @param entryPath - The path to the file or directory.
* @throws An error with a stack trace if removal fails in any way.
*/
export async function forceRemove(entryPath) {
try {
return await fs.promises.rm(entryPath, {
recursive: true,
force: true,
});
}
catch (error) {
throw wrapError(error, `Could not remove file or directory '${entryPath}'`);
}
}
/**
* Construct a sandbox object which can be used in tests that need temporary
* access to the filesystem.
*
* @param projectName - The name of the project.
* @returns The sandbox object. This contains a `withinSandbox` function which
* can be used in tests (see example).
* @example
* ```typescript
* const { withinSandbox } = createSandbox('utils');
*
* // ... later ...
*
* it('does something with the filesystem', async () => {
* await withinSandbox(async ({ directoryPath }) => {
* await fs.promises.writeFile(
* path.join(directoryPath, 'some-file'),
* 'some content',
* 'utf8'
* );
* })
* });
* ```
*/
export function createSandbox(projectName) {
const directoryPath = path.join(os.tmpdir(), projectName, uuid.v4());
return {
directoryPath,
async withinSandbox(test) {
if (await directoryExists(directoryPath)) {
throw new Error(`${directoryPath} already exists. Cannot continue.`);
}
await ensureDirectoryStructureExists(directoryPath);
try {
await test({ directoryPath });
}
finally {
await forceRemove(directoryPath);
}
},
};
}
//# sourceMappingURL=fs.mjs.map