@next/codemod 15.0.0-rc.0 → 15.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/next-codemod.js +55 -3
- package/bin/shared.js +7 -0
- package/bin/transform.js +124 -0
- package/bin/upgrade.js +390 -0
- package/lib/cra-to-next/global-css-transform.js +5 -5
- package/lib/cra-to-next/index-to-component.js +5 -5
- package/lib/handle-package.js +110 -0
- package/lib/install.js +2 -3
- package/lib/parser.js +28 -0
- package/lib/run-jscodeshift.js +18 -2
- package/lib/utils.js +115 -0
- package/package.json +13 -6
- package/transforms/add-missing-react-import.js +4 -3
- package/transforms/app-dir-runtime-config-experimental-edge.js +34 -0
- package/transforms/built-in-next-font.js +4 -3
- package/transforms/cra-to-next.js +238 -236
- package/transforms/lib/async-request-api/index.js +16 -0
- package/transforms/lib/async-request-api/next-async-dynamic-api.js +274 -0
- package/transforms/lib/async-request-api/next-async-dynamic-prop.js +715 -0
- package/transforms/lib/async-request-api/utils.js +374 -0
- package/transforms/metadata-to-viewport-export.js +4 -3
- package/transforms/name-default-component.js +6 -6
- package/transforms/new-link.js +9 -7
- package/transforms/next-async-request-api.js +9 -0
- package/transforms/next-dynamic-access-named-export.js +66 -0
- package/transforms/next-image-experimental.js +12 -15
- package/transforms/next-image-to-legacy-image.js +8 -9
- package/transforms/next-og-import.js +4 -3
- package/transforms/next-request-geo-ip.js +339 -0
- package/transforms/url-to-withrouter.js +1 -1
- package/transforms/withamp-to-config.js +1 -1
- package/bin/cli.js +0 -216
- package/lib/uninstall-package.js +0 -32
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.transformDynamicProps = transformDynamicProps;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
const parser_1 = require("../../../lib/parser");
|
|
6
|
+
const PAGE_PROPS = 'props';
|
|
7
|
+
function findFunctionBody(path) {
|
|
8
|
+
let functionBody = path.node.body;
|
|
9
|
+
if (functionBody && functionBody.type === 'BlockStatement') {
|
|
10
|
+
return functionBody.body;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function awaitMemberAccessOfProp(propIdName, path, j) {
|
|
15
|
+
// search the member access of the prop
|
|
16
|
+
const functionBody = findFunctionBody(path);
|
|
17
|
+
const memberAccess = j(functionBody).find(j.MemberExpression, {
|
|
18
|
+
object: {
|
|
19
|
+
type: 'Identifier',
|
|
20
|
+
name: propIdName,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
let hasAwaited = false;
|
|
24
|
+
// await each member access
|
|
25
|
+
memberAccess.forEach((memberAccessPath) => {
|
|
26
|
+
const member = memberAccessPath.value;
|
|
27
|
+
const memberProperty = member.property;
|
|
28
|
+
const isAccessingMatchedProperty = j.Identifier.check(memberProperty) &&
|
|
29
|
+
utils_1.TARGET_PROP_NAMES.has(memberProperty.name);
|
|
30
|
+
if (!isAccessingMatchedProperty) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (isParentPromiseAllCallExpression(memberAccessPath, j)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// check if it's already awaited
|
|
37
|
+
if (memberAccessPath.parentPath?.value.type === 'AwaitExpression') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const awaitedExpr = j.awaitExpression(member);
|
|
41
|
+
const awaitMemberAccess = (0, utils_1.wrapParentheseIfNeeded)(true, j, awaitedExpr);
|
|
42
|
+
memberAccessPath.replace(awaitMemberAccess);
|
|
43
|
+
hasAwaited = true;
|
|
44
|
+
});
|
|
45
|
+
// If there's any awaited member access, we need to make the function async
|
|
46
|
+
if (hasAwaited) {
|
|
47
|
+
if (!path.value.async) {
|
|
48
|
+
if ('async' in path.value) {
|
|
49
|
+
path.value.async = true;
|
|
50
|
+
(0, utils_1.turnFunctionReturnTypeToAsync)(path.value, j);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return hasAwaited;
|
|
55
|
+
}
|
|
56
|
+
function isParentUseCallExpression(path, j) {
|
|
57
|
+
if (
|
|
58
|
+
// member access parentPath is argument
|
|
59
|
+
j.CallExpression.check(path.parent.value) &&
|
|
60
|
+
// member access is first argument
|
|
61
|
+
path.parent.value.arguments[0] === path.value &&
|
|
62
|
+
// function name is `use`
|
|
63
|
+
j.Identifier.check(path.parent.value.callee) &&
|
|
64
|
+
path.parent.value.callee.name === 'use') {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
function isParentPromiseAllCallExpression(path, j) {
|
|
70
|
+
const argsParent = path.parent;
|
|
71
|
+
const callParent = argsParent?.parent;
|
|
72
|
+
if (j.ArrayExpression.check(argsParent.value) &&
|
|
73
|
+
j.CallExpression.check(callParent.value) &&
|
|
74
|
+
j.MemberExpression.check(callParent.value.callee) &&
|
|
75
|
+
j.Identifier.check(callParent.value.callee.object) &&
|
|
76
|
+
callParent.value.callee.object.name === 'Promise' &&
|
|
77
|
+
j.Identifier.check(callParent.value.callee.property) &&
|
|
78
|
+
callParent.value.callee.property.name === 'all') {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function applyUseAndRenameAccessedProp(propIdName, path, j) {
|
|
84
|
+
// search the member access of the prop, and rename the member access to the member value
|
|
85
|
+
// e.g.
|
|
86
|
+
// props.params => params
|
|
87
|
+
// props.params.foo => params.foo
|
|
88
|
+
// props.searchParams.search => searchParams.search
|
|
89
|
+
let modified = false;
|
|
90
|
+
const functionBody = findFunctionBody(path);
|
|
91
|
+
const memberAccess = j(functionBody).find(j.MemberExpression, {
|
|
92
|
+
object: {
|
|
93
|
+
type: 'Identifier',
|
|
94
|
+
name: propIdName,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const accessedNames = [];
|
|
98
|
+
// rename each member access
|
|
99
|
+
memberAccess.forEach((memberAccessPath) => {
|
|
100
|
+
// If the member access expression is first argument of `use()`, we skip
|
|
101
|
+
if (isParentUseCallExpression(memberAccessPath, j)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const member = memberAccessPath.value;
|
|
105
|
+
const memberProperty = member.property;
|
|
106
|
+
if (j.Identifier.check(memberProperty)) {
|
|
107
|
+
accessedNames.push(memberProperty.name);
|
|
108
|
+
}
|
|
109
|
+
else if (j.MemberExpression.check(memberProperty)) {
|
|
110
|
+
let currentMember = memberProperty;
|
|
111
|
+
if (j.Identifier.check(currentMember.object)) {
|
|
112
|
+
accessedNames.push(currentMember.object.name);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
memberAccessPath.replace(memberProperty);
|
|
116
|
+
});
|
|
117
|
+
// If there's any renamed member access, need to call `use()` onto member access
|
|
118
|
+
// e.g. ['params'] => insert `const params = use(props.params)`
|
|
119
|
+
if (accessedNames.length > 0) {
|
|
120
|
+
const accessedPropId = j.identifier(propIdName);
|
|
121
|
+
const accessedProp = j.memberExpression(accessedPropId, j.identifier(accessedNames[0]));
|
|
122
|
+
const useCall = j.callExpression(j.identifier('use'), [accessedProp]);
|
|
123
|
+
const useDeclaration = j.variableDeclaration('const', [
|
|
124
|
+
j.variableDeclarator(j.identifier(accessedNames[0]), useCall),
|
|
125
|
+
]);
|
|
126
|
+
if (functionBody) {
|
|
127
|
+
functionBody.unshift(useDeclaration);
|
|
128
|
+
}
|
|
129
|
+
modified = true;
|
|
130
|
+
}
|
|
131
|
+
return modified;
|
|
132
|
+
}
|
|
133
|
+
function commentOnMatchedReExports(root, j) {
|
|
134
|
+
let modified = false;
|
|
135
|
+
root.find(j.ExportNamedDeclaration).forEach((path) => {
|
|
136
|
+
if (j.ExportSpecifier.check(path.value.specifiers[0])) {
|
|
137
|
+
const specifiers = path.value.specifiers;
|
|
138
|
+
for (const specifier of specifiers) {
|
|
139
|
+
if (j.ExportSpecifier.check(specifier) &&
|
|
140
|
+
// Find matched named exports and default export
|
|
141
|
+
(utils_1.TARGET_NAMED_EXPORTS.has(specifier.exported.name) ||
|
|
142
|
+
specifier.exported.name === 'default')) {
|
|
143
|
+
if (j.Literal.check(path.value.source)) {
|
|
144
|
+
const localName = specifier.local.name;
|
|
145
|
+
const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
|
|
146
|
+
modified ||= commentInserted;
|
|
147
|
+
}
|
|
148
|
+
else if (path.value.source === null) {
|
|
149
|
+
const localIdentifier = specifier.local;
|
|
150
|
+
const localName = localIdentifier.name;
|
|
151
|
+
// search if local identifier is from imports
|
|
152
|
+
const importDeclaration = root
|
|
153
|
+
.find(j.ImportDeclaration)
|
|
154
|
+
.filter((importPath) => {
|
|
155
|
+
return importPath.value.specifiers.some((importSpecifier) => importSpecifier.local.name === localName);
|
|
156
|
+
});
|
|
157
|
+
if (importDeclaration.size() > 0) {
|
|
158
|
+
const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
|
|
159
|
+
modified ||= commentInserted;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
return modified;
|
|
167
|
+
}
|
|
168
|
+
function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
169
|
+
let modified = false;
|
|
170
|
+
if (paramTypeAnnotation && paramTypeAnnotation.typeAnnotation) {
|
|
171
|
+
const typeAnnotation = paramTypeAnnotation.typeAnnotation;
|
|
172
|
+
if (typeAnnotation.type === 'TSTypeLiteral') {
|
|
173
|
+
const typeLiteral = typeAnnotation;
|
|
174
|
+
// Find the type property for `params`
|
|
175
|
+
typeLiteral.members.forEach((member) => {
|
|
176
|
+
if (member.type === 'TSPropertySignature' &&
|
|
177
|
+
member.key.type === 'Identifier' &&
|
|
178
|
+
utils_1.TARGET_PROP_NAMES.has(member.key.name)) {
|
|
179
|
+
// if it's already a Promise, don't wrap it again, return
|
|
180
|
+
if (member.typeAnnotation &&
|
|
181
|
+
member.typeAnnotation.typeAnnotation &&
|
|
182
|
+
member.typeAnnotation.typeAnnotation.type === 'TSTypeReference' &&
|
|
183
|
+
member.typeAnnotation.typeAnnotation.typeName.type ===
|
|
184
|
+
'Identifier' &&
|
|
185
|
+
member.typeAnnotation.typeAnnotation.typeName.name === 'Promise') {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Wrap the `params` type in Promise<>
|
|
189
|
+
if (member.typeAnnotation &&
|
|
190
|
+
member.typeAnnotation.typeAnnotation &&
|
|
191
|
+
j.TSType.check(member.typeAnnotation.typeAnnotation)) {
|
|
192
|
+
member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
|
|
193
|
+
// @ts-ignore
|
|
194
|
+
member.typeAnnotation.typeAnnotation,
|
|
195
|
+
]));
|
|
196
|
+
modified = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else if (typeAnnotation.type === 'TSTypeReference') {
|
|
202
|
+
// If typeAnnotation is a type or interface, change the properties to Promise<type of property>
|
|
203
|
+
// e.g. interface PageProps { params: { slug: string } } => interface PageProps { params: Promise<{ slug: string }> }
|
|
204
|
+
const typeReference = typeAnnotation;
|
|
205
|
+
if (typeReference.typeName.type === 'Identifier') {
|
|
206
|
+
// Find the actual type of the type reference
|
|
207
|
+
const foundTypes = findAllTypes(root, j, typeReference.typeName.name);
|
|
208
|
+
// Deal with interfaces
|
|
209
|
+
if (foundTypes.interfaces.length > 0) {
|
|
210
|
+
const interfaceDeclaration = foundTypes.interfaces[0];
|
|
211
|
+
if (interfaceDeclaration.type === 'TSInterfaceDeclaration' &&
|
|
212
|
+
interfaceDeclaration.body?.type === 'TSInterfaceBody') {
|
|
213
|
+
const typeBody = interfaceDeclaration.body.body;
|
|
214
|
+
// if it's already a Promise, don't wrap it again, return
|
|
215
|
+
// traverse the typeReference's properties, if any is in propNames, wrap it in Promise<> if needed
|
|
216
|
+
typeBody.forEach((member) => {
|
|
217
|
+
if (member.type === 'TSPropertySignature' &&
|
|
218
|
+
member.key.type === 'Identifier' &&
|
|
219
|
+
utils_1.TARGET_PROP_NAMES.has(member.key.name)) {
|
|
220
|
+
// if it's already a Promise, don't wrap it again, return
|
|
221
|
+
if (member.typeAnnotation &&
|
|
222
|
+
member.typeAnnotation.typeAnnotation &&
|
|
223
|
+
member.typeAnnotation?.typeAnnotation?.typeName?.name ===
|
|
224
|
+
'Promise') {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Wrap the prop type in Promise<>
|
|
228
|
+
if (member.typeAnnotation &&
|
|
229
|
+
member.typeAnnotation.typeAnnotation &&
|
|
230
|
+
// check if member name is in propNames
|
|
231
|
+
utils_1.TARGET_PROP_NAMES.has(member.key.name)) {
|
|
232
|
+
member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
|
|
233
|
+
member.typeAnnotation.typeAnnotation,
|
|
234
|
+
]));
|
|
235
|
+
modified = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
propsIdentifier.typeAnnotation = paramTypeAnnotation;
|
|
244
|
+
modified = true;
|
|
245
|
+
}
|
|
246
|
+
return modified;
|
|
247
|
+
}
|
|
248
|
+
function transformDynamicProps(source, _api, filePath) {
|
|
249
|
+
const isEntryFile = utils_1.NEXTJS_ENTRY_FILES.test(filePath);
|
|
250
|
+
if (!isEntryFile) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
let modified = false;
|
|
254
|
+
let modifiedPropArgument = false;
|
|
255
|
+
const j = (0, parser_1.createParserFromPath)(filePath);
|
|
256
|
+
const root = j(source);
|
|
257
|
+
// Check if 'use' from 'react' needs to be imported
|
|
258
|
+
let needsReactUseImport = false;
|
|
259
|
+
// Based on the prop names
|
|
260
|
+
// e.g. destruct `params` { slug } = params
|
|
261
|
+
// e.g. destruct `searchParams `{ search } = searchParams
|
|
262
|
+
let insertedDestructPropNames = new Set();
|
|
263
|
+
function processAsyncPropOfEntryFile(isClientComponent) {
|
|
264
|
+
// find `params` and `searchParams` in file, and transform the access to them
|
|
265
|
+
function renameAsyncPropIfExisted(path, isDefaultExport) {
|
|
266
|
+
const decl = path.value;
|
|
267
|
+
const params = decl.params;
|
|
268
|
+
let functionName = decl.id?.name;
|
|
269
|
+
// If it's const <id> = function () {}, locate the <id> to get function name
|
|
270
|
+
if (!decl.id) {
|
|
271
|
+
functionName = (0, utils_1.getVariableDeclaratorId)(path, j)?.name;
|
|
272
|
+
}
|
|
273
|
+
// target properties mapping, only contains `params` and `searchParams`
|
|
274
|
+
const propertiesMap = new Map();
|
|
275
|
+
let allProperties = [];
|
|
276
|
+
const isRoute = !isDefaultExport && utils_1.TARGET_ROUTE_EXPORTS.has(functionName);
|
|
277
|
+
// generateMetadata API has 2 params
|
|
278
|
+
if (functionName === 'generateMetadata') {
|
|
279
|
+
if (params.length > 2 || params.length === 0)
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
else if (isRoute) {
|
|
283
|
+
if (params.length !== 2)
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
// Page/Layout default export have 1 param
|
|
288
|
+
if (params.length !== 1)
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const propsIdentifier = (0, utils_1.generateUniqueIdentifier)(PAGE_PROPS, path, j);
|
|
292
|
+
const propsArgumentIndex = isRoute ? 1 : 0;
|
|
293
|
+
const currentParam = params[propsArgumentIndex];
|
|
294
|
+
if (!currentParam)
|
|
295
|
+
return;
|
|
296
|
+
// Argument destructuring case
|
|
297
|
+
if (currentParam.type === 'ObjectPattern') {
|
|
298
|
+
// Validate if the properties are not `params` and `searchParams`,
|
|
299
|
+
// if they are, quit the transformation
|
|
300
|
+
let foundTargetProp = false;
|
|
301
|
+
for (const prop of currentParam.properties) {
|
|
302
|
+
if ('key' in prop && prop.key.type === 'Identifier') {
|
|
303
|
+
const propName = prop.key.name;
|
|
304
|
+
if (utils_1.TARGET_PROP_NAMES.has(propName)) {
|
|
305
|
+
foundTargetProp = true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// If there's no `params` or `searchParams` matched, return
|
|
310
|
+
if (!foundTargetProp)
|
|
311
|
+
return;
|
|
312
|
+
allProperties = currentParam.properties;
|
|
313
|
+
currentParam.properties.forEach((prop) => {
|
|
314
|
+
if (
|
|
315
|
+
// Could be `Property` or `ObjectProperty`
|
|
316
|
+
(j.Property.check(prop) || j.ObjectProperty.check(prop)) &&
|
|
317
|
+
j.Identifier.check(prop.key) &&
|
|
318
|
+
utils_1.TARGET_PROP_NAMES.has(prop.key.name)) {
|
|
319
|
+
const value = 'value' in prop ? prop.value : null;
|
|
320
|
+
propertiesMap.set(prop.key.name, value);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
modifiedPropArgument = true;
|
|
324
|
+
}
|
|
325
|
+
else if (currentParam.type === 'Identifier') {
|
|
326
|
+
// case of accessing the props.params.<name>:
|
|
327
|
+
// Page(props) {}
|
|
328
|
+
// generateMetadata(props, parent?) {}
|
|
329
|
+
const argName = currentParam.name;
|
|
330
|
+
if (isClientComponent) {
|
|
331
|
+
const modifiedProp = applyUseAndRenameAccessedProp(argName, path, j);
|
|
332
|
+
if (modifiedProp) {
|
|
333
|
+
needsReactUseImport = true;
|
|
334
|
+
modified = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const awaited = awaitMemberAccessOfProp(argName, path, j);
|
|
339
|
+
modified ||= awaited;
|
|
340
|
+
}
|
|
341
|
+
// cases of passing down `props` into any function
|
|
342
|
+
// Page(props) { callback(props) }
|
|
343
|
+
// search for all the argument of CallExpression, where currentParam is one of the arguments
|
|
344
|
+
const callExpressions = j(path).find(j.CallExpression, {
|
|
345
|
+
arguments: (args) => {
|
|
346
|
+
return args.some((arg) => {
|
|
347
|
+
return (j.Identifier.check(arg) &&
|
|
348
|
+
arg.name === argName &&
|
|
349
|
+
arg.type === 'Identifier');
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
// Add a comment to warn users that properties of `props` need to be awaited when accessed
|
|
354
|
+
callExpressions.forEach((callExpression) => {
|
|
355
|
+
// find the argument `currentParam`
|
|
356
|
+
const args = callExpression.value.arguments;
|
|
357
|
+
const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
|
|
358
|
+
const comment = ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `;
|
|
359
|
+
const inserted = (0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
|
|
360
|
+
modified ||= inserted;
|
|
361
|
+
});
|
|
362
|
+
if (modified) {
|
|
363
|
+
modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (modifiedPropArgument) {
|
|
367
|
+
const isModified = resolveAsyncProp(path, propertiesMap, propsIdentifier.name, allProperties, isDefaultExport);
|
|
368
|
+
if (isModified) {
|
|
369
|
+
// Make TS happy
|
|
370
|
+
if (j.ObjectPattern.check(currentParam)) {
|
|
371
|
+
modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
|
|
372
|
+
}
|
|
373
|
+
// Override the first param to `props`
|
|
374
|
+
params[propsArgumentIndex] = propsIdentifier;
|
|
375
|
+
modified = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// When the prop argument is not destructured, we need to add comments to the spread properties
|
|
380
|
+
if (j.Identifier.check(currentParam)) {
|
|
381
|
+
const commented = commentSpreadProps(path, currentParam.name, j);
|
|
382
|
+
const modifiedTypes = modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
|
|
383
|
+
modified ||= commented || modifiedTypes;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Helper function to insert `const params = await asyncParams;` at the beginning of the function body
|
|
388
|
+
function resolveAsyncProp(path, propertiesMap, propsIdentifierName, allProperties, isDefaultExport) {
|
|
389
|
+
// Rename props to `prop` argument for the function
|
|
390
|
+
const insertedRenamedPropFunctionNames = new Set();
|
|
391
|
+
const node = path.value;
|
|
392
|
+
// If it's sync default export, and it's also server component, make the function async
|
|
393
|
+
if (isDefaultExport && !isClientComponent) {
|
|
394
|
+
if (!node.async) {
|
|
395
|
+
if ('async' in node) {
|
|
396
|
+
node.async = true;
|
|
397
|
+
(0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// If it's arrow function and function body is not block statement, check if the properties are used there
|
|
402
|
+
if (j.ArrowFunctionExpression.check(path.node) &&
|
|
403
|
+
!j.BlockStatement.check(path.node.body)) {
|
|
404
|
+
const objectExpression = path.node.body;
|
|
405
|
+
let hasUsedProps = false;
|
|
406
|
+
j(objectExpression)
|
|
407
|
+
.find(j.Identifier)
|
|
408
|
+
.forEach((identifierPath) => {
|
|
409
|
+
const idName = identifierPath.value.name;
|
|
410
|
+
if (propertiesMap.has(idName)) {
|
|
411
|
+
hasUsedProps = true;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
// Turn the function body to block statement, return the object expression
|
|
416
|
+
if (hasUsedProps) {
|
|
417
|
+
path.node.body = j.blockStatement([
|
|
418
|
+
j.returnStatement(objectExpression),
|
|
419
|
+
]);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const isAsyncFunc = !!node.async;
|
|
423
|
+
const functionName = path.value.id?.name || 'default';
|
|
424
|
+
const functionBody = findFunctionBody(path);
|
|
425
|
+
const hasOtherProperties = allProperties.length > propertiesMap.size;
|
|
426
|
+
function createDestructuringDeclaration(properties, destructPropsIdentifierName) {
|
|
427
|
+
const propsToKeep = [];
|
|
428
|
+
let restProperty = null;
|
|
429
|
+
// Iterate over the destructured properties
|
|
430
|
+
properties.forEach((property) => {
|
|
431
|
+
if (j.ObjectProperty.check(property)) {
|
|
432
|
+
// Handle normal and computed properties
|
|
433
|
+
const keyName = j.Identifier.check(property.key)
|
|
434
|
+
? property.key.name
|
|
435
|
+
: j.Literal.check(property.key)
|
|
436
|
+
? property.key.value
|
|
437
|
+
: null; // for computed properties
|
|
438
|
+
if (typeof keyName === 'string') {
|
|
439
|
+
propsToKeep.push(property);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else if (j.RestElement.check(property)) {
|
|
443
|
+
restProperty = property;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
if (propsToKeep.length === 0 && !restProperty) {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
if (restProperty) {
|
|
450
|
+
propsToKeep.push(restProperty);
|
|
451
|
+
}
|
|
452
|
+
return j.variableDeclaration('const', [
|
|
453
|
+
j.variableDeclarator(j.objectPattern(propsToKeep), j.identifier(destructPropsIdentifierName)),
|
|
454
|
+
]);
|
|
455
|
+
}
|
|
456
|
+
if (hasOtherProperties) {
|
|
457
|
+
/**
|
|
458
|
+
* If there are other properties, we need to keep the original param with destructuring
|
|
459
|
+
* e.g.
|
|
460
|
+
* input:
|
|
461
|
+
* Page({ params: { slug }, otherProp }) {
|
|
462
|
+
* const { slug } = await props.params;
|
|
463
|
+
* }
|
|
464
|
+
*
|
|
465
|
+
* output:
|
|
466
|
+
* Page(props) {
|
|
467
|
+
* const { otherProp } = props; // inserted
|
|
468
|
+
* // ...rest of the function body
|
|
469
|
+
* }
|
|
470
|
+
*/
|
|
471
|
+
const restProperties = allProperties.filter((prop) => {
|
|
472
|
+
const isTargetProps = 'key' in prop &&
|
|
473
|
+
prop.key.type === 'Identifier' &&
|
|
474
|
+
utils_1.TARGET_PROP_NAMES.has(prop.key.name);
|
|
475
|
+
return !isTargetProps;
|
|
476
|
+
});
|
|
477
|
+
const destructionOtherPropertiesDeclaration = createDestructuringDeclaration(restProperties, propsIdentifierName);
|
|
478
|
+
if (functionBody && destructionOtherPropertiesDeclaration) {
|
|
479
|
+
functionBody.unshift(destructionOtherPropertiesDeclaration);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
let modifiedPropertyCount = 0;
|
|
483
|
+
for (const [matchedPropName, paramsProperty] of propertiesMap) {
|
|
484
|
+
if (!utils_1.TARGET_PROP_NAMES.has(matchedPropName)) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
// In client component, if the param is already wrapped with `use()`, skip the transformation
|
|
488
|
+
if (isClientComponent) {
|
|
489
|
+
let shouldSkip = false;
|
|
490
|
+
const propPaths = j(path).find(j.Identifier, {
|
|
491
|
+
name: matchedPropName,
|
|
492
|
+
});
|
|
493
|
+
for (const propPath of propPaths.paths()) {
|
|
494
|
+
if (isParentUseCallExpression(propPath, j)) {
|
|
495
|
+
// Skip transformation
|
|
496
|
+
shouldSkip = true;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (shouldSkip) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const paramsPropertyName = j.Identifier.check(paramsProperty)
|
|
505
|
+
? paramsProperty.name
|
|
506
|
+
: null;
|
|
507
|
+
const paramPropertyName = paramsPropertyName || matchedPropName;
|
|
508
|
+
// if propName is not used in lower scope, and it stars with unused prefix `_`,
|
|
509
|
+
// also skip the transformation
|
|
510
|
+
const functionBodyPath = path.get('body');
|
|
511
|
+
const hasUsedInBody = j(functionBodyPath)
|
|
512
|
+
.find(j.Identifier, {
|
|
513
|
+
name: paramPropertyName,
|
|
514
|
+
})
|
|
515
|
+
.size() > 0;
|
|
516
|
+
if (!hasUsedInBody && paramPropertyName.startsWith('_'))
|
|
517
|
+
continue;
|
|
518
|
+
// Search the usage of propName in the function body,
|
|
519
|
+
// if they're all awaited or wrapped with use(), skip the transformation
|
|
520
|
+
const propUsages = j(functionBodyPath).find(j.Identifier, {
|
|
521
|
+
name: paramPropertyName,
|
|
522
|
+
});
|
|
523
|
+
// if there's usage of the propName, then do the check
|
|
524
|
+
if (propUsages.size()) {
|
|
525
|
+
let hasMissingAwaited = false;
|
|
526
|
+
propUsages.forEach((propUsage) => {
|
|
527
|
+
// If the parent is not AwaitExpression, it's not awaited
|
|
528
|
+
const isAwaited = propUsage.parentPath?.value.type === 'AwaitExpression';
|
|
529
|
+
const isAwaitedByUse = isParentUseCallExpression(propUsage, j);
|
|
530
|
+
if (!isAwaited && !isAwaitedByUse) {
|
|
531
|
+
hasMissingAwaited = true;
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
// If all the usages of parm are awaited, skip the transformation
|
|
536
|
+
if (!hasMissingAwaited) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
modifiedPropertyCount++;
|
|
541
|
+
const propNameIdentifier = j.identifier(matchedPropName);
|
|
542
|
+
const propsIdentifier = j.identifier(propsIdentifierName);
|
|
543
|
+
const accessedPropIdExpr = j.memberExpression(propsIdentifier, propNameIdentifier);
|
|
544
|
+
// Check param property value, if it's destructed, we need to destruct it as well
|
|
545
|
+
// e.g.
|
|
546
|
+
// input: Page({ params: { slug } })
|
|
547
|
+
// output: const { slug } = await props.params; rather than const props = await props.params;
|
|
548
|
+
const uid = functionName + ':' + paramPropertyName;
|
|
549
|
+
if (paramsProperty?.type === 'ObjectPattern') {
|
|
550
|
+
const objectPattern = paramsProperty;
|
|
551
|
+
const objectPatternProperties = objectPattern.properties;
|
|
552
|
+
// destruct the object pattern, e.g. { slug } => const { slug } = params;
|
|
553
|
+
const destructedObjectPattern = j.variableDeclaration('const', [
|
|
554
|
+
j.variableDeclarator(j.objectPattern(objectPatternProperties.map((prop) => {
|
|
555
|
+
if (prop.type === 'Property' &&
|
|
556
|
+
prop.key.type === 'Identifier') {
|
|
557
|
+
return j.objectProperty(j.identifier(prop.key.name), j.identifier(prop.key.name));
|
|
558
|
+
}
|
|
559
|
+
return prop;
|
|
560
|
+
})), propNameIdentifier),
|
|
561
|
+
]);
|
|
562
|
+
if (!insertedDestructPropNames.has(uid) && functionBody) {
|
|
563
|
+
functionBody.unshift(destructedObjectPattern);
|
|
564
|
+
insertedDestructPropNames.add(uid);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (isAsyncFunc) {
|
|
568
|
+
// If it's async function, add await to the async props.<propName>
|
|
569
|
+
const paramAssignment = j.variableDeclaration('const', [
|
|
570
|
+
j.variableDeclarator(j.identifier(paramPropertyName), j.awaitExpression(accessedPropIdExpr)),
|
|
571
|
+
]);
|
|
572
|
+
if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
|
|
573
|
+
functionBody.unshift(paramAssignment);
|
|
574
|
+
insertedRenamedPropFunctionNames.add(uid);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
// const isFromExport = true
|
|
579
|
+
if (!isClientComponent) {
|
|
580
|
+
// If it's export function, populate the function to async
|
|
581
|
+
if ((0, utils_1.isFunctionType)(node.type) &&
|
|
582
|
+
// Make TS happy
|
|
583
|
+
'async' in node) {
|
|
584
|
+
node.async = true;
|
|
585
|
+
(0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
|
|
586
|
+
// Insert `const <propName> = await props.<propName>;` at the beginning of the function body
|
|
587
|
+
const paramAssignment = j.variableDeclaration('const', [
|
|
588
|
+
j.variableDeclarator(j.identifier(paramPropertyName), j.awaitExpression(accessedPropIdExpr)),
|
|
589
|
+
]);
|
|
590
|
+
if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
|
|
591
|
+
functionBody.unshift(paramAssignment);
|
|
592
|
+
insertedRenamedPropFunctionNames.add(uid);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
const paramAssignment = j.variableDeclaration('const', [
|
|
598
|
+
j.variableDeclarator(j.identifier(paramPropertyName), j.callExpression(j.identifier('use'), [accessedPropIdExpr])),
|
|
599
|
+
]);
|
|
600
|
+
if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
|
|
601
|
+
needsReactUseImport = true;
|
|
602
|
+
functionBody.unshift(paramAssignment);
|
|
603
|
+
insertedRenamedPropFunctionNames.add(uid);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return modifiedPropertyCount > 0;
|
|
609
|
+
}
|
|
610
|
+
const defaultExportsDeclarations = root.find(j.ExportDefaultDeclaration);
|
|
611
|
+
defaultExportsDeclarations.forEach((path) => {
|
|
612
|
+
const functionPath = (0, utils_1.getFunctionPathFromExportPath)(path, j, root, () => true);
|
|
613
|
+
if (functionPath) {
|
|
614
|
+
renameAsyncPropIfExisted(functionPath, true);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
// Matching Next.js functional named export of route entry:
|
|
618
|
+
// - export function <named>(...) { ... }
|
|
619
|
+
// - export const <named> = ...
|
|
620
|
+
const namedExportDeclarations = root.find(j.ExportNamedDeclaration);
|
|
621
|
+
namedExportDeclarations.forEach((path) => {
|
|
622
|
+
const functionPath = (0, utils_1.getFunctionPathFromExportPath)(path, j, root, (idName) => utils_1.TARGET_NAMED_EXPORTS.has(idName));
|
|
623
|
+
if (functionPath) {
|
|
624
|
+
renameAsyncPropIfExisted(functionPath, false);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
const isClientComponent = (0, utils_1.determineClientDirective)(root, j);
|
|
629
|
+
// Apply to `params` and `searchParams`
|
|
630
|
+
processAsyncPropOfEntryFile(isClientComponent);
|
|
631
|
+
// Add import { use } from 'react' if needed and not already imported
|
|
632
|
+
if (needsReactUseImport) {
|
|
633
|
+
(0, utils_1.insertReactUseImport)(root, j);
|
|
634
|
+
}
|
|
635
|
+
const commented = commentOnMatchedReExports(root, j);
|
|
636
|
+
modified ||= commented;
|
|
637
|
+
return modified ? root.toSource() : null;
|
|
638
|
+
}
|
|
639
|
+
function findAllTypes(root, j, typeName) {
|
|
640
|
+
const types = {
|
|
641
|
+
interfaces: [],
|
|
642
|
+
typeAliases: [],
|
|
643
|
+
imports: [],
|
|
644
|
+
references: [],
|
|
645
|
+
};
|
|
646
|
+
// Step 1: Find all interface declarations with the specified name
|
|
647
|
+
root
|
|
648
|
+
.find(j.TSInterfaceDeclaration, {
|
|
649
|
+
id: {
|
|
650
|
+
type: 'Identifier',
|
|
651
|
+
name: typeName,
|
|
652
|
+
},
|
|
653
|
+
})
|
|
654
|
+
.forEach((path) => {
|
|
655
|
+
types.interfaces.push(path.node);
|
|
656
|
+
});
|
|
657
|
+
// Step 2: Find all type alias declarations with the specified name
|
|
658
|
+
root
|
|
659
|
+
.find(j.TSTypeAliasDeclaration, {
|
|
660
|
+
id: {
|
|
661
|
+
type: 'Identifier',
|
|
662
|
+
name: typeName,
|
|
663
|
+
},
|
|
664
|
+
})
|
|
665
|
+
.forEach((path) => {
|
|
666
|
+
types.typeAliases.push(path.node);
|
|
667
|
+
});
|
|
668
|
+
// Step 3: Find all imported types with the specified name
|
|
669
|
+
root
|
|
670
|
+
.find(j.ImportSpecifier, {
|
|
671
|
+
imported: {
|
|
672
|
+
type: 'Identifier',
|
|
673
|
+
name: typeName,
|
|
674
|
+
},
|
|
675
|
+
})
|
|
676
|
+
.forEach((path) => {
|
|
677
|
+
types.imports.push(path.node);
|
|
678
|
+
});
|
|
679
|
+
// Step 4: Find all references to the specified type
|
|
680
|
+
root
|
|
681
|
+
.find(j.TSTypeReference, {
|
|
682
|
+
typeName: {
|
|
683
|
+
type: 'Identifier',
|
|
684
|
+
name: typeName,
|
|
685
|
+
},
|
|
686
|
+
})
|
|
687
|
+
.forEach((path) => {
|
|
688
|
+
types.references.push(path.node);
|
|
689
|
+
});
|
|
690
|
+
return types;
|
|
691
|
+
}
|
|
692
|
+
function commentSpreadProps(path, propsIdentifierName, j) {
|
|
693
|
+
let modified = false;
|
|
694
|
+
const functionBody = findFunctionBody(path);
|
|
695
|
+
const functionBodyCollection = j(functionBody);
|
|
696
|
+
// Find all the usage of spreading properties of `props`
|
|
697
|
+
const jsxSpreadProperties = functionBodyCollection.find(j.JSXSpreadAttribute, { argument: { name: propsIdentifierName } });
|
|
698
|
+
const objSpreadProperties = functionBodyCollection.find(j.SpreadElement, {
|
|
699
|
+
argument: { name: propsIdentifierName },
|
|
700
|
+
});
|
|
701
|
+
const comment = ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} '${propsIdentifierName}' is used with spread syntax (...). Any asynchronous properties of '${propsIdentifierName}' must be awaited when accessed. `;
|
|
702
|
+
// Add comment before it
|
|
703
|
+
jsxSpreadProperties.forEach((spread) => {
|
|
704
|
+
const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
|
|
705
|
+
if (inserted)
|
|
706
|
+
modified = true;
|
|
707
|
+
});
|
|
708
|
+
objSpreadProperties.forEach((spread) => {
|
|
709
|
+
const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
|
|
710
|
+
if (inserted)
|
|
711
|
+
modified = true;
|
|
712
|
+
});
|
|
713
|
+
return modified;
|
|
714
|
+
}
|
|
715
|
+
//# sourceMappingURL=next-async-dynamic-prop.js.map
|