diff --git a/docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md b/docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md index 0784526cb57..521e2bc2bca 100644 --- a/docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md +++ b/docs/framework/angular/angular-httpclient-and-other-data-fetching-clients.md @@ -12,7 +12,7 @@ Because TanStack Query's fetching mechanisms are agnostically built on Promises, - Mock responses in unit tests using [provideHttpClientTesting](https://angular.dev/guide/http/testing). - [Interceptors](https://angular.dev/guide/http/interceptors) can be used for a wide range of functionality including adding authentication headers, performing logging, etc. While some data fetching libraries have their own interceptor system, `HttpClient` interceptors are integrated with Angular's dependency injection system. - `HttpClient` automatically informs [`PendingTasks`](https://angular.dev/api/core/PendingTasks#), which enables Angular to be aware of pending requests. Unit tests and SSR can use the resulting application _stableness_ information to wait for pending requests to finish. This makes unit testing much easier for [Zoneless](https://angular.dev/guide/zoneless) applications. -- When using SSR, `HttpClient` will [cache requests](https://angular.dev/guide/ssr#caching-data-when-using-HttpClient) performed on the server. This will prevent unneeded requests on the client. `HttpClient` SSR caching works out of the box. TanStack Query has its own hydration functionality which may be more powerful but requires some setup. Which one fits your needs best depends on your use case. +- When using SSR, `HttpClient` will [cache requests](https://angular.dev/guide/ssr#caching-data-when-using-HttpClient) performed on the server. This will prevent unneeded requests on the client. TanStack Query also supports built-in SSR dehydration and hydration through `provideTanStackQuery`, `TransferState`, and features such as `withHydrationKey` and `withNoQueryHydration`. Which one fits your needs best depends on your use case. ### Using observables in `queryFn` diff --git a/docs/framework/angular/devtools.md b/docs/framework/angular/devtools.md index c0cfb9141c0..938d6cad129 100644 --- a/docs/framework/angular/devtools.md +++ b/docs/framework/angular/devtools.md @@ -11,9 +11,15 @@ title: Devtools ## Enable devtools +Add the devtools package (in addition to `@tanstack/angular-query-experimental`): + +```bash +npm install @tanstack/angular-query-devtools +``` + The devtools help you debug and inspect your queries and mutations. You can enable the devtools by adding `withDevtools` to `provideTanStackQuery`. -By default, Angular Query Devtools are only included in development mode bundles, so you don't need to worry about excluding them during a production build. +By default, Angular Query Devtools only load in development. ```ts import { @@ -21,7 +27,7 @@ import { provideTanStackQuery, } from '@tanstack/angular-query-experimental' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' export const appConfig: ApplicationConfig = { providers: [provideTanStackQuery(new QueryClient(), withDevtools())], @@ -30,28 +36,28 @@ export const appConfig: ApplicationConfig = { ## Devtools in production -Devtools are automatically excluded from production builds. However, it might be desirable to lazy load the devtools in production. - -To use `withDevtools` in production builds, import using the `production` sub-path. The function exported from the production subpath is identical to the main one, but won't be excluded from production builds. +If you need the real implementation in production, import from the `production` entrypoint. ```ts -import { withDevtools } from '@tanstack/angular-query-experimental/devtools/production' +import { withDevtools } from '@tanstack/angular-query-devtools/production' ``` -To control when devtools are loaded, you can use the `loadDevtools` option. +To control when devtools are loaded, use the `loadDevtools` option. -When not setting the option or setting it to 'auto', the devtools will be loaded automatically only when Angular runs in development mode. +When omitted or set to `'auto'`, devtools only load in development mode. ```ts -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' -provideTanStackQuery(new QueryClient(), withDevtools()) +providers: [provideTanStackQuery(new QueryClient(), withDevtools())] // which is equivalent to -provideTanStackQuery( - new QueryClient(), - withDevtools(() => ({ loadDevtools: 'auto' })), -) +providers: [ + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ loadDevtools: 'auto' })), + ), +] ``` When setting the option to true, the devtools will be loaded in both development and production mode. @@ -61,29 +67,30 @@ This is useful if you want to load devtools based on [Angular environment config ```ts import { environment } from './environments/environment' // Make sure to use the production sub-path to load devtools in production builds -import { withDevtools } from '@tanstack/angular-query-experimental/devtools/production' - -provideTanStackQuery( - new QueryClient(), - withDevtools(() => ({ loadDevtools: environment.loadDevtools })), -) +import { withDevtools } from '@tanstack/angular-query-devtools/production' + +providers: [ + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ loadDevtools: environment.loadDevtools })), + ), +] ``` When setting the option to false, the devtools will not be loaded. ```ts -provideTanStackQuery( - new QueryClient(), - withDevtools(() => ({ loadDevtools: false })), -) +providers: [ + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ loadDevtools: false })), + ), +] ``` ## Derive options through reactivity -Options are passed to `withDevtools` from a callback function to support reactivity through signals. In the following example -a signal is created from a RxJS observable that emits on a keyboard shortcut. When the derived signal is set to true, the devtools are lazily loaded. - -The example below always loads devtools in development mode and loads on-demand in production mode when a keyboard shortcut is pressed. +Options can be returned from a callback so they can react to signals. For example, a signal derived from a keyboard shortcut can enable devtools on demand: ```ts import { Injectable, isDevMode } from '@angular/core' @@ -107,14 +114,12 @@ export class DevtoolsOptionsManager { } ``` -If you want to use an injectable such as a service in the callback you can use `deps`. The injected value will be passed as parameter to the callback function. - -This is similar to `deps` in Angular's [`useFactory`](https://angular.dev/guide/di/dependency-injection-providers#factory-providers-usefactory) provider. +To use an injectable such as a service in the callback, pass it through `deps`: ```ts // ... // 👇 Note we import from the production sub-path to enable devtools lazy loading in production builds -import { withDevtools } from '@tanstack/angular-query-experimental/devtools/production' +import { withDevtools } from '@tanstack/angular-query-devtools/production' export const appConfig: ApplicationConfig = { providers: [ @@ -126,7 +131,6 @@ export const appConfig: ApplicationConfig = { loadDevtools: devToolsOptionsManager.loadDevtools(), }), { - // `deps` is used to inject and pass `DevtoolsOptionsManager` to the `withDevtools` callback. deps: [DevtoolsOptionsManager], }, ), @@ -140,8 +144,8 @@ export const appConfig: ApplicationConfig = { Of these options `loadDevtools`, `client`, `position`, `errorTypes`, `buttonPosition`, and `initialIsOpen` support reactivity through signals. - `loadDevtools?: 'auto' | boolean` - - Defaults to `auto`: lazily loads devtools when in development mode. Skips loading in production mode. - - Use this to control if the devtools are loaded. + - Omit or `'auto'`: load devtools only in development mode. + - Use this to control whether devtools load when using the `/production` import. - `initialIsOpen?: Boolean` - Set this to `true` if you want the tools to default to being open - `buttonPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right" | "relative"` diff --git a/docs/framework/angular/guides/dependent-queries.md b/docs/framework/angular/guides/dependent-queries.md index 38afbd491f7..a1a58286120 100644 --- a/docs/framework/angular/guides/dependent-queries.md +++ b/docs/framework/angular/guides/dependent-queries.md @@ -27,7 +27,13 @@ projectsQuery = injectQuery(() => ({ [//]: # 'Example2' ```ts -// injectQueries is under development for Angular Query +projectsQueries = injectQueries(() => ({ + queries: + this.userQuery.data()?.projectIds.map((projectId) => ({ + queryKey: ['project', projectId], + queryFn: () => getProjectById(projectId), + })) ?? [], +})) ``` [//]: # 'Example2' diff --git a/docs/framework/angular/guides/parallel-queries.md b/docs/framework/angular/guides/parallel-queries.md index f88756a2a88..a7e7c9b6e4c 100644 --- a/docs/framework/angular/guides/parallel-queries.md +++ b/docs/framework/angular/guides/parallel-queries.md @@ -17,6 +17,9 @@ replace: [//]: # 'Example' ```ts +@Component({ + // ... +}) export class AppComponent { // The following queries will execute in parallel usersQuery = injectQuery(() => ({ queryKey: ['users'], queryFn: fetchUsers })) @@ -39,12 +42,14 @@ TanStack Query provides `injectQueries`, which you can use to dynamically execut [//]: # 'Example2' ```ts +@Component({ + // ... +}) export class AppComponent { users = signal>([]) - // Please note injectQueries is under development and this code does not work yet userQueries = injectQueries(() => ({ - queries: users().map((user) => { + queries: this.users().map((user) => { return { queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), diff --git a/docs/framework/angular/guides/ssr.md b/docs/framework/angular/guides/ssr.md new file mode 100644 index 00000000000..3c344d4cc68 --- /dev/null +++ b/docs/framework/angular/guides/ssr.md @@ -0,0 +1,100 @@ +--- +id: ssr +title: SSR +--- + +For [Angular SSR](https://angular.dev/guide/ssr), you can run queries on the server, embed the serialized cache in the HTML response, and hydrate the same data in the browser so the client does not refetch immediately. + +[`provideTanStackQuery`](../reference/functions/provideTanStackQuery.md) serializes the `QueryClient` cache during SSR and restores it when the browser app boots. This uses Angular's `TransferState` internally. + +An end-to-end sample lives at `examples/angular/ssr`. The `examples/angular/ssr-persist` example builds on the same setup with browser persistence. + +## Query client token + +For SSR, define an `InjectionToken` with a factory and provide that token to `provideTanStackQuery`. This keeps the docs and examples aligned with Angular's DI model and avoids helper-function-based query-client setup. + +```ts +import { InjectionToken } from '@angular/core' +import { QueryClient } from '@tanstack/angular-query-experimental' + +export const SHARED_QUERY_DEFAULTS = { + staleTime: 1000 * 30, + gcTime: 1000 * 60 * 60 * 24, +} as const + +export const QUERY_CLIENT = new InjectionToken('QUERY_CLIENT', { + factory: () => + new QueryClient({ + defaultOptions: { + queries: { + ...SHARED_QUERY_DEFAULTS, + }, + }, + }), +}) +``` + +## Browser config + +Use the token with `provideTanStackQuery` in your application config. If you want devtools, import them from the standalone devtools package. + +```ts +import type { ApplicationConfig } from '@angular/core' +import { provideHttpClient } from '@angular/common/http' +import { + provideClientHydration, + withEventReplay, +} from '@angular/platform-browser' +import { provideTanStackQuery } from '@tanstack/angular-query-experimental' +import { withDevtools } from '@tanstack/angular-query-devtools' +import { QUERY_CLIENT } from './query-client' + +export const appConfig: ApplicationConfig = { + providers: [ + provideHttpClient(), + provideClientHydration(withEventReplay()), + provideTanStackQuery(QUERY_CLIENT, withDevtools()), + ], +} +``` + +## Server config + +Each SSR request should bootstrap a fresh application, and Angular will resolve the token factory in that request-scoped injector. Merge your browser config with `provideServerRendering` in the server config. + +```ts +import { mergeApplicationConfig } from '@angular/core' +import { provideServerRendering, withRoutes } from '@angular/ssr' +import { appConfig } from './app.config' +import { serverRoutes } from './app.routes.server' + +export const serverConfig = mergeApplicationConfig(appConfig, { + providers: [provideServerRendering(withRoutes(serverRoutes))], +}) +``` + +## Multiple query clients + +Built-in hydration uses a default transfer key. For a second `QueryClient` in a child injector, pass a distinct key with `withHydrationKey` so each client's serialized cache stays separate. + +```ts +providers: [ + provideTanStackQuery( + SECONDARY_QUERY_CLIENT, + withHydrationKey('my-secondary-query-cache'), + ), +] +``` + +## Disabling built-in hydration + +If you need to opt out of TanStack Query's built-in `TransferState` integration for a specific injector, add `withNoQueryHydration()`. + +```ts +providers: [provideTanStackQuery(QUERY_CLIENT, withNoQueryHydration())] +``` + +## See also + +- [Angular HttpClient and data fetching](../angular-httpclient-and-other-data-fetching-clients.md) +- [Devtools](../devtools.md) diff --git a/docs/framework/angular/installation.md b/docs/framework/angular/installation.md index dffc092e7cd..954975f4b97 100644 --- a/docs/framework/angular/installation.md +++ b/docs/framework/angular/installation.md @@ -7,7 +7,7 @@ title: Installation ### NPM -_Angular Query is compatible with Angular v16 and higher_ +_Angular Query is compatible with Angular v19 and higher_ ```bash npm i @tanstack/angular-query-experimental @@ -31,4 +31,10 @@ or bun add @tanstack/angular-query-experimental ``` +If you want Angular Query devtools, install the standalone devtools package as well: + +```bash +npm i @tanstack/angular-query-devtools +``` + > Wanna give it a spin before you download? Try out the [simple](./examples/simple) or [basic](./examples/basic) examples! diff --git a/docs/framework/angular/overview.md b/docs/framework/angular/overview.md index 69ebf2eac3f..152eb085a7e 100644 --- a/docs/framework/angular/overview.md +++ b/docs/framework/angular/overview.md @@ -13,7 +13,7 @@ We are in the process of getting to a stable API for TanStack Query on Angular. ## Supported Angular Versions -TanStack Query is compatible with Angular v16 and higher. +TanStack Query is compatible with Angular v19 and higher. TanStack Query (FKA React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes **fetching, caching, synchronizing and updating server state** in your web applications a breeze. @@ -73,11 +73,10 @@ import { lastValueFrom } from 'rxjs' template: ` @if (query.isPending()) { Loading... - } - @if (query.error()) { + } @else if (query.isError()) { An error has occurred: {{ query.error().message }} - } - @if (query.data(); as data) { + } @else if (query.isSuccess()) { + @let data = query.data();

{{ data.name }}

{{ data.description }}

👀 {{ data.subscribers_count }} diff --git a/docs/framework/angular/quick-start.md b/docs/framework/angular/quick-start.md index 462ca0b17ac..f011935b46f 100644 --- a/docs/framework/angular/quick-start.md +++ b/docs/framework/angular/quick-start.md @@ -35,7 +35,7 @@ import { @NgModule({ declarations: [AppComponent], imports: [BrowserModule], - providers: [provideTanStackQuery(new QueryClient())], + providers: [provideTanStackQuery(new QueryClient()), provideHttpClient()], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/docs/framework/angular/reference/functions/injectInfiniteQuery.md b/docs/framework/angular/reference/functions/injectInfiniteQuery.md index 020ef039fa5..7bb0285b6ea 100644 --- a/docs/framework/angular/reference/functions/injectInfiniteQuery.md +++ b/docs/framework/angular/reference/functions/injectInfiniteQuery.md @@ -6,7 +6,7 @@ title: injectInfiniteQuery # Function: injectInfiniteQuery() Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. -Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" +Infinite queries can additively "load more" data onto an existing set of data or support infinite scroll. ## Param @@ -16,16 +16,17 @@ A function that returns infinite query options. Additional configuration. +## See + +https://tanstack.com/query/latest/docs/framework/angular/guides/infinite-queries + ## Call Signature ```ts function injectInfiniteQuery(injectInfiniteQueryFn, options?): DefinedCreateInfiniteQueryResult; ``` -Defined in: [inject-infinite-query.ts:41](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L41) - -Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. -Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" +Defined in: [inject-infinite-query.ts:36](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L36) ### Type Parameters @@ -55,30 +56,21 @@ Infinite queries can additively "load more" data onto an existing set of data or () => [`DefinedInitialDataInfiniteOptions`](../type-aliases/DefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> -A function that returns infinite query options. - #### options? [`InjectInfiniteQueryOptions`](../interfaces/InjectInfiniteQueryOptions.md) -Additional configuration. - ### Returns [`DefinedCreateInfiniteQueryResult`](../type-aliases/DefinedCreateInfiniteQueryResult.md)\<`TData`, `TError`\> -The infinite query result. - ## Call Signature ```ts function injectInfiniteQuery(injectInfiniteQueryFn, options?): CreateInfiniteQueryResult; ``` -Defined in: [inject-infinite-query.ts:65](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L65) - -Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. -Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" +Defined in: [inject-infinite-query.ts:53](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L53) ### Type Parameters @@ -108,30 +100,21 @@ Infinite queries can additively "load more" data onto an existing set of data or () => [`UndefinedInitialDataInfiniteOptions`](../type-aliases/UndefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> -A function that returns infinite query options. - #### options? [`InjectInfiniteQueryOptions`](../interfaces/InjectInfiniteQueryOptions.md) -Additional configuration. - ### Returns [`CreateInfiniteQueryResult`](../type-aliases/CreateInfiniteQueryResult.md)\<`TData`, `TError`\> -The infinite query result. - ## Call Signature ```ts function injectInfiniteQuery(injectInfiniteQueryFn, options?): CreateInfiniteQueryResult; ``` -Defined in: [inject-infinite-query.ts:89](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L89) - -Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. -Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" +Defined in: [inject-infinite-query.ts:70](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L70) ### Type Parameters @@ -161,16 +144,10 @@ Infinite queries can additively "load more" data onto an existing set of data or () => [`CreateInfiniteQueryOptions`](../interfaces/CreateInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> -A function that returns infinite query options. - #### options? [`InjectInfiniteQueryOptions`](../interfaces/InjectInfiniteQueryOptions.md) -Additional configuration. - ### Returns [`CreateInfiniteQueryResult`](../type-aliases/CreateInfiniteQueryResult.md)\<`TData`, `TError`\> - -The infinite query result. diff --git a/docs/framework/angular/reference/functions/injectMutation.md b/docs/framework/angular/reference/functions/injectMutation.md index 5b4690eb464..8b2adfd525c 100644 --- a/docs/framework/angular/reference/functions/injectMutation.md +++ b/docs/framework/angular/reference/functions/injectMutation.md @@ -9,7 +9,7 @@ title: injectMutation function injectMutation(injectMutationFn, options?): CreateMutationResult; ``` -Defined in: [inject-mutation.ts:45](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L45) +Defined in: [inject-mutation.ts:44](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L44) Injects a mutation: an imperative function that can be invoked which typically performs server side effects. diff --git a/docs/framework/angular/reference/functions/injectQueries.md b/docs/framework/angular/reference/functions/injectQueries.md new file mode 100644 index 00000000000..d05f95e3793 --- /dev/null +++ b/docs/framework/angular/reference/functions/injectQueries.md @@ -0,0 +1,40 @@ +--- +id: injectQueries +title: injectQueries +--- + +# Function: injectQueries() + +```ts +function injectQueries(optionsFn, injector?): Signal; +``` + +Defined in: [inject-queries.ts:278](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L278) + +## Type Parameters + +### T + +`T` *extends* `any`[] + +### TCombinedResult + +`TCombinedResult` = `T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<`Head`\>\[`"data"`\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<`Head`\>\[`"data"`\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>\>, `GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<`Head`\>\[`"data"`\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GenericGetDefinedOrUndefinedQueryResult`\<`Head`, ...\[...\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<..., ...\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<..., ...\>\>, `GenericGetDefinedOrUndefinedQueryResult`\<`Head`, ...\[...\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<..., ...\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<..., ...\>\>, `GenericGetDefinedOrUndefinedQueryResult`\<`Head`, ...\[...\], [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<..., ...\>, [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<..., ...\>\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : \[`...{ [K in (...)]: (...) }[]`\] : \[...\{ \[K in string \| number \| symbol\]: GenericGetDefinedOrUndefinedQueryResult\\], InferDataAndError\<(...)\>\["data"\], CreateQueryResult\<(...)\[(...)\], (...)\[(...)\]\>, DefinedCreateQueryResult\<(...)\[(...)\], (...)\[(...)\]\>\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GenericGetDefinedOrUndefinedQueryResult\\], InferDataAndError\\]\>\["data"\], CreateQueryResult\\]\>\["data"\], InferDataAndError\\]\>\["error"\]\>, DefinedCreateQueryResult\\]\>\["data"\], InferDataAndError\\]\>\["error"\]\>\> \} + +## Parameters + +### optionsFn + +() => [`InjectQueriesOptions`](../interfaces/InjectQueriesOptions.md)\<`T`, `TCombinedResult`\> + +A function that returns queries' options. + +### injector? + +`Injector` + +The Angular injector to use. + +## Returns + +`Signal`\<`TCombinedResult`\> diff --git a/docs/framework/angular/reference/functions/injectQuery.md b/docs/framework/angular/reference/functions/injectQuery.md index 8fa6832b09b..282fcb8f5d9 100644 --- a/docs/framework/angular/reference/functions/injectQuery.md +++ b/docs/framework/angular/reference/functions/injectQuery.md @@ -9,11 +9,15 @@ Injects a query: a declarative dependency on an asynchronous source of data that **Basic example** ```ts +import { lastValueFrom } from 'rxjs' + class ServiceOrComponent { query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => - this.#http.get('https://api.github.com/repos/tanstack/query'), + lastValueFrom( + this.#http.get('https://api.github.com/repos/tanstack/query'), + ), })) } ``` @@ -42,7 +46,7 @@ A function that returns query options. ## Param -Additional configuration +Additional configuration. ## See @@ -54,38 +58,7 @@ https://tanstack.com/query/latest/docs/framework/angular/guides/queries function injectQuery(injectQueryFn, options?): DefinedCreateQueryResult; ``` -Defined in: [inject-query.ts:65](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L65) - -Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. - -**Basic example** -```ts -class ServiceOrComponent { - query = injectQuery(() => ({ - queryKey: ['repoData'], - queryFn: () => - this.#http.get('https://api.github.com/repos/tanstack/query'), - })) -} -``` - -Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context. -In the example below, the query will be automatically enabled and executed when the filter signal changes -to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled. - -**Reactive example** -```ts -class ServiceOrComponent { - filter = signal('') - - todosQuery = injectQuery(() => ({ - queryKey: ['todos', this.filter()], - queryFn: () => fetchTodos(this.filter()), - // Signals can be combined with expressions - enabled: !!this.filter(), - })) -} -``` +Defined in: [inject-query.ts:34](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L34) ### Type Parameters @@ -111,62 +84,21 @@ class ServiceOrComponent { () => [`DefinedInitialDataOptions`](../type-aliases/DefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> -A function that returns query options. - #### options? [`InjectQueryOptions`](../interfaces/InjectQueryOptions.md) -Additional configuration - ### Returns [`DefinedCreateQueryResult`](../type-aliases/DefinedCreateQueryResult.md)\<`TData`, `TError`\> -The query result. - -### See - -https://tanstack.com/query/latest/docs/framework/angular/guides/queries - ## Call Signature ```ts function injectQuery(injectQueryFn, options?): CreateQueryResult; ``` -Defined in: [inject-query.ts:116](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L116) - -Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. - -**Basic example** -```ts -class ServiceOrComponent { - query = injectQuery(() => ({ - queryKey: ['repoData'], - queryFn: () => - this.#http.get('https://api.github.com/repos/tanstack/query'), - })) -} -``` - -Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context. -In the example below, the query will be automatically enabled and executed when the filter signal changes -to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled. - -**Reactive example** -```ts -class ServiceOrComponent { - filter = signal('') - - todosQuery = injectQuery(() => ({ - queryKey: ['todos', this.filter()], - queryFn: () => fetchTodos(this.filter()), - // Signals can be combined with expressions - enabled: !!this.filter(), - })) -} -``` +Defined in: [inject-query.ts:49](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L49) ### Type Parameters @@ -192,62 +124,21 @@ class ServiceOrComponent { () => [`UndefinedInitialDataOptions`](../type-aliases/UndefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> -A function that returns query options. - #### options? [`InjectQueryOptions`](../interfaces/InjectQueryOptions.md) -Additional configuration - ### Returns [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`TData`, `TError`\> -The query result. - -### See - -https://tanstack.com/query/latest/docs/framework/angular/guides/queries - ## Call Signature ```ts function injectQuery(injectQueryFn, options?): CreateQueryResult; ``` -Defined in: [inject-query.ts:167](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L167) - -Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. - -**Basic example** -```ts -class ServiceOrComponent { - query = injectQuery(() => ({ - queryKey: ['repoData'], - queryFn: () => - this.#http.get('https://api.github.com/repos/tanstack/query'), - })) -} -``` - -Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context. -In the example below, the query will be automatically enabled and executed when the filter signal changes -to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled. - -**Reactive example** -```ts -class ServiceOrComponent { - filter = signal('') - - todosQuery = injectQuery(() => ({ - queryKey: ['todos', this.filter()], - queryFn: () => fetchTodos(this.filter()), - // Signals can be combined with expressions - enabled: !!this.filter(), - })) -} -``` +Defined in: [inject-query.ts:64](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L64) ### Type Parameters @@ -271,22 +162,12 @@ class ServiceOrComponent { #### injectQueryFn -() => [`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> - -A function that returns query options. +() => [`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> #### options? [`InjectQueryOptions`](../interfaces/InjectQueryOptions.md) -Additional configuration - ### Returns [`CreateQueryResult`](../type-aliases/CreateQueryResult.md)\<`TData`, `TError`\> - -The query result. - -### See - -https://tanstack.com/query/latest/docs/framework/angular/guides/queries diff --git a/docs/framework/angular/reference/functions/provideAngularQuery.md b/docs/framework/angular/reference/functions/provideAngularQuery.md index 9894b6a5af2..f5b9d3e2739 100644 --- a/docs/framework/angular/reference/functions/provideAngularQuery.md +++ b/docs/framework/angular/reference/functions/provideAngularQuery.md @@ -6,10 +6,10 @@ title: provideAngularQuery # ~~Function: provideAngularQuery()~~ ```ts -function provideAngularQuery(queryClient): Provider[]; +function provideAngularQuery(queryClient): EnvironmentProviders; ``` -Defined in: [providers.ts:124](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L124) +Defined in: [providers.ts:176](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L176) Sets up providers necessary to enable TanStack Query functionality for Angular applications. @@ -25,7 +25,7 @@ A `QueryClient` instance. ## Returns -`Provider`[] +`EnvironmentProviders` A set of providers to set up TanStack Query. diff --git a/docs/framework/angular/reference/functions/provideQueryClient.md b/docs/framework/angular/reference/functions/provideQueryClient.md index bb42a4a94bb..9d5f6736216 100644 --- a/docs/framework/angular/reference/functions/provideQueryClient.md +++ b/docs/framework/angular/reference/functions/provideQueryClient.md @@ -6,10 +6,10 @@ title: provideQueryClient # Function: provideQueryClient() ```ts -function provideQueryClient(queryClient): Provider; +function provideQueryClient(queryClient): EnvironmentProviders; ``` -Defined in: [providers.ts:14](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L14) +Defined in: [providers.ts:79](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L79) Usually [provideTanStackQuery](provideTanStackQuery.md) is used once to set up TanStack Query and the [https://tanstack.com/query/latest/docs/reference/QueryClient\|QueryClient](https://tanstack.com/query/latest/docs/reference/QueryClient|QueryClient) @@ -27,6 +27,6 @@ A `QueryClient` instance, or an `InjectionToken` which provides a `QueryClient`. ## Returns -`Provider` +`EnvironmentProviders` -a provider object that can be used to provide the `QueryClient` instance. +A single EnvironmentProviders value to add to environment `providers` (do not spread). diff --git a/docs/framework/angular/reference/functions/provideTanStackQuery.md b/docs/framework/angular/reference/functions/provideTanStackQuery.md index 2f8d79f83f3..000e546093b 100644 --- a/docs/framework/angular/reference/functions/provideTanStackQuery.md +++ b/docs/framework/angular/reference/functions/provideTanStackQuery.md @@ -6,10 +6,10 @@ title: provideTanStackQuery # Function: provideTanStackQuery() ```ts -function provideTanStackQuery(queryClient, ...features): Provider[]; +function provideTanStackQuery(queryClient, ...features): EnvironmentProviders; ``` -Defined in: [providers.ts:105](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L105) +Defined in: [providers.ts:156](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L156) Sets up providers necessary to enable TanStack Query functionality for Angular applications. @@ -47,20 +47,14 @@ export class AppModule {} You can also enable optional developer tools by adding `withDevtools`. By default the tools will then be loaded when your app is in development mode. + ```ts -import { - provideTanStackQuery, - withDevtools - QueryClient, -} from '@tanstack/angular-query-experimental' +import { provideTanStackQuery, QueryClient } from '@tanstack/angular-query-experimental' +import { withDevtools } from '@tanstack/angular-query-devtools' -bootstrapApplication(AppComponent, - { - providers: [ - provideTanStackQuery(new QueryClient(), withDevtools()) - ] - } -) +bootstrapApplication(AppComponent, { + providers: [provideTanStackQuery(new QueryClient(), withDevtools())], +}) ``` **Example: using an InjectionToken** @@ -94,11 +88,12 @@ Optional features to configure additional Query functionality. ## Returns -`Provider`[] +`EnvironmentProviders` -A set of providers to set up TanStack Query. +A single EnvironmentProviders value (do not spread into `providers`). ## See - https://tanstack.com/query/v5/docs/framework/angular/quick-start - - withDevtools + - https://tanstack.com/query/v5/docs/framework/angular/devtools + - https://tanstack.com/query/latest/docs/framework/angular/guides/ssr diff --git a/docs/framework/angular/reference/functions/queryFeature.md b/docs/framework/angular/reference/functions/queryFeature.md index d3b67f1bfbb..af920ea9b19 100644 --- a/docs/framework/angular/reference/functions/queryFeature.md +++ b/docs/framework/angular/reference/functions/queryFeature.md @@ -9,7 +9,7 @@ title: queryFeature function queryFeature(kind, providers): QueryFeature; ``` -Defined in: [providers.ts:146](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L146) +Defined in: [providers.ts:200](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L200) Helper function to create an object that represents a Query feature. @@ -17,7 +17,7 @@ Helper function to create an object that represents a Query feature. ### TFeatureKind -`TFeatureKind` *extends* `"Devtools"` \| `"PersistQueryClient"` +`TFeatureKind` *extends* `"Hydration"` \| `"Devtools"` \| `"PersistQueryClient"` ## Parameters @@ -27,7 +27,7 @@ Helper function to create an object that represents a Query feature. ### providers -`Provider`[] +`EnvironmentProviders` | (`Provider` \| `EnvironmentProviders`)[] ## Returns diff --git a/docs/framework/angular/reference/functions/queryOptions.md b/docs/framework/angular/reference/functions/queryOptions.md index 34640111ec1..75714cc312e 100644 --- a/docs/framework/angular/reference/functions/queryOptions.md +++ b/docs/framework/angular/reference/functions/queryOptions.md @@ -30,28 +30,10 @@ The query options to tag with the type from `queryFn`. ## Call Signature ```ts -function queryOptions(options): Omit, "queryFn"> & object & object; +function queryOptions(options): CreateQueryOptions & object & object; ``` -Defined in: [query-options.ts:76](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L76) - -Allows to share and re-use query options in a type-safe way. - -The `queryKey` will be tagged with the type from `queryFn`. - -**Example** - -```ts - const { queryKey } = queryOptions({ - queryKey: ['key'], - queryFn: () => Promise.resolve(5), - // ^? Promise - }) - - const queryClient = new QueryClient() - const data = queryClient.getQueryData(queryKey) - // ^? number | undefined -``` +Defined in: [query-options.ts:50](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L50) ### Type Parameters @@ -77,13 +59,9 @@ The `queryKey` will be tagged with the type from `queryFn`. [`DefinedInitialDataOptions`](../type-aliases/DefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> -The query options to tag with the type from `queryFn`. - ### Returns -`Omit`\<[`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` - -The tagged query options. +[`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` ## Call Signature @@ -91,25 +69,7 @@ The tagged query options. function queryOptions(options): OmitKeyof, "queryFn"> & object & object; ``` -Defined in: [query-options.ts:108](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L108) - -Allows to share and re-use query options in a type-safe way. - -The `queryKey` will be tagged with the type from `queryFn`. - -**Example** - -```ts - const { queryKey } = queryOptions({ - queryKey: ['key'], - queryFn: () => Promise.resolve(5), - // ^? Promise - }) - - const queryClient = new QueryClient() - const data = queryClient.getQueryData(queryKey) - // ^? number | undefined -``` +Defined in: [query-options.ts:61](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L61) ### Type Parameters @@ -135,13 +95,9 @@ The `queryKey` will be tagged with the type from `queryFn`. [`UnusedSkipTokenOptions`](../type-aliases/UnusedSkipTokenOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> -The query options to tag with the type from `queryFn`. - ### Returns -`OmitKeyof`\<[`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` - -The tagged query options. +`OmitKeyof`\<[`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` ## Call Signature @@ -149,25 +105,7 @@ The tagged query options. function queryOptions(options): CreateQueryOptions & object & object; ``` -Defined in: [query-options.ts:140](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L140) - -Allows to share and re-use query options in a type-safe way. - -The `queryKey` will be tagged with the type from `queryFn`. - -**Example** - -```ts - const { queryKey } = queryOptions({ - queryKey: ['key'], - queryFn: () => Promise.resolve(5), - // ^? Promise - }) - - const queryClient = new QueryClient() - const data = queryClient.getQueryData(queryKey) - // ^? number | undefined -``` +Defined in: [query-options.ts:72](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L72) ### Type Parameters @@ -193,10 +131,6 @@ The `queryKey` will be tagged with the type from `queryFn`. [`UndefinedInitialDataOptions`](../type-aliases/UndefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> -The query options to tag with the type from `queryFn`. - ### Returns -[`CreateQueryOptions`](../interfaces/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` - -The tagged query options. +[`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` diff --git a/docs/framework/angular/reference/functions/withHydrationKey.md b/docs/framework/angular/reference/functions/withHydrationKey.md new file mode 100644 index 00000000000..2a557f3611b --- /dev/null +++ b/docs/framework/angular/reference/functions/withHydrationKey.md @@ -0,0 +1,34 @@ +--- +id: withHydrationKey +title: withHydrationKey +--- + +# Function: withHydrationKey() + +```ts +function withHydrationKey(key): QueryFeature<"Hydration">; +``` + +Defined in: [providers.ts:233](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L233) + +Sets a non-default serialization key for this injector's `QueryClient` cache (server dehydrate / +browser hydrate via `TransferState`). Use this when you have multiple `QueryClient` instances +so each has its own key. The default key applies when you do not add this feature. + +```ts +providers: [ + provideTanStackQuery(secondaryClient, withHydrationKey('my-secondary-query-cache')), +] +``` + +## Parameters + +### key + +`string` + +A unique string for this client's `TransferState` entry. + +## Returns + +[`QueryFeature`](../interfaces/QueryFeature.md)\<`"Hydration"`\> diff --git a/docs/framework/angular/reference/functions/withNoQueryHydration.md b/docs/framework/angular/reference/functions/withNoQueryHydration.md new file mode 100644 index 00000000000..1fb0618c800 --- /dev/null +++ b/docs/framework/angular/reference/functions/withNoQueryHydration.md @@ -0,0 +1,18 @@ +--- +id: withNoQueryHydration +title: withNoQueryHydration +--- + +# Function: withNoQueryHydration() + +```ts +function withNoQueryHydration(): QueryFeature<"Hydration">; +``` + +Defined in: [providers.ts:248](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L248) + +Disables `TransferState` hydration and dehydration for the current environment injector. + +## Returns + +[`QueryFeature`](../interfaces/QueryFeature.md)\<`"Hydration"`\> diff --git a/docs/framework/angular/reference/index.md b/docs/framework/angular/reference/index.md index c74d256bceb..0ba0f78dfb2 100644 --- a/docs/framework/angular/reference/index.md +++ b/docs/framework/angular/reference/index.md @@ -9,26 +9,27 @@ title: "@tanstack/angular-query-experimental" - [BaseMutationNarrowing](interfaces/BaseMutationNarrowing.md) - [BaseQueryNarrowing](interfaces/BaseQueryNarrowing.md) -- [CreateBaseQueryOptions](interfaces/CreateBaseQueryOptions.md) - [CreateInfiniteQueryOptions](interfaces/CreateInfiniteQueryOptions.md) - [CreateMutationOptions](interfaces/CreateMutationOptions.md) -- [CreateQueryOptions](interfaces/CreateQueryOptions.md) - [InjectInfiniteQueryOptions](interfaces/InjectInfiniteQueryOptions.md) - [InjectIsFetchingOptions](interfaces/InjectIsFetchingOptions.md) - [InjectIsMutatingOptions](interfaces/InjectIsMutatingOptions.md) - [InjectMutationOptions](interfaces/InjectMutationOptions.md) - [InjectMutationStateOptions](interfaces/InjectMutationStateOptions.md) +- [InjectQueriesOptions](interfaces/InjectQueriesOptions.md) - [InjectQueryOptions](interfaces/InjectQueryOptions.md) - [QueryFeature](interfaces/QueryFeature.md) ## Type Aliases - [CreateBaseMutationResult](type-aliases/CreateBaseMutationResult.md) +- [CreateBaseQueryOptions](type-aliases/CreateBaseQueryOptions.md) - [CreateBaseQueryResult](type-aliases/CreateBaseQueryResult.md) - [CreateInfiniteQueryResult](type-aliases/CreateInfiniteQueryResult.md) - [CreateMutateAsyncFunction](type-aliases/CreateMutateAsyncFunction.md) - [CreateMutateFunction](type-aliases/CreateMutateFunction.md) - [CreateMutationResult](type-aliases/CreateMutationResult.md) +- [CreateQueryOptions](type-aliases/CreateQueryOptions.md) - [CreateQueryResult](type-aliases/CreateQueryResult.md) - [DefinedCreateInfiniteQueryResult](type-aliases/DefinedCreateInfiniteQueryResult.md) - [DefinedCreateQueryResult](type-aliases/DefinedCreateQueryResult.md) @@ -53,6 +54,7 @@ title: "@tanstack/angular-query-experimental" - [injectIsRestoring](functions/injectIsRestoring.md) - [injectMutation](functions/injectMutation.md) - [injectMutationState](functions/injectMutationState.md) +- [injectQueries](functions/injectQueries.md) - [injectQuery](functions/injectQuery.md) - [~~injectQueryClient~~](functions/injectQueryClient.md) - [mutationOptions](functions/mutationOptions.md) @@ -62,3 +64,5 @@ title: "@tanstack/angular-query-experimental" - [provideTanStackQuery](functions/provideTanStackQuery.md) - [queryFeature](functions/queryFeature.md) - [queryOptions](functions/queryOptions.md) +- [withHydrationKey](functions/withHydrationKey.md) +- [withNoQueryHydration](functions/withNoQueryHydration.md) diff --git a/docs/framework/angular/reference/interfaces/BaseQueryNarrowing.md b/docs/framework/angular/reference/interfaces/BaseQueryNarrowing.md index bc7811b6ae2..24a00f39cb3 100644 --- a/docs/framework/angular/reference/interfaces/BaseQueryNarrowing.md +++ b/docs/framework/angular/reference/interfaces/BaseQueryNarrowing.md @@ -5,7 +5,7 @@ title: BaseQueryNarrowing # Interface: BaseQueryNarrowing\ -Defined in: [types.ts:57](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L57) +Defined in: [types.ts:45](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L45) ## Type Parameters @@ -25,7 +25,7 @@ Defined in: [types.ts:57](https://github.com/TanStack/query/blob/main/packages/a isError: (this) => this is CreateBaseQueryResult>; ``` -Defined in: [types.ts:65](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L65) +Defined in: [types.ts:53](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L53) #### Parameters @@ -45,7 +45,7 @@ Defined in: [types.ts:65](https://github.com/TanStack/query/blob/main/packages/a isPending: (this) => this is CreateBaseQueryResult>; ``` -Defined in: [types.ts:72](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L72) +Defined in: [types.ts:60](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L60) #### Parameters @@ -65,7 +65,7 @@ Defined in: [types.ts:72](https://github.com/TanStack/query/blob/main/packages/a isSuccess: (this) => this is CreateBaseQueryResult>; ``` -Defined in: [types.ts:58](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L58) +Defined in: [types.ts:46](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L46) #### Parameters diff --git a/docs/framework/angular/reference/interfaces/CreateInfiniteQueryOptions.md b/docs/framework/angular/reference/interfaces/CreateInfiniteQueryOptions.md index ab21d5bc32b..1f8883c8d7e 100644 --- a/docs/framework/angular/reference/interfaces/CreateInfiniteQueryOptions.md +++ b/docs/framework/angular/reference/interfaces/CreateInfiniteQueryOptions.md @@ -5,7 +5,7 @@ title: CreateInfiniteQueryOptions # Interface: CreateInfiniteQueryOptions\ -Defined in: [types.ts:81](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L81) +Defined in: [types.ts:69](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L69) ## Extends diff --git a/docs/framework/angular/reference/interfaces/CreateQueryOptions.md b/docs/framework/angular/reference/interfaces/CreateQueryOptions.md deleted file mode 100644 index 113fbbc5d27..00000000000 --- a/docs/framework/angular/reference/interfaces/CreateQueryOptions.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -id: CreateQueryOptions -title: CreateQueryOptions ---- - -# Interface: CreateQueryOptions\ - -Defined in: [types.ts:35](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L35) - -## Extends - -- `OmitKeyof`\<[`CreateBaseQueryOptions`](CreateBaseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryFnData`, `TQueryKey`\>, `"suspense"`\> - -## Type Parameters - -### TQueryFnData - -`TQueryFnData` = `unknown` - -### TError - -`TError` = `DefaultError` - -### TData - -`TData` = `TQueryFnData` - -### TQueryKey - -`TQueryKey` *extends* `QueryKey` = `QueryKey` diff --git a/docs/framework/angular/reference/interfaces/InjectInfiniteQueryOptions.md b/docs/framework/angular/reference/interfaces/InjectInfiniteQueryOptions.md index 3b552aa3811..33880ffae2a 100644 --- a/docs/framework/angular/reference/interfaces/InjectInfiniteQueryOptions.md +++ b/docs/framework/angular/reference/interfaces/InjectInfiniteQueryOptions.md @@ -5,7 +5,7 @@ title: InjectInfiniteQueryOptions # Interface: InjectInfiniteQueryOptions -Defined in: [inject-infinite-query.ts:25](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L25) +Defined in: [inject-infinite-query.ts:27](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L27) ## Properties @@ -15,7 +15,7 @@ Defined in: [inject-infinite-query.ts:25](https://github.com/TanStack/query/blob optional injector: Injector; ``` -Defined in: [inject-infinite-query.ts:31](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L31) +Defined in: [inject-infinite-query.ts:33](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-infinite-query.ts#L33) The `Injector` in which to create the infinite query. diff --git a/docs/framework/angular/reference/interfaces/InjectMutationOptions.md b/docs/framework/angular/reference/interfaces/InjectMutationOptions.md index 0638baa3723..c313951b254 100644 --- a/docs/framework/angular/reference/interfaces/InjectMutationOptions.md +++ b/docs/framework/angular/reference/interfaces/InjectMutationOptions.md @@ -5,7 +5,7 @@ title: InjectMutationOptions # Interface: InjectMutationOptions -Defined in: [inject-mutation.ts:28](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L28) +Defined in: [inject-mutation.ts:27](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L27) ## Properties @@ -15,7 +15,7 @@ Defined in: [inject-mutation.ts:28](https://github.com/TanStack/query/blob/main/ optional injector: Injector; ``` -Defined in: [inject-mutation.ts:34](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L34) +Defined in: [inject-mutation.ts:33](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-mutation.ts#L33) The `Injector` in which to create the mutation. diff --git a/docs/framework/angular/reference/interfaces/InjectQueriesOptions.md b/docs/framework/angular/reference/interfaces/InjectQueriesOptions.md new file mode 100644 index 00000000000..c9f860b872f --- /dev/null +++ b/docs/framework/angular/reference/interfaces/InjectQueriesOptions.md @@ -0,0 +1,50 @@ +--- +id: InjectQueriesOptions +title: InjectQueriesOptions +--- + +# Interface: InjectQueriesOptions\ + +Defined in: [inject-queries.ts:257](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L257) + +## Type Parameters + +### T + +`T` *extends* `any`[] + +### TCombinedResult + +`TCombinedResult` = [`QueriesResults`](../type-aliases/QueriesResults.md)\<`T`\> + +## Properties + +### combine()? + +```ts +optional combine: (result) => TCombinedResult; +``` + +Defined in: [inject-queries.ts:266](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L266) + +#### Parameters + +##### result + +`T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<`Head`\>\[`"data"`\], `QueryObserverResult`\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>, `DefinedQueryObserverResult`\<`InferDataAndError`\<`Head`\>\[`"data"`\], `InferDataAndError`\<`Head`\>\[`"error"`\]\>\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<...\>\[`"data"`\], `QueryObserverResult`\<...\[...\], ...\[...\]\>, `DefinedQueryObserverResult`\<...\[...\], ...\[...\]\>\>, `GenericGetDefinedOrUndefinedQueryResult`\<`Head`, `InferDataAndError`\<...\>\[`"data"`\], `QueryObserverResult`\<...\[...\], ...\[...\]\>, `DefinedQueryObserverResult`\<...\[...\], ...\[...\]\>\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...(...)[]`\] *extends* \[...\] ? \[..., ..., ...\] : ... *extends* ... ? ... : ... : \[...\{ \[K in (...) \| (...) \| (...)\]: GenericGetDefinedOrUndefinedQueryResult\<(...), (...), (...), (...)\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GenericGetDefinedOrUndefinedQueryResult\\], InferDataAndError\\]\>\["data"\], QueryObserverResult\\["data"\], InferDataAndError\<(...)\[(...)\]\>\["error"\]\>, DefinedQueryObserverResult\\["data"\], InferDataAndError\<(...)\[(...)\]\>\["error"\]\>\> \} + +#### Returns + +`TCombinedResult` + +*** + +### queries + +```ts +queries: + | readonly [{ [K in string | number | symbol]: GetCreateQueryOptionsForCreateQueries]> }] + | readonly [T extends [] ? [] : T extends [Head] ? [GetCreateQueryOptionsForCreateQueries] : T extends [Head, ...Tails[]] ? [...Tails[]] extends [] ? [] : [...Tails[]] extends [Head] ? [GetCreateQueryOptionsForCreateQueries, GetCreateQueryOptionsForCreateQueries] : [...Tails[]] extends [Head, ...Tails[]] ? [...Tails[]] extends [] ? [] : [...(...)[]] extends [...] ? [..., ..., ...] : ... extends ... ? ... : ... : readonly unknown[] extends [...Tails[]] ? [...Tails[]] : [...(...)[]] extends ...[] ? ...[] : ...[] : readonly unknown[] extends T ? T : T extends QueryObserverOptionsForCreateQueries[] ? QueryObserverOptionsForCreateQueries[] : QueryObserverOptionsForCreateQueries[]]; +``` + +Defined in: [inject-queries.ts:261](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L261) diff --git a/docs/framework/angular/reference/interfaces/InjectQueryOptions.md b/docs/framework/angular/reference/interfaces/InjectQueryOptions.md index eecbef28048..8f513c31b5f 100644 --- a/docs/framework/angular/reference/interfaces/InjectQueryOptions.md +++ b/docs/framework/angular/reference/interfaces/InjectQueryOptions.md @@ -5,7 +5,7 @@ title: InjectQueryOptions # Interface: InjectQueryOptions -Defined in: [inject-query.ts:20](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L20) +Defined in: [inject-query.ts:25](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L25) ## Properties @@ -15,7 +15,7 @@ Defined in: [inject-query.ts:20](https://github.com/TanStack/query/blob/main/pac optional injector: Injector; ``` -Defined in: [inject-query.ts:26](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L26) +Defined in: [inject-query.ts:31](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-query.ts#L31) The `Injector` in which to create the query. diff --git a/docs/framework/angular/reference/interfaces/QueryFeature.md b/docs/framework/angular/reference/interfaces/QueryFeature.md index b2444878ac9..5af5897dae4 100644 --- a/docs/framework/angular/reference/interfaces/QueryFeature.md +++ b/docs/framework/angular/reference/interfaces/QueryFeature.md @@ -5,7 +5,7 @@ title: QueryFeature # Interface: QueryFeature\ -Defined in: [providers.ts:135](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L135) +Defined in: [providers.ts:189](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L189) Helper type to represent a Query feature. @@ -23,14 +23,14 @@ Helper type to represent a Query feature. ɵkind: TFeatureKind; ``` -Defined in: [providers.ts:136](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L136) +Defined in: [providers.ts:190](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L190) *** ### ɵproviders ```ts -ɵproviders: Provider[]; +ɵproviders: EnvironmentProviders | (Provider | EnvironmentProviders)[]; ``` -Defined in: [providers.ts:137](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L137) +Defined in: [providers.ts:191](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L191) diff --git a/docs/framework/angular/reference/interfaces/CreateBaseQueryOptions.md b/docs/framework/angular/reference/type-aliases/CreateBaseQueryOptions.md similarity index 63% rename from docs/framework/angular/reference/interfaces/CreateBaseQueryOptions.md rename to docs/framework/angular/reference/type-aliases/CreateBaseQueryOptions.md index 48a4b7dcc6c..29bacc3ac0d 100644 --- a/docs/framework/angular/reference/interfaces/CreateBaseQueryOptions.md +++ b/docs/framework/angular/reference/type-aliases/CreateBaseQueryOptions.md @@ -3,13 +3,13 @@ id: CreateBaseQueryOptions title: CreateBaseQueryOptions --- -# Interface: CreateBaseQueryOptions\ +# Type Alias: CreateBaseQueryOptions\ -Defined in: [types.ts:21](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L21) - -## Extends +```ts +type CreateBaseQueryOptions = QueryObserverOptions; +``` -- `QueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryData`, `TQueryKey`\> +Defined in: [types.ts:21](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L21) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/CreateBaseQueryResult.md b/docs/framework/angular/reference/type-aliases/CreateBaseQueryResult.md index 784f89c5e17..0ed7f2524bd 100644 --- a/docs/framework/angular/reference/type-aliases/CreateBaseQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/CreateBaseQueryResult.md @@ -6,10 +6,10 @@ title: CreateBaseQueryResult # Type Alias: CreateBaseQueryResult\ ```ts -type CreateBaseQueryResult = BaseQueryNarrowing & MapToSignals>; +type CreateBaseQueryResult = BaseQueryNarrowing & MapToSignals, MethodKeys>>; ``` -Defined in: [types.ts:98](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L98) +Defined in: [types.ts:86](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L86) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/CreateInfiniteQueryResult.md b/docs/framework/angular/reference/type-aliases/CreateInfiniteQueryResult.md index f4c01a674bd..fc9a51d4b0e 100644 --- a/docs/framework/angular/reference/type-aliases/CreateInfiniteQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/CreateInfiniteQueryResult.md @@ -6,10 +6,10 @@ title: CreateInfiniteQueryResult # Type Alias: CreateInfiniteQueryResult\ ```ts -type CreateInfiniteQueryResult = BaseQueryNarrowing & MapToSignals>; +type CreateInfiniteQueryResult = BaseQueryNarrowing & MapToSignals, MethodKeys>>; ``` -Defined in: [types.ts:117](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L117) +Defined in: [types.ts:111](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L111) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/CreateMutationResult.md b/docs/framework/angular/reference/type-aliases/CreateMutationResult.md index b5573544d02..86c2056181e 100644 --- a/docs/framework/angular/reference/type-aliases/CreateMutationResult.md +++ b/docs/framework/angular/reference/type-aliases/CreateMutationResult.md @@ -6,7 +6,7 @@ title: CreateMutationResult # Type Alias: CreateMutationResult\ ```ts -type CreateMutationResult = BaseMutationNarrowing & MapToSignals>; +type CreateMutationResult = BaseMutationNarrowing & MapToSignals, MethodKeys>>; ``` Defined in: [types.ts:266](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L266) diff --git a/docs/framework/angular/reference/type-aliases/CreateQueryOptions.md b/docs/framework/angular/reference/type-aliases/CreateQueryOptions.md new file mode 100644 index 00000000000..799c2d3b115 --- /dev/null +++ b/docs/framework/angular/reference/type-aliases/CreateQueryOptions.md @@ -0,0 +1,30 @@ +--- +id: CreateQueryOptions +title: CreateQueryOptions +--- + +# Type Alias: CreateQueryOptions\ + +```ts +type CreateQueryOptions = OmitKeyof, "suspense">; +``` + +Defined in: [types.ts:29](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L29) + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` diff --git a/docs/framework/angular/reference/type-aliases/CreateQueryResult.md b/docs/framework/angular/reference/type-aliases/CreateQueryResult.md index c532a874632..67d2f6cd868 100644 --- a/docs/framework/angular/reference/type-aliases/CreateQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/CreateQueryResult.md @@ -9,7 +9,7 @@ title: CreateQueryResult type CreateQueryResult = CreateBaseQueryResult; ``` -Defined in: [types.ts:105](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L105) +Defined in: [types.ts:96](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L96) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/DefinedCreateInfiniteQueryResult.md b/docs/framework/angular/reference/type-aliases/DefinedCreateInfiniteQueryResult.md index 932114c7d18..67600319924 100644 --- a/docs/framework/angular/reference/type-aliases/DefinedCreateInfiniteQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/DefinedCreateInfiniteQueryResult.md @@ -6,10 +6,10 @@ title: DefinedCreateInfiniteQueryResult # Type Alias: DefinedCreateInfiniteQueryResult\ ```ts -type DefinedCreateInfiniteQueryResult = MapToSignals; +type DefinedCreateInfiniteQueryResult = MapToSignals>; ``` -Defined in: [types.ts:123](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L123) +Defined in: [types.ts:120](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L120) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/DefinedCreateQueryResult.md b/docs/framework/angular/reference/type-aliases/DefinedCreateQueryResult.md index 60fa8774919..9ef090ccf8c 100644 --- a/docs/framework/angular/reference/type-aliases/DefinedCreateQueryResult.md +++ b/docs/framework/angular/reference/type-aliases/DefinedCreateQueryResult.md @@ -6,10 +6,10 @@ title: DefinedCreateQueryResult # Type Alias: DefinedCreateQueryResult\ ```ts -type DefinedCreateQueryResult = BaseQueryNarrowing & MapToSignals>; +type DefinedCreateQueryResult = BaseQueryNarrowing & MapToSignals, MethodKeys>>; ``` -Defined in: [types.ts:110](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L110) +Defined in: [types.ts:101](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/types.ts#L101) ## Type Parameters diff --git a/docs/framework/angular/reference/type-aliases/DefinedInitialDataOptions.md b/docs/framework/angular/reference/type-aliases/DefinedInitialDataOptions.md index 4bcea1da72a..4c8ad5c0ab3 100644 --- a/docs/framework/angular/reference/type-aliases/DefinedInitialDataOptions.md +++ b/docs/framework/angular/reference/type-aliases/DefinedInitialDataOptions.md @@ -6,10 +6,10 @@ title: DefinedInitialDataOptions # Type Alias: DefinedInitialDataOptions\ ```ts -type DefinedInitialDataOptions = Omit, "queryFn"> & object; +type DefinedInitialDataOptions = CreateQueryOptions & object; ``` -Defined in: [query-options.ts:40](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L40) +Defined in: [query-options.ts:39](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L39) ## Type Declaration @@ -21,12 +21,6 @@ initialData: | () => NonUndefinedGuard; ``` -### queryFn? - -```ts -optional queryFn: QueryFunction; -``` - ## Type Parameters ### TQueryFnData diff --git a/docs/framework/angular/reference/type-aliases/DevtoolsFeature.md b/docs/framework/angular/reference/type-aliases/DevtoolsFeature.md index a085d243f8e..3956a93a713 100644 --- a/docs/framework/angular/reference/type-aliases/DevtoolsFeature.md +++ b/docs/framework/angular/reference/type-aliases/DevtoolsFeature.md @@ -9,7 +9,7 @@ title: DevtoolsFeature type DevtoolsFeature = QueryFeature<"Devtools">; ``` -Defined in: [providers.ts:158](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L158) +Defined in: [providers.ts:212](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L212) A type alias that represents a feature which enables developer tools. The type is used to describe the return value of the `withDevtools` function. diff --git a/docs/framework/angular/reference/type-aliases/PersistQueryClientFeature.md b/docs/framework/angular/reference/type-aliases/PersistQueryClientFeature.md index 07fa8cfd3b1..5addef4711a 100644 --- a/docs/framework/angular/reference/type-aliases/PersistQueryClientFeature.md +++ b/docs/framework/angular/reference/type-aliases/PersistQueryClientFeature.md @@ -9,7 +9,7 @@ title: PersistQueryClientFeature type PersistQueryClientFeature = QueryFeature<"PersistQueryClient">; ``` -Defined in: [providers.ts:164](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L164) +Defined in: [providers.ts:218](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L218) A type alias that represents a feature which enables persistence. The type is used to describe the return value of the `withPersistQueryClient` function. diff --git a/docs/framework/angular/reference/type-aliases/QueriesOptions.md b/docs/framework/angular/reference/type-aliases/QueriesOptions.md index 2def13c9c92..c8f95bb1a70 100644 --- a/docs/framework/angular/reference/type-aliases/QueriesOptions.md +++ b/docs/framework/angular/reference/type-aliases/QueriesOptions.md @@ -9,7 +9,7 @@ title: QueriesOptions type QueriesOptions = TDepth["length"] extends MAXIMUM_DEPTH ? QueryObserverOptionsForCreateQueries[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryOptionsForCreateQueries] : T extends [infer Head, ...(infer Tails)] ? QueriesOptions<[...Tails], [...TResults, GetCreateQueryOptionsForCreateQueries], [...TDepth, 1]> : ReadonlyArray extends T ? T : T extends QueryObserverOptionsForCreateQueries[] ? QueryObserverOptionsForCreateQueries[] : QueryObserverOptionsForCreateQueries[]; ``` -Defined in: [inject-queries.ts:144](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L144) +Defined in: [inject-queries.ts:178](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L178) QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param diff --git a/docs/framework/angular/reference/type-aliases/QueriesResults.md b/docs/framework/angular/reference/type-aliases/QueriesResults.md index 6d5ecf6dd4d..b34b39cb608 100644 --- a/docs/framework/angular/reference/type-aliases/QueriesResults.md +++ b/docs/framework/angular/reference/type-aliases/QueriesResults.md @@ -9,7 +9,7 @@ title: QueriesResults type QueriesResults = TDepth["length"] extends MAXIMUM_DEPTH ? CreateQueryResult[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryResult] : T extends [infer Head, ...(infer Tails)] ? QueriesResults<[...Tails], [...TResults, GetCreateQueryResult], [...TDepth, 1]> : { [K in keyof T]: GetCreateQueryResult }; ``` -Defined in: [inject-queries.ts:186](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L186) +Defined in: [inject-queries.ts:220](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/inject-queries.ts#L220) QueriesResults reducer recursively maps type param to results diff --git a/docs/framework/angular/reference/type-aliases/QueryFeatures.md b/docs/framework/angular/reference/type-aliases/QueryFeatures.md index d7d79a75d6b..9183d93406a 100644 --- a/docs/framework/angular/reference/type-aliases/QueryFeatures.md +++ b/docs/framework/angular/reference/type-aliases/QueryFeatures.md @@ -8,10 +8,11 @@ title: QueryFeatures ```ts type QueryFeatures = | DevtoolsFeature + | QueryFeature<"Hydration"> | PersistQueryClientFeature; ``` -Defined in: [providers.ts:173](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L173) +Defined in: [providers.ts:266](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/providers.ts#L266) A type alias that represents all Query features available for use with `provideTanStackQuery`. Features can be enabled by adding special functions to the `provideTanStackQuery` call. diff --git a/docs/framework/angular/reference/type-aliases/UndefinedInitialDataOptions.md b/docs/framework/angular/reference/type-aliases/UndefinedInitialDataOptions.md index f1a48e74e6f..339c1e56841 100644 --- a/docs/framework/angular/reference/type-aliases/UndefinedInitialDataOptions.md +++ b/docs/framework/angular/reference/type-aliases/UndefinedInitialDataOptions.md @@ -9,7 +9,7 @@ title: UndefinedInitialDataOptions type UndefinedInitialDataOptions = CreateQueryOptions & object; ``` -Defined in: [query-options.ts:13](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L13) +Defined in: [query-options.ts:12](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L12) ## Type Declaration diff --git a/docs/framework/angular/reference/type-aliases/UnusedSkipTokenOptions.md b/docs/framework/angular/reference/type-aliases/UnusedSkipTokenOptions.md index 9a65d5b3f34..bebe298be72 100644 --- a/docs/framework/angular/reference/type-aliases/UnusedSkipTokenOptions.md +++ b/docs/framework/angular/reference/type-aliases/UnusedSkipTokenOptions.md @@ -9,7 +9,7 @@ title: UnusedSkipTokenOptions type UnusedSkipTokenOptions = OmitKeyof, "queryFn"> & object; ``` -Defined in: [query-options.ts:25](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L25) +Defined in: [query-options.ts:24](https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/query-options.ts#L24) ## Type Declaration diff --git a/examples/angular/auto-refetching/package.json b/examples/angular/auto-refetching/package.json index f92d63996df..16862189ce5 100644 --- a/examples/angular/auto-refetching/package.json +++ b/examples/angular/auto-refetching/package.json @@ -13,6 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "rxjs": "^7.8.2", "tslib": "^2.8.1", @@ -22,6 +23,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/auto-refetching/src/app/app.config.ts b/examples/angular/auto-refetching/src/app/app.config.ts index b9b2b6c36fa..1b9deaa3dd7 100644 --- a/examples/angular/auto-refetching/src/app/app.config.ts +++ b/examples/angular/auto-refetching/src/app/app.config.ts @@ -7,7 +7,7 @@ import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' import { mockInterceptor } from './interceptor/mock-api.interceptor' import type { ApplicationConfig } from '@angular/core' diff --git a/examples/angular/basic-persister/package.json b/examples/angular/basic-persister/package.json index f5e27a20fe9..0fbbd847dad 100644 --- a/examples/angular/basic-persister/package.json +++ b/examples/angular/basic-persister/package.json @@ -13,6 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "@tanstack/angular-query-persist-client": "^5.97.0", "@tanstack/query-async-storage-persister": "^5.97.0", @@ -24,6 +25,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/basic-persister/src/app/app.config.ts b/examples/angular/basic-persister/src/app/app.config.ts index ff5634cbb63..57710d79969 100644 --- a/examples/angular/basic-persister/src/app/app.config.ts +++ b/examples/angular/basic-persister/src/app/app.config.ts @@ -4,7 +4,7 @@ import { provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { withPersistQueryClient } from '@tanstack/angular-query-persist-client' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' import type { ApplicationConfig } from '@angular/core' diff --git a/examples/angular/basic/package.json b/examples/angular/basic/package.json index 574c3a25de7..cd49738cacc 100644 --- a/examples/angular/basic/package.json +++ b/examples/angular/basic/package.json @@ -13,6 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "rxjs": "^7.8.2", "tslib": "^2.8.1", @@ -22,6 +23,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/basic/src/app/app.config.ts b/examples/angular/basic/src/app/app.config.ts index 77281147d1f..a345139069d 100644 --- a/examples/angular/basic/src/app/app.config.ts +++ b/examples/angular/basic/src/app/app.config.ts @@ -3,7 +3,7 @@ import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { diff --git a/examples/angular/devtools-panel/package.json b/examples/angular/devtools-panel/package.json index e848bc139f1..c0cb5784623 100644 --- a/examples/angular/devtools-panel/package.json +++ b/examples/angular/devtools-panel/package.json @@ -14,6 +14,7 @@ "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "rxjs": "^7.8.2", "tslib": "^2.8.1", @@ -23,6 +24,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/devtools-panel/src/app/components/basic-devtools-panel-example.component.ts b/examples/angular/devtools-panel/src/app/components/basic-devtools-panel-example.component.ts index f0a56611eb0..8f3801c52d8 100644 --- a/examples/angular/devtools-panel/src/app/components/basic-devtools-panel-example.component.ts +++ b/examples/angular/devtools-panel/src/app/components/basic-devtools-panel-example.component.ts @@ -4,7 +4,7 @@ import { signal, viewChild, } from '@angular/core' -import { injectDevtoolsPanel } from '@tanstack/angular-query-experimental/devtools-panel' +import { injectDevtoolsPanel } from '@tanstack/angular-query-devtools/devtools-panel' import { ExampleQueryComponent } from './example-query.component' import type { ElementRef } from '@angular/core' diff --git a/examples/angular/devtools-panel/src/app/components/lazy-load-devtools-panel-example.component.ts b/examples/angular/devtools-panel/src/app/components/lazy-load-devtools-panel-example.component.ts index 9bb23b11924..9b4b40e88cc 100644 --- a/examples/angular/devtools-panel/src/app/components/lazy-load-devtools-panel-example.component.ts +++ b/examples/angular/devtools-panel/src/app/components/lazy-load-devtools-panel-example.component.ts @@ -10,7 +10,7 @@ import { } from '@angular/core' import { ExampleQueryComponent } from './example-query.component' import type { ElementRef } from '@angular/core' -import type { DevtoolsPanelRef } from '@tanstack/angular-query-experimental/devtools-panel' +import type { DevtoolsPanelRef } from '@tanstack/angular-query-devtools/devtools-panel' @Component({ selector: 'lazy-load-devtools-panel-example', @@ -49,7 +49,7 @@ export default class LazyLoadDevtoolsPanelExampleComponent { if (this.devtools()) return if (this.isOpen()) { this.devtools.set( - import('@tanstack/angular-query-experimental/devtools-panel').then( + import('@tanstack/angular-query-devtools/devtools-panel').then( ({ injectDevtoolsPanel }) => injectDevtoolsPanel(this.devToolsOptions, { injector: this.injector, diff --git a/examples/angular/dynamic-devtools/.devcontainer/devcontainer.json b/examples/angular/dynamic-devtools/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..365adf8f4c3 --- /dev/null +++ b/examples/angular/dynamic-devtools/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22" +} diff --git a/examples/angular/dynamic-devtools/.eslintrc.cjs b/examples/angular/dynamic-devtools/.eslintrc.cjs new file mode 100644 index 00000000000..cca134ce166 --- /dev/null +++ b/examples/angular/dynamic-devtools/.eslintrc.cjs @@ -0,0 +1,6 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = {} + +module.exports = config diff --git a/examples/angular/dynamic-devtools/README.md b/examples/angular/dynamic-devtools/README.md new file mode 100644 index 00000000000..fab7a9d9df2 --- /dev/null +++ b/examples/angular/dynamic-devtools/README.md @@ -0,0 +1,10 @@ +# TanStack Query Angular dynamic devtools example + +Devtools load automatically in development. In production builds, press **⌘ Ctrl Shift D** (macOS) to load them on demand. + +To run: + +- From the repo root: `pnpm install` then `pnpm --filter @tanstack/query-example-angular-dynamic-devtools start` +- From this folder: `pnpm start` + +For a production-like devserver (so you can try the shortcut), use `ng serve --configuration production`. diff --git a/examples/angular/dynamic-devtools/angular.json b/examples/angular/dynamic-devtools/angular.json new file mode 100644 index 00000000000..2c63d40fe77 --- /dev/null +++ b/examples/angular/dynamic-devtools/angular.json @@ -0,0 +1,130 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "dynamic-devtools": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "outputPath": "dist/dynamic-devtools", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "assets": ["src/favicon.ico"], + "styles": ["src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "dynamic-devtools:build:production" + }, + "development": { + "buildTarget": "dynamic-devtools:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n", + "options": { + "buildTarget": "dynamic-devtools:build" + } + } + } + } + }, + "schematics": { + "@schematics/angular:component": { + "type": "component" + }, + "@schematics/angular:directive": { + "type": "directive" + }, + "@schematics/angular:service": { + "type": "service" + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." + } + } +} diff --git a/examples/angular/dynamic-devtools/package.json b/examples/angular/dynamic-devtools/package.json new file mode 100644 index 00000000000..63f8e4f65ec --- /dev/null +++ b/examples/angular/dynamic-devtools/package.json @@ -0,0 +1,28 @@ +{ + "name": "@tanstack/query-example-angular-dynamic-devtools", + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "private": true, + "dependencies": { + "@angular/common": "^20.0.0", + "@angular/compiler": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/platform-browser": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", + "@tanstack/angular-query-experimental": "^5.97.0", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.16.0" + }, + "devDependencies": { + "@angular/build": "^20.0.0", + "@angular/cli": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "typescript": "5.9.3" + } +} diff --git a/examples/angular/dynamic-devtools/src/app/app.component.ts b/examples/angular/dynamic-devtools/src/app/app.component.ts new file mode 100644 index 00000000000..4f389242fb8 --- /dev/null +++ b/examples/angular/dynamic-devtools/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { DynamicDevtoolsExampleComponent } from './components/dynamic-devtools-example.component' + +@Component({ + selector: 'app-root', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [DynamicDevtoolsExampleComponent], + template: ``, +}) +export class AppComponent {} diff --git a/examples/angular/dynamic-devtools/src/app/app.config.ts b/examples/angular/dynamic-devtools/src/app/app.config.ts new file mode 100644 index 00000000000..d9d187d5e5b --- /dev/null +++ b/examples/angular/dynamic-devtools/src/app/app.config.ts @@ -0,0 +1,25 @@ +import { provideHttpClient, withFetch } from '@angular/common/http' +import { + QueryClient, + provideTanStackQuery, +} from '@tanstack/angular-query-experimental' +import { withDevtools } from '@tanstack/angular-query-devtools/production' +import type { ApplicationConfig } from '@angular/core' +import { DevtoolsOptionsManager } from './devtools-options.manager' + +export const appConfig: ApplicationConfig = { + providers: [ + provideHttpClient(withFetch()), + provideTanStackQuery( + new QueryClient(), + withDevtools( + (devToolsOptionsManager: DevtoolsOptionsManager) => ({ + loadDevtools: devToolsOptionsManager.loadDevtools(), + }), + { + deps: [DevtoolsOptionsManager], + }, + ), + ), + ], +} diff --git a/examples/angular/dynamic-devtools/src/app/components/dynamic-devtools-example.component.html b/examples/angular/dynamic-devtools/src/app/components/dynamic-devtools-example.component.html new file mode 100644 index 00000000000..9d68159ed16 --- /dev/null +++ b/examples/angular/dynamic-devtools/src/app/components/dynamic-devtools-example.component.html @@ -0,0 +1,18 @@ +

+ Devtools load in development automatically. For a production build, press + CtrlShiftD once to load them. +

+ +@if (query.isPending()) { +
Loading...
+} +@if (query.isError()) { +
An error has occurred: {{ query.error().message }}
+} +@if (query.data(); as data) { +

{{ data.name }}

+

{{ data.description }}

+ 👀 {{ data.subscribers_count }} + ✨ {{ data.stargazers_count }} + 🍴 {{ data.forks_count }} +} diff --git a/examples/angular/dynamic-devtools/src/app/components/dynamic-devtools-example.component.ts b/examples/angular/dynamic-devtools/src/app/components/dynamic-devtools-example.component.ts new file mode 100644 index 00000000000..a7797b33aea --- /dev/null +++ b/examples/angular/dynamic-devtools/src/app/components/dynamic-devtools-example.component.ts @@ -0,0 +1,29 @@ +import { HttpClient } from '@angular/common/http' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { injectQuery } from '@tanstack/angular-query-experimental' +import { lastValueFrom } from 'rxjs' + +interface Response { + name: string + description: string + subscribers_count: number + stargazers_count: number + forks_count: number +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'dynamic-devtools-example', + templateUrl: './dynamic-devtools-example.component.html', +}) +export class DynamicDevtoolsExampleComponent { + readonly #http = inject(HttpClient) + + readonly query = injectQuery(() => ({ + queryKey: ['repoData'], + queryFn: () => + lastValueFrom( + this.#http.get('https://api.github.com/repos/tanstack/query'), + ), + })) +} diff --git a/examples/angular/dynamic-devtools/src/app/devtools-options.manager.ts b/examples/angular/dynamic-devtools/src/app/devtools-options.manager.ts new file mode 100644 index 00000000000..23525f841ac --- /dev/null +++ b/examples/angular/dynamic-devtools/src/app/devtools-options.manager.ts @@ -0,0 +1,19 @@ +import { Injectable, isDevMode } from '@angular/core' +import { toSignal } from '@angular/core/rxjs-interop' +import { fromEvent, map, scan } from 'rxjs' + +@Injectable({ providedIn: 'root' }) +export class DevtoolsOptionsManager { + readonly loadDevtools = toSignal( + fromEvent(document, 'keydown').pipe( + map( + (event): boolean => + event.metaKey && event.ctrlKey && event.shiftKey && event.key === 'D', + ), + scan((acc, curr) => acc || curr, false), + ), + { + initialValue: false, + }, + ) +} diff --git a/examples/angular/dynamic-devtools/src/favicon.ico b/examples/angular/dynamic-devtools/src/favicon.ico new file mode 100644 index 00000000000..57614f9c967 Binary files /dev/null and b/examples/angular/dynamic-devtools/src/favicon.ico differ diff --git a/examples/angular/dynamic-devtools/src/index.html b/examples/angular/dynamic-devtools/src/index.html new file mode 100644 index 00000000000..9988a64448b --- /dev/null +++ b/examples/angular/dynamic-devtools/src/index.html @@ -0,0 +1,13 @@ + + + + + TanStack Query Angular dynamic devtools example + + + + + + + + diff --git a/examples/angular/dynamic-devtools/src/main.ts b/examples/angular/dynamic-devtools/src/main.ts new file mode 100644 index 00000000000..c3d8f9af997 --- /dev/null +++ b/examples/angular/dynamic-devtools/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) diff --git a/examples/angular/dynamic-devtools/src/styles.css b/examples/angular/dynamic-devtools/src/styles.css new file mode 100644 index 00000000000..256d518a5e1 --- /dev/null +++ b/examples/angular/dynamic-devtools/src/styles.css @@ -0,0 +1,18 @@ +/* You can add global styles to this file, and also import other style files */ + +.hint { + font-size: 0.9rem; + color: #444; + max-width: 42rem; + line-height: 1.45; +} + +kbd { + display: inline-block; + padding: 0.1rem 0.35rem; + margin: 0 0.1rem; + font-size: 0.8rem; + border: 1px solid #ccc; + border-radius: 3px; + background: #f7f7f7; +} diff --git a/examples/angular/dynamic-devtools/tsconfig.app.json b/examples/angular/dynamic-devtools/tsconfig.app.json new file mode 100644 index 00000000000..5b9d3c5ecb0 --- /dev/null +++ b/examples/angular/dynamic-devtools/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/dynamic-devtools/tsconfig.json b/examples/angular/dynamic-devtools/tsconfig.json new file mode 100644 index 00000000000..44e0a5238b1 --- /dev/null +++ b/examples/angular/dynamic-devtools/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "Bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictStandalone": true, + "strictTemplates": true + } +} diff --git a/examples/angular/infinite-query-with-max-pages/package.json b/examples/angular/infinite-query-with-max-pages/package.json index 80723c3c897..249f94cae10 100644 --- a/examples/angular/infinite-query-with-max-pages/package.json +++ b/examples/angular/infinite-query-with-max-pages/package.json @@ -13,6 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "rxjs": "^7.8.2", "tslib": "^2.8.1", @@ -22,6 +23,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/infinite-query-with-max-pages/src/app/app.config.ts b/examples/angular/infinite-query-with-max-pages/src/app/app.config.ts index 06854c4a7d4..377e2d12471 100644 --- a/examples/angular/infinite-query-with-max-pages/src/app/app.config.ts +++ b/examples/angular/infinite-query-with-max-pages/src/app/app.config.ts @@ -7,7 +7,7 @@ import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' import { projectsMockInterceptor } from './api/projects-mock.interceptor' import type { ApplicationConfig } from '@angular/core' diff --git a/examples/angular/optimistic-updates/package.json b/examples/angular/optimistic-updates/package.json index 6b9c369d19d..4b4d156c786 100644 --- a/examples/angular/optimistic-updates/package.json +++ b/examples/angular/optimistic-updates/package.json @@ -14,6 +14,7 @@ "@angular/core": "^20.0.0", "@angular/forms": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "rxjs": "^7.8.2", "tslib": "^2.8.1", @@ -23,6 +24,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/optimistic-updates/src/app/app.config.ts b/examples/angular/optimistic-updates/src/app/app.config.ts index b9b2b6c36fa..1b9deaa3dd7 100644 --- a/examples/angular/optimistic-updates/src/app/app.config.ts +++ b/examples/angular/optimistic-updates/src/app/app.config.ts @@ -7,7 +7,7 @@ import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' import { mockInterceptor } from './interceptor/mock-api.interceptor' import type { ApplicationConfig } from '@angular/core' diff --git a/examples/angular/pagination/package.json b/examples/angular/pagination/package.json index 1833e486f7d..9e945ab19a8 100644 --- a/examples/angular/pagination/package.json +++ b/examples/angular/pagination/package.json @@ -13,6 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "rxjs": "^7.8.2", "tslib": "^2.8.1", @@ -22,6 +23,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/pagination/src/app/app.config.ts b/examples/angular/pagination/src/app/app.config.ts index dc3189ba84b..0344e32f25c 100644 --- a/examples/angular/pagination/src/app/app.config.ts +++ b/examples/angular/pagination/src/app/app.config.ts @@ -7,7 +7,7 @@ import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' import { projectsMockInterceptor } from './api/projects-mock.interceptor' import type { ApplicationConfig } from '@angular/core' diff --git a/examples/angular/query-options-from-a-service/package.json b/examples/angular/query-options-from-a-service/package.json index 784a015ae3f..546a5ff8d9a 100644 --- a/examples/angular/query-options-from-a-service/package.json +++ b/examples/angular/query-options-from-a-service/package.json @@ -14,6 +14,7 @@ "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "rxjs": "^7.8.2", "tslib": "^2.8.1", @@ -23,6 +24,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/query-options-from-a-service/src/app/app.config.ts b/examples/angular/query-options-from-a-service/src/app/app.config.ts index dd6675f997b..8418196fe7d 100644 --- a/examples/angular/query-options-from-a-service/src/app/app.config.ts +++ b/examples/angular/query-options-from-a-service/src/app/app.config.ts @@ -5,7 +5,7 @@ import { provideTanStackQuery, } from '@tanstack/angular-query-experimental' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' import { routes } from './app.routes' import type { ApplicationConfig } from '@angular/core' diff --git a/examples/angular/router/package.json b/examples/angular/router/package.json index e8f9a881ffc..2d21e2f41af 100644 --- a/examples/angular/router/package.json +++ b/examples/angular/router/package.json @@ -14,6 +14,7 @@ "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", "@angular/router": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "rxjs": "^7.8.2", "tslib": "^2.8.1", @@ -23,6 +24,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/router/src/app/app.config.ts b/examples/angular/router/src/app/app.config.ts index 3fbec800b1f..32b0cb484cc 100644 --- a/examples/angular/router/src/app/app.config.ts +++ b/examples/angular/router/src/app/app.config.ts @@ -5,7 +5,7 @@ import { provideTanStackQuery, } from '@tanstack/angular-query-experimental' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' import { routes } from './app.routes' import type { ApplicationConfig } from '@angular/core' diff --git a/examples/angular/rxjs/package.json b/examples/angular/rxjs/package.json index 0758231913e..69b2e31866d 100644 --- a/examples/angular/rxjs/package.json +++ b/examples/angular/rxjs/package.json @@ -14,6 +14,7 @@ "@angular/core": "^20.0.0", "@angular/forms": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "rxjs": "^7.8.2", "tslib": "^2.8.1", @@ -23,6 +24,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/rxjs/src/app/app.config.ts b/examples/angular/rxjs/src/app/app.config.ts index 27eb15a53b1..ef7dbb8e5e3 100644 --- a/examples/angular/rxjs/src/app/app.config.ts +++ b/examples/angular/rxjs/src/app/app.config.ts @@ -7,7 +7,7 @@ import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' import { autocompleteMockInterceptor } from './api/autocomplete-mock.interceptor' import type { ApplicationConfig } from '@angular/core' diff --git a/examples/angular/simple/package.json b/examples/angular/simple/package.json index a4e4b39c567..5443fbdfe67 100644 --- a/examples/angular/simple/package.json +++ b/examples/angular/simple/package.json @@ -13,6 +13,7 @@ "@angular/compiler": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", "@tanstack/angular-query-experimental": "^5.97.0", "rxjs": "^7.8.2", "tslib": "^2.8.1", @@ -22,6 +23,6 @@ "@angular/build": "^20.0.0", "@angular/cli": "^20.0.0", "@angular/compiler-cli": "^20.0.0", - "typescript": "5.8.3" + "typescript": "5.9.3" } } diff --git a/examples/angular/simple/src/app/app.config.ts b/examples/angular/simple/src/app/app.config.ts index 9c7eda25974..2eb412e0dca 100644 --- a/examples/angular/simple/src/app/app.config.ts +++ b/examples/angular/simple/src/app/app.config.ts @@ -3,7 +3,7 @@ import { QueryClient, provideTanStackQuery, } from '@tanstack/angular-query-experimental' -import { withDevtools } from '@tanstack/angular-query-experimental/devtools' +import { withDevtools } from '@tanstack/angular-query-devtools' import type { ApplicationConfig } from '@angular/core' export const appConfig: ApplicationConfig = { diff --git a/examples/angular/ssr-persist/.devcontainer/devcontainer.json b/examples/angular/ssr-persist/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..365adf8f4c3 --- /dev/null +++ b/examples/angular/ssr-persist/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22" +} diff --git a/examples/angular/ssr-persist/.eslintrc.cjs b/examples/angular/ssr-persist/.eslintrc.cjs new file mode 100644 index 00000000000..cca134ce166 --- /dev/null +++ b/examples/angular/ssr-persist/.eslintrc.cjs @@ -0,0 +1,6 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = {} + +module.exports = config diff --git a/examples/angular/ssr-persist/README.md b/examples/angular/ssr-persist/README.md new file mode 100644 index 00000000000..4be92f2b947 --- /dev/null +++ b/examples/angular/ssr-persist/README.md @@ -0,0 +1,14 @@ +# TanStack Query Angular SSR + persistence example + +Combines [SSR/hydration](https://tanstack.com/query/latest/docs/framework/angular/guides/ssr), a `localStorage` persister, and a **client-only island**: `ClientPersistDemoComponent` is mounted with `afterNextRender`, so its `injectQuery` `queryFn` does not run during SSR (unlike `@defer` main content, which is still rendered on the server for incremental hydration). + +- **Server:** same bootstrap config shape as the base SSR example; `withPersistQueryClient` uses a factory and skips work when not in the browser. +- **Client:** `showClientDemo` is set to `true` in `afterNextRender`, then `` is created — optional `dehydrateOptions.shouldDehydrateQuery` can scope persistence to `client-persist` query keys only. + +To run: + +- From the repo root: `pnpm install` then + `pnpm --filter @tanstack/query-example-angular-ssr-persist start` +- Production SSR server after build: + `pnpm --filter @tanstack/query-example-angular-ssr-persist run build` + then `pnpm --filter @tanstack/query-example-angular-ssr-persist run serve:ssr` diff --git a/examples/angular/ssr-persist/angular.json b/examples/angular/ssr-persist/angular.json new file mode 100644 index 00000000000..bc7b35f9a82 --- /dev/null +++ b/examples/angular/ssr-persist/angular.json @@ -0,0 +1,137 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "ssr-persist": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "outputPath": "dist/ssr-persist", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "styles": ["src/styles.css"], + "scripts": [], + "server": "src/main.server.ts", + "outputMode": "server", + "security": { + "allowedHosts": ["localhost", "127.0.0.1"] + }, + "ssr": { + "entry": "src/server.ts" + } + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "ssr-persist:build:production" + }, + "development": { + "buildTarget": "ssr-persist:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n", + "options": { + "buildTarget": "ssr-persist:build" + } + } + } + } + }, + "schematics": { + "@schematics/angular:component": { + "type": "component" + }, + "@schematics/angular:directive": { + "type": "directive" + }, + "@schematics/angular:service": { + "type": "service" + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." + } + } +} diff --git a/examples/angular/ssr-persist/package.json b/examples/angular/ssr-persist/package.json new file mode 100644 index 00000000000..c6e6ff80521 --- /dev/null +++ b/examples/angular/ssr-persist/package.json @@ -0,0 +1,35 @@ +{ + "name": "@tanstack/query-example-angular-ssr-persist", + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "serve:ssr": "node dist/ssr-persist/server/server.mjs" + }, + "private": true, + "dependencies": { + "@angular/common": "^20.0.0", + "@angular/compiler": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/platform-browser": "^20.0.0", + "@angular/platform-server": "^20.0.0", + "@angular/ssr": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", + "@tanstack/angular-query-experimental": "^5.97.0", + "@tanstack/angular-query-persist-client": "^5.97.0", + "@tanstack/query-async-storage-persister": "^5.97.0", + "express": "^5.1.0", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.16.1" + }, + "devDependencies": { + "@angular/build": "^20.0.0", + "@angular/cli": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "@types/express": "^5.0.1", + "typescript": "5.9.3" + } +} diff --git a/examples/angular/ssr-persist/src/app/app.component.ts b/examples/angular/ssr-persist/src/app/app.component.ts new file mode 100644 index 00000000000..ffdde21e388 --- /dev/null +++ b/examples/angular/ssr-persist/src/app/app.component.ts @@ -0,0 +1,127 @@ +import { + ChangeDetectionStrategy, + Component, + afterNextRender, + signal, +} from '@angular/core' +import { ClientPersistDemoComponent } from './components/client-persist-demo.component' +import { PostsComponent } from './components/posts.component' + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'ssr-persist-example', + imports: [ClientPersistDemoComponent, PostsComponent], + template: ` +
+
+

Angular SSR + client-only island + persistence

+

SSR queries vs client-only persisted queries

+

+ Posts use queryKey: ['posts'] and run on the server. + Configure the persister with + dehydrateOptions.shouldDehydrateQuery if you want only + client-persist keys in localStorage. +

+

+ The panel below is mounted only in the browser via + afterNextRender, so its query never runs during SSR. + After the first visit, hard-reload: the timestamp can be restored from + persistence while posts render from SSR again. +

+
+ +
+

Server data

+ +
+ +
+

Client-only (afterNextRender)

+ @if (showClientDemo()) { + + } @else { +

+ Server render: this placeholder has no + client-persist-demo component yet — it is created after + the first browser frame via afterNextRender. +

+ } +
+
+ `, + styles: ` + .page { + max-width: 960px; + margin: 0 auto; + padding: 48px 20px 72px; + } + + .hero { + margin-bottom: 32px; + } + + .eyebrow { + margin: 0 0 8px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #9b5c00; + } + + h1 { + margin: 0 0 12px; + font-size: clamp(2.2rem, 5vw, 4rem); + line-height: 1; + } + + .lede { + max-width: 640px; + margin: 0 0 12px; + font-size: 1rem; + line-height: 1.6; + color: #4b5563; + } + + .hint { + font-size: 0.95rem; + color: #6b7280; + } + + code { + font-size: 0.88em; + padding: 0.1em 0.35em; + border-radius: 4px; + background: #f3f4f6; + } + + .region { + margin-bottom: 40px; + } + + .region-title { + margin: 0 0 16px; + font-size: 1.25rem; + color: #1f2937; + } + + .placeholder { + margin: 0; + padding: 16px; + border-radius: 12px; + background: #f9fafb; + color: #6b7280; + line-height: 1.5; + } + `, +}) +export class SsrPersistExampleComponent { + /** When `true`, `ClientPersistDemoComponent` exists only in the browser (not during SSR). */ + readonly showClientDemo = signal(false) + + constructor() { + afterNextRender(() => { + this.showClientDemo.set(true) + }) + } +} diff --git a/examples/angular/ssr-persist/src/app/app.config.server.ts b/examples/angular/ssr-persist/src/app/app.config.server.ts new file mode 100644 index 00000000000..a1a957f51c4 --- /dev/null +++ b/examples/angular/ssr-persist/src/app/app.config.server.ts @@ -0,0 +1,8 @@ +import { mergeApplicationConfig } from '@angular/core' +import { provideServerRendering, withRoutes } from '@angular/ssr' +import { appConfig } from './app.config' +import { serverRoutes } from './app.routes.server' + +export const serverConfig = mergeApplicationConfig(appConfig, { + providers: [provideServerRendering(withRoutes(serverRoutes))], +}) diff --git a/examples/angular/ssr-persist/src/app/app.config.ts b/examples/angular/ssr-persist/src/app/app.config.ts new file mode 100644 index 00000000000..aed263640be --- /dev/null +++ b/examples/angular/ssr-persist/src/app/app.config.ts @@ -0,0 +1,31 @@ +import { provideHttpClient, withFetch } from '@angular/common/http' +import type { ApplicationConfig } from '@angular/core' +import { + provideClientHydration, + withEventReplay, +} from '@angular/platform-browser' +import { provideTanStackQuery } from '@tanstack/angular-query-experimental' +import { withDevtools } from '@tanstack/angular-query-devtools' +import { withPersistQueryClient } from '@tanstack/angular-query-persist-client' +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister' +import { PERSIST_STORAGE_KEY, QUERY_CLIENT } from './query-client' + +export const appConfig: ApplicationConfig = { + providers: [ + provideHttpClient(withFetch()), + provideClientHydration(withEventReplay()), + provideTanStackQuery( + QUERY_CLIENT, + withDevtools(), + withPersistQueryClient(() => ({ + persistOptions: { + persister: createAsyncStoragePersister({ + storage: localStorage, + key: PERSIST_STORAGE_KEY, + throttleTime: 1000, + }), + }, + })), + ), + ], +} diff --git a/examples/angular/ssr-persist/src/app/app.routes.server.ts b/examples/angular/ssr-persist/src/app/app.routes.server.ts new file mode 100644 index 00000000000..09fcfa3cc95 --- /dev/null +++ b/examples/angular/ssr-persist/src/app/app.routes.server.ts @@ -0,0 +1,9 @@ +import { RenderMode } from '@angular/ssr' +import type { ServerRoute } from '@angular/ssr' + +export const serverRoutes: Array = [ + { + path: '**', + renderMode: RenderMode.Server, + }, +] diff --git a/examples/angular/ssr-persist/src/app/components/client-persist-demo.component.ts b/examples/angular/ssr-persist/src/app/components/client-persist-demo.component.ts new file mode 100644 index 00000000000..b20aa84f39c --- /dev/null +++ b/examples/angular/ssr-persist/src/app/components/client-persist-demo.component.ts @@ -0,0 +1,84 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { injectQuery } from '@tanstack/angular-query-experimental' +import { CLIENT_PERSIST_QUERY_ROOT } from '../query-persist-scope' + +/** + * Mounted only in the browser (parent uses `afterNextRender`), so `queryFn` is not run during SSR. + * Pair with `dehydrateOptions.shouldDehydrateQuery` if you want only `client-persist` keys in storage. + */ +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'client-persist-demo', + template: ` +
+

Client-only island

+

Client-only persisted query

+ @if (demo.isPending()) { +

Loading client-only data…

+ } @else if (demo.isError()) { +

Failed to load.

+ } @else if (demo.data(); as data) { +

{{ data.createdAt }}

+

+ This value is stored under the client-persist query key. + Hard-reload the page: it should reappear from persistence while the + posts list above is rendered from SSR again. +

+ } +
+ `, + styles: ` + .panel { + margin-top: 8px; + padding: 20px; + border-radius: 18px; + border: 1px dashed #c4b5a0; + background: #fffefb; + } + + .badge { + margin: 0 0 8px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: #7c5a10; + } + + h3 { + margin: 0 0 12px; + font-size: 1.1rem; + } + + .mono { + margin: 0 0 12px; + font-family: ui-monospace, monospace; + font-size: 0.85rem; + word-break: break-all; + } + + .note { + margin: 0; + font-size: 0.9rem; + line-height: 1.5; + color: #4b5563; + } + + code { + font-size: 0.88em; + padding: 0.1em 0.35em; + border-radius: 4px; + background: #f3f4f6; + } + `, +}) +export class ClientPersistDemoComponent { + readonly demo = injectQuery(() => ({ + queryKey: [CLIENT_PERSIST_QUERY_ROOT, 'timestamp-demo'], + queryFn: async () => { + await new Promise((r) => setTimeout(r, 100)) + return { createdAt: new Date().toISOString() } + }, + staleTime: Infinity, + })) +} diff --git a/examples/angular/ssr-persist/src/app/components/posts.component.ts b/examples/angular/ssr-persist/src/app/components/posts.component.ts new file mode 100644 index 00000000000..6c87c6e9b43 --- /dev/null +++ b/examples/angular/ssr-persist/src/app/components/posts.component.ts @@ -0,0 +1,70 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { injectQuery } from '@tanstack/angular-query-experimental' +import { lastValueFrom } from 'rxjs' +import { PostsService } from '../services/posts.service' + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'posts', + template: ` + @if (postsQuery.isPending()) { +

Loading posts...

+ } @else if (postsQuery.isError()) { +

Failed to load posts.

+ } @else { +
+ @for (post of postsQuery.data(); track post.id) { +
+

Post #{{ post.id }}

+

{{ post.title }}

+

{{ post.body }}

+
+ } +
+ } + `, + styles: ` + .grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + } + + .card { + padding: 20px; + border: 1px solid #eadfcb; + border-radius: 18px; + background: #fffdf8; + box-shadow: 0 12px 30px rgba(123, 88, 31, 0.08); + } + + .meta { + margin: 0 0 10px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #8b5e34; + } + + h2 { + margin: 0 0 10px; + font-size: 1.1rem; + line-height: 1.3; + } + + p { + margin: 0; + line-height: 1.5; + color: #374151; + } + `, +}) +export class PostsComponent { + readonly #postsService = inject(PostsService) + + readonly postsQuery = injectQuery(() => ({ + queryKey: ['posts'], + queryFn: () => lastValueFrom(this.#postsService.allPosts$()), + })) +} diff --git a/examples/angular/ssr-persist/src/app/query-client.ts b/examples/angular/ssr-persist/src/app/query-client.ts new file mode 100644 index 00000000000..8402e8711a6 --- /dev/null +++ b/examples/angular/ssr-persist/src/app/query-client.ts @@ -0,0 +1,22 @@ +import { isPlatformServer } from '@angular/common' +import { InjectionToken, PLATFORM_ID, inject } from '@angular/core' +import { QueryClient } from '@tanstack/angular-query-experimental' + +export const PERSIST_STORAGE_KEY = 'tanstack-query-angular-ssr-persist-example' + +export const SHARED_QUERY_DEFAULTS = { + staleTime: 1000 * 30, + gcTime: 1000 * 60 * 60 * 24, +} as const + +export const QUERY_CLIENT = new InjectionToken('QUERY_CLIENT', { + factory: () => + new QueryClient({ + defaultOptions: { + queries: { + ...SHARED_QUERY_DEFAULTS, + ...(isPlatformServer(inject(PLATFORM_ID)) ? { retry: false } : {}), + }, + }, + }), +}) diff --git a/examples/angular/ssr-persist/src/app/query-persist-scope.ts b/examples/angular/ssr-persist/src/app/query-persist-scope.ts new file mode 100644 index 00000000000..08a420f7932 --- /dev/null +++ b/examples/angular/ssr-persist/src/app/query-persist-scope.ts @@ -0,0 +1,5 @@ +/** + * Query key prefix for data that should be written to `localStorage` by the global persister. + * SSR queries (e.g. `['posts']`) use different keys and are excluded in `dehydrateOptions`. + */ +export const CLIENT_PERSIST_QUERY_ROOT = 'client-persist' as const diff --git a/examples/angular/ssr-persist/src/app/services/posts.service.ts b/examples/angular/ssr-persist/src/app/services/posts.service.ts new file mode 100644 index 00000000000..edb8e084f0e --- /dev/null +++ b/examples/angular/ssr-persist/src/app/services/posts.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core' +import { of, tap } from 'rxjs' +import { delay } from 'rxjs/operators' + +@Injectable({ + providedIn: 'root', +}) +export class PostsService { + allPosts$ = () => + of(posts).pipe( + tap(() => console.log('fetching posts')), + delay(50), + ) +} + +export interface Post { + id: number + title: string + body: string +} + +const posts: Array = [ + { + id: 1, + title: 'Render on the server', + body: 'The initial HTML is produced by Angular SSR before the browser bootstraps the app.', + }, + { + id: 2, + title: 'Hydrate on the client', + body: 'Angular reuses the rendered DOM and turns it into a live client application.', + }, + { + id: 3, + title: 'Query on both sides', + body: 'TanStack Query runs during SSR, hydrates on the client, and the persisted cache can speed up the next full load.', + }, + { + id: 3.5, + title: 'Persist after hydration', + body: 'withPersistQueryClient uses a factory so localStorage is only touched in the browser, not during server render.', + }, + { + id: 4, + title: 'No API needed', + body: 'This example uses a deterministic in-memory data source so the SSR example works without external network access.', + }, + { + id: 5, + title: 'Keep the setup small', + body: 'Only the Angular CLI SSR pieces remain: main.server.ts, server.ts and the server application config.', + }, + { + id: 6, + title: 'Match the other examples', + body: 'The rest of the app keeps the same lightweight structure as the existing Angular examples in this repo.', + }, + { + id: 7, + title: 'Persist after hydration', + body: 'withPersistQueryClient uses a factory so localStorage is only touched in the browser, not during server render.', + }, +] diff --git a/examples/angular/ssr-persist/src/index.html b/examples/angular/ssr-persist/src/index.html new file mode 100644 index 00000000000..c00f1bf3ca5 --- /dev/null +++ b/examples/angular/ssr-persist/src/index.html @@ -0,0 +1,12 @@ + + + + + TanStack Query Angular SSR + persist example + + + + + + + diff --git a/examples/angular/ssr-persist/src/main.server.ts b/examples/angular/ssr-persist/src/main.server.ts new file mode 100644 index 00000000000..1bdb237f23f --- /dev/null +++ b/examples/angular/ssr-persist/src/main.server.ts @@ -0,0 +1,11 @@ +import { + bootstrapApplication, + type BootstrapContext, +} from '@angular/platform-browser' +import { serverConfig } from './app/app.config.server' +import { SsrPersistExampleComponent } from './app/app.component' + +const bootstrap = (context: BootstrapContext) => + bootstrapApplication(SsrPersistExampleComponent, serverConfig, context) + +export default bootstrap diff --git a/examples/angular/ssr-persist/src/main.ts b/examples/angular/ssr-persist/src/main.ts new file mode 100644 index 00000000000..9c2c5ea1079 --- /dev/null +++ b/examples/angular/ssr-persist/src/main.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { SsrPersistExampleComponent } from './app/app.component' + +bootstrapApplication(SsrPersistExampleComponent, appConfig).catch((err) => + console.error(err), +) diff --git a/examples/angular/ssr-persist/src/server.ts b/examples/angular/ssr-persist/src/server.ts new file mode 100644 index 00000000000..db670c6eb0d --- /dev/null +++ b/examples/angular/ssr-persist/src/server.ts @@ -0,0 +1,43 @@ +import { + AngularNodeAppEngine, + createNodeRequestHandler, + isMainModule, + writeResponseToNodeResponse, +} from '@angular/ssr/node' +import express from 'express' +import { join } from 'node:path' + +const browserDistFolder = join(import.meta.dirname, '../browser') + +const app = express() +const angularApp = new AngularNodeAppEngine() + +app.use( + express.static(browserDistFolder, { + maxAge: '1y', + index: false, + redirect: false, + }), +) + +app.use((req, res, next) => { + angularApp + .handle(req) + .then((response) => + response ? writeResponseToNodeResponse(response, res) : next(), + ) + .catch(next) +}) + +if (isMainModule(import.meta.url)) { + const port = process.env['PORT'] || 4000 + app.listen(port, (error) => { + if (error) { + throw error + } + + console.log(`Node Express server listening on http://localhost:${port}`) + }) +} + +export const reqHandler = createNodeRequestHandler(app) diff --git a/examples/angular/ssr-persist/src/styles.css b/examples/angular/ssr-persist/src/styles.css new file mode 100644 index 00000000000..6718c269c49 --- /dev/null +++ b/examples/angular/ssr-persist/src/styles.css @@ -0,0 +1,16 @@ +body { + margin: 0; + font-family: + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + background: #faf8f2; + color: #1b1f23; +} + +button { + font: inherit; +} diff --git a/examples/angular/ssr-persist/tsconfig.app.json b/examples/angular/ssr-persist/tsconfig.app.json new file mode 100644 index 00000000000..00e9b764198 --- /dev/null +++ b/examples/angular/ssr-persist/tsconfig.app.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/examples/angular/ssr-persist/tsconfig.json b/examples/angular/ssr-persist/tsconfig.json new file mode 100644 index 00000000000..5aa05d2d231 --- /dev/null +++ b/examples/angular/ssr-persist/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "typeCheckHostBindings": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/examples/angular/ssr/.devcontainer/devcontainer.json b/examples/angular/ssr/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..365adf8f4c3 --- /dev/null +++ b/examples/angular/ssr/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "Node.js", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22" +} diff --git a/examples/angular/ssr/.eslintrc.cjs b/examples/angular/ssr/.eslintrc.cjs new file mode 100644 index 00000000000..cca134ce166 --- /dev/null +++ b/examples/angular/ssr/.eslintrc.cjs @@ -0,0 +1,6 @@ +// @ts-check + +/** @type {import('eslint').Linter.Config} */ +const config = {} + +module.exports = config diff --git a/examples/angular/ssr/README.md b/examples/angular/ssr/README.md new file mode 100644 index 00000000000..b15e6f599d1 --- /dev/null +++ b/examples/angular/ssr/README.md @@ -0,0 +1,6 @@ +# TanStack Query Angular SSR example + +To run this example: + +- `npm install` or `yarn` or `pnpm i` or `bun i` +- `npm run start` or `yarn start` or `pnpm start` or `bun start` diff --git a/examples/angular/ssr/angular.json b/examples/angular/ssr/angular.json new file mode 100644 index 00000000000..3de24182b5f --- /dev/null +++ b/examples/angular/ssr/angular.json @@ -0,0 +1,137 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "pnpm", + "analytics": false, + "cache": { + "enabled": false + } + }, + "newProjectRoot": "projects", + "projects": { + "ssr": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "outputPath": "dist/ssr", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "styles": ["src/styles.css"], + "scripts": [], + "server": "src/main.server.ts", + "outputMode": "server", + "security": { + "allowedHosts": ["localhost", "127.0.0.1"] + }, + "ssr": { + "entry": "src/server.ts" + } + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "ssr:build:production" + }, + "development": { + "buildTarget": "ssr:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n", + "options": { + "buildTarget": "ssr:build" + } + } + } + } + }, + "schematics": { + "@schematics/angular:component": { + "type": "component" + }, + "@schematics/angular:directive": { + "type": "directive" + }, + "@schematics/angular:service": { + "type": "service" + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." + } + } +} diff --git a/examples/angular/ssr/package.json b/examples/angular/ssr/package.json new file mode 100644 index 00000000000..a8b60f64218 --- /dev/null +++ b/examples/angular/ssr/package.json @@ -0,0 +1,33 @@ +{ + "name": "@tanstack/query-example-angular-ssr", + "type": "module", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "serve:ssr": "node dist/ssr/server/server.mjs" + }, + "private": true, + "dependencies": { + "@angular/common": "^20.0.0", + "@angular/compiler": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/platform-browser": "^20.0.0", + "@angular/platform-server": "^20.0.0", + "@angular/ssr": "^20.0.0", + "@tanstack/angular-query-devtools": "^5.97.0", + "@tanstack/angular-query-experimental": "^5.97.0", + "express": "^5.1.0", + "rxjs": "^7.8.2", + "tslib": "^2.8.1", + "zone.js": "0.16.1" + }, + "devDependencies": { + "@angular/build": "^20.0.0", + "@angular/cli": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "@types/express": "^5.0.1", + "typescript": "5.9.3" + } +} diff --git a/examples/angular/ssr/src/app/app.component.ts b/examples/angular/ssr/src/app/app.component.ts new file mode 100644 index 00000000000..1f17c32569c --- /dev/null +++ b/examples/angular/ssr/src/app/app.component.ts @@ -0,0 +1,57 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { PostsComponent } from './components/posts.component' + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'ssr-example', + imports: [PostsComponent], + template: ` +
+
+

Angular SSR

+

Server-rendered posts with TanStack Query

+

+ The first render comes from the server. Angular then hydrates the app + on the client and TanStack Query continues from a fresh client cache. +

+
+ + +
+ `, + styles: ` + .page { + max-width: 960px; + margin: 0 auto; + padding: 48px 20px 72px; + } + + .hero { + margin-bottom: 32px; + } + + .eyebrow { + margin: 0 0 8px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #9b5c00; + } + + h1 { + margin: 0 0 12px; + font-size: clamp(2.2rem, 5vw, 4rem); + line-height: 1; + } + + .lede { + max-width: 640px; + margin: 0; + font-size: 1rem; + line-height: 1.6; + color: #4b5563; + } + `, +}) +export class SsrExampleComponent {} diff --git a/examples/angular/ssr/src/app/app.config.server.ts b/examples/angular/ssr/src/app/app.config.server.ts new file mode 100644 index 00000000000..a1a957f51c4 --- /dev/null +++ b/examples/angular/ssr/src/app/app.config.server.ts @@ -0,0 +1,8 @@ +import { mergeApplicationConfig } from '@angular/core' +import { provideServerRendering, withRoutes } from '@angular/ssr' +import { appConfig } from './app.config' +import { serverRoutes } from './app.routes.server' + +export const serverConfig = mergeApplicationConfig(appConfig, { + providers: [provideServerRendering(withRoutes(serverRoutes))], +}) diff --git a/examples/angular/ssr/src/app/app.config.ts b/examples/angular/ssr/src/app/app.config.ts new file mode 100644 index 00000000000..48edd17083b --- /dev/null +++ b/examples/angular/ssr/src/app/app.config.ts @@ -0,0 +1,17 @@ +import { provideHttpClient, withFetch } from '@angular/common/http' +import type { ApplicationConfig } from '@angular/core' +import { + provideClientHydration, + withEventReplay, +} from '@angular/platform-browser' +import { provideTanStackQuery } from '@tanstack/angular-query-experimental' +import { withDevtools } from '@tanstack/angular-query-devtools' +import { QUERY_CLIENT } from './query-client' + +export const appConfig: ApplicationConfig = { + providers: [ + provideHttpClient(withFetch()), + provideClientHydration(withEventReplay()), + provideTanStackQuery(QUERY_CLIENT, withDevtools()), + ], +} diff --git a/examples/angular/ssr/src/app/app.routes.server.ts b/examples/angular/ssr/src/app/app.routes.server.ts new file mode 100644 index 00000000000..09fcfa3cc95 --- /dev/null +++ b/examples/angular/ssr/src/app/app.routes.server.ts @@ -0,0 +1,9 @@ +import { RenderMode } from '@angular/ssr' +import type { ServerRoute } from '@angular/ssr' + +export const serverRoutes: Array = [ + { + path: '**', + renderMode: RenderMode.Server, + }, +] diff --git a/examples/angular/ssr/src/app/components/posts.component.ts b/examples/angular/ssr/src/app/components/posts.component.ts new file mode 100644 index 00000000000..6c87c6e9b43 --- /dev/null +++ b/examples/angular/ssr/src/app/components/posts.component.ts @@ -0,0 +1,70 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { injectQuery } from '@tanstack/angular-query-experimental' +import { lastValueFrom } from 'rxjs' +import { PostsService } from '../services/posts.service' + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'posts', + template: ` + @if (postsQuery.isPending()) { +

Loading posts...

+ } @else if (postsQuery.isError()) { +

Failed to load posts.

+ } @else { +
+ @for (post of postsQuery.data(); track post.id) { +
+

Post #{{ post.id }}

+

{{ post.title }}

+

{{ post.body }}

+
+ } +
+ } + `, + styles: ` + .grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + } + + .card { + padding: 20px; + border: 1px solid #eadfcb; + border-radius: 18px; + background: #fffdf8; + box-shadow: 0 12px 30px rgba(123, 88, 31, 0.08); + } + + .meta { + margin: 0 0 10px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #8b5e34; + } + + h2 { + margin: 0 0 10px; + font-size: 1.1rem; + line-height: 1.3; + } + + p { + margin: 0; + line-height: 1.5; + color: #374151; + } + `, +}) +export class PostsComponent { + readonly #postsService = inject(PostsService) + + readonly postsQuery = injectQuery(() => ({ + queryKey: ['posts'], + queryFn: () => lastValueFrom(this.#postsService.allPosts$()), + })) +} diff --git a/examples/angular/ssr/src/app/query-client.ts b/examples/angular/ssr/src/app/query-client.ts new file mode 100644 index 00000000000..09aa8389b3e --- /dev/null +++ b/examples/angular/ssr/src/app/query-client.ts @@ -0,0 +1,20 @@ +import { isPlatformServer } from '@angular/common' +import { InjectionToken, PLATFORM_ID, inject } from '@angular/core' +import { QueryClient } from '@tanstack/angular-query-experimental' + +export const SHARED_QUERY_DEFAULTS = { + staleTime: 1000 * 30, + gcTime: 1000 * 60 * 60 * 24, +} as const + +export const QUERY_CLIENT = new InjectionToken('QUERY_CLIENT', { + factory: () => + new QueryClient({ + defaultOptions: { + queries: { + ...SHARED_QUERY_DEFAULTS, + ...(isPlatformServer(inject(PLATFORM_ID)) ? { retry: false } : {}), + }, + }, + }), +}) diff --git a/examples/angular/ssr/src/app/services/posts.service.ts b/examples/angular/ssr/src/app/services/posts.service.ts new file mode 100644 index 00000000000..8f7981cbccf --- /dev/null +++ b/examples/angular/ssr/src/app/services/posts.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core' +import { of, tap } from 'rxjs' +import { delay } from 'rxjs/operators' + +@Injectable({ + providedIn: 'root', +}) +export class PostsService { + allPosts$ = () => + of(posts).pipe( + tap(() => console.log('fetching posts')), + delay(50), + ) +} + +export interface Post { + id: number + title: string + body: string +} + +const posts: Array = [ + { + id: 1, + title: 'Render on the server', + body: 'The initial HTML is produced by Angular SSR before the browser bootstraps the app.', + }, + { + id: 2, + title: 'Hydrate on the client', + body: 'Angular reuses the rendered DOM and turns it into a live client application.', + }, + { + id: 3, + title: 'Query on both sides', + body: 'TanStack Query resolves during server rendering here, then the browser starts with a fresh client cache.', + }, + { + id: 4, + title: 'No API needed', + body: 'This example uses a deterministic in-memory data source so the SSR example works without external network access.', + }, + { + id: 5, + title: 'Keep the setup small', + body: 'Only the Angular CLI SSR pieces remain: main.server.ts, server.ts and the server application config.', + }, + { + id: 6, + title: 'Match the other examples', + body: 'The rest of the app keeps the same lightweight structure as the existing Angular examples in this repo.', + }, +] diff --git a/examples/angular/ssr/src/index.html b/examples/angular/ssr/src/index.html new file mode 100644 index 00000000000..ab7606ebbf6 --- /dev/null +++ b/examples/angular/ssr/src/index.html @@ -0,0 +1,12 @@ + + + + + TanStack Query Angular SSR Example + + + + + + + diff --git a/examples/angular/ssr/src/main.server.ts b/examples/angular/ssr/src/main.server.ts new file mode 100644 index 00000000000..df44a7c47b7 --- /dev/null +++ b/examples/angular/ssr/src/main.server.ts @@ -0,0 +1,11 @@ +import { + bootstrapApplication, + type BootstrapContext, +} from '@angular/platform-browser' +import { serverConfig } from './app/app.config.server' +import { SsrExampleComponent } from './app/app.component' + +const bootstrap = (context: BootstrapContext) => + bootstrapApplication(SsrExampleComponent, serverConfig, context) + +export default bootstrap diff --git a/examples/angular/ssr/src/main.ts b/examples/angular/ssr/src/main.ts new file mode 100644 index 00000000000..f63fdacda8d --- /dev/null +++ b/examples/angular/ssr/src/main.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { SsrExampleComponent } from './app/app.component' + +bootstrapApplication(SsrExampleComponent, appConfig).catch((err) => + console.error(err), +) diff --git a/examples/angular/ssr/src/server.ts b/examples/angular/ssr/src/server.ts new file mode 100644 index 00000000000..db670c6eb0d --- /dev/null +++ b/examples/angular/ssr/src/server.ts @@ -0,0 +1,43 @@ +import { + AngularNodeAppEngine, + createNodeRequestHandler, + isMainModule, + writeResponseToNodeResponse, +} from '@angular/ssr/node' +import express from 'express' +import { join } from 'node:path' + +const browserDistFolder = join(import.meta.dirname, '../browser') + +const app = express() +const angularApp = new AngularNodeAppEngine() + +app.use( + express.static(browserDistFolder, { + maxAge: '1y', + index: false, + redirect: false, + }), +) + +app.use((req, res, next) => { + angularApp + .handle(req) + .then((response) => + response ? writeResponseToNodeResponse(response, res) : next(), + ) + .catch(next) +}) + +if (isMainModule(import.meta.url)) { + const port = process.env['PORT'] || 4000 + app.listen(port, (error) => { + if (error) { + throw error + } + + console.log(`Node Express server listening on http://localhost:${port}`) + }) +} + +export const reqHandler = createNodeRequestHandler(app) diff --git a/examples/angular/ssr/src/styles.css b/examples/angular/ssr/src/styles.css new file mode 100644 index 00000000000..6718c269c49 --- /dev/null +++ b/examples/angular/ssr/src/styles.css @@ -0,0 +1,16 @@ +body { + margin: 0; + font-family: + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + background: #faf8f2; + color: #1b1f23; +} + +button { + font: inherit; +} diff --git a/examples/angular/ssr/tsconfig.app.json b/examples/angular/ssr/tsconfig.app.json new file mode 100644 index 00000000000..00e9b764198 --- /dev/null +++ b/examples/angular/ssr/tsconfig.app.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/examples/angular/ssr/tsconfig.json b/examples/angular/ssr/tsconfig.json new file mode 100644 index 00000000000..5aa05d2d231 --- /dev/null +++ b/examples/angular/ssr/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "typeCheckHostBindings": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/package.json b/package.json index b00e2ce430b..fa5d6e9130d 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,13 @@ "typescript-eslint": "8.58.1", "vite": "^6.4.1", "esbuild": "^0.27.2" + }, + "packageExtensions": { + "@angular/compiler-cli@20.3.18": { + "optionalDependencies": { + "typescript": "5.9.3" + } + } } } } diff --git a/packages/angular-query-devtools/package.json b/packages/angular-query-devtools/package.json new file mode 100644 index 00000000000..004f64336a0 --- /dev/null +++ b/packages/angular-query-devtools/package.json @@ -0,0 +1,105 @@ +{ + "name": "@tanstack/angular-query-devtools", + "version": "5.97.0", + "description": "Developer tools to interact with and visualize the TanStack Angular Query cache", + "author": "Arnoud de Vries", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/query.git", + "directory": "packages/angular-query-devtools" + }, + "homepage": "https://tanstack.com/query", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "angular", + "angular query", + "devtools", + "tanstack" + ], + "scripts": { + "clean": "premove ./dist ./coverage ./dist-ts", + "compile": "tsc --build", + "test:eslint": "eslint --concurrency=auto ./src", + "test:types": "npm-run-all --serial test:types:*", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build", + "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --build", + "test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --build", + "test:types:tscurrent": "tsc --build", + "test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build", + "test:lib": "vitest", + "test:lib:dev": "pnpm run test:lib --watch", + "test:build": "pnpm pack && publint ./dist/*.tgz --strict && attw ./dist/*.tgz; premove ./dist/*.tgz", + "build": "vite build", + "prepack": "node scripts/prepack.js" + }, + "type": "module", + "types": "dist/index.d.ts", + "module": "dist/index.mjs", + "exports": { + ".": { + "@tanstack/custom-condition": "./src/index.ts", + "types": "./dist/index.d.ts", + "development": "./dist/index.mjs", + "default": "./dist/stub.mjs" + }, + "./package.json": "./package.json", + "./production": { + "types": "./dist/production/index.d.ts", + "default": "./dist/index.mjs" + }, + "./devtools-panel": { + "@tanstack/custom-condition": "./src/devtools-panel/index.ts", + "types": "./dist/devtools-panel/index.d.ts", + "development": "./dist/devtools-panel/index.mjs", + "default": "./dist/devtools-panel/stub.mjs" + }, + "./devtools-panel/production": { + "types": "./dist/devtools-panel/production/index.d.ts", + "default": "./dist/devtools-panel/index.mjs" + } + }, + "sideEffects": false, + "files": [ + "**/*.d.ts", + "**/*.mjs", + "**/*.mjs.map" + ], + "dependencies": { + "@tanstack/query-core": "workspace:*", + "@tanstack/query-devtools": "workspace:*" + }, + "devDependencies": { + "@analogjs/vite-plugin-angular": "^2.3.1", + "@analogjs/vitest-angular": "^2.3.1", + "@angular/build": "^20.0.0", + "@angular/common": "^20.0.0", + "@angular/compiler": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/platform-browser": "^20.0.0", + "@tanstack/angular-query-experimental": "workspace:*", + "@testing-library/jest-dom": "^6.8.0", + "npm-run-all2": "^5.0.0", + "rxjs": "^7.8.2", + "typescript": "5.8.3", + "vite-plugin-dts": "4.2.3", + "vite-plugin-externalize-deps": "^0.9.0", + "vite-tsconfig-paths": "^5.1.4", + "zone.js": "^0.16.0" + }, + "peerDependencies": { + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", + "@tanstack/angular-query-experimental": "workspace:^" + }, + "publishConfig": { + "directory": "dist", + "linkDirectory": false + } +} diff --git a/packages/angular-query-devtools/src/__tests__/inject-devtools-panel.test.ts b/packages/angular-query-devtools/src/__tests__/inject-devtools-panel.test.ts new file mode 100644 index 00000000000..048e063b40d --- /dev/null +++ b/packages/angular-query-devtools/src/__tests__/inject-devtools-panel.test.ts @@ -0,0 +1,188 @@ +import { ElementRef, signal } from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { QueryClient } from '@tanstack/query-core' +import { injectDevtoolsPanel } from '../devtools-panel' +import { setupTanStackQueryTestBed } from './test-utils' + +const mockDevtoolsPanelInstance = { + mount: vi.fn(), + unmount: vi.fn(), + setClient: vi.fn(), + setErrorTypes: vi.fn(), + setOnClose: vi.fn(), +} + +const mocks = vi.hoisted(() => { + function MockTanstackQueryDevtoolsPanel() { + return mockDevtoolsPanelInstance + } + + return { + mockTanstackQueryDevtoolsPanel: vi.fn(MockTanstackQueryDevtoolsPanel), + } +}) + +vi.mock('@tanstack/query-devtools', () => ({ + TanstackQueryDevtoolsPanel: mocks.mockTanstackQueryDevtoolsPanel, +})) + +describe('injectDevtoolsPanel', () => { + let queryClient: QueryClient + let mockElementRef: ElementRef + + const waitForDevtoolsToBeCreated = async () => { + await vi.waitFor(() => { + expect(mocks.mockTanstackQueryDevtoolsPanel).toHaveBeenCalledTimes(1) + }) + } + + beforeEach(() => { + vi.clearAllMocks() + queryClient = new QueryClient() + mockElementRef = new ElementRef(document.createElement('div')) + setupTanStackQueryTestBed(queryClient, { + providers: [{ provide: ElementRef, useValue: signal(mockElementRef) }], + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should return a DevtoolsPanelRef', () => { + const result = TestBed.runInInjectionContext(() => { + return injectDevtoolsPanel(() => ({ + hostElement: TestBed.inject(ElementRef), + })) + }) + + expect(result).toEqual({ + destroy: expect.any(Function), + }) + }) + + it('should initialize TanstackQueryDevtoolsPanel', async () => { + TestBed.runInInjectionContext(() => { + injectDevtoolsPanel(() => ({ + hostElement: TestBed.inject(ElementRef), + })) + }) + + TestBed.tick() + + await waitForDevtoolsToBeCreated() + + expect(mockDevtoolsPanelInstance.mount).toHaveBeenCalledTimes(1) + }) + + it('should destroy TanstackQueryDevtoolsPanel', async () => { + const result = TestBed.runInInjectionContext(() => { + return injectDevtoolsPanel(() => ({ + hostElement: TestBed.inject(ElementRef), + })) + }) + + TestBed.tick() + + await waitForDevtoolsToBeCreated() + + result.destroy() + + expect(mockDevtoolsPanelInstance.unmount).toHaveBeenCalledTimes(1) + }) + + it('should destroy TanstackQueryDevtoolsPanel when hostElement is removed', async () => { + const hostElement = signal(mockElementRef) + + TestBed.runInInjectionContext(() => { + return injectDevtoolsPanel(() => ({ + hostElement: hostElement(), + })) + }) + + TestBed.tick() + + await waitForDevtoolsToBeCreated() + + expect(mockDevtoolsPanelInstance.unmount).toHaveBeenCalledTimes(0) + + hostElement.set(null as unknown as ElementRef) + + TestBed.tick() + + expect(mockDevtoolsPanelInstance.unmount).toHaveBeenCalledTimes(1) + }) + + it('should update client', async () => { + const client = signal(new QueryClient()) + + TestBed.runInInjectionContext(() => { + return injectDevtoolsPanel(() => ({ + hostElement: TestBed.inject(ElementRef), + client: client(), + })) + }) + + TestBed.tick() + + await waitForDevtoolsToBeCreated() + + expect(mockDevtoolsPanelInstance.setClient).toHaveBeenCalledTimes(0) + + client.set(new QueryClient()) + + TestBed.tick() + + expect(mockDevtoolsPanelInstance.setClient).toHaveBeenCalledTimes(1) + }) + + it('should update error types', async () => { + const errorTypes = signal([]) + + TestBed.runInInjectionContext(() => { + return injectDevtoolsPanel(() => ({ + hostElement: TestBed.inject(ElementRef), + errorTypes: errorTypes(), + })) + }) + + TestBed.tick() + + await waitForDevtoolsToBeCreated() + + expect(mockDevtoolsPanelInstance.setErrorTypes).toHaveBeenCalledTimes(0) + + errorTypes.set([]) + + TestBed.tick() + + expect(mockDevtoolsPanelInstance.setErrorTypes).toHaveBeenCalledTimes(1) + }) + + it('should update onclose', async () => { + const functionA = () => {} + const functionB = () => {} + + const onClose = signal(functionA) + + TestBed.runInInjectionContext(() => { + return injectDevtoolsPanel(() => ({ + hostElement: TestBed.inject(ElementRef), + onClose: onClose(), + })) + }) + + TestBed.tick() + + await waitForDevtoolsToBeCreated() + + expect(mockDevtoolsPanelInstance.setOnClose).toHaveBeenCalledTimes(0) + + onClose.set(functionB) + + TestBed.tick() + + expect(mockDevtoolsPanelInstance.setOnClose).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/angular-query-devtools/src/__tests__/test-utils.ts b/packages/angular-query-devtools/src/__tests__/test-utils.ts new file mode 100644 index 00000000000..1c1a15c1b73 --- /dev/null +++ b/packages/angular-query-devtools/src/__tests__/test-utils.ts @@ -0,0 +1,24 @@ +import { provideZonelessChangeDetection } from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { vi } from 'vitest' +import { provideTanStackQuery } from '@tanstack/angular-query-experimental' +import type { QueryClient } from '@tanstack/query-core' +import type { EnvironmentProviders, Provider } from '@angular/core' + +export function setupTanStackQueryTestBed( + queryClient: QueryClient, + options: { providers?: Array } = {}, +) { + TestBed.resetTestingModule() + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery(queryClient), + ...(options.providers ?? []), + ], + }) +} + +export async function flushQueryUpdates() { + await vi.advanceTimersByTimeAsync(0) +} diff --git a/packages/angular-query-devtools/src/__tests__/with-devtools.test.ts b/packages/angular-query-devtools/src/__tests__/with-devtools.test.ts new file mode 100644 index 00000000000..dc36a720f8a --- /dev/null +++ b/packages/angular-query-devtools/src/__tests__/with-devtools.test.ts @@ -0,0 +1,607 @@ +import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest' +import { QueryClient } from '@tanstack/query-core' +import { TestBed } from '@angular/core/testing' +import { + ENVIRONMENT_INITIALIZER, + EnvironmentInjector, + InjectionToken, + PLATFORM_ID, + createEnvironmentInjector, + isDevMode, + provideZonelessChangeDetection, + signal, +} from '@angular/core' +import { provideTanStackQuery } from '@tanstack/angular-query-experimental' +import { withDevtools } from '../index' +import { flushQueryUpdates } from './test-utils' +import type { + DevtoolsButtonPosition, + DevtoolsErrorType, + DevtoolsPosition, +} from '@tanstack/query-devtools' +import type { DevtoolsOptions } from '../types' + +const mockDevtoolsInstance = { + mount: vi.fn(), + unmount: vi.fn(), + setClient: vi.fn(), + setPosition: vi.fn(), + setErrorTypes: vi.fn(), + setButtonPosition: vi.fn(), + setInitialIsOpen: vi.fn(), +} + +function MockTanstackQueryDevtools() { + return mockDevtoolsInstance +} + +const mockTanstackQueryDevtools = vi.fn(MockTanstackQueryDevtools) + +vi.mock('@tanstack/query-devtools', () => ({ + TanstackQueryDevtools: mockTanstackQueryDevtools, +})) + +vi.mock('@angular/core', async () => { + const actual = await vi.importActual('@angular/core') + return { + ...actual, + isDevMode: vi.fn(), + } +}) + +const mockIsDevMode = vi.mocked(isDevMode) + +describe('withDevtools feature', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + }) + + afterEach(() => { + vi.restoreAllMocks() + vi.useRealTimers() + TestBed.resetTestingModule() + }) + + test.each([ + { + description: 'should load devtools in development mode', + isDevMode: true, + expectedCalled: true, + }, + { + description: 'should not load devtools in production mode', + isDevMode: false, + expectedCalled: false, + }, + { + description: `should load devtools in development mode when 'loadDevtools' is set to 'auto'`, + isDevMode: true, + loadDevtools: 'auto', + expectedCalled: true, + }, + { + description: `should not load devtools in production mode when 'loadDevtools' is set to 'auto'`, + isDevMode: false, + loadDevtools: 'auto', + expectedCalled: false, + }, + { + description: + "should load devtools in development mode when 'loadDevtools' is set to true", + isDevMode: true, + loadDevtools: true, + expectedCalled: true, + }, + { + description: + "should load devtools in production mode when 'loadDevtools' is set to true", + isDevMode: false, + loadDevtools: true, + expectedCalled: true, + }, + { + description: + "should not load devtools in development mode when 'loadDevtools' is set to false", + isDevMode: true, + loadDevtools: false, + expectedCalled: false, + }, + { + description: + "should not load devtools in production mode when 'loadDevtools' is set to false", + isDevMode: false, + loadDevtools: false, + expectedCalled: false, + }, + ])( + '$description', + async ({ isDevMode: isDevModeValue, loadDevtools, expectedCalled }) => { + mockIsDevMode.mockReturnValue(isDevModeValue) + + const providers = [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + loadDevtools !== undefined + ? withDevtools( + () => + ({ + loadDevtools, + }) as DevtoolsOptions, + ) + : withDevtools(), + ), + ] + + TestBed.configureTestingModule({ + providers, + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + TestBed.tick() + await vi.dynamicImportSettled() + TestBed.tick() + await vi.dynamicImportSettled() + + expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes( + expectedCalled ? 1 : 0, + ) + expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes( + expectedCalled ? 1 : 0, + ) + }, + ) + + it('should not continue loading devtools after injector is destroyed', async () => { + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + // Destroys injector + TestBed.resetTestingModule() + await flushQueryUpdates() + await vi.dynamicImportSettled() + + expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() + }) + + it('should not create devtools again when already provided', async () => { + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes(1) + + const injector = TestBed.inject(EnvironmentInjector) + + createEnvironmentInjector( + [ + withDevtools(() => ({ + loadDevtools: true, + })).ɵproviders, + ], + injector, + ) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes(1) + }) + + it('should not load devtools if platform is not browser', async () => { + TestBed.configureTestingModule({ + providers: [ + { + provide: PLATFORM_ID, + useValue: 'server', + }, + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await vi.runAllTimersAsync() + + expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() + }) + + it('should update error types', async () => { + const errorTypes = signal([] as Array) + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + errorTypes: errorTypes(), + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + TestBed.tick() + + expect(mockDevtoolsInstance.setErrorTypes).toHaveBeenCalledTimes(0) + + const newErrorTypes = [ + { + name: '', + initializer: () => new Error(), + }, + ] + + errorTypes.set(newErrorTypes) + + TestBed.tick() + + expect(mockDevtoolsInstance.setErrorTypes).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.setErrorTypes).toHaveBeenCalledWith( + newErrorTypes, + ) + }) + + it('should update client', async () => { + const client = signal(new QueryClient()) + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + client: client(), + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + TestBed.tick() + + expect(mockDevtoolsInstance.setClient).toHaveBeenCalledTimes(0) + + const newClient = new QueryClient() + client.set(newClient) + + TestBed.tick() + + expect(mockDevtoolsInstance.setClient).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.setClient).toHaveBeenCalledWith(newClient) + }) + + it('should update position', async () => { + const position = signal('top') + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + position: position(), + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + TestBed.tick() + + expect(mockDevtoolsInstance.setPosition).toHaveBeenCalledTimes(0) + + position.set('left') + + TestBed.tick() + + expect(mockDevtoolsInstance.setPosition).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.setPosition).toHaveBeenCalledWith('left') + }) + + it('should update button position', async () => { + const buttonPosition = signal('bottom-left') + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + buttonPosition: buttonPosition(), + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + TestBed.tick() + + expect(mockDevtoolsInstance.setButtonPosition).toHaveBeenCalledTimes(0) + + buttonPosition.set('bottom-right') + + TestBed.tick() + + expect(mockDevtoolsInstance.setButtonPosition).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.setButtonPosition).toHaveBeenCalledWith( + 'bottom-right', + ) + }) + + it('should update initialIsOpen', async () => { + const initialIsOpen = signal(false) + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + initialIsOpen: initialIsOpen(), + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + TestBed.tick() + + expect(mockDevtoolsInstance.setInitialIsOpen).toHaveBeenCalledTimes(0) + + initialIsOpen.set(true) + + TestBed.tick() + + expect(mockDevtoolsInstance.setInitialIsOpen).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.setInitialIsOpen).toHaveBeenCalledWith(true) + }) + + it('should destroy devtools', async () => { + const loadDevtools = signal(true) + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: loadDevtools(), + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(0) + + loadDevtools.set(false) + + TestBed.tick() + + expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(1) + }) + + it('should unmount devtools when injector is destroyed', async () => { + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: true, + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + TestBed.tick() + await vi.dynamicImportSettled() + + expect(mockTanstackQueryDevtools).toHaveBeenCalled() + expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(0) + + // Destroy the injector + TestBed.resetTestingModule() + + expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(1) + }) + + it('should remount devtools when toggled from false to true', async () => { + const loadDevtools = signal(false) + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(() => ({ + loadDevtools: loadDevtools(), + })), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() + expect(mockDevtoolsInstance.mount).not.toHaveBeenCalled() + + loadDevtools.set(true) + TestBed.tick() + await vi.dynamicImportSettled() + + expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.unmount).not.toHaveBeenCalled() + + loadDevtools.set(false) + TestBed.tick() + + expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(1) + expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(1) + + loadDevtools.set(true) + TestBed.tick() + await vi.dynamicImportSettled() + + // Should remount (mount called twice now) + expect(mockDevtoolsInstance.mount).toHaveBeenCalledTimes(2) + expect(mockDevtoolsInstance.unmount).toHaveBeenCalledTimes(1) + }) + + describe('deps parameter', () => { + it('should inject dependencies and pass them to withDevtoolsFn in correct order', async () => { + const mockService1 = { value: 'service1' } + const mockService2 = { value: 'service2' } + const mockService1Token = new InjectionToken('MockService1') + const mockService2Token = new InjectionToken('MockService2') + const withDevtoolsFn = vi.fn().mockReturnValue({ loadDevtools: true }) + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + { + provide: mockService1Token, + useValue: mockService1, + }, + { + provide: mockService2Token, + useValue: mockService2, + }, + provideTanStackQuery( + new QueryClient(), + withDevtools(withDevtoolsFn, { + deps: [mockService1Token, mockService2Token], + }), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + expect(withDevtoolsFn).toHaveBeenCalledWith(mockService1, mockService2) + }) + + it('should work with empty deps array', async () => { + const withDevtoolsFn = vi.fn().mockReturnValue({ loadDevtools: true }) + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + new QueryClient(), + withDevtools(withDevtoolsFn, { + deps: [], + }), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + expect(withDevtoolsFn).toHaveBeenCalledWith() + }) + + it('should reactively update when injected services change', async () => { + class ReactiveService { + enabled = signal(false) + position = signal('bottom') + } + + const withDevtoolsFn = (service: ReactiveService) => ({ + loadDevtools: service.enabled(), + position: service.position(), + }) + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + ReactiveService, + provideTanStackQuery( + new QueryClient(), + withDevtools(withDevtoolsFn, { + deps: [ReactiveService], + }), + ), + ], + }) + + TestBed.inject(ENVIRONMENT_INITIALIZER) + await flushQueryUpdates() + + const service = TestBed.inject(ReactiveService) + + expect(mockTanstackQueryDevtools).not.toHaveBeenCalled() + + service.enabled.set(true) + TestBed.tick() + await vi.dynamicImportSettled() + + expect(mockTanstackQueryDevtools).toHaveBeenCalledTimes(1) + expect(mockTanstackQueryDevtools).toHaveBeenCalledWith( + expect.objectContaining({ + position: 'bottom', + }), + ) + + service.position.set('top') + TestBed.tick() + + expect(mockDevtoolsInstance.setPosition).toHaveBeenCalledWith('top') + }) + }) +}) diff --git a/packages/angular-query-devtools/src/devtools-panel/index.ts b/packages/angular-query-devtools/src/devtools-panel/index.ts new file mode 100644 index 00000000000..2742a154692 --- /dev/null +++ b/packages/angular-query-devtools/src/devtools-panel/index.ts @@ -0,0 +1,8 @@ +export type { + InjectDevtoolsPanel, + DevtoolsPanelOptions, + InjectDevtoolsPanelOptions, + DevtoolsPanelRef, +} from './types' + +export { injectDevtoolsPanel } from './inject-devtools-panel' diff --git a/packages/angular-query-devtools/src/devtools-panel/inject-devtools-panel.ts b/packages/angular-query-devtools/src/devtools-panel/inject-devtools-panel.ts new file mode 100644 index 00000000000..b45a5584d06 --- /dev/null +++ b/packages/angular-query-devtools/src/devtools-panel/inject-devtools-panel.ts @@ -0,0 +1,110 @@ +import { + DestroyRef, + Injector, + PLATFORM_ID, + assertInInjectionContext, + computed, + effect, + inject, + runInInjectionContext, + untracked, +} from '@angular/core' +import { QueryClient, onlineManager } from '@tanstack/query-core' +import { isPlatformBrowser } from '@angular/common' +import type { TanstackQueryDevtoolsPanel } from '@tanstack/query-devtools' +import type { + DevtoolsPanelOptions, + InjectDevtoolsPanel, + InjectDevtoolsPanelOptions, +} from './types' + +/** + * Inject a TanStack Query devtools panel and render it in the DOM. + * + * Devtools panel allows programmatic control over the devtools, for example if you want to render + * the devtools as part of your own devtools. + * + * Consider `withDevtools` instead if you don't need this. + * @param injectDevtoolsPanelFn - A function that returns devtools panel options. + * @param options - Additional configuration + * @returns DevtoolsPanelRef + * @see https://tanstack.com/query/v5/docs/framework/angular/devtools + */ +export const injectDevtoolsPanel: InjectDevtoolsPanel = ( + injectDevtoolsPanelFn: () => DevtoolsPanelOptions, + options?: InjectDevtoolsPanelOptions, +) => { + !options?.injector && assertInInjectionContext(injectDevtoolsPanel) + const currentInjector = options?.injector ?? inject(Injector) + + return runInInjectionContext(currentInjector, () => { + const destroyRef = inject(DestroyRef) + const isBrowser = isPlatformBrowser(inject(PLATFORM_ID)) + const injectedClient = inject(QueryClient, { optional: true }) + + const queryOptions = computed(injectDevtoolsPanelFn) + let devtools: TanstackQueryDevtoolsPanel | null = null + + const destroy = () => { + devtools?.unmount() + devtools = null + } + + if (!isBrowser) + return { + destroy, + } + + effect(() => { + const { + client = injectedClient, + errorTypes = [], + styleNonce, + shadowDOMTarget, + onClose, + hostElement, + } = queryOptions() + + untracked(() => { + if (!client) throw new Error('No QueryClient found') + if (!devtools && hostElement) { + import('@tanstack/query-devtools') + .then((queryDevtools) => { + devtools = new queryDevtools.TanstackQueryDevtoolsPanel({ + client, + queryFlavor: 'Angular Query', + version: '5', + buttonPosition: 'bottom-left', + position: 'bottom', + initialIsOpen: true, + errorTypes, + styleNonce, + shadowDOMTarget, + onClose, + onlineManager, + }) + devtools.mount(hostElement.nativeElement) + }) + .catch((error) => { + console.error( + 'Failed to load @tanstack/query-devtools.', + error, + ) + }) + } else if (devtools && hostElement) { + devtools.setClient(client) + devtools.setErrorTypes(errorTypes) + onClose && devtools.setOnClose(onClose) + } else if (devtools && !hostElement) { + destroy() + } + }) + }) + + destroyRef.onDestroy(destroy) + + return { + destroy, + } + }) +} diff --git a/packages/angular-query-devtools/src/devtools-panel/production/index.ts b/packages/angular-query-devtools/src/devtools-panel/production/index.ts new file mode 100644 index 00000000000..546becea7f8 --- /dev/null +++ b/packages/angular-query-devtools/src/devtools-panel/production/index.ts @@ -0,0 +1 @@ +export * from '..' diff --git a/packages/angular-query-devtools/src/devtools-panel/stub.ts b/packages/angular-query-devtools/src/devtools-panel/stub.ts new file mode 100644 index 00000000000..63e836eb5c8 --- /dev/null +++ b/packages/angular-query-devtools/src/devtools-panel/stub.ts @@ -0,0 +1,7 @@ +import { noop } from '@tanstack/query-core' +import type { InjectDevtoolsPanel } from './types' + +// Stub which replaces `injectDevtoolsPanel` in production builds +export const injectDevtoolsPanel: InjectDevtoolsPanel = () => ({ + destroy: noop, +}) diff --git a/packages/angular-query-devtools/src/devtools-panel/types.ts b/packages/angular-query-devtools/src/devtools-panel/types.ts new file mode 100644 index 00000000000..b87373ad953 --- /dev/null +++ b/packages/angular-query-devtools/src/devtools-panel/types.ts @@ -0,0 +1,57 @@ +import type { DevtoolsErrorType } from '@tanstack/query-devtools' +import type { ElementRef, Injector } from '@angular/core' +import type { QueryClient } from '@tanstack/query-core' + +export interface InjectDevtoolsPanelOptions { + /** + * The `Injector` in which to create the devtools panel. + * + * If this is not provided, the current injection context will be used instead (via `inject`). + */ + injector?: Injector +} + +/** + * A devtools panel, which can be manually destroyed. + */ +export interface DevtoolsPanelRef { + /** + * Destroy the panel, removing it from the DOM and stops listening to signal changes. + */ + destroy: () => void +} + +export interface DevtoolsPanelOptions { + /** + * Custom instance of QueryClient + */ + client?: QueryClient + /** + * Use this so you can define custom errors that can be shown in the devtools. + */ + errorTypes?: Array + /** + * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. + */ + styleNonce?: string + /** + * Use this so you can attach the devtool's styles to specific element in the DOM. + */ + shadowDOMTarget?: ShadowRoot + + /** + * Callback function that is called when the devtools panel is closed + */ + onClose?: () => unknown + + /** + * Element where to render the devtools panel. When set to undefined or null, the devtools panel will not be created, or destroyed if existing. + * If changed from undefined to a ElementRef, the devtools panel will be created. + */ + hostElement?: ElementRef +} + +export type InjectDevtoolsPanel = ( + injectDevtoolsPanelFn: () => DevtoolsPanelOptions, + options?: InjectDevtoolsPanelOptions, +) => DevtoolsPanelRef diff --git a/packages/angular-query-devtools/src/index.ts b/packages/angular-query-devtools/src/index.ts new file mode 100644 index 00000000000..d4f7e9238f7 --- /dev/null +++ b/packages/angular-query-devtools/src/index.ts @@ -0,0 +1,8 @@ +export type { + DevtoolsOptions, + WithDevtools, + WithDevtoolsFn, + WithDevtoolsOptions, +} from './types' + +export { withDevtools } from './with-devtools' diff --git a/packages/angular-query-devtools/src/production/index.ts b/packages/angular-query-devtools/src/production/index.ts new file mode 100644 index 00000000000..546becea7f8 --- /dev/null +++ b/packages/angular-query-devtools/src/production/index.ts @@ -0,0 +1 @@ +export * from '..' diff --git a/packages/angular-query-devtools/src/stub.ts b/packages/angular-query-devtools/src/stub.ts new file mode 100644 index 00000000000..8a9369f7569 --- /dev/null +++ b/packages/angular-query-devtools/src/stub.ts @@ -0,0 +1,8 @@ +import type { WithDevtools } from './types' +import { makeEnvironmentProviders } from '@angular/core' + +// Stub which replaces `withDevtools` in production builds +export const withDevtools: WithDevtools = () => ({ + ɵkind: 'Devtools', + ɵproviders: makeEnvironmentProviders([]), +}) diff --git a/packages/angular-query-devtools/src/types.ts b/packages/angular-query-devtools/src/types.ts new file mode 100644 index 00000000000..58b778735c7 --- /dev/null +++ b/packages/angular-query-devtools/src/types.ts @@ -0,0 +1,107 @@ +import type { QueryClient } from '@tanstack/query-core' +import type { + DevtoolsButtonPosition, + DevtoolsErrorType, + DevtoolsPosition, +} from '@tanstack/query-devtools' +import type { DevtoolsFeature } from '@tanstack/angular-query-experimental' + +/** + * Options for configuring withDevtools. + */ +export interface WithDevtoolsOptions { + /** + * An array of dependencies to be injected and passed to the `withDevtoolsFn` function. + * + * **Example** + * ```ts + * export const appConfig: ApplicationConfig = { + * providers: [ + * provideTanStackQuery( + * new QueryClient(), + * withDevtools( + * (devToolsOptionsManager: DevtoolsOptionsManager) => ({ + * loadDevtools: devToolsOptionsManager.loadDevtools(), + * }), + * { + * deps: [DevtoolsOptionsManager], + * }, + * ), + * ), + * ], + * } + * ``` + */ + deps?: Array +} + +/** + * Options for configuring the TanStack Query devtools. + */ +export interface DevtoolsOptions { + /** + * Set this true if you want the devtools to default to being open + */ + initialIsOpen?: boolean + /** + * The position of the TanStack logo to open and close the devtools panel. + * `top-left` | `top-right` | `bottom-left` | `bottom-right` | `relative` + * Defaults to `bottom-right`. + */ + buttonPosition?: DevtoolsButtonPosition + /** + * The position of the TanStack Query devtools panel. + * `top` | `bottom` | `left` | `right` + * Defaults to `bottom`. + */ + position?: DevtoolsPosition + /** + * Custom instance of QueryClient + */ + client?: QueryClient + /** + * Use this so you can define custom errors that can be shown in the devtools. + */ + errorTypes?: Array + /** + * Use this to pass a nonce to the style tag that is added to the document head. This is useful if you are using a Content Security Policy (CSP) nonce to allow inline styles. + */ + styleNonce?: string + /** + * Use this so you can attach the devtool's styles to a specific element in the DOM. + */ + shadowDOMTarget?: ShadowRoot + /** + * Set this to true to hide disabled queries from the devtools panel. + */ + hideDisabledQueries?: boolean + + /** + * Whether the developer tools should load. + * - `auto`- (Default) Lazily loads devtools when in development mode. Skips loading in production mode. + * - `true`- Always load the devtools, regardless of the environment. + * - `false`- Never load the devtools, regardless of the environment. + * + * You can use `true` and `false` to override loading developer tools from an environment file. + * For example, a test environment might run in production mode but you may want to load developer tools. + * + * Additionally, you can use a signal in the callback to dynamically load the devtools based on a condition. For example, + * a signal created from a RxJS observable that listens for a keyboard shortcut. + * + * **Example** + * ```ts + * withDevtools(() => ({ + * initialIsOpen: true, + * loadDevtools: inject(ExampleService).loadDevtools() + * })) + * ``` + */ + loadDevtools?: 'auto' | boolean +} + +export type WithDevtoolsFn = (...deps: Array) => DevtoolsOptions + +export type WithDevtools = ( + withDevtoolsFn?: WithDevtoolsFn, + options?: WithDevtoolsOptions, +) => DevtoolsFeature diff --git a/packages/angular-query-devtools/src/with-devtools.ts b/packages/angular-query-devtools/src/with-devtools.ts new file mode 100644 index 00000000000..1e2dac8618a --- /dev/null +++ b/packages/angular-query-devtools/src/with-devtools.ts @@ -0,0 +1,187 @@ +import { isPlatformBrowser } from '@angular/common' +import { + DestroyRef, + ENVIRONMENT_INITIALIZER, + InjectionToken, + Injector, + PLATFORM_ID, + computed, + effect, + inject, + isDevMode, + runInInjectionContext, + afterNextRender, + makeEnvironmentProviders, +} from '@angular/core' +import { QueryClient, onlineManager } from '@tanstack/query-core' +import { queryFeature } from '@tanstack/angular-query-experimental' +import type { Signal } from '@angular/core' +import type { + DevtoolsOptions, + WithDevtools, + WithDevtoolsFn, + WithDevtoolsOptions, +} from './types' +import type { TanstackQueryDevtools } from '@tanstack/query-devtools' + +/** + * Internal token used to prevent double providing of devtools in child injectors + */ +const DEVTOOLS_PROVIDED = new InjectionToken('', { + factory: () => ({ + isProvided: false, + }), +}) + +/** + * Internal token for providing devtools options + */ +const DEVTOOLS_OPTIONS_SIGNAL = new InjectionToken>('') + +/** + * Enables developer tools in Angular development builds. + * + * **Example** + * + * ```ts + * export const appConfig: ApplicationConfig = { + * providers: [ + * provideTanStackQuery(new QueryClient(), withDevtools()), + * ] + * } + * ``` + * The devtools will be rendered in ``. + * + * If you need more control over when devtools are loaded, you can use the `loadDevtools` option. + * + * If you need more control over where devtools are rendered, consider `injectDevtoolsPanel`. This allows rendering devtools inside your own devtools for example. + * @param withDevtoolsFn - A function that returns `DevtoolsOptions`. + * @param options - Additional options for configuring `withDevtools`. + * @returns A set of providers for use with `provideTanStackQuery`. + * @see {@link provideTanStackQuery} + * @see {@link DevtoolsOptions} + */ +export const withDevtools: WithDevtools = ( + withDevtoolsFn?: WithDevtoolsFn, + options: WithDevtoolsOptions = {}, +) => + queryFeature('Devtools', makeEnvironmentProviders([ + { + provide: DEVTOOLS_OPTIONS_SIGNAL, + useFactory: (...deps: Array) => + computed(() => withDevtoolsFn?.(...deps) ?? {}), + deps: options.deps || [], + }, + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: () => { + const devtoolsProvided = inject(DEVTOOLS_PROVIDED) + if ( + !isPlatformBrowser(inject(PLATFORM_ID)) || + devtoolsProvided.isProvided + ) + return + + devtoolsProvided.isProvided = true + + const injector = inject(Injector) + + // Do not run on SSR + afterNextRender(() => { + runInInjectionContext(injector, () => { + let injectorIsDestroyed = false + inject(DestroyRef).onDestroy(() => (injectorIsDestroyed = true)) + + const injectedClient = inject(QueryClient, { + optional: true, + }) + const destroyRef = inject(DestroyRef) + const devtoolsOptions = inject(DEVTOOLS_OPTIONS_SIGNAL) + + let devtools: TanstackQueryDevtools | null = null + let el: HTMLElement | null = null + + const shouldLoadToolsSignal = computed(() => { + const { loadDevtools } = devtoolsOptions() + return typeof loadDevtools === 'boolean' + ? loadDevtools + : isDevMode() + }) + + const getResolvedQueryClient = () => { + const client = devtoolsOptions().client ?? injectedClient + if (!client) { + throw new Error('No QueryClient found') + } + return client + } + + const destroyDevtools = () => { + devtools?.unmount() + el?.remove() + devtools = null + } + + effect( + () => { + const shouldLoadTools = shouldLoadToolsSignal() + const { + client, + position, + errorTypes, + buttonPosition, + initialIsOpen, + } = devtoolsOptions() + + if (!shouldLoadTools) { + // Destroy or do nothing + devtools && destroyDevtools() + return + } + + if (devtools) { + // Update existing devtools config + client && devtools.setClient(client) + position && devtools.setPosition(position) + errorTypes && devtools.setErrorTypes(errorTypes) + buttonPosition && devtools.setButtonPosition(buttonPosition) + typeof initialIsOpen === 'boolean' && + devtools.setInitialIsOpen(initialIsOpen) + return + } + + // Create devtools + import('@tanstack/query-devtools') + .then((queryDevtools) => { + // As this code runs async, the injector could have been destroyed + if (injectorIsDestroyed) return + + devtools = new queryDevtools.TanstackQueryDevtools({ + ...devtoolsOptions(), + client: getResolvedQueryClient(), + queryFlavor: 'Angular Query', + version: '5', + onlineManager, + }) + + el = document.body.appendChild(document.createElement('div')) + el.classList.add('tsqd-parent-container') + devtools.mount(el) + + destroyRef.onDestroy(destroyDevtools) + }) + .catch((error) => { + console.error( + 'Failed to load @tanstack/query-devtools.', + error, + ) + }) + }, + { injector }, + ) + }) + }) + }, + }, + ])) diff --git a/packages/angular-query-devtools/test-setup.ts b/packages/angular-query-devtools/test-setup.ts new file mode 100644 index 00000000000..eb69f3f05a9 --- /dev/null +++ b/packages/angular-query-devtools/test-setup.ts @@ -0,0 +1,9 @@ +import '@testing-library/jest-dom/vitest' +import '@angular/compiler' +import { getTestBed } from '@angular/core/testing' +import { + BrowserTestingModule, + platformBrowserTesting, +} from '@angular/platform-browser/testing' + +getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting()) diff --git a/packages/angular-query-devtools/tsconfig.json b/packages/angular-query-devtools/tsconfig.json new file mode 100644 index 00000000000..f08bdc6ea46 --- /dev/null +++ b/packages/angular-query-devtools/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist-ts", + "rootDir": ".", + "noFallthroughCasesInSwitch": true, + "useDefineForClassFields": false, + "target": "ES2022" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictStandalone": true, + "strictTemplates": true + }, + "include": ["src", "scripts", "test-setup.ts", "*.config.*", "package.json"], + "references": [ + { "path": "../angular-query-experimental" }, + { "path": "../query-core" }, + { "path": "../query-devtools" } + ] +} diff --git a/packages/angular-query-devtools/tsconfig.prod.json b/packages/angular-query-devtools/tsconfig.prod.json new file mode 100644 index 00000000000..b470042ddc1 --- /dev/null +++ b/packages/angular-query-devtools/tsconfig.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "incremental": false, + "composite": false, + "rootDir": "../../", + "customConditions": null + } +} diff --git a/packages/angular-query-devtools/tsconfig.spec.json b/packages/angular-query-devtools/tsconfig.spec.json new file mode 100644 index 00000000000..b15a2a37c7b --- /dev/null +++ b/packages/angular-query-devtools/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["vitest/globals", "node"], + "noEmit": false, + "emitDeclarationOnly": false, + "declaration": false, + "declarationMap": false, + "composite": false, + "target": "ES2022" + }, + "files": ["test-setup.ts"], + "include": ["src/**/*.test.ts", "src/**/*.test-d.ts"], + "references": [{ "path": "./tsconfig.prod.json" }] +} diff --git a/packages/angular-query-devtools/vite.config.ts b/packages/angular-query-devtools/vite.config.ts new file mode 100644 index 00000000000..c6a458fc211 --- /dev/null +++ b/packages/angular-query-devtools/vite.config.ts @@ -0,0 +1,116 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { defineConfig, mergeConfig } from 'vite' +import { externalizeDeps } from 'vite-plugin-externalize-deps' +import tsconfigPaths from 'vite-tsconfig-paths' +import dts from 'vite-plugin-dts' +import type { Options } from '@tanstack/vite-config' + +const packageDir = path.dirname(fileURLToPath(import.meta.url)) +const queryDevtoolsEntry = path.join( + packageDir, + '../query-devtools/src/index.ts', +) + +function ensureImportFileExtension({ + content, + extension, +}: { + content: string + extension: string +}) { + content = content.replace( + /(im|ex)port\s[\w{}/*\s,]+from\s['"](?:\.\.?\/)+?[^.'"]+(?=['"];?)/gm, + `$&.${extension}`, + ) + + content = content.replace( + /import\(['"](?:\.\.?\/)+?[^.'"]+(?=['"];?)/gm, + `$&.${extension}`, + ) + return content +} + +const config = defineConfig({ + resolve: { + conditions: ['@tanstack/custom-condition'], + ...(process.env.VITEST === 'true' + ? { + alias: { + '@tanstack/query-devtools': queryDevtoolsEntry, + }, + } + : {}), + }, +}) + +export const tanstackViteConfig = (options: Options) => { + const outDir = options.outDir ?? 'dist' + const cjs = options.cjs ?? true + + return defineConfig({ + plugins: [ + externalizeDeps({ include: options.externalDeps ?? [] }), + tsconfigPaths({ + projects: options.tsconfigPath ? [options.tsconfigPath] : undefined, + }), + dts({ + outDir, + entryRoot: options.srcDir, + include: options.srcDir, + exclude: options.exclude, + tsconfigPath: options.tsconfigPath, + compilerOptions: { + module: 99, // ESNext + declarationMap: false, + }, + beforeWriteFile: (filePath, content) => { + return { + filePath, + content: ensureImportFileExtension({ content, extension: 'js' }), + } + }, + afterDiagnostic: (diagnostics) => { + if (diagnostics.length > 0) { + console.error('Please fix the above type errors') + process.exit(1) + } + }, + }), + ], + build: { + outDir, + minify: false, + sourcemap: true, + lib: { + entry: options.entry, + formats: cjs ? ['es', 'cjs'] : ['es'], + fileName: () => '[name].mjs', + }, + rollupOptions: { + output: { + preserveModules: true, + preserveModulesRoot: 'src', + }, + }, + }, + }) +} + +export default mergeConfig( + config, + tanstackViteConfig({ + cjs: false, + entry: [ + './src/index.ts', + './src/stub.ts', + './src/production/index.ts', + './src/devtools-panel/index.ts', + './src/devtools-panel/stub.ts', + './src/devtools-panel/production/index.ts', + ], + exclude: ['src/__tests__'], + srcDir: './src', + tsconfigPath: 'tsconfig.prod.json', + }), +) diff --git a/packages/angular-query-devtools/vitest.config.ts b/packages/angular-query-devtools/vitest.config.ts new file mode 100644 index 00000000000..4195b0c8c1b --- /dev/null +++ b/packages/angular-query-devtools/vitest.config.ts @@ -0,0 +1,33 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import angular from '@analogjs/vite-plugin-angular' +import { defineConfig } from 'vitest/config' +import packageJson from './package.json' with { type: 'json' } + +const packageDir = path.dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + esbuild: { + target: 'es2022', + }, + plugins: [angular({ tsconfig: './tsconfig.spec.json' })], + resolve: { + alias: { + '@tanstack/query-devtools': path.join( + packageDir, + '../query-devtools/src/index.ts', + ), + }, + }, + test: { + name: packageJson.name, + dir: './src', + watch: false, + environment: 'jsdom', + setupFiles: ['./test-setup.ts'], + coverage: { enabled: true, provider: 'istanbul', include: ['src/**/*'] }, + include: ['**/*.{test,spec}.{ts,mts,cts,tsx,js,mjs,cjs,jsx}'], + globals: true, + restoreMocks: true, + }, +}) diff --git a/packages/angular-query-experimental/package.json b/packages/angular-query-experimental/package.json index cd5c2a295ea..bf7d7e0536e 100644 --- a/packages/angular-query-experimental/package.json +++ b/packages/angular-query-experimental/package.json @@ -31,7 +31,6 @@ "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", - "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build", @@ -88,24 +87,32 @@ "@tanstack/query-core": "workspace:*" }, "devDependencies": { + "@analogjs/vite-plugin-angular": "^2.3.1", + "@analogjs/vitest-angular": "^2.3.1", + "@angular/build": "^20.0.0", "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@angular/platform-server": "^20.0.0", + "@angular/router": "^20.0.0", "@tanstack/query-test-utils": "workspace:*", - "@testing-library/angular": "^18.0.0", + "@testing-library/angular": "^18.1.1", "npm-run-all2": "^5.0.0", "rxjs": "^7.8.2", + "typescript": "5.8.3", "vite-plugin-dts": "4.2.3", "vite-plugin-externalize-deps": "^0.9.0", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "zone.js": "^0.16.0" }, "optionalDependencies": { "@tanstack/query-devtools": "workspace:*" }, "peerDependencies": { - "@angular/common": ">=16.0.0", - "@angular/core": ">=16.0.0" + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0" }, "publishConfig": { "directory": "dist", diff --git a/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts b/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts index e9db0edafb9..c34c580ddcb 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts @@ -1,43 +1,97 @@ -import { TestBed } from '@angular/core/testing' -import { afterEach, beforeEach, describe, expectTypeOf, test, vi } from 'vitest' -import { provideZonelessChangeDetection } from '@angular/core' -import { queryKey, sleep } from '@tanstack/query-test-utils' -import { QueryClient, injectInfiniteQuery, provideTanStackQuery } from '..' +import { describe, expectTypeOf, it, test } from 'vitest' +import { injectInfiniteQuery } from '..' +import type { Signal } from '@angular/core' import type { InfiniteData } from '@tanstack/query-core' describe('injectInfiniteQuery', () => { - let queryClient: QueryClient - - beforeEach(() => { - queryClient = new QueryClient() - vi.useFakeTimers() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], + describe('Discriminated union return type', () => { + test('data should be possibly undefined by default', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], + queryFn: ({ pageParam }) => + Promise.resolve('data on page ' + pageParam), + initialPageParam: 0, + getNextPageParam: () => 12, + })) + + expectTypeOf(query.data).toEqualTypeOf< + Signal | Signal> + >() }) - }) - afterEach(() => { - vi.useRealTimers() - }) + test('data should be defined when query is success', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], + queryFn: ({ pageParam }) => + Promise.resolve('data on page ' + pageParam), + initialPageParam: 0, + getNextPageParam: () => 12, + })) + + if (query.isSuccess()) { + expectTypeOf(query.data).toEqualTypeOf< + Signal> + >() + } + }) + + test('error should be null when query is success', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], + queryFn: ({ pageParam }) => + Promise.resolve('data on page ' + pageParam), + initialPageParam: 0, + getNextPageParam: () => 12, + })) + + if (query.isSuccess()) { + expectTypeOf(query.error).toEqualTypeOf>() + } + }) - test('should narrow type after isSuccess', () => { - const key = queryKey() - const query = TestBed.runInInjectionContext(() => { - return injectInfiniteQuery(() => ({ - queryKey: key, + test('data should be undefined when query is pending', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], queryFn: ({ pageParam }) => - sleep(0).then(() => 'data on page ' + pageParam), + Promise.resolve('data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, })) + + if (query.isPending()) { + expectTypeOf(query.data).toEqualTypeOf>() + } }) - if (query.isSuccess()) { - const data = query.data() - expectTypeOf(data).toEqualTypeOf>() - } + test('error should be defined when query is error', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], + queryFn: ({ pageParam }) => + Promise.resolve('data on page ' + pageParam), + initialPageParam: 0, + getNextPageParam: () => 12, + })) + + if (query.isError()) { + expectTypeOf(query.error).toEqualTypeOf>() + } + }) + }) + + it('should provide the correct types to the select function', () => { + const query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], + queryFn: ({ pageParam }) => Promise.resolve('data on page ' + pageParam), + initialPageParam: 0, + getNextPageParam: () => 12, + select: (data) => { + expectTypeOf(data).toEqualTypeOf>() + return data + }, + })) + + expectTypeOf(query.data).toEqualTypeOf< + Signal | Signal> + >() }) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts b/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts index 55890b38d2a..f587f6a2fe2 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-infinite-query.test.ts @@ -1,9 +1,9 @@ import { TestBed } from '@angular/core/testing' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { Injector, provideZonelessChangeDetection } from '@angular/core' -import { queryKey, sleep } from '@tanstack/query-test-utils' -import { QueryClient, injectInfiniteQuery, provideTanStackQuery } from '..' -import { expectSignals } from './test-utils' +import { ChangeDetectionStrategy, Component, Injector } from '@angular/core' +import { sleep } from '@tanstack/query-test-utils' +import { QueryClient, injectInfiniteQuery } from '..' +import { expectSignals, setupTanStackQueryTestBed } from './test-utils' describe('injectInfiniteQuery', () => { let queryClient: QueryClient @@ -11,12 +11,7 @@ describe('injectInfiniteQuery', () => { beforeEach(() => { queryClient = new QueryClient() vi.useFakeTimers() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], - }) + setupTanStackQueryTestBed(queryClient) }) afterEach(() => { @@ -24,16 +19,24 @@ describe('injectInfiniteQuery', () => { }) test('should properly execute infinite query', async () => { - const key = queryKey() - const query = TestBed.runInInjectionContext(() => { - return injectInfiniteQuery(() => ({ - queryKey: key, + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectInfiniteQuery(() => ({ + queryKey: ['infiniteQuery'], queryFn: ({ pageParam }) => sleep(10).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expectSignals(query, { data: undefined, @@ -65,33 +68,36 @@ describe('injectInfiniteQuery', () => { describe('injection context', () => { test('throws NG0203 with descriptive error outside injection context', () => { - const key = queryKey() expect(() => { injectInfiniteQuery(() => ({ - queryKey: key, + queryKey: ['injectionContextError'], queryFn: ({ pageParam }) => sleep(0).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, })) - }).toThrow(/NG0203(.*?)injectInfiniteQuery/) + }).toThrowError(/NG0203(.*?)injectInfiniteQuery/) }) test('can be used outside injection context when passing an injector', () => { - const key = queryKey() + const injector = TestBed.inject(Injector) + + // Call injectInfiniteQuery directly outside any component const query = injectInfiniteQuery( () => ({ - queryKey: key, + queryKey: ['manualInjector'], queryFn: ({ pageParam }) => sleep(0).then(() => 'data on page ' + pageParam), initialPageParam: 0, getNextPageParam: () => 12, }), { - injector: TestBed.inject(Injector), + injector: injector, }, ) + TestBed.tick() + expect(query.status()).toBe('pending') }) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts b/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts index e06d0322358..358a5da3e31 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-mutation-state.test.ts @@ -1,21 +1,22 @@ import { + ChangeDetectionStrategy, Component, Injector, input, + inputBinding, provideZonelessChangeDetection, signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' +import { render } from '@testing-library/angular' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { By } from '@angular/platform-browser' -import { queryKey, sleep } from '@tanstack/query-test-utils' +import { sleep } from '@tanstack/query-test-utils' import { QueryClient, injectMutation, injectMutationState, provideTanStackQuery, } from '..' -import { setFixtureSignalInputs } from './test-utils' describe('injectMutationState', () => { let queryClient: QueryClient @@ -37,7 +38,7 @@ describe('injectMutationState', () => { describe('injectMutationState', () => { test('should return variables after calling mutate 1', () => { - const mutationKey = queryKey() + const mutationKey = ['mutation'] const variables = 'foo123' const mutation = TestBed.runInInjectionContext(() => { @@ -60,8 +61,8 @@ describe('injectMutationState', () => { }) test('reactive options should update injectMutationState', () => { - const mutationKey1 = queryKey() - const mutationKey2 = queryKey() + const mutationKey1 = ['mutation1'] + const mutationKey2 = ['mutation2'] const variables1 = 'foo123' const variables2 = 'bar234' @@ -98,7 +99,7 @@ describe('injectMutationState', () => { test('should return variables after calling mutate 2', () => { queryClient.clear() - const mutationKey = queryKey() + const mutationKey = ['mutation'] const variables = 'bar234' const mutation = TestBed.runInInjectionContext(() => { @@ -145,6 +146,7 @@ describe('injectMutationState', () => { {{ mutation.status }} } `, + changeDetection: ChangeDetectionStrategy.OnPush, }) class FakeComponent { name = input.required() @@ -157,23 +159,36 @@ describe('injectMutationState', () => { })) } - const fixture = TestBed.createComponent(FakeComponent) - const { debugElement } = fixture - setFixtureSignalInputs(fixture, { name: fakeName }) + TestBed.resetTestingModule() + const name = signal(fakeName) + const rendered = await render(FakeComponent, { + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery(queryClient), + ], + bindings: [inputBinding('name', name.asReadonly())], + detectChangesOnRender: false, + }) + rendered.fixture.detectChanges() + + const fixture = rendered.fixture await vi.advanceTimersByTimeAsync(0) - let spans = debugElement - .queryAll(By.css('span')) - .map((span) => span.nativeNode.textContent) + const readSpans = () => + Array.from( + fixture.nativeElement.querySelectorAll( + 'span', + ) as NodeListOf, + ).map((span) => span.textContent) + + let spans = readSpans() expect(spans).toEqual(['pending', 'pending']) await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() - spans = debugElement - .queryAll(By.css('span')) - .map((span) => span.nativeNode.textContent) + spans = readSpans() expect(spans).toEqual(['success', 'error']) }) @@ -182,7 +197,7 @@ describe('injectMutationState', () => { test('throws NG0203 with descriptive error outside injection context', () => { expect(() => { injectMutationState() - }).toThrow(/NG0203(.*?)injectMutationState/) + }).toThrowError(/NG0203(.*?)injectMutationState/) }) test('can be used outside injection context when passing an injector', () => { diff --git a/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts b/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts index 7a7120ce02b..1899e9441d9 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-mutation.test.ts @@ -1,17 +1,21 @@ import { ApplicationRef, + ChangeDetectionStrategy, Component, Injector, + NgZone, input, + inputBinding, provideZonelessChangeDetection, signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' +import { render } from '@testing-library/angular' +import { sleep } from '@tanstack/query-test-utils' +import { firstValueFrom } from 'rxjs' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { By } from '@angular/platform-browser' -import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryClient, injectMutation, provideTanStackQuery } from '..' -import { expectSignals, setFixtureSignalInputs } from './test-utils' +import { expectSignals } from './test-utils' describe('injectMutation', () => { let queryClient: QueryClient @@ -118,9 +122,7 @@ describe('injectMutation', () => { const mutationCache = queryClient.getMutationCache() // Signal will be updated before the mutation is called // this test confirms that the mutation uses the updated value - const key1 = queryKey() - const key2 = queryKey() - const mutationKey = signal(key1) + const mutationKey = signal(['1']) const mutation = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ mutationKey: mutationKey(), @@ -128,13 +130,13 @@ describe('injectMutation', () => { })) }) - mutationKey.set(key2) + mutationKey.set(['2']) mutation.mutate('xyz') - const mutations = mutationCache.find({ mutationKey: key2 }) + const mutations = mutationCache.find({ mutationKey: ['2'] }) - expect(mutations?.options.mutationKey).toEqual(key2) + expect(mutations?.options.mutationKey).toEqual(['2']) }) test('should reset state after invoking mutation.reset', async () => { @@ -309,6 +311,7 @@ describe('injectMutation', () => { {{ mutation.data() }} `, + changeDetection: ChangeDetectionStrategy.OnPush, }) class FakeComponent { name = input.required() @@ -323,19 +326,28 @@ describe('injectMutation', () => { } } - const fixture = TestBed.createComponent(FakeComponent) - const { debugElement } = fixture - setFixtureSignalInputs(fixture, { name: 'value' }) + const name = signal('value') + const rendered = await render(FakeComponent, { + bindings: [inputBinding('name', name.asReadonly())], + detectChangesOnRender: false, + }) + rendered.fixture.detectChanges() + + const fixture = rendered.fixture - const button = debugElement.query(By.css('button')) - button.triggerEventHandler('click') + const hostButton = fixture.nativeElement.querySelector( + 'button', + ) as HTMLButtonElement + hostButton.click() await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() - const text = debugElement.query(By.css('span')).nativeElement.textContent - expect(text).toEqual('value') - const mutation = mutationCache.find({ mutationKey: ['fake', 'value'] }) + const span = fixture.nativeElement.querySelector('span') as HTMLSpanElement + expect(span.textContent).toEqual('value') + const mutation = mutationCache.find({ + mutationKey: ['fake', 'value'], + }) expect(mutation).toBeDefined() expect(mutation!.options.mutationKey).toStrictEqual(['fake', 'value']) }) @@ -349,6 +361,7 @@ describe('injectMutation', () => { {{ mutation.data() }} `, + changeDetection: ChangeDetectionStrategy.OnPush, }) class FakeComponent { name = input.required() @@ -361,28 +374,38 @@ describe('injectMutation', () => { mutate(): void { this.mutation.mutate() } + } - const fixture = TestBed.createComponent(FakeComponent) - const { debugElement } = fixture - setFixtureSignalInputs(fixture, { name: 'value' }) + const name = signal('value') + const rendered = await render(FakeComponent, { + bindings: [inputBinding('name', name.asReadonly())], + detectChangesOnRender: false, + }) + rendered.fixture.detectChanges() - const button = debugElement.query(By.css('button')) - const span = debugElement.query(By.css('span')) + const fixture = rendered.fixture - button.triggerEventHandler('click') + let button = fixture.nativeElement.querySelector( + 'button', + ) as HTMLButtonElement + button.click() await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() - expect(span.nativeElement.textContent).toEqual('value') + let span = fixture.nativeElement.querySelector('span') as HTMLSpanElement + expect(span.textContent).toEqual('value') - setFixtureSignalInputs(fixture, { name: 'updatedValue' }) + name.set('updatedValue') + fixture.detectChanges() - button.triggerEventHandler('click') + button = fixture.nativeElement.querySelector('button') as HTMLButtonElement + button.click() await vi.advanceTimersByTimeAsync(11) fixture.detectChanges() - expect(span.nativeElement.textContent).toEqual('updatedValue') + span = fixture.nativeElement.querySelector('span') as HTMLSpanElement + expect(span.textContent).toEqual('updatedValue') const mutations = mutationCache.findAll() expect(mutations.length).toBe(2) @@ -393,12 +416,11 @@ describe('injectMutation', () => { describe('throwOnError', () => { test('should evaluate throwOnError when mutation is expected to throw', async () => { - const key = queryKey() const err = new Error('Expected mock error. All is well!') const boundaryFn = vi.fn() const { mutate } = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ - mutationKey: key, + mutationKey: ['fake'], mutationFn: () => { return Promise.reject(err) }, @@ -416,36 +438,42 @@ describe('injectMutation', () => { expect(boundaryFn).toHaveBeenCalledWith(err) }) - test('should throw when throwOnError is true and mutate is used', async () => { - const key = queryKey() - const { mutate } = TestBed.runInInjectionContext(() => { - return injectMutation(() => ({ - mutationKey: key, + test('should emit zone error when throwOnError is true and mutate is used', async () => { + const err = new Error('Expected mock error. All is well!') + const zone = TestBed.inject(NgZone) + const zoneErrorEmitSpy = vi.spyOn(zone.onError, 'emit') + const runSpy = vi.spyOn(zone, 'run').mockImplementation((callback: any) => { + try { + return callback() + } catch { + return undefined + } + }) + + const { mutate } = TestBed.runInInjectionContext(() => + injectMutation(() => ({ + mutationKey: ['fake'], mutationFn: () => { - return Promise.reject( - new Error('Expected mock error. All is well!'), - ) + return sleep(0).then(() => Promise.reject(err)) }, throwOnError: true, - })) - }) - - TestBed.tick() + })), + ) mutate() - await expect(vi.advanceTimersByTimeAsync(0)).rejects.toThrow( - 'Expected mock error. All is well!', - ) + await vi.runAllTimersAsync() + + expect(zoneErrorEmitSpy).toHaveBeenCalledWith(err) + expect(runSpy).toHaveBeenCalled() }) }) test('should throw when throwOnError is true', async () => { - const key = queryKey() const err = new Error('Expected mock error. All is well!') const { mutateAsync } = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ - mutationKey: key, + mutationKey: ['fake'], mutationFn: () => { return Promise.reject(err) }, @@ -453,15 +481,14 @@ describe('injectMutation', () => { })) }) - await expect(() => mutateAsync()).rejects.toThrow(err) + await expect(() => mutateAsync()).rejects.toThrowError(err) }) test('should throw when throwOnError function returns true', async () => { - const key = queryKey() const err = new Error('Expected mock error. All is well!') const { mutateAsync } = TestBed.runInInjectionContext(() => { return injectMutation(() => ({ - mutationKey: key, + mutationKey: ['fake'], mutationFn: () => { return Promise.reject(err) }, @@ -469,26 +496,24 @@ describe('injectMutation', () => { })) }) - await expect(() => mutateAsync()).rejects.toThrow(err) + await expect(() => mutateAsync()).rejects.toThrowError(err) }) describe('injection context', () => { test('throws NG0203 with descriptive error outside injection context', () => { - const key = queryKey() expect(() => { injectMutation(() => ({ - mutationKey: key, + mutationKey: ['injectionContextError'], mutationFn: () => Promise.resolve(), })) - }).toThrow(/NG0203(.*?)injectMutation/) + }).toThrowError(/NG0203(.*?)injectMutation/) }) test('can be used outside injection context when passing an injector', () => { - const key = queryKey() expect(() => { injectMutation( () => ({ - mutationKey: key, + mutationKey: ['injectionContextError'], mutationFn: () => Promise.resolve(), }), { @@ -503,10 +528,9 @@ describe('injectMutation', () => { let mutationStarted = false let mutationCompleted = false - const key = queryKey() const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ - mutationKey: key, + mutationKey: ['pendingTasksTest'], mutationFn: async (data: string) => { mutationStarted = true await sleep(50) @@ -597,11 +621,9 @@ describe('injectMutation', () => { const app = TestBed.inject(ApplicationRef) let callCount = 0 - const key = queryKey() - const mutation1 = TestBed.runInInjectionContext(() => injectMutation(() => ({ - mutationKey: key, + mutationKey: ['sync-mutation-key'], mutationFn: async (data: string) => { callCount++ return `mutation1: ${data}` @@ -611,7 +633,7 @@ describe('injectMutation', () => { const mutation2 = TestBed.runInInjectionContext(() => injectMutation(() => ({ - mutationKey: key, + mutationKey: ['sync-mutation-key'], mutationFn: async (data: string) => { callCount++ return `mutation2: ${data}` @@ -649,7 +671,7 @@ describe('injectMutation', () => { }) const app = TestBed.inject(ApplicationRef) - const testQueryKey = queryKey() + const testQueryKey = ['sync-optimistic'] let onMutateCalled = false let onSuccessCalled = false @@ -658,12 +680,13 @@ describe('injectMutation', () => { const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ - mutationFn: async (data: string) => `final: ${data}`, // Synchronous resolution + mutationFn: async (data: string) => { + await sleep(50) + return `final: ${data}` + }, onMutate: async (variables) => { onMutateCalled = true - const previousData = queryClient.getQueryData(testQueryKey) queryClient.setQueryData(testQueryKey, `optimistic: ${variables}`) - return { previousData } }, onSuccess: (data) => { onSuccessCalled = true @@ -672,19 +695,30 @@ describe('injectMutation', () => { })), ) + // Run effects + TestBed.tick() + // Start mutation + expect(queryClient.getQueryData(testQueryKey)).toBe('initial') mutation.mutate('test') - // Synchronize pending effects - TestBed.tick() - - const stablePromise = app.whenStable() // Flush microtasks to allow TanStack Query's scheduled notifications to process await Promise.resolve() - await vi.advanceTimersByTimeAsync(1) - await stablePromise + // Check for optimistic update in the same macro task expect(onMutateCalled).toBe(true) + expect(queryClient.getQueryData(testQueryKey)).toBe('optimistic: test') + + // Check stability before the mutation completes, waiting for the next macro task + await vi.advanceTimersByTimeAsync(0) + expect(mutation.isPending()).toBe(true) + expect(await firstValueFrom(app.isStable)).toBe(false) + + // Wait for the mutation to complete + const stablePromise = app.whenStable() + await vi.advanceTimersByTimeAsync(60) + await stablePromise + expect(onSuccessCalled).toBe(true) expect(mutation.isSuccess()).toBe(true) expect(mutation.data()).toBe('final: test') @@ -702,10 +736,9 @@ describe('injectMutation', () => { const app = TestBed.inject(ApplicationRef) - const key = queryKey() const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ - mutationKey: key, + mutationKey: ['cancel-sync'], mutationFn: async (data: string) => `processed: ${data}`, // Synchronous resolution })), ) diff --git a/packages/angular-query-experimental/src/__tests__/inject-queries.test-d.ts b/packages/angular-query-experimental/src/__tests__/inject-queries.test-d.ts index b133575746c..6c2e373e442 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-queries.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-queries.test-d.ts @@ -1,196 +1,261 @@ import { describe, expectTypeOf, it } from 'vitest' -import { queryKey } from '@tanstack/query-test-utils' -import { skipToken } from '..' -import { injectQueries } from '../inject-queries' +import { injectQueries, skipToken } from '..' import { queryOptions } from '../query-options' import type { CreateQueryOptions, CreateQueryResult, OmitKeyof } from '..' import type { Signal } from '@angular/core' -describe('injectQueries', () => { - describe('config object overload', () => { - it('TData should always be defined when initialData is provided as an object', () => { - const key1 = queryKey() - const key2 = queryKey() - const key3 = queryKey() - - const query1 = { - queryKey: key1, - queryFn: () => { - return { - wow: true, - } - }, - initialData: { - wow: false, - }, - } +describe('InjectQueries config object overload', () => { + it('TData should always be defined when initialData is provided as an object', () => { + const query1 = { + queryKey: ['key1'], + queryFn: () => { + return { + wow: true, + } + }, + initialData: { + wow: false, + }, + } - const query2 = { - queryKey: key2, - queryFn: () => 'Query Data', - initialData: 'initial data', - } + const query2 = { + queryKey: ['key2'], + queryFn: () => 'Query Data', + initialData: 'initial data', + } - const query3 = { - queryKey: key3, - queryFn: () => 'Query Data', - } + const query3 = { + queryKey: ['key2'], + queryFn: () => 'Query Data', + } - const queryResults = injectQueries(() => ({ - queries: [query1, query2, query3], - })) + const queryResults = injectQueries(() => ({ + queries: [query1, query2, query3], + })) - const query1Data = queryResults()[0].data() - const query2Data = queryResults()[1].data() - const query3Data = queryResults()[2].data() + const query1Data = queryResults()[0].data() + const query2Data = queryResults()[1].data() + const query3Data = queryResults()[2].data() - expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean }>() - expectTypeOf(query2Data).toEqualTypeOf() - expectTypeOf(query3Data).toEqualTypeOf() - }) + expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean }>() + expectTypeOf(query2Data).toEqualTypeOf() + expectTypeOf(query3Data).toEqualTypeOf() + }) - it('TData should be defined when passed through queryOptions', () => { - const key = queryKey() - const options = queryOptions({ - queryKey: key, - queryFn: () => { - return { - wow: true, - } - }, - initialData: { + it('TData should be defined when passed through queryOptions', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => { + return { wow: true, - }, - }) - const queryResults = injectQueries(() => ({ queries: [options] })) + } + }, + initialData: { + wow: true, + }, + }) + const queryResults = injectQueries(() => ({ queries: [options] })) - const data = queryResults()[0].data() + const data = queryResults()[0].data() + + expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() + }) - expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() + it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into injectQuery', () => { + const query1 = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(1), + select: (data) => data > 1, }) - it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into injectQuery', () => { - const key1 = queryKey() - const key2 = queryKey() + const query2 = { + queryKey: ['key'], + queryFn: () => Promise.resolve(1), + select: (data: number) => data > 1, + } - const query1 = queryOptions({ - queryKey: key1, - queryFn: () => Promise.resolve(1), - select: (data) => data > 1, - }) + const queryResults = injectQueries(() => ({ queries: [query1, query2] })) + const query1Data = queryResults()[0].data() + const query2Data = queryResults()[1].data() - const query2 = { - queryKey: key2, - queryFn: () => Promise.resolve(1), - select: (data: number) => data > 1, - } + expectTypeOf(query1Data).toEqualTypeOf() + expectTypeOf(query2Data).toEqualTypeOf() + }) - const queryResults = injectQueries(() => ({ queries: [query1, query2] })) - const query1Data = queryResults()[0].data() - const query2Data = queryResults()[1].data() + it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { + const queryResults = injectQueries(() => ({ + queries: [ + { + queryKey: ['key'], + queryFn: () => { + return { + wow: true, + } + }, + initialData: () => undefined as { wow: boolean } | undefined, + }, + ], + })) - expectTypeOf(query1Data).toEqualTypeOf() - expectTypeOf(query2Data).toEqualTypeOf() - }) + const data = queryResults()[0].data() + + expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() + }) - it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { - const key = queryKey() - const queryResults = injectQueries(() => ({ - queries: [ - { - queryKey: key, - queryFn: () => { - return { - wow: true, - } + describe('custom injectable', () => { + it('should allow custom hooks using UseQueryOptions', () => { + type Data = string + + const injectCustomQueries = ( + options?: OmitKeyof, 'queryKey' | 'queryFn'>, + ) => { + return injectQueries(() => ({ + queries: [ + { + ...options, + queryKey: ['todos-key'], + queryFn: () => Promise.resolve('data'), }, - initialData: () => undefined as { wow: boolean } | undefined, - }, - ], - })) + ], + })) + } + const queryResults = injectCustomQueries() const data = queryResults()[0].data() - expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() + expectTypeOf(data).toEqualTypeOf() }) + }) - describe('custom injectable', () => { - it('should allow custom hooks using UseQueryOptions', () => { - type Data = string - const key = queryKey() - - const injectCustomQueries = ( - options?: OmitKeyof, 'queryKey' | 'queryFn'>, - ) => { - return injectQueries(() => ({ - queries: [ - { - ...options, - queryKey: key, - queryFn: () => Promise.resolve('data'), - }, - ], - })) - } + it('TData should have correct type when conditional skipToken is passed', () => { + const queryResults = injectQueries(() => ({ + queries: [ + { + queryKey: ['withSkipToken'], + queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), + }, + ], + })) - const queryResults = injectCustomQueries() - const data = queryResults()[0].data() + const firstResult = queryResults()[0] - expectTypeOf(data).toEqualTypeOf() - }) - }) + expectTypeOf(firstResult).toEqualTypeOf>() + expectTypeOf(firstResult.data()).toEqualTypeOf() + }) - it('TData should have correct type when conditional skipToken is passed', () => { - const key = queryKey() - const queryResults = injectQueries(() => ({ - queries: [ - { - queryKey: key, - queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), - }, - ], - })) + it('should return correct data for dynamic queries with mixed result types', () => { + const Queries1 = { + get: () => + queryOptions({ + queryKey: ['key1'], + queryFn: () => Promise.resolve(1), + }), + } + const Queries2 = { + get: () => + queryOptions({ + queryKey: ['key2'], + queryFn: () => Promise.resolve(true), + }), + } - const firstResult = queryResults()[0] + const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) + const result = injectQueries(() => ({ + queries: [...queries1List, { ...Queries2.get() }], + })) - expectTypeOf(firstResult).toEqualTypeOf< - CreateQueryResult - >() - expectTypeOf(firstResult.data()).toEqualTypeOf() - }) + expectTypeOf(result).branded.toEqualTypeOf< + Signal< + [ + ...Array>, + CreateQueryResult, + ] + > + >() + }) +}) - it('should return correct data for dynamic queries with mixed result types', () => { - const key1 = queryKey() - const key2 = queryKey() +describe('InjectQueries combine', () => { + it('should provide the correct type for the combine function', () => { + injectQueries(() => ({ + queries: [ + { + queryKey: ['key'], + queryFn: () => Promise.resolve(1), + }, + { + queryKey: ['key2'], + queryFn: () => Promise.resolve(true), + }, + ], + combine: (results) => { + expectTypeOf(results[0].data).toEqualTypeOf() + expectTypeOf(results[0].refetch).toBeCallableWith() + expectTypeOf(results[1].data).toEqualTypeOf() + expectTypeOf(results[1].refetch).toBeCallableWith() + }, + })) + }) - const Queries1 = { - get: () => - queryOptions({ - queryKey: key1, - queryFn: () => Promise.resolve(1), - }), - } - const Queries2 = { - get: () => - queryOptions({ - queryKey: key2, - queryFn: () => Promise.resolve(true), - }), - } + it('should provide the correct types on the combined result with initial data', () => { + injectQueries(() => ({ + queries: [ + { + queryKey: ['key'], + queryFn: () => Promise.resolve(1), + initialData: 1, + }, + ], + combine: (results) => { + expectTypeOf(results[0].data).toEqualTypeOf() + expectTypeOf(results[0].refetch).toBeCallableWith() + }, + })) + }) - const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) - const result = injectQueries(() => ({ - queries: [...queries1List, { ...Queries2.get() }], - })) - - expectTypeOf(result).branded.toEqualTypeOf< - Signal< - [ - ...Array>, - CreateQueryResult, - ] - > - >() - }) + it('should provide the correct result type', () => { + const queryResults = injectQueries(() => ({ + queries: [ + { + queryKey: ['key'], + queryFn: () => Promise.resolve(1), + }, + { + queryKey: ['key2'], + queryFn: () => Promise.resolve(true), + }, + ], + combine: (results) => ({ + data: { + 1: results[0].data, + 2: results[1].data, + }, + fn: () => {}, + }), + })) + + expectTypeOf(queryResults).branded.toEqualTypeOf< + Signal<{ + data: { + 1: number | undefined + 2: boolean | undefined + } + fn: () => void + }> + >() + }) + + it('should provide the correct types on the result with initial data', () => { + const queryResults = injectQueries(() => ({ + queries: [ + { + queryKey: ['key'], + queryFn: () => Promise.resolve(1), + initialData: 1, + }, + ], + })) + + expectTypeOf(queryResults()[0].data()).toEqualTypeOf() }) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-queries.test.ts b/packages/angular-query-experimental/src/__tests__/inject-queries.test.ts index d85e1985b3e..8c4a6a8c09c 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-queries.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-queries.test.ts @@ -1,26 +1,30 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { render } from '@testing-library/angular' import { + ApplicationRef, + ChangeDetectionStrategy, Component, + Injector, + computed, effect, + input, + inputBinding, provideZonelessChangeDetection, + signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' -import { render } from '@testing-library/angular' import { queryKey, sleep } from '@tanstack/query-test-utils' -import { QueryClient, provideTanStackQuery } from '..' +import { QueryClient, provideIsRestoring, provideTanStackQuery } from '..' import { injectQueries } from '../inject-queries' +import { setupTanStackQueryTestBed } from './test-utils' let queryClient: QueryClient beforeEach(() => { vi.useFakeTimers() queryClient = new QueryClient() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], - }) + vi.useFakeTimers({ shouldAdvanceTime: true }) + setupTanStackQueryTestBed(queryClient) }) afterEach(() => { @@ -28,6 +32,36 @@ afterEach(() => { }) describe('injectQueries', () => { + it('throws NG0203 with descriptive error outside injection context', () => { + expect(() => { + injectQueries(() => ({ + queries: [ + { + queryKey: ['injectionContextError'], + queryFn: () => Promise.resolve(1), + }, + ], + })) + }).toThrowError(/NG0203(.*?)injectQueries/) + }) + + it('can be used outside injection context when passing an injector', () => { + const injector = TestBed.inject(Injector) + const queries = injectQueries( + () => ({ + queries: [ + { + queryKey: ['manualInjector'], + queryFn: () => Promise.resolve(1), + }, + ], + }), + injector, + ) + + expect(queries()[0].status()).toBe('pending') + }) + it('should return the correct states', async () => { const key1 = queryKey() const key2 = queryKey() @@ -37,14 +71,18 @@ describe('injectQueries', () => { template: `
- data1: {{ result()[0].data() ?? 'null' }}, data2: - {{ result()[1].data() ?? 'null' }} + data1: {{ queries()[0].data() ?? 'null' }}, data2: + {{ queries()[1].data() ?? 'null' }}
`, + changeDetection: ChangeDetectionStrategy.OnPush, }) class Page { - result = injectQueries(() => ({ + toString(val: any) { + return String(val) + } + queries = injectQueries(() => ({ queries: [ { queryKey: key1, @@ -57,8 +95,8 @@ describe('injectQueries', () => { ], })) - _ = effect(() => { - const snapshot = this.result().map((q) => ({ data: q.data() })) + _pushResults = effect(() => { + const snapshot = this.queries().map((q) => ({ data: q.data() })) results.push(snapshot) }) } @@ -80,4 +118,542 @@ describe('injectQueries', () => { expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }]) expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) }) + + it('should support combining results', async () => { + const key1 = queryKey() + const key2 = queryKey() + let count = 0 + + const results: Array<{ data: string; refetch: () => void }> = [] + + @Component({ + template: `
data: {{ queries().data }}
`, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: key1, + queryFn: async () => { + await new Promise((r) => setTimeout(r, 10)) + count++ + return count + }, + }, + { + queryKey: key2, + queryFn: async () => { + await new Promise((r) => setTimeout(r, 100)) + count++ + return count + }, + }, + ], + combine: (queryResults) => { + return { + refetch: () => queryResults.forEach((r) => r.refetch()), + data: queryResults.map((r) => r.data).join(','), + } + }, + })) + + _pushResults = effect(() => { + results.push(this.queries()) + }) + } + + const rendered = await render(Page) + const instance = rendered.fixture.componentInstance + await rendered.findByText('data: 1,2') + expect(instance.queries().data).toBe('1,2') + + instance.queries().refetch() + + await rendered.findByText('data: 3,4') + expect(instance.queries().data).toBe('3,4') + + expect(results).toHaveLength(5) + expect(results[0]).toMatchObject({ + data: ',', + refetch: expect.any(Function), + }) + expect(results[1]).toMatchObject({ + data: '1,', + refetch: expect.any(Function), + }) + expect(results[2]).toMatchObject({ + data: '1,2', + refetch: expect.any(Function), + }) + expect(results[3]).toMatchObject({ + data: '3,2', + refetch: expect.any(Function), + }) + expect(results[4]).toMatchObject({ + data: '3,4', + refetch: expect.any(Function), + }) + }) + + it('should handle mixed success and error query states', async () => { + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['mixed-error'], + retry: false, + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + throw new Error('mixed-error') + }, + }, + { + queryKey: ['mixed-success'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 20)) + return 'mixed-success' + }, + }, + ], + })) + } + + const rendered = await render(Page) + await vi.advanceTimersByTimeAsync(25) + await Promise.resolve() + + const [errorQuery, successQuery] = rendered.fixture.componentInstance.queries() + expect(errorQuery.status()).toBe('error') + expect(errorQuery.error()?.message).toBe('mixed-error') + expect(successQuery.status()).toBe('success') + expect(successQuery.data()).toBe('mixed-success') + }) + + it('should cleanup pending tasks when component with active queries is destroyed', async () => { + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['destroy-query-1'], + queryFn: async () => { + await sleep(100) + return 'one' + }, + }, + { + queryKey: ['destroy-query-2'], + queryFn: async () => { + await sleep(100) + return 'two' + }, + }, + ], + })) + } + + // Use a fixture here on purpose: we need component teardown + whenStable() semantics. + const fixture = TestBed.createComponent(Page) + fixture.detectChanges() + expect(fixture.isStable()).toBe(false) + + fixture.destroy() + + const stablePromise = fixture.whenStable() + await vi.advanceTimersByTimeAsync(150) + await stablePromise + + expect(fixture.isStable()).toBe(true) + }) + + it('should react to enabled signal changes', async () => { + const enabled = signal(false) + const fetchSpy = vi.fn(() => sleep(10).then(() => 'enabled-data')) + + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + enabled = enabled + fetchSpy = fetchSpy + + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['enabled', this.enabled()], + queryFn: this.fetchSpy, + enabled: this.enabled(), + }, + ], + })) + } + + const rendered = await render(Page) + const query = rendered.fixture.componentInstance.queries()[0] + + expect(fetchSpy).not.toHaveBeenCalled() + expect(query.status()).toBe('pending') + + enabled.set(true) + rendered.fixture.detectChanges() + await vi.advanceTimersByTimeAsync(11) + await Promise.resolve() + + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(query.status()).toBe('success') + expect(query.data()).toBe('enabled-data') + }) + + it('should refetch only changed keys when queries length stays the same', async () => { + const ids = signal<[string, string]>(['a', 'b']) + const firstSpy = vi.fn((context: any) => + sleep(10).then(() => `first-${context.queryKey[1]}`), + ) + const secondSpy = vi.fn((context: any) => + sleep(10).then(() => `second-${context.queryKey[1]}`), + ) + + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + ids = ids + firstSpy = firstSpy + secondSpy = secondSpy + + queries = injectQueries(() => ({ + queries: [ + { + staleTime: Number.POSITIVE_INFINITY, + queryKey: ['first', this.ids()[0]], + queryFn: this.firstSpy, + }, + { + staleTime: Number.POSITIVE_INFINITY, + queryKey: ['second', this.ids()[1]], + queryFn: this.secondSpy, + }, + ], + })) + } + + const rendered = await render(Page) + await vi.advanceTimersByTimeAsync(11) + await Promise.resolve() + + let [firstQuery, secondQuery] = rendered.fixture.componentInstance.queries() + expect(firstQuery.data()).toBe('first-a') + expect(secondQuery.data()).toBe('second-b') + expect(firstSpy).toHaveBeenCalledTimes(1) + expect(secondSpy).toHaveBeenCalledTimes(1) + + ids.set(['c', 'b']) + rendered.fixture.detectChanges() + await vi.advanceTimersByTimeAsync(11) + await Promise.resolve() + + ;[firstQuery, secondQuery] = rendered.fixture.componentInstance.queries() + expect(firstQuery.data()).toBe('first-c') + expect(secondQuery.data()).toBe('second-b') + expect(firstSpy).toHaveBeenCalledTimes(2) + expect(secondSpy).toHaveBeenCalledTimes(1) + }) + + it('should support changes on the queries array', async () => { + const results: Array>> = [] + + @Component({ + template: `
data: {{ mapped() }}
`, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: queries().map((q) => ({ + queryKey: ['query', q], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 20 * q)) + return q + }, + })), + })) + + mapped = computed(() => { + const queryData = this.queries().map((q) => q.data()) + if (queryData.length === 0) return 'empty' + return queryData.join(',') + }) + + _pushResults = effect(() => { + const snapshot = this.queries().map((q) => ({ data: q.data() })) + results.push(snapshot) + }) + } + + const queries = signal([1, 2, 4]) + + const rendered = await render(Page) + const instance = rendered.fixture.componentInstance + + await rendered.findByText('data: 1,2,4') + expect(instance.mapped()).toBe('1,2,4') + + expect(results.length).toBe(4) + expect(results[0]).toMatchObject([ + { data: undefined }, + { data: undefined }, + { data: undefined }, + ]) + expect(results[1]).toMatchObject([ + { data: 1 }, + { data: undefined }, + { data: undefined }, + ]) + expect(results[2]).toMatchObject([ + { data: 1 }, + { data: 2 }, + { data: undefined }, + ]) + expect(results[3]).toMatchObject([{ data: 1 }, { data: 2 }, { data: 4 }]) + + queries.set([3, 4]) + await rendered.findByText('data: 3,4') + expect(instance.mapped()).toBe('3,4') + + const hasOptimisticTransition = results.some( + (snapshot) => + snapshot.length === 2 && + snapshot[0]?.data === undefined && + snapshot[1]?.data === 4, + ) + expect(hasOptimisticTransition).toBe(true) + expect(results[results.length - 1]).toMatchObject([{ data: 3 }, { data: 4 }]) + + queries.set([]) + await rendered.findByText('data: empty') + expect(instance.mapped()).toBe('empty') + + expect(results[results.length - 1]).toMatchObject([]) + }) + + it('should change the rendered component when the queries array changes', async () => { + const userIds = signal([1, 2]) + + @Component({ + template: ` +
    + @for (query of queries(); track $index) { + @if (query.data(); as data) { +
  • {{ data.value }}
  • + } + } +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + userIds = userIds + + queries = injectQueries(() => ({ + queries: this.userIds().map((id) => ({ + queryKey: ['user', id], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 20)) + return { value: String(id) } + }, + })), + })) + } + + const rendered = await render(Page) + + await rendered.findByText('1') + await rendered.findByText('2') + + userIds.set([3]) + rendered.fixture.detectChanges() + + await rendered.findByText('3') + expect(rendered.queryByText('1')).toBeNull() + expect(rendered.queryByText('2')).toBeNull() + }) + + it('should support required signal inputs', async () => { + @Component({ + selector: 'app-fake', + template: `{{ queries()[0].data() }}`, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class FakeComponent { + name = input.required() + + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['fake', this.name()], + queryFn: () => this.name(), + }, + ], + })) + } + + const name = signal('signal-input-required-test') + const rendered = await render(FakeComponent, { + bindings: [inputBinding('name', name.asReadonly())], + detectChangesOnRender: false, + }) + rendered.fixture.detectChanges() + await vi.advanceTimersByTimeAsync(0) + + const result = rendered.fixture.nativeElement.textContent + expect(result).toEqual('signal-input-required-test') + }) + + it('should pause fetching while restoring and fetch once restoring is disabled', async () => { + const isRestoring = signal(true) + const fetchSpy = vi.fn(() => sleep(10).then(() => 'restored-data')) + setupTanStackQueryTestBed(queryClient, { + providers: [provideIsRestoring(isRestoring.asReadonly())], + }) + + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['restoring'], + queryFn: fetchSpy, + }, + ], + })) + } + + const fixture = TestBed.createComponent(Page) + fixture.detectChanges() + + expect(fetchSpy).not.toHaveBeenCalled() + expect(fixture.componentInstance.queries()[0].status()).toBe('pending') + + const stablePromise = fixture.whenStable() + await Promise.resolve() + await stablePromise + + isRestoring.set(false) + fixture.detectChanges() + + await vi.advanceTimersByTimeAsync(11) + await fixture.whenStable() + + const result = fixture.componentInstance.queries()[0] + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(result.status()).toBe('success') + expect(result.data()).toBe('restored-data') + }) + + it('should complete queries before whenStable resolves', async () => { + const app = TestBed.inject(ApplicationRef) + + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['query-1'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return 1 + }, + }, + { + queryKey: ['query-2'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 20)) + return 2 + }, + }, + ], + })) + } + + const fixture = TestBed.createComponent(Page) + fixture.detectChanges() + + const stablePromise = app.whenStable() + let stableResolved = false + void stablePromise.then(() => { + stableResolved = true + }) + + await Promise.resolve() + expect(stableResolved).toBe(false) + + await vi.advanceTimersByTimeAsync(25) + await stablePromise + + const result = fixture.componentInstance.queries() + expect(result[0].status()).toBe('success') + expect(result[1].status()).toBe('success') + expect(result[0].data()).toBe(1) + expect(result[1].data()).toBe(2) + }) + + it('should use latest query key for aliased refetch function', async () => { + const key = signal('one') + const fetchSpy = vi.fn(async (context: any) => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return context.queryKey[1] + }) + + @Component({ + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class Page { + key = key + fetchSpy = fetchSpy + + queries = injectQueries(() => ({ + queries: [ + { + queryKey: ['query', this.key()], + queryFn: this.fetchSpy, + enabled: false, + }, + ], + })) + } + + const rendered = await render(Page) + const query = rendered.fixture.componentInstance.queries()[0] + const refetch = query.refetch + + key.set('two') + rendered.fixture.detectChanges() + + const refetchPromise = refetch() + await vi.advanceTimersByTimeAsync(15) + await refetchPromise + + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(fetchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ['query', 'two'], + }), + ) + }) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts index d48146aa677..353890bff44 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts @@ -1,194 +1,191 @@ import { describe, expectTypeOf, it, test } from 'vitest' -import { queryKey, sleep } from '@tanstack/query-test-utils' +import { sleep } from '@tanstack/query-test-utils' import { injectQuery, queryOptions } from '..' import type { Signal } from '@angular/core' -describe('injectQuery', () => { - describe('initialData', () => { - describe('Config object overload', () => { - it('TData should always be defined when initialData is provided as an object', () => { - const key = queryKey() - const { data } = injectQuery(() => ({ - queryKey: key, - queryFn: () => ({ wow: true }), - initialData: { wow: true }, - })) - - expectTypeOf(data).toEqualTypeOf>() - }) - - it('TData should be defined when passed through queryOptions', () => { - const key = queryKey() - const options = () => - queryOptions({ - queryKey: key, - queryFn: () => { - return { - wow: true, - } - }, - initialData: { - wow: true, - }, - }) - const { data } = injectQuery(options) - - expectTypeOf(data).toEqualTypeOf>() - }) - - it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { - const key = queryKey() - const options = queryOptions({ - queryKey: key, - queryFn: () => Promise.resolve(1), - }) - - const query = injectQuery(() => ({ - ...options, - select: (data) => data > 1, - })) +describe('initialData', () => { + describe('Config object overload', () => { + it('TData should always be defined when initialData is provided as an object', () => { + const { data } = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => ({ wow: true }), + initialData: { wow: true }, + })) - expectTypeOf(query.data).toEqualTypeOf>() - }) + expectTypeOf(data).toEqualTypeOf>() + }) - it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { - const key = queryKey() - const { data } = injectQuery(() => ({ - queryKey: key, + it('TData should be defined when passed through queryOptions', () => { + const options = () => + queryOptions({ + queryKey: ['key'], queryFn: () => { return { wow: true, } }, - initialData: () => ({ + initialData: { wow: true, - }), - })) - - expectTypeOf(data).toEqualTypeOf>() - }) - - it('TData should have undefined in the union when initialData is NOT provided', () => { - const key = queryKey() - const { data } = injectQuery(() => ({ - queryKey: key, - queryFn: () => { - return { - wow: true, - } }, - })) + }) + const { data } = injectQuery(options) - expectTypeOf(data).toEqualTypeOf>() - }) + expectTypeOf(data).toEqualTypeOf>() + }) - it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { - const key = queryKey() - const { data } = injectQuery(() => ({ - queryKey: key, - queryFn: () => { - return { - wow: true, - } - }, - initialData: () => undefined as { wow: boolean } | undefined, - })) + it('should support selection function with select', () => { + const options = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => '1', + select: (data) => { + expectTypeOf(data).toEqualTypeOf() + return parseInt(data) + }, + })) + expectTypeOf(options.data).toEqualTypeOf>() + }) - expectTypeOf(data).toEqualTypeOf>() + it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQuery', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(1), }) - it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => { - const key = queryKey() - const query = injectQuery(() => ({ - queryKey: key, - queryFn: () => { - return { - wow: true, - } - }, - initialData: () => undefined as { wow: boolean } | undefined, - })) + const query = injectQuery(() => ({ + ...options, + select: (data) => data > 1, + })) - if (query.isSuccess()) { - expectTypeOf(query.data).toEqualTypeOf>() - } - }) + expectTypeOf(query.data).toEqualTypeOf>() }) - describe('structuralSharing', () => { - it('should be able to use structuralSharing with unknown types', () => { - const key = queryKey() - // https://github.com/TanStack/query/issues/6525#issuecomment-1938411343 - injectQuery(() => ({ - queryKey: key, - queryFn: () => 5, - structuralSharing: (oldData, newData) => { - expectTypeOf(oldData).toBeUnknown() - expectTypeOf(newData).toBeUnknown() - return newData - }, - })) - }) + it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => { + const { data } = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => { + return { + wow: true, + } + }, + initialData: () => ({ + wow: true, + }), + })) + + expectTypeOf(data).toEqualTypeOf>() }) - }) - describe('Discriminated union return type', () => { - test('data should be possibly undefined by default', () => { - const key = queryKey() - const query = injectQuery(() => ({ - queryKey: key, - queryFn: () => sleep(0).then(() => 'Some data'), + it('TData should have undefined in the union when initialData is NOT provided', () => { + const { data } = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => { + return { + wow: true, + } + }, })) - expectTypeOf(query.data).toEqualTypeOf>() + expectTypeOf(data).toEqualTypeOf>() }) - test('data should be defined when query is success', () => { - const key = queryKey() - const query = injectQuery(() => ({ - queryKey: key, - queryFn: () => sleep(0).then(() => 'Some data'), + it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { + const { data } = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => { + return { + wow: true, + } + }, + initialData: () => undefined as { wow: boolean } | undefined, })) - if (query.isSuccess()) { - expectTypeOf(query.data).toEqualTypeOf>() - } + expectTypeOf(data).toEqualTypeOf>() }) - test('error should be null when query is success', () => { - const key = queryKey() + it('TData should be narrowed after an isSuccess check when initialData is provided as a function which can return undefined', () => { const query = injectQuery(() => ({ - queryKey: key, - queryFn: () => sleep(0).then(() => 'Some data'), + queryKey: ['key'], + queryFn: () => { + return { + wow: true, + } + }, + initialData: () => undefined as { wow: boolean } | undefined, })) if (query.isSuccess()) { - expectTypeOf(query.error).toEqualTypeOf>() + expectTypeOf(query.data).toEqualTypeOf>() } }) + }) - test('data should be undefined when query is pending', () => { - const key = queryKey() - const query = injectQuery(() => ({ - queryKey: key, - queryFn: () => sleep(0).then(() => 'Some data'), + describe('structuralSharing', () => { + it('should be able to use structuralSharing with unknown types', () => { + // https://github.com/TanStack/query/issues/6525#issuecomment-1938411343 + injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => 5, + structuralSharing: (oldData, newData) => { + expectTypeOf(oldData).toBeUnknown() + expectTypeOf(newData).toBeUnknown() + return newData + }, })) - - if (query.isPending()) { - expectTypeOf(query.data).toEqualTypeOf>() - } }) + }) +}) - test('error should be defined when query is error', () => { - const key = queryKey() - const query = injectQuery(() => ({ - queryKey: key, - queryFn: () => sleep(0).then(() => 'Some data'), - })) +describe('Discriminated union return type', () => { + test('data should be possibly undefined by default', () => { + const query = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => sleep(0).then(() => 'Some data'), + })) - if (query.isError()) { - expectTypeOf(query.error).toEqualTypeOf>() - } - }) + expectTypeOf(query.data).toEqualTypeOf>() + }) + + test('data should be defined when query is success', () => { + const query = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => sleep(0).then(() => 'Some data'), + })) + + if (query.isSuccess()) { + expectTypeOf(query.data).toEqualTypeOf>() + } + }) + + test('error should be null when query is success', () => { + const query = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => sleep(0).then(() => 'Some data'), + })) + + if (query.isSuccess()) { + expectTypeOf(query.error).toEqualTypeOf>() + } + }) + + test('data should be undefined when query is pending', () => { + const query = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => sleep(0).then(() => 'Some data'), + })) + + if (query.isPending()) { + expectTypeOf(query.data).toEqualTypeOf>() + } + }) + + test('error should be defined when query is error', () => { + const query = injectQuery(() => ({ + queryKey: ['key'], + queryFn: () => sleep(0).then(() => 'Some data'), + })) + + if (query.isError()) { + expectTypeOf(query.error).toEqualTypeOf>() + } }) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts index 9b1a92f5338..897131b8ae6 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts @@ -1,10 +1,13 @@ import { ApplicationRef, + ChangeDetectionStrategy, Component, Injector, + NgZone, computed, effect, input, + inputBinding, provideZonelessChangeDetection, signal, } from '@angular/core' @@ -23,6 +26,7 @@ import { test, vi, } from 'vitest' +import { render } from '@testing-library/angular' import { queryKey, sleep } from '@tanstack/query-test-utils' import { lastValueFrom } from 'rxjs' import { @@ -32,7 +36,6 @@ import { provideIsRestoring, provideTanStackQuery, } from '..' -import { setSignalInputs } from './test-utils' import type { CreateQueryOptions, OmitKeyof, QueryFunction } from '..' describe('injectQuery', () => { @@ -56,102 +59,190 @@ describe('injectQuery', () => { test('should return the correct types', () => { const key = queryKey() - // unspecified query function should default to unknown - const noQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + // unspecified query function should default to unknown + noQueryFn = injectQuery(() => ({ queryKey: key, - })), - ) - expectTypeOf(noQueryFn.data()).toEqualTypeOf() - expectTypeOf(noQueryFn.error()).toEqualTypeOf() + })) - // it should infer the result type from the query function - const fromQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should infer the result type from the query function + fromQueryFn = injectQuery(() => ({ queryKey: key, queryFn: () => 'test', - })), - ) - expectTypeOf(fromQueryFn.data()).toEqualTypeOf() - expectTypeOf(fromQueryFn.error()).toEqualTypeOf() + })) - // it should be possible to specify the result type - const withResult = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should be possible to specify the result type + withResult = injectQuery(() => ({ queryKey: key, queryFn: () => 'test', - })), - ) - expectTypeOf(withResult.data()).toEqualTypeOf() - expectTypeOf(withResult.error()).toEqualTypeOf() + })) - // it should be possible to specify the error type - type CustomErrorType = { message: string } - const withError = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should be possible to specify the error type + withError = injectQuery(() => ({ queryKey: key, queryFn: () => 'test', - })), - ) - expectTypeOf(withError.data()).toEqualTypeOf() - expectTypeOf(withError.error()).toEqualTypeOf() + })) - // it should infer the result type from the configuration - const withResultInfer = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should infer the result type from the configuration + withResultInfer = injectQuery(() => ({ queryKey: key, queryFn: () => true, - })), - ) - expectTypeOf(withResultInfer.data()).toEqualTypeOf() - expectTypeOf(withResultInfer.error()).toEqualTypeOf() + })) - // it should be possible to specify a union type as result type - const unionTypeSync = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should be possible to specify a union type as result type + unionTypeSync = injectQuery(() => ({ queryKey: key, queryFn: () => (Math.random() > 0.5 ? ('a' as const) : ('b' as const)), - })), - ) - expectTypeOf(unionTypeSync.data()).toEqualTypeOf<'a' | 'b' | undefined>() - const unionTypeAsync = TestBed.runInInjectionContext(() => - injectQuery<'a' | 'b'>(() => ({ + })) + + unionTypeAsync = injectQuery<'a' | 'b'>(() => ({ queryKey: key, queryFn: () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'), - })), - ) - expectTypeOf(unionTypeAsync.data()).toEqualTypeOf<'a' | 'b' | undefined>() + })) - // it should error when the query function result does not match with the specified type - TestBed.runInInjectionContext(() => - // @ts-expect-error - injectQuery(() => ({ queryKey: key, queryFn: () => 'test' })), - ) + // it should infer the result type from a generic query function + fromGenericQueryFn = (() => { + function queryFn(): Promise { + return Promise.resolve({} as T) + } + return injectQuery(() => ({ + queryKey: key, + queryFn: () => queryFn(), + })) + })() - // it should infer the result type from a generic query function - /** - * - */ - function queryFn(): Promise { - return Promise.resolve({} as T) - } + // todo use query options? + fromGenericOptionsQueryFn = (() => { + function queryFn(): Promise { + return Promise.resolve({} as T) + } + return injectQuery(() => ({ + queryKey: key, + queryFn: () => queryFn(), + })) + })() + + fromMyDataArrayKeyQueryFn = (() => { + type MyData = number + type MyQueryKey = readonly ['my-data', number] + const getMyDataArrayKey: QueryFunction = ({ + queryKey: [, n], + }) => { + return n + 42 + } + return injectQuery(() => ({ + queryKey: ['my-data', 100] as const, + queryFn: getMyDataArrayKey, + })) + })() - const fromGenericQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + // it should handle query-functions that return Promise + fromPromiseAnyQueryFn = injectQuery(() => ({ queryKey: key, - queryFn: () => queryFn(), - })), - ) + queryFn: () => fetch('return Promise').then((resp) => resp.json()), + })) + + fromGetMyDataStringKeyQueryFn = (() => { + type MyData = number + const getMyDataStringKey: QueryFunction = (context) => { + expectTypeOf(context.queryKey).toEqualTypeOf<['1']>() + return Number(context.queryKey[0]) + 42 + } + return injectQuery(() => ({ + queryKey: ['1'] as ['1'], + queryFn: getMyDataStringKey, + })) + })() + + // Wrapped queries + fromWrappedQuery = (() => { + const createWrappedQuery = < + TQueryKey extends [string, Record?], + TQueryFnData, + TError, + TData = TQueryFnData, + >( + qk: TQueryKey, + fetcher: (obj: TQueryKey[1], token: string) => Promise, + options?: OmitKeyof< + CreateQueryOptions, + 'queryKey' | 'queryFn' | 'initialData', + 'safely' + >, + ) => + injectQuery(() => ({ + queryKey: qk, + queryFn: () => fetcher(qk[1], 'token'), + ...options, + })) + return createWrappedQuery([''], () => Promise.resolve('1')) + })() + + fromWrappedFuncStyleQuery = (() => { + const createWrappedFuncStyleQuery = < + TQueryKey extends [string, Record?], + TQueryFnData, + TError, + TData = TQueryFnData, + >( + qk: TQueryKey, + fetcher: () => Promise, + options?: OmitKeyof< + CreateQueryOptions, + 'queryKey' | 'queryFn' | 'initialData', + 'safely' + >, + ) => injectQuery(() => ({ queryKey: qk, queryFn: fetcher, ...options })) + return createWrappedFuncStyleQuery([''], () => Promise.resolve(true)) + })() + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const { + noQueryFn, + fromQueryFn, + withResult, + withError, + withResultInfer, + unionTypeSync, + unionTypeAsync, + fromGenericQueryFn, + fromGenericOptionsQueryFn, + fromMyDataArrayKeyQueryFn, + fromPromiseAnyQueryFn, + fromGetMyDataStringKeyQueryFn, + fromWrappedQuery, + fromWrappedFuncStyleQuery, + } = fixture.componentInstance + + expectTypeOf(noQueryFn.data()).toEqualTypeOf() + expectTypeOf(noQueryFn.error()).toEqualTypeOf() + + expectTypeOf(fromQueryFn.data()).toEqualTypeOf() + expectTypeOf(fromQueryFn.error()).toEqualTypeOf() + + expectTypeOf(withResult.data()).toEqualTypeOf() + expectTypeOf(withResult.error()).toEqualTypeOf() + + expectTypeOf(withError.data()).toEqualTypeOf() + expectTypeOf(withError.error()).toEqualTypeOf<{ message: string } | null>() + + expectTypeOf(withResultInfer.data()).toEqualTypeOf() + expectTypeOf(withResultInfer.error()).toEqualTypeOf() + + expectTypeOf(unionTypeSync.data()).toEqualTypeOf<'a' | 'b' | undefined>() + expectTypeOf(unionTypeAsync.data()).toEqualTypeOf<'a' | 'b' | undefined>() + expectTypeOf(fromGenericQueryFn.data()).toEqualTypeOf() expectTypeOf(fromGenericQueryFn.error()).toEqualTypeOf() - // todo use query options? - const fromGenericOptionsQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, - queryFn: () => queryFn(), - })), - ) expectTypeOf(fromGenericOptionsQueryFn.data()).toEqualTypeOf< string | undefined >() @@ -159,121 +250,36 @@ describe('injectQuery', () => { fromGenericOptionsQueryFn.error(), ).toEqualTypeOf() - type MyData = number - type MyQueryKey = readonly ['my-data', number] - - const getMyDataArrayKey: QueryFunction = ({ - queryKey: [, n], - }) => { - return n + 42 - } - - const fromMyDataArrayKeyQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: ['my-data', 100] as const, - queryFn: getMyDataArrayKey, - })), - ) expectTypeOf(fromMyDataArrayKeyQueryFn.data()).toEqualTypeOf< number | undefined >() - // it should handle query-functions that return Promise - const fromPromiseAnyQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, - queryFn: () => fetch('return Promise').then((resp) => resp.json()), - })), - ) expectTypeOf(fromPromiseAnyQueryFn.data()).toEqualTypeOf() - - TestBed.runInInjectionContext(() => - effect(() => { - expect(fromMyDataArrayKeyQueryFn.data()).toBe(142) - }), - ) - - const getMyDataStringKey: QueryFunction = (context) => { - expectTypeOf(context.queryKey).toEqualTypeOf<['1']>() - return Number(context.queryKey[0]) + 42 - } - - const fromGetMyDataStringKeyQueryFn = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: ['1'] as ['1'], - queryFn: getMyDataStringKey, - })), - ) expectTypeOf(fromGetMyDataStringKeyQueryFn.data()).toEqualTypeOf< number | undefined >() - - TestBed.runInInjectionContext(() => - effect(() => { - expect(fromGetMyDataStringKeyQueryFn.data()).toBe(43) - }), - ) - - // handles wrapped queries with custom fetcher passed as inline queryFn - const createWrappedQuery = < - TQueryKey extends [string, Record?], - TQueryFnData, - TError, - TData = TQueryFnData, - >( - qk: TQueryKey, - fetcher: ( - obj: TQueryKey[1], - token: string, - // return type must be wrapped with TQueryFnReturn - ) => Promise, - options?: OmitKeyof< - CreateQueryOptions, - 'queryKey' | 'queryFn' | 'initialData', - 'safely' - >, - ) => - injectQuery(() => ({ - queryKey: qk, - queryFn: () => fetcher(qk[1], 'token'), - ...options, - })) - const fromWrappedQuery = TestBed.runInInjectionContext(() => - createWrappedQuery([''], () => Promise.resolve('1')), - ) expectTypeOf(fromWrappedQuery.data()).toEqualTypeOf() - - // handles wrapped queries with custom fetcher passed directly to createQuery - const createWrappedFuncStyleQuery = < - TQueryKey extends [string, Record?], - TQueryFnData, - TError, - TData = TQueryFnData, - >( - qk: TQueryKey, - fetcher: () => Promise, - options?: OmitKeyof< - CreateQueryOptions, - 'queryKey' | 'queryFn' | 'initialData', - 'safely' - >, - ) => injectQuery(() => ({ queryKey: qk, queryFn: fetcher, ...options })) - const fromWrappedFuncStyleQuery = TestBed.runInInjectionContext(() => - createWrappedFuncStyleQuery([''], () => Promise.resolve(true)), - ) expectTypeOf(fromWrappedFuncStyleQuery.data()).toEqualTypeOf< boolean | undefined >() }) test('should return pending status initially', () => { - const key = queryKey() - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: key, + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['key1'], queryFn: () => sleep(10).then(() => 'Some data'), })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(query.status()).toBe('pending') expect(query.isPending()).toBe(true) @@ -283,13 +289,21 @@ describe('injectQuery', () => { }) test('should resolve to success and update signal: injectQuery()', async () => { - const key = queryKey() - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: key, + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['key2'], queryFn: () => sleep(10).then(() => 'result2'), })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query await vi.advanceTimersByTimeAsync(11) expect(query.status()).toBe('success') @@ -301,15 +315,23 @@ describe('injectQuery', () => { }) test('should reject and update signal', async () => { - const key = queryKey() - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ retry: false, - queryKey: key, + queryKey: ['key3'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query await vi.advanceTimersByTimeAsync(11) expect(query.status()).toBe('error') @@ -323,17 +345,26 @@ describe('injectQuery', () => { }) test('should update query on options contained signal change', async () => { - const key1 = queryKey() - const key2 = queryKey() - const key = signal(key1) + const key = signal(['key6', 'key7']) const spy = vi.fn(() => sleep(10).then(() => 'Some data')) - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: key(), - queryFn: spy, - })) + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, }) + class TestComponent { + key = key + spy = spy + query = injectQuery(() => ({ + queryKey: this.key(), + queryFn: this.spy, + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query await vi.advanceTimersByTimeAsync(0) expect(spy).toHaveBeenCalledTimes(1) @@ -341,31 +372,41 @@ describe('injectQuery', () => { await vi.advanceTimersByTimeAsync(11) expect(query.status()).toBe('success') - key.set(key2) - TestBed.tick() + key.set(['key8']) + fixture.detectChanges() expect(spy).toHaveBeenCalledTimes(2) // should call queryFn with context containing the new queryKey expect(spy).toBeCalledWith({ client: queryClient, meta: undefined, - queryKey: key2, + queryKey: ['key8'], signal: expect.anything(), }) }) test('should only run query once enabled signal is set to true', async () => { - const key = queryKey() const spy = vi.fn(() => sleep(10).then(() => 'Some data')) const enabled = signal(false) - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: key, - queryFn: spy, - enabled: enabled(), - })) + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, }) + class TestComponent { + enabled = enabled + spy = spy + query = injectQuery(() => ({ + queryKey: ['key9'], + queryFn: this.spy, + enabled: this.enabled(), + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(spy).not.toHaveBeenCalled() expect(query.status()).toBe('pending') @@ -377,30 +418,34 @@ describe('injectQuery', () => { expect(query.status()).toBe('success') }) - test('should properly execute dependent queries', async () => { - const key1 = queryKey() - const key2 = queryKey() - - const query1 = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: key1, - queryFn: () => sleep(10).then(() => 'Some data'), - })) - }) - + test('should properly execute dependant queries', async () => { const dependentQueryFn = vi .fn() .mockImplementation(() => sleep(1000).then(() => 'Some data')) - const query2 = TestBed.runInInjectionContext(() => { - return injectQuery( + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query1 = injectQuery(() => ({ + queryKey: ['dependant1'], + queryFn: () => sleep(10).then(() => 'Some data'), + })) + + query2 = injectQuery( computed(() => ({ - queryKey: key2, + queryKey: ['dependent2'], queryFn: dependentQueryFn, - enabled: !!query1.data(), + enabled: !!this.query1.data(), })), ) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const { query1, query2 } = fixture.componentInstance expect(query1.data()).toStrictEqual(undefined) expect(query2.fetchStatus()).toStrictEqual('idle') @@ -417,22 +462,32 @@ describe('injectQuery', () => { expect(query2.status()).toStrictEqual('success') expect(dependentQueryFn).toHaveBeenCalledTimes(1) expect(dependentQueryFn).toHaveBeenCalledWith( - expect.objectContaining({ queryKey: key2 }), + expect.objectContaining({ queryKey: ['dependent2'] }), ) }) test('should use the current value for the queryKey when refetch is called', async () => { - const key = queryKey() const fetchFn = vi.fn(() => sleep(10).then(() => 'Some data')) const keySignal = signal('key11') - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: [...key, keySignal()], - queryFn: fetchFn, + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + keySignal = keySignal + fetchFn = fetchFn + query = injectQuery(() => ({ + queryKey: ['key10', this.keySignal()], + queryFn: this.fetchFn, enabled: false, })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(fetchFn).not.toHaveBeenCalled() @@ -440,7 +495,7 @@ describe('injectQuery', () => { expect(fetchFn).toHaveBeenCalledTimes(1) expect(fetchFn).toHaveBeenCalledWith( expect.objectContaining({ - queryKey: [...key, 'key11'], + queryKey: ['key10', 'key11'], }), ) }) @@ -448,12 +503,13 @@ describe('injectQuery', () => { await vi.advanceTimersByTimeAsync(11) keySignal.set('key12') + fixture.detectChanges() void query.refetch().then(() => { expect(fetchFn).toHaveBeenCalledTimes(2) expect(fetchFn).toHaveBeenCalledWith( expect.objectContaining({ - queryKey: [...key, 'key12'], + queryKey: ['key10', 'key12'], }), ) }) @@ -461,19 +517,58 @@ describe('injectQuery', () => { await vi.advanceTimersByTimeAsync(11) }) + test('should support selection function with select', async () => { + const app = TestBed.inject(ApplicationRef) + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['key13'], + queryFn: () => [{ id: 1 }, { id: 2 }], + select: (data) => data.map((item) => item.id), + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query + + // Wait for query to complete (even synchronous queryFn needs time to process) + const stablePromise = app.whenStable() + await Promise.resolve() + await vi.advanceTimersByTimeAsync(10) + await stablePromise + + expect(query.status()).toBe('success') + expect(query.data()).toEqual([1, 2]) + }) + describe('throwOnError', () => { test('should evaluate throwOnError when query is expected to throw', async () => { - const key = queryKey() const boundaryFn = vi.fn() - TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: key, + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + boundaryFn = boundaryFn + query = injectQuery(() => ({ + queryKey: ['key12'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), retry: false, - throwOnError: boundaryFn, + throwOnError: this.boundaryFn, })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() await vi.advanceTimersByTimeAsync(11) expect(boundaryFn).toHaveBeenCalledTimes(1) @@ -486,44 +581,112 @@ describe('injectQuery', () => { }) test('should throw when throwOnError is true', async () => { - const key = queryKey() - TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: key, + const zone = TestBed.inject(NgZone) + const zoneErrorPromise = new Promise((resolve) => { + const sub = zone.onError.subscribe((error) => { + sub.unsubscribe() + resolve(error as Error) + }) + }) + let handler: ((error: Error) => void) | null = null + const processErrorPromise = new Promise((resolve) => { + handler = (error: Error) => { + process.off('uncaughtException', handler!) + resolve(error) + } + process.on('uncaughtException', handler) + }) + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['key13'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), throwOnError: true, })) - }) - - await expect(vi.runAllTimersAsync()).rejects.toThrow('Some error') + } + + TestBed.createComponent(TestComponent).detectChanges() + + try { + await vi.runAllTimersAsync() + await expect(zoneErrorPromise).resolves.toEqual(Error('Some error')) + await expect(processErrorPromise).resolves.toEqual(Error('Some error')) + } finally { + if (handler) { + process.off('uncaughtException', handler) + } + } }) test('should throw when throwOnError function returns true', async () => { - const key = queryKey() - TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ - queryKey: key, + const zone = TestBed.inject(NgZone) + const zoneErrorPromise = new Promise((resolve) => { + const sub = zone.onError.subscribe((error) => { + sub.unsubscribe() + resolve(error as Error) + }) + }) + let handler: ((error: Error) => void) | null = null + const processErrorPromise = new Promise((resolve) => { + handler = (error: Error) => { + process.off('uncaughtException', handler!) + resolve(error) + } + process.on('uncaughtException', handler) + }) + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['key14'], queryFn: () => sleep(0).then(() => Promise.reject(new Error('Some error'))), throwOnError: () => true, })) - }) - - await expect(vi.runAllTimersAsync()).rejects.toThrow('Some error') + } + + TestBed.createComponent(TestComponent).detectChanges() + + try { + await vi.runAllTimersAsync() + await expect(zoneErrorPromise).resolves.toEqual(Error('Some error')) + await expect(processErrorPromise).resolves.toEqual(Error('Some error')) + } finally { + if (handler) { + process.off('uncaughtException', handler) + } + } }) }) test('should set state to error when queryFn returns reject promise', async () => { - const key = queryKey() - const query = TestBed.runInInjectionContext(() => { - return injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ retry: false, - queryKey: key, + queryKey: ['key15'], queryFn: () => sleep(10).then(() => Promise.reject(new Error('Some error'))), })) - }) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(query.status()).toBe('pending') @@ -536,6 +699,7 @@ describe('injectQuery', () => { @Component({ selector: 'app-fake', template: `{{ query.data() }}`, + changeDetection: ChangeDetectionStrategy.OnPush, }) class FakeComponent { name = input.required() @@ -546,99 +710,284 @@ describe('injectQuery', () => { })) } - const fixture = TestBed.createComponent(FakeComponent) - setSignalInputs(fixture.componentInstance, { - name: 'signal-input-required-test', + const name = signal('signal-input-required-test') + const rendered = await render(FakeComponent, { + bindings: [inputBinding('name', name.asReadonly())], + detectChangesOnRender: false, }) + rendered.fixture.detectChanges() + await vi.advanceTimersByTimeAsync(0) - fixture.detectChanges() + const result = rendered.fixture.nativeElement.textContent + expect(result).toEqual('signal-input-required-test') + }) + + test('should support aliasing query.data on required signal inputs', async () => { + @Component({ + selector: 'app-fake', + template: `{{ data() }}`, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class FakeComponent { + name = input.required() + + query = injectQuery(() => ({ + queryKey: ['fake-alias', this.name()], + queryFn: () => this.name(), + })) + + data = this.query.data + } + + const name = signal('signal-input-alias-test') + const rendered = await render(FakeComponent, { + bindings: [inputBinding('name', name.asReadonly())], + detectChangesOnRender: false, + }) + rendered.fixture.detectChanges() await vi.advanceTimersByTimeAsync(0) - expect(fixture.componentInstance.query.data()).toEqual( - 'signal-input-required-test', - ) + const result = rendered.fixture.nativeElement.textContent + expect(result).toEqual('signal-input-alias-test') }) - describe('isRestoring', () => { - test('should not fetch for the duration of the restoring period when isRestoring is true', async () => { - const key = queryKey() - const queryFn = vi - .fn() - .mockImplementation(() => sleep(10).then(() => 'data')) + test('should allow reading the query data on effect registered before injection', () => { + const spy = vi.fn() + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + readEffect = effect(() => { + spy(this.query.data()) + }) - TestBed.resetTestingModule() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - provideIsRestoring(signal(true).asReadonly()), - ], + query = injectQuery(() => ({ + queryKey: ['effect-before-injection'], + queryFn: () => sleep(0).then(() => 'Some data'), + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + expect(spy).toHaveBeenCalledWith(undefined) + }) + + test('should render with an initial value for input signal if available before change detection', async () => { + const key1 = queryKey() as [string] + const key2 = queryKey() as [string] + queryClient.setQueryData(key1, 'value 1') + queryClient.setQueryData(key2, 'value 2') + + @Component({ + selector: 'app-test', + template: '{{ query.data() }}', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + inputKey = input.required<[string]>() + query = injectQuery(() => ({ + queryKey: this.inputKey(), + queryFn: () => sleep(0).then(() => 'Some data'), + })) + } + + const inputKey = signal<[string]>(key1) + const rendered = await render(TestComponent, { + bindings: [inputBinding('inputKey', inputKey.asReadonly())], + detectChangesOnRender: false, + }) + rendered.fixture.detectChanges() + + const instance = rendered.fixture.componentInstance + const query = instance.query + + expect(() => instance.inputKey()).not.toThrow() + + expect(instance.inputKey()).toEqual(key1) + expect(query.data()).toEqual('value 1') + + inputKey.set(key2) + rendered.fixture.detectChanges() + + expect(instance.inputKey()).toEqual(key2) + expect(query.data()).toEqual('value 2') + }) + + test('should allow reading the query data on component ngOnInit with required signal input', async () => { + const spy = vi.fn() + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + key = input.required<[string]>() + query = injectQuery(() => ({ + queryKey: this.key(), + queryFn: () => Promise.resolve(() => 'Some data'), + })) + + initialStatus!: string + + ngOnInit() { + this.initialStatus = this.query.status() + + // effect should not have been called yet + expect(spy).not.toHaveBeenCalled() + } + + _spyEffect = effect(() => { + spy() }) + } - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, - queryFn, - })), - ) + const key = signal<[string]>(['ngOnInitTest']) + const rendered = await render(TestComponent, { + bindings: [inputBinding('key', key.asReadonly())], + detectChangesOnRender: false, + }) + rendered.fixture.detectChanges() - await vi.advanceTimersByTimeAsync(0) - expect(query.status()).toBe('pending') - expect(query.fetchStatus()).toBe('idle') - expect(query.data()).toBeUndefined() - expect(queryFn).toHaveBeenCalledTimes(0) + const fixture = rendered.fixture + expect(spy).toHaveBeenCalled() - await vi.advanceTimersByTimeAsync(11) - expect(query.status()).toBe('pending') - expect(query.fetchStatus()).toBe('idle') - expect(query.data()).toBeUndefined() - expect(queryFn).toHaveBeenCalledTimes(0) + const instance = fixture.componentInstance + expect(instance.initialStatus).toEqual('pending') + }) + + test('should update query data on the same macro task when query data changes', async () => { + const query = TestBed.runInInjectionContext(() => + injectQuery(() => ({ + queryKey: ['test'], + initialData: 'initial data', + })), + ) + + // Run effects + TestBed.tick() + + expect(query.data()).toBe('initial data') + queryClient.setQueryData(['test'], 'new data') + + // Flush microtasks + await Promise.resolve() + + expect(query.data()).toBe('new data') + }) + + test('should pause fetching while restoring and fetch once restoring is disabled', async () => { + const isRestoring = signal(true) + const fetchSpy = vi.fn(() => sleep(10).then(() => 'restored-data')) + + TestBed.resetTestingModule() + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery(queryClient), + provideIsRestoring(isRestoring.asReadonly()), + ], + }) + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['restoring'], + queryFn: fetchSpy, + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + + const query = fixture.componentInstance.query + expect(fetchSpy).not.toHaveBeenCalled() + expect(query.status()).toBe('pending') + + const stablePromise = fixture.whenStable() + await Promise.resolve() + await stablePromise + + isRestoring.set(false) + fixture.detectChanges() + + await vi.advanceTimersByTimeAsync(11) + await fixture.whenStable() + + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(query.status()).toBe('success') + expect(query.data()).toBe('restored-data') }) describe('injection context', () => { test('throws NG0203 with descriptive error outside injection context', () => { - const key = queryKey() expect(() => { injectQuery(() => ({ - queryKey: key, + queryKey: ['injectionContextError'], queryFn: () => sleep(0).then(() => 'Some data'), })) - }).toThrow(/NG0203(.*?)injectQuery/) + }).toThrowError(/NG0203(.*?)injectQuery/) }) test('can be used outside injection context when passing an injector', () => { - const key = queryKey() - const query = injectQuery( - () => ({ - queryKey: key, - queryFn: () => sleep(0).then(() => 'Some data'), - }), - { - injector: TestBed.inject(Injector), - }, - ) + const injector = TestBed.inject(Injector) + + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery( + () => ({ + queryKey: ['manualInjector'], + queryFn: () => sleep(0).then(() => 'Some data'), + }), + { + injector: injector, + }, + ) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(query.status()).toBe('pending') }) test('should complete queries before whenStable() resolves', async () => { - const key = queryKey() const app = TestBed.inject(ApplicationRef) - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, - queryFn: () => sleep(50).then(() => 'test data'), - })), - ) + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['pendingTasksTest'], + queryFn: async () => { + await sleep(50) + return 'test data' + }, + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query expect(query.status()).toBe('pending') expect(query.data()).toBeUndefined() - const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(60) - await stablePromise + await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('test data') @@ -659,15 +1008,25 @@ describe('injectQuery', () => { const httpClient = TestBed.inject(HttpClient) const httpTestingController = TestBed.inject(HttpTestingController) - // Create a query using HttpClient - const key = queryKey() - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + httpClient = httpClient + query = injectQuery(() => ({ + queryKey: ['httpClientTest'], queryFn: () => - lastValueFrom(httpClient.get<{ message: string }>('/api/test')), - })), - ) + lastValueFrom( + this.httpClient.get<{ message: string }>('/api/test'), + ), + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query // Schedule the HTTP response setTimeout(() => { @@ -679,9 +1038,8 @@ describe('injectQuery', () => { expect(query.status()).toBe('pending') // Advance timers and wait for Angular to be "stable" - const stablePromise = app.whenStable() await vi.advanceTimersByTimeAsync(20) - await stablePromise + await app.whenStable() // Query should be complete after whenStable() thanks to PendingTasks integration expect(query.status()).toBe('success') @@ -700,29 +1058,36 @@ describe('injectQuery', () => { }) const app = TestBed.inject(ApplicationRef) - let callCount = 0 - const key = queryKey() - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + callCount = 0 + query = injectQuery(() => ({ + queryKey: ['sync-stale'], staleTime: 1000, queryFn: () => { - callCount++ - return `sync-data-${callCount}` + this.callCount++ + return `sync-data-${this.callCount}` }, - })), - ) + })) + } - // Synchronize pending effects - TestBed.tick() + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const component = fixture.componentInstance + const query = component.query const stablePromise = app.whenStable() + await vi.advanceTimersToNextTimerAsync() await stablePromise expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-1') - expect(callCount).toBe(1) + expect(component.callCount).toBe(1) await query.refetch() await Promise.resolve() @@ -731,7 +1096,7 @@ describe('injectQuery', () => { expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-2') - expect(callCount).toBe(2) + expect(component.callCount).toBe(2) }) test('should handle enabled/disabled transitions with synchronous queryFn', async () => { @@ -745,35 +1110,48 @@ describe('injectQuery', () => { const app = TestBed.inject(ApplicationRef) const enabledSignal = signal(false) - let callCount = 0 - const key = queryKey() - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, - enabled: enabledSignal(), + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + enabledSignal = enabledSignal + callCount = 0 + query = injectQuery(() => ({ + queryKey: ['sync-enabled'], + enabled: this.enabledSignal(), queryFn: () => { - callCount++ - return `sync-data-${callCount}` + this.callCount++ + return `sync-data-${this.callCount}` }, - })), - ) + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const component = fixture.componentInstance + const query = component.query // Initially disabled - TestBed.tick() + await vi.advanceTimersByTimeAsync(0) await app.whenStable() expect(query.status()).toBe('pending') expect(query.data()).toBeUndefined() - expect(callCount).toBe(0) + expect(component.callCount).toBe(0) // Enable the query enabledSignal.set(true) - TestBed.tick() + fixture.detectChanges() + + const stablePromise = app.whenStable() + await vi.advanceTimersToNextTimerAsync() + await stablePromise - await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-1') - expect(callCount).toBe(1) + expect(component.callCount).toBe(1) }) test('should handle query invalidation with synchronous data', async () => { @@ -786,40 +1164,48 @@ describe('injectQuery', () => { }) const app = TestBed.inject(ApplicationRef) - const testKey = queryKey() - let callCount = 0 + const testKey = ['sync-invalidate'] - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + callCount = 0 + query = injectQuery(() => ({ queryKey: testKey, queryFn: () => { - callCount++ - return `sync-data-${callCount}` + this.callCount++ + return `sync-data-${this.callCount}` }, - })), - ) + })) + } - // Synchronize pending effects - TestBed.tick() + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const component = fixture.componentInstance + const query = component.query + + const stablePromise = app.whenStable() + await vi.advanceTimersToNextTimerAsync() + await stablePromise - await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-1') - expect(callCount).toBe(1) + expect(component.callCount).toBe(1) // Invalidate the query queryClient.invalidateQueries({ queryKey: testKey }) - TestBed.tick() // Wait for the invalidation to trigger a refetch await Promise.resolve() await vi.advanceTimersByTimeAsync(10) - TestBed.tick() await app.whenStable() expect(query.status()).toBe('success') expect(query.data()).toBe('sync-data-2') - expect(callCount).toBe(2) + expect(component.callCount).toBe(2) }) }) }) diff --git a/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts b/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts index 65f1395f933..5448bbbcac1 100644 --- a/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts +++ b/packages/angular-query-experimental/src/__tests__/pending-tasks.test.ts @@ -1,7 +1,7 @@ import { ApplicationRef, + ChangeDetectionStrategy, Component, - provideZonelessChangeDetection, } from '@angular/core' import { TestBed } from '@angular/core/testing' import { HttpClient, provideHttpClient } from '@angular/common/http' @@ -10,15 +10,10 @@ import { provideHttpClientTesting, } from '@angular/common/http/testing' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { queryKey, sleep } from '@tanstack/query-test-utils' +import { sleep } from '@tanstack/query-test-utils' import { lastValueFrom } from 'rxjs' -import { - QueryClient, - injectMutation, - injectQuery, - onlineManager, - provideTanStackQuery, -} from '..' +import { QueryClient, injectMutation, injectQuery, onlineManager } from '..' +import { flushQueryUpdates, setupTanStackQueryTestBed } from './test-utils' describe('PendingTasks Integration', () => { let queryClient: QueryClient @@ -37,12 +32,7 @@ describe('PendingTasks Integration', () => { }, }) - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - ], - }) + setupTanStackQueryTestBed(queryClient) }) afterEach(() => { @@ -55,13 +45,21 @@ describe('PendingTasks Integration', () => { test('should handle synchronous queryFn with whenStable()', async () => { const app = TestBed.inject(ApplicationRef) - const key = queryKey() - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query = injectQuery(() => ({ + queryKey: ['sync'], queryFn: () => 'instant-data', // Resolves synchronously - })), - ) + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query // Should start as pending even with synchronous data expect(query.status()).toBe('pending') @@ -81,10 +79,9 @@ describe('PendingTasks Integration', () => { test('should handle synchronous error with whenStable()', async () => { const app = TestBed.inject(ApplicationRef) - const key = queryKey() const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ - queryKey: key, + queryKey: ['sync-error'], queryFn: () => { throw new Error('instant-error') }, // Throws synchronously @@ -142,7 +139,6 @@ describe('PendingTasks Integration', () => { ) mutation.mutate() - TestBed.tick() const stablePromise = app.whenStable() @@ -157,7 +153,6 @@ describe('PendingTasks Integration', () => { describe('Race Conditions', () => { test('should handle query that completes during initial subscription', async () => { - const key = queryKey() const app = TestBed.inject(ApplicationRef) let resolveQuery: (value: string) => void @@ -167,7 +162,7 @@ describe('PendingTasks Integration', () => { const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ - queryKey: key, + queryKey: ['race-condition'], queryFn: () => queryPromise, })), ) @@ -186,19 +181,27 @@ describe('PendingTasks Integration', () => { test('should handle rapid refetches without task leaks', async () => { const app = TestBed.inject(ApplicationRef) - let callCount = 0 - const key = queryKey() - const query = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key, + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + callCount = 0 + query = injectQuery(() => ({ + queryKey: ['rapid-refetch'], queryFn: async () => { - callCount++ + this.callCount++ await sleep(10) - return `data-${callCount}` + return `data-${this.callCount}` }, - })), - ) + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const query = fixture.componentInstance.query // Trigger multiple rapid refetches query.refetch() @@ -213,14 +216,61 @@ describe('PendingTasks Integration', () => { expect(query.data()).toMatch(/^data-\d+$/) }) + test('should keep PendingTasks active when query starts offline (never reaches fetching)', async () => { + const app = TestBed.inject(ApplicationRef) + + onlineManager.setOnline(false) + + const query = TestBed.runInInjectionContext(() => + injectQuery(() => ({ + queryKey: ['start-offline'], + networkMode: 'online', // Default: won't fetch while offline + queryFn: async () => { + await sleep(10) + return 'online-data' + }, + })), + ) + + // Allow query to initialize + await Promise.resolve() + await flushQueryUpdates() + + // Query should initialize directly to 'paused' (never goes through 'fetching') + expect(query.status()).toBe('pending') + expect(query.fetchStatus()).toBe('paused') + + const stablePromise = app.whenStable() + let stableResolved = false + void stablePromise.then(() => { + stableResolved = true + }) + + await Promise.resolve() + + // PendingTasks should block stability even though we never hit 'fetching' + expect(stableResolved).toBe(false) + + // Bring the app back online so the query can fetch + onlineManager.setOnline(true) + + await vi.advanceTimersByTimeAsync(20) + await Promise.resolve() + + await stablePromise + + expect(stableResolved).toBe(true) + expect(query.status()).toBe('success') + expect(query.data()).toBe('online-data') + }) + test('should keep PendingTasks active while query retry is paused offline', async () => { const app = TestBed.inject(ApplicationRef) let attempt = 0 - const key = queryKey() const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ - queryKey: key, + queryKey: ['paused-offline'], retry: 1, retryDelay: 50, // Longer delay to ensure we can go offline before retry queryFn: async () => { @@ -235,7 +285,7 @@ describe('PendingTasks Integration', () => { ) // Allow the initial attempt to start and fail - await vi.advanceTimersByTimeAsync(0) + await flushQueryUpdates() await Promise.resolve() // Wait for the first attempt to complete and start retry delay @@ -284,80 +334,100 @@ describe('PendingTasks Integration', () => { describe('Component Destruction', () => { @Component({ template: '', + changeDetection: ChangeDetectionStrategy.OnPush, }) class TestComponent { query = injectQuery(() => ({ queryKey: ['component-query'], - queryFn: () => sleep(100).then(() => 'component-data'), + queryFn: async () => { + await sleep(100) + return 'component-data' + }, })) mutation = injectMutation(() => ({ - mutationFn: (data: string) => - sleep(100).then(() => `processed: ${data}`), + mutationFn: async (data: string) => { + await sleep(100) + return `processed: ${data}` + }, })) } test('should cleanup pending tasks when component with active query is destroyed', async () => { - const app = TestBed.inject(ApplicationRef) const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() // Start the query expect(fixture.componentInstance.query.status()).toBe('pending') + expect(fixture.isStable()).toBe(false) // Destroy component while query is running fixture.destroy() // Angular should become stable even though component was destroyed - const stablePromise = app.whenStable() + const stablePromise = fixture.whenStable() await vi.advanceTimersByTimeAsync(150) - - await expect(stablePromise).resolves.toEqual(undefined) + await stablePromise + expect(fixture.isStable()).toBe(true) }) test('should cleanup pending tasks when component with active mutation is destroyed', async () => { - const app = TestBed.inject(ApplicationRef) const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() fixture.componentInstance.mutation.mutate('test') + fixture.detectChanges() + expect(fixture.isStable()).toBe(false) // Destroy component while mutation is running fixture.destroy() + fixture.detectChanges() + expect(fixture.isStable()).toBe(true) // Angular should become stable even though component was destroyed - const stablePromise = app.whenStable() - await vi.advanceTimersByTimeAsync(150) + const stablePromise = fixture.whenStable() + await vi.advanceTimersByTimeAsync(200) + await stablePromise - await expect(stablePromise).resolves.toEqual(undefined) + expect(fixture.isStable()).toBe(true) }) }) describe('Concurrent Operations', () => { test('should handle multiple queries running simultaneously', async () => { - const key1 = queryKey() - const key2 = queryKey() - const key3 = queryKey() const app = TestBed.inject(ApplicationRef) - const query1 = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key1, - queryFn: () => sleep(30).then(() => 'data-1'), - })), - ) + @Component({ + selector: 'app-test', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + query1 = injectQuery(() => ({ + queryKey: ['concurrent-1'], + queryFn: async () => { + await sleep(30) + return 'data-1' + }, + })) - const query2 = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key2, - queryFn: () => sleep(50).then(() => 'data-2'), - })), - ) + query2 = injectQuery(() => ({ + queryKey: ['concurrent-2'], + queryFn: async () => { + await sleep(50) + return 'data-2' + }, + })) - const query3 = TestBed.runInInjectionContext(() => - injectQuery(() => ({ - queryKey: key3, + query3 = injectQuery(() => ({ + queryKey: ['concurrent-3'], queryFn: () => 'instant-data', // Synchronous - })), - ) + })) + } + + const fixture = TestBed.createComponent(TestComponent) + fixture.detectChanges() + const { query1, query2, query3 } = fixture.componentInstance // All queries should start expect(query1.status()).toBe('pending') @@ -382,15 +452,19 @@ describe('PendingTasks Integration', () => { const mutation1 = TestBed.runInInjectionContext(() => injectMutation(() => ({ - mutationFn: (data: string) => - sleep(30).then(() => `processed-1: ${data}`), + mutationFn: async (data: string) => { + await sleep(30) + return `processed-1: ${data}` + }, })), ) const mutation2 = TestBed.runInInjectionContext(() => injectMutation(() => ({ - mutationFn: (data: string) => - sleep(50).then(() => `processed-2: ${data}`), + mutationFn: async (data: string) => { + await sleep(50) + return `processed-2: ${data}` + }, })), ) @@ -426,18 +500,22 @@ describe('PendingTasks Integration', () => { test('should handle mixed queries and mutations', async () => { const app = TestBed.inject(ApplicationRef) - const key = queryKey() const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ - queryKey: key, - queryFn: () => sleep(40).then(() => 'query-data'), + queryKey: ['mixed-query'], + queryFn: async () => { + await sleep(40) + return 'query-data' + }, })), ) const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ - mutationFn: (data: string) => - sleep(60).then(() => `mutation: ${data}`), + mutationFn: async (data: string) => { + await sleep(60) + return `mutation: ${data}` + }, })), ) @@ -458,14 +536,8 @@ describe('PendingTasks Integration', () => { describe('HttpClient Integration', () => { beforeEach(() => { - TestBed.resetTestingModule() - TestBed.configureTestingModule({ - providers: [ - provideZonelessChangeDetection(), - provideTanStackQuery(queryClient), - provideHttpClient(), - provideHttpClientTesting(), - ], + setupTanStackQueryTestBed(queryClient, { + providers: [provideHttpClient(), provideHttpClientTesting()], }) }) @@ -474,12 +546,9 @@ describe('PendingTasks Integration', () => { const httpClient = TestBed.inject(HttpClient) const httpTestingController = TestBed.inject(HttpTestingController) - const key1 = queryKey() - const key2 = queryKey() - const query1 = TestBed.runInInjectionContext(() => injectQuery(() => ({ - queryKey: key1, + queryKey: ['http-1'], queryFn: () => lastValueFrom(httpClient.get<{ id: number }>('/api/1')), })), @@ -487,7 +556,7 @@ describe('PendingTasks Integration', () => { const query2 = TestBed.runInInjectionContext(() => injectQuery(() => ({ - queryKey: key2, + queryKey: ['http-2'], queryFn: () => lastValueFrom(httpClient.get<{ id: number }>('/api/2')), })), @@ -519,10 +588,9 @@ describe('PendingTasks Integration', () => { const httpClient = TestBed.inject(HttpClient) const httpTestingController = TestBed.inject(HttpTestingController) - const key = queryKey() const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ - queryKey: key, + queryKey: ['http-cancel'], queryFn: () => lastValueFrom(httpClient.get<{ data: string }>('/api/cancel')), })), @@ -549,19 +617,21 @@ describe('PendingTasks Integration', () => { describe('Edge Cases', () => { test('should handle query cancellation mid-flight', async () => { - const key = queryKey() const app = TestBed.inject(ApplicationRef) const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ - queryKey: key, - queryFn: () => sleep(100).then(() => 'data'), + queryKey: ['cancel-test'], + queryFn: async () => { + await sleep(100) + return 'data' + }, })), ) // Cancel the query after a short delay setTimeout(() => { - queryClient.cancelQueries({ queryKey: key }) + queryClient.cancelQueries({ queryKey: ['cancel-test'] }) }, 20) // Advance to the cancellation point @@ -582,10 +652,9 @@ describe('PendingTasks Integration', () => { const app = TestBed.inject(ApplicationRef) let attemptCount = 0 - const key = queryKey() const query = TestBed.runInInjectionContext(() => injectQuery(() => ({ - queryKey: key, + queryKey: ['retry-test'], retry: 2, retryDelay: 10, queryFn: async () => { @@ -609,13 +678,16 @@ describe('PendingTasks Integration', () => { test('should handle mutation with optimistic updates', async () => { const app = TestBed.inject(ApplicationRef) - const testQueryKey = queryKey() + const testQueryKey = ['optimistic-test'] queryClient.setQueryData(testQueryKey, 'initial-data') const mutation = TestBed.runInInjectionContext(() => injectMutation(() => ({ - mutationFn: (newData: string) => sleep(50).then(() => newData), + mutationFn: async (newData: string) => { + await sleep(50) + return newData + }, onMutate: async (newData) => { // Optimistic update const previousData = queryClient.getQueryData(testQueryKey) diff --git a/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts b/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts index d06aef67230..8fc00b07973 100644 --- a/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts +++ b/packages/angular-query-experimental/src/__tests__/signal-proxy.test.ts @@ -1,10 +1,26 @@ -import { isSignal, signal } from '@angular/core' -import { describe, expect, test } from 'vitest' +import { + ChangeDetectionStrategy, + Component, + computed, + input, + inputBinding, + isSignal, + provideZonelessChangeDetection, + signal, + untracked, +} from '@angular/core' +import { beforeEach, describe, expect, test } from 'vitest' +import { TestBed } from '@angular/core/testing' import { signalProxy } from '../signal-proxy' describe('signalProxy', () => { - const inputSignal = signal({ fn: () => 'bar', baz: 'qux' }) - const proxy = signalProxy(inputSignal) + const inputSignal = signal({ + fn: () => 'bar', + baz: 'qux', + falsy: false, + zero: 0, + }) + const proxy = signalProxy(inputSignal, ['fn']) test('should have computed fields', () => { expect(proxy.baz()).toEqual('qux') @@ -18,10 +34,72 @@ describe('signalProxy', () => { test('supports "in" operator', () => { expect('baz' in proxy).toBe(true) + expect('falsy' in proxy).toBe(true) + expect('zero' in proxy).toBe(true) expect('foo' in proxy).toBe(false) }) test('supports "Object.keys"', () => { - expect(Object.keys(proxy)).toEqual(['fn', 'baz']) + expect(Object.keys(proxy)).toEqual(['fn', 'baz', 'falsy', 'zero']) + }) + + describe('in component fixture', () => { + @Component({ + selector: 'app-test', + standalone: true, + template: '{{ proxy.baz() }}', + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class TestComponent { + number = input.required() + obj = computed(() => ({ + number: this.number(), + fn: () => untracked(this.number) + 1, + })) + proxy = signalProxy(this.obj, ['fn']) + shortNumber = this.proxy.number + shortFn = this.proxy.fn + } + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideZonelessChangeDetection()], + }) + }) + + test('should generate fixed fields after initial change detection run', async () => { + const number = signal(1) + const fixture = TestBed.createComponent(TestComponent, { + bindings: [inputBinding('number', number.asReadonly())], + }) + fixture.detectChanges() + const instance = fixture.componentInstance + + expect(isSignal(instance.proxy.number)).toBe(true) + expect(instance.proxy.number()).toBe(1) + expect(instance.shortNumber).toBe(instance.proxy.number) + + expect(instance.proxy.fn()).toBe(2) + expect(isSignal(instance.proxy.fn)).toBe(false) + expect(instance.shortFn).toBe(instance.proxy.fn) + }) + + test('should reflect updates on the proxy', async () => { + const number = signal(0) + const fixture = TestBed.createComponent(TestComponent, { + bindings: [inputBinding('number', number.asReadonly())], + }) + fixture.detectChanges() + const instance = fixture.componentInstance + + expect(instance.shortNumber()).toBe(0) + expect(instance.shortFn()).toBe(1) + + number.set(1) + fixture.detectChanges() + + expect(instance.shortNumber()).toBe(1) + expect(instance.shortFn()).toBe(2) + }) }) }) diff --git a/packages/angular-query-experimental/src/__tests__/test-utils.ts b/packages/angular-query-experimental/src/__tests__/test-utils.ts index 218cdea5f6c..56244db0ce4 100644 --- a/packages/angular-query-experimental/src/__tests__/test-utils.ts +++ b/packages/angular-query-experimental/src/__tests__/test-utils.ts @@ -1,8 +1,9 @@ -import { isSignal, untracked } from '@angular/core' -import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals' -import { expect } from 'vitest' -import type { InputSignal, Signal } from '@angular/core' -import type { ComponentFixture } from '@angular/core/testing' +import { isSignal, provideZonelessChangeDetection, untracked } from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { expect, vi } from 'vitest' +import { provideTanStackQuery } from '..' +import type { QueryClient } from '@tanstack/query-core' +import type { EnvironmentProviders, Provider, Signal } from '@angular/core' // Evaluate all signals on an object and return the result function evaluateSignals>( @@ -35,43 +36,28 @@ export const expectSignals = >( expect(evaluateSignals(obj)).toMatchObject(expected) } -type ToSignalInputUpdatableMap = { - [K in keyof T as T[K] extends InputSignal - ? K - : never]: T[K] extends InputSignal ? Value : never -} - -function componentHasSignalInputProperty( - component: object, - property: TProperty, -): component is { [key in TProperty]: InputSignal } { - return ( - component.hasOwnProperty(property) && (component as any)[property][SIGNAL] - ) -} - /** - * Set required signal input value to component fixture - * @see https://github.com/angular/angular/issues/54013 + * Reset Angular's TestBed and configure the standard TanStack Query providers for tests. + * Pass additional providers (including EnvironmentProviders) via the options argument. */ -export function setSignalInputs>( - component: T, - inputs: ToSignalInputUpdatableMap, +export function setupTanStackQueryTestBed( + queryClient: QueryClient, + options: { providers?: Array } = {}, ) { - for (const inputKey in inputs) { - if (componentHasSignalInputProperty(component, inputKey)) { - signalSetFn(component[inputKey][SIGNAL], inputs[inputKey]) - } - } + TestBed.resetTestingModule() + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery(queryClient), + ...(options.providers ?? []), + ], + }) } -export function setFixtureSignalInputs>( - componentFixture: ComponentFixture, - inputs: ToSignalInputUpdatableMap, - options: { detectChanges: boolean } = { detectChanges: true }, -) { - setSignalInputs(componentFixture.componentInstance, inputs) - if (options.detectChanges) { - componentFixture.detectChanges() - } +/** + * TanStack Query schedules notifyManager updates with setTimeout(0); when fake timers + * are enabled, advance them so PendingTasks sees the queued work. + */ +export async function flushQueryUpdates() { + await vi.advanceTimersByTimeAsync(0) } diff --git a/packages/angular-query-experimental/src/__tests__/with-hydration.test.ts b/packages/angular-query-experimental/src/__tests__/with-hydration.test.ts new file mode 100644 index 00000000000..5d5732827fa --- /dev/null +++ b/packages/angular-query-experimental/src/__tests__/with-hydration.test.ts @@ -0,0 +1,316 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { + QueryClient, + dehydrate, + injectQuery, + provideTanStackQuery, + withHydrationKey, + withNoQueryHydration, +} from '..' +import { + Component, + EnvironmentInjector, + PLATFORM_ID, + TransferState, + createEnvironmentInjector, + effect, + inject, + makeStateKey, + provideZonelessChangeDetection, + provideEnvironmentInitializer, +} from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { render } from '@testing-library/angular' +import { queryKey, sleep } from '@tanstack/query-test-utils' +import type { DehydratedState } from '@tanstack/query-core' +import { INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY } from '../hydration-state-key' + +beforeEach(() => { + vi.useFakeTimers() +}) + +afterEach(() => { + vi.restoreAllMocks() + vi.useRealTimers() +}) + +describe('TransferState hydration (client)', () => { + test('browser hydrates from default key', async () => { + const key = queryKey() + const sourceClient = new QueryClient() + sourceClient.setQueryData(key, 'from-server') + const dehydrated = dehydrate(sourceClient) + const appClient = new QueryClient() + + @Component({ + template: `
{{ state.data() ?? '' }}
`, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: () => sleep(10).then(() => 'from-client'), + })) + } + + const rendered = await render(Page, { + providers: [ + provideZonelessChangeDetection(), + provideEnvironmentInitializer(() => { + inject(TransferState).set( + INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY, + dehydrated, + ) + }), + provideTanStackQuery(appClient), + ], + }) + + rendered.fixture.detectChanges() + expect(rendered.getByText('from-server')).toBeInTheDocument() + expect( + rendered.fixture.debugElement.injector + .get(TransferState) + .get(INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY, null), + ).toBeNull() + }) + + test('browser hydrates from custom key', async () => { + const customKeyName = 'tanstack-test-custom-hydration' + const customKey = makeStateKey(customKeyName) + const key = queryKey() + const sourceClient = new QueryClient() + sourceClient.setQueryData(key, 'from-custom-key') + const dehydrated = dehydrate(sourceClient) + const appClient = new QueryClient() + + @Component({ + template: `
{{ state.data() ?? '' }}
`, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: () => Promise.resolve('from-client'), + })) + } + + const rendered = await render(Page, { + providers: [ + provideZonelessChangeDetection(), + provideEnvironmentInitializer(() => { + inject(TransferState).set(customKey, dehydrated) + }), + provideTanStackQuery(appClient, withHydrationKey(customKeyName)), + ], + }) + + rendered.fixture.detectChanges() + expect(rendered.getByText('from-custom-key')).toBeTruthy() + expect(rendered.fixture.debugElement.injector.get(TransferState).get(customKey, null)).toBeNull() + }) + + test('browser does not re-fetch when hydrated state is fresh', async () => { + const key = queryKey() + const sourceClient = new QueryClient() + sourceClient.setQueryData(key, 'cached') + const dehydrated = dehydrate(sourceClient) + const appClient = new QueryClient() + let queryFnCalls = 0 + + @Component({ + template: `
{{ state.data() ?? '' }}
`, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + staleTime: Infinity, + queryFn: () => { + queryFnCalls++ + return sleep(10).then(() => 'should-not-run') + }, + })) + + _ = effect(() => { + void this.state.data() + }) + } + + await render(Page, { + providers: [ + provideZonelessChangeDetection(), + provideEnvironmentInitializer(() => { + inject(TransferState).set( + INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY, + dehydrated, + ) + }), + provideTanStackQuery(appClient), + ], + }) + + await vi.advanceTimersByTimeAsync(100) + expect(queryFnCalls).toBe(0) + }) + + test('withNoQueryHydration disables browser hydration', async () => { + const key = queryKey() + const sourceClient = new QueryClient() + sourceClient.setQueryData(key, 'from-server') + const dehydrated = dehydrate(sourceClient) + const appClient = new QueryClient() + + @Component({ + template: `
{{ state.data() ?? '' }}
`, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: () => sleep(10).then(() => 'from-client'), + })) + } + + const rendered = await render(Page, { + providers: [ + provideZonelessChangeDetection(), + provideEnvironmentInitializer(() => { + inject(TransferState).set( + INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY, + dehydrated, + ) + }), + provideTanStackQuery(appClient, withNoQueryHydration()), + ], + }) + + rendered.fixture.detectChanges() + await vi.advanceTimersByTimeAsync(10) + rendered.fixture.detectChanges() + + expect(rendered.getByText('from-client')).toBeInTheDocument() + expect( + rendered.fixture.debugElement.injector + .get(TransferState) + .get(INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY, null), + ).toEqual(dehydrated) + }) +}) + +describe('TransferState dehydration (server)', () => { + function createQueryInjector( + queryClient: QueryClient, + platformId: 'server' | 'browser', + ...features: Parameters[1][] + ) { + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + { provide: PLATFORM_ID, useValue: platformId }, + ], + }) + + return createEnvironmentInjector( + [provideTanStackQuery(queryClient, ...features)], + TestBed.inject(EnvironmentInjector), + ) + } + + test('server serializes dehydrated queries on TransferState.toJson', async () => { + const key = queryKey() + const queryClient = new QueryClient() + const injector = createQueryInjector(queryClient, 'server') + + queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => Promise.resolve('ssr-data'), + }) + await vi.advanceTimersByTimeAsync(0) + + injector.get(TransferState).toJson() + + const stored = injector.get(TransferState).get( + INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY, + null, + ) + expect(stored).not.toBeNull() + if (!stored) throw new Error('expected dehydrated state') + expect(stored.queries.length).toBe(1) + expect(stored.queries[0]?.queryKey).toEqual(key) + }) + + test('browser platform does not write server dehydration state', async () => { + const key = queryKey() + const queryClient = new QueryClient() + const injector = createQueryInjector(queryClient, 'browser') + + queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => Promise.resolve('data'), + }) + await vi.advanceTimersByTimeAsync(0) + + injector.get(TransferState).toJson() + + expect( + injector.get(TransferState).get( + INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY, + null, + ), + ).toBeNull() + }) + + test('withNoQueryHydration disables server dehydration', async () => { + const key = queryKey() + const queryClient = new QueryClient() + const injector = createQueryInjector( + queryClient, + 'server', + withNoQueryHydration(), + ) + + queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => Promise.resolve('data'), + }) + await vi.advanceTimersByTimeAsync(0) + + injector.get(TransferState).toJson() + + expect( + injector.get(TransferState).get( + INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY, + null, + ), + ).toBeNull() + }) + + test('multiple query clients can use distinct hydration keys', async () => { + const keyA = queryKey() + const keyB = queryKey() + const clientA = new QueryClient() + const clientB = new QueryClient() + + TestBed.configureTestingModule({ + providers: [ + provideZonelessChangeDetection(), + { provide: PLATFORM_ID, useValue: 'server' }, + ], + }) + + createEnvironmentInjector( + [provideTanStackQuery(clientA, withHydrationKey('client-a'))], + TestBed.inject(EnvironmentInjector), + ) + createEnvironmentInjector( + [provideTanStackQuery(clientB, withHydrationKey('client-b'))], + TestBed.inject(EnvironmentInjector), + ) + + clientA.setQueryData(keyA, 'a') + clientB.setQueryData(keyB, 'b') + + const transferState = TestBed.inject(TransferState) + transferState.toJson() + + expect(transferState.get(makeStateKey('client-a'), null)?.queries[0]?.queryKey).toEqual(keyA) + expect(transferState.get(makeStateKey('client-b'), null)?.queries[0]?.queryKey).toEqual(keyB) + }) +}) diff --git a/packages/angular-query-experimental/src/create-base-query.ts b/packages/angular-query-experimental/src/create-base-query.ts index 4daede76844..2e97c5929ac 100644 --- a/packages/angular-query-experimental/src/create-base-query.ts +++ b/packages/angular-query-experimental/src/create-base-query.ts @@ -1,10 +1,11 @@ import { + DestroyRef, NgZone, - VERSION, + PendingTasks, computed, effect, inject, - signal, + linkedSignal, untracked, } from '@angular/core' import { @@ -14,9 +15,9 @@ import { } from '@tanstack/query-core' import { signalProxy } from './signal-proxy' import { injectIsRestoring } from './inject-is-restoring' -import { PENDING_TASKS } from './pending-tasks-compat' -import type { PendingTaskRef } from './pending-tasks-compat' +import type { MethodKeys } from './signal-proxy' import type { + DefaultedQueryObserverOptions, QueryKey, QueryObserver, QueryObserverResult, @@ -27,6 +28,7 @@ import type { CreateBaseQueryOptions } from './types' * Base implementation for `injectQuery` and `injectInfiniteQuery`. * @param optionsFn * @param Observer + * @param excludeFunctions */ export function createBaseQuery< TQueryFnData, @@ -43,11 +45,29 @@ export function createBaseQuery< TQueryKey >, Observer: typeof QueryObserver, + excludeFunctions: ReadonlyArray, ) { const ngZone = inject(NgZone) - const pendingTasks = inject(PENDING_TASKS) + const pendingTasks = inject(PendingTasks) const queryClient = inject(QueryClient) const isRestoring = injectIsRestoring() + const destroyRef = inject(DestroyRef) + + let destroyed = false + let taskCleanupRef: (() => void) | null = null + + const startPendingTask = () => { + if (!taskCleanupRef && !destroyed) { + taskCleanupRef = pendingTasks.add() + } + } + + const stopPendingTask = () => { + if (taskCleanupRef) { + taskCleanupRef() + taskCleanupRef = null + } + } /** * Signal that has the default options from query client applied @@ -63,113 +83,120 @@ export function createBaseQuery< return defaultedOptions }) - const observerSignal = (() => { - let instance: QueryObserver< + // Computed without deps to lazy initialize the observer + const observerSignal = computed(() => { + return new Observer(queryClient, untracked(defaultedOptionsSignal)) + }) + + effect(() => { + observerSignal().setOptions(defaultedOptionsSignal()) + }) + + const trackObserverResult = ( + result: QueryObserverResult, + notifyOnChangeProps?: DefaultedQueryObserverOptions< TQueryFnData, TError, TData, TQueryData, TQueryKey - > | null = null + >['notifyOnChangeProps'], + ) => { + const observer = untracked(observerSignal) + const trackedResult = observer.trackResult(result) + + if (!notifyOnChangeProps) { + autoTrackResultProperties(trackedResult) + } + + return trackedResult + } + + const autoTrackResultProperties = ( + result: QueryObserverResult, + ) => { + for (const key of Object.keys(result) as Array< + keyof QueryObserverResult + >) { + if (key === 'promise') continue + const value = result[key] + if (typeof value === 'function') continue + // Access value once so QueryObserver knows this prop is tracked. + void value + } + } + + const subscribeToObserver = () => { + const observer = untracked(observerSignal) + const initialState = observer.getCurrentResult() + if (initialState.fetchStatus !== 'idle') { + startPendingTask() + } + + return observer.subscribe((state) => { + if (state.fetchStatus !== 'idle') { + startPendingTask() + } else { + stopPendingTask() + } - return computed(() => { - return (instance ||= new Observer(queryClient, defaultedOptionsSignal())) + queueMicrotask(() => { + if (destroyed) return + notifyManager.batch(() => { + ngZone.run(() => { + if ( + state.isError && + !state.isFetching && + shouldThrowError(observer.options.throwOnError, [ + state.error, + observer.getCurrentQuery(), + ]) + ) { + ngZone.onError.emit(state.error) + throw state.error + } + const trackedState = trackObserverResult( + state, + observer.options.notifyOnChangeProps, + ) + resultSignal.set(trackedState) + }) + }) + }) }) - })() + } - const optimisticResultSignal = computed(() => - observerSignal().getOptimisticResult(defaultedOptionsSignal()), - ) - - const resultFromSubscriberSignal = signal | null>(null) - - effect( - (onCleanup) => { - const observer = observerSignal() + const resultSignal = linkedSignal({ + source: defaultedOptionsSignal, + computation: () => { + const observer = untracked(observerSignal) const defaultedOptions = defaultedOptionsSignal() - untracked(() => { - observer.setOptions(defaultedOptions) - }) - onCleanup(() => { - ngZone.run(() => resultFromSubscriberSignal.set(null)) - }) + const result = observer.getOptimisticResult(defaultedOptions) + return trackObserverResult(result, defaultedOptions.notifyOnChangeProps) }, - { - // Set allowSignalWrites to support Angular < v19 - // Set to undefined to avoid warning on newer versions - allowSignalWrites: VERSION.major < '19' || undefined, - }, - ) + }) effect((onCleanup) => { - // observer.trackResult is not used as this optimization is not needed for Angular - const observer = observerSignal() - let pendingTaskRef: PendingTaskRef | null = null - - const unsubscribe = isRestoring() - ? () => undefined - : untracked(() => - ngZone.runOutsideAngular(() => { - return observer.subscribe( - notifyManager.batchCalls((state) => { - ngZone.run(() => { - if (state.fetchStatus === 'fetching' && !pendingTaskRef) { - pendingTaskRef = pendingTasks.add() - } - - if (state.fetchStatus === 'idle' && pendingTaskRef) { - pendingTaskRef() - pendingTaskRef = null - } - - if ( - state.isError && - !state.isFetching && - shouldThrowError(observer.options.throwOnError, [ - state.error, - observer.getCurrentQuery(), - ]) - ) { - ngZone.onError.emit(state.error) - throw state.error - } - resultFromSubscriberSignal.set(state) - }) - }), - ) - }), - ) - + if (isRestoring()) { + return + } + const unsubscribe = untracked(() => + ngZone.runOutsideAngular(() => subscribeToObserver()), + ) onCleanup(() => { - if (pendingTaskRef) { - pendingTaskRef() - pendingTaskRef = null - } unsubscribe() + stopPendingTask() }) }) + destroyRef.onDestroy(() => { + destroyed = true + stopPendingTask() + }) + return signalProxy( - computed(() => { - const subscriberResult = resultFromSubscriberSignal() - const optimisticResult = optimisticResultSignal() - const result = subscriberResult ?? optimisticResult - - // Wrap methods to ensure observer has latest options before execution - const observer = observerSignal() - - const originalRefetch = result.refetch - return { - ...result, - refetch: ((...args: Parameters) => { - observer.setOptions(defaultedOptionsSignal()) - return originalRefetch(...args) - }) as typeof originalRefetch, - } - }), + resultSignal.asReadonly(), + excludeFunctions as Array>>, ) } diff --git a/packages/angular-query-experimental/src/hydration-state-key.ts b/packages/angular-query-experimental/src/hydration-state-key.ts new file mode 100644 index 00000000000..c788f1901fa --- /dev/null +++ b/packages/angular-query-experimental/src/hydration-state-key.ts @@ -0,0 +1,12 @@ +import { InjectionToken, makeStateKey, type StateKey } from '@angular/core' +import type { DehydratedState } from '@tanstack/query-core' + +export const INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY = + makeStateKey('tanstack-query-hydration-state') + +export const INTERNAL_TANSTACK_QUERY_HYDRATION_TRANSFER_KEY = new InjectionToken< + StateKey +>('tanstack-query-hydration-transfer-key', { + providedIn: 'root', + factory: () => INTERNAL_TANSTACK_QUERY_HYDRATION_STATE_KEY, +}) diff --git a/packages/angular-query-experimental/src/index.ts b/packages/angular-query-experimental/src/index.ts index fc033224e51..a24d791d1df 100644 --- a/packages/angular-query-experimental/src/index.ts +++ b/packages/angular-query-experimental/src/index.ts @@ -39,7 +39,12 @@ export { injectMutation } from './inject-mutation' export type { InjectMutationStateOptions } from './inject-mutation-state' export { injectMutationState } from './inject-mutation-state' -export type { QueriesOptions, QueriesResults } from './inject-queries' +export type { + InjectQueriesOptions, + QueriesOptions, + QueriesResults, +} from './inject-queries' +export { injectQueries } from './inject-queries' export type { InjectQueryOptions } from './inject-query' export { injectQuery } from './inject-query' @@ -57,4 +62,6 @@ export { provideQueryClient, provideTanStackQuery, queryFeature, + withHydrationKey, + withNoQueryHydration, } from './providers' diff --git a/packages/angular-query-experimental/src/inject-infinite-query.ts b/packages/angular-query-experimental/src/inject-infinite-query.ts index ee6de032409..3fe5ce00923 100644 --- a/packages/angular-query-experimental/src/inject-infinite-query.ts +++ b/packages/angular-query-experimental/src/inject-infinite-query.ts @@ -6,9 +6,11 @@ import { runInInjectionContext, } from '@angular/core' import { createBaseQuery } from './create-base-query' +import type { MethodKeys } from './signal-proxy' import type { DefaultError, InfiniteData, + InfiniteQueryObserverResult, QueryKey, QueryObserver, } from '@tanstack/query-core' @@ -31,13 +33,6 @@ export interface InjectInfiniteQueryOptions { injector?: Injector } -/** - * Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. - * Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" - * @param injectInfiniteQueryFn - A function that returns infinite query options. - * @param options - Additional configuration. - * @returns The infinite query result. - */ export function injectInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -55,13 +50,6 @@ export function injectInfiniteQuery< options?: InjectInfiniteQueryOptions, ): DefinedCreateInfiniteQueryResult -/** - * Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. - * Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" - * @param injectInfiniteQueryFn - A function that returns infinite query options. - * @param options - Additional configuration. - * @returns The infinite query result. - */ export function injectInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -79,13 +67,6 @@ export function injectInfiniteQuery< options?: InjectInfiniteQueryOptions, ): CreateInfiniteQueryResult -/** - * Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. - * Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" - * @param injectInfiniteQueryFn - A function that returns infinite query options. - * @param options - Additional configuration. - * @returns The infinite query result. - */ export function injectInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -105,13 +86,27 @@ export function injectInfiniteQuery< /** * Injects an infinite query: a declarative dependency on an asynchronous source of data that is tied to a unique key. - * Infinite queries can additively "load more" data onto an existing set of data or "infinite scroll" + * Infinite queries can additively "load more" data onto an existing set of data or support infinite scroll. + * * @param injectInfiniteQueryFn - A function that returns infinite query options. * @param options - Additional configuration. * @returns The infinite query result. + * @see https://tanstack.com/query/latest/docs/framework/angular/guides/infinite-queries */ -export function injectInfiniteQuery( - injectInfiniteQueryFn: () => CreateInfiniteQueryOptions, +export function injectInfiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + injectInfiniteQueryFn: () => CreateInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, options?: InjectInfiniteQueryOptions, ) { !options?.injector && assertInInjectionContext(injectInfiniteQuery) @@ -120,6 +115,13 @@ export function injectInfiniteQuery( createBaseQuery( injectInfiniteQueryFn, InfiniteQueryObserver as typeof QueryObserver, + methodsToExclude, ), ) } + +const methodsToExclude: Array> = [ + 'fetchNextPage', + 'fetchPreviousPage', + 'refetch', +] diff --git a/packages/angular-query-experimental/src/inject-mutation.ts b/packages/angular-query-experimental/src/inject-mutation.ts index 7eb605047f3..3399d5f3325 100644 --- a/packages/angular-query-experimental/src/inject-mutation.ts +++ b/packages/angular-query-experimental/src/inject-mutation.ts @@ -1,6 +1,7 @@ import { Injector, NgZone, + PendingTasks, assertInInjectionContext, computed, effect, @@ -16,8 +17,6 @@ import { shouldThrowError, } from '@tanstack/query-core' import { signalProxy } from './signal-proxy' -import { PENDING_TASKS } from './pending-tasks-compat' -import type { PendingTaskRef } from './pending-tasks-compat' import type { DefaultError, MutationObserverResult } from '@tanstack/query-core' import type { CreateMutateFunction, @@ -59,7 +58,7 @@ export function injectMutation< !options?.injector && assertInInjectionContext(injectMutation) const injector = options?.injector ?? inject(Injector) const ngZone = injector.get(NgZone) - const pendingTasks = injector.get(PENDING_TASKS) + const pendingTasks = injector.get(PendingTasks) const queryClient = injector.get(QueryClient) /** @@ -69,18 +68,23 @@ export function injectMutation< */ const optionsSignal = computed(injectMutationFn) - const observerSignal = (() => { - let instance: MutationObserver< - TData, - TError, - TVariables, - TOnMutateResult - > | null = null + const observerSignal = computed(() => new MutationObserver(queryClient, untracked(optionsSignal))) - return computed(() => { - return (instance ||= new MutationObserver(queryClient, optionsSignal())) - }) - })() + let destroyed = false + let taskCleanupRef: (() => void) | null = null + + const startPendingTask = () => { + if (!taskCleanupRef && !destroyed) { + taskCleanupRef = pendingTasks.add() + } + } + + const stopPendingTask = () => { + if (taskCleanupRef) { + taskCleanupRef() + taskCleanupRef = null + } + } const mutateFnSignal = computed< CreateMutateFunction @@ -125,24 +129,19 @@ export function injectMutation< effect( (onCleanup) => { - // observer.trackResult is not used as this optimization is not needed for Angular const observer = observerSignal() - let pendingTaskRef: PendingTaskRef | null = null untracked(() => { const unsubscribe = ngZone.runOutsideAngular(() => observer.subscribe( notifyManager.batchCalls((state) => { ngZone.run(() => { - // Track pending task when mutation is pending - if (state.isPending && !pendingTaskRef) { - pendingTaskRef = pendingTasks.add() - } + if (destroyed) return - // Clear pending task when mutation is no longer pending - if (!state.isPending && pendingTaskRef) { - pendingTaskRef() - pendingTaskRef = null + if (state.isPending) { + startPendingTask() + } else { + stopPendingTask() } if ( @@ -159,11 +158,8 @@ export function injectMutation< ), ) onCleanup(() => { - // Clean up any pending task on destroy - if (pendingTaskRef) { - pendingTaskRef() - pendingTaskRef = null - } + destroyed = true + stopPendingTask() unsubscribe() }) }) @@ -186,10 +182,9 @@ export function injectMutation< } }) - return signalProxy(resultSignal) as CreateMutationResult< - TData, - TError, - TVariables, - TOnMutateResult - > + return signalProxy(resultSignal, [ + 'mutate', + 'mutateAsync', + 'reset', + ]) as CreateMutationResult } diff --git a/packages/angular-query-experimental/src/inject-queries-experimental/index.ts b/packages/angular-query-experimental/src/inject-queries-experimental/index.ts index d300d7d2a55..b5664a83cb2 100644 --- a/packages/angular-query-experimental/src/inject-queries-experimental/index.ts +++ b/packages/angular-query-experimental/src/inject-queries-experimental/index.ts @@ -1 +1,4 @@ +/** + * @deprecated Use `injectQueries` from the package root instead. + */ export * from '../inject-queries' diff --git a/packages/angular-query-experimental/src/inject-queries.ts b/packages/angular-query-experimental/src/inject-queries.ts index 2f201799e74..eb9a7af7f2b 100644 --- a/packages/angular-query-experimental/src/inject-queries.ts +++ b/packages/angular-query-experimental/src/inject-queries.ts @@ -7,24 +7,27 @@ import { DestroyRef, Injector, NgZone, + PendingTasks, assertInInjectionContext, computed, effect, inject, + linkedSignal, runInInjectionContext, - signal, untracked, } from '@angular/core' import { signalProxy } from './signal-proxy' import { injectIsRestoring } from './inject-is-restoring' import type { DefaultError, + DefinedQueryObserverResult, OmitKeyof, QueriesObserverOptions, QueriesPlaceholderDataFunction, QueryFunction, QueryKey, QueryObserverOptions, + QueryObserverResult, ThrowOnError, } from '@tanstack/query-core' import type { @@ -33,6 +36,7 @@ import type { DefinedCreateQueryResult, } from './types' import type { Signal } from '@angular/core' +import type { MethodKeys } from './signal-proxy' // This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. // `placeholderData` function always gets undefined passed @@ -90,39 +94,42 @@ type GetCreateQueryOptionsForCreateQueries = : // Fallback QueryObserverOptionsForCreateQueries -// A defined initialData setting should return a DefinedCreateQueryResult rather than CreateQueryResult -type GetDefinedOrUndefinedQueryResult = T extends { - initialData?: infer TInitialData -} - ? unknown extends TInitialData - ? CreateQueryResult - : TInitialData extends TData - ? DefinedCreateQueryResult - : TInitialData extends () => infer TInitialDataResult - ? unknown extends TInitialDataResult - ? CreateQueryResult - : TInitialDataResult extends TData - ? DefinedCreateQueryResult - : CreateQueryResult - : CreateQueryResult - : CreateQueryResult - -type GetCreateQueryResult = - // Part 1: responsible for mapping explicit type parameter to function result, if object +// Generic wrapper that handles initialData logic for any result type pair +type GenericGetDefinedOrUndefinedQueryResult = + T extends { + initialData?: infer TInitialData + } + ? unknown extends TInitialData + ? TUndefined + : TInitialData extends TData + ? TDefined + : TInitialData extends () => infer TInitialDataResult + ? unknown extends TInitialDataResult + ? TUndefined + : TInitialDataResult extends TData + ? TDefined + : TUndefined + : TUndefined + : TUndefined + +// Infer TData and TError from query options +// Shared type between the results with and without the combine function +type InferDataAndError = + // Part 1: explicit type parameter as object { queryFnData, error, data } T extends { queryFnData: any; error?: infer TError; data: infer TData } - ? GetDefinedOrUndefinedQueryResult + ? { data: TData; error: TError } : T extends { queryFnData: infer TQueryFnData; error?: infer TError } - ? GetDefinedOrUndefinedQueryResult + ? { data: TQueryFnData; error: TError } : T extends { data: infer TData; error?: infer TError } - ? GetDefinedOrUndefinedQueryResult - : // Part 2: responsible for mapping explicit type parameter to function result, if tuple + ? { data: TData; error: TError } + : // Part 2: explicit type parameter as tuple [TQueryFnData, TError, TData] T extends [any, infer TError, infer TData] - ? GetDefinedOrUndefinedQueryResult + ? { data: TData; error: TError } : T extends [infer TQueryFnData, infer TError] - ? GetDefinedOrUndefinedQueryResult + ? { data: TQueryFnData; error: TError } : T extends [infer TQueryFnData] - ? GetDefinedOrUndefinedQueryResult - : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided + ? { data: TQueryFnData; error: unknown } + : // Part 3: infer from queryFn, select, throwOnError T extends { queryFn?: | QueryFunction @@ -130,13 +137,40 @@ type GetCreateQueryResult = select?: (data: any) => infer TData throwOnError?: ThrowOnError } - ? GetDefinedOrUndefinedQueryResult< - T, - unknown extends TData ? TQueryFnData : TData, - unknown extends TError ? DefaultError : TError - > + ? { + data: unknown extends TData ? TQueryFnData : TData + error: unknown extends TError ? DefaultError : TError + } : // Fallback - CreateQueryResult + { data: unknown; error: DefaultError } + +// Maps query options to Angular's signal-wrapped CreateQueryResult +type GetCreateQueryResult = GenericGetDefinedOrUndefinedQueryResult< + T, + InferDataAndError['data'], + CreateQueryResult< + InferDataAndError['data'], + InferDataAndError['error'] + >, + DefinedCreateQueryResult< + InferDataAndError['data'], + InferDataAndError['error'] + > +> + +// Maps query options to plain QueryObserverResult for combine function +type GetQueryObserverResult = GenericGetDefinedOrUndefinedQueryResult< + T, + InferDataAndError['data'], + QueryObserverResult< + InferDataAndError['data'], + InferDataAndError['error'] + >, + DefinedQueryObserverResult< + InferDataAndError['data'], + InferDataAndError['error'] + > +> /** * QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param @@ -201,6 +235,25 @@ export type QueriesResults< > : { [K in keyof T]: GetCreateQueryResult } +// Maps query options array to plain QueryObserverResult types for combine function +type RawQueriesResults< + T extends Array, + TResults extends Array = [], + TDepth extends ReadonlyArray = [], +> = TDepth['length'] extends MAXIMUM_DEPTH + ? Array + : T extends [] + ? [] + : T extends [infer Head] + ? [...TResults, GetQueryObserverResult] + : T extends [infer Head, ...infer Tails] + ? RawQueriesResults< + [...Tails], + [...TResults, GetQueryObserverResult], + [...TDepth, 1] + > + : { [K in keyof T]: GetQueryObserverResult } + export interface InjectQueriesOptions< T extends Array, TCombinedResult = QueriesResults, @@ -210,9 +263,14 @@ export interface InjectQueriesOptions< | readonly [ ...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueries }, ] - combine?: (result: QueriesResults) => TCombinedResult + combine?: (result: RawQueriesResults) => TCombinedResult } +const methodsToExclude: Array> = ['refetch'] + +const hasPendingQueriesState = (results: Array): boolean => + results.some((result) => result.fetchStatus !== 'idle') + /** * @param optionsFn - A function that returns queries' options. * @param injector - The Angular injector to use. @@ -228,8 +286,24 @@ export function injectQueries< return runInInjectionContext(injector ?? inject(Injector), () => { const destroyRef = inject(DestroyRef) const ngZone = inject(NgZone) + const pendingTasks = inject(PendingTasks) const queryClient = inject(QueryClient) const isRestoring = injectIsRestoring() + let destroyed = false + let taskCleanupRef: (() => void) | null = null + + const startPendingTask = () => { + if (!taskCleanupRef && !destroyed) { + taskCleanupRef = pendingTasks.add() + } + } + + const stopPendingTask = () => { + if (taskCleanupRef) { + taskCleanupRef() + taskCleanupRef = null + } + } /** * Signal that has the default options from query client applied @@ -255,76 +329,125 @@ export function injectQueries< }) }) - const observerSignal = (() => { - let instance: QueriesObserver | null = null + const observerOptionsSignal = computed( + () => optionsSignal() as QueriesObserverOptions, + ) - return computed(() => { - return (instance ||= new QueriesObserver( - queryClient, - defaultedQueries(), - optionsSignal() as QueriesObserverOptions, - )) - }) - })() + // Computed without deps to lazy initialize the observer + const observerSignal = computed(() => { + return new QueriesObserver( + queryClient, + untracked(defaultedQueries), + untracked(observerOptionsSignal), + ) + }) const optimisticResultSignal = computed(() => observerSignal().getOptimisticResult( defaultedQueries(), - (optionsSignal() as QueriesObserverOptions).combine, + observerOptionsSignal().combine, ), ) // Do not notify on updates because of changes in the options because // these changes should already be reflected in the optimistic result. effect(() => { - observerSignal().setQueries( - defaultedQueries(), - optionsSignal() as QueriesObserverOptions, - ) + observerSignal().setQueries(defaultedQueries(), observerOptionsSignal()) }) - const optimisticCombinedResultSignal = computed(() => { - const [_optimisticResult, getCombinedResult, trackResult] = - optimisticResultSignal() - return getCombinedResult(trackResult()) + const optimisticResultSourceSignal = computed(() => { + const options = observerOptionsSignal() + return { queries: defaultedQueries(), combine: options.combine } }) - const resultFromSubscriberSignal = signal(null) + const resultSignal = linkedSignal({ + source: optimisticResultSourceSignal, + computation: () => { + const observer = untracked(observerSignal) + const [_optimisticResult, getCombinedResult, trackResult] = + observer.getOptimisticResult( + defaultedQueries(), + observerOptionsSignal().combine, + ) + return getCombinedResult(trackResult()) + }, + }) - effect(() => { + effect((onCleanup) => { const observer = observerSignal() - const [_optimisticResult, getCombinedResult] = optimisticResultSignal() - - untracked(() => { - const unsubscribe = isRestoring() - ? () => undefined - : ngZone.runOutsideAngular(() => - observer.subscribe( - notifyManager.batchCalls((state) => { - resultFromSubscriberSignal.set(getCombinedResult(state)) - }), - ), - ) - - destroyRef.onDestroy(unsubscribe) + const [optimisticResult, getCombinedResult] = optimisticResultSignal() + + if (isRestoring()) { + stopPendingTask() + return + } + + if (hasPendingQueriesState(optimisticResult)) { + startPendingTask() + } else { + stopPendingTask() + } + + const unsubscribe = untracked(() => + ngZone.runOutsideAngular(() => + observer.subscribe((state) => { + if (hasPendingQueriesState(state)) { + startPendingTask() + } else { + stopPendingTask() + } + + queueMicrotask(() => { + if (destroyed) return + notifyManager.batch(() => { + ngZone.run(() => { + resultSignal.set(getCombinedResult(state)) + }) + }) + }) + }), + ), + ) + + onCleanup(() => { + unsubscribe() + stopPendingTask() }) }) - const resultSignal = computed(() => { - const subscriberResult = resultFromSubscriberSignal() - const optimisticResult = optimisticCombinedResultSignal() - return subscriberResult ?? optimisticResult + // Angular does not use reactive getters on plain objects, so we wrap each + // QueryObserverResult in a signal-backed proxy to keep field-level tracking + // (`result.data()`, `result.status()`, etc.). + // Solid uses a related proxy approach in useQueries, but there it proxies + // object fields for store/resource reactivity rather than callable signals. + const createResultProxy = (index: number) => + signalProxy( + computed(() => (resultSignal() as Array)[index]!), + methodsToExclude, + ) + + // Keep this positional to match QueriesObserver semantics. + // Like Solid/Vue adapters, proxies are rebuilt from current observer output. + const proxiedResultsSignal = computed(() => + (resultSignal() as Array).map((_, index) => + createResultProxy(index), + ), + ) + + destroyRef.onDestroy(() => { + destroyed = true + stopPendingTask() }) return computed(() => { const result = resultSignal() const { combine } = optionsSignal() - return combine - ? result - : (result as QueriesResults).map((query) => - signalProxy(signal(query)), - ) + if (combine) { + return result + } + + return proxiedResultsSignal() as unknown as TCombinedResult }) }) as unknown as Signal } diff --git a/packages/angular-query-experimental/src/inject-query.ts b/packages/angular-query-experimental/src/inject-query.ts index 1dac0ab6949..02d6f9113d8 100644 --- a/packages/angular-query-experimental/src/inject-query.ts +++ b/packages/angular-query-experimental/src/inject-query.ts @@ -6,7 +6,12 @@ import { runInInjectionContext, } from '@angular/core' import { createBaseQuery } from './create-base-query' -import type { DefaultError, QueryKey } from '@tanstack/query-core' +import type { MethodKeys } from './signal-proxy' +import type { + DefaultError, + QueryKey, + QueryObserverResult, +} from '@tanstack/query-core' import type { CreateQueryOptions, CreateQueryResult, @@ -26,42 +31,6 @@ export interface InjectQueryOptions { injector?: Injector } -/** - * Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. - * - * **Basic example** - * ```ts - * class ServiceOrComponent { - * query = injectQuery(() => ({ - * queryKey: ['repoData'], - * queryFn: () => - * this.#http.get('https://api.github.com/repos/tanstack/query'), - * })) - * } - * ``` - * - * Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context. - * In the example below, the query will be automatically enabled and executed when the filter signal changes - * to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled. - * - * **Reactive example** - * ```ts - * class ServiceOrComponent { - * filter = signal('') - * - * todosQuery = injectQuery(() => ({ - * queryKey: ['todos', this.filter()], - * queryFn: () => fetchTodos(this.filter()), - * // Signals can be combined with expressions - * enabled: !!this.filter(), - * })) - * } - * ``` - * @param injectQueryFn - A function that returns query options. - * @param options - Additional configuration - * @returns The query result. - * @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries - */ export function injectQuery< TQueryFnData = unknown, TError = DefaultError, @@ -77,42 +46,6 @@ export function injectQuery< options?: InjectQueryOptions, ): DefinedCreateQueryResult -/** - * Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. - * - * **Basic example** - * ```ts - * class ServiceOrComponent { - * query = injectQuery(() => ({ - * queryKey: ['repoData'], - * queryFn: () => - * this.#http.get('https://api.github.com/repos/tanstack/query'), - * })) - * } - * ``` - * - * Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context. - * In the example below, the query will be automatically enabled and executed when the filter signal changes - * to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled. - * - * **Reactive example** - * ```ts - * class ServiceOrComponent { - * filter = signal('') - * - * todosQuery = injectQuery(() => ({ - * queryKey: ['todos', this.filter()], - * queryFn: () => fetchTodos(this.filter()), - * // Signals can be combined with expressions - * enabled: !!this.filter(), - * })) - * } - * ``` - * @param injectQueryFn - A function that returns query options. - * @param options - Additional configuration - * @returns The query result. - * @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries - */ export function injectQuery< TQueryFnData = unknown, TError = DefaultError, @@ -128,42 +61,6 @@ export function injectQuery< options?: InjectQueryOptions, ): CreateQueryResult -/** - * Injects a query: a declarative dependency on an asynchronous source of data that is tied to a unique key. - * - * **Basic example** - * ```ts - * class ServiceOrComponent { - * query = injectQuery(() => ({ - * queryKey: ['repoData'], - * queryFn: () => - * this.#http.get('https://api.github.com/repos/tanstack/query'), - * })) - * } - * ``` - * - * Similar to `computed` from Angular, the function passed to `injectQuery` will be run in the reactive context. - * In the example below, the query will be automatically enabled and executed when the filter signal changes - * to a truthy value. When the filter signal changes back to a falsy value, the query will be disabled. - * - * **Reactive example** - * ```ts - * class ServiceOrComponent { - * filter = signal('') - * - * todosQuery = injectQuery(() => ({ - * queryKey: ['todos', this.filter()], - * queryFn: () => fetchTodos(this.filter()), - * // Signals can be combined with expressions - * enabled: !!this.filter(), - * })) - * } - * ``` - * @param injectQueryFn - A function that returns query options. - * @param options - Additional configuration - * @returns The query result. - * @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries - */ export function injectQuery< TQueryFnData = unknown, TError = DefaultError, @@ -184,11 +81,15 @@ export function injectQuery< * * **Basic example** * ```ts + * import { lastValueFrom } from 'rxjs' + * * class ServiceOrComponent { * query = injectQuery(() => ({ * queryKey: ['repoData'], * queryFn: () => - * this.#http.get('https://api.github.com/repos/tanstack/query'), + * lastValueFrom( + * this.#http.get('https://api.github.com/repos/tanstack/query'), + * ), * })) * } * ``` @@ -211,7 +112,7 @@ export function injectQuery< * } * ``` * @param injectQueryFn - A function that returns query options. - * @param options - Additional configuration + * @param options - Additional configuration. * @returns The query result. * @see https://tanstack.com/query/latest/docs/framework/angular/guides/queries */ @@ -221,6 +122,8 @@ export function injectQuery( ) { !options?.injector && assertInInjectionContext(injectQuery) return runInInjectionContext(options?.injector ?? inject(Injector), () => - createBaseQuery(injectQueryFn, QueryObserver), + createBaseQuery(injectQueryFn, QueryObserver, methodsToExclude), ) as unknown as CreateQueryResult } + +const methodsToExclude: Array> = ['refetch'] diff --git a/packages/angular-query-experimental/src/pending-tasks-compat.ts b/packages/angular-query-experimental/src/pending-tasks-compat.ts deleted file mode 100644 index e156996993e..00000000000 --- a/packages/angular-query-experimental/src/pending-tasks-compat.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { InjectionToken, inject } from '@angular/core' -import * as ng from '@angular/core' -import { noop } from '@tanstack/query-core' - -type PendingTasksCompat = { add: () => PendingTaskRef } - -export type PendingTaskRef = () => void - -export const PENDING_TASKS = new InjectionToken( - 'PENDING_TASKS', - { - factory: (): PendingTasksCompat => { - // Access via Reflect so bundlers stay quiet when the token is absent (Angular < 19). - const token = Reflect.get(ng, 'PendingTasks') as unknown as - | Parameters[0] - | undefined - - const svc: PendingTasksCompat | null = token - ? (inject(token, { optional: true }) as PendingTasksCompat | null) - : null - - // Without PendingTasks we fall back to a stable no-op shim. - return { - add: svc ? () => svc.add() : () => noop, - } - }, - }, -) diff --git a/packages/angular-query-experimental/src/providers.ts b/packages/angular-query-experimental/src/providers.ts index 076d76d0c34..40a9899c782 100644 --- a/packages/angular-query-experimental/src/providers.ts +++ b/packages/angular-query-experimental/src/providers.ts @@ -1,6 +1,63 @@ -import { DestroyRef, InjectionToken, inject } from '@angular/core' -import { QueryClient } from '@tanstack/query-core' -import type { Provider } from '@angular/core' +import { isPlatformBrowser, isPlatformServer } from '@angular/common' +import { + DOCUMENT, + DestroyRef, + ENVIRONMENT_INITIALIZER, + InjectionToken, + PLATFORM_ID, + TransferState, + inject, + makeEnvironmentProviders, + makeStateKey, +} from '@angular/core' +import { + QueryClient, + dehydrate, + hydrate, + type DehydratedState, +} from '@tanstack/query-core' +import { INTERNAL_TANSTACK_QUERY_HYDRATION_TRANSFER_KEY } from './hydration-state-key' +import type { EnvironmentProviders, Provider } from '@angular/core' + +const INTERNAL_QUERY_CLIENT_SHOULD_HYDRATE = new InjectionToken('', { + providedIn: 'root', + factory: () => true, +}) + +function configureQueryClient() { + const queryClient = inject(QueryClient) + const destroyRef = inject(DestroyRef) + const platformId = inject(PLATFORM_ID) + const shouldHydrate = inject(INTERNAL_QUERY_CLIENT_SHOULD_HYDRATE) + const hydrationStateKey = inject(INTERNAL_TANSTACK_QUERY_HYDRATION_TRANSFER_KEY) + + if (inject(DOCUMENT, { optional: true })) { + const transferState = inject(TransferState) + + if (shouldHydrate && isPlatformServer(platformId)) { + transferState.onSerialize(hydrationStateKey, () => dehydrate(queryClient)) + } else if (shouldHydrate && isPlatformBrowser(platformId)) { + const dehydratedState = transferState.get(hydrationStateKey, null) + if (dehydratedState) { + hydrate(queryClient, dehydratedState) + transferState.remove(hydrationStateKey) + } + } + } + + queryClient.mount() + destroyRef.onDestroy(() => queryClient.unmount()) +} + +function createQueryClientProviders( + queryClient: QueryClient | InjectionToken, +): Array { + return [ + queryClient instanceof InjectionToken + ? { provide: QueryClient, useExisting: queryClient } + : { provide: QueryClient, useValue: queryClient }, + ] +} /** * Usually {@link provideTanStackQuery} is used once to set up TanStack Query and the @@ -8,25 +65,21 @@ import type { Provider } from '@angular/core' * for the entire application. Internally it calls `provideQueryClient`. * You can use `provideQueryClient` to provide a different `QueryClient` instance for a part * of the application or for unit testing purposes. + * * @param queryClient - A `QueryClient` instance, or an `InjectionToken` which provides a `QueryClient`. - * @returns a provider object that can be used to provide the `QueryClient` instance. + * @returns A single {@link EnvironmentProviders} value to add to environment `providers` (do not spread). */ export function provideQueryClient( queryClient: QueryClient | InjectionToken, -): Provider { - return { - provide: QueryClient, - useFactory: () => { - const client = - queryClient instanceof InjectionToken - ? inject(queryClient) - : queryClient - // Unmount the query client on injector destroy - inject(DestroyRef).onDestroy(() => client.unmount()) - client.mount() - return client +): EnvironmentProviders { + return makeEnvironmentProviders([ + ...createQueryClientProviders(queryClient), + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: configureQueryClient, }, - } + ]) } /** @@ -66,20 +119,14 @@ export function provideQueryClient( * * You can also enable optional developer tools by adding `withDevtools`. By * default the tools will then be loaded when your app is in development mode. + * * ```ts - * import { - * provideTanStackQuery, - * withDevtools - * QueryClient, - * } from '@tanstack/angular-query-experimental' + * import { provideTanStackQuery, QueryClient } from '@tanstack/angular-query-experimental' + * import { withDevtools } from '@tanstack/angular-query-devtools' * - * bootstrapApplication(AppComponent, - * { - * providers: [ - * provideTanStackQuery(new QueryClient(), withDevtools()) - * ] - * } - * ) + * bootstrapApplication(AppComponent, { + * providers: [provideTanStackQuery(new QueryClient(), withDevtools())], + * }) * ``` * * **Example: using an InjectionToken** @@ -98,18 +145,24 @@ export function provideQueryClient( * Note that this is a small optimization and for most applications it's preferable to provide the `QueryClient` in the main application config. * @param queryClient - A `QueryClient` instance, or an `InjectionToken` which provides a `QueryClient`. * @param features - Optional features to configure additional Query functionality. - * @returns A set of providers to set up TanStack Query. + * @returns A single {@link EnvironmentProviders} value (do not spread into `providers`). * @see https://tanstack.com/query/v5/docs/framework/angular/quick-start - * @see withDevtools + * @see https://tanstack.com/query/v5/docs/framework/angular/devtools + * @see https://tanstack.com/query/latest/docs/framework/angular/guides/ssr */ export function provideTanStackQuery( queryClient: QueryClient | InjectionToken, ...features: Array -): Array { - return [ - provideQueryClient(queryClient), - features.map((feature) => feature.ɵproviders), - ] +): EnvironmentProviders { + return makeEnvironmentProviders([ + ...createQueryClientProviders(queryClient), + ...features.map((feature) => feature.ɵproviders), + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: configureQueryClient, + }, + ]) } /** @@ -121,11 +174,13 @@ export function provideTanStackQuery( * @see https://tanstack.com/query/v5/docs/framework/angular/quick-start * @deprecated Use `provideTanStackQuery` instead. */ -export function provideAngularQuery(queryClient: QueryClient): Array { +export function provideAngularQuery( + queryClient: QueryClient, +): EnvironmentProviders { return provideTanStackQuery(queryClient) } -const queryFeatures = ['Devtools', 'PersistQueryClient'] as const +const queryFeatures = ['Devtools', 'Hydration', 'PersistQueryClient'] as const type QueryFeatureKind = (typeof queryFeatures)[number] @@ -134,7 +189,7 @@ type QueryFeatureKind = (typeof queryFeatures)[number] */ export interface QueryFeature { ɵkind: TFeatureKind - ɵproviders: Array + ɵproviders: Array | EnvironmentProviders } /** @@ -145,7 +200,7 @@ export interface QueryFeature { */ export function queryFeature( kind: TFeatureKind, - providers: Array, + providers: Array | EnvironmentProviders, ): QueryFeature { return { ɵkind: kind, ɵproviders: providers } } @@ -163,6 +218,46 @@ export type DevtoolsFeature = QueryFeature<'Devtools'> */ export type PersistQueryClientFeature = QueryFeature<'PersistQueryClient'> +/** + * Sets a non-default serialization key for this injector's `QueryClient` cache (server dehydrate / + * browser hydrate via `TransferState`). Use this when you have multiple `QueryClient` instances + * so each has its own key. The default key applies when you do not add this feature. + * + * ```ts + * providers: [ + * provideTanStackQuery(secondaryClient, withHydrationKey('my-secondary-query-cache')), + * ] + * ``` + * + * @param key - A unique string for this client's `TransferState` entry. + */ +export function withHydrationKey(key: string): QueryFeature<'Hydration'> { + return queryFeature( + 'Hydration', + makeEnvironmentProviders([ + { + provide: INTERNAL_TANSTACK_QUERY_HYDRATION_TRANSFER_KEY, + useValue: makeStateKey(key), + }, + ]), + ) +} + +/** + * Disables `TransferState` hydration and dehydration for the current environment injector. + */ +export function withNoQueryHydration(): QueryFeature<'Hydration'> { + return queryFeature( + 'Hydration', + makeEnvironmentProviders([ + { + provide: INTERNAL_QUERY_CLIENT_SHOULD_HYDRATE, + useValue: false, + }, + ]), + ) +} + /** * A type alias that represents all Query features available for use with `provideTanStackQuery`. * Features can be enabled by adding special functions to the `provideTanStackQuery` call. @@ -170,4 +265,7 @@ export type PersistQueryClientFeature = QueryFeature<'PersistQueryClient'> * documentation on how to use those functions. * @see {@link provideTanStackQuery} */ -export type QueryFeatures = DevtoolsFeature | PersistQueryClientFeature +export type QueryFeatures = + | DevtoolsFeature + | QueryFeature<'Hydration'> + | PersistQueryClientFeature diff --git a/packages/angular-query-experimental/src/query-options.ts b/packages/angular-query-experimental/src/query-options.ts index 069472b9032..b8252602b89 100644 --- a/packages/angular-query-experimental/src/query-options.ts +++ b/packages/angular-query-experimental/src/query-options.ts @@ -4,7 +4,6 @@ import type { InitialDataFunction, NonUndefinedGuard, OmitKeyof, - QueryFunction, QueryKey, SkipToken, } from '@tanstack/query-core' @@ -42,37 +41,12 @@ export type DefinedInitialDataOptions< TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> = Omit< - CreateQueryOptions, - 'queryFn' -> & { +> = CreateQueryOptions & { initialData: | NonUndefinedGuard | (() => NonUndefinedGuard) - queryFn?: QueryFunction } -/** - * Allows to share and re-use query options in a type-safe way. - * - * The `queryKey` will be tagged with the type from `queryFn`. - * - * **Example** - * - * ```ts - * const { queryKey } = queryOptions({ - * queryKey: ['key'], - * queryFn: () => Promise.resolve(5), - * // ^? Promise - * }) - * - * const queryClient = new QueryClient() - * const data = queryClient.getQueryData(queryKey) - * // ^? number | undefined - * ``` - * @param options - The query options to tag with the type from `queryFn`. - * @returns The tagged query options. - */ export function queryOptions< TQueryFnData = unknown, TError = DefaultError, @@ -84,27 +58,6 @@ export function queryOptions< queryKey: DataTag } -/** - * Allows to share and re-use query options in a type-safe way. - * - * The `queryKey` will be tagged with the type from `queryFn`. - * - * **Example** - * - * ```ts - * const { queryKey } = queryOptions({ - * queryKey: ['key'], - * queryFn: () => Promise.resolve(5), - * // ^? Promise - * }) - * - * const queryClient = new QueryClient() - * const data = queryClient.getQueryData(queryKey) - * // ^? number | undefined - * ``` - * @param options - The query options to tag with the type from `queryFn`. - * @returns The tagged query options. - */ export function queryOptions< TQueryFnData = unknown, TError = DefaultError, @@ -116,27 +69,6 @@ export function queryOptions< queryKey: DataTag } -/** - * Allows to share and re-use query options in a type-safe way. - * - * The `queryKey` will be tagged with the type from `queryFn`. - * - * **Example** - * - * ```ts - * const { queryKey } = queryOptions({ - * queryKey: ['key'], - * queryFn: () => Promise.resolve(5), - * // ^? Promise - * }) - * - * const queryClient = new QueryClient() - * const data = queryClient.getQueryData(queryKey) - * // ^? number | undefined - * ``` - * @param options - The query options to tag with the type from `queryFn`. - * @returns The tagged query options. - */ export function queryOptions< TQueryFnData = unknown, TError = DefaultError, diff --git a/packages/angular-query-experimental/src/signal-proxy.ts b/packages/angular-query-experimental/src/signal-proxy.ts index e2a9de345f6..079222eb70e 100644 --- a/packages/angular-query-experimental/src/signal-proxy.ts +++ b/packages/angular-query-experimental/src/signal-proxy.ts @@ -1,46 +1,61 @@ import { computed, untracked } from '@angular/core' import type { Signal } from '@angular/core' -export type MapToSignals = { - [K in keyof T]: T[K] extends Function ? T[K] : Signal +export type MethodKeys = { + [K in keyof T]: T[K] extends (...args: Array) => any ? K : never +}[keyof T] + +export type MapToSignals = never> = { + [K in keyof T]: K extends TExcludeFields ? T[K] : Signal } /** * Exposes fields of an object passed via an Angular `Signal` as `Computed` signals. * Functions on the object are passed through as-is. * @param inputSignal - `Signal` that must return an object. + * @param excludeFields - Array of function property names that should NOT be converted to signals. * @returns A proxy object with the same fields as the input object, but with each field wrapped in a `Computed` signal. */ -export function signalProxy>( - inputSignal: Signal, -) { - const internalState = {} as MapToSignals +export function signalProxy< + TInput extends Record, + const TExcludeFields extends ReadonlyArray> = [], +>(inputSignal: Signal, excludeFields: TExcludeFields) { + const internalState = {} as MapToSignals + const excludeFieldsArray = excludeFields as ReadonlyArray - return new Proxy>(internalState, { - get(target, prop) { - // first check if we have it in our internal state and return it - const computedField = target[prop] - if (computedField) return computedField + return new Proxy>( + internalState, + { + get(target, prop) { + // first check if we have it in our internal state and return it + const computedField = target[prop] + if (computedField) return computedField - // then, check if it's a function on the resultState and return it - const targetField = untracked(inputSignal)[prop] - if (typeof targetField === 'function') return targetField + // if it is an excluded function, return it without tracking + if (excludeFieldsArray.includes(prop as string)) { + const fn = (...args: Parameters) => + untracked(inputSignal)[prop](...args) + // @ts-expect-error + target[prop] = fn + return fn + } - // finally, create a computed field, store it and return it - // @ts-expect-error - return (target[prop] = computed(() => inputSignal()[prop])) - }, - has(_, prop) { - return !!untracked(inputSignal)[prop] - }, - ownKeys() { - return Reflect.ownKeys(untracked(inputSignal)) - }, - getOwnPropertyDescriptor() { - return { - enumerable: true, - configurable: true, - } + // otherwise, make a computed field + // @ts-expect-error + return (target[prop] = computed(() => inputSignal()[prop])) + }, + has(_, prop) { + return prop in untracked(inputSignal) + }, + ownKeys() { + return Reflect.ownKeys(untracked(inputSignal)) + }, + getOwnPropertyDescriptor() { + return { + enumerable: true, + configurable: true, + } + }, }, - }) + ) } diff --git a/packages/angular-query-experimental/src/types.ts b/packages/angular-query-experimental/src/types.ts index d71bec248f7..5c36a6bb27a 100644 --- a/packages/angular-query-experimental/src/types.ts +++ b/packages/angular-query-experimental/src/types.ts @@ -16,31 +16,25 @@ import type { QueryObserverResult, } from '@tanstack/query-core' import type { Signal } from '@angular/core' -import type { MapToSignals } from './signal-proxy' +import type { MapToSignals, MethodKeys } from './signal-proxy' -export interface CreateBaseQueryOptions< +export type CreateBaseQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> extends QueryObserverOptions< - TQueryFnData, - TError, - TData, - TQueryData, - TQueryKey -> {} +> = QueryObserverOptions -export interface CreateQueryOptions< +export type CreateQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> extends OmitKeyof< +> = OmitKeyof< CreateBaseQueryOptions, 'suspense' -> {} +> type CreateStatusBasedQueryResult< TStatus extends QueryObserverResult['status'], @@ -94,7 +88,10 @@ export type CreateBaseQueryResult< TError = DefaultError, TState = QueryObserverResult, > = BaseQueryNarrowing & - MapToSignals> + MapToSignals< + OmitKeyof, + MethodKeys> + > export type CreateQueryResult< TData = unknown, @@ -106,13 +103,19 @@ export type DefinedCreateQueryResult< TError = DefaultError, TState = DefinedQueryObserverResult, > = BaseQueryNarrowing & - MapToSignals> + MapToSignals< + OmitKeyof, + MethodKeys> + > export type CreateInfiniteQueryResult< TData = unknown, TError = DefaultError, > = BaseQueryNarrowing & - MapToSignals> + MapToSignals< + InfiniteQueryObserverResult, + MethodKeys> + > export type DefinedCreateInfiniteQueryResult< TData = unknown, @@ -121,7 +124,10 @@ export type DefinedCreateInfiniteQueryResult< TData, TError >, -> = MapToSignals +> = MapToSignals< + TDefinedInfiniteQueryObserver, + MethodKeys +> export interface CreateMutationOptions< TData = unknown, @@ -270,4 +276,7 @@ export type CreateMutationResult< TOnMutateResult >, > = BaseMutationNarrowing & - MapToSignals> + MapToSignals< + OmitKeyof, + MethodKeys> + > diff --git a/packages/angular-query-experimental/test-setup.ts b/packages/angular-query-experimental/test-setup.ts index cb8519a824c..eb69f3f05a9 100644 --- a/packages/angular-query-experimental/test-setup.ts +++ b/packages/angular-query-experimental/test-setup.ts @@ -1,4 +1,5 @@ import '@testing-library/jest-dom/vitest' +import '@angular/compiler' import { getTestBed } from '@angular/core/testing' import { BrowserTestingModule, diff --git a/packages/angular-query-experimental/tsconfig.spec.json b/packages/angular-query-experimental/tsconfig.spec.json new file mode 100644 index 00000000000..12a44f02612 --- /dev/null +++ b/packages/angular-query-experimental/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["vitest/globals", "node"], + "noEmit": false, + "emitDeclarationOnly": false, + "declaration": false, + "declarationMap": false, + "composite": false, + "target": "ES2022" + }, + "files": ["test-setup.ts"], + "include": [ + "src/**/*.test.ts", + "../query-test-utils/src/**/*.ts" + ], + "exclude": ["src/**/*.test-d.ts"] +} diff --git a/packages/angular-query-experimental/vite.config.ts b/packages/angular-query-experimental/vite.config.ts index a4d58a77a5a..ddacb9d7187 100644 --- a/packages/angular-query-experimental/vite.config.ts +++ b/packages/angular-query-experimental/vite.config.ts @@ -1,8 +1,7 @@ -import { defineConfig, mergeConfig } from 'vitest/config' +import { defineConfig, mergeConfig } from 'vite' import { externalizeDeps } from 'vite-plugin-externalize-deps' import tsconfigPaths from 'vite-tsconfig-paths' import dts from 'vite-plugin-dts' -import packageJson from './package.json' import type { Options } from '@tanstack/vite-config' function ensureImportFileExtension({ @@ -31,29 +30,6 @@ const config = defineConfig({ resolve: { conditions: ['@tanstack/custom-condition'], }, - environments: { - ssr: { - resolve: { - conditions: ['@tanstack/custom-condition'], - }, - }, - }, - test: { - name: packageJson.name, - dir: './src', - watch: false, - environment: 'jsdom', - setupFiles: ['test-setup.ts'], - coverage: { - enabled: true, - provider: 'istanbul', - include: ['src/**/*'], - exclude: ['src/__tests__/**'], - }, - typecheck: { enabled: true }, - globals: true, - restoreMocks: true, - }, }) // copy from @tanstack/config/vite with changes: diff --git a/packages/angular-query-experimental/vitest.config.ts b/packages/angular-query-experimental/vitest.config.ts new file mode 100644 index 00000000000..f5d27c1a0c7 --- /dev/null +++ b/packages/angular-query-experimental/vitest.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from 'vitest/config' +import angular from '@analogjs/vite-plugin-angular' +import packageJson from './package.json' with { type: 'json' } + +export default defineConfig({ + esbuild: { + target: 'es2022', + }, + plugins: [ + angular({ + tsconfig: './tsconfig.spec.json', + // Vitest sets VITEST; Analog defaults jit: true, which skips ngtsc transforms for + // signal inputs so inputBinding() fails (NG0315). jit: false needs compiler-cli on + // TypeScript 5.9+ to avoid a TS 5.8 bind crash (root pnpm override). + jit: false, + }), + ], + test: { + name: packageJson.name, + dir: './src/__tests__', + watch: false, + environment: 'jsdom', + setupFiles: ['./test-setup.ts'], + coverage: { + enabled: true, + provider: 'istanbul', + include: ['src/**/*'], + exclude: ['src/__tests__/**'], + }, + include: ['**/*.{test,spec}.{ts,mts,cts,tsx,js,mjs,cjs,jsx}'], + globals: true, + restoreMocks: true, + }, +}) diff --git a/packages/angular-query-persist-client/package.json b/packages/angular-query-persist-client/package.json index 64fcd8a497e..f80c6386e43 100644 --- a/packages/angular-query-persist-client/package.json +++ b/packages/angular-query-persist-client/package.json @@ -20,7 +20,6 @@ "compile": "tsc --build", "test:eslint": "eslint --concurrency=auto ./src", "test:types": "npm-run-all --serial test:types:*", - "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build", "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build", "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build", "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build", @@ -56,21 +55,27 @@ "@tanstack/query-persist-client-core": "workspace:*" }, "devDependencies": { - "@angular/animations": "^20.0.0", + "@analogjs/vite-plugin-angular": "^2.3.1", + "@analogjs/vitest-angular": "^2.3.1", + "@angular/build": "^20.0.0", "@angular/common": "^20.0.0", "@angular/compiler": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", "@angular/core": "^20.0.0", "@angular/platform-browser": "^20.0.0", + "@angular/router": "^20.0.0", "@tanstack/angular-query-experimental": "workspace:*", "@tanstack/query-test-utils": "workspace:*", - "@testing-library/angular": "^18.0.0", + "@testing-library/angular": "^18.1.1", "@testing-library/dom": "^10.4.0", "eslint-plugin-jsdoc": "^50.5.0", - "npm-run-all2": "^5.0.0" + "npm-run-all2": "^5.0.0", + "typescript": "5.8.3", + "zone.js": "^0.16.0" }, "peerDependencies": { - "@angular/common": ">=16.0.0", - "@angular/core": ">=16.0.0", + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", "@tanstack/angular-query-experimental": "workspace:^" } } diff --git a/packages/angular-query-persist-client/src/__tests__/with-persist-query-client.test.ts b/packages/angular-query-persist-client/src/__tests__/with-persist-query-client.test.ts index 0ff84e82258..5de53473ca3 100644 --- a/packages/angular-query-persist-client/src/__tests__/with-persist-query-client.test.ts +++ b/packages/angular-query-persist-client/src/__tests__/with-persist-query-client.test.ts @@ -1,15 +1,22 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { + injectIsRestoring, QueryClient, + injectQueries, injectQuery, provideTanStackQuery, } from '@tanstack/angular-query-experimental' import { persistQueryClientSave } from '@tanstack/query-persist-client-core' import { Component, + EnvironmentInjector, + PLATFORM_ID, + InjectionToken, + createEnvironmentInjector, effect, provideZonelessChangeDetection, } from '@angular/core' +import { TestBed } from '@angular/core/testing' import { render } from '@testing-library/angular' import { queryKey, sleep } from '@tanstack/query-test-utils' import { withPersistQueryClient } from '../with-persist-query-client' @@ -17,6 +24,7 @@ import type { PersistedClient, Persister, } from '@tanstack/query-persist-client-core' +import * as persistClientCore from '@tanstack/query-persist-client-core' beforeEach(() => { vi.useFakeTimers() @@ -146,9 +154,115 @@ describe('withPersistQueryClient', () => { }) }) - test.todo( - '(Once injectQueries is functional) verify that injectQueries transitions to an idle state', - ) + test('restores cache for injectQueries and keeps it idle while restoring', async () => { + const key1 = queryKey() + const key2 = queryKey() + const states: Array< + Array<{ + status: string + fetchStatus: string + data: string | undefined + }> + > = [] + + const queryClient = new QueryClient() + queryClient.prefetchQuery({ + queryKey: key1, + queryFn: () => sleep(10).then(() => 'hydrated-1'), + }) + queryClient.prefetchQuery({ + queryKey: key2, + queryFn: () => sleep(10).then(() => 'hydrated-2'), + }) + await vi.advanceTimersByTimeAsync(10) + + const persister = createMockPersister() + persistQueryClientSave({ queryClient, persister }) + await vi.advanceTimersByTimeAsync(0) + queryClient.clear() + + @Component({ + template: ` +
+

{{ formattedData() }}

+

fetchStatus: {{ formattedFetchStatus() }}

+
+ `, + }) + class Page { + state = injectQueries(() => ({ + queries: [ + { + queryKey: key1, + queryFn: () => sleep(10).then(() => 'fetched-1'), + }, + { + queryKey: key2, + queryFn: () => sleep(10).then(() => 'fetched-2'), + }, + ], + })) + + _ = effect(() => { + states.push( + this.state().map((query) => ({ + status: query.status(), + fetchStatus: query.fetchStatus(), + data: query.data(), + })), + ) + }) + + formattedData() { + return this.state() + .map((query) => query.data() ?? 'null') + .join(',') + } + + formattedFetchStatus() { + return this.state() + .map((query) => query.fetchStatus()) + .join(',') + } + } + + const rendered = await render(Page, { + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + queryClient, + withPersistQueryClient({ persistOptions: { persister } }), + ), + ], + }) + + expect(rendered.getByText('fetchStatus: idle,idle')).toBeInTheDocument() + + await vi.advanceTimersByTimeAsync(10) + rendered.fixture.detectChanges() + expect(rendered.getByText('hydrated-1,hydrated-2')).toBeInTheDocument() + expect( + rendered.getByText('fetchStatus: fetching,fetching'), + ).toBeInTheDocument() + + await vi.advanceTimersByTimeAsync(11) + rendered.fixture.detectChanges() + expect(rendered.getByText('fetched-1,fetched-2')).toBeInTheDocument() + expect(rendered.getByText('fetchStatus: idle,idle')).toBeInTheDocument() + + expect(states[0]).toMatchObject([ + { status: 'pending', fetchStatus: 'idle', data: undefined }, + { status: 'pending', fetchStatus: 'idle', data: undefined }, + ]) + expect(states).toContainEqual([ + { status: 'success', fetchStatus: 'fetching', data: 'hydrated-1' }, + { status: 'success', fetchStatus: 'fetching', data: 'hydrated-2' }, + ]) + expect(states[states.length - 1]).toMatchObject([ + { status: 'success', fetchStatus: 'idle', data: 'fetched-1' }, + { status: 'success', fetchStatus: 'idle', data: 'fetched-2' }, + ]) + }) test('should show initialData while restoring', async () => { const key = queryKey() @@ -377,6 +491,75 @@ describe('withPersistQueryClient', () => { expect(rendered.getByText('fetched')).toBeInTheDocument() }) + test('should await onSuccess before refetching or subscribing', async () => { + const key = queryKey() + const queryClient = new QueryClient() + queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => sleep(10).then(() => 'hydrated'), + }) + await vi.advanceTimersByTimeAsync(10) + + const persister = createMockPersister() + persistQueryClientSave({ queryClient, persister }) + await vi.advanceTimersByTimeAsync(0) + + queryClient.clear() + + const fetchSpy = vi.fn(() => sleep(10).then(() => 'fetched')) + const onSuccess = vi.fn(async () => { + await sleep(20) + }) + + @Component({ + template: ` +
+

{{ state.data() ?? 'null' }}

+

fetchStatus: {{ state.fetchStatus() }}

+
+ `, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: fetchSpy, + })) + } + + const rendered = await render(Page, { + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + queryClient, + withPersistQueryClient({ + persistOptions: { persister }, + onSuccess, + }), + ), + ], + }) + + await vi.advanceTimersByTimeAsync(10) + rendered.fixture.detectChanges() + expect(onSuccess).toHaveBeenCalledTimes(1) + expect(fetchSpy).toHaveBeenCalledTimes(0) + expect(rendered.getByText('fetchStatus: idle')).toBeInTheDocument() + + await vi.advanceTimersByTimeAsync(19) + rendered.fixture.detectChanges() + expect(fetchSpy).toHaveBeenCalledTimes(0) + + await vi.advanceTimersByTimeAsync(1) + rendered.fixture.detectChanges() + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(rendered.getByText('hydrated')).toBeInTheDocument() + expect(rendered.getByText('fetchStatus: fetching')).toBeInTheDocument() + + await vi.advanceTimersByTimeAsync(11) + rendered.fixture.detectChanges() + expect(rendered.getByText('fetched')).toBeInTheDocument() + }) + test('should remove cache after non-successful restoring', async () => { const key = queryKey() const onErrorMock = vi @@ -431,4 +614,187 @@ describe('withPersistQueryClient', () => { expect(onErrorMock).toHaveBeenNthCalledWith(1, error) onErrorMock.mockRestore() }) + + test('should await onError before starting queries after restore failure', async () => { + const key = queryKey() + const onErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined) + const queryClient = new QueryClient() + const removeClient = vi.fn() + const [, persister] = createMockErrorPersister(removeClient) + const fetchSpy = vi.fn(() => sleep(10).then(() => 'fetched')) + const onError = vi.fn(async () => { + await sleep(20) + }) + + @Component({ + template: ` +
+

{{ state.data() ?? 'null' }}

+

fetchStatus: {{ state.fetchStatus() }}

+
+ `, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: fetchSpy, + })) + } + + const rendered = await render(Page, { + providers: [ + provideZonelessChangeDetection(), + provideTanStackQuery( + queryClient, + withPersistQueryClient({ + persistOptions: { persister }, + onError, + }), + ), + ], + }) + + await vi.advanceTimersByTimeAsync(10) + rendered.fixture.detectChanges() + expect(onError).toHaveBeenCalledTimes(1) + expect(fetchSpy).toHaveBeenCalledTimes(0) + expect(rendered.getByText('fetchStatus: idle')).toBeInTheDocument() + + await vi.advanceTimersByTimeAsync(19) + rendered.fixture.detectChanges() + expect(fetchSpy).toHaveBeenCalledTimes(0) + + await vi.advanceTimersByTimeAsync(1) + rendered.fixture.detectChanges() + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(rendered.getByText('fetchStatus: fetching')).toBeInTheDocument() + + await vi.advanceTimersByTimeAsync(11) + rendered.fixture.detectChanges() + expect(rendered.getByText('fetched')).toBeInTheDocument() + + onErrorMock.mockRestore() + }) + + test('factory form with deps receives injected token and restores cache', async () => { + const key = queryKey() + const holder = { persister: createMockPersister() } + const HOLDER = new InjectionToken<{ persister: Persister }>( + 'persist-test-holder', + ) + + const queryClient = new QueryClient() + queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => sleep(10).then(() => 'hydrated'), + }) + await vi.advanceTimersByTimeAsync(10) + + persistQueryClientSave({ queryClient, persister: holder.persister }) + await vi.advanceTimersByTimeAsync(0) + + queryClient.clear() + + @Component({ + template: ` +
+

{{ state.data() }}

+
+ `, + }) + class Page { + state = injectQuery(() => ({ + queryKey: key, + queryFn: () => sleep(10).then(() => 'fetched'), + })) + } + + const rendered = await render(Page, { + providers: [ + provideZonelessChangeDetection(), + { provide: HOLDER, useValue: holder }, + provideTanStackQuery( + queryClient, + withPersistQueryClient( + (h) => ({ + persistOptions: { persister: h.persister }, + }), + { deps: [HOLDER] }, + ), + ), + ], + }) + + await vi.advanceTimersByTimeAsync(10) + rendered.fixture.detectChanges() + expect(rendered.getByText('hydrated')).toBeInTheDocument() + }) + + test('factory callback runs only in browser mode', async () => { + const factory = vi.fn(() => ({ + persistOptions: { + persister: createMockPersister(), + }, + })) + + @Component({ + template: `{{ isRestoring() }}`, + }) + class Page { + isRestoring = injectIsRestoring() + } + + const rendered = await render(Page, { + providers: [ + provideZonelessChangeDetection(), + { provide: PLATFORM_ID, useValue: 'server' }, + provideTanStackQuery(new QueryClient(), withPersistQueryClient(factory)), + ], + }) + + rendered.fixture.detectChanges() + + expect(factory).not.toHaveBeenCalled() + expect(rendered.fixture.nativeElement.textContent.trim()).toBe('false') + }) + + test('cleanup subscription runs on injector destroy', async () => { + const key = queryKey() + const queryClient = new QueryClient() + const persister = createMockPersister() + const cleanup = vi.fn() + const subscribeSpy = vi + .spyOn(persistClientCore, 'persistQueryClientSubscribe') + .mockReturnValue(cleanup) + + queryClient.prefetchQuery({ + queryKey: key, + queryFn: () => sleep(10).then(() => 'hydrated'), + }) + await vi.advanceTimersByTimeAsync(10) + persistQueryClientSave({ queryClient, persister }) + await vi.advanceTimersByTimeAsync(0) + queryClient.clear() + TestBed.configureTestingModule({ + providers: [provideZonelessChangeDetection()], + }) + + const injector = createEnvironmentInjector( + [ + provideTanStackQuery( + queryClient, + withPersistQueryClient({ persistOptions: { persister } }), + ), + ], + TestBed.inject(EnvironmentInjector), + ) + + await vi.advanceTimersByTimeAsync(10) + injector.destroy() + + expect(subscribeSpy).toHaveBeenCalledTimes(1) + expect(cleanup).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/angular-query-persist-client/src/with-persist-query-client.ts b/packages/angular-query-persist-client/src/with-persist-query-client.ts index 43f562450a1..8da04e552c7 100644 --- a/packages/angular-query-persist-client/src/with-persist-query-client.ts +++ b/packages/angular-query-persist-client/src/with-persist-query-client.ts @@ -8,6 +8,7 @@ import { ENVIRONMENT_INITIALIZER, PLATFORM_ID, inject, + makeEnvironmentProviders, signal, } from '@angular/core' import { isPlatformBrowser } from '@angular/common' @@ -15,74 +16,117 @@ import { persistQueryClientRestore, persistQueryClientSubscribe, } from '@tanstack/query-persist-client-core' -import type { PersistQueryClientOptions as PersistQueryClientOptionsCore } from '@tanstack/query-persist-client-core' import type { PersistQueryClientFeature } from '@tanstack/angular-query-experimental' +import type { + PersistQueryClientUserOptions, + WithPersistQueryClientFn, + WithPersistQueryClientOptions, +} from './with-persist-query-client.types' -type PersistQueryClientOptions = { - persistOptions: Omit - onSuccess?: () => Promise | unknown - onError?: () => Promise | unknown +export type { + PersistQueryClientUserOptions, + WithPersistQueryClientFn, + WithPersistQueryClientOptions, +} from './with-persist-query-client.types' + +function resolvePersistOptions( + input: PersistQueryClientUserOptions | WithPersistQueryClientFn, + withOptions: WithPersistQueryClientOptions | undefined, + injectDep: (token: any) => T, +): PersistQueryClientUserOptions { + if (typeof input === 'function') { + const deps = withOptions?.deps ?? [] + const depValues = deps.map((token) => injectDep(token)) + return input(...depValues) + } + return input } /** * Enables persistence. * - * **Example** + * **Example (static options)** - avoid browser-only globals at module scope when the same config + * runs on the server; prefer the factory form below for `localStorage`. * * ```ts - * const localStoragePersister = createAsyncStoragePersister({ - * storage: window.localStorage, + * withPersistQueryClient({ + * persistOptions: { persister }, + * onSuccess: () => console.log('Restored.'), * }) + * ``` + * + * **Example (factory, browser only, optional deps)** - the callback only runs in the browser, so + * it can safely reference browser APIs such as `localStorage`. * - * export const appConfig: ApplicationConfig = { - * providers: [ - * provideTanStackQuery( - * new QueryClient(), - * withPersistQueryClient({ - * persistOptions: { - * persister: localStoragePersister, - * }, - * onSuccess: () => console.log('Restoration completed successfully.'), - * }) - * ), - * ], - * }; + * ```ts + * withPersistQueryClient(() => ({ + * persistOptions: { + * persister: createAsyncStoragePersister({ storage: localStorage }), + * }, + * })) * ``` - * @param persistQueryClientOptions - persistence options and optional onSuccess and onError callbacks which get called when the restoration process is complete. + * + * ```ts + * withPersistQueryClient( + * (storage: StorageService) => ({ + * persistOptions: { persister: storage.createPersister() }, + * }), + * { deps: [StorageService] }, + * ) + * ``` + * @param factoryOrOptions - Either a callback (runs only in the browser) or a static options object. + * @param withOptions - When using a callback, optional `deps` passed as arguments (like `useFactory`). * @returns A set of providers for use with `provideTanStackQuery`. * @public */ export function withPersistQueryClient( - persistQueryClientOptions: PersistQueryClientOptions, + factoryOrOptions: WithPersistQueryClientFn, + withOptions?: WithPersistQueryClientOptions, +): PersistQueryClientFeature +export function withPersistQueryClient( + options: PersistQueryClientUserOptions, +): PersistQueryClientFeature +export function withPersistQueryClient( + factoryOrOptions: PersistQueryClientUserOptions | WithPersistQueryClientFn, + withOptions?: WithPersistQueryClientOptions, ): PersistQueryClientFeature { const isRestoring = signal(true) - const providers = [ - provideIsRestoring(isRestoring.asReadonly()), - { - // Do not use provideEnvironmentInitializer while Angular < v19 is supported - provide: ENVIRONMENT_INITIALIZER, - multi: true, - useValue: () => { - if (!isPlatformBrowser(inject(PLATFORM_ID))) return - const destroyRef = inject(DestroyRef) - const queryClient = inject(QueryClient) - - const { onSuccess, onError, persistOptions } = persistQueryClientOptions - const options = { queryClient, ...persistOptions } - persistQueryClientRestore(options) - .then(() => { - onSuccess?.() - }) - .catch(() => { - onError?.() - }) - .finally(() => { + return queryFeature( + 'PersistQueryClient', + makeEnvironmentProviders([ + provideIsRestoring(isRestoring.asReadonly()), + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: () => { + if (!isPlatformBrowser(inject(PLATFORM_ID))) { isRestoring.set(false) - const cleanup = persistQueryClientSubscribe(options) - destroyRef.onDestroy(cleanup) - }) + return + } + const destroyRef = inject(DestroyRef) + const queryClient = inject(QueryClient) + + const { onSuccess, onError, persistOptions } = resolvePersistOptions( + factoryOrOptions, + withOptions, + inject, + ) + const options = { queryClient, ...persistOptions } + void persistQueryClientRestore(options) + .then(() => { + return onSuccess?.() + }) + .catch(() => { + return onError?.() + }) + .finally(() => { + if (destroyRef.destroyed) return + isRestoring.set(false) + const cleanup = persistQueryClientSubscribe(options) + destroyRef.onDestroy(cleanup) + }) + }, }, - }, - ] - return queryFeature('PersistQueryClient', providers) + ]), + ) } diff --git a/packages/angular-query-persist-client/src/with-persist-query-client.types.ts b/packages/angular-query-persist-client/src/with-persist-query-client.types.ts new file mode 100644 index 00000000000..5cd0ce2d628 --- /dev/null +++ b/packages/angular-query-persist-client/src/with-persist-query-client.types.ts @@ -0,0 +1,15 @@ +import type { PersistQueryClientOptions as PersistQueryClientOptionsCore } from '@tanstack/query-persist-client-core' + +export type PersistQueryClientUserOptions = { + persistOptions: Omit + onSuccess?: () => Promise | unknown + onError?: () => Promise | unknown +} + +export interface WithPersistQueryClientOptions { + deps?: Array +} + +export type WithPersistQueryClientFn = ( + ...deps: Array +) => PersistQueryClientUserOptions diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 322b0ed55aa..8dac1f409c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,8 @@ overrides: vite: ^6.4.1 esbuild: ^0.27.2 +packageExtensionsChecksum: sha256-6Gca75Use15IPQigUOHANEkX1ZKRK6Fq52dIKaPsg08= + importers: .: @@ -168,6 +170,9 @@ importers: '@angular/platform-browser': specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -183,16 +188,16 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 examples/angular/basic: dependencies: @@ -208,6 +213,9 @@ importers: '@angular/platform-browser': specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -223,16 +231,16 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 examples/angular/basic-persister: dependencies: @@ -248,6 +256,9 @@ importers: '@angular/platform-browser': specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -269,16 +280,16 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 examples/angular/devtools-panel: dependencies: @@ -297,6 +308,9 @@ importers: '@angular/router': specifier: ^20.0.0 version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -312,16 +326,59 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 + + examples/angular/dynamic-devtools: + dependencies: + '@angular/common': + specifier: ^20.0.0 + version: 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^20.0.0 + version: 20.3.18 + '@angular/core': + specifier: ^20.0.0 + version: 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0) + '@angular/platform-browser': + specifier: ^20.0.0 + version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools + '@tanstack/angular-query-experimental': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-experimental + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + zone.js: + specifier: 0.16.0 + version: 0.16.0 + devDependencies: + '@angular/build': + specifier: ^20.0.0 + version: 20.3.22(de9e232eae1e2140ae7dcbb8ed95ad66) + '@angular/cli': + specifier: ^20.0.0 + version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) + '@angular/compiler-cli': + specifier: ^20.0.0 + version: 20.3.18(@angular/compiler@20.3.18) + typescript: + specifier: 5.9.3 + version: 5.9.3 examples/angular/infinite-query-with-max-pages: dependencies: @@ -337,6 +394,9 @@ importers: '@angular/platform-browser': specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -352,16 +412,16 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 examples/angular/optimistic-updates: dependencies: @@ -380,6 +440,9 @@ importers: '@angular/platform-browser': specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -395,16 +458,16 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 examples/angular/pagination: dependencies: @@ -420,6 +483,9 @@ importers: '@angular/platform-browser': specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -435,16 +501,16 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 examples/angular/query-options-from-a-service: dependencies: @@ -463,6 +529,9 @@ importers: '@angular/router': specifier: ^20.0.0 version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -478,16 +547,16 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 examples/angular/router: dependencies: @@ -506,6 +575,9 @@ importers: '@angular/router': specifier: ^20.0.0 version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -521,16 +593,16 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 examples/angular/rxjs: dependencies: @@ -549,6 +621,9 @@ importers: '@angular/platform-browser': specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -564,16 +639,16 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 examples/angular/simple: dependencies: @@ -589,6 +664,9 @@ importers: '@angular/platform-browser': specifier: ^20.0.0 version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools '@tanstack/angular-query-experimental': specifier: ^5.97.0 version: link:../../../packages/angular-query-experimental @@ -604,16 +682,132 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(587c5808af39ff95d3fbed740d6c24e4) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: - specifier: 5.8.3 - version: 5.8.3 + specifier: 5.9.3 + version: 5.9.3 + + examples/angular/ssr: + dependencies: + '@angular/common': + specifier: ^20.0.0 + version: 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^20.0.0 + version: 20.3.18 + '@angular/core': + specifier: ^20.0.0 + version: 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': + specifier: ^20.0.0 + version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-server': + specifier: ^20.0.0 + version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/ssr': + specifier: ^20.0.0 + version: 20.3.23(2a5c89866ef71e03da2f0de649d610c6) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools + '@tanstack/angular-query-experimental': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-experimental + express: + specifier: ^5.1.0 + version: 5.2.1 + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + zone.js: + specifier: 0.16.1 + version: 0.16.1 + devDependencies: + '@angular/build': + specifier: ^20.0.0 + version: 20.3.22(1d334df1acb359ebe3039e59524b7757) + '@angular/cli': + specifier: ^20.0.0 + version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) + '@angular/compiler-cli': + specifier: ^20.0.0 + version: 20.3.18(@angular/compiler@20.3.18) + '@types/express': + specifier: ^5.0.1 + version: 5.0.6 + typescript: + specifier: 5.9.3 + version: 5.9.3 + + examples/angular/ssr-persist: + dependencies: + '@angular/common': + specifier: ^20.0.0 + version: 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^20.0.0 + version: 20.3.18 + '@angular/core': + specifier: ^20.0.0 + version: 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': + specifier: ^20.0.0 + version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-server': + specifier: ^20.0.0 + version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/ssr': + specifier: ^20.0.0 + version: 20.3.23(2a5c89866ef71e03da2f0de649d610c6) + '@tanstack/angular-query-devtools': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-devtools + '@tanstack/angular-query-experimental': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-experimental + '@tanstack/angular-query-persist-client': + specifier: ^5.97.0 + version: link:../../../packages/angular-query-persist-client + '@tanstack/query-async-storage-persister': + specifier: ^5.97.0 + version: link:../../../packages/query-async-storage-persister + express: + specifier: ^5.1.0 + version: 5.2.1 + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + zone.js: + specifier: 0.16.1 + version: 0.16.1 + devDependencies: + '@angular/build': + specifier: ^20.0.0 + version: 20.3.22(1d334df1acb359ebe3039e59524b7757) + '@angular/cli': + specifier: ^20.0.0 + version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) + '@angular/compiler-cli': + specifier: ^20.0.0 + version: 20.3.18(@angular/compiler@20.3.18) + '@types/express': + specifier: ^5.0.1 + version: 5.0.6 + typescript: + specifier: 5.9.3 + version: 5.9.3 examples/preact/simple: dependencies: @@ -2087,13 +2281,13 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(5ed3c60e1eec6696797b7d138989d984) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) '@angular/compiler-cli': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + version: 20.3.18(@angular/compiler@20.3.18) typescript: specifier: ~5.8.2 version: 5.8.3 @@ -2341,45 +2535,131 @@ importers: specifier: ^2.2.8 version: 2.2.12(typescript@5.8.3) + packages/angular-query-devtools: + dependencies: + '@tanstack/query-core': + specifier: workspace:* + version: link:../query-core + '@tanstack/query-devtools': + specifier: workspace:* + version: link:../query-devtools + devDependencies: + '@analogjs/vite-plugin-angular': + specifier: ^2.3.1 + version: 2.4.5(@angular/build@20.3.22(4ba73e8bfe4bd751c32c30d172460bda)) + '@analogjs/vitest-angular': + specifier: ^2.3.1 + version: 2.4.5(@analogjs/vite-plugin-angular@2.4.5(@angular/build@20.3.22(4ba73e8bfe4bd751c32c30d172460bda)))(@angular-devkit/architect@0.2003.22(chokidar@4.0.3))(@angular-devkit/schematics@20.3.22(chokidar@4.0.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(zone.js@0.16.1) + '@angular/build': + specifier: ^20.0.0 + version: 20.3.22(4ba73e8bfe4bd751c32c30d172460bda) + '@angular/common': + specifier: ^20.0.0 + version: 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^20.0.0 + version: 20.3.18 + '@angular/compiler-cli': + specifier: ^20.0.0 + version: 20.3.18(@angular/compiler@20.3.18) + '@angular/core': + specifier: ^20.0.0 + version: 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': + specifier: ^20.0.0 + version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + '@tanstack/angular-query-experimental': + specifier: workspace:* + version: link:../angular-query-experimental + '@testing-library/jest-dom': + specifier: ^6.8.0 + version: 6.9.1 + npm-run-all2: + specifier: ^5.0.0 + version: 5.0.2 + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + typescript: + specifier: 5.8.3 + version: 5.8.3 + vite-plugin-dts: + specifier: 4.2.3 + version: 4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite-plugin-externalize-deps: + specifier: ^0.9.0 + version: 0.9.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + zone.js: + specifier: ^0.16.0 + version: 0.16.1 + publishDirectory: dist + packages/angular-query-experimental: dependencies: '@tanstack/query-core': specifier: workspace:* version: link:../query-core devDependencies: + '@analogjs/vite-plugin-angular': + specifier: ^2.3.1 + version: 2.4.5(@angular/build@20.3.22(4ba73e8bfe4bd751c32c30d172460bda)) + '@analogjs/vitest-angular': + specifier: ^2.3.1 + version: 2.4.5(@analogjs/vite-plugin-angular@2.4.5(@angular/build@20.3.22(4ba73e8bfe4bd751c32c30d172460bda)))(@angular-devkit/architect@0.2003.22(chokidar@4.0.3))(@angular-devkit/schematics@20.3.22(chokidar@4.0.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(zone.js@0.16.1) + '@angular/build': + specifier: ^20.0.0 + version: 20.3.22(4ba73e8bfe4bd751c32c30d172460bda) '@angular/common': specifier: ^20.0.0 - version: 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + version: 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) '@angular/compiler': specifier: ^20.0.0 version: 20.3.18 + '@angular/compiler-cli': + specifier: ^20.0.0 + version: 20.3.18(@angular/compiler@20.3.18) '@angular/core': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) + version: 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) '@angular/platform-browser': specifier: ^20.0.0 - version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-server': + specifier: ^20.0.0 + version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/router': + specifier: ^20.0.0 + version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) '@tanstack/query-test-utils': specifier: workspace:* version: link:../query-test-utils '@testing-library/angular': - specifier: ^18.0.0 - version: 18.1.1(3436010aec00f8e828978cdcb451dc36) + specifier: ^18.1.1 + version: 18.1.1(ccac552f84520fbac2fdb1dd18566519) npm-run-all2: specifier: ^5.0.0 version: 5.0.2 rxjs: specifier: ^7.8.2 version: 7.8.2 + typescript: + specifier: 5.8.3 + version: 5.8.3 vite-plugin-dts: specifier: 4.2.3 - version: 4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) vite-plugin-externalize-deps: specifier: ^0.9.0 version: 0.9.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.4(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + zone.js: + specifier: ^0.16.0 + version: 0.16.1 optionalDependencies: '@tanstack/query-devtools': specifier: workspace:* @@ -2392,21 +2672,33 @@ importers: specifier: workspace:* version: link:../query-persist-client-core devDependencies: - '@angular/animations': + '@analogjs/vite-plugin-angular': + specifier: ^2.3.1 + version: 2.4.5(@angular/build@20.3.22(4ba73e8bfe4bd751c32c30d172460bda)) + '@analogjs/vitest-angular': + specifier: ^2.3.1 + version: 2.4.5(@analogjs/vite-plugin-angular@2.4.5(@angular/build@20.3.22(4ba73e8bfe4bd751c32c30d172460bda)))(@angular-devkit/architect@0.2003.22(chokidar@4.0.3))(@angular-devkit/schematics@20.3.22(chokidar@4.0.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(zone.js@0.16.1) + '@angular/build': specifier: ^20.0.0 - version: 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + version: 20.3.22(4ba73e8bfe4bd751c32c30d172460bda) '@angular/common': specifier: ^20.0.0 - version: 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + version: 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) '@angular/compiler': specifier: ^20.0.0 version: 20.3.18 + '@angular/compiler-cli': + specifier: ^20.0.0 + version: 20.3.18(@angular/compiler@20.3.18) '@angular/core': specifier: ^20.0.0 - version: 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) + version: 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) '@angular/platform-browser': specifier: ^20.0.0 - version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + version: 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/router': + specifier: ^20.0.0 + version: 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) '@tanstack/angular-query-experimental': specifier: workspace:* version: link:../angular-query-experimental @@ -2414,8 +2706,8 @@ importers: specifier: workspace:* version: link:../query-test-utils '@testing-library/angular': - specifier: ^18.0.0 - version: 18.1.1(3436010aec00f8e828978cdcb451dc36) + specifier: ^18.1.1 + version: 18.1.1(ccac552f84520fbac2fdb1dd18566519) '@testing-library/dom': specifier: ^10.4.0 version: 10.4.1 @@ -2425,6 +2717,12 @@ importers: npm-run-all2: specifier: ^5.0.0 version: 5.0.2 + typescript: + specifier: 5.8.3 + version: 5.8.3 + zone.js: + specifier: ^0.16.0 + version: 0.16.1 packages/eslint-plugin-query: dependencies: @@ -3150,6 +3448,29 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@analogjs/vite-plugin-angular@2.4.5': + resolution: {integrity: sha512-nrDV7vqbclBuACykxO5H1TBuG9G1GAT/IW7I8if1oGDHyiNNhjSs/lhxTomJXrgYeJ4SXWBnx+x+8ctb/3tpFg==} + peerDependencies: + '@angular-devkit/build-angular': ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 + '@angular/build': ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 + peerDependenciesMeta: + '@angular-devkit/build-angular': + optional: true + '@angular/build': + optional: true + + '@analogjs/vitest-angular@2.4.5': + resolution: {integrity: sha512-FtLDeaHnxdv0pUwZMZJfeTYpROqhFaf1gjktbkHGCy3hGG/ngcxO4SJhgEI1OAkkhVi3eQUCfPFJhveGvMCHcg==} + peerDependencies: + '@analogjs/vite-plugin-angular': '*' + '@angular-devkit/architect': '>=0.1500.0 < 0.2200.0' + '@angular-devkit/schematics': '>=17.0.0' + vitest: ^1.3.1 || ^2.0.0 || ^3.0.0 || ^4.0.0 + zone.js: '>=0.14.0' + peerDependenciesMeta: + zone.js: + optional: true + '@andrewbranch/untar.js@1.0.3': resolution: {integrity: sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==} @@ -3240,10 +3561,6 @@ packages: hasBin: true peerDependencies: '@angular/compiler': 20.3.18 - typescript: '>=5.8 <6.0' - peerDependenciesMeta: - typescript: - optional: true '@angular/compiler@20.3.18': resolution: {integrity: sha512-AaP/LCiDNcYmF135EEozjyR04NRBT38ZfBHQwjhgwiBBTejmvcpHwJaHSkraLpZqZzE4BQqqmgiQ1EJqxEwLVA==} @@ -3282,6 +3599,16 @@ packages: '@angular/animations': optional: true + '@angular/platform-server@20.3.18': + resolution: {integrity: sha512-iw4QSmEWEKbyMT5u8QdhalNiPqRc7cRuo6lulU75pjXqVLwLb3Gq8it+Vo+LSKY6qI/bMO7olS7iyw09wXJ0OQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@angular/common': 20.3.18 + '@angular/compiler': 20.3.18 + '@angular/core': 20.3.18 + '@angular/platform-browser': 20.3.18 + rxjs: ^6.5.3 || ^7.4.0 + '@angular/router@20.3.18': resolution: {integrity: sha512-3CWejsEYr+ze+ktvWN/qHdyq5WLrj96QZpGYJyxh1pchIcpMPE9MmLpdjf0CUrWYB7g/85u0Geq/xsz72JrGng==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -3291,6 +3618,17 @@ packages: '@angular/platform-browser': 20.3.18 rxjs: ^6.5.3 || ^7.4.0 + '@angular/ssr@20.3.23': + resolution: {integrity: sha512-wrxOeZSwnYYpN6IGWgTfHYWx4Dspf9KwiIX7E4nC9g1EUOh1Vw7gaHISuZCf/aUNmFS5Yod4949qVxJdCb1WZQ==} + peerDependencies: + '@angular/common': ^20.0.0 + '@angular/core': ^20.0.0 + '@angular/platform-server': ^20.0.0 + '@angular/router': ^20.0.0 + peerDependenciesMeta: + '@angular/platform-server': + optional: true + '@arethetypeswrong/cli@0.15.4': resolution: {integrity: sha512-YDbImAi1MGkouT7f2yAECpUMFhhA1J0EaXzIqoC5GGtK0xDgauLtcsZezm8tNq7d3wOFXH7OnY+IORYcG212rw==} engines: {node: '>=18'} @@ -7163,6 +7501,9 @@ packages: vitest: optional: true + '@ts-morph/common@0.22.0': + resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==} + '@tsconfig/svelte@5.0.8': resolution: {integrity: sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==} @@ -7198,12 +7539,18 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/braces@3.0.5': resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -7225,6 +7572,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -7240,6 +7593,9 @@ packages: '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -7276,6 +7632,12 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -7287,6 +7649,12 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/sort-by@1.2.3': resolution: {integrity: sha512-Q8Pg7o2iHWFf7pR4jIGb+ntxwwL7a/WWLFNJj8jEN14tPQdfwZLCqK68q6mo1WONqa68OysEPuFvNA3uGm0crw==} @@ -8910,6 +9278,9 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + code-block-writer@12.0.0: + resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==} + collection-visit@1.0.0: resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} engines: {node: '>=0.10.0'} @@ -12381,6 +12752,11 @@ packages: engines: {node: '>=10'} hasBin: true + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -15063,6 +15439,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-morph@21.0.1: + resolution: {integrity: sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==} + ts-pattern@5.9.0: resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} @@ -16148,6 +16527,10 @@ packages: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} + xhr2@0.2.1: + resolution: {integrity: sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==} + engines: {node: '>= 6'} + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -16303,6 +16686,12 @@ packages: zone.js@0.15.0: resolution: {integrity: sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==} + zone.js@0.16.0: + resolution: {integrity: sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==} + + zone.js@0.16.1: + resolution: {integrity: sha512-dpvY17vxYIW3+bNrP0ClUlaiY0CiIRK3tnoLaGoQsQcY9/I/NpzIWQ7tQNhbV7LacQMpCII6wVzuL3tuWOyfuA==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -16423,6 +16812,22 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@analogjs/vite-plugin-angular@2.4.5(@angular/build@20.3.22(4ba73e8bfe4bd751c32c30d172460bda))': + dependencies: + tinyglobby: 0.2.15 + ts-morph: 21.0.1 + optionalDependencies: + '@angular/build': 20.3.22(4ba73e8bfe4bd751c32c30d172460bda) + + '@analogjs/vitest-angular@2.4.5(@analogjs/vite-plugin-angular@2.4.5(@angular/build@20.3.22(4ba73e8bfe4bd751c32c30d172460bda)))(@angular-devkit/architect@0.2003.22(chokidar@4.0.3))(@angular-devkit/schematics@20.3.22(chokidar@4.0.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(zone.js@0.16.1)': + dependencies: + '@analogjs/vite-plugin-angular': 2.4.5(@angular/build@20.3.22(4ba73e8bfe4bd751c32c30d172460bda)) + '@angular-devkit/architect': 0.2003.22(chokidar@4.0.3) + '@angular-devkit/schematics': 20.3.22(chokidar@4.0.3) + vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + optionalDependencies: + zone.js: 0.16.1 + '@andrewbranch/untar.js@1.0.3': {} '@angular-devkit/architect@0.2003.22(chokidar@4.0.3)': @@ -16457,13 +16862,188 @@ snapshots: dependencies: '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) tslib: 2.8.1 + optional: true + + '@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))': + dependencies: + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0) + tslib: 2.8.1 + optional: true + + '@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))': + dependencies: + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + tslib: 2.8.1 + optional: true - '@angular/build@20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3)': + '@angular/build@20.3.22(1d334df1acb359ebe3039e59524b7757)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2003.22(chokidar@4.0.3) '@angular/compiler': 20.3.18 - '@angular/compiler-cli': 20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3) + '@angular/compiler-cli': 20.3.18(@angular/compiler@20.3.18) + '@babel/core': 7.28.3 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-split-export-declaration': 7.24.7 + '@inquirer/confirm': 5.1.14(@types/node@22.19.15) + '@vitejs/plugin-basic-ssl': 2.1.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + beasties: 0.3.5 + browserslist: 4.28.2 + esbuild: 0.27.4 + https-proxy-agent: 7.0.6 + istanbul-lib-instrument: 6.0.3 + jsonc-parser: 3.3.1 + listr2: 9.0.1 + magic-string: 0.30.17 + mrmime: 2.0.1 + parse5-html-rewriting-stream: 8.0.0 + picomatch: 4.0.4 + piscina: 5.1.3 + rollup: 4.59.0 + sass: 1.90.0 + semver: 7.7.2 + source-map-support: 0.5.21 + tinyglobby: 0.2.14 + tslib: 2.8.1 + typescript: 5.9.3 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + watchpack: 2.4.4 + optionalDependencies: + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-server': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/ssr': 20.3.23(2a5c89866ef71e03da2f0de649d610c6) + lmdb: 3.4.2 + postcss: 8.5.8 + tailwindcss: 4.2.2 + vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - '@types/node' + - chokidar + - jiti + - lightningcss + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + '@angular/build@20.3.22(4ba73e8bfe4bd751c32c30d172460bda)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular-devkit/architect': 0.2003.22(chokidar@4.0.3) + '@angular/compiler': 20.3.18 + '@angular/compiler-cli': 20.3.18(@angular/compiler@20.3.18) + '@babel/core': 7.28.3 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-split-export-declaration': 7.24.7 + '@inquirer/confirm': 5.1.14(@types/node@22.19.15) + '@vitejs/plugin-basic-ssl': 2.1.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + beasties: 0.3.5 + browserslist: 4.28.2 + esbuild: 0.27.4 + https-proxy-agent: 7.0.6 + istanbul-lib-instrument: 6.0.3 + jsonc-parser: 3.3.1 + listr2: 9.0.1 + magic-string: 0.30.17 + mrmime: 2.0.1 + parse5-html-rewriting-stream: 8.0.0 + picomatch: 4.0.4 + piscina: 5.1.3 + rollup: 4.59.0 + sass: 1.90.0 + semver: 7.7.2 + source-map-support: 0.5.21 + tinyglobby: 0.2.14 + tslib: 2.8.1 + typescript: 5.8.3 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + watchpack: 2.4.4 + optionalDependencies: + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-server': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/ssr': 20.3.23(2a5c89866ef71e03da2f0de649d610c6) + lmdb: 3.4.2 + postcss: 8.5.8 + tailwindcss: 4.2.2 + vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - '@types/node' + - chokidar + - jiti + - lightningcss + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + '@angular/build@20.3.22(587c5808af39ff95d3fbed740d6c24e4)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular-devkit/architect': 0.2003.22(chokidar@4.0.3) + '@angular/compiler': 20.3.18 + '@angular/compiler-cli': 20.3.18(@angular/compiler@20.3.18) + '@babel/core': 7.28.3 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-split-export-declaration': 7.24.7 + '@inquirer/confirm': 5.1.14(@types/node@22.19.15) + '@vitejs/plugin-basic-ssl': 2.1.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + beasties: 0.3.5 + browserslist: 4.28.2 + esbuild: 0.27.4 + https-proxy-agent: 7.0.6 + istanbul-lib-instrument: 6.0.3 + jsonc-parser: 3.3.1 + listr2: 9.0.1 + magic-string: 0.30.17 + mrmime: 2.0.1 + parse5-html-rewriting-stream: 8.0.0 + picomatch: 4.0.4 + piscina: 5.1.3 + rollup: 4.59.0 + sass: 1.90.0 + semver: 7.7.2 + source-map-support: 0.5.21 + tinyglobby: 0.2.14 + tslib: 2.8.1 + typescript: 5.9.3 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + watchpack: 2.4.4 + optionalDependencies: + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-server': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@angular/ssr': 20.3.23(3db094c414ea1e8cf7366033a9880aad) + lmdb: 3.4.2 + postcss: 8.5.8 + tailwindcss: 4.2.2 + vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - '@types/node' + - chokidar + - jiti + - lightningcss + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + '@angular/build@20.3.22(5ed3c60e1eec6696797b7d138989d984)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular-devkit/architect': 0.2003.22(chokidar@4.0.3) + '@angular/compiler': 20.3.18 + '@angular/compiler-cli': 20.3.18(@angular/compiler@20.3.18) '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 @@ -16493,6 +17073,62 @@ snapshots: optionalDependencies: '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-server': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@angular/ssr': 20.3.23(3db094c414ea1e8cf7366033a9880aad) + lmdb: 3.4.2 + postcss: 8.5.8 + tailwindcss: 4.2.2 + vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - '@types/node' + - chokidar + - jiti + - lightningcss + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + '@angular/build@20.3.22(de9e232eae1e2140ae7dcbb8ed95ad66)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular-devkit/architect': 0.2003.22(chokidar@4.0.3) + '@angular/compiler': 20.3.18 + '@angular/compiler-cli': 20.3.18(@angular/compiler@20.3.18) + '@babel/core': 7.28.3 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-split-export-declaration': 7.24.7 + '@inquirer/confirm': 5.1.14(@types/node@22.19.15) + '@vitejs/plugin-basic-ssl': 2.1.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + beasties: 0.3.5 + browserslist: 4.28.2 + esbuild: 0.27.4 + https-proxy-agent: 7.0.6 + istanbul-lib-instrument: 6.0.3 + jsonc-parser: 3.3.1 + listr2: 9.0.1 + magic-string: 0.30.17 + mrmime: 2.0.1 + parse5-html-rewriting-stream: 8.0.0 + picomatch: 4.0.4 + piscina: 5.1.3 + rollup: 4.59.0 + sass: 1.90.0 + semver: 7.7.2 + source-map-support: 0.5.21 + tinyglobby: 0.2.14 + tslib: 2.8.1 + typescript: 5.9.3 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + watchpack: 2.4.4 + optionalDependencies: + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)) + '@angular/platform-server': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2) + '@angular/ssr': 20.3.23(2a3a151cfe80a56f8b006c3bea53d033) lmdb: 3.4.2 postcss: 8.5.8 tailwindcss: 4.2.2 @@ -16542,7 +17178,19 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - '@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3)': + '@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2)': + dependencies: + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2)': + dependencies: + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)': dependencies: '@angular/compiler': 20.3.18 '@babel/core': 7.28.3 @@ -16554,7 +17202,7 @@ snapshots: tslib: 2.8.1 yargs: 18.0.0 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -16570,6 +17218,22 @@ snapshots: '@angular/compiler': 20.3.18 zone.js: 0.15.0 + '@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)': + dependencies: + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + '@angular/compiler': 20.3.18 + zone.js: 0.16.0 + + '@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)': + dependencies: + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + '@angular/compiler': 20.3.18 + zone.js: 0.16.1 + '@angular/forms@20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': dependencies: '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) @@ -16586,6 +17250,54 @@ snapshots: optionalDependencies: '@angular/animations': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))': + dependencies: + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0) + tslib: 2.8.1 + optionalDependencies: + '@angular/animations': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)) + + '@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))': + dependencies: + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + tslib: 2.8.1 + optionalDependencies: + '@angular/animations': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + + '@angular/platform-server@20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/compiler': 20.3.18 + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + rxjs: 7.8.2 + tslib: 2.8.1 + xhr2: 0.2.1 + optional: true + + '@angular/platform-server@20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/compiler': 20.3.18 + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)) + rxjs: 7.8.2 + tslib: 2.8.1 + xhr2: 0.2.1 + optional: true + + '@angular/platform-server@20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/compiler': 20.3.18 + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + rxjs: 7.8.2 + tslib: 2.8.1 + xhr2: 0.2.1 + '@angular/router@20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': dependencies: '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) @@ -16594,6 +17306,52 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 + '@angular/router@20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)) + rxjs: 7.8.2 + tslib: 2.8.1 + optional: true + + '@angular/router@20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/ssr@20.3.23(2a3a151cfe80a56f8b006c3bea53d033)': + dependencies: + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2) + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0) + '@angular/router': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@angular/platform-server': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.0)))(rxjs@7.8.2) + optional: true + + '@angular/ssr@20.3.23(2a5c89866ef71e03da2f0de649d610c6)': + dependencies: + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/router': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@angular/platform-server': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + + '@angular/ssr@20.3.23(3db094c414ea1e8cf7366033a9880aad)': + dependencies: + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/router': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@angular/platform-server': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + optional: true + '@arethetypeswrong/cli@0.15.4': dependencies: '@arethetypeswrong/core': 0.15.1 @@ -20977,12 +21735,12 @@ snapshots: - supports-color - typescript - '@testing-library/angular@18.1.1(3436010aec00f8e828978cdcb451dc36)': + '@testing-library/angular@18.1.1(ccac552f84520fbac2fdb1dd18566519)': dependencies: - '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) - '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) - '@angular/router': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/router': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) '@testing-library/dom': 10.4.1 tslib: 2.8.1 @@ -21063,6 +21821,13 @@ snapshots: vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@ts-morph/common@0.22.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 9.0.9 + mkdirp: 3.0.1 + path-browserify: 1.0.1 + '@tsconfig/svelte@5.0.8': {} '@tufjs/canonical-json@2.0.0': {} @@ -21106,6 +21871,11 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.19.15 + '@types/braces@3.0.5': {} '@types/chai@5.2.3': @@ -21113,6 +21883,10 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.19.15 + '@types/cookie@0.6.0': {} '@types/debug@4.1.13': @@ -21135,6 +21909,19 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 22.19.15 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 22.19.15 @@ -21149,6 +21936,8 @@ snapshots: '@types/html-minifier-terser@6.1.0': {} + '@types/http-errors@2.0.5': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -21190,6 +21979,10 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -21200,6 +21993,15 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/send@1.2.1': + dependencies: + '@types/node': 22.19.15 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.19.15 + '@types/sort-by@1.2.3': {} '@types/source-list-map@0.1.6': {} @@ -21664,6 +22466,15 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 + '@vitest/mocker@4.1.2(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.14(@types/node@22.19.15)(typescript@5.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + '@vitest/mocker@4.1.2(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.2 @@ -21808,7 +22619,7 @@ snapshots: '@vue/devtools-api@6.6.4': {} - '@vue/language-core@2.1.6(typescript@5.9.3)': + '@vue/language-core@2.1.6(typescript@5.8.3)': dependencies: '@volar/language-core': 2.4.28 '@vue/compiler-dom': 3.5.31 @@ -21819,9 +22630,9 @@ snapshots: muggle-string: 0.4.1 path-browserify: 1.0.1 optionalDependencies: - typescript: 5.9.3 + typescript: 5.8.3 - '@vue/language-core@2.1.6(typescript@6.0.1-rc)': + '@vue/language-core@2.1.6(typescript@5.9.3)': dependencies: '@volar/language-core': 2.4.28 '@vue/compiler-dom': 3.5.31 @@ -21832,7 +22643,7 @@ snapshots: muggle-string: 0.4.1 path-browserify: 1.0.1 optionalDependencies: - typescript: 6.0.1-rc + typescript: 5.9.3 '@vue/language-core@2.2.12(typescript@5.8.3)': dependencies: @@ -23420,6 +24231,8 @@ snapshots: cluster-key-slot@1.1.2: {} + code-block-writer@12.0.0: {} + collection-visit@1.0.0: dependencies: map-visit: 1.0.0 @@ -27801,6 +28614,8 @@ snapshots: mkdirp@1.0.4: {} + mkdirp@3.0.1: {} + mlly@1.8.2: dependencies: acorn: 8.16.0 @@ -31162,6 +31977,11 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-morph@21.0.1: + dependencies: + '@ts-morph/common': 0.22.0 + code-block-writer: 12.0.0 + ts-pattern@5.9.0: {} tsconfck@3.1.6(typescript@5.8.3): @@ -31172,10 +31992,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - tsconfck@3.1.6(typescript@6.0.1-rc): - optionalDependencies: - typescript: 6.0.1-rc - tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 @@ -31783,18 +32599,18 @@ snapshots: - xml2js - yaml - vite-plugin-dts@4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-plugin-dts@4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: '@microsoft/api-extractor': 7.47.7(@types/node@22.19.15) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) '@volar/typescript': 2.4.28 - '@vue/language-core': 2.1.6(typescript@5.9.3) + '@vue/language-core': 2.1.6(typescript@5.8.3) compare-versions: 6.1.1 debug: 4.4.3 kolorist: 1.8.0 local-pkg: 0.5.1 magic-string: 0.30.21 - typescript: 5.9.3 + typescript: 5.8.3 optionalDependencies: vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) transitivePeerDependencies: @@ -31802,18 +32618,18 @@ snapshots: - rollup - supports-color - vite-plugin-dts@4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-plugin-dts@4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: '@microsoft/api-extractor': 7.47.7(@types/node@22.19.15) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) '@volar/typescript': 2.4.28 - '@vue/language-core': 2.1.6(typescript@6.0.1-rc) + '@vue/language-core': 2.1.6(typescript@5.9.3) compare-versions: 6.1.1 debug: 4.4.3 kolorist: 1.8.0 local-pkg: 0.5.1 magic-string: 0.30.21 - typescript: 6.0.1-rc + typescript: 5.9.3 optionalDependencies: vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) transitivePeerDependencies: @@ -31869,22 +32685,22 @@ snapshots: stack-trace: 1.0.0-pre2 vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) + tsconfck: 3.1.6(typescript@5.8.3) optionalDependencies: vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 - tsconfck: 3.1.6(typescript@6.0.1-rc) + tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) transitivePeerDependencies: @@ -31933,6 +32749,34 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.15 + jsdom: 27.4.0 + transitivePeerDependencies: + - msw + vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 @@ -32454,6 +33298,8 @@ snapshots: xdg-basedir@5.1.0: {} + xhr2@0.2.1: {} + xml-name-validator@4.0.0: {} xml-name-validator@5.0.0: {} @@ -32598,4 +33444,8 @@ snapshots: zone.js@0.15.0: {} + zone.js@0.16.0: {} + + zone.js@0.16.1: {} + zwitch@2.0.4: {}