diff --git a/src/hooks/npm-update-notifier.ts b/src/hooks/npm-update-notifier.ts index b5d40e81..a57cce08 100644 --- a/src/hooks/npm-update-notifier.ts +++ b/src/hooks/npm-update-notifier.ts @@ -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) => { + 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); } @@ -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}`; @@ -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.'; } diff --git a/test/hooks/npm-update-notifier.test.ts b/test/hooks/npm-update-notifier.test.ts index 8cd93316..82acd0cf 100644 --- a/test/hooks/npm-update-notifier.test.ts +++ b/test/hooks/npm-update-notifier.test.ts @@ -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', () => { @@ -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', () => { @@ -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, + }); + }); }); });