Skip to content

Feat lit-query: add initial lit-query package#10501

Open
chughgaurav wants to merge 6 commits intoTanStack:mainfrom
chughgaurav:feature/lit-query-adapter
Open

Feat lit-query: add initial lit-query package#10501
chughgaurav wants to merge 6 commits intoTanStack:mainfrom
chughgaurav:feature/lit-query-adapter

Conversation

@chughgaurav
Copy link
Copy Markdown

@chughgaurav chughgaurav commented Apr 14, 2026

🎯 Changes

Adds an initial @tanstack/lit-query package that brings TanStack Query Core to Lit via reactive controllers.

  • New Features

Included in this PR:

  • QueryClientProvider and query-client resolution helpers
  • query, mutation, infinite-query, and queries controllers
  • useIsFetching, useIsMutating, and useMutationState
  • queryOptions, infiniteQueryOptions, and mutationOptions
  • Lit examples covering basic usage, pagination, and SSR
  • unit/type tests for controller lifecycle, client switching, counters/state, and inference
  • Documentation

    • New package README with installation, quick-start, API surface, and example instructions.
  • Tests / E2E

    • Comprehensive unit + Playwright E2E suites and helpers covering smoke, error recovery, lifecycle, pagination, mutations, and SSR.

Notes

This PR is focused on the initial package integration and API surface for Lit.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features
    • Added @tanstack/lit-query package, a TanStack Query integration for Lit components
    • Introduced reactive controllers for queries, mutations, and infinite queries
    • Added QueryClientProvider component for context management
    • Included hooks for tracking fetching/mutating state and mutation status
    • Provided example applications demonstrating pagination, server-side rendering, and core functionality

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new @tanstack/lit-query package: Lit Query controllers/hooks and provider/context integration, comprehensive Vitest suites and Playwright E2E flows, build/tooling and packaging scripts, and three example apps (E2E todo, pagination with API, SSR) including demo servers and orchestration scripts.

Changes

Cohort / File(s) Summary
Package manifest & tooling
packages/lit-query/package.json, packages/lit-query/prettier.config.js, packages/lit-query/.editorconfig, packages/lit-query/.npmignore, packages/lit-query/.prettierignore, packages/lit-query/eslint.config.js, packages/lit-query/vitest.config.ts, packages/lit-query/tsconfig*.json, packages/lit-query/tsconfig.build*.json
New package manifest, formatting/lint configs, build/test scripts, ESM/CJS export map, and TypeScript build configs.
Core runtime & public API
packages/lit-query/src/index.ts, packages/lit-query/src/*.ts (accessor.ts, context.ts, types.ts, QueryClientProvider.ts, controllers/BaseController.ts, createQueryController.ts, createMutationController.ts, createInfiniteQueryController.ts, createQueriesController.ts, queryOptions.ts, infiniteQueryOptions.ts, mutationOptions.ts, useIsFetching.ts, useIsMutating.ts, useMutationState.ts)
New library implementation: provider, global context registry, BaseController lifecycle, controllers for queries/mutations/infinite/multi-query, accessor helpers, option helpers, hooks, and consolidated public exports.
Tests & test utilities
packages/lit-query/src/tests/*, packages/lit-query/src/tests/testHost.ts
Large Vitest suites (unit/integration/type-inference/stress) and test host utilities added; focus on test determinism and isolation.
E2E todo demo & helpers
packages/lit-query/examples/lit-query-e2e-app/... (package.json, src/*.ts, index.html, *.html, e2e/*.mjs, config/port.*, e2e/lib.mjs)
New Vite demo app with in-memory todo API, Playwright helpers for server lifecycle, artifact capture, and multiple E2E scenario scripts.
Pagination example & server
packages/lit-query/examples/lit-query-pagination-app/... (package.json, src/*.ts, server/index.mjs, e2e/*.mjs, config/ports.*, scripts/dev.mjs)
Pagination demo plus Node API server, dev orchestration, and E2E suites (smoke, boundary, prefetch, mutations, error recovery).
SSR example & server
packages/lit-query/examples/lit-query-ssr-app/... (package.json, src/*.ts, server/index.mjs, vite.config.ts, e2e/*.mjs, scripts/*.mjs, config/ports.*)
SSR demo with server-side render/hydration, SSR server implementation, build/hydration flow, and E2E tests including public-origin wrapper.
Build, packaging & verification scripts
packages/lit-query/scripts/*.mjs (check-package.mjs, check-cjs-types-smoke.mjs, write-cjs-package.mjs, measure-bundle.mjs, l3-stress.mjs, ...)
New scripts for package validation, CJS d.ts rewriting, CJS-type smoke check, bundle measurement, and a lifecycle stress harness.
Examples: HTML entrypoints & assets
packages/lit-query/examples/**/index.html, *.html, examples/**/src/*.ts
Multiple HTML entrypoints and example sources added (basic query, mutation, lifecycle fixtures, pagination, SSR) to support demos and E2E runs.
Repository config & ignores
.gitignore, knip.json, .changeset/*
Updated ignores for dist-cjs and Playwright artifacts, knip workspace entry for the package, and a changeset for a patch release.

Sequence Diagram(s)

sequenceDiagram
    participant Host as Lit Host
    participant Controller as Query Controller
    participant Context as Lit Context
    participant Client as QueryClient

    Host->>Controller: addController(host)
    Controller->>Controller: queueMicrotask(onConnected)
    Host->>Controller: hostConnected()
    Controller->>Context: dispatch ContextEvent(queryClientContext)
    Context-->>Controller: resolved QueryClient or undefined
    alt client available
        Controller->>Client: create Observer & subscribe
        Client-->>Controller: emit initial result/updates
        Controller->>Host: requestUpdate()
    else missing client
        Controller->>Controller: set missing-client placeholder
    end
    Host->>Controller: hostDisconnected()
    Controller->>Client: unsubscribe observer (if any)
Loading
sequenceDiagram
    participant Runner as Playwright Runner
    participant DevServer as Dev Server (pnpm/vite)
    participant Browser as Chromium
    participant App as Demo App

    Runner->>DevServer: ensureServer() (probe, spawn if needed)
    DevServer-->>Runner: ready or error
    Runner->>Browser: launch()
    Browser->>App: navigate(baseUrl)
    App-->>Browser: render UI / respond to API
    Browser->>Runner: scenario assertions (waitForText, actions)
    alt failure
        Runner->>Browser: capture screenshot & HTML
    end
    Runner->>Browser: close()
    Runner->>DevServer: stopServer()
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I hopped through code both wide and deep,
Providers set and controllers keep,
Servers wake and tests all run,
Playwright dances, examples hum.
A tiny carrot: build complete!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (4)
packages/lit-query/src/useIsFetching.ts (1)

13-108: Extract the shared counter-controller lifecycle.

IsFetchingController is now almost identical to IsMutatingController in packages/lit-query/src/useIsMutating.ts; the only real differences are which cache is subscribed to and which QueryClient counter is read. Pulling the shared sync/subscribe lifecycle into a small generic helper will keep future fixes from drifting between the two hooks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/useIsFetching.ts` around lines 13 - 108, The
IsFetchingController and IsMutatingController share nearly identical lifecycle
logic (constructor, syncClient, subscribe/unsubscribe,
onConnected/onDisconnected/onHostUpdate/onQueryClientChanged); extract that into
a small reusable generic helper or base class (e.g., SharedCounterController or
lifecycle mixin) that accepts the cache-subscribe function and the counter-read
function as parameters, then refactor IsFetchingController and
IsMutatingController to delegate to that helper: keep unique methods
computeValue (or the counter-read lambda) and the cache selection
(queryClient.getQueryCache() vs queryClient.getMutationCache()) as injectable
dependencies, reuse tryGetQueryClient/connectedState from BaseController, and
ensure subscribe() uses the injected cache subscribe and the result setter from
BaseController so both controllers share the sync/subscribe lifecycle.
packages/lit-query/examples/lit-query-pagination-app/vite.config.ts (1)

5-14: Optional: deduplicate identical server/preview settings.

This is correct as-is; a tiny DRY refactor can reduce future drift.

♻️ Proposed refactor
 import { defineConfig } from 'vite'
 import { DEMO_PORT } from './config/ports.js'
 
+const localServerConfig = () => ({
+  host: '127.0.0.1',
+  port: DEMO_PORT,
+  strictPort: true,
+})
+
 export default defineConfig({
-  server: {
-    host: '127.0.0.1',
-    port: DEMO_PORT,
-    strictPort: true,
-  },
-  preview: {
-    host: '127.0.0.1',
-    port: DEMO_PORT,
-    strictPort: true,
-  },
+  server: localServerConfig(),
+  preview: localServerConfig(),
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-pagination-app/vite.config.ts` around
lines 5 - 14, Duplicate server and preview configs (server and preview) both use
identical settings; extract the shared settings into a single constant (e.g.,
commonHostConfig or hostConfig) that references DEMO_PORT and then use that
constant for both server and preview (via direct assignment or spreading) so
changes to host, port or strictPort only need to be made in one place.
packages/lit-query/scripts/check-cjs-types-smoke.mjs (1)

46-47: Add defensive check for npm pack --json output.

If npm pack fails or returns an empty array, the destructuring const [{ filename }] will throw an unclear error. Consider adding validation.

♻️ Proposed defensive parsing
-  const [{ filename }] = JSON.parse(stdout)
+  const packResults = JSON.parse(stdout)
+  if (!Array.isArray(packResults) || packResults.length === 0 || !packResults[0].filename) {
+    throw new Error(`Unexpected npm pack output: ${stdout}`)
+  }
+  const [{ filename }] = packResults
   return resolve(destination, filename)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/scripts/check-cjs-types-smoke.mjs` around lines 46 - 47,
The current destructuring const [{ filename }] = JSON.parse(stdout) can throw if
npm pack returns empty or invalid JSON; change parse logic in this module to
validate the parsed value before destructuring: parse stdout with
JSON.parse(stdout), ensure the result is an array with at least one element and
that result[0].filename is a string, and only then call resolve(destination,
filename); otherwise throw or return a clear error indicating npm pack returned
no artifacts (include stdout in the error for debugging). Ensure checks are
applied where JSON.parse(stdout) and resolve(destination, filename) are used so
the code fails with a clear message instead of an uncaught destructuring error.
packages/lit-query/src/tests/context-provider.test.ts (1)

16-31: Consider adding afterEach cleanup for DOM elements.

The test appends elements to document.body and removes them manually, but if an assertion fails before provider.remove(), the element remains in the DOM and could affect subsequent tests.

♻️ Suggested pattern for test isolation
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

// At describe level:
let testContainer: HTMLDivElement

beforeEach(() => {
  testContainer = document.createElement('div')
  document.body.append(testContainer)
})

afterEach(() => {
  testContainer.remove()
})

// Then in tests, append to testContainer instead:
testContainer.append(provider)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/tests/context-provider.test.ts` around lines 16 - 31,
Add test-level DOM isolation by creating a test container in beforeEach and
cleaning it up in afterEach, then append the QueryClientProvider instance
(provider) to that container instead of document.body; update the test suite
around the use of QueryClientProvider, useQueryClient, and resolveQueryClient to
rely on the testContainer so that provider.remove() is not required for cleanup
and the DOM is always removed even if assertions fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/lit-query/examples/lit-query-e2e-app/config/port.js`:
- Around line 9-12: The port validation currently uses Number.parseInt on
envPort which accepts prefixes like "4173abc"; update the logic around
envPort/parsedPort/isValidPort to first verify envPort contains only digits
(e.g., /^\d+$/) before parsing, then parse to a number and enforce
Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535 so
non-numeric suffixes are rejected; adjust any error/default branch to rely on
this stricter check.

In `@packages/lit-query/examples/lit-query-pagination-app/config/ports.js`:
- Around line 10-12: The current logic uses Number.parseInt(rawValue, 10) and
only checks the numeric range, which accepts strings with trailing characters;
update the validation in ports.js so rawValue is first tested against a
full-digit regex (e.g., /^\d+$/) before parsing, then parse with
Number.parseInt(rawValue, 10) into parsed and keep the existing range check
(parsed >= 1 && parsed <= 65535) to set valid; reference the variables rawValue,
parsed and valid when making this change.

In `@packages/lit-query/examples/lit-query-pagination-app/server/index.mjs`:
- Around line 37-60: Both parsePositiveInt and parseNonNegativeInt currently use
Number.parseInt which accepts partial numeric strings like "1abc"; change them
to first verify the entire rawValue is a pure integer string (e.g., use a regex
that matches only digits for non-negative values, and digits for positive
checks) before converting, then parse with Number.parseInt or Number() and apply
the existing bounds checks; ensure the functions still return fallback when
rawValue is null/empty and return undefined for malformed or out-of-range
values.

In `@packages/lit-query/examples/lit-query-pagination-app/src/api.ts`:
- Around line 68-85: readJsonOrThrow currently catches JSON parse errors and
returns null, hiding malformed/empty 2xx bodies; change its logic so you do not
swallow parse errors for successful responses: attempt to parse the body with
response.json() without a blanket .catch for OK responses (let the parse error
propagate or explicitly throw a descriptive error for malformed JSON when
response.ok), while preserving tolerant behavior for non-ok responses (e.g., try
to parse error payload and if parsing fails fall back to the provided
fallbackMessage). Update the implementation in readJsonOrThrow to parse and
handle errors accordingly so successful responses never return null due to
swallowed JSON parse failures.

In `@packages/lit-query/examples/lit-query-ssr-app/scripts/dev.mjs`:
- Around line 23-39: The child process `server` can emit an `error` (spawn
failure) that will never resolve the awaited `once(server, 'exit')`, so modify
the shutdown/wait logic around `server` (and keep `stopServer`) to race the
`'error'` event with `'exit'`/`'close'`: create Promises using `once(server,
'error')` and `once(server, 'exit')` (or `'close'`) and use Promise.race to
await whichever happens first, then on `error` ensure you log/throw the error so
the script fails fast instead of hanging.

In
`@packages/lit-query/examples/lit-query-ssr-app/scripts/run-script-with-public-origin.mjs`:
- Around line 32-38: The code awaits the child process 'exit' event which can
hang if a spawn 'error' occurs; update the wait to listen for 'close' instead of
'exit' (i.e., change the await once(child, 'exit') usage to once(child,
'close')) and keep the existing child.on('error', ...) handler so
process.exitCode is set on errors; this ensures the wrapper always proceeds
after either 'exit' or 'error' and that stdio is closed (look for the
variables/function names child and once and the lines awaiting the 'exit'
event).

In `@packages/lit-query/examples/lit-query-ssr-app/src/api.ts`:
- Around line 51-57: The queryFn in createDataQueryOptions currently ignores the
TanStack Query context and doesn't forward the AbortSignal, breaking
cancellation; update the queryFn signature to accept the context parameter (from
createDataQueryOptions → queryFn: async ({ signal }) => { ... }) and pass that
signal into fetch(resolveApiUrl('/api/data', apiBaseUrl), { signal }); keep
using DATA_QUERY_KEY and return readJson<DataResponse>(response) unchanged so
cancellation is preserved when unmounting or aborting queries.

In `@packages/lit-query/package.json`:
- Around line 85-93: The package manifest currently pins "@tanstack/query-core"
to a published version under "dependencies"; replace that entry with a workspace
link by setting "@tanstack/query-core": "workspace:*" in the package.json
dependencies (keep the peerDependencies constraint as-is) so local monorepo
changes to query-core are used during builds and tests; update the
"dependencies" object where "@tanstack/query-core" appears to the workspace
spec.

In `@packages/lit-query/README.md`:
- Around line 216-233: The README's "Quality Gates" section references missing
docs files (`docs/TEST_MATRIX.md`, `docs/TEST_MATRIX_INTEGRATION.md`,
`docs/TEST_MATRIX_PERF.md`, `docs/RFC-v4.1.md`); either add those document files
with the expected content or remove/replace those links in the README to avoid
broken references. Update the section in packages/lit-query/README.md to either
point to existing documents or to inline the current gate commands (e.g., the
`Current local quality gate` and `Full local demo gate`) so contributors aren't
directed to non-existent files.

In `@packages/lit-query/scripts/check-package.mjs`:
- Around line 8-11: The execFileSync call that assigns rawOutput uses the
literal 'npm' which fails on Windows because the executable is 'npm.cmd'; update
the invocation in check-package.mjs to pick the correct binary based on platform
(e.g., determine npmBinary = process.platform === 'win32' ? 'npm.cmd' : 'npm' or
derive from process.env.npm_execpath) and call execFileSync(npmBinary,
['pack','--dry-run','--json'], { cwd: repoRoot, encoding: 'utf8' }) so the
script works on Windows and POSIX systems; keep the same args and options and
only change the binary selection logic around the execFileSync usage that sets
rawOutput.

In `@packages/lit-query/src/createMutationController.ts`:
- Around line 55-83: The idle fallback mutation result returned in
createMutationController/createIdleMutationResult is missing the public
MutationObserverResult field isPaused, causing mutation().isPaused to be
undefined; add isPaused: false to the returned object literal (alongside
isIdle/isPending/isSuccess/isError) so the shape matches
MutationObserverResult<TData,TError,TVariables,TOnMutateResult> and consumers
receive a boolean in the idle state.

In `@packages/lit-query/src/tests/client-switch-controllers.test.ts`:
- Around line 312-320: Tests currently call custom-element lifecycle methods
directly (consumer.connectedCallback() and consumer.disconnectedCallback()),
which double-runs controller hooks; instead remove those manual calls and use
DOM operations to drive lifecycle: use providerA.removeChild(consumer) and
providerB.append(consumer) (or append consumer to document.body to simulate
“connected without a provider”) so the browser invokes
disconnectedCallback/connectedCallback naturally; update the assertions around
consumer.mutation.mutateAsync to run after the DOM-based attach/detach and apply
the same fix to the other occurrences mentioned (lines near 377-389 and 451-465)
referencing providerA, providerB and consumer.

In `@packages/lit-query/src/tests/counters-and-state.test.ts`:
- Line 1: explicitCountersClient is shared mutable state causing leaks if a test
fails before the manual cleanup at the end of the file; move its teardown into
an afterEach hook so every test resets it. Add an afterEach that checks
explicitCountersClient and, if set, calls its close/dispose/shutdown method (or
nulls it) and then assigns undefined (or resets to initial value); remove the
one-off cleanup at the end of the file so teardown always runs even on test
failures.

In `@packages/lit-query/src/tests/infinite-and-options.test.ts`:
- Line 1: The tests leak shared state by cleaning up explicitInfiniteClient
inline; move the explicitInfiniteClient teardown into an afterEach hook so it's
always disposed even if a test fails (reference the explicitInfiniteClient
variable and the test suite in infinite-and-options.test.ts), i.e., add an
afterEach that checks for and closes/disposes explicitInfiniteClient and resets
it to undefined, and remove the inline cleanup to ensure each test runs with a
fresh provider/client.

In `@packages/lit-query/src/tests/testHost.ts`:
- Around line 86-97: The waitFor helper currently only checks boolean returns
and fails to handle assertion functions that throw or return undefined; update
waitFor to support sync or async assertions by calling await
Promise.resolve(assertion()) inside a try/catch loop, treat any truthy
(non-false) return value as success and return immediately, and on exceptions
capture the thrown error into a lastError variable and continue polling until
timeout, at which point throw the lastError (or a timeout Error if none) so
failing expectations bubble correctly; modify the function named waitFor
accordingly to implement this behavior.

---

Nitpick comments:
In `@packages/lit-query/examples/lit-query-pagination-app/vite.config.ts`:
- Around line 5-14: Duplicate server and preview configs (server and preview)
both use identical settings; extract the shared settings into a single constant
(e.g., commonHostConfig or hostConfig) that references DEMO_PORT and then use
that constant for both server and preview (via direct assignment or spreading)
so changes to host, port or strictPort only need to be made in one place.

In `@packages/lit-query/scripts/check-cjs-types-smoke.mjs`:
- Around line 46-47: The current destructuring const [{ filename }] =
JSON.parse(stdout) can throw if npm pack returns empty or invalid JSON; change
parse logic in this module to validate the parsed value before destructuring:
parse stdout with JSON.parse(stdout), ensure the result is an array with at
least one element and that result[0].filename is a string, and only then call
resolve(destination, filename); otherwise throw or return a clear error
indicating npm pack returned no artifacts (include stdout in the error for
debugging). Ensure checks are applied where JSON.parse(stdout) and
resolve(destination, filename) are used so the code fails with a clear message
instead of an uncaught destructuring error.

In `@packages/lit-query/src/tests/context-provider.test.ts`:
- Around line 16-31: Add test-level DOM isolation by creating a test container
in beforeEach and cleaning it up in afterEach, then append the
QueryClientProvider instance (provider) to that container instead of
document.body; update the test suite around the use of QueryClientProvider,
useQueryClient, and resolveQueryClient to rely on the testContainer so that
provider.remove() is not required for cleanup and the DOM is always removed even
if assertions fail.

In `@packages/lit-query/src/useIsFetching.ts`:
- Around line 13-108: The IsFetchingController and IsMutatingController share
nearly identical lifecycle logic (constructor, syncClient,
subscribe/unsubscribe,
onConnected/onDisconnected/onHostUpdate/onQueryClientChanged); extract that into
a small reusable generic helper or base class (e.g., SharedCounterController or
lifecycle mixin) that accepts the cache-subscribe function and the counter-read
function as parameters, then refactor IsFetchingController and
IsMutatingController to delegate to that helper: keep unique methods
computeValue (or the counter-read lambda) and the cache selection
(queryClient.getQueryCache() vs queryClient.getMutationCache()) as injectable
dependencies, reuse tryGetQueryClient/connectedState from BaseController, and
ensure subscribe() uses the injected cache subscribe and the result setter from
BaseController so both controllers share the sync/subscribe lifecycle.
🪄 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: 33495bdd-700c-4c8d-b2ea-f30ed64c2f1e

📥 Commits

Reviewing files that changed from the base of the PR and between 7ffa1ed and bf31fc5.

📒 Files selected for processing (95)
  • packages/lit-query/.editorconfig
  • packages/lit-query/.npmignore
  • packages/lit-query/.prettierignore
  • packages/lit-query/README.md
  • packages/lit-query/eslint.config.js
  • packages/lit-query/examples/lit-query-e2e-app/basic-query.html
  • packages/lit-query/examples/lit-query-e2e-app/config/port.d.ts
  • packages/lit-query/examples/lit-query-e2e-app/config/port.js
  • packages/lit-query/examples/lit-query-e2e-app/e2e/lib.mjs
  • packages/lit-query/examples/lit-query-e2e-app/e2e/lifecycle-contract.mjs
  • packages/lit-query/examples/lit-query-e2e-app/e2e/lifecycle-reconnect.mjs
  • packages/lit-query/examples/lit-query-e2e-app/e2e/mutation-error.mjs
  • packages/lit-query/examples/lit-query-e2e-app/e2e/query-error.mjs
  • packages/lit-query/examples/lit-query-e2e-app/e2e/refetch-button.mjs
  • packages/lit-query/examples/lit-query-e2e-app/e2e/smoke.mjs
  • packages/lit-query/examples/lit-query-e2e-app/index.html
  • packages/lit-query/examples/lit-query-e2e-app/lifecycle-contract.html
  • packages/lit-query/examples/lit-query-e2e-app/mutation.html
  • packages/lit-query/examples/lit-query-e2e-app/package.json
  • packages/lit-query/examples/lit-query-e2e-app/src/basic-query.ts
  • packages/lit-query/examples/lit-query-e2e-app/src/lifecycle-contract.ts
  • packages/lit-query/examples/lit-query-e2e-app/src/main.ts
  • packages/lit-query/examples/lit-query-e2e-app/src/mutation.ts
  • packages/lit-query/examples/lit-query-e2e-app/src/todoApi.ts
  • packages/lit-query/examples/lit-query-e2e-app/tsconfig.json
  • packages/lit-query/examples/lit-query-e2e-app/vite.config.ts
  • packages/lit-query/examples/lit-query-pagination-app/config/ports.d.ts
  • packages/lit-query/examples/lit-query-pagination-app/config/ports.js
  • packages/lit-query/examples/lit-query-pagination-app/e2e/boundary.mjs
  • packages/lit-query/examples/lit-query-pagination-app/e2e/error-recovery.mjs
  • packages/lit-query/examples/lit-query-pagination-app/e2e/lib.mjs
  • packages/lit-query/examples/lit-query-pagination-app/e2e/mutations.mjs
  • packages/lit-query/examples/lit-query-pagination-app/e2e/prefetch.mjs
  • packages/lit-query/examples/lit-query-pagination-app/e2e/smoke.mjs
  • packages/lit-query/examples/lit-query-pagination-app/index.html
  • packages/lit-query/examples/lit-query-pagination-app/package.json
  • packages/lit-query/examples/lit-query-pagination-app/scripts/dev.mjs
  • packages/lit-query/examples/lit-query-pagination-app/server/index.mjs
  • packages/lit-query/examples/lit-query-pagination-app/src/api.ts
  • packages/lit-query/examples/lit-query-pagination-app/src/main.ts
  • packages/lit-query/examples/lit-query-pagination-app/src/vite-env.d.ts
  • packages/lit-query/examples/lit-query-pagination-app/tsconfig.json
  • packages/lit-query/examples/lit-query-pagination-app/vite.config.ts
  • packages/lit-query/examples/lit-query-ssr-app/config/ports.d.ts
  • packages/lit-query/examples/lit-query-ssr-app/config/ports.js
  • packages/lit-query/examples/lit-query-ssr-app/e2e/error-recovery.mjs
  • packages/lit-query/examples/lit-query-ssr-app/e2e/lib.mjs
  • packages/lit-query/examples/lit-query-ssr-app/e2e/refreshing.mjs
  • packages/lit-query/examples/lit-query-ssr-app/e2e/ssr-smoke.mjs
  • packages/lit-query/examples/lit-query-ssr-app/index.html
  • packages/lit-query/examples/lit-query-ssr-app/package.json
  • packages/lit-query/examples/lit-query-ssr-app/scripts/dev.mjs
  • packages/lit-query/examples/lit-query-ssr-app/scripts/run-script-with-public-origin.mjs
  • packages/lit-query/examples/lit-query-ssr-app/server/index.mjs
  • packages/lit-query/examples/lit-query-ssr-app/src/api.ts
  • packages/lit-query/examples/lit-query-ssr-app/src/app.ts
  • packages/lit-query/examples/lit-query-ssr-app/src/main.ts
  • packages/lit-query/examples/lit-query-ssr-app/tsconfig.json
  • packages/lit-query/examples/lit-query-ssr-app/vite.config.ts
  • packages/lit-query/package.json
  • packages/lit-query/prettier.config.js
  • packages/lit-query/scripts/check-cjs-types-smoke.mjs
  • packages/lit-query/scripts/check-package.mjs
  • packages/lit-query/scripts/l3-stress.mjs
  • packages/lit-query/scripts/measure-bundle.mjs
  • packages/lit-query/scripts/write-cjs-package.mjs
  • packages/lit-query/src/QueryClientProvider.ts
  • packages/lit-query/src/accessor.ts
  • packages/lit-query/src/context.ts
  • packages/lit-query/src/controllers/BaseController.ts
  • packages/lit-query/src/createInfiniteQueryController.ts
  • packages/lit-query/src/createMutationController.ts
  • packages/lit-query/src/createQueriesController.ts
  • packages/lit-query/src/createQueryController.ts
  • packages/lit-query/src/index.ts
  • packages/lit-query/src/infiniteQueryOptions.ts
  • packages/lit-query/src/mutationOptions.ts
  • packages/lit-query/src/queryOptions.ts
  • packages/lit-query/src/tests/client-switch-controllers.test.ts
  • packages/lit-query/src/tests/context-provider.test.ts
  • packages/lit-query/src/tests/counters-and-state.test.ts
  • packages/lit-query/src/tests/infinite-and-options.test.ts
  • packages/lit-query/src/tests/mutation-controller.test.ts
  • packages/lit-query/src/tests/queries-controller.test.ts
  • packages/lit-query/src/tests/query-controller.test.ts
  • packages/lit-query/src/tests/testHost.ts
  • packages/lit-query/src/tests/type-inference.test.ts
  • packages/lit-query/src/types.ts
  • packages/lit-query/src/useIsFetching.ts
  • packages/lit-query/src/useIsMutating.ts
  • packages/lit-query/src/useMutationState.ts
  • packages/lit-query/tsconfig.build.cjs.json
  • packages/lit-query/tsconfig.build.json
  • packages/lit-query/tsconfig.json
  • packages/lit-query/vitest.config.ts

Comment on lines +9 to +12
const parsedPort = Number.parseInt(envPort, 10)
const isValidPort =
Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In JavaScript, does Number.parseInt("4173abc", 10) return 4173, and what is the recommended strict validation pattern for environment variable ports?

💡 Result:

Yes, Number.parseInt("4173abc", 10) returns 4173. This is because parseInt parses until it encounters a non-digit character in the specified radix (base 10 here), ignoring trailing invalid characters. The recommended strict validation pattern for environment variable ports in Node.js combines parsing with validation to ensure the entire string consists only of valid digits (no trailing junk, decimals, etc.) and the value is in the valid port range (typically 0-65535, often >0 and <=65535 for TCP ports). A common pattern, seen in Express generator's normalizePort and various best practices: function normalizePort(val) { const port = parseInt(val, 10); if (isNaN(port)) return val; // Allow named pipes if (port >= 0) return port; return false; } For stricter integer-only validation (rejecting anything not purely digits): - Test if the string matches /^\d+$/ (optionally allowing leading + or - if needed, but ports are unsigned). - Then parseInt and check range. Example strict port parser: function parseStrictPort(str) { if (!/^\d+$/.test(str)) return NaN; const port = Number.parseInt(str, 10); return (port >= 0 && port <= 65535) ? port : NaN; } Usage: const port = parseStrictPort(process.env.PORT); if (isNaN(port)) { throw new Error('Invalid PORT'); }

Citations:


🏁 Script executed:

cat -n packages/lit-query/examples/lit-query-e2e-app/config/port.js | sed -n '1,20p'

Repository: TanStack/query

Length of output: 649


Use strict numeric validation for DEMO_PORT.

Lines 9-12 currently accept values like "4173abc" because Number.parseInt is prefix-tolerant, silently accepting invalid environment input. Enforce digit-only validation before parsing.

Proposed fix
-  const parsedPort = Number.parseInt(envPort, 10)
-  const isValidPort =
-    Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
+  const isDigitsOnly = /^\d+$/.test(envPort)
+  const parsedPort = Number(envPort)
+  const isValidPort =
+    isDigitsOnly && Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
📝 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.

Suggested change
const parsedPort = Number.parseInt(envPort, 10)
const isValidPort =
Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
const isDigitsOnly = /^\d+$/.test(envPort)
const parsedPort = Number(envPort)
const isValidPort =
isDigitsOnly && Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-e2e-app/config/port.js` around lines 9
- 12, The port validation currently uses Number.parseInt on envPort which
accepts prefixes like "4173abc"; update the logic around
envPort/parsedPort/isValidPort to first verify envPort contains only digits
(e.g., /^\d+$/) before parsing, then parse to a number and enforce
Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535 so
non-numeric suffixes are rejected; adjust any error/default branch to rely on
this stricter check.

Comment on lines +10 to +12
const parsed = Number.parseInt(rawValue, 10)
const valid = Number.isInteger(parsed) && parsed >= 1 && parsed <= 65_535

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does JavaScript parseInt accept trailing non-numeric characters, and how should a port env var be validated to ensure the whole string is numeric?

💡 Result:

Yes, JavaScript's parseInt accepts trailing non-numeric characters and parses only the leading numeric portion, returning the integer value up to the first invalid character. For example, parseInt("123abc", 10) returns 123. To validate a port environment variable (e.g., process.env.PORT) and ensure the whole string is numeric (a valid integer port 0-65535), use this approach: 1. Trim whitespace. 2. Parse with parseInt(string, 10). 3. Check if the parsed number equals the original trimmed string (ensures no trailing invalid chars). 4. Verify it's an integer within port range. Example code: function validatePort(str) { const trimmed = str.trim; if (trimmed === '') return false; const num = parseInt(trimmed, 10); return num >= 0 && num <= 65535 && trimmed === num.toString; } // Usage: const portStr = process.env.PORT; const port = parseInt(portStr, 10); if (validatePort(portStr) && port !== NaN) { // Use port } else { // Handle invalid: use default like 3000 } This catches cases like "3000foo" (parses to 3000 but validation fails), "3000.5" (parses to 3000 but string mismatch), empty, non-numeric. Alternatively, use regex /^\d+$/ for digits-only check before parsing, or libraries like envalid.port or zod.coerce.number.int.min(0).max(65535).

Citations:


🏁 Script executed:

find . -name "ports.js" -type f | head -20

Repository: TanStack/query

Length of output: 192


🏁 Script executed:

if [ -f "packages/lit-query/examples/lit-query-pagination-app/config/ports.js" ]; then
  echo "=== File exists ===" 
  wc -l "packages/lit-query/examples/lit-query-pagination-app/config/ports.js"
  echo "=== Lines 1-20 ===" 
  sed -n '1,20p' "packages/lit-query/examples/lit-query-pagination-app/config/ports.js"
fi

Repository: TanStack/query

Length of output: 616


Tighten env port parsing to reject malformed strings with trailing characters.

Number.parseInt() accepts strings with trailing non-numeric characters (e.g., "4184foo" parses as 4184). Line 10-12 would incorrectly validate such inputs since it only checks if the parsed result is a valid integer in range, not if the entire input string is numeric.

Use a regex check before parsing to ensure the full string contains only digits:

Proposed fix
-  const parsed = Number.parseInt(rawValue, 10)
-  const valid = Number.isInteger(parsed) && parsed >= 1 && parsed <= 65_535
+  const isDigitsOnly = /^\d+$/.test(rawValue)
+  const parsed = Number(rawValue)
+  const valid = isDigitsOnly && Number.isInteger(parsed) && parsed >= 1 && parsed <= 65_535
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-pagination-app/config/ports.js` around
lines 10 - 12, The current logic uses Number.parseInt(rawValue, 10) and only
checks the numeric range, which accepts strings with trailing characters; update
the validation in ports.js so rawValue is first tested against a full-digit
regex (e.g., /^\d+$/) before parsing, then parse with Number.parseInt(rawValue,
10) into parsed and keep the existing range check (parsed >= 1 && parsed <=
65535) to set valid; reference the variables rawValue, parsed and valid when
making this change.

Comment on lines +37 to +60
function parsePositiveInt(rawValue, fallback) {
if (rawValue == null || rawValue === '') {
return fallback
}

const parsed = Number.parseInt(rawValue, 10)
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) {
return undefined
}

return parsed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject partial numeric strings here.

Number.parseInt will accept inputs like "1abc" and "10ms", so these helpers currently treat malformed query params as valid. If the endpoint is meant to validate integers, use a full-string check before converting.

Safer integer parsing
 function parsePositiveInt(rawValue, fallback) {
   if (rawValue == null || rawValue === '') {
     return fallback
   }
 
-  const parsed = Number.parseInt(rawValue, 10)
-  if (!Number.isInteger(parsed) || parsed < 1) {
+  if (!/^[1-9]\d*$/.test(rawValue)) {
     return undefined
   }
+  const parsed = Number(rawValue)
 
   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)
 
   return parsed
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-pagination-app/server/index.mjs` around
lines 37 - 60, Both parsePositiveInt and parseNonNegativeInt currently use
Number.parseInt which accepts partial numeric strings like "1abc"; change them
to first verify the entire rawValue is a pure integer string (e.g., use a regex
that matches only digits for non-negative values, and digits for positive
checks) before converting, then parse with Number.parseInt or Number() and apply
the existing bounds checks; ensure the functions still return fallback when
rawValue is null/empty and return undefined for malformed or out-of-range
values.

Comment thread packages/lit-query/examples/lit-query-pagination-app/src/api.ts Outdated
Comment thread packages/lit-query/examples/lit-query-ssr-app/scripts/dev.mjs
Comment thread packages/lit-query/src/createMutationController.ts
Comment on lines +312 to +320
providerA.removeChild(consumer)
await new Promise((resolve) => setTimeout(resolve, 0))
consumer.connectedCallback()
await expect(consumer.mutation.mutateAsync(2)).rejects.toThrow(
/No QueryClient available/,
)

consumer.disconnectedCallback()
providerB.append(consumer)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid calling custom-element lifecycle callbacks manually.

These tests are putting the host into states the browser never creates. removeChild/append already drive disconnectedCallback and connectedCallback; the extra manual calls double-run controller hooks and can mask real client-resolution or observer-cleanup bugs. To test “connected without a provider”, append the consumer to document.body instead of invoking the callbacks directly.

Also applies to: 377-389, 451-465

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/tests/client-switch-controllers.test.ts` around lines
312 - 320, Tests currently call custom-element lifecycle methods directly
(consumer.connectedCallback() and consumer.disconnectedCallback()), which
double-runs controller hooks; instead remove those manual calls and use DOM
operations to drive lifecycle: use providerA.removeChild(consumer) and
providerB.append(consumer) (or append consumer to document.body to simulate
“connected without a provider”) so the browser invokes
disconnectedCallback/connectedCallback naturally; update the assertions around
consumer.mutation.mutateAsync to run after the DOM-based attach/detach and apply
the same fix to the other occurrences mentioned (lines near 377-389 and 451-465)
referencing providerA, providerB and consumer.

@@ -0,0 +1,604 @@
import { describe, expect, it } from 'vitest'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Move explicitCountersClient cleanup into afterEach.

explicitCountersClient is shared mutable state. If an assertion fails before Line 211 runs, later tests in this file stop covering the provider path and start using the leaked explicit client instead.

🧪 Suggested test hardening
-import { describe, expect, it } from 'vitest'
+import { afterEach, describe, expect, it } from 'vitest'
 import { QueryClient } from '@tanstack/query-core'
@@
 let explicitCountersClient: QueryClient | undefined
+
+afterEach(() => {
+  explicitCountersClient = undefined
+})

Also applies to: 22-22

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/tests/counters-and-state.test.ts` at line 1,
explicitCountersClient is shared mutable state causing leaks if a test fails
before the manual cleanup at the end of the file; move its teardown into an
afterEach hook so every test resets it. Add an afterEach that checks
explicitCountersClient and, if set, calls its close/dispose/shutdown method (or
nulls it) and then assigns undefined (or resets to initial value); remove the
one-off cleanup at the end of the file so teardown always runs even on test
failures.

@@ -0,0 +1,377 @@
import { describe, expect, it } from 'vitest'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Move explicitInfiniteClient cleanup into afterEach.

This file has the same shared-state leak risk: if LC-INF-02 fails before Line 129, later tests can accidentally bypass provider coverage by reusing the leaked explicit client.

🧪 Suggested test hardening
-import { describe, expect, it } from 'vitest'
+import { afterEach, describe, expect, it } from 'vitest'
 import { QueryClient } from '@tanstack/query-core'
@@
 let explicitInfiniteClient: QueryClient | undefined
+
+afterEach(() => {
+  explicitInfiniteClient = undefined
+})

Also applies to: 23-23

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/tests/infinite-and-options.test.ts` at line 1, The
tests leak shared state by cleaning up explicitInfiniteClient inline; move the
explicitInfiniteClient teardown into an afterEach hook so it's always disposed
even if a test fails (reference the explicitInfiniteClient variable and the test
suite in infinite-and-options.test.ts), i.e., add an afterEach that checks for
and closes/disposes explicitInfiniteClient and resets it to undefined, and
remove the inline cleanup to ensure each test runs with a fresh provider/client.

Comment on lines +86 to +97
export async function waitFor(
assertion: () => boolean,
timeoutMs = 2000,
): Promise<void> {
const startedAt = Date.now()
while (!assertion()) {
if (Date.now() - startedAt > timeoutMs) {
throw new Error(`Timed out waiting for assertion after ${timeoutMs}ms`)
}
await new Promise((resolve) => setTimeout(resolve, 10))
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

waitFor only handles boolean predicates right now.

A passing callback like () => expect(x).toBe(1) returns undefined, so this loop keeps polling until timeout. Thrown assertions also abort immediately instead of retrying, which makes the helper brittle for normal test usage.

💡 Suggested fix
 export async function waitFor(
-  assertion: () => boolean,
+  assertion: () => unknown,
   timeoutMs = 2000,
 ): Promise<void> {
   const startedAt = Date.now()
-  while (!assertion()) {
-    if (Date.now() - startedAt > timeoutMs) {
-      throw new Error(`Timed out waiting for assertion after ${timeoutMs}ms`)
+  let lastError: unknown
+
+  while (Date.now() - startedAt <= timeoutMs) {
+    try {
+      const result = assertion()
+      if (result !== false) {
+        return
+      }
+    } catch (error) {
+      lastError = error
     }
+
     await new Promise((resolve) => setTimeout(resolve, 10))
   }
+
+  throw lastError ?? new Error(`Timed out waiting for assertion after ${timeoutMs}ms`)
 }
📝 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.

Suggested change
export async function waitFor(
assertion: () => boolean,
timeoutMs = 2000,
): Promise<void> {
const startedAt = Date.now()
while (!assertion()) {
if (Date.now() - startedAt > timeoutMs) {
throw new Error(`Timed out waiting for assertion after ${timeoutMs}ms`)
}
await new Promise((resolve) => setTimeout(resolve, 10))
}
}
export async function waitFor(
assertion: () => unknown,
timeoutMs = 2000,
): Promise<void> {
const startedAt = Date.now()
let lastError: unknown
while (Date.now() - startedAt <= timeoutMs) {
try {
const result = assertion()
if (result !== false) {
return
}
} catch (error) {
lastError = error
}
await new Promise((resolve) => setTimeout(resolve, 10))
}
throw lastError ?? new Error(`Timed out waiting for assertion after ${timeoutMs}ms`)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/tests/testHost.ts` around lines 86 - 97, The waitFor
helper currently only checks boolean returns and fails to handle assertion
functions that throw or return undefined; update waitFor to support sync or
async assertions by calling await Promise.resolve(assertion()) inside a
try/catch loop, treat any truthy (non-false) return value as success and return
immediately, and on exceptions capture the thrown error into a lastError
variable and continue polling until timeout, at which point throw the lastError
(or a timeout Error if none) so failing expectations bubble correctly; modify
the function named waitFor accordingly to implement this behavior.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
packages/lit-query/src/useMutationState.ts (2)

113-118: Simplify condition: select is always a function by type definition.

Since select is typed as (mutation: Mutation) => TResult, the typeof check is redundant—it will always be 'function' when select is defined. Using !== undefined is clearer.

♻️ Suggested simplification
   private shouldRefreshOnHostUpdate(): boolean {
     return (
       typeof this.options.filters === 'function' ||
-      typeof this.options.select === 'function'
+      this.options.select !== undefined
     )
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/useMutationState.ts` around lines 113 - 118, The
condition in shouldRefreshOnHostUpdate() uses typeof this.options.select ===
'function' but select is statically typed as a function when present; change the
check to test presence instead (e.g., this.options.select !== undefined) and
keep the filters check as-is (typeof this.options.filters === 'function');
update the shouldRefreshOnHostUpdate method to use the presence check for
options.select to simplify the logic and avoid a redundant typeof check.

54-58: Consider removing unnecessary syncClient() call in onDisconnected.

After unsubscribing on line 55-56, calling syncClient() appears unnecessary—we're disconnecting so the client reference state doesn't need updating, and the return value is discarded.

♻️ Suggested simplification
   protected onDisconnected(): void {
     this.unsubscribe?.()
     this.unsubscribe = undefined
-    this.syncClient()
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/useMutationState.ts` around lines 54 - 58, The
onDisconnected method calls this.unsubscribe and then calls this.syncClient(),
but since we're disconnecting the client the syncClient() call is unnecessary
and its return value is ignored; remove the this.syncClient() call from the
onDisconnected() method (leave the unsubscribe clearing logic intact) so only
the unsubscribe cleanup runs in onDisconnected.
packages/lit-query/src/createQueriesController.ts (2)

179-179: Consider using undefined instead of empty array for initial result.

The empty array [] cast to TCombinedResult could cause unexpected behavior if TCombinedResult is not array-like (e.g., when combine returns an object). While this value is typically replaced before access, using undefined as the initial sentinel and handling it in readCurrent would be more type-safe.

♻️ Suggested approach
-    super(host, [] as unknown as TCombinedResult, queryClient)
+    super(host, undefined as unknown as TCombinedResult, queryClient)

Then ensure readCurrent handles the undefined case, which it already does via placeholderInitialized tracking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/createQueriesController.ts` at line 179, The
constructor currently passes an empty array cast to TCombinedResult into super
(super(host, [] as unknown as TCombinedResult, queryClient)), which is unsafe if
TCombinedResult isn't array-like; change that initial result to undefined (pass
undefined as the initial sentinel) and ensure the class handles this by using
the existing placeholderInitialized logic and readCurrent to return a proper
value when the result is undefined; update the super call to use undefined and
verify readCurrent and any consumers of the initial value (e.g.,
placeholderInitialized, readCurrent) correctly handle undefined.

346-361: Document the early-read handshake pattern.

The error swallowing based on placeholderRetryableFailure is clever for handling reads during class-field initialization (before accessors are ready), but this subtle timing behavior could confuse maintainers. Consider adding a brief inline comment explaining the microtask window.

📝 Suggested documentation
   readCurrent(): TCombinedResult {
     if (!this.queryClient && !this.observer && !this.placeholderInitialized) {
       try {
-        // Early reads can happen during class-field initialization, before
-        // accessors referenced by queries/combine are ready. Retry normally
-        // after construction finishes and only surface errors after that point.
+        // Early reads can happen during class-field initialization, before
+        // accessors referenced by queries/combine are ready. The queueMicrotask
+        // in the constructor gives a brief window where errors are swallowed,
+        // allowing the host to complete initialization before we surface errors.
         this.setPlaceholderResult()
       } catch (error) {
         if (!this.placeholderRetryableFailure) {
           throw error
         }
+        // Error swallowed during handshake window - will retry on next access
       }
     }

     return this.current
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/createQueriesController.ts` around lines 346 - 361,
Add an inline comment in readCurrent explaining the early-read handshake:
document that setPlaceholderResult is retried to handle reads during class-field
initialization (a microtask window before accessors are wired), why
placeholderRetryableFailure suppresses errors during that window, and when
errors will be surfaced (after placeholderInitialized becomes true); reference
the readCurrent method, setPlaceholderResult call, placeholderInitialized and
placeholderRetryableFailure flags, and the returned current result so future
maintainers understand the timing semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/lit-query/src/createQueriesController.ts`:
- Line 179: The constructor currently passes an empty array cast to
TCombinedResult into super (super(host, [] as unknown as TCombinedResult,
queryClient)), which is unsafe if TCombinedResult isn't array-like; change that
initial result to undefined (pass undefined as the initial sentinel) and ensure
the class handles this by using the existing placeholderInitialized logic and
readCurrent to return a proper value when the result is undefined; update the
super call to use undefined and verify readCurrent and any consumers of the
initial value (e.g., placeholderInitialized, readCurrent) correctly handle
undefined.
- Around line 346-361: Add an inline comment in readCurrent explaining the
early-read handshake: document that setPlaceholderResult is retried to handle
reads during class-field initialization (a microtask window before accessors are
wired), why placeholderRetryableFailure suppresses errors during that window,
and when errors will be surfaced (after placeholderInitialized becomes true);
reference the readCurrent method, setPlaceholderResult call,
placeholderInitialized and placeholderRetryableFailure flags, and the returned
current result so future maintainers understand the timing semantics.

In `@packages/lit-query/src/useMutationState.ts`:
- Around line 113-118: The condition in shouldRefreshOnHostUpdate() uses typeof
this.options.select === 'function' but select is statically typed as a function
when present; change the check to test presence instead (e.g.,
this.options.select !== undefined) and keep the filters check as-is (typeof
this.options.filters === 'function'); update the shouldRefreshOnHostUpdate
method to use the presence check for options.select to simplify the logic and
avoid a redundant typeof check.
- Around line 54-58: The onDisconnected method calls this.unsubscribe and then
calls this.syncClient(), but since we're disconnecting the client the
syncClient() call is unnecessary and its return value is ignored; remove the
this.syncClient() call from the onDisconnected() method (leave the unsubscribe
clearing logic intact) so only the unsubscribe cleanup runs in onDisconnected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e23f0f9a-ca96-457f-8103-84e2e95b80a0

📥 Commits

Reviewing files that changed from the base of the PR and between bf31fc5 and a322b9c.

📒 Files selected for processing (7)
  • packages/lit-query/src/createQueriesController.ts
  • packages/lit-query/src/index.ts
  • packages/lit-query/src/queryOptions.ts
  • packages/lit-query/src/tests/counters-and-state.test.ts
  • packages/lit-query/src/tests/type-inference.test.ts
  • packages/lit-query/src/types.ts
  • packages/lit-query/src/useMutationState.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/lit-query/src/tests/counters-and-state.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/lit-query/src/types.ts
  • packages/lit-query/src/tests/type-inference.test.ts
  • packages/lit-query/src/index.ts

  - 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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
packages/lit-query/package.json (1)

51-51: Avoid rebuilding twice in demo:gate to reduce CI/dev runtime.

demo:gate runs build, then package:build (which runs build again). Folding package:check into package:build lets the gate invoke one packaging command without redundant work.

♻️ Suggested script cleanup
-    "package:build": "pnpm run build && pnpm run test:build",
+    "package:build": "pnpm run build && pnpm run package:check && pnpm run test:build",
...
-    "demo:gate": "pnpm run typecheck && pnpm run lint && pnpm run format:check && pnpm test && pnpm run build && pnpm run package:check && pnpm run package:build && pnpm run example:e2e:all:nobuild && pnpm run example:ssr:e2e:all:nobuild && pnpm run measure:bundle:raw && pnpm run perf:l3:raw",
+    "demo:gate": "pnpm run typecheck && pnpm run lint && pnpm run format:check && pnpm test && pnpm run package:build && pnpm run example:e2e:all:nobuild && pnpm run example:ssr:e2e:all:nobuild && pnpm run measure:bundle:raw && pnpm run perf:l3:raw",

Also applies to: 89-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/package.json` at line 51, Update the npm scripts to remove
the duplicate build: change the "package:build" script so it performs the
packaging/verification steps currently in "package:check" (fold package:check's
duties into package:build) and no longer redundantly runs "build" if the gate
already calls build; then adjust "demo:gate" to invoke only "pnpm run
package:build" (instead of running "build" and then "package:build") so the
CI/dev flow runs the package command once. Ensure references to "package:check",
"package:build", and "demo:gate" are updated accordingly and remove or repurpose
the old "package:check" script if no longer needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/lit-query/src/createQueriesController.ts`:
- Around line 1-22: The import block mixes type-only and value imports causing
lint errors; split type-only imports from value imports and remove duplicates
for both `@tanstack/query-core` and ./accessor.js: import runtime values
(QueriesObserver, createValueAccessor, readAccessor, BaseController,
createMissingQueryClientError) in one group and import only types (DefaultError,
DefinedQueryObserverResult, OmitKeyof, QueriesObserverOptions, QueryFunction,
QueryKey, QueryObserverOptions, QueryObserverResult, ThrowOnError, QueryClient,
ReactiveControllerHost, Accessor, ValueAccessor) in a separate "import type"
group, and reorder groups to satisfy import/order so all external deps come
before local modules.
- Around line 289-295: The synchronous call to resolveQueriesOptions(options,
queryClient) can throw during construction when an explicit queryClient is
provided; wrap that call and the subsequent QueriesObserver creation and result
computation (resolveQueriesOptions, new QueriesObserver(...),
observer.getCurrentResult(), computeResult) in a try-catch so any thrown error
is caught and the element is left in a safe state (mirroring readCurrent’s
guarded path): on catch assign this.queryClient and this.observer only if
created, set this.result to an error or empty fallback result, and rethrow or
store the error state as appropriate for later handling so construction does not
allow an uncaught exception to propagate.
- Around line 435-442: The placeholders are created with undefined data, so pass
each query's initialData into createPendingQueryObserverResult before calling
resolvedOptions.combine: for each item in queries (obtained via
readAccessor(resolvedOptions.queries)), extract its options.initialData (or
resolved initialData via readAccessor if options is an accessor) and call
createPendingQueryObserverResult(initialData) to build placeholders that satisfy
the DefinedQueryObserverResult contract; then pass that placeholders array to
resolvedOptions.combine (or return placeholders) so consumers (e.g.,
result[0].data.name) see the initial data. Ensure you reference resolvedOptions,
queries, createPendingQueryObserverResult, combine, and readAccessor when making
the change.

In `@packages/lit-query/src/QueryClientProvider.ts`:
- Around line 1-10: The import ordering violates the lint rule because a
type-only import (QueryClient from '@tanstack/query-core') appears before value
imports from 'lit' and './context.js'; reorder the import statements in
QueryClientProvider.ts so that value imports (ContextProvider from
'@lit/context', LitElement, html, TemplateResult from 'lit', and the named
exports createMissingQueryClientError, queryClientContext,
registerDefaultQueryClient, unregisterDefaultQueryClient from './context.js')
come first, and then place the type-only import of QueryClient after them to
satisfy import/order.
- Around line 47-53: When this.client becomes undefined the code currently
throws createMissingQueryClientError() before cleaning up, which leaves the
previously mounted client/state registered; update the QueryClientProvider
branch that checks const nextClient = this.client and if (!nextClient) to first
perform the provider's teardown/cleanup logic for the previous client
(unregister it as the default client and clear subscriptions/state) and then
throw createMissingQueryClientError(); place the cleanup call(s) immediately
before the throw so symbols like client, isConnected and
createMissingQueryClientError remain as-is and only the order changes.

In `@packages/lit-query/src/tests/type-inference.test.ts`:
- Around line 1-7: The import line mixes value and type specifiers causing lint
failures; split into two imports: one importing runtime values (dataTagSymbol,
QueryClient) and a separate type-only import for DefinedQueryObserverResult and
QueryObserverResult using the type keyword (e.g. import { type
DefinedQueryObserverResult, type QueryObserverResult } from
'@tanstack/query-core'), and ensure the two import statements are ordered/sorted
according to project rules (values import first, then type-only import).

---

Nitpick comments:
In `@packages/lit-query/package.json`:
- Line 51: Update the npm scripts to remove the duplicate build: change the
"package:build" script so it performs the packaging/verification steps currently
in "package:check" (fold package:check's duties into package:build) and no
longer redundantly runs "build" if the gate already calls build; then adjust
"demo:gate" to invoke only "pnpm run package:build" (instead of running "build"
and then "package:build") so the CI/dev flow runs the package command once.
Ensure references to "package:check", "package:build", and "demo:gate" are
updated accordingly and remove or repurpose the old "package:check" script if no
longer needed.
🪄 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: c1f340c5-0f5b-4dda-9bc0-d68f3409b53c

📥 Commits

Reviewing files that changed from the base of the PR and between d545bf7 and a87a875.

📒 Files selected for processing (11)
  • packages/lit-query/package.json
  • packages/lit-query/scripts/check-cjs-types-smoke.mjs
  • packages/lit-query/src/QueryClientProvider.ts
  • packages/lit-query/src/createQueriesController.ts
  • packages/lit-query/src/queryOptions.ts
  • packages/lit-query/src/tests/type-inference.test.ts
  • packages/lit-query/src/types.ts
  • packages/lit-query/tsconfig.build.cjs.json
  • packages/lit-query/tsconfig.build.json
  • packages/lit-query/tsconfig.json
  • packages/lit-query/vitest.config.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/lit-query/tsconfig.build.json
  • packages/lit-query/tsconfig.build.cjs.json
  • packages/lit-query/tsconfig.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/lit-query/vitest.config.ts
  • packages/lit-query/scripts/check-cjs-types-smoke.mjs
  • packages/lit-query/src/queryOptions.ts

Comment on lines +1 to +22
import {
QueriesObserver,
type DefaultError,
type DefinedQueryObserverResult,
type OmitKeyof,
type QueriesObserverOptions,
type QueryFunction,
type QueryKey,
type QueryObserverOptions,
type QueryObserverResult,
type ThrowOnError,
} from '@tanstack/query-core'
import type { QueryClient } from '@tanstack/query-core'
import type { ReactiveControllerHost } from 'lit'
import {
createValueAccessor,
readAccessor,
type Accessor,
type ValueAccessor,
} from './accessor.js'
import { createMissingQueryClientError } from './context.js'
import { BaseController } from './controllers/BaseController.js'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Normalize this import block before lint runs.

This file currently trips import/consistent-type-specifier-style, import/no-duplicates, and import/order. Split the value imports from the type-only imports for both @tanstack/query-core and ./accessor.js.

Suggested diff
-import {
-  QueriesObserver,
-  type DefaultError,
-  type DefinedQueryObserverResult,
-  type OmitKeyof,
-  type QueriesObserverOptions,
-  type QueryFunction,
-  type QueryKey,
-  type QueryObserverOptions,
-  type QueryObserverResult,
-  type ThrowOnError,
-} from '@tanstack/query-core'
-import type { QueryClient } from '@tanstack/query-core'
-import type { ReactiveControllerHost } from 'lit'
-import {
-  createValueAccessor,
-  readAccessor,
-  type Accessor,
-  type ValueAccessor,
-} from './accessor.js'
-import { createMissingQueryClientError } from './context.js'
-import { BaseController } from './controllers/BaseController.js'
+import { QueriesObserver } from '@tanstack/query-core'
+import { createValueAccessor, readAccessor } from './accessor.js'
+import { createMissingQueryClientError } from './context.js'
+import { BaseController } from './controllers/BaseController.js'
+
+import type {
+  DefaultError,
+  DefinedQueryObserverResult,
+  OmitKeyof,
+  QueriesObserverOptions,
+  QueryClient,
+  QueryFunction,
+  QueryKey,
+  QueryObserverOptions,
+  QueryObserverResult,
+  ThrowOnError,
+} from '@tanstack/query-core'
+import type { ReactiveControllerHost } from 'lit'
+import type { Accessor, ValueAccessor } from './accessor.js'
🧰 Tools
🪛 ESLint

[error] 3-3: Member 'DefaultError' of the import declaration should be sorted alphabetically.

(sort-imports)


[error] 3-3: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 4-4: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 5-5: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 6-6: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 7-7: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 8-8: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 9-9: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 10-10: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 11-11: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 12-12: '@tanstack/query-core' imported multiple times.

(import/no-duplicates)


[error] 13-13: @tanstack/query-core type import should occur after import of ./controllers/BaseController.js

(import/order)


[error] 13-13: '@tanstack/query-core' imported multiple times.

(import/no-duplicates)


[error] 14-14: lit type import should occur after import of ./controllers/BaseController.js

(import/order)


[error] 18-18: Member 'Accessor' of the import declaration should be sorted alphabetically.

(sort-imports)


[error] 18-18: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 19-19: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/createQueriesController.ts` around lines 1 - 22, The
import block mixes type-only and value imports causing lint errors; split
type-only imports from value imports and remove duplicates for both
`@tanstack/query-core` and ./accessor.js: import runtime values (QueriesObserver,
createValueAccessor, readAccessor, BaseController,
createMissingQueryClientError) in one group and import only types (DefaultError,
DefinedQueryObserverResult, OmitKeyof, QueriesObserverOptions, QueryFunction,
QueryKey, QueryObserverOptions, QueryObserverResult, ThrowOnError, QueryClient,
ReactiveControllerHost, Accessor, ValueAccessor) in a separate "import type"
group, and reorder groups to satisfy import/order so all external deps come
before local modules.

Comment on lines +289 to +295
const { queries, combine } = resolveQueriesOptions(options, queryClient)
const observer = new QueriesObserver(queryClient, queries, {
combine,
} as QueriesObserverOptions<TCombinedResult>)
this.queryClient = queryClient
this.observer = observer
this.result = this.computeResult(observer.getCurrentResult(), combine)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the file
fd -t f "createQueriesController.ts" packages/lit-query/src/

Repository: TanStack/query

Length of output: 107


🏁 Script executed:

# Read the file to check its size first
wc -l packages/lit-query/src/createQueriesController.ts

Repository: TanStack/query

Length of output: 111


🏁 Script executed:

# Read lines 280-300 to see the constructor code
sed -n '280,300p' packages/lit-query/src/createQueriesController.ts

Repository: TanStack/query

Length of output: 638


🏁 Script executed:

# Read lines 440-465 to see the readCurrent() method
sed -n '440,465p' packages/lit-query/src/createQueriesController.ts

Repository: TanStack/query

Length of output: 870


🏁 Script executed:

# Search for resolveQueriesOptions definition
rg "resolveQueriesOptions" packages/lit-query/src/ -A 10

Repository: TanStack/query

Length of output: 2804


🏁 Script executed:

# Get more context on the constructor to see the full flow
sed -n '260,310p' packages/lit-query/src/createQueriesController.ts

Repository: TanStack/query

Length of output: 1494


🏁 Script executed:

# Check if there's a try-catch around the constructor code or any protection
sed -n '250,320p' packages/lit-query/src/createQueriesController.ts

Repository: TanStack/query

Length of output: 1887


🏁 Script executed:

# Look at the entire setClient method to understand the full flow
rg "private setClient|private.*client" packages/lit-query/src/createQueriesController.ts -A 20

Repository: TanStack/query

Length of output: 579


Wrap resolveQueriesOptions() in a try-catch when initializing with an explicit client.

The constructor calls resolveQueriesOptions() synchronously when a queryClient is passed (lines 289-295). If the queries accessor throws during this call, the error will propagate uncaught during element construction. The readCurrent() method handles this hazard for deferred initialization with a try-catch guard, but the explicit-client path bypasses that protection entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/createQueriesController.ts` around lines 289 - 295,
The synchronous call to resolveQueriesOptions(options, queryClient) can throw
during construction when an explicit queryClient is provided; wrap that call and
the subsequent QueriesObserver creation and result computation
(resolveQueriesOptions, new QueriesObserver(...), observer.getCurrentResult(),
computeResult) in a try-catch so any thrown error is caught and the element is
left in a safe state (mirroring readCurrent’s guarded path): on catch assign
this.queryClient and this.observer only if created, set this.result to an error
or empty fallback result, and rethrow or store the error state as appropriate
for later handling so construction does not allow an uncaught exception to
propagate.

Comment thread packages/lit-query/src/createQueriesController.ts
Comment on lines +1 to +10
import { ContextProvider } from '@lit/context'
import type { QueryClient } from '@tanstack/query-core'
import type { TemplateResult } from 'lit'
import { LitElement, html } from 'lit'
import {
createMissingQueryClientError,
queryClientContext,
registerDefaultQueryClient,
unregisterDefaultQueryClient,
} from './context.js'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the import grouping so this file passes lint.

lit and ./context.js need to be ordered before the type-only @tanstack/query-core import, otherwise import/order keeps failing here.

💡 Minimal reorder
 import { ContextProvider } from '@lit/context'
-import type { QueryClient } from '@tanstack/query-core'
-import type { TemplateResult } from 'lit'
-import { LitElement, html } from 'lit'
+import { LitElement, html, type TemplateResult } from 'lit'
 import {
   createMissingQueryClientError,
   queryClientContext,
   registerDefaultQueryClient,
   unregisterDefaultQueryClient,
 } from './context.js'
+import type { QueryClient } from '@tanstack/query-core'
📝 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.

Suggested change
import { ContextProvider } from '@lit/context'
import type { QueryClient } from '@tanstack/query-core'
import type { TemplateResult } from 'lit'
import { LitElement, html } from 'lit'
import {
createMissingQueryClientError,
queryClientContext,
registerDefaultQueryClient,
unregisterDefaultQueryClient,
} from './context.js'
import { ContextProvider } from '@lit/context'
import { LitElement, html, type TemplateResult } from 'lit'
import {
createMissingQueryClientError,
queryClientContext,
registerDefaultQueryClient,
unregisterDefaultQueryClient,
} from './context.js'
import type { QueryClient } from '@tanstack/query-core'
🧰 Tools
🪛 ESLint

[error] 4-4: lit import should occur before type import of @tanstack/query-core

(import/order)


[error] 5-10: ./context.js import should occur before type import of @tanstack/query-core

(import/order)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/QueryClientProvider.ts` around lines 1 - 10, The
import ordering violates the lint rule because a type-only import (QueryClient
from '@tanstack/query-core') appears before value imports from 'lit' and
'./context.js'; reorder the import statements in QueryClientProvider.ts so that
value imports (ContextProvider from '@lit/context', LitElement, html,
TemplateResult from 'lit', and the named exports createMissingQueryClientError,
queryClientContext, registerDefaultQueryClient, unregisterDefaultQueryClient
from './context.js') come first, and then place the type-only import of
QueryClient after them to satisfy import/order.

Comment on lines +47 to +53
const nextClient = this.client
if (!nextClient) {
if (this.isConnected) {
throw createMissingQueryClientError()
}

return
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Tear down the previous client before throwing on client = undefined.

Line 50 throws before any cleanup. If a connected provider had already mounted A and client is later cleared, A stays mounted and registered as the default client until disconnect, so the provider keeps stale subscriptions/state alive after the error.

🧹 Minimal cleanup before raising
     const nextClient = this.client
     if (!nextClient) {
       if (this.isConnected) {
+        this.unmountClient(this.mountedClient)
         throw createMissingQueryClientError()
       }

       return
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/QueryClientProvider.ts` around lines 47 - 53, When
this.client becomes undefined the code currently throws
createMissingQueryClientError() before cleaning up, which leaves the previously
mounted client/state registered; update the QueryClientProvider branch that
checks const nextClient = this.client and if (!nextClient) to first perform the
provider's teardown/cleanup logic for the previous client (unregister it as the
default client and clear subscriptions/state) and then throw
createMissingQueryClientError(); place the cleanup call(s) immediately before
the throw so symbols like client, isConnected and createMissingQueryClientError
remain as-is and only the order changes.

Comment on lines +1 to +7
import {
dataTagSymbol,
QueryClient,
type DefinedQueryObserverResult,
type QueryObserverResult,
} from '@tanstack/query-core'
import { describe, expectTypeOf, it } from 'vitest'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Split the value and type imports here.

This block currently fails import/consistent-type-specifier-style and sort-imports, so the new type test will not pass lint as written.

Suggested diff
-import {
-  dataTagSymbol,
-  QueryClient,
-  type DefinedQueryObserverResult,
-  type QueryObserverResult,
-} from '@tanstack/query-core'
+import { QueryClient, dataTagSymbol } from '@tanstack/query-core'
+import type {
+  DefinedQueryObserverResult,
+  QueryObserverResult,
+} from '@tanstack/query-core'
📝 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.

Suggested change
import {
dataTagSymbol,
QueryClient,
type DefinedQueryObserverResult,
type QueryObserverResult,
} from '@tanstack/query-core'
import { describe, expectTypeOf, it } from 'vitest'
import { QueryClient, dataTagSymbol } from '@tanstack/query-core'
import type {
DefinedQueryObserverResult,
QueryObserverResult,
} from '@tanstack/query-core'
import { describe, expectTypeOf, it } from 'vitest'
🧰 Tools
🪛 ESLint

[error] 3-3: Member 'QueryClient' of the import declaration should be sorted alphabetically.

(sort-imports)


[error] 4-4: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)


[error] 5-5: Prefer using a top-level type-only import instead of inline type specifiers.

(import/consistent-type-specifier-style)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/tests/type-inference.test.ts` around lines 1 - 7, The
import line mixes value and type specifiers causing lint failures; split into
two imports: one importing runtime values (dataTagSymbol, QueryClient) and a
separate type-only import for DefinedQueryObserverResult and QueryObserverResult
using the type keyword (e.g. import { type DefinedQueryObserverResult, type
QueryObserverResult } from '@tanstack/query-core'), and ensure the two import
statements are ordered/sorted according to project rules (values import first,
then type-only import).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (2)
packages/lit-query/examples/lit-query-ssr-app/scripts/run-script-with-public-origin.mjs (1)

37-38: ⚠️ Potential issue | 🟠 Major

Await 'close' instead of 'exit' to avoid hangs on spawn failure.

Node's child_process may not emit 'exit' if an 'error' occurs (e.g., spawn failure), so await once(child, 'exit') can hang. 'close' reliably fires after either 'exit' or 'error' and guarantees stdio is closed.

Proposed fix
-const [code] = await once(child, 'exit')
+const [code] = await once(child, 'close')
 process.exitCode = code ?? 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/lit-query/examples/lit-query-ssr-app/scripts/run-script-with-public-origin.mjs`
around lines 37 - 38, The code awaits once(child, 'exit') which can hang on
spawn failures; change the event awaited to 'close' instead so the promise
resolves after either 'exit' or 'error' and stdio is closed—update the call in
this file where once(child, 'exit') is used (referencing the once function and
the child variable) to await once(child, 'close') and then set process.exitCode
from the returned code value as before.
packages/lit-query/package.json (1)

92-100: ⚠️ Potential issue | 🟠 Major

Fix dependency duplication in lit-query to match the pattern used across all other framework adapters.

lit is declared as both a runtime dependency (^3.3.1) and a peer dependency (>=2.8.0 <4). Similarly, @tanstack/query-core appears in both dependencies (workspace:*) and peerDependencies (^5.0.0). This deviates from the consistent pattern across all sibling adapters: @tanstack/query-core should be a dependency only, and the framework (lit, react, preact, etc.) should be peer-only.

Having lit in both places forces consumers to bundle Lit at runtime, which breaks ReactiveElement instanceof checks, decorator metadata, and context propagation. Listing @tanstack/query-core as a peer also introduces unnecessary pnpm peer-resolution warnings.

Suggested change
   "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"
   },
   "devDependencies": {
+    "lit": "^3.3.1",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/package.json` around lines 92 - 100, In package.json for
the lit-query package, remove the duplicate entries so that `@tanstack/query-core`
exists only under "dependencies" (keep "workspace:*") and "lit" exists only
under "peerDependencies" (keep ">=2.8.0 <4"); specifically delete "lit":
"^3.3.1" from "dependencies" and delete "@tanstack/query-core": "^5.0.0" from
"peerDependencies" so the file follows the same adapter pattern as the other
framework adapters.
🧹 Nitpick comments (11)
packages/lit-query/package.json (1)

33-36: build:deps hardcodes a sibling-package build and bypasses the monorepo orchestrator.

"build:deps": "pnpm --dir ../query-core run build" reaches across packages by relative path and rebuilds query-core unconditionally on every build. In this repo, cross-package build ordering is normally handled by Nx (nx run-many / task graph), which is much faster (cache-aware) and won't break if the directory layout changes. Consider dropping build:deps from build and relying on the workspace task runner to ensure query-core is built first.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/package.json` around lines 33 - 36, The "build" script
currently invokes the sibling-targeted "build:deps" which hardcodes a relative
path to ../query-core; remove that cross-package invocation by editing the
"build" npm script to stop calling "build:deps" (e.g., change "build": "pnpm run
build:deps && pnpm run build:esm && pnpm run build:cjs" to only run the local
steps) and either delete or keep "build:deps" as a standalone script if needed,
relying on the monorepo/task runner (Nx) to orchestrate cross-package builds
instead of pnpm --dir ../query-core; ensure references to "build:deps" in
package.json are removed so local "build" no longer rebuilds query-core via a
relative path.
packages/lit-query/examples/lit-query-ssr-app/server/index.mjs (2)

188-224: No timeout around the SSR prefetch.

queryClient.prefetchQuery(...) inherits retry: false but has no upstream deadline. If the upstream /api/data endpoint (or SSR_PUBLIC_ORIGIN) hangs, this request will stall indefinitely, occupying a Node socket and eventually hitting the client's own timeout with no 500 fallback. Since this is an example/test server used by E2E, an AbortSignal.timeout(...) (or similar) around the prefetch would make hangs observable and still trigger the 500 branch in the route handler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-ssr-app/server/index.mjs` around lines
188 - 224, Add a bounded deadline around the SSR prefetch in renderPage: create
an AbortSignal via AbortSignal.timeout(ms) (or equivalent) and pass it into
queryClient.prefetchQuery (alongside/createDataQueryOptions(SSR_PUBLIC_ORIGIN)
or merged into the options) so the upstream fetch to SSR_PUBLIC_ORIGIN can't
hang indefinitely; ensure you clean up/revoke the timeout if needed and let the
aborted/error case propagate so the existing error branch that returns a 500 is
exercised (i.e., throw or let the prefetch rejection surface when the signal
aborts).

48-57: Function name is misleading in a server-side context.

createBrowserQueryClient() is invoked only on the server (inside renderPage) to drive SSR prefetch + dehydration. The "browser" prefix reads as if this were the client-side instance and can confuse future readers. Consider renaming to createSsrQueryClient (or similar) for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-ssr-app/server/index.mjs` around lines
48 - 57, The function name createBrowserQueryClient is misleading for a
server-side SSR use; rename the function to createSsrQueryClient (or
createServerQueryClient) and update all references (e.g., where renderPage calls
createBrowserQueryClient) to the new name so the intent is clear; ensure the
exported symbol (if any) and any imports/uses are renamed consistently to avoid
breaking references and update any comments/docs accordingly.
packages/lit-query/examples/lit-query-ssr-app/e2e/lib.mjs (2)

163-198: No request timeout on the test-control endpoints.

resetTestState, getRequestCount, armNextDataError, and armNextDataDelay all do bare fetch(...) without an AbortSignal. If the SSR server hangs while serving /api/*, every E2E script that calls these helpers hangs until Playwright's outer timeout kicks in (or, for node scripts, indefinitely). A short AbortSignal.timeout(...) would fail fast with a clear message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-ssr-app/e2e/lib.mjs` around lines 163 -
198, The four test-control helpers (resetTestState, getRequestCount,
armNextDataError, armNextDataDelay) call fetch without an AbortSignal which lets
E2E scripts hang if the SSR server stalls; update each fetch call to pass a
short AbortSignal.timeout (e.g., const DEFAULT_TIMEOUT_MS = 5000) via the fetch
options' signal to fail fast and return a clear timeout error, and reuse that
constant across the functions so all requests (including the GET in
getRequestCount) time out consistently; ensure the signal is passed for both
POST and GET calls and that downstream code (e.g., awaiting response.json())
will receive the abort if the timeout fires.

267-326: runBuild/ensureServer leak on scenario-setup failure; also not parity with sibling harnesses.

Two small things:

  1. runBuild() and ensureServer() run outside the try/finally block (lines 274–278). If runBuild throws there's nothing to clean up, but if anything between ensureServer() returning and entering the try fails (extremely unlikely, but e.g. a synchronous throw from assigning let browser), the started server would leak. Moving them inside the try keeps stopServer(server) as the single cleanup path.
  2. This file hard-codes httpProbeTimeoutMs, serverReadyTimeoutMs, selectorTimeoutMs whereas the e2e-app/pagination-app harnesses expose PW_HTTP_PROBE_TIMEOUT_MS, PW_SERVER_READY_TIMEOUT_MS, PW_WAIT_FOR_TEXT_TIMEOUT_MS. Slow CI runners will hit these silently. Aligning the three modules (or extracting a shared one) would avoid the drift.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-ssr-app/e2e/lib.mjs` around lines 267 -
326, The setup leaks: move the runBuild() and ensureServer() calls into the try
block inside withBrowserPage so that stopServer(server) in the finally always
runs if either setup step or subsequent synchronous code throws; keep server
declared outside try so finally can reference it and ensure
captureFailureArtifacts(page) still runs on catches. Also stop hard-coding
probe/ready/selector timeouts in this module—replace the local
httpProbeTimeoutMs, serverReadyTimeoutMs, selectorTimeoutMs with the same
env-driven constants used by sibling harnesses (PW_HTTP_PROBE_TIMEOUT_MS,
PW_SERVER_READY_TIMEOUT_MS, PW_WAIT_FOR_TEXT_TIMEOUT_MS) or import a shared
timeout config so slow CI runners get the higher timeouts.
packages/lit-query/examples/lit-query-e2e-app/e2e/lib.mjs (2)

273-300: Prefer Playwright locator over page.evaluate for text polling.

The pagination example's twin helper (packages/lit-query/examples/lit-query-pagination-app/e2e/lib.mjs) already uses page.locator(selector).textContent({ timeout: 500 }) for the same job, which avoids round-tripping through evaluate, is auto-retriable, and won't throw on detached frames. Since the two modules are otherwise near-duplicates, it's worth aligning them so future maintainers only have one pattern to learn.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-e2e-app/e2e/lib.mjs` around lines 273 -
300, The waitForText function currently polls via page.evaluate which causes
unnecessary round-trips and can fail on detached frames; replace its polling
logic to use Playwright's Locator API (use page.locator(selector).textContent({
timeout: 500 }) inside the loop or use locator.waitFor with a short timeout) so
you read text via locator (auto-retriable and frame-safe) until the expectedText
is found or textWaitTimeoutMs elapses; update references to the function name
waitForText and keep the same timeout behavior but rely on locator.textContent({
timeout: 500 }) instead of page.evaluate.

1-313: Heavy duplication with the other two e2e/lib.mjs harnesses.

This file is ~95% identical to packages/lit-query/examples/lit-query-pagination-app/e2e/lib.mjs and partially overlaps packages/lit-query/examples/lit-query-ssr-app/e2e/lib.mjs (server spawn, strict-port detection, isServerReady, waitForServer, stopProcess/stopServer, artifact capture, withBrowserPage, readNumberFromTail). Any future fix (e.g., the ENOENT/timeout tweaks, Playwright API changes) has to be made in triplicate. Consider extracting a shared harness under packages/lit-query/examples/_shared/ (or similar) that the three example apps consume, keeping only app-specific knobs (ports, dev-script names, extra API wiring) in each example.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-e2e-app/e2e/lib.mjs` around lines 1 -
313, This file duplicates most of the E2E harness logic (startServer,
isServerReady, waitForServer, stopServer, captureFailureArtifacts,
withBrowserPage, readNumberFromTail, etc.); extract a shared harness module
under packages/lit-query/examples/_shared/ that exports the generic functions
(startServer, isServerReady, waitForServer, stopServer, captureFailureArtifacts,
withBrowserPage, waitForText, readNumberFromTail and helpers like
sanitizeForPath, readPositiveIntEnv) and update each example (this file and the
two other e2e/lib.mjs) to import the shared module and only define app-specific
knobs (host, port/DEMO_PORT, baseUrl, packageManagerCommand, projectDir,
allowExistingServer, artifactDir, timeouts) and any different dev-script args,
so fixes and API changes are made once in the shared harness while each example
keeps minimal per-app overrides.
packages/lit-query/examples/lit-query-pagination-app/e2e/lib.mjs (1)

298-329: waitForNthText duplicates waitForText.

These two functions differ only in page.locator(selector) vs page.locator(selector).nth(index). A single helper that accepts an optional index (or a pre-built locator) would halve the surface area and keep future changes (timeouts, retry cadence, error message format) in sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-pagination-app/e2e/lib.mjs` around
lines 298 - 329, Refactor the duplicated helpers waitForNthText and waitForText
into a single function (e.g., waitForTextWithIndex) that accepts either an
optional index parameter or a Locator parameter so it can use
page.locator(selector) or page.locator(selector).nth(index) without duplicating
logic; move the retry loop, timeout handling, text extraction and final error
formatting into that single function (keep the same default timeout and retry
cadence), replace existing calls to waitForText and waitForNthText to call the
new helper (or pass a pre-built Locator) and remove the old functions to keep
behavior and messages consistent.
packages/lit-query/src/createQueryController.ts (1)

234-261: Consider isolating the this.queryClient assignment out of defaultOptions.

defaultOptions mutates this.queryClient at line 248, which is a side effect hidden inside an options-resolution helper. It works today because every caller (refreshOptions, suspense, the constructor) already established this.queryClient via syncClient or the constructor path. Pulling that side effect out (and letting defaultOptions be a pure function of client/this.options) would make the controller easier to reason about and bring it in line with the sibling controllers (createInfiniteQueryController, createMutationController) whose defaultOptions is side-effect free.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/createQueryController.ts` around lines 234 - 261, The
defaultOptions method currently mutates this.queryClient (assignment at
defaultOptions) which hides side effects; make defaultOptions a pure function by
removing the assignment this.queryClient = resolvedClient and instead have
callers establish the client before calling defaultOptions: call
tryGetQueryClient() (or pass the resolved client) and assign it to
this.queryClient in the caller sites such as the constructor, refreshOptions,
and suspense (and any other places that call defaultOptions), mirroring the
side-effect-free behavior of createInfiniteQueryController and
createMutationController; ensure defaultOptions still throws
createMissingQueryClientError() if no client is provided/resolved but does not
modify object state.
packages/lit-query/src/createMutationController.ts (1)

168-179: Confirm mutate swallow is intended.

void this.observer.mutate(...).catch(() => {}) intentionally suppresses the rejection so mutate() is non-throwing, while error state is still surfaced via the observer subscription (and any per-call mutateOptions.onError). This mirrors React's useMutation.mutate. Worth a one-line code comment beyond the current "swallow" note documenting that error visibility flows through the subscription so future readers don't try to add re-throwing here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/createMutationController.ts` around lines 168 - 179,
In createMutationController.ts, update the mutate implementation (mutate method
calling this.observer.mutate) to keep the current swallow behavior but add a
single clarifying comment: state that rejections are intentionally not re-thrown
here because mutation errors are surfaced via the observer subscription and
per-call mutateOptions.onError (mirroring useMutation.mutate), so future readers
don't reintroduce a throw; leave the existing void
this.observer.mutate(...).catch(() => { /* ... */ }) behavior intact.
packages/lit-query/src/createInfiniteQueryController.ts (1)

240-260: Optional: keep syncClient consistent across controllers.

createQueryController.ts line 211 guards reuse with nextClient === this.queryClient && this.observer; this controller (and the mutation/queries controllers) only checks the client. In current flows queryClient and observer are always set/cleared together, so this isn't a bug, but adding the same && this.observer defensive check would make the four controllers' lifecycle paths uniform and easier to reason about during future edits.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/createInfiniteQueryController.ts` around lines 240 -
260, The syncClient method in createInfiniteQueryController currently returns
early when nextClient === this.queryClient but doesn't check that this.observer
is set; update the early-return guard to mirror other controllers by using
nextClient === this.queryClient && this.observer so we only skip
reinitialization when both the client and observer are present. Modify the
syncClient function (which calls tryGetQueryClient, unsubscribeObserver,
defaultOptions, new InfiniteQueryObserver, setResult, and
createPendingInfiniteQueryResult) to use this combined condition to keep
lifecycle behavior consistent across controllers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/lit-query/examples/lit-query-pagination-app/e2e/lib.mjs`:
- Around line 169-210: The current ensureServers() treats api/web readiness as a
single gate, which can return both as reused even if only one is up; change
ensureServers() to probe api and web independently using existingApiReady and
existingWebReady, and then only reuse the ones that are actually ready: if
existingApiReady is true set apiResult = undefined else startProcess('api',
['run','dev:api']) and waitForServer(...) for it; similarly for web with
startProcess('web', ['run','dev:web'], { VITE_PAGINATION_API_PORT:
String(API_PORT) }) and waitForServer(...); when allowExistingServer is false,
throw only if both exist? or follow current policy but ensure you never return {
api: undefined, web: undefined } unless both probes succeeded—so ensure the
function stops/cleans up via stopProcess(...) for any started process on errors
(use the same stopProcess and try/catch pattern already used for startProcess,
waitForServer, and stopProcess).

In `@packages/lit-query/examples/lit-query-pagination-app/src/api.ts`:
- Line 14: The property declaration uses the shorthand array type "projects:
Project[]" which triggers the `@typescript-eslint/array-type` rule; update the
declaration to use the generic form "Array<Project>" for the projects property
(the "projects" field in the relevant type/interface in src/api.ts) so it
conforms to the project's configured array-type style.

In `@packages/lit-query/examples/lit-query-ssr-app/e2e/lib.mjs`:
- Around line 250-265: The captureFailureArtifacts function can throw during
mkdir/screenshot/writeFile and mask the original test failure; wrap the
filesystem and Puppeteer calls inside a try/catch within captureFailureArtifacts
(around mkdir, page.screenshot, writeFile and the console.error lines) and on
error log a non-throwing warning (including the caught error) instead of
allowing the exception to propagate so the original scenario error remains the
one re-thrown by the outer catch; keep the existing behavior when page is falsy.

In `@packages/lit-query/examples/lit-query-ssr-app/server/index.mjs`:
- Around line 262-266: The POST handler that sets failNextDataRequest for
'/api/test/fail-next-data' is unguarded (see the branch that checks method ===
'POST' && requestUrl.pathname === '/api/test/fail-next-data' and sets
failNextDataRequest), so add a guard to prevent remote arming: require either
process.env.NODE_ENV !== 'production' or verify a shared secret header (e.g.,
check req.headers['x-test-secret'] === process.env.TEST_SECRET) before flipping
failNextDataRequest, and return a 403 if the check fails; ensure the guard is
applied before calling writeJson so the example route cannot be triggered in
production or by unauthenticated clients.
- Around line 152-164: The readTemplate function currently treats any
SystemError with a code property as "missing build"; change the error branch to
only handle file-not-found scenarios by checking error.code === 'ENOENT' (and
optionally error.code === 'ENOTDIR') before throwing the "Run pnpm run build"
Error, and rethrow all other errors unchanged; locate the logic inside
readTemplate that inspects error and narrow the condition to match only 'ENOENT'
(and 'ENOTDIR') so permissions/other FS errors propagate.
- Around line 22-25: The import of ../src/app.ts should use a .js specifier to
follow idiomatic ESM and match the rest of the file; update the import statement
that brings in getSsrQueryControllerCreationCount and
resetSsrQueryControllerCreationCount to reference ../src/app.js instead of
../src/app.ts so tooling can map the .ts source while the runtime uses .js
specifiers.

In `@packages/lit-query/package.json`:
- Around line 56-88: The three npm install scripts (example:install,
example:pagination:install, example:ssr:install) use "npm --prefix" which
creates package-lock.json and bypasses pnpm workspace behavior; update each
script to use "pnpm --dir <example-dir> install" to match the other scripts
(e.g., change the command for example:install, example:pagination:install, and
example:ssr:install to use pnpm --dir instead of npm --prefix).

In `@packages/lit-query/src/tests/mutation-controller.test.ts`:
- Line 18: Move the shared variable explicitMutationClient reset into an
afterEach hook to avoid test leakage: currently explicitMutationClient is
declared at top-level and only reset in-line, so if LC-MUT-02 fails earlier
subsequent tests that create ContextMutationHostElement can inherit the leaked
client and bypass provider resolution; add an afterEach(() => {
explicitMutationClient = undefined }) (or equivalent cleanup) in the
mutation-controller.test.ts test suite and ensure any existing inline resets
around lines 71-107 are removed/relocated to this afterEach to guarantee each
test starts with a clean explicitMutationClient.

In `@packages/lit-query/src/tests/queries-controller.test.ts`:
- Line 19: Move the cleanup of the top-level variable explicitQueriesClient into
an afterEach block to avoid leaking the explicit QueryClient between tests;
specifically, add or update an afterEach that sets explicitQueriesClient =
undefined (and any related teardown used for explicit client state) so tests
that instantiate ContextQueriesHostElement never inadvertently reuse a previous
explicit client; target the tests in queries-controller.test.ts that reference
explicitQueriesClient (lines around 144-192 and the declaration at the top) and
remove any existing resets from inside individual tests so the single afterEach
handles it.

---

Duplicate comments:
In
`@packages/lit-query/examples/lit-query-ssr-app/scripts/run-script-with-public-origin.mjs`:
- Around line 37-38: The code awaits once(child, 'exit') which can hang on spawn
failures; change the event awaited to 'close' instead so the promise resolves
after either 'exit' or 'error' and stdio is closed—update the call in this file
where once(child, 'exit') is used (referencing the once function and the child
variable) to await once(child, 'close') and then set process.exitCode from the
returned code value as before.

In `@packages/lit-query/package.json`:
- Around line 92-100: In package.json for the lit-query package, remove the
duplicate entries so that `@tanstack/query-core` exists only under "dependencies"
(keep "workspace:*") and "lit" exists only under "peerDependencies" (keep
">=2.8.0 <4"); specifically delete "lit": "^3.3.1" from "dependencies" and
delete "@tanstack/query-core": "^5.0.0" from "peerDependencies" so the file
follows the same adapter pattern as the other framework adapters.

---

Nitpick comments:
In `@packages/lit-query/examples/lit-query-e2e-app/e2e/lib.mjs`:
- Around line 273-300: The waitForText function currently polls via
page.evaluate which causes unnecessary round-trips and can fail on detached
frames; replace its polling logic to use Playwright's Locator API (use
page.locator(selector).textContent({ timeout: 500 }) inside the loop or use
locator.waitFor with a short timeout) so you read text via locator
(auto-retriable and frame-safe) until the expectedText is found or
textWaitTimeoutMs elapses; update references to the function name waitForText
and keep the same timeout behavior but rely on locator.textContent({ timeout:
500 }) instead of page.evaluate.
- Around line 1-313: This file duplicates most of the E2E harness logic
(startServer, isServerReady, waitForServer, stopServer, captureFailureArtifacts,
withBrowserPage, readNumberFromTail, etc.); extract a shared harness module
under packages/lit-query/examples/_shared/ that exports the generic functions
(startServer, isServerReady, waitForServer, stopServer, captureFailureArtifacts,
withBrowserPage, waitForText, readNumberFromTail and helpers like
sanitizeForPath, readPositiveIntEnv) and update each example (this file and the
two other e2e/lib.mjs) to import the shared module and only define app-specific
knobs (host, port/DEMO_PORT, baseUrl, packageManagerCommand, projectDir,
allowExistingServer, artifactDir, timeouts) and any different dev-script args,
so fixes and API changes are made once in the shared harness while each example
keeps minimal per-app overrides.

In `@packages/lit-query/examples/lit-query-pagination-app/e2e/lib.mjs`:
- Around line 298-329: Refactor the duplicated helpers waitForNthText and
waitForText into a single function (e.g., waitForTextWithIndex) that accepts
either an optional index parameter or a Locator parameter so it can use
page.locator(selector) or page.locator(selector).nth(index) without duplicating
logic; move the retry loop, timeout handling, text extraction and final error
formatting into that single function (keep the same default timeout and retry
cadence), replace existing calls to waitForText and waitForNthText to call the
new helper (or pass a pre-built Locator) and remove the old functions to keep
behavior and messages consistent.

In `@packages/lit-query/examples/lit-query-ssr-app/e2e/lib.mjs`:
- Around line 163-198: The four test-control helpers (resetTestState,
getRequestCount, armNextDataError, armNextDataDelay) call fetch without an
AbortSignal which lets E2E scripts hang if the SSR server stalls; update each
fetch call to pass a short AbortSignal.timeout (e.g., const DEFAULT_TIMEOUT_MS =
5000) via the fetch options' signal to fail fast and return a clear timeout
error, and reuse that constant across the functions so all requests (including
the GET in getRequestCount) time out consistently; ensure the signal is passed
for both POST and GET calls and that downstream code (e.g., awaiting
response.json()) will receive the abort if the timeout fires.
- Around line 267-326: The setup leaks: move the runBuild() and ensureServer()
calls into the try block inside withBrowserPage so that stopServer(server) in
the finally always runs if either setup step or subsequent synchronous code
throws; keep server declared outside try so finally can reference it and ensure
captureFailureArtifacts(page) still runs on catches. Also stop hard-coding
probe/ready/selector timeouts in this module—replace the local
httpProbeTimeoutMs, serverReadyTimeoutMs, selectorTimeoutMs with the same
env-driven constants used by sibling harnesses (PW_HTTP_PROBE_TIMEOUT_MS,
PW_SERVER_READY_TIMEOUT_MS, PW_WAIT_FOR_TEXT_TIMEOUT_MS) or import a shared
timeout config so slow CI runners get the higher timeouts.

In `@packages/lit-query/examples/lit-query-ssr-app/server/index.mjs`:
- Around line 188-224: Add a bounded deadline around the SSR prefetch in
renderPage: create an AbortSignal via AbortSignal.timeout(ms) (or equivalent)
and pass it into queryClient.prefetchQuery
(alongside/createDataQueryOptions(SSR_PUBLIC_ORIGIN) or merged into the options)
so the upstream fetch to SSR_PUBLIC_ORIGIN can't hang indefinitely; ensure you
clean up/revoke the timeout if needed and let the aborted/error case propagate
so the existing error branch that returns a 500 is exercised (i.e., throw or let
the prefetch rejection surface when the signal aborts).
- Around line 48-57: The function name createBrowserQueryClient is misleading
for a server-side SSR use; rename the function to createSsrQueryClient (or
createServerQueryClient) and update all references (e.g., where renderPage calls
createBrowserQueryClient) to the new name so the intent is clear; ensure the
exported symbol (if any) and any imports/uses are renamed consistently to avoid
breaking references and update any comments/docs accordingly.

In `@packages/lit-query/package.json`:
- Around line 33-36: The "build" script currently invokes the sibling-targeted
"build:deps" which hardcodes a relative path to ../query-core; remove that
cross-package invocation by editing the "build" npm script to stop calling
"build:deps" (e.g., change "build": "pnpm run build:deps && pnpm run build:esm
&& pnpm run build:cjs" to only run the local steps) and either delete or keep
"build:deps" as a standalone script if needed, relying on the monorepo/task
runner (Nx) to orchestrate cross-package builds instead of pnpm --dir
../query-core; ensure references to "build:deps" in package.json are removed so
local "build" no longer rebuilds query-core via a relative path.

In `@packages/lit-query/src/createInfiniteQueryController.ts`:
- Around line 240-260: The syncClient method in createInfiniteQueryController
currently returns early when nextClient === this.queryClient but doesn't check
that this.observer is set; update the early-return guard to mirror other
controllers by using nextClient === this.queryClient && this.observer so we only
skip reinitialization when both the client and observer are present. Modify the
syncClient function (which calls tryGetQueryClient, unsubscribeObserver,
defaultOptions, new InfiniteQueryObserver, setResult, and
createPendingInfiniteQueryResult) to use this combined condition to keep
lifecycle behavior consistent across controllers.

In `@packages/lit-query/src/createMutationController.ts`:
- Around line 168-179: In createMutationController.ts, update the mutate
implementation (mutate method calling this.observer.mutate) to keep the current
swallow behavior but add a single clarifying comment: state that rejections are
intentionally not re-thrown here because mutation errors are surfaced via the
observer subscription and per-call mutateOptions.onError (mirroring
useMutation.mutate), so future readers don't reintroduce a throw; leave the
existing void this.observer.mutate(...).catch(() => { /* ... */ }) behavior
intact.

In `@packages/lit-query/src/createQueryController.ts`:
- Around line 234-261: The defaultOptions method currently mutates
this.queryClient (assignment at defaultOptions) which hides side effects; make
defaultOptions a pure function by removing the assignment this.queryClient =
resolvedClient and instead have callers establish the client before calling
defaultOptions: call tryGetQueryClient() (or pass the resolved client) and
assign it to this.queryClient in the caller sites such as the constructor,
refreshOptions, and suspense (and any other places that call defaultOptions),
mirroring the side-effect-free behavior of createInfiniteQueryController and
createMutationController; ensure defaultOptions still throws
createMissingQueryClientError() if no client is provided/resolved but does not
modify object state.
🪄 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: 36bdf0ae-b6fa-4a43-8178-f697ee79031f

📥 Commits

Reviewing files that changed from the base of the PR and between a87a875 and 35e3ec0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • .changeset/lemon-memes-divide.md
  • .gitignore
  • knip.json
  • packages/lit-query/examples/lit-query-e2e-app/e2e/lib.mjs
  • packages/lit-query/examples/lit-query-e2e-app/package.json
  • packages/lit-query/examples/lit-query-pagination-app/e2e/lib.mjs
  • packages/lit-query/examples/lit-query-pagination-app/package.json
  • packages/lit-query/examples/lit-query-pagination-app/scripts/dev.mjs
  • packages/lit-query/examples/lit-query-pagination-app/src/api.ts
  • packages/lit-query/examples/lit-query-ssr-app/e2e/lib.mjs
  • packages/lit-query/examples/lit-query-ssr-app/package.json
  • packages/lit-query/examples/lit-query-ssr-app/scripts/dev.mjs
  • packages/lit-query/examples/lit-query-ssr-app/scripts/run-script-with-public-origin.mjs
  • packages/lit-query/examples/lit-query-ssr-app/server/index.mjs
  • packages/lit-query/package.json
  • packages/lit-query/src/createInfiniteQueryController.ts
  • packages/lit-query/src/createMutationController.ts
  • packages/lit-query/src/createQueriesController.ts
  • packages/lit-query/src/createQueryController.ts
  • packages/lit-query/src/tests/infinite-and-options.test.ts
  • packages/lit-query/src/tests/mutation-controller.test.ts
  • packages/lit-query/src/tests/queries-controller.test.ts
  • packages/lit-query/src/tests/query-controller.test.ts
  • packages/lit-query/src/types.ts
✅ Files skipped from review due to trivial changes (8)
  • .changeset/lemon-memes-divide.md
  • knip.json
  • .gitignore
  • packages/lit-query/examples/lit-query-pagination-app/package.json
  • packages/lit-query/examples/lit-query-pagination-app/scripts/dev.mjs
  • packages/lit-query/examples/lit-query-ssr-app/package.json
  • packages/lit-query/examples/lit-query-e2e-app/package.json
  • packages/lit-query/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/lit-query/examples/lit-query-ssr-app/scripts/dev.mjs

Comment on lines +169 to +210
async function ensureServers() {
const existingApiReady = await isServerReady(`${apiUrl}/api/projects?page=1`)
const existingWebReady = await isServerReady(baseUrl)

if (existingApiReady || existingWebReady) {
if (!allowExistingServer) {
throw new Error(
`Refusing to reuse existing server(s) at ${baseUrl} / ${apiUrl}. Set PW_ALLOW_EXISTING_SERVER=true to allow reuse.`,
)
}

console.log(`Using existing servers at ${baseUrl} and ${apiUrl}`)
return { api: undefined, web: undefined }
}

const api = startProcess('api', ['run', 'dev:api'])

try {
await waitForServer(
`${apiUrl}/api/projects?page=1`,
api.child,
api.getStartupError,
)
} catch (error) {
await stopProcess(api)
throw error
}

const web = startProcess('web', ['run', 'dev:web'], {
VITE_PAGINATION_API_PORT: String(API_PORT),
})

try {
await waitForServer(baseUrl, web.child, web.getStartupError)
} catch (error) {
await stopProcess(web)
await stopProcess(api)
throw error
}

return { api, web }
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Asymmetric "existing server" detection can leave the harness in a half-booted state.

The readiness check treats the api-or-web pair as a single gate: if only one side responds, the logic either refuses outright (misleading: the user never asked to reuse the other half) or, when PW_ALLOW_EXISTING_SERVER=true, returns { api: undefined, web: undefined } and proceeds to the scenario with whichever side isn't actually up. Probing each independently and only starting the missing one (or at minimum requiring both to be ready for the reuse path) would make the behavior predictable. It would also avoid the API's request counter being bumped by the readiness probe against /api/projects?page=1 on a shared server between runs.

🛠 Sketch
-  const existingApiReady = await isServerReady(`${apiUrl}/api/projects?page=1`)
-  const existingWebReady = await isServerReady(baseUrl)
-
-  if (existingApiReady || existingWebReady) {
-    if (!allowExistingServer) {
-      throw new Error(
-        `Refusing to reuse existing server(s) at ${baseUrl} / ${apiUrl}. Set PW_ALLOW_EXISTING_SERVER=true to allow reuse.`,
-      )
-    }
-
-    console.log(`Using existing servers at ${baseUrl} and ${apiUrl}`)
-    return { api: undefined, web: undefined }
-  }
+  const existingApiReady = await isServerReady(`${apiUrl}/api/projects?page=1`)
+  const existingWebReady = await isServerReady(baseUrl)
+
+  if (existingApiReady && existingWebReady) {
+    if (!allowExistingServer) {
+      throw new Error(
+        `Refusing to reuse existing servers at ${baseUrl} / ${apiUrl}. Set PW_ALLOW_EXISTING_SERVER=true to allow reuse.`,
+      )
+    }
+    console.log(`Using existing servers at ${baseUrl} and ${apiUrl}`)
+    return { api: undefined, web: undefined }
+  }
+
+  if (existingApiReady !== existingWebReady) {
+    throw new Error(
+      `Only one of api (${apiUrl}) / web (${baseUrl}) is already running; refusing to start a half-booted stack.`,
+    )
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-pagination-app/e2e/lib.mjs` around
lines 169 - 210, The current ensureServers() treats api/web readiness as a
single gate, which can return both as reused even if only one is up; change
ensureServers() to probe api and web independently using existingApiReady and
existingWebReady, and then only reuse the ones that are actually ready: if
existingApiReady is true set apiResult = undefined else startProcess('api',
['run','dev:api']) and waitForServer(...) for it; similarly for web with
startProcess('web', ['run','dev:web'], { VITE_PAGINATION_API_PORT:
String(API_PORT) }) and waitForServer(...); when allowExistingServer is false,
throw only if both exist? or follow current policy but ensure you never return {
api: undefined, web: undefined } unless both probes succeeded—so ensure the
function stops/cleans up via stopProcess(...) for any started process on errors
(use the same stopProcess and try/catch pattern already used for startProcess,
waitForServer, and stopProcess).

totalPages: number
totalProjects: number
hasMore: boolean
projects: Project[]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use Array<Project> to satisfy ESLint @typescript-eslint/array-type.

Static analysis flags Project[] here; align with the project's configured array-type style.

✏️ Proposed fix
-  projects: Project[]
+  projects: Array<Project>
📝 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.

Suggested change
projects: Project[]
projects: Array<Project>
🧰 Tools
🪛 ESLint

[error] 14-14: Array type using 'Project[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-pagination-app/src/api.ts` at line 14,
The property declaration uses the shorthand array type "projects: Project[]"
which triggers the `@typescript-eslint/array-type` rule; update the declaration to
use the generic form "Array<Project>" for the projects property (the "projects"
field in the relevant type/interface in src/api.ts) so it conforms to the
project's configured array-type style.

Comment on lines +250 to +265
async function captureFailureArtifacts(page) {
if (!page) {
return
}

const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const screenshotPath = join(artifactDir, `${timestamp}-ssr-e2e.png`)
const htmlPath = join(artifactDir, `${timestamp}-ssr-e2e.html`)

await mkdir(artifactDir, { recursive: true })
await page.screenshot({ path: screenshotPath, fullPage: true })
await writeFile(htmlPath, await page.content(), 'utf8')

console.error(`Saved failure screenshot: ${screenshotPath}`)
console.error(`Saved failure page HTML: ${htmlPath}`)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Artifact capture can throw and shadow the real failure.

Unlike the other two harnesses, captureFailureArtifacts here doesn't guard mkdir/screenshot/writeFile. If any of those reject (e.g., disk full, page already closed), the error from the scenario is lost because the catch block at lines 320–322 awaits this function before re-throwing the original error.

🛠 Proposed fix
 async function captureFailureArtifacts(page) {
   if (!page) {
     return
   }

   const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
   const screenshotPath = join(artifactDir, `${timestamp}-ssr-e2e.png`)
   const htmlPath = join(artifactDir, `${timestamp}-ssr-e2e.html`)

-  await mkdir(artifactDir, { recursive: true })
-  await page.screenshot({ path: screenshotPath, fullPage: true })
-  await writeFile(htmlPath, await page.content(), 'utf8')
-
-  console.error(`Saved failure screenshot: ${screenshotPath}`)
-  console.error(`Saved failure page HTML: ${htmlPath}`)
+  try {
+    await mkdir(artifactDir, { recursive: true })
+    await page.screenshot({ path: screenshotPath, fullPage: true })
+    await writeFile(htmlPath, await page.content(), 'utf8')
+    console.error(`Saved failure screenshot: ${screenshotPath}`)
+    console.error(`Saved failure page HTML: ${htmlPath}`)
+  } catch (artifactError) {
+    console.error('Failed to capture failure artifacts:', artifactError)
+  }
 }
📝 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.

Suggested change
async function captureFailureArtifacts(page) {
if (!page) {
return
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const screenshotPath = join(artifactDir, `${timestamp}-ssr-e2e.png`)
const htmlPath = join(artifactDir, `${timestamp}-ssr-e2e.html`)
await mkdir(artifactDir, { recursive: true })
await page.screenshot({ path: screenshotPath, fullPage: true })
await writeFile(htmlPath, await page.content(), 'utf8')
console.error(`Saved failure screenshot: ${screenshotPath}`)
console.error(`Saved failure page HTML: ${htmlPath}`)
}
async function captureFailureArtifacts(page) {
if (!page) {
return
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
const screenshotPath = join(artifactDir, `${timestamp}-ssr-e2e.png`)
const htmlPath = join(artifactDir, `${timestamp}-ssr-e2e.html`)
try {
await mkdir(artifactDir, { recursive: true })
await page.screenshot({ path: screenshotPath, fullPage: true })
await writeFile(htmlPath, await page.content(), 'utf8')
console.error(`Saved failure screenshot: ${screenshotPath}`)
console.error(`Saved failure page HTML: ${htmlPath}`)
} catch (artifactError) {
console.error('Failed to capture failure artifacts:', artifactError)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-ssr-app/e2e/lib.mjs` around lines 250 -
265, The captureFailureArtifacts function can throw during
mkdir/screenshot/writeFile and mask the original test failure; wrap the
filesystem and Puppeteer calls inside a try/catch within captureFailureArtifacts
(around mkdir, page.screenshot, writeFile and the console.error lines) and on
error log a non-throwing warning (including the caught error) instead of
allowing the exception to propagate so the original scenario error remains the
one re-thrown by the outer catch; keep the existing behavior when page is falsy.

Comment on lines +22 to +25
import {
getSsrQueryControllerCreationCount,
resetSsrQueryControllerCreationCount,
} from '../src/app.ts'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how this server is invoked and whether a TS loader is wired up.
fd -H -t f 'package.json' packages/lit-query/examples/lit-query-ssr-app | xargs -I{} sh -c 'echo "=== {} ==="; cat {}'
echo
echo "=== Searching for loader flags / tsx usage ==="
rg -nP -C2 '(tsx|ts-node|--loader|--experimental-(strip|transform)-types|--import)' packages/lit-query/examples/lit-query-ssr-app
echo
echo "=== Does src/app.ts exist and is it referenced via .js elsewhere? ==="
fd -H 'app.(ts|mts|js|mjs)' packages/lit-query/examples/lit-query-ssr-app/src
rg -nP "from '(\.\./)+src/app\.(ts|js|mjs)'" packages/lit-query/examples/lit-query-ssr-app

Repository: TanStack/query

Length of output: 2422


🏁 Script executed:

#!/bin/bash
# Check vite configuration
fd -H -t f 'vite.config' packages/lit-query/examples/lit-query-ssr-app | head -5
echo
# Look at the full vite config
cat packages/lit-query/examples/lit-query-ssr-app/vite.config.ts 2>/dev/null || cat packages/lit-query/examples/lit-query-ssr-app/vite.config.js 2>/dev/null
echo
# Check e2e and other scripts to see if server is launched differently
echo "=== Checking e2e scripts for server invocation ==="
fd -H -t f '\.mjs$|\.ts$' packages/lit-query/examples/lit-query-ssr-app/e2e packages/lit-query/examples/lit-query-ssr-app/scripts | head -10
echo
rg -n 'server|localhost|import.*index.mjs' packages/lit-query/examples/lit-query-ssr-app/e2e packages/lit-query/examples/lit-query-ssr-app/scripts

Repository: TanStack/query

Length of output: 3652


🏁 Script executed:

#!/bin/bash
# Look at server/index.mjs to see all imports
echo "=== server/index.mjs imports ==="
head -30 packages/lit-query/examples/lit-query-ssr-app/server/index.mjs

echo
echo "=== Check if app.ts file exists and what it exports ==="
head -20 packages/lit-query/examples/lit-query-ssr-app/src/app.ts

echo
echo "=== Search for any other server entry points ==="
fd -H -t f 'index\.(mjs|js|ts)' packages/lit-query/examples/lit-query-ssr-app/server

echo
echo "=== Verify .js imports are actually used ==="
rg -n "from '\.\./src" packages/lit-query/examples/lit-query-ssr-app/server/index.mjs

Repository: TanStack/query

Length of output: 1852


Use .js extension for TypeScript imports to maintain idiomatic ESM pattern and consistency.

While tsx is configured for dev:server, the import of ../src/app.ts is inconsistent with the rest of the file which uses ../src/api.js. Node's ESM standard is to use .js specifiers for all imports (even for .ts sources), letting the build tools handle the mapping. This makes the code more portable and aligns with the pattern already used for api.js. Change to '../src/app.js'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-ssr-app/server/index.mjs` around lines
22 - 25, The import of ../src/app.ts should use a .js specifier to follow
idiomatic ESM and match the rest of the file; update the import statement that
brings in getSsrQueryControllerCreationCount and
resetSsrQueryControllerCreationCount to reference ../src/app.js instead of
../src/app.ts so tooling can map the .ts source while the runtime uses .js
specifiers.

Comment on lines +152 to +164
async function readTemplate() {
try {
return await readFile(templatePath, 'utf8')
} catch (error) {
if (error && typeof error === 'object' && 'code' in error) {
throw new Error(
'Missing built client assets. Run "pnpm run build" in examples/lit-query-ssr-app first.',
)
}

throw error
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

readTemplate masks non-ENOENT errors behind the "missing build" message.

The 'code' in error guard matches any SystemError (e.g. EACCES, EISDIR, EMFILE), not just "file not found". A real permissions or FS error while reading an existing dist/index.html will be reported as "Run pnpm run build first", which is misleading. Narrow the branch to ENOENT (and ideally ENOTDIR) so other errors propagate unchanged.

🛠 Proposed fix
   } catch (error) {
-    if (error && typeof error === 'object' && 'code' in error) {
-      throw new Error(
-        'Missing built client assets. Run "pnpm run build" in examples/lit-query-ssr-app first.',
-      )
-    }
-
-    throw error
+    if (
+      error &&
+      typeof error === 'object' &&
+      'code' in error &&
+      (error.code === 'ENOENT' || error.code === 'ENOTDIR')
+    ) {
+      throw new Error(
+        'Missing built client assets. Run "pnpm run build" in examples/lit-query-ssr-app first.',
+      )
+    }
+
+    throw error
   }
📝 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.

Suggested change
async function readTemplate() {
try {
return await readFile(templatePath, 'utf8')
} catch (error) {
if (error && typeof error === 'object' && 'code' in error) {
throw new Error(
'Missing built client assets. Run "pnpm run build" in examples/lit-query-ssr-app first.',
)
}
throw error
}
}
async function readTemplate() {
try {
return await readFile(templatePath, 'utf8')
} catch (error) {
if (
error &&
typeof error === 'object' &&
'code' in error &&
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
) {
throw new Error(
'Missing built client assets. Run "pnpm run build" in examples/lit-query-ssr-app first.',
)
}
throw error
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-ssr-app/server/index.mjs` around lines
152 - 164, The readTemplate function currently treats any SystemError with a
code property as "missing build"; change the error branch to only handle
file-not-found scenarios by checking error.code === 'ENOENT' (and optionally
error.code === 'ENOTDIR') before throwing the "Run pnpm run build" Error, and
rethrow all other errors unchanged; locate the logic inside readTemplate that
inspects error and narrow the condition to match only 'ENOENT' (and 'ENOTDIR')
so permissions/other FS errors propagate.

Comment on lines +262 to +266
if (method === 'POST' && requestUrl.pathname === '/api/test/fail-next-data') {
failNextDataRequest = true
writeJson(res, 200, { ok: true })
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

/api/test/fail-next-data has no arming guard.

Anyone reaching this server (including the SSR page itself if something hit /api/test/* by accident) can arm the next failure, and CORS is wide open (access-control-allow-origin: *). For an in-repo example/test server this is acceptable, but please keep these routes clearly scoped to local E2E use and consider gating them on process.env.NODE_ENV !== 'production' or a shared secret header so the example isn't ever deployed as-is with remotely writable test hooks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/examples/lit-query-ssr-app/server/index.mjs` around lines
262 - 266, The POST handler that sets failNextDataRequest for
'/api/test/fail-next-data' is unguarded (see the branch that checks method ===
'POST' && requestUrl.pathname === '/api/test/fail-next-data' and sets
failNextDataRequest), so add a guard to prevent remote arming: require either
process.env.NODE_ENV !== 'production' or verify a shared secret header (e.g.,
check req.headers['x-test-secret'] === process.env.TEST_SECRET) before flipping
failNextDataRequest, and return a 403 if the check fails; ensure the guard is
applied before calling writeJson so the example route cannot be triggered in
production or by unauthenticated clients.

Comment on lines +56 to +88
"example:install": "npm --prefix examples/lit-query-e2e-app install",
"example:dev": "pnpm --dir examples/lit-query-e2e-app run dev",
"example:e2e": "pnpm run build && pnpm --dir examples/lit-query-e2e-app run e2e",
"example:e2e:headed": "pnpm run build && pnpm --dir examples/lit-query-e2e-app run e2e:headed",
"example:e2e:query-error": "pnpm run build && pnpm --dir examples/lit-query-e2e-app run e2e:query-error",
"example:e2e:mutation-error": "pnpm run build && pnpm --dir examples/lit-query-e2e-app run e2e:mutation-error",
"example:e2e:refetch-button": "pnpm run build && pnpm --dir examples/lit-query-e2e-app run e2e:refetch-button",
"example:e2e:lifecycle-reconnect": "pnpm run build && pnpm --dir examples/lit-query-e2e-app run e2e:lifecycle-reconnect",
"example:e2e:lifecycle-contract": "pnpm run build && pnpm --dir examples/lit-query-e2e-app run e2e:lifecycle-contract",
"example:e2e:all": "pnpm run build && pnpm --dir examples/lit-query-e2e-app run e2e:all",
"example:e2e:all:nobuild": "pnpm --dir examples/lit-query-e2e-app run e2e:all",
"example:e2e:all:headed": "pnpm run build && pnpm --dir examples/lit-query-e2e-app run e2e:all:headed",
"example:pagination:install": "npm --prefix examples/lit-query-pagination-app install",
"example:pagination:dev": "pnpm --dir examples/lit-query-pagination-app run dev",
"example:pagination:e2e": "pnpm run build && pnpm --dir examples/lit-query-pagination-app run e2e",
"example:pagination:e2e:prefetch": "pnpm run build && pnpm --dir examples/lit-query-pagination-app run e2e:prefetch",
"example:pagination:e2e:error": "pnpm run build && pnpm --dir examples/lit-query-pagination-app run e2e:error",
"example:pagination:e2e:mutations": "pnpm run build && pnpm --dir examples/lit-query-pagination-app run e2e:mutations",
"example:pagination:e2e:boundary": "pnpm run build && pnpm --dir examples/lit-query-pagination-app run e2e:boundary",
"example:pagination:e2e:all": "pnpm run build && pnpm --dir examples/lit-query-pagination-app run e2e:all",
"example:pagination:e2e:all:headed": "pnpm run build && pnpm --dir examples/lit-query-pagination-app run e2e:all:headed",
"example:ssr:install": "npm --prefix examples/lit-query-ssr-app install",
"example:ssr:dev": "pnpm run build && pnpm --dir examples/lit-query-ssr-app run dev",
"example:ssr:dev:public-origin": "pnpm run build && pnpm --dir examples/lit-query-ssr-app run dev:public-origin",
"example:ssr:e2e:default": "pnpm run build && pnpm --dir examples/lit-query-ssr-app run e2e",
"example:ssr:e2e:error": "pnpm run build && pnpm --dir examples/lit-query-ssr-app run e2e:error",
"example:ssr:e2e:refreshing": "pnpm run build && pnpm --dir examples/lit-query-ssr-app run e2e:refreshing",
"example:ssr:e2e:public-origin": "pnpm run build && pnpm --dir examples/lit-query-ssr-app run e2e:public-origin",
"example:ssr:e2e": "pnpm run example:ssr:e2e:default && pnpm run example:ssr:e2e:public-origin",
"example:ssr:e2e:all:default": "pnpm run build && pnpm --dir examples/lit-query-ssr-app run e2e:all",
"example:ssr:e2e:all:nobuild": "pnpm --dir examples/lit-query-ssr-app run e2e:all && pnpm --dir examples/lit-query-ssr-app run e2e:public-origin",
"example:ssr:e2e:all": "pnpm run example:ssr:e2e:all:default && pnpm run example:ssr:e2e:public-origin",
"demo:setup": "pnpm run example:install && npm --prefix examples/lit-query-e2e-app exec playwright install chromium",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the example apps are part of the pnpm workspace and how they reference lit-query.
fd -t f 'pnpm-workspace.yaml' --max-depth 2
fd -t f '^package.json$' packages/lit-query/examples | xargs -I{} sh -c 'echo "=== {} ==="; jq "{name, dependencies, devDependencies}" {}'

Repository: TanStack/query

Length of output: 1178


Avoid npm --prefix for example installs inside a pnpm workspace.

Using npm --prefix examples/... creates package-lock.json files in the pnpm-managed workspace and bypasses workspace features. The other scripts consistently use pnpm --dir examples/..., so replace the three npm --prefix install commands (example:install, example:pagination:install, example:ssr:install) with pnpm --dir examples/... install for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/package.json` around lines 56 - 88, The three npm install
scripts (example:install, example:pagination:install, example:ssr:install) use
"npm --prefix" which creates package-lock.json and bypasses pnpm workspace
behavior; update each script to use "pnpm --dir <example-dir> install" to match
the other scripts (e.g., change the command for example:install,
example:pagination:install, and example:ssr:install to use pnpm --dir instead of
npm --prefix).

customElements.define(providerTagName, QueryClientProvider)
}

let explicitMutationClient: QueryClient | undefined
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Move explicitMutationClient reset into afterEach to avoid shared-state leakage.

If LC-MUT-02 fails before line 105, subsequent tests that construct ContextMutationHostElement will inadvertently bind to the leaked explicitMutationClient and bypass provider context resolution, masking real failures. Same hardening pattern noted previously for infinite-and-options.test.ts.

🧪 Suggested hardening
-import { describe, expect, it } from 'vitest'
+import { afterEach, describe, expect, it } from 'vitest'
@@
 let explicitMutationClient: QueryClient | undefined
+
+afterEach(() => {
+  explicitMutationClient = undefined
+})

Also applies to: 71-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lit-query/src/tests/mutation-controller.test.ts` at line 18, Move
the shared variable explicitMutationClient reset into an afterEach hook to avoid
test leakage: currently explicitMutationClient is declared at top-level and only
reset in-line, so if LC-MUT-02 fails earlier subsequent tests that create
ContextMutationHostElement can inherit the leaked client and bypass provider
resolution; add an afterEach(() => { explicitMutationClient = undefined }) (or
equivalent cleanup) in the mutation-controller.test.ts test suite and ensure any
existing inline resets around lines 71-107 are removed/relocated to this
afterEach to guarantee each test starts with a clean explicitMutationClient.

customElements.define(providerTagName, QueryClientProvider)
}

let explicitQueriesClient: QueryClient | undefined
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Move explicitQueriesClient reset into afterEach.

Same risk pattern as in mutation-controller.test.ts and the previously flagged infinite-and-options.test.ts: if LC-QUERIES-02 fails before line 190, later tests using ContextQueriesHostElement silently bind to the leaked explicit client and skip provider-context coverage.

🧪 Suggested hardening
-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 the current code and only fix it if needed.

In `@packages/lit-query/src/tests/queries-controller.test.ts` at line 19, Move the
cleanup of the top-level variable explicitQueriesClient into an afterEach block
to avoid leaking the explicit QueryClient between tests; specifically, add or
update an afterEach that sets explicitQueriesClient = undefined (and any related
teardown used for explicit client state) so tests that instantiate
ContextQueriesHostElement never inadvertently reuse a previous explicit client;
target the tests in queries-controller.test.ts that reference
explicitQueriesClient (lines around 144-192 and the declaration at the top) and
remove any existing resets from inside individual tests so the single afterEach
handles it.

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.

1 participant