Skip to content

fix: let npm run lint join CI - #324

Closed
FrameAutomata wants to merge 3 commits into
mainfrom
fix/316-frontend-lint
Closed

fix: let npm run lint join CI#324
FrameAutomata wants to merge 3 commits into
mainfrom
fix/316-frontend-lint

Conversation

@FrameAutomata

Copy link
Copy Markdown
Collaborator

Closes #316.

npm run lint (prettier --check . && eslint .) exits 1 on main. This makes it exit 0 so it can become a CI gate — #315 is stacked on this branch and adds the step.

What was actually there

The issue expected the three svelte/prefer-svelte-reactivity errors to be latent staleness bugs — plain Map/URLSearchParams mutated under runes, so the UI never updates. They aren't. prefer-svelte-reactivity.js does no escape or scope analysis: it flags any new Map() / new URLSearchParams() on which set/delete/clear is called anywhere in a .svelte file. All three instances are function-local scratch — two inside $derived.by callbacks that return arrays, one inside a pure function returning a string.

So reaching for SvelteMap would have added reactive proxying to paths that re-run on every search keystroke, while implying a requirement that isn't there. Two sites drop the mutation instead (build the Map from entries; fold the conditional search param into the constructor) and the third, which genuinely needs get-or-create, carries a disable with the reason.

Three of the seven no-navigation-without-resolve errors were false positives too — both overview-tab helpers already resolved internally, but the rule only recognises a literal resolve() at the link site. Those now use resolveHref behind the {...{ href }} spread, the idiom six other <a> elements already use. A plain href={instanceHref(server)} still errors, so this doesn't quietly widen what the rule accepts.

The four real ones

Site Why it needed its own shape
root + connection static route — just takes resolve()
dashboards setServerScope was re-emitting url.pathname read back from window.location, which already carries the base pathresolve() would prepend it twice. Resolves the route id instead.
finish-setup see below

finish-setup is the one worth review time. returnTo is a concrete path from the SSO handoff, and there is no safe resolve() form for it — resolve() substitutes [params], and resolve_route('/a[b]c', undefined) throws TypeError: Cannot read properties of undefined (reading 'b') (verified against the installed kit 2.49.2). It prefixes base directly with a disable explaining why.

gotoHref/resolveHref have this same latent crash, already reachable via user-supplied returnTo at login/+page.svelte:94 and auth/callback/+page.svelte:45. Pre-existing, not touched here — I'll file it separately. Note that passing {} for params does not fix it; it just becomes Missing parameter.

Scope

paths.base is unset in svelte.config.js, so resolve() is identity and nothing here changes runtime behaviour. This is explicitly not a claim that the app is base-path correct — roughly 30 sites the rule cannot see (redirect() in load functions, window.location.href in api.ts, window.open in the row click handlers) still emit unresolved absolute paths. That's a separate job.

CLAUDE.md

Two entries traceable to this drift: npm run lint was missing from the command table (an agent runs npm run check, which passes on lint-failing code, and never learns the gate exists), and the "Adding a New Frontend Page" template taught catch (e: any) — which trips no-explicit-any, and which #314 introduced by following it. No page actually does that; it's catch (e) 93× and catch (e: unknown) 31× via the $lib/utils/errors helpers.

Verification

npm run lint 0 · npm run check 0 errors (12 warnings, unchanged) · npm run test 25 passed · npm run build ok.

The prettier commit is separate and is npm run format output only — reflow, no token changes.

🤖 Generated with Claude Code

FrameAutomata and others added 3 commits August 27, 2026 09:38
`npm run lint` is `prettier --check . && eslint .`, and the prettier half
fails on `main`. This is `npm run format` and nothing else -- reflow only,
no token changes -- kept as its own commit so it does not bury the eslint
fixes that follow.

Refs #316

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`npm run lint` is `prettier --check . && eslint .`; the previous commit fixed
the prettier half, this fixes the eslint half.

Scope note: `paths.base` is unset in svelte.config.js, so `resolve()` is
identity today and none of this changes behaviour. It is NOT a claim that the
app is base-path correct -- ~30 sites the rule cannot see (`redirect()` in
load functions, `window.location.href` in api.ts, `window.open` in the row
click handlers) still emit unresolved absolute paths. This clears the flagged
sites so the gate can go green; base-path support is a separate job.

## svelte/prefer-svelte-reactivity (3, all in overview-tab)

All three were false positives, not the latent staleness bugs #316 suspected.
The rule does no escape or scope analysis: it flags any `new Map()` /
`new URLSearchParams()` on which `set`/`delete`/`clear` is called anywhere in
a `.svelte` file. All three instances are function-local scratch -- two inside
`$derived.by` callbacks that return arrays, one inside a pure function
returning a string -- so a reactive SvelteMap would only add proxying to paths
that re-run on every search keystroke.

Two sites drop the mutation instead, which is both simpler and satisfies the
rule honestly: `projectOptions` builds its Map from entries, and
`instanceHref` folds its conditional search param into the constructor. The
third genuinely needs get-or-create and carries a disable with the reason.

## svelte/no-navigation-without-resolve (7)

Four were real, and each needed a different shape:

- root + connection pages: `goto('/setup')` is a static route, so it just
  takes `resolve()`.
- dashboards `setServerScope`: was re-emitting `url.pathname` read back from
  `window.location`, which already carries the base path -- wrapping that in
  `resolve()` would prepend it twice. This function only ever runs on
  /dashboards, so it resolves the route id instead. The `let` split is forced:
  the rule follows an identifier to its declarator but not through a `+`
  chain.
- finish-setup: `returnTo` is a concrete path from the SSO handoff, and there
  is no safe `resolve()` form for it -- `resolve()` substitutes `[params]`,
  and `resolve_route('/a[b]c', undefined)` throws
  `TypeError: Cannot read properties of undefined (reading 'b')` (verified
  against the installed kit 2.49.2). So it prefixes `base` directly and
  carries a disable explaining why. Note `gotoHref`/`resolveHref` in
  utils/navigation and utils/links have this same latent crash, which already
  reaches login/+page.svelte:94 and auth/callback/+page.svelte:45; that is
  pre-existing and left for its own change.

The three `href` errors in overview-tab were false positives -- both helpers
already resolved internally, but the rule only recognises a literal
`resolve()` at the link site. These now use `resolveHref` from utils/links
behind the `{...{ href }}` spread, the idiom six other `<a>` elements already
use for a helper-built href, with the helpers returning unresolved paths so
resolution happens exactly once. A plain `href={instanceHref(server)}` still
errors, so this does not quietly widen what the rule accepts.

Closes #316

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things this repo's own drift is traceable to.

`npm run lint` was missing from the Development Commands table, which lists
dev/build/check only. An agent following CLAUDE.md runs `npm run check` --
which passes on unformatted, lint-failing code -- and never learns the gate
exists. That is how 11 unformatted files and 10 eslint errors accumulated.

The "Adding a New Frontend Page" template taught `catch (e: any)`, which trips
@typescript-eslint/no-explicit-any; #314 introduced one by following it. No
page actually does this -- it is `catch (e)` 93 times and `catch (e: unknown)`
31 times, reading status and message through getErrorStatus/getErrorMessage
from $lib/utils/errors. The template now shows that, matching e.g.
routes/tasks/[task]/+page.svelte.

Refs #316

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FrameAutomata

Copy link
Copy Markdown
Collaborator Author

Filed the pre-existing resolveHref/gotoHref crash referenced in the description as #325 — it is reachable today via /login?returnTo= and the SSO callback, independently of this PR.

The base + prefix plus disable comment in finish-setup/+page.svelte here is a local workaround for the one site this PR had to touch; it should collapse into the shared helper fix when #325 lands.

@FrameAutomata

Copy link
Copy Markdown
Collaborator Author

Folded into #315 and closing.

#315 is now retargeted to main and already carries all three commits from here unchanged (62eab4ff, db902dac, 9cc87436) — they were the base of its branch, so nothing needed rebasing and no work is lost.

Keeping both open would reproduce the exact problem raised on #315: two PRs against main where one is a strict subset of the other still have to merge in an order. Folded, there is one PR that fixes npm run lint and adds the workflow that keeps it fixed, which is a single reviewable unit — the fixes exist only to let the gate exist.

Verified on the #315 branch just now, in nix develop .#frontend: npm run lint, npm run check (0 errors), npm run test (25 passed), npm run build all green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Frontend lint fails on pre-existing code, blocking npm run lint from joining CI

1 participant