Skip to content

Commit 32f494c

Browse files
vercel-ai-sdk[bot]dancergr2m
authored
Backport: feat(codemod): add usechat input state transformation for v5 (#8970)
This is an automated backport of #8725 to the release-v5.0 branch. Co-authored-by: josh <[email protected]> Co-authored-by: Gregor Martynus <[email protected]>
1 parent 2210769 commit 32f494c

File tree

7 files changed

+394
-0
lines changed

7 files changed

+394
-0
lines changed

.changeset/silent-queens-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/codemod': patch
3+
---
4+
5+
feat(codemod): add usechat input state transformation for v5

packages/codemod/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ npx @ai-sdk/codemod v5/rename-format-stream-part .
125125
| `v5/replace-simulate-streaming` | Transforms v5/replace simulate streaming |
126126
| `v5/replace-textdelta-with-text` | Transforms v5/replace textdelta with text |
127127
| `v5/replace-usage-token-properties` | Transforms v5/replace usage token properties |
128+
| `v5/replace-usechat-input-with-state` | Transforms v5/replace usechat input with state |
128129
| `v5/replace-zod-import-with-v3` | Transforms v5/replace zod import with v3 |
129130
| `v5/require-createIdGenerator-size-argument` | Transforms v5/require createIdGenerator size argument |
130131
| `v5/restructure-file-stream-parts` | Transforms v5/restructure file stream parts |
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import { createTransformer } from '../lib/create-transformer';
2+
3+
export default createTransformer((fileInfo, api, options, context) => {
4+
const { j, root } = context;
5+
6+
// Track all useChat import names (including aliases)
7+
const useChatNames = new Set<string>();
8+
9+
// Replace old import path with new one and collect useChat import names
10+
root
11+
.find(j.ImportDeclaration, {
12+
source: { value: 'ai/react' },
13+
})
14+
.forEach(path => {
15+
const importDeclaration = path.node;
16+
importDeclaration.source.value = '@ai-sdk/react';
17+
18+
// Collect useChat import names
19+
importDeclaration.specifiers?.forEach(spec => {
20+
if (
21+
spec.type === 'ImportSpecifier' &&
22+
spec.imported.type === 'Identifier' &&
23+
spec.imported.name === 'useChat'
24+
) {
25+
useChatNames.add(spec.local?.name || 'useChat');
26+
}
27+
});
28+
29+
context.hasChanges = true;
30+
});
31+
32+
// Also collect useChat names from existing @ai-sdk/react imports
33+
root
34+
.find(j.ImportDeclaration, {
35+
source: { value: '@ai-sdk/react' },
36+
})
37+
.forEach(path => {
38+
const importDeclaration = path.node;
39+
40+
// Collect useChat import names
41+
importDeclaration.specifiers?.forEach(spec => {
42+
if (
43+
spec.type === 'ImportSpecifier' &&
44+
spec.imported.type === 'Identifier' &&
45+
spec.imported.name === 'useChat'
46+
) {
47+
useChatNames.add(spec.local?.name || 'useChat');
48+
}
49+
});
50+
});
51+
52+
let needsUseStateImport = false;
53+
const inputStates: Array<{
54+
inputName: string;
55+
setterName: string;
56+
parentPath: any;
57+
}> = [];
58+
59+
root.find(j.VariableDeclarator).forEach(path => {
60+
const init = path.node.init;
61+
const id = path.node.id;
62+
63+
if (
64+
!init ||
65+
init.type !== 'CallExpression' ||
66+
init.callee.type !== 'Identifier' ||
67+
!useChatNames.has(init.callee.name) ||
68+
id.type !== 'ObjectPattern'
69+
) {
70+
return;
71+
}
72+
73+
const objectPattern = id;
74+
let inputName = '';
75+
let handleInputChangeName = '';
76+
let foundInput = false;
77+
let foundHandleInputChange = false;
78+
const filteredProperties: any[] = [];
79+
80+
objectPattern.properties.forEach((prop: any) => {
81+
if (prop.type === 'Property' || prop.type === 'ObjectProperty') {
82+
if (prop.key.type === 'Identifier') {
83+
if (prop.key.name === 'input') {
84+
foundInput = true;
85+
if (prop.value && prop.value.type === 'Identifier') {
86+
inputName = prop.value.name;
87+
} else if (prop.shorthand) {
88+
inputName = 'input';
89+
}
90+
} else if (prop.key.name === 'handleInputChange') {
91+
foundHandleInputChange = true;
92+
if (prop.value && prop.value.type === 'Identifier') {
93+
handleInputChangeName = prop.value.name;
94+
} else if (prop.shorthand) {
95+
handleInputChangeName = 'handleInputChange';
96+
}
97+
} else {
98+
filteredProperties.push(prop);
99+
}
100+
} else {
101+
filteredProperties.push(prop);
102+
}
103+
} else {
104+
filteredProperties.push(prop);
105+
}
106+
});
107+
108+
if (foundInput || foundHandleInputChange) {
109+
context.hasChanges = true;
110+
111+
objectPattern.properties = filteredProperties;
112+
113+
if (foundInput && inputName) {
114+
needsUseStateImport = true;
115+
const setterName = `set${inputName.charAt(0).toUpperCase()}${inputName.slice(1)}`;
116+
117+
inputStates.push({
118+
inputName,
119+
setterName,
120+
parentPath: path.parent,
121+
});
122+
123+
if (foundHandleInputChange && handleInputChangeName) {
124+
const functionScope =
125+
j(path).closest(j.FunctionDeclaration).size() > 0
126+
? j(path).closest(j.FunctionDeclaration)
127+
: j(path).closest(j.FunctionExpression).size() > 0
128+
? j(path).closest(j.FunctionExpression)
129+
: j(path).closest(j.ArrowFunctionExpression);
130+
131+
functionScope
132+
.find(j.Identifier, { name: handleInputChangeName })
133+
.filter(idPath => {
134+
const parent = idPath.parent.node;
135+
return !(
136+
((parent.type === 'Property' ||
137+
parent.type === 'ObjectProperty') &&
138+
parent.key === idPath.node) ||
139+
(parent.type === 'MemberExpression' &&
140+
parent.property === idPath.node &&
141+
!parent.computed)
142+
);
143+
})
144+
.replaceWith(() => {
145+
return j.arrowFunctionExpression(
146+
[j.identifier('e')],
147+
j.callExpression(j.identifier(setterName), [
148+
j.memberExpression(
149+
j.memberExpression(
150+
j.identifier('e'),
151+
j.identifier('target'),
152+
),
153+
j.identifier('value'),
154+
),
155+
]),
156+
);
157+
});
158+
}
159+
} else if (foundHandleInputChange && handleInputChangeName) {
160+
needsUseStateImport = true;
161+
const inputName = 'input';
162+
const setterName = 'setInput';
163+
164+
inputStates.push({
165+
inputName,
166+
setterName,
167+
parentPath: path.parent,
168+
});
169+
170+
const functionScope =
171+
j(path).closest(j.FunctionDeclaration).size() > 0
172+
? j(path).closest(j.FunctionDeclaration)
173+
: j(path).closest(j.FunctionExpression).size() > 0
174+
? j(path).closest(j.FunctionExpression)
175+
: j(path).closest(j.ArrowFunctionExpression);
176+
177+
functionScope
178+
.find(j.Identifier, { name: handleInputChangeName })
179+
.filter(idPath => {
180+
const parent = idPath.parent.node;
181+
return !(
182+
((parent.type === 'Property' ||
183+
parent.type === 'ObjectProperty') &&
184+
parent.key === idPath.node) ||
185+
(parent.type === 'MemberExpression' &&
186+
parent.property === idPath.node &&
187+
!parent.computed)
188+
);
189+
})
190+
.replaceWith(() => {
191+
return j.arrowFunctionExpression(
192+
[j.identifier('e')],
193+
j.callExpression(j.identifier(setterName), [
194+
j.memberExpression(
195+
j.memberExpression(j.identifier('e'), j.identifier('target')),
196+
j.identifier('value'),
197+
),
198+
]),
199+
);
200+
});
201+
}
202+
}
203+
});
204+
205+
inputStates.forEach(({ inputName, setterName, parentPath }) => {
206+
const useStateDeclaration = j.variableDeclaration('const', [
207+
j.variableDeclarator(
208+
j.arrayPattern([j.identifier(inputName), j.identifier(setterName)]),
209+
j.callExpression(j.identifier('useState'), [j.literal('')]),
210+
),
211+
]);
212+
213+
j(parentPath).insertBefore(useStateDeclaration);
214+
});
215+
216+
if (needsUseStateImport) {
217+
const reactImports = root.find(j.ImportDeclaration, {
218+
source: { value: 'react' },
219+
});
220+
221+
if (reactImports.length > 0) {
222+
const firstReactImport = reactImports.at(0);
223+
const specifiers = firstReactImport.get().node.specifiers || [];
224+
225+
const hasUseState = specifiers.some(
226+
(spec: any) =>
227+
spec.type === 'ImportSpecifier' &&
228+
spec.imported.type === 'Identifier' &&
229+
spec.imported.name === 'useState',
230+
);
231+
232+
if (!hasUseState) {
233+
specifiers.push(j.importSpecifier(j.identifier('useState')));
234+
}
235+
} else {
236+
const imports = root.find(j.ImportDeclaration);
237+
if (imports.length > 0) {
238+
imports
239+
.at(0)
240+
.insertAfter(
241+
j.importDeclaration(
242+
[j.importSpecifier(j.identifier('useState'))],
243+
j.literal('react'),
244+
),
245+
);
246+
}
247+
}
248+
}
249+
});

packages/codemod/src/lib/upgrade.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const bundle = [
6969
'v5/replace-simulate-streaming',
7070
'v5/replace-textdelta-with-text',
7171
'v5/replace-usage-token-properties',
72+
'v5/replace-usechat-input-with-state',
7273
'v5/replace-zod-import-with-v3',
7374
'v5/require-createIdGenerator-size-argument',
7475
'v5/restructure-file-stream-parts',
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// @ts-nocheck
2+
import { useChat } from 'ai/react';
3+
4+
export function ChatComponent() {
5+
const { messages, input, handleInputChange, handleSubmit } = useChat({
6+
api: '/api/chat',
7+
});
8+
9+
const handleChange = handleInputChange;
10+
const currentInput = input;
11+
}
12+
13+
export function AnotherComponent() {
14+
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
15+
api: '/api/chat',
16+
onError: (error) => console.error(error),
17+
});
18+
19+
const submitHandler = handleSubmit;
20+
const onChange = handleInputChange;
21+
return { input, onChange, submitHandler };
22+
}
23+
24+
export function ComponentWithAlias() {
25+
const {
26+
messages,
27+
input: chatInput,
28+
handleInputChange: handleChatInputChange,
29+
handleSubmit
30+
} = useChat();
31+
32+
const value = chatInput;
33+
const handler = handleChatInputChange;
34+
return { value, handler };
35+
}
36+
37+
export function PartialExtraction() {
38+
const { input, handleSubmit } = useChat({
39+
api: '/api/chat',
40+
});
41+
42+
return input;
43+
}
44+
45+
export function OnlyHandleInputChange() {
46+
const { messages, handleInputChange } = useChat();
47+
return handleInputChange;
48+
}
49+
50+
// Test with new import syntax
51+
import { useChat as useChatNew } from '@ai-sdk/react';
52+
53+
export function WithNewImportSyntax() {
54+
const { input } = useChatNew();
55+
return input;
56+
}

0 commit comments

Comments
 (0)