@next/codemod 15.0.0-canary.162 → 15.0.0-canary.163

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/cli.js CHANGED
@@ -23,7 +23,7 @@ const path_1 = __importDefault(require("path"));
23
23
  const execa_1 = __importDefault(require("execa"));
24
24
  const picocolors_1 = require("picocolors");
25
25
  const is_git_clean_1 = __importDefault(require("is-git-clean"));
26
- const uninstall_package_1 = require("../lib/uninstall-package");
26
+ const handle_package_1 = require("../lib/handle-package");
27
27
  exports.jscodeshiftExecutable = require.resolve('.bin/jscodeshift');
28
28
  exports.transformerDirectory = path_1.default.join(__dirname, '../', 'transforms');
29
29
  function checkGitStatus(force) {
@@ -87,12 +87,16 @@ function runTransform({ files, flags, transformer }) {
87
87
  if (!dry && transformer === 'built-in-next-font') {
88
88
  console.log('Uninstalling `@next/font`');
89
89
  try {
90
- (0, uninstall_package_1.uninstallPackage)('@next/font');
90
+ (0, handle_package_1.uninstallPackage)('@next/font');
91
91
  }
92
92
  catch (_a) {
93
93
  console.error("Couldn't uninstall `@next/font`, please uninstall it manually");
94
94
  }
95
95
  }
96
+ if (!dry && transformer === 'next-request-geo-ip') {
97
+ console.log('Installing `@vercel/functions`...');
98
+ (0, handle_package_1.installPackage)('@vercel/functions');
99
+ }
96
100
  }
97
101
  const TRANSFORMER_INQUIRER_CHOICES = [
98
102
  {
@@ -143,6 +147,10 @@ const TRANSFORMER_INQUIRER_CHOICES = [
143
147
  name: 'built-in-next-font: Uninstall `@next/font` and transform imports to `next/font`',
144
148
  value: 'built-in-next-font',
145
149
  },
150
+ {
151
+ name: 'next-request-geo-ip: Install `@vercel/functions` to replace `geo` and `ip` properties on `NextRequest`',
152
+ value: 'next-request-geo-ip',
153
+ },
146
154
  ];
147
155
  function expandFilePathsIfNeeded(filesBeforeExpansion) {
148
156
  const shouldExpandFiles = filesBeforeExpansion.some((file) => file.includes('*'));
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.uninstallPackage = uninstallPackage;
7
+ exports.installPackage = installPackage;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const execa_1 = __importDefault(require("execa"));
@@ -28,4 +29,15 @@ function uninstallPackage(packageToUninstall) {
28
29
  }
29
30
  execa_1.default.sync(pkgManager, [command, packageToUninstall], { stdio: 'inherit' });
30
31
  }
31
- //# sourceMappingURL=uninstall-package.js.map
32
+ function installPackage(packageToInstall) {
33
+ const pkgManager = getPkgManager(process.cwd());
34
+ if (!pkgManager)
35
+ throw new Error('Failed to find package manager');
36
+ try {
37
+ execa_1.default.sync(pkgManager, ['add', packageToInstall], { stdio: 'inherit' });
38
+ }
39
+ catch (error) {
40
+ throw new Error(`Failed to install "${packageToInstall}". Please install it manually.`, { cause: error });
41
+ }
42
+ }
43
+ //# sourceMappingURL=handle-package.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "15.0.0-canary.162",
3
+ "version": "15.0.0-canary.163",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = default_1;
4
+ const GEO = 'geo';
5
+ const IP = 'ip';
6
+ const GEOLOCATION = 'geolocation';
7
+ const IP_ADDRESS = 'ipAddress';
8
+ const GEO_TYPE = 'Geo';
9
+ function default_1(fileInfo, api) {
10
+ const j = api.jscodeshift.withParser('tsx');
11
+ const root = j(fileInfo.source);
12
+ if (!root.length) {
13
+ return fileInfo.source;
14
+ }
15
+ const nextReqType = root
16
+ .find(j.FunctionDeclaration)
17
+ .find(j.Identifier, (id) => {
18
+ var _a;
19
+ if (((_a = id.typeAnnotation) === null || _a === void 0 ? void 0 : _a.type) !== 'TSTypeAnnotation') {
20
+ return false;
21
+ }
22
+ const typeAnn = id.typeAnnotation.typeAnnotation;
23
+ return (typeAnn.type === 'TSTypeReference' &&
24
+ typeAnn.typeName.type === 'Identifier' &&
25
+ typeAnn.typeName.name === 'NextRequest');
26
+ });
27
+ const vercelFuncImports = root.find(j.ImportDeclaration, {
28
+ source: {
29
+ value: '@vercel/functions',
30
+ },
31
+ });
32
+ const vercelFuncImportSpecifiers = vercelFuncImports
33
+ .find(j.ImportSpecifier)
34
+ .nodes();
35
+ const vercelFuncImportNames = new Set(vercelFuncImportSpecifiers.map((node) => node.imported.name));
36
+ const hasGeolocation = vercelFuncImportNames.has(GEOLOCATION);
37
+ const hasIpAddress = vercelFuncImportNames.has(IP_ADDRESS);
38
+ const hasGeoType = vercelFuncImportNames.has(GEO_TYPE);
39
+ let identifierNames = new Set();
40
+ // if all identifiers are already imported, we don't need to create
41
+ // a unique identifier for them, therefore we don't look for all
42
+ // identifier names in the file
43
+ if (!hasGeolocation || !hasIpAddress || !hasGeoType) {
44
+ const allIdentifiers = root.find(j.Identifier).nodes();
45
+ identifierNames = new Set(allIdentifiers.map((node) => node.name));
46
+ }
47
+ let geoIdentifier = hasGeolocation
48
+ ? getExistingIdentifier(vercelFuncImportSpecifiers, GEOLOCATION)
49
+ : getUniqueIdentifier(identifierNames, GEOLOCATION);
50
+ let ipIdentifier = hasIpAddress
51
+ ? getExistingIdentifier(vercelFuncImportSpecifiers, IP_ADDRESS)
52
+ : getUniqueIdentifier(identifierNames, IP_ADDRESS);
53
+ let geoTypeIdentifier = hasGeoType
54
+ ? getExistingIdentifier(vercelFuncImportSpecifiers, GEO_TYPE)
55
+ : getUniqueIdentifier(identifierNames, GEO_TYPE);
56
+ let { needImportGeolocation, needImportIpAddress } = replaceGeoIpValues(j, nextReqType, geoIdentifier, ipIdentifier);
57
+ let { needImportGeoType, hasChangedIpType } = replaceGeoIpTypes(j, root, geoTypeIdentifier);
58
+ let needChanges = needImportGeolocation ||
59
+ needImportIpAddress ||
60
+ needImportGeoType ||
61
+ hasChangedIpType;
62
+ if (!needChanges) {
63
+ return fileInfo.source;
64
+ }
65
+ // Even if there was a change above, if there's an existing import,
66
+ // we don't need to import them again.
67
+ needImportGeolocation = !hasGeolocation && needImportGeolocation;
68
+ needImportIpAddress = !hasIpAddress && needImportIpAddress;
69
+ needImportGeoType = !hasGeoType && needImportGeoType;
70
+ insertImportDeclarations(j, root, vercelFuncImports, needImportGeolocation, needImportIpAddress, needImportGeoType, geoIdentifier, ipIdentifier, geoTypeIdentifier);
71
+ return root.toSource();
72
+ }
73
+ /**
74
+ * Returns an existing identifier from the Vercel functions import declaration.
75
+ */
76
+ function getExistingIdentifier(vercelFuncImportSpecifiers, identifier) {
77
+ var _a;
78
+ const existingIdentifier = vercelFuncImportSpecifiers.find((node) => node.imported.name === identifier);
79
+ return (((_a = existingIdentifier === null || existingIdentifier === void 0 ? void 0 : existingIdentifier.local) === null || _a === void 0 ? void 0 : _a.name) ||
80
+ existingIdentifier.imported.name ||
81
+ identifier);
82
+ }
83
+ /**
84
+ * Returns a unique identifier by adding a suffix to the original identifier
85
+ * if it already exists in the set.
86
+ */
87
+ function getUniqueIdentifier(identifierNames, identifier) {
88
+ let suffix = 1;
89
+ let uniqueIdentifier = identifier;
90
+ while (identifierNames.has(uniqueIdentifier)) {
91
+ uniqueIdentifier = `${identifier}${suffix}`;
92
+ suffix++;
93
+ }
94
+ return uniqueIdentifier;
95
+ }
96
+ /**
97
+ * Replaces accessors `geo` and `ip` of NextRequest with corresponding
98
+ * function calls from `@vercel/functions`.
99
+ *
100
+ * Creates new variable declarations for destructuring assignments.
101
+ */
102
+ function replaceGeoIpValues(j, nextReqType, geoIdentifier, ipIdentifier) {
103
+ let needImportGeolocation = false;
104
+ let needImportIpAddress = false;
105
+ for (const nextReqPath of nextReqType.paths()) {
106
+ const fnPath = nextReqPath.parentPath.parentPath;
107
+ const fn = j(fnPath);
108
+ const blockStatement = fn.find(j.BlockStatement);
109
+ const varDeclarators = fn.find(j.VariableDeclarator);
110
+ // req.geo, req.ip
111
+ const geoAccesses = blockStatement.find(j.MemberExpression, (me) => me.object.type === 'Identifier' &&
112
+ me.object.name === nextReqPath.node.name &&
113
+ me.property.type === 'Identifier' &&
114
+ me.property.name === GEO);
115
+ const ipAccesses = blockStatement.find(j.MemberExpression, (me) => me.object.type === 'Identifier' &&
116
+ me.object.name === nextReqPath.node.name &&
117
+ me.property.type === 'Identifier' &&
118
+ me.property.name === IP);
119
+ // var { geo, ip } = req
120
+ const geoDestructuring = varDeclarators.filter((path) => path.node.id.type === 'ObjectPattern' &&
121
+ path.node.id.properties.some((prop) => prop.type === 'ObjectProperty' &&
122
+ prop.key.type === 'Identifier' &&
123
+ prop.key.name === GEO));
124
+ const ipDestructuring = varDeclarators.filter((path) => path.node.id.type === 'ObjectPattern' &&
125
+ path.node.id.properties.some((prop) => prop.type === 'ObjectProperty' &&
126
+ prop.key.type === 'Identifier' &&
127
+ prop.key.name === IP));
128
+ // geolocation(req), ipAddress(req)
129
+ const geoCall = j.callExpression(j.identifier(geoIdentifier), [
130
+ Object.assign(Object.assign({}, nextReqPath.node), { typeAnnotation: null }),
131
+ ]);
132
+ const ipCall = j.callExpression(j.identifier(ipIdentifier), [
133
+ Object.assign(Object.assign({}, nextReqPath.node), { typeAnnotation: null }),
134
+ ]);
135
+ geoAccesses.replaceWith(geoCall);
136
+ ipAccesses.replaceWith(ipCall);
137
+ /**
138
+ * For each destructuring assignment, we create a new variable
139
+ * declaration and insert it after the current block statement.
140
+ *
141
+ * Before:
142
+ *
143
+ * ```
144
+ * const { buildId, geo, ip } = req;
145
+ * ```
146
+ *
147
+ * After:
148
+ *
149
+ * ```
150
+ * const { buildId } = req;
151
+ * const geo = geolocation(req);
152
+ * const ip = ipAddress(req);
153
+ * ```
154
+ */
155
+ geoDestructuring.forEach((path) => {
156
+ if (path.node.id.type === 'ObjectPattern') {
157
+ const properties = path.node.id.properties;
158
+ const geoProperty = properties.find((prop) => prop.type === 'ObjectProperty' &&
159
+ prop.key.type === 'Identifier' &&
160
+ prop.key.name === GEO);
161
+ const otherProperties = properties.filter((prop) => prop !== geoProperty);
162
+ const geoDeclaration = j.variableDeclaration('const', [
163
+ j.variableDeclarator(j.identifier(
164
+ // Use alias from destructuring (if present) to retain references:
165
+ // const { geo: geoAlias } = req; -> const geoAlias = geolocation(req);
166
+ // This prevents errors from undeclared variables.
167
+ geoProperty.type === 'ObjectProperty' &&
168
+ geoProperty.value.type === 'Identifier'
169
+ ? geoProperty.value.name
170
+ : GEO), geoCall),
171
+ ]);
172
+ path.node.id.properties = otherProperties;
173
+ path.parent.insertAfter(geoDeclaration);
174
+ }
175
+ });
176
+ ipDestructuring.forEach((path) => {
177
+ if (path.node.id.type === 'ObjectPattern') {
178
+ const properties = path.node.id.properties;
179
+ const ipProperty = properties.find((prop) => prop.type === 'ObjectProperty' &&
180
+ prop.key.type === 'Identifier' &&
181
+ prop.key.name === IP);
182
+ const otherProperties = properties.filter((prop) => prop !== ipProperty);
183
+ const ipDeclaration = j.variableDeclaration('const', [
184
+ j.variableDeclarator(j.identifier(
185
+ // Use alias from destructuring (if present) to retain references:
186
+ // const { ip: ipAlias } = req; -> const ipAlias = ipAddress(req);
187
+ // This prevents errors from undeclared variables.
188
+ ipProperty.type === 'ObjectProperty' &&
189
+ ipProperty.value.type === 'Identifier'
190
+ ? ipProperty.value.name
191
+ : IP), ipCall),
192
+ ]);
193
+ path.node.id.properties = otherProperties;
194
+ path.parent.insertAfter(ipDeclaration);
195
+ }
196
+ });
197
+ needImportGeolocation =
198
+ needImportGeolocation ||
199
+ geoAccesses.length > 0 ||
200
+ geoDestructuring.length > 0;
201
+ needImportIpAddress =
202
+ needImportIpAddress || ipAccesses.length > 0 || ipDestructuring.length > 0;
203
+ }
204
+ return {
205
+ needImportGeolocation,
206
+ needImportIpAddress,
207
+ };
208
+ }
209
+ /**
210
+ * Replaces the types of `NextRequest["geo"]` and `NextRequest["ip"]` with
211
+ * corresponding types from `@vercel/functions`.
212
+ */
213
+ function replaceGeoIpTypes(j, root, geoTypeIdentifier) {
214
+ let needImportGeoType = false;
215
+ let hasChangedIpType = false;
216
+ // get the type of NextRequest that has accessed for ip and geo
217
+ // NextRequest['geo'], NextRequest['ip']
218
+ const nextReqGeoType = root.find(j.TSIndexedAccessType, (tsIndexedAccessType) => {
219
+ return (tsIndexedAccessType.objectType.type === 'TSTypeReference' &&
220
+ tsIndexedAccessType.objectType.typeName.type === 'Identifier' &&
221
+ tsIndexedAccessType.objectType.typeName.name === 'NextRequest' &&
222
+ tsIndexedAccessType.indexType.type === 'TSLiteralType' &&
223
+ tsIndexedAccessType.indexType.literal.type === 'StringLiteral' &&
224
+ tsIndexedAccessType.indexType.literal.value === GEO);
225
+ });
226
+ const nextReqIpType = root.find(j.TSIndexedAccessType, (tsIndexedAccessType) => {
227
+ return (tsIndexedAccessType.objectType.type === 'TSTypeReference' &&
228
+ tsIndexedAccessType.objectType.typeName.type === 'Identifier' &&
229
+ tsIndexedAccessType.objectType.typeName.name === 'NextRequest' &&
230
+ tsIndexedAccessType.indexType.type === 'TSLiteralType' &&
231
+ tsIndexedAccessType.indexType.literal.type === 'StringLiteral' &&
232
+ tsIndexedAccessType.indexType.literal.value === IP);
233
+ });
234
+ if (nextReqGeoType.length > 0) {
235
+ needImportGeoType = true;
236
+ // replace with type Geo
237
+ nextReqGeoType.replaceWith(j.identifier(geoTypeIdentifier));
238
+ }
239
+ if (nextReqIpType.length > 0) {
240
+ hasChangedIpType = true;
241
+ // replace with type string | undefined
242
+ nextReqIpType.replaceWith(j.tsUnionType([j.tsStringKeyword(), j.tsUndefinedKeyword()]));
243
+ }
244
+ return {
245
+ needImportGeoType,
246
+ hasChangedIpType,
247
+ };
248
+ }
249
+ /**
250
+ * Inserts import declarations from `"@vercel/functions"`.
251
+ *
252
+ * For the `Geo` type that needs to be imported to replace `NextRequest["geo"]`,
253
+ * if it is the only import needed, we import it as a type.
254
+ *
255
+ * ```ts
256
+ * import type { Geo } from "@vercel/functions";
257
+ * ```
258
+ *
259
+ * Otherwise, we import it as an inline type import.
260
+ *
261
+ * ```ts
262
+ * import { type Geo, ... } from "@vercel/functions";
263
+ * ```
264
+ */
265
+ function insertImportDeclarations(j, root, vercelFuncImports, needImportGeolocation, needImportIpAddress, needImportGeoType, geoIdentifier, ipIdentifier, geoTypeIdentifier) {
266
+ // No need inserting import.
267
+ if (!needImportGeolocation && !needImportIpAddress && !needImportGeoType) {
268
+ return;
269
+ }
270
+ // As we run this only when `NextRequest` is imported, there should be
271
+ // a "next/server" import.
272
+ const firstNextServerImport = root
273
+ .find(j.ImportDeclaration, { source: { value: 'next/server' } })
274
+ .at(0);
275
+ const firstVercelFuncImport = vercelFuncImports.at(0);
276
+ const hasVercelFuncImport = firstVercelFuncImport.length > 0;
277
+ // If there's no "@vercel/functions" import, and we only need to import
278
+ // `Geo` type, we create a type import to avoid side effect with
279
+ // `verbatimModuleSyntax`.
280
+ // x-ref: https://typescript-eslint.io/rules/no-import-type-side-effects
281
+ if (!hasVercelFuncImport &&
282
+ !needImportGeolocation &&
283
+ !needImportIpAddress &&
284
+ needImportGeoType) {
285
+ const geoTypeImportDeclaration = j.importDeclaration([
286
+ needImportGeoType
287
+ ? j.importSpecifier(j.identifier(GEO_TYPE), j.identifier(geoTypeIdentifier))
288
+ : null,
289
+ ].filter(Boolean), j.literal('@vercel/functions'),
290
+ // import type { Geo } ...
291
+ 'type');
292
+ firstNextServerImport.insertAfter(geoTypeImportDeclaration);
293
+ return;
294
+ }
295
+ const importDeclaration = j.importDeclaration([
296
+ // If there was a duplicate identifier, we add an
297
+ // incremental number suffix to it and we use alias:
298
+ // `import { geolocation as geolocation1 } from ...`
299
+ needImportGeolocation
300
+ ? j.importSpecifier(j.identifier(GEOLOCATION), j.identifier(geoIdentifier))
301
+ : null,
302
+ needImportIpAddress
303
+ ? j.importSpecifier(j.identifier(IP_ADDRESS), j.identifier(ipIdentifier))
304
+ : null,
305
+ needImportGeoType
306
+ ? j.importSpecifier(j.identifier(GEO_TYPE), j.identifier(geoTypeIdentifier))
307
+ : null,
308
+ ].filter(Boolean), j.literal('@vercel/functions'));
309
+ if (hasVercelFuncImport) {
310
+ firstVercelFuncImport
311
+ .get()
312
+ .node.specifiers.push(...importDeclaration.specifiers);
313
+ if (needImportGeoType) {
314
+ const targetGeo = firstVercelFuncImport
315
+ .get()
316
+ .node.specifiers.find((specifier) => specifier.imported.name === GEO_TYPE);
317
+ if (targetGeo) {
318
+ targetGeo.importKind = 'type';
319
+ }
320
+ }
321
+ }
322
+ else {
323
+ if (needImportGeoType) {
324
+ const targetGeo = importDeclaration.specifiers.find((specifier) => specifier.type === 'ImportSpecifier' &&
325
+ specifier.imported.name === GEO_TYPE);
326
+ if (targetGeo) {
327
+ // @ts-expect-error -- Missing types in jscodeshift.
328
+ targetGeo.importKind = 'type';
329
+ }
330
+ }
331
+ firstNextServerImport.insertAfter(importDeclaration);
332
+ }
333
+ }
334
+ //# sourceMappingURL=next-request-geo-ip.js.map