@next/codemod 15.0.0-canary.173 → 15.0.0-canary.175
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 +6 -1
- package/bin/transform.js +1 -1
- package/bin/upgrade.js +67 -110
- 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 +3 -6
- package/transforms/lib/async-request-api/next-async-dynamic-prop.js +127 -91
- package/transforms/lib/async-request-api/utils.js +31 -34
package/bin/next-codemod.js
CHANGED
|
@@ -30,7 +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
|
-
.
|
|
33
|
+
.argument('[revision]', 'NPM dist tag or exact version to upgrade to (e.g. "latest" or "15.0.0-canary.167"). Valid dist-tags are "latest", "canary" or "rc".', packageJson.version.includes('-canary.')
|
|
34
|
+
? 'canary'
|
|
35
|
+
: packageJson.version.includes('-rc.')
|
|
36
|
+
? 'rc'
|
|
37
|
+
: 'latest')
|
|
38
|
+
.option('--verbose', 'Verbose output', false)
|
|
34
39
|
.action(upgrade_1.runUpgrade);
|
|
35
40
|
program.parse(process.argv);
|
|
36
41
|
//# 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,93 +12,73 @@ 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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const res = await fetch(`https://registry.npmjs.org/next/${shortcutVersion}`);
|
|
24
|
-
if (res.status === 200) {
|
|
25
|
-
targetNextPackageJson = await res.json();
|
|
26
|
-
targetVersionSpecifier = targetNextPackageJson.version;
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
console.error(`${chalk_1.default.yellow('Next.js ' + shortcutVersion)} 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
|
-
}
|
|
34
|
+
const res = await fetch(`https://registry.npmjs.org/next/${revision}`);
|
|
35
|
+
if (res.status === 200) {
|
|
36
|
+
targetNextPackageJson = await res.json();
|
|
31
37
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let nextPackageJson = {};
|
|
35
|
-
try {
|
|
36
|
-
const resCanary = await fetch(`https://registry.npmjs.org/next/canary`);
|
|
37
|
-
nextPackageJson['canary'] = await resCanary.json();
|
|
38
|
-
const resRc = await fetch(`https://registry.npmjs.org/next/rc`);
|
|
39
|
-
nextPackageJson['rc'] = await resRc.json();
|
|
40
|
-
const resLatest = await fetch(`https://registry.npmjs.org/next/latest`);
|
|
41
|
-
nextPackageJson['latest'] = await resLatest.json();
|
|
42
|
-
}
|
|
43
|
-
catch (error) {
|
|
44
|
-
console.error('Failed to fetch versions from npm registry.');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
let showRc = true;
|
|
48
|
-
if (nextPackageJson['latest'].version && nextPackageJson['rc'].version) {
|
|
49
|
-
showRc =
|
|
50
|
-
(0, compare_versions_1.compareVersions)(nextPackageJson['rc'].version, nextPackageJson['latest'].version) === 1;
|
|
51
|
-
}
|
|
52
|
-
const choices = [
|
|
53
|
-
{
|
|
54
|
-
title: 'Canary',
|
|
55
|
-
value: 'canary',
|
|
56
|
-
description: `Experimental version with latest features (${nextPackageJson['canary'].version})`,
|
|
57
|
-
},
|
|
58
|
-
];
|
|
59
|
-
if (showRc) {
|
|
60
|
-
choices.push({
|
|
61
|
-
title: 'Release Candidate',
|
|
62
|
-
value: 'rc',
|
|
63
|
-
description: `Pre-release version for final testing (${nextPackageJson['rc'].version})`,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
choices.push({
|
|
67
|
-
title: 'Stable',
|
|
68
|
-
value: 'latest',
|
|
69
|
-
description: `Production-ready release (${nextPackageJson['latest'].version})`,
|
|
70
|
-
});
|
|
71
|
-
if (installedNextVersion) {
|
|
72
|
-
console.log(`You are currently using ${chalk_1.default.blue('Next.js ' + installedNextVersion)}`);
|
|
73
|
-
}
|
|
74
|
-
const initialVersionSpecifierIdx = await getVersionSpecifierIdx(installedNextVersion, showRc);
|
|
75
|
-
const response = await (0, prompts_1.default)({
|
|
76
|
-
type: 'select',
|
|
77
|
-
name: 'version',
|
|
78
|
-
message: 'What Next.js version do you want to upgrade to?',
|
|
79
|
-
choices: choices,
|
|
80
|
-
initial: initialVersionSpecifierIdx,
|
|
81
|
-
}, { onCancel: () => process.exit(0) });
|
|
82
|
-
targetNextPackageJson = nextPackageJson[response.version];
|
|
83
|
-
targetVersionSpecifier = response.version;
|
|
38
|
+
else {
|
|
39
|
+
throw new 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')}.`);
|
|
84
40
|
}
|
|
41
|
+
const installedNextVersion = await getInstalledNextVersion();
|
|
85
42
|
const targetNextVersion = targetNextPackageJson.version;
|
|
86
|
-
|
|
87
|
-
|
|
43
|
+
// We're resolving a specific version here to avoid including "ugly" version queries
|
|
44
|
+
// in the manifest.
|
|
45
|
+
// E.g. in peerDependencies we could have `^18.2.0 || ^19.0.0 || 20.0.0-canary`
|
|
46
|
+
// If we'd just `npm add` that, the manifest would read the same version query.
|
|
47
|
+
// This is basically a `npm --save-exact react@$versionQuery` that works for every package manager.
|
|
48
|
+
const [targetReactVersion, targetReactTypesVersion, targetReactDOMTypesVersion,] = await Promise.all([
|
|
49
|
+
loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`),
|
|
50
|
+
loadHighestNPMVersionMatching(`@types/react@${targetNextPackageJson.peerDependencies['react']}`),
|
|
51
|
+
loadHighestNPMVersionMatching(`@types/react-dom@${targetNextPackageJson.peerDependencies['react']}`),
|
|
52
|
+
]);
|
|
53
|
+
if ((0, compare_versions_1.compareVersions)(targetNextVersion, '15.0.0-canary') >= 0) {
|
|
88
54
|
await suggestTurbopack(appPackageJson);
|
|
89
55
|
}
|
|
56
|
+
const codemods = await suggestCodemods(installedNextVersion, targetNextVersion);
|
|
90
57
|
fs_1.default.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2));
|
|
91
58
|
const packageManager = (0, handle_package_1.getPkgManager)(process.cwd());
|
|
92
59
|
const nextDependency = `next@${targetNextVersion}`;
|
|
93
60
|
const reactDependencies = [
|
|
94
|
-
`react@${
|
|
95
|
-
|
|
96
|
-
`react-dom@${targetNextPackageJson.peerDependencies['react-dom']}`,
|
|
97
|
-
`@types/react-dom@${targetNextPackageJson.devDependencies['@types/react-dom']}`,
|
|
61
|
+
`react@${targetReactVersion}`,
|
|
62
|
+
`react-dom@${targetReactVersion}`,
|
|
98
63
|
];
|
|
99
|
-
(0
|
|
100
|
-
|
|
101
|
-
|
|
64
|
+
if (targetReactVersion.startsWith('19.0.0-canary') ||
|
|
65
|
+
targetReactVersion.startsWith('19.0.0-beta') ||
|
|
66
|
+
targetReactVersion.startsWith('19.0.0-rc')) {
|
|
67
|
+
reactDependencies.push(`@types/react@npm:types-react@rc`);
|
|
68
|
+
reactDependencies.push(`@types/react-dom@npm:types-react-dom@rc`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
reactDependencies.push(`@types/react@${targetReactTypesVersion}`);
|
|
72
|
+
reactDependencies.push(`@types/react-dom@${targetReactDOMTypesVersion}`);
|
|
73
|
+
}
|
|
74
|
+
console.log(`Upgrading your project to ${chalk_1.default.blue('Next.js ' + targetNextVersion)}...\n`);
|
|
75
|
+
(0, handle_package_1.installPackages)([nextDependency, ...reactDependencies], {
|
|
76
|
+
packageManager,
|
|
77
|
+
silent: !verbose,
|
|
78
|
+
});
|
|
79
|
+
for (const codemod of codemods) {
|
|
80
|
+
await (0, transform_1.runTransform)(codemod, process.cwd(), { force: true });
|
|
81
|
+
}
|
|
102
82
|
console.log(`\n${chalk_1.default.green('✔')} Your Next.js project has been upgraded successfully. ${chalk_1.default.bold('Time to ship! 🚢')}`);
|
|
103
83
|
}
|
|
104
84
|
async function detectWorkspace(appPackageJson) {
|
|
@@ -124,22 +104,6 @@ async function getInstalledNextVersion() {
|
|
|
124
104
|
const installedNextPackageJson = JSON.parse(fs_1.default.readFileSync(installedNextPackageJsonDir, 'utf8'));
|
|
125
105
|
return installedNextPackageJson.version;
|
|
126
106
|
}
|
|
127
|
-
/*
|
|
128
|
-
* Returns the index of the current version's specifier in the
|
|
129
|
-
* array ['canary', 'rc', 'latest'] or ['canary', 'latest']
|
|
130
|
-
*/
|
|
131
|
-
async function getVersionSpecifierIdx(installedNextVersion, showRc) {
|
|
132
|
-
if (installedNextVersion == null) {
|
|
133
|
-
return 0;
|
|
134
|
-
}
|
|
135
|
-
if (installedNextVersion.includes('canary')) {
|
|
136
|
-
return 0;
|
|
137
|
-
}
|
|
138
|
-
if (installedNextVersion.includes('rc')) {
|
|
139
|
-
return 1;
|
|
140
|
-
}
|
|
141
|
-
return showRc ? 2 : 1;
|
|
142
|
-
}
|
|
143
107
|
/*
|
|
144
108
|
* Heuristics are used to determine whether to Turbopack is enabled or not and
|
|
145
109
|
* to determine how to update the dev script.
|
|
@@ -184,7 +148,7 @@ async function suggestTurbopack(packageJson) {
|
|
|
184
148
|
async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
185
149
|
const initialVersionIndex = codemods_1.availableCodemods.findIndex((versionCodemods) => (0, compare_versions_1.compareVersions)(versionCodemods.version, initialNextVersion) > 0);
|
|
186
150
|
if (initialVersionIndex === -1) {
|
|
187
|
-
return;
|
|
151
|
+
return [];
|
|
188
152
|
}
|
|
189
153
|
let targetVersionIndex = codemods_1.availableCodemods.findIndex((versionCodemods) => (0, compare_versions_1.compareVersions)(versionCodemods.version, targetNextVersion) > 0);
|
|
190
154
|
if (targetVersionIndex === -1) {
|
|
@@ -194,31 +158,24 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
|
194
158
|
.slice(initialVersionIndex, targetVersionIndex)
|
|
195
159
|
.flatMap((versionCodemods) => versionCodemods.codemods);
|
|
196
160
|
if (relevantCodemods.length === 0) {
|
|
197
|
-
return;
|
|
161
|
+
return [];
|
|
198
162
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
163
|
+
const { codemods } = await (0, prompts_1.default)({
|
|
164
|
+
type: 'multiselect',
|
|
165
|
+
name: 'codemods',
|
|
166
|
+
message: `\nThe following ${chalk_1.default.blue('codemods')} are recommended for your upgrade. Would you like to apply them?`,
|
|
167
|
+
choices: relevantCodemods.map((codemod) => {
|
|
168
|
+
return {
|
|
169
|
+
title: `${codemod.title} ${chalk_1.default.grey(`(${codemod.value})`)}`,
|
|
170
|
+
value: codemod.value,
|
|
171
|
+
selected: true,
|
|
172
|
+
};
|
|
173
|
+
}),
|
|
210
174
|
}, {
|
|
211
175
|
onCancel: () => {
|
|
212
176
|
process.exit(0);
|
|
213
177
|
},
|
|
214
178
|
});
|
|
215
|
-
|
|
216
|
-
return;
|
|
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
|
-
});
|
|
222
|
-
}
|
|
179
|
+
return codemods;
|
|
223
180
|
}
|
|
224
181
|
//# 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
|
@@ -17,7 +17,7 @@ function findDynamicImportsAndComment(root, j) {
|
|
|
17
17
|
arguments: [{ value: 'next/headers' }],
|
|
18
18
|
});
|
|
19
19
|
importPaths.forEach((path) => {
|
|
20
|
-
(0, utils_1.insertCommentOnce)(path, j, DYNAMIC_IMPORT_WARN_COMMENT);
|
|
20
|
+
(0, utils_1.insertCommentOnce)(path.node, j, DYNAMIC_IMPORT_WARN_COMMENT);
|
|
21
21
|
modified = true;
|
|
22
22
|
});
|
|
23
23
|
return modified;
|
|
@@ -145,7 +145,7 @@ function transformDynamicAPI(source, api, filePath) {
|
|
|
145
145
|
}
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
|
-
const isClientComponent = (0, utils_1.determineClientDirective)(root, j
|
|
148
|
+
const isClientComponent = (0, utils_1.determineClientDirective)(root, j);
|
|
149
149
|
// Only transform the valid calls in server or shared components
|
|
150
150
|
if (isClientComponent)
|
|
151
151
|
return null;
|
|
@@ -219,10 +219,7 @@ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, in
|
|
|
219
219
|
}
|
|
220
220
|
else {
|
|
221
221
|
// Otherwise for JS file, leave a message to the user to manually handle the transformation
|
|
222
|
-
path.node
|
|
223
|
-
j.commentBlock(customMessage),
|
|
224
|
-
...(path.node.comments || []),
|
|
225
|
-
];
|
|
222
|
+
(0, utils_1.insertCommentOnce)(path.node, j, customMessage);
|
|
226
223
|
}
|
|
227
224
|
}
|
|
228
225
|
function findImportMappingFromNextHeaders(root, j) {
|
|
@@ -89,7 +89,87 @@ function applyUseAndRenameAccessedProp(propIdName, path, j) {
|
|
|
89
89
|
}
|
|
90
90
|
return modified;
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
const MATCHED_FILE_PATTERNS = /([\\/]|^)(page|layout)\.(t|j)sx?$/;
|
|
93
|
+
function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
94
|
+
if (paramTypeAnnotation && paramTypeAnnotation.typeAnnotation) {
|
|
95
|
+
const typeAnnotation = paramTypeAnnotation.typeAnnotation;
|
|
96
|
+
if (typeAnnotation.type === 'TSTypeLiteral') {
|
|
97
|
+
const typeLiteral = typeAnnotation;
|
|
98
|
+
// Find the type property for `params`
|
|
99
|
+
typeLiteral.members.forEach((member) => {
|
|
100
|
+
if (member.type === 'TSPropertySignature' &&
|
|
101
|
+
member.key.type === 'Identifier' &&
|
|
102
|
+
utils_1.TARGET_PROP_NAMES.has(member.key.name)) {
|
|
103
|
+
// if it's already a Promise, don't wrap it again, return
|
|
104
|
+
if (member.typeAnnotation &&
|
|
105
|
+
member.typeAnnotation.typeAnnotation &&
|
|
106
|
+
member.typeAnnotation.typeAnnotation.type === 'TSTypeReference' &&
|
|
107
|
+
member.typeAnnotation.typeAnnotation.typeName.type ===
|
|
108
|
+
'Identifier' &&
|
|
109
|
+
member.typeAnnotation.typeAnnotation.typeName.name === 'Promise') {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Wrap the `params` type in Promise<>
|
|
113
|
+
if (member.typeAnnotation &&
|
|
114
|
+
member.typeAnnotation.typeAnnotation &&
|
|
115
|
+
j.TSType.check(member.typeAnnotation.typeAnnotation)) {
|
|
116
|
+
member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
member.typeAnnotation.typeAnnotation,
|
|
119
|
+
]));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else if (typeAnnotation.type === 'TSTypeReference') {
|
|
125
|
+
// If typeAnnotation is a type or interface, change the properties to Promise<type of property>
|
|
126
|
+
// e.g. interface PageProps { params: { slug: string } } => interface PageProps { params: Promise<{ slug: string }> }
|
|
127
|
+
const typeReference = typeAnnotation;
|
|
128
|
+
if (typeReference.typeName.type === 'Identifier') {
|
|
129
|
+
// Find the actual type of the type reference
|
|
130
|
+
const foundTypes = findAllTypes(root, j, typeReference.typeName.name);
|
|
131
|
+
// Deal with interfaces
|
|
132
|
+
if (foundTypes.interfaces.length > 0) {
|
|
133
|
+
const interfaceDeclaration = foundTypes.interfaces[0];
|
|
134
|
+
if (interfaceDeclaration.type === 'TSInterfaceDeclaration' &&
|
|
135
|
+
interfaceDeclaration.body?.type === 'TSInterfaceBody') {
|
|
136
|
+
const typeBody = interfaceDeclaration.body.body;
|
|
137
|
+
// if it's already a Promise, don't wrap it again, return
|
|
138
|
+
// traverse the typeReference's properties, if any is in propNames, wrap it in Promise<> if needed
|
|
139
|
+
typeBody.forEach((member) => {
|
|
140
|
+
if (member.type === 'TSPropertySignature' &&
|
|
141
|
+
member.key.type === 'Identifier' &&
|
|
142
|
+
utils_1.TARGET_PROP_NAMES.has(member.key.name)) {
|
|
143
|
+
// if it's already a Promise, don't wrap it again, return
|
|
144
|
+
if (member.typeAnnotation &&
|
|
145
|
+
member.typeAnnotation.typeAnnotation &&
|
|
146
|
+
member.typeAnnotation?.typeAnnotation?.typeName?.name ===
|
|
147
|
+
'Promise') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Wrap the prop type in Promise<>
|
|
151
|
+
if (member.typeAnnotation &&
|
|
152
|
+
member.typeAnnotation.typeAnnotation &&
|
|
153
|
+
// check if member name is in propNames
|
|
154
|
+
utils_1.TARGET_PROP_NAMES.has(member.key.name)) {
|
|
155
|
+
member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
|
|
156
|
+
member.typeAnnotation.typeAnnotation,
|
|
157
|
+
]));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
propsIdentifier.typeAnnotation = paramTypeAnnotation;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function transformDynamicProps(source, api, filePath) {
|
|
169
|
+
const isMatched = MATCHED_FILE_PATTERNS.test(filePath);
|
|
170
|
+
if (!isMatched) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
93
173
|
let modified = false;
|
|
94
174
|
let modifiedPropArgument = false;
|
|
95
175
|
const j = api.jscodeshift.withParser('tsx');
|
|
@@ -100,29 +180,39 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
100
180
|
// e.g. destruct `params` { slug } = params
|
|
101
181
|
// e.g. destruct `searchParams `{ search } = searchParams
|
|
102
182
|
let insertedDestructPropNames = new Set();
|
|
103
|
-
// Rename props to `prop` argument for the function
|
|
104
|
-
let insertedRenamedPropFunctionNames = new Set();
|
|
105
183
|
function processAsyncPropOfEntryFile(isClientComponent) {
|
|
106
184
|
// find `params` and `searchParams` in file, and transform the access to them
|
|
107
185
|
function renameAsyncPropIfExisted(path, isDefaultExport) {
|
|
108
186
|
const decl = path.value;
|
|
109
187
|
const params = decl.params;
|
|
110
|
-
|
|
188
|
+
let functionName = decl.id?.name;
|
|
189
|
+
// If it's const <id> = function () {}, locate the <id> to get function name
|
|
190
|
+
if (!decl.id) {
|
|
191
|
+
functionName = (0, utils_1.getVariableDeclaratorId)(path, j)?.name;
|
|
192
|
+
}
|
|
111
193
|
// target properties mapping, only contains `params` and `searchParams`
|
|
112
194
|
const propertiesMap = new Map();
|
|
113
195
|
let allProperties = [];
|
|
196
|
+
const isRoute = !isDefaultExport && utils_1.TARGET_ROUTE_EXPORTS.has(functionName);
|
|
114
197
|
// generateMetadata API has 2 params
|
|
115
198
|
if (functionName === 'generateMetadata') {
|
|
116
199
|
if (params.length > 2 || params.length === 0)
|
|
117
200
|
return;
|
|
118
201
|
}
|
|
202
|
+
else if (isRoute) {
|
|
203
|
+
if (params.length !== 2)
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
119
206
|
else {
|
|
120
|
-
// Page/Layout
|
|
207
|
+
// Page/Layout default export have 1 param
|
|
121
208
|
if (params.length !== 1)
|
|
122
209
|
return;
|
|
123
210
|
}
|
|
124
211
|
const propsIdentifier = (0, utils_1.generateUniqueIdentifier)(PAGE_PROPS, path, j);
|
|
125
|
-
const
|
|
212
|
+
const propsArgumentIndex = isRoute ? 1 : 0;
|
|
213
|
+
const currentParam = params[propsArgumentIndex];
|
|
214
|
+
if (!currentParam)
|
|
215
|
+
return;
|
|
126
216
|
// Argument destructuring case
|
|
127
217
|
if (currentParam.type === 'ObjectPattern') {
|
|
128
218
|
// Validate if the properties are not `params` and `searchParams`,
|
|
@@ -150,85 +240,9 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
150
240
|
propertiesMap.set(prop.key.name, value);
|
|
151
241
|
}
|
|
152
242
|
});
|
|
153
|
-
|
|
154
|
-
if (paramTypeAnnotation && paramTypeAnnotation.typeAnnotation) {
|
|
155
|
-
const typeAnnotation = paramTypeAnnotation.typeAnnotation;
|
|
156
|
-
if (typeAnnotation.type === 'TSTypeLiteral') {
|
|
157
|
-
const typeLiteral = typeAnnotation;
|
|
158
|
-
// Find the type property for `params`
|
|
159
|
-
typeLiteral.members.forEach((member) => {
|
|
160
|
-
if (member.type === 'TSPropertySignature' &&
|
|
161
|
-
member.key.type === 'Identifier' &&
|
|
162
|
-
propertiesMap.has(member.key.name)) {
|
|
163
|
-
// if it's already a Promise, don't wrap it again, return
|
|
164
|
-
if (member.typeAnnotation &&
|
|
165
|
-
member.typeAnnotation.typeAnnotation &&
|
|
166
|
-
member.typeAnnotation.typeAnnotation.type ===
|
|
167
|
-
'TSTypeReference' &&
|
|
168
|
-
member.typeAnnotation.typeAnnotation.typeName.type ===
|
|
169
|
-
'Identifier' &&
|
|
170
|
-
member.typeAnnotation.typeAnnotation.typeName.name ===
|
|
171
|
-
'Promise') {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
// Wrap the `params` type in Promise<>
|
|
175
|
-
if (member.typeAnnotation &&
|
|
176
|
-
member.typeAnnotation.typeAnnotation &&
|
|
177
|
-
j.TSType.check(member.typeAnnotation.typeAnnotation)) {
|
|
178
|
-
member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
|
|
179
|
-
// @ts-ignore
|
|
180
|
-
member.typeAnnotation.typeAnnotation,
|
|
181
|
-
]));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
else if (typeAnnotation.type === 'TSTypeReference') {
|
|
187
|
-
// If typeAnnotation is a type or interface, change the properties to Promise<type of property>
|
|
188
|
-
// e.g. interface PageProps { params: { slug: string } } => interface PageProps { params: Promise<{ slug: string }> }
|
|
189
|
-
const typeReference = typeAnnotation;
|
|
190
|
-
if (typeReference.typeName.type === 'Identifier') {
|
|
191
|
-
// Find the actual type of the type reference
|
|
192
|
-
const foundTypes = findAllTypes(root, j, typeReference.typeName.name);
|
|
193
|
-
// Deal with interfaces
|
|
194
|
-
if (foundTypes.interfaces.length > 0) {
|
|
195
|
-
const interfaceDeclaration = foundTypes.interfaces[0];
|
|
196
|
-
if (interfaceDeclaration.type === 'TSInterfaceDeclaration' &&
|
|
197
|
-
interfaceDeclaration.body?.type === 'TSInterfaceBody') {
|
|
198
|
-
const typeBody = interfaceDeclaration.body.body;
|
|
199
|
-
// if it's already a Promise, don't wrap it again, return
|
|
200
|
-
// traverse the typeReference's properties, if any is in propNames, wrap it in Promise<> if needed
|
|
201
|
-
typeBody.forEach((member) => {
|
|
202
|
-
if (member.type === 'TSPropertySignature' &&
|
|
203
|
-
member.key.type === 'Identifier' &&
|
|
204
|
-
utils_1.TARGET_PROP_NAMES.has(member.key.name)) {
|
|
205
|
-
// if it's already a Promise, don't wrap it again, return
|
|
206
|
-
if (member.typeAnnotation &&
|
|
207
|
-
member.typeAnnotation.typeAnnotation &&
|
|
208
|
-
member.typeAnnotation?.typeAnnotation?.typeName
|
|
209
|
-
?.name === 'Promise') {
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
// Wrap the prop type in Promise<>
|
|
213
|
-
if (member.typeAnnotation &&
|
|
214
|
-
member.typeAnnotation.typeAnnotation &&
|
|
215
|
-
// check if member name is in propNames
|
|
216
|
-
utils_1.TARGET_PROP_NAMES.has(member.key.name)) {
|
|
217
|
-
member.typeAnnotation.typeAnnotation =
|
|
218
|
-
j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
|
|
219
|
-
member.typeAnnotation.typeAnnotation,
|
|
220
|
-
]));
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
propsIdentifier.typeAnnotation = paramTypeAnnotation;
|
|
229
|
-
}
|
|
243
|
+
modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
|
|
230
244
|
// Override the first param to `props`
|
|
231
|
-
params[
|
|
245
|
+
params[propsArgumentIndex] = propsIdentifier;
|
|
232
246
|
modified = true;
|
|
233
247
|
modifiedPropArgument = true;
|
|
234
248
|
}
|
|
@@ -264,14 +278,13 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
264
278
|
// find the argument `currentParam`
|
|
265
279
|
const args = callExpression.value.arguments;
|
|
266
280
|
const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
propPassedAsArg.comments = [
|
|
270
|
-
comment,
|
|
271
|
-
...(propPassedAsArg.comments || []),
|
|
272
|
-
];
|
|
281
|
+
const comment = ` '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `;
|
|
282
|
+
(0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
|
|
273
283
|
modified = true;
|
|
274
284
|
});
|
|
285
|
+
if (modified) {
|
|
286
|
+
modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
|
|
287
|
+
}
|
|
275
288
|
}
|
|
276
289
|
if (modifiedPropArgument) {
|
|
277
290
|
resolveAsyncProp(path, propertiesMap, propsIdentifier.name, allProperties, isDefaultExport);
|
|
@@ -279,6 +292,8 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
279
292
|
}
|
|
280
293
|
// Helper function to insert `const params = await asyncParams;` at the beginning of the function body
|
|
281
294
|
function resolveAsyncProp(path, propertiesMap, propsIdentifierName, allProperties, isDefaultExport) {
|
|
295
|
+
// Rename props to `prop` argument for the function
|
|
296
|
+
const insertedRenamedPropFunctionNames = new Set();
|
|
282
297
|
const node = path.value;
|
|
283
298
|
// If it's sync default export, and it's also server component, make the function async
|
|
284
299
|
if (isDefaultExport && !isClientComponent) {
|
|
@@ -289,6 +304,27 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
289
304
|
}
|
|
290
305
|
}
|
|
291
306
|
}
|
|
307
|
+
// If it's arrow function and function body is not block statement, check if the properties are used there
|
|
308
|
+
if (j.ArrowFunctionExpression.check(path.node) &&
|
|
309
|
+
!j.BlockStatement.check(path.node.body)) {
|
|
310
|
+
const objectExpression = path.node.body;
|
|
311
|
+
let hasUsedProps = false;
|
|
312
|
+
j(objectExpression)
|
|
313
|
+
.find(j.Identifier)
|
|
314
|
+
.forEach((identifierPath) => {
|
|
315
|
+
const idName = identifierPath.value.name;
|
|
316
|
+
if (propertiesMap.has(idName)) {
|
|
317
|
+
hasUsedProps = true;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
// Turn the function body to block statement, return the object expression
|
|
322
|
+
if (hasUsedProps) {
|
|
323
|
+
path.node.body = j.blockStatement([
|
|
324
|
+
j.returnStatement(objectExpression),
|
|
325
|
+
]);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
292
328
|
const isAsyncFunc = !!node.async;
|
|
293
329
|
const functionName = path.value.id?.name || 'default';
|
|
294
330
|
const functionBody = findFunctionBody(path);
|
|
@@ -448,7 +484,7 @@ function transformDynamicProps(source, api, _filePath) {
|
|
|
448
484
|
}
|
|
449
485
|
});
|
|
450
486
|
}
|
|
451
|
-
const isClientComponent = (0, utils_1.determineClientDirective)(root, j
|
|
487
|
+
const isClientComponent = (0, utils_1.determineClientDirective)(root, j);
|
|
452
488
|
// Apply to `params` and `searchParams`
|
|
453
489
|
processAsyncPropOfEntryFile(isClientComponent);
|
|
454
490
|
// Add import { use } from 'react' if needed and not already imported
|
|
@@ -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 = void 0;
|
|
3
|
+
exports.TARGET_PROP_NAMES = exports.TARGET_NAMED_EXPORTS = exports.TARGET_ROUTE_EXPORTS = void 0;
|
|
4
4
|
exports.isFunctionType = isFunctionType;
|
|
5
5
|
exports.isMatchedFunctionExported = isMatchedFunctionExported;
|
|
6
6
|
exports.determineClientDirective = determineClientDirective;
|
|
@@ -13,17 +13,20 @@ exports.findClosetParentFunctionScope = findClosetParentFunctionScope;
|
|
|
13
13
|
exports.getFunctionPathFromExportPath = getFunctionPathFromExportPath;
|
|
14
14
|
exports.wrapParentheseIfNeeded = wrapParentheseIfNeeded;
|
|
15
15
|
exports.insertCommentOnce = insertCommentOnce;
|
|
16
|
-
exports.
|
|
17
|
-
|
|
16
|
+
exports.getVariableDeclaratorId = getVariableDeclaratorId;
|
|
17
|
+
exports.TARGET_ROUTE_EXPORTS = new Set([
|
|
18
18
|
'GET',
|
|
19
|
-
'HEAD',
|
|
20
19
|
'POST',
|
|
21
20
|
'PUT',
|
|
22
|
-
'DELETE',
|
|
23
21
|
'PATCH',
|
|
22
|
+
'DELETE',
|
|
24
23
|
'OPTIONS',
|
|
24
|
+
'HEAD',
|
|
25
|
+
]);
|
|
26
|
+
exports.TARGET_NAMED_EXPORTS = new Set([
|
|
25
27
|
// For page and layout
|
|
26
28
|
'generateMetadata',
|
|
29
|
+
...exports.TARGET_ROUTE_EXPORTS,
|
|
27
30
|
]);
|
|
28
31
|
exports.TARGET_PROP_NAMES = new Set(['params', 'searchParams']);
|
|
29
32
|
function isFunctionType(type) {
|
|
@@ -87,31 +90,15 @@ function isMatchedFunctionExported(path, j) {
|
|
|
87
90
|
return true;
|
|
88
91
|
return isNamedExport;
|
|
89
92
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const hasStringDirectiveWithSemicolon = root
|
|
100
|
-
.find(j.StringLiteral)
|
|
101
|
-
.filter((path) => {
|
|
102
|
-
const expr = path.node;
|
|
103
|
-
return (expr.type === 'StringLiteral' &&
|
|
104
|
-
expr.value === 'use client' &&
|
|
105
|
-
path.parentPath.node.type === 'Program');
|
|
106
|
-
})
|
|
107
|
-
.size() > 0;
|
|
108
|
-
if (hasStringDirective || hasStringDirectiveWithSemicolon)
|
|
109
|
-
return true;
|
|
110
|
-
// Since the client detection is not reliable with AST in jscodeshift,
|
|
111
|
-
// determine if 'use client' or "use client" is leading in the source code.
|
|
112
|
-
const trimmedSource = source.trim();
|
|
113
|
-
const containsClientDirective = /^'use client'/.test(trimmedSource) || /^"use client"/g.test(trimmedSource);
|
|
114
|
-
return containsClientDirective;
|
|
93
|
+
// directive is not parsed into AST, so we need to manually find it
|
|
94
|
+
// by going through the tokens. Use the 1st string token as the directive
|
|
95
|
+
function determineClientDirective(root, j) {
|
|
96
|
+
const { program } = root.get().node;
|
|
97
|
+
const directive = program.directives[0];
|
|
98
|
+
if (j.Directive.check(directive)) {
|
|
99
|
+
return directive.value.value === 'use client';
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
115
102
|
}
|
|
116
103
|
function isPromiseType(typeAnnotation) {
|
|
117
104
|
return (typeAnnotation.type === 'TSTypeReference' &&
|
|
@@ -338,13 +325,23 @@ function getFunctionPathFromExportPath(exportPath, j, root, namedExportFilter) {
|
|
|
338
325
|
function wrapParentheseIfNeeded(hasChainAccess, j, expression) {
|
|
339
326
|
return hasChainAccess ? j.parenthesizedExpression(expression) : expression;
|
|
340
327
|
}
|
|
341
|
-
function insertCommentOnce(
|
|
342
|
-
if (
|
|
343
|
-
const hasComment =
|
|
328
|
+
function insertCommentOnce(node, j, comment) {
|
|
329
|
+
if (node.comments) {
|
|
330
|
+
const hasComment = node.comments.some((commentNode) => commentNode.value === comment);
|
|
344
331
|
if (hasComment) {
|
|
345
332
|
return;
|
|
346
333
|
}
|
|
347
334
|
}
|
|
348
|
-
|
|
335
|
+
node.comments = [j.commentBlock(comment), ...(node.comments || [])];
|
|
336
|
+
}
|
|
337
|
+
function getVariableDeclaratorId(path, j) {
|
|
338
|
+
const parent = path.parentPath;
|
|
339
|
+
if (j.VariableDeclarator.check(parent.node)) {
|
|
340
|
+
const id = parent.node.id;
|
|
341
|
+
if (j.Identifier.check(id)) {
|
|
342
|
+
return id;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return undefined;
|
|
349
346
|
}
|
|
350
347
|
//# sourceMappingURL=utils.js.map
|