Skip to content

Commit

Permalink
fix: stop using npm version to bump package and instead use `semver…
Browse files Browse the repository at this point in the history
…` directly
  • Loading branch information
favna committed Jun 3, 2022
1 parent a7d68b6 commit 662b90e
Show file tree
Hide file tree
Showing 4 changed files with 3,995 additions and 4,172 deletions.
46 changes: 40 additions & 6 deletions src/commands/bump-version.ts
@@ -1,12 +1,46 @@
import { releasePrefix } from '#lib/constants';
import { packageCwd } from '#lib/constants';
import { logVerboseError } from '#lib/logger';
import { readPackageJson, writePackageJson } from '#lib/package-json-parser';
import { doActionAndLog } from '#lib/utils';
import { isNullishOrEmpty } from '@sapphire/utilities';
import type { OptionValues } from 'commander';
import type { Callback as ConventionalChangelogCallback } from 'conventional-recommended-bump';
import { execSync } from 'node:child_process';
import { join } from 'node:path';
import Semver from 'semver';

export function bumpVersion(options: OptionValues, releaseType: ConventionalChangelogCallback.Recommendation.ReleaseType) {
return doActionAndLog(
'Bumping version in package.json',
execSync(`npm version ${releasePrefix}${releaseType} --git-tag-version=false --preid=${options.preid ?? ''}`)
);
return doActionAndLog('Bumping version in package.json', async () => {
const packageJsonPath = join(packageCwd, 'package.json');
const packageJsonContent = await readPackageJson(packageJsonPath);

const currentVersion = packageJsonContent.version;
const currentClean = Semver.clean(currentVersion);

if (isNullishOrEmpty(currentClean)) {
return logVerboseError({
text: ['No current version was found. Make sure there is a package.json at your current working directory'],
logWithThrownError: true,
verbose: options.verbose
});
}

const newVersion = Semver.inc(currentClean, releaseType, { loose: true }, options.preid ?? '');

if (isNullishOrEmpty(newVersion)) {
return logVerboseError({
text: ['Failed to assign new version.'],
verboseText: [
`The resolved current version is ${currentVersion} which was cleaned to ${currentClean} by semver clean`,
`A bump with release type ${releaseType} was attempted but failed`,
'Either validate your setup or contact the developer with reproducible code.'
],
logWithThrownError: true,
verbose: options.verbose
});
}

packageJsonContent.version = newVersion;

await writePackageJson(packageJsonPath, packageJsonContent);
});
}
81 changes: 81 additions & 0 deletions src/lib/package-json-parser.ts
@@ -0,0 +1,81 @@
/**
* This code is based on https://github.com/npm/json-parse-even-better-errors
* @license MIT
* @copyright 2017 Kat Marchán Copyright npm, Inc.
*/

import type { PathLike } from 'node:fs';
import { readFile, writeFile } from 'node:fs/promises';

/** A {@link Symbol} for the indent identifier in a parsed package.json */
const packageJsonParseIndentSymbol = Symbol.for('indent');

/** A {@link Symbol} for the newline identifier in a parsed package.json */
const packageJsonParseNewlineSymbol = Symbol.for('newline');

/**
* Parses a package.json file while preserving the indents and newlines
* as {@link packageJsonParseIndentSymbol} and {@link packageJsonParseNewlineSymbol}
*
* @param pathLike The {@link PathLike} to read with {@link readFile}
*/
export async function readPackageJson(pathLike: PathLike): Promise<PackageJsonStructure> {
// only respect indentation if we got a line break, otherwise squash it
// things other than objects and arrays aren't indented, so ignore those
// Important: in both of these regexps, the $1 capture group is the newline
// or undefined, and the $2 capture group is the indent, or undefined.
const formatRE = /^\s*[{\[]((?:\r?\n)+)([\s\t]*)/;
const emptyRE = /^(?:\{\}|\[\])((?:\r?\n)+)?$/;

const parseJson = (txt: string | any[]) => {
const parseText = stripBOM(txt);

// get the indentation so that we can save it back nicely
// if the file starts with {" then we have an indent of '', ie, none
// otherwise, pick the indentation of the next line after the first \n
// If the pattern doesn't match, then it means no indentation.
// JSON.stringify ignores symbols, so this is reasonably safe.
// if the string is '{}' or '[]', then use the default 2-space indent.
// eslint-disable-next-line no-sparse-arrays
const [, newline = '\n', indent = ' '] = parseText.match(emptyRE) || parseText.match(formatRE) || [, '', ''];

const result = JSON.parse(parseText);
if (result && typeof result === 'object') {
result[packageJsonParseNewlineSymbol] = newline;
result[packageJsonParseIndentSymbol] = indent;
}
return result;
};

// Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
// because the buffer-to-string conversion in `fs.readFileSync()`
// translates it to FEFF, the UTF-16 BOM.
const stripBOM = (txt: string | any[]) => String(txt).replace(/^\uFEFF/, '');

return parseJson(await readFile(pathLike, { encoding: 'utf-8' }));
}

/**
* Writes to a package.json file
* @param pathLike The {@link PathLike} to read with {@link readFile}
* @param pkg The package.json data to write
*/
export async function writePackageJson(pathLike: PathLike, pkg: Record<any, any>): Promise<void> {
const indent = Reflect.get(pkg, packageJsonParseIndentSymbol) ?? 2;
const newline = Reflect.get(pkg, packageJsonParseNewlineSymbol) ?? '\n';

Reflect.deleteProperty(pkg, '_id');

const raw = `${JSON.stringify(pkg, null, indent)}\n`;

const data = newline === '\n' ? raw : raw.split('\n').join(newline);

return writeFile(pathLike, data);
}

interface PackageJsonStructure {
name: string;
version: string;
[packageJsonParseIndentSymbol]: number;
[packageJsonParseNewlineSymbol]: string;
}
7 changes: 4 additions & 3 deletions src/lib/utils.ts
@@ -1,6 +1,6 @@
import { packageCwd } from '#lib/constants';
import { fromAsync, isErr } from '@sapphire/result';
import { Awaitable, isThenable } from '@sapphire/utilities';
import { Awaitable, isFunction, isThenable } from '@sapphire/utilities';
import { cyan, green, red } from 'colorette';
import type { OptionValues } from 'commander';
import { load } from 'js-yaml';
Expand Down Expand Up @@ -49,11 +49,12 @@ export function getFullPackageName(options: OptionValues) {
return options.org ? `@${options.org}/${options.name}` : options.name;
}

export async function doActionAndLog<T>(preActionLog: string, action: Awaitable<T>): Promise<T> {
export async function doActionAndLog<T>(preActionLog: string, action: Awaitable<T> | (() => Awaitable<T>)): Promise<T> {
process.stdout.write(cyan(`${preActionLog}... `));

const result = await fromAsync(async () => {
const returnValue = isThenable(action) ? ((await action) as T) : action;
const executedFunctionResult = isFunction(action) ? action() : action;
const returnValue = isThenable(executedFunctionResult) ? ((await executedFunctionResult) as T) : executedFunctionResult;
console.log(green('✅ Done'));
return returnValue;
});
Expand Down

0 comments on commit 662b90e

Please sign in to comment.