mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-21 09:39:46 -05:00
client_v2: add ESLint + wire lint/format/typecheck into lefthook
client_v2 has matured, so enforce its checks like the old client: - Add ESLint (typescript-eslint + eslint-plugin-svelte) with a flat config; `lint` now runs prettier --check and eslint. - Add a "Client v2" group to lefthook pre-commit and ci: eslint, prettier, and svelte-check type checking (svelte-kit sync first). - Exclude generated paraglide output from prettier. Fix the linting warnings this surfaced: drop dead vars and an unused svelte-ignore, mark intentional reactive reads with `void`, remove a useless children snippet, and clean up useless string-literal mustaches. Disable svelte/no-navigation-without-resolve (base-path resolution is centralized in params.ts helpers, so the rule only false-positives and wrapping call sites would double-apply the base path). Keep svelte/prefer-svelte-reactivity on with per-line disables at the transient-local Set/Map sites. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
52b0580df9
commit
5eb660f420
21 files changed
+1728
-59
No files matched your search
@@ -6,3 +6,6 @@ build
|
||||
.svelte-kit
|
||||
dist
|
||||
node_modules
|
||||
|
||||
# Generated code (paraglide i18n compiler output — also gitignored)
|
||||
src/lib/paraglide
|
||||
+2
-2
@@ -82,7 +82,7 @@ Everything viewed is subscribed to and live-updates. The seam is `api/live.ts`
|
||||
- **`inventory`** is a reactive normalized **cache**. `api/liveSync.ts` runs one
|
||||
central subscription per resource and applies remote events to it, so every
|
||||
cache-backed view (inspectors, locations) is live automatically.
|
||||
- The **paginated list** fetches server-paged data *outside* the cache, so it
|
||||
- The **paginated list** fetches server-paged data _outside_ the cache, so it
|
||||
keeps its **own** subscription and refetches the current page on relevant
|
||||
events.
|
||||
- Local edits patch the cache **and** emit a `local` event (mirroring the server
|
||||
@@ -101,7 +101,7 @@ CSS custom properties in `src/app.css`.
|
||||
- **Desktop** — two panes: the filament/spool list on the left, a live inspector
|
||||
on the right.
|
||||
- **Mobile** — the list becomes full-width and front-and-centre; selecting an item
|
||||
slides the *same* inspector up as a bottom-sheet **Drawer**
|
||||
slides the _same_ inspector up as a bottom-sheet **Drawer**
|
||||
(`src/lib/components/Drawer.svelte`). The top-bar search/nav reflow onto their
|
||||
own rows and the "Add spools" button becomes a FAB.
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import globals from 'globals';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
export default ts.config(
|
||||
// Generated, tooling-managed, and build-output directories are not linted.
|
||||
{
|
||||
ignores: [
|
||||
'build/',
|
||||
'.svelte-kit/',
|
||||
'dist/',
|
||||
'package/',
|
||||
'src/lib/paraglide/', // auto-generated by inlang paraglide-js
|
||||
'project.inlang/', // inlang-managed
|
||||
'locales/' // Weblate-managed translation files
|
||||
]
|
||||
},
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
prettier,
|
||||
...svelte.configs.prettier,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.node }
|
||||
},
|
||||
rules: {
|
||||
// Typed as `no-undef`; TypeScript already reports undefined identifiers.
|
||||
'no-undef': 'off',
|
||||
// Base-path resolution is centralized in src/lib/library/params.ts helpers
|
||||
// (selectHref/libraryHref/etc. already call resolve() from $app/paths), and
|
||||
// generic components take a pre-resolved href prop. This rule can't see through
|
||||
// those helpers, so it only produces false positives here — and blindly wrapping
|
||||
// call sites in resolve() would double-apply the deploy base path.
|
||||
'svelte/no-navigation-without-resolve': 'off'
|
||||
// svelte/prefer-svelte-reactivity is left ON to guard against in-place mutation of
|
||||
// reactive Set/Map. The current hits are all transient computation-local collections
|
||||
// and are silenced with per-line eslint-disable comments at each site.
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
extraFileExtensions: ['.svelte'],
|
||||
parser: ts.parser,
|
||||
svelteConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
Generated
+1586
File diff suppressed because it is too large.
Load diff
@@ -13,19 +13,26 @@
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check ."
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"lint:eslint": "eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.5",
|
||||
"@inlang/paraglide-js": "^2.18.2",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.8.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.22.0",
|
||||
"globals": "^16.5.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.7",
|
||||
"svelte": "^5.1.9",
|
||||
"svelte-check": "^4.0.5",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "^5.4.10"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -37,4 +37,4 @@
|
||||
"lt",
|
||||
"tr"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,7 @@
|
||||
]).then(([vn, localMats, extMats]) => {
|
||||
vendorNames = vn;
|
||||
const specs: typeof materialSpecs = {};
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient local, not reactive state
|
||||
const names = new Set<string>(localMats);
|
||||
// Seed with the built-in common materials first…
|
||||
for (const m of COMMON_MATERIALS) {
|
||||
@@ -388,9 +389,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
let targetName = $derived(
|
||||
creating ? nf.name.trim() || m['add.newFilament']() : chosen ? cName(chosen) : ''
|
||||
);
|
||||
let summary = $derived(chosen || creating ? m['add.summary']({ count: countN }) : '');
|
||||
|
||||
// --- validation ---------------------------------------------------------
|
||||
@@ -520,11 +518,11 @@
|
||||
{#each externalResults as ext (ext.id)}
|
||||
<button class="res-item" onclick={() => choose({ source: 'external', ext })}>
|
||||
<Swatch
|
||||
colors={externalColors(ext)}
|
||||
direction={externalDirection(ext)}
|
||||
size={18}
|
||||
radius={5}
|
||||
/>
|
||||
colors={externalColors(ext)}
|
||||
direction={externalDirection(ext)}
|
||||
size={18}
|
||||
radius={5}
|
||||
/>
|
||||
<div class="res-name">
|
||||
<span class="rn">{ext.name}</span>
|
||||
<span class="rs">{ext.manufacturer} · {ext.material}</span>
|
||||
|
||||
@@ -9,13 +9,7 @@
|
||||
let { value, placeholder = '—', mono = false, oninput }: Props = $props();
|
||||
</script>
|
||||
|
||||
<input
|
||||
class="edit"
|
||||
class:mono
|
||||
{value}
|
||||
{placeholder}
|
||||
oninput={(e) => oninput?.(e.currentTarget.value)}
|
||||
/>
|
||||
<input class="edit" class:mono {value} {placeholder} oninput={(e) => oninput?.(e.currentTarget.value)} />
|
||||
|
||||
<style>
|
||||
.edit {
|
||||
|
||||
@@ -58,7 +58,11 @@
|
||||
{#if pageCount > 1}
|
||||
<div class="nums">
|
||||
{#if hrefFor && page > 1}
|
||||
<a class="pg nav" href={hrefFor(page - 1)} data-sveltekit-keepfocus data-sveltekit-noscroll
|
||||
<a
|
||||
class="pg nav"
|
||||
href={hrefFor(page - 1)}
|
||||
data-sveltekit-keepfocus
|
||||
data-sveltekit-noscroll
|
||||
aria-label={m['pagination.prev']()}><ChevronLeft size={16} /></a
|
||||
>
|
||||
{:else}
|
||||
@@ -79,7 +83,11 @@
|
||||
{/if}
|
||||
{/each}
|
||||
{#if hrefFor && page < pageCount}
|
||||
<a class="pg nav" href={hrefFor(page + 1)} data-sveltekit-keepfocus data-sveltekit-noscroll
|
||||
<a
|
||||
class="pg nav"
|
||||
href={hrefFor(page + 1)}
|
||||
data-sveltekit-keepfocus
|
||||
data-sveltekit-noscroll
|
||||
aria-label={m['pagination.next']()}><ChevronRight size={16} /></a
|
||||
>
|
||||
{:else}
|
||||
|
||||
@@ -137,7 +137,6 @@
|
||||
<p class="hint">{m['scanner.description']()}</p>
|
||||
|
||||
<div class="stage">
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video bind:this={video} playsinline></video>
|
||||
{#if error}
|
||||
<div class="msg error">{error}</div>
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
});
|
||||
async function loadInitial(signal: AbortSignal) {
|
||||
try {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient local, not reactive state
|
||||
const seen = new Set<number>();
|
||||
const ids: number[] = [];
|
||||
let offset = 0;
|
||||
@@ -149,6 +150,7 @@
|
||||
async function runSearch(q: string, signal: AbortSignal) {
|
||||
try {
|
||||
const { spools, filaments } = await searchAll(q, COLOR_THRESHOLD, signal);
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient local, not reactive state
|
||||
const ids = new Set<number>(spools.map((m) => m.entity.id));
|
||||
// The /search spool pass only matches spool-native fields, so also pull
|
||||
// the spools of any matched filament — that's how a filament- or
|
||||
@@ -196,6 +198,7 @@
|
||||
const recentCount = $derived(visibleSpools.filter(isRecent).length);
|
||||
|
||||
function toggle(id: number) {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient local; `selected` updates via reassignment below
|
||||
const next = new Set(selected);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
@@ -258,6 +261,7 @@
|
||||
});
|
||||
|
||||
function toggleF(id: string) {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient local; `selectedF` updates via reassignment below
|
||||
const next = new Set(selectedF);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
$effect(() => {
|
||||
const id = filamentId;
|
||||
const archived = showArchived;
|
||||
revision; // refetch on live spool events
|
||||
void revision; // refetch on live spool events
|
||||
let cancelled = false;
|
||||
spoolSource
|
||||
.listSpools({
|
||||
@@ -121,9 +121,7 @@
|
||||
</div>
|
||||
|
||||
<SectionLabel>
|
||||
{#snippet children()}<span style="padding-left:20px"
|
||||
>{m['inspector.spoolsCount']({ count: spools.length })}</span
|
||||
>{/snippet}
|
||||
<span style="padding-left:20px">{m['inspector.spoolsCount']({ count: spools.length })}</span>
|
||||
{#snippet right()}
|
||||
<button
|
||||
class="arch-toggle"
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
const ctrl = new AbortController();
|
||||
const gq = isGrouped ? buildGroupQuery(libraryState, ctrl.signal) : null;
|
||||
const sq = isGrouped ? null : buildFlatSpoolQuery(libraryState, ctrl.signal);
|
||||
revision; // refetch on live events
|
||||
void revision; // refetch on live events
|
||||
const mine = ++reqId;
|
||||
loading = true;
|
||||
errored = false;
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
let reqId = 0;
|
||||
$effect(() => {
|
||||
const ctrl = new AbortController();
|
||||
revision; // refetch on live events
|
||||
void revision; // refetch on live events
|
||||
const want = untrack(() => spools.length) || PAGE;
|
||||
const q = buildScopedSpoolQuery(libraryState, group, want, 0, ctrl.signal);
|
||||
const mine = ++reqId;
|
||||
|
||||
@@ -160,16 +160,6 @@
|
||||
|
||||
// "matched: <field>" label. Extra fields resolve to the field's display name;
|
||||
// native fields get a translated label (falling back to the raw name).
|
||||
const NATIVE_MATCH = new Set([
|
||||
'name',
|
||||
'material',
|
||||
'article_number',
|
||||
'comment',
|
||||
'location',
|
||||
'lot_nr',
|
||||
'id',
|
||||
'color'
|
||||
]);
|
||||
const MATCH_LABEL: Record<EntityKind, Record<string, () => string>> = {
|
||||
filament: {
|
||||
name: m['filament.fields.name'],
|
||||
@@ -257,11 +247,7 @@
|
||||
onclick={onResultClick}
|
||||
>
|
||||
<span class="id mono">#{spool.entity.id}</span>
|
||||
<Swatch
|
||||
colors={filament?.colors ?? []}
|
||||
direction={filament?.multiColorDirection}
|
||||
size={20}
|
||||
/>
|
||||
<Swatch colors={filament?.colors ?? []} direction={filament?.multiColorDirection} size={20} />
|
||||
<span class="text">
|
||||
<span class="title">{filament?.name || m['search.unknownFilament']()}</span>
|
||||
{#if spool.entity.location}<span class="sub">{spool.entity.location}</span>{/if}
|
||||
@@ -308,12 +294,7 @@
|
||||
<a
|
||||
class="result"
|
||||
class:active={activeIndex === indexOf('vendor', vendor.entity.id)}
|
||||
href={searchResultHref(
|
||||
page.url.searchParams,
|
||||
page.url.pathname,
|
||||
'vendor',
|
||||
vendor.entity.id
|
||||
)}
|
||||
href={searchResultHref(page.url.searchParams, page.url.pathname, 'vendor', vendor.entity.id)}
|
||||
data-sveltekit-noscroll
|
||||
onclick={onResultClick}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { getFields, setField, deleteField, type EntityType, type FieldDef, type FieldParams } from '$lib/api/fields';
|
||||
import {
|
||||
getFields,
|
||||
setField,
|
||||
deleteField,
|
||||
type EntityType,
|
||||
type FieldDef,
|
||||
type FieldParams
|
||||
} from '$lib/api/fields';
|
||||
|
||||
// Reactive cache of extra-field definitions per entity type. Loaded on demand
|
||||
// (inspectors) and by the settings manager; refetched after mutations.
|
||||
|
||||
@@ -28,8 +28,7 @@ export function hexRgb(h: string): [number, number, number] {
|
||||
export function swatchStyle(colors: string[] | undefined, direction?: MultiColorDirection): string {
|
||||
if (!colors || !colors.length) return 'background:#555';
|
||||
if (colors.length > 1) {
|
||||
const angle =
|
||||
direction === 'coaxial' ? '90deg' : direction === 'longitudinal' ? '0deg' : '135deg';
|
||||
const angle = direction === 'coaxial' ? '90deg' : direction === 'longitudinal' ? '0deg' : '135deg';
|
||||
// Each colour gets its own band, but boundaries are softened by a narrow
|
||||
// feather rather than a hard edge or a full-slot blur: colour i stays solid
|
||||
// across most of [i*step, (i+1)*step] and only fades over `feather`% on each
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// Spoolman ships as a client-side SPA served from the backend, so disable SSR
|
||||
// and prerender the shell.
|
||||
export const ssr = false;
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
});
|
||||
// A NULL location and an empty-string one are distinct rows to the database
|
||||
// but the same "No location" card here, so counts are summed by key.
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient local, not reactive state
|
||||
const totals = new Map<string, number>();
|
||||
for (const g of page.items) totals.set(g.key, (totals.get(g.key) ?? 0) + g.spoolCount);
|
||||
|
||||
@@ -360,6 +361,7 @@
|
||||
|
||||
// Which locations currently hold spools, by display name.
|
||||
function presentNames(): Set<string> {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient local, not reactive state
|
||||
const names = new Set<string>();
|
||||
for (const [key, b] of Object.entries(buckets)) {
|
||||
if (b.total > 0) names.add(key === EMPTY ? NO_LOCATION : key);
|
||||
@@ -666,12 +668,14 @@
|
||||
<div class="chip-info">
|
||||
<div class="chip-title">
|
||||
<span class="chip-id mono">#{s.id}</span>
|
||||
{#if v.name !== '?'}{v.name}{' - '}{/if}{f.name}
|
||||
{#if v.name !== '?'}{v.name} -
|
||||
{/if}{f.name}
|
||||
</div>
|
||||
<div class="chip-subtitle">
|
||||
{f.material}{' - '}<span class:low={settings.isLow(s.remaining, s.unused)}
|
||||
>{weightAuto(s.remaining)}</span
|
||||
>{' / '}{weightAuto(f.weight)}{#if s.lastUsedLabel}{' - '}{m['locations.lastUsed']({
|
||||
{f.material} -
|
||||
<span class:low={settings.isLow(s.remaining, s.unused)}>{weightAuto(s.remaining)}</span>
|
||||
/ {weightAuto(f.weight)}{#if s.lastUsedLabel}
|
||||
- {m['locations.lastUsed']({
|
||||
time: s.lastUsedLabel
|
||||
})}{/if}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ export default defineConfig({
|
||||
paraglideVitePlugin({
|
||||
project: './project.inlang',
|
||||
outdir: './src/lib/paraglide',
|
||||
strategy: ["localStorage", "preferredLanguage", "baseLocale"]
|
||||
strategy: ['localStorage', 'preferredLanguage', 'baseLocale']
|
||||
})
|
||||
],
|
||||
server: { port: 5174 }
|
||||
|
||||
@@ -23,6 +23,21 @@ pre-commit:
|
||||
root: "client/"
|
||||
run: ./node_modules/.bin/prettier --write .
|
||||
stage_fixed: true
|
||||
- group: # Client v2 checks
|
||||
parallel: false
|
||||
jobs:
|
||||
- name: Run lint check on client_v2 files
|
||||
root: "client_v2/"
|
||||
run: ./node_modules/.bin/eslint --fix . # Hardcoded path because Windows messes up if we use npx
|
||||
stage_fixed: true
|
||||
- name: Run format check on client_v2 files
|
||||
root: "client_v2/"
|
||||
run: ./node_modules/.bin/prettier --write .
|
||||
stage_fixed: true
|
||||
- name: Run type check on client_v2 files
|
||||
root: "client_v2/"
|
||||
# svelte-kit sync generates .svelte-kit/tsconfig.json, which svelte-check needs.
|
||||
run: ./node_modules/.bin/svelte-kit sync && ./node_modules/.bin/svelte-check --tsconfig ./tsconfig.json
|
||||
|
||||
# Same as pre-commit but no fixes
|
||||
ci:
|
||||
@@ -46,4 +61,17 @@ ci:
|
||||
- name: Run format check on client files
|
||||
root: "client/"
|
||||
run: ./node_modules/.bin/prettier --check .
|
||||
- group: # Client v2 checks
|
||||
parallel: false
|
||||
jobs:
|
||||
- name: Run lint check on client_v2 files
|
||||
root: "client_v2/"
|
||||
run: ./node_modules/.bin/eslint . # Hardcoded path because Windows messes up if we use npx
|
||||
- name: Run format check on client_v2 files
|
||||
root: "client_v2/"
|
||||
run: ./node_modules/.bin/prettier --check .
|
||||
- name: Run type check on client_v2 files
|
||||
root: "client_v2/"
|
||||
# svelte-kit sync generates .svelte-kit/tsconfig.json, which svelte-check needs.
|
||||
run: ./node_modules/.bin/svelte-kit sync && ./node_modules/.bin/svelte-check --tsconfig ./tsconfig.json
|
||||
|
||||
Reference in new issue
Block a user