diff --git a/.prettierrc b/.prettierrc index 1ef9a3d..46cb3a5 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,4 @@ { - "plugins": ["prettier-plugin-organize-imports"], "singleQuote": true, "jsxSingleQuote": true, "tabWidth": 2, diff --git a/collab-server/src/server.ts b/collab-server/src/server.ts index efcfbe3..cb6eb0d 100644 --- a/collab-server/src/server.ts +++ b/collab-server/src/server.ts @@ -1,9 +1,10 @@ -import { Database } from '@hocuspocus/extension-database' -import { Server } from '@hocuspocus/server' import { once } from 'node:events' import fs from 'node:fs/promises' import path from 'node:path' import { DatabaseSync } from 'node:sqlite' + +import { Database } from '@hocuspocus/extension-database' +import { Server } from '@hocuspocus/server' import * as Y from 'yjs' // -- CLI -- diff --git a/eslint.config.mjs b/eslint.config.mjs index e4981f1..a23f4d4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,8 +1,10 @@ +import { defineConfig, globalIgnores } from 'eslint/config' import nextVitals from 'eslint-config-next/core-web-vitals' import nextTs from 'eslint-config-next/typescript' import prettier from 'eslint-config-prettier/flat' import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths' -import { defineConfig, globalIgnores } from 'eslint/config' +import simpleImportSort from 'eslint-plugin-simple-import-sort' +import unusedImports from 'eslint-plugin-unused-imports' const eslintConfig = defineConfig([ ...nextVitals, @@ -21,6 +23,19 @@ const eslintConfig = defineConfig([ 'branch-*/', ]), { + plugins: { + 'simple-import-sort': simpleImportSort, + 'unused-imports': unusedImports, + }, + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'simple-import-sort/imports': 'warn', + 'unused-imports/no-unused-imports': 'warn', + 'unused-imports/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + }, + }, + { + // Rules that only make sense in the app files: ['src/**/*.{mts,ts,tsx}'], plugins: { 'no-relative-import-paths': noRelativeImportPaths }, rules: { diff --git a/package-lock.json b/package-lock.json index 784fd7a..14ee395 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,10 +37,11 @@ "eslint-config-next": "16.2.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-no-relative-import-paths": "^1.6.1", + "eslint-plugin-simple-import-sort": "^13.0.0", + "eslint-plugin-unused-imports": "^4.4.1", "husky": "^9.1.7", "lint-staged": "^16.4.0", "prettier": "^3.8.2", - "prettier-plugin-organize-imports": "^4.3.0", "typescript": "^5" } }, @@ -5422,6 +5423,32 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-13.0.0.tgz", + "integrity": "sha512-McAc+/Nlvcg4byY/CABGH8kqnefWBj8s3JA2okEtz8ixbECQgU46p0HkTUKa4YS7wvgGceimlc34p1nXqbWqtA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -8411,23 +8438,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-plugin-organize-imports": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.3.0.tgz", - "integrity": "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "prettier": ">=2.0", - "typescript": ">=2.9", - "vue-tsc": "^2.1.0 || 3" - }, - "peerDependenciesMeta": { - "vue-tsc": { - "optional": true - } - } - }, "node_modules/prisma": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", diff --git a/package.json b/package.json index dbd0c63..0634941 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,11 @@ "eslint-config-next": "16.2.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-no-relative-import-paths": "^1.6.1", + "eslint-plugin-simple-import-sort": "^13.0.0", + "eslint-plugin-unused-imports": "^4.4.1", "husky": "^9.1.7", "lint-staged": "^16.4.0", "prettier": "^3.8.2", - "prettier-plugin-organize-imports": "^4.3.0", "typescript": "^5" }, "lint-staged": { diff --git a/src/app/AvatarMenu.tsx b/src/app/AvatarMenu.tsx index db0a9a8..d6ca715 100644 --- a/src/app/AvatarMenu.tsx +++ b/src/app/AvatarMenu.tsx @@ -1,12 +1,13 @@ 'use client' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { useContext } from 'react' + import AvatarIcon from '@/app/components/AvatarIcon' import authClient from '@/lib/client/auth' import { ConfigCtx } from '@/lib/contexts' import { setIsAdmin } from '@/lib/server/actions' -import Link from 'next/link' -import { useRouter } from 'next/navigation' -import { useContext } from 'react' export default function AvatarMenu() { const session = authClient.useSession() diff --git a/src/app/[userName]/NewProjectForm.tsx b/src/app/[userName]/NewProjectForm.tsx index 8e9f80d..28643bc 100644 --- a/src/app/[userName]/NewProjectForm.tsx +++ b/src/app/[userName]/NewProjectForm.tsx @@ -1,9 +1,11 @@ 'use client' -import { useServerAction } from '@/lib/client/util' import { useRouter } from 'next/navigation' import { useState } from 'react' import useSWR from 'swr' + +import { useServerAction } from '@/lib/client/util' + import { createProject, listTemplates, type TemplateInfo } from './actions' export function NewProjectForm() { diff --git a/src/app/[userName]/ProjectRow.tsx b/src/app/[userName]/ProjectRow.tsx index b6393a5..66efc0c 100644 --- a/src/app/[userName]/ProjectRow.tsx +++ b/src/app/[userName]/ProjectRow.tsx @@ -1,11 +1,13 @@ 'use client' -import { useServerAction } from '@/lib/client/util' import { Route } from 'next' import Link from 'next/link' import { useRouter } from 'next/navigation' import { startTransition, useState } from 'react' -import { deleteProject, renameProject, toggleVisibility, type ProjectInfo } from './actions' + +import { useServerAction } from '@/lib/client/util' + +import { deleteProject, type ProjectInfo, renameProject, toggleVisibility } from './actions' export function ProjectRow({ project, username }: { project: ProjectInfo; username: string }) { const router = useRouter() diff --git a/src/app/[userName]/[projectName]/page.tsx b/src/app/[userName]/[projectName]/page.tsx index 37797cd..c31d291 100644 --- a/src/app/[userName]/[projectName]/page.tsx +++ b/src/app/[userName]/[projectName]/page.tsx @@ -1,11 +1,12 @@ +import { notFound } from 'next/navigation' +import z from 'zod' + import Error from '@/app/components/Error' import { requireAuth } from '@/lib/server/auth' import { getDb } from '@/lib/server/db' import { getEditorSessionManager } from '@/lib/server/editorSessions' import { canAccessProject } from '@/lib/server/util' import { zProjectName, zUserName } from '@/lib/util' -import { notFound } from 'next/navigation' -import z from 'zod' const zParams = z.object({ userName: zUserName, diff --git a/src/app/[userName]/[projectName]/preview/[...relPath]/route.ts b/src/app/[userName]/[projectName]/preview/[...relPath]/route.ts index e0e938e..f19a1bc 100644 --- a/src/app/[userName]/[projectName]/preview/[...relPath]/route.ts +++ b/src/app/[userName]/[projectName]/preview/[...relPath]/route.ts @@ -1,12 +1,13 @@ +import type { Route } from 'next' +import { notFound, redirect } from 'next/navigation' +import z from 'zod' + import { requireAuth } from '@/lib/server/auth' import { getProjectDir } from '@/lib/server/config' import { getDb } from '@/lib/server/db' import { mintSignedDirToken, signedDirFileUrl } from '@/lib/server/dirToken' import { canAccessProject } from '@/lib/server/util' import { zProjectName, zUserName } from '@/lib/util' -import type { Route } from 'next' -import { notFound, redirect } from 'next/navigation' -import z from 'zod' const zParams = z.object({ userName: zUserName, diff --git a/src/app/[userName]/actions.ts b/src/app/[userName]/actions.ts index a736598..1338bb3 100644 --- a/src/app/[userName]/actions.ts +++ b/src/app/[userName]/actions.ts @@ -1,16 +1,18 @@ 'use server' +import crypto from 'node:crypto' +import fs from 'node:fs/promises' +import path from 'node:path' + +import { forbidden } from 'next/navigation' +import z from 'zod' + import { requireAuth } from '@/lib/server/auth' import { getPackageSetsDir, getTemplatesDir, getWorkspacesDir } from '@/lib/server/config' import { getDb } from '@/lib/server/db' import { existsAsync, serverAction } from '@/lib/server/util' -import { zProjectId, zProjectName, zTemplateId, type ActionResponse } from '@/lib/util' +import { type ActionResponse, zProjectId, zProjectName, zTemplateId } from '@/lib/util' import { Project } from '@/prisma/generated/client' -import { forbidden } from 'next/navigation' -import crypto from 'node:crypto' -import fs from 'node:fs/promises' -import path from 'node:path' -import z from 'zod' const zTemplateMetadata = z.object({ name: z.string(), diff --git a/src/app/[userName]/page.tsx b/src/app/[userName]/page.tsx index 20eb773..b0f3007 100644 --- a/src/app/[userName]/page.tsx +++ b/src/app/[userName]/page.tsx @@ -1,10 +1,12 @@ -import { requireAuth } from '@/lib/server/auth' -import { getDb } from '@/lib/server/db' -import { zUserName } from '@/lib/util' import { Route } from 'next' import Link from 'next/link' import { notFound } from 'next/navigation' import z from 'zod' + +import { requireAuth } from '@/lib/server/auth' +import { getDb } from '@/lib/server/db' +import { zUserName } from '@/lib/util' + import { NewProjectForm } from './NewProjectForm' import { ProjectRow } from './ProjectRow' diff --git a/src/app/admin/actions.ts b/src/app/admin/actions.ts index 3e74590..c43b124 100644 --- a/src/app/admin/actions.ts +++ b/src/app/admin/actions.ts @@ -1,16 +1,18 @@ 'use server' +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' + +import { forbidden } from 'next/navigation' +import z from 'zod' + import { initAuth, requireAuth } from '@/lib/server/auth' import { getConfig, getDataDir, getWorkspacesDir, saveConfig, zGithubAuthConfig } from '@/lib/server/config' import { getDb } from '@/lib/server/db' import { getEditorSessionManager } from '@/lib/server/editorSessions' import { serverAction } from '@/lib/server/util' -import { zProjectId, zUserId, zUserName, type ActionResponse } from '@/lib/util' -import { forbidden } from 'next/navigation' -import { execFileSync } from 'node:child_process' -import fs from 'node:fs' -import path from 'node:path' -import z from 'zod' +import { type ActionResponse, zProjectId, zUserId, zUserName } from '@/lib/util' export async function requireAdmin() { const session = await requireAuth() diff --git a/src/app/admin/components/AccessControl.tsx b/src/app/admin/components/AccessControl.tsx index 9018da1..22cf688 100644 --- a/src/app/admin/components/AccessControl.tsx +++ b/src/app/admin/components/AccessControl.tsx @@ -1,5 +1,6 @@ import { getConfig } from '@/lib/server/config' import { getDb } from '@/lib/server/db' + import { AllowlistEditor } from './AllowlistEditor' import { RegistrationModeControl } from './RegistrationModeControl' diff --git a/src/app/admin/components/AllowlistEditor.tsx b/src/app/admin/components/AllowlistEditor.tsx index efdee20..3e6a144 100644 --- a/src/app/admin/components/AllowlistEditor.tsx +++ b/src/app/admin/components/AllowlistEditor.tsx @@ -1,10 +1,11 @@ 'use client' -import { addAllowedUser, removeAllowedUser } from '@/app/admin/actions' -import { useServerAction } from '@/lib/client/util' import { useRouter } from 'next/navigation' import { startTransition, useRef } from 'react' +import { addAllowedUser, removeAllowedUser } from '@/app/admin/actions' +import { useServerAction } from '@/lib/client/util' + export function AllowlistEditor({ users }: { users: string[] }) { const router = useRouter() const inputRef = useRef(null) diff --git a/src/app/admin/components/HealthMonitor.tsx b/src/app/admin/components/HealthMonitor.tsx index bbc01dc..35496b9 100644 --- a/src/app/admin/components/HealthMonitor.tsx +++ b/src/app/admin/components/HealthMonitor.tsx @@ -1,9 +1,11 @@ 'use client' -import { fetchDiskUsage, fetchHealth } from '@/app/admin/actions' -import { useServerAction } from '@/lib/client/util' import { startTransition, useState } from 'react' import useSWR from 'swr' + +import { fetchDiskUsage, fetchHealth } from '@/app/admin/actions' +import { useServerAction } from '@/lib/client/util' + import { formatBytes, formatUptime } from './utils' const labelStyle = { padding: '4px 12px 4px 0', color: '#666' } diff --git a/src/app/admin/components/OAuthConfig.tsx b/src/app/admin/components/OAuthConfig.tsx index 452afa4..7138e48 100644 --- a/src/app/admin/components/OAuthConfig.tsx +++ b/src/app/admin/components/OAuthConfig.tsx @@ -1,10 +1,11 @@ 'use client' -import { fetchOAuthConfig, updateOAuthConfig } from '@/app/admin/actions' -import { useServerAction } from '@/lib/client/util' import { useState } from 'react' import useSWR from 'swr' +import { fetchOAuthConfig, updateOAuthConfig } from '@/app/admin/actions' +import { useServerAction } from '@/lib/client/util' + export function OAuthConfig() { const { data, mutate } = useSWR('adminOAuthConfig', () => fetchOAuthConfig()) const [editing, setEditing] = useState(false) diff --git a/src/app/admin/components/RegistrationModeControl.tsx b/src/app/admin/components/RegistrationModeControl.tsx index 52423a9..2a722c0 100644 --- a/src/app/admin/components/RegistrationModeControl.tsx +++ b/src/app/admin/components/RegistrationModeControl.tsx @@ -1,10 +1,11 @@ 'use client' +import { useRouter } from 'next/navigation' +import { startTransition, useState } from 'react' + import { setRegistrationMode } from '@/app/admin/actions' import { useServerAction } from '@/lib/client/util' import type { RegistrationMode } from '@/lib/server/config' -import { useRouter } from 'next/navigation' -import { startTransition, useState } from 'react' const MODES: { value: RegistrationMode; label: string }[] = [ { value: 'open', label: 'Open registration' }, diff --git a/src/app/admin/components/SessionRow.tsx b/src/app/admin/components/SessionRow.tsx index e1a487b..68728ca 100644 --- a/src/app/admin/components/SessionRow.tsx +++ b/src/app/admin/components/SessionRow.tsx @@ -1,11 +1,12 @@ 'use client' +import { useRouter } from 'next/navigation' +import { startTransition } from 'react' + import { killEditorSession } from '@/app/admin/actions' import ProjectLink from '@/app/components/ProjectLink' import { useServerAction } from '@/lib/client/util' import type { EditorSessionInfo } from '@/lib/server/editorSessions' -import { useRouter } from 'next/navigation' -import { startTransition } from 'react' export function SessionRow({ info }: { info: EditorSessionInfo }) { const router = useRouter() diff --git a/src/app/admin/components/SessionViewer.tsx b/src/app/admin/components/SessionViewer.tsx index c7d04c0..6d4db56 100644 --- a/src/app/admin/components/SessionViewer.tsx +++ b/src/app/admin/components/SessionViewer.tsx @@ -1,5 +1,6 @@ import { requireAdmin } from '@/app/admin/actions' import { getEditorSessionManager } from '@/lib/server/editorSessions' + import { SessionRow } from './SessionRow' export async function SessionViewer() { diff --git a/src/app/admin/components/UserManagement.tsx b/src/app/admin/components/UserManagement.tsx index 7de526e..fd2b972 100644 --- a/src/app/admin/components/UserManagement.tsx +++ b/src/app/admin/components/UserManagement.tsx @@ -1,5 +1,6 @@ import { requireAdmin } from '@/app/admin/actions' import { getDb } from '@/lib/server/db' + import { UserRow } from './UserRow' export async function UserManagement() { diff --git a/src/app/admin/components/UserRow.tsx b/src/app/admin/components/UserRow.tsx index 880b7c2..13a8eaa 100644 --- a/src/app/admin/components/UserRow.tsx +++ b/src/app/admin/components/UserRow.tsx @@ -1,10 +1,11 @@ 'use client' -import { deleteUser, toggleAdmin } from '@/app/admin/actions' -import { useServerAction } from '@/lib/client/util' import { useRouter } from 'next/navigation' import { startTransition, useState } from 'react' +import { deleteUser, toggleAdmin } from '@/app/admin/actions' +import { useServerAction } from '@/lib/client/util' + type User = { id: string; name: string; isAdmin: boolean } export function UserRow({ user, isSelf }: { user: User; isSelf: boolean }) { diff --git a/src/app/api/auth-route/file/route.ts b/src/app/api/auth-route/file/route.ts index 42584dc..4c591b3 100644 --- a/src/app/api/auth-route/file/route.ts +++ b/src/app/api/auth-route/file/route.ts @@ -1,8 +1,10 @@ -import { verifySignedDirToken } from '@/lib/server/dirToken' -import { forbidden } from 'next/navigation' import fs from 'node:fs/promises' import path from 'node:path' +import { forbidden } from 'next/navigation' + +import { verifySignedDirToken } from '@/lib/server/dirToken' + /** Queried by Nginx to authorize a `/_file//` request. * Any 2xx makes Nginx serve the file named in the `X-Validated-File-Path` response header, * whereas other codes cause Nginx to reject the original request. */ diff --git a/src/app/api/auth-route/vs/route.ts b/src/app/api/auth-route/vs/route.ts index e8e22fa..e696486 100644 --- a/src/app/api/auth-route/vs/route.ts +++ b/src/app/api/auth-route/vs/route.ts @@ -1,6 +1,7 @@ +import { forbidden } from 'next/navigation' + import { requireAuth } from '@/lib/server/auth' import { getEditorSessionManager } from '@/lib/server/editorSessions' -import { forbidden } from 'next/navigation' /** Queried by Nginx to ensure the sending user can access the given editor session. * Any 2xx response counts for successful authentication, diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts index f81a0e8..00e7281 100644 --- a/src/app/api/auth/[...all]/route.ts +++ b/src/app/api/auth/[...all]/route.ts @@ -1,6 +1,7 @@ -import { getAuth } from '@/lib/server/auth' import { toNextJsHandler } from 'better-auth/next-js' +import { getAuth } from '@/lib/server/auth' + export const { GET, POST } = toNextJsHandler(async req => { // Retrieve the auth instance on each request, in case it had been re-initialized const auth = await getAuth() diff --git a/src/app/components/AvatarIcon.tsx b/src/app/components/AvatarIcon.tsx index b432a3c..5fc28fc 100644 --- a/src/app/components/AvatarIcon.tsx +++ b/src/app/components/AvatarIcon.tsx @@ -1,6 +1,7 @@ -import type { User } from '@/lib/server/auth' import Image from 'next/image' +import type { User } from '@/lib/server/auth' + export default function AvatarIcon({ user }: { user: Pick }) { return (