@elsikora/setup-wizard
Version:
Setup Wizard - CLI scaffolding utility
327 lines (324 loc) • 14.3 kB
JavaScript
import { EPackageJsonDependencyType } from '../../domain/enum/package-json-dependency-type.enum.js';
import { EPackageJsonDependencyVersionFlag } from '../../domain/enum/package-json-dependency-version-flag.enum.js';
import { PACKAGE_JSON_FILE_PATH } from '../constant/package-json-file-path.constant.js';
/**
* Service for managing package.json operations.
* Handles reading, writing, and manipulating package.json file contents.
*/
class PackageJsonService {
fileSystemService;
commandService;
/** Command service for executing npm commands */
COMMAND_SERVICE;
/** File system service for reading and writing files */
FILE_SYSTEM_SERVICE;
/**
* Initializes a new instance of the PackageJsonService.
* @param fileSystemService - Service for file system operations
* @param commandService - Service for executing commands
*/
constructor(fileSystemService, commandService) {
this.fileSystemService = fileSystemService;
this.commandService = commandService;
this.FILE_SYSTEM_SERVICE = fileSystemService;
this.COMMAND_SERVICE = commandService;
}
/**
* Adds a dependency to the package.json file.
* @param name - The name of the dependency
* @param version - The version string of the dependency
* @param type - The type of dependency (prod, dev, peer, etc.), defaults to PROD
* @returns Promise that resolves when the dependency is added
*/
async addDependency(name, version, type = EPackageJsonDependencyType.PROD) {
const packageJson = await this.get();
if (type === EPackageJsonDependencyType.ANY) {
packageJson.dependencies = packageJson.dependencies ?? {};
packageJson.dependencies[name] = version;
}
else {
packageJson[type] = packageJson[type] ?? {};
packageJson[type][name] = version;
}
await this.set(packageJson);
}
/**
* Adds a script to the package.json file.
* @param name - The name of the script
* @param command - The command to execute for the script
* @returns Promise that resolves when the script is added
*/
async addScript(name, command) {
const packageJson = await this.get();
packageJson.scripts = packageJson.scripts ?? {};
packageJson.scripts[name] = command;
await this.set(packageJson);
}
/**
* Checks if the package.json file exists.
* @returns Promise that resolves to true if the file exists, false otherwise
*/
async exists() {
return this.fileSystemService.isPathExists(PACKAGE_JSON_FILE_PATH);
}
/**
* Gets the contents of the package.json file.
* @returns Promise that resolves to the parsed package.json contents
*/
async get() {
const content = await this.fileSystemService.readFile(PACKAGE_JSON_FILE_PATH);
return JSON.parse(content);
}
/**
* Gets the dependencies of a specified type from the package.json file.
* @param type - The type of dependencies to get, defaults to PROD
* @returns Promise that resolves to a record of dependency names and versions
*/
async getDependencies(type = EPackageJsonDependencyType.PROD) {
const packageJson = await this.get();
return type === EPackageJsonDependencyType.ANY
? {
...packageJson.dependencies,
...packageJson.devDependencies,
...packageJson.peerDependencies,
...packageJson.optionalDependencies,
}
: (packageJson[type] ?? {});
}
/**
* Gets detailed version information for an installed dependency.
* Parses the version string to extract version parts and flags.
* @param name - The name of the dependency
* @param type - The type of dependency to check, defaults to ANY
* @returns Promise that resolves to detailed version information or undefined if not found
*/
async getInstalledDependencyVersion(name, type = EPackageJsonDependencyType.ANY) {
const packageJson = await this.get();
const versionString = type === EPackageJsonDependencyType.ANY ? (packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name] ?? packageJson.peerDependencies?.[name] ?? packageJson.optionalDependencies?.[name]) : packageJson[type]?.[name];
if (!versionString)
return undefined;
let flag = EPackageJsonDependencyVersionFlag.ANY;
let version = versionString;
if (versionString.startsWith(">=")) {
flag = EPackageJsonDependencyVersionFlag.GREATER_THAN_OR_EQUAL;
// eslint-disable-next-line @elsikora/typescript/no-magic-numbers
version = versionString.slice(2);
}
else if (versionString.startsWith(">")) {
flag = EPackageJsonDependencyVersionFlag.GREATER_THAN;
version = versionString.slice(1);
}
else if (versionString.startsWith("<=")) {
flag = EPackageJsonDependencyVersionFlag.LESS_THAN_OR_EQUAL;
// eslint-disable-next-line @elsikora/typescript/no-magic-numbers
version = versionString.slice(2);
}
else if (versionString.startsWith("<")) {
flag = EPackageJsonDependencyVersionFlag.LESS_THAN;
version = versionString.slice(1);
}
else if (versionString.startsWith("=")) {
flag = EPackageJsonDependencyVersionFlag.EXACT;
version = versionString.slice(1);
}
else if (versionString.startsWith("~")) {
flag = EPackageJsonDependencyVersionFlag.TILDE;
version = versionString.slice(1);
}
else if (versionString.startsWith("^")) {
flag = EPackageJsonDependencyVersionFlag.CARET;
version = versionString.slice(1);
}
const cleanVersion = version.trim();
let versionOnly = cleanVersion;
let prereleaseChannel;
let isPrerelease = false;
const prereleaseMatch = /[-+]([a-z0-9](?:[.-][a-z0-9])*)(?:\+[a-z0-9](?:[.-][a-z0-9])*)?$/i.exec(cleanVersion);
if (prereleaseMatch) {
isPrerelease = true;
prereleaseChannel = prereleaseMatch[1];
versionOnly = cleanVersion.split(/[-+]/)[0];
}
const versionParts = versionOnly.split(".").map((part) => Number.parseInt(part, 10));
const majorVersion = versionParts[0] || 0;
const minorVersion = versionParts[1] || 0;
// eslint-disable-next-line @elsikora/typescript/no-magic-numbers
const patchVersion = versionParts[2] || 0;
return {
flag,
isPrerelease,
majorVersion,
minorVersion,
patchVersion,
prereleaseChannel,
version: cleanVersion,
};
}
/**
* Gets a specific property from the package.json file.
* @param property - The property key to get
* @returns Promise that resolves to the property value
*/
async getProperty(property) {
const packageJson = await this.get();
return packageJson[property];
}
/**
* Installs packages using npm.
* @param packages - The package(s) to install (string or array of strings)
* @param version - Optional version to install (only works with single package)
* @param type - The type of dependency to install as, defaults to PROD
* @returns Promise that resolves when installation is complete
*/
async installPackages(packages, version, type = EPackageJsonDependencyType.PROD) {
const packageList = Array.isArray(packages) ? packages : [packages];
const typeFlag = this.getDependencyTypeFlag(type);
if (version && !Array.isArray(packages)) {
const packageWithVersion = `${packages}@${version}`;
await this.commandService.execute(`npm install ${typeFlag} ${packageWithVersion}`);
return;
}
const packageString = version ? packageList.map((packageName) => `${packageName}@${version}`).join(" ") : packageList.join(" ");
await this.commandService.execute(`npm install ${typeFlag} ${packageString}`);
}
/**
* Checks if a dependency exists in the package.json file.
* @param name - The name of the dependency to check
* @param type - The type of dependency to check, defaults to ANY
* @returns Promise that resolves to true if the dependency exists, false otherwise
*/
async isExistsDependency(name, type = EPackageJsonDependencyType.ANY) {
const packageJson = await this.get();
const dependencies = packageJson.dependencies ?? {};
const devDependencies = packageJson.devDependencies ?? {};
const peerDependencies = packageJson.peerDependencies ?? {};
const optionalDependencies = packageJson.optionalDependencies ?? {};
if (type === EPackageJsonDependencyType.ANY) {
return !!dependencies[name] || !!devDependencies[name] || !!peerDependencies[name] || !!optionalDependencies[name];
}
else {
return packageJson[type] ? !!packageJson[type][name] : false;
}
}
/**
* Merges partial package.json data with the existing file.
* @param partial - The partial package.json data to merge
* @returns Promise that resolves when the merge is complete
*/
async merge(partial) {
const packageJson = await this.get();
const merged = { ...packageJson, ...partial };
await this.set(merged);
}
/**
* Removes a dependency from the package.json file.
* @param name - The name of the dependency to remove
* @param type - The type of dependency to remove, defaults to PROD
* @returns Promise that resolves when the dependency is removed
*/
async removeDependency(name, type = EPackageJsonDependencyType.PROD) {
const packageJson = await this.get();
let wasModified = false;
if (type === EPackageJsonDependencyType.ANY) {
if (packageJson.dependencies?.[name]) {
// eslint-disable-next-line @elsikora/typescript/no-dynamic-delete
delete packageJson.dependencies[name];
wasModified = true;
}
if (packageJson.devDependencies?.[name]) {
// eslint-disable-next-line @elsikora/typescript/no-dynamic-delete
delete packageJson.devDependencies[name];
wasModified = true;
}
if (packageJson.peerDependencies?.[name]) {
// eslint-disable-next-line @elsikora/typescript/no-dynamic-delete
delete packageJson.peerDependencies[name];
wasModified = true;
}
if (packageJson.optionalDependencies?.[name]) {
// eslint-disable-next-line @elsikora/typescript/no-dynamic-delete
delete packageJson.optionalDependencies[name];
wasModified = true;
}
if (wasModified) {
await this.set(packageJson);
}
}
else if (packageJson[type]?.[name]) {
// eslint-disable-next-line @elsikora/typescript/no-dynamic-delete
delete packageJson[type][name];
await this.set(packageJson);
}
}
/**
* Removes a script from the package.json file.
* @param name - The name of the script to remove
* @returns Promise that resolves when the script is removed
*/
async removeScript(name) {
const packageJson = await this.get();
if (packageJson.scripts?.[name]) {
// eslint-disable-next-line @elsikora/typescript/no-dynamic-delete
delete packageJson.scripts[name];
await this.set(packageJson);
}
}
/**
* Writes the package.json file with the provided content.
* @param packageJson - The package.json content to write
* @returns Promise that resolves when the file is written
*/
async set(packageJson) {
// eslint-disable-next-line @elsikora/typescript/no-magic-numbers
await this.fileSystemService.writeFile(PACKAGE_JSON_FILE_PATH, JSON.stringify(packageJson, null, 2));
}
/**
* Sets a specific property in the package.json file.
* @param property - The property key to set
* @param value - The value to set for the property
* @returns Promise that resolves when the property is set
*/
async setProperty(property, value) {
const packageJson = await this.get();
packageJson[property] = value;
await this.set(packageJson);
}
/**
* Uninstalls packages using npm.
* @param packages - The package(s) to uninstall (string or array of strings)
* @returns Promise that resolves when uninstallation is complete
*/
async uninstallPackages(packages) {
const packageList = Array.isArray(packages) ? packages : [packages];
const packageString = packageList.join(" ");
await this.commandService.execute(`npm uninstall ${packageString}`);
}
/**
* Validates the package.json file for required fields.
* @returns Promise that resolves to an array of missing field names
*/
async validate() {
const packageJson = await this.get();
const requiredFields = ["name", "version"];
const missingFields = requiredFields.filter((field) => !packageJson[field]);
return missingFields;
}
/**
* Gets the npm flag for a dependency type.
* @param type - The type of dependency
* @returns The corresponding npm flag string
*/
getDependencyTypeFlag(type) {
const flags = {
[EPackageJsonDependencyType.ANY]: "--save",
[EPackageJsonDependencyType.DEV]: "--save-dev",
[EPackageJsonDependencyType.OPTIONAL]: "--save-optional",
[EPackageJsonDependencyType.PEER]: "--save-peer",
[EPackageJsonDependencyType.PROD]: "--save",
};
return flags[type];
}
}
export { PackageJsonService };
//# sourceMappingURL=package-json.service.js.map