@next/codemod 15.0.0-canary.172 → 15.0.0-canary.173
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/package.json
CHANGED
|
@@ -2,8 +2,25 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.transformDynamicAPI = transformDynamicAPI;
|
|
4
4
|
const utils_1 = require("./utils");
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const DYNAMIC_IMPORT_WARN_COMMENT = ` The APIs under 'next/headers' are async now, need to be manually awaited. `;
|
|
6
|
+
function findDynamicImportsAndComment(root, j) {
|
|
7
|
+
let modified = false;
|
|
8
|
+
// find all the dynamic imports of `next/headers`,
|
|
9
|
+
// and add a comment to the import expression to inform this needs to be manually handled
|
|
10
|
+
// find all the dynamic imports of `next/cookies`,
|
|
11
|
+
// Notice, import() is not handled as ImportExpression in current jscodeshift version,
|
|
12
|
+
// we need to use CallExpression to capture the dynamic imports.
|
|
13
|
+
const importPaths = root.find(j.CallExpression, {
|
|
14
|
+
callee: {
|
|
15
|
+
type: 'Import',
|
|
16
|
+
},
|
|
17
|
+
arguments: [{ value: 'next/headers' }],
|
|
18
|
+
});
|
|
19
|
+
importPaths.forEach((path) => {
|
|
20
|
+
(0, utils_1.insertCommentOnce)(path, j, DYNAMIC_IMPORT_WARN_COMMENT);
|
|
21
|
+
modified = true;
|
|
22
|
+
});
|
|
23
|
+
return modified;
|
|
7
24
|
}
|
|
8
25
|
function transformDynamicAPI(source, api, filePath) {
|
|
9
26
|
const j = api.jscodeshift.withParser('tsx');
|
|
@@ -32,22 +49,26 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
32
49
|
if (!isImportedTopLevel) {
|
|
33
50
|
return;
|
|
34
51
|
}
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
let parentFunctionPath = (0, utils_1.findClosetParentFunctionScope)(path, j);
|
|
53
|
+
// We found the parent scope is not a function
|
|
54
|
+
let parentFunctionNode;
|
|
55
|
+
if (parentFunctionPath) {
|
|
56
|
+
if ((0, utils_1.isFunctionScope)(parentFunctionPath, j)) {
|
|
57
|
+
parentFunctionNode = parentFunctionPath.node;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const scopeNode = parentFunctionPath.node;
|
|
61
|
+
if (scopeNode.type === 'ReturnStatement' &&
|
|
62
|
+
(0, utils_1.isFunctionType)(scopeNode.argument.type)) {
|
|
63
|
+
parentFunctionNode = scopeNode.argument;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
44
66
|
}
|
|
45
|
-
const isAsyncFunction =
|
|
46
|
-
.nodes()
|
|
47
|
-
.some((node) => node.async);
|
|
67
|
+
const isAsyncFunction = parentFunctionNode?.async || false;
|
|
48
68
|
const isCallAwaited = path.parentPath?.node?.type === 'AwaitExpression';
|
|
49
69
|
const hasChainAccess = path.parentPath.value.type === 'MemberExpression' &&
|
|
50
70
|
path.parentPath.value.object === path.node;
|
|
71
|
+
const closetScope = j(path).closestScope();
|
|
51
72
|
// For cookies/headers API, only transform server and shared components
|
|
52
73
|
if (isAsyncFunction) {
|
|
53
74
|
if (!isCallAwaited) {
|
|
@@ -55,7 +76,7 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
55
76
|
const expr = j.awaitExpression(
|
|
56
77
|
// add parentheses to wrap the function call
|
|
57
78
|
j.callExpression(j.identifier(asyncRequestApiName), []));
|
|
58
|
-
j(path).replaceWith(
|
|
79
|
+
j(path).replaceWith((0, utils_1.wrapParentheseIfNeeded)(hasChainAccess, j, expr));
|
|
59
80
|
modified = true;
|
|
60
81
|
}
|
|
61
82
|
}
|
|
@@ -94,7 +115,7 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
94
115
|
}
|
|
95
116
|
if (canConvertToAsync) {
|
|
96
117
|
const expr = j.awaitExpression(j.callExpression(j.identifier(asyncRequestApiName), []));
|
|
97
|
-
j(path).replaceWith(
|
|
118
|
+
j(path).replaceWith((0, utils_1.wrapParentheseIfNeeded)(hasChainAccess, j, expr));
|
|
98
119
|
(0, utils_1.turnFunctionReturnTypeToAsync)(closetScopePath.node, j);
|
|
99
120
|
modified = true;
|
|
100
121
|
}
|
|
@@ -102,10 +123,8 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
102
123
|
}
|
|
103
124
|
else {
|
|
104
125
|
// if parent is function and it's a hook, which starts with 'use', wrap the api call with 'use()'
|
|
105
|
-
const parentFunction =
|
|
106
|
-
|
|
107
|
-
j(path).closest(j.ArrowFunctionExpression);
|
|
108
|
-
if (parentFunction.size() > 0) {
|
|
126
|
+
const parentFunction = (0, utils_1.findClosetParentFunctionScope)(path, j);
|
|
127
|
+
if (parentFunction) {
|
|
109
128
|
const parentFunctionName = parentFunction.get().node.id?.name;
|
|
110
129
|
const isParentFunctionHook = parentFunctionName?.startsWith('use');
|
|
111
130
|
if (isParentFunctionHook) {
|
|
@@ -115,11 +134,11 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
115
134
|
needsReactUseImport = true;
|
|
116
135
|
}
|
|
117
136
|
else {
|
|
118
|
-
castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes);
|
|
137
|
+
castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` TODO: please manually await this call, if it's a server component, you can turn it to async function `);
|
|
119
138
|
}
|
|
120
139
|
}
|
|
121
140
|
else {
|
|
122
|
-
castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes);
|
|
141
|
+
castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ' TODO: please manually await this call, codemod cannot transform due to undetermined async scope ');
|
|
123
142
|
}
|
|
124
143
|
modified = true;
|
|
125
144
|
}
|
|
@@ -127,17 +146,21 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
127
146
|
});
|
|
128
147
|
}
|
|
129
148
|
const isClientComponent = (0, utils_1.determineClientDirective)(root, j, source);
|
|
130
|
-
const importedNextAsyncRequestApisMapping = findImportMappingFromNextHeaders(root, j);
|
|
131
149
|
// Only transform the valid calls in server or shared components
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
if (isClientComponent)
|
|
151
|
+
return null;
|
|
152
|
+
// Import declaration case, e.g. import { cookies } from 'next/headers'
|
|
153
|
+
const importedNextAsyncRequestApisMapping = findImportMappingFromNextHeaders(root, j);
|
|
154
|
+
for (const originName in importedNextAsyncRequestApisMapping) {
|
|
155
|
+
const aliasName = importedNextAsyncRequestApisMapping[originName];
|
|
156
|
+
processAsyncApiCalls(aliasName, originName);
|
|
157
|
+
}
|
|
158
|
+
// Add import { use } from 'react' if needed and not already imported
|
|
159
|
+
if (needsReactUseImport) {
|
|
160
|
+
(0, utils_1.insertReactUseImport)(root, j);
|
|
161
|
+
}
|
|
162
|
+
if (findDynamicImportsAndComment(root, j)) {
|
|
163
|
+
modified = true;
|
|
141
164
|
}
|
|
142
165
|
return modified ? root.toSource() : null;
|
|
143
166
|
}
|
|
@@ -147,9 +170,12 @@ const API_CAST_TYPE_MAP = {
|
|
|
147
170
|
headers: 'UnsafeUnwrappedHeaders',
|
|
148
171
|
draftMode: 'UnsafeUnwrappedDraftMode',
|
|
149
172
|
};
|
|
150
|
-
function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes) {
|
|
173
|
+
function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, customMessage) {
|
|
151
174
|
const isTsFile = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
|
152
175
|
if (isTsFile) {
|
|
176
|
+
// if the path of call expression is already being awaited, no need to cast
|
|
177
|
+
if (path.parentPath?.node?.type === 'AwaitExpression')
|
|
178
|
+
return;
|
|
153
179
|
/* Do type cast for headers, cookies, draftMode
|
|
154
180
|
import {
|
|
155
181
|
type UnsafeUnwrappedHeaders,
|
|
@@ -194,7 +220,7 @@ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, in
|
|
|
194
220
|
else {
|
|
195
221
|
// Otherwise for JS file, leave a message to the user to manually handle the transformation
|
|
196
222
|
path.node.comments = [
|
|
197
|
-
j.commentBlock(
|
|
223
|
+
j.commentBlock(customMessage),
|
|
198
224
|
...(path.node.comments || []),
|
|
199
225
|
];
|
|
200
226
|
}
|
|
@@ -3,16 +3,95 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.transformDynamicProps = transformDynamicProps;
|
|
4
4
|
const utils_1 = require("./utils");
|
|
5
5
|
const PAGE_PROPS = 'props';
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
function findFunctionBody(path) {
|
|
7
|
+
let functionBody = path.node.body;
|
|
8
|
+
if (functionBody && functionBody.type === 'BlockStatement') {
|
|
9
|
+
return functionBody.body;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function awaitMemberAccessOfProp(propIdName, path, j) {
|
|
14
|
+
// search the member access of the prop
|
|
15
|
+
const functionBody = findFunctionBody(path);
|
|
16
|
+
const memberAccess = j(functionBody).find(j.MemberExpression, {
|
|
17
|
+
object: {
|
|
18
|
+
type: 'Identifier',
|
|
19
|
+
name: propIdName,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
let hasAwaited = false;
|
|
23
|
+
// await each member access
|
|
24
|
+
memberAccess.forEach((memberAccessPath) => {
|
|
25
|
+
const member = memberAccessPath.value;
|
|
26
|
+
// check if it's already awaited
|
|
27
|
+
if (memberAccessPath.parentPath?.value.type === 'AwaitExpression') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const awaitedExpr = j.awaitExpression(member);
|
|
31
|
+
const awaitMemberAccess = (0, utils_1.wrapParentheseIfNeeded)(true, j, awaitedExpr);
|
|
32
|
+
memberAccessPath.replace(awaitMemberAccess);
|
|
33
|
+
hasAwaited = true;
|
|
34
|
+
});
|
|
35
|
+
// If there's any awaited member access, we need to make the function async
|
|
36
|
+
if (hasAwaited) {
|
|
37
|
+
if (!path.value.async) {
|
|
38
|
+
if ('async' in path.value) {
|
|
39
|
+
path.value.async = true;
|
|
40
|
+
(0, utils_1.turnFunctionReturnTypeToAsync)(path.value, j);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return hasAwaited;
|
|
45
|
+
}
|
|
46
|
+
function applyUseAndRenameAccessedProp(propIdName, path, j) {
|
|
47
|
+
// search the member access of the prop, and rename the member access to the member value
|
|
48
|
+
// e.g.
|
|
49
|
+
// props.params => params
|
|
50
|
+
// props.params.foo => params.foo
|
|
51
|
+
// props.searchParams.search => searchParams.search
|
|
52
|
+
let modified = false;
|
|
53
|
+
const functionBody = findFunctionBody(path);
|
|
54
|
+
const memberAccess = j(functionBody).find(j.MemberExpression, {
|
|
55
|
+
object: {
|
|
56
|
+
type: 'Identifier',
|
|
57
|
+
name: propIdName,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const accessedNames = [];
|
|
61
|
+
// rename each member access
|
|
62
|
+
memberAccess.forEach((memberAccessPath) => {
|
|
63
|
+
const member = memberAccessPath.value;
|
|
64
|
+
const memberProperty = member.property;
|
|
65
|
+
if (j.Identifier.check(memberProperty)) {
|
|
66
|
+
accessedNames.push(memberProperty.name);
|
|
67
|
+
}
|
|
68
|
+
else if (j.MemberExpression.check(memberProperty)) {
|
|
69
|
+
let currentMember = memberProperty;
|
|
70
|
+
if (j.Identifier.check(currentMember.object)) {
|
|
71
|
+
accessedNames.push(currentMember.object.name);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
memberAccessPath.replace(memberProperty);
|
|
75
|
+
});
|
|
76
|
+
// If there's any renamed member access, need to call `use()` onto member access
|
|
77
|
+
// e.g. ['params'] => insert `const params = use(props.params)`
|
|
78
|
+
if (accessedNames.length > 0) {
|
|
79
|
+
const accessedPropId = j.identifier(propIdName);
|
|
80
|
+
const accessedProp = j.memberExpression(accessedPropId, j.identifier(accessedNames[0]));
|
|
81
|
+
const useCall = j.callExpression(j.identifier('use'), [accessedProp]);
|
|
82
|
+
const useDeclaration = j.variableDeclaration('const', [
|
|
83
|
+
j.variableDeclarator(j.identifier(accessedNames[0]), useCall),
|
|
84
|
+
]);
|
|
85
|
+
if (functionBody) {
|
|
86
|
+
functionBody.unshift(useDeclaration);
|
|
87
|
+
}
|
|
88
|
+
modified = true;
|
|
89
|
+
}
|
|
90
|
+
return modified;
|
|
13
91
|
}
|
|
14
92
|
function transformDynamicProps(source, api, _filePath) {
|
|
15
93
|
let modified = false;
|
|
94
|
+
let modifiedPropArgument = false;
|
|
16
95
|
const j = api.jscodeshift.withParser('tsx');
|
|
17
96
|
const root = j(source);
|
|
18
97
|
// Check if 'use' from 'react' needs to be imported
|
|
@@ -25,18 +104,22 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
25
104
|
let insertedRenamedPropFunctionNames = new Set();
|
|
26
105
|
function processAsyncPropOfEntryFile(isClientComponent) {
|
|
27
106
|
// find `params` and `searchParams` in file, and transform the access to them
|
|
28
|
-
function renameAsyncPropIfExisted(path) {
|
|
29
|
-
const decl = path.value
|
|
30
|
-
if (decl.type !== 'FunctionDeclaration' &&
|
|
31
|
-
decl.type !== 'FunctionExpression' &&
|
|
32
|
-
decl.type !== 'ArrowFunctionExpression') {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
107
|
+
function renameAsyncPropIfExisted(path, isDefaultExport) {
|
|
108
|
+
const decl = path.value;
|
|
35
109
|
const params = decl.params;
|
|
110
|
+
const functionName = decl.id?.name || 'default';
|
|
111
|
+
// target properties mapping, only contains `params` and `searchParams`
|
|
36
112
|
const propertiesMap = new Map();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
113
|
+
let allProperties = [];
|
|
114
|
+
// generateMetadata API has 2 params
|
|
115
|
+
if (functionName === 'generateMetadata') {
|
|
116
|
+
if (params.length > 2 || params.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Page/Layout/Route handlers have 1 param
|
|
121
|
+
if (params.length !== 1)
|
|
122
|
+
return;
|
|
40
123
|
}
|
|
41
124
|
const propsIdentifier = (0, utils_1.generateUniqueIdentifier)(PAGE_PROPS, path, j);
|
|
42
125
|
const currentParam = params[0];
|
|
@@ -44,19 +127,25 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
44
127
|
if (currentParam.type === 'ObjectPattern') {
|
|
45
128
|
// Validate if the properties are not `params` and `searchParams`,
|
|
46
129
|
// if they are, quit the transformation
|
|
130
|
+
let foundTargetProp = false;
|
|
47
131
|
for (const prop of currentParam.properties) {
|
|
48
132
|
if ('key' in prop && prop.key.type === 'Identifier') {
|
|
49
133
|
const propName = prop.key.name;
|
|
50
|
-
if (
|
|
51
|
-
|
|
134
|
+
if (utils_1.TARGET_PROP_NAMES.has(propName)) {
|
|
135
|
+
foundTargetProp = true;
|
|
52
136
|
}
|
|
53
137
|
}
|
|
54
138
|
}
|
|
139
|
+
// If there's no `params` or `searchParams` matched, return
|
|
140
|
+
if (!foundTargetProp)
|
|
141
|
+
return;
|
|
142
|
+
allProperties = currentParam.properties;
|
|
55
143
|
currentParam.properties.forEach((prop) => {
|
|
56
144
|
if (
|
|
57
145
|
// Could be `Property` or `ObjectProperty`
|
|
58
146
|
'key' in prop &&
|
|
59
|
-
prop.key.type === 'Identifier'
|
|
147
|
+
prop.key.type === 'Identifier' &&
|
|
148
|
+
utils_1.TARGET_PROP_NAMES.has(prop.key.name)) {
|
|
60
149
|
const value = 'value' in prop ? prop.value : null;
|
|
61
150
|
propertiesMap.set(prop.key.name, value);
|
|
62
151
|
}
|
|
@@ -141,41 +230,139 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
141
230
|
// Override the first param to `props`
|
|
142
231
|
params[0] = propsIdentifier;
|
|
143
232
|
modified = true;
|
|
233
|
+
modifiedPropArgument = true;
|
|
144
234
|
}
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
235
|
+
else if (currentParam.type === 'Identifier') {
|
|
236
|
+
// case of accessing the props.params.<name>:
|
|
237
|
+
// Page(props) {}
|
|
238
|
+
// generateMetadata(props, parent?) {}
|
|
239
|
+
const argName = currentParam.name;
|
|
240
|
+
if (isClientComponent) {
|
|
241
|
+
const modifiedProp = applyUseAndRenameAccessedProp(argName, path, j);
|
|
242
|
+
if (modifiedProp) {
|
|
243
|
+
needsReactUseImport = true;
|
|
244
|
+
modified = true;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
modified = awaitMemberAccessOfProp(argName, path, j);
|
|
157
249
|
}
|
|
250
|
+
// cases of passing down `props` into any function
|
|
251
|
+
// Page(props) { callback(props) }
|
|
252
|
+
// search for all the argument of CallExpression, where currentParam is one of the arguments
|
|
253
|
+
const callExpressions = j(path).find(j.CallExpression, {
|
|
254
|
+
arguments: (args) => {
|
|
255
|
+
return args.some((arg) => {
|
|
256
|
+
return (j.Identifier.check(arg) &&
|
|
257
|
+
arg.name === argName &&
|
|
258
|
+
arg.type === 'Identifier');
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
// Add a comment to warn users that properties of `props` need to be awaited when accessed
|
|
263
|
+
callExpressions.forEach((callExpression) => {
|
|
264
|
+
// find the argument `currentParam`
|
|
265
|
+
const args = callExpression.value.arguments;
|
|
266
|
+
const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
|
|
267
|
+
// insert a comment to the argument
|
|
268
|
+
const comment = j.commentBlock(` '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `, true, false);
|
|
269
|
+
propPassedAsArg.comments = [
|
|
270
|
+
comment,
|
|
271
|
+
...(propPassedAsArg.comments || []),
|
|
272
|
+
];
|
|
273
|
+
modified = true;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (modifiedPropArgument) {
|
|
277
|
+
resolveAsyncProp(path, propertiesMap, propsIdentifier.name, allProperties, isDefaultExport);
|
|
158
278
|
}
|
|
159
|
-
return functionBody;
|
|
160
279
|
}
|
|
161
280
|
// Helper function to insert `const params = await asyncParams;` at the beginning of the function body
|
|
162
|
-
function resolveAsyncProp(path, propertiesMap, propsIdentifierName) {
|
|
163
|
-
const
|
|
281
|
+
function resolveAsyncProp(path, propertiesMap, propsIdentifierName, allProperties, isDefaultExport) {
|
|
282
|
+
const node = path.value;
|
|
164
283
|
// If it's sync default export, and it's also server component, make the function async
|
|
165
|
-
if (isDefaultExport &&
|
|
166
|
-
!
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
284
|
+
if (isDefaultExport && !isClientComponent) {
|
|
285
|
+
if (!node.async) {
|
|
286
|
+
if ('async' in node) {
|
|
287
|
+
node.async = true;
|
|
288
|
+
(0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
|
|
289
|
+
}
|
|
171
290
|
}
|
|
172
291
|
}
|
|
173
|
-
const isAsyncFunc =
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
const
|
|
292
|
+
const isAsyncFunc = !!node.async;
|
|
293
|
+
const functionName = path.value.id?.name || 'default';
|
|
294
|
+
const functionBody = findFunctionBody(path);
|
|
295
|
+
const hasOtherProperties = allProperties.length > propertiesMap.size;
|
|
296
|
+
function createDestructuringDeclaration(properties, destructPropsIdentifierName) {
|
|
297
|
+
const propsToKeep = [];
|
|
298
|
+
let restProperty = null;
|
|
299
|
+
// Iterate over the destructured properties
|
|
300
|
+
properties.forEach((property) => {
|
|
301
|
+
if (j.ObjectProperty.check(property)) {
|
|
302
|
+
// Handle normal and computed properties
|
|
303
|
+
const keyName = j.Identifier.check(property.key)
|
|
304
|
+
? property.key.name
|
|
305
|
+
: j.Literal.check(property.key)
|
|
306
|
+
? property.key.value
|
|
307
|
+
: null; // for computed properties
|
|
308
|
+
if (typeof keyName === 'string') {
|
|
309
|
+
propsToKeep.push(property);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else if (j.RestElement.check(property)) {
|
|
313
|
+
restProperty = property;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
if (propsToKeep.length === 0 && !restProperty) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
if (restProperty) {
|
|
320
|
+
propsToKeep.push(restProperty);
|
|
321
|
+
}
|
|
322
|
+
return j.variableDeclaration('const', [
|
|
323
|
+
j.variableDeclarator(j.objectPattern(propsToKeep), j.identifier(destructPropsIdentifierName)),
|
|
324
|
+
]);
|
|
325
|
+
}
|
|
326
|
+
if (hasOtherProperties) {
|
|
327
|
+
/**
|
|
328
|
+
* If there are other properties, we need to keep the original param with destructuring
|
|
329
|
+
* e.g.
|
|
330
|
+
* input:
|
|
331
|
+
* Page({ params: { slug }, otherProp }) {
|
|
332
|
+
* const { slug } = await props.params;
|
|
333
|
+
* }
|
|
334
|
+
*
|
|
335
|
+
* output:
|
|
336
|
+
* Page(props) {
|
|
337
|
+
* const { otherProp } = props; // inserted
|
|
338
|
+
* // ...rest of the function body
|
|
339
|
+
* }
|
|
340
|
+
*/
|
|
341
|
+
const restProperties = allProperties.filter((prop) => {
|
|
342
|
+
const isTargetProps = 'key' in prop &&
|
|
343
|
+
prop.key.type === 'Identifier' &&
|
|
344
|
+
utils_1.TARGET_PROP_NAMES.has(prop.key.name);
|
|
345
|
+
return !isTargetProps;
|
|
346
|
+
});
|
|
347
|
+
const destructionOtherPropertiesDeclaration = createDestructuringDeclaration(restProperties, propsIdentifierName);
|
|
348
|
+
if (functionBody && destructionOtherPropertiesDeclaration) {
|
|
349
|
+
functionBody.unshift(destructionOtherPropertiesDeclaration);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const [matchedPropName, paramsProperty] of propertiesMap) {
|
|
353
|
+
if (!utils_1.TARGET_PROP_NAMES.has(matchedPropName)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const propRenamedId = j.Identifier.check(paramsProperty)
|
|
357
|
+
? paramsProperty.name
|
|
358
|
+
: null;
|
|
359
|
+
const propName = propRenamedId || matchedPropName;
|
|
360
|
+
// if propName is not used in lower scope, and it stars with unused prefix `_`,
|
|
361
|
+
// also skip the transformation
|
|
362
|
+
const hasDeclared = path.scope.declares(propName);
|
|
363
|
+
if (!hasDeclared && propName.startsWith('_'))
|
|
364
|
+
continue;
|
|
365
|
+
const propNameIdentifier = j.identifier(matchedPropName);
|
|
179
366
|
const propsIdentifier = j.identifier(propsIdentifierName);
|
|
180
367
|
const accessedPropId = j.memberExpression(propsIdentifier, propNameIdentifier);
|
|
181
368
|
// Check param property value, if it's destructed, we need to destruct it as well
|
|
@@ -212,14 +399,14 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
212
399
|
}
|
|
213
400
|
}
|
|
214
401
|
else {
|
|
215
|
-
const isFromExport =
|
|
216
|
-
if (
|
|
402
|
+
// const isFromExport = true
|
|
403
|
+
if (!isClientComponent) {
|
|
217
404
|
// If it's export function, populate the function to async
|
|
218
|
-
if ((0, utils_1.isFunctionType)(
|
|
405
|
+
if ((0, utils_1.isFunctionType)(node.type) &&
|
|
219
406
|
// Make TS happy
|
|
220
|
-
'async' in
|
|
221
|
-
|
|
222
|
-
(0, utils_1.turnFunctionReturnTypeToAsync)(
|
|
407
|
+
'async' in node) {
|
|
408
|
+
node.async = true;
|
|
409
|
+
(0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
|
|
223
410
|
// Insert `const <propName> = await props.<propName>;` at the beginning of the function body
|
|
224
411
|
const paramAssignment = j.variableDeclaration('const', [
|
|
225
412
|
j.variableDeclarator(j.identifier(propName), j.awaitExpression(accessedPropId)),
|
|
@@ -243,42 +430,23 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
243
430
|
}
|
|
244
431
|
}
|
|
245
432
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
type === 'ArrowFunctionExpression',
|
|
253
|
-
},
|
|
254
|
-
});
|
|
255
|
-
defaultExportFunctionDeclarations.forEach((path) => {
|
|
256
|
-
renameAsyncPropIfExisted(path);
|
|
433
|
+
const defaultExportsDeclarations = root.find(j.ExportDefaultDeclaration);
|
|
434
|
+
defaultExportsDeclarations.forEach((path) => {
|
|
435
|
+
const functionPath = (0, utils_1.getFunctionPathFromExportPath)(path, j, root, () => true);
|
|
436
|
+
if (functionPath) {
|
|
437
|
+
renameAsyncPropIfExisted(functionPath, true);
|
|
438
|
+
}
|
|
257
439
|
});
|
|
258
440
|
// Matching Next.js functional named export of route entry:
|
|
259
441
|
// - export function <named>(...) { ... }
|
|
260
442
|
// - export const <named> = ...
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return utils_1.TARGET_NAMED_EXPORTS.has(idName);
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
targetNamedExportDeclarations.forEach((path) => {
|
|
273
|
-
renameAsyncPropIfExisted(path);
|
|
443
|
+
const namedExportDeclarations = root.find(j.ExportNamedDeclaration);
|
|
444
|
+
namedExportDeclarations.forEach((path) => {
|
|
445
|
+
const functionPath = (0, utils_1.getFunctionPathFromExportPath)(path, j, root, (idName) => utils_1.TARGET_NAMED_EXPORTS.has(idName));
|
|
446
|
+
if (functionPath) {
|
|
447
|
+
renameAsyncPropIfExisted(functionPath, false);
|
|
448
|
+
}
|
|
274
449
|
});
|
|
275
|
-
// TDOO: handle targetNamedDeclarators
|
|
276
|
-
// const targetNamedDeclarators = root.find(
|
|
277
|
-
// j.VariableDeclarator,
|
|
278
|
-
// (node) =>
|
|
279
|
-
// node.id.type === 'Identifier' &&
|
|
280
|
-
// TARGET_NAMED_EXPORTS.has(node.id.name)
|
|
281
|
-
// )
|
|
282
450
|
}
|
|
283
451
|
const isClientComponent = (0, utils_1.determineClientDirective)(root, j, source);
|
|
284
452
|
// Apply to `params` and `searchParams`
|
|
@@ -8,6 +8,11 @@ exports.isPromiseType = isPromiseType;
|
|
|
8
8
|
exports.turnFunctionReturnTypeToAsync = turnFunctionReturnTypeToAsync;
|
|
9
9
|
exports.insertReactUseImport = insertReactUseImport;
|
|
10
10
|
exports.generateUniqueIdentifier = generateUniqueIdentifier;
|
|
11
|
+
exports.isFunctionScope = isFunctionScope;
|
|
12
|
+
exports.findClosetParentFunctionScope = findClosetParentFunctionScope;
|
|
13
|
+
exports.getFunctionPathFromExportPath = getFunctionPathFromExportPath;
|
|
14
|
+
exports.wrapParentheseIfNeeded = wrapParentheseIfNeeded;
|
|
15
|
+
exports.insertCommentOnce = insertCommentOnce;
|
|
11
16
|
exports.TARGET_NAMED_EXPORTS = new Set([
|
|
12
17
|
// For custom route
|
|
13
18
|
'GET',
|
|
@@ -195,4 +200,151 @@ function generateUniqueIdentifier(defaultIdName, path, j) {
|
|
|
195
200
|
const propsIdentifier = j.identifier(idName);
|
|
196
201
|
return propsIdentifier;
|
|
197
202
|
}
|
|
203
|
+
function isFunctionScope(path, j) {
|
|
204
|
+
if (!path)
|
|
205
|
+
return false;
|
|
206
|
+
const node = path.node;
|
|
207
|
+
// Check if the node is a function (declaration, expression, or arrow function)
|
|
208
|
+
return (j.FunctionDeclaration.check(node) ||
|
|
209
|
+
j.FunctionExpression.check(node) ||
|
|
210
|
+
j.ArrowFunctionExpression.check(node));
|
|
211
|
+
}
|
|
212
|
+
function findClosetParentFunctionScope(path, j) {
|
|
213
|
+
let parentFunctionPath = path.scope.path;
|
|
214
|
+
while (parentFunctionPath && !isFunctionScope(parentFunctionPath, j)) {
|
|
215
|
+
parentFunctionPath = parentFunctionPath.parent;
|
|
216
|
+
}
|
|
217
|
+
return parentFunctionPath;
|
|
218
|
+
}
|
|
219
|
+
function getFunctionNodeFromBinding(bindingPath, idName, j, root) {
|
|
220
|
+
const bindingNode = bindingPath.node;
|
|
221
|
+
if (j.FunctionDeclaration.check(bindingNode) ||
|
|
222
|
+
j.FunctionExpression.check(bindingNode) ||
|
|
223
|
+
j.ArrowFunctionExpression.check(bindingNode)) {
|
|
224
|
+
return bindingPath;
|
|
225
|
+
}
|
|
226
|
+
else if (j.VariableDeclarator.check(bindingNode)) {
|
|
227
|
+
const init = bindingNode.init;
|
|
228
|
+
// If the initializer is a function (arrow or function expression), record it
|
|
229
|
+
if (j.FunctionExpression.check(init) ||
|
|
230
|
+
j.ArrowFunctionExpression.check(init)) {
|
|
231
|
+
return bindingPath.get('init');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else if (j.Identifier.check(bindingNode)) {
|
|
235
|
+
const variablePath = root.find(j.VariableDeclaration, {
|
|
236
|
+
// declarations, each is VariableDeclarator
|
|
237
|
+
declarations: [
|
|
238
|
+
{
|
|
239
|
+
// VariableDeclarator
|
|
240
|
+
type: 'VariableDeclarator',
|
|
241
|
+
// id is Identifier
|
|
242
|
+
id: {
|
|
243
|
+
type: 'Identifier',
|
|
244
|
+
name: idName,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
if (variablePath.size()) {
|
|
250
|
+
const variableDeclarator = variablePath.get()?.node?.declarations?.[0];
|
|
251
|
+
if (j.VariableDeclarator.check(variableDeclarator)) {
|
|
252
|
+
const init = variableDeclarator.init;
|
|
253
|
+
if (j.FunctionExpression.check(init) ||
|
|
254
|
+
j.ArrowFunctionExpression.check(init)) {
|
|
255
|
+
return variablePath.get('declarations', 0, 'init');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const functionDeclarations = root.find(j.FunctionDeclaration, {
|
|
260
|
+
id: {
|
|
261
|
+
name: idName,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
if (functionDeclarations.size()) {
|
|
265
|
+
return functionDeclarations.get();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
function getFunctionPathFromExportPath(exportPath, j, root, namedExportFilter) {
|
|
271
|
+
// Default export
|
|
272
|
+
if (j.ExportDefaultDeclaration.check(exportPath.node)) {
|
|
273
|
+
const { declaration } = exportPath.node;
|
|
274
|
+
if (declaration) {
|
|
275
|
+
if (j.FunctionDeclaration.check(declaration) ||
|
|
276
|
+
j.FunctionExpression.check(declaration) ||
|
|
277
|
+
j.ArrowFunctionExpression.check(declaration)) {
|
|
278
|
+
return exportPath.get('declaration');
|
|
279
|
+
}
|
|
280
|
+
else if (j.Identifier.check(declaration)) {
|
|
281
|
+
const idName = declaration.name;
|
|
282
|
+
if (!namedExportFilter(idName))
|
|
283
|
+
return;
|
|
284
|
+
const exportBinding = exportPath.scope.getBindings()[idName]?.[0];
|
|
285
|
+
if (exportBinding) {
|
|
286
|
+
return getFunctionNodeFromBinding(exportBinding, idName, j, root);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (
|
|
292
|
+
// Named exports
|
|
293
|
+
j.ExportNamedDeclaration.check(exportPath.node)) {
|
|
294
|
+
const namedExportPath = exportPath;
|
|
295
|
+
// extract the named exports, name specifiers, and default specifiers
|
|
296
|
+
const { declaration, specifiers } = namedExportPath.node;
|
|
297
|
+
if (declaration) {
|
|
298
|
+
if (j.VariableDeclaration.check(declaration)) {
|
|
299
|
+
const { declarations } = declaration;
|
|
300
|
+
for (const decl of declarations) {
|
|
301
|
+
if (j.VariableDeclarator.check(decl) && j.Identifier.check(decl.id)) {
|
|
302
|
+
const idName = decl.id.name;
|
|
303
|
+
if (!namedExportFilter(idName))
|
|
304
|
+
return;
|
|
305
|
+
// get bindings for each variable declarator
|
|
306
|
+
const exportBinding = namedExportPath.scope.getBindings()[idName]?.[0];
|
|
307
|
+
if (exportBinding) {
|
|
308
|
+
return getFunctionNodeFromBinding(exportBinding, idName, j, root);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else if (j.FunctionDeclaration.check(declaration) ||
|
|
314
|
+
j.FunctionExpression.check(declaration) ||
|
|
315
|
+
j.ArrowFunctionExpression.check(declaration)) {
|
|
316
|
+
const funcName = declaration.id?.name;
|
|
317
|
+
if (!namedExportFilter(funcName))
|
|
318
|
+
return;
|
|
319
|
+
return namedExportPath.get('declaration');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (specifiers) {
|
|
323
|
+
for (const specifier of specifiers) {
|
|
324
|
+
if (j.ExportSpecifier.check(specifier)) {
|
|
325
|
+
const idName = specifier.local.name;
|
|
326
|
+
if (!namedExportFilter(idName))
|
|
327
|
+
return;
|
|
328
|
+
const exportBinding = namedExportPath.scope.getBindings()[idName]?.[0];
|
|
329
|
+
if (exportBinding) {
|
|
330
|
+
return getFunctionNodeFromBinding(exportBinding, idName, j, root);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
function wrapParentheseIfNeeded(hasChainAccess, j, expression) {
|
|
339
|
+
return hasChainAccess ? j.parenthesizedExpression(expression) : expression;
|
|
340
|
+
}
|
|
341
|
+
function insertCommentOnce(path, j, comment) {
|
|
342
|
+
if (path.node.comments) {
|
|
343
|
+
const hasComment = path.node.comments.some((commentNode) => commentNode.value === comment);
|
|
344
|
+
if (hasComment) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
path.node.comments = [j.commentBlock(comment), ...(path.node.comments || [])];
|
|
349
|
+
}
|
|
198
350
|
//# sourceMappingURL=utils.js.map
|
|
@@ -13,7 +13,7 @@ function transformer(file, api) {
|
|
|
13
13
|
const importDecl = dynamicImportDeclaration.get(0).node;
|
|
14
14
|
const dynamicImportName = importDecl.specifiers?.[0]?.local?.name;
|
|
15
15
|
if (!dynamicImportName) {
|
|
16
|
-
return
|
|
16
|
+
return file.source;
|
|
17
17
|
}
|
|
18
18
|
// Find call expressions where the callee is the imported 'dynamic'
|
|
19
19
|
root
|