diff --git a/.github/workflows/develop-ui.yaml b/.github/workflows/develop-ui.yaml index 911d08e59..f1ceb5a70 100644 --- a/.github/workflows/develop-ui.yaml +++ b/.github/workflows/develop-ui.yaml @@ -62,8 +62,8 @@ jobs: cache-dependency-path: './frontend/pnpm-lock.yaml' - run: pnpm install - run: pnpm run -r build # We need code/types generated by the build (e.g. gql/generated) - - run: pnpm run check --output machine - - run: pnpm run lint:report + - run: pnpm run -r check --output machine + - run: pnpm run -r lint:report - name: Annotate Code Linting REsults uses: ataylorme/eslint-annotate-action@d57a1193d4c59cbfbf3f86c271f42612f9dbd9e9 # v3.0.0 if: always() diff --git a/frontend/viewer/eslint.config.js b/frontend/viewer/eslint.config.js new file mode 100644 index 000000000..edc1ab2ba --- /dev/null +++ b/frontend/viewer/eslint.config.js @@ -0,0 +1,142 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import { fileURLToPath } from 'url'; +import globals from 'globals'; +import js from '@eslint/js'; +import path from 'path'; +import svelteParser from 'svelte-eslint-parser'; +import tsParser from '@typescript-eslint/parser'; + +// mimic CommonJS variables +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname +}); + +export default [ + { + ignores: [ + '**/*js', + '**/generated/**', + '**/vite.config.*', + ], + }, + js.configs.recommended, + // TypeScript and Svelte plugins don't seem to support the new config format yet + // So, using backwards compatibility util: https://eslint.org/blog/2022/08/new-config-system-part-2/#backwards-compatibility-utility + ...compat.config({ + plugins: ['@typescript-eslint'], + extends: ['plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking'], + overrides: [ + { + files: ['*.svelte'], + rules: { + // The Svelte plugin doesn't seem to have typing quite figured out + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + }, + } + ] + }), + ...compat.config({ + extends: ['plugin:svelte/recommended'], + }), + { + rules: { + // https://typescript-eslint.io/rules/ + '@typescript-eslint/naming-convention': [ + 'error', + { + 'selector': 'default', + 'format': ['camelCase'], + 'leadingUnderscore': 'allow', + }, + { + 'selector': 'function', + 'filter': {'regex': 'GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS', 'match': true}, + 'format': ['UPPER_CASE'], + }, + { + 'selector': 'default', + 'modifiers': ['const'], + 'format': ['camelCase', 'UPPER_CASE'], + 'leadingUnderscore': 'allow', + }, + { + 'selector': ['typeLike', 'enumMember'], + 'format': ['PascalCase'], + }, + { + 'selector': 'default', + 'modifiers': ['requiresQuotes'], + 'format': null, + }, + { + 'selector': 'import', + 'format': ['camelCase', 'PascalCase'], + } + ], + '@typescript-eslint/quotes': ['error', 'single', { 'allowTemplateLiterals': true }], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + 'argsIgnorePattern': '^_', + 'destructuredArrayIgnorePattern': '^_', + 'caughtErrors': 'all', + 'ignoreRestSiblings': true, + }, + ], + '@typescript-eslint/explicit-function-return-type': [ + 'warn', + { + 'allowExpressions': true, + 'allowedNames': ['load'], + }, + ], + '@typescript-eslint/consistent-type-imports': ['error', {'fixStyle': 'inline-type-imports'}], + // https://sveltejs.github.io/eslint-plugin-svelte/rules/ + 'svelte/html-quotes': 'error', + 'svelte/no-dom-manipulating': 'warn', + 'svelte/no-reactive-reassign': ['warn', { 'props': false }], + 'svelte/no-store-async': 'error', + 'svelte/require-store-reactive-access': 'error', + 'svelte/mustache-spacing': 'error', + 'func-style': ['warn', 'declaration'], + "no-restricted-imports": ["error", { + "patterns": [{ + "group": ["svelte-intl-precompile"], + "message": "Use $lib/i18n instead." + }] + }] + }, + }, + { + languageOptions: { + parser: tsParser, + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + extraFileExtensions: ['.svelte'], // Yes, TS-Parser, relax when you're fed svelte files + }, + globals: { + ...globals.browser, + ...globals.node, + }, + }, + }, + { + files: ['**/*.svelte'], + languageOptions: { + parser: svelteParser, + parserOptions: { + parser: tsParser, + }, + globals: { + '$$Generic': 'readonly', + } + }, + }, +]; diff --git a/frontend/viewer/package.json b/frontend/viewer/package.json index 2179ca939..dc79238af 100644 --- a/frontend/viewer/package.json +++ b/frontend/viewer/package.json @@ -21,7 +21,8 @@ "build": "vite build -m web-component", "build-app": "vite build", "preview": "vite preview", - "check": "svelte-check --tsconfig ./tsconfig.json" + "check": "svelte-check --tsconfig ./tsconfig.json", + "lint": "eslint ." }, "devDependencies": { "@egoist/tailwindcss-icons": "^1.8.0", diff --git a/frontend/viewer/src/App.svelte b/frontend/viewer/src/App.svelte index 07cae2605..e2da56fc8 100644 --- a/frontend/viewer/src/App.svelte +++ b/frontend/viewer/src/App.svelte @@ -10,6 +10,7 @@ export let url = ''; + /* eslint-disable @typescript-eslint/naming-convention */ settings({ components: { MenuItem: { @@ -36,7 +37,7 @@ } }, }); - + /* eslint-enable @typescript-eslint/naming-convention */ @@ -69,7 +70,7 @@ - {setTimeout(() => navigate("/", { replace: true }))} + {setTimeout(() => navigate('/', { replace: true }))} diff --git a/frontend/viewer/src/CrdtProjectView.svelte b/frontend/viewer/src/CrdtProjectView.svelte index d436ab10e..a82224f83 100644 --- a/frontend/viewer/src/CrdtProjectView.svelte +++ b/frontend/viewer/src/CrdtProjectView.svelte @@ -3,7 +3,7 @@ import ProjectView from './ProjectView.svelte'; export let projectName: string; - const {connected, lexboxApi} = SetupSignalR(`/api/hub/${projectName}/lexbox`, { + const {connected} = SetupSignalR(`/api/hub/${projectName}/lexbox`, { history: true, write: true, feedback: true, diff --git a/frontend/viewer/src/FwDataProjectView.svelte b/frontend/viewer/src/FwDataProjectView.svelte index 9094b2368..24fea4943 100644 --- a/frontend/viewer/src/FwDataProjectView.svelte +++ b/frontend/viewer/src/FwDataProjectView.svelte @@ -8,7 +8,7 @@ import {useEventBus} from './lib/services/event-bus'; export let projectName: string; - const {connected, lexboxApi} = SetupSignalR(`/api/hub/${projectName}/fwdata`, { + const {connected} = SetupSignalR(`/api/hub/${projectName}/fwdata`, { history: false, write: true, openWithFlex: true, diff --git a/frontend/viewer/src/HomeView.svelte b/frontend/viewer/src/HomeView.svelte index f619eb159..85d963cab 100644 --- a/frontend/viewer/src/HomeView.svelte +++ b/frontend/viewer/src/HomeView.svelte @@ -20,19 +20,17 @@ let createError: string; - async function createProject() { - + async function createProject(): Promise { const response = await projectsService.createProject(newProjectName); createError = response.error ?? ''; if (createError) return; newProjectName = ''; - void refreshProjects(); + await refreshProjects(); } - let importing = ''; - async function importFwDataProject(name: string) { + async function importFwDataProject(name: string): Promise { importing = name; await projectsService.importFwDataProject(name); await refreshProjects(); @@ -41,7 +39,7 @@ let downloading = ''; - async function downloadCrdtProject(project: Project) { + async function downloadCrdtProject(project: Project): Promise { downloading = project.name; await projectsService.downloadCrdtProject(project); await refreshProjects(); @@ -51,7 +49,7 @@ let projectsPromise = projectsService.fetchProjects().then(p => projects = p); let projects: Project[] = []; - async function refreshProjects() { + async function refreshProjects(): Promise { let promise = projectsService.fetchProjects(); projects = await promise;//avoids clearing out the list until the new list is fetched projectsPromise = promise; @@ -59,12 +57,16 @@ let remoteProjects: { [server: string]: Project[] } = {}; let loadingRemoteProjects = false; - async function fetchRemoteProjects() { + async function fetchRemoteProjects(): Promise { loadingRemoteProjects = true; remoteProjects = await projectsService.fetchRemoteProjects(); loadingRemoteProjects = false; } - fetchRemoteProjects(); + + fetchRemoteProjects().catch((error) => { + console.error(`Failed to fetch remote projects`, error); + throw error; + }); let servers: ServerStatus[] = []; @@ -97,7 +99,7 @@ : []), ] satisfies ColumnDef[]; - function matchesProject(projects: Project[], project: Project) { + function matchesProject(projects: Project[], project: Project): Project | undefined { let matches: Project | undefined = undefined; if (project.id) { matches = projects.find(p => p.id == project.id && p.serverAuthority == project.serverAuthority); @@ -120,7 +122,7 @@ }; } let authority = Object.entries(serversProjects) - .find(([server, projects]) => matchesProject(projects, project))?.[0]; + .find(([_server, projects]) => matchesProject(projects, project))?.[0]; return authority ? servers.find(s => s.authority == authority) : undefined; } @@ -152,7 +154,7 @@ $isDev || p.fwdata).sort((p1, p2) => p1.name.localeCompare(p2.name))} classes={{ th: 'p-4' }}> - + {#each data ?? [] as project, rowIndex} {#each columns as column (column.name)} @@ -262,7 +264,7 @@ - diff --git a/frontend/viewer/src/lib/entry-editor/inputs/CrdtTextField.svelte b/frontend/viewer/src/lib/entry-editor/inputs/CrdtTextField.svelte index 472b3d757..d31bfd5be 100644 --- a/frontend/viewer/src/lib/entry-editor/inputs/CrdtTextField.svelte +++ b/frontend/viewer/src/lib/entry-editor/inputs/CrdtTextField.svelte @@ -3,7 +3,7 @@ import CrdtField from './CrdtField.svelte'; import { TextField } from 'svelte-ux'; - export let value: string | number; + export let value: string | number | null; export let unsavedChanges = false; export let label: string | undefined = undefined; export let labelPlacement: ComponentProps['labelPlacement'] = undefined; @@ -33,9 +33,3 @@ - - diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/EntityEditor.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/EntityEditor.svelte index 4b96da3be..5fa0fb049 100644 --- a/frontend/viewer/src/lib/entry-editor/object-editors/EntityEditor.svelte +++ b/frontend/viewer/src/lib/entry-editor/object-editors/EntityEditor.svelte @@ -1,15 +1,14 @@ - {#each customFieldConfigs as fieldConfig} {/each} diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte index 8e14f50e1..36d4aa102 100644 --- a/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte +++ b/frontend/viewer/src/lib/entry-editor/object-editors/EntryEditor.svelte @@ -7,7 +7,7 @@ import {mdiPlus, mdiTrashCanOutline} from '@mdi/js'; import { Button, portal } from 'svelte-ux'; import EntityListItemActions from '../EntityListItemActions.svelte'; - import {defaultExampleSentence, defaultSense, emptyId, firstDefOrGlossVal, firstSentenceOrTranslationVal} from '../../utils'; + import {defaultExampleSentence, defaultSense, firstDefOrGlossVal, firstSentenceOrTranslationVal} from '../../utils'; import HistoryView from '../../history/HistoryView.svelte'; import SenseEditor from './SenseEditor.svelte'; import ExampleEditor from './ExampleEditor.svelte'; @@ -24,38 +24,38 @@ export let entry: IEntry; - function addSense() { + function addSense(): void { const sense = defaultSense(); highlightedEntity = sense; entry.senses = [...entry.senses, sense]; } - function addExample(sense: ISense) { + function addExample(sense: ISense): void { const sentence = defaultExampleSentence(); highlightedEntity = sentence; sense.exampleSentences = [...sense.exampleSentences, sentence]; entry = entry; // examples counts are not updated without this } - function deleteEntry() { + function deleteEntry(): void { dispatch('delete', {entry}); } - function deleteSense(sense: ISense) { + function deleteSense(sense: ISense): void { entry.senses = entry.senses.filter(s => s !== sense); dispatch('delete', {entry, sense}); } - function moveSense(sense: ISense, i: number) { + function moveSense(sense: ISense, i: number): void { entry.senses.splice(entry.senses.indexOf(sense), 1); entry.senses.splice(i, 0, sense); dispatch('change', {entry, sense}); highlightedEntity = sense; } - function deleteExample(sense: ISense, example: IExampleSentence) { + function deleteExample(sense: ISense, example: IExampleSentence): void { sense.exampleSentences = sense.exampleSentences.filter(e => e !== example); dispatch('delete', {entry, sense, example}); entry = entry; // examples are not updated without this } - function moveExample(sense: ISense, example: IExampleSentence, i: number) { + function moveExample(sense: ISense, example: IExampleSentence, i: number): void { sense.exampleSentences.splice(sense.exampleSentences.indexOf(example), 1); sense.exampleSentences.splice(i, 0, example); dispatch('change', {entry, sense, example}); diff --git a/frontend/viewer/src/lib/history/HistoryView.svelte b/frontend/viewer/src/lib/history/HistoryView.svelte index 6f6dc9042..4725112b6 100644 --- a/frontend/viewer/src/lib/history/HistoryView.svelte +++ b/frontend/viewer/src/lib/history/HistoryView.svelte @@ -42,7 +42,7 @@ loading = false; } - async function showEntry(row: typeof history[number]) { + async function showEntry(row: typeof history[number]): Promise { if (!row.entity || !row.snapshotId) { const data = await fetch(`/api/history/${projectName}/snapshot/at/${row.timestamp}?entityId=${id}`).then(res => res.json()); record = {...row, entity: data.entity, entityName: data.typeName}; @@ -92,13 +92,13 @@
{#if record?.entity} - {#if record.entityName === "Entry"} + {#if record.entityName === 'Entry'} - {:else if record.entityName === "Sense"} + {:else if record.entityName === 'Sense'}
- {:else if record.entityName === "ExampleSentence"} + {:else if record.entityName === 'ExampleSentence'}
diff --git a/frontend/viewer/src/lib/i18n.ts b/frontend/viewer/src/lib/i18n.ts index e71de67a9..da6db1ed5 100644 --- a/frontend/viewer/src/lib/i18n.ts +++ b/frontend/viewer/src/lib/i18n.ts @@ -1,8 +1,8 @@ -import type { FieldConfig, WellKnownFieldId } from './config-types'; +import type { WellKnownFieldId } from './config-types'; import type {FieldIds} from './entry-editor/field-data'; -type I18n = Record & Record, string>; +// type I18n = Record & Record, string>; type I18nKey = FieldIds; /** * I18n type is used to specify which i18n group to use for a field. If empty, the default i18n is used. diff --git a/frontend/viewer/src/lib/in-memory-api-service.ts b/frontend/viewer/src/lib/in-memory-api-service.ts index 986e79379..17ea3539d 100644 --- a/frontend/viewer/src/lib/in-memory-api-service.ts +++ b/frontend/viewer/src/lib/in-memory-api-service.ts @@ -1,4 +1,5 @@ -import type { +/* eslint-disable @typescript-eslint/naming-convention */ +import type { LexboxApiClient, IEntry, IExampleSentence, @@ -17,7 +18,7 @@ import {type WritingSystem} from './mini-lcm'; import {headword} from './utils'; import {applyPatch} from 'fast-json-patch'; -function filterEntries(entries: IEntry[], query: string) { +function filterEntries(entries: IEntry[], query: string): IEntry[] { return entries.filter(entry => [ ...Object.values(entry.lexemeForm ?? {}), @@ -31,8 +32,8 @@ function filterEntries(entries: IEntry[], query: string) { export class InMemoryApiService implements LexboxApiClient { GetPartsOfSpeech(): Promise { return Promise.resolve([ - {id: 'noun', name: {en: 'noun'},}, - {id: 'verb', name: {en: 'verb'},} + {id: '86ff66f6-0774-407a-a0dc-3eeaf873daf7', name: {en: 'Verb'},}, + {id: 'a8e41fd3-e343-4c7c-aa05-01ea3dd5cfb5', name: {en: 'Noun'},} ]); } @@ -54,7 +55,7 @@ export class InMemoryApiService implements LexboxApiClient { private _entries = entries; private _Entries(): IEntry[] { - return JSON.parse(JSON.stringify(this._entries)); + return JSON.parse(JSON.stringify(this._entries)) as IEntry[]; } GetEntries(options: QueryOptions | undefined): Promise { @@ -90,7 +91,7 @@ export class InMemoryApiService implements LexboxApiClient { const v2 = headword(e2, sortWs); if (!v2) return -1; if (!v1) return 1; - let compare = v1.localeCompare(v2, sortWs); + const compare = v1.localeCompare(v2, sortWs); if (compare !== 0) return compare; return e1.id.localeCompare(e2.id); }) @@ -155,11 +156,11 @@ export class InMemoryApiService implements LexboxApiClient { return Promise.resolve(); } - CreateWritingSystem(type: WritingSystemType, writingSystem: WritingSystem): Promise { + CreateWritingSystem(_type: WritingSystemType, _writingSystem: WritingSystem): Promise { throw new Error('Method not implemented.'); } - UpdateWritingSystem(wsId: string, type: WritingSystemType, update: JsonPatch): Promise { + UpdateWritingSystem(_wsId: string, _type: WritingSystemType, _update: JsonPatch): Promise { throw new Error('Method not implemented.'); } } diff --git a/frontend/viewer/src/lib/layout/DictionaryEntryViewer.svelte b/frontend/viewer/src/lib/layout/DictionaryEntryViewer.svelte index 3079f09a3..8cf46cbff 100644 --- a/frontend/viewer/src/lib/layout/DictionaryEntryViewer.svelte +++ b/frontend/viewer/src/lib/layout/DictionaryEntryViewer.svelte @@ -1,8 +1,8 @@ diff --git a/frontend/viewer/src/lib/mini-lcm/complex-form-component.ts b/frontend/viewer/src/lib/mini-lcm/complex-form-component.ts index 6aee0e1a6..659fc0706 100644 --- a/frontend/viewer/src/lib/mini-lcm/complex-form-component.ts +++ b/frontend/viewer/src/lib/mini-lcm/complex-form-component.ts @@ -3,7 +3,7 @@ * Any changes made to this file can be lost when this file is regenerated. */ -import type { IComplexFormComponent } from "./i-complex-form-component"; +import type { IComplexFormComponent } from './i-complex-form-component'; export class ComplexFormComponent implements IComplexFormComponent { constructor(id: string, complexFormEntryId: string, componentEntryId: string) { diff --git a/frontend/viewer/src/lib/mini-lcm/i-complex-form-type.ts b/frontend/viewer/src/lib/mini-lcm/i-complex-form-type.ts index aef9493a1..a03cc5c2a 100644 --- a/frontend/viewer/src/lib/mini-lcm/i-complex-form-type.ts +++ b/frontend/viewer/src/lib/mini-lcm/i-complex-form-type.ts @@ -1,4 +1,4 @@ -import type { IMultiString } from "./i-multi-string"; +import type { IMultiString } from './i-multi-string'; export interface IComplexFormType { id: string; diff --git a/frontend/viewer/src/lib/mini-lcm/query-options.ts b/frontend/viewer/src/lib/mini-lcm/query-options.ts index b3f6255ee..f773e096e 100644 --- a/frontend/viewer/src/lib/mini-lcm/query-options.ts +++ b/frontend/viewer/src/lib/mini-lcm/query-options.ts @@ -2,14 +2,14 @@ * This is a TypeGen auto-generated file. * Any changes made to this file can be lost when this file is regenerated. */ - +/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ export interface QueryOptions { order: { field: 'headword', - writingSystem: string | 'default', + writingSystem: Exclude | 'default', ascending?: boolean }; count: number; offset: number; - exemplar?: { value: string, writingSystem: string | 'default' }; + exemplar?: { value: string, writingSystem: Exclude | 'default' }; } diff --git a/frontend/viewer/src/lib/notifications/NotificationOutlet.svelte b/frontend/viewer/src/lib/notifications/NotificationOutlet.svelte index 46e0db57e..acdfaff52 100644 --- a/frontend/viewer/src/lib/notifications/NotificationOutlet.svelte +++ b/frontend/viewer/src/lib/notifications/NotificationOutlet.svelte @@ -14,7 +14,7 @@
{#each $notifications as notification}
- +
{#if notification.type === 'success'} diff --git a/frontend/viewer/src/lib/notifications/notifications.ts b/frontend/viewer/src/lib/notifications/notifications.ts index 671c93741..5fd18deed 100644 --- a/frontend/viewer/src/lib/notifications/notifications.ts +++ b/frontend/viewer/src/lib/notifications/notifications.ts @@ -1,4 +1,4 @@ -import {writable, type Writable, type Readable, readonly} from 'svelte/store'; +import {writable, type Writable} from 'svelte/store'; interface NotificationAction { label: string; @@ -11,7 +11,7 @@ export class AppNotification { return this._notifications; } - public static display(message: string, type: 'success' | 'error' | 'info' | 'warning', timeout: 'short' | 'long' | number = 'short') { + public static display(message: string, type: 'success' | 'error' | 'info' | 'warning', timeout: 'short' | 'long' | number = 'short'): void { const notification = new AppNotification(message, type); this._notifications.update(notifications => [...notifications, notification]); if (timeout === -1) return; @@ -23,7 +23,7 @@ export class AppNotification { }, timeout); } - public static displayAction(message: string, type: 'success' | 'error' | 'info' | 'warning', action: NotificationAction) { + public static displayAction(message: string, type: 'success' | 'error' | 'info' | 'warning', action: NotificationAction): void { const notification = new AppNotification(message, type, action); this._notifications.update(notifications => [...notifications, notification]); } diff --git a/frontend/viewer/src/lib/parts-of-speech.ts b/frontend/viewer/src/lib/parts-of-speech.ts index c31e96bd2..69a20fc02 100644 --- a/frontend/viewer/src/lib/parts-of-speech.ts +++ b/frontend/viewer/src/lib/parts-of-speech.ts @@ -9,6 +9,9 @@ export function usePartsOfSpeech(): Readable { partsOfSpeechStore = writable([], (set) => { useLexboxApi().GetPartsOfSpeech().then(partsOfSpeech => { set(partsOfSpeech); + }).catch(error => { + console.error('Failed to load parts of speech', error); + throw error; }); }); } diff --git a/frontend/viewer/src/lib/search-bar/SearchBar.svelte b/frontend/viewer/src/lib/search-bar/SearchBar.svelte index 2b57c4b28..ac2380288 100644 --- a/frontend/viewer/src/lib/search-bar/SearchBar.svelte +++ b/frontend/viewer/src/lib/search-bar/SearchBar.svelte @@ -1,6 +1,6 @@