Lit query docs#10652
Conversation
- Fix createQueriesController tuple type inference with recursive types - Add DataTag support to queryOptions - Fix build config and vitest custom-condition resolution - Fix example install scripts for standalone bootstrap
Co-authored-by: Dominik Dorfmeister 🔮 <office@dorfmeister.cc>
… monorepo is used
…zed at the monorepo root
…iases/gates, keeping the standard Nx target names so root CI can pick them up consistently
…ecause lit-query publishes custom CJS output/types
There was a problem hiding this comment.
Actionable comments posted: 9
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (16)
examples/lit/pagination/server/index.mjs-42-44 (1)
42-44:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTighten numeric query-param validation to reject partial parses.
Number.parseIntcurrently accepts values like1abc. That makes malformedpage/delaylook valid instead of returning400.✅ Suggested stricter parsing
function parsePositiveInt(rawValue, fallback) { if (rawValue == null || rawValue === '') { return fallback } - const parsed = Number.parseInt(rawValue, 10) - if (!Number.isInteger(parsed) || parsed < 1) { + if (!/^\d+$/.test(rawValue)) { + return undefined + } + const parsed = Number(rawValue) + if (!Number.isInteger(parsed) || parsed < 1) { return undefined } return parsed }function parseNonNegativeInt(rawValue, fallback) { if (rawValue == null || rawValue === '') { return fallback } - const parsed = Number.parseInt(rawValue, 10) - if (!Number.isInteger(parsed) || parsed < 0) { + if (!/^\d+$/.test(rawValue)) { + return undefined + } + const parsed = Number(rawValue) + if (!Number.isInteger(parsed) || parsed < 0) { return undefined } return parsed }Also applies to: 55-57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/lit/pagination/server/index.mjs` around lines 42 - 44, The current parse uses Number.parseInt on rawValue which accepts partial parses like "1abc"; change the validation so partial parses are rejected by verifying the rawValue is a pure integer string before or after parsing: either require /^\d+$/ (or /^\d+$/u) on rawValue then parse, or parse with Number(rawValue) and assert String(parsed) === rawValue and Number.isInteger(parsed) and parsed >= 1; update the block that computes parsed from rawValue (and the analogous block that parses delay) to implement this stricter check and return undefined / trigger a 400 for malformed input.examples/lit/basic/src/todoApi.ts-42-52 (1)
42-52:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard against empty todo titles before creating records.
addTodoOnServercurrently persists blank/whitespace-only titles, which leads to poor data quality in the demo state.🧩 Suggested input validation
export async function addTodoOnServer(title: string): Promise<Todo> { await delay(70) if (failNextMutation) { failNextMutation = false throw new Error('Forced mutation failure (test)') } + const normalizedTitle = title.trim() + if (!normalizedTitle) { + throw new Error('Title is required') + } + const nextTodo: Todo = { id: nextTodoId, - title, + title: normalizedTitle, }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/lit/basic/src/todoApi.ts` around lines 42 - 52, addTodoOnServer currently allows blank or whitespace-only titles; update the function (addTodoOnServer) to trim the incoming title and validate it before creating nextTodo: if title.trim() is empty, throw a clear Error (e.g., "Title cannot be empty") or return a rejected Promise so the caller can handle validation failures; use the trimmed value when constructing the Todo (id: nextTodoId, title) to avoid persisting whitespace-only strings and keep other logic (failNextMutation, delay, nextTodoId) unchanged.examples/lit/basic/config/port.js-9-11 (1)
9-11:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReject partially numeric
DEMO_PORTvalues instead of truncating them.
Number.parseIntwill accept values like"4173abc"as valid (4173). That makes malformed env input silently pass.Suggested fix
- const parsedPort = Number.parseInt(envPort, 10) + const isNumeric = /^\d+$/.test(envPort) + const parsedPort = isNumeric ? Number(envPort) : Number.NaN const isValidPort = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/lit/basic/config/port.js` around lines 9 - 11, The current parsing uses Number.parseInt which accepts "4173abc" and truncates it; change the validation so envPort is strictly numeric before parsing (e.g., check envPort matches /^\d+$/ or that String(parsedPort) === envPort.trim()), then compute parsedPort and set isValidPort to Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535; update the variables parsedPort and isValidPort accordingly to reject partially numeric DEMO_PORT values instead of silently truncating them.packages/lit-query/tsconfig.build.cjs.json-4-4 (1)
4-4:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse an empty array for
customConditionsinstead ofnull.
customConditions: nullis valid in TypeScript's tsconfig.json, but using an empty array[]is the idiomatic and recommended approach. TypeScript's official documentation describescustomConditionsas a "list of additional conditions" and all examples use arrays rather than null. An empty array provides the same functional result (no custom conditions) with better semantic clarity.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/lit-query/tsconfig.build.cjs.json` at line 4, Replace the "customConditions": null entry in the tsconfig.build.cjs.json with an empty array to follow TypeScript idioms: locate the JSON property named customConditions and change its value from null to [] so the config expresses "no custom conditions" as an empty list rather than null.examples/lit/ssr/scripts/dev.mjs-38-45 (1)
38-45:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSignal-killed server exit code silently maps to
0.When the spawned server is killed by a signal,
codein theexitevent isnull.outcome.code ?? 0then maps it to0, which makes the runner appear successful even though the server was killed externally.🔧 Proposed fix
- process.exitCode = outcome.code ?? 0 + process.exitCode = outcome.code ?? 1🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/lit/ssr/scripts/dev.mjs` around lines 38 - 45, The current race handler only captures the numeric exit code and maps null to 0, so if the server was killed by a signal it incorrectly reports success; update the 'exit' listener to capture both code and signal (e.g., once(server, 'exit').then(([code, signal]) => ({ code, signal })) ), then set process.exitCode to outcome.code if non-null, otherwise to a non-zero value when outcome.signal is present (e.g., 1) and only default to 0 when neither code nor signal exist; adjust references to once, server, 'exit', outcome, and process.exitCode accordingly.docs/framework/lit/reference/functions/createQueriesController.md-8-13 (1)
8-13:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMisaligned
queryClient?parameter in the signature block.
queryClient?is indented with extra leading space compared tohostandoptions, making the signature appear as if it is a nested parameter rather than a sibling.🔧 Proposed fix
function createQueriesController<TQueryOptions, TCombinedResult>( host, options, -queryClient?): QueriesResultAccessor<TCombinedResult>; + queryClient?): QueriesResultAccessor<TCombinedResult>;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/framework/lit/reference/functions/createQueriesController.md` around lines 8 - 13, The function signature for createQueriesController has the optional parameter queryClient? misaligned (extra leading space) compared to host and options; fix by aligning queryClient? with the other parameters in the signature block so all three parameters start at the same column (adjust the indentation in the code fence where createQueriesController<TQueryOptions, TCombinedResult>( host, options, queryClient?): QueriesResultAccessor<TCombinedResult>; is declared).docs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.md-29-32 (1)
29-32:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMinor inconsistency in
TDatadefault between reference pages.
CreateInfiniteQueryOptionsshowsTData = InfiniteData<TQueryFnData>(one type argument) whileinfiniteQueryOptions.mdshowsTData = InfiniteData<TQueryFnData, unknown>(two arguments). Both resolve identically at runtime, but the docs are inconsistent. Pick one form and apply it uniformly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.md` around lines 29 - 32, Update the docs to use a consistent `TData` default for CreateInfiniteQueryOptions: choose either `TData = InfiniteData<TQueryFnData>` or `TData = InfiniteData<TQueryFnData, unknown>` and apply that form uniformly across the CreateInfiniteQueryOptions reference page and the infiniteQueryOptions.md page; ensure both files reference the same `InfiniteData` signature for `TData` so the documentation no longer inconsistently shows different type-argument counts.packages/lit-query/scripts/l3-stress.mjs-80-100 (1)
80-100:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
cacheQueryreference may be stale after GC whengcTime: 0.
cacheQueryis captured on line 84 beforehost.disconnect()andquery.destroy(). WithgcTime: 0, the cache entry may be garbage-collected synchronously after the observer count drops to zero, leavingcacheQueryas a detached object.getObserversCount()on the evicted entry may not reflect the live cache state.Re-fetching the cache entry after disconnect/destroy makes the assertion reliable:
🛡️ Proposed fix
- const cacheQuery = client.getQueryCache().find({ queryKey }) - const connectedCount = cacheQuery?.getObserversCount() ?? 0 + const connectedCount = + client.getQueryCache().find({ queryKey })?.getObserversCount() ?? 0 if (connectedCount !== 1) { throw new Error( `observer_count_connected_invalid:${connectedCount}:cycle:${cycle}`, ) } host.disconnect() query.destroy() - const disconnectedCount = cacheQuery?.getObserversCount() ?? 0 + const disconnectedCount = + client.getQueryCache().find({ queryKey })?.getObserversCount() ?? 0 if (disconnectedCount !== 0) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/lit-query/scripts/l3-stress.mjs` around lines 80 - 100, The test captures cacheQuery before calling host.disconnect() and query.destroy(), which can become stale if the cache entry is GC'ed immediately (gcTime: 0); re-fetch the cache entry after teardown and use that fresh reference for the disconnected assertion instead of the earlier cacheQuery — i.e., call client.getQueryCache().find({ queryKey }) again after host.disconnect()/query.destroy() and call getObserversCount() on that re-fetched entry to validate the disconnected count.examples/lit/basic/src/mutation.ts-79-84 (1)
79-84:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winConsider guarding against double-submit while mutation is in-flight.
submit()firesmutateunconditionally, so rapid clicks will queue duplicate mutations. A simple guard on the mutation status would prevent this.🛡️ Proposed fix
private submit(): void { const title = this.nextTitle.trim() if (!title) return + if (this.addTodo().isPending) return this.addTodo.mutate(title) this.nextTitle = '' }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/lit/basic/src/mutation.ts` around lines 79 - 84, The submit() handler currently calls addTodo.mutate unconditionally which allows double-submits; update submit() to early-return if the mutation is in-flight (e.g., check addTodo.isLoading or addTodo.status === 'loading') before calling addTodo.mutate, and consider switching to addTodo.mutateAsync and awaiting it so you only clear nextTitle after success (or handle errors) to further avoid duplicate or inconsistent state.docs/framework/lit/reference/functions/createQueryController.md-34-34 (1)
34-34:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate
TErrordefault fromErrortoDefaultErrorin createQueryController documentation.The docs at line 34 incorrectly declare
TError = Error, but the actual implementation exportsTError = DefaultError. This matches the CreateInfiniteQueryOptions documentation and aligns with TanStack Query v5's standard default.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/framework/lit/reference/functions/createQueryController.md` at line 34, Update the createQueryController docs to change the generic default from TError = Error to TError = DefaultError; specifically edit the createQueryController documentation block where `TError` is declared so it matches the implementation's exported `TError = DefaultError` (and aligns with CreateInfiniteQueryOptions / TanStack Query v5 conventions).docs/framework/lit/reference/functions/createInfiniteQueryController.md-9-13 (1)
9-13:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMinor indentation inconsistency in the function signature.
queryClient?is missing the leading spaces thathostandoptionshave, causing visual misalignment in the rendered code block.✏️ Proposed fix
function createInfiniteQueryController<TQueryFnData, TError, TData, TQueryKey, TPageParam>( host, options, -queryClient?): InfiniteQueryResultAccessor<TData, TError>; + queryClient?): InfiniteQueryResultAccessor<TData, TError>;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/framework/lit/reference/functions/createInfiniteQueryController.md` around lines 9 - 13, The function signature for createInfiniteQueryController has a minor indentation mismatch: the parameter queryClient? is not aligned with host and options; update the signature in createInfiniteQueryController<TQueryFnData, TError, TData, TQueryKey, TPageParam> so that the queryClient? parameter has the same leading spaces as host and options to keep consistent formatting and visual alignment in the rendered documentation.docs/framework/lit/guides/ssr.md-73-78 (1)
73-78:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard against a missing
__QUERY_STATE__element before callinghydrate.
JSON.parse(... ?? 'null')producesnullwhen the script tag is absent, and the subsequenthydrate(queryClient, null)call's behavior is undefined. Consider adding an explicit guard:🛡️ Suggested defensive pattern
-const dehydratedState = JSON.parse( - document.getElementById('__QUERY_STATE__')?.textContent ?? 'null', -) as DehydratedState +const rawState = document.getElementById('__QUERY_STATE__')?.textContent +const dehydratedState: DehydratedState | null = rawState + ? (JSON.parse(rawState) as DehydratedState) + : null queryClient.mount() -hydrate(queryClient, dehydratedState) +if (dehydratedState) { + hydrate(queryClient, dehydratedState) +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/framework/lit/guides/ssr.md` around lines 73 - 78, The code reads and parses the '__QUERY_STATE__' script into dehydratedState and immediately calls hydrate(queryClient, dehydratedState), but JSON.parse(... ?? 'null') yields null when the element is missing, so hydrate may be called with null; update the logic around document.getElementById('__QUERY_STATE__') and dehydratedState so you only call hydrate(queryClient, dehydratedState) when the element exists and dehydratedState is non-null (otherwise skip hydrate or handle the missing state explicitly), keeping queryClient.mount() behavior unchanged.integrations/lit-vite/src/main.ts-40-52 (1)
40-52:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
render()should useprotected overridefor consistency.Both
createRenderRoot()overrides on lines 22 and 36 correctly useprotected override, butrender()on line 40 is missing these modifiers. While the integration's tsconfig doesn't enablenoImplicitOverride, maintaining consistent override declarations across all overridden methods is a best practice.✏️ Proposed fix
- render() { + protected override render() {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@integrations/lit-vite/src/main.ts` around lines 40 - 52, The render() method is missing the same override modifiers as other lifecycle overrides; update the render() declaration to use "protected override render()" so it matches the createRenderRoot() overrides and clearly indicates it's overriding the base class's render method (locate the render() function in main.ts and change its signature accordingly).examples/lit/ssr/src/main.ts-27-28 (1)
27-28:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
?? 'null'is dead code and the empty-content case is not caught.
stateElement.textContenton a DOM element is always astring(nevernullorundefined), so the?.and?? 'null'never fire. An empty or non-JSONtextContentfalls straight through toJSON.parse, throwing an opaqueSyntaxErrorrather than a descriptive error. Consider throwing a clear error on falsy/non-JSON content instead:🛡️ Proposed fix
- const stateText = stateElement.textContent?.trim() ?? 'null' - return JSON.parse(stateText) as DehydratedState + const stateText = stateElement.textContent?.trim() + if (!stateText) { + throw new Error('Dehydrated state script element is empty.') + } + return JSON.parse(stateText) as DehydratedState🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/lit/ssr/src/main.ts` around lines 27 - 28, The current parsing of the serialized state uses stateElement.textContent and can throw an opaque SyntaxError for empty or invalid JSON; update the logic around stateElement/stateText (the code returning JSON.parse(...) as DehydratedState) to explicitly check for empty or falsy stateText and throw a clear, descriptive error for missing content, and wrap JSON.parse in a try/catch to catch malformed JSON and rethrow a helpful error that includes the original text or parse message so callers can diagnose bad SSR state.docs/framework/lit/guides/mutations.md-6-6 (1)
6-6:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix hyphenation: "server-side effects".
"Server-side" modifies "effects" as a compound adjective and should be hyphenated.
✏️ Proposed fix
-Unlike queries, mutations are used to create, update, delete, or otherwise perform server side effects. +Unlike queries, mutations are used to create, update, delete, or otherwise perform server-side effects.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/framework/lit/guides/mutations.md` at line 6, Update the sentence describing mutations to hyphenate "server-side" (change "server side effects" to "server-side effects") in the guide text that references createMutationController so it reads: "Unlike queries, mutations are used to create, update, delete, or otherwise perform server-side effects. In Lit, use createMutationController(...)." Locate the sentence containing createMutationController and replace the unhyphenated phrase with the hyphenated one.packages/lit-query/src/tests/client-switch-controllers.test.ts-372-407 (1)
372-407:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRedundant
providerA.remove()at cleanup (already removed mid-test).
providerAis removed at line 380 as part of the reparent sequence, then removed again at line 404 in the cleanup block. The same issue exists in the "reparents infinite query" test at line 452 vs 478. Both double-removes are no-ops on a detached element but mislead readers into thinkingproviderAis still attached at cleanup time.🛠️ Proposed fix
consumer.queries.destroy() - providerA.remove() providerB.remove() await Promise.resolve()(Apply the equivalent removal for the infinite-query test as well.)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/lit-query/src/tests/client-switch-controllers.test.ts` around lines 372 - 407, The cleanup block contains a redundant call to providerA.remove() that was already removed earlier during the test reparenting; remove the duplicate providerA.remove() from the cleanup sequence in the test containing consumer/providerA/providerB interactions (and apply the same removal to the analogous cleanup in the "reparents infinite query" test) so each provider is only removed once; keep the other cleanup calls (providerB.remove(), consumer.queries.destroy(), Promise.resolve()) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/framework/lit/reference/type-aliases/ValueAccessor.md`:
- Line 9: The documented ValueAccessor type is wrong; replace the current `type
ValueAccessor<T> = () => T & object;` with the actual implementation shape: make
it a callable that also has a readonly current property by changing the
declaration to the intersection form used in packages/lit-query/src/accessor.ts
— i.e. a (() => T) callable intersected with `{ readonly current: T }` so the
docs match the runtime type ValueAccessor<T>.
In `@examples/lit/pagination/scripts/dev.mjs`:
- Around line 63-74: The current logic in dev.mjs using the winner variable sets
process.exitCode = 1 for the else branch which catches both clean exits
(winner.code === 0) and signal exits (winner.code === null), causing false
failures; change the conditional to explicitly handle the clean exit case: if
winner.code === 0 set process.exitCode = 0, otherwise if winner.code === null
set process.exitCode = 1 (or another non-zero value) and for any numeric
non-zero code set process.exitCode = winner.code — update the block that checks
winner.code after shutdown (the lines referencing winner and process.exitCode)
accordingly.
In `@examples/lit/pagination/src/main.ts`:
- Around line 83-117: The projectsQueryOptions object is being passed as a plain
object to createQueryController which snapshots it and prevents onHostUpdate
from applying changes in syncProjectsQueryOptions; change to pass an accessor
function that returns the options so they are re-derived on each host update
(e.g. replace the plain projectsQueryOptions object or the createQueryController
call to use a () => ({ queryKey: projectsQueryKey(this.page, this.delayMs,
this.forceErrorMode), queryFn: () => fetchProjectsPage(this.page, this.delayMs,
this.forceErrorMode), placeholderData: keepPreviousData }) so the queryKey and
queryFn are recalculated when this.page changes and the controller will pick up
updates via onHostUpdate/syncProjectsQueryOptions).
In `@examples/lit/ssr/index.html`:
- Around line 10-12: The template injects raw __QUERY_STATE_JSON__ into the
<script id="__QUERY_STATE__"> tag which can be broken out of by special
characters; before replacing the placeholder __QUERY_STATE_JSON__ on the server,
escape JSON content (replace at minimum '<', '>', '&' and any case-insensitive
'</script' sequence) so the injected string cannot close the script tag or
create executable HTML/JS; apply this escaping in the server-side template
rendering code that performs the placeholder replacement.
In `@packages/lit-query/package.json`:
- Around line 51-57: The package.json currently lists "lit" in both
"dependencies" (pinned to ^3.3.1) and "peerDependencies" (>=2.8.0 <4), which can
create dual instances of Lit; fix by either removing "lit" from "dependencies"
so Lit is peer-only (recommended for adapter packages) or by keeping it in
"dependencies" but narrowing the peer range to ">=3.3.1 <4" so the peer floor
matches the installed version; update the "dependencies" and "peerDependencies"
entries in packages/lit-query/package.json accordingly.
In `@packages/lit-query/README.md`:
- Around line 85-95: The SSR example's dev server port isn't set in
vite.config.ts so it falls back to Vite's default; import the existing
DEFAULT_SSR_PORT from examples/lit/ssr/config/ports.js in the SSR vite.config.ts
and set server.port (inside the exported defineConfig or dev server config) to
DEFAULT_SSR_PORT—mirror how the basic/pagination configs set server.port to
their DEFAULT_* constants so running pnpm --dir examples/lit/ssr run dev serves
on 4174.
In `@packages/lit-query/scripts/write-cjs-package.mjs`:
- Around line 66-72: The current replacement only special-cases 'lit' vs
'lit-html' by checking packageName === 'lit-html'; update the guard to use the
esmOnlyPackages set instead so any ESM-only package (e.g., 'lit') is handled: in
the importTypeExpressionRegex replacer (function using packageName) replace the
equality check with a membership test against esmOnlyPackages (e.g.,
!esmOnlyPackages.has(packageName)) and add the resolution-mode assertion for any
matching packageName, ensuring you still interpolate the original packageName
into the returned import(...) string; reference importTypeExpressionRegex and
esmOnlyPackages to locate the change.
In `@packages/lit-query/src/tests/counters-and-state.test.ts`:
- Line 22: The test uses a module-scoped variable explicitCountersClient that is
only reset conditionally inside a test, which can leak state between tests; add
an afterEach() hook in the test file that unconditionally sets
explicitCountersClient = undefined (and if tests create DOM nodes with
document.createElement(contextCountersTagName), also remove those fixtures) so
any subsequent call to document.createElement(contextCountersTagName) constructs
controllers with a clean client; update the teardown to reference the
explicitCountersClient symbol to guarantee reset after every test.
In `@packages/lit-query/src/tests/queries-controller.test.ts`:
- Line 19: Add an afterEach teardown that defensively resets the module-level
explicitQueriesClient to undefined to prevent cross-test contamination;
specifically, in the tests file add an afterEach hook that checks and sets
explicitQueriesClient = undefined (affecting the variable explicitQueriesClient
and ensuring tests like LC-QUERIES-02 cannot leak into LC-QUERIES-01/03/04 and
any setup that constructs ContextQueriesHostElement or relies on QueryClient);
place the hook near the other test lifecycle hooks so it always runs even if a
test throws.
---
Minor comments:
In `@docs/framework/lit/guides/mutations.md`:
- Line 6: Update the sentence describing mutations to hyphenate "server-side"
(change "server side effects" to "server-side effects") in the guide text that
references createMutationController so it reads: "Unlike queries, mutations are
used to create, update, delete, or otherwise perform server-side effects. In
Lit, use createMutationController(...)." Locate the sentence containing
createMutationController and replace the unhyphenated phrase with the hyphenated
one.
In `@docs/framework/lit/guides/ssr.md`:
- Around line 73-78: The code reads and parses the '__QUERY_STATE__' script into
dehydratedState and immediately calls hydrate(queryClient, dehydratedState), but
JSON.parse(... ?? 'null') yields null when the element is missing, so hydrate
may be called with null; update the logic around
document.getElementById('__QUERY_STATE__') and dehydratedState so you only call
hydrate(queryClient, dehydratedState) when the element exists and
dehydratedState is non-null (otherwise skip hydrate or handle the missing state
explicitly), keeping queryClient.mount() behavior unchanged.
In `@docs/framework/lit/reference/functions/createInfiniteQueryController.md`:
- Around line 9-13: The function signature for createInfiniteQueryController has
a minor indentation mismatch: the parameter queryClient? is not aligned with
host and options; update the signature in
createInfiniteQueryController<TQueryFnData, TError, TData, TQueryKey,
TPageParam> so that the queryClient? parameter has the same leading spaces as
host and options to keep consistent formatting and visual alignment in the
rendered documentation.
In `@docs/framework/lit/reference/functions/createQueriesController.md`:
- Around line 8-13: The function signature for createQueriesController has the
optional parameter queryClient? misaligned (extra leading space) compared to
host and options; fix by aligning queryClient? with the other parameters in the
signature block so all three parameters start at the same column (adjust the
indentation in the code fence where createQueriesController<TQueryOptions,
TCombinedResult>( host, options, queryClient?):
QueriesResultAccessor<TCombinedResult>; is declared).
In `@docs/framework/lit/reference/functions/createQueryController.md`:
- Line 34: Update the createQueryController docs to change the generic default
from TError = Error to TError = DefaultError; specifically edit the
createQueryController documentation block where `TError` is declared so it
matches the implementation's exported `TError = DefaultError` (and aligns with
CreateInfiniteQueryOptions / TanStack Query v5 conventions).
In `@docs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.md`:
- Around line 29-32: Update the docs to use a consistent `TData` default for
CreateInfiniteQueryOptions: choose either `TData = InfiniteData<TQueryFnData>`
or `TData = InfiniteData<TQueryFnData, unknown>` and apply that form uniformly
across the CreateInfiniteQueryOptions reference page and the
infiniteQueryOptions.md page; ensure both files reference the same
`InfiniteData` signature for `TData` so the documentation no longer
inconsistently shows different type-argument counts.
In `@examples/lit/basic/config/port.js`:
- Around line 9-11: The current parsing uses Number.parseInt which accepts
"4173abc" and truncates it; change the validation so envPort is strictly numeric
before parsing (e.g., check envPort matches /^\d+$/ or that String(parsedPort)
=== envPort.trim()), then compute parsedPort and set isValidPort to
Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535; update
the variables parsedPort and isValidPort accordingly to reject partially numeric
DEMO_PORT values instead of silently truncating them.
In `@examples/lit/basic/src/mutation.ts`:
- Around line 79-84: The submit() handler currently calls addTodo.mutate
unconditionally which allows double-submits; update submit() to early-return if
the mutation is in-flight (e.g., check addTodo.isLoading or addTodo.status ===
'loading') before calling addTodo.mutate, and consider switching to
addTodo.mutateAsync and awaiting it so you only clear nextTitle after success
(or handle errors) to further avoid duplicate or inconsistent state.
In `@examples/lit/basic/src/todoApi.ts`:
- Around line 42-52: addTodoOnServer currently allows blank or whitespace-only
titles; update the function (addTodoOnServer) to trim the incoming title and
validate it before creating nextTodo: if title.trim() is empty, throw a clear
Error (e.g., "Title cannot be empty") or return a rejected Promise so the caller
can handle validation failures; use the trimmed value when constructing the Todo
(id: nextTodoId, title) to avoid persisting whitespace-only strings and keep
other logic (failNextMutation, delay, nextTodoId) unchanged.
In `@examples/lit/pagination/server/index.mjs`:
- Around line 42-44: The current parse uses Number.parseInt on rawValue which
accepts partial parses like "1abc"; change the validation so partial parses are
rejected by verifying the rawValue is a pure integer string before or after
parsing: either require /^\d+$/ (or /^\d+$/u) on rawValue then parse, or parse
with Number(rawValue) and assert String(parsed) === rawValue and
Number.isInteger(parsed) and parsed >= 1; update the block that computes parsed
from rawValue (and the analogous block that parses delay) to implement this
stricter check and return undefined / trigger a 400 for malformed input.
In `@examples/lit/ssr/scripts/dev.mjs`:
- Around line 38-45: The current race handler only captures the numeric exit
code and maps null to 0, so if the server was killed by a signal it incorrectly
reports success; update the 'exit' listener to capture both code and signal
(e.g., once(server, 'exit').then(([code, signal]) => ({ code, signal })) ), then
set process.exitCode to outcome.code if non-null, otherwise to a non-zero value
when outcome.signal is present (e.g., 1) and only default to 0 when neither code
nor signal exist; adjust references to once, server, 'exit', outcome, and
process.exitCode accordingly.
In `@examples/lit/ssr/src/main.ts`:
- Around line 27-28: The current parsing of the serialized state uses
stateElement.textContent and can throw an opaque SyntaxError for empty or
invalid JSON; update the logic around stateElement/stateText (the code returning
JSON.parse(...) as DehydratedState) to explicitly check for empty or falsy
stateText and throw a clear, descriptive error for missing content, and wrap
JSON.parse in a try/catch to catch malformed JSON and rethrow a helpful error
that includes the original text or parse message so callers can diagnose bad SSR
state.
In `@integrations/lit-vite/src/main.ts`:
- Around line 40-52: The render() method is missing the same override modifiers
as other lifecycle overrides; update the render() declaration to use "protected
override render()" so it matches the createRenderRoot() overrides and clearly
indicates it's overriding the base class's render method (locate the render()
function in main.ts and change its signature accordingly).
In `@packages/lit-query/scripts/l3-stress.mjs`:
- Around line 80-100: The test captures cacheQuery before calling
host.disconnect() and query.destroy(), which can become stale if the cache entry
is GC'ed immediately (gcTime: 0); re-fetch the cache entry after teardown and
use that fresh reference for the disconnected assertion instead of the earlier
cacheQuery — i.e., call client.getQueryCache().find({ queryKey }) again after
host.disconnect()/query.destroy() and call getObserversCount() on that
re-fetched entry to validate the disconnected count.
In `@packages/lit-query/src/tests/client-switch-controllers.test.ts`:
- Around line 372-407: The cleanup block contains a redundant call to
providerA.remove() that was already removed earlier during the test reparenting;
remove the duplicate providerA.remove() from the cleanup sequence in the test
containing consumer/providerA/providerB interactions (and apply the same removal
to the analogous cleanup in the "reparents infinite query" test) so each
provider is only removed once; keep the other cleanup calls (providerB.remove(),
consumer.queries.destroy(), Promise.resolve()) intact.
In `@packages/lit-query/tsconfig.build.cjs.json`:
- Line 4: Replace the "customConditions": null entry in the
tsconfig.build.cjs.json with an empty array to follow TypeScript idioms: locate
the JSON property named customConditions and change its value from null to [] so
the config expresses "no custom conditions" as an empty list rather than null.
---
Nitpick comments:
In `@docs/framework/lit/quick-start.md`:
- Around line 50-62: The code captures the mutation result into const mutation =
this.createTodo() and checks mutation.isPending, but the click handler calls
this.createTodo.mutate(...), which is inconsistent and confusing; either change
the click handler to use mutation.mutate({ title: 'Write Lit docs' }) so it
matches the stored result, or add a brief inline comment near the
this.createTodo accessor explaining that mutate is intentionally a stable method
exposed on the controller (i.e., this.createTodo.mutate is valid) and why the
pattern differs from React Query; update references for mutation.isPending,
mutation.mutate, and this.createTodo accordingly.
In `@examples/lit/pagination/config/ports.js`:
- Around line 4-20: The check in readPortFromEnv uses if (!rawValue) which
treats the string "0" as falsy and silently uses the fallback; change the
presence check to only treat an unset env var as missing (e.g., rawValue ===
undefined or rawValue == null) so an explicit "0" will be parsed and then
rejected by the integer range check; update the condition in readPortFromEnv
accordingly so the function still returns fallback when the variable is
genuinely absent but throws for "0" and other invalid strings.
In `@examples/lit/pagination/src/api.ts`:
- Around line 104-114: fetchProjectsPage currently calls fetch + readJsonOrThrow
directly, duplicating logic; replace that with the existing requestJson helper
to unify request handling. Modify fetchProjectsPage to call
requestJson(buildProjectsUrl(page, delayMs, forceError), {}) (an empty
RequestInit for the GET) and return its parsed ProjectsPageResponse, removing
the direct fetch/readJsonOrThrow usage; keep the same error context/message
passed into requestJson if the helper accepts it or ensure the helper preserves
similar error text.
In `@examples/lit/pagination/src/main.ts`:
- Around line 208-210: The updated() override is calling maybePrefetchNextPage()
and discarding its returned Promise, which triggers no-floating-promises; make
the fire-and-forget explicit by prefixing the call with void in updated(), i.e.
change the call inside the override to void this.maybePrefetchNextPage() so the
intention is clear and eslint's no-floating-promises is satisfied (references:
updated() method and maybePrefetchNextPage()).
In `@examples/lit/ssr/scripts/dev.mjs`:
- Around line 35-36: Registered SIGINT/SIGTERM handlers using process.on persist
after the server finishes; update the dev server flow to register cleanupable
handlers (e.g., create named handler functions passed to process.on for 'SIGINT'
and 'SIGTERM' or use process.once) and remove them (process.off or ensure they
run only once) after the server has stopped or after the outcome promise
resolves so the handlers don't remain active; specifically modify the places
where process.on('SIGINT', () => stopServer('SIGINT')) and process.on('SIGTERM',
() => stopServer('SIGTERM')) are added to use removable handlers and call
process.off for those handlers once stopServer completes or outcome resolves.
In `@packages/lit-query/eslint.config.js`:
- Around line 10-20: The ignores array in eslint.config.js contains a redundant
pattern 'dist/**' that is already matched by the broader '**/dist/**' entry;
remove the narrower 'dist/**' string from the ignores array to eliminate
duplication and keep the list tidy (edit the ignores array in the file to delete
the 'dist/**' element).
In `@packages/lit-query/scripts/measure-bundle.mjs`:
- Around line 6-8: The identifier repoRoot is misleading because
path.resolve(scriptDir, '..') points to the package root (packages/lit-query)
not the monorepo root; rename repoRoot to packageRoot (or packageDir) and update
all usages (e.g., where distDir is computed: distDir = path.join(packageRoot,
'dist')) so variable names reflect the actual path and avoid confusion when
referencing the real repository root elsewhere.
In `@packages/lit-query/scripts/write-cjs-package.mjs`:
- Around line 59-65: Add a brief inline comment above the esmValueImportRegex
replacement handler explaining that in .d.cts declaration files all imported
symbols are type-level, so converting value imports from ESM-only packages to
type-only imports (the code path using esmValueImportRegex, esmOnlyPackages and
the replacement string with "resolution-mode": "import") is intentional and
safe; place the comment immediately above the .replace callback that returns
`import type {${specifiers}} ...` to clarify the assumption for future
maintainers.
In `@packages/lit-query/src/accessor.ts`:
- Around line 36-43: The descriptor passed to Object.defineProperty in
createValueAccessor currently relies on defaults and thus implicitly sets
configurable to false; make this explicit by adding configurable: false
alongside enumerable: true in the descriptor for the 'current' property on the
accessor returned by createValueAccessor so the intent is clear and resilient to
test mocks or future changes (keep the getter as-is and do not add writable
since this is an accessor property).
- Around line 15-17: The readAccessor function cannot distinguish between a
plain function value and a zero-argument getter because it checks typeof value
=== 'function', so it will erroneously call a function when T itself is a
function type; update the readAccessor<T>(value: Accessor<T>) declaration to
include a clear doc comment above the function (and update the Accessor type
comment if present) that explains this ambiguity, warns consumers not to use
Accessor<FnType> (i.e., avoid passing function-typed T) or to wrap actual
function values in a no-op container so they are not invoked, and include a
short example or recommended pattern for returning function values safely;
reference the readAccessor symbol in the comment so future maintainers see the
limitation.
In `@packages/lit-query/src/context.ts`:
- Around line 20-63: The module-level globals registeredClients and
defaultClient can leak across tests; add and export a small reset helper (e.g.,
resetQueryClientRegistry or resetDefaultQueryClientRegistry) that clears
registeredClients and sets defaultClient = undefined so test suites can call it
after each test (or in finally blocks); place the new function alongside
registerDefaultQueryClient/unregisterDefaultQueryClient and export it so tests
can import and run the cleanup.
In `@packages/lit-query/src/controllers/BaseController.ts`:
- Around line 98-115: The destroy() method currently always invokes
onDisconnected() even when the controller was never connected; update destroy()
to mirror hostDisconnected() by only calling onDisconnected() when
this.connected is true (i.e., add a guard like if (!this.connected) return
before invoking onDisconnected()), or alternatively explicitly document that
onDisconnected() is used for both disconnect and teardown—prefer changing
destroy() to check this.connected so onDisconnected() only runs for actual
disconnects.
In `@packages/lit-query/src/createMutationController.ts`:
- Around line 218-230: The reset method (reset) quietly no-ops when there is no
client while mutate and mutateAsync throw/reject, so add a short JSDoc comment
above the reset implementation (createMutationController.ts) documenting this
behavior and its rationale: explain that reset is idempotent and will silently
return if syncClient() or observer is missing, whereas mutate/mutateAsync will
surface errors, and call out that callers who need uniform behavior should check
syncClient() or handle both cases explicitly.
In `@packages/lit-query/src/createQueriesController.ts`:
- Around line 37-42: The type alias CreateQueriesInputForController is a no-op
because OmitKeyof<..., never> removes nothing; replace usages of
CreateQueriesInputForController with CreateQueriesInput (or remove the alias
entirely) to eliminate the unnecessary indirection, or if you intend future
omissions add a clarifying comment above CreateQueriesInputForController
explaining the planned purpose; update references in createQueriesController.ts
that mention CreateQueriesInputForController and remove the OmitKeyof wrapper so
the code uses CreateQueriesInput directly.
- Around line 396-399: The observer callback currently calls
readResolvedOptions(), which can throw if this.queryClient is undefined; cache
the combine function at subscription time and use that in the callback instead
of re-reading options. Specifically, when calling this.observer.subscribe(...)
capture const { combine } = this.readResolvedOptions() (or a safe default)
before subscribing, then change the subscription callback to call
this.setResult(this.computeResult(next, combine)) using the captured combine;
update the subscribe/unsubscribe logic around this.unsubscribe and any teardown
to keep the same ordering but avoid invoking readResolvedOptions from inside the
observer callback.
In `@packages/lit-query/src/createQueryController.ts`:
- Around line 250-277: The method defaultOptions mutates controller state by
assigning this.queryClient = resolvedClient as a side effect; change
defaultOptions to avoid mutating this.queryClient (keep it pure) by removing the
assignment and using the local resolvedClient when calling
resolvedClient.defaultQueryOptions(readAccessor(this.options)); ensure callers
that rely on this.queryClient being set instead set it explicitly (e.g., via
existing tryGetQueryClient or a setter) so only defaultOptions,
tryGetQueryClient, resolvedClient, and queryClient symbols are involved and no
state is changed silently.
In `@packages/lit-query/src/tests/client-switch-controllers.test.ts`:
- Around line 15-47: Remove the duplicated BaseControllerHostElement class and
use the existing exported TestElementHost from testHost.ts instead: replace the
local definition of BaseControllerHostElement with an import of TestElementHost
and update any usages to reference TestElementHost (or export it under the same
name if tests expect BaseControllerHostElement). This avoids drift if
TestElementHost gains lifecycle methods like flushHostUpdate and keeps a single
source of truth for ReactiveControllerHost test behavior.
In `@packages/lit-query/src/tests/mutation-controller.test.ts`:
- Around line 18-31: The module-level mutable explicitMutationClient leaks state
between tests; move initialization of explicitMutationClient into each test (or
create it inside the test before calling document.createElement for
ContextMutationHostElement) and ensure it is reset after the test using a
try/finally or Vitest afterEach that sets explicitMutationClient = undefined;
update uses of createMutationController on ContextMutationHostElement (and any
tests around LC-MUT-02 / the block that currently sets explicitMutationClient)
so the controller is constructed with a test-local client and always cleaned up
to avoid implicit ordering dependencies.
In `@packages/lit-query/src/tests/testHost.ts`:
- Around line 86-97: The waitFor helper currently lets exceptions thrown by
assertion() escape immediately; update waitFor to call assertion() inside a
try/catch, treating any thrown exception as a failed check (continue retrying)
until timeoutMs elapses, and capture the last thrown error to include in the
final timeout Error message so that waitFor still throws after timing out but
with context from the last assertion exception; make this change inside the
waitFor function (use startedAt, timeoutMs, and the existing sleep loop) so
behavior is retry-on-exception rather than immediate failure.
In `@packages/lit-query/src/types.ts`:
- Around line 54-69: The type parameter name TOnMutateResult diverges from
TanStack Query core naming; rename TOnMutateResult to TContext across the
exported types to match CreateMutationOptions and MutationObserverResult
conventions. Update MutationControllerOptions and MutationControllerResult
generic declarations (and any other occurrences in this file) to use TContext
instead of TOnMutateResult, preserving the same default type (unknown); ensure
the type alias references (CreateMutationOptions and MutationObserverResult) use
the new TContext identifier so external consumers see the conventional TContext
name.
In `@packages/lit-query/src/useIsMutating.ts`:
- Around line 93-105: The early-return guard in subscribe() that checks
this.unsubscribe can silently prevent resubscription when a different
QueryClient is expected; add a concise comment above the guard in subscribe()
explaining that the guard prevents duplicate subscriptions, and that
syncClient() is responsible for clearing this.unsubscribe whenever the client
changes so callers must call syncClient() or use
onQueryClientChanged()/onConnected() before calling subscribe(); reference the
subscribe() method, the this.unsubscribe field, syncClient(), and the
queryClient.getMutationCache().subscribe callback to make the dependency and
rationale explicit for future maintainers.
- Around line 48-52: Remove the call to syncClient() from onDisconnected because
when an explicit queryClient is injected tryGetQueryClient() will return the
same client and syncClient() becomes a no-op; instead just unsubscribe and set
this.unsubscribe = undefined, and if you need to clear a context-derived client
explicitly set this.queryClient = undefined there (or in the existing disconnect
flow) so state is cleared only for context-based clients; update
onDisconnected() to stop calling syncClient() and rely on explicit
this.queryClient handling and the unsubscribe logic (references: onDisconnected,
syncClient, tryGetQueryClient, this.queryClient).
In `@scripts/generate-docs.ts`:
- Around line 38-45: The variable named "stack" is misleading and the current
for...of iteration mutates the array during iteration (stack.push(...)) which
relies on iterator behavior; either rename it to "queue" and implement an
explicit FIFO loop (e.g., while (queue.length) { const reflection =
queue.shift(); queue.push(...(reflection.children ?? [])); ... }) to document
BFS intent, or change to a true DFS by keeping the "stack" name and using a
while (stack.length) { const reflection = stack.pop();
stack.push(...(reflection.children ?? [])); ... } — update uses of
TypeDocReflectionWithSignatures, project, reflection.children, the for...of +
push pattern, and the createQueriesController check accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b6b7b76f-bfbb-4130-b9dd-7e9fefec0af8
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (146)
.changeset/lemon-memes-divide.md.gitignoredocs/config.jsondocs/framework/lit/guides/infinite-queries.mddocs/framework/lit/guides/mutations.mddocs/framework/lit/guides/parallel-queries.mddocs/framework/lit/guides/queries.mddocs/framework/lit/guides/query-functions.mddocs/framework/lit/guides/query-invalidation.mddocs/framework/lit/guides/query-keys.mddocs/framework/lit/guides/reactive-controllers-vs-hooks.mddocs/framework/lit/guides/ssr.mddocs/framework/lit/installation.mddocs/framework/lit/overview.mddocs/framework/lit/quick-start.mddocs/framework/lit/reference/classes/QueryClientProvider.mddocs/framework/lit/reference/functions/createInfiniteQueryController.mddocs/framework/lit/reference/functions/createMutationController.mddocs/framework/lit/reference/functions/createQueriesController.mddocs/framework/lit/reference/functions/createQueryController.mddocs/framework/lit/reference/functions/getDefaultQueryClient.mddocs/framework/lit/reference/functions/infiniteQueryOptions.mddocs/framework/lit/reference/functions/mutationOptions.mddocs/framework/lit/reference/functions/queryOptions.mddocs/framework/lit/reference/functions/registerDefaultQueryClient.mddocs/framework/lit/reference/functions/resolveQueryClient.mddocs/framework/lit/reference/functions/unregisterDefaultQueryClient.mddocs/framework/lit/reference/functions/useIsFetching.mddocs/framework/lit/reference/functions/useIsMutating.mddocs/framework/lit/reference/functions/useMutationState.mddocs/framework/lit/reference/functions/useQueryClient.mddocs/framework/lit/reference/index.mddocs/framework/lit/reference/type-aliases/Accessor.mddocs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.mddocs/framework/lit/reference/type-aliases/CreateMutationOptions.mddocs/framework/lit/reference/type-aliases/CreateQueriesControllerOptions.mddocs/framework/lit/reference/type-aliases/CreateQueriesInput.mddocs/framework/lit/reference/type-aliases/CreateQueryOptions.mddocs/framework/lit/reference/type-aliases/DefinedInitialDataOptions.mddocs/framework/lit/reference/type-aliases/InfiniteQueryControllerOptions.mddocs/framework/lit/reference/type-aliases/InfiniteQueryResultAccessor.mddocs/framework/lit/reference/type-aliases/IsFetchingAccessor.mddocs/framework/lit/reference/type-aliases/IsMutatingAccessor.mddocs/framework/lit/reference/type-aliases/MutationControllerOptions.mddocs/framework/lit/reference/type-aliases/MutationControllerResult.mddocs/framework/lit/reference/type-aliases/MutationResultAccessor.mddocs/framework/lit/reference/type-aliases/MutationStateAccessor.mddocs/framework/lit/reference/type-aliases/MutationStateOptions.mddocs/framework/lit/reference/type-aliases/QueriesControllerOptions.mddocs/framework/lit/reference/type-aliases/QueriesResultAccessor.mddocs/framework/lit/reference/type-aliases/QueryControllerOptions.mddocs/framework/lit/reference/type-aliases/QueryControllerResult.mddocs/framework/lit/reference/type-aliases/QueryResultAccessor.mddocs/framework/lit/reference/type-aliases/UndefinedInitialDataOptions.mddocs/framework/lit/reference/type-aliases/UnusedSkipTokenOptions.mddocs/framework/lit/reference/type-aliases/ValueAccessor.mddocs/framework/lit/reference/variables/queryClientContext.mddocs/framework/lit/typescript.mdexamples/lit/basic/README.mdexamples/lit/basic/basic-query.htmlexamples/lit/basic/config/port.d.tsexamples/lit/basic/config/port.jsexamples/lit/basic/index.htmlexamples/lit/basic/lifecycle-contract.htmlexamples/lit/basic/mutation.htmlexamples/lit/basic/package.jsonexamples/lit/basic/src/basic-query.tsexamples/lit/basic/src/lifecycle-contract.tsexamples/lit/basic/src/main.tsexamples/lit/basic/src/mutation.tsexamples/lit/basic/src/todoApi.tsexamples/lit/basic/tsconfig.jsonexamples/lit/basic/vite.config.tsexamples/lit/pagination/README.mdexamples/lit/pagination/config/ports.d.tsexamples/lit/pagination/config/ports.jsexamples/lit/pagination/index.htmlexamples/lit/pagination/package.jsonexamples/lit/pagination/scripts/dev.mjsexamples/lit/pagination/server/index.mjsexamples/lit/pagination/src/api.tsexamples/lit/pagination/src/main.tsexamples/lit/pagination/src/vite-env.d.tsexamples/lit/pagination/tsconfig.jsonexamples/lit/pagination/vite.config.tsexamples/lit/ssr/README.mdexamples/lit/ssr/config/ports.d.tsexamples/lit/ssr/config/ports.jsexamples/lit/ssr/index.htmlexamples/lit/ssr/package.jsonexamples/lit/ssr/scripts/dev.mjsexamples/lit/ssr/server/index.mjsexamples/lit/ssr/src/api.tsexamples/lit/ssr/src/app.tsexamples/lit/ssr/src/main.tsexamples/lit/ssr/tsconfig.jsonexamples/lit/ssr/vite.config.tsintegrations/lit-vite/index.htmlintegrations/lit-vite/package.jsonintegrations/lit-vite/src/main.tsintegrations/lit-vite/tsconfig.jsonintegrations/lit-vite/vite.config.tsknip.jsonlabeler-config.ymlpackages/lit-query/.editorconfigpackages/lit-query/.npmignorepackages/lit-query/.prettierignorepackages/lit-query/README.mdpackages/lit-query/eslint.config.jspackages/lit-query/package.jsonpackages/lit-query/scripts/check-cjs-types-smoke.mjspackages/lit-query/scripts/l3-stress.mjspackages/lit-query/scripts/measure-bundle.mjspackages/lit-query/scripts/write-cjs-package.mjspackages/lit-query/src/QueryClientProvider.tspackages/lit-query/src/accessor.tspackages/lit-query/src/context.tspackages/lit-query/src/controllers/BaseController.tspackages/lit-query/src/createInfiniteQueryController.tspackages/lit-query/src/createMutationController.tspackages/lit-query/src/createQueriesController.tspackages/lit-query/src/createQueryController.tspackages/lit-query/src/index.tspackages/lit-query/src/infiniteQueryOptions.tspackages/lit-query/src/mutationOptions.tspackages/lit-query/src/queryOptions.tspackages/lit-query/src/tests/base-controller.test.tspackages/lit-query/src/tests/client-switch-controllers.test.tspackages/lit-query/src/tests/context-provider.test.tspackages/lit-query/src/tests/counters-and-state.test.tspackages/lit-query/src/tests/infinite-and-options.test.tspackages/lit-query/src/tests/mutation-controller.test.tspackages/lit-query/src/tests/queries-controller.test.tspackages/lit-query/src/tests/query-controller.test.tspackages/lit-query/src/tests/testHost.tspackages/lit-query/src/tests/type-inference.test.tspackages/lit-query/src/types.tspackages/lit-query/src/useIsFetching.tspackages/lit-query/src/useIsMutating.tspackages/lit-query/src/useMutationState.tspackages/lit-query/tsconfig.build.cjs.jsonpackages/lit-query/tsconfig.build.jsonpackages/lit-query/tsconfig.jsonpackages/lit-query/vitest.config.tspnpm-workspace.yamlscripts/generate-docs.ts
| # Type Alias: ValueAccessor\<T\> | ||
|
|
||
| ```ts | ||
| type ValueAccessor<T> = () => T & object; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check the actual ValueAccessor type definition in source
# Extract the ValueAccessor type definition from the source
rg -A5 "type ValueAccessor" packages/lit-query/src/accessor.tsRepository: TanStack/query
Length of output: 260
Update the type definition in the documentation to match the actual implementation.
The documented type is incorrect. The actual ValueAccessor type in packages/lit-query/src/accessor.ts is:
type ValueAccessor<T> = (() => T) & {
readonly current: T
}Update line 9 in the markdown file to reflect this instead of () => T & object.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/framework/lit/reference/type-aliases/ValueAccessor.md` at line 9, The
documented ValueAccessor type is wrong; replace the current `type
ValueAccessor<T> = () => T & object;` with the actual implementation shape: make
it a callable that also has a readonly current property by changing the
declaration to the intersection form used in packages/lit-query/src/accessor.ts
— i.e. a (() => T) callable intersected with `{ readonly current: T }` so the
docs match the runtime type ValueAccessor<T>.
| const [winner] = await Promise.race([ | ||
| once(api, 'exit').then(([code]) => ({ name: 'api', code })), | ||
| once(web, 'exit').then(([code]) => ({ name: 'web', code })), | ||
| ]) | ||
|
|
||
| await shutdown() | ||
|
|
||
| if (winner.code !== 0 && winner.code !== null) { | ||
| process.exitCode = winner.code | ||
| } else { | ||
| process.exitCode = 1 | ||
| } |
There was a problem hiding this comment.
Exit status is always non-zero after shutdown.
Line 73 forces process.exitCode = 1 for both clean exits (code === 0) and signal exits (code === null), so this script effectively always reports failure.
Proposed fix
- const [winner] = await Promise.race([
- once(api, 'exit').then(([code]) => ({ name: 'api', code })),
- once(web, 'exit').then(([code]) => ({ name: 'web', code })),
- ])
+ const [winner] = await Promise.race([
+ once(api, 'exit').then(([code, signal]) => ({ name: 'api', code, signal })),
+ once(web, 'exit').then(([code, signal]) => ({ name: 'web', code, signal })),
+ ])
await shutdown()
- if (winner.code !== 0 && winner.code !== null) {
+ if (winner.code !== null) {
process.exitCode = winner.code
+ } else if (winner.signal === 'SIGINT' || winner.signal === 'SIGTERM') {
+ process.exitCode = 0
} else {
process.exitCode = 1
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [winner] = await Promise.race([ | |
| once(api, 'exit').then(([code]) => ({ name: 'api', code })), | |
| once(web, 'exit').then(([code]) => ({ name: 'web', code })), | |
| ]) | |
| await shutdown() | |
| if (winner.code !== 0 && winner.code !== null) { | |
| process.exitCode = winner.code | |
| } else { | |
| process.exitCode = 1 | |
| } | |
| const [winner] = await Promise.race([ | |
| once(api, 'exit').then(([code, signal]) => ({ name: 'api', code, signal })), | |
| once(web, 'exit').then(([code, signal]) => ({ name: 'web', code, signal })), | |
| ]) | |
| await shutdown() | |
| if (winner.code !== null) { | |
| process.exitCode = winner.code | |
| } else if (winner.signal === 'SIGINT' || winner.signal === 'SIGTERM') { | |
| process.exitCode = 0 | |
| } else { | |
| process.exitCode = 1 | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/lit/pagination/scripts/dev.mjs` around lines 63 - 74, The current
logic in dev.mjs using the winner variable sets process.exitCode = 1 for the
else branch which catches both clean exits (winner.code === 0) and signal exits
(winner.code === null), causing false failures; change the conditional to
explicitly handle the clean exit case: if winner.code === 0 set process.exitCode
= 0, otherwise if winner.code === null set process.exitCode = 1 (or another
non-zero value) and for any numeric non-zero code set process.exitCode =
winner.code — update the block that checks winner.code after shutdown (the lines
referencing winner and process.exitCode) accordingly.
| private readonly projectsQueryOptions: CreateQueryOptions< | ||
| ProjectsPageResponse, | ||
| Error | ||
| > | ||
| private readonly projectsQuery: QueryResultAccessor< | ||
| ProjectsPageResponse, | ||
| Error | ||
| > | ||
| private readonly createProjectMutation: MutationResultAccessor< | ||
| Project, | ||
| Error, | ||
| CreateProjectInput, | ||
| unknown | ||
| > | ||
| private readonly favoriteMutation: MutationResultAccessor< | ||
| Project, | ||
| Error, | ||
| ToggleProjectFavoriteInput, | ||
| FavoriteMutationContext | ||
| > | ||
|
|
||
| constructor() { | ||
| super() | ||
|
|
||
| this.projectsQueryOptions = { | ||
| queryKey: projectsQueryKey(this.page, this.delayMs, this.forceErrorMode), | ||
| queryFn: () => | ||
| fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode), | ||
| placeholderData: keepPreviousData, | ||
| } | ||
|
|
||
| this.projectsQuery = createQueryController<ProjectsPageResponse, Error>( | ||
| this, | ||
| this.projectsQueryOptions, | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how createQueryController reads/evaluates the options argument
ast-grep --pattern 'function createQueryController($$$) {
$$$
}'
ast-grep --pattern 'createQueryController($_, $options, $$$) {
$$$
}'
# Also look for where the options accessor is evaluated in the controller
rg -n "options" packages/lit-query/src/createQueryController.ts -A 3 -B 3
rg -n "Accessor" packages/lit-query/src/accessor.tsRepository: TanStack/query
Length of output: 4192
Pass an accessor function instead of a plain object to createQueryController.
The plain projectsQueryOptions object is passed directly to createQueryController, which snapshots it at construction time. The onHostUpdate() lifecycle method early-returns for plain objects (it only proceeds when options is a function). This means the mutations in syncProjectsQueryOptions() never take effect—the original queryKey (derived from the initial this.page value) remains frozen in the observer.
When this.page changes, the queryKey should also change to partition cache entries correctly, but it doesn't. The queryFn closure always fetches the correct page's data, but under the stale cache key, causing keepPreviousData and prefetch logic to operate on incorrect cache entries.
Use an accessor function to derive options fresh on each host update:
♻️ Proposed fix
- private readonly projectsQueryOptions: CreateQueryOptions<
- ProjectsPageResponse,
- Error
- >
private readonly projectsQuery: QueryResultAccessor<
ProjectsPageResponse,
Error
>
constructor() {
super()
-
- this.projectsQueryOptions = {
- queryKey: projectsQueryKey(this.page, this.delayMs, this.forceErrorMode),
- queryFn: () =>
- fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode),
- placeholderData: keepPreviousData,
- }
-
this.projectsQuery = createQueryController<ProjectsPageResponse, Error>(
this,
- this.projectsQueryOptions,
+ () => ({
+ queryKey: projectsQueryKey(this.page, this.delayMs, this.forceErrorMode),
+ queryFn: () =>
+ fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode),
+ placeholderData: keepPreviousData,
+ }),
)
// ...
}
- private syncProjectsQueryOptions(): void {
- this.projectsQueryOptions.queryKey = projectsQueryKey(
- this.page,
- this.delayMs,
- this.forceErrorMode,
- )
- this.projectsQueryOptions.queryFn = () =>
- fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode)
- }
-
private refetchForCurrentState(): void {
- this.syncProjectsQueryOptions()
void this.projectsQuery.refetch()
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/lit/pagination/src/main.ts` around lines 83 - 117, The
projectsQueryOptions object is being passed as a plain object to
createQueryController which snapshots it and prevents onHostUpdate from applying
changes in syncProjectsQueryOptions; change to pass an accessor function that
returns the options so they are re-derived on each host update (e.g. replace the
plain projectsQueryOptions object or the createQueryController call to use a ()
=> ({ queryKey: projectsQueryKey(this.page, this.delayMs, this.forceErrorMode),
queryFn: () => fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode),
placeholderData: keepPreviousData }) so the queryKey and queryFn are
recalculated when this.page changes and the controller will pick up updates via
onHostUpdate/syncProjectsQueryOptions).
| <script id="__QUERY_STATE__" type="application/json"> | ||
| __QUERY_STATE_JSON__ | ||
| </script> |
There was a problem hiding this comment.
Escape injected query-state JSON before template replacement.
__QUERY_STATE_JSON__ is embedded in a <script> tag; unescaped payloads can break out of the tag and become executable HTML/JS. Ensure server injection escapes <, >, &, and </script sequences.
🔒 Suggested server-side escaping pattern
-const queryStateJson = JSON.stringify(dehydratedState)
+const queryStateJson = JSON.stringify(dehydratedState)
+ .replace(/</g, '\\u003c')
+ .replace(/>/g, '\\u003e')
+ .replace(/&/g, '\\u0026')
+ .replace(/<\/script/gi, '<\\/script')🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/lit/ssr/index.html` around lines 10 - 12, The template injects raw
__QUERY_STATE_JSON__ into the <script id="__QUERY_STATE__"> tag which can be
broken out of by special characters; before replacing the placeholder
__QUERY_STATE_JSON__ on the server, escape JSON content (replace at minimum '<',
'>', '&' and any case-insensitive '</script' sequence) so the injected string
cannot close the script tag or create executable HTML/JS; apply this escaping in
the server-side template rendering code that performs the placeholder
replacement.
| "@lit/context": "^1.1.6", | ||
| "@tanstack/query-core": "workspace:*", | ||
| "lit": "^3.3.1" | ||
| }, | ||
| "peerDependencies": { | ||
| "@tanstack/query-core": "^5.0.0", | ||
| "lit": ">=2.8.0 <4" |
There was a problem hiding this comment.
lit in both dependencies and peerDependencies with incompatible version ranges risks dual-instance breakage.
dependencies pins lit at ^3.3.1, but peerDependencies advertises >=2.8.0 <4 — which includes Lit 2.x. A consumer running Lit 2.x would satisfy the peer range but npm/pnpm would still install a separate lit@^3.3.1 inside the package's own node_modules, producing two distinct LitElement base classes. This tells the package manager that it can choose to have duplicate packages in node_modules, which is pretty bad when doing instanceof checks. Lit's customElements.define, @lit/context, and the LitElement prototype chain all depend on a single shared Lit instance — having two breaks them silently at runtime.
Other TanStack adapters (e.g., @tanstack/react-query) place the UI framework in peerDependencies only, not in dependencies. Consider one of:
- Remove
litfromdependencies(peer-only, the idiomatic approach for adapter packages) — consumers are expected to bring their own Lit. - Keep
litindependenciesAND narrowpeerDependenciesto">=3.3.1 <4"to match the actual floor your implementation requires, eliminating the Lit 2.x false-positive.
🛠️ Option 1 — peer-only (recommended)
"dependencies": {
"@lit/context": "^1.1.6",
- "@tanstack/query-core": "workspace:*",
- "lit": "^3.3.1"
+ "@tanstack/query-core": "workspace:*"
},
"peerDependencies": {
"@tanstack/query-core": "^5.0.0",
"lit": ">=2.8.0 <4"
},🛠️ Option 2 — keep both, but align the peer floor
"peerDependencies": {
"@tanstack/query-core": "^5.0.0",
- "lit": ">=2.8.0 <4"
+ "lit": ">=3.3.1 <4"
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "@lit/context": "^1.1.6", | |
| "@tanstack/query-core": "workspace:*", | |
| "lit": "^3.3.1" | |
| }, | |
| "peerDependencies": { | |
| "@tanstack/query-core": "^5.0.0", | |
| "lit": ">=2.8.0 <4" | |
| "peerDependencies": { | |
| "@tanstack/query-core": "^5.0.0", | |
| "lit": ">=3.3.1 <4" | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit-query/package.json` around lines 51 - 57, The package.json
currently lists "lit" in both "dependencies" (pinned to ^3.3.1) and
"peerDependencies" (>=2.8.0 <4), which can create dual instances of Lit; fix by
either removing "lit" from "dependencies" so Lit is peer-only (recommended for
adapter packages) or by keeping it in "dependencies" but narrowing the peer
range to ">=3.3.1 <4" so the peer floor matches the installed version; update
the "dependencies" and "peerDependencies" entries in
packages/lit-query/package.json accordingly.
| .replace(importTypeExpressionRegex, (match, quote, packageName) => { | ||
| if (packageName !== 'lit-html') { | ||
| return match | ||
| } | ||
|
|
||
| return `import(${quote}${packageName}${quote}, { with: { "resolution-mode": "import" } })` | ||
| }) |
There was a problem hiding this comment.
import(...) expression rewriting only handles lit-html, not lit.
The importTypeExpressionRegex handler on lines 66–72 adds the resolution-mode assertion only when packageName === 'lit-html', but esmOnlyPackages is defined as new Set(['lit']). If any declaration file contains an import('lit') expression (rather than import('lit-html')), it will be left unchanged and will fail in a CJS consumer.
Check whether generated .d.ts files ever contain import('lit') expressions, and extend the guard to cover all members of esmOnlyPackages for consistency:
🛡️ Proposed fix
- .replace(importTypeExpressionRegex, (match, quote, packageName) => {
- if (packageName !== 'lit-html') {
- return match
- }
-
- return `import(${quote}${packageName}${quote}, { with: { "resolution-mode": "import" } })`
- })
+ .replace(importTypeExpressionRegex, (match, quote, packageName) => {
+ if (packageName !== 'lit-html' && !esmOnlyPackages.has(packageName)) {
+ return match
+ }
+
+ return `import(${quote}${packageName}${quote}, { with: { "resolution-mode": "import" } })`
+ })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit-query/scripts/write-cjs-package.mjs` around lines 66 - 72, The
current replacement only special-cases 'lit' vs 'lit-html' by checking
packageName === 'lit-html'; update the guard to use the esmOnlyPackages set
instead so any ESM-only package (e.g., 'lit') is handled: in the
importTypeExpressionRegex replacer (function using packageName) replace the
equality check with a membership test against esmOnlyPackages (e.g.,
!esmOnlyPackages.has(packageName)) and add the resolution-mode assertion for any
matching packageName, ensuring you still interpolate the original packageName
into the returned import(...) string; reference importTypeExpressionRegex and
esmOnlyPackages to locate the change.
| customElements.define(providerTagName, QueryClientProvider) | ||
| } | ||
|
|
||
| let explicitCountersClient: QueryClient | undefined |
There was a problem hiding this comment.
Test isolation risk: explicitCountersClient not reset in a guaranteed afterEach.
explicitCountersClient is reset on line 211 only if LC-COUNTERS-02 completes without throwing. If the test fails mid-flight, the value stays set and any subsequent test that calls document.createElement(contextCountersTagName) will construct controllers with the wrong (non-undefined) client, silently corrupting LC-COUNTERS-01 or LC-COUNTERS-03.
🛡️ Proposed fix: use `afterEach` to guarantee reset
+import { afterEach, describe, expect, it } from 'vitest'
-import { describe, expect, it } from 'vitest'
let explicitCountersClient: QueryClient | undefined
+afterEach(() => {
+ explicitCountersClient = undefined
+})
// … in LC-COUNTERS-02 …
consumer.query.destroy()
consumer.mutation.destroy()
consumer.isFetching.destroy()
consumer.isMutating.destroy()
consumer.mutationStatuses.destroy()
provider.remove()
- explicitCountersClient = undefined
await Promise.resolve()Also applies to: 163-163, 211-211
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit-query/src/tests/counters-and-state.test.ts` at line 22, The test
uses a module-scoped variable explicitCountersClient that is only reset
conditionally inside a test, which can leak state between tests; add an
afterEach() hook in the test file that unconditionally sets
explicitCountersClient = undefined (and if tests create DOM nodes with
document.createElement(contextCountersTagName), also remove those fixtures) so
any subsequent call to document.createElement(contextCountersTagName) constructs
controllers with a clean client; update the teardown to reference the
explicitCountersClient symbol to guarantee reset after every test.
| customElements.define(providerTagName, QueryClientProvider) | ||
| } | ||
|
|
||
| let explicitQueriesClient: QueryClient | undefined |
There was a problem hiding this comment.
Missing afterEach guard for explicitQueriesClient risks cross-test contamination.
explicitQueriesClient is reset to undefined in the body of LC-QUERIES-02 (line 190), but only if the test completes successfully. If the test throws before reaching that line, all subsequent tests that construct ContextQueriesHostElement (LC-QUERIES-01, LC-QUERIES-03, LC-QUERIES-04) will resolve to the stale explicitClient instead of the context-provided client, producing wrong results silently.
🛡️ Proposed fix
-import { describe, expect, it } from 'vitest'
+import { afterEach, describe, expect, it } from 'vitest'
...
let explicitQueriesClient: QueryClient | undefined
+afterEach(() => {
+ explicitQueriesClient = undefined
+})Also applies to: 144-192
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/lit-query/src/tests/queries-controller.test.ts` at line 19, Add an
afterEach teardown that defensively resets the module-level
explicitQueriesClient to undefined to prevent cross-test contamination;
specifically, in the tests file add an afterEach hook that checks and sets
explicitQueriesClient = undefined (affecting the variable explicitQueriesClient
and ensuring tests like LC-QUERIES-02 cannot leak into LC-QUERIES-01/03/04 and
any setup that constructs ContextQueriesHostElement or relies on QueryClient);
place the hook near the other test lifecycle hooks so it always runs even if a
test throws.
🎯 Changes
Summary
@tanstack/lit-query@tanstack/lit-queryimports✅ Checklist
pnpm run test:pr.🚀 Release Impact
Summary by CodeRabbit
New Features
Documentation
Examples
Tests
Chores