@next/codemod 15.0.0-canary.181 → 15.0.0-canary.182
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/upgrade.js +28 -14
- package/lib/handle-package.js +15 -33
- package/lib/utils.js +2 -2
- package/package.json +5 -3
- package/transforms/lib/async-request-api/next-async-dynamic-api.js +18 -13
- package/transforms/lib/async-request-api/next-async-dynamic-prop.js +79 -6
- package/transforms/lib/async-request-api/utils.js +8 -9
package/bin/next-codemod.js
CHANGED
|
@@ -30,11 +30,12 @@ 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
|
-
.argument('[revision]', '
|
|
33
|
+
.argument('[revision]', 'Specify the target Next.js version using an NPM dist tag (e.g. "latest", "canary", "rc") or an exact version number (e.g. "15.0.0").', packageJson.version.includes('-canary.')
|
|
34
34
|
? 'canary'
|
|
35
35
|
: packageJson.version.includes('-rc.')
|
|
36
36
|
? 'rc'
|
|
37
37
|
: 'latest')
|
|
38
|
+
.usage('[revision] [options]')
|
|
38
39
|
.option('--verbose', 'Verbose output', false)
|
|
39
40
|
.action(upgrade_1.runUpgrade);
|
|
40
41
|
program.parse(process.argv);
|
package/bin/upgrade.js
CHANGED
|
@@ -9,7 +9,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const compare_versions_1 = require("compare-versions");
|
|
12
|
-
const
|
|
12
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
13
13
|
const handle_package_1 = require("../lib/handle-package");
|
|
14
14
|
const transform_1 = require("./transform");
|
|
15
15
|
const utils_1 = require("../lib/utils");
|
|
@@ -19,11 +19,16 @@ const utils_1 = require("../lib/utils");
|
|
|
19
19
|
*/
|
|
20
20
|
async function loadHighestNPMVersionMatching(query) {
|
|
21
21
|
const versionsJSON = (0, child_process_1.execSync)(`npm --silent view "${query}" --json --field version`, { encoding: 'utf-8' });
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
22
|
+
const versionOrVersions = JSON.parse(versionsJSON);
|
|
23
|
+
if (versionOrVersions.length < 1) {
|
|
24
24
|
throw new Error(`Found no React versions matching "${query}". This is a bug in the upgrade tool.`);
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
// npm-view returns an array if there are multiple versions matching the query.
|
|
27
|
+
if (Array.isArray(versionOrVersions)) {
|
|
28
|
+
// The last entry will be the latest version published.
|
|
29
|
+
return versionOrVersions[versionOrVersions.length - 1];
|
|
30
|
+
}
|
|
31
|
+
return versionOrVersions;
|
|
27
32
|
}
|
|
28
33
|
async function runUpgrade(revision, options) {
|
|
29
34
|
const { verbose } = options;
|
|
@@ -39,20 +44,21 @@ async function runUpgrade(revision, options) {
|
|
|
39
44
|
'version' in targetNextPackageJson &&
|
|
40
45
|
'peerDependencies' in targetNextPackageJson;
|
|
41
46
|
if (!validRevision) {
|
|
42
|
-
throw new Error(
|
|
47
|
+
throw new Error(`Invalid revision provided: "${revision}". Please provide a valid Next.js version or dist-tag (e.g. "latest", "canary", "rc", or "15.0.0").\nCheck available versions at https://www.npmjs.com/package/next?activeTab=versions.`);
|
|
43
48
|
}
|
|
44
49
|
const installedNextVersion = getInstalledNextVersion();
|
|
50
|
+
console.log(`Current Next.js version: v${installedNextVersion}`);
|
|
45
51
|
const targetNextVersion = targetNextPackageJson.version;
|
|
52
|
+
if ((0, compare_versions_1.compareVersions)(installedNextVersion, targetNextVersion) >= 0) {
|
|
53
|
+
console.log(`${picocolors_1.default.green('✓')} Current Next.js version is already on or higher than the target version "v${targetNextVersion}".`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
46
56
|
// We're resolving a specific version here to avoid including "ugly" version queries
|
|
47
57
|
// in the manifest.
|
|
48
58
|
// E.g. in peerDependencies we could have `^18.2.0 || ^19.0.0 || 20.0.0-canary`
|
|
49
59
|
// If we'd just `npm add` that, the manifest would read the same version query.
|
|
50
60
|
// This is basically a `npm --save-exact react@$versionQuery` that works for every package manager.
|
|
51
|
-
const
|
|
52
|
-
loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`),
|
|
53
|
-
loadHighestNPMVersionMatching(`@types/react@${targetNextPackageJson.peerDependencies['react']}`),
|
|
54
|
-
loadHighestNPMVersionMatching(`@types/react-dom@${targetNextPackageJson.peerDependencies['react']}`),
|
|
55
|
-
]);
|
|
61
|
+
const targetReactVersion = await loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`);
|
|
56
62
|
if ((0, compare_versions_1.compareVersions)(targetNextVersion, '15.0.0-canary') >= 0) {
|
|
57
63
|
await suggestTurbopack(appPackageJson);
|
|
58
64
|
}
|
|
@@ -71,10 +77,14 @@ async function runUpgrade(revision, options) {
|
|
|
71
77
|
reactDependencies.push(`@types/react-dom@npm:types-react-dom@rc`);
|
|
72
78
|
}
|
|
73
79
|
else {
|
|
80
|
+
const [targetReactTypesVersion, targetReactDOMTypesVersion] = await Promise.all([
|
|
81
|
+
loadHighestNPMVersionMatching(`@types/react@${targetNextPackageJson.peerDependencies['react']}`),
|
|
82
|
+
loadHighestNPMVersionMatching(`@types/react-dom@${targetNextPackageJson.peerDependencies['react']}`),
|
|
83
|
+
]);
|
|
74
84
|
reactDependencies.push(`@types/react@${targetReactTypesVersion}`);
|
|
75
85
|
reactDependencies.push(`@types/react-dom@${targetReactDOMTypesVersion}`);
|
|
76
86
|
}
|
|
77
|
-
console.log(`Upgrading your project to ${
|
|
87
|
+
console.log(`Upgrading your project to ${picocolors_1.default.blue('Next.js ' + targetNextVersion)}...\n`);
|
|
78
88
|
(0, handle_package_1.installPackages)([nextDependency, ...reactDependencies], {
|
|
79
89
|
packageManager,
|
|
80
90
|
silent: !verbose,
|
|
@@ -82,7 +92,11 @@ async function runUpgrade(revision, options) {
|
|
|
82
92
|
for (const codemod of codemods) {
|
|
83
93
|
await (0, transform_1.runTransform)(codemod, process.cwd(), { force: true });
|
|
84
94
|
}
|
|
85
|
-
console.log(
|
|
95
|
+
console.log(); // new line
|
|
96
|
+
if (codemods.length > 0) {
|
|
97
|
+
console.log(`${picocolors_1.default.green('✔')} Codemods have been applied successfully.`);
|
|
98
|
+
}
|
|
99
|
+
console.log(`Please review the local changes and read the Next.js 15 migration guide to complete the migration. https://nextjs.org/docs/canary/app/building-your-application/upgrading/version-15`);
|
|
86
100
|
}
|
|
87
101
|
function getInstalledNextVersion() {
|
|
88
102
|
try {
|
|
@@ -124,7 +138,7 @@ async function suggestTurbopack(packageJson) {
|
|
|
124
138
|
packageJson.scripts['dev'] = devScript.replace('next dev', 'next dev --turbo');
|
|
125
139
|
return;
|
|
126
140
|
}
|
|
127
|
-
console.log(`${
|
|
141
|
+
console.log(`${picocolors_1.default.yellow('⚠')} Could not find "${picocolors_1.default.bold('next dev')}" in your dev script.`);
|
|
128
142
|
const responseCustomDevScript = await (0, prompts_1.default)({
|
|
129
143
|
type: 'text',
|
|
130
144
|
name: 'customDevScript',
|
|
@@ -150,7 +164,7 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
|
150
164
|
const { codemods } = await (0, prompts_1.default)({
|
|
151
165
|
type: 'multiselect',
|
|
152
166
|
name: 'codemods',
|
|
153
|
-
message: `The following ${
|
|
167
|
+
message: `The following ${picocolors_1.default.blue('codemods')} are recommended for your upgrade. Select the ones to apply.`,
|
|
154
168
|
choices: relevantCodemods.reverse().map(({ title, value, version }) => {
|
|
155
169
|
return {
|
|
156
170
|
title: `(v${version}) ${value}`,
|
package/lib/handle-package.js
CHANGED
|
@@ -6,42 +6,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.getPkgManager = getPkgManager;
|
|
7
7
|
exports.uninstallPackage = uninstallPackage;
|
|
8
8
|
exports.installPackages = installPackages;
|
|
9
|
-
const
|
|
10
|
-
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const find_up_1 = __importDefault(require("find-up"));
|
|
11
10
|
const execa_1 = __importDefault(require("execa"));
|
|
11
|
+
const node_path_1 = require("node:path");
|
|
12
12
|
function getPkgManager(baseDir) {
|
|
13
13
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return 'yarn';
|
|
28
|
-
}
|
|
29
|
-
else if (userAgent.startsWith('pnpm')) {
|
|
30
|
-
return 'pnpm';
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
try {
|
|
34
|
-
execa_1.default.sync('yarn --version', { stdio: 'ignore' });
|
|
35
|
-
return 'yarn';
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
try {
|
|
39
|
-
execa_1.default.sync('pnpm --version', { stdio: 'ignore' });
|
|
40
|
-
return 'pnpm';
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
execa_1.default.sync('bun --version', { stdio: 'ignore' });
|
|
44
|
-
return 'bun';
|
|
14
|
+
const lockFile = find_up_1.default.sync(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb'], { cwd: baseDir });
|
|
15
|
+
if (lockFile) {
|
|
16
|
+
switch ((0, node_path_1.basename)(lockFile)) {
|
|
17
|
+
case 'package-lock.json':
|
|
18
|
+
return 'npm';
|
|
19
|
+
case 'yarn.lock':
|
|
20
|
+
return 'yarn';
|
|
21
|
+
case 'pnpm-lock.yaml':
|
|
22
|
+
return 'pnpm';
|
|
23
|
+
case 'bun.lockb':
|
|
24
|
+
return 'bun';
|
|
25
|
+
default:
|
|
26
|
+
return 'npm';
|
|
45
27
|
}
|
|
46
28
|
}
|
|
47
29
|
}
|
package/lib/utils.js
CHANGED
|
@@ -92,7 +92,7 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
|
|
|
92
92
|
version: '14.0',
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
|
-
title: '
|
|
95
|
+
title: 'Transform `next/dynamic` imports accessing named exports to return an object with a `default` property',
|
|
96
96
|
value: 'next-dynamic-access-named-export',
|
|
97
97
|
version: '15.0.0-canary.44',
|
|
98
98
|
},
|
|
@@ -107,7 +107,7 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
|
|
|
107
107
|
version: '15.0.0-canary.171',
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
|
-
title: '
|
|
110
|
+
title: 'Transform App Router Route Segment Config `runtime` value from `experimental-edge` to `edge`',
|
|
111
111
|
value: 'app-dir-runtime-config-experimental-edge',
|
|
112
112
|
version: '15.0.0-canary.179',
|
|
113
113
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@next/codemod",
|
|
3
|
-
"version": "15.0.0-canary.
|
|
3
|
+
"version": "15.0.0-canary.182",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"directory": "packages/next-codemod"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"chalk": "4.1.2",
|
|
12
11
|
"cheerio": "1.0.0-rc.9",
|
|
13
12
|
"commander": "12.1.0",
|
|
14
13
|
"compare-versions": "6.1.1",
|
|
15
14
|
"execa": "4.0.3",
|
|
15
|
+
"find-up": "4.1.0",
|
|
16
16
|
"globby": "11.0.1",
|
|
17
17
|
"is-git-clean": "1.1.0",
|
|
18
18
|
"jscodeshift": "17.0.0",
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"bin": "./bin/next-codemod.js",
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/
|
|
37
|
+
"@types/find-up": "4.0.0",
|
|
38
|
+
"@types/jscodeshift": "0.11.0",
|
|
39
|
+
"@types/prompts": "2.4.2"
|
|
38
40
|
}
|
|
39
41
|
}
|
|
@@ -17,12 +17,13 @@ function findDynamicImportsAndComment(root, j) {
|
|
|
17
17
|
arguments: [{ value: 'next/headers' }],
|
|
18
18
|
});
|
|
19
19
|
importPaths.forEach((path) => {
|
|
20
|
-
(0, utils_1.insertCommentOnce)(path.node, j, DYNAMIC_IMPORT_WARN_COMMENT);
|
|
21
|
-
modified
|
|
20
|
+
const inserted = (0, utils_1.insertCommentOnce)(path.node, j, DYNAMIC_IMPORT_WARN_COMMENT);
|
|
21
|
+
modified ||= inserted;
|
|
22
22
|
});
|
|
23
23
|
return modified;
|
|
24
24
|
}
|
|
25
25
|
function transformDynamicAPI(source, api, filePath) {
|
|
26
|
+
const isEntryFile = utils_1.NEXTJS_ENTRY_FILES.test(filePath);
|
|
26
27
|
const j = api.jscodeshift.withParser('tsx');
|
|
27
28
|
const root = j(source);
|
|
28
29
|
let modified = false;
|
|
@@ -83,7 +84,7 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
83
84
|
else {
|
|
84
85
|
// Determine if the function is an export
|
|
85
86
|
const closetScopePath = closetScope.get();
|
|
86
|
-
const
|
|
87
|
+
const isEntryFileExport = isEntryFile && (0, utils_1.isMatchedFunctionExported)(closetScopePath, j);
|
|
87
88
|
const closestFunctionNode = closetScope.size()
|
|
88
89
|
? closetScopePath.node
|
|
89
90
|
: null;
|
|
@@ -92,7 +93,7 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
92
93
|
// If it's exporting a variable declaration, exportFunctionNode is the function declaration
|
|
93
94
|
// e.g. export const MyComponent = function() {}
|
|
94
95
|
let exportFunctionNode;
|
|
95
|
-
if (
|
|
96
|
+
if (isEntryFileExport) {
|
|
96
97
|
if (closestFunctionNode &&
|
|
97
98
|
(0, utils_1.isFunctionType)(closestFunctionNode.type)) {
|
|
98
99
|
exportFunctionNode = closestFunctionNode;
|
|
@@ -104,7 +105,7 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
104
105
|
}
|
|
105
106
|
let canConvertToAsync = false;
|
|
106
107
|
// check if current path is under the default export function
|
|
107
|
-
if (
|
|
108
|
+
if (isEntryFileExport) {
|
|
108
109
|
// if default export function is not async, convert it to async, and await the api call
|
|
109
110
|
if (!isCallAwaited) {
|
|
110
111
|
// If the scoped function is async function
|
|
@@ -134,13 +135,14 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
134
135
|
needsReactUseImport = true;
|
|
135
136
|
}
|
|
136
137
|
else {
|
|
137
|
-
castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` Next.js Dynamic Async API Codemod: Manually await this call, if it's a Server Component `);
|
|
138
|
+
const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` Next.js Dynamic Async API Codemod: Manually await this call, if it's a Server Component `);
|
|
139
|
+
modified ||= casted;
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
else {
|
|
141
|
-
castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ' Next.js Dynamic Async API Codemod: please manually await this call, codemod cannot transform due to undetermined async scope ');
|
|
143
|
+
const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ' Next.js Dynamic Async API Codemod: please manually await this call, codemod cannot transform due to undetermined async scope ');
|
|
144
|
+
modified ||= casted;
|
|
142
145
|
}
|
|
143
|
-
modified = true;
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
});
|
|
@@ -183,9 +185,8 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
183
185
|
if (needsReactUseImport) {
|
|
184
186
|
(0, utils_1.insertReactUseImport)(root, j);
|
|
185
187
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
188
|
+
const commented = findDynamicImportsAndComment(root, j);
|
|
189
|
+
modified ||= commented;
|
|
189
190
|
return modified ? root.toSource() : null;
|
|
190
191
|
}
|
|
191
192
|
// cast to unknown first, then the specific type
|
|
@@ -195,11 +196,12 @@ const API_CAST_TYPE_MAP = {
|
|
|
195
196
|
draftMode: 'UnsafeUnwrappedDraftMode',
|
|
196
197
|
};
|
|
197
198
|
function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, customMessage) {
|
|
199
|
+
let modified = false;
|
|
198
200
|
const isTsFile = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
|
199
201
|
if (isTsFile) {
|
|
200
202
|
// if the path of call expression is already being awaited, no need to cast
|
|
201
203
|
if (path.parentPath?.node?.type === 'AwaitExpression')
|
|
202
|
-
return;
|
|
204
|
+
return false;
|
|
203
205
|
/* Do type cast for headers, cookies, draftMode
|
|
204
206
|
import {
|
|
205
207
|
type UnsafeUnwrappedHeaders,
|
|
@@ -218,6 +220,7 @@ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, in
|
|
|
218
220
|
// Replace the original expression with the new cast expression,
|
|
219
221
|
// also wrap () around the new cast expression.
|
|
220
222
|
j(path).replaceWith(j.parenthesizedExpression(newCastExpression));
|
|
223
|
+
modified = true;
|
|
221
224
|
// If cast types are not imported, add them to the import list
|
|
222
225
|
const importDeclaration = root.find(j.ImportDeclaration, {
|
|
223
226
|
source: { value: 'next/headers' },
|
|
@@ -243,8 +246,10 @@ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, in
|
|
|
243
246
|
}
|
|
244
247
|
else {
|
|
245
248
|
// Otherwise for JS file, leave a message to the user to manually handle the transformation
|
|
246
|
-
(0, utils_1.insertCommentOnce)(path.node, j, customMessage);
|
|
249
|
+
const inserted = (0, utils_1.insertCommentOnce)(path.node, j, customMessage);
|
|
250
|
+
modified ||= inserted;
|
|
247
251
|
}
|
|
252
|
+
return modified;
|
|
248
253
|
}
|
|
249
254
|
function findImportMappingFromNextHeaders(root, j) {
|
|
250
255
|
const mappings = {};
|
|
@@ -123,8 +123,43 @@ function applyUseAndRenameAccessedProp(propIdName, path, j) {
|
|
|
123
123
|
}
|
|
124
124
|
return modified;
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
function commentOnMatchedReExports(root, j) {
|
|
127
|
+
let modified = false;
|
|
128
|
+
root.find(j.ExportNamedDeclaration).forEach((path) => {
|
|
129
|
+
if (j.ExportSpecifier.check(path.value.specifiers[0])) {
|
|
130
|
+
const specifiers = path.value.specifiers;
|
|
131
|
+
for (const specifier of specifiers) {
|
|
132
|
+
if (j.ExportSpecifier.check(specifier) &&
|
|
133
|
+
// Find matched named exports and default export
|
|
134
|
+
(utils_1.TARGET_NAMED_EXPORTS.has(specifier.exported.name) ||
|
|
135
|
+
specifier.exported.name === 'default')) {
|
|
136
|
+
if (j.Literal.check(path.value.source)) {
|
|
137
|
+
const localName = specifier.local.name;
|
|
138
|
+
const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` Next.js Dynamic Async API Codemod: \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
|
|
139
|
+
modified ||= commentInserted;
|
|
140
|
+
}
|
|
141
|
+
else if (path.value.source === null) {
|
|
142
|
+
const localIdentifier = specifier.local;
|
|
143
|
+
const localName = localIdentifier.name;
|
|
144
|
+
// search if local identifier is from imports
|
|
145
|
+
const importDeclaration = root
|
|
146
|
+
.find(j.ImportDeclaration)
|
|
147
|
+
.filter((importPath) => {
|
|
148
|
+
return importPath.value.specifiers.some((importSpecifier) => importSpecifier.local.name === localName);
|
|
149
|
+
});
|
|
150
|
+
if (importDeclaration.size() > 0) {
|
|
151
|
+
const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` Next.js Dynamic Async API Codemod: \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
|
|
152
|
+
modified ||= commentInserted;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return modified;
|
|
160
|
+
}
|
|
127
161
|
function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
162
|
+
let modified = false;
|
|
128
163
|
if (paramTypeAnnotation && paramTypeAnnotation.typeAnnotation) {
|
|
129
164
|
const typeAnnotation = paramTypeAnnotation.typeAnnotation;
|
|
130
165
|
if (typeAnnotation.type === 'TSTypeLiteral') {
|
|
@@ -151,6 +186,7 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
|
151
186
|
// @ts-ignore
|
|
152
187
|
member.typeAnnotation.typeAnnotation,
|
|
153
188
|
]));
|
|
189
|
+
modified = true;
|
|
154
190
|
}
|
|
155
191
|
}
|
|
156
192
|
});
|
|
@@ -189,6 +225,7 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
|
189
225
|
member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
|
|
190
226
|
member.typeAnnotation.typeAnnotation,
|
|
191
227
|
]));
|
|
228
|
+
modified = true;
|
|
192
229
|
}
|
|
193
230
|
}
|
|
194
231
|
});
|
|
@@ -197,11 +234,13 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
|
197
234
|
}
|
|
198
235
|
}
|
|
199
236
|
propsIdentifier.typeAnnotation = paramTypeAnnotation;
|
|
237
|
+
modified = true;
|
|
200
238
|
}
|
|
239
|
+
return modified;
|
|
201
240
|
}
|
|
202
241
|
function transformDynamicProps(source, api, filePath) {
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
242
|
+
const isEntryFile = utils_1.NEXTJS_ENTRY_FILES.test(filePath);
|
|
243
|
+
if (!isEntryFile) {
|
|
205
244
|
return null;
|
|
206
245
|
}
|
|
207
246
|
let modified = false;
|
|
@@ -289,7 +328,8 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
289
328
|
}
|
|
290
329
|
}
|
|
291
330
|
else {
|
|
292
|
-
|
|
331
|
+
const awaited = awaitMemberAccessOfProp(argName, path, j);
|
|
332
|
+
modified ||= awaited;
|
|
293
333
|
}
|
|
294
334
|
// cases of passing down `props` into any function
|
|
295
335
|
// Page(props) { callback(props) }
|
|
@@ -309,8 +349,8 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
309
349
|
const args = callExpression.value.arguments;
|
|
310
350
|
const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
|
|
311
351
|
const comment = ` Next.js Dynamic Async API Codemod: '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `;
|
|
312
|
-
(0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
|
|
313
|
-
modified
|
|
352
|
+
const inserted = (0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
|
|
353
|
+
modified ||= inserted;
|
|
314
354
|
});
|
|
315
355
|
if (modified) {
|
|
316
356
|
modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
|
|
@@ -328,6 +368,14 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
328
368
|
modified = true;
|
|
329
369
|
}
|
|
330
370
|
}
|
|
371
|
+
else {
|
|
372
|
+
// When the prop argument is not destructured, we need to add comments to the spread properties
|
|
373
|
+
if (j.Identifier.check(currentParam)) {
|
|
374
|
+
const commented = commentSpreadProps(path, currentParam.name, j);
|
|
375
|
+
const modifiedTypes = modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
|
|
376
|
+
modified ||= commented || modifiedTypes;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
331
379
|
}
|
|
332
380
|
// Helper function to insert `const params = await asyncParams;` at the beginning of the function body
|
|
333
381
|
function resolveAsyncProp(path, propertiesMap, propsIdentifierName, allProperties, isDefaultExport) {
|
|
@@ -555,6 +603,8 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
555
603
|
if (needsReactUseImport) {
|
|
556
604
|
(0, utils_1.insertReactUseImport)(root, j);
|
|
557
605
|
}
|
|
606
|
+
const commented = commentOnMatchedReExports(root, j);
|
|
607
|
+
modified ||= commented;
|
|
558
608
|
return modified ? root.toSource() : null;
|
|
559
609
|
}
|
|
560
610
|
function findAllTypes(root, j, typeName) {
|
|
@@ -610,4 +660,27 @@ function findAllTypes(root, j, typeName) {
|
|
|
610
660
|
});
|
|
611
661
|
return types;
|
|
612
662
|
}
|
|
663
|
+
function commentSpreadProps(path, propsIdentifierName, j) {
|
|
664
|
+
let modified = false;
|
|
665
|
+
const functionBody = findFunctionBody(path);
|
|
666
|
+
const functionBodyCollection = j(functionBody);
|
|
667
|
+
// Find all the usage of spreading properties of `props`
|
|
668
|
+
const jsxSpreadProperties = functionBodyCollection.find(j.JSXSpreadAttribute, { argument: { name: propsIdentifierName } });
|
|
669
|
+
const objSpreadProperties = functionBodyCollection.find(j.SpreadElement, {
|
|
670
|
+
argument: { name: propsIdentifierName },
|
|
671
|
+
});
|
|
672
|
+
const comment = ` Next.js Dynamic Async API Codemod: '${propsIdentifierName}' is used with spread syntax (...). Any asynchronous properties of '${propsIdentifierName}' must be awaited when accessed. `;
|
|
673
|
+
// Add comment before it
|
|
674
|
+
jsxSpreadProperties.forEach((spread) => {
|
|
675
|
+
const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
|
|
676
|
+
if (inserted)
|
|
677
|
+
modified = true;
|
|
678
|
+
});
|
|
679
|
+
objSpreadProperties.forEach((spread) => {
|
|
680
|
+
const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
|
|
681
|
+
if (inserted)
|
|
682
|
+
modified = true;
|
|
683
|
+
});
|
|
684
|
+
return modified;
|
|
685
|
+
}
|
|
613
686
|
//# sourceMappingURL=next-async-dynamic-prop.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TARGET_PROP_NAMES = exports.TARGET_NAMED_EXPORTS = exports.TARGET_ROUTE_EXPORTS = void 0;
|
|
3
|
+
exports.TARGET_PROP_NAMES = exports.TARGET_NAMED_EXPORTS = exports.TARGET_ROUTE_EXPORTS = exports.NEXTJS_ENTRY_FILES = void 0;
|
|
4
4
|
exports.isFunctionType = isFunctionType;
|
|
5
5
|
exports.isMatchedFunctionExported = isMatchedFunctionExported;
|
|
6
6
|
exports.determineClientDirective = determineClientDirective;
|
|
@@ -14,6 +14,7 @@ exports.getFunctionPathFromExportPath = getFunctionPathFromExportPath;
|
|
|
14
14
|
exports.wrapParentheseIfNeeded = wrapParentheseIfNeeded;
|
|
15
15
|
exports.insertCommentOnce = insertCommentOnce;
|
|
16
16
|
exports.getVariableDeclaratorId = getVariableDeclaratorId;
|
|
17
|
+
exports.NEXTJS_ENTRY_FILES = /([\\/]|^)(page|layout|route|default)\.(t|j)sx?$/;
|
|
17
18
|
exports.TARGET_ROUTE_EXPORTS = new Set([
|
|
18
19
|
'GET',
|
|
19
20
|
'POST',
|
|
@@ -144,6 +145,8 @@ function insertReactUseImport(root, j) {
|
|
|
144
145
|
source: {
|
|
145
146
|
value: 'react',
|
|
146
147
|
},
|
|
148
|
+
// Skip the type only react imports
|
|
149
|
+
importKind: 'value',
|
|
147
150
|
});
|
|
148
151
|
if (reactImportDeclaration.size() > 0) {
|
|
149
152
|
const importNode = reactImportDeclaration.get().node;
|
|
@@ -152,13 +155,8 @@ function insertReactUseImport(root, j) {
|
|
|
152
155
|
}
|
|
153
156
|
else {
|
|
154
157
|
// Final all type imports to 'react'
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
value: 'react',
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
if (reactImport.size() > 0) {
|
|
161
|
-
reactImport
|
|
158
|
+
if (reactImportDeclaration.size() > 0) {
|
|
159
|
+
reactImportDeclaration
|
|
162
160
|
.get()
|
|
163
161
|
.node.specifiers.push(j.importSpecifier(j.identifier('use')));
|
|
164
162
|
}
|
|
@@ -327,10 +325,11 @@ function insertCommentOnce(node, j, comment) {
|
|
|
327
325
|
if (node.comments) {
|
|
328
326
|
const hasComment = node.comments.some((commentNode) => commentNode.value === comment);
|
|
329
327
|
if (hasComment) {
|
|
330
|
-
return;
|
|
328
|
+
return false;
|
|
331
329
|
}
|
|
332
330
|
}
|
|
333
331
|
node.comments = [j.commentBlock(comment), ...(node.comments || [])];
|
|
332
|
+
return true;
|
|
334
333
|
}
|
|
335
334
|
function getVariableDeclaratorId(path, j) {
|
|
336
335
|
const parent = path.parentPath;
|