@elsikora/setup-wizard
Version:
Setup Wizard - CLI scaffolding utility
343 lines (340 loc) • 17.2 kB
JavaScript
import { EModule } from '../../domain/enum/module.enum.js';
import { EPackageJsonDependencyType } from '../../domain/enum/package-json-dependency-type.enum.js';
import { NodeCommandService } from '../../infrastructure/service/node-command.service.js';
import { SEMANTIC_RELEASE_CONFIG_CORE_DEPENDENCIES } from '../constant/semantic-release-config-core-dependencies.constant.js';
import { SEMANTIC_RELEASE_CONFIG_FILE_NAME } from '../constant/semantic-release-config-file-name.constant.js';
import { SEMANTIC_RELEASE_CONFIG_FILE_NAMES } from '../constant/semantic-release-config-file-names.constant.js';
import { SEMANTIC_RELEASE_CONFIG } from '../constant/semantic-release-config.constant.js';
import { PackageJsonService } from './package-json.service.js';
/**
* Service for setting up and managing semantic-release configuration.
* Provides functionality to automate version management and package publishing
* based on commit messages following conventional commits standard.
*/
class SemanticReleaseModuleService {
/** CLI interface service for user interaction */
CLI_INTERFACE_SERVICE;
/** Command service for executing shell commands */
COMMAND_SERVICE;
/** Configuration service for managing app configuration */
CONFIG_SERVICE;
/** File system service for file operations */
FILE_SYSTEM_SERVICE;
/** Service for managing package.json */
PACKAGE_JSON_SERVICE;
/** Cached semantic-release configuration */
config = null;
/**
* Initializes a new instance of the SemanticReleaseModuleService.
* @param cliInterfaceService - Service for CLI user interactions
* @param fileSystemService - Service for file system operations
* @param configService - Service for managing app configuration
*/
constructor(cliInterfaceService, fileSystemService, configService) {
this.CLI_INTERFACE_SERVICE = cliInterfaceService;
this.FILE_SYSTEM_SERVICE = fileSystemService;
this.COMMAND_SERVICE = new NodeCommandService(cliInterfaceService);
this.PACKAGE_JSON_SERVICE = new PackageJsonService(fileSystemService, this.COMMAND_SERVICE);
this.CONFIG_SERVICE = configService;
}
/**
* Handles existing semantic-release setup.
* Checks for existing configuration files and asks if user wants to remove them.
* @returns Promise resolving to true if setup should proceed, false otherwise
*/
async handleExistingSetup() {
const existingFiles = await this.findExistingConfigFiles();
if (existingFiles.length > 0) {
const messageLines = ["Existing Semantic Release configuration files detected:"];
messageLines.push("");
if (existingFiles.length > 0) {
for (const file of existingFiles) {
messageLines.push(`- ${file}`);
}
}
messageLines.push("", "Do you want to delete them?");
const shouldDelete = await this.CLI_INTERFACE_SERVICE.confirm(messageLines.join("\n"), true);
if (shouldDelete) {
await Promise.all(existingFiles.map((file) => this.FILE_SYSTEM_SERVICE.deleteFile(file)));
}
else {
this.CLI_INTERFACE_SERVICE.warn("Existing Semantic Release configuration files detected. Setup aborted.");
return false;
}
}
return true;
}
/**
* Installs and configures semantic-release.
* Guides the user through setting up automated versioning and publishing.
* @returns Promise resolving to the module setup result
*/
async install() {
try {
this.config = await this.CONFIG_SERVICE.getModuleConfig(EModule.SEMANTIC_RELEASE);
if (!(await this.shouldInstall())) {
return { wasInstalled: false };
}
if (!(await this.handleExistingSetup())) {
return { wasInstalled: false };
}
const setupParameters = await this.setupSemanticRelease();
return {
customProperties: setupParameters,
wasInstalled: true,
};
}
catch (error) {
this.CLI_INTERFACE_SERVICE.handleError("Failed to complete Semantic Release setup", error);
throw error;
}
}
/**
* Determines if semantic-release should be installed.
* Asks the user if they want to set up automated versioning and publishing.
* Uses the saved config value as default if it exists.
* @returns Promise resolving to true if the module should be installed, false otherwise
*/
async shouldInstall() {
try {
return await this.CLI_INTERFACE_SERVICE.confirm("Do you want to set up Semantic Release for automated versioning and publishing?", await this.CONFIG_SERVICE.isModuleEnabled(EModule.SEMANTIC_RELEASE));
}
catch (error) {
this.CLI_INTERFACE_SERVICE.handleError("Failed to get user confirmation", error);
return false;
}
}
/**
* Creates semantic-release configuration files.
* Generates the config file with repository URL and branch settings.
* @param repositoryUrl - The repository URL for semantic-release
* @param mainBranch - The main branch for production releases
* @param preReleaseBranch - Optional branch for pre-releases
* @param preReleaseChannel - Optional channel name for pre-releases
* @param isBackmergeEnabled - Optional flag to enable backmerge to development branch
* @param developBranch - Optional development branch name for backmerge
*/
async createConfigs(repositoryUrl, mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled = false, developBranch) {
await this.FILE_SYSTEM_SERVICE.writeFile(SEMANTIC_RELEASE_CONFIG_FILE_NAME, SEMANTIC_RELEASE_CONFIG.template(repositoryUrl, mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled, developBranch), "utf8");
}
/**
* Displays a summary of the semantic-release setup results.
* Lists configured branches, scripts, and usage instructions.
* @param mainBranch - The main branch for production releases
* @param preReleaseBranch - Optional branch for pre-releases
* @param preReleaseChannel - Optional channel name for pre-releases
* @param isBackmergeEnabled - Optional flag indicating if backmerge is enabled
* @param developBranch - Optional development branch name for backmerge
*/
displaySetupSummary(mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled = false, developBranch) {
const summary = ["Semantic Release configuration has been created.", "", "Release branches:", `- Main release branch: ${mainBranch}`];
if (preReleaseBranch && preReleaseChannel) {
summary.push(`- Pre-release branch: ${preReleaseBranch} (channel: ${preReleaseChannel})`);
}
if (isBackmergeEnabled && developBranch) {
summary.push(`- Backmerge enabled: Changes from ${mainBranch} will be automatically merged to ${developBranch} after release`);
}
summary.push("", "Generated scripts:", "- npm run release", "", "Configuration files:", `- ${SEMANTIC_RELEASE_CONFIG_FILE_NAME}`, "", "Changelog location:", "- CHANGELOG.md", "", "Note: To use Semantic Release effectively, you should:", "1. Configure CI/CD in your repository", "2. Set up required access tokens (GITHUB_TOKEN, NPM_TOKEN)", "3. Use conventional commits (works with the Commitlint setup)");
this.CLI_INTERFACE_SERVICE.note("Semantic Release Setup", summary.join("\n"));
}
/**
* Finds existing semantic-release configuration files.
* @returns Promise resolving to an array of file paths for existing configuration files
*/
async findExistingConfigFiles() {
const existingFiles = [];
for (const file of SEMANTIC_RELEASE_CONFIG_FILE_NAMES) {
if (await this.FILE_SYSTEM_SERVICE.isPathExists(file)) {
existingFiles.push(file);
}
}
// Check for CHANGELOG.md in the root directory
if (await this.FILE_SYSTEM_SERVICE.isPathExists("CHANGELOG.md")) {
existingFiles.push("CHANGELOG.md");
}
// Also check for legacy docs/CHANGELOG.md
if (await this.FILE_SYSTEM_SERVICE.isPathExists("docs/CHANGELOG.md")) {
existingFiles.push("docs/CHANGELOG.md");
}
return existingFiles;
}
/**
* Prompts the user for the development branch name for backmerge.
* @returns Promise resolving to the development branch name
*/
async getDevelopBranch() {
const initialBranch = this.config?.developBranch ?? "dev";
return await this.CLI_INTERFACE_SERVICE.text("Enter the name of your development branch for backmerge:", "dev", initialBranch, (value) => {
if (!value) {
return "Branch name is required";
}
if (value.includes(" ")) {
return "Branch name cannot contain spaces";
}
});
}
/**
* Prompts the user for the main release branch name.
* @returns Promise resolving to the main branch name
*/
async getMainBranch() {
const initialBranch = this.config?.mainBranch ?? "main";
return await this.CLI_INTERFACE_SERVICE.text("Enter the name of your main release branch:", "main", initialBranch, (value) => {
if (!value) {
return "Branch name is required";
}
if (value.includes(" ")) {
return "Branch name cannot contain spaces";
}
});
}
/**
* Prompts the user for the pre-release branch name.
* @returns Promise resolving to the pre-release branch name
*/
async getPreReleaseBranch() {
const initialBranch = this.config?.preReleaseBranch ?? "dev";
return await this.CLI_INTERFACE_SERVICE.text("Enter the name of your pre-release branch:", "dev", initialBranch, (value) => {
if (!value) {
return "Branch name is required";
}
if (value.includes(" ")) {
return "Branch name cannot contain spaces";
}
});
}
/**
* Prompts the user for the pre-release channel name.
* @returns Promise resolving to the pre-release channel name
*/
async getPreReleaseChannel() {
const initialChannel = this.config?.preReleaseChannel ?? "beta";
return await this.CLI_INTERFACE_SERVICE.text("Enter the pre-release channel name (e.g., beta, alpha, next):", "beta", initialChannel, (value) => {
if (!value) {
return "Channel name is required";
}
if (value.includes(" ")) {
return "Channel name cannot contain spaces";
}
});
}
/**
* Gets the repository URL for semantic-release.
* Attempts to detect URL from package.json before prompting the user.
* @returns Promise resolving to the repository URL
*/
async getRepositoryUrl() {
let savedRepoUrl = this.config?.repositoryUrl ?? "";
if (!savedRepoUrl) {
const packageJson = await this.PACKAGE_JSON_SERVICE.get();
if (packageJson.repository) {
savedRepoUrl = typeof packageJson.repository === "string" ? packageJson.repository : packageJson.repository.url || "";
}
if (savedRepoUrl.startsWith("git+")) {
// eslint-disable-next-line @elsikora/typescript/no-magic-numbers
savedRepoUrl = savedRepoUrl.slice(4);
}
if (savedRepoUrl.endsWith(".git")) {
// eslint-disable-next-line @elsikora/typescript/no-magic-numbers
savedRepoUrl = savedRepoUrl.slice(0, Math.max(0, savedRepoUrl.length - 4));
}
}
if (savedRepoUrl) {
const shouldUseFoundedUrl = await this.CLI_INTERFACE_SERVICE.confirm(`Found repository URL: ${savedRepoUrl}\nIs this correct?`, true);
if (!shouldUseFoundedUrl) {
savedRepoUrl = await this.CLI_INTERFACE_SERVICE.text("Enter your repository URL (e.g., https://github.com/username/repo):", undefined, savedRepoUrl, (value) => {
if (!value) {
return "Repository URL is required";
}
if (!value.startsWith("https://") && !value.startsWith("http://")) {
return "Repository URL must start with 'https://' or 'http://'";
}
});
}
}
else {
savedRepoUrl = await this.CLI_INTERFACE_SERVICE.text("Enter your repository URL (e.g., https://github.com/username/repo):", undefined, undefined, (value) => {
if (!value) {
return "Repository URL is required";
}
if (!value.startsWith("https://") && !value.startsWith("http://")) {
return "Repository URL must start with 'https://' or 'http://'";
}
});
}
return savedRepoUrl;
}
/**
* Prompts the user if they want to enable backmerge to development branch.
* Only applicable for the main branch.
* @param mainBranch - The main branch name
* @returns Promise resolving to true if backmerge should be enabled, false otherwise
*/
async isBackmergeEnabled(mainBranch) {
const isConfirmedByDefault = this.config?.isBackmergeEnabled === true;
return await this.CLI_INTERFACE_SERVICE.confirm(`Do you want to enable automatic backmerge from ${mainBranch} to development branch after release?`, isConfirmedByDefault);
}
/**
* Prompts the user if they want to enable pre-release channels.
* @returns Promise resolving to true if pre-release should be enabled, false otherwise
*/
async isPrereleaseEnabledChannel() {
const isConfirmedByDefault = this.config?.isPrereleaseEnabled === true;
return await this.CLI_INTERFACE_SERVICE.confirm("Do you want to configure a pre-release channel for development branches?", isConfirmedByDefault);
}
/**
* Sets up npm scripts for semantic-release.
* Adds scripts for running semantic-release and CI processes.
*/
async setupScripts() {
await this.PACKAGE_JSON_SERVICE.addScript("release", "semantic-release");
}
/**
* Sets up semantic-release configuration.
* Collects user input, installs dependencies, creates config files,
* and sets up scripts.
* @returns Promise resolving to an object containing setup parameters
*/
async setupSemanticRelease() {
try {
const parameters = {};
const repositoryUrl = await this.getRepositoryUrl();
parameters.repositoryUrl = repositoryUrl;
const mainBranch = await this.getMainBranch();
parameters.mainBranch = mainBranch;
const isPrereleaseEnabled = await this.isPrereleaseEnabledChannel();
parameters.isPrereleaseEnabled = isPrereleaseEnabled;
let preReleaseBranch = undefined;
let preReleaseChannel = undefined;
if (isPrereleaseEnabled) {
preReleaseBranch = await this.getPreReleaseBranch();
parameters.preReleaseBranch = preReleaseBranch;
preReleaseChannel = await this.getPreReleaseChannel();
parameters.preReleaseChannel = preReleaseChannel;
}
// Backmerge configuration
let developBranch = undefined;
// Only ask about backmerge if we're not in a pre-release branch
const isBackmergeEnabled = await this.isBackmergeEnabled(mainBranch);
parameters.isBackmergeEnabled = isBackmergeEnabled;
if (isBackmergeEnabled) {
developBranch = await this.getDevelopBranch();
parameters.developBranch = developBranch;
}
this.CLI_INTERFACE_SERVICE.startSpinner("Setting up Semantic Release configuration...");
await this.PACKAGE_JSON_SERVICE.installPackages(SEMANTIC_RELEASE_CONFIG_CORE_DEPENDENCIES, "latest", EPackageJsonDependencyType.DEV);
await this.createConfigs(repositoryUrl, mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled, developBranch);
await this.setupScripts();
this.CLI_INTERFACE_SERVICE.stopSpinner("Semantic Release configuration completed successfully!");
this.displaySetupSummary(mainBranch, preReleaseBranch, preReleaseChannel, isBackmergeEnabled, developBranch);
return parameters;
}
catch (error) {
this.CLI_INTERFACE_SERVICE.stopSpinner("Failed to setup Semantic Release configuration");
throw error;
}
}
}
export { SemanticReleaseModuleService };
//# sourceMappingURL=semantic-release-module.service.js.map