1#![allow(clippy::all)]
2use swc_common::{FileName, SourceMap, SourceMapper, Spanned, sync::Lrc};
3use swc_ecma_ast::*;
4use swc_ecma_parser::{Parser, StringInput, Syntax, lexer::Lexer};
5use swc_ecma_visit::{Visit, VisitWith};
6
7#[derive(Debug, Clone, Default)]
8pub struct TransformStats {
9 pub express_init_count: usize,
10 pub route_count: usize,
11 pub router_count: usize,
12 pub async_handlers: usize,
13 pub middleware_count: usize,
14 pub res_json_calls: usize,
15 pub res_send_calls: usize,
16}
17
18#[derive(Clone)]
19#[allow(dead_code)]
20enum TransformChange {
21 ExpressInit,
22 RouterInit,
23 ImportMigration { _old: String, new: String },
24 BodyParserPlugin,
25 Replacement { original: String, replacement: String },
26}
27
28impl TransformChange {
29 fn apply(&self, source: &str) -> String {
30 match self {
31 Self::ExpressInit => source
32 .replace("express()", "fastify()")
33 .replace("require('express')", "require('fastify')")
34 .replace(" from 'express'", " from 'fastify'"),
35 Self::RouterInit => source.replace("Router()", "Router()"),
36 Self::ImportMigration { new, .. } => {
37 source.replace(" from 'express'", &format!(" from '{}'", new))
38 }
39 Self::BodyParserPlugin => source
40 .replace("bodyParser.json()", "// TODO: register @fastify/json")
41 .replace("body-parser", "// TODO: @fastify/json"),
42 Self::Replacement { original, replacement } => source.replace(original, replacement),
43 }
44 }
45}
46
47pub struct ExpressToFastifyTransform {
48 warnings: Vec<String>,
49 unsupported: Vec<String>,
50 stats: TransformStats,
51}
52
53impl ExpressToFastifyTransform {
54 pub fn new() -> Self {
55 Self {
56 warnings: Vec::new(),
57 unsupported: Vec::new(),
58 stats: TransformStats::default(),
59 }
60 }
61
62 pub fn transform_source(&mut self, source: &str, path: &std::path::Path) -> TransformOutcome {
63 let cm: Lrc<SourceMap> = Lrc::new(SourceMap::default());
64 let fm = cm.new_source_file(
65 FileName::Real(path.to_path_buf()).into(),
66 source.to_string(),
67 );
68
69 let syntax = if path.to_string_lossy().ends_with(".ts")
70 || path.to_string_lossy().ends_with(".tsx")
71 {
72 Syntax::Typescript(Default::default())
73 } else {
74 Syntax::Es(Default::default())
75 };
76
77 let lexer = Lexer::new(syntax, Default::default(), StringInput::from(&*fm), None);
78 let mut parser = Parser::new_from(lexer);
79 let module = match parser.parse_module() {
80 Ok(m) => m,
81 Err(_) => return TransformOutcome::unsupported("Parse error".to_string()),
82 };
83
84 let mut visitor = TransformVisitor::new(cm.clone());
85 visitor.visit_module(&module);
86
87 let changes = visitor.changes.clone();
88 let changes_empty = changes.is_empty();
89 self.stats = visitor.stats.clone();
90 self.warnings = visitor.warnings.clone();
91 self.unsupported = visitor.unsupported.clone();
92
93 let mut output = source.to_string();
94 for change in changes {
95 output = change.apply(&output);
96 }
97 output = output.replace(";;", ";");
98
99 TransformOutcome {
100 transformed: output,
101 changed: !changes_empty,
102 warnings: self.warnings.clone(),
103 unsupported: self.unsupported.clone(),
104 stats: self.stats.clone(),
105 }
106 }
107}
108
109impl Default for ExpressToFastifyTransform {
110 fn default() -> Self {
111 Self::new()
112 }
113}
114
115struct TransformVisitor {
116 changes: Vec<TransformChange>,
117 warnings: Vec<String>,
118 unsupported: Vec<String>,
119 stats: TransformStats,
120 cm: Lrc<SourceMap>,
121 processed_spans: Vec<swc_common::Span>,
122}
123
124struct RouteChain {
125 base_obj: String,
126 path: String,
127 calls: Vec<(String, Vec<String>)>,
128 _span: swc_common::Span,
129}
130
131fn extract_route_chain(call: &CallExpr, cm: &SourceMap) -> Option<RouteChain> {
132 let mut current_expr = Expr::Call(call.clone());
133 let mut calls = Vec::new();
134 let mut path = None;
135 let mut base_obj = None;
136
137 loop {
138 let temp_expr = current_expr.clone();
139 match temp_expr {
140 Expr::Call(c) => {
141 let mut is_route_method = false;
142 if let Callee::Expr(callee_expr) = &c.callee {
143 if let Expr::Member(MemberExpr { obj, prop, .. }) = callee_expr.as_ref() {
144 if let MemberProp::Ident(method_ident) = prop {
145 let method_name = method_ident.sym.as_ref().to_lowercase();
146 if ["get", "post", "put", "delete", "patch", "head", "options"].contains(&method_name.as_str()) {
147 is_route_method = true;
148 let args_list: Vec<String> = c.args.iter().map(|arg| {
149 cm.span_to_snippet(arg.span()).unwrap_or_default()
150 }).collect();
151
152 calls.push((method_name, args_list));
153 current_expr = *obj.clone();
154 }
155 }
156 }
157 }
158
159 if !is_route_method {
160 if let Callee::Expr(callee_expr) = &c.callee {
161 if let Expr::Member(MemberExpr { obj, prop, .. }) = callee_expr.as_ref() {
162 if let MemberProp::Ident(prop_ident) = prop {
163 if prop_ident.sym.as_ref() == "route" {
164 if let Expr::Ident(obj_ident) = obj.as_ref() {
165 base_obj = Some(obj_ident.sym.as_ref().to_string());
166 }
167 if let Some(first_arg) = c.args.first() {
168 path = Some(cm.span_to_snippet(first_arg.span()).unwrap_or_default());
169 }
170 }
171 }
172 }
173 }
174 break;
175 }
176 }
177 _ => break,
178 }
179 }
180
181 if let (Some(base), Some(p), false) = (base_obj, path, calls.is_empty()) {
182 calls.reverse();
183 Some(RouteChain {
184 base_obj: base,
185 path: p,
186 calls,
187 _span: call.span(),
188 })
189 } else {
190 None
191 }
192}
193
194fn migrate_handler(handler_src: &str) -> String {
195 let mut result = handler_src.to_string();
196
197 result = result.replace("(req, res, next)", "(req, reply, next)");
199 result = result.replace("(request, response, next)", "(request, reply, next)");
200 result = result.replace("(req, res)", "(req, reply)");
201 result = result.replace("(request, response)", "(request, reply)");
202 result = result.replace("async (req, res, next)", "async (req, reply, next)");
203 result = result.replace("async (request, response, next)", "async (request, reply, next)");
204 result = result.replace("async (req, res)", "async (req, reply)");
205 result = result.replace("async (request, response)", "async (request, reply)");
206
207 result = result.replace("res.status(", "reply.status(");
209 result = result.replace("response.status(", "reply.status(");
210 result = result.replace("res.send(", "reply.send(");
211 result = result.replace("response.send(", "reply.send(");
212 result = result.replace("res.json(", "reply.send(");
213 result = result.replace("response.json(", "reply.send(");
214 result = result.replace("res.sendStatus(", "reply.status(");
215 result = result.replace(".json(", ".send(");
216
217 result = result.replace("res.", "reply.");
219 result = result.replace("response.", "reply.");
220
221 let mut final_res = String::new();
224 let mut remaining = result.as_str();
225 while let Some(idx) = remaining.find("req.param(") {
226 final_res.push_str(&remaining[..idx]);
227 let rest = &remaining[idx + 10..];
228 if let Some(end_idx) = rest.find(')') {
229 let param_arg = rest[..end_idx].trim();
230 let clean_name = param_arg.trim_matches(|c| c == '\'' || c == '"');
231 final_res.push_str(&format!("req.params.{}", clean_name));
232 remaining = &rest[end_idx + 1..];
233 } else {
234 final_res.push_str("req.param(");
235 remaining = rest;
236 }
237 }
238 final_res.push_str(remaining);
239 result = final_res;
240
241 let mut final_res_req = String::new();
242 let mut remaining_req = result.as_str();
243 while let Some(idx) = remaining_req.find("request.param(") {
244 final_res_req.push_str(&remaining_req[..idx]);
245 let rest = &remaining_req[idx + 14..];
246 if let Some(end_idx) = rest.find(')') {
247 let param_arg = rest[..end_idx].trim();
248 let clean_name = param_arg.trim_matches(|c| c == '\'' || c == '"');
249 final_res_req.push_str(&format!("request.params.{}", clean_name));
250 remaining_req = &rest[end_idx + 1..];
251 } else {
252 final_res_req.push_str("request.param(");
253 remaining_req = rest;
254 }
255 }
256 final_res_req.push_str(remaining_req);
257 result = final_res_req;
258
259 result
260}
261
262fn migrate_route_call(
263 base: &str,
264 method: &str,
265 path: &str,
266 args: &[String],
267) -> String {
268 if args.is_empty() {
269 return format!("{}.{}({})", base, method, path);
270 }
271
272 if args.len() == 1 {
273 let migrated_handler = migrate_handler(&args[0]);
274 return format!("{}.{}({}, {})", base, method, path, migrated_handler);
275 }
276
277 let middlewares = &args[0..args.len() - 1];
278 let handler = &args[args.len() - 1];
279 let migrated_handler = migrate_handler(handler);
280
281 format!(
282 "{}.{}({}, {{ preHandler: [{}] }}, {})",
283 base,
284 method,
285 path,
286 middlewares.join(", "),
287 migrated_handler
288 )
289}
290
291fn migrate_use_call(
292 base: &str,
293 args: &[String],
294) -> String {
295 if args.is_empty() {
296 return format!("{}.register()", base);
297 }
298
299 if args.len() == 1 {
300 let mw = &args[0];
301 let mw_ref = if mw.ends_with("()") {
302 &mw[..mw.len() - 2]
303 } else {
304 mw
305 };
306 return format!("{}.register({})", base, mw_ref);
307 }
308
309 let path = &args[0];
310 let last_arg = &args[args.len() - 1];
311 let middlewares = &args[1..args.len() - 1];
312
313 if middlewares.is_empty() {
314 return format!("{}.register({}, {{ prefix: {} }})", base, last_arg, path);
315 }
316
317 let mw_refs: Vec<String> = middlewares.iter().map(|mw| {
318 if mw.ends_with("()") {
319 mw[..mw.len() - 2].to_string()
320 } else {
321 mw.clone()
322 }
323 }).collect();
324
325 format!(
326 "{}.register({}, {{ prefix: {}, preHandler: [{}] }})",
327 base,
328 last_arg,
329 path,
330 mw_refs.join(", ")
331 )
332}
333
334impl TransformVisitor {
335 fn new(cm: Lrc<SourceMap>) -> Self {
336 Self {
337 changes: Vec::new(),
338 warnings: Vec::new(),
339 unsupported: Vec::new(),
340 stats: TransformStats::default(),
341 cm,
342 processed_spans: Vec::new(),
343 }
344 }
345
346 fn add_express_init(&mut self) {
347 if !self
348 .changes
349 .iter()
350 .any(|c| matches!(c, TransformChange::ExpressInit))
351 {
352 self.changes.push(TransformChange::ExpressInit);
353 self.stats.express_init_count += 1;
354 }
355 }
356
357 fn check_express_helpers(&mut self, method_name: &str) {
358 match method_name {
359 "download" => {
360 self.warnings.push("res.download() is Express-specific. Use reply.send() or @fastify/static instead.".to_string());
361 }
362 "sendFile" => {
363 self.warnings.push("res.sendFile() is Express-specific. Use @fastify/static to serve static files.".to_string());
364 }
365 "render" => {
366 self.warnings.push("res.render() is Express-specific. Use @fastify/view to render templates.".to_string());
367 }
368 "redirect" => {
369 self.warnings.push("res.redirect() is Express-specific. Use reply.redirect() instead.".to_string());
370 }
371 "cookie" => {
372 self.warnings.push("res.cookie() is Express-specific. Use @fastify/cookie to set cookies.".to_string());
373 }
374 "clearCookie" => {
375 self.warnings.push("res.clearCookie() is Express-specific. Use @fastify/cookie to clear cookies.".to_string());
376 }
377 "attachment" => {
378 self.warnings.push("res.attachment() is Express-specific. Use custom reply headers instead.".to_string());
379 }
380 "format" => {
381 self.warnings.push("res.format() is Express-specific. Perform content negotiation manually in Fastify.".to_string());
382 }
383 "links" => {
384 self.warnings.push("res.links() is Express-specific. Set the Link header manually.".to_string());
385 }
386 "location" => {
387 self.warnings.push("res.location() is Express-specific. Use reply.redirect() or set Location header manually.".to_string());
388 }
389 "vary" => {
390 self.warnings.push("res.vary() is Express-specific. Use reply.header('Vary', ...) instead.".to_string());
391 }
392 "type" | "contentType" => {
393 self.warnings.push("res.type() is Express-specific. Use reply.type() instead.".to_string());
394 }
395 "append" => {
396 self.warnings.push("res.append() is Express-specific. Use reply.header() instead.".to_string());
397 }
398 "set" | "header" => {
399 self.warnings.push("res.set() / res.header() is Express-specific. Use reply.header() instead.".to_string());
400 }
401 "get" => {
402 self.warnings.push("res.get() is Express-specific. Use reply.getHeader() instead.".to_string());
403 }
404 "locals" => {
405 self.warnings.push("res.locals is Express-specific. Use request/reply decorators instead.".to_string());
406 }
407 "write" => {
408 self.warnings.push("res.write() is Express-specific low-level stream. Fastify handles streams by returning them from the handler.".to_string());
409 }
410 "end" => {
411 self.warnings.push("res.end() is Express-specific low-level stream. Use reply.send() instead.".to_string());
412 }
413 _ => {}
414 }
415 }
416
417 fn check_middleware_warnings(&mut self, middleware_src: &str) {
418 let lower = middleware_src.to_lowercase();
419 if lower.contains("passport") {
420 self.unsupported.push("passport - use @fastify/passport instead".to_string());
421 } else if lower.contains("session") {
422 self.warnings.push("session - use @fastify/session instead".to_string());
423 } else if lower.contains("multer") {
424 self.unsupported.push("multer - use @fastify/multipart instead".to_string());
425 } else if lower.contains("csrf") {
426 self.warnings.push("csrf - use @fastify/csrf-protection instead".to_string());
427 } else if lower.contains("helmet") {
428 self.warnings.push("helmet - use @fastify/helmet instead".to_string());
429 } else if lower.contains("cors") {
430 self.warnings.push("cors - use @fastify/cors instead".to_string());
431 } else if lower.contains("morgan") {
432 self.warnings.push("morgan - use fastify's built-in logger instead".to_string());
433 } else if lower.contains("cookieparser") || lower.contains("cookie-parser") {
434 self.warnings.push("cookie-parser - use @fastify/cookie instead".to_string());
435 } else if lower.contains("compression") {
436 self.warnings.push("compression - use @fastify/compress instead".to_string());
437 } else if lower.contains("body-parser") || lower.contains("bodyparser") {
438 self.warnings.push("body-parser - Fastify parses JSON bodies by default. For URL-encoded forms, use @fastify/formbody.".to_string());
439 } else if lower.contains("cookie-session") || lower.contains("cookiesession") {
440 self.warnings.push("cookie-session - use @fastify/session or @fastify/secure-session instead.".to_string());
441 } else if lower.contains("express-validator") || lower.contains("expressvalidator") {
442 self.warnings.push("express-validator - use fastify's built-in schema validation (AJV) instead.".to_string());
443 } else if lower.contains("serve-static") || lower.contains("servestatic") {
444 self.warnings.push("serve-static - use @fastify/static instead.".to_string());
445 } else if lower.contains("method-override") || lower.contains("methodoverride") {
446 self.warnings.push("method-override - use @fastify/method-override instead.".to_string());
447 } else if lower.contains("connect-flash") || lower.contains("flash") {
448 self.warnings.push("connect-flash - use @fastify/flash instead.".to_string());
449 }
450 }
451}
452
453impl Visit for TransformVisitor {
454 fn visit_import_decl(&mut self, import: &ImportDecl) {
455 let src_str = import.src.value.to_string();
456 if src_str.contains("express") && !src_str.contains("fastify") {
457 if src_str.contains("socket.io") || src_str.contains("ws") {
458 self.unsupported
459 .push("socket.io/ws - manual migration required".to_string());
460 } else {
461 self.changes.push(TransformChange::ImportMigration {
462 _old: src_str.clone(),
463 new: "fastify".to_string(),
464 });
465 self.add_express_init();
466 }
467 }
468 }
469
470 fn visit_call_expr(&mut self, call: &CallExpr) {
471 if let Some(chain) = extract_route_chain(call, &self.cm) {
472 let span = call.span();
473 if !self.processed_spans.iter().any(|ps| ps.contains(span)) {
474 self.processed_spans.push(span);
475
476 let mut replacement = String::new();
477 for (method, args) in &chain.calls {
478 let migrated_call = migrate_route_call(&chain.base_obj, method, &chain.path, args);
479 replacement.push_str(&migrated_call);
480 replacement.push_str(";\n");
481 }
482
483 let original = self.cm.span_to_snippet(span).unwrap_or_default();
484 self.changes.push(TransformChange::Replacement {
485 original,
486 replacement,
487 });
488 self.stats.route_count += chain.calls.len();
489
490 for (_method, args) in &chain.calls {
491 if args.iter().any(|arg| arg.contains("async ")) {
492 self.stats.async_handlers += 1;
493 }
494 }
495 }
496 call.visit_children_with(self);
497 return;
498 }
499
500 if let Callee::Expr(expr) = &call.callee {
501 match expr.as_ref() {
502 Expr::Ident(i) if i.sym.as_ref() == "express" => {
503 self.add_express_init();
504 }
505 Expr::Member(MemberExpr { obj, prop, .. }) => {
506 if let Expr::Ident(i) = obj.as_ref() {
507 let obj_name = i.sym.as_ref();
508 if let MemberProp::Ident(p) = prop {
509 let method = p.sym.as_ref();
510 if method == "Router" && (obj_name == "express" || obj_name == "Router")
511 {
512 self.changes.push(TransformChange::RouterInit);
513 self.stats.router_count += 1;
514 } else if ["get", "post", "put", "delete", "patch", "head", "options"]
515 .contains(&method)
516 {
517 if obj_name == "app" || obj_name == "router" || obj_name == "server" || obj_name == "api" {
518 let span = call.span();
519 if !self.processed_spans.iter().any(|ps| ps.contains(span)) {
520 self.processed_spans.push(span);
521
522 let original = self.cm.span_to_snippet(span).unwrap_or_default();
523 let args: Vec<String> = call.args.iter().map(|arg| {
524 self.cm.span_to_snippet(arg.span()).unwrap_or_default()
525 }).collect();
526
527 if !args.is_empty() {
528 let path = &args[0];
529 let rest_args = &args[1..];
530 let replacement = migrate_route_call(obj_name, method, path, rest_args);
531
532 self.changes.push(TransformChange::Replacement {
533 original,
534 replacement,
535 });
536 self.stats.route_count += 1;
537
538 if let Some(last) = call.args.last() {
539 if self.is_async(&last.expr) {
540 self.stats.async_handlers += 1;
541 }
542 }
543 }
544 }
545 }
546 } else if method == "use" && (obj_name == "app" || obj_name == "router" || obj_name == "server" || obj_name == "api")
547 {
548 let span = call.span();
549 if !self.processed_spans.iter().any(|ps| ps.contains(span)) {
550 self.processed_spans.push(span);
551
552 let original = self.cm.span_to_snippet(span).unwrap_or_default();
553 let args: Vec<String> = call.args.iter().map(|arg| {
554 self.cm.span_to_snippet(arg.span()).unwrap_or_default()
555 }).collect();
556
557 for arg in &args {
558 self.check_middleware_warnings(arg);
559 }
560
561 let replacement = migrate_use_call(obj_name, &args);
562 self.changes.push(TransformChange::Replacement {
563 original,
564 replacement,
565 });
566 self.stats.middleware_count += 1;
567 }
568 }
569 }
570 }
571 }
572 _ => {}
573 }
574 }
575 call.visit_children_with(self);
576 }
577
578 fn visit_member_expr(&mut self, expr: &MemberExpr) {
579 if let MemberProp::Ident(p) = &expr.prop {
580 let prop = p.sym.as_ref();
581 if let Expr::Ident(i) = expr.obj.as_ref() {
582 let obj_name = i.sym.as_ref();
583 if obj_name == "res" || obj_name == "response" {
584 if prop == "json" {
585 self.stats.res_json_calls += 1;
586 } else if prop == "send" {
587 self.stats.res_send_calls += 1;
588 }
589 self.check_express_helpers(prop);
590 } else if obj_name == "req" || obj_name == "request" {
591 if prop == "path" {
592 self.warnings.push("req.path is Express-specific. Use req.routerPath or req.url instead.".to_string());
593 } else if prop == "xhr" {
594 self.warnings.push("req.xhr is Express-specific. Check req.headers['x-requested-with'] instead.".to_string());
595 }
596 }
597 }
598 }
599 expr.visit_children_with(self);
600 }
601
602 fn visit_assign_expr(&mut self, expr: &AssignExpr) {
603 if let AssignTarget::Simple(simple) = &expr.left {
604 if let SimpleAssignTarget::Member(member) = simple {
605 if let Expr::Ident(i) = member.obj.as_ref() {
606 let name = i.sym.as_ref();
607 if name == "req" || name == "request" || name == "res" || name == "response" {
608 if let MemberProp::Ident(p) = &member.prop {
609 let prop = p.sym.as_ref();
610 if !["session", "user", "body", "query", "params", "headers"].contains(&prop) {
611 self.warnings.push(format!(
612 "Unsafe route mutation: assigning directly to {}.{} is discouraged in Fastify. Use decorators instead.",
613 name, prop
614 ));
615 }
616 }
617 }
618 }
619 }
620 }
621 expr.visit_children_with(self);
622 }
623}
624
625impl TransformVisitor {
626 fn is_async(&self, expr: &Expr) -> bool {
627 if let Expr::Arrow(arrow) = expr {
628 return arrow.is_async;
629 }
630 if let Expr::Fn(f) = expr {
631 return f.function.is_async;
632 }
633 false
634 }
635}
636
637#[derive(Debug, Clone)]
638pub struct TransformOutcome {
639 pub transformed: String,
640 pub changed: bool,
641 pub warnings: Vec<String>,
642 pub unsupported: Vec<String>,
643 pub stats: TransformStats,
644}
645
646impl TransformOutcome {
647 fn unsupported(reason: String) -> Self {
648 Self {
649 transformed: String::new(),
650 changed: false,
651 warnings: vec![],
652 unsupported: vec![reason],
653 stats: TransformStats::default(),
654 }
655 }
656
657 pub fn confidence_summary(&self) -> String {
658 let supported = self.stats.express_init_count + self.stats.route_count;
659 let total = supported + self.unsupported.len();
660 if total == 0 {
661 "No Express patterns detected".to_string()
662 } else {
663 format!("Confidence: {}/{} transforms supported", supported, total)
664 }
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[test]
673 fn test_express_to_fastify_basic() {
674 let source = r#"const express = require('express');
675const app = express();
676app.get('/', (req, res) => res.json({ ok: true }));
677app.listen(3000);"#;
678 let mut transform = ExpressToFastifyTransform::new();
679 let result = transform.transform_source(source, std::path::Path::new("test.js"));
680 assert!(result.changed);
681 assert!(result.transformed.contains("fastify()"));
682 }
683
684 #[test]
685 fn test_express_import() {
686 let source = r#"import express from 'express';
687const app = express();"#;
688 let mut transform = ExpressToFastifyTransform::new();
689 let result = transform.transform_source(source, std::path::Path::new("test.js"));
690 assert!(result.changed);
691 assert!(result.transformed.contains("fastify"));
692 }
693
694 #[test]
695 fn test_router_init() {
696 let source = r#"const router = express.Router();"#;
697 let mut transform = ExpressToFastifyTransform::new();
698 let result = transform.transform_source(source, std::path::Path::new("test.js"));
699 assert!(result.changed);
700 }
701
702 #[test]
703 fn test_all_http_methods() {
704 let source = "app.get('/users', handler); app.post('/users', h);";
705 let mut transform = ExpressToFastifyTransform::new();
706 let result = transform.transform_source(source, std::path::Path::new("test.js"));
707 assert!(result.changed);
708 assert!(result.stats.route_count >= 2);
709 }
710
711 #[test]
712 fn test_async_handler() {
713 let source = r#"app.get('/async', async (req, res) => {});"#;
714 let mut transform = ExpressToFastifyTransform::new();
715 let result = transform.transform_source(source, std::path::Path::new("test.js"));
716 assert_eq!(result.stats.async_handlers, 1);
717 }
718
719 #[test]
720 fn test_no_change_for_non_express() {
721 let source = "const x = 1;";
722 let mut transform = ExpressToFastifyTransform::new();
723 let result = transform.transform_source(source, std::path::Path::new("test.js"));
724 assert!(!result.changed);
725 }
726
727 #[test]
728 fn test_middleware() {
729 let source = "app.use(cors());";
730 let mut transform = ExpressToFastifyTransform::new();
731 let result = transform.transform_source(source, std::path::Path::new("test.js"));
732 assert!(result.stats.middleware_count >= 1);
733 }
734
735 #[test]
736 fn test_res_json() {
737 let source = "res.json({ ok: true });";
738 let mut transform = ExpressToFastifyTransform::new();
739 let result = transform.transform_source(source, std::path::Path::new("test.js"));
740 assert_eq!(result.stats.res_json_calls, 1);
741 }
742
743 #[test]
744 fn test_res_send() {
745 let source = "res.send('hello');";
746 let mut transform = ExpressToFastifyTransform::new();
747 let result = transform.transform_source(source, std::path::Path::new("test.js"));
748 assert_eq!(result.stats.res_send_calls, 1);
749 }
750
751 #[test]
752 fn test_confidence_summary() {
753 let source = "const app = express(); app.get('/', h);";
754 let mut transform = ExpressToFastifyTransform::new();
755 let result = transform.transform_source(source, std::path::Path::new("test.js"));
756 let summary = result.confidence_summary();
757 assert!(summary.contains("Confidence"));
758 }
759
760 #[test]
761 fn test_typescript() {
762 let source = r#"import express, { Application } from 'express';
763const app: Application = express();"#;
764 let mut transform = ExpressToFastifyTransform::new();
765 let result = transform.transform_source(source, std::path::Path::new("test.ts"));
766 assert!(result.changed);
767 }
768
769 #[test]
770 fn test_router_chaining() {
771 let source = "router.route('/users').get(h1).post(h2);";
772 let mut transform = ExpressToFastifyTransform::new();
773 let result = transform.transform_source(source, std::path::Path::new("test.js"));
774 assert!(result.changed);
775 assert!(result.transformed.contains("router.get('/users', h1);"));
776 assert!(result.transformed.contains("router.post('/users', h2);"));
777 }
778
779 #[test]
780 fn test_router_chaining_middleware() {
781 let source = "router.route('/users').get(m1, h1).post(m2, m3, h2);";
782 let mut transform = ExpressToFastifyTransform::new();
783 let result = transform.transform_source(source, std::path::Path::new("test.js"));
784 assert!(result.changed);
785 assert!(result.transformed.contains("router.get('/users', { preHandler: [m1] }, h1);"));
786 assert!(result.transformed.contains("router.post('/users', { preHandler: [m2, m3] }, h2);"));
787 }
788
789 #[test]
790 fn test_simple_middleware_and_handler() {
791 let source = "app.get('/users', m1, m2, (req, res) => { res.status(200).json({ ok: true }); });";
792 let mut transform = ExpressToFastifyTransform::new();
793 let result = transform.transform_source(source, std::path::Path::new("test.js"));
794 assert!(result.changed);
795 assert!(result.transformed.contains("{ preHandler: [m1, m2] }"));
796 assert!(result.transformed.contains("(req, reply)"));
797 assert!(result.transformed.contains("reply.status(200).send({ ok: true })"));
798 }
799
800 #[test]
801 fn test_unsafe_route_mutations() {
802 let source = "app.get('/', (req, res) => { req.customProp = 'unsafe'; });";
803 let mut transform = ExpressToFastifyTransform::new();
804 let result = transform.transform_source(source, std::path::Path::new("test.js"));
805 assert!(!result.warnings.is_empty());
806 assert!(result.warnings[0].contains("Unsafe route mutation"));
807 }
808
809 #[test]
810 fn test_query_params_access() {
811 let source = "app.get('/', (req, res) => { const name = req.param('name'); const id = req.query.id; });";
812 let mut transform = ExpressToFastifyTransform::new();
813 let result = transform.transform_source(source, std::path::Path::new("test.js"));
814 assert!(result.changed);
815 assert!(result.transformed.contains("req.params.name"));
816 assert!(result.transformed.contains("req.query.id"));
817 }
818}