@@ -38,6 +38,30 @@ export async function fetchLatestVersion(options = {}) {
3838 }
3939}
4040
41+ /**
42+ * Return whether `candidate` is newer than `current` using the package's
43+ * semver-shaped versions. Invalid versions are treated as not newer so a
44+ * malformed registry response cannot trigger a bogus update notice.
45+ *
46+ * @param {string } candidate
47+ * @param {string } current
48+ * @returns {boolean }
49+ */
50+ export function isNewerVersion ( candidate , current ) {
51+ const next = parseVersion ( candidate )
52+ const base = parseVersion ( current )
53+ if ( ! next || ! base ) return false
54+ for ( const key of [ 'major' , 'minor' , 'patch' ] ) {
55+ const k = /** @type {'major' | 'minor' | 'patch' } */ ( key )
56+ if ( next [ k ] > base [ k ] ) return true
57+ if ( next [ k ] < base [ k ] ) return false
58+ }
59+ if ( next . prerelease === base . prerelease ) return false
60+ if ( ! next . prerelease ) return Boolean ( base . prerelease )
61+ if ( ! base . prerelease ) return false
62+ return comparePrerelease ( next . prerelease , base . prerelease ) > 0
63+ }
64+
4165/**
4266 * Whether the running script lives somewhere `npm install -g` can replace.
4367 *
@@ -116,7 +140,7 @@ export async function selfUpdate(options = {}) {
116140
117141 const currentVersion = readVersion ( )
118142 const latest = await fetchLatest ( )
119- if ( ! latest || latest === currentVersion ) return
143+ if ( ! latest || ! isNewerVersion ( latest , currentVersion ) ) return
120144
121145 log . write ( `[collectivus] update available: ${ currentVersion } -> ${ latest } ; running npm install -g collectivus@${ latest } \n` )
122146 const installed = await install ( latest )
@@ -163,3 +187,45 @@ function defaultRun(command, args, opts) {
163187 child . once ( 'exit' , ( code ) => resolve ( code === null ? - 1 : code ) )
164188 } )
165189}
190+
191+ /**
192+ * @param {string } value
193+ * @returns {{ major: number, minor: number, patch: number, prerelease: string } | undefined }
194+ */
195+ function parseVersion ( value ) {
196+ const match = / ^ ( \d + ) \. ( \d + ) \. ( \d + ) (?: - ( [ 0 - 9 A - Z a - z . - ] + ) ) ? $ / . exec ( value )
197+ if ( ! match ) return undefined
198+ return {
199+ major : Number ( match [ 1 ] ) ,
200+ minor : Number ( match [ 2 ] ) ,
201+ patch : Number ( match [ 3 ] ) ,
202+ prerelease : match [ 4 ] ?? '' ,
203+ }
204+ }
205+
206+ /**
207+ * @param {string } a
208+ * @param {string } b
209+ * @returns {number }
210+ */
211+ function comparePrerelease ( a , b ) {
212+ const aa = a . split ( '.' )
213+ const bb = b . split ( '.' )
214+ const len = Math . max ( aa . length , bb . length )
215+ for ( let i = 0 ; i < len ; i ++ ) {
216+ const av = aa [ i ]
217+ const bv = bb [ i ]
218+ if ( av === undefined ) return - 1
219+ if ( bv === undefined ) return 1
220+ const an = / ^ \d + $ / . test ( av ) ? Number ( av ) : undefined
221+ const bn = / ^ \d + $ / . test ( bv ) ? Number ( bv ) : undefined
222+ if ( an !== undefined && bn !== undefined ) {
223+ if ( an !== bn ) return an - bn
224+ continue
225+ }
226+ if ( an !== undefined ) return - 1
227+ if ( bn !== undefined ) return 1
228+ if ( av !== bv ) return av < bv ? - 1 : 1
229+ }
230+ return 0
231+ }
0 commit comments