1use core::fmt;
7use std::collections::{HashMap, HashSet};
8
9use crate::actions::ActionInvocation;
10
11pub mod abstract_command;
12pub mod add_command;
13pub mod build_command;
14pub mod command_input;
15pub mod command_loader;
16pub mod generate_command;
17pub mod info_command;
18pub mod new_command;
19pub mod start_command;
20
21pub use command_input::{Input, InputOptions, InputValue};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
25pub enum CommandName {
26 Add,
27 Build,
28 Generate,
29 Info,
30 New,
31 Start,
32}
33
34impl CommandName {
35 pub const fn as_str(self) -> &'static str {
36 match self {
37 Self::Add => "add",
38 Self::Build => "build",
39 Self::Generate => "generate",
40 Self::Info => "info",
41 Self::New => "new",
42 Self::Start => "start",
43 }
44 }
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum CommandParseError {
50 MissingCommand,
51 UnknownCommand(String),
52 UnknownOption {
53 command: CommandName,
54 option: String,
55 },
56 MissingRequiredArgument {
57 command: CommandName,
58 argument: &'static str,
59 },
60 MissingRequiredOptionValue {
61 command: CommandName,
62 option: &'static str,
63 },
64 TooManyArguments {
65 command: CommandName,
66 extra: Vec<String>,
67 },
68 InvalidBuilder(String),
69 InvalidLanguage(String),
70}
71
72impl fmt::Display for CommandParseError {
73 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 Self::MissingCommand => formatter.write_str("missing command"),
76 Self::UnknownCommand(command) => write!(formatter, "invalid command: {command}"),
77 Self::UnknownOption { command, option } => {
78 write!(formatter, "unknown option for {command}: {option}")
79 }
80 Self::MissingRequiredArgument { command, argument } => {
81 write!(
82 formatter,
83 "missing required argument {argument} for {command}"
84 )
85 }
86 Self::MissingRequiredOptionValue { command, option } => {
87 write!(
88 formatter,
89 "missing required value for {command} option --{option}"
90 )
91 }
92 Self::TooManyArguments { command, extra } => write!(
93 formatter,
94 "too many arguments for {command}: {}",
95 extra.join(", ")
96 ),
97 Self::InvalidBuilder(builder) => write!(
98 formatter,
99 "Invalid builder option: {builder}. Available builder: cargo"
100 ),
101 Self::InvalidLanguage(language) => write!(
102 formatter,
103 "Invalid language \"{language}\" selected. Available language is \"rust\""
104 ),
105 }
106 }
107}
108
109impl std::error::Error for CommandParseError {}
110
111impl fmt::Display for CommandName {
112 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113 formatter.write_str(self.as_str())
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum ArgumentArity {
120 Required,
121 Optional,
122 OptionalVariadic,
123}
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub struct CommandArgumentSpec {
128 pub name: &'static str,
129 pub arity: ArgumentArity,
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
134pub enum OptionValueKind {
135 Bool,
136 OptionalString,
137 RequiredString,
138 StringList,
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub enum OptionDefault {
144 None,
145 Bool(bool),
146 String(&'static str),
147 StringListEmpty,
148}
149
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub struct CommandOptionSpec {
153 pub short: Option<char>,
154 pub long: &'static str,
155 pub input_name: &'static str,
156 pub value_kind: OptionValueKind,
157 pub default: OptionDefault,
158 pub negates: Option<&'static str>,
159}
160
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub struct CommandSpec {
164 pub name: CommandName,
165 pub signature: &'static str,
166 pub aliases: &'static [&'static str],
167 pub description: &'static str,
168 pub usage: Option<&'static str>,
169 pub allow_unknown_options: bool,
170 pub args: &'static [CommandArgumentSpec],
171 pub options: &'static [CommandOptionSpec],
172}
173
174impl CommandSpec {
175 pub fn option(self, long: &str) -> Option<&'static CommandOptionSpec> {
176 self.options.iter().find(|option| option.long == long)
177 }
178}
179
180const ADD_ARGS: &[CommandArgumentSpec] = &[CommandArgumentSpec {
181 name: "library",
182 arity: ArgumentArity::Required,
183}];
184
185const ADD_OPTIONS: &[CommandOptionSpec] = &[
186 CommandOptionSpec {
187 short: Some('d'),
188 long: "dry-run",
189 input_name: "dry-run",
190 value_kind: OptionValueKind::Bool,
191 default: OptionDefault::None,
192 negates: None,
193 },
194 CommandOptionSpec {
195 short: Some('s'),
196 long: "skip-install",
197 input_name: "skip-install",
198 value_kind: OptionValueKind::Bool,
199 default: OptionDefault::Bool(false),
200 negates: None,
201 },
202 CommandOptionSpec {
203 short: Some('p'),
204 long: "project",
205 input_name: "project",
206 value_kind: OptionValueKind::OptionalString,
207 default: OptionDefault::None,
208 negates: None,
209 },
210];
211
212const BUILD_ARGS: &[CommandArgumentSpec] = &[CommandArgumentSpec {
213 name: "apps",
214 arity: ArgumentArity::OptionalVariadic,
215}];
216
217const BUILD_OPTIONS: &[CommandOptionSpec] = &[
218 option('c', "config", "config", OptionValueKind::OptionalString),
219 option('p', "path", "path", OptionValueKind::OptionalString),
220 option('w', "watch", "watch", OptionValueKind::Bool),
221 option('b', "builder", "builder", OptionValueKind::OptionalString),
222 option_no_short("watchAssets", "watchAssets", OptionValueKind::Bool),
223 option_no_short("webpack", "webpack", OptionValueKind::Bool),
224 option_no_short("type-check", "typeCheck", OptionValueKind::Bool),
225 option_no_short(
226 "webpackPath",
227 "webpackPath",
228 OptionValueKind::OptionalString,
229 ),
230 option_no_short("tsc", "tsc", OptionValueKind::Bool),
231 option_no_short(
232 "preserveWatchOutput",
233 "preserveWatchOutput",
234 OptionValueKind::Bool,
235 ),
236 option_no_short("all", "all", OptionValueKind::Bool),
237];
238
239const GENERATE_ARGS: &[CommandArgumentSpec] = &[
240 CommandArgumentSpec {
241 name: "schematic",
242 arity: ArgumentArity::Required,
243 },
244 CommandArgumentSpec {
245 name: "name",
246 arity: ArgumentArity::Optional,
247 },
248 CommandArgumentSpec {
249 name: "path",
250 arity: ArgumentArity::Optional,
251 },
252];
253
254const GENERATE_OPTIONS: &[CommandOptionSpec] = &[
255 option('d', "dry-run", "dry-run", OptionValueKind::Bool),
256 option('p', "project", "project", OptionValueKind::OptionalString),
257 CommandOptionSpec {
258 short: None,
259 long: "flat",
260 input_name: "flat",
261 value_kind: OptionValueKind::Bool,
262 default: OptionDefault::None,
263 negates: None,
264 },
265 CommandOptionSpec {
266 short: None,
267 long: "no-flat",
268 input_name: "flat",
269 value_kind: OptionValueKind::Bool,
270 default: OptionDefault::None,
271 negates: Some("flat"),
272 },
273 CommandOptionSpec {
274 short: None,
275 long: "spec",
276 input_name: "spec",
277 value_kind: OptionValueKind::Bool,
278 default: OptionDefault::Bool(true),
279 negates: None,
280 },
281 option_no_short(
282 "spec-file-suffix",
283 "specFileSuffix",
284 OptionValueKind::OptionalString,
285 ),
286 CommandOptionSpec {
287 short: None,
288 long: "skip-import",
289 input_name: "skipImport",
290 value_kind: OptionValueKind::Bool,
291 default: OptionDefault::Bool(false),
292 negates: None,
293 },
294 CommandOptionSpec {
295 short: None,
296 long: "no-spec",
297 input_name: "spec",
298 value_kind: OptionValueKind::Bool,
299 default: OptionDefault::None,
300 negates: Some("spec"),
301 },
302 option(
303 'c',
304 "collection",
305 "collection",
306 OptionValueKind::OptionalString,
307 ),
308 option_no_short("type", "type", OptionValueKind::RequiredString),
309 option_no_short("crud", "crud", OptionValueKind::OptionalString),
310];
311
312const INFO_ARGS: &[CommandArgumentSpec] = &[];
313const INFO_OPTIONS: &[CommandOptionSpec] = &[];
314
315const NEW_ARGS: &[CommandArgumentSpec] = &[CommandArgumentSpec {
316 name: "name",
317 arity: ArgumentArity::Optional,
318}];
319
320const NEW_OPTIONS: &[CommandOptionSpec] = &[
321 option_no_short("directory", "directory", OptionValueKind::OptionalString),
322 CommandOptionSpec {
323 short: Some('d'),
324 long: "dry-run",
325 input_name: "dry-run",
326 value_kind: OptionValueKind::Bool,
327 default: OptionDefault::Bool(false),
328 negates: None,
329 },
330 CommandOptionSpec {
331 short: Some('g'),
332 long: "skip-git",
333 input_name: "skip-git",
334 value_kind: OptionValueKind::Bool,
335 default: OptionDefault::Bool(false),
336 negates: None,
337 },
338 CommandOptionSpec {
339 short: Some('s'),
340 long: "skip-install",
341 input_name: "skip-install",
342 value_kind: OptionValueKind::Bool,
343 default: OptionDefault::Bool(false),
344 negates: None,
345 },
346 option(
347 'p',
348 "package-manager",
349 "packageManager",
350 OptionValueKind::OptionalString,
351 ),
352 CommandOptionSpec {
353 short: Some('l'),
354 long: "language",
355 input_name: "language",
356 value_kind: OptionValueKind::OptionalString,
357 default: OptionDefault::String("Rust"),
358 negates: None,
359 },
360 CommandOptionSpec {
361 short: Some('c'),
362 long: "collection",
363 input_name: "collection",
364 value_kind: OptionValueKind::OptionalString,
365 default: OptionDefault::String("@nestrs/schematics"),
366 negates: None,
367 },
368 CommandOptionSpec {
369 short: None,
370 long: "strict",
371 input_name: "strict",
372 value_kind: OptionValueKind::Bool,
373 default: OptionDefault::Bool(false),
374 negates: None,
375 },
376];
377
378const START_ARGS: &[CommandArgumentSpec] = &[CommandArgumentSpec {
379 name: "app",
380 arity: ArgumentArity::Optional,
381}];
382
383const START_OPTIONS: &[CommandOptionSpec] = &[
384 option('c', "config", "config", OptionValueKind::OptionalString),
385 option('p', "path", "path", OptionValueKind::OptionalString),
386 option('w', "watch", "watch", OptionValueKind::Bool),
387 option('b', "builder", "builder", OptionValueKind::OptionalString),
388 option_no_short("watchAssets", "watchAssets", OptionValueKind::Bool),
389 option('d', "debug", "debug", OptionValueKind::OptionalString),
390 option_no_short("webpack", "webpack", OptionValueKind::Bool),
391 option_no_short(
392 "webpackPath",
393 "webpackPath",
394 OptionValueKind::OptionalString,
395 ),
396 option_no_short("type-check", "typeCheck", OptionValueKind::Bool),
397 option_no_short("tsc", "tsc", OptionValueKind::Bool),
398 option_no_short("sourceRoot", "sourceRoot", OptionValueKind::OptionalString),
399 option_no_short("entryFile", "entryFile", OptionValueKind::OptionalString),
400 option('e', "exec", "exec", OptionValueKind::OptionalString),
401 option_no_short(
402 "preserveWatchOutput",
403 "preserveWatchOutput",
404 OptionValueKind::Bool,
405 ),
406 CommandOptionSpec {
407 short: None,
408 long: "shell",
409 input_name: "shell",
410 value_kind: OptionValueKind::Bool,
411 default: OptionDefault::Bool(true),
412 negates: None,
413 },
414 CommandOptionSpec {
415 short: None,
416 long: "no-shell",
417 input_name: "shell",
418 value_kind: OptionValueKind::Bool,
419 default: OptionDefault::None,
420 negates: Some("shell"),
421 },
422 CommandOptionSpec {
423 short: None,
424 long: "env-file",
425 input_name: "envFile",
426 value_kind: OptionValueKind::StringList,
427 default: OptionDefault::StringListEmpty,
428 negates: None,
429 },
430];
431
432pub const COMMAND_SPECS: &[CommandSpec] = &[
433 CommandSpec {
434 name: CommandName::New,
435 signature: "new [name]",
436 aliases: &["n"],
437 description: "Generate Nest application.",
438 usage: None,
439 allow_unknown_options: false,
440 args: NEW_ARGS,
441 options: NEW_OPTIONS,
442 },
443 CommandSpec {
444 name: CommandName::Build,
445 signature: "build [apps...]",
446 aliases: &[],
447 description: "Build Nest application.",
448 usage: None,
449 allow_unknown_options: false,
450 args: BUILD_ARGS,
451 options: BUILD_OPTIONS,
452 },
453 CommandSpec {
454 name: CommandName::Start,
455 signature: "start [app]",
456 aliases: &[],
457 description: "Run Nest application.",
458 usage: None,
459 allow_unknown_options: true,
460 args: START_ARGS,
461 options: START_OPTIONS,
462 },
463 CommandSpec {
464 name: CommandName::Info,
465 signature: "info",
466 aliases: &["i"],
467 description: "Display Nest project details.",
468 usage: None,
469 allow_unknown_options: false,
470 args: INFO_ARGS,
471 options: INFO_OPTIONS,
472 },
473 CommandSpec {
474 name: CommandName::Add,
475 signature: "add <library>",
476 aliases: &[],
477 description: "Adds support for an external library to your project.",
478 usage: Some("<library> [options] [library-specific-options]"),
479 allow_unknown_options: true,
480 args: ADD_ARGS,
481 options: ADD_OPTIONS,
482 },
483 CommandSpec {
484 name: CommandName::Generate,
485 signature: "generate <schematic> [name] [path]",
486 aliases: &["g"],
487 description: "Generate a Nest element.",
488 usage: None,
489 allow_unknown_options: false,
490 args: GENERATE_ARGS,
491 options: GENERATE_OPTIONS,
492 },
493];
494
495pub fn command_specs() -> &'static [CommandSpec] {
496 COMMAND_SPECS
497}
498
499pub fn command_spec(name: CommandName) -> Option<&'static CommandSpec> {
500 COMMAND_SPECS.iter().find(|command| command.name == name)
501}
502
503pub fn resolve_command_name(name_or_alias: &str) -> Option<CommandName> {
504 COMMAND_SPECS
505 .iter()
506 .find(|command| {
507 command.name.as_str() == name_or_alias || command.aliases.contains(&name_or_alias)
508 })
509 .map(|command| command.name)
510}
511
512pub fn resolve_command_spec(name_or_alias: &str) -> Option<&'static CommandSpec> {
513 resolve_command_name(name_or_alias).and_then(command_spec)
514}
515
516pub fn load_command_invocation<I, S>(args: I) -> Result<ActionInvocation, CommandParseError>
520where
521 I: IntoIterator<Item = S>,
522 S: AsRef<str>,
523{
524 let args: Vec<String> = args
525 .into_iter()
526 .map(|argument| argument.as_ref().to_owned())
527 .collect();
528 let (command_name, rest) = args
529 .split_first()
530 .ok_or(CommandParseError::MissingCommand)?;
531 let spec = resolve_command_spec(command_name)
532 .ok_or_else(|| CommandParseError::UnknownCommand(command_name.clone()))?;
533 let parsed = parse_tokens(spec, rest)?;
534
535 match spec.name {
536 CommandName::Add => build_add_invocation(parsed),
537 CommandName::Build => build_build_invocation(parsed),
538 CommandName::Generate => build_generate_invocation(parsed),
539 CommandName::Info => build_info_invocation(parsed),
540 CommandName::New => build_new_invocation(parsed),
541 CommandName::Start => build_start_invocation(parsed),
542 }
543}
544
545#[derive(Clone, Debug)]
546struct ParsedCommand {
547 positionals: Vec<String>,
548 options: HashMap<&'static str, InputValue>,
549 explicit_options: HashSet<&'static str>,
550 extra_flags: Vec<String>,
551}
552
553fn parse_tokens(
554 spec: &'static CommandSpec,
555 tokens: &[String],
556) -> Result<ParsedCommand, CommandParseError> {
557 let mut parsed = ParsedCommand {
558 positionals: Vec::new(),
559 options: HashMap::new(),
560 explicit_options: HashSet::new(),
561 extra_flags: Vec::new(),
562 };
563 let mut index = 0;
564
565 while index < tokens.len() {
566 let token = &tokens[index];
567 if token == "--" {
568 if spec.allow_unknown_options {
569 parsed
570 .extra_flags
571 .extend(tokens[index + 1..].iter().cloned());
572 } else {
573 parsed
574 .positionals
575 .extend(tokens[index + 1..].iter().cloned());
576 }
577 break;
578 }
579
580 if let Some(raw) = token.strip_prefix("--") {
581 let (long, inline_value) = split_long_option(raw);
582 if let Some(option) = spec.option(long) {
583 index = parse_known_option(spec, option, inline_value, tokens, index, &mut parsed)?;
584 } else if spec.allow_unknown_options {
585 parsed.extra_flags.push(token.clone());
586 if inline_value.is_none()
587 && index + 1 < tokens.len()
588 && !tokens[index + 1].starts_with('-')
589 {
590 index += 1;
591 parsed.extra_flags.push(tokens[index].clone());
592 }
593 } else {
594 return Err(CommandParseError::UnknownOption {
595 command: spec.name,
596 option: token.clone(),
597 });
598 }
599 } else if token.starts_with('-') && token.len() > 1 {
600 let (short, inline_value) = split_short_option(token);
601 if let Some(option) = spec
602 .options
603 .iter()
604 .find(|option| option.short == Some(short))
605 {
606 index = parse_known_option(spec, option, inline_value, tokens, index, &mut parsed)?;
607 } else if spec.allow_unknown_options {
608 parsed.extra_flags.push(token.clone());
609 if inline_value.is_none()
610 && index + 1 < tokens.len()
611 && !tokens[index + 1].starts_with('-')
612 {
613 index += 1;
614 parsed.extra_flags.push(tokens[index].clone());
615 }
616 } else {
617 return Err(CommandParseError::UnknownOption {
618 command: spec.name,
619 option: token.clone(),
620 });
621 }
622 } else {
623 parsed.positionals.push(token.clone());
624 }
625
626 index += 1;
627 }
628
629 validate_positionals(spec, &parsed.positionals)?;
630 Ok(parsed)
631}
632
633fn parse_known_option(
634 spec: &CommandSpec,
635 option: &'static CommandOptionSpec,
636 inline_value: Option<String>,
637 tokens: &[String],
638 index: usize,
639 parsed: &mut ParsedCommand,
640) -> Result<usize, CommandParseError> {
641 let mut next_index = index;
642 let value = match option.value_kind {
643 OptionValueKind::Bool => InputValue::Bool(option.negates.is_none()),
644 OptionValueKind::OptionalString => {
645 if let Some(value) = inline_value {
646 InputValue::String(value)
647 } else if index + 1 < tokens.len() && !tokens[index + 1].starts_with('-') {
648 next_index += 1;
649 InputValue::String(tokens[next_index].clone())
650 } else {
651 InputValue::Bool(true)
652 }
653 }
654 OptionValueKind::RequiredString => {
655 if let Some(value) = inline_value {
656 InputValue::String(value)
657 } else if index + 1 < tokens.len() && !tokens[index + 1].starts_with('-') {
658 next_index += 1;
659 InputValue::String(tokens[next_index].clone())
660 } else {
661 return Err(CommandParseError::MissingRequiredOptionValue {
662 command: spec.name,
663 option: option.long,
664 });
665 }
666 }
667 OptionValueKind::StringList => {
668 let value = if let Some(value) = inline_value {
669 value
670 } else if index + 1 < tokens.len() && !tokens[index + 1].starts_with('-') {
671 next_index += 1;
672 tokens[next_index].clone()
673 } else {
674 String::from("true")
675 };
676 let mut values = match parsed.options.remove(option.input_name) {
677 Some(InputValue::StringList(values)) => values,
678 _ => Vec::new(),
679 };
680 values.push(value);
681 InputValue::StringList(values)
682 }
683 };
684
685 parsed.options.insert(option.input_name, value);
686 parsed.explicit_options.insert(option.input_name);
687 Ok(next_index)
688}
689
690fn split_long_option(raw: &str) -> (&str, Option<String>) {
691 raw.split_once('=')
692 .map(|(name, value)| (name, Some(value.to_owned())))
693 .unwrap_or((raw, None))
694}
695
696fn split_short_option(raw: &str) -> (char, Option<String>) {
697 let short = raw.chars().nth(1).expect("short option marker");
698 let rest = &raw[2..];
699 let value = if rest.is_empty() {
700 None
701 } else {
702 Some(rest.trim_start_matches('=').to_owned())
703 };
704 (short, value)
705}
706
707fn validate_positionals(
708 spec: &CommandSpec,
709 positionals: &[String],
710) -> Result<(), CommandParseError> {
711 let required_count = spec
712 .args
713 .iter()
714 .filter(|argument| argument.arity == ArgumentArity::Required)
715 .count();
716 if positionals.len() < required_count {
717 let argument = spec.args[positionals.len()].name;
718 return Err(CommandParseError::MissingRequiredArgument {
719 command: spec.name,
720 argument,
721 });
722 }
723
724 let has_variadic = spec
725 .args
726 .iter()
727 .any(|argument| argument.arity == ArgumentArity::OptionalVariadic);
728 if !has_variadic && positionals.len() > spec.args.len() {
729 return Err(CommandParseError::TooManyArguments {
730 command: spec.name,
731 extra: positionals[spec.args.len()..].to_vec(),
732 });
733 }
734
735 Ok(())
736}
737
738fn build_add_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
739 let mut invocation = ActionInvocation::for_command(CommandName::Add);
740 invocation.inputs.push(Input::new(
741 "library",
742 string_positional(&parsed, 0).map(InputValue::String),
743 ));
744 invocation.options.push(Input::new(
745 "dry-run",
746 Some(InputValue::Bool(bool_option(&parsed, "dry-run"))),
747 ));
748 invocation.options.push(Input::new(
749 "skip-install",
750 Some(InputValue::Bool(bool_option(&parsed, "skip-install"))),
751 ));
752 invocation
753 .options
754 .push(Input::new("project", option_value(&parsed, "project")));
755 invocation.extra_flags = parsed.extra_flags;
756 Ok(invocation)
757}
758
759fn build_build_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
760 let mut invocation = ActionInvocation::for_command(CommandName::Build);
761 if parsed.positionals.is_empty() {
762 invocation.inputs.push(Input::new("app", None));
763 } else {
764 invocation.inputs.extend(
765 parsed
766 .positionals
767 .iter()
768 .cloned()
769 .map(|app| Input::new("app", Some(InputValue::String(app)))),
770 );
771 }
772
773 let is_webpack_enabled = !bool_option(&parsed, "tsc") && bool_option(&parsed, "webpack");
774 invocation
775 .options
776 .push(Input::new("config", option_value(&parsed, "config")));
777 invocation.options.push(Input::new(
778 "webpack",
779 Some(InputValue::Bool(is_webpack_enabled)),
780 ));
781 invocation.options.push(Input::new(
782 "watch",
783 Some(InputValue::Bool(bool_option(&parsed, "watch"))),
784 ));
785 invocation.options.push(Input::new(
786 "watchAssets",
787 Some(InputValue::Bool(bool_option(&parsed, "watchAssets"))),
788 ));
789 invocation
790 .options
791 .push(Input::new("path", option_value(&parsed, "path")));
792 invocation.options.push(Input::new(
793 "webpackPath",
794 option_value(&parsed, "webpackPath"),
795 ));
796 push_validated_builder(&mut invocation, &parsed)?;
797 invocation
798 .options
799 .push(Input::new("typeCheck", option_value(&parsed, "typeCheck")));
800 invocation.options.push(Input::new(
801 "preserveWatchOutput",
802 Some(InputValue::Bool(
803 bool_option(&parsed, "preserveWatchOutput")
804 && bool_option(&parsed, "watch")
805 && !is_webpack_enabled,
806 )),
807 ));
808 invocation.options.push(Input::new(
809 "all",
810 Some(InputValue::Bool(bool_option(&parsed, "all"))),
811 ));
812 Ok(invocation)
813}
814
815fn build_generate_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
816 let mut invocation = ActionInvocation::for_command(CommandName::Generate);
817 invocation.inputs.push(Input::new(
818 "schematic",
819 string_positional(&parsed, 0).map(InputValue::String),
820 ));
821 invocation.inputs.push(Input::new(
822 "name",
823 string_positional(&parsed, 1).map(InputValue::String),
824 ));
825 invocation.inputs.push(Input::new(
826 "path",
827 string_positional(&parsed, 2).map(InputValue::String),
828 ));
829
830 invocation.options.push(Input::new(
831 "dry-run",
832 Some(InputValue::Bool(bool_option(&parsed, "dry-run"))),
833 ));
834 if parsed.explicit_options.contains("flat") {
835 invocation
836 .options
837 .push(Input::new("flat", option_value(&parsed, "flat")));
838 }
839 invocation.options.push(Input::with_options(
840 "spec",
841 Some(InputValue::Bool(bool_option_default(&parsed, "spec", true))),
842 InputOptions {
843 passed_as_input: parsed.explicit_options.contains("spec"),
844 },
845 ));
846 invocation.options.push(Input::new(
847 "specFileSuffix",
848 option_value(&parsed, "specFileSuffix"),
849 ));
850 invocation.options.push(Input::new(
851 "collection",
852 option_value(&parsed, "collection"),
853 ));
854 invocation
855 .options
856 .push(Input::new("project", option_value(&parsed, "project")));
857 invocation.options.push(Input::new(
858 "skipImport",
859 Some(InputValue::Bool(bool_option(&parsed, "skipImport"))),
860 ));
861 invocation
862 .options
863 .push(Input::new("type", option_value(&parsed, "type")));
864 if let Some(value) = option_value(&parsed, "crud") {
865 let crud_enabled = match &value {
866 InputValue::Bool(value) => *value,
867 InputValue::String(value) => value == "true",
868 InputValue::StringList(values) => !values.is_empty(),
869 };
870 invocation
871 .options
872 .push(Input::new("crud", Some(InputValue::Bool(crud_enabled))));
873 }
874 Ok(invocation)
875}
876
877fn build_info_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
878 let mut invocation = ActionInvocation::for_command(CommandName::Info);
879 invocation.extra_flags = parsed.extra_flags;
880 Ok(invocation)
881}
882
883fn build_new_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
884 let mut invocation = ActionInvocation::for_command(CommandName::New);
885 invocation.inputs.push(Input::new(
886 "name",
887 string_positional(&parsed, 0).map(InputValue::String),
888 ));
889 invocation
890 .options
891 .push(Input::new("directory", option_value(&parsed, "directory")));
892 invocation.options.push(Input::new(
893 "dry-run",
894 Some(InputValue::Bool(bool_option(&parsed, "dry-run"))),
895 ));
896 invocation.options.push(Input::new(
897 "skip-git",
898 Some(InputValue::Bool(bool_option(&parsed, "skip-git"))),
899 ));
900 invocation.options.push(Input::new(
901 "skip-install",
902 Some(InputValue::Bool(bool_option(&parsed, "skip-install"))),
903 ));
904 invocation.options.push(Input::new(
905 "strict",
906 Some(InputValue::Bool(bool_option(&parsed, "strict"))),
907 ));
908 invocation.options.push(Input::new(
909 "packageManager",
910 option_value(&parsed, "packageManager"),
911 ));
912 invocation.options.push(Input::new(
913 "collection",
914 Some(
915 option_value(&parsed, "collection")
916 .unwrap_or_else(|| InputValue::String(String::from("@nestrs/schematics"))),
917 ),
918 ));
919 invocation.options.push(Input::new(
920 "language",
921 Some(InputValue::String(normalize_language(option_value(
922 &parsed, "language",
923 ))?)),
924 ));
925 Ok(invocation)
926}
927
928fn build_start_invocation(parsed: ParsedCommand) -> Result<ActionInvocation, CommandParseError> {
929 let mut invocation = ActionInvocation::for_command(CommandName::Start);
930 invocation.inputs.push(Input::new(
931 "app",
932 string_positional(&parsed, 0).map(InputValue::String),
933 ));
934
935 let is_webpack_enabled = !bool_option(&parsed, "tsc") && bool_option(&parsed, "webpack");
936 invocation
937 .options
938 .push(Input::new("config", option_value(&parsed, "config")));
939 invocation.options.push(Input::new(
940 "webpack",
941 Some(InputValue::Bool(is_webpack_enabled)),
942 ));
943 invocation
944 .options
945 .push(Input::new("debug", option_value(&parsed, "debug")));
946 invocation.options.push(Input::new(
947 "watch",
948 Some(InputValue::Bool(bool_option(&parsed, "watch"))),
949 ));
950 invocation.options.push(Input::new(
951 "watchAssets",
952 Some(InputValue::Bool(bool_option(&parsed, "watchAssets"))),
953 ));
954 invocation
955 .options
956 .push(Input::new("path", option_value(&parsed, "path")));
957 invocation.options.push(Input::new(
958 "webpackPath",
959 option_value(&parsed, "webpackPath"),
960 ));
961 invocation
962 .options
963 .push(Input::new("exec", option_value(&parsed, "exec")));
964 invocation.options.push(Input::new(
965 "sourceRoot",
966 option_value(&parsed, "sourceRoot"),
967 ));
968 invocation
969 .options
970 .push(Input::new("entryFile", option_value(&parsed, "entryFile")));
971 invocation.options.push(Input::new(
972 "preserveWatchOutput",
973 Some(InputValue::Bool(
974 bool_option(&parsed, "preserveWatchOutput")
975 && bool_option(&parsed, "watch")
976 && !is_webpack_enabled,
977 )),
978 ));
979 invocation.options.push(Input::new(
980 "shell",
981 Some(InputValue::Bool(bool_option_default(
982 &parsed, "shell", true,
983 ))),
984 ));
985 invocation.options.push(Input::new(
986 "envFile",
987 Some(
988 option_value(&parsed, "envFile").unwrap_or_else(|| InputValue::StringList(Vec::new())),
989 ),
990 ));
991 push_validated_builder(&mut invocation, &parsed)?;
992 invocation
993 .options
994 .push(Input::new("typeCheck", option_value(&parsed, "typeCheck")));
995 invocation.extra_flags = parsed.extra_flags;
996 Ok(invocation)
997}
998
999fn push_validated_builder(
1000 invocation: &mut ActionInvocation,
1001 parsed: &ParsedCommand,
1002) -> Result<(), CommandParseError> {
1003 if let Some(builder) = option_value(parsed, "builder") {
1004 match &builder {
1005 InputValue::String(value) if value == "cargo" => {
1006 invocation
1007 .options
1008 .push(Input::new("builder", Some(builder)));
1009 Ok(())
1010 }
1011 value => Err(CommandParseError::InvalidBuilder(value_to_string(value))),
1012 }
1013 } else {
1014 invocation.options.push(Input::new("builder", None));
1015 Ok(())
1016 }
1017}
1018
1019fn normalize_language(value: Option<InputValue>) -> Result<String, CommandParseError> {
1020 let language = match value {
1021 Some(InputValue::String(value)) => value,
1022 Some(value) => value_to_string(&value),
1023 None => String::from("Rust"),
1024 };
1025 match language.to_lowercase().as_str() {
1026 "rust" | "rs" => Ok(String::from("rs")),
1027 _ => Err(CommandParseError::InvalidLanguage(language)),
1028 }
1029}
1030
1031fn option_value(parsed: &ParsedCommand, name: &'static str) -> Option<InputValue> {
1032 parsed.options.get(name).cloned()
1033}
1034
1035fn bool_option(parsed: &ParsedCommand, name: &'static str) -> bool {
1036 bool_option_default(parsed, name, false)
1037}
1038
1039fn bool_option_default(parsed: &ParsedCommand, name: &'static str, default: bool) -> bool {
1040 match parsed.options.get(name) {
1041 Some(InputValue::Bool(value)) => *value,
1042 Some(InputValue::String(value)) => value == "true",
1043 Some(InputValue::StringList(values)) => !values.is_empty(),
1044 None => default,
1045 }
1046}
1047
1048fn string_positional(parsed: &ParsedCommand, index: usize) -> Option<String> {
1049 parsed.positionals.get(index).cloned()
1050}
1051
1052fn value_to_string(value: &InputValue) -> String {
1053 match value {
1054 InputValue::Bool(value) => value.to_string(),
1055 InputValue::String(value) => value.clone(),
1056 InputValue::StringList(values) => values.join(","),
1057 }
1058}
1059
1060const fn option(
1061 short: char,
1062 long: &'static str,
1063 input_name: &'static str,
1064 value_kind: OptionValueKind,
1065) -> CommandOptionSpec {
1066 CommandOptionSpec {
1067 short: Some(short),
1068 long,
1069 input_name,
1070 value_kind,
1071 default: OptionDefault::None,
1072 negates: None,
1073 }
1074}
1075
1076const fn option_no_short(
1077 long: &'static str,
1078 input_name: &'static str,
1079 value_kind: OptionValueKind,
1080) -> CommandOptionSpec {
1081 CommandOptionSpec {
1082 short: None,
1083 long,
1084 input_name,
1085 value_kind,
1086 default: OptionDefault::None,
1087 negates: None,
1088 }
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094
1095 #[test]
1096 fn captures_upstream_command_names_in_loader_order() {
1097 let names: Vec<&str> = command_specs()
1098 .iter()
1099 .map(|command| command.name.as_str())
1100 .collect();
1101
1102 assert_eq!(names, ["new", "build", "start", "info", "add", "generate"]);
1103 }
1104
1105 #[test]
1106 fn captures_generate_negated_options() {
1107 let generate = command_spec(CommandName::Generate).expect("generate command");
1108
1109 assert_eq!(generate.aliases, ["g"]);
1110 assert_eq!(
1111 generate.option("no-spec").map(|option| option.negates),
1112 Some(Some("spec"))
1113 );
1114 assert_eq!(
1115 generate.option("no-flat").map(|option| option.negates),
1116 Some(Some("flat"))
1117 );
1118 }
1119
1120 #[test]
1121 fn captures_start_common_runtime_options() {
1122 let start = command_spec(CommandName::Start).expect("start command");
1123
1124 assert!(start.allow_unknown_options);
1125 assert_eq!(
1126 start.option("env-file").map(|option| option.value_kind),
1127 Some(OptionValueKind::StringList)
1128 );
1129 assert_eq!(
1130 start.option("shell").map(|option| option.default),
1131 Some(OptionDefault::Bool(true))
1132 );
1133 }
1134}