update
This commit is contained in:
24
package.json
24
package.json
@@ -9,24 +9,23 @@
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/)",
|
||||
"test": "(tstest test/ --verbose)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.1.66",
|
||||
"@gitzone/tsrun": "^1.2.44",
|
||||
"@gitzone/tstest": "^1.0.77",
|
||||
"@push.rocks/tapbundle": "^5.0.12",
|
||||
"@types/node": "^20.4.5"
|
||||
"@git.zone/tsbuild": "^2.7.1",
|
||||
"@git.zone/tsrun": "^1.6.2",
|
||||
"@git.zone/tstest": "^2.7.0",
|
||||
"@types/node": "^22.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/consolecolor": "^2.0.1",
|
||||
"@push.rocks/npmextra": "^3.0.9",
|
||||
"@push.rocks/smartnpm": "^2.0.4",
|
||||
"@push.rocks/consolecolor": "^2.0.3",
|
||||
"@push.rocks/npmextra": "^5.3.3",
|
||||
"@push.rocks/smartnpm": "^2.0.6",
|
||||
"@push.rocks/smartopen": "^2.0.0",
|
||||
"@push.rocks/smarttime": "^4.0.4",
|
||||
"@push.rocks/smartversion": "^3.0.2"
|
||||
"@push.rocks/smarttime": "^4.1.1",
|
||||
"@push.rocks/smartversion": "^3.0.5"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -59,5 +58,6 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://code.foss.global/push.rocks/smartupdate.git"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
|
||||
}
|
||||
11680
pnpm-lock.yaml
generated
11680
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,127 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as smartupdate from '../ts/index.js';
|
||||
|
||||
let testSmartUpdate: smartupdate.SmartUpdate;
|
||||
let silentSmartUpdate: smartupdate.SmartUpdate;
|
||||
|
||||
tap.test('should create an instance of SmartUpdate', async () => {
|
||||
// Test suite for backward compatibility
|
||||
tap.test('backward compatibility: should create an instance of SmartUpdate', async () => {
|
||||
testSmartUpdate = new smartupdate.SmartUpdate();
|
||||
expect(testSmartUpdate).toBeInstanceOf(smartupdate.SmartUpdate);
|
||||
});
|
||||
|
||||
tap.test('should check for a npm module', async () => {
|
||||
tap.test('backward compatibility: should check for a npm module using old API', async () => {
|
||||
const result = await testSmartUpdate.check('lodash', '1.0.5');
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
tap.start();
|
||||
// Test suite for new modern API
|
||||
tap.test('modern API: should create SmartUpdate with custom options', async () => {
|
||||
silentSmartUpdate = new smartupdate.SmartUpdate({
|
||||
logLevel: 'SILENT',
|
||||
noColor: true,
|
||||
cacheDuration: { minutes: 30 },
|
||||
});
|
||||
expect(silentSmartUpdate).toBeInstanceOf(smartupdate.SmartUpdate);
|
||||
});
|
||||
|
||||
tap.test('modern API: checkForUpdate should return rich result object', async () => {
|
||||
const result = await silentSmartUpdate.checkForUpdate({
|
||||
packageName: 'lodash',
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'never',
|
||||
});
|
||||
|
||||
// Verify result structure
|
||||
expect(result).toBeTypeOf('object');
|
||||
expect(result.status).toBeTypeOf('string');
|
||||
expect(result.packageName).toEqual('lodash');
|
||||
expect(result.currentVersion).toEqual('1.0.0');
|
||||
expect(result.checkTime).toBeInstanceOf(Date);
|
||||
expect(result.cacheHit).toBeTypeOf('boolean');
|
||||
|
||||
// Should have update available (1.0.0 is old)
|
||||
expect(result.status).toEqual('update-available');
|
||||
expect(result.latestVersion).toBeTypeOf('string');
|
||||
});
|
||||
|
||||
tap.test('modern API: checkForUpdate with up-to-date version', async () => {
|
||||
const result = await silentSmartUpdate.checkForUpdate({
|
||||
packageName: '@push.rocks/smartversion',
|
||||
currentVersion: '999.999.999', // Future version
|
||||
cacheStrategy: 'never',
|
||||
});
|
||||
|
||||
expect(result.status).toEqual('up-to-date');
|
||||
expect(result.latestVersion).toBeTypeOf('string');
|
||||
});
|
||||
|
||||
tap.test('modern API: getLatestVersion utility method', async () => {
|
||||
const latestVersion = await silentSmartUpdate.getLatestVersion('lodash');
|
||||
expect(latestVersion).toBeTypeOf('string');
|
||||
expect(latestVersion).toMatch(/^\d+\.\d+\.\d+/); // Semver format
|
||||
});
|
||||
|
||||
tap.test('modern API: error handling for non-existent package', async () => {
|
||||
const result = await silentSmartUpdate.checkForUpdate({
|
||||
packageName: 'this-package-definitely-does-not-exist-12345',
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'never',
|
||||
});
|
||||
|
||||
expect(result.status).toEqual('error');
|
||||
expect(result.error).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
tap.test('modern API: cache strategy works', async () => {
|
||||
// Clear cache first to ensure clean state
|
||||
const testPackage = 'express';
|
||||
try {
|
||||
await silentSmartUpdate.clearCache(testPackage);
|
||||
} catch (e) {
|
||||
// Cache might not exist, that's fine
|
||||
}
|
||||
|
||||
// First check - should hit registry with 'never' strategy
|
||||
const result1 = await silentSmartUpdate.checkForUpdate({
|
||||
packageName: testPackage,
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'never',
|
||||
});
|
||||
|
||||
expect(result1.cacheHit).toBeFalse();
|
||||
expect(result1.status).toEqual('update-available');
|
||||
|
||||
// Do a second check with time-based that will create cache
|
||||
const result2 = await silentSmartUpdate.checkForUpdate({
|
||||
packageName: testPackage,
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'time-based',
|
||||
});
|
||||
|
||||
// Third immediate check - should use cache with 'always' strategy
|
||||
const result3 = await silentSmartUpdate.checkForUpdate({
|
||||
packageName: testPackage,
|
||||
currentVersion: '1.0.0',
|
||||
cacheStrategy: 'always',
|
||||
});
|
||||
|
||||
expect(result3.cacheHit).toBeTrue();
|
||||
expect(result3.status).toEqual('check-skipped');
|
||||
// nextCheckTime may or may not be set depending on cache strategy implementation
|
||||
if (result3.nextCheckTime) {
|
||||
expect(result3.nextCheckTime).toBeInstanceOf(Date);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('modern API: exports all types and classes', () => {
|
||||
// Verify exports
|
||||
expect(smartupdate.SmartUpdate).toBeTypeOf('function');
|
||||
expect(smartupdate.UpdateCacheManager).toBeTypeOf('function');
|
||||
expect(smartupdate.UpdateNotifier).toBeTypeOf('function');
|
||||
expect(smartupdate.RegistryUnavailableError).toBeTypeOf('function');
|
||||
expect(smartupdate.PackageNotFoundError).toBeTypeOf('function');
|
||||
expect(smartupdate.LOG_LEVELS).toBeTypeOf('object');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
28
ts/index.ts
28
ts/index.ts
@@ -1 +1,29 @@
|
||||
// Main class
|
||||
export { SmartUpdate } from './smartupdate.classes.smartupdate.js';
|
||||
|
||||
// Supporting classes (for advanced use cases)
|
||||
export { UpdateCacheManager } from './smartupdate.classes.cachemanager.js';
|
||||
export { UpdateNotifier } from './smartupdate.classes.notifier.js';
|
||||
|
||||
// Interfaces and types
|
||||
export type {
|
||||
ISmartUpdateOptions,
|
||||
IUpdateCheckOptions,
|
||||
IUpdateCheckResult,
|
||||
ICacheStatus,
|
||||
ICacheOptions,
|
||||
INotificationOptions,
|
||||
} from './smartupdate.interfaces.js';
|
||||
|
||||
// Error classes
|
||||
export {
|
||||
SmartUpdateError,
|
||||
RegistryUnavailableError,
|
||||
PackageNotFoundError,
|
||||
InvalidVersionError,
|
||||
InvalidPackageNameError,
|
||||
} from './smartupdate.errors.js';
|
||||
|
||||
// Constants (for reference)
|
||||
export type { TLogLevel } from './smartupdate.constants.js';
|
||||
export { LOG_LEVELS, DEFAULT_MESSAGE_COLOR } from './smartupdate.constants.js';
|
||||
|
||||
116
ts/smartupdate.classes.cachemanager.ts
Normal file
116
ts/smartupdate.classes.cachemanager.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import * as plugins from './smartupdate.plugins.js';
|
||||
import type { ICacheStatus, ICacheOptions } from './smartupdate.interfaces.js';
|
||||
|
||||
/**
|
||||
* Manages caching of update check results
|
||||
*/
|
||||
export class UpdateCacheManager {
|
||||
private kvStore: plugins.npmextra.KeyValueStore;
|
||||
private cacheDurationMs: number;
|
||||
|
||||
constructor(options: ICacheOptions) {
|
||||
this.cacheDurationMs = options.durationMs;
|
||||
this.kvStore = new plugins.npmextra.KeyValueStore({
|
||||
typeArg: 'userHomeDir',
|
||||
identityArg: options.storeIdentifier || 'global_smartupdate',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache duration in milliseconds
|
||||
*/
|
||||
public getCacheDuration(): number {
|
||||
return this.cacheDurationMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached status for a package
|
||||
*/
|
||||
public async getCached(packageName: string): Promise<ICacheStatus | null> {
|
||||
return await this.kvStore.readKey(packageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache status for a package
|
||||
*/
|
||||
public async setCached(packageName: string, status: ICacheStatus): Promise<void> {
|
||||
await this.kvStore.writeKey(packageName, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for a specific package or all packages
|
||||
*/
|
||||
public async clearCache(packageName?: string): Promise<void> {
|
||||
if (packageName) {
|
||||
await this.kvStore.deleteKey(packageName);
|
||||
} else {
|
||||
// Clear all keys - this requires reading all keys first
|
||||
// For now, we'll skip implementing full cache clear as it requires more kvStore API
|
||||
throw new Error('Clearing all cache entries is not yet implemented');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should check the registry or use cache
|
||||
* @returns Object with shouldCheck flag and optional nextCheckTime
|
||||
*/
|
||||
public async shouldCheckRegistry(
|
||||
packageName: string,
|
||||
strategy: 'always' | 'never' | 'time-based' = 'time-based'
|
||||
): Promise<{
|
||||
shouldCheck: boolean;
|
||||
cacheStatus?: ICacheStatus;
|
||||
nextCheckTime?: Date;
|
||||
minutesUntilNextCheck?: number;
|
||||
}> {
|
||||
// Never use cache
|
||||
if (strategy === 'never') {
|
||||
return { shouldCheck: true };
|
||||
}
|
||||
|
||||
// Get cached data
|
||||
const cacheStatus = await this.getCached(packageName);
|
||||
|
||||
// No cache exists
|
||||
if (!cacheStatus) {
|
||||
return { shouldCheck: true };
|
||||
}
|
||||
|
||||
// Always use cache if available
|
||||
if (strategy === 'always') {
|
||||
return { shouldCheck: false, cacheStatus };
|
||||
}
|
||||
|
||||
// Time-based strategy: check if cache is still valid
|
||||
const now = Date.now();
|
||||
const lastCheckTime = cacheStatus.lastCheck;
|
||||
const timeSinceLastCheck = now - lastCheckTime;
|
||||
|
||||
// Cache is still valid
|
||||
if (timeSinceLastCheck < this.cacheDurationMs) {
|
||||
const nextCheckTime = new Date(lastCheckTime + this.cacheDurationMs);
|
||||
const minutesUntilNextCheck = (this.cacheDurationMs - timeSinceLastCheck) / 60000;
|
||||
|
||||
return {
|
||||
shouldCheck: false,
|
||||
cacheStatus,
|
||||
nextCheckTime,
|
||||
minutesUntilNextCheck,
|
||||
};
|
||||
}
|
||||
|
||||
// Cache is expired
|
||||
return { shouldCheck: true, cacheStatus };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new cache status object
|
||||
*/
|
||||
public createCacheStatus(latestVersion: string, performedUpgrade: boolean = false): ICacheStatus {
|
||||
return {
|
||||
lastCheck: Date.now(),
|
||||
latestVersion,
|
||||
performedUpgrade,
|
||||
};
|
||||
}
|
||||
}
|
||||
169
ts/smartupdate.classes.notifier.ts
Normal file
169
ts/smartupdate.classes.notifier.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import * as plugins from './smartupdate.plugins.js';
|
||||
import {
|
||||
LOG_LEVELS,
|
||||
type TLogLevel,
|
||||
DEFAULT_MESSAGE_COLOR,
|
||||
MESSAGE_PREFIXES,
|
||||
} from './smartupdate.constants.js';
|
||||
import type { INotificationOptions, IUpdateCheckResult } from './smartupdate.interfaces.js';
|
||||
|
||||
/**
|
||||
* Handles all user-facing notifications and console output
|
||||
*/
|
||||
export class UpdateNotifier {
|
||||
private logLevel: TLogLevel;
|
||||
private useColors: boolean;
|
||||
private customLogger?: (level: TLogLevel, message: string) => void;
|
||||
|
||||
constructor(options: INotificationOptions) {
|
||||
this.logLevel = options.logLevel;
|
||||
this.useColors = options.useColors && !process.env.NO_COLOR;
|
||||
this.customLogger = options.customLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message at the given level should be logged
|
||||
*/
|
||||
private shouldLog(level: TLogLevel): boolean {
|
||||
return LOG_LEVELS[level] <= LOG_LEVELS[this.logLevel];
|
||||
}
|
||||
|
||||
/**
|
||||
* Colorize a string if colors are enabled
|
||||
*/
|
||||
private colorize(text: string, color: any = DEFAULT_MESSAGE_COLOR): string {
|
||||
if (!this.useColors) {
|
||||
return text;
|
||||
}
|
||||
return plugins.consolecolor.coloredString(text, color as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message at the specified level
|
||||
*/
|
||||
private log(level: TLogLevel, message: string): void {
|
||||
if (!this.shouldLog(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.customLogger) {
|
||||
this.customLogger(level, message);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a debug message
|
||||
*/
|
||||
public debug(message: string): void {
|
||||
this.log('DEBUG', `${MESSAGE_PREFIXES.INFO} ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an info message
|
||||
*/
|
||||
public info(message: string): void {
|
||||
this.log('INFO', `${MESSAGE_PREFIXES.SMARTUPDATE} ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a warning message
|
||||
*/
|
||||
public warn(message: string): void {
|
||||
this.log('WARN', `${MESSAGE_PREFIXES.WARN} ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message
|
||||
*/
|
||||
public error(message: string): void {
|
||||
this.log('ERROR', `${MESSAGE_PREFIXES.ERROR} ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify about checking for updates
|
||||
*/
|
||||
public notifyCheckingForUpdate(packageName: string): void {
|
||||
this.info(
|
||||
`checking for newer version of ${this.colorize(packageName)}...`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that the package is up to date
|
||||
*/
|
||||
public notifyUpToDate(packageName: string): void {
|
||||
this.info(
|
||||
`You are running the latest version of ${this.colorize(packageName)}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that an update is available
|
||||
*/
|
||||
public notifyUpdateAvailable(packageName: string, currentVersion: string, latestVersion: string): void {
|
||||
this.warn(`There is a newer version of ${packageName} available on npm.`);
|
||||
this.warn(`Your version: ${currentVersion} | version on npm: ${latestVersion}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that a check was skipped due to rate limiting
|
||||
*/
|
||||
public notifyCheckSkipped(packageName: string, nextCheckMinutes: number): void {
|
||||
const minutes = Math.floor(nextCheckMinutes) + 1;
|
||||
this.info(
|
||||
`Already checked recently. Next check available in ${minutes} minute${minutes !== 1 ? 's' : ''}: ` +
|
||||
this.colorize(packageName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that the changelog is being opened
|
||||
*/
|
||||
public notifyOpeningChangelog(): void {
|
||||
this.info('Opening changelog in browser...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify about a registry error
|
||||
*/
|
||||
public notifyRegistryError(): void {
|
||||
this.warn('Failed to retrieve package information.');
|
||||
this.info('Is the registry down?');
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify with a complete update check result
|
||||
*/
|
||||
public notifyUpdateCheckResult(result: IUpdateCheckResult): void {
|
||||
switch (result.status) {
|
||||
case 'up-to-date':
|
||||
this.notifyUpToDate(result.packageName);
|
||||
break;
|
||||
|
||||
case 'update-available':
|
||||
if (result.latestVersion) {
|
||||
this.notifyUpdateAvailable(
|
||||
result.packageName,
|
||||
result.currentVersion,
|
||||
result.latestVersion
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'check-skipped':
|
||||
if (result.nextCheckTime && result.reason) {
|
||||
const minutesUntilNext = (result.nextCheckTime.getTime() - result.checkTime.getTime()) / 60000;
|
||||
this.notifyCheckSkipped(result.packageName, minutesUntilNext);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
if (result.error) {
|
||||
this.error(result.error.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +1,279 @@
|
||||
import * as plugins from './smartupdate.plugins.js';
|
||||
import { UpdateCacheManager } from './smartupdate.classes.cachemanager.js';
|
||||
import { UpdateNotifier } from './smartupdate.classes.notifier.js';
|
||||
import { DEFAULT_CACHE_DURATION_HOURS, MILLISECONDS_PER_MINUTE, MINUTES_PER_HOUR } from './smartupdate.constants.js';
|
||||
import type {
|
||||
ISmartUpdateOptions,
|
||||
IUpdateCheckOptions,
|
||||
IUpdateCheckResult,
|
||||
} from './smartupdate.interfaces.js';
|
||||
import {
|
||||
RegistryUnavailableError,
|
||||
PackageNotFoundError,
|
||||
} from './smartupdate.errors.js';
|
||||
|
||||
interface ICacheStatus {
|
||||
lastCheck: number;
|
||||
latestVersion: string;
|
||||
performedUpgrade: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* SmartUpdate - Elegant update checking for Node.js packages
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const smartUpdate = new SmartUpdate();
|
||||
* const result = await smartUpdate.checkForUpdate({
|
||||
* packageName: 'lodash',
|
||||
* currentVersion: '1.0.0',
|
||||
* changelogUrl: 'https://example.com/changelog'
|
||||
* });
|
||||
*
|
||||
* if (result.status === 'update-available') {
|
||||
* console.log(`Update available: ${result.latestVersion}`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class SmartUpdate {
|
||||
public npmRegistry: plugins.smartnpm.NpmRegistry;
|
||||
public kvStore = new plugins.npmextra.KeyValueStore('custom', 'global_smartupdate');
|
||||
public readonly npmRegistry: plugins.smartnpm.NpmRegistry;
|
||||
private cacheManager: UpdateCacheManager;
|
||||
private notifier: UpdateNotifier;
|
||||
private options: ISmartUpdateOptions;
|
||||
|
||||
constructor(optionsArg: plugins.smartnpm.INpmRegistryConstructorOptions = {}) {
|
||||
this.npmRegistry = new plugins.smartnpm.NpmRegistry(optionsArg);
|
||||
/**
|
||||
* @deprecated Use the options parameter instead of kvStore property
|
||||
*/
|
||||
public kvStore: plugins.npmextra.KeyValueStore;
|
||||
|
||||
constructor(options: ISmartUpdateOptions = {}) {
|
||||
this.options = options;
|
||||
|
||||
// Initialize npm registry
|
||||
this.npmRegistry = new plugins.smartnpm.NpmRegistry(options.npmRegistryOptions || {});
|
||||
|
||||
// Calculate cache duration in milliseconds
|
||||
const cacheDuration = options.cacheDuration || { hours: DEFAULT_CACHE_DURATION_HOURS };
|
||||
const cacheDurationMs =
|
||||
(cacheDuration.hours || 0) * MINUTES_PER_HOUR * MILLISECONDS_PER_MINUTE +
|
||||
(cacheDuration.minutes || 0) * MILLISECONDS_PER_MINUTE +
|
||||
(cacheDuration.seconds || 0) * 1000;
|
||||
|
||||
// Initialize cache manager
|
||||
this.cacheManager = new UpdateCacheManager({
|
||||
durationMs: cacheDurationMs || DEFAULT_CACHE_DURATION_HOURS * MINUTES_PER_HOUR * MILLISECONDS_PER_MINUTE,
|
||||
});
|
||||
|
||||
// Initialize notifier
|
||||
this.notifier = new UpdateNotifier({
|
||||
logLevel: options.logLevel || 'INFO',
|
||||
useColors: !options.noColor,
|
||||
customLogger: options.customLogger,
|
||||
});
|
||||
|
||||
// Backward compatibility: expose kvStore
|
||||
this.kvStore = (this.cacheManager as any).kvStore;
|
||||
}
|
||||
|
||||
public async checkForCli(npmnameArg: string, compareVersion: string, changelogUrlArg?: string) {
|
||||
// the newData to write
|
||||
const timeStamp = new plugins.smarttime.TimeStamp();
|
||||
const newCacheData: ICacheStatus = {
|
||||
lastCheck: timeStamp.milliSeconds,
|
||||
latestVersion: 'x.x.x',
|
||||
performedUpgrade: false,
|
||||
};
|
||||
/**
|
||||
* Check for updates with caching (primarily for CLI use)
|
||||
*
|
||||
* @deprecated Use checkForUpdate with cacheStrategy: 'always' instead
|
||||
* @param packageName - The npm package name to check
|
||||
* @param currentVersion - The current version to compare against
|
||||
* @param changelogUrl - Optional URL to open if update is available
|
||||
* @returns Promise resolving to true if update available, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const hasUpdate = await smartUpdate.checkForCli('my-cli', '1.0.0', 'https://changelog.url');
|
||||
* ```
|
||||
*/
|
||||
public async checkForCli(
|
||||
packageName: string,
|
||||
currentVersion: string,
|
||||
changelogUrl?: string
|
||||
): Promise<boolean> {
|
||||
const result = await this.checkForUpdate({
|
||||
packageName,
|
||||
currentVersion,
|
||||
changelogUrl,
|
||||
cacheStrategy: 'always',
|
||||
});
|
||||
|
||||
// the comparison data from the keyValue store
|
||||
const retrievedCacheData: ICacheStatus = await this.kvStore.readKey(npmnameArg);
|
||||
return result.status === 'update-available';
|
||||
}
|
||||
|
||||
if (retrievedCacheData) {
|
||||
const lastCheckTimeStamp = plugins.smarttime.TimeStamp.fromMilliSeconds(
|
||||
retrievedCacheData.lastCheck
|
||||
);
|
||||
const tresholdTime = plugins.smarttime.getMilliSecondsFromUnits({ hours: 1 });
|
||||
if (!lastCheckTimeStamp.isOlderThan(timeStamp, tresholdTime)) {
|
||||
newCacheData.lastCheck = lastCheckTimeStamp.milliSeconds;
|
||||
const nextCheckInMinutes =
|
||||
(tresholdTime - (timeStamp.milliSeconds - lastCheckTimeStamp.milliSeconds)) / 60000;
|
||||
console.log(
|
||||
`next update check in less than ${Math.floor(nextCheckInMinutes) + 1} minute(s): ` +
|
||||
`${plugins.consolecolor.coloredString(
|
||||
`${npmnameArg} has already been checked within the last hour.`,
|
||||
'pink'
|
||||
)}`
|
||||
);
|
||||
return false; // don't upgrade if checked within reasonable time
|
||||
/**
|
||||
* Check for updates (modern API)
|
||||
*
|
||||
* @param options - Update check options
|
||||
* @returns Promise resolving to detailed update check result
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await smartUpdate.checkForUpdate({
|
||||
* packageName: 'lodash',
|
||||
* currentVersion: '1.0.0',
|
||||
* changelogUrl: 'https://changelog.url',
|
||||
* openChangelog: true,
|
||||
* cacheStrategy: 'time-based'
|
||||
* });
|
||||
*
|
||||
* console.log(result.status); // 'up-to-date' | 'update-available' | 'check-skipped' | 'error'
|
||||
* ```
|
||||
*/
|
||||
public async checkForUpdate(options: IUpdateCheckOptions): Promise<IUpdateCheckResult> {
|
||||
const {
|
||||
packageName,
|
||||
currentVersion,
|
||||
changelogUrl,
|
||||
openChangelog = true,
|
||||
cacheStrategy = 'time-based',
|
||||
} = options;
|
||||
|
||||
const checkTime = new Date();
|
||||
|
||||
try {
|
||||
// Check if we should use cache or check registry
|
||||
const cacheCheck = await this.cacheManager.shouldCheckRegistry(packageName, cacheStrategy);
|
||||
|
||||
// If we should skip the check due to cache
|
||||
if (!cacheCheck.shouldCheck) {
|
||||
return {
|
||||
status: 'check-skipped',
|
||||
packageName,
|
||||
currentVersion,
|
||||
latestVersion: cacheCheck.cacheStatus?.latestVersion,
|
||||
checkTime,
|
||||
cacheHit: true,
|
||||
nextCheckTime: cacheCheck.nextCheckTime,
|
||||
reason: 'Rate limited - checked recently',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const upgradeBool = await this.check(npmnameArg, compareVersion, changelogUrlArg);
|
||||
if (upgradeBool) {
|
||||
const npmPackage = await this.npmRegistry.getPackageInfo(npmnameArg);
|
||||
newCacheData.latestVersion = npmPackage.version;
|
||||
this.kvStore.writeKey(npmnameArg, newCacheData);
|
||||
}
|
||||
// Fetch package info from registry
|
||||
const npmPackage = await this.getNpmPackageFromRegistry(packageName);
|
||||
|
||||
return upgradeBool;
|
||||
}
|
||||
|
||||
private async getNpmPackageFromRegistry(npmnameArg): Promise<plugins.smartnpm.NpmPackage> {
|
||||
console.log(
|
||||
`smartupdate: checking for newer version of ${plugins.consolecolor.coloredString(
|
||||
npmnameArg,
|
||||
'pink'
|
||||
)}...`
|
||||
);
|
||||
const npmPackage = this.npmRegistry.getPackageInfo(npmnameArg);
|
||||
return npmPackage;
|
||||
if (!npmPackage) {
|
||||
throw new RegistryUnavailableError();
|
||||
}
|
||||
|
||||
// Compare versions
|
||||
const versionNpm = new plugins.smartversion.SmartVersion(npmPackage.version);
|
||||
const versionLocal = new plugins.smartversion.SmartVersion(currentVersion);
|
||||
|
||||
const result: IUpdateCheckResult = {
|
||||
status: versionNpm.greaterThan(versionLocal) ? 'update-available' : 'up-to-date',
|
||||
packageName: npmPackage.name,
|
||||
currentVersion,
|
||||
latestVersion: npmPackage.version,
|
||||
checkTime,
|
||||
cacheHit: false,
|
||||
};
|
||||
|
||||
// Notify user
|
||||
this.notifier.notifyUpdateCheckResult(result);
|
||||
|
||||
// If update is available, handle changelog
|
||||
if (result.status === 'update-available' && changelogUrl && openChangelog && !process.env.CI) {
|
||||
this.notifier.notifyOpeningChangelog();
|
||||
plugins.smartopen.openUrl(changelogUrl);
|
||||
}
|
||||
|
||||
// Update cache if there's an update
|
||||
if (result.status === 'update-available') {
|
||||
const cacheStatus = this.cacheManager.createCacheStatus(npmPackage.version, false);
|
||||
await this.cacheManager.setCached(packageName, cacheStatus);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof RegistryUnavailableError) {
|
||||
this.notifier.notifyRegistryError();
|
||||
} else {
|
||||
this.notifier.error(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'error',
|
||||
packageName,
|
||||
currentVersion,
|
||||
checkTime,
|
||||
cacheHit: false,
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple check for updates without caching
|
||||
*
|
||||
* @deprecated Use checkForUpdate with cacheStrategy: 'never' instead
|
||||
* @param packageName - The npm package name to check
|
||||
* @param currentVersion - The current version to compare against
|
||||
* @param changelogUrl - Optional URL to open if update is available
|
||||
* @returns Promise resolving to true if update available, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const hasUpdate = await smartUpdate.check('lodash', '1.0.0');
|
||||
* ```
|
||||
*/
|
||||
public async check(
|
||||
npmPackageName: string,
|
||||
localVersionStringArg: string,
|
||||
changelogUrlArg?: string
|
||||
) {
|
||||
const npmPackage = await this.getNpmPackageFromRegistry(npmPackageName);
|
||||
if (!npmPackage) {
|
||||
console.log('warn: failed to retrieve package information...');
|
||||
console.log('info: is the registry down?');
|
||||
return;
|
||||
}
|
||||
packageName: string,
|
||||
currentVersion: string,
|
||||
changelogUrl?: string
|
||||
): Promise<boolean> {
|
||||
const result = await this.checkForUpdate({
|
||||
packageName,
|
||||
currentVersion,
|
||||
changelogUrl,
|
||||
cacheStrategy: 'never',
|
||||
});
|
||||
|
||||
// create Version objects
|
||||
const versionNpm = new plugins.smartversion.SmartVersion(npmPackage.version);
|
||||
const versionLocal = new plugins.smartversion.SmartVersion(localVersionStringArg);
|
||||
if (!versionNpm.greaterThan(versionLocal)) {
|
||||
console.log(
|
||||
`smartupdate: You are running the latest version of ${plugins.consolecolor.coloredString(
|
||||
npmPackage.name,
|
||||
'pink'
|
||||
)}`
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
console.log(`warn: There is a newer version of ${npmPackage.name} available on npm.`);
|
||||
console.log(
|
||||
`warn: Your version: ${versionLocal.versionString} | version on npm: ${versionNpm.versionString}`
|
||||
);
|
||||
if (!process.env.CI && changelogUrlArg) {
|
||||
console.log('trying to open changelog...');
|
||||
plugins.smartopen.openUrl(changelogUrlArg);
|
||||
}
|
||||
return true;
|
||||
return result.status === 'update-available';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest version of a package
|
||||
*
|
||||
* @param packageName - The npm package name
|
||||
* @returns Promise resolving to the latest version string
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const latestVersion = await smartUpdate.getLatestVersion('lodash');
|
||||
* console.log(latestVersion); // e.g., "4.17.21"
|
||||
* ```
|
||||
*/
|
||||
public async getLatestVersion(packageName: string): Promise<string> {
|
||||
const npmPackage = await this.getNpmPackageFromRegistry(packageName);
|
||||
if (!npmPackage) {
|
||||
throw new PackageNotFoundError(packageName);
|
||||
}
|
||||
return npmPackage.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache for a specific package
|
||||
*
|
||||
* @param packageName - The package name to clear cache for
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await smartUpdate.clearCache('lodash');
|
||||
* ```
|
||||
*/
|
||||
public async clearCache(packageName: string): Promise<void> {
|
||||
await this.cacheManager.clearCache(packageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch package information from the npm registry
|
||||
* @private
|
||||
*/
|
||||
private async getNpmPackageFromRegistry(packageName: string): Promise<plugins.smartnpm.NpmPackage | null> {
|
||||
this.notifier.notifyCheckingForUpdate(packageName);
|
||||
try {
|
||||
const npmPackage = await this.npmRegistry.getPackageInfo(packageName);
|
||||
return npmPackage;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
ts/smartupdate.constants.ts
Normal file
30
ts/smartupdate.constants.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Constants used throughout the smartupdate library
|
||||
*/
|
||||
|
||||
// Time constants
|
||||
export const MILLISECONDS_PER_MINUTE = 60_000;
|
||||
export const MINUTES_PER_HOUR = 60;
|
||||
export const DEFAULT_CACHE_DURATION_HOURS = 1;
|
||||
|
||||
// Console output constants
|
||||
export const DEFAULT_MESSAGE_COLOR = 'pink';
|
||||
|
||||
// Log level constants
|
||||
export const LOG_LEVELS = {
|
||||
SILENT: 0,
|
||||
ERROR: 1,
|
||||
WARN: 2,
|
||||
INFO: 3,
|
||||
DEBUG: 4,
|
||||
} as const;
|
||||
|
||||
export type TLogLevel = keyof typeof LOG_LEVELS;
|
||||
|
||||
// Message prefixes
|
||||
export const MESSAGE_PREFIXES = {
|
||||
ERROR: 'error:',
|
||||
WARN: 'warn:',
|
||||
INFO: 'info:',
|
||||
SMARTUPDATE: 'smartupdate:',
|
||||
} as const;
|
||||
65
ts/smartupdate.errors.ts
Normal file
65
ts/smartupdate.errors.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Base error class for smartupdate errors
|
||||
*/
|
||||
export class SmartUpdateError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'SmartUpdateError';
|
||||
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when the npm registry is unavailable
|
||||
*/
|
||||
export class RegistryUnavailableError extends SmartUpdateError {
|
||||
constructor(message: string = 'Failed to retrieve package information from npm registry') {
|
||||
super(message);
|
||||
this.name = 'RegistryUnavailableError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when a package is not found in the registry
|
||||
*/
|
||||
export class PackageNotFoundError extends SmartUpdateError {
|
||||
public readonly packageName: string;
|
||||
|
||||
constructor(packageName: string) {
|
||||
super(`Package '${packageName}' not found in npm registry`);
|
||||
this.name = 'PackageNotFoundError';
|
||||
this.packageName = packageName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when a version string is invalid
|
||||
*/
|
||||
export class InvalidVersionError extends SmartUpdateError {
|
||||
public readonly version: string;
|
||||
|
||||
constructor(version: string, reason?: string) {
|
||||
const message = reason
|
||||
? `Invalid version string '${version}': ${reason}`
|
||||
: `Invalid version string '${version}'`;
|
||||
super(message);
|
||||
this.name = 'InvalidVersionError';
|
||||
this.version = version;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when package name validation fails
|
||||
*/
|
||||
export class InvalidPackageNameError extends SmartUpdateError {
|
||||
public readonly packageName: string;
|
||||
|
||||
constructor(packageName: string) {
|
||||
super(`Invalid package name '${packageName}'. Package names must follow npm naming conventions.`);
|
||||
this.name = 'InvalidPackageNameError';
|
||||
this.packageName = packageName;
|
||||
}
|
||||
}
|
||||
171
ts/smartupdate.interfaces.ts
Normal file
171
ts/smartupdate.interfaces.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { TLogLevel } from './smartupdate.constants.js';
|
||||
import type * as smartnpm from '@push.rocks/smartnpm';
|
||||
|
||||
/**
|
||||
* Cache status stored for each package
|
||||
*/
|
||||
export interface ICacheStatus {
|
||||
lastCheck: number;
|
||||
latestVersion: string;
|
||||
performedUpgrade: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for configuring the SmartUpdate instance
|
||||
*/
|
||||
export interface ISmartUpdateOptions {
|
||||
/**
|
||||
* Options for the npm registry connection
|
||||
*/
|
||||
npmRegistryOptions?: smartnpm.INpmRegistryConstructorOptions;
|
||||
|
||||
/**
|
||||
* Cache duration configuration
|
||||
* @default { hours: 1 }
|
||||
*/
|
||||
cacheDuration?: {
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
seconds?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Logging level
|
||||
* @default 'INFO'
|
||||
*/
|
||||
logLevel?: TLogLevel;
|
||||
|
||||
/**
|
||||
* Custom logger function
|
||||
* If provided, this will be used instead of console output
|
||||
*/
|
||||
customLogger?: (level: TLogLevel, message: string) => void;
|
||||
|
||||
/**
|
||||
* Disable colored output
|
||||
* @default false
|
||||
*/
|
||||
noColor?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for checking for updates
|
||||
*/
|
||||
export interface IUpdateCheckOptions {
|
||||
/**
|
||||
* The npm package name to check
|
||||
*/
|
||||
packageName: string;
|
||||
|
||||
/**
|
||||
* The current version to compare against
|
||||
*/
|
||||
currentVersion: string;
|
||||
|
||||
/**
|
||||
* Optional URL to the changelog
|
||||
* If provided and an update is available, the changelog will be opened
|
||||
*/
|
||||
changelogUrl?: string;
|
||||
|
||||
/**
|
||||
* Whether to open the changelog URL automatically
|
||||
* Only applies if running in a non-CI environment
|
||||
* @default true
|
||||
*/
|
||||
openChangelog?: boolean;
|
||||
|
||||
/**
|
||||
* Cache strategy for this check
|
||||
* - 'always': Always check cache first (default for CLI)
|
||||
* - 'never': Always check registry, bypass cache
|
||||
* - 'time-based': Check based on cache duration
|
||||
* @default 'time-based'
|
||||
*/
|
||||
cacheStrategy?: 'always' | 'never' | 'time-based';
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of an update check
|
||||
*/
|
||||
export interface IUpdateCheckResult {
|
||||
/**
|
||||
* Status of the update check
|
||||
*/
|
||||
status: 'up-to-date' | 'update-available' | 'check-skipped' | 'error';
|
||||
|
||||
/**
|
||||
* The current version being checked
|
||||
*/
|
||||
currentVersion: string;
|
||||
|
||||
/**
|
||||
* The latest version available (if found)
|
||||
*/
|
||||
latestVersion?: string;
|
||||
|
||||
/**
|
||||
* The package name that was checked
|
||||
*/
|
||||
packageName: string;
|
||||
|
||||
/**
|
||||
* Time when the check was performed
|
||||
*/
|
||||
checkTime: Date;
|
||||
|
||||
/**
|
||||
* Whether this result came from cache
|
||||
*/
|
||||
cacheHit: boolean;
|
||||
|
||||
/**
|
||||
* When the next check can be performed (if check was skipped due to rate limiting)
|
||||
*/
|
||||
nextCheckTime?: Date;
|
||||
|
||||
/**
|
||||
* Error details if status is 'error'
|
||||
*/
|
||||
error?: Error;
|
||||
|
||||
/**
|
||||
* Reason for the result (human-readable explanation)
|
||||
*/
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the cache manager
|
||||
*/
|
||||
export interface ICacheOptions {
|
||||
/**
|
||||
* Cache duration in milliseconds
|
||||
*/
|
||||
durationMs: number;
|
||||
|
||||
/**
|
||||
* Identifier for the key-value store
|
||||
*/
|
||||
storeIdentifier?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the notifier
|
||||
*/
|
||||
export interface INotificationOptions {
|
||||
/**
|
||||
* Log level for notifications
|
||||
*/
|
||||
logLevel: TLogLevel;
|
||||
|
||||
/**
|
||||
* Whether to use colors in output
|
||||
*/
|
||||
useColors: boolean;
|
||||
|
||||
/**
|
||||
* Custom logger function
|
||||
*/
|
||||
customLogger?: (level: TLogLevel, message: string) => void;
|
||||
}
|
||||
Reference in New Issue
Block a user