diff --git a/docs/config.json b/docs/config.json index 3e77bad407..03bc29afe7 100644 --- a/docs/config.json +++ b/docs/config.json @@ -796,6 +796,10 @@ { "label": "No Unstable Deps", "to": "eslint/no-unstable-deps" + }, + { + "label": "Infinite Query Property Order", + "to": "eslint/infinite-query-property-order" } ] }, diff --git a/docs/eslint/infinite-query-property-order.md b/docs/eslint/infinite-query-property-order.md new file mode 100644 index 0000000000..71c5ebf150 --- /dev/null +++ b/docs/eslint/infinite-query-property-order.md @@ -0,0 +1,63 @@ +--- +id: infinite-query-property-order +title: Ensure correct order of inference sensitive properties for infinite queries +--- + +For the following functions, the property order of the passed in object matters due to type inference: + +- `useInfiniteQuery` +- `useSuspenseInfiniteQuery` +- `infiniteQueryOptions` + +The correct property order is as follows: + +- `queryFn` +- `getPreviousPageParam` +- `getNextPageParam` + +All other properties are insensitive to the order as they do not depend on type inference. + +## Rule Details + +Examples of **incorrect** code for this rule: + +```tsx +/* eslint "@tanstack/query/infinite-query-property-order": "warn" */ +import { useInfiniteQuery } from '@tanstack/react-query' + +const query = useInfiniteQuery({ + queryKey: ['projects'], + getNextPageParam: (lastPage) => lastPage.nextId ?? undefined, + queryFn: async ({ pageParam }) => { + const response = await fetch(`/api/projects?cursor=${pageParam}`) + return await response.json() + }, + initialPageParam: 0, + getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined, + maxPages: 3, +}) +``` + +Examples of **correct** code for this rule: + +```tsx +/* eslint "@tanstack/query/infinite-query-property-order": "warn" */ +import { useInfiniteQuery } from '@tanstack/react-query' + +const query = useInfiniteQuery({ + queryKey: ['projects'], + queryFn: async ({ pageParam }) => { + const response = await fetch(`/api/projects?cursor=${pageParam}`) + return await response.json() + }, + initialPageParam: 0, + getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined, + getNextPageParam: (lastPage) => lastPage.nextId ?? undefined, + maxPages: 3, +}) +``` + +## Attributes + +- [x] ✅ Recommended +- [x] 🔧 Fixable diff --git a/packages/eslint-plugin-query/package.json b/packages/eslint-plugin-query/package.json index 5413a5f687..a6700935af 100644 --- a/packages/eslint-plugin-query/package.json +++ b/packages/eslint-plugin-query/package.json @@ -54,6 +54,7 @@ }, "devDependencies": { "@typescript-eslint/rule-tester": "^8.3.0", + "combinate": "^1.1.11", "eslint": "^9.9.1" }, "peerDependencies": { diff --git a/packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.rule.test.ts b/packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.rule.test.ts new file mode 100644 index 0000000000..d81acd0ef4 --- /dev/null +++ b/packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.rule.test.ts @@ -0,0 +1,142 @@ +import { RuleTester } from '@typescript-eslint/rule-tester' +import combinate from 'combinate' + +import { + checkedProperties, + infiniteQueryFunctions, +} from '../rules/infinite-query-property-order/constants' +import { + name, + rule, +} from '../rules/infinite-query-property-order/infinite-query-property-order.rule' +import { + generateInterleavedCombinations, + generatePartialCombinations, + generatePermutations, +} from './test-utils' +import type { InfiniteQueryFunctions } from '../rules/infinite-query-property-order/constants' + +const ruleTester = new RuleTester() + +// reduce the number of test cases by only testing a subset of the checked properties + +type CheckedProperties = (typeof checkedProperties)[number] +const orderIndependentProps = ['queryKey', '...foo'] as const +type OrderIndependentProps = (typeof orderIndependentProps)[number] + +interface TestCase { + infiniteQueryFunction: InfiniteQueryFunctions + properties: Array +} + +const validTestMatrix = combinate({ + infiniteQueryFunction: [...infiniteQueryFunctions], + properties: generatePartialCombinations(checkedProperties, 2), +}) + +export function generateInvalidPermutations( + arr: ReadonlyArray, +): Array<{ invalid: Array; valid: Array }> { + const combinations = generatePartialCombinations(arr, 2) + const allPermutations: Array<{ invalid: Array; valid: Array }> = [] + + for (const combination of combinations) { + const permutations = generatePermutations(combination) + // skip the first permutation as it matches the original combination + const invalidPermutations = permutations.slice(1) + allPermutations.push( + ...invalidPermutations.map((p) => ({ invalid: p, valid: combination })), + ) + } + + return allPermutations +} + +const invalidPermutations = generateInvalidPermutations(checkedProperties) + +type Interleaved = CheckedProperties | OrderIndependentProps +const interleavedInvalidPermutations: Array<{ + invalid: Array + valid: Array +}> = [] +for (const invalidPermutation of invalidPermutations) { + const invalid = generateInterleavedCombinations( + invalidPermutation.invalid, + orderIndependentProps, + ) + const valid = generateInterleavedCombinations( + invalidPermutation.valid, + orderIndependentProps, + ) + + for (let i = 0; i < invalid.length; i++) { + interleavedInvalidPermutations.push({ + invalid: invalid[i]!, + valid: valid[i]!, + }) + } +} + +const invalidTestMatrix = combinate({ + infiniteQueryFunction: [...infiniteQueryFunctions], + properties: interleavedInvalidPermutations, +}) + +function getCode({ + infiniteQueryFunction: infiniteQueryFunction, + properties, +}: TestCase) { + function getPropertyCode( + property: CheckedProperties | OrderIndependentProps, + ) { + if (property.startsWith('...')) { + return property + } + switch (property) { + case 'queryKey': + return `queryKey: ['projects']` + case 'queryFn': + return 'queryFn: async ({ pageParam }) => { \n await fetch(`/api/projects?cursor=${pageParam}`) \n return await response.json() \n }' + case 'getPreviousPageParam': + return 'getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined' + case 'getNextPageParam': + return 'getNextPageParam: (lastPage) => lastPage.nextId ?? undefined' + } + + return `${property}: () => null` + } + return ` + import { ${infiniteQueryFunction} } from '@tanstack/react-query' + + ${infiniteQueryFunction}({ + ${properties.map(getPropertyCode).join(',\n ')} + }) + ` +} + +const validTestCases = validTestMatrix.map( + ({ infiniteQueryFunction, properties }) => ({ + name: `should pass when order is correct for ${infiniteQueryFunction} with order: ${properties.join(', ')}`, + code: getCode({ infiniteQueryFunction, properties }), + }), +) + +const invalidTestCases = invalidTestMatrix.map( + ({ infiniteQueryFunction, properties }) => ({ + name: `incorrect property order is detected for ${infiniteQueryFunction} with order: ${properties.invalid.join(', ')}`, + code: getCode({ + infiniteQueryFunction: infiniteQueryFunction, + properties: properties.invalid, + }), + errors: [{ messageId: 'invalidOrder' }], + output: getCode({ + infiniteQueryFunction: infiniteQueryFunction, + properties: properties.valid, + }), + }), +) + +ruleTester.run(name, rule, { + valid: validTestCases, + invalid: invalidTestCases, +}) diff --git a/packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.utils.test.ts b/packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.utils.test.ts new file mode 100644 index 0000000000..6871c6831b --- /dev/null +++ b/packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.utils.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from 'vitest' +import { sortDataByOrder } from '../rules/infinite-query-property-order/infinite-query-property-order.utils' + +describe('create-route-property-order utils', () => { + describe('sortDataByOrder', () => { + const testCases = [ + { + data: [{ key: 'a' }, { key: 'c' }, { key: 'b' }], + orderArray: ['a', 'b', 'c'], + key: 'key', + expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }], + }, + { + data: [{ key: 'b' }, { key: 'a' }, { key: 'c' }], + orderArray: ['a', 'b', 'c'], + key: 'key', + expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }], + }, + { + data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }], + orderArray: ['a', 'b', 'c'], + key: 'key', + expected: null, + }, + { + data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }], + orderArray: ['a', 'b', 'c'], + key: 'key', + expected: null, + }, + { + data: [{ key: 'a' }, { key: 'b' }, { key: 'd' }, { key: 'c' }], + orderArray: ['a', 'b', 'c'], + key: 'key', + expected: null, + }, + { + data: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }], + orderArray: ['a', 'b', 'c'], + key: 'key', + expected: null, + }, + { + data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }], + orderArray: ['a', 'b', 'c'], + key: 'key', + expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }], + }, + ] as const + test.each(testCases)( + '$data $orderArray $key $expected', + ({ data, orderArray, key, expected }) => { + const sortedData = sortDataByOrder(data, orderArray, key) + expect(sortedData).toEqual(expected) + }, + ) + }) +}) diff --git a/packages/eslint-plugin-query/src/__tests__/test-utils.test.ts b/packages/eslint-plugin-query/src/__tests__/test-utils.test.ts new file mode 100644 index 0000000000..5997c342a7 --- /dev/null +++ b/packages/eslint-plugin-query/src/__tests__/test-utils.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test } from 'vitest' +import { + expectArrayEqualIgnoreOrder, + generateInterleavedCombinations, + generatePartialCombinations, + generatePermutations, +} from './test-utils' + +describe('test-utils', () => { + describe('generatePermutations', () => { + const testCases = [ + { + input: ['a', 'b', 'c'], + expected: [ + ['a', 'b', 'c'], + ['a', 'c', 'b'], + ['b', 'a', 'c'], + ['b', 'c', 'a'], + ['c', 'a', 'b'], + ['c', 'b', 'a'], + ], + }, + { + input: ['a', 'b'], + expected: [ + ['a', 'b'], + ['b', 'a'], + ], + }, + { + input: ['a'], + expected: [['a']], + }, + ] + test.each(testCases)('$input $expected', ({ input, expected }) => { + const permutations = generatePermutations(input) + expect(permutations).toEqual(expected) + }) + }) + + describe('generatePartialCombinations', () => { + const testCases = [ + { + input: ['a', 'b', 'c'], + minLength: 2, + expected: [ + ['a', 'b'], + ['a', 'c'], + ['b', 'c'], + ['a', 'b', 'c'], + ], + }, + { + input: ['a', 'b'], + expected: [['a', 'b']], + minLength: 2, + }, + { + input: ['a'], + expected: [], + minLength: 2, + }, + { + input: ['a'], + expected: [['a']], + minLength: 1, + }, + { + input: ['a'], + expected: [[], ['a']], + minLength: 0, + }, + ] + test.each(testCases)( + '$input $minLength $expected', + ({ input, minLength, expected }) => { + const combinations = generatePartialCombinations(input, minLength) + expectArrayEqualIgnoreOrder(combinations, expected) + }, + ) + }) + + describe('generateInterleavedCombinations', () => { + const testCases = [ + { + data: ['a', 'b'], + additional: ['x'], + expected: [ + ['a', 'b'], + ['x', 'a', 'b'], + ['a', 'x', 'b'], + ['a', 'b', 'x'], + ], + }, + ] + test.each(testCases)( + '$input $expected', + ({ data, additional, expected }) => { + const combinations = generateInterleavedCombinations(data, additional) + expectArrayEqualIgnoreOrder(combinations, expected) + }, + ) + }) +}) diff --git a/packages/eslint-plugin-query/src/__tests__/test-utils.ts b/packages/eslint-plugin-query/src/__tests__/test-utils.ts index 5459f60d0c..bfefa6c2b3 100644 --- a/packages/eslint-plugin-query/src/__tests__/test-utils.ts +++ b/packages/eslint-plugin-query/src/__tests__/test-utils.ts @@ -1,5 +1,108 @@ +import { expect } from 'vitest' + export function normalizeIndent(template: TemplateStringsArray) { const codeLines = template[0]?.split('\n') ?? [''] const leftPadding = codeLines[1]?.match(/\s+/)?.[0] ?? '' return codeLines.map((line) => line.slice(leftPadding.length)).join('\n') } + +export function generatePermutations(arr: Array): Array> { + if (arr.length <= 1) { + return [arr] + } + + const result: Array> = [] + for (let i = 0; i < arr.length; i++) { + const rest = arr.slice(0, i).concat(arr.slice(i + 1)) + const restPermutations = generatePermutations(rest) + for (const perm of restPermutations) { + result.push([arr[i]!, ...perm]) + } + } + + return result +} + +export function generatePartialCombinations( + arr: ReadonlyArray, + minLength: number, +): Array> { + const result: Array> = [] + + function backtrack(start: number, current: Array) { + if (current.length > minLength - 1) { + result.push([...current]) + } + for (let i = start; i < arr.length; i++) { + current.push(arr[i]!) + backtrack(i + 1, current) + current.pop() + } + } + backtrack(0, []) + return result +} + +export function expectArrayEqualIgnoreOrder(a: Array, b: Array) { + expect([...a].sort()).toEqual([...b].sort()) +} + +export function generateInterleavedCombinations< + TData, + TAdditional, + TResult extends TData | TAdditional, +>( + data: Array | ReadonlyArray, + additional: Array | ReadonlyArray, +): Array> { + const result: Array> = [] + + function getSubsets(array: Array): Array> { + return array.reduce( + (subsets, value) => { + return subsets.concat(subsets.map((set) => [...set, value])) + }, + [[]] as Array>, + ) + } + + function insertAtPositions( + data: Array, + subset: Array, + ): Array> { + const combinations: Array> = [] + + const recurse = ( + currentData: Array, + currentSubset: Array, + start: number, + ): void => { + if (currentSubset.length === 0) { + combinations.push([...currentData]) + return + } + + for (let i = start; i <= currentData.length; i++) { + const newData = [ + ...currentData.slice(0, i), + currentSubset[0]!, + ...currentData.slice(i), + ] + recurse(newData, currentSubset.slice(1), i + 1) + } + } + + recurse(data, subset, 0) + return combinations + } + + const subsets = getSubsets(additional as Array) + + subsets.forEach((subset) => { + result.push( + ...insertAtPositions(data as Array, subset as Array), + ) + }) + + return result +} diff --git a/packages/eslint-plugin-query/src/index.ts b/packages/eslint-plugin-query/src/index.ts index f70ccf7a88..bd042379ea 100644 --- a/packages/eslint-plugin-query/src/index.ts +++ b/packages/eslint-plugin-query/src/index.ts @@ -29,6 +29,7 @@ Object.assign(plugin.configs, { '@tanstack/query/no-rest-destructuring': 'warn', '@tanstack/query/stable-query-client': 'error', '@tanstack/query/no-unstable-deps': 'error', + '@tanstack/query/infinite-query-property-order': 'error', }, }, 'flat/recommended': [ @@ -41,6 +42,7 @@ Object.assign(plugin.configs, { '@tanstack/query/no-rest-destructuring': 'warn', '@tanstack/query/stable-query-client': 'error', '@tanstack/query/no-unstable-deps': 'error', + '@tanstack/query/infinite-query-property-order': 'error', }, }, ], diff --git a/packages/eslint-plugin-query/src/rules.ts b/packages/eslint-plugin-query/src/rules.ts index 9bc18ae9ea..73a6efdd51 100644 --- a/packages/eslint-plugin-query/src/rules.ts +++ b/packages/eslint-plugin-query/src/rules.ts @@ -2,6 +2,7 @@ import * as exhaustiveDeps from './rules/exhaustive-deps/exhaustive-deps.rule' import * as stableQueryClient from './rules/stable-query-client/stable-query-client.rule' import * as noRestDestructuring from './rules/no-rest-destructuring/no-rest-destructuring.rule' import * as noUnstableDeps from './rules/no-unstable-deps/no-unstable-deps.rule' +import * as infiniteQueryPropertyOrder from './rules/infinite-query-property-order/infinite-query-property-order.rule' import type { ESLintUtils } from '@typescript-eslint/utils' import type { ExtraRuleDocs } from './types' @@ -18,4 +19,5 @@ export const rules: Record< [stableQueryClient.name]: stableQueryClient.rule, [noRestDestructuring.name]: noRestDestructuring.rule, [noUnstableDeps.name]: noUnstableDeps.rule, + [infiniteQueryPropertyOrder.name]: infiniteQueryPropertyOrder.rule, } diff --git a/packages/eslint-plugin-query/src/rules/infinite-query-property-order/constants.ts b/packages/eslint-plugin-query/src/rules/infinite-query-property-order/constants.ts new file mode 100644 index 0000000000..33d74a49e4 --- /dev/null +++ b/packages/eslint-plugin-query/src/rules/infinite-query-property-order/constants.ts @@ -0,0 +1,13 @@ +export const infiniteQueryFunctions = [ + 'infiniteQueryOptions', + 'useInfiniteQuery', + 'useSuspenseInfiniteQuery', +] as const + +export type InfiniteQueryFunctions = (typeof infiniteQueryFunctions)[number] + +export const checkedProperties = [ + 'queryFn', + 'getPreviousPageParam', + 'getNextPageParam', +] as const diff --git a/packages/eslint-plugin-query/src/rules/infinite-query-property-order/infinite-query-property-order.rule.ts b/packages/eslint-plugin-query/src/rules/infinite-query-property-order/infinite-query-property-order.rule.ts new file mode 100644 index 0000000000..f88166bf19 --- /dev/null +++ b/packages/eslint-plugin-query/src/rules/infinite-query-property-order/infinite-query-property-order.rule.ts @@ -0,0 +1,114 @@ +import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils' + +import { getDocsUrl } from '../../utils/get-docs-url' +import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports' +import { sortDataByOrder } from './infinite-query-property-order.utils' +import { checkedProperties, infiniteQueryFunctions } from './constants' +import type { InfiniteQueryFunctions } from './constants' +import type { ExtraRuleDocs } from '../../types' + +const createRule = ESLintUtils.RuleCreator(getDocsUrl) + +const infiniteQueryFunctionsSet = new Set(infiniteQueryFunctions) +function isInfiniteQueryFunction(node: any): node is InfiniteQueryFunctions { + return infiniteQueryFunctionsSet.has(node) +} + +export const name = 'infinite-query-property-order' + +export const rule = createRule({ + name, + meta: { + type: 'problem', + docs: { + description: + 'Ensure correct order of inference sensitive properties for infinite queries', + recommended: 'error', + }, + messages: { + invalidOrder: 'Invalid order of properties for `{{function}}`.', + }, + schema: [], + hasSuggestions: true, + fixable: 'code', + }, + defaultOptions: [], + + create: detectTanstackQueryImports((context) => { + return { + CallExpression(node) { + if (node.callee.type !== AST_NODE_TYPES.Identifier) { + return + } + const infiniteQueryFunction = node.callee.name + if (!isInfiniteQueryFunction(infiniteQueryFunction)) { + return + } + const argument = node.arguments[0] + if (argument === undefined || argument.type !== 'ObjectExpression') { + return + } + + const allProperties = argument.properties + if (allProperties.length < 2) { + return + } + + const properties = allProperties.flatMap((p) => { + if ( + p.type === AST_NODE_TYPES.Property && + p.key.type === AST_NODE_TYPES.Identifier + ) { + return { name: p.key.name, property: p } + } else if (p.type === AST_NODE_TYPES.SpreadElement) { + if (p.argument.type === AST_NODE_TYPES.Identifier) { + return { name: p.argument.name, property: p } + } else { + throw new Error('Unsupported spread element') + } + } + return [] + }) + + const sortedProperties = sortDataByOrder( + properties, + checkedProperties, + 'name', + ) + if (sortedProperties === null) { + return + } + context.report({ + node: argument, + data: { function: node.callee.name }, + messageId: 'invalidOrder', + fix(fixer) { + const sourceCode = context.sourceCode + + const text = sortedProperties.reduce( + (sourceText, specifier, index) => { + let text = '' + if (index < allProperties.length - 1) { + text = sourceCode + .getText() + .slice( + allProperties[index]!.range[1], + allProperties[index + 1]!.range[0], + ) + } + return ( + sourceText + sourceCode.getText(specifier.property) + text + ) + }, + '', + ) + return fixer.replaceTextRange( + [allProperties[0]!.range[0], allProperties.at(-1)!.range[1]], + text, + ) + }, + }) + }, + } + }), +}) diff --git a/packages/eslint-plugin-query/src/rules/infinite-query-property-order/infinite-query-property-order.utils.ts b/packages/eslint-plugin-query/src/rules/infinite-query-property-order/infinite-query-property-order.utils.ts new file mode 100644 index 0000000000..0f7d54a743 --- /dev/null +++ b/packages/eslint-plugin-query/src/rules/infinite-query-property-order/infinite-query-property-order.utils.ts @@ -0,0 +1,38 @@ +export function sortDataByOrder( + data: Array | ReadonlyArray, + orderArray: Array | ReadonlyArray, + key: TKey, +): Array | null { + const orderMap = new Map(orderArray.map((item, index) => [item, index])) + + // Separate items that are in orderArray from those that are not + const inOrderArray = data + .filter((item) => orderMap.has(item[key])) + .sort((a, b) => { + const indexA = orderMap.get(a[key])! + const indexB = orderMap.get(b[key])! + + return indexA - indexB + }) + + const inOrderIterator = inOrderArray.values() + + // `as boolean` is needed to avoid TS incorrectly inferring that wasResorted is always `true` + let wasResorted = false as boolean + + const result = data.map((item) => { + if (orderMap.has(item[key])) { + const sortedItem = inOrderIterator.next().value! + if (sortedItem[key] !== item[key]) { + wasResorted = true + } + return sortedItem + } + return item + }) + + if (!wasResorted) { + return null + } + return result +} diff --git a/packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts b/packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts index d82738d158..35831ef403 100644 --- a/packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts +++ b/packages/eslint-plugin-query/src/rules/no-unstable-deps/no-unstable-deps.rule.ts @@ -24,7 +24,7 @@ export const rule = createRule({ type: 'problem', docs: { description: - 'Disallow putting the result of useMutation directly in a React hook dependency array', + 'Disallow putting the result of query hooks directly in a React hook dependency array', recommended: 'error', }, messages: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed942db507..5c0f980857 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,7 +162,7 @@ importers: devDependencies: '@angular-devkit/build-angular': specifier: ^17.3.8 - version: 17.3.8(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3))(@types/express@4.17.21)(@types/node@22.0.2)(chokidar@3.6.0)(html-webpack-plugin@5.6.0(webpack@5.90.3(esbuild@0.20.1)))(ng-packagr@17.3.0(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3))(tailwindcss@3.4.7)(tslib@2.6.3)(typescript@5.3.3))(tailwindcss@3.4.7)(typescript@5.3.3) + version: 17.3.8(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3))(@types/express@4.17.21)(@types/node@22.0.2)(chokidar@3.6.0)(html-webpack-plugin@5.6.0(webpack@5.90.3(esbuild@0.19.12)))(ng-packagr@17.3.0(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3))(tailwindcss@3.4.7)(tslib@2.6.3)(typescript@5.3.3))(tailwindcss@3.4.7)(typescript@5.3.3) '@angular/cli': specifier: ^17.3.8 version: 17.3.8(chokidar@3.6.0) @@ -1125,7 +1125,7 @@ importers: version: 5.1.0(astro@4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3))(tailwindcss@3.4.7) '@astrojs/vercel': specifier: ^7.7.2 - version: 7.7.2(astro@4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3))(encoding@0.1.13)(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8))(react@18.3.1) + version: 7.7.2(astro@4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3))(encoding@0.1.13)(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) '@tanstack/solid-query': specifier: ^5.55.4 version: link:../../../packages/solid-query @@ -1925,6 +1925,9 @@ importers: '@typescript-eslint/rule-tester': specifier: ^8.3.0 version: 8.3.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.4.2) + combinate: + specifier: ^1.1.11 + version: 1.1.11 eslint: specifier: ^9.9.1 version: 9.9.1(jiti@1.21.6) @@ -7751,6 +7754,9 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combinate@1.1.11: + resolution: {integrity: sha512-+2MNAQ29HtNejOxkgaTQPC2Bm+pQvFuqf7o18uObl/Bx3daX06kjLUNY/qa9f+YSqzqm/ic3SdrlfN0fvTlw2g==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -17038,7 +17044,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-angular@17.3.8(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3))(@types/express@4.17.21)(@types/node@22.0.2)(chokidar@3.6.0)(html-webpack-plugin@5.6.0(webpack@5.90.3(esbuild@0.20.1)))(ng-packagr@17.3.0(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3))(tailwindcss@3.4.7)(tslib@2.6.3)(typescript@5.3.3))(tailwindcss@3.4.7)(typescript@5.3.3)': + '@angular-devkit/build-angular@17.3.8(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3))(@types/express@4.17.21)(@types/node@22.0.2)(chokidar@3.6.0)(html-webpack-plugin@5.6.0(webpack@5.90.3(esbuild@0.19.12)))(ng-packagr@17.3.0(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3))(tailwindcss@3.4.7)(tslib@2.6.3)(typescript@5.3.3))(tailwindcss@3.4.7)(typescript@5.3.3)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1703.8(chokidar@3.6.0) @@ -17104,7 +17110,7 @@ snapshots: webpack-dev-middleware: 6.1.2(webpack@5.90.3(esbuild@0.20.1)) webpack-dev-server: 4.15.1(webpack@5.90.3(esbuild@0.20.1)) webpack-merge: 5.10.0 - webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.0(webpack@5.90.3(esbuild@0.20.1)))(webpack@5.90.3(esbuild@0.20.1)) + webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.0(webpack@5.90.3(esbuild@0.19.12)))(webpack@5.90.3(esbuild@0.20.1)) optionalDependencies: esbuild: 0.20.1 ng-packagr: 17.3.0(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3))(tailwindcss@3.4.7)(tslib@2.6.3)(typescript@5.3.3) @@ -17190,7 +17196,7 @@ snapshots: undici: 6.11.1 vite: 5.1.7(@types/node@22.0.2)(less@4.2.0)(sass@1.71.1)(terser@5.29.1) watchpack: 2.4.0 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) webpack-dev-middleware: 6.1.2(webpack@5.90.3(esbuild@0.20.1)) webpack-dev-server: 4.15.1(webpack@5.90.3(esbuild@0.20.1)) webpack-merge: 5.10.0 @@ -17222,7 +17228,7 @@ snapshots: dependencies: '@angular-devkit/architect': 0.1703.8(chokidar@3.6.0) rxjs: 7.8.1 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) webpack-dev-server: 4.15.1(webpack@5.90.3(esbuild@0.20.1)) transitivePeerDependencies: - chokidar @@ -17462,10 +17468,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/vercel@7.7.2(astro@4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3))(encoding@0.1.13)(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': + '@astrojs/vercel@7.7.2(astro@4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3))(encoding@0.1.13)(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': dependencies: '@astrojs/internal-helpers': 0.4.1 - '@vercel/analytics': 1.3.1(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8))(react@18.3.1) + '@vercel/analytics': 1.3.1(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1) '@vercel/edge': 1.1.2 '@vercel/nft': 0.27.3(encoding@0.1.13) astro: 4.12.3(@types/node@22.0.2)(less@4.2.0)(sass@1.77.8)(terser@5.31.3)(typescript@5.3.3) @@ -20939,7 +20945,7 @@ snapshots: dependencies: '@angular/compiler-cli': 17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3) typescript: 5.3.3 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) '@ngtools/webpack@17.3.8(@angular/compiler-cli@17.3.12(@angular/compiler@17.3.12(@angular/core@17.3.12(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.3.3))(typescript@5.3.3)(webpack@5.93.0(esbuild@0.19.12))': dependencies: @@ -22723,11 +22729,11 @@ snapshots: graphql: 15.8.0 wonka: 4.0.15 - '@vercel/analytics@1.3.1(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': + '@vercel/analytics@1.3.1(next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8))(react@18.3.1)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8) + next: 14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8) react: 18.3.1 '@vercel/edge@1.1.2': {} @@ -23793,7 +23799,7 @@ snapshots: '@babel/core': 7.24.0 find-cache-dir: 4.0.0 schema-utils: 4.2.0 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) babel-plugin-add-module-exports@0.2.1: {} @@ -24624,6 +24630,8 @@ snapshots: colorette@2.0.20: {} + combinate@1.1.11: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -24799,7 +24807,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.2.0 serialize-javascript: 6.0.2 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) core-js-compat@3.37.1: dependencies: @@ -25082,7 +25090,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.6.3 optionalDependencies: - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) css-loader@6.11.0(webpack@5.93.0(esbuild@0.19.12)): dependencies: @@ -27569,7 +27577,7 @@ snapshots: util.promisify: 1.0.0 webpack: 4.44.2 - html-webpack-plugin@5.6.0(webpack@5.90.3(esbuild@0.20.1)): + html-webpack-plugin@5.6.0(webpack@5.90.3(esbuild@0.19.12)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -28615,7 +28623,7 @@ snapshots: dependencies: klona: 2.0.6 less: 4.2.0 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) less@4.2.0: dependencies: @@ -28647,7 +28655,7 @@ snapshots: dependencies: webpack-sources: 3.2.3 optionalDependencies: - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) lie@3.1.1: dependencies: @@ -29600,7 +29608,7 @@ snapshots: dependencies: schema-utils: 4.2.0 tapable: 2.2.1 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) mini-css-extract-plugin@2.9.0(webpack@5.93.0(esbuild@0.19.12)): dependencies: @@ -29837,7 +29845,7 @@ snapshots: next-tick@1.1.0: {} - next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522))(react@18.3.1)(sass@1.77.8): + next@14.2.5(@babel/core@7.25.2)(react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1))(react@18.3.1)(sass@1.77.8): dependencies: '@next/env': 14.2.5 '@swc/helpers': 0.5.5 @@ -29846,7 +29854,7 @@ snapshots: graceful-fs: 4.2.11 postcss: 8.4.31 react: 18.3.1 - react-dom: 19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522) + react-dom: 19.0.0-rc-4c2e457c7c-20240522(react@18.3.1) styled-jsx: 5.1.1(@babel/core@7.25.2)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.5 @@ -31252,7 +31260,7 @@ snapshots: postcss: 8.4.35 semver: 7.6.3 optionalDependencies: - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) transitivePeerDependencies: - typescript @@ -32144,6 +32152,12 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-dom@19.0.0-rc-4c2e457c7c-20240522(react@18.3.1): + dependencies: + react: 18.3.1 + scheduler: 0.25.0-rc-4c2e457c7c-20240522 + optional: true + react-dom@19.0.0-rc-4c2e457c7c-20240522(react@19.0.0-rc-4c2e457c7c-20240522): dependencies: react: 19.0.0-rc-4c2e457c7c-20240522 @@ -33062,7 +33076,7 @@ snapshots: neo-async: 2.6.2 optionalDependencies: sass: 1.71.1 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) sass@1.71.1: dependencies: @@ -33537,7 +33551,7 @@ snapshots: dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.0 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) source-map-resolve@0.5.3: dependencies: @@ -34209,6 +34223,17 @@ snapshots: optionalDependencies: esbuild: 0.19.12 + terser-webpack-plugin@5.3.10(esbuild@0.20.1)(webpack@5.90.3(esbuild@0.20.1)): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.31.3 + webpack: 5.90.3(esbuild@0.20.1) + optionalDependencies: + esbuild: 0.20.1 + terser@4.8.1: dependencies: acorn: 8.12.1 @@ -35461,7 +35486,7 @@ snapshots: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.2.0 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) webpack-dev-middleware@5.3.4(webpack@5.93.0(esbuild@0.19.12)): dependencies: @@ -35480,7 +35505,7 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.2.0 optionalDependencies: - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) webpack-dev-server@3.11.1(webpack@4.44.2): dependencies: @@ -35555,7 +35580,7 @@ snapshots: webpack-dev-middleware: 5.3.4(webpack@5.90.3(esbuild@0.20.1)) ws: 8.18.0 optionalDependencies: - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) transitivePeerDependencies: - bufferutil - debug @@ -35639,17 +35664,17 @@ snapshots: webpack-sources@3.2.3: {} - webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.0(webpack@5.90.3(esbuild@0.20.1)))(webpack@5.90.3(esbuild@0.20.1)): + webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.0(webpack@5.90.3(esbuild@0.19.12)))(webpack@5.90.3(esbuild@0.20.1)): dependencies: typed-assert: 1.0.9 webpack: 5.90.3(esbuild@0.19.12) optionalDependencies: - html-webpack-plugin: 5.6.0(webpack@5.90.3(esbuild@0.20.1)) + html-webpack-plugin: 5.6.0(webpack@5.90.3(esbuild@0.19.12)) webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.0(webpack@5.93.0(esbuild@0.19.12)))(webpack@5.90.3(esbuild@0.20.1)): dependencies: typed-assert: 1.0.9 - webpack: 5.90.3(esbuild@0.19.12) + webpack: 5.90.3(esbuild@0.20.1) optionalDependencies: html-webpack-plugin: 5.6.0(webpack@5.93.0(esbuild@0.19.12)) @@ -35714,6 +35739,37 @@ snapshots: - esbuild - uglify-js + webpack@5.90.3(esbuild@0.20.1): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.5 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/wasm-edit': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + acorn: 8.12.1 + acorn-import-assertions: 1.9.0(acorn@8.12.1) + browserslist: 4.23.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.1 + es-module-lexer: 1.5.4 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(esbuild@0.20.1)(webpack@5.90.3(esbuild@0.20.1)) + watchpack: 2.4.1 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.93.0(esbuild@0.19.12): dependencies: '@types/eslint-scope': 3.7.7