mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
chore(eslint): add ESLint toolchain and clean src to strict 0/0 (#1165)
* chore(eslint): add eslint toolchain and configs * fix(eslint): resolve gradual-config errors (rules-of-hooks, no-empty, prefer-const, …) * chore(eslint): clear unused vars in config, contexts, app and test * chore(eslint): clear unused vars in api and music-network * chore(eslint): clear unused vars in utils * chore(eslint): clear unused vars in store * chore(eslint): clear unused vars in cover and hooks * chore(eslint): clear unused vars in components * chore(eslint): clear unused vars in pages * chore(eslint): remove explicit any in src * chore(eslint): align react-hooks exhaustive-deps * chore(eslint): zero gradual config on src * chore(eslint): strict hook rules in store and utils * chore(eslint): strict hook rules in hooks and cover * chore(eslint): strict hook rules in components * chore(eslint): strict hook rules in pages and contexts * chore(eslint): document scripts ignore in eslint config * chore(eslint): add lint script and zero strict findings * chore(eslint): address review round 1 (gradual 0/0, per-site disable reasons) * chore(eslint): tighten two set-state disable comments (review round 2 LOW) * chore(nix): sync npmDepsHash with package-lock.json * docs(changelog): add Under the Hood entry for ESLint setup (PR #1165) --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
+42
-14
@@ -25,7 +25,43 @@ export type CoverArtImageProps = {
|
||||
ensurePriority?: CoverPrefetchPriority;
|
||||
} & Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'>;
|
||||
|
||||
export function CoverArtImage({
|
||||
/**
|
||||
* Guard wrapper: with no `coverRef` we render a provisional placeholder and run
|
||||
* no hooks. The hook-bearing body lives in `CoverArtImageResolved`, which only
|
||||
* ever mounts with a non-null `coverRef` — so its hooks are unconditional.
|
||||
*/
|
||||
export function CoverArtImage(props: CoverArtImageProps) {
|
||||
if (!props.coverRef) {
|
||||
const {
|
||||
coverRef: _coverRef,
|
||||
displayCssPx: _displayCssPx,
|
||||
surface: _surface,
|
||||
fullRes: _fullRes,
|
||||
fetchQueueBias: _fetchQueueBias,
|
||||
observeRootMargin: _observeRootMargin,
|
||||
observeScrollRootId: _observeScrollRootId,
|
||||
ensurePriority: _ensurePriority,
|
||||
onError: _onError,
|
||||
className,
|
||||
alt,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
data-cover-provisional="true"
|
||||
role="img"
|
||||
aria-label={alt ?? ''}
|
||||
{...(rest as React.HTMLAttributes<HTMLDivElement>)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <CoverArtImageResolved {...props} coverRef={props.coverRef} />;
|
||||
}
|
||||
|
||||
type CoverArtImageResolvedProps = Omit<CoverArtImageProps, 'coverRef'> & { coverRef: CoverArtRef };
|
||||
|
||||
function CoverArtImageResolved({
|
||||
coverRef,
|
||||
displayCssPx,
|
||||
surface,
|
||||
@@ -38,19 +74,7 @@ export function CoverArtImage({
|
||||
ensurePriority: ensurePriorityProp,
|
||||
onError: restOnError,
|
||||
...rest
|
||||
}: CoverArtImageProps) {
|
||||
if (!coverRef) {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
data-cover-provisional="true"
|
||||
role="img"
|
||||
aria-label={alt ?? ''}
|
||||
{...(rest as React.HTMLAttributes<HTMLDivElement>)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}: CoverArtImageResolvedProps) {
|
||||
const pinnedHigh = ensurePriorityProp === 'high';
|
||||
const [ensurePriority, setEnsurePriority] = useState<CoverPrefetchPriority>(
|
||||
ensurePriorityProp ?? 'middle',
|
||||
@@ -61,6 +85,8 @@ export function CoverArtImage({
|
||||
const [imgLoadFailed, setImgLoadFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
if (ensurePriorityProp) setEnsurePriority(ensurePriorityProp);
|
||||
}, [ensurePriorityProp]);
|
||||
|
||||
@@ -69,6 +95,8 @@ export function CoverArtImage({
|
||||
}, [seenViewport]);
|
||||
|
||||
useEffect(() => {
|
||||
// React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setImgLoadFailed(false);
|
||||
}, [coverRef.cacheEntityId, coverRef.cacheKind, coverRef.fetchCoverArtId, displayCssPx, surface, fullRes]);
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ export function ArtistHeroCover({
|
||||
}, [artistInfo?.largeImageUrl, artistInfo?.mediumImageUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
// React Compiler set-state-in-effect rule: local state synced with store/prop inputs when the effect’s dependencies change.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setExternalFailed(false);
|
||||
setExternalUrl('');
|
||||
if (!candidateUrl) return;
|
||||
|
||||
@@ -16,6 +16,8 @@ export function useCoverLightboxSrc(
|
||||
useEffect(() => {
|
||||
if (!open || !ref) return;
|
||||
let cancelled = false;
|
||||
// React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setLoading(true);
|
||||
void (async () => {
|
||||
const diskSrc = await ensureCoverTierDiskSrc(ref, 2000);
|
||||
@@ -30,10 +32,15 @@ export function useCoverLightboxSrc(
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// Keyed on the ref's identity fields intentionally; depending on the `ref`
|
||||
// object itself would re-fetch the lightbox source on every render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, ref?.cacheEntityId, ref?.cacheKind, ref?.fetchCoverArtId, ref?.serverScope]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) return;
|
||||
// React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setSrc('');
|
||||
setLoading(false);
|
||||
}, [open]);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { getDiskSrc } from './diskSrcCache';
|
||||
import { getDiskSrcForGrid } from './diskSrcLookup';
|
||||
import { coverTrafficServerSwitchPaused } from './coverTraffic';
|
||||
import { rememberGridDiskSrc } from './diskSrcLookup';
|
||||
import { coverStorageKeyFromRef } from './storageKeys';
|
||||
import type { CoverArtRef, CoverArtTier } from './types';
|
||||
|
||||
function peekMemoryHit(storageKey: string, ref: CoverArtRef, tier: CoverArtTier): boolean {
|
||||
|
||||
@@ -37,6 +37,8 @@ function useArtistExternalImage(
|
||||
useEffect(() => {
|
||||
if (!enabled || !artistId) {
|
||||
// Nothing will resolve — not pending, so callers fall back immediately.
|
||||
// React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setImage({ src: '', pending: false });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export function useAlbumCoverRef(
|
||||
const id = albumId?.trim();
|
||||
if (!id) return null;
|
||||
return albumCoverRef(id, fallbackCoverArt, { serverScope, distinctDiscCovers });
|
||||
}, [albumId, fallbackCoverArt, scopeKey, serverScope, distinctDiscCovers]);
|
||||
}, [albumId, fallbackCoverArt, serverScope, distinctDiscCovers]);
|
||||
|
||||
const [ref, setRef] = useState<CoverArtRef | null>(syncRef);
|
||||
|
||||
@@ -83,6 +83,10 @@ export function useAlbumCoverRef(
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// serverScope is keyed via the stable `scopeKey` string (and via syncRef);
|
||||
// depending on the object directly would re-resolve from SQLite on every
|
||||
// render when the scope identity changes but its content does not.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [albumId, fallbackCoverArt, scopeKey, syncRef, libraryResolve]);
|
||||
|
||||
return libraryResolve ? ref : syncRef;
|
||||
@@ -101,7 +105,7 @@ export function useArtistCoverRef(
|
||||
const id = artistId?.trim();
|
||||
if (!id) return null;
|
||||
return artistCoverRef(id, fallbackCoverArt, serverScope);
|
||||
}, [artistId, fallbackCoverArt, scopeKey, serverScope]);
|
||||
}, [artistId, fallbackCoverArt, serverScope]);
|
||||
|
||||
const [ref, setRef] = useState<CoverArtRef | null>(syncRef);
|
||||
|
||||
@@ -119,6 +123,10 @@ export function useArtistCoverRef(
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// serverScope is keyed via the stable `scopeKey` string (and via syncRef);
|
||||
// depending on the object directly would re-resolve from SQLite on every
|
||||
// render when the scope identity changes but its content does not.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [artistId, fallbackCoverArt, scopeKey, syncRef, libraryResolve]);
|
||||
|
||||
return libraryResolve ? ref : syncRef;
|
||||
@@ -193,6 +201,10 @@ export function useTrackCoverRef(
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// serverScope is keyed via the stable `scopeKey` string; depending on the
|
||||
// object directly would re-resolve from SQLite on every render when the
|
||||
// scope identity changes but its content does not.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [song, songId, albumId, coverArt, discNumber, scopeKey, syncRef, libraryResolve, distinctDiscCovers]);
|
||||
|
||||
return libraryResolve ? ref : syncRef;
|
||||
@@ -226,6 +238,11 @@ export function usePlaybackTrackCoverRef(
|
||||
}
|
||||
}
|
||||
return resolvePlaybackCoverScope();
|
||||
// queueServerId/queueLength/activeServerId/serversFingerprint look unused but
|
||||
// are intentional recompute triggers: resolvePlaybackCoverScope() and
|
||||
// resolveServerIdForIndexKey() read global server/queue state, so the scope
|
||||
// must re-derive when those change.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [track, queueItems, queueIndex, queueServerId, queueLength, activeServerId, serversFingerprint]);
|
||||
const scopeKey = coverScopeKey(scope);
|
||||
|
||||
@@ -237,6 +254,9 @@ export function usePlaybackTrackCoverRef(
|
||||
const syncRef = useMemo(() => {
|
||||
if (!albumId?.trim() || !track) return undefined;
|
||||
return albumCoverRefForPlayback(track, scope);
|
||||
// `scope` is keyed via the stable `scopeKey` string; the primitive track
|
||||
// fields recompute the ref when the playing track changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [track, trackId, albumId, coverArt, discNumber, scopeKey]);
|
||||
|
||||
const [ref, setRef] = useState<CoverArtRef | undefined>(syncRef);
|
||||
@@ -279,6 +299,9 @@ export function usePlaybackTrackCoverRef(
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// `scope` is keyed via the stable `scopeKey` string; depending on the object
|
||||
// directly would re-resolve from SQLite on every render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [track, trackId, albumId, coverArt, discNumber, scopeKey, syncRef]);
|
||||
|
||||
return ref;
|
||||
|
||||
@@ -26,6 +26,10 @@ export function usePlaybackCoverArt(
|
||||
|
||||
const scope = useMemo(
|
||||
() => resolvePlaybackCoverScope(),
|
||||
// resolvePlaybackCoverScope() reads global server/queue state; the listed
|
||||
// values look unused but are intentional recompute triggers so the scope
|
||||
// re-derives whenever the playback server or queue position changes.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[queueServerId, queueIndex, playingServerId, queueLength, activeServerId, serversFingerprint],
|
||||
);
|
||||
const refWithScope = useMemo(
|
||||
|
||||
Reference in New Issue
Block a user