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

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/transform.js CHANGED
@@ -36,8 +36,8 @@ async function runTransform(transform, path, options) {
36
36
  type: 'text',
37
37
  name: 'path',
38
38
  message: 'On which files or directory should the codemods be applied?',
39
- default: '.',
40
- });
39
+ initial: '.',
40
+ }, { onCancel: utils_1.onCancel });
41
41
  directory = res.path;
42
42
  }
43
43
  if (!transform) {
@@ -45,8 +45,14 @@ async function runTransform(transform, path, options) {
45
45
  type: 'select',
46
46
  name: 'transformer',
47
47
  message: 'Which transform would you like to apply?',
48
- choices: utils_1.TRANSFORMER_INQUIRER_CHOICES,
49
- });
48
+ choices: utils_1.TRANSFORMER_INQUIRER_CHOICES.reverse().map(({ title, value, version }) => {
49
+ return {
50
+ title: `(v${version}) ${value}`,
51
+ description: title,
52
+ value,
53
+ };
54
+ }),
55
+ }, { onCancel: utils_1.onCancel });
50
56
  transformer = res.transformer;
51
57
  }
52
58
  const filesExpanded = expandFilePathsIfNeeded([directory]);
package/bin/upgrade.js CHANGED
@@ -10,9 +10,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
12
  const chalk_1 = __importDefault(require("chalk"));
13
- const codemods_1 = require("../lib/codemods");
14
13
  const handle_package_1 = require("../lib/handle-package");
15
14
  const transform_1 = require("./transform");
15
+ const utils_1 = require("../lib/utils");
16
16
  /**
17
17
  * @param query
18
18
  * @example loadHighestNPMVersionMatching("react@^18.3.0 || ^19.0.0") === Promise<"19.0.0">
@@ -29,16 +29,19 @@ async function runUpgrade(revision, options) {
29
29
  const { verbose } = options;
30
30
  const appPackageJsonPath = path_1.default.resolve(process.cwd(), 'package.json');
31
31
  let appPackageJson = JSON.parse(fs_1.default.readFileSync(appPackageJsonPath, 'utf8'));
32
- await detectWorkspace(appPackageJson);
33
32
  let targetNextPackageJson;
34
33
  const res = await fetch(`https://registry.npmjs.org/next/${revision}`);
35
34
  if (res.status === 200) {
36
35
  targetNextPackageJson = await res.json();
37
36
  }
38
- else {
39
- throw new Error(`${chalk_1.default.yellow(`next@${revision}`)} does not exist. Check available versions at ${chalk_1.default.underline('https://www.npmjs.com/package/next?activeTab=versions')}.`);
37
+ const validRevision = targetNextPackageJson !== null &&
38
+ typeof targetNextPackageJson === 'object' &&
39
+ 'version' in targetNextPackageJson &&
40
+ 'peerDependencies' in targetNextPackageJson;
41
+ 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')}.`);
40
43
  }
41
- const installedNextVersion = await getInstalledNextVersion();
44
+ const installedNextVersion = getInstalledNextVersion();
42
45
  const targetNextVersion = targetNextPackageJson.version;
43
46
  // We're resolving a specific version here to avoid including "ugly" version queries
44
47
  // in the manifest.
@@ -81,28 +84,17 @@ async function runUpgrade(revision, options) {
81
84
  }
82
85
  console.log(`\n${chalk_1.default.green('✔')} Your Next.js project has been upgraded successfully. ${chalk_1.default.bold('Time to ship! 🚢')}`);
83
86
  }
84
- async function detectWorkspace(appPackageJson) {
85
- let isWorkspace = appPackageJson.workspaces ||
86
- fs_1.default.existsSync(path_1.default.resolve(process.cwd(), 'pnpm-workspace.yaml'));
87
- if (!isWorkspace)
88
- return;
89
- console.log(`${chalk_1.default.red('⚠️')} You seem to be in the root of a monorepo. ${chalk_1.default.blue('@next/upgrade')} should be run in a specific app directory within the monorepo.`);
90
- const response = await (0, prompts_1.default)({
91
- type: 'confirm',
92
- name: 'value',
93
- message: 'Do you still want to continue?',
94
- initial: false,
95
- }, { onCancel: () => process.exit(0) });
96
- if (!response.value) {
97
- process.exit(0);
87
+ function getInstalledNextVersion() {
88
+ try {
89
+ return require(require.resolve('next/package.json', {
90
+ paths: [process.cwd()],
91
+ })).version;
92
+ }
93
+ catch (error) {
94
+ throw new Error(`Failed to get the installed Next.js version at "${process.cwd()}".\nIf you're using a monorepo, please run this command from the Next.js app directory.`, {
95
+ cause: error,
96
+ });
98
97
  }
99
- }
100
- async function getInstalledNextVersion() {
101
- const installedNextPackageJsonDir = require.resolve('next/package.json', {
102
- paths: [process.cwd()],
103
- });
104
- const installedNextPackageJson = JSON.parse(fs_1.default.readFileSync(installedNextPackageJsonDir, 'utf8'));
105
- return installedNextPackageJson.version;
106
98
  }
107
99
  /*
108
100
  * Heuristics are used to determine whether to Turbopack is enabled or not and
@@ -122,13 +114,9 @@ async function suggestTurbopack(packageJson) {
122
114
  const responseTurbopack = await (0, prompts_1.default)({
123
115
  type: 'confirm',
124
116
  name: 'enable',
125
- message: 'Turbopack is now the stable default for dev mode. Enable it?',
117
+ message: 'Enable Turbopack for next dev?',
126
118
  initial: true,
127
- }, {
128
- onCancel: () => {
129
- process.exit(0);
130
- },
131
- });
119
+ }, { onCancel: utils_1.onCancel });
132
120
  if (!responseTurbopack.enable) {
133
121
  return;
134
122
  }
@@ -136,46 +124,42 @@ async function suggestTurbopack(packageJson) {
136
124
  packageJson.scripts['dev'] = devScript.replace('next dev', 'next dev --turbo');
137
125
  return;
138
126
  }
127
+ console.log(`${chalk_1.default.yellow('⚠')} Could not find "${chalk_1.default.bold('next dev')}" in your dev script.`);
139
128
  const responseCustomDevScript = await (0, prompts_1.default)({
140
129
  type: 'text',
141
130
  name: 'customDevScript',
142
- message: 'Please add `--turbo` to your dev command:',
131
+ message: 'Please manually add "--turbo" to your dev command.',
143
132
  initial: devScript,
144
- });
133
+ }, { onCancel: utils_1.onCancel });
145
134
  packageJson.scripts['dev'] =
146
135
  responseCustomDevScript.customDevScript || devScript;
147
136
  }
148
137
  async function suggestCodemods(initialNextVersion, targetNextVersion) {
149
- const initialVersionIndex = codemods_1.availableCodemods.findIndex((versionCodemods) => (0, compare_versions_1.compareVersions)(versionCodemods.version, initialNextVersion) > 0);
138
+ const initialVersionIndex = utils_1.TRANSFORMER_INQUIRER_CHOICES.findIndex((versionCodemods) => (0, compare_versions_1.compareVersions)(versionCodemods.version, initialNextVersion) > 0);
150
139
  if (initialVersionIndex === -1) {
151
140
  return [];
152
141
  }
153
- let targetVersionIndex = codemods_1.availableCodemods.findIndex((versionCodemods) => (0, compare_versions_1.compareVersions)(versionCodemods.version, targetNextVersion) > 0);
142
+ let targetVersionIndex = utils_1.TRANSFORMER_INQUIRER_CHOICES.findIndex((versionCodemods) => (0, compare_versions_1.compareVersions)(versionCodemods.version, targetNextVersion) > 0);
154
143
  if (targetVersionIndex === -1) {
155
- targetVersionIndex = codemods_1.availableCodemods.length;
144
+ targetVersionIndex = utils_1.TRANSFORMER_INQUIRER_CHOICES.length;
156
145
  }
157
- const relevantCodemods = codemods_1.availableCodemods
158
- .slice(initialVersionIndex, targetVersionIndex)
159
- .flatMap((versionCodemods) => versionCodemods.codemods);
146
+ const relevantCodemods = utils_1.TRANSFORMER_INQUIRER_CHOICES.slice(initialVersionIndex, targetVersionIndex);
160
147
  if (relevantCodemods.length === 0) {
161
148
  return [];
162
149
  }
163
150
  const { codemods } = await (0, prompts_1.default)({
164
151
  type: 'multiselect',
165
152
  name: 'codemods',
166
- message: `\nThe following ${chalk_1.default.blue('codemods')} are recommended for your upgrade. Would you like to apply them?`,
167
- choices: relevantCodemods.map((codemod) => {
153
+ message: `The following ${chalk_1.default.blue('codemods')} are recommended for your upgrade. Select the ones to apply.`,
154
+ choices: relevantCodemods.reverse().map(({ title, value, version }) => {
168
155
  return {
169
- title: `${codemod.title} ${chalk_1.default.grey(`(${codemod.value})`)}`,
170
- value: codemod.value,
156
+ title: `(v${version}) ${value}`,
157
+ description: title,
158
+ value,
171
159
  selected: true,
172
160
  };
173
161
  }),
174
- }, {
175
- onCancel: () => {
176
- process.exit(0);
177
- },
178
- });
162
+ }, { onCancel: utils_1.onCancel });
179
163
  return codemods;
180
164
  }
181
165
  //# sourceMappingURL=upgrade.js.map
@@ -10,7 +10,20 @@ function runJscodeshift(transformerPath, flags, files) {
10
10
  // we run jscodeshift in the same process to be able to
11
11
  // share state between the main CRA transform and sub-transforms
12
12
  return Runner_1.default.run(transformerPath, files, {
13
- ignorePattern: ['**/node_modules/**', '**/.next/**', '**/build/**'],
13
+ ignorePattern: [
14
+ '**/node_modules/**',
15
+ '**/.next/**',
16
+ '**/build/**',
17
+ // type files
18
+ '**/*.d.ts',
19
+ '**/*.d.cts',
20
+ '**/*.d.mts',
21
+ // test files
22
+ '**/*.test.*',
23
+ '**/*.spec.*',
24
+ '**/__tests__/**',
25
+ '**/__mocks__/**',
26
+ ],
14
27
  extensions: 'tsx,ts,jsx,js',
15
28
  parser: 'tsx',
16
29
  verbose: 2,
package/lib/utils.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.TRANSFORMER_INQUIRER_CHOICES = void 0;
7
7
  exports.checkGitStatus = checkGitStatus;
8
+ exports.onCancel = onCancel;
8
9
  const picocolors_1 = require("picocolors");
9
10
  const is_git_clean_1 = __importDefault(require("is-git-clean"));
10
11
  function checkGitStatus(force) {
@@ -31,62 +32,84 @@ function checkGitStatus(force) {
31
32
  }
32
33
  }
33
34
  }
35
+ function onCancel() {
36
+ process.exit(1);
37
+ }
34
38
  exports.TRANSFORMER_INQUIRER_CHOICES = [
35
39
  {
36
- title: 'name-default-component: Transforms anonymous components into named components to make sure they work with Fast Refresh',
37
- value: 'name-default-component',
40
+ title: 'Transform the deprecated automatically injected url property on top level pages to using withRouter',
41
+ value: 'url-to-withrouter',
42
+ version: '6.0',
38
43
  },
39
44
  {
40
- title: 'add-missing-react-import: Transforms files that do not import `React` to include the import in order for the new React JSX transform',
41
- value: 'add-missing-react-import',
45
+ title: 'Transforms the withAmp HOC into Next.js 9 page configuration',
46
+ value: 'withamp-to-config',
47
+ version: '8.0',
42
48
  },
43
49
  {
44
- title: 'withamp-to-config: Transforms the withAmp HOC into Next.js 9 page configuration',
45
- value: 'withamp-to-config',
50
+ title: 'Transforms anonymous components into named components to make sure they work with Fast Refresh',
51
+ value: 'name-default-component',
52
+ version: '9.0',
46
53
  },
47
54
  {
48
- title: 'url-to-withrouter: Transforms the deprecated automatically injected url property on top level pages to using withRouter',
49
- value: 'url-to-withrouter',
55
+ title: 'Transforms files that do not import `React` to include the import in order for the new React JSX transform',
56
+ value: 'add-missing-react-import',
57
+ version: '10.0',
50
58
  },
51
59
  {
52
- title: 'cra-to-next (experimental): automatically migrates a Create React App project to Next.js',
60
+ title: 'Automatically migrates a Create React App project to Next.js (experimental)',
53
61
  value: 'cra-to-next',
62
+ version: '11.0',
54
63
  },
55
64
  {
56
- title: 'new-link: Ensures your <Link> usage is backwards compatible.',
65
+ title: 'Ensures your <Link> usage is backwards compatible',
57
66
  value: 'new-link',
67
+ version: '13.0',
58
68
  },
59
69
  {
60
- title: 'next-og-import: Transforms imports from `next/server` to `next/og` for usage of Dynamic OG Image Generation.',
61
- value: 'next-og-import',
70
+ title: 'Dangerously migrates from `next/legacy/image` to the new `next/image` by adding inline styles and removing unused props (experimental)',
71
+ value: 'next-image-experimental',
72
+ version: '13.0',
62
73
  },
63
74
  {
64
- title: 'metadata-to-viewport-export: Migrates certain viewport related metadata from the `metadata` export to a new `viewport` export.',
65
- value: 'metadata-to-viewport-export',
75
+ title: 'Safely migrate Next.js 10, 11, 12 applications importing `next/image` to the renamed `next/legacy/image` import in Next.js 13',
76
+ value: 'next-image-to-legacy-image',
77
+ version: '13.0',
66
78
  },
67
79
  {
68
- title: 'next-dynamic-access-named-export: Transforms dynamic imports that return the named export itself to a module like object.',
69
- value: 'next-dynamic-access-named-export',
80
+ title: 'Uninstall `@next/font` and transform imports to `next/font`',
81
+ value: 'built-in-next-font',
82
+ version: '13.2',
70
83
  },
71
84
  {
72
- title: 'next-image-to-legacy-image: safely migrate Next.js 10, 11, 12 applications importing `next/image` to the renamed `next/legacy/image` import in Next.js 13',
73
- value: 'next-image-to-legacy-image',
85
+ title: 'Migrates certain viewport related metadata from the `metadata` export to a new `viewport` export',
86
+ value: 'metadata-to-viewport-export',
87
+ version: '14.0',
74
88
  },
75
89
  {
76
- title: 'next-image-experimental (experimental): dangerously migrates from `next/legacy/image` to the new `next/image` by adding inline styles and removing unused props',
77
- value: 'next-image-experimental',
90
+ title: 'Transforms imports from `next/server` to `next/og` for usage of Dynamic OG Image Generation',
91
+ value: 'next-og-import',
92
+ version: '14.0',
78
93
  },
79
94
  {
80
- title: 'built-in-next-font: Uninstall `@next/font` and transform imports to `next/font`',
81
- value: 'built-in-next-font',
95
+ title: 'Transforms dynamic imports that return the named export itself to a module like object',
96
+ value: 'next-dynamic-access-named-export',
97
+ version: '15.0.0-canary.44',
98
+ },
99
+ {
100
+ title: 'Install `@vercel/functions` to replace `geo` and `ip` properties on `NextRequest`',
101
+ value: 'next-request-geo-ip',
102
+ version: '15.0.0-canary.153',
82
103
  },
83
104
  {
84
- title: 'next-async-request-api: Transforms usage of Next.js async Request APIs',
105
+ title: 'Transforms usage of Next.js async Request APIs',
85
106
  value: 'next-async-request-api',
107
+ version: '15.0.0-canary.171',
86
108
  },
87
109
  {
88
- title: 'next-request-geo-ip: Install `@vercel/functions` to replace `geo` and `ip` properties on `NextRequest`',
89
- value: 'next-request-geo-ip',
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',
90
113
  },
91
114
  ];
92
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.179",
3
+ "version": "15.0.0-canary.181",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = transformer;
4
+ function transformer(file, api) {
5
+ if (process.env.NODE_ENV !== 'test' &&
6
+ !/[/\\]app[/\\].*?(page|layout|route)\.[^/\\]+$/.test(file.path)) {
7
+ return file.source;
8
+ }
9
+ const j = api.jscodeshift.withParser('tsx');
10
+ const root = j(file.source);
11
+ const runtimeExport = root.find(j.ExportNamedDeclaration, {
12
+ declaration: {
13
+ type: 'VariableDeclaration',
14
+ declarations: [
15
+ {
16
+ id: { name: 'runtime' },
17
+ },
18
+ ],
19
+ },
20
+ });
21
+ if (runtimeExport.size() !== 1) {
22
+ return file.source;
23
+ }
24
+ const runtimeValue = runtimeExport.find(j.StringLiteral, {
25
+ value: 'experimental-edge',
26
+ });
27
+ if (runtimeValue.size() !== 1) {
28
+ return file.source;
29
+ }
30
+ runtimeValue.replaceWith(j.stringLiteral('edge'));
31
+ return root.toSource();
32
+ }
33
+ //# sourceMappingURL=app-dir-runtime-config-experimental-edge.js.map
@@ -4,11 +4,6 @@ exports.default = transform;
4
4
  const next_async_dynamic_prop_1 = require("./next-async-dynamic-prop");
5
5
  const next_async_dynamic_api_1 = require("./next-async-dynamic-api");
6
6
  function transform(file, api) {
7
- const filePath = file.path;
8
- // if it's node_modules or types file, skip
9
- if (/node_modules/.test(filePath) || /\.d\.(m|c)?ts$/.test(filePath)) {
10
- return file.source;
11
- }
12
7
  const transforms = [next_async_dynamic_prop_1.transformDynamicProps, next_async_dynamic_api_1.transformDynamicAPI];
13
8
  return transforms.reduce((source, transformFn) => {
14
9
  const result = transformFn(source, api, file.path);
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.transformDynamicAPI = transformDynamicAPI;
4
4
  const utils_1 = require("./utils");
5
- const DYNAMIC_IMPORT_WARN_COMMENT = ` The APIs under 'next/headers' are async now, need to be manually awaited. `;
5
+ const DYNAMIC_IMPORT_WARN_COMMENT = ` Next.js Dynamic Async API Codemod: The APIs under 'next/headers' are async now, need to be manually awaited. `;
6
6
  function findDynamicImportsAndComment(root, j) {
7
7
  let modified = false;
8
8
  // find all the dynamic imports of `next/headers`,
@@ -134,11 +134,11 @@ function transformDynamicAPI(source, api, filePath) {
134
134
  needsReactUseImport = true;
135
135
  }
136
136
  else {
137
- castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` TODO: please manually await this call, if it's a server component, you can turn it to async function `);
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
138
  }
139
139
  }
140
140
  else {
141
- castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ' TODO: please manually await this call, codemod cannot transform due to undetermined async scope ');
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 ');
142
142
  }
143
143
  modified = true;
144
144
  }
@@ -23,6 +23,9 @@ function awaitMemberAccessOfProp(propIdName, path, j) {
23
23
  // await each member access
24
24
  memberAccess.forEach((memberAccessPath) => {
25
25
  const member = memberAccessPath.value;
26
+ if (isParentPromiseAllCallExpression(memberAccessPath, j)) {
27
+ return;
28
+ }
26
29
  // check if it's already awaited
27
30
  if (memberAccessPath.parentPath?.value.type === 'AwaitExpression') {
28
31
  return;
@@ -43,6 +46,33 @@ function awaitMemberAccessOfProp(propIdName, path, j) {
43
46
  }
44
47
  return hasAwaited;
45
48
  }
49
+ function isParentUseCallExpression(path, j) {
50
+ if (
51
+ // member access parentPath is argument
52
+ j.CallExpression.check(path.parent.value) &&
53
+ // member access is first argument
54
+ path.parent.value.arguments[0] === path.value &&
55
+ // function name is `use`
56
+ j.Identifier.check(path.parent.value.callee) &&
57
+ path.parent.value.callee.name === 'use') {
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+ function isParentPromiseAllCallExpression(path, j) {
63
+ const argsParent = path.parent;
64
+ const callParent = argsParent?.parent;
65
+ if (j.ArrayExpression.check(argsParent.value) &&
66
+ j.CallExpression.check(callParent.value) &&
67
+ j.MemberExpression.check(callParent.value.callee) &&
68
+ j.Identifier.check(callParent.value.callee.object) &&
69
+ callParent.value.callee.object.name === 'Promise' &&
70
+ j.Identifier.check(callParent.value.callee.property) &&
71
+ callParent.value.callee.property.name === 'all') {
72
+ return true;
73
+ }
74
+ return false;
75
+ }
46
76
  function applyUseAndRenameAccessedProp(propIdName, path, j) {
47
77
  // search the member access of the prop, and rename the member access to the member value
48
78
  // e.g.
@@ -60,6 +90,10 @@ function applyUseAndRenameAccessedProp(propIdName, path, j) {
60
90
  const accessedNames = [];
61
91
  // rename each member access
62
92
  memberAccess.forEach((memberAccessPath) => {
93
+ // If the member access expression is first argument of `use()`, we skip
94
+ if (isParentUseCallExpression(memberAccessPath, j)) {
95
+ return;
96
+ }
63
97
  const member = memberAccessPath.value;
64
98
  const memberProperty = member.property;
65
99
  if (j.Identifier.check(memberProperty)) {
@@ -89,7 +123,7 @@ function applyUseAndRenameAccessedProp(propIdName, path, j) {
89
123
  }
90
124
  return modified;
91
125
  }
92
- const MATCHED_FILE_PATTERNS = /([\\/]|^)(page|layout)\.(t|j)sx?$/;
126
+ const MATCHED_FILE_PATTERNS = /([\\/]|^)(page|layout|route)\.(t|j)sx?$/;
93
127
  function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
94
128
  if (paramTypeAnnotation && paramTypeAnnotation.typeAnnotation) {
95
129
  const typeAnnotation = paramTypeAnnotation.typeAnnotation;
@@ -233,17 +267,13 @@ function transformDynamicProps(source, api, filePath) {
233
267
  currentParam.properties.forEach((prop) => {
234
268
  if (
235
269
  // Could be `Property` or `ObjectProperty`
236
- 'key' in prop &&
237
- prop.key.type === 'Identifier' &&
270
+ (j.Property.check(prop) || j.ObjectProperty.check(prop)) &&
271
+ j.Identifier.check(prop.key) &&
238
272
  utils_1.TARGET_PROP_NAMES.has(prop.key.name)) {
239
273
  const value = 'value' in prop ? prop.value : null;
240
274
  propertiesMap.set(prop.key.name, value);
241
275
  }
242
276
  });
243
- modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
244
- // Override the first param to `props`
245
- params[propsArgumentIndex] = propsIdentifier;
246
- modified = true;
247
277
  modifiedPropArgument = true;
248
278
  }
249
279
  else if (currentParam.type === 'Identifier') {
@@ -278,7 +308,7 @@ function transformDynamicProps(source, api, filePath) {
278
308
  // find the argument `currentParam`
279
309
  const args = callExpression.value.arguments;
280
310
  const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
281
- const comment = ` '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `;
311
+ const comment = ` Next.js Dynamic Async API Codemod: '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `;
282
312
  (0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
283
313
  modified = true;
284
314
  });
@@ -287,7 +317,16 @@ function transformDynamicProps(source, api, filePath) {
287
317
  }
288
318
  }
289
319
  if (modifiedPropArgument) {
290
- resolveAsyncProp(path, propertiesMap, propsIdentifier.name, allProperties, isDefaultExport);
320
+ const isModified = resolveAsyncProp(path, propertiesMap, propsIdentifier.name, allProperties, isDefaultExport);
321
+ if (isModified) {
322
+ // Make TS happy
323
+ if (j.ObjectPattern.check(currentParam)) {
324
+ modifyTypes(currentParam.typeAnnotation, propsIdentifier, root, j);
325
+ }
326
+ // Override the first param to `props`
327
+ params[propsArgumentIndex] = propsIdentifier;
328
+ modified = true;
329
+ }
291
330
  }
292
331
  }
293
332
  // Helper function to insert `const params = await asyncParams;` at the beginning of the function body
@@ -385,22 +424,46 @@ function transformDynamicProps(source, api, filePath) {
385
424
  functionBody.unshift(destructionOtherPropertiesDeclaration);
386
425
  }
387
426
  }
427
+ let modifiedPropertyCount = 0;
388
428
  for (const [matchedPropName, paramsProperty] of propertiesMap) {
389
429
  if (!utils_1.TARGET_PROP_NAMES.has(matchedPropName)) {
390
430
  continue;
391
431
  }
432
+ // In client component, if the param is already wrapped with `use()`, skip the transformation
433
+ if (isClientComponent) {
434
+ let shouldSkip = false;
435
+ const propPaths = j(path).find(j.Identifier, {
436
+ name: matchedPropName,
437
+ });
438
+ for (const propPath of propPaths.paths()) {
439
+ if (isParentUseCallExpression(propPath, j)) {
440
+ // Skip transformation
441
+ shouldSkip = true;
442
+ break;
443
+ }
444
+ }
445
+ if (shouldSkip) {
446
+ continue;
447
+ }
448
+ }
392
449
  const propRenamedId = j.Identifier.check(paramsProperty)
393
450
  ? paramsProperty.name
394
451
  : null;
395
452
  const propName = propRenamedId || matchedPropName;
396
453
  // if propName is not used in lower scope, and it stars with unused prefix `_`,
397
454
  // also skip the transformation
398
- const hasDeclared = path.scope.declares(propName);
399
- if (!hasDeclared && propName.startsWith('_'))
455
+ const functionBodyPath = path.get('body');
456
+ const hasUsedInBody = j(functionBodyPath)
457
+ .find(j.Identifier, {
458
+ name: propName,
459
+ })
460
+ .size() > 0;
461
+ if (!hasUsedInBody && propName.startsWith('_'))
400
462
  continue;
463
+ modifiedPropertyCount++;
401
464
  const propNameIdentifier = j.identifier(matchedPropName);
402
465
  const propsIdentifier = j.identifier(propsIdentifierName);
403
- const accessedPropId = j.memberExpression(propsIdentifier, propNameIdentifier);
466
+ const accessedPropIdExpr = j.memberExpression(propsIdentifier, propNameIdentifier);
404
467
  // Check param property value, if it's destructed, we need to destruct it as well
405
468
  // e.g.
406
469
  // input: Page({ params: { slug } })
@@ -427,7 +490,7 @@ function transformDynamicProps(source, api, filePath) {
427
490
  if (isAsyncFunc) {
428
491
  // If it's async function, add await to the async props.<propName>
429
492
  const paramAssignment = j.variableDeclaration('const', [
430
- j.variableDeclarator(j.identifier(propName), j.awaitExpression(accessedPropId)),
493
+ j.variableDeclarator(j.identifier(propName), j.awaitExpression(accessedPropIdExpr)),
431
494
  ]);
432
495
  if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
433
496
  functionBody.unshift(paramAssignment);
@@ -445,7 +508,7 @@ function transformDynamicProps(source, api, filePath) {
445
508
  (0, utils_1.turnFunctionReturnTypeToAsync)(node, j);
446
509
  // Insert `const <propName> = await props.<propName>;` at the beginning of the function body
447
510
  const paramAssignment = j.variableDeclaration('const', [
448
- j.variableDeclarator(j.identifier(propName), j.awaitExpression(accessedPropId)),
511
+ j.variableDeclarator(j.identifier(propName), j.awaitExpression(accessedPropIdExpr)),
449
512
  ]);
450
513
  if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
451
514
  functionBody.unshift(paramAssignment);
@@ -455,7 +518,7 @@ function transformDynamicProps(source, api, filePath) {
455
518
  }
456
519
  else {
457
520
  const paramAssignment = j.variableDeclaration('const', [
458
- j.variableDeclarator(j.identifier(propName), j.callExpression(j.identifier('use'), [accessedPropId])),
521
+ j.variableDeclarator(j.identifier(propName), j.callExpression(j.identifier('use'), [accessedPropIdExpr])),
459
522
  ]);
460
523
  if (!insertedRenamedPropFunctionNames.has(uid) && functionBody) {
461
524
  needsReactUseImport = true;
@@ -465,6 +528,7 @@ function transformDynamicProps(source, api, filePath) {
465
528
  }
466
529
  }
467
530
  }
531
+ return modifiedPropertyCount > 0;
468
532
  }
469
533
  const defaultExportsDeclarations = root.find(j.ExportDefaultDeclaration);
470
534
  defaultExportsDeclarations.forEach((path) => {
@@ -26,6 +26,7 @@ exports.TARGET_ROUTE_EXPORTS = new Set([
26
26
  exports.TARGET_NAMED_EXPORTS = new Set([
27
27
  // For page and layout
28
28
  'generateMetadata',
29
+ 'generateViewport',
29
30
  ...exports.TARGET_ROUTE_EXPORTS,
30
31
  ]);
31
32
  exports.TARGET_PROP_NAMES = new Set(['params', 'searchParams']);
package/lib/codemods.js DELETED
@@ -1,108 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.availableCodemods = void 0;
4
- exports.availableCodemods = [
5
- {
6
- version: '6',
7
- codemods: [
8
- {
9
- title: 'Use withRouter',
10
- value: 'url-to-withrouter',
11
- },
12
- ],
13
- },
14
- {
15
- version: '8',
16
- codemods: [
17
- {
18
- title: 'Transform AMP HOC into page config',
19
- value: 'withamp-to-config',
20
- },
21
- ],
22
- },
23
- {
24
- version: '9',
25
- codemods: [
26
- {
27
- title: 'Transform Anonymous Components into Named Components',
28
- value: 'name-default-component',
29
- },
30
- ],
31
- },
32
- {
33
- version: '10',
34
- codemods: [
35
- {
36
- title: 'Add React Import',
37
- value: 'add-missing-react-import',
38
- },
39
- ],
40
- },
41
- {
42
- version: '11',
43
- codemods: [
44
- {
45
- title: 'Migrate from CRA',
46
- value: 'cra-to-next',
47
- },
48
- ],
49
- },
50
- {
51
- version: '13.0',
52
- codemods: [
53
- {
54
- title: 'Remove <a> Tags From Link Components',
55
- value: 'new-link',
56
- },
57
- {
58
- title: 'Migrate to the New Image Component',
59
- value: 'next-image-experimental',
60
- },
61
- {
62
- title: 'Rename Next Image Imports',
63
- value: 'next-image-to-legacy-image',
64
- },
65
- ],
66
- },
67
- {
68
- version: '13.2',
69
- codemods: [
70
- {
71
- title: 'Use Built-in Font',
72
- value: 'built-in-next-font',
73
- },
74
- ],
75
- },
76
- {
77
- version: '14.0',
78
- codemods: [
79
- {
80
- title: 'Migrate ImageResponse imports',
81
- value: 'next-og-import',
82
- },
83
- {
84
- title: 'Use viewport export',
85
- value: 'metadata-to-viewport-export',
86
- },
87
- ],
88
- },
89
- {
90
- version: '15.0.0-canary.153',
91
- codemods: [
92
- {
93
- title: 'Migrate `geo` and `ip` properties on `NextRequest`',
94
- value: 'next-request-geo-ip',
95
- },
96
- ],
97
- },
98
- {
99
- version: '15.0.0-canary.171',
100
- codemods: [
101
- {
102
- title: 'Transforms usage of Next.js async Request APIs',
103
- value: 'next-async-request-api',
104
- },
105
- ],
106
- },
107
- ];
108
- //# sourceMappingURL=codemods.js.map