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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/upgrade.js CHANGED
@@ -53,18 +53,59 @@ async function runUpgrade(revision, options) {
53
53
  console.log(`${picocolors_1.default.green('✓')} Current Next.js version is already on or higher than the target version "v${targetNextVersion}".`);
54
54
  return;
55
55
  }
56
+ const installedReactVersion = getInstalledReactVersion();
57
+ console.log(`Current React version: v${installedReactVersion}`);
58
+ let shouldStayOnReact18 = false;
59
+ if (
60
+ // From release v14.3.0-canary.45, Next.js expects the React version to be 19.0.0-beta.0
61
+ // If the user is on a version higher than this but is still on React 18, we ask them
62
+ // if they still want to stay on React 18 after the upgrade.
63
+ // IF THE USER USES APP ROUTER, we expect them to upgrade React to > 19.0.0-beta.0,
64
+ // we should only let the user stay on React 18 if they are using pure Pages Router.
65
+ // x-ref(PR): https://github.com/vercel/next.js/pull/65058
66
+ // x-ref(release): https://github.com/vercel/next.js/releases/tag/v14.3.0-canary.45
67
+ (0, compare_versions_1.compareVersions)(installedNextVersion, '14.3.0-canary.45') >= 0 &&
68
+ installedReactVersion.startsWith('18')) {
69
+ const shouldStayOnReact18Res = await (0, prompts_1.default)({
70
+ type: 'confirm',
71
+ name: 'shouldStayOnReact18',
72
+ message: `Are you using ${picocolors_1.default.underline('only the Pages Router')} (no App Router) and prefer to stay on React 18?`,
73
+ initial: false,
74
+ active: 'Yes',
75
+ inactive: 'No',
76
+ }, { onCancel: utils_1.onCancel });
77
+ shouldStayOnReact18 = shouldStayOnReact18Res.shouldStayOnReact18;
78
+ }
56
79
  // We're resolving a specific version here to avoid including "ugly" version queries
57
80
  // in the manifest.
58
81
  // E.g. in peerDependencies we could have `^18.2.0 || ^19.0.0 || 20.0.0-canary`
59
82
  // If we'd just `npm add` that, the manifest would read the same version query.
60
83
  // This is basically a `npm --save-exact react@$versionQuery` that works for every package manager.
61
- const targetReactVersion = await loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`);
84
+ const targetReactVersion = shouldStayOnReact18
85
+ ? '18.3.1'
86
+ : await loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`);
62
87
  if ((0, compare_versions_1.compareVersions)(targetNextVersion, '15.0.0-canary') >= 0) {
63
88
  await suggestTurbopack(appPackageJson);
64
89
  }
65
90
  const codemods = await suggestCodemods(installedNextVersion, targetNextVersion);
66
- fs_1.default.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2));
67
91
  const packageManager = (0, handle_package_1.getPkgManager)(process.cwd());
92
+ let shouldRunReactCodemods = false;
93
+ let shouldRunReactTypesCodemods = false;
94
+ let execCommand = 'npx';
95
+ // The following React codemods are for React 19
96
+ if (!shouldStayOnReact18 &&
97
+ (0, compare_versions_1.compareVersions)(targetReactVersion, '19.0.0-beta.0') >= 0) {
98
+ shouldRunReactCodemods = await suggestReactCodemods();
99
+ shouldRunReactTypesCodemods = await suggestReactTypesCodemods();
100
+ const execCommandMap = {
101
+ yarn: 'yarn dlx',
102
+ pnpm: 'pnpx',
103
+ bun: 'bunx',
104
+ npm: 'npx',
105
+ };
106
+ execCommand = execCommandMap[packageManager];
107
+ }
108
+ fs_1.default.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2));
68
109
  const nextDependency = `next@${targetNextVersion}`;
69
110
  const reactDependencies = [
70
111
  `react@${targetReactVersion}`,
@@ -92,6 +133,23 @@ async function runUpgrade(revision, options) {
92
133
  for (const codemod of codemods) {
93
134
  await (0, transform_1.runTransform)(codemod, process.cwd(), { force: true, verbose });
94
135
  }
136
+ // To reduce user-side burden of selecting which codemods to run as it needs additional
137
+ // understanding of the codemods, we run all of the applicable codemods.
138
+ if (shouldRunReactCodemods) {
139
+ // https://react.dev/blog/2024/04/25/react-19-upgrade-guide#run-all-react-19-codemods
140
+ (0, child_process_1.execSync)(
141
+ // `--no-interactive` skips the interactive prompt that asks for confirmation
142
+ // https://github.com/codemod-com/codemod/blob/c0cf00d13161a0ec0965b6cc6bc5d54076839cc8/apps/cli/src/flags.ts#L160
143
+ `${execCommand} codemod@latest react/19/migration-recipe --no-interactive`, { stdio: 'inherit' });
144
+ }
145
+ if (shouldRunReactTypesCodemods) {
146
+ // https://react.dev/blog/2024/04/25/react-19-upgrade-guide#typescript-changes
147
+ // `--yes` skips prompts and applies all codemods automatically
148
+ // https://github.com/eps1lon/types-react-codemod/blob/8463103233d6b70aad3cd6bee1814001eae51b28/README.md?plain=1#L52
149
+ (0, child_process_1.execSync)(`${execCommand} types-react-codemod@latest --yes preset-19 .`, {
150
+ stdio: 'inherit',
151
+ });
152
+ }
95
153
  console.log(); // new line
96
154
  if (codemods.length > 0) {
97
155
  console.log(`${picocolors_1.default.green('✔')} Codemods have been applied successfully.`);
@@ -110,6 +168,18 @@ function getInstalledNextVersion() {
110
168
  });
111
169
  }
112
170
  }
171
+ function getInstalledReactVersion() {
172
+ try {
173
+ return require(require.resolve('react/package.json', {
174
+ paths: [process.cwd()],
175
+ })).version;
176
+ }
177
+ catch (error) {
178
+ throw new Error(`Failed to detect the installed React version in "${process.cwd()}".\nIf you're working in a monorepo, please run this command from the Next.js app directory.`, {
179
+ cause: error,
180
+ });
181
+ }
182
+ }
113
183
  /*
114
184
  * Heuristics are used to determine whether to Turbopack is enabled or not and
115
185
  * to determine how to update the dev script.
@@ -176,4 +246,26 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
176
246
  }, { onCancel: utils_1.onCancel });
177
247
  return codemods;
178
248
  }
249
+ async function suggestReactCodemods() {
250
+ const { runReactCodemod } = await (0, prompts_1.default)({
251
+ type: 'toggle',
252
+ name: 'runReactCodemod',
253
+ message: 'Would you like to run the React 19 upgrade codemod?',
254
+ initial: true,
255
+ active: 'Yes',
256
+ inactive: 'No',
257
+ }, { onCancel: utils_1.onCancel });
258
+ return runReactCodemod;
259
+ }
260
+ async function suggestReactTypesCodemods() {
261
+ const { runReactTypesCodemod } = await (0, prompts_1.default)({
262
+ type: 'toggle',
263
+ name: 'runReactTypesCodemod',
264
+ message: 'Would you like to run the React 19 Types upgrade codemod?',
265
+ initial: true,
266
+ active: 'Yes',
267
+ inactive: 'No',
268
+ }, { onCancel: utils_1.onCancel });
269
+ return runReactTypesCodemod;
270
+ }
179
271
  //# sourceMappingURL=upgrade.js.map
@@ -6,13 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.globalCssContext = void 0;
7
7
  exports.default = transformer;
8
8
  const path_1 = __importDefault(require("path"));
9
+ const parser_1 = require("../parser");
9
10
  exports.globalCssContext = {
10
11
  cssImports: new Set(),
11
12
  reactSvgImports: new Set(),
12
13
  };
13
14
  const globalStylesRegex = /(?<!\.module)\.(css|scss|sass)$/i;
14
- function transformer(file, api, options) {
15
- const j = api.jscodeshift.withParser('tsx');
15
+ function transformer(file, _api, options) {
16
+ const j = (0, parser_1.createParserFromPath)(file.path);
16
17
  const root = j(file.source);
17
18
  let hasModifications = false;
18
19
  root
@@ -2,12 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.indexContext = void 0;
4
4
  exports.default = transformer;
5
+ const parser_1 = require("../parser");
5
6
  exports.indexContext = {
6
7
  multipleRenderRoots: false,
7
8
  nestedRender: false,
8
9
  };
9
- function transformer(file, api, options) {
10
- const j = api.jscodeshift.withParser('tsx');
10
+ function transformer(file, _api, options) {
11
+ const j = (0, parser_1.createParserFromPath)(file.path);
11
12
  const root = j(file.source);
12
13
  let hasModifications = false;
13
14
  let foundReactRender = 0;
package/lib/parser.js ADDED
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createParserFromPath = createParserFromPath;
7
+ const jscodeshift_1 = __importDefault(require("jscodeshift"));
8
+ const babylon_1 = __importDefault(require("jscodeshift/parser/babylon"));
9
+ const tsOptions_1 = __importDefault(require("jscodeshift/parser/tsOptions"));
10
+ const dtsOptions = {
11
+ ...tsOptions_1.default,
12
+ plugins: [
13
+ ...tsOptions_1.default.plugins.filter((plugin) => plugin !== 'typescript'),
14
+ ['typescript', { dts: true }],
15
+ ],
16
+ };
17
+ function createParserFromPath(filePath) {
18
+ const isDeclarationFile = /\.d\.(m|c)?ts$/.test(filePath);
19
+ if (isDeclarationFile) {
20
+ return jscodeshift_1.default.withParser((0, babylon_1.default)(dtsOptions));
21
+ }
22
+ // jsx is allowed in .js files, feed them into the tsx parser.
23
+ // tsx parser :.js, .jsx, .tsx
24
+ // ts parser: .ts, .mts, .cts
25
+ const isTsFile = /\.(m|c)?.ts$/.test(filePath);
26
+ return isTsFile ? jscodeshift_1.default.withParser('ts') : jscodeshift_1.default.withParser('tsx');
27
+ }
28
+ //# sourceMappingURL=parser.js.map
@@ -14,10 +14,6 @@ function runJscodeshift(transformerPath, flags, files) {
14
14
  '**/node_modules/**',
15
15
  '**/.next/**',
16
16
  '**/build/**',
17
- // type files
18
- '**/*.d.ts',
19
- '**/*.d.cts',
20
- '**/*.d.mts',
21
17
  // test files
22
18
  '**/*.test.*',
23
19
  '**/*.spec.*',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "15.0.0-canary.183",
3
+ "version": "15.0.0-canary.184",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
+ const parser_1 = require("../lib/parser");
4
5
  function addReactImport(j, root) {
5
6
  // We create an import specifier, this is the value of an import, eg:
6
7
  // import React from 'react'
@@ -34,8 +35,8 @@ function addReactImport(j, root) {
34
35
  node.value.body.unshift(ReactImport);
35
36
  });
36
37
  }
37
- function transformer(file, api, options) {
38
- const j = api.jscodeshift.withParser('tsx');
38
+ function transformer(file, _api, options) {
39
+ const j = (0, parser_1.createParserFromPath)(file.path);
39
40
  const root = j(file.source);
40
41
  const hasReactImport = (r) => {
41
42
  return (r.find(j.ImportDefaultSpecifier, {
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
- function transformer(file, api) {
4
+ const parser_1 = require("../lib/parser");
5
+ function transformer(file, _api) {
5
6
  if (process.env.NODE_ENV !== 'test' &&
6
7
  !/[/\\]app[/\\].*?(page|layout|route)\.[^/\\]+$/.test(file.path)) {
7
8
  return file.source;
8
9
  }
9
- const j = api.jscodeshift.withParser('tsx');
10
+ const j = (0, parser_1.createParserFromPath)(file.path);
10
11
  const root = j(file.source);
11
12
  const runtimeExport = root.find(j.ExportNamedDeclaration, {
12
13
  declaration: {
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
- function transformer(file, api, options) {
5
- const j = api.jscodeshift.withParser('tsx');
4
+ const parser_1 = require("../lib/parser");
5
+ function transformer(file, _api, options) {
6
+ const j = (0, parser_1.createParserFromPath)(file.path);
6
7
  const root = j(file.source);
7
8
  let hasChanges = false;
8
9
  // Before: import { ... } from '@next/font'
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.transformDynamicAPI = transformDynamicAPI;
4
4
  const utils_1 = require("./utils");
5
+ const parser_1 = require("../../../lib/parser");
5
6
  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
7
  function findDynamicImportsAndComment(root, j) {
7
8
  let modified = false;
@@ -22,9 +23,9 @@ function findDynamicImportsAndComment(root, j) {
22
23
  });
23
24
  return modified;
24
25
  }
25
- function transformDynamicAPI(source, api, filePath) {
26
+ function transformDynamicAPI(source, _api, filePath) {
26
27
  const isEntryFile = utils_1.NEXTJS_ENTRY_FILES.test(filePath);
27
- const j = api.jscodeshift.withParser('tsx');
28
+ const j = (0, parser_1.createParserFromPath)(filePath);
28
29
  const root = j(source);
29
30
  let modified = false;
30
31
  // Check if 'use' from 'react' needs to be imported
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.transformDynamicProps = transformDynamicProps;
4
4
  const utils_1 = require("./utils");
5
+ const parser_1 = require("../../../lib/parser");
5
6
  const PAGE_PROPS = 'props';
6
7
  function findFunctionBody(path) {
7
8
  let functionBody = path.node.body;
@@ -244,14 +245,14 @@ function modifyTypes(paramTypeAnnotation, propsIdentifier, root, j) {
244
245
  }
245
246
  return modified;
246
247
  }
247
- function transformDynamicProps(source, api, filePath) {
248
+ function transformDynamicProps(source, _api, filePath) {
248
249
  const isEntryFile = utils_1.NEXTJS_ENTRY_FILES.test(filePath);
249
250
  if (!isEntryFile) {
250
251
  return null;
251
252
  }
252
253
  let modified = false;
253
254
  let modifiedPropArgument = false;
254
- const j = api.jscodeshift.withParser('tsx');
255
+ const j = (0, parser_1.createParserFromPath)(filePath);
255
256
  const root = j(source);
256
257
  // Check if 'use' from 'react' needs to be imported
257
258
  let needsReactUseImport = false;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
- function transformer(file, api) {
5
- const j = api.jscodeshift;
4
+ const parser_1 = require("../lib/parser");
5
+ function transformer(file, _api) {
6
+ const j = (0, parser_1.createParserFromPath)(file.path);
6
7
  const root = j(file.source);
7
8
  // Find the metadata export
8
9
  const metadataExport = root.find(j.ExportNamedDeclaration, {
@@ -2,13 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
4
  const path_1 = require("path");
5
+ const parser_1 = require("../lib/parser");
5
6
  const camelCase = (value) => {
6
7
  const val = value.replace(/[-_\s.]+(.)?/g, (_match, chr) => chr ? chr.toUpperCase() : '');
7
8
  return val.slice(0, 1).toUpperCase() + val.slice(1);
8
9
  };
9
10
  const isValidIdentifier = (value) => /^[a-zA-ZÀ-ÿ][0-9a-zA-ZÀ-ÿ]+$/.test(value);
10
- function transformer(file, api, options) {
11
- const j = api.jscodeshift.withParser('tsx');
11
+ function transformer(file, _api, options) {
12
+ const j = (0, parser_1.createParserFromPath)(file.path);
12
13
  const root = j(file.source);
13
14
  let hasModifications;
14
15
  const returnsJSX = (node) => node.type === 'JSXElement' ||
@@ -3,8 +3,9 @@
3
3
  // x-ref: https://github.com/facebook/jscodeshift/issues/534
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.default = transformer;
6
- function transformer(file, api) {
7
- const j = api.jscodeshift.withParser('tsx');
6
+ const parser_1 = require("../lib/parser");
7
+ function transformer(file, _api) {
8
+ const j = (0, parser_1.createParserFromPath)(file.path);
8
9
  const $j = j(file.source);
9
10
  return $j
10
11
  .find(j.ImportDeclaration, { source: { value: 'next/link' } })
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
- function transformer(file, api) {
5
- const j = api.jscodeshift;
4
+ const parser_1 = require("../lib/parser");
5
+ function transformer(file, _api) {
6
+ const j = (0, parser_1.createParserFromPath)(file.path);
6
7
  const root = j(file.source);
7
8
  // Find the import declaration for 'next/dynamic'
8
9
  const dynamicImportDeclaration = root.find(j.ImportDeclaration, {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
4
  const path_1 = require("path");
5
5
  const fs_1 = require("fs");
6
+ const parser_1 = require("../lib/parser");
6
7
  function findAndReplaceProps(j, root, tagName) {
7
8
  const layoutToStyle = {
8
9
  intrinsic: { maxWidth: '100%', height: 'auto' },
@@ -188,8 +189,8 @@ function nextConfigTransformer(j, root, appDir) {
188
189
  });
189
190
  return root;
190
191
  }
191
- function transformer(file, api, options) {
192
- const j = api.jscodeshift.withParser('tsx');
192
+ function transformer(file, _api, options) {
193
+ const j = (0, parser_1.createParserFromPath)(file.path);
193
194
  const root = j(file.source);
194
195
  const parsed = (0, path_1.parse)(file.path || '/');
195
196
  const isConfig = parsed.base === 'next.config.js' ||
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
- function transformer(file, api, options) {
5
- const j = api.jscodeshift.withParser('tsx');
4
+ const parser_1 = require("../lib/parser");
5
+ function transformer(file, _api, options) {
6
+ const j = (0, parser_1.createParserFromPath)(file.path);
6
7
  const root = j(file.source);
7
8
  // Before: import Image from "next/image"
8
9
  // After: import Image from "next/legacy/image"
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
+ const parser_1 = require("../lib/parser");
4
5
  const importToChange = 'ImageResponse';
5
- function transformer(file, api) {
6
- const j = api.jscodeshift;
6
+ function transformer(file, _api) {
7
+ const j = (0, parser_1.createParserFromPath)(file.path);
7
8
  // Find import declarations that match the pattern
8
9
  file.source = j(file.source)
9
10
  .find(j.ImportDeclaration, {
@@ -1,16 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = default_1;
4
+ const parser_1 = require("../lib/parser");
4
5
  const GEO = 'geo';
5
6
  const IP = 'ip';
6
7
  const GEOLOCATION = 'geolocation';
7
8
  const IP_ADDRESS = 'ipAddress';
8
9
  const GEO_TYPE = 'Geo';
9
- function default_1(fileInfo, api) {
10
- const j = api.jscodeshift.withParser('tsx');
11
- const root = j(fileInfo.source);
10
+ function default_1(file, _api) {
11
+ const j = (0, parser_1.createParserFromPath)(file.path);
12
+ const root = j(file.source);
12
13
  if (!root.length) {
13
- return fileInfo.source;
14
+ return file.source;
14
15
  }
15
16
  const nextReqType = root
16
17
  .find(j.FunctionDeclaration)
@@ -59,7 +60,7 @@ function default_1(fileInfo, api) {
59
60
  needImportGeoType ||
60
61
  hasChangedIpType;
61
62
  if (!needChanges) {
62
- return fileInfo.source;
63
+ return file.source;
63
64
  }
64
65
  // Even if there was a change above, if there's an existing import,
65
66
  // we don't need to import them again.