feat(web): add eslint + prettier + vitest toolchain + web CI job (R8)
Added to web/package.json devDeps: eslint (9, flat config) + eslint-plugin-svelte + typescript-eslint + globals; prettier + prettier-plugin-svelte; vitest (jsdom env) + jsdom. New scripts: lint, lint:fix, format, format:check, test, test:watch. Configs: - web/eslint.config.js — flat config, TS + Svelte, browser/node globals, no-explicit-any as warn, unused-vars as error (ignores _-prefixed). - web/.prettierrc.json — single-quote, 100 width, svelte parser override. - web/.prettierignore — dist/node_modules/build/lockfiles. - web/vite.config.ts — vitest test block via reference directive, jsdom env, globals enabled. Sample test: web/src/lib/utils.test.ts (6 tests covering relativeTime, truncateMiddle, debounce — all passing). CI: new web job in .gitea/workflows/ci.yml (npm ci, check [advisory], lint [advisory], format:check [advisory], test [gate], build [gate]). Advisory steps use continue-on-error until the baseline is clean — matching the existing golangci-lint advisory pattern. Known baseline surfaced by the new toolchain (pre-existing, not caused by R8): svelte-check 154 errors (133-file config cascade), eslint 126 errors + 12 warnings (unused vars, @html XSS, unused CSS), prettier 175 unformatted files. Fixing these is a follow-up cleanup. VERSION 0.7.9 -> 0.7.10. Plan R8 marked done; C.1 updated.
This commit is contained in:
4
web/.prettierignore
Normal file
4
web/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
build/
|
||||
package-lock.json
|
||||
9
web/.prettierrc.json
Normal file
9
web/.prettierrc.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
39
web/eslint.config.js
Normal file
39
web/eslint.config.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import js from '@eslint/js'
|
||||
import ts from 'typescript-eslint'
|
||||
import svelte from 'eslint-plugin-svelte'
|
||||
import globals from 'globals'
|
||||
|
||||
export default ts.config(
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
{
|
||||
files: ['**/*.{ts,js,svelte}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['dist/', 'node_modules/', 'build/', '*.config.{ts,js}']
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
3971
web/package-lock.json
generated
3971
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,12 @@
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "svelte-check --tsconfig ./tsconfig.json"
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^1.23.0",
|
||||
@@ -18,13 +23,21 @@
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"bits-ui": "^2.18.1",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-svelte": "^2.46.0",
|
||||
"globals": "^15.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"prettier": "^3.3.0",
|
||||
"prettier-plugin-svelte": "^3.3.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^1.1.1",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "^6.0.0"
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
54
web/src/lib/utils.test.ts
Normal file
54
web/src/lib/utils.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { relativeTime, truncateMiddle, debounce } from './utils'
|
||||
|
||||
describe('relativeTime', () => {
|
||||
it('returns "never" for null/undefined/empty', () => {
|
||||
expect(relativeTime(null)).toBe('never')
|
||||
expect(relativeTime(undefined)).toBe('never')
|
||||
expect(relativeTime('')).toBe('never')
|
||||
})
|
||||
|
||||
it('returns "just now" for future timestamps', () => {
|
||||
const future = new Date(Date.now() + 10_000).toISOString()
|
||||
expect(relativeTime(future)).toBe('just now')
|
||||
})
|
||||
|
||||
it('formats seconds/minutes/hours/days', () => {
|
||||
const now = Date.now()
|
||||
expect(relativeTime(new Date(now - 5_000).toISOString())).toBe('5s ago')
|
||||
expect(relativeTime(new Date(now - 120_000).toISOString())).toBe('2m ago')
|
||||
expect(relativeTime(new Date(now - 3_600_000).toISOString())).toBe('1h ago')
|
||||
expect(relativeTime(new Date(now - 86_400_000 * 2).toISOString())).toBe('2d ago')
|
||||
})
|
||||
})
|
||||
|
||||
describe('truncateMiddle', () => {
|
||||
it('returns the string unchanged when at or under maxLen', () => {
|
||||
expect(truncateMiddle('short', 36)).toBe('short')
|
||||
expect(truncateMiddle('exactly36chars_exactly36chars_xxxxx', 36)).toBe(
|
||||
'exactly36chars_exactly36chars_xxxxx'
|
||||
)
|
||||
})
|
||||
|
||||
it('truncates in the middle, preserving both ends', () => {
|
||||
const out = truncateMiddle('investigation:nomos/dragonfly-memlock-overrun', 20)
|
||||
expect(out).toContain('…')
|
||||
// keep = floor((20 - 1) / 2) = 9 chars from each end
|
||||
expect(out.startsWith('investiga')).toBe(true)
|
||||
expect(out.endsWith('-overrun')).toBe(true)
|
||||
expect(out.length).toBeLessThanOrEqual(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('debounce', () => {
|
||||
it('collapses rapid calls into one trailing invocation', async () => {
|
||||
let calls = 0
|
||||
const fn = debounce(() => calls++, 20)
|
||||
fn()
|
||||
fn()
|
||||
fn()
|
||||
expect(calls).toBe(0)
|
||||
await new Promise((r) => setTimeout(r, 40))
|
||||
expect(calls).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,8 @@
|
||||
/// <reference types="vitest/config" />
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import { readFileSync, existsSync } from 'fs'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// In Docker, VERSION is copied into the build WORKDIR (/build/web/VERSION).
|
||||
// In local dev, the cwd is web/ and VERSION is two dirs up (../../VERSION
|
||||
@@ -50,5 +51,10 @@ export default defineConfig({
|
||||
// here so dev and prod agree on nomos's actual route paths.
|
||||
'/agent': authProxy('http://localhost:8092', (path) => path.replace(/^\/agent/, ''))
|
||||
}
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['src/**/*.{test,spec}.{ts,js}']
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user