/** * Helper functions for working with * [package.json](https://docs.npmjs.com/cli/v10/configuring-npm/package-json) files. * * @module */ import type { ReadonlyRecord } from "complete-common"; import { assertDefined, assertObject, assertString, isObject, } from "complete-common"; import type { DependencyType } from "../types/DependencyType.js"; import { getFilePath } from "./file.js"; import { readFile } from "./readWrite.js"; /** * Helper function to asynchronously get a "package.json" file as an object. * * @param filePathOrDirPath Either the path to a "package.json" file or the path to a directory * which contains a "package.json" file. If undefined is passed, the * current working directory will be used. * @throws If the "package.json" file cannot be found or is otherwise invalid. */ export async function getPackageJSON( filePathOrDirPath: string | undefined, ): Promise> { const filePath = await getFilePath("package.json", filePathOrDirPath); const packageJSONContents = await readFile(filePath); const packageJSON: unknown = JSON.parse(packageJSONContents); if (!isObject(packageJSON)) { throw new Error( `Failed to parse a "package.json" file at the following path: ${filePath}`, ); } return packageJSON; } /** * Helper function to asynchronously get the "dependencies" or "devDependencies" or * "peerDependencies" field from a "package.json" file. If the corresponding field does not exist, * `undefined` will be returned. * * @param filePathOrDirPathOrRecord Either the path to a "package.json" file, the path to a * directory which contains a "package.json" file, or a parsed * JavaScript object from a JSON file. If undefined is passed, the * current working directory will be used. * @param dependencyType Optional. The specific dependencies field to get. Defaults to * "dependencies". * @throws If the "package.json" file cannot be found or is otherwise invalid. */ export async function getPackageJSONDependencies( filePathOrDirPathOrRecord: string | ReadonlyRecord | undefined, dependencyType: DependencyType = "dependencies", ): Promise | undefined> { const packageJSON = typeof filePathOrDirPathOrRecord === "object" ? filePathOrDirPathOrRecord : await getPackageJSON(filePathOrDirPathOrRecord); const field = packageJSON[dependencyType]; if (field === undefined) { return undefined; } if (!isObject(field)) { throw new Error( typeof filePathOrDirPathOrRecord === "string" ? `Failed to parse the "${dependencyType}" field as an object in a "package.json" file: ${filePathOrDirPathOrRecord}` : `Failed to parse the "${dependencyType}" field as an object in a "package.json" file.`, ); } for (const [key, value] of Object.entries(field)) { assertString( value, typeof filePathOrDirPathOrRecord === "string" ? `Failed to parse the "${dependencyType}" --> "${key}" field as a string in a "package.json" file: ${filePathOrDirPathOrRecord}` : `Failed to parse the "${dependencyType}" --> "${key}" field as a string in a "package.json" file.`, ); } return field as Record; } /** * Helper function to asynchronously get an arbitrary string field from a "package.json" file. If * the field does not exist, `undefined` will be returned. * * @param filePathOrDirPathOrRecord Either the path to a "package.json" file, the path to a * directory which contains a "package.json" file, or a parsed * JavaScript object from a JSON file. If undefined is passed, the * current working directory will be used. * @param fieldName The name of the field to retrieve. * @throws If the "package.json" file cannot be found or the field is not a string. */ export async function getPackageJSONField( filePathOrDirPathOrRecord: string | ReadonlyRecord | undefined, fieldName: string, ): Promise { const packageJSON = typeof filePathOrDirPathOrRecord === "object" ? filePathOrDirPathOrRecord : await getPackageJSON(filePathOrDirPathOrRecord); const field = packageJSON[fieldName]; if (field === undefined) { return undefined; } // Assume that all fields are strings. For objects (like e.g., "dependencies"), other helper // functions should be used. assertString( field, typeof filePathOrDirPathOrRecord === "string" ? `Failed to parse the "${fieldName}" field as a string in a "package.json" file: ${filePathOrDirPathOrRecord}` : `Failed to parse the "${fieldName}" field as a string in a "package.json" file.`, ); return field; } /** * Helper function to asynchronously get an arbitrary string field from a "package.json" file. This * will throw an error if the "package.json" file cannot be found or the field does not exist or if * the field is not a string. * * @param filePathOrDirPathOrRecord Either the path to a "package.json" file, the path to a * directory which contains a "package.json" file, or a parsed * JavaScript object from a JSON file. If undefined is passed, the * current working directory will be used. * @param fieldName The name of the field to retrieve. */ export async function getPackageJSONFieldMandatory( filePathOrDirPathOrRecord: string | ReadonlyRecord | undefined, fieldName: string, ): Promise { const field = await getPackageJSONField(filePathOrDirPathOrRecord, fieldName); assertDefined( field, typeof filePathOrDirPathOrRecord === "string" ? `Failed to find the "${fieldName}" field in a "package.json" file: ${filePathOrDirPathOrRecord}` : `Failed to find the "${fieldName}" field in a "package.json" file.`, ); return field; } /** * Helper function to asynchronously get N arbitrary string fields from a "package.json" file. This * will throw an error if the "package.json" file cannot be found or any of the fields do not exist * or any of the fields are not strings. * * @param filePathOrDirPath Either the path to a "package.json" file or the path to a directory * which contains a "package.json" file. If undefined is passed, the * current working directory will be used. * @param fieldNames The names of the fields to retrieve. */ export async function getPackageJSONFieldsMandatory( filePathOrDirPath: string | undefined, ...fieldNames: readonly T[] ): Promise> { const packageJSON = await getPackageJSON(filePathOrDirPath); const fields: Partial> = {}; for (const fieldName of fieldNames) { // Since we already have the contents of the "package.json" file, nothing asynchronous is // actually happening in the `getPackageJSONField` function. // eslint-disable-next-line no-await-in-loop const field = await getPackageJSONField(packageJSON, fieldName); assertDefined( field, `Failed to find the "${fieldName}" field in a "package.json" file: ${filePathOrDirPath}`, ); fields[fieldName] = field; } return fields as Record; } /** * Helper function to asynchronously get the "scripts" field from a "package.json" file. If the * field does not exist, `undefined` will be returned. * * @param filePathOrDirPathOrRecord Either the path to a "package.json" file, the path to a * directory which contains a "package.json" file, or a parsed * JavaScript object from a JSON file. If undefined is passed, the * current working directory will be used. * @throws If the "package.json" file cannot be found or is otherwise invalid. */ export async function getPackageJSONScripts( filePathOrDirPathOrRecord: string | ReadonlyRecord | undefined, ): Promise | undefined> { const packageJSON = typeof filePathOrDirPathOrRecord === "object" ? filePathOrDirPathOrRecord : await getPackageJSON(filePathOrDirPathOrRecord); const { scripts } = packageJSON; if (scripts === undefined) { return undefined; } assertObject( scripts, typeof filePathOrDirPathOrRecord === "string" ? `Failed to parse the "scripts" field as an object in a "package.json" file: ${filePathOrDirPathOrRecord}` : 'Failed to parse the "scripts" field as an object in a "package.json" file.', ); for (const [key, value] of Object.entries(scripts)) { assertString( value, typeof filePathOrDirPathOrRecord === "string" ? `Failed to parse the "scripts" --> "${key}" field as a string in a "package.json" file: ${filePathOrDirPathOrRecord}.` : `Failed to parse the "scripts" --> "${key}" field as a string in a "package.json" file.`, ); } return scripts as Record; } /** * Helper function to asynchronously get the "version" field from a "package.json" file. This will * throw an error if the "package.json" file cannot be found or the "version" field does not exist * or the "version" field is not a string. * * If you want to allow for the "version" field to not exist, use the `getPackageJSONField` helper * function instead. * * @param filePathOrDirPathOrRecord Either the path to a "package.json" file, the path to a * directory which contains a "package.json" file, or a parsed * JavaScript object from a JSON file. If undefined is passed, the * current working directory will be used. */ export async function getPackageJSONVersion( filePathOrDirPathOrRecord: string | ReadonlyRecord | undefined, ): Promise { const version = await getPackageJSONField( filePathOrDirPathOrRecord, "version", ); assertDefined( version, typeof filePathOrDirPathOrRecord === "string" ? `Failed to find the "version" field in a "package.json" file: ${filePathOrDirPathOrRecord}` : 'Failed to find the "version" field in a "package.json" file.', ); return version; } /** @see https://bun.com/docs/pm/catalogs */ export async function packageJSONHasCatalog( filePathOrDirPathOrRecord: string | ReadonlyRecord | undefined, ): Promise { const packageJSON = typeof filePathOrDirPathOrRecord === "object" ? filePathOrDirPathOrRecord : await getPackageJSON(filePathOrDirPathOrRecord); const { workspaces } = packageJSON; if (!isObject(workspaces)) { return false; } const { catalog } = workspaces; return isObject(catalog); } /** * Helper function to asynchronously check for a "dependencies" or "devDependencies" or * "peerDependencies" field from a "package.json" file. * * @param filePathOrDirPathOrRecord Either the path to a "package.json" file, the path to a * directory which contains a "package.json" file, or a parsed * JavaScript object from a JSON file. If undefined is passed, the * current working directory will be used. * @param dependencyName The name of the dependency to check for. * @param dependencyType Optional. The specific dependencies field to get. Defaults to * "dependencies". * @throws If the "package.json" file cannot be found or is otherwise invalid. */ export async function packageJSONHasDependency( filePathOrDirPathOrRecord: string | ReadonlyRecord | undefined, dependencyName: string, dependencyType: DependencyType = "dependencies", ): Promise { const packageJSONDependencies = await getPackageJSONDependencies( filePathOrDirPathOrRecord, dependencyType, ); if (packageJSONDependencies === undefined) { return false; } const dependency = packageJSONDependencies[dependencyName]; return dependency !== undefined; } /** * Helper function to asynchronously check if a "package.json" file has a particular script. This * will throw an error if the "package.json" file cannot be found or is otherwise invalid. * * @param filePathOrDirPathOrRecord Either the path to a "package.json" file, the path to a * directory which contains a "package.json" file, or a parsed * JavaScript object from a JSON file. If undefined is passed, the * current working directory will be used. * @param scriptName The name of the script to check for. */ export async function packageJSONHasScript( filePathOrDirPathOrRecord: string | ReadonlyRecord | undefined, scriptName: string, ): Promise { const packageJSON = typeof filePathOrDirPathOrRecord === "object" ? filePathOrDirPathOrRecord : await getPackageJSON(filePathOrDirPathOrRecord); const scripts = await getPackageJSONScripts(packageJSON); if (scripts === undefined) { return false; } const script = scripts[scriptName]; return script !== undefined; }