diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..86dc4c9 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,62 @@ +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + env: { + node: true, + browser: true, + }, + plugins: ['import', '@typescript-eslint', 'prettier'], + parser: '@typescript-eslint/parser', + rules: { + 'import/order': [ + 'warn', + { + pathGroups: [ + { + pattern: '~/**', + group: 'parent', + position: 'before', + }, + ], + groups: [ + ['builtin', 'external'], + ['parent', 'sibling', 'index'], + ], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + }, + ], + '@typescript-eslint/no-explicit-any': ['off', {}], + '@typescript-eslint/ban-types': [ + 'error', + { + types: { + Function: false, + 'extend-defaults': true, + }, + }, + ], + 'no-unused-vars': ['off'], + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + vars: 'all', + args: 'after-used', + ignoreRestSiblings: false, + argsIgnorePattern: '^_', + }, + ], + }, + overrides: [ + { + files: ['**/*.test.ts', '**/*.test.tsx'], + plugins: ['jest'], + }, + ], +}; diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml new file mode 100644 index 0000000..6bc462c --- /dev/null +++ b/.github/workflows/_test.yml @@ -0,0 +1,26 @@ +on: workflow_call + +jobs: + test: + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + - name: Install dependencies + run: npm install + + - name: Check generated files are up to date + run: | + npm run generate + git diff --exit-code + + - name: Lint + run: npm run lint + + - name: Test + run: npm run test:ci diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..3f27542 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,7 @@ +name: On push + +on: push + +jobs: + test: + uses: ./.github/workflows/_test.yml diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 65c72bb..0000000 --- a/eslint.config.js +++ /dev/null @@ -1,64 +0,0 @@ -module.exports = [ - { - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", - ], - env: { - node: true, - browser: true, - }, - plugins: ["import", "@typescript-eslint", "prettier"], - parser: "@typescript-eslint/parser", - rules: { - "import/order": [ - "warn", - { - pathGroups: [ - { - pattern: "~/**", - group: "parent", - position: "before", - }, - ], - groups: [ - ["builtin", "external"], - ["parent", "sibling", "index"], - ], - "newlines-between": "always", - alphabetize: { - order: "asc", - caseInsensitive: true, - }, - }, - ], - "@typescript-eslint/no-explicit-any": ["off", {}], - "@typescript-eslint/ban-types": [ - "error", - { - types: { - Function: false, - "extend-defaults": true, - }, - }, - ], - "no-unused-vars": ["off"], - "@typescript-eslint/no-unused-vars": [ - "warn", - { - vars: "all", - args: "after-used", - ignoreRestSiblings: false, - argsIgnorePattern: "^_", - }, - ], - }, - overrides: [ - { - files: ["**/*.test.ts", "**/*.test.tsx"], - plugins: ["jest"], - }, - ], - }, -]; diff --git a/package.json b/package.json index 71131fa..95185a9 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,10 @@ "start": "node dist/index.js", "generate": "tsx bin/generate-queries.ts schema/queries.sql src/lib/cache/queries.ts", "debug": "nodemon", - "lint": "eslint \"src/**/*.ts\"", + "lint": "eslint \"src/**/*.{ts,tsx}\"", "lint:fix": "prettier --write ./src && yarn lint --fix", "test": "jest", + "test:ci": "jest --runInBand --detectOpenHandles --silent", "schema:diff": "atlas schema diff --from sqlite://local.db --to file://schema/tables.sql --dev-url 'sqlite://dev?mode=memory'", "schema:apply": "atlas schema apply --url sqlite://local.db --to file://schema/tables.sql --dev-url 'sqlite://dev?mode=memory'" }, diff --git a/src/config.ts b/src/config.ts index 1973121..c39f00f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,4 @@ -import { FastifyReply, FastifyRequest } from 'fastify'; +import { FastifyRequest } from 'fastify'; import pino from 'pino'; import { assertEnv, boolEnv, enumEnv, numEnv, obscure, serialiseError } from './lib/utils'; diff --git a/src/lib/AsyncQueue.test.ts b/src/lib/AsyncQueue.test.ts index 930982c..e0be1f4 100644 --- a/src/lib/AsyncQueue.test.ts +++ b/src/lib/AsyncQueue.test.ts @@ -1,7 +1,8 @@ import { describe, it, expect, jest } from '@jest/globals'; -import { AsyncQueue } from './AsyncQueue'; import { promisify } from 'util'; +import { AsyncQueue } from './AsyncQueue'; + const sleep = promisify(setTimeout); describe('AsyncQueue', () => { diff --git a/src/lib/AsyncQueue.ts b/src/lib/AsyncQueue.ts index f766536..fa7242e 100644 --- a/src/lib/AsyncQueue.ts +++ b/src/lib/AsyncQueue.ts @@ -21,6 +21,7 @@ export declare interface AsyncQueue { emit(event: JobEvent, job: Job): boolean; } +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class AsyncQueue extends EventEmitter { concurrency: number; items: Job[] = []; diff --git a/src/lib/AudioFile.ts b/src/lib/AudioFile.ts index 685a634..1c39cba 100644 --- a/src/lib/AudioFile.ts +++ b/src/lib/AudioFile.ts @@ -2,10 +2,10 @@ import fs from 'fs/promises'; import path from 'path'; import { isMatching, P } from 'ts-pattern'; -import { log } from '../config'; import { Bucket } from './Bucket'; import { download, downloaderOutputDir } from './Downloader'; import { GuardType } from './utils'; +import { log } from '../config'; export type AudioFileMetadata = GuardType; export const isAudioFileMetadata = isMatching({ diff --git a/src/lib/CountDown.ts b/src/lib/CountDown.ts index 9e7ed7d..249fccb 100644 --- a/src/lib/CountDown.ts +++ b/src/lib/CountDown.ts @@ -9,7 +9,7 @@ export class CountDown { } get ticking() { - return typeof this.ticker != undefined && this.remainder < this.duration; + return typeof this.ticker !== 'undefined' && this.remainder < this.duration; } get runtime() { diff --git a/src/lib/Downloader.ts b/src/lib/Downloader.ts index 56be92c..bc02df6 100644 --- a/src/lib/Downloader.ts +++ b/src/lib/Downloader.ts @@ -1,9 +1,9 @@ import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; import path from 'path'; +import { AsyncQueue } from './AsyncQueue'; import { trimToJsonObject, tryParseJSON } from './utils'; import { config, log } from '../config'; -import { AsyncQueue } from './AsyncQueue'; export const downloaderCacheDir = path.join(config.cacheDir, 'ytdl'); export const downloaderOutputDir = path.join(config.cacheDir, 'out'); diff --git a/src/lib/PlaylistItem.ts b/src/lib/PlaylistItem.ts index 88534d8..2624dbb 100644 --- a/src/lib/PlaylistItem.ts +++ b/src/lib/PlaylistItem.ts @@ -50,8 +50,8 @@ export class PlaylistItem extends AudioFile { value: this.timer.ticking ? `${runtime} / ${duration}` : playlist.current?.videoId !== this.videoId - ? `${duration} (eta ${eta})` - : duration, + ? `${duration} (eta ${eta})` + : duration, inline: true, }, ]); diff --git a/src/lib/Spotify.ts b/src/lib/Spotify.ts index 45a0738..ad17c4e 100644 --- a/src/lib/Spotify.ts +++ b/src/lib/Spotify.ts @@ -2,8 +2,8 @@ import 'dotenv/config'; import axios, { AxiosInstance } from 'axios'; -import { config, log } from '../config'; import { encodeQueryParams } from './utils'; +import { config, log } from '../config'; interface AccessTokenResponse { access_token: string; diff --git a/src/lib/audio.ts b/src/lib/audio.ts index 40e2282..4d28d69 100644 --- a/src/lib/audio.ts +++ b/src/lib/audio.ts @@ -14,8 +14,8 @@ import { VoiceBasedChannel, } from 'discord.js'; -import { log } from '../config'; import { getPlayer, Player } from './Player'; +import { log } from '../config'; type VoiceCommandOptions = { allowConnect?: boolean; allowRetry?: boolean }; diff --git a/src/lib/cache/database.ts b/src/lib/cache/database.ts index b4671cc..cbb4c86 100644 --- a/src/lib/cache/database.ts +++ b/src/lib/cache/database.ts @@ -1,7 +1,7 @@ import { Client, ResultSet, createClient } from '@libsql/client'; +import { ZodSchema, z } from 'zod'; import { config, log } from '../../config'; -import { ZodSchema, z } from 'zod'; export const expandResultSet = (results: ResultSet): any[] => { if (results.rows.length < 1) { diff --git a/src/server/fragments/QueueTable.tsx b/src/server/fragments/QueueTable.tsx index a906b56..1a4ff11 100644 --- a/src/server/fragments/QueueTable.tsx +++ b/src/server/fragments/QueueTable.tsx @@ -1,11 +1,11 @@ import React from 'preact/compat'; import { Button } from './Button'; +import { Link } from './Link'; +import { Pagination } from './Pagination'; import { QueuedSong } from '../../lib/cache'; import { secToTimeFragments } from '../../lib/utils'; import { Trigger } from '../consts'; -import { Pagination } from './Pagination'; -import { Link } from './Link'; export interface QueueTableProps { channelId: string; diff --git a/src/server/fragments/SongSearchForm.tsx b/src/server/fragments/SongSearchForm.tsx index 88bfcf7..49948ef 100644 --- a/src/server/fragments/SongSearchForm.tsx +++ b/src/server/fragments/SongSearchForm.tsx @@ -1,6 +1,6 @@ -import { QueryResult } from '../../lib/Youtube'; import { Button } from './Button'; import { Link } from './Link'; +import { QueryResult } from '../../lib/Youtube'; export interface SongSearchFormProps { channelId?: string; diff --git a/src/server/fragments/SongTable.tsx b/src/server/fragments/SongTable.tsx index 825ff18..977002a 100644 --- a/src/server/fragments/SongTable.tsx +++ b/src/server/fragments/SongTable.tsx @@ -1,11 +1,11 @@ import React from 'preact/compat'; import { Button } from './Button'; +import { Link } from './Link'; import { Pagination } from './Pagination'; import { Song } from '../../lib/cache'; import { secToTimeFragments, slugify } from '../../lib/utils'; import { Trigger } from '../consts'; -import { Link } from './Link'; export interface SongTableProps { channelId?: string; diff --git a/src/server/router.tsx b/src/server/router.tsx index 5f86228..152c6ec 100644 --- a/src/server/router.tsx +++ b/src/server/router.tsx @@ -4,12 +4,14 @@ import React from 'preact/compat'; import { render } from 'preact-render-to-string'; import { ContentType, Header, Trigger } from './consts'; +import { ChannelList } from './fragments/ChannelList'; import { Diagnostics } from './fragments/Diagnostics'; import { ErrorSurface } from './fragments/ErrorSurface'; import { QueueTable } from './fragments/QueueTable'; import { SongForm } from './fragments/SongForm'; import { SongSearchResultsTable } from './fragments/SongSearchForm'; import { SongTable } from './fragments/SongTable'; +import { Channels } from './views/Channels'; import { Layout } from './views/Layout'; import { Queue } from './views/Queue'; import { Songs } from './views/Songs'; @@ -17,8 +19,6 @@ import { QueueDAO, SongDAO, SongSchema } from '../lib/cache'; import { Pagination } from '../lib/cache/database'; import { version } from '../lib/Downloader'; import { Youtube } from '../lib/Youtube'; -import { Channels } from './views/Channels'; -import { ChannelList } from './fragments/ChannelList'; export const router = async (app: FastifyInstance) => { app.get('/', async (_, reply) => reply.redirect('/diagnostics')); diff --git a/src/server/views/Songs.tsx b/src/server/views/Songs.tsx index 80842cc..22034e7 100644 --- a/src/server/views/Songs.tsx +++ b/src/server/views/Songs.tsx @@ -1,8 +1,8 @@ import React from 'preact/compat'; -import { SongSearchForm } from '../fragments/SongSearchForm'; -import { SongForm } from '../fragments/SongForm'; import { Trigger } from '../consts'; +import { SongForm } from '../fragments/SongForm'; +import { SongSearchForm } from '../fragments/SongSearchForm'; export interface SongProps { channelId: string;