@next/codemod 15.0.0-canary.181 → 15.0.0-canary.183
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 +12 -3
- package/bin/transform.js +23 -9
- package/bin/upgrade.js +29 -15
- package/lib/handle-package.js +21 -34
- package/lib/utils.js +6 -6
- 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 +115 -14
- package/transforms/lib/async-request-api/utils.js +8 -9
package/bin/next-codemod.js
CHANGED
|
@@ -23,18 +23,27 @@ const program = new commander_1.Command(packageJson.name)
|
|
|
23
23
|
.helpOption('-h, --help', 'Display this help message.')
|
|
24
24
|
.option('-f, --force', 'Bypass Git safety checks and forcibly run codemods')
|
|
25
25
|
.option('-d, --dry', 'Dry run (no changes are made to files)')
|
|
26
|
-
.option('-p, --print', 'Print transformed files to
|
|
26
|
+
.option('-p, --print', 'Print transformed files to stdout, useful for development')
|
|
27
|
+
.option('--verbose', 'Show more information about the transform process')
|
|
27
28
|
.option('-j, --jscodeshift', '(Advanced) Pass options directly to jscodeshift')
|
|
28
29
|
.action(transform_1.runTransform)
|
|
29
|
-
.allowUnknownOption()
|
|
30
|
+
.allowUnknownOption()
|
|
31
|
+
// This is needed for options for subcommands to be passed correctly.
|
|
32
|
+
// Because by default the options are not positional, which will pass options
|
|
33
|
+
// to the main command "@next/codemod" even if it was passed after subcommands,
|
|
34
|
+
// e.g. "@next/codemod upgrade --verbose" will be treated as "next-codemod --verbose upgrade"
|
|
35
|
+
// By enabling this, it will respect the position of the options and pass it to subcommands.
|
|
36
|
+
// x-ref: https://github.com/tj/commander.js/pull/1427
|
|
37
|
+
.enablePositionalOptions();
|
|
30
38
|
program
|
|
31
39
|
.command('upgrade')
|
|
32
40
|
.description('Upgrade Next.js apps to desired versions with a single command.')
|
|
33
|
-
.argument('[revision]', '
|
|
41
|
+
.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
42
|
? 'canary'
|
|
35
43
|
: packageJson.version.includes('-rc.')
|
|
36
44
|
? 'rc'
|
|
37
45
|
: 'latest')
|
|
46
|
+
.usage('[revision] [options]')
|
|
38
47
|
.option('--verbose', 'Verbose output', false)
|
|
39
48
|
.action(upgrade_1.runUpgrade);
|
|
40
49
|
program.parse(process.argv);
|
package/bin/transform.js
CHANGED
|
@@ -66,7 +66,7 @@ async function runTransform(transform, path, options) {
|
|
|
66
66
|
return require(transformerPath).default(filesExpanded, options);
|
|
67
67
|
}
|
|
68
68
|
let args = [];
|
|
69
|
-
const { dry, print, runInBand, jscodeshift } = options;
|
|
69
|
+
const { dry, print, runInBand, jscodeshift, verbose } = options;
|
|
70
70
|
if (dry) {
|
|
71
71
|
args.push('--dry');
|
|
72
72
|
}
|
|
@@ -76,7 +76,10 @@ async function runTransform(transform, path, options) {
|
|
|
76
76
|
if (runInBand) {
|
|
77
77
|
args.push('--run-in-band');
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
if (verbose) {
|
|
80
|
+
args.push('--verbose=2');
|
|
81
|
+
}
|
|
82
|
+
args.push('--no-babel');
|
|
80
83
|
args.push('--ignore-pattern=**/node_modules/**');
|
|
81
84
|
args.push('--ignore-pattern=**/.next/**');
|
|
82
85
|
args.push('--extensions=tsx,ts,jsx,js');
|
|
@@ -94,17 +97,28 @@ async function runTransform(transform, path, options) {
|
|
|
94
97
|
throw new Error(`jscodeshift exited with code ${result.exitCode}`);
|
|
95
98
|
}
|
|
96
99
|
if (!dry && transformer === 'built-in-next-font') {
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
const { uninstallNextFont } = await (0, prompts_1.default)({
|
|
101
|
+
type: 'confirm',
|
|
102
|
+
name: 'uninstallNextFont',
|
|
103
|
+
message: 'Do you want to uninstall `@next/font`?',
|
|
104
|
+
initial: true,
|
|
105
|
+
});
|
|
106
|
+
if (uninstallNextFont) {
|
|
107
|
+
console.log('Uninstalling `@next/font`');
|
|
99
108
|
(0, handle_package_1.uninstallPackage)('@next/font');
|
|
100
109
|
}
|
|
101
|
-
catch {
|
|
102
|
-
console.error("Couldn't uninstall `@next/font`, please uninstall it manually");
|
|
103
|
-
}
|
|
104
110
|
}
|
|
105
111
|
if (!dry && transformer === 'next-request-geo-ip') {
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
const { installVercelFunctions } = await (0, prompts_1.default)({
|
|
113
|
+
type: 'confirm',
|
|
114
|
+
name: 'installVercelFunctions',
|
|
115
|
+
message: 'Do you want to install `@vercel/functions`?',
|
|
116
|
+
initial: true,
|
|
117
|
+
});
|
|
118
|
+
if (installVercelFunctions) {
|
|
119
|
+
console.log('Installing `@vercel/functions`...');
|
|
120
|
+
(0, handle_package_1.installPackages)(['@vercel/functions']);
|
|
121
|
+
}
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
//# sourceMappingURL=transform.js.map
|
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,18 +77,26 @@ 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,
|
|
81
91
|
});
|
|
82
92
|
for (const codemod of codemods) {
|
|
83
|
-
await (0, transform_1.runTransform)(codemod, process.cwd(), { force: true });
|
|
93
|
+
await (0, transform_1.runTransform)(codemod, process.cwd(), { force: true, verbose });
|
|
94
|
+
}
|
|
95
|
+
console.log(); // new line
|
|
96
|
+
if (codemods.length > 0) {
|
|
97
|
+
console.log(`${picocolors_1.default.green('✔')} Codemods have been applied successfully.`);
|
|
84
98
|
}
|
|
85
|
-
console.log(
|
|
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
|
}
|
|
@@ -57,7 +39,12 @@ function uninstallPackage(packageToUninstall, pkgManager) {
|
|
|
57
39
|
if (pkgManager === 'yarn') {
|
|
58
40
|
command = 'remove';
|
|
59
41
|
}
|
|
60
|
-
|
|
42
|
+
try {
|
|
43
|
+
execa_1.default.sync(pkgManager, [command, packageToUninstall], { stdio: 'inherit' });
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new Error(`Failed to uninstall "${packageToUninstall}". Please uninstall it manually.`, { cause: error });
|
|
47
|
+
}
|
|
61
48
|
}
|
|
62
49
|
function installPackages(packageToInstall, options = {}) {
|
|
63
50
|
const { packageManager = getPkgManager(process.cwd()), silent = false } = options;
|
package/lib/utils.js
CHANGED
|
@@ -76,6 +76,11 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
|
|
|
76
76
|
value: 'next-image-to-legacy-image',
|
|
77
77
|
version: '13.0',
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
title: 'Transform App Router Route Segment Config `runtime` value from `experimental-edge` to `edge`',
|
|
81
|
+
value: 'app-dir-runtime-config-experimental-edge',
|
|
82
|
+
version: '13.1.2',
|
|
83
|
+
},
|
|
79
84
|
{
|
|
80
85
|
title: 'Uninstall `@next/font` and transform imports to `next/font`',
|
|
81
86
|
value: 'built-in-next-font',
|
|
@@ -92,7 +97,7 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
|
|
|
92
97
|
version: '14.0',
|
|
93
98
|
},
|
|
94
99
|
{
|
|
95
|
-
title: '
|
|
100
|
+
title: 'Transform `next/dynamic` imports accessing named exports to return an object with a `default` property',
|
|
96
101
|
value: 'next-dynamic-access-named-export',
|
|
97
102
|
version: '15.0.0-canary.44',
|
|
98
103
|
},
|
|
@@ -106,10 +111,5 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
|
|
|
106
111
|
value: 'next-async-request-api',
|
|
107
112
|
version: '15.0.0-canary.171',
|
|
108
113
|
},
|
|
109
|
-
{
|
|
110
|
-
title: 'Transforms `experimental-edge` to `edge` in the `runtime` route segment configuration within the App Router',
|
|
111
|
-
value: 'app-dir-runtime-config-experimental-edge',
|
|
112
|
-
version: '15.0.0-canary.179',
|
|
113
|
-
},
|
|
114
114
|
];
|
|
115
115
|
//# sourceMappingURL=utils.js.map
|
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.183",
|
|
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 = {};
|
|
@@ -23,6 +23,12 @@ function awaitMemberAccessOfProp(propIdName, path, j) {
|
|
|
23
23
|
// await each member access
|
|
24
24
|
memberAccess.forEach((memberAccessPath) => {
|
|
25
25
|
const member = memberAccessPath.value;
|
|
26
|
+
const memberProperty = member.property;
|
|
27
|
+
const isAccessingMatchedProperty = j.Identifier.check(memberProperty) &&
|
|
28
|
+
utils_1.TARGET_PROP_NAMES.has(memberProperty.name);
|
|
29
|
+
if (!isAccessingMatchedProperty) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
26
32
|
if (isParentPromiseAllCallExpression(memberAccessPath, j)) {
|
|
27
33
|
return;
|
|
28
34
|
}
|
|
@@ -123,8 +129,43 @@ function applyUseAndRenameAccessedProp(propIdName, path, j) {
|
|
|
123
129
|
}
|
|
124
130
|
return modified;
|
|
125
131
|
}
|
|
126
|
-
|
|
132
|
+
function commentOnMatchedReExports(root, j) {
|
|
133
|
+
let modified = false;
|
|
134
|
+
root.find(j.ExportNamedDeclaration).forEach((path) => {
|
|
135
|
+
if (j.ExportSpecifier.check(path.value.specifiers[0])) {
|
|
136
|
+
const specifiers = path.value.specifiers;
|
|
137
|
+
for (const specifier of specifiers) {
|
|
138
|
+
if (j.ExportSpecifier.check(specifier) &&
|
|
139
|
+
// Find matched named exports and default export
|
|
140
|
+
(utils_1.TARGET_NAMED_EXPORTS.has(specifier.exported.name) ||
|
|
141
|
+
specifier.exported.name === 'default')) {
|
|
142
|
+
if (j.Literal.check(path.value.source)) {
|
|
143
|
+
const localName = specifier.local.name;
|
|
144
|
+
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\``);
|
|
145
|
+
modified ||= commentInserted;
|
|
146
|
+
}
|
|
147
|
+
else if (path.value.source === null) {
|
|
148
|
+
const localIdentifier = specifier.local;
|
|
149
|
+
const localName = localIdentifier.name;
|
|
150
|
+
// search if local identifier is from imports
|
|
151
|
+
const importDeclaration = root
|
|
152
|
+
.find(j.ImportDeclaration)
|
|
153
|
+
.filter((importPath) => {
|
|
154
|
+
return importPath.value.specifiers.some((importSpecifier) => importSpecifier.local.name === localName);
|
|
155
|
+
});
|
|
156
|
+
if (importDeclaration.size() > 0) {
|
|
157
|
+
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\``);
|
|
158
|
+
modified ||= commentInserted;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return modified;
|
|
166
|
+
}
|
|
127
167
|
function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
168
|
+
let modified = false;
|
|
128
169
|
if (paramTypeAnnotation && paramTypeAnnotation.typeAnnotation) {
|
|
129
170
|
const typeAnnotation = paramTypeAnnotation.typeAnnotation;
|
|
130
171
|
if (typeAnnotation.type === 'TSTypeLiteral') {
|
|
@@ -151,6 +192,7 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
|
151
192
|
// @ts-ignore
|
|
152
193
|
member.typeAnnotation.typeAnnotation,
|
|
153
194
|
]));
|
|
195
|
+
modified = true;
|
|
154
196
|
}
|
|
155
197
|
}
|
|
156
198
|
});
|
|
@@ -189,6 +231,7 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
|
189
231
|
member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
|
|
190
232
|
member.typeAnnotation.typeAnnotation,
|
|
191
233
|
]));
|
|
234
|
+
modified = true;
|
|
192
235
|
}
|
|
193
236
|
}
|
|
194
237
|
});
|
|
@@ -197,11 +240,13 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
|
197
240
|
}
|
|
198
241
|
}
|
|
199
242
|
propsIdentifier.typeAnnotation = paramTypeAnnotation;
|
|
243
|
+
modified = true;
|
|
200
244
|
}
|
|
245
|
+
return modified;
|
|
201
246
|
}
|
|
202
247
|
function transformDynamicProps(source, api, filePath) {
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
248
|
+
const isEntryFile = utils_1.NEXTJS_ENTRY_FILES.test(filePath);
|
|
249
|
+
if (!isEntryFile) {
|
|
205
250
|
return null;
|
|
206
251
|
}
|
|
207
252
|
let modified = false;
|
|
@@ -289,7 +334,8 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
289
334
|
}
|
|
290
335
|
}
|
|
291
336
|
else {
|
|
292
|
-
|
|
337
|
+
const awaited = awaitMemberAccessOfProp(argName, path, j);
|
|
338
|
+
modified ||= awaited;
|
|
293
339
|
}
|
|
294
340
|
// cases of passing down `props` into any function
|
|
295
341
|
// Page(props) { callback(props) }
|
|
@@ -309,8 +355,8 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
309
355
|
const args = callExpression.value.arguments;
|
|
310
356
|
const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
|
|
311
357
|
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
|
|
358
|
+
const inserted = (0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
|
|
359
|
+
modified ||= inserted;
|
|
314
360
|
});
|
|
315
361
|
if (modified) {
|
|
316
362
|
modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
|
|
@@ -328,6 +374,14 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
328
374
|
modified = true;
|
|
329
375
|
}
|
|
330
376
|
}
|
|
377
|
+
else {
|
|
378
|
+
// When the prop argument is not destructured, we need to add comments to the spread properties
|
|
379
|
+
if (j.Identifier.check(currentParam)) {
|
|
380
|
+
const commented = commentSpreadProps(path, currentParam.name, j);
|
|
381
|
+
const modifiedTypes = modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
|
|
382
|
+
modified ||= commented || modifiedTypes;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
331
385
|
}
|
|
332
386
|
// Helper function to insert `const params = await asyncParams;` at the beginning of the function body
|
|
333
387
|
function resolveAsyncProp(path, propertiesMap, propsIdentifierName, allProperties, isDefaultExport) {
|
|
@@ -446,20 +500,42 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
446
500
|
continue;
|
|
447
501
|
}
|
|
448
502
|
}
|
|
449
|
-
const
|
|
503
|
+
const paramsPropertyName = j.Identifier.check(paramsProperty)
|
|
450
504
|
? paramsProperty.name
|
|
451
505
|
: null;
|
|
452
|
-
const
|
|
506
|
+
const paramPropertyName = paramsPropertyName || matchedPropName;
|
|
453
507
|
// if propName is not used in lower scope, and it stars with unused prefix `_`,
|
|
454
508
|
// also skip the transformation
|
|
455
509
|
const functionBodyPath = path.get('body');
|
|
456
510
|
const hasUsedInBody = j(functionBodyPath)
|
|
457
511
|
.find(j.Identifier, {
|
|
458
|
-
name:
|
|
512
|
+
name: paramPropertyName,
|
|
459
513
|
})
|
|
460
514
|
.size() > 0;
|
|
461
|
-
if (!hasUsedInBody &&
|
|
515
|
+
if (!hasUsedInBody && paramPropertyName.startsWith('_'))
|
|
462
516
|
continue;
|
|
517
|
+
// Search the usage of propName in the function body,
|
|
518
|
+
// if they're all awaited or wrapped with use(), skip the transformation
|
|
519
|
+
const propUsages = j(functionBodyPath).find(j.Identifier, {
|
|
520
|
+
name: paramPropertyName,
|
|
521
|
+
});
|
|
522
|
+
// if there's usage of the propName, then do the check
|
|
523
|
+
if (propUsages.size()) {
|
|
524
|
+
let hasMissingAwaited = false;
|
|
525
|
+
propUsages.forEach((propUsage) => {
|
|
526
|
+
// If the parent is not AwaitExpression, it's not awaited
|
|
527
|
+
const isAwaited = propUsage.parentPath?.value.type === 'AwaitExpression';
|
|
528
|
+
const isAwaitedByUse = isParentUseCallExpression(propUsage, j);
|
|
529
|
+
if (!isAwaited && !isAwaitedByUse) {
|
|
530
|
+
hasMissingAwaited = true;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
// If all the usages of parm are awaited, skip the transformation
|
|
535
|
+
if (!hasMissingAwaited) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
463
539
|
modifiedPropertyCount++;
|
|
464
540
|
const propNameIdentifier = j.identifier(matchedPropName);
|
|
465
541
|
const propsIdentifier = j.identifier(propsIdentifierName);
|
|
@@ -468,7 +544,7 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
468
544
|
// e.g.
|
|
469
545
|
// input: Page({ params: { slug } })
|
|
470
546
|
// output: const { slug } = await props.params; rather than const props = await props.params;
|
|
471
|
-
const uid = functionName + ':' +
|
|
547
|
+
const uid = functionName + ':' + paramPropertyName;
|
|
472
548
|
if (paramsProperty?.type === 'ObjectPattern') {
|
|
473
549
|
const objectPattern = paramsProperty;
|
|
474
550
|
const objectPatternProperties = objectPattern.properties;
|
|
@@ -490,7 +566,7 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
490
566
|
if (isAsyncFunc) {
|
|
491
567
|
// If it's async function, add await to the async props.<propName>
|
|
492
568
|
const paramAssignment = j.variableDeclaration('const', [
|
|
493
|
-
j.variableDeclarator(j.identifier(
|
|
569
|
+
j.variableDeclarator(j.identifier(paramPropertyName), j.awaitExpression(accessedPropIdExpr)),
|
|
494
570
|
]);
|
|
495
571
|
if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
|
|
496
572
|
functionBody.unshift(paramAssignment);
|
|
@@ -508,7 +584,7 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
508
584
|
(0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
|
|
509
585
|
// Insert `const <propName> = await props.<propName>;` at the beginning of the function body
|
|
510
586
|
const paramAssignment = j.variableDeclaration('const', [
|
|
511
|
-
j.variableDeclarator(j.identifier(
|
|
587
|
+
j.variableDeclarator(j.identifier(paramPropertyName), j.awaitExpression(accessedPropIdExpr)),
|
|
512
588
|
]);
|
|
513
589
|
if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
|
|
514
590
|
functionBody.unshift(paramAssignment);
|
|
@@ -518,7 +594,7 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
518
594
|
}
|
|
519
595
|
else {
|
|
520
596
|
const paramAssignment = j.variableDeclaration('const', [
|
|
521
|
-
j.variableDeclarator(j.identifier(
|
|
597
|
+
j.variableDeclarator(j.identifier(paramPropertyName), j.callExpression(j.identifier('use'), [accessedPropIdExpr])),
|
|
522
598
|
]);
|
|
523
599
|
if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
|
|
524
600
|
needsReactUseImport = true;
|
|
@@ -555,6 +631,8 @@ function transformDynamicProps(source, api, filePath) {
|
|
|
555
631
|
if (needsReactUseImport) {
|
|
556
632
|
(0, utils_1.insertReactUseImport)(root, j);
|
|
557
633
|
}
|
|
634
|
+
const commented = commentOnMatchedReExports(root, j);
|
|
635
|
+
modified ||= commented;
|
|
558
636
|
return modified ? root.toSource() : null;
|
|
559
637
|
}
|
|
560
638
|
function findAllTypes(root, j, typeName) {
|
|
@@ -610,4 +688,27 @@ function findAllTypes(root, j, typeName) {
|
|
|
610
688
|
});
|
|
611
689
|
return types;
|
|
612
690
|
}
|
|
691
|
+
function commentSpreadProps(path, propsIdentifierName, j) {
|
|
692
|
+
let modified = false;
|
|
693
|
+
const functionBody = findFunctionBody(path);
|
|
694
|
+
const functionBodyCollection = j(functionBody);
|
|
695
|
+
// Find all the usage of spreading properties of `props`
|
|
696
|
+
const jsxSpreadProperties = functionBodyCollection.find(j.JSXSpreadAttribute, { argument: { name: propsIdentifierName } });
|
|
697
|
+
const objSpreadProperties = functionBodyCollection.find(j.SpreadElement, {
|
|
698
|
+
argument: { name: propsIdentifierName },
|
|
699
|
+
});
|
|
700
|
+
const comment = ` Next.js Dynamic Async API Codemod: '${propsIdentifierName}' is used with spread syntax (...). Any asynchronous properties of '${propsIdentifierName}' must be awaited when accessed. `;
|
|
701
|
+
// Add comment before it
|
|
702
|
+
jsxSpreadProperties.forEach((spread) => {
|
|
703
|
+
const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
|
|
704
|
+
if (inserted)
|
|
705
|
+
modified = true;
|
|
706
|
+
});
|
|
707
|
+
objSpreadProperties.forEach((spread) => {
|
|
708
|
+
const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
|
|
709
|
+
if (inserted)
|
|
710
|
+
modified = true;
|
|
711
|
+
});
|
|
712
|
+
return modified;
|
|
713
|
+
}
|
|
613
714
|
//# 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;
|