@next/codemod 15.0.0-rc.1 → 15.0.0
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/upgrade.js
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
2
25
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
27
|
};
|
|
5
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
29
|
exports.runUpgrade = runUpgrade;
|
|
30
|
+
const os = __importStar(require("os"));
|
|
7
31
|
const prompts_1 = __importDefault(require("prompts"));
|
|
8
32
|
const fs_1 = __importDefault(require("fs"));
|
|
9
|
-
const semver_1 = __importDefault(require("semver"));
|
|
10
33
|
const compare_1 = __importDefault(require("semver/functions/compare"));
|
|
11
34
|
const child_process_1 = require("child_process");
|
|
12
35
|
const path_1 = __importDefault(require("path"));
|
|
@@ -15,6 +38,22 @@ const handle_package_1 = require("../lib/handle-package");
|
|
|
15
38
|
const transform_1 = require("./transform");
|
|
16
39
|
const utils_1 = require("../lib/utils");
|
|
17
40
|
const shared_1 = require("./shared");
|
|
41
|
+
const optionalNextjsPackages = [
|
|
42
|
+
'create-next-app',
|
|
43
|
+
'eslint-config-next',
|
|
44
|
+
'@next/bundle-analyzer',
|
|
45
|
+
'@next/codemod',
|
|
46
|
+
'@next/env',
|
|
47
|
+
'@next/eslint-plugin-next',
|
|
48
|
+
'@next/font',
|
|
49
|
+
'@next/mdx',
|
|
50
|
+
'@next/plugin-storybook',
|
|
51
|
+
'@next/polyfill-module',
|
|
52
|
+
'@next/polyfill-nomodule',
|
|
53
|
+
'@next/swc',
|
|
54
|
+
'@next/react-refresh-utils',
|
|
55
|
+
'@next/third-parties',
|
|
56
|
+
];
|
|
18
57
|
/**
|
|
19
58
|
* @param query
|
|
20
59
|
* @example loadHighestNPMVersionMatching("react@^18.3.0 || ^19.0.0") === Promise<"19.0.0">
|
|
@@ -71,6 +110,10 @@ async function runUpgrade(revision, options) {
|
|
|
71
110
|
console.log(` - React: v${installedReactVersion}`);
|
|
72
111
|
console.log(` - Next.js: v${installedNextVersion}`);
|
|
73
112
|
let shouldStayOnReact18 = false;
|
|
113
|
+
const usesAppDir = isUsingAppDir(process.cwd());
|
|
114
|
+
const usesPagesDir = isUsingPagesDir(process.cwd());
|
|
115
|
+
const isPureAppRouter = usesAppDir && !usesPagesDir;
|
|
116
|
+
const isMixedApp = usesPagesDir && usesAppDir;
|
|
74
117
|
if (
|
|
75
118
|
// From release v14.3.0-canary.45, Next.js expects the React version to be 19.0.0-beta.0
|
|
76
119
|
// If the user is on a version higher than this but is still on React 18, we ask them
|
|
@@ -79,12 +122,19 @@ async function runUpgrade(revision, options) {
|
|
|
79
122
|
// we should only let the user stay on React 18 if they are using pure Pages Router.
|
|
80
123
|
// x-ref(PR): https://github.com/vercel/next.js/pull/65058
|
|
81
124
|
// x-ref(release): https://github.com/vercel/next.js/releases/tag/v14.3.0-canary.45
|
|
82
|
-
(0, compare_1.default)(
|
|
83
|
-
installedReactVersion.startsWith('18')
|
|
125
|
+
(0, compare_1.default)(targetNextVersion, '14.3.0-canary.45') >= 0 &&
|
|
126
|
+
installedReactVersion.startsWith('18') &&
|
|
127
|
+
// Pure App Router always uses React 19
|
|
128
|
+
// The mixed case is tricky to handle from a types perspective.
|
|
129
|
+
// We'll recommend to upgrade in the prompt but users can decide to try 18.
|
|
130
|
+
!isPureAppRouter) {
|
|
84
131
|
const shouldStayOnReact18Res = await (0, prompts_1.default)({
|
|
85
132
|
type: 'confirm',
|
|
86
133
|
name: 'shouldStayOnReact18',
|
|
87
|
-
message: `
|
|
134
|
+
message: `Do you prefer to stay on React 18?` +
|
|
135
|
+
(isMixedApp
|
|
136
|
+
? " Since you're using both pages/ and app/, we recommend upgrading React to use a consistent version throughout your app."
|
|
137
|
+
: ''),
|
|
88
138
|
initial: false,
|
|
89
139
|
active: 'Yes',
|
|
90
140
|
inactive: 'No',
|
|
@@ -109,7 +159,8 @@ async function runUpgrade(revision, options) {
|
|
|
109
159
|
let execCommand = 'npx';
|
|
110
160
|
// The following React codemods are for React 19
|
|
111
161
|
if (!shouldStayOnReact18 &&
|
|
112
|
-
(0, compare_1.default)(targetReactVersion, '19.0.0-
|
|
162
|
+
(0, compare_1.default)(targetReactVersion, '19.0.0-0') >= 0 &&
|
|
163
|
+
(0, compare_1.default)(installedReactVersion, '19.0.0-0') < 0) {
|
|
113
164
|
shouldRunReactCodemods = await suggestReactCodemods();
|
|
114
165
|
shouldRunReactTypesCodemods = await suggestReactTypesCodemods();
|
|
115
166
|
const execCommandMap = {
|
|
@@ -133,6 +184,12 @@ async function runUpgrade(revision, options) {
|
|
|
133
184
|
'react-dom': { version: targetReactVersion, required: true },
|
|
134
185
|
'react-is': { version: targetReactVersion, required: false },
|
|
135
186
|
};
|
|
187
|
+
for (const optionalNextjsPackage of optionalNextjsPackages) {
|
|
188
|
+
versionMapping[optionalNextjsPackage] = {
|
|
189
|
+
version: targetNextVersion,
|
|
190
|
+
required: false,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
136
193
|
if (targetReactVersion.startsWith('19.0.0-canary') ||
|
|
137
194
|
targetReactVersion.startsWith('19.0.0-beta') ||
|
|
138
195
|
targetReactVersion.startsWith('19.0.0-rc')) {
|
|
@@ -196,7 +253,9 @@ async function runUpgrade(revision, options) {
|
|
|
196
253
|
for (const [dep, version] of devDependenciesToInstall) {
|
|
197
254
|
(0, handle_package_1.addPackageDependency)(appPackageJson, dep, version, true);
|
|
198
255
|
}
|
|
199
|
-
fs_1.default.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2)
|
|
256
|
+
fs_1.default.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2) +
|
|
257
|
+
// Common IDE formatters would add a newline as well.
|
|
258
|
+
os.EOL);
|
|
200
259
|
(0, handle_package_1.runInstallation)(packageManager);
|
|
201
260
|
for (const codemod of codemods) {
|
|
202
261
|
await (0, transform_1.runTransform)(codemod, process.cwd(), { force: true, verbose });
|
|
@@ -248,6 +307,14 @@ function getInstalledReactVersion() {
|
|
|
248
307
|
});
|
|
249
308
|
}
|
|
250
309
|
}
|
|
310
|
+
function isUsingPagesDir(projectPath) {
|
|
311
|
+
return (fs_1.default.existsSync(path_1.default.resolve(projectPath, 'pages')) ||
|
|
312
|
+
fs_1.default.existsSync(path_1.default.resolve(projectPath, 'src/pages')));
|
|
313
|
+
}
|
|
314
|
+
function isUsingAppDir(projectPath) {
|
|
315
|
+
return (fs_1.default.existsSync(path_1.default.resolve(projectPath, 'app')) ||
|
|
316
|
+
fs_1.default.existsSync(path_1.default.resolve(projectPath, 'src/app')));
|
|
317
|
+
}
|
|
251
318
|
/*
|
|
252
319
|
* Heuristics are used to determine whether to Turbopack is enabled or not and
|
|
253
320
|
* to determine how to update the dev script.
|
|
@@ -287,25 +354,21 @@ async function suggestTurbopack(packageJson) {
|
|
|
287
354
|
responseCustomDevScript.customDevScript || devScript;
|
|
288
355
|
}
|
|
289
356
|
async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const initial = semver_1.default.parse(initialNextVersion);
|
|
301
|
-
const initialVersionIndex = utils_1.TRANSFORMER_INQUIRER_CHOICES.findIndex((versionCodemods) => {
|
|
302
|
-
const codemod = semver_1.default.parse(versionCodemods.version);
|
|
303
|
-
return ((0, compare_1.default)(`${codemod.major}.${codemod.minor}.${codemod.patch}`, `${initial.major}.${initial.minor}.${initial.patch}`) >= 0);
|
|
357
|
+
// example:
|
|
358
|
+
// codemod version: 15.0.0-canary.45
|
|
359
|
+
// 14.3 -> 15.0.0-canary.45: apply
|
|
360
|
+
// 14.3 -> 15.0.0-canary.44: don't apply
|
|
361
|
+
// 15.0.0-canary.44 -> 15.0.0-canary.45: apply
|
|
362
|
+
// 15.0.0-canary.45 -> 15.0.0-canary.46: don't apply
|
|
363
|
+
// 15.0.0-canary.45 -> 15.0.0 : don't apply
|
|
364
|
+
// 15.0.0-canary.44 -> 15.0.0 : apply
|
|
365
|
+
const initialVersionIndex = utils_1.TRANSFORMER_INQUIRER_CHOICES.findIndex((codemod) => {
|
|
366
|
+
return (0, compare_1.default)(codemod.version, initialNextVersion) > 0;
|
|
304
367
|
});
|
|
305
368
|
if (initialVersionIndex === -1) {
|
|
306
369
|
return [];
|
|
307
370
|
}
|
|
308
|
-
let targetVersionIndex = utils_1.TRANSFORMER_INQUIRER_CHOICES.findIndex((
|
|
371
|
+
let targetVersionIndex = utils_1.TRANSFORMER_INQUIRER_CHOICES.findIndex((codemod) => (0, compare_1.default)(codemod.version, targetNextVersion) > 0);
|
|
309
372
|
if (targetVersionIndex === -1) {
|
|
310
373
|
targetVersionIndex = utils_1.TRANSFORMER_INQUIRER_CHOICES.length;
|
|
311
374
|
}
|
|
@@ -330,39 +393,41 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
|
330
393
|
}
|
|
331
394
|
async function suggestReactCodemods() {
|
|
332
395
|
const { runReactCodemod } = await (0, prompts_1.default)({
|
|
333
|
-
type: '
|
|
396
|
+
type: 'confirm',
|
|
334
397
|
name: 'runReactCodemod',
|
|
335
398
|
message: 'Would you like to run the React 19 upgrade codemod?',
|
|
336
399
|
initial: true,
|
|
337
|
-
active: 'Yes',
|
|
338
|
-
inactive: 'No',
|
|
339
400
|
}, { onCancel: utils_1.onCancel });
|
|
340
401
|
return runReactCodemod;
|
|
341
402
|
}
|
|
342
403
|
async function suggestReactTypesCodemods() {
|
|
343
404
|
const { runReactTypesCodemod } = await (0, prompts_1.default)({
|
|
344
|
-
type: '
|
|
405
|
+
type: 'confirm',
|
|
345
406
|
name: 'runReactTypesCodemod',
|
|
346
407
|
message: 'Would you like to run the React 19 Types upgrade codemod?',
|
|
347
408
|
initial: true,
|
|
348
|
-
active: 'Yes',
|
|
349
|
-
inactive: 'No',
|
|
350
409
|
}, { onCancel: utils_1.onCancel });
|
|
351
410
|
return runReactTypesCodemod;
|
|
352
411
|
}
|
|
353
412
|
function writeOverridesField(packageJson, packageManager, overrides) {
|
|
354
|
-
|
|
413
|
+
const entries = Object.entries(overrides);
|
|
414
|
+
// Avoids writing an empty overrides field into package.json
|
|
415
|
+
// which would be an unnecessary diff.
|
|
416
|
+
if (entries.length === 0) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (packageManager === 'npm') {
|
|
355
420
|
if (!packageJson.overrides) {
|
|
356
421
|
packageJson.overrides = {};
|
|
357
422
|
}
|
|
358
|
-
for (const [key, value] of
|
|
423
|
+
for (const [key, value] of entries) {
|
|
359
424
|
packageJson.overrides[key] = value;
|
|
360
425
|
}
|
|
361
426
|
}
|
|
362
427
|
else if (packageManager === 'pnpm') {
|
|
363
428
|
// pnpm supports pnpm.overrides and pnpm.resolutions
|
|
364
429
|
if (packageJson.resolutions) {
|
|
365
|
-
for (const [key, value] of
|
|
430
|
+
for (const [key, value] of entries) {
|
|
366
431
|
packageJson.resolutions[key] = value;
|
|
367
432
|
}
|
|
368
433
|
}
|
|
@@ -373,7 +438,7 @@ function writeOverridesField(packageJson, packageManager, overrides) {
|
|
|
373
438
|
if (!packageJson.pnpm.overrides) {
|
|
374
439
|
packageJson.pnpm.overrides = {};
|
|
375
440
|
}
|
|
376
|
-
for (const [key, value] of
|
|
441
|
+
for (const [key, value] of entries) {
|
|
377
442
|
packageJson.pnpm.overrides[key] = value;
|
|
378
443
|
}
|
|
379
444
|
}
|
|
@@ -382,9 +447,27 @@ function writeOverridesField(packageJson, packageManager, overrides) {
|
|
|
382
447
|
if (!packageJson.resolutions) {
|
|
383
448
|
packageJson.resolutions = {};
|
|
384
449
|
}
|
|
385
|
-
for (const [key, value] of
|
|
450
|
+
for (const [key, value] of entries) {
|
|
386
451
|
packageJson.resolutions[key] = value;
|
|
387
452
|
}
|
|
388
453
|
}
|
|
454
|
+
else if (packageManager === 'bun') {
|
|
455
|
+
// bun supports both overrides and resolutions
|
|
456
|
+
// x-ref: https://bun.sh/docs/install/overrides
|
|
457
|
+
if (packageJson.resolutions) {
|
|
458
|
+
for (const [key, value] of entries) {
|
|
459
|
+
packageJson.resolutions[key] = value;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
// add overrides field if it's missing and add overrides
|
|
464
|
+
if (!packageJson.overrides) {
|
|
465
|
+
packageJson.overrides = {};
|
|
466
|
+
}
|
|
467
|
+
for (const [key, value] of entries) {
|
|
468
|
+
packageJson.overrides[key] = value;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
389
472
|
}
|
|
390
473
|
//# sourceMappingURL=upgrade.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@next/codemod",
|
|
3
|
-
"version": "15.0.0
|
|
3
|
+
"version": "15.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"build": "pnpm tsc -d -p tsconfig.json",
|
|
31
31
|
"prepublishOnly": "cd ../../ && turbo run build",
|
|
32
32
|
"dev": "pnpm tsc -d -w -p tsconfig.json",
|
|
33
|
-
"test": "jest"
|
|
33
|
+
"test": "jest",
|
|
34
|
+
"test:upgrade-fixture": "./scripts/test-upgrade-fixture.sh"
|
|
34
35
|
},
|
|
35
36
|
"bin": "./bin/next-codemod.js",
|
|
36
37
|
"devDependencies": {
|
|
@@ -108,10 +108,10 @@ function transformDynamicAPI(source, _api, filePath) {
|
|
|
108
108
|
// check if current path is under the default export function
|
|
109
109
|
if (isEntryFileExport) {
|
|
110
110
|
// if default export function is not async, convert it to async, and await the api call
|
|
111
|
-
if (!isCallAwaited) {
|
|
111
|
+
if (!isCallAwaited && (0, utils_1.isFunctionType)(exportFunctionNode.type)) {
|
|
112
|
+
const hasReactHooksUsage = (0, utils_1.containsReactHooksCallExpressions)(closetScopePath, j);
|
|
112
113
|
// If the scoped function is async function
|
|
113
|
-
if (
|
|
114
|
-
exportFunctionNode.async === false) {
|
|
114
|
+
if (exportFunctionNode.async === false && !hasReactHooksUsage) {
|
|
115
115
|
canConvertToAsync = true;
|
|
116
116
|
exportFunctionNode.async = true;
|
|
117
117
|
}
|
|
@@ -121,15 +121,25 @@ function transformDynamicAPI(source, _api, filePath) {
|
|
|
121
121
|
(0, utils_1.turnFunctionReturnTypeToAsync)(closetScopePath.node, j);
|
|
122
122
|
modified = true;
|
|
123
123
|
}
|
|
124
|
+
else {
|
|
125
|
+
// If it's still sync function that cannot be converted to async, wrap the api call with 'use()' if needed
|
|
126
|
+
if (!(0, utils_1.isParentUseCallExpression)(path, j)) {
|
|
127
|
+
j(path).replaceWith(j.callExpression(j.identifier('use'), [
|
|
128
|
+
j.callExpression(j.identifier(asyncRequestApiName), []),
|
|
129
|
+
]));
|
|
130
|
+
needsReactUseImport = true;
|
|
131
|
+
modified = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
124
134
|
}
|
|
125
135
|
}
|
|
126
136
|
else {
|
|
127
137
|
// if parent is function and it's a hook, which starts with 'use', wrap the api call with 'use()'
|
|
128
138
|
const parentFunction = (0, utils_1.findClosetParentFunctionScope)(path, j);
|
|
129
139
|
if (parentFunction) {
|
|
130
|
-
const parentFunctionName = parentFunction.get().node.id?.name;
|
|
131
|
-
const isParentFunctionHook =
|
|
132
|
-
if (isParentFunctionHook) {
|
|
140
|
+
const parentFunctionName = parentFunction.get().node.id?.name || '';
|
|
141
|
+
const isParentFunctionHook = (0, utils_1.isReactHookName)(parentFunctionName);
|
|
142
|
+
if (isParentFunctionHook && !(0, utils_1.isParentUseCallExpression)(path, j)) {
|
|
133
143
|
j(path).replaceWith(j.callExpression(j.identifier('use'), [
|
|
134
144
|
j.callExpression(j.identifier(asyncRequestApiName), []),
|
|
135
145
|
]));
|
|
@@ -4,16 +4,9 @@ exports.transformDynamicProps = transformDynamicProps;
|
|
|
4
4
|
const utils_1 = require("./utils");
|
|
5
5
|
const parser_1 = require("../../../lib/parser");
|
|
6
6
|
const PAGE_PROPS = 'props';
|
|
7
|
-
function findFunctionBody(path) {
|
|
8
|
-
let functionBody = path.node.body;
|
|
9
|
-
if (functionBody && functionBody.type === 'BlockStatement') {
|
|
10
|
-
return functionBody.body;
|
|
11
|
-
}
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
7
|
function awaitMemberAccessOfProp(propIdName, path, j) {
|
|
15
8
|
// search the member access of the prop
|
|
16
|
-
const functionBody = findFunctionBody(path);
|
|
9
|
+
const functionBody = (0, utils_1.findFunctionBody)(path);
|
|
17
10
|
const memberAccess = j(functionBody).find(j.MemberExpression, {
|
|
18
11
|
object: {
|
|
19
12
|
type: 'Identifier',
|
|
@@ -30,7 +23,7 @@ function awaitMemberAccessOfProp(propIdName, path, j) {
|
|
|
30
23
|
if (!isAccessingMatchedProperty) {
|
|
31
24
|
return;
|
|
32
25
|
}
|
|
33
|
-
if (isParentPromiseAllCallExpression(memberAccessPath, j)) {
|
|
26
|
+
if ((0, utils_1.isParentPromiseAllCallExpression)(memberAccessPath, j)) {
|
|
34
27
|
return;
|
|
35
28
|
}
|
|
36
29
|
// check if it's already awaited
|
|
@@ -42,44 +35,16 @@ function awaitMemberAccessOfProp(propIdName, path, j) {
|
|
|
42
35
|
memberAccessPath.replace(awaitMemberAccess);
|
|
43
36
|
hasAwaited = true;
|
|
44
37
|
});
|
|
38
|
+
const hasReactHooksUsage = (0, utils_1.containsReactHooksCallExpressions)(path.get('body'), j);
|
|
45
39
|
// If there's any awaited member access, we need to make the function async
|
|
46
40
|
if (hasAwaited) {
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
(0, utils_1.turnFunctionReturnTypeToAsync)(path.value, j);
|
|
51
|
-
}
|
|
41
|
+
if (path.value.async === false && !hasReactHooksUsage) {
|
|
42
|
+
path.value.async = true;
|
|
43
|
+
(0, utils_1.turnFunctionReturnTypeToAsync)(path.value, j);
|
|
52
44
|
}
|
|
53
45
|
}
|
|
54
46
|
return hasAwaited;
|
|
55
47
|
}
|
|
56
|
-
function isParentUseCallExpression(path, j) {
|
|
57
|
-
if (
|
|
58
|
-
// member access parentPath is argument
|
|
59
|
-
j.CallExpression.check(path.parent.value) &&
|
|
60
|
-
// member access is first argument
|
|
61
|
-
path.parent.value.arguments[0] === path.value &&
|
|
62
|
-
// function name is `use`
|
|
63
|
-
j.Identifier.check(path.parent.value.callee) &&
|
|
64
|
-
path.parent.value.callee.name === 'use') {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
function isParentPromiseAllCallExpression(path, j) {
|
|
70
|
-
const argsParent = path.parent;
|
|
71
|
-
const callParent = argsParent?.parent;
|
|
72
|
-
if (j.ArrayExpression.check(argsParent.value) &&
|
|
73
|
-
j.CallExpression.check(callParent.value) &&
|
|
74
|
-
j.MemberExpression.check(callParent.value.callee) &&
|
|
75
|
-
j.Identifier.check(callParent.value.callee.object) &&
|
|
76
|
-
callParent.value.callee.object.name === 'Promise' &&
|
|
77
|
-
j.Identifier.check(callParent.value.callee.property) &&
|
|
78
|
-
callParent.value.callee.property.name === 'all') {
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
48
|
function applyUseAndRenameAccessedProp(propIdName, path, j) {
|
|
84
49
|
// search the member access of the prop, and rename the member access to the member value
|
|
85
50
|
// e.g.
|
|
@@ -87,7 +52,7 @@ function applyUseAndRenameAccessedProp(propIdName, path, j) {
|
|
|
87
52
|
// props.params.foo => params.foo
|
|
88
53
|
// props.searchParams.search => searchParams.search
|
|
89
54
|
let modified = false;
|
|
90
|
-
const functionBody = findFunctionBody(path);
|
|
55
|
+
const functionBody = (0, utils_1.findFunctionBody)(path);
|
|
91
56
|
const memberAccess = j(functionBody).find(j.MemberExpression, {
|
|
92
57
|
object: {
|
|
93
58
|
type: 'Identifier',
|
|
@@ -98,7 +63,7 @@ function applyUseAndRenameAccessedProp(propIdName, path, j) {
|
|
|
98
63
|
// rename each member access
|
|
99
64
|
memberAccess.forEach((memberAccessPath) => {
|
|
100
65
|
// If the member access expression is first argument of `use()`, we skip
|
|
101
|
-
if (isParentUseCallExpression(memberAccessPath, j)) {
|
|
66
|
+
if ((0, utils_1.isParentUseCallExpression)(memberAccessPath, j)) {
|
|
102
67
|
return;
|
|
103
68
|
}
|
|
104
69
|
const member = memberAccessPath.value;
|
|
@@ -190,7 +155,6 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
|
190
155
|
member.typeAnnotation.typeAnnotation &&
|
|
191
156
|
j.TSType.check(member.typeAnnotation.typeAnnotation)) {
|
|
192
157
|
member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
|
|
193
|
-
// @ts-ignore
|
|
194
158
|
member.typeAnnotation.typeAnnotation,
|
|
195
159
|
]));
|
|
196
160
|
modified = true;
|
|
@@ -238,6 +202,42 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
|
|
|
238
202
|
});
|
|
239
203
|
}
|
|
240
204
|
}
|
|
205
|
+
// Deal with type aliases
|
|
206
|
+
if (foundTypes.typeAliases.length > 0) {
|
|
207
|
+
const typeAliasDeclaration = foundTypes.typeAliases[0];
|
|
208
|
+
if (j.TSTypeAliasDeclaration.check(typeAliasDeclaration)) {
|
|
209
|
+
const typeAlias = typeAliasDeclaration.typeAnnotation;
|
|
210
|
+
if (j.TSTypeLiteral.check(typeAlias) &&
|
|
211
|
+
typeAlias.members.length > 0) {
|
|
212
|
+
const typeLiteral = typeAlias;
|
|
213
|
+
typeLiteral.members.forEach((member) => {
|
|
214
|
+
if (j.TSPropertySignature.check(member) &&
|
|
215
|
+
j.Identifier.check(member.key) &&
|
|
216
|
+
utils_1.TARGET_PROP_NAMES.has(member.key.name)) {
|
|
217
|
+
// if it's already a Promise, don't wrap it again, return
|
|
218
|
+
if (member.typeAnnotation &&
|
|
219
|
+
member.typeAnnotation.typeAnnotation &&
|
|
220
|
+
member.typeAnnotation.typeAnnotation.type ===
|
|
221
|
+
'TSTypeReference' &&
|
|
222
|
+
member.typeAnnotation.typeAnnotation.typeName.type ===
|
|
223
|
+
'Identifier' &&
|
|
224
|
+
member.typeAnnotation.typeAnnotation.typeName.name ===
|
|
225
|
+
'Promise') {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Wrap the prop type in Promise<>
|
|
229
|
+
if (member.typeAnnotation &&
|
|
230
|
+
j.TSTypeLiteral.check(member.typeAnnotation.typeAnnotation)) {
|
|
231
|
+
member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
|
|
232
|
+
member.typeAnnotation.typeAnnotation,
|
|
233
|
+
]));
|
|
234
|
+
modified = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
propsIdentifier.typeAnnotation = paramTypeAnnotation;
|
|
@@ -338,6 +338,7 @@ function transformDynamicProps(source, _api, filePath) {
|
|
|
338
338
|
const awaited = awaitMemberAccessOfProp(argName, path, j);
|
|
339
339
|
modified ||= awaited;
|
|
340
340
|
}
|
|
341
|
+
modified ||= modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
|
|
341
342
|
// cases of passing down `props` into any function
|
|
342
343
|
// Page(props) { callback(props) }
|
|
343
344
|
// search for all the argument of CallExpression, where currentParam is one of the arguments
|
|
@@ -391,11 +392,10 @@ function transformDynamicProps(source, _api, filePath) {
|
|
|
391
392
|
const node = path.value;
|
|
392
393
|
// If it's sync default export, and it's also server component, make the function async
|
|
393
394
|
if (isDefaultExport && !isClientComponent) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
395
|
+
const hasReactHooksUsage = (0, utils_1.containsReactHooksCallExpressions)(path.get('body'), j);
|
|
396
|
+
if (node.async === false && !hasReactHooksUsage) {
|
|
397
|
+
node.async = true;
|
|
398
|
+
(0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
// If it's arrow function and function body is not block statement, check if the properties are used there
|
|
@@ -421,7 +421,9 @@ function transformDynamicProps(source, _api, filePath) {
|
|
|
421
421
|
}
|
|
422
422
|
const isAsyncFunc = !!node.async;
|
|
423
423
|
const functionName = path.value.id?.name || 'default';
|
|
424
|
-
const functionBody = findFunctionBody(path);
|
|
424
|
+
const functionBody = (0, utils_1.findFunctionBody)(path);
|
|
425
|
+
const functionBodyPath = path.get('body');
|
|
426
|
+
const hasReactHooksUsage = (0, utils_1.containsReactHooksCallExpressions)(functionBodyPath, j);
|
|
425
427
|
const hasOtherProperties = allProperties.length > propertiesMap.size;
|
|
426
428
|
function createDestructuringDeclaration(properties, destructPropsIdentifierName) {
|
|
427
429
|
const propsToKeep = [];
|
|
@@ -491,7 +493,7 @@ function transformDynamicProps(source, _api, filePath) {
|
|
|
491
493
|
name: matchedPropName,
|
|
492
494
|
});
|
|
493
495
|
for (const propPath of propPaths.paths()) {
|
|
494
|
-
if (isParentUseCallExpression(propPath, j)) {
|
|
496
|
+
if ((0, utils_1.isParentUseCallExpression)(propPath, j)) {
|
|
495
497
|
// Skip transformation
|
|
496
498
|
shouldSkip = true;
|
|
497
499
|
break;
|
|
@@ -507,7 +509,6 @@ function transformDynamicProps(source, _api, filePath) {
|
|
|
507
509
|
const paramPropertyName = paramsPropertyName || matchedPropName;
|
|
508
510
|
// if propName is not used in lower scope, and it stars with unused prefix `_`,
|
|
509
511
|
// also skip the transformation
|
|
510
|
-
const functionBodyPath = path.get('body');
|
|
511
512
|
const hasUsedInBody = j(functionBodyPath)
|
|
512
513
|
.find(j.Identifier, {
|
|
513
514
|
name: paramPropertyName,
|
|
@@ -526,7 +527,7 @@ function transformDynamicProps(source, _api, filePath) {
|
|
|
526
527
|
propUsages.forEach((propUsage) => {
|
|
527
528
|
// If the parent is not AwaitExpression, it's not awaited
|
|
528
529
|
const isAwaited = propUsage.parentPath?.value.type === 'AwaitExpression';
|
|
529
|
-
const isAwaitedByUse = isParentUseCallExpression(propUsage, j);
|
|
530
|
+
const isAwaitedByUse = (0, utils_1.isParentUseCallExpression)(propUsage, j);
|
|
530
531
|
if (!isAwaited && !isAwaitedByUse) {
|
|
531
532
|
hasMissingAwaited = true;
|
|
532
533
|
return;
|
|
@@ -575,22 +576,19 @@ function transformDynamicProps(source, _api, filePath) {
|
|
|
575
576
|
}
|
|
576
577
|
}
|
|
577
578
|
else {
|
|
578
|
-
|
|
579
|
-
|
|
579
|
+
if (!isClientComponent &&
|
|
580
|
+
(0, utils_1.isFunctionType)(node.type) &&
|
|
581
|
+
!hasReactHooksUsage) {
|
|
580
582
|
// If it's export function, populate the function to async
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
|
|
591
|
-
functionBody.unshift(paramAssignment);
|
|
592
|
-
insertedRenamedPropFunctionNames.add(uid);
|
|
593
|
-
}
|
|
583
|
+
node.async = true;
|
|
584
|
+
(0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
|
|
585
|
+
// Insert `const <propName> = await props.<propName>;` at the beginning of the function body
|
|
586
|
+
const paramAssignment = j.variableDeclaration('const', [
|
|
587
|
+
j.variableDeclarator(j.identifier(paramPropertyName), j.awaitExpression(accessedPropIdExpr)),
|
|
588
|
+
]);
|
|
589
|
+
if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
|
|
590
|
+
functionBody.unshift(paramAssignment);
|
|
591
|
+
insertedRenamedPropFunctionNames.add(uid);
|
|
594
592
|
}
|
|
595
593
|
}
|
|
596
594
|
else {
|
|
@@ -691,7 +689,7 @@ function findAllTypes(root, j, typeName) {
|
|
|
691
689
|
}
|
|
692
690
|
function commentSpreadProps(path, propsIdentifierName, j) {
|
|
693
691
|
let modified = false;
|
|
694
|
-
const functionBody = findFunctionBody(path);
|
|
692
|
+
const functionBody = (0, utils_1.findFunctionBody)(path);
|
|
695
693
|
const functionBodyCollection = j(functionBody);
|
|
696
694
|
// Find all the usage of spreading properties of `props`
|
|
697
695
|
const jsxSpreadProperties = functionBodyCollection.find(j.JSXSpreadAttribute, { argument: { name: propsIdentifierName } });
|
|
@@ -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 = exports.NEXT_CODEMOD_ERROR_PREFIX = exports.NEXTJS_ENTRY_FILES = void 0;
|
|
3
|
+
exports.isReactHookName = exports.TARGET_PROP_NAMES = exports.TARGET_NAMED_EXPORTS = exports.TARGET_ROUTE_EXPORTS = exports.NEXT_CODEMOD_ERROR_PREFIX = exports.NEXTJS_ENTRY_FILES = void 0;
|
|
4
4
|
exports.isFunctionType = isFunctionType;
|
|
5
5
|
exports.isMatchedFunctionExported = isMatchedFunctionExported;
|
|
6
6
|
exports.determineClientDirective = determineClientDirective;
|
|
@@ -14,6 +14,10 @@ exports.getFunctionPathFromExportPath = getFunctionPathFromExportPath;
|
|
|
14
14
|
exports.wrapParentheseIfNeeded = wrapParentheseIfNeeded;
|
|
15
15
|
exports.insertCommentOnce = insertCommentOnce;
|
|
16
16
|
exports.getVariableDeclaratorId = getVariableDeclaratorId;
|
|
17
|
+
exports.findFunctionBody = findFunctionBody;
|
|
18
|
+
exports.containsReactHooksCallExpressions = containsReactHooksCallExpressions;
|
|
19
|
+
exports.isParentUseCallExpression = isParentUseCallExpression;
|
|
20
|
+
exports.isParentPromiseAllCallExpression = isParentPromiseAllCallExpression;
|
|
17
21
|
exports.NEXTJS_ENTRY_FILES = /([\\/]|^)(page|layout|route|default)\.(t|j)sx?$/;
|
|
18
22
|
exports.NEXT_CODEMOD_ERROR_PREFIX = '@next-codemod-error';
|
|
19
23
|
const NEXT_CODEMOD_IGNORE_ERROR_PREFIX = '@next-codemod-ignore';
|
|
@@ -371,4 +375,99 @@ function getVariableDeclaratorId(path, j) {
|
|
|
371
375
|
}
|
|
372
376
|
return undefined;
|
|
373
377
|
}
|
|
378
|
+
function findFunctionBody(path) {
|
|
379
|
+
let functionBody = path.node.body;
|
|
380
|
+
if (functionBody && functionBody.type === 'BlockStatement') {
|
|
381
|
+
return functionBody.body;
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
const isPascalCase = (s) => /^[A-Z][a-z0-9]*$/.test(s);
|
|
386
|
+
const isReactHookName = (name) =>
|
|
387
|
+
// function name is `use`
|
|
388
|
+
name === 'use' ||
|
|
389
|
+
// function name is `useX*`
|
|
390
|
+
(name.startsWith('use') && name[3] === name[3].toUpperCase());
|
|
391
|
+
exports.isReactHookName = isReactHookName;
|
|
392
|
+
// Determine a path of function contains any React hooks call expressions.
|
|
393
|
+
// e.g. if there's any of those call expressions in the function body:
|
|
394
|
+
// use() => true
|
|
395
|
+
// React.use() => false
|
|
396
|
+
// useXxxx() => true
|
|
397
|
+
// Foo.use() => true
|
|
398
|
+
// Foo.useXxxx() => true
|
|
399
|
+
function containsReactHooksCallExpressions(path, j) {
|
|
400
|
+
const hasReactHooks = j(path)
|
|
401
|
+
.find(j.CallExpression)
|
|
402
|
+
.filter((callPath) => {
|
|
403
|
+
// It's matching:
|
|
404
|
+
// - use(<callPath>) => true
|
|
405
|
+
// - useX*(<callPath>) => true
|
|
406
|
+
const isUseHookOrReactHookCall = j.Identifier.check(callPath.value.callee) &&
|
|
407
|
+
(0, exports.isReactHookName)(callPath.value.callee.name);
|
|
408
|
+
// It's matching member access:
|
|
409
|
+
// - React.use(<callPath>) => true
|
|
410
|
+
// - Foo.useFoo(<callPath>) => true
|
|
411
|
+
// - foo.useFoo(<callPath>) => false
|
|
412
|
+
// - foo.use(<callPath>) => false
|
|
413
|
+
const isReactUseCall = j.MemberExpression.check(callPath.value.callee) &&
|
|
414
|
+
j.Identifier.check(callPath.value.callee.object) &&
|
|
415
|
+
j.Identifier.check(callPath.value.callee.property) &&
|
|
416
|
+
isPascalCase(callPath.value.callee.object.name) &&
|
|
417
|
+
(0, exports.isReactHookName)(callPath.value.callee.property.name);
|
|
418
|
+
return isUseHookOrReactHookCall || isReactUseCall;
|
|
419
|
+
})
|
|
420
|
+
.size() > 0;
|
|
421
|
+
return hasReactHooks;
|
|
422
|
+
}
|
|
423
|
+
// Capture the parent of the current path is wrapped by `use()` call expression
|
|
424
|
+
// e.g.
|
|
425
|
+
// use(<path>) => true
|
|
426
|
+
// use2(<path>) => false
|
|
427
|
+
// React.use(<path>) => true
|
|
428
|
+
// Robust.use(<path>) => false
|
|
429
|
+
function isParentUseCallExpression(path, j) {
|
|
430
|
+
const isParentUseCall =
|
|
431
|
+
// member access parentPath is argument
|
|
432
|
+
j.CallExpression.check(path.parent.value) &&
|
|
433
|
+
// member access is first argument
|
|
434
|
+
path.parent.value.arguments[0] === path.value &&
|
|
435
|
+
path.parent.value.arguments.length === 1 &&
|
|
436
|
+
// function name is `use`
|
|
437
|
+
j.Identifier.check(path.parent.value.callee) &&
|
|
438
|
+
path.parent.value.callee.name === 'use';
|
|
439
|
+
const isParentReactUseCall =
|
|
440
|
+
// member access parentPath is argument
|
|
441
|
+
j.CallExpression.check(path.parent.value) &&
|
|
442
|
+
// member access is first argument
|
|
443
|
+
path.parent.value.arguments[0] === path.value &&
|
|
444
|
+
path.parent.value.arguments.length === 1 &&
|
|
445
|
+
// function name is `use`
|
|
446
|
+
j.MemberExpression.check(path.parent.value.callee) &&
|
|
447
|
+
j.Identifier.check(path.parent.value.callee.object) &&
|
|
448
|
+
path.parent.value.callee.object.name === 'React' &&
|
|
449
|
+
j.Identifier.check(path.parent.value.callee.property) &&
|
|
450
|
+
path.parent.value.callee.property.name === 'use';
|
|
451
|
+
return isParentUseCall || isParentReactUseCall;
|
|
452
|
+
}
|
|
453
|
+
// Determine if a path is wrapped by `Promise.all()`
|
|
454
|
+
// e.g.
|
|
455
|
+
// Promise.all(<path>) => true
|
|
456
|
+
// Promise.allSettled(<path>) => false
|
|
457
|
+
function isParentPromiseAllCallExpression(path, j) {
|
|
458
|
+
const argsParent = path.parent;
|
|
459
|
+
const callParent = argsParent?.parent;
|
|
460
|
+
if (argsParent &&
|
|
461
|
+
callParent &&
|
|
462
|
+
j.ArrayExpression.check(argsParent.value) &&
|
|
463
|
+
j.CallExpression.check(callParent.value) &&
|
|
464
|
+
j.MemberExpression.check(callParent.value.callee) &&
|
|
465
|
+
j.Identifier.check(callParent.value.callee.object) &&
|
|
466
|
+
callParent.value.callee.object.name === 'Promise' &&
|
|
467
|
+
j.Identifier.check(callParent.value.callee.property) &&
|
|
468
|
+
callParent.value.callee.property.name === 'all') {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
374
473
|
//# sourceMappingURL=utils.js.map
|