@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)(installedNextVersion, '14.3.0-canary.45') >= 0 &&
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: `Are you using ${picocolors_1.default.underline('only the Pages Router')} (no App Router) and prefer to stay on React 18?`,
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-beta.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
- // Here we suggest pre-released codemods by their "stable" version.
291
- // It is because if we suggest by the version range (installed ~ target),
292
- // pre-released codemods for the target version are not suggested when upgrading.
293
- // Let's say we have a codemod for v15.0.0-canary.x, and we're upgrading from
294
- // v15.x -> v15.x. Our initial version is higher than the codemod's version,
295
- // so the codemod will not be suggested.
296
- // This is not ideal as the codemods for pre-releases are also targeting the major version.
297
- // Also, when the user attempts to run the upgrade command twice, and have installed the
298
- // target version, the behavior must be idempotent and suggest the codemods including the
299
- // pre-releases of the target version.
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((versionCodemods) => (0, compare_1.default)(versionCodemods.version, targetNextVersion) > 0);
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: 'toggle',
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: 'toggle',
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
- if (packageManager === 'bun' || packageManager === 'npm') {
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 Object.entries(overrides)) {
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 Object.entries(overrides)) {
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 Object.entries(overrides)) {
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 Object.entries(overrides)) {
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-rc.1",
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 ((0, utils_1.isFunctionType)(exportFunctionNode.type) &&
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 = parentFunctionName?.startsWith('use');
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 (!path.value.async) {
48
- if ('async' in path.value) {
49
- path.value.async = true;
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
- if (!node.async) {
395
- if ('async' in node) {
396
- node.async = true;
397
- (0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
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
- // const isFromExport = true
579
- if (!isClientComponent) {
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
- if ((0, utils_1.isFunctionType)(node.type) &&
582
- // Make TS happy
583
- 'async' in node) {
584
- node.async = true;
585
- (0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
586
- // Insert `const <propName> = await props.<propName>;` at the beginning of the function body
587
- const paramAssignment = j.variableDeclaration('const', [
588
- j.variableDeclarator(j.identifier(paramPropertyName), j.awaitExpression(accessedPropIdExpr)),
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