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

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.
@@ -30,11 +30,12 @@ const program = new commander_1.Command(packageJson.name)
30
30
  program
31
31
  .command('upgrade')
32
32
  .description('Upgrade Next.js apps to desired versions with a single command.')
33
- .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.')
33
+ .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
34
  ? 'canary'
35
35
  : packageJson.version.includes('-rc.')
36
36
  ? 'rc'
37
37
  : 'latest')
38
+ .usage('[revision] [options]')
38
39
  .option('--verbose', 'Verbose output', false)
39
40
  .action(upgrade_1.runUpgrade);
40
41
  program.parse(process.argv);
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,10 +77,14 @@ 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,
@@ -82,7 +92,11 @@ async function runUpgrade(revision, options) {
82
92
  for (const codemod of codemods) {
83
93
  await (0, transform_1.runTransform)(codemod, process.cwd(), { force: true });
84
94
  }
85
- console.log(`\n${chalk_1.default.green('✔')} Your Next.js project has been upgraded successfully. ${chalk_1.default.bold('Time to ship! 🚢')}`);
95
+ console.log(); // new line
96
+ if (codemods.length > 0) {
97
+ console.log(`${picocolors_1.default.green('✔')} Codemods have been applied successfully.`);
98
+ }
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
  }
package/lib/utils.js CHANGED
@@ -92,7 +92,7 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
92
92
  version: '14.0',
93
93
  },
94
94
  {
95
- title: 'Transforms dynamic imports that return the named export itself to a module like object',
95
+ title: 'Transform `next/dynamic` imports accessing named exports to return an object with a `default` property',
96
96
  value: 'next-dynamic-access-named-export',
97
97
  version: '15.0.0-canary.44',
98
98
  },
@@ -107,7 +107,7 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
107
107
  version: '15.0.0-canary.171',
108
108
  },
109
109
  {
110
- title: 'Transforms `experimental-edge` to `edge` in the `runtime` route segment configuration within the App Router',
110
+ title: 'Transform App Router Route Segment Config `runtime` value from `experimental-edge` to `edge`',
111
111
  value: 'app-dir-runtime-config-experimental-edge',
112
112
  version: '15.0.0-canary.179',
113
113
  },
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.182",
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 = {};
@@ -123,8 +123,43 @@ function applyUseAndRenameAccessedProp(propIdName, path, j) {
123
123
  }
124
124
  return modified;
125
125
  }
126
- const MATCHED_FILE_PATTERNS = /([\\/]|^)(page|layout|route)\.(t|j)sx?$/;
126
+ function commentOnMatchedReExports(root, j) {
127
+ let modified = false;
128
+ root.find(j.ExportNamedDeclaration).forEach((path) => {
129
+ if (j.ExportSpecifier.check(path.value.specifiers[0])) {
130
+ const specifiers = path.value.specifiers;
131
+ for (const specifier of specifiers) {
132
+ if (j.ExportSpecifier.check(specifier) &&
133
+ // Find matched named exports and default export
134
+ (utils_1.TARGET_NAMED_EXPORTS.has(specifier.exported.name) ||
135
+ specifier.exported.name === 'default')) {
136
+ if (j.Literal.check(path.value.source)) {
137
+ const localName = specifier.local.name;
138
+ 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\``);
139
+ modified ||= commentInserted;
140
+ }
141
+ else if (path.value.source === null) {
142
+ const localIdentifier = specifier.local;
143
+ const localName = localIdentifier.name;
144
+ // search if local identifier is from imports
145
+ const importDeclaration = root
146
+ .find(j.ImportDeclaration)
147
+ .filter((importPath) => {
148
+ return importPath.value.specifiers.some((importSpecifier) => importSpecifier.local.name === localName);
149
+ });
150
+ if (importDeclaration.size() > 0) {
151
+ 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\``);
152
+ modified ||= commentInserted;
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+ });
159
+ return modified;
160
+ }
127
161
  function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
162
+ let modified = false;
128
163
  if (paramTypeAnnotation && paramTypeAnnotation.typeAnnotation) {
129
164
  const typeAnnotation = paramTypeAnnotation.typeAnnotation;
130
165
  if (typeAnnotation.type === 'TSTypeLiteral') {
@@ -151,6 +186,7 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
151
186
  // @ts-ignore
152
187
  member.typeAnnotation.typeAnnotation,
153
188
  ]));
189
+ modified = true;
154
190
  }
155
191
  }
156
192
  });
@@ -189,6 +225,7 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
189
225
  member.typeAnnotation.typeAnnotation = j.tsTypeReference(j.identifier('Promise'), j.tsTypeParameterInstantiation([
190
226
  member.typeAnnotation.typeAnnotation,
191
227
  ]));
228
+ modified = true;
192
229
  }
193
230
  }
194
231
  });
@@ -197,11 +234,13 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
197
234
  }
198
235
  }
199
236
  propsIdentifier.typeAnnotation = paramTypeAnnotation;
237
+ modified = true;
200
238
  }
239
+ return modified;
201
240
  }
202
241
  function transformDynamicProps(source, api, filePath) {
203
- const isMatched = MATCHED_FILE_PATTERNS.test(filePath);
204
- if (!isMatched) {
242
+ const isEntryFile = utils_1.NEXTJS_ENTRY_FILES.test(filePath);
243
+ if (!isEntryFile) {
205
244
  return null;
206
245
  }
207
246
  let modified = false;
@@ -289,7 +328,8 @@ function transformDynamicProps(source, api, filePath) {
289
328
  }
290
329
  }
291
330
  else {
292
- modified = awaitMemberAccessOfProp(argName, path, j);
331
+ const awaited = awaitMemberAccessOfProp(argName, path, j);
332
+ modified ||= awaited;
293
333
  }
294
334
  // cases of passing down `props` into any function
295
335
  // Page(props) { callback(props) }
@@ -309,8 +349,8 @@ function transformDynamicProps(source, api, filePath) {
309
349
  const args = callExpression.value.arguments;
310
350
  const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
311
351
  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;
352
+ const inserted = (0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
353
+ modified ||= inserted;
314
354
  });
315
355
  if (modified) {
316
356
  modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
@@ -328,6 +368,14 @@ function transformDynamicProps(source, api, filePath) {
328
368
  modified = true;
329
369
  }
330
370
  }
371
+ else {
372
+ // When the prop argument is not destructured, we need to add comments to the spread properties
373
+ if (j.Identifier.check(currentParam)) {
374
+ const commented = commentSpreadProps(path, currentParam.name, j);
375
+ const modifiedTypes = modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
376
+ modified ||= commented || modifiedTypes;
377
+ }
378
+ }
331
379
  }
332
380
  // Helper function to insert `const params = await asyncParams;` at the beginning of the function body
333
381
  function resolveAsyncProp(path, propertiesMap, propsIdentifierName, allProperties, isDefaultExport) {
@@ -555,6 +603,8 @@ function transformDynamicProps(source, api, filePath) {
555
603
  if (needsReactUseImport) {
556
604
  (0, utils_1.insertReactUseImport)(root, j);
557
605
  }
606
+ const commented = commentOnMatchedReExports(root, j);
607
+ modified ||= commented;
558
608
  return modified ? root.toSource() : null;
559
609
  }
560
610
  function findAllTypes(root, j, typeName) {
@@ -610,4 +660,27 @@ function findAllTypes(root, j, typeName) {
610
660
  });
611
661
  return types;
612
662
  }
663
+ function commentSpreadProps(path, propsIdentifierName, j) {
664
+ let modified = false;
665
+ const functionBody = findFunctionBody(path);
666
+ const functionBodyCollection = j(functionBody);
667
+ // Find all the usage of spreading properties of `props`
668
+ const jsxSpreadProperties = functionBodyCollection.find(j.JSXSpreadAttribute, { argument: { name: propsIdentifierName } });
669
+ const objSpreadProperties = functionBodyCollection.find(j.SpreadElement, {
670
+ argument: { name: propsIdentifierName },
671
+ });
672
+ const comment = ` Next.js Dynamic Async API Codemod: '${propsIdentifierName}' is used with spread syntax (...). Any asynchronous properties of '${propsIdentifierName}' must be awaited when accessed. `;
673
+ // Add comment before it
674
+ jsxSpreadProperties.forEach((spread) => {
675
+ const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
676
+ if (inserted)
677
+ modified = true;
678
+ });
679
+ objSpreadProperties.forEach((spread) => {
680
+ const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
681
+ if (inserted)
682
+ modified = true;
683
+ });
684
+ return modified;
685
+ }
613
686
  //# 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;