Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions src/hooks/npm-update-notifier.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import type { Hook } from '@oclif/core';
import updateNotifier, { type UpdateInfo } from 'update-notifier';
import pkg from '../../package.json' with { type: 'json' };
import { debugLogger } from '../service/log.svc.ts';

const updateNotifierHook: Hook.Init = async (options) => {
Comment thread
edezekiel marked this conversation as resolved.
debugLogger('pkg.version', pkg.version);

const distTag = getDistTag(pkg.version);
debugLogger('distTag', distTag);
const ONE_DAY_MS = 1000 * 60 * 60 * 24;
// If we're on the latest dist-tag, check for updates every time
const updateCheckInterval = distTag === 'latest' ? 0 : ONE_DAY_MS;

debugLogger('updateCheckInterval', updateCheckInterval);

const notifier = updateNotifier({
pkg,
updateCheckInterval: 1000 * 60 * 60 * 24, // Check once per day
distTag,
updateCheckInterval,
shouldNotifyInNpmScript: true,
});

debugLogger('updateNotifierHook', { notifier });

if (notifier.update) {
const notification = handleUpdate(notifier.update, pkg.version);
debugLogger('notification', notification);

if (notification) {
notifier.notify(notification);
}
Expand All @@ -18,11 +35,19 @@ const updateNotifierHook: Hook.Init = async (options) => {

export default updateNotifierHook;

type DistTag = 'latest' | 'beta' | 'alpha' | 'next';

export function getDistTag(version: string): DistTag {
if (version.includes('-beta')) return 'beta';
if (version.includes('-alpha')) return 'alpha';
if (version.includes('-next')) return 'next';
return 'latest';
}

export function handleUpdate(update: UpdateInfo, currentVersion: string) {
const isPreV1 = currentVersion.startsWith('0.');
const isBeta = currentVersion.includes('-beta') || update.latest.includes('-beta');
const isAlpha = currentVersion.includes('-alpha') || update.latest.includes('-alpha');
const isNext = currentVersion.includes('-next') || update.latest.includes('-next');
const isPreV1 = currentVersion.startsWith('0.') || update.latest.startsWith('0.');
const currentDistTag = getDistTag(currentVersion);
const updateDistTag = getDistTag(update.latest);

let message = `Update available! v${currentVersion} → v${update.latest}`;

Expand All @@ -35,7 +60,7 @@ export function handleUpdate(update: UpdateInfo, currentVersion: string) {
* [1]https://semver.org/#spec-item-4
* [2]https://antfu.me/posts/epoch-semver#leading-zero-major-versioning
*/
if (isPreV1 || isBeta || isAlpha || isNext || update.type === 'major') {
if (isPreV1 || currentDistTag !== 'latest' || updateDistTag !== 'latest' || update.type === 'major') {
message += '\nThis update may contain breaking changes.';
}

Expand Down
92 changes: 91 additions & 1 deletion test/hooks/npm-update-notifier.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import assert from 'node:assert';
import { describe, it } from 'node:test';
import { handleUpdate } from '../../src/hooks/npm-update-notifier.ts';
import { getDistTag, handleUpdate } from '../../src/hooks/npm-update-notifier.ts';

describe('getDistTag', () => {
it('should return beta for beta versions', () => {
assert.strictEqual(getDistTag('1.0.0-beta.1'), 'beta');
assert.strictEqual(getDistTag('2.3.4-beta.42'), 'beta');
});

it('should return alpha for alpha versions', () => {
assert.strictEqual(getDistTag('1.0.0-alpha.1'), 'alpha');
assert.strictEqual(getDistTag('2.3.4-alpha.42'), 'alpha');
});

it('should return next for next versions', () => {
assert.strictEqual(getDistTag('1.0.0-next.1'), 'next');
assert.strictEqual(getDistTag('2.3.4-next.42'), 'next');
});

it('should return latest for stable versions', () => {
assert.strictEqual(getDistTag('1.0.0'), 'latest');
assert.strictEqual(getDistTag('2.3.4'), 'latest');
});
});

describe('handleUpdate', () => {
describe('updates that may contain breaking changes', () => {
Expand Down Expand Up @@ -88,6 +110,57 @@ describe('handleUpdate', () => {
defer: false,
});
});

it('should warn when updating from stable to beta', () => {
const result = handleUpdate(
{
type: 'minor',
latest: '1.4.0-beta.1',
current: '1.3.0',
name: '@herodevs/cli',
},
'1.3.0',
);

assert.deepStrictEqual(result, {
message: 'Update available! v1.3.0 → v1.4.0-beta.1\nThis update may contain breaking changes.',
defer: false,
});
});

it('should warn when updating from beta to stable', () => {
const result = handleUpdate(
{
type: 'minor',
latest: '1.4.0',
current: '1.4.0-beta.1',
name: '@herodevs/cli',
},
'1.4.0-beta.1',
);

assert.deepStrictEqual(result, {
message: 'Update available! v1.4.0-beta.1 → v1.4.0\nThis update may contain breaking changes.',
defer: false,
});
});

it('should warn about minor updates between beta versions', () => {
const result = handleUpdate(
{
type: 'minor',
latest: '1.4.0-beta.2',
current: '1.3.0-beta.1',
name: '@herodevs/cli',
},
'1.3.0-beta.1',
);

assert.deepStrictEqual(result, {
message: 'Update available! v1.3.0-beta.1 → v1.4.0-beta.2\nThis update may contain breaking changes.',
defer: false,
});
});
});

describe('updates that should not contain breaking changes', () => {
Expand Down Expand Up @@ -124,5 +197,22 @@ describe('handleUpdate', () => {
defer: false,
});
});

it('should not warn about minor updates between stable versions', () => {
const result = handleUpdate(
{
type: 'minor',
latest: '1.4.0',
current: '1.3.0',
name: '@herodevs/cli',
},
'1.3.0',
);

assert.deepStrictEqual(result, {
message: 'Update available! v1.3.0 → v1.4.0',
defer: false,
});
});
});
});