@next/codemod 15.0.0-canary.181 → 15.0.0-canary.183

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,18 +23,27 @@ const program = new commander_1.Command(packageJson.name)
23
23
  .helpOption('-h, --help', 'Display this help message.')
24
24
  .option('-f, --force', 'Bypass Git safety checks and forcibly run codemods')
25
25
  .option('-d, --dry', 'Dry run (no changes are made to files)')
26
- .option('-p, --print', 'Print transformed files to your terminal')
26
+ .option('-p, --print', 'Print transformed files to stdout, useful for development')
27
+ .option('--verbose', 'Show more information about the transform process')
27
28
  .option('-j, --jscodeshift', '(Advanced) Pass options directly to jscodeshift')
28
29
  .action(transform_1.runTransform)
29
- .allowUnknownOption();
30
+ .allowUnknownOption()
31
+ // This is needed for options for subcommands to be passed correctly.
32
+ // Because by default the options are not positional, which will pass options
33
+ // to the main command "@next/codemod" even if it was passed after subcommands,
34
+ // e.g. "@next/codemod upgrade --verbose" will be treated as "next-codemod --verbose upgrade"
35
+ // By enabling this, it will respect the position of the options and pass it to subcommands.
36
+ // x-ref: https://github.com/tj/commander.js/pull/1427
37
+ .enablePositionalOptions();
30
38
  program
31
39
  .command('upgrade')
32
40
  .description('Upgrade Next.js apps to desired versions with a single command.')
33
- .argument('[revision]', '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.')
41
+ .argument('[revision]', 'Specify the target Next.js version using an NPM dist tag (e.g. "latest", "canary", "rc") or an exact version number (e.g. "15.0.0").', packageJson.version.includes('-canary.')
34
42
  ? 'canary'
35
43
  : packageJson.version.includes('-rc.')
36
44
  ? 'rc'
37
45
  : 'latest')
46
+ .usage('[revision] [options]')
38
47
  .option('--verbose', 'Verbose output', false)
39
48
  .action(upgrade_1.runUpgrade);
40
49
  program.parse(process.argv);
package/bin/transform.js CHANGED
@@ -66,7 +66,7 @@ async function runTransform(transform, path, options) {
66
66
  return require(transformerPath).default(filesExpanded, options);
67
67
  }
68
68
  let args = [];
69
- const { dry, print, runInBand, jscodeshift } = options;
69
+ const { dry, print, runInBand, jscodeshift, verbose } = options;
70
70
  if (dry) {
71
71
  args.push('--dry');
72
72
  }
@@ -76,7 +76,10 @@ async function runTransform(transform, path, options) {
76
76
  if (runInBand) {
77
77
  args.push('--run-in-band');
78
78
  }
79
- args.push('--verbose=2');
79
+ if (verbose) {
80
+ args.push('--verbose=2');
81
+ }
82
+ args.push('--no-babel');
80
83
  args.push('--ignore-pattern=**/node_modules/**');
81
84
  args.push('--ignore-pattern=**/.next/**');
82
85
  args.push('--extensions=tsx,ts,jsx,js');
@@ -94,17 +97,28 @@ async function runTransform(transform, path, options) {
94
97
  throw new Error(`jscodeshift exited with code ${result.exitCode}`);
95
98
  }
96
99
  if (!dry && transformer === 'built-in-next-font') {
97
- console.log('Uninstalling `@next/font`');
98
- try {
100
+ const { uninstallNextFont } = await (0, prompts_1.default)({
101
+ type: 'confirm',
102
+ name: 'uninstallNextFont',
103
+ message: 'Do you want to uninstall `@next/font`?',
104
+ initial: true,
105
+ });
106
+ if (uninstallNextFont) {
107
+ console.log('Uninstalling `@next/font`');
99
108
  (0, handle_package_1.uninstallPackage)('@next/font');
100
109
  }
101
- catch {
102
- console.error("Couldn't uninstall `@next/font`, please uninstall it manually");
103
- }
104
110
  }
105
111
  if (!dry && transformer === 'next-request-geo-ip') {
106
- console.log('Installing `@vercel/functions`...');
107
- (0, handle_package_1.installPackages)(['@vercel/functions']);
112
+ const { installVercelFunctions } = await (0, prompts_1.default)({
113
+ type: 'confirm',
114
+ name: 'installVercelFunctions',
115
+ message: 'Do you want to install `@vercel/functions`?',
116
+ initial: true,
117
+ });
118
+ if (installVercelFunctions) {
119
+ console.log('Installing `@vercel/functions`...');
120
+ (0, handle_package_1.installPackages)(['@vercel/functions']);
121
+ }
108
122
  }
109
123
  }
110
124
  //# sourceMappingURL=transform.js.map
package/bin/upgrade.js CHANGED
@@ -9,7 +9,7 @@ const fs_1 = __importDefault(require("fs"));
9
9
  const child_process_1 = require("child_process");
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const compare_versions_1 = require("compare-versions");
12
- const chalk_1 = __importDefault(require("chalk"));
12
+ const picocolors_1 = __importDefault(require("picocolors"));
13
13
  const handle_package_1 = require("../lib/handle-package");
14
14
  const transform_1 = require("./transform");
15
15
  const utils_1 = require("../lib/utils");
@@ -19,11 +19,16 @@ const utils_1 = require("../lib/utils");
19
19
  */
20
20
  async function loadHighestNPMVersionMatching(query) {
21
21
  const versionsJSON = (0, child_process_1.execSync)(`npm --silent view "${query}" --json --field version`, { encoding: 'utf-8' });
22
- const versions = JSON.parse(versionsJSON);
23
- if (versions.length < 1) {
22
+ const versionOrVersions = JSON.parse(versionsJSON);
23
+ if (versionOrVersions.length < 1) {
24
24
  throw new Error(`Found no React versions matching "${query}". This is a bug in the upgrade tool.`);
25
25
  }
26
- return versions[versions.length - 1];
26
+ // npm-view returns an array if there are multiple versions matching the query.
27
+ if (Array.isArray(versionOrVersions)) {
28
+ // The last entry will be the latest version published.
29
+ return versionOrVersions[versionOrVersions.length - 1];
30
+ }
31
+ return versionOrVersions;
27
32
  }
28
33
  async function runUpgrade(revision, options) {
29
34
  const { verbose } = options;
@@ -39,20 +44,21 @@ async function runUpgrade(revision, options) {
39
44
  'version' in targetNextPackageJson &&
40
45
  'peerDependencies' in targetNextPackageJson;
41
46
  if (!validRevision) {
42
- throw new Error(`${chalk_1.default.yellow(`next@${revision}`)} does not exist. Make sure you entered a valid Next.js version or dist-tag. Check available versions at ${chalk_1.default.underline('https://www.npmjs.com/package/next?activeTab=versions')}.`);
47
+ throw new Error(`Invalid revision provided: "${revision}". Please provide a valid Next.js version or dist-tag (e.g. "latest", "canary", "rc", or "15.0.0").\nCheck available versions at https://www.npmjs.com/package/next?activeTab=versions.`);
43
48
  }
44
49
  const installedNextVersion = getInstalledNextVersion();
50
+ console.log(`Current Next.js version: v${installedNextVersion}`);
45
51
  const targetNextVersion = targetNextPackageJson.version;
52
+ if ((0, compare_versions_1.compareVersions)(installedNextVersion, targetNextVersion) >= 0) {
53
+ console.log(`${picocolors_1.default.green('✓')} Current Next.js version is already on or higher than the target version "v${targetNextVersion}".`);
54
+ return;
55
+ }
46
56
  // We're resolving a specific version here to avoid including "ugly" version queries
47
57
  // in the manifest.
48
58
  // E.g. in peerDependencies we could have `^18.2.0 || ^19.0.0 || 20.0.0-canary`
49
59
  // If we'd just `npm add` that, the manifest would read the same version query.
50
60
  // This is basically a `npm --save-exact react@$versionQuery` that works for every package manager.
51
- const [targetReactVersion, targetReactTypesVersion, targetReactDOMTypesVersion,] = await Promise.all([
52
- loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`),
53
- loadHighestNPMVersionMatching(`@types/react@${targetNextPackageJson.peerDependencies['react']}`),
54
- loadHighestNPMVersionMatching(`@types/react-dom@${targetNextPackageJson.peerDependencies['react']}`),
55
- ]);
61
+ const targetReactVersion = await loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`);
56
62
  if ((0, compare_versions_1.compareVersions)(targetNextVersion, '15.0.0-canary') >= 0) {
57
63
  await suggestTurbopack(appPackageJson);
58
64
  }
@@ -71,18 +77,26 @@ async function runUpgrade(revision, options) {
71
77
  reactDependencies.push(`@types/react-dom@npm:types-react-dom@rc`);
72
78
  }
73
79
  else {
80
+ const [targetReactTypesVersion, targetReactDOMTypesVersion] = await Promise.all([
81
+ loadHighestNPMVersionMatching(`@types/react@${targetNextPackageJson.peerDependencies['react']}`),
82
+ loadHighestNPMVersionMatching(`@types/react-dom@${targetNextPackageJson.peerDependencies['react']}`),
83
+ ]);
74
84
  reactDependencies.push(`@types/react@${targetReactTypesVersion}`);
75
85
  reactDependencies.push(`@types/react-dom@${targetReactDOMTypesVersion}`);
76
86
  }
77
- console.log(`Upgrading your project to ${chalk_1.default.blue('Next.js ' + targetNextVersion)}...\n`);
87
+ console.log(`Upgrading your project to ${picocolors_1.default.blue('Next.js ' + targetNextVersion)}...\n`);
78
88
  (0, handle_package_1.installPackages)([nextDependency, ...reactDependencies], {
79
89
  packageManager,
80
90
  silent: !verbose,
81
91
  });
82
92
  for (const codemod of codemods) {
83
- await (0, transform_1.runTransform)(codemod, process.cwd(), { force: true });
93
+ await (0, transform_1.runTransform)(codemod, process.cwd(), { force: true, verbose });
94
+ }
95
+ console.log(); // new line
96
+ if (codemods.length > 0) {
97
+ console.log(`${picocolors_1.default.green('✔')} Codemods have been applied successfully.`);
84
98
  }
85
- console.log(`\n${chalk_1.default.green('✔')} Your Next.js project has been upgraded successfully. ${chalk_1.default.bold('Time to ship! 🚢')}`);
99
+ console.log(`Please review the local changes and read the Next.js 15 migration guide to complete the migration. https://nextjs.org/docs/canary/app/building-your-application/upgrading/version-15`);
86
100
  }
87
101
  function getInstalledNextVersion() {
88
102
  try {
@@ -124,7 +138,7 @@ async function suggestTurbopack(packageJson) {
124
138
  packageJson.scripts['dev'] = devScript.replace('next dev', 'next dev --turbo');
125
139
  return;
126
140
  }
127
- console.log(`${chalk_1.default.yellow('⚠')} Could not find "${chalk_1.default.bold('next dev')}" in your dev script.`);
141
+ console.log(`${picocolors_1.default.yellow('⚠')} Could not find "${picocolors_1.default.bold('next dev')}" in your dev script.`);
128
142
  const responseCustomDevScript = await (0, prompts_1.default)({
129
143
  type: 'text',
130
144
  name: 'customDevScript',
@@ -150,7 +164,7 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
150
164
  const { codemods } = await (0, prompts_1.default)({
151
165
  type: 'multiselect',
152
166
  name: 'codemods',
153
- message: `The following ${chalk_1.default.blue('codemods')} are recommended for your upgrade. Select the ones to apply.`,
167
+ message: `The following ${picocolors_1.default.blue('codemods')} are recommended for your upgrade. Select the ones to apply.`,
154
168
  choices: relevantCodemods.reverse().map(({ title, value, version }) => {
155
169
  return {
156
170
  title: `(v${version}) ${value}`,
@@ -6,42 +6,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getPkgManager = getPkgManager;
7
7
  exports.uninstallPackage = uninstallPackage;
8
8
  exports.installPackages = installPackages;
9
- const fs_1 = __importDefault(require("fs"));
10
- const path_1 = __importDefault(require("path"));
9
+ const find_up_1 = __importDefault(require("find-up"));
11
10
  const execa_1 = __importDefault(require("execa"));
11
+ const node_path_1 = require("node:path");
12
12
  function getPkgManager(baseDir) {
13
13
  try {
14
- for (const { lockFile, packageManager } of [
15
- { lockFile: 'yarn.lock', packageManager: 'yarn' },
16
- { lockFile: 'pnpm-lock.yaml', packageManager: 'pnpm' },
17
- { lockFile: 'package-lock.json', packageManager: 'npm' },
18
- { lockFile: 'bun.lockb', packageManager: 'bun' },
19
- ]) {
20
- if (fs_1.default.existsSync(path_1.default.join(baseDir, lockFile))) {
21
- return packageManager;
22
- }
23
- }
24
- const userAgent = process.env.npm_config_user_agent;
25
- if (userAgent) {
26
- if (userAgent.startsWith('yarn')) {
27
- return 'yarn';
28
- }
29
- else if (userAgent.startsWith('pnpm')) {
30
- return 'pnpm';
31
- }
32
- }
33
- try {
34
- execa_1.default.sync('yarn --version', { stdio: 'ignore' });
35
- return 'yarn';
36
- }
37
- catch {
38
- try {
39
- execa_1.default.sync('pnpm --version', { stdio: 'ignore' });
40
- return 'pnpm';
41
- }
42
- catch {
43
- execa_1.default.sync('bun --version', { stdio: 'ignore' });
44
- return 'bun';
14
+ const lockFile = find_up_1.default.sync(['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lockb'], { cwd: baseDir });
15
+ if (lockFile) {
16
+ switch ((0, node_path_1.basename)(lockFile)) {
17
+ case 'package-lock.json':
18
+ return 'npm';
19
+ case 'yarn.lock':
20
+ return 'yarn';
21
+ case 'pnpm-lock.yaml':
22
+ return 'pnpm';
23
+ case 'bun.lockb':
24
+ return 'bun';
25
+ default:
26
+ return 'npm';
45
27
  }
46
28
  }
47
29
  }
@@ -57,7 +39,12 @@ function uninstallPackage(packageToUninstall, pkgManager) {
57
39
  if (pkgManager === 'yarn') {
58
40
  command = 'remove';
59
41
  }
60
- execa_1.default.sync(pkgManager, [command, packageToUninstall], { stdio: 'inherit' });
42
+ try {
43
+ execa_1.default.sync(pkgManager, [command, packageToUninstall], { stdio: 'inherit' });
44
+ }
45
+ catch (error) {
46
+ throw new Error(`Failed to uninstall "${packageToUninstall}". Please uninstall it manually.`, { cause: error });
47
+ }
61
48
  }
62
49
  function installPackages(packageToInstall, options = {}) {
63
50
  const { packageManager = getPkgManager(process.cwd()), silent = false } = options;
package/lib/utils.js CHANGED
@@ -76,6 +76,11 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
76
76
  value: 'next-image-to-legacy-image',
77
77
  version: '13.0',
78
78
  },
79
+ {
80
+ title: 'Transform App Router Route Segment Config `runtime` value from `experimental-edge` to `edge`',
81
+ value: 'app-dir-runtime-config-experimental-edge',
82
+ version: '13.1.2',
83
+ },
79
84
  {
80
85
  title: 'Uninstall `@next/font` and transform imports to `next/font`',
81
86
  value: 'built-in-next-font',
@@ -92,7 +97,7 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
92
97
  version: '14.0',
93
98
  },
94
99
  {
95
- title: 'Transforms dynamic imports that return the named export itself to a module like object',
100
+ title: 'Transform `next/dynamic` imports accessing named exports to return an object with a `default` property',
96
101
  value: 'next-dynamic-access-named-export',
97
102
  version: '15.0.0-canary.44',
98
103
  },
@@ -106,10 +111,5 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
106
111
  value: 'next-async-request-api',
107
112
  version: '15.0.0-canary.171',
108
113
  },
109
- {
110
- title: 'Transforms `experimental-edge` to `edge` in the `runtime` route segment configuration within the App Router',
111
- value: 'app-dir-runtime-config-experimental-edge',
112
- version: '15.0.0-canary.179',
113
- },
114
114
  ];
115
115
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "15.0.0-canary.181",
3
+ "version": "15.0.0-canary.183",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,11 +8,11 @@
8
8
  "directory": "packages/next-codemod"
9
9
  },
10
10
  "dependencies": {
11
- "chalk": "4.1.2",
12
11
  "cheerio": "1.0.0-rc.9",
13
12
  "commander": "12.1.0",
14
13
  "compare-versions": "6.1.1",
15
14
  "execa": "4.0.3",
15
+ "find-up": "4.1.0",
16
16
  "globby": "11.0.1",
17
17
  "is-git-clean": "1.1.0",
18
18
  "jscodeshift": "17.0.0",
@@ -34,6 +34,8 @@
34
34
  },
35
35
  "bin": "./bin/next-codemod.js",
36
36
  "devDependencies": {
37
- "@types/jscodeshift": "0.11.0"
37
+ "@types/find-up": "4.0.0",
38
+ "@types/jscodeshift": "0.11.0",
39
+ "@types/prompts": "2.4.2"
38
40
  }
39
41
  }
@@ -17,12 +17,13 @@ function findDynamicImportsAndComment(root, j) {
17
17
  arguments: [{ value: 'next/headers' }],
18
18
  });
19
19
  importPaths.forEach((path) => {
20
- (0, utils_1.insertCommentOnce)(path.node, j, DYNAMIC_IMPORT_WARN_COMMENT);
21
- modified = true;
20
+ const inserted = (0, utils_1.insertCommentOnce)(path.node, j, DYNAMIC_IMPORT_WARN_COMMENT);
21
+ modified ||= inserted;
22
22
  });
23
23
  return modified;
24
24
  }
25
25
  function transformDynamicAPI(source, api, filePath) {
26
+ const isEntryFile = utils_1.NEXTJS_ENTRY_FILES.test(filePath);
26
27
  const j = api.jscodeshift.withParser('tsx');
27
28
  const root = j(source);
28
29
  let modified = false;
@@ -83,7 +84,7 @@ function transformDynamicAPI(source, api, filePath) {
83
84
  else {
84
85
  // Determine if the function is an export
85
86
  const closetScopePath = closetScope.get();
86
- const isFromExport = (0, utils_1.isMatchedFunctionExported)(closetScopePath, j);
87
+ const isEntryFileExport = isEntryFile && (0, utils_1.isMatchedFunctionExported)(closetScopePath, j);
87
88
  const closestFunctionNode = closetScope.size()
88
89
  ? closetScopePath.node
89
90
  : null;
@@ -92,7 +93,7 @@ function transformDynamicAPI(source, api, filePath) {
92
93
  // If it's exporting a variable declaration, exportFunctionNode is the function declaration
93
94
  // e.g. export const MyComponent = function() {}
94
95
  let exportFunctionNode;
95
- if (isFromExport) {
96
+ if (isEntryFileExport) {
96
97
  if (closestFunctionNode &&
97
98
  (0, utils_1.isFunctionType)(closestFunctionNode.type)) {
98
99
  exportFunctionNode = closestFunctionNode;
@@ -104,7 +105,7 @@ function transformDynamicAPI(source, api, filePath) {
104
105
  }
105
106
  let canConvertToAsync = false;
106
107
  // check if current path is under the default export function
107
- if (isFromExport) {
108
+ if (isEntryFileExport) {
108
109
  // if default export function is not async, convert it to async, and await the api call
109
110
  if (!isCallAwaited) {
110
111
  // If the scoped function is async function
@@ -134,13 +135,14 @@ function transformDynamicAPI(source, api, filePath) {
134
135
  needsReactUseImport = true;
135
136
  }
136
137
  else {
137
- castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` Next.js Dynamic Async API Codemod: Manually await this call, if it's a Server Component `);
138
+ const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` Next.js Dynamic Async API Codemod: Manually await this call, if it's a Server Component `);
139
+ modified ||= casted;
138
140
  }
139
141
  }
140
142
  else {
141
- castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ' Next.js Dynamic Async API Codemod: please manually await this call, codemod cannot transform due to undetermined async scope ');
143
+ const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ' Next.js Dynamic Async API Codemod: please manually await this call, codemod cannot transform due to undetermined async scope ');
144
+ modified ||= casted;
142
145
  }
143
- modified = true;
144
146
  }
145
147
  }
146
148
  });
@@ -183,9 +185,8 @@ function transformDynamicAPI(source, api, filePath) {
183
185
  if (needsReactUseImport) {
184
186
  (0, utils_1.insertReactUseImport)(root, j);
185
187
  }
186
- if (findDynamicImportsAndComment(root, j)) {
187
- modified = true;
188
- }
188
+ const commented = findDynamicImportsAndComment(root, j);
189
+ modified ||= commented;
189
190
  return modified ? root.toSource() : null;
190
191
  }
191
192
  // cast to unknown first, then the specific type
@@ -195,11 +196,12 @@ const API_CAST_TYPE_MAP = {
195
196
  draftMode: 'UnsafeUnwrappedDraftMode',
196
197
  };
197
198
  function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, customMessage) {
199
+ let modified = false;
198
200
  const isTsFile = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
199
201
  if (isTsFile) {
200
202
  // if the path of call expression is already being awaited, no need to cast
201
203
  if (path.parentPath?.node?.type === 'AwaitExpression')
202
- return;
204
+ return false;
203
205
  /* Do type cast for headers, cookies, draftMode
204
206
  import {
205
207
  type UnsafeUnwrappedHeaders,
@@ -218,6 +220,7 @@ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, in
218
220
  // Replace the original expression with the new cast expression,
219
221
  // also wrap () around the new cast expression.
220
222
  j(path).replaceWith(j.parenthesizedExpression(newCastExpression));
223
+ modified = true;
221
224
  // If cast types are not imported, add them to the import list
222
225
  const importDeclaration = root.find(j.ImportDeclaration, {
223
226
  source: { value: 'next/headers' },
@@ -243,8 +246,10 @@ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, in
243
246
  }
244
247
  else {
245
248
  // Otherwise for JS file, leave a message to the user to manually handle the transformation
246
- (0, utils_1.insertCommentOnce)(path.node, j, customMessage);
249
+ const inserted = (0, utils_1.insertCommentOnce)(path.node, j, customMessage);
250
+ modified ||= inserted;
247
251
  }
252
+ return modified;
248
253
  }
249
254
  function findImportMappingFromNextHeaders(root, j) {
250
255
  const mappings = {};
@@ -23,6 +23,12 @@ function awaitMemberAccessOfProp(propIdName, path, j) {
23
23
  // await each member access
24
24
  memberAccess.forEach((memberAccessPath) => {
25
25
  const member = memberAccessPath.value;
26
+ const memberProperty = member.property;
27
+ const isAccessingMatchedProperty = j.Identifier.check(memberProperty) &&
28
+ utils_1.TARGET_PROP_NAMES.has(memberProperty.name);
29
+ if (!isAccessingMatchedProperty) {
30
+ return;
31
+ }
26
32
  if (isParentPromiseAllCallExpression(memberAccessPath, j)) {
27
33
  return;
28
34
  }
@@ -123,8 +129,43 @@ function applyUseAndRenameAccessedProp(propIdName, path, j) {
123
129
  }
124
130
  return modified;
125
131
  }
126
- const MATCHED_FILE_PATTERNS = /([\\/]|^)(page|layout|route)\.(t|j)sx?$/;
132
+ function commentOnMatchedReExports(root, j) {
133
+ let modified = false;
134
+ root.find(j.ExportNamedDeclaration).forEach((path) => {
135
+ if (j.ExportSpecifier.check(path.value.specifiers[0])) {
136
+ const specifiers = path.value.specifiers;
137
+ for (const specifier of specifiers) {
138
+ if (j.ExportSpecifier.check(specifier) &&
139
+ // Find matched named exports and default export
140
+ (utils_1.TARGET_NAMED_EXPORTS.has(specifier.exported.name) ||
141
+ specifier.exported.name === 'default')) {
142
+ if (j.Literal.check(path.value.source)) {
143
+ const localName = specifier.local.name;
144
+ const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` Next.js Dynamic Async API Codemod: \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
145
+ modified ||= commentInserted;
146
+ }
147
+ else if (path.value.source === null) {
148
+ const localIdentifier = specifier.local;
149
+ const localName = localIdentifier.name;
150
+ // search if local identifier is from imports
151
+ const importDeclaration = root
152
+ .find(j.ImportDeclaration)
153
+ .filter((importPath) => {
154
+ return importPath.value.specifiers.some((importSpecifier) => importSpecifier.local.name === localName);
155
+ });
156
+ if (importDeclaration.size() > 0) {
157
+ const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` Next.js Dynamic Async API Codemod: \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
158
+ modified ||= commentInserted;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ });
165
+ return modified;
166
+ }
127
167
  function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
168
+ let modified = false;
128
169
  if (paramTypeAnnotation && paramTypeAnnotation.typeAnnotation) {
129
170
  const typeAnnotation = paramTypeAnnotation.typeAnnotation;
130
171
  if (typeAnnotation.type === 'TSTypeLiteral') {
@@ -151,6 +192,7 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
151
192
  // @ts-ignore
152
193
  member.typeAnnotation.typeAnnotation,
153
194
  ]));
195
+ modified = true;
154
196
  }
155
197
  }
156
198
  });
@@ -189,6 +231,7 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
189
231
  member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
190
232
  member.typeAnnotation.typeAnnotation,
191
233
  ]));
234
+ modified = true;
192
235
  }
193
236
  }
194
237
  });
@@ -197,11 +240,13 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
197
240
  }
198
241
  }
199
242
  propsIdentifier.typeAnnotation = paramTypeAnnotation;
243
+ modified = true;
200
244
  }
245
+ return modified;
201
246
  }
202
247
  function transformDynamicProps(source, api, filePath) {
203
- const isMatched = MATCHED_FILE_PATTERNS.test(filePath);
204
- if (!isMatched) {
248
+ const isEntryFile = utils_1.NEXTJS_ENTRY_FILES.test(filePath);
249
+ if (!isEntryFile) {
205
250
  return null;
206
251
  }
207
252
  let modified = false;
@@ -289,7 +334,8 @@ function transformDynamicProps(source, api, filePath) {
289
334
  }
290
335
  }
291
336
  else {
292
- modified = awaitMemberAccessOfProp(argName, path, j);
337
+ const awaited = awaitMemberAccessOfProp(argName, path, j);
338
+ modified ||= awaited;
293
339
  }
294
340
  // cases of passing down `props` into any function
295
341
  // Page(props) { callback(props) }
@@ -309,8 +355,8 @@ function transformDynamicProps(source, api, filePath) {
309
355
  const args = callExpression.value.arguments;
310
356
  const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
311
357
  const comment = ` Next.js Dynamic Async API Codemod: '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `;
312
- (0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
313
- modified = true;
358
+ const inserted = (0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
359
+ modified ||= inserted;
314
360
  });
315
361
  if (modified) {
316
362
  modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
@@ -328,6 +374,14 @@ function transformDynamicProps(source, api, filePath) {
328
374
  modified = true;
329
375
  }
330
376
  }
377
+ else {
378
+ // When the prop argument is not destructured, we need to add comments to the spread properties
379
+ if (j.Identifier.check(currentParam)) {
380
+ const commented = commentSpreadProps(path, currentParam.name, j);
381
+ const modifiedTypes = modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
382
+ modified ||= commented || modifiedTypes;
383
+ }
384
+ }
331
385
  }
332
386
  // Helper function to insert `const params = await asyncParams;` at the beginning of the function body
333
387
  function resolveAsyncProp(path, propertiesMap, propsIdentifierName, allProperties, isDefaultExport) {
@@ -446,20 +500,42 @@ function transformDynamicProps(source, api, filePath) {
446
500
  continue;
447
501
  }
448
502
  }
449
- const propRenamedId = j.Identifier.check(paramsProperty)
503
+ const paramsPropertyName = j.Identifier.check(paramsProperty)
450
504
  ? paramsProperty.name
451
505
  : null;
452
- const propName = propRenamedId || matchedPropName;
506
+ const paramPropertyName = paramsPropertyName || matchedPropName;
453
507
  // if propName is not used in lower scope, and it stars with unused prefix `_`,
454
508
  // also skip the transformation
455
509
  const functionBodyPath = path.get('body');
456
510
  const hasUsedInBody = j(functionBodyPath)
457
511
  .find(j.Identifier, {
458
- name: propName,
512
+ name: paramPropertyName,
459
513
  })
460
514
  .size() > 0;
461
- if (!hasUsedInBody && propName.startsWith('_'))
515
+ if (!hasUsedInBody && paramPropertyName.startsWith('_'))
462
516
  continue;
517
+ // Search the usage of propName in the function body,
518
+ // if they're all awaited or wrapped with use(), skip the transformation
519
+ const propUsages = j(functionBodyPath).find(j.Identifier, {
520
+ name: paramPropertyName,
521
+ });
522
+ // if there's usage of the propName, then do the check
523
+ if (propUsages.size()) {
524
+ let hasMissingAwaited = false;
525
+ propUsages.forEach((propUsage) => {
526
+ // If the parent is not AwaitExpression, it's not awaited
527
+ const isAwaited = propUsage.parentPath?.value.type === 'AwaitExpression';
528
+ const isAwaitedByUse = isParentUseCallExpression(propUsage, j);
529
+ if (!isAwaited && !isAwaitedByUse) {
530
+ hasMissingAwaited = true;
531
+ return;
532
+ }
533
+ });
534
+ // If all the usages of parm are awaited, skip the transformation
535
+ if (!hasMissingAwaited) {
536
+ continue;
537
+ }
538
+ }
463
539
  modifiedPropertyCount++;
464
540
  const propNameIdentifier = j.identifier(matchedPropName);
465
541
  const propsIdentifier = j.identifier(propsIdentifierName);
@@ -468,7 +544,7 @@ function transformDynamicProps(source, api, filePath) {
468
544
  // e.g.
469
545
  // input: Page({ params: { slug } })
470
546
  // output: const { slug } = await props.params; rather than const props = await props.params;
471
- const uid = functionName + ':' + propName;
547
+ const uid = functionName + ':' + paramPropertyName;
472
548
  if (paramsProperty?.type === 'ObjectPattern') {
473
549
  const objectPattern = paramsProperty;
474
550
  const objectPatternProperties = objectPattern.properties;
@@ -490,7 +566,7 @@ function transformDynamicProps(source, api, filePath) {
490
566
  if (isAsyncFunc) {
491
567
  // If it's async function, add await to the async props.<propName>
492
568
  const paramAssignment = j.variableDeclaration('const', [
493
- j.variableDeclarator(j.identifier(propName), j.awaitExpression(accessedPropIdExpr)),
569
+ j.variableDeclarator(j.identifier(paramPropertyName), j.awaitExpression(accessedPropIdExpr)),
494
570
  ]);
495
571
  if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
496
572
  functionBody.unshift(paramAssignment);
@@ -508,7 +584,7 @@ function transformDynamicProps(source, api, filePath) {
508
584
  (0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
509
585
  // Insert `const <propName> = await props.<propName>;` at the beginning of the function body
510
586
  const paramAssignment = j.variableDeclaration('const', [
511
- j.variableDeclarator(j.identifier(propName), j.awaitExpression(accessedPropIdExpr)),
587
+ j.variableDeclarator(j.identifier(paramPropertyName), j.awaitExpression(accessedPropIdExpr)),
512
588
  ]);
513
589
  if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
514
590
  functionBody.unshift(paramAssignment);
@@ -518,7 +594,7 @@ function transformDynamicProps(source, api, filePath) {
518
594
  }
519
595
  else {
520
596
  const paramAssignment = j.variableDeclaration('const', [
521
- j.variableDeclarator(j.identifier(propName), j.callExpression(j.identifier('use'), [accessedPropIdExpr])),
597
+ j.variableDeclarator(j.identifier(paramPropertyName), j.callExpression(j.identifier('use'), [accessedPropIdExpr])),
522
598
  ]);
523
599
  if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
524
600
  needsReactUseImport = true;
@@ -555,6 +631,8 @@ function transformDynamicProps(source, api, filePath) {
555
631
  if (needsReactUseImport) {
556
632
  (0, utils_1.insertReactUseImport)(root, j);
557
633
  }
634
+ const commented = commentOnMatchedReExports(root, j);
635
+ modified ||= commented;
558
636
  return modified ? root.toSource() : null;
559
637
  }
560
638
  function findAllTypes(root, j, typeName) {
@@ -610,4 +688,27 @@ function findAllTypes(root, j, typeName) {
610
688
  });
611
689
  return types;
612
690
  }
691
+ function commentSpreadProps(path, propsIdentifierName, j) {
692
+ let modified = false;
693
+ const functionBody = findFunctionBody(path);
694
+ const functionBodyCollection = j(functionBody);
695
+ // Find all the usage of spreading properties of `props`
696
+ const jsxSpreadProperties = functionBodyCollection.find(j.JSXSpreadAttribute, { argument: { name: propsIdentifierName } });
697
+ const objSpreadProperties = functionBodyCollection.find(j.SpreadElement, {
698
+ argument: { name: propsIdentifierName },
699
+ });
700
+ const comment = ` Next.js Dynamic Async API Codemod: '${propsIdentifierName}' is used with spread syntax (...). Any asynchronous properties of '${propsIdentifierName}' must be awaited when accessed. `;
701
+ // Add comment before it
702
+ jsxSpreadProperties.forEach((spread) => {
703
+ const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
704
+ if (inserted)
705
+ modified = true;
706
+ });
707
+ objSpreadProperties.forEach((spread) => {
708
+ const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
709
+ if (inserted)
710
+ modified = true;
711
+ });
712
+ return modified;
713
+ }
613
714
  //# sourceMappingURL=next-async-dynamic-prop.js.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TARGET_PROP_NAMES = exports.TARGET_NAMED_EXPORTS = exports.TARGET_ROUTE_EXPORTS = void 0;
3
+ exports.TARGET_PROP_NAMES = exports.TARGET_NAMED_EXPORTS = exports.TARGET_ROUTE_EXPORTS = exports.NEXTJS_ENTRY_FILES = void 0;
4
4
  exports.isFunctionType = isFunctionType;
5
5
  exports.isMatchedFunctionExported = isMatchedFunctionExported;
6
6
  exports.determineClientDirective = determineClientDirective;
@@ -14,6 +14,7 @@ exports.getFunctionPathFromExportPath = getFunctionPathFromExportPath;
14
14
  exports.wrapParentheseIfNeeded = wrapParentheseIfNeeded;
15
15
  exports.insertCommentOnce = insertCommentOnce;
16
16
  exports.getVariableDeclaratorId = getVariableDeclaratorId;
17
+ exports.NEXTJS_ENTRY_FILES = /([\\/]|^)(page|layout|route|default)\.(t|j)sx?$/;
17
18
  exports.TARGET_ROUTE_EXPORTS = new Set([
18
19
  'GET',
19
20
  'POST',
@@ -144,6 +145,8 @@ function insertReactUseImport(root, j) {
144
145
  source: {
145
146
  value: 'react',
146
147
  },
148
+ // Skip the type only react imports
149
+ importKind: 'value',
147
150
  });
148
151
  if (reactImportDeclaration.size() > 0) {
149
152
  const importNode = reactImportDeclaration.get().node;
@@ -152,13 +155,8 @@ function insertReactUseImport(root, j) {
152
155
  }
153
156
  else {
154
157
  // Final all type imports to 'react'
155
- const reactImport = root.find(j.ImportDeclaration, {
156
- source: {
157
- value: 'react',
158
- },
159
- });
160
- if (reactImport.size() > 0) {
161
- reactImport
158
+ if (reactImportDeclaration.size() > 0) {
159
+ reactImportDeclaration
162
160
  .get()
163
161
  .node.specifiers.push(j.importSpecifier(j.identifier('use')));
164
162
  }
@@ -327,10 +325,11 @@ function insertCommentOnce(node, j, comment) {
327
325
  if (node.comments) {
328
326
  const hasComment = node.comments.some((commentNode) => commentNode.value === comment);
329
327
  if (hasComment) {
330
- return;
328
+ return false;
331
329
  }
332
330
  }
333
331
  node.comments = [j.commentBlock(comment), ...(node.comments || [])];
332
+ return true;
334
333
  }
335
334
  function getVariableDeclaratorId(path, j) {
336
335
  const parent = path.parentPath;