Bump to TypeScript 5.5 and enable noImplicitAny
This commit upgrades TypeScript from 5.4 to 5.5 and enables the
`noImplicitAny` option for stricter type checking. It refactors code to
comply with `noImplicitAny` and adapts to new TypeScript features and
limitations.
Key changes:
- Migrate from TypeScript 5.4 to 5.5
- Enable `noImplicitAny` for stricter type checking
- Refactor code to comply with new TypeScript features and limitations
Other supporting changes:
- Refactor progress bar handling for type safety
- Drop 'I' prefix from interfaces to align with new code convention
- Update TypeScript target from `ES2017` and `ES2018`.
This allows named capturing groups. Otherwise, new TypeScript compiler
does not compile the project and shows the following error:
```
...
TimestampedFilenameGenerator.spec.ts:105:23 - error TS1503: Named capturing groups are only available when targeting 'ES2018' or later
const pattern = /^(?<timestamp>\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})-(?<scriptName>[^.]+?)(?:\.(?<extension>[^.]+))?$/;// timestamp-scriptName.extension
...
```
- Refactor usage of `electron-progressbar` for type safety and
less complexity.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { app, dialog } from 'electron/main';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { UpdateProgressBar } from './UpdateProgressBar';
|
||||
import { UpdateProgressBar } from './ProgressBar/UpdateProgressBar';
|
||||
import { getAutoUpdater } from './ElectronAutoUpdaterFactory';
|
||||
import type { AppUpdater, UpdateInfo } from 'electron-updater';
|
||||
import type { ProgressInfo } from 'electron-builder';
|
||||
@@ -26,11 +26,13 @@ function startHandlingUpdateProgress(autoUpdater: AppUpdater) {
|
||||
So the indeterminate progress will continue until download is finished.
|
||||
*/
|
||||
ElectronLogger.debug('@download-progress@\n', progress);
|
||||
progressBar.showProgress(progress);
|
||||
if (progressBar.isOpen) { // May be closed by the user
|
||||
progressBar.showProgress(progress);
|
||||
}
|
||||
});
|
||||
autoUpdater.on('update-downloaded', async (info: UpdateInfo) => {
|
||||
ElectronLogger.info('@update-downloaded@\n', info);
|
||||
progressBar.close();
|
||||
progressBar.closeIfOpen();
|
||||
await handleUpdateDownloaded(autoUpdater);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { unlink, mkdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { app } from 'electron/main';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { UpdateProgressBar } from '../UpdateProgressBar';
|
||||
import { UpdateProgressBar } from '../ProgressBar/UpdateProgressBar';
|
||||
import { retryFileSystemAccess } from './RetryFileSystemAccess';
|
||||
import type { UpdateInfo } from 'electron-updater';
|
||||
import type { ReadableStream } from 'node:stream/web';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { GitHubProjectDetails } from '@/domain/Project/GitHubProjectDetails';
|
||||
import { Version } from '@/domain/Version';
|
||||
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { UpdateProgressBar } from '../UpdateProgressBar';
|
||||
import { UpdateProgressBar } from '../ProgressBar/UpdateProgressBar';
|
||||
import {
|
||||
promptForManualUpdate, promptInstallerOpenError,
|
||||
promptIntegrityCheckFailure, promptDownloadError,
|
||||
@@ -100,7 +100,7 @@ async function withProgressBar(
|
||||
) {
|
||||
const progressBar = new UpdateProgressBar();
|
||||
await action(progressBar);
|
||||
progressBar.close();
|
||||
progressBar.closeIfOpen();
|
||||
}
|
||||
|
||||
async function isIntegrityPreserved(
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// @ts-expect-error Outdated `@types/electron-progressbar` causes build failure on macOS
|
||||
import ProgressBar from 'electron-progressbar';
|
||||
import { BrowserWindow } from 'electron/main';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import type {
|
||||
InitialProgressBarWindowOptions,
|
||||
ProgressBarUpdater,
|
||||
ProgressBarStatus,
|
||||
ProgressBarUpdateCallback, ProgressBarWithLifecycle,
|
||||
} from './ProgressBarWithLifecycle';
|
||||
|
||||
/**
|
||||
* It provides a type-safe way to manage `electron-progressbar` instance,
|
||||
* through its lifecycle, ensuring correct usage, state transitions, and cleanup.
|
||||
*/
|
||||
export class ElectronProgressBarWithLifecycle implements ProgressBarWithLifecycle {
|
||||
private state: ProgressBarWithState = { status: 'closed' };
|
||||
|
||||
public readonly statusChanged = new EventSource<ProgressBarStatus>();
|
||||
|
||||
private readyCallbacks = new Array<ProgressBarUpdateCallback>();
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
) { }
|
||||
|
||||
public update(handler: ProgressBarUpdateCallback): void {
|
||||
switch (this.state.status) { // eslint-disable-line default-case
|
||||
case 'closed':
|
||||
// Throwing an error here helps catch bugs early in the development process.
|
||||
throw new Error('Cannot update the progress bar because it is not currently open.');
|
||||
case 'ready':
|
||||
handler(wrapForUpdate(this.state));
|
||||
break;
|
||||
case 'loading':
|
||||
this.readyCallbacks.push(handler);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public resetAndOpen(options: InitialProgressBarWindowOptions): void {
|
||||
this.closeIfOpen();
|
||||
const bar = createElectronProgressBar(options);
|
||||
this.state = { status: 'loading', progressBar: bar };
|
||||
bar.on('ready', () => this.handleReadyEvent(bar));
|
||||
bar.on('aborted' /* closed by user */, (value: number) => {
|
||||
this.changeState({ status: 'closed' });
|
||||
this.logger.info(`Progress bar window closed by user. State: ${this.state.status}, Value: ${value}.`);
|
||||
});
|
||||
}
|
||||
|
||||
public closeIfOpen() {
|
||||
if (this.state.status === 'closed') {
|
||||
return;
|
||||
}
|
||||
this.state.progressBar.close();
|
||||
this.changeState({
|
||||
status: 'closed',
|
||||
});
|
||||
}
|
||||
|
||||
private handleReadyEvent(bar: ProgressBar): void {
|
||||
if (this.state.status !== 'loading' || this.state.progressBar !== bar) {
|
||||
// Handle race conditions if `open` called rapidly without closing to avoid leaks
|
||||
this.logger.warn('Unexpected state when handling ready event. Closing the progress bar.');
|
||||
bar.close();
|
||||
return;
|
||||
}
|
||||
const readyBar: ReadyProgressBar = {
|
||||
status: 'ready',
|
||||
progressBar: bar,
|
||||
browserWindow: getWindow(bar),
|
||||
};
|
||||
this.readyCallbacks.forEach((callback) => callback(wrapForUpdate(readyBar)));
|
||||
this.changeState(readyBar);
|
||||
}
|
||||
|
||||
private changeState(newState: ProgressBarWithState): void {
|
||||
if (isSameState(this.state, newState)) {
|
||||
return;
|
||||
}
|
||||
this.readyCallbacks = [];
|
||||
this.state = newState;
|
||||
this.statusChanged.notify(newState.status);
|
||||
}
|
||||
}
|
||||
|
||||
type ProgressBarWithState = { readonly status: 'closed' }
|
||||
| { readonly status: 'loading', readonly progressBar: ProgressBar }
|
||||
| ReadyProgressBar;
|
||||
|
||||
interface ReadyProgressBar {
|
||||
readonly status: 'ready';
|
||||
readonly progressBar: ProgressBar;
|
||||
readonly browserWindow: BrowserWindow;
|
||||
}
|
||||
|
||||
function getWindow(bar: ProgressBar): BrowserWindow {
|
||||
// Note: The ProgressBar library does not provide a public method or event
|
||||
// to access the BrowserWindow, so we access the internal `_window` property directly.
|
||||
if (!('_window' in bar)) {
|
||||
throw new Error('Unable to access the progress bar window.');
|
||||
}
|
||||
const browserWindow = bar._window as BrowserWindow; // eslint-disable-line no-underscore-dangle
|
||||
if (!browserWindow) {
|
||||
throw new Error('Missing internal browser window');
|
||||
}
|
||||
return browserWindow;
|
||||
}
|
||||
|
||||
function isSameState( // eslint-disable-line consistent-return
|
||||
first: ProgressBarWithState,
|
||||
second: ProgressBarWithState,
|
||||
): boolean {
|
||||
switch (first.status) { // eslint-disable-line default-case
|
||||
case 'closed':
|
||||
return second.status === 'closed';
|
||||
case 'loading':
|
||||
return second.status === 'loading'
|
||||
&& second.progressBar === first.progressBar;
|
||||
case 'ready':
|
||||
return second.status === 'ready'
|
||||
&& second.progressBar === first.progressBar
|
||||
&& second.browserWindow === first.browserWindow;
|
||||
}
|
||||
}
|
||||
|
||||
function wrapForUpdate(bar: ReadyProgressBar): ProgressBarUpdater {
|
||||
return {
|
||||
setText: (text: string) => {
|
||||
bar.progressBar.detail = text;
|
||||
},
|
||||
setClosable: (closable: boolean) => {
|
||||
bar.browserWindow.setClosable(closable);
|
||||
},
|
||||
setProgress: (progress: number) => {
|
||||
bar.progressBar.value = progress;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createElectronProgressBar(
|
||||
options: InitialProgressBarWindowOptions,
|
||||
): ProgressBar {
|
||||
const bar = new ProgressBar({
|
||||
indeterminate: options.type === 'indeterminate',
|
||||
title: options.title,
|
||||
text: options.initialText,
|
||||
});
|
||||
if (options.type === 'percentile') { // Indeterminate progress bar does not fire `completed` event, see `electron-progressbar` docs
|
||||
bar.on('completed', () => {
|
||||
bar.detail = options.textOnCompleted;
|
||||
});
|
||||
}
|
||||
return bar;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
|
||||
/*
|
||||
Defines interfaces to abstract the progress bar implementation,
|
||||
serving as an anti-corruption layer. This approach allows for
|
||||
flexibility in switching implementations if needed in the future,
|
||||
while maintaining a consistent API for the rest of the application.
|
||||
*/
|
||||
export interface ProgressBarWithLifecycle {
|
||||
resetAndOpen(options: InitialProgressBarWindowOptions): void;
|
||||
closeIfOpen(): void;
|
||||
update(handler: ProgressBarUpdateCallback): void;
|
||||
readonly statusChanged: IEventSource<ProgressBarStatus>;
|
||||
}
|
||||
|
||||
export type ProgressBarStatus = 'closed' | 'loading' | 'ready';
|
||||
|
||||
export type ProgressBarUpdateCallback = (bar: ProgressBarUpdater) => void;
|
||||
|
||||
export interface InitialProgressBarWindowOptions {
|
||||
readonly type: 'indeterminate' | 'percentile';
|
||||
readonly title: string,
|
||||
readonly initialText: string;
|
||||
readonly textOnCompleted: string;
|
||||
}
|
||||
|
||||
export interface ProgressBarUpdater {
|
||||
setText(text: string): void;
|
||||
setClosable(closable: boolean): void;
|
||||
setProgress(progress: number): void;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { app } from 'electron/main';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronProgressBarWithLifecycle } from './ElectronProgressBarWithLifecycle';
|
||||
import type { ProgressInfo } from 'electron-builder';
|
||||
import type { InitialProgressBarWindowOptions, ProgressBarWithLifecycle } from './ProgressBarWithLifecycle';
|
||||
|
||||
export class UpdateProgressBar {
|
||||
private visibilityState: ProgressBarVisibilityState = 'closed';
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
private readonly currentApp: Electron.App = app,
|
||||
private readonly progressBar: ProgressBarWithLifecycle = new ElectronProgressBarWithLifecycle(),
|
||||
) {
|
||||
this.progressBar.statusChanged.on((status) => {
|
||||
if (status === 'closed') {
|
||||
this.visibilityState = 'closed';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public get isOpen(): boolean {
|
||||
return this.visibilityState !== 'closed';
|
||||
}
|
||||
|
||||
public showIndeterminateState() {
|
||||
if (this.visibilityState === 'showingIndeterminate') {
|
||||
return;
|
||||
}
|
||||
this.progressBar.resetAndOpen(createInitialProgressBarWindowOptions('indeterminate', this.currentApp.name));
|
||||
this.visibilityState = 'showingIndeterminate';
|
||||
}
|
||||
|
||||
public showProgress(progress: ProgressInfo) {
|
||||
const percentage = getUpdatePercent(progress);
|
||||
this.showPercentage(percentage);
|
||||
}
|
||||
|
||||
public showPercentage(percentage: number) {
|
||||
if (this.visibilityState !== 'showingPercentile') {
|
||||
this.progressBar.resetAndOpen(createInitialProgressBarWindowOptions('percentile', this.currentApp.name));
|
||||
this.visibilityState = 'showingPercentile';
|
||||
}
|
||||
this.progressBar.update((bar) => {
|
||||
bar.setProgress(percentage);
|
||||
bar.setText(`${percentage}% ...`);
|
||||
});
|
||||
}
|
||||
|
||||
public showError(e: Error) {
|
||||
this.logger.warn(`Error displayed in progress bar. Visibility state: ${this.visibilityState}. Error message: ${e.message}`);
|
||||
if (this.visibilityState === 'closed') {
|
||||
throw new Error('Cannot display error because the progress bar is not visible.');
|
||||
}
|
||||
this.progressBar.update((bar) => {
|
||||
bar.setText('An error occurred while downloading updates.'
|
||||
+ `\n${e && e.message ? e.message : e}`);
|
||||
bar.setClosable(true);
|
||||
});
|
||||
}
|
||||
|
||||
public closeIfOpen() {
|
||||
if (this.visibilityState === 'closed') {
|
||||
return;
|
||||
}
|
||||
this.progressBar.closeIfOpen();
|
||||
this.visibilityState = 'closed';
|
||||
}
|
||||
}
|
||||
|
||||
type ProgressBarVisibilityState = 'closed' | 'showingIndeterminate' | 'showingPercentile';
|
||||
|
||||
function getUpdatePercent(progress: ProgressInfo) {
|
||||
let { percent } = progress;
|
||||
if (percent) {
|
||||
percent = Math.round(percent * 100) / 100;
|
||||
}
|
||||
return percent;
|
||||
}
|
||||
|
||||
function createInitialProgressBarWindowOptions(
|
||||
type: 'indeterminate' | 'percentile',
|
||||
appName: string,
|
||||
): InitialProgressBarWindowOptions {
|
||||
return {
|
||||
type,
|
||||
title: `${appName} Update`,
|
||||
initialText: `Downloading ${appName} update...`,
|
||||
textOnCompleted: 'Download completed.',
|
||||
};
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import ProgressBar from 'electron-progressbar';
|
||||
import { app, BrowserWindow } from 'electron/main';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import type { ProgressInfo } from 'electron-builder';
|
||||
|
||||
export class UpdateProgressBar {
|
||||
private progressBar: ProgressBar | undefined;
|
||||
|
||||
private get innerProgressBarWindow(): BrowserWindow {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return this.progressBar._window;
|
||||
}
|
||||
|
||||
private showingProgress = false;
|
||||
|
||||
public showIndeterminateState() {
|
||||
this.progressBar?.close();
|
||||
this.progressBar = progressBarFactory.createWithIndeterminateState();
|
||||
}
|
||||
|
||||
public showProgress(progress: ProgressInfo) {
|
||||
const percentage = getUpdatePercent(progress);
|
||||
this.showPercentage(percentage);
|
||||
}
|
||||
|
||||
public showPercentage(percentage: number) {
|
||||
if (!this.showingProgress) { // First time showing progress
|
||||
this.progressBar?.close();
|
||||
this.showingProgress = true;
|
||||
this.progressBar = progressBarFactory.createWithPercentile(percentage);
|
||||
} else {
|
||||
this.progressBar.value = percentage;
|
||||
}
|
||||
}
|
||||
|
||||
public showError(e: Error) {
|
||||
const reportUpdateError = () => {
|
||||
this.progressBar.detail = 'An error occurred while fetching updates.'
|
||||
+ `\n${e && e.message ? e.message : e}`;
|
||||
this.innerProgressBarWindow.setClosable(true);
|
||||
};
|
||||
if (this.progressBar?.innerProgressBarWindow) {
|
||||
reportUpdateError();
|
||||
} else {
|
||||
this.progressBar?.on('ready', () => reportUpdateError());
|
||||
}
|
||||
}
|
||||
|
||||
public close() {
|
||||
if (!this.progressBar?.isCompleted()) {
|
||||
this.progressBar?.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUpdatePercent(progress: ProgressInfo) {
|
||||
let { percent } = progress;
|
||||
if (percent) {
|
||||
percent = Math.round(percent * 100) / 100;
|
||||
}
|
||||
return percent;
|
||||
}
|
||||
|
||||
const progressBarFactory = {
|
||||
createWithIndeterminateState: () => {
|
||||
return new ProgressBar({
|
||||
title: `${app.name} Update`,
|
||||
text: `Downloading ${app.name} update...`,
|
||||
});
|
||||
},
|
||||
createWithPercentile: (initialPercentage: number) => {
|
||||
const progressBar = new ProgressBar({
|
||||
indeterminate: false,
|
||||
title: `${app.name} Update`,
|
||||
text: `Downloading ${app.name} update...`,
|
||||
detail: `${initialPercentage}% ...`,
|
||||
initialValue: initialPercentage,
|
||||
});
|
||||
progressBar
|
||||
.on('completed', () => {
|
||||
progressBar.detail = 'Download completed.';
|
||||
})
|
||||
.on('aborted', (value: number) => {
|
||||
ElectronLogger.info(`Progress aborted... ${value}`);
|
||||
})
|
||||
.on('progress', (value: number) => {
|
||||
progressBar.detail = `${value}% ...`;
|
||||
})
|
||||
.on('ready', () => {
|
||||
// initialValue doesn't set the UI, so this is needed to render it correctly
|
||||
progressBar.value = initialPercentage;
|
||||
});
|
||||
return progressBar;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user