@next/codemod 15.0.0-canary.172 → 15.0.0-canary.174
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 +2 -1
- package/bin/transform.js +1 -1
- package/bin/upgrade.js +58 -30
- package/lib/codemods.js +10 -5
- package/lib/handle-package.js +8 -8
- package/package.json +1 -1
- package/transforms/lib/async-request-api/next-async-dynamic-api.js +60 -34
- package/transforms/lib/async-request-api/next-async-dynamic-prop.js +252 -84
- package/transforms/lib/async-request-api/utils.js +152 -0
- package/transforms/next-dynamic-access-named-export.js +1 -1
package/bin/next-codemod.js
CHANGED
|
@@ -30,7 +30,8 @@ const program = new commander_1.Command(packageJson.name)
|
|
|
30
30
|
program
|
|
31
31
|
.command('upgrade')
|
|
32
32
|
.description('Upgrade Next.js apps to desired versions with a single command.')
|
|
33
|
-
.
|
|
33
|
+
.argument('[revision]', 'NPM dist tag or exact version to upgrade to (e.g. "latest" or "15.0.0-canary.167"). Prompts to choose a dist tag if omitted.')
|
|
34
|
+
.option('--verbose', 'Verbose output', false)
|
|
34
35
|
.action(upgrade_1.runUpgrade);
|
|
35
36
|
program.parse(process.argv);
|
|
36
37
|
//# sourceMappingURL=next-codemod.js.map
|
package/bin/transform.js
CHANGED
|
@@ -98,7 +98,7 @@ async function runTransform(transform, path, options) {
|
|
|
98
98
|
}
|
|
99
99
|
if (!dry && transformer === 'next-request-geo-ip') {
|
|
100
100
|
console.log('Installing `@vercel/functions`...');
|
|
101
|
-
(0, handle_package_1.
|
|
101
|
+
(0, handle_package_1.installPackages)(['@vercel/functions']);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
//# sourceMappingURL=transform.js.map
|
package/bin/upgrade.js
CHANGED
|
@@ -12,21 +12,34 @@ const compare_versions_1 = require("compare-versions");
|
|
|
12
12
|
const chalk_1 = __importDefault(require("chalk"));
|
|
13
13
|
const codemods_1 = require("../lib/codemods");
|
|
14
14
|
const handle_package_1 = require("../lib/handle-package");
|
|
15
|
-
|
|
15
|
+
const transform_1 = require("./transform");
|
|
16
|
+
/**
|
|
17
|
+
* @param query
|
|
18
|
+
* @example loadHighestNPMVersionMatching("react@^18.3.0 || ^19.0.0") === Promise<"19.0.0">
|
|
19
|
+
*/
|
|
20
|
+
async function loadHighestNPMVersionMatching(query) {
|
|
21
|
+
const versionsJSON = (0, child_process_1.execSync)(`npm --silent view "${query}" --json --field version`, { encoding: 'utf-8' });
|
|
22
|
+
const versions = JSON.parse(versionsJSON);
|
|
23
|
+
if (versions.length < 1) {
|
|
24
|
+
throw new Error(`Found no React versions matching "${query}". This is a bug in the upgrade tool.`);
|
|
25
|
+
}
|
|
26
|
+
return versions[versions.length - 1];
|
|
27
|
+
}
|
|
28
|
+
async function runUpgrade(revision, options) {
|
|
29
|
+
const { verbose } = options;
|
|
16
30
|
const appPackageJsonPath = path_1.default.resolve(process.cwd(), 'package.json');
|
|
17
31
|
let appPackageJson = JSON.parse(fs_1.default.readFileSync(appPackageJsonPath, 'utf8'));
|
|
18
32
|
await detectWorkspace(appPackageJson);
|
|
19
33
|
let targetNextPackageJson;
|
|
20
34
|
let targetVersionSpecifier = '';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const res = await fetch(`https://registry.npmjs.org/next/${shortcutVersion}`);
|
|
35
|
+
if (revision !== undefined) {
|
|
36
|
+
const res = await fetch(`https://registry.npmjs.org/next/${revision}`);
|
|
24
37
|
if (res.status === 200) {
|
|
25
38
|
targetNextPackageJson = await res.json();
|
|
26
39
|
targetVersionSpecifier = targetNextPackageJson.version;
|
|
27
40
|
}
|
|
28
41
|
else {
|
|
29
|
-
console.error(`${chalk_1.default.yellow(
|
|
42
|
+
console.error(`${chalk_1.default.yellow(`next@${revision}`)} does not exist. Check available versions at ${chalk_1.default.underline('https://www.npmjs.com/package/next?activeTab=versions')}, or choose one from below\n`);
|
|
30
43
|
}
|
|
31
44
|
}
|
|
32
45
|
const installedNextVersion = await getInstalledNextVersion();
|
|
@@ -83,21 +96,41 @@ async function runUpgrade() {
|
|
|
83
96
|
targetVersionSpecifier = response.version;
|
|
84
97
|
}
|
|
85
98
|
const targetNextVersion = targetNextPackageJson.version;
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
// We're resolving a specific version here to avoid including "ugly" version queries
|
|
100
|
+
// in the manifest.
|
|
101
|
+
// E.g. in peerDependencies we could have `^18.2.0 || ^19.0.0 || 20.0.0-canary`
|
|
102
|
+
// If we'd just `npm add` that, the manifest would read the same version query.
|
|
103
|
+
// This is basically a `npm --save-exact react@$versionQuery` that works for every package manager.
|
|
104
|
+
const [targetReactVersion, targetReactTypesVersion, targetReactDOMTypesVersion,] = await Promise.all([
|
|
105
|
+
loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`),
|
|
106
|
+
loadHighestNPMVersionMatching(`@types/react@${targetNextPackageJson.peerDependencies['react']}`),
|
|
107
|
+
loadHighestNPMVersionMatching(`@types/react-dom@${targetNextPackageJson.peerDependencies['react']}`),
|
|
108
|
+
]);
|
|
109
|
+
if ((0, compare_versions_1.compareVersions)(targetNextVersion, '15.0.0-canary') >= 0) {
|
|
88
110
|
await suggestTurbopack(appPackageJson);
|
|
89
111
|
}
|
|
90
112
|
fs_1.default.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2));
|
|
91
113
|
const packageManager = (0, handle_package_1.getPkgManager)(process.cwd());
|
|
92
114
|
const nextDependency = `next@${targetNextVersion}`;
|
|
93
115
|
const reactDependencies = [
|
|
94
|
-
`react@${
|
|
95
|
-
|
|
96
|
-
`react-dom@${targetNextPackageJson.peerDependencies['react-dom']}`,
|
|
97
|
-
`@types/react-dom@${targetNextPackageJson.devDependencies['@types/react-dom']}`,
|
|
116
|
+
`react@${targetReactVersion}`,
|
|
117
|
+
`react-dom@${targetReactVersion}`,
|
|
98
118
|
];
|
|
99
|
-
(0
|
|
119
|
+
if (targetReactVersion.startsWith('19.0.0-canary') ||
|
|
120
|
+
targetReactVersion.startsWith('19.0.0-beta') ||
|
|
121
|
+
targetReactVersion.startsWith('19.0.0-rc')) {
|
|
122
|
+
reactDependencies.push(`@types/react@npm:types-react@rc`);
|
|
123
|
+
reactDependencies.push(`@types/react-dom@npm:types-react-dom@rc`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
reactDependencies.push(`@types/react@${targetReactTypesVersion}`);
|
|
127
|
+
reactDependencies.push(`@types/react-dom@${targetReactDOMTypesVersion}`);
|
|
128
|
+
}
|
|
100
129
|
console.log(`Upgrading your project to ${chalk_1.default.blue('Next.js ' + targetVersionSpecifier)}...\n`);
|
|
130
|
+
(0, handle_package_1.installPackages)([nextDependency, ...reactDependencies], {
|
|
131
|
+
packageManager,
|
|
132
|
+
silent: !verbose,
|
|
133
|
+
});
|
|
101
134
|
await suggestCodemods(installedNextVersion, targetNextVersion);
|
|
102
135
|
console.log(`\n${chalk_1.default.green('✔')} Your Next.js project has been upgraded successfully. ${chalk_1.default.bold('Time to ship! 🚢')}`);
|
|
103
136
|
}
|
|
@@ -196,29 +229,24 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
|
196
229
|
if (relevantCodemods.length === 0) {
|
|
197
230
|
return;
|
|
198
231
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
232
|
+
const { codemods } = await (0, prompts_1.default)({
|
|
233
|
+
type: 'multiselect',
|
|
234
|
+
name: 'codemods',
|
|
235
|
+
message: `\nThe following ${chalk_1.default.blue('codemods')} are recommended for your upgrade. Would you like to apply them?`,
|
|
236
|
+
choices: relevantCodemods.map((codemod) => {
|
|
237
|
+
return {
|
|
238
|
+
title: `${codemod.title} ${chalk_1.default.grey(`(${codemod.value})`)}`,
|
|
239
|
+
value: codemod.value,
|
|
240
|
+
selected: true,
|
|
241
|
+
};
|
|
242
|
+
}),
|
|
210
243
|
}, {
|
|
211
244
|
onCancel: () => {
|
|
212
245
|
process.exit(0);
|
|
213
246
|
},
|
|
214
247
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
for (const codemod of relevantCodemods) {
|
|
219
|
-
(0, child_process_1.execSync)(`npx @next/codemod@latest ${codemod.value} ${process.cwd()} --force`, {
|
|
220
|
-
stdio: 'inherit',
|
|
221
|
-
});
|
|
248
|
+
for (const codemod of codemods) {
|
|
249
|
+
await (0, transform_1.runTransform)(codemod, process.cwd(), { force: true });
|
|
222
250
|
}
|
|
223
251
|
}
|
|
224
252
|
//# sourceMappingURL=upgrade.js.map
|
package/lib/codemods.js
CHANGED
|
@@ -87,17 +87,22 @@ exports.availableCodemods = [
|
|
|
87
87
|
],
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
|
-
version: '15.0',
|
|
90
|
+
version: '15.0.0-canary.153',
|
|
91
91
|
codemods: [
|
|
92
|
-
{
|
|
93
|
-
title: 'Transforms usage of Next.js async Request APIs',
|
|
94
|
-
value: 'next-async-request-api',
|
|
95
|
-
},
|
|
96
92
|
{
|
|
97
93
|
title: 'Migrate `geo` and `ip` properties on `NextRequest`',
|
|
98
94
|
value: 'next-request-geo-ip',
|
|
99
95
|
},
|
|
100
96
|
],
|
|
101
97
|
},
|
|
98
|
+
{
|
|
99
|
+
version: '15.0.0-canary.171',
|
|
100
|
+
codemods: [
|
|
101
|
+
{
|
|
102
|
+
title: 'Transforms usage of Next.js async Request APIs',
|
|
103
|
+
value: 'next-async-request-api',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
102
107
|
];
|
|
103
108
|
//# sourceMappingURL=codemods.js.map
|
package/lib/handle-package.js
CHANGED
|
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getPkgManager = getPkgManager;
|
|
7
7
|
exports.uninstallPackage = uninstallPackage;
|
|
8
|
-
exports.
|
|
8
|
+
exports.installPackages = installPackages;
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const execa_1 = __importDefault(require("execa"));
|
|
@@ -59,15 +59,15 @@ function uninstallPackage(packageToUninstall, pkgManager) {
|
|
|
59
59
|
}
|
|
60
60
|
execa_1.default.sync(pkgManager, [command, packageToUninstall], { stdio: 'inherit' });
|
|
61
61
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
if (!
|
|
62
|
+
function installPackages(packageToInstall, options = {}) {
|
|
63
|
+
const { packageManager = getPkgManager(process.cwd()), silent = false } = options;
|
|
64
|
+
if (!packageManager)
|
|
65
65
|
throw new Error('Failed to find package manager');
|
|
66
|
-
if (Array.isArray(packageToInstall)) {
|
|
67
|
-
packageToInstall = packageToInstall.join(' ');
|
|
68
|
-
}
|
|
69
66
|
try {
|
|
70
|
-
execa_1.default.sync(
|
|
67
|
+
execa_1.default.sync(packageManager, ['add', ...packageToInstall], {
|
|
68
|
+
// Keeping stderr since it'll likely be relevant later when it fails.
|
|
69
|
+
stdio: silent ? ['ignore', 'ignore', 'inherit'] : 'inherit',
|
|
70
|
+
});
|
|
71
71
|
}
|
|
72
72
|
catch (error) {
|
|
73
73
|
throw new Error(`Failed to install "${packageToInstall}". Please install it manually.`, { cause: error });
|
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
|