293 lines
8.6 KiB
JavaScript

import path from 'path';
import { fileURLToPath } from 'url';
import moment from 'moment';
import replace from 'replace-in-file';
import inquirer from 'inquirer';
import semver from 'semver';
import {
displayErrorMessage,
displayConfirmationMessage
} from './index.mjs';
import mainPackageJson from '../../package.json' with { type: 'json' };
import hotConfig from '../../hot.config.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const workspacePackages = mainPackageJson.workspaces;
/**
* Current version of the monorepo (before updating).
*
* @type {string}
*/
export const CURRENT_VERSION = hotConfig.HOT_VERSION;
/**
* Check if the provided version number is a valid semver version number.
*
* @param {string} version Version number.
* @returns {boolean} `true` if the version number is a valid semver version number, `false` otherwise.
*/
export function isVersionValid(version) {
return !!semver.valid(version);
}
/**
* Check if the provided release date in a format of 'DD/MM/YYYY' is a valid future date.
*
* @param {string} date The date in format of 'DD/MM/YYYY'.
* @returns {object} Object containing information about the release validity in a form of `{valid: boolean, error:
* string}`.
*/
export function validateReleaseDate(date) {
const dateObj = moment(date, 'DD/MM/YYYY', true);
const now = moment();
const returnObj = {
valid: true,
error: null
};
if (!dateObj.isValid()) {
returnObj.valid = false;
returnObj.error = 'The provided date is invalid.';
} else if (!dateObj.isAfter(now)) {
returnObj.valid = false;
returnObj.error = 'The release date has to be a future date.';
}
return returnObj;
}
/**
* Set the provided version number to the Handsontable's `package.json` and other packages' `dependency` fields.
*
* @param {string} version The version number.
* @param {Array} [packages] Array of package paths. Defaults to the workspace config.
*/
export function setVersion(version, packages = workspacePackages) {
// Set the new version number to hot.config.js.
const hotConfigPath = path.resolve(__dirname, '../../hot.config.js');
validateReplacementStatus(replace.sync({
files: hotConfigPath,
from: /HOT_VERSION: '(.*)'/,
to: `HOT_VERSION: '${version}'`,
}), version);
// Set the new version number to all the packages.
packages.forEach((packagesLocation) => {
validateReplacementStatus(replace.sync({
files: `${packagesLocation}${packagesLocation === '.' ? '' : '*'}/package.json`,
from: [/"version": "(.*)"/, /"handsontable": "([^\d]*)((\d+)\.(\d+).(\d+)(.*))"/g],
to: (fullMatch, ...[semverPrefix, previousVersion]) => {
if (fullMatch.indexOf('version') > 0) {
// Replace the version with the new version.
return `"version": "${version}"`;
} else {
const isPreRelease = version.includes('-next-');
const newVersion = isPreRelease ? version : `${semverPrefix}${semver.major(
semver.maxSatisfying([version, previousVersion], '*')
)}.0.0`;
// Replace the `handsontable` dependency with the current major (or previous major, if it's a prerelease).
return `"handsontable": "${newVersion}"`;
}
},
ignore: [
`${packagesLocation}*/node_modules/**/*`,
`${packagesLocation}*/projects/hot-table/package.json`,
`${packagesLocation}*/dist/hot-table/package.json`,
],
}), version);
});
}
/**
* Set the provided release date in the `hot.config.js` file.
*
* @param {string} date The release date in a format of 'DD/MM/YYYY'.
*/
export function setReleaseDate(date) {
const hotConfigPath = path.resolve(__dirname, '../../hot.config.js');
const replacementStatus = replace.sync({
files: hotConfigPath,
from: /HOT_RELEASE_DATE: '(.*)'/,
to: `HOT_RELEASE_DATE: '${date}'`,
});
validateReplacementStatus(replacementStatus, date);
}
/**
* Get the new version from the provided release type (major/minor/patch).
*
* @param {string} type 'major'/'minor'/'patch'.
* @param {string} currentVersion Current version string.
* @returns {string} A new semver-based version.
*/
export function getVersionFromReleaseType(type, currentVersion) {
return semver.inc(currentVersion, type);
}
/**
* Schedule the release by setting the version number to the `package.json` files in all of the packages and release
* date in the Handsontable config.
*
* @param {string} [version] Version number.
* @param {string} [releaseDate] Release date in the `DD/MM/YYYY` format.
* @returns {object}
*/
export async function scheduleRelease(version, releaseDate) {
const currentVersion = hotConfig.HOT_VERSION;
const questions = [
{
type: 'list',
name: 'changeType',
message: 'Select the type of the release.',
choices: [
'Major',
'Minor',
'Patch',
'Custom',
],
filter: value => value.toLowerCase(),
},
{
type: 'input',
name: 'customVersion',
message: 'Enter the custom version number.',
when: answers => answers.changeType === 'custom',
validate: (value) => {
if (isVersionValid(value)) {
return true;
}
return 'The provided version is not a proper semver version number.';
},
},
{
type: 'input',
name: 'releaseDate',
message: 'Enter the release date in a form of DD/MM/YYYY.',
validate: (value) => {
const releaseDateValidity = validateReleaseDate(value);
if (releaseDateValidity.valid) {
return true;
}
return releaseDateValidity.error;
},
}
];
const getConfirmationQuestion = (newVersion, formattedDate) => [
{
type: 'confirm',
name: 'isReleaseDateConfirmed',
message: `
* New version: ${newVersion}
* Release date: ${formattedDate}
Are the version number and release date above correct?`,
default: true,
},
];
let finalVersion = null;
let finalReleaseDate = null;
// Version and release date were passed as arguments.
if (version && releaseDate) {
const semverBumpOptions = ['major', 'minor', 'patch'];
const releaseDateValidity = validateReleaseDate(releaseDate);
let newVersion = '';
if (!releaseDateValidity.valid) {
displayErrorMessage(releaseDateValidity.error);
process.exit(1);
}
if (isVersionValid(version)) {
newVersion = version;
} else if (semverBumpOptions.includes(version)) {
newVersion = semver.inc(currentVersion, version);
} else {
displayErrorMessage(
`${version} is not a valid version number, nor a semver change type (major/minor/patch).`);
process.exit(1);
}
if (!newVersion) {
displayErrorMessage('Something went wrong while updating the version number with semver.');
process.exit(1);
}
displayConfirmationMessage(
`\nChanging the version number to ${newVersion}, to be released on ${releaseDate}. \n`
);
finalVersion = newVersion;
finalReleaseDate = releaseDate;
setVersion(newVersion, workspacePackages);
setReleaseDate(releaseDate);
} else {
const answers = await inquirer.prompt(questions);
const releaseDateObj = moment(answers.releaseDate, 'DD/MM/YYYY', true);
const newVersion =
answers.changeType !== 'custom' ?
getVersionFromReleaseType(answers.changeType, currentVersion) :
answers.customVersion;
const confirmationAnswers = await inquirer.prompt(
getConfirmationQuestion(newVersion, releaseDateObj.format('DD MMMM YYYY'))
);
if (confirmationAnswers.isReleaseDateConfirmed) {
finalVersion = newVersion;
finalReleaseDate = answers.releaseDate;
setVersion(newVersion);
setReleaseDate(answers.releaseDate);
}
}
return {
version: finalVersion,
releaseDate: finalReleaseDate
};
}
/**
* Helper function validating the return status of `replace-in-file`'s `replace` method.
*
* @private
* @param {Array} replacementStatus Replacement status array returned from `replace-in-file`'s `replace` method.
* @param {string} replacedString The string replaced in the source file.
*/
function validateReplacementStatus(replacementStatus, replacedString) {
let versionReplaced = true;
replacementStatus.forEach((infoObj) => {
const filePath = infoObj.file.replace('./', '');
if (!infoObj.hasChanged) {
displayErrorMessage(`${filePath} was not modified.`);
versionReplaced = false;
} else {
displayConfirmationMessage(`- Saved '${replacedString}' to ${path.relative(process.cwd(), filePath)}.`);
}
});
if (!versionReplaced) {
process.exit(1);
}
}