Skip to content

Commit de3cfee

Browse files
mhamriclaude
andcommitted
fix(query): emit plain SolidMutationOptions for user-facing mutation param (#3365)
Solid Query's `Use*Options` / `Create*Options` aliases are `Accessor<…>` wrappers, so typing `options.mutation` as `UseMutationOptions<…>` made callers fail with "object literal may only specify known properties, and 'onSuccess' does not exist". The runtime then spreads it as a plain object, which only "worked" because no caller could legally produce that shape. The mutation now uses the adapter's plain options interface (`SolidMutationOptions` < 5.100.6, `MutationOptions` ≥ 5.100.6 after TanStack dropped the `Solid` prefix) for both the helper return type and the user-facing `options.mutation` param. Detection lives in a new `isSolidQueryWithRenamedOptionsTypes` helper, plumbed into the solid adapter so generated imports stay in sync with the installed package. Queries keep their existing prefix-based `Use*Options` shape for the user-facing `options.query` param: Solid's `useQuery` overloads only discriminate against `Undefined/DefinedInitialDataOptions`, both of which are Accessor types, so widening to the plain options here would break `initialData` discrimination at the call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2603c31 commit de3cfee

7 files changed

Lines changed: 295 additions & 19 deletions

File tree

packages/query/src/dependencies.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
isQueryV5,
66
isQueryV5WithDataTagError,
77
isQueryV5WithInfiniteQueryOptionsError,
8+
isSolidQueryWithRenamedOptionsTypes,
9+
isSolidQueryWithUsePrefix,
810
} from './dependencies';
911

1012
describe('isQueryV5', () => {
@@ -112,3 +114,57 @@ describe('isQueryV5WithDataTagError', () => {
112114
expect(isQueryV5WithDataTagError(packageJson, 'react-query')).toBe(false);
113115
});
114116
});
117+
118+
describe('isSolidQueryWithUsePrefix', () => {
119+
it('should return true for 5.71.5 (boundary)', () => {
120+
const packageJson: PackageJson = {
121+
resolvedVersions: { '@tanstack/solid-query': '5.71.5' },
122+
};
123+
expect(isSolidQueryWithUsePrefix(packageJson)).toBe(true);
124+
});
125+
126+
it('should return false for 5.71.4', () => {
127+
const packageJson: PackageJson = {
128+
resolvedVersions: { '@tanstack/solid-query': '5.71.4' },
129+
};
130+
expect(isSolidQueryWithUsePrefix(packageJson)).toBe(false);
131+
});
132+
133+
it('should return false when solid-query is not installed', () => {
134+
expect(isSolidQueryWithUsePrefix({})).toBe(false);
135+
});
136+
});
137+
138+
describe('isSolidQueryWithRenamedOptionsTypes', () => {
139+
it('should return true for 5.100.6 (boundary — Solid prefix dropped)', () => {
140+
const packageJson: PackageJson = {
141+
resolvedVersions: { '@tanstack/solid-query': '5.100.6' },
142+
};
143+
expect(isSolidQueryWithRenamedOptionsTypes(packageJson)).toBe(true);
144+
});
145+
146+
it('should return true for 5.100.10', () => {
147+
const packageJson: PackageJson = {
148+
resolvedVersions: { '@tanstack/solid-query': '5.100.10' },
149+
};
150+
expect(isSolidQueryWithRenamedOptionsTypes(packageJson)).toBe(true);
151+
});
152+
153+
it('should return false for 5.100.5 (just below boundary)', () => {
154+
const packageJson: PackageJson = {
155+
resolvedVersions: { '@tanstack/solid-query': '5.100.5' },
156+
};
157+
expect(isSolidQueryWithRenamedOptionsTypes(packageJson)).toBe(false);
158+
});
159+
160+
it('should return false for 5.90.21 (well below boundary)', () => {
161+
const packageJson: PackageJson = {
162+
resolvedVersions: { '@tanstack/solid-query': '5.90.21' },
163+
};
164+
expect(isSolidQueryWithRenamedOptionsTypes(packageJson)).toBe(false);
165+
});
166+
167+
it('should return false when solid-query is not installed', () => {
168+
expect(isSolidQueryWithRenamedOptionsTypes({})).toBe(false);
169+
});
170+
});

packages/query/src/dependencies.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,25 @@ const VUE_QUERY_DEPENDENCIES: GeneratorDependency[] = [
285285

286286
const getSolidQueryImports = (
287287
prefix: 'use' | 'create',
288+
hasRenamedOptionsTypes: boolean,
288289
): GeneratorDependency[] => {
289290
const capitalized = prefix === 'use' ? 'Use' : 'Create';
291+
// Solid Query renamed the plain options interfaces in v5.100.6, dropping the
292+
// `Solid` prefix: `SolidQueryOptions` → `QueryOptions`, `SolidInfiniteQueryOptions` →
293+
// `InfiniteQueryOptions`, `SolidMutationOptions` → `MutationOptions`. The
294+
// `Use*Options` / `Create*Options` Accessor aliases keep their names but
295+
// are still imported because queries use them as the user-facing
296+
// `options.query` param type (Solid's `useQuery` overloads rely on that
297+
// shape so the `initialData?` discrimination keeps working).
298+
const queryOptionsTypeName = hasRenamedOptionsTypes
299+
? 'QueryOptions'
300+
: 'SolidQueryOptions';
301+
const infiniteQueryOptionsTypeName = hasRenamedOptionsTypes
302+
? 'InfiniteQueryOptions'
303+
: 'SolidInfiniteQueryOptions';
304+
const mutationOptionsTypeName = hasRenamedOptionsTypes
305+
? 'MutationOptions'
306+
: 'SolidMutationOptions';
290307
return [
291308
{
292309
exports: [
@@ -295,10 +312,9 @@ const getSolidQueryImports = (
295312
{ name: `${prefix}Mutation`, values: true },
296313
{ name: `${capitalized}QueryOptions` },
297314
{ name: `${capitalized}InfiniteQueryOptions` },
298-
{ name: `${capitalized}MutationOptions` },
299-
{ name: 'SolidQueryOptions' },
300-
{ name: 'SolidInfiniteQueryOptions' },
301-
{ name: 'SolidMutationOptions' },
315+
{ name: queryOptionsTypeName },
316+
{ name: infiniteQueryOptionsTypeName },
317+
{ name: mutationOptionsTypeName },
302318
{ name: 'QueryFunction' },
303319
{ name: 'MutationFunction' },
304320
{ name: `${capitalized}QueryResult` },
@@ -393,6 +409,7 @@ export const getSolidQueryDependencies: ClientDependenciesBuilder = (
393409
...(hasParamsSerializerOptions ? PARAMS_SERIALIZER_DEPENDENCIES : []),
394410
...getSolidQueryImports(
395411
isSolidQueryWithUsePrefix(packageJson) ? 'use' : 'create',
412+
isSolidQueryWithRenamedOptionsTypes(packageJson),
396413
),
397414
];
398415
};
@@ -561,6 +578,33 @@ export const isSolidQueryWithUsePrefix = (
561578
return compareVersions(withoutRc, '5.71.5');
562579
};
563580

581+
/**
582+
* Solid Query renamed its plain options interfaces in v5.100.6, dropping the
583+
* `Solid` prefix:
584+
* - `SolidQueryOptions` → `QueryOptions`
585+
* - `SolidInfiniteQueryOptions` → `InfiniteQueryOptions`
586+
* - `SolidMutationOptions` → `MutationOptions`
587+
*
588+
* The Accessor wrappers `UseQueryOptions` / `UseInfiniteQueryOptions` /
589+
* `UseMutationOptions` keep the same names but reference the renamed
590+
* interfaces internally.
591+
*
592+
* https://github.com/TanStack/query/commit/<rename-commit>
593+
*/
594+
export const isSolidQueryWithRenamedOptionsTypes = (
595+
packageJson: PackageJson | undefined,
596+
) => {
597+
const version = getPackageByQueryClient(packageJson, 'solid-query');
598+
599+
if (!version) {
600+
return false;
601+
}
602+
603+
const withoutRc = version.split('-')[0];
604+
605+
return compareVersions(withoutRc, '5.100.6');
606+
};
607+
564608
const getPackageByQueryClient = (
565609
packageJson: PackageJson | undefined,
566610
queryClient:

packages/query/src/framework-adapter.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,26 @@ export interface FrameworkAdapter {
185185
getQueryOptionsDefinitionPrefix(): string;
186186

187187
/**
188-
* Get the options type name for return types of getOptions functions.
189-
* Solid: 'SolidQueryOptions' / 'SolidMutationOptions'. Others: use prefix + type.
188+
* Get the plain (non-Accessor) options interface name for this framework.
189+
*
190+
* Used as:
191+
* - the helper function's return type for queries, infinite queries, and
192+
* mutations (`isReturnType: true`), AND
193+
* - the user-facing `options.mutation` parameter type
194+
* (`isReturnType: false`, mutation only — issue #3365).
195+
*
196+
* Queries intentionally keep the prefix-based `Use*Options` / `Create*Options`
197+
* for the user-facing param: in Solid Query that aliases an `Accessor<…>`,
198+
* which is the shape `useQuery`'s `Undefined/DefinedInitialDataOptions`
199+
* overloads accept, preserving the `initialData?` discrimination at the call
200+
* site.
201+
*
202+
* Solid Query overrides this with `SolidQueryOptions` / `SolidMutationOptions`
203+
* (pre-v5.100.6) or `QueryOptions` / `MutationOptions` (v5.100.6+).
204+
*
205+
* Other adapters can leave this undefined and fall back to the prefix-based
206+
* default (`UseMutationOptions`, `CreateMutationOptions`, …), which IS the
207+
* plain options shape in their target libraries.
190208
*/
191209
getOptionsReturnTypeName?(
192210
type: 'query' | 'infiniteQuery' | 'mutation',

packages/query/src/frameworks/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
isQueryV5WithInfiniteQueryOptionsError,
1919
isQueryV5WithMutationContextOnSuccess,
2020
isQueryV5WithRequiredContextOnSuccess,
21+
isSolidQueryWithRenamedOptionsTypes,
2122
isSolidQueryWithUsePrefix,
2223
isSvelteQueryV3,
2324
isSvelteQueryV6,
@@ -143,6 +144,7 @@ const withDefaults = (adapter: FrameworkAdapterConfig): FrameworkAdapter => ({
143144
queryParam,
144145
isReturnType: false,
145146
initialData,
147+
adapter: adapter as FrameworkAdapter,
146148
});
147149

148150
if (!isRequestOptions) {
@@ -297,6 +299,8 @@ export const createFrameworkAdapter = ({
297299

298300
case OutputClientConst.SOLID_QUERY: {
299301
const hasSolidQueryWithUsePrefix = isSolidQueryWithUsePrefix(packageJson);
302+
const hasSolidQueryWithRenamedOptionsTypes =
303+
isSolidQueryWithRenamedOptionsTypes(packageJson);
300304
return withDefaults(
301305
createSolidAdapter({
302306
hasQueryV5: _hasQueryV5,
@@ -308,6 +312,8 @@ export const createFrameworkAdapter = ({
308312
hasQueryV5WithRequiredContextOnSuccess:
309313
_hasQueryV5WithRequiredContextOnSuccess,
310314
hasSolidQueryUsePrefix: hasSolidQueryWithUsePrefix,
315+
hasSolidQueryRenamedOptionsTypes:
316+
hasSolidQueryWithRenamedOptionsTypes,
311317
}),
312318
);
313319
}

packages/query/src/frameworks/solid.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ export const createSolidAdapter = ({
2424
hasQueryV5WithMutationContextOnSuccess,
2525
hasQueryV5WithRequiredContextOnSuccess,
2626
hasSolidQueryUsePrefix,
27+
hasSolidQueryRenamedOptionsTypes,
2728
}: {
2829
hasQueryV5: boolean;
2930
hasQueryV5WithDataTagError: boolean;
3031
hasQueryV5WithInfiniteQueryOptionsError: boolean;
3132
hasQueryV5WithMutationContextOnSuccess: boolean;
3233
hasQueryV5WithRequiredContextOnSuccess: boolean;
3334
hasSolidQueryUsePrefix: boolean;
35+
hasSolidQueryRenamedOptionsTypes: boolean;
3436
}): FrameworkAdapterConfig => ({
3537
outputClient: OutputClient.SOLID_QUERY,
3638
hookPrefix: hasSolidQueryUsePrefix ? 'use' : 'create',
@@ -47,11 +49,26 @@ export const createSolidAdapter = ({
4749
getOptionsReturnTypeName(
4850
type: 'query' | 'infiniteQuery' | 'mutation',
4951
): string | undefined {
50-
// Solid Query uses SolidQueryOptions for queries, SolidInfiniteQueryOptions for infinite queries,
51-
// and SolidMutationOptions for mutations (these are accessors)
52-
if (type === 'mutation') return 'SolidMutationOptions';
53-
if (type === 'infiniteQuery') return 'SolidInfiniteQueryOptions';
54-
return 'SolidQueryOptions';
52+
// Solid Query exposes plain (non-Accessor) options interfaces. The
53+
// Accessor-wrapped `Use*Options` / `Create*Options` variants cannot be
54+
// used here because the generated code passes options as a plain object
55+
// (`{ ...options.mutation }`) before the call site wraps the whole result
56+
// in an accessor (`useMutation(() => mutationOptions(...))`).
57+
//
58+
// v5.100.6 renamed these interfaces to drop the `Solid` prefix.
59+
if (type === 'mutation') {
60+
return hasSolidQueryRenamedOptionsTypes
61+
? 'MutationOptions'
62+
: 'SolidMutationOptions';
63+
}
64+
if (type === 'infiniteQuery') {
65+
return hasSolidQueryRenamedOptionsTypes
66+
? 'InfiniteQueryOptions'
67+
: 'SolidInfiniteQueryOptions';
68+
}
69+
return hasSolidQueryRenamedOptionsTypes
70+
? 'QueryOptions'
71+
: 'SolidQueryOptions';
5572
},
5673

5774
getQueryKeyPrefix(): string {

0 commit comments

Comments
 (0)