-
-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathupdate-route-types.ts
More file actions
391 lines (328 loc) · 11.1 KB
/
update-route-types.ts
File metadata and controls
391 lines (328 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
import { promises as fs } from "node:fs";
import path from "node:path";
import createDebug from "debug";
import { toForwardSlashPath } from "../util/forward-slash-path.js";
import {
OperationTypeCoder,
type SecurityScheme,
} from "../typescript-generator/operation-type-coder.js";
import { Specification } from "../typescript-generator/specification.js";
const debug = createDebug("counterfact:migrate:update-route-types");
const HTTP_METHODS = [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"OPTIONS",
] as const;
type HttpMethod = (typeof HTTP_METHODS)[number];
// Pre-compile regex patterns derived from HTTP_METHODS
const HTTP_METHOD_ALTERNATION = HTTP_METHODS.join("|");
// eslint-disable-next-line security/detect-non-literal-regexp
const NEEDS_MIGRATION_REGEX = new RegExp(
`import\\s+type\\s+\\{[^}]*HTTP_(?:${HTTP_METHOD_ALTERNATION})[^}]*\\}`,
"iu",
);
// eslint-disable-next-line security/detect-non-literal-regexp
const HTTP_TYPE_NAME_REGEX = new RegExp(
`^HTTP_(?<method>${HTTP_METHOD_ALTERNATION})$`,
"u",
);
// Pre-build import/export replacement patterns for each HTTP method type name
const IMPORT_REPLACE_PATTERNS = new Map(
HTTP_METHODS.map((method) => [
`HTTP_${method}`,
// eslint-disable-next-line security/detect-non-literal-regexp
new RegExp(
`(import\\s+type\\s+\\{[^}]*\\b)HTTP_${method}(\\b[^}]*\\}\\s+from)`,
"g",
),
]),
);
const EXPORT_REPLACE_PATTERNS = new Map(
HTTP_METHODS.map((method) => [
`HTTP_${method}`,
// eslint-disable-next-line security/detect-non-literal-regexp
new RegExp(
`(export\\s+const\\s+${method}\\s*:\\s*)HTTP_${method}(\\b)`,
"g",
),
]),
);
/**
* Converts an OpenAPI path to a file system path
* e.g., "/hello/{name}" -> "hello/{name}"
*/
function openApiPathToFilePath(openApiPath: string): string {
if (openApiPath === "/") {
return "index";
}
return openApiPath.startsWith("/") ? openApiPath.slice(1) : openApiPath;
}
/**
* Builds a mapping of route file paths to their operation type names per method
* @param specification - The OpenAPI specification
* @returns Map of filePath -> Map of method -> typeName
*/
async function buildTypeNameMapping(
specification: Specification,
): Promise<Map<string, Map<string, string>>> {
debug("building type name mapping from specification");
const mapping = new Map<string, Map<string, string>>();
try {
const paths = specification.getRequirement("#/paths");
if (!paths) {
debug("no paths found in specification");
return mapping;
}
const securityRequirement = specification.getRequirement(
"#/components/securitySchemes",
);
const securitySchemes = Object.values(
(securityRequirement?.data as Record<string, unknown>) ?? {},
) as SecurityScheme[];
paths.forEach((pathDefinition, openApiPath: string) => {
const filePath = openApiPathToFilePath(openApiPath);
const methodMap = new Map<string, string>();
pathDefinition.forEach((operation, requestMethod: string) => {
// Skip if not a standard HTTP method
if (!HTTP_METHODS.includes(requestMethod.toUpperCase() as HttpMethod)) {
return;
}
// Create the type coder to get the correct type name
const typeCoder = new OperationTypeCoder(
operation,
requestMethod,
securitySchemes,
);
// Get the type name (first from the names generator)
const typeName = typeCoder.names().next().value as string;
methodMap.set(requestMethod.toUpperCase(), typeName);
debug(
"mapped %s %s -> %s",
requestMethod.toUpperCase(),
openApiPath,
typeName,
);
});
if (methodMap.size > 0) {
mapping.set(filePath, methodMap);
}
});
debug("built mapping for %d routes", mapping.size);
} catch (error) {
debug("error building type name mapping: %o", error);
throw error;
}
return mapping;
}
/**
* Checks if a route file needs migration by looking for old-style HTTP_ imports
* @param content - The file content
*/
function needsMigration(content: string): boolean {
return NEEDS_MIGRATION_REGEX.test(content);
}
/**
* Updates a single route file with the correct type names
* @param filePath - Absolute path to the route file
* @param methodToTypeName - Map of HTTP method to type name
* @returns True if file was updated
*/
async function updateRouteFile(
filePath: string,
methodToTypeName: Map<string, string>,
): Promise<boolean> {
debug("processing route file: %s", filePath);
let content = await fs.readFile(filePath, "utf8");
// Check if migration is needed
if (!needsMigration(content)) {
debug("file does not need migration: %s", filePath);
return false;
}
let modified = false;
// Build a map of old type names to new type names found in this file
const replacements = new Map<string, string>();
// Find all import statements with HTTP_ patterns
const importRegex =
/import\s+type\s+\{(?<types>[^}]+)\}\s+from\s+["'][^"']+["'];?/gu;
let importMatch;
while ((importMatch = importRegex.exec(content)) !== null) {
const importedTypes = (importMatch.groups?.["types"] ?? "")
.split(",")
.map((t) => t.trim())
.filter((t) => t.length > 0);
for (const importedType of importedTypes) {
// Check if this is an HTTP_ type
const httpMethodMatch = importedType.match(HTTP_TYPE_NAME_REGEX);
if (httpMethodMatch) {
const method = httpMethodMatch.groups?.["method"] ?? "";
const newTypeName = methodToTypeName.get(method);
if (newTypeName && newTypeName !== importedType) {
replacements.set(importedType, newTypeName);
debug("will replace %s with %s", importedType, newTypeName);
}
}
}
}
if (replacements.size === 0) {
debug("no replacements needed for: %s", filePath);
return false;
}
// Apply replacements
for (const [oldName, newName] of replacements.entries()) {
// Replace in import statement
const importPattern = IMPORT_REPLACE_PATTERNS.get(oldName);
if (importPattern) {
importPattern.lastIndex = 0;
content = content.replace(importPattern, `$1${newName}$2`);
}
// Replace in export statement (e.g., "export const GET: HTTP_GET")
// Match the method from the old type name
const methodMatch = oldName.match(HTTP_TYPE_NAME_REGEX);
if (methodMatch) {
const exportPattern = EXPORT_REPLACE_PATTERNS.get(oldName);
if (exportPattern) {
exportPattern.lastIndex = 0;
content = content.replace(exportPattern, `$1${newName}$2`);
}
}
modified = true;
}
if (modified) {
await fs.writeFile(filePath, content, "utf8");
debug("updated file: %s", filePath);
}
return modified;
}
/**
* Recursively processes route files in a directory
* @param routesDir - Path to routes directory
* @param currentPath - Current subdirectory being processed
* @param mapping - Type name mapping
* @returns Number of files updated
*/
async function processRouteDirectory(
routesDir: string,
currentPath: string,
mapping: Map<string, Map<string, string>>,
): Promise<number> {
let updatedCount = 0;
try {
const entries = await fs.readdir(path.join(routesDir, currentPath), {
withFileTypes: true,
});
for (const entry of entries) {
const relativePath = path.join(currentPath, entry.name);
const absolutePath = path.join(routesDir, relativePath);
if (entry.isDirectory()) {
// Recursively process subdirectories
updatedCount += await processRouteDirectory(
routesDir,
relativePath,
mapping,
);
} else if (entry.name.endsWith(".ts") && entry.name !== "_.context.ts") {
// Process TypeScript route files (skip context files)
const routePath = toForwardSlashPath(relativePath.replace(/\.ts$/, ""));
const methodMap = mapping.get(routePath);
if (methodMap) {
const wasUpdated = await updateRouteFile(absolutePath, methodMap);
if (wasUpdated) {
updatedCount++;
}
}
}
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
debug("error processing directory %s: %o", currentPath, error);
}
}
return updatedCount;
}
/**
* Checks if any route files need migration
* @param routesDir - Path to routes directory
*/
async function checkIfMigrationNeeded(routesDir: string): Promise<boolean> {
try {
const entries = await fs.readdir(routesDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.endsWith(".ts") && entry.name !== "_.context.ts") {
const content = await fs.readFile(
path.join(routesDir, entry.name),
"utf8",
);
if (needsMigration(content)) {
return true;
}
} else if (entry.isDirectory() && entry.name !== "node_modules") {
// Recursively check subdirectories
const subDirPath = path.join(routesDir, entry.name);
const found = await checkIfMigrationNeeded(subDirPath);
if (found) {
return true;
}
}
}
} catch (error) {
debug("error checking for migration need: %o", error);
}
return false;
}
/**
* Main migration function - updates route type imports to use new naming convention
* @param basePath - Base path where routes and types are located
* @param openApiPath - Path or URL to OpenAPI specification
* @returns True if migration was performed
*/
export async function updateRouteTypes(
basePath: string,
openApiPath: string,
): Promise<boolean> {
debug("starting route type migration for base path: %s", basePath);
// Skip if running without OpenAPI spec
if (openApiPath === "_") {
debug("skipping migration - no OpenAPI spec provided");
return false;
}
const routesDir = path.join(basePath, "routes");
try {
// Check if routes directory exists
await fs.access(routesDir);
} catch {
debug("routes directory does not exist: %s", routesDir);
return false;
}
// Quick check if migration is needed
const migrationNeeded = await checkIfMigrationNeeded(routesDir);
if (!migrationNeeded) {
debug("no migration needed - no old-style HTTP_ imports found");
return false;
}
try {
// Load the OpenAPI specification
debug("loading OpenAPI specification from: %s", openApiPath);
const specification = await Specification.fromFile(openApiPath);
// Build the mapping of paths to type names
const mapping = await buildTypeNameMapping(specification);
if (mapping.size === 0) {
debug("no routes found in specification");
return false;
}
// Process all route files
debug("processing route files in: %s", routesDir);
const updatedCount = await processRouteDirectory(routesDir, "", mapping);
debug("migration complete - updated %d files", updatedCount);
return updatedCount > 0;
} catch (error) {
debug("error during migration: %o", error);
process.stderr.write(
`Warning: Could not migrate route types: ${(error as Error).message}\n`,
);
return false;
}
}