Repository: denoland/deno
Pull request: feat(eval): auto-detect CJS vs ESM in deno eval

Working on Node compat with AI agents it's often frustrating, because they try to reproduce issue by using `deno eval 'const fs = require("fs"); ...'. Also it would be helpful in most situations to be able to use ESM/CJS interchangeably. So this PR adds auto-detect whether code passed to `deno eval` is CommonJS or ESM.

Uses `deno_ast::parse_program` + `compute_is_script()` to check if the code contains `import`/`export` declarations. If it does, treat as ESM (`.mjs`); otherwise treat as CJS (`.cjs`).

This means `deno eval "require('fs')"` now works without needing `--ext=cjs`, and `deno eval "import { ok } from 'node:assert'; ..."` continues to work as ESM.

Changed files:
filename: cli/tools/run/mod.rs
patch: @@ -223,6 +223,47 @@ pub async fn eval_command(
   flags: Arc<Flags>,
   eval_flags: EvalFlags,
 ) -> Result<i32, AnyError> {
+  // Auto-detect CJS vs ESM if --ext was not explicitly provided.
+  // Default is ESM (preserving existing behavior). Only switch to CJS if
+  // the code contains CJS-specific patterns like require() calls.
+  // We check for import/export declarations first — if present, it's
+  // definitely ESM. If absent, we look for CJS patterns to decide.
+  let flags = if flags.ext.is_none() {
+    let source_code = if eval_flags.print {
+      format!("console.log({})", &eval_flags.code)
+    } else {
+      eval_flags.code.clone()
+    };
+    let specifier = deno_core::url::Url::parse("file:///eval.js").unwrap();
+    let is_script = deno_ast::parse_program(deno_ast::ParseParams {
+      specifier,
+      text: source_code.clone().into(),
+      media_type: deno_ast::MediaType::JavaScript,
+      capture_tokens: false,
+      scope_analysis: false,
+      maybe_syntax: None,
+    })
+    .map(|parsed| parsed.compute_is_script())
+    .unwrap_or(true);
+    // Only treat as CJS if it parses as a script AND contains CJS patterns.
+    // This preserves backward compatibility: code without imports/exports
+    // defaults to ESM (the longstanding deno eval behavior).
+    let has_cjs_patterns = is_script
+      && (source_code.contains("require(")
+        || source_code.contains("module.exports")
+        || source_code.contains("exports.")
+        || source_code.contains("__dirname")
+        || source_code.contains("__filename"));
+    if has_cjs_patterns {
+      let mut flags = (*flags).clone();
+      flags.ext = Some("cjs".to_string());
+      Arc::new(flags)
+    } else {
+      flags
+    }
+  } else {
+    flags
+  };
   let factory = CliFactory::from_flags(flags);
   let cli_options = factory.cli_options()?;
   let file_fetcher = factory.file_fetcher()?;

filename: tests/specs/eval/eval_auto_cjs_detection/__test__.jsonc
patch: @@ -0,0 +1,20 @@
+{
+  "tests": {
+    "auto_detect_cjs": {
+      "args": ["eval", "--quiet", "require('./say_hello')"],
+      "output": "Hello!\n"
+    },
+    "auto_detect_esm": {
+      "args": [
+        "eval",
+        "--quiet",
+        "import { ok } from 'node:assert'; ok(true); console.log('esm works')"
+      ],
+      "output": "esm works\n"
+    },
+    "ext_override": {
+      "args": ["eval", "--quiet", "--ext=mjs", "console.log('esm')"],
+      "output": "esm\n"
+    }
+  }
+}

filename: tests/specs/eval/eval_auto_cjs_detection/say_hello.js
patch: @@ -0,0 +1 @@
+console.log("Hello!");