Refactor code to comply with ESLint rules

Major refactoring using ESLint with rules from AirBnb and Vue.

Enable most of the ESLint rules and do necessary linting in the code.
Also add more information for rules that are disabled to describe what
they are and why they are disabled.

Allow logging (`console.log`) in test files, and in development mode
(e.g. when working with `npm run serve`), but disable it when
environment is production (as pre-configured by Vue). Also add flag
(`--mode production`) in `lint:eslint` command so production linting is
executed earlier in lifecycle.

Disable rules that requires a separate work. Such as ESLint rules that
are broken in TypeScript: no-useless-constructor (eslint/eslint#14118)
and no-shadow (eslint/eslint#13014).
This commit is contained in:
undergroundwires
2022-01-02 18:20:14 +01:00
parent 96265b75de
commit 5b1fbe1e2f
341 changed files with 16126 additions and 15101 deletions

View File

@@ -1,13 +1,13 @@
export class Clipboard {
public static copyText(text: string): void {
const el = document.createElement('textarea');
el.value = text;
el.setAttribute('readonly', ''); // to avoid focus
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
}
public static copyText(text: string): void {
const el = document.createElement('textarea');
el.value = text;
el.setAttribute('readonly', ''); // to avoid focus
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
}
}

View File

@@ -1,73 +1,79 @@
import { Environment } from '@/application/Environment/Environment';
import os from 'os';
import path from 'path';
import fs from 'fs';
// eslint-disable-next-line camelcase
import child_process from 'child_process';
import { Environment } from '@/application/Environment/Environment';
import { OperatingSystem } from '@/domain/OperatingSystem';
export class CodeRunner {
constructor(
private readonly node = getNodeJs(),
private readonly environment = Environment.CurrentEnvironment) {
}
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
await this.node.fs.promises.mkdir(dir, {recursive: true});
const filePath = this.node.path.join(dir, `run.${fileExtension}`);
await this.node.fs.promises.writeFile(filePath, code);
await this.node.fs.promises.chmod(filePath, '755');
const command = getExecuteCommand(filePath, this.environment);
this.node.child_process.exec(command);
}
constructor(
private readonly node = getNodeJs(),
private readonly environment = Environment.CurrentEnvironment,
) {
}
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
await this.node.fs.promises.mkdir(dir, { recursive: true });
const filePath = this.node.path.join(dir, `run.${fileExtension}`);
await this.node.fs.promises.writeFile(filePath, code);
await this.node.fs.promises.chmod(filePath, '755');
const command = getExecuteCommand(filePath, this.environment);
this.node.child_process.exec(command);
}
}
function getExecuteCommand(scriptPath: string, environment: Environment): string {
switch (environment.os) {
case OperatingSystem.macOS:
return `open -a Terminal.app ${scriptPath}`;
// Another option with graphical sudo would be
// `osascript -e "do shell script \\"${scriptPath}\\" with administrator privileges"`
// However it runs in background
case OperatingSystem.Windows:
return scriptPath;
default:
throw Error('undefined os');
}
switch (environment.os) {
case OperatingSystem.macOS:
return `open -a Terminal.app ${scriptPath}`;
// Another option with graphical sudo would be
// `osascript -e "do shell script \\"${scriptPath}\\" with administrator privileges"`
// However it runs in background
case OperatingSystem.Windows:
return scriptPath;
default:
throw Error('undefined os');
}
}
function getNodeJs(): INodeJs {
return { os, path, fs, child_process };
return {
os, path, fs, child_process,
};
}
export interface INodeJs {
os: INodeOs;
path: INodePath;
fs: INodeFs;
child_process: INodeChildProcess;
os: INodeOs;
path: INodePath;
fs: INodeFs;
// eslint-disable-next-line camelcase
child_process: INodeChildProcess;
}
export interface INodeOs {
tmpdir(): string;
tmpdir(): string;
}
export interface INodePath {
join(...paths: string[]): string;
join(...paths: string[]): string;
}
export interface INodeChildProcess {
exec(command: string): void;
exec(command: string): void;
}
export interface INodeFs {
readonly promises: INodeFsPromises;
readonly promises: INodeFsPromises;
}
interface INodeFsPromisesMakeDirectoryOptions {
recursive?: boolean;
recursive?: boolean;
}
interface INodeFsPromises { // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v13/fs.d.ts
chmod(path: string, mode: string | number): Promise<void>;
mkdir(path: string, options: INodeFsPromisesMakeDirectoryOptions): Promise<string>;
writeFile(path: string, data: string): Promise<void>;
chmod(path: string, mode: string | number): Promise<void>;
mkdir(path: string, options: INodeFsPromisesMakeDirectoryOptions): Promise<string>;
writeFile(path: string, data: string): Promise<void>;
}

View File

@@ -1,13 +1,13 @@
import { IEntity } from './IEntity';
export abstract class BaseEntity<TId> implements IEntity<TId> {
protected constructor(public id: TId) {
if (typeof id !== 'number' && !id) {
throw new Error('Id cannot be null or empty');
}
protected constructor(public id: TId) {
if (typeof id !== 'number' && !id) {
throw new Error('Id cannot be null or empty');
}
}
public equals(otherId: TId): boolean {
return this.id === otherId;
}
public equals(otherId: TId): boolean {
return this.id === otherId;
}
}

View File

@@ -1,5 +1,5 @@
/** Aggregate root */
export interface IEntity<TId> {
id: TId;
equals(other: TId): boolean;
id: TId;
equals(other: TId): boolean;
}

View File

@@ -1,27 +1,27 @@
import { EventHandler, IEventSource, IEventSubscription } from './IEventSource';
export class EventSource<T> implements IEventSource<T> {
private handlers = new Map<number, EventHandler<T>>();
private handlers = new Map<number, EventHandler<T>>();
public on(handler: EventHandler<T>): IEventSubscription {
const id = this.getUniqueEventHandlerId();
this.handlers.set(id, handler);
return {
unsubscribe: () => this.handlers.delete(id),
};
}
public on(handler: EventHandler<T>): IEventSubscription {
const id = this.getUniqueEventHandlerId();
this.handlers.set(id, handler);
return {
unsubscribe: () => this.handlers.delete(id),
};
}
public notify(data: T) {
for (const handler of Array.from(this.handlers.values())) {
handler(data);
}
public notify(data: T) {
for (const handler of Array.from(this.handlers.values())) {
handler(data);
}
}
private getUniqueEventHandlerId(): number {
const id = Math.random();
if (this.handlers.has(id)) {
return this.getUniqueEventHandlerId();
}
return id;
private getUniqueEventHandlerId(): number {
const id = Math.random();
if (this.handlers.has(id)) {
return this.getUniqueEventHandlerId();
}
return id;
}
}

View File

@@ -1,12 +1,14 @@
import { IEventSubscription } from './IEventSource';
export class EventSubscriptionCollection {
private readonly subscriptions = new Array<IEventSubscription>();
public register(...subscriptions: IEventSubscription[]) {
this.subscriptions.push(...subscriptions);
}
public unsubscribeAll() {
this.subscriptions.forEach((listener) => listener.unsubscribe());
this.subscriptions.splice(0, this.subscriptions.length);
}
private readonly subscriptions = new Array<IEventSubscription>();
public register(...subscriptions: IEventSubscription[]) {
this.subscriptions.push(...subscriptions);
}
public unsubscribeAll() {
this.subscriptions.forEach((listener) => listener.unsubscribe());
this.subscriptions.splice(0, this.subscriptions.length);
}
}

View File

@@ -1,11 +1,9 @@
export interface IEventSource<T> {
on(handler: EventHandler<T>): IEventSubscription;
on(handler: EventHandler<T>): IEventSubscription;
}
export interface IEventSubscription {
unsubscribe(): void;
unsubscribe(): void;
}
export type EventHandler<T> = (data: T) => void;

View File

@@ -1,11 +1,11 @@
import { IEntity } from '../Entity/IEntity';
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
readonly length: number;
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
getById(id: TKey): TEntity | undefined;
addItem(item: TEntity): void;
addOrUpdateItem(item: TEntity): void;
removeItem(id: TKey): void;
exists(id: TKey): boolean;
readonly length: number;
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
getById(id: TKey): TEntity | undefined;
addItem(item: TEntity): void;
addOrUpdateItem(item: TEntity): void;
removeItem(id: TKey): void;
exists(id: TKey): boolean;
}

View File

@@ -1,59 +1,60 @@
import { IEntity } from '../Entity/IEntity';
import { IRepository } from './IRepository';
export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> implements IRepository<TKey, TEntity> {
private readonly items: TEntity[];
export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>>
implements IRepository<TKey, TEntity> {
private readonly items: TEntity[];
constructor(items?: TEntity[]) {
this.items = items || new Array<TEntity>();
}
constructor(items?: TEntity[]) {
this.items = items || new Array<TEntity>();
}
public get length(): number {
return this.items.length;
}
public get length(): number {
return this.items.length;
}
public getItems(predicate?: (entity: TEntity) => boolean): TEntity[] {
return predicate ? this.items.filter(predicate) : this.items;
}
public getItems(predicate?: (entity: TEntity) => boolean): TEntity[] {
return predicate ? this.items.filter(predicate) : this.items;
}
public getById(id: TKey): TEntity | undefined {
const items = this.getItems((entity) => entity.id === id);
if (!items.length) {
return undefined;
}
return items[0];
public getById(id: TKey): TEntity | undefined {
const items = this.getItems((entity) => entity.id === id);
if (!items.length) {
return undefined;
}
return items[0];
}
public addItem(item: TEntity): void {
if (!item) {
throw new Error('item is null or undefined');
}
if (this.exists(item.id)) {
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
}
this.items.push(item);
public addItem(item: TEntity): void {
if (!item) {
throw new Error('item is null or undefined');
}
if (this.exists(item.id)) {
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
}
this.items.push(item);
}
public addOrUpdateItem(item: TEntity): void {
if (!item) {
throw new Error('item is null or undefined');
}
if (this.exists(item.id)) {
this.removeItem(item.id);
}
this.items.push(item);
public addOrUpdateItem(item: TEntity): void {
if (!item) {
throw new Error('item is null or undefined');
}
if (this.exists(item.id)) {
this.removeItem(item.id);
}
this.items.push(item);
}
public removeItem(id: TKey): void {
const index = this.items.findIndex((item) => item.id === id);
if (index === -1) {
throw new Error(`Cannot remove (id: ${id}) as it does not exist`);
}
this.items.splice(index, 1);
public removeItem(id: TKey): void {
const index = this.items.findIndex((item) => item.id === id);
if (index === -1) {
throw new Error(`Cannot remove (id: ${id}) as it does not exist`);
}
this.items.splice(index, 1);
}
public exists(id: TKey): boolean {
const index = this.items.findIndex((item) => item.id === id);
return index !== -1;
}
public exists(id: TKey): boolean {
const index = this.items.findIndex((item) => item.id === id);
return index !== -1;
}
}

View File

@@ -1,27 +1,29 @@
import fileSaver from 'file-saver';
export enum FileType {
BatchFile,
ShellScript,
BatchFile,
ShellScript,
}
export class SaveFileDialog {
public static saveFile(text: string, fileName: string, type: FileType): void {
const mimeType = this.mimeTypes.get(type);
this.saveBlob(text, mimeType, fileName);
}
private static readonly mimeTypes = new Map<FileType, string>([
// Some browsers (including firefox + IE) require right mime type
// otherwise they ignore extension and save the file as text.
[ FileType.BatchFile, 'application/bat' ], // https://en.wikipedia.org/wiki/Batch_file
[ FileType.ShellScript, 'text/x-shellscript' ], // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
]);
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
try {
const blob = new Blob([file], { type: fileType });
fileSaver.saveAs(blob, fileName);
} catch (e) {
window.open(`data:${fileType},${encodeURIComponent(file.toString())}`, '_blank', '');
}
export class SaveFileDialog {
public static saveFile(text: string, fileName: string, type: FileType): void {
const mimeType = this.mimeTypes.get(type);
this.saveBlob(text, mimeType, fileName);
}
private static readonly mimeTypes = new Map<FileType, string>([
// Some browsers (including firefox + IE) require right mime type
// otherwise they ignore extension and save the file as text.
[FileType.BatchFile, 'application/bat'], // https://en.wikipedia.org/wiki/Batch_file
[FileType.ShellScript, 'text/x-shellscript'], // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
]);
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
try {
const blob = new Blob([file], { type: fileType });
fileSaver.saveAs(blob, fileName);
} catch (e) {
window.open(`data:${fileType},${encodeURIComponent(file.toString())}`, '_blank', '');
}
}
}

View File

@@ -1,34 +1,37 @@
import { EventSource } from '../Events/EventSource';
export class AsyncLazy<T> {
private valueCreated = new EventSource();
private isValueCreated = false;
private isCreatingValue = false;
private value: T | undefined;
private valueCreated = new EventSource();
constructor(private valueFactory: () => Promise<T>) {}
private isValueCreated = false;
public setValueFactory(valueFactory: () => Promise<T>) {
this.valueFactory = valueFactory;
private isCreatingValue = false;
private value: T | undefined;
constructor(private valueFactory: () => Promise<T>) {}
public setValueFactory(valueFactory: () => Promise<T>) {
this.valueFactory = valueFactory;
}
public async getValue(): Promise<T> {
// If value is already created, return the value directly
if (this.isValueCreated) {
return Promise.resolve(this.value);
}
public async getValue(): Promise<T> {
// If value is already created, return the value directly
if (this.isValueCreated) {
return Promise.resolve(this.value);
}
// If value is being created, wait until the value is created and then return it.
if (this.isCreatingValue) {
return new Promise<T>((resolve, reject) => {
// Return/result when valueCreated event is triggered.
this.valueCreated.on(() => resolve(this.value));
});
}
this.isCreatingValue = true;
this.value = await this.valueFactory();
this.isCreatingValue = false;
this.isValueCreated = true;
this.valueCreated.notify(null);
return Promise.resolve(this.value);
// If value is being created, wait until the value is created and then return it.
if (this.isCreatingValue) {
return new Promise<T>((resolve) => {
// Return/result when valueCreated event is triggered.
this.valueCreated.on(() => resolve(this.value));
});
}
this.isCreatingValue = true;
this.value = await this.valueFactory();
this.isCreatingValue = false;
this.isValueCreated = true;
this.valueCreated.notify(null);
return Promise.resolve(this.value);
}
}

View File

@@ -1,5 +1,8 @@
export type SchedulerType = (callback: (...args: any[]) => void, ms: number) => void;
export type SchedulerCallbackType = (...args: unknown[]) => void;
export type SchedulerType = (callback: SchedulerCallbackType, ms: number) => void;
export function sleep(time: number, scheduler: SchedulerType = setTimeout) {
return new Promise((resolve) => scheduler(() => resolve(undefined), time));
return new Promise((resolve) => {
scheduler(() => resolve(undefined), time);
});
}