Skip to main content

deno_node/ops/
node_cli_parser.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3//! Node.js CLI Argument Parser - Uses node_shim crate
4//!
5//! This module uses the node_shim crate to parse Node.js CLI arguments
6//! and translates them to Deno CLI arguments.
7
8use deno_core::op2;
9
10#[derive(Debug, thiserror::Error, deno_error::JsError)]
11#[class(generic)]
12pub enum CliParserError {
13  #[error(
14    "Failed to parse Node.js CLI arguments: {message}. If you believe this is a valid Node.js flag, please report it at https://github.com/denoland/deno/issues"
15  )]
16  ParseError { message: String },
17}
18
19pub use node_shim::DebugOptions;
20pub use node_shim::EnvironmentOptions;
21pub use node_shim::HostPort;
22pub use node_shim::InspectPublishUid;
23pub use node_shim::OptionEnvvarSettings;
24pub use node_shim::OptionType;
25pub use node_shim::OptionsParser;
26pub use node_shim::ParseResult;
27pub use node_shim::PerIsolateOptions;
28pub use node_shim::PerProcessOptions;
29pub use node_shim::TranslateOptions;
30pub use node_shim::TranslatedArgs as NodeShimTranslatedArgs;
31// Re-export types from node_shim for use elsewhere in Deno
32pub use node_shim::parse_args;
33pub use node_shim::parse_node_options_env_var;
34pub use node_shim::translate_to_deno_args as translate_to_deno_args_impl;
35pub use node_shim::wrap_eval_code;
36use serde::Serialize;
37
38/// Result of translating Node.js CLI args to Deno args
39#[derive(Debug, Clone, Serialize)]
40pub struct TranslatedArgs {
41  /// The Deno CLI arguments
42  pub deno_args: Vec<String>,
43  /// Node options that should be added to NODE_OPTIONS env var
44  pub node_options: Vec<String>,
45  /// Whether the child process needs npm process state
46  pub needs_npm_process_state: bool,
47}
48
49/// Translate parsed Node.js CLI arguments to Deno CLI arguments.
50/// This is used by child_process when spawning a Deno process as Node.js.
51fn translate_to_deno_args(
52  parsed_args: ParseResult,
53  script_in_npm_package: bool,
54  wrap_eval: bool,
55) -> TranslatedArgs {
56  let options = if wrap_eval {
57    TranslateOptions::for_child_process()
58  } else {
59    TranslateOptions::for_shell_command()
60  };
61  let result = translate_to_deno_args_impl(parsed_args, &options);
62
63  TranslatedArgs {
64    deno_args: result.deno_args,
65    node_options: result.node_options,
66    needs_npm_process_state: script_in_npm_package,
67  }
68}
69
70/// Op that parses Node.js CLI arguments and translates them to Deno CLI arguments.
71/// Returns an object with deno_args, node_options, and needs_npm_process_state.
72/// Throws an error if parsing fails - this helps identify unsupported flags
73/// so they can be added to node_shim.
74///
75/// When `wrap_eval` is true, eval code is wrapped for Node.js compatibility
76/// (used for direct child_process spawning). When false, eval code is passed
77/// through as-is (used for shell command transformation).
78#[op2]
79#[serde]
80pub fn op_node_translate_cli_args(
81  #[serde] args: Vec<String>,
82  script_in_npm_package: bool,
83  wrap_eval: bool,
84) -> Result<TranslatedArgs, CliParserError> {
85  // If no args, return early with run -A -
86  // `-` tells Deno to read from stdin, matching Node.js behavior where
87  // `node` with no args reads and executes piped stdin.
88  if args.is_empty() {
89    return Ok(TranslatedArgs {
90      deno_args: vec!["run".to_string(), "-A".to_string(), "-".to_string()],
91      node_options: vec![],
92      needs_npm_process_state: script_in_npm_package,
93    });
94  }
95
96  // Parse the args
97  match parse_args(args.clone()) {
98    Ok(parsed) => Ok(translate_to_deno_args(
99      parsed,
100      script_in_npm_package,
101      wrap_eval,
102    )),
103    Err(unknown_flags) => Err(CliParserError::ParseError {
104      message: unknown_flags.join(", "),
105    }),
106  }
107}
108
109#[cfg(test)]
110mod tests {
111  use super::*;
112
113  /// Macro to create a Vec<String> from string literals
114  macro_rules! svec {
115        ($($x:expr),* $(,)?) => {
116            vec![$($x.to_string()),*]
117        };
118    }
119
120  #[test]
121  fn test_basic_parsing() {
122    let result = parse_args(svec!["--version"]).unwrap();
123    assert!(result.options.print_version);
124  }
125
126  #[test]
127  fn test_help_parsing() {
128    let result = parse_args(svec!["--help"]).unwrap();
129    assert!(result.options.print_help);
130  }
131
132  #[test]
133  fn test_debug_options() {
134    let result = parse_args(svec!["--inspect"]).unwrap();
135    assert!(
136      result
137        .options
138        .per_isolate
139        .per_env
140        .debug_options
141        .inspector_enabled
142    );
143  }
144
145  #[test]
146  fn test_string_option() {
147    let result = parse_args(svec!["--title", "myapp"]).unwrap();
148    assert_eq!(result.options.title, "myapp");
149  }
150
151  #[test]
152  fn test_boolean_negation() {
153    let result = parse_args(svec!["--no-warnings"]).unwrap();
154    assert!(!result.options.per_isolate.per_env.warnings);
155  }
156
157  #[test]
158  fn test_alias_expansion() {
159    let result = parse_args(svec!["-v"]).unwrap();
160    assert!(result.options.print_version);
161  }
162
163  #[test]
164  fn test_node_options_parsing() {
165    let env_args =
166      parse_node_options_env_var("--inspect --title \"my app\"").unwrap();
167    assert_eq!(env_args, vec!["--inspect", "--title", "my app"]);
168  }
169
170  #[test]
171  fn test_host_port_parsing() {
172    let result = parse_args(svec!["--inspect-port", "127.0.0.1:9229"]).unwrap();
173    assert_eq!(
174      result
175        .options
176        .per_isolate
177        .per_env
178        .debug_options
179        .host_port
180        .host,
181      "127.0.0.1"
182    );
183    assert_eq!(
184      result
185        .options
186        .per_isolate
187        .per_env
188        .debug_options
189        .host_port
190        .port,
191      9229
192    );
193  }
194
195  #[test]
196  fn test_translate_basic_script() {
197    let parsed = parse_args(svec!["script.js"]).unwrap();
198    let result = translate_to_deno_args(parsed, false, true);
199    assert_eq!(
200      result.deno_args,
201      svec![
202        "run",
203        "-A",
204        "--unstable-node-globals",
205        "--unstable-bare-node-builtins",
206        "--unstable-detect-cjs",
207        "script.js"
208      ]
209    );
210    assert!(result.node_options.is_empty());
211    assert!(!result.needs_npm_process_state);
212  }
213
214  #[test]
215  fn test_translate_version() {
216    let parsed = parse_args(svec!["--version"]).unwrap();
217    let result = translate_to_deno_args(parsed, false, true);
218    assert_eq!(result.deno_args, svec!["--version"]);
219  }
220
221  #[test]
222  fn test_translate_help() {
223    let parsed = parse_args(svec!["--help"]).unwrap();
224    let result = translate_to_deno_args(parsed, false, true);
225    assert_eq!(result.deno_args, svec!["--help"]);
226  }
227
228  #[test]
229  fn test_translate_eval() {
230    let parsed = parse_args(svec!["--eval", "console.log(42)"]).unwrap();
231    let result = translate_to_deno_args(parsed, false, true);
232    // Eval code should be wrapped for child_process
233    assert!(result.deno_args.contains(&"eval".to_string()));
234    // Note: deno eval has implicit permissions, so -A is not added
235    // The wrapped code should contain process.getBuiltinModule("vm").runInThisContext
236    assert!(result.deno_args.iter().any(|a| {
237      a.contains(r#"process.getBuiltinModule("vm").runInThisContext"#)
238    }));
239  }
240
241  #[test]
242  fn test_translate_inspect() {
243    let parsed = parse_args(svec!["--inspect", "script.js"]).unwrap();
244    let result = translate_to_deno_args(parsed, false, true);
245    assert!(
246      result
247        .deno_args
248        .contains(&"--inspect=127.0.0.1:9229".to_string())
249    );
250    assert!(result.deno_args.contains(&"script.js".to_string()));
251  }
252
253  #[test]
254  fn test_translate_inspect_brk() {
255    let parsed = parse_args(svec!["--inspect-brk", "script.js"]).unwrap();
256    let result = translate_to_deno_args(parsed, false, true);
257    assert!(
258      result
259        .deno_args
260        .contains(&"--inspect-brk=127.0.0.1:9229".to_string())
261    );
262  }
263
264  #[test]
265  fn test_translate_watch() {
266    let parsed = parse_args(svec!["--watch", "script.js"]).unwrap();
267    let result = translate_to_deno_args(parsed, false, true);
268    assert!(result.deno_args.contains(&"--watch".to_string()));
269  }
270
271  #[test]
272  fn test_translate_no_warnings() {
273    let parsed = parse_args(svec!["--no-warnings", "script.js"]).unwrap();
274    let result = translate_to_deno_args(parsed, false, true);
275    assert!(result.deno_args.contains(&"--quiet".to_string()));
276    assert!(result.node_options.contains(&"--no-warnings".to_string()));
277  }
278
279  #[test]
280  fn test_translate_conditions() {
281    let parsed =
282      parse_args(svec!["--conditions", "development", "script.js"]).unwrap();
283    let result = translate_to_deno_args(parsed, false, true);
284    assert!(
285      result
286        .deno_args
287        .contains(&"--conditions=development".to_string())
288    );
289  }
290
291  #[test]
292  fn test_translate_conditions_equals_format() {
293    // Test the --conditions=custom format (with equals sign)
294    let parsed = parse_args(svec!["--conditions=custom", "script.js"]).unwrap();
295    let result = translate_to_deno_args(parsed, false, true);
296    assert!(
297      result
298        .deno_args
299        .contains(&"--conditions=custom".to_string()),
300    );
301  }
302
303  #[test]
304  fn test_translate_conditions_short_alias() {
305    // Test -C custom format (short alias)
306    let parsed = parse_args(svec!["-C", "custom", "script.js"]).unwrap();
307    let result = translate_to_deno_args(parsed, false, true);
308    assert!(
309      result
310        .deno_args
311        .contains(&"--conditions=custom".to_string()),
312    );
313  }
314
315  #[test]
316  fn test_translate_v8_flags() {
317    let parsed =
318      parse_args(svec!["--max-old-space-size=4096", "script.js"]).unwrap();
319    let result = translate_to_deno_args(parsed, false, true);
320    assert!(result.deno_args.iter().any(|a| a.contains("--v8-flags=")));
321  }
322
323  #[test]
324  fn test_translate_repl() {
325    let parsed = parse_args(svec![]).unwrap();
326    let result = translate_to_deno_args(parsed, false, true);
327    // REPL should have empty deno_args (triggers Deno's REPL behavior)
328    assert!(result.deno_args.is_empty());
329  }
330
331  #[test]
332  fn test_translate_npm_package() {
333    let parsed = parse_args(svec!["script.js"]).unwrap();
334    let result = translate_to_deno_args(parsed, true, true);
335    assert!(result.needs_npm_process_state);
336  }
337
338  #[test]
339  fn test_translate_run_script() {
340    let parsed = parse_args(svec!["--run", "build"]).unwrap();
341    let result = translate_to_deno_args(parsed, false, true);
342    assert_eq!(result.deno_args, svec!["task", "build"]);
343  }
344
345  #[test]
346  fn test_translate_test_runner() {
347    let parsed = parse_args(svec!["--test", "test.js"]).unwrap();
348    let result = translate_to_deno_args(parsed, false, true);
349    assert!(result.deno_args.contains(&"test".to_string()));
350    assert!(result.deno_args.contains(&"-A".to_string()));
351    assert!(result.deno_args.contains(&"test.js".to_string()));
352  }
353
354  #[test]
355  fn test_translate_test_with_watch() {
356    let parsed = parse_args(svec!["--test", "--watch", "test.js"]).unwrap();
357    let result = translate_to_deno_args(parsed, false, true);
358    assert!(result.deno_args.contains(&"test".to_string()));
359    assert!(result.deno_args.contains(&"--watch".to_string()));
360  }
361
362  #[test]
363  fn test_wrap_eval_code() {
364    let wrapped = wrap_eval_code("console.log(42)");
365    assert!(
366      wrapped.contains(r#"process.getBuiltinModule("vm").runInThisContext"#)
367    );
368    assert!(wrapped.contains("process.getBuiltinModule"));
369    assert!(wrapped.contains("\"console.log(42)\""));
370  }
371}