1use std::collections::{HashMap, HashSet};
13use std::path::Path;
14
15#[derive(Debug, Default)]
17pub struct ScriptAnalysis {
18 pub used_packages: HashSet<String>,
20 pub config_files: Vec<String>,
22 pub entry_files: Vec<String>,
24}
25
26#[derive(Debug, PartialEq)]
28struct ScriptCommand {
29 binary: String,
31 config_args: Vec<String>,
33 file_args: Vec<String>,
35}
36
37fn binary_to_package_map() -> HashMap<&'static str, &'static str> {
39 HashMap::from([
40 ("tsc", "typescript"),
41 ("tsserver", "typescript"),
42 ("ng", "@angular/cli"),
43 ("nuxi", "nuxt"),
44 ("run-s", "npm-run-all"),
45 ("run-p", "npm-run-all"),
46 ("run-s2", "npm-run-all2"),
47 ("run-p2", "npm-run-all2"),
48 ("sb", "storybook"),
49 ("biome", "@biomejs/biome"),
50 ("oxlint", "oxlint"),
51 ])
52}
53
54const ENV_WRAPPERS: &[&str] = &["cross-env", "dotenv", "env"];
56
57const NODE_RUNNERS: &[&str] = &["node", "ts-node", "tsx", "babel-node", "bun"];
59
60pub fn filter_production_scripts(scripts: &HashMap<String, String>) -> HashMap<String, String> {
65 scripts
66 .iter()
67 .filter(|(name, _)| is_production_script(name))
68 .map(|(k, v)| (k.clone(), v.clone()))
69 .collect()
70}
71
72fn is_production_script(name: &str) -> bool {
77 let root_name = name.split(':').next().unwrap_or(name);
79
80 if matches!(
82 root_name,
83 "start" | "build" | "serve" | "preview" | "prepare" | "prepublishOnly" | "postinstall"
84 ) {
85 return true;
86 }
87
88 let base = root_name
90 .strip_prefix("pre")
91 .or_else(|| root_name.strip_prefix("post"));
92
93 if let Some(base) = base {
94 matches!(base, "start" | "build" | "serve" | "install")
95 } else {
96 false
97 }
98}
99
100pub fn analyze_scripts(scripts: &HashMap<String, String>, root: &Path) -> ScriptAnalysis {
105 let mut result = ScriptAnalysis::default();
106
107 for script_value in scripts.values() {
108 for wrapper in ENV_WRAPPERS {
110 if script_value
111 .split_whitespace()
112 .any(|token| token == *wrapper)
113 {
114 let pkg = resolve_binary_to_package(wrapper, root);
115 if !is_builtin_command(wrapper) {
116 result.used_packages.insert(pkg);
117 }
118 }
119 }
120
121 let commands = parse_script(script_value);
122
123 for cmd in commands {
124 if !cmd.binary.is_empty() && !is_builtin_command(&cmd.binary) {
126 if NODE_RUNNERS.contains(&cmd.binary.as_str()) {
127 if cmd.binary != "node" && cmd.binary != "bun" {
129 let pkg = resolve_binary_to_package(&cmd.binary, root);
130 result.used_packages.insert(pkg);
131 }
132 } else {
133 let pkg = resolve_binary_to_package(&cmd.binary, root);
134 result.used_packages.insert(pkg);
135 }
136 }
137
138 result.config_files.extend(cmd.config_args);
139 result.entry_files.extend(cmd.file_args);
140 }
141 }
142
143 result
144}
145
146fn parse_script(script: &str) -> Vec<ScriptCommand> {
150 let mut commands = Vec::new();
151
152 for segment in split_shell_operators(script) {
153 let segment = segment.trim();
154 if segment.is_empty() {
155 continue;
156 }
157 if let Some(cmd) = parse_command_segment(segment) {
158 commands.push(cmd);
159 }
160 }
161
162 commands
163}
164
165fn split_shell_operators(script: &str) -> Vec<&str> {
168 let mut segments = Vec::new();
169 let mut start = 0;
170 let bytes = script.as_bytes();
171 let len = bytes.len();
172 let mut i = 0;
173 let mut in_single_quote = false;
174 let mut in_double_quote = false;
175
176 while i < len {
177 let b = bytes[i];
178
179 if b == b'\'' && !in_double_quote {
180 in_single_quote = !in_single_quote;
181 i += 1;
182 continue;
183 }
184 if b == b'"' && !in_single_quote {
185 in_double_quote = !in_double_quote;
186 i += 1;
187 continue;
188 }
189 if in_single_quote || in_double_quote {
190 i += 1;
191 continue;
192 }
193
194 if i + 1 < len
196 && ((b == b'&' && bytes[i + 1] == b'&') || (b == b'|' && bytes[i + 1] == b'|'))
197 {
198 segments.push(&script[start..i]);
199 i += 2;
200 start = i;
201 continue;
202 }
203
204 if b == b';' || (b == b'|' && (i + 1 >= len || bytes[i + 1] != b'|')) {
206 segments.push(&script[start..i]);
207 i += 1;
208 start = i;
209 continue;
210 }
211
212 i += 1;
213 }
214
215 if start < len {
216 segments.push(&script[start..]);
217 }
218
219 segments
220}
221
222fn parse_command_segment(segment: &str) -> Option<ScriptCommand> {
224 let tokens: Vec<&str> = segment.split_whitespace().collect();
225 if tokens.is_empty() {
226 return None;
227 }
228
229 let mut idx = 0;
230
231 while idx < tokens.len() && is_env_assignment(tokens[idx]) {
233 idx += 1;
234 }
235 if idx >= tokens.len() {
236 return None;
237 }
238
239 while idx < tokens.len() && ENV_WRAPPERS.contains(&tokens[idx]) {
241 idx += 1;
242 while idx < tokens.len() && is_env_assignment(tokens[idx]) {
244 idx += 1;
245 }
246 if idx < tokens.len() && tokens[idx] == "--" {
248 idx += 1;
249 }
250 }
251 if idx >= tokens.len() {
252 return None;
253 }
254
255 let token = tokens[idx];
257 if matches!(token, "npx" | "pnpx" | "bunx") {
258 idx += 1;
259 while idx < tokens.len() && tokens[idx].starts_with('-') {
261 let flag = tokens[idx];
262 idx += 1;
263 if matches!(flag, "--package" | "-p") && idx < tokens.len() {
265 idx += 1;
266 }
267 }
268 } else if matches!(token, "yarn" | "pnpm" | "npm" | "bun") {
269 if idx + 1 < tokens.len() {
270 let subcmd = tokens[idx + 1];
271 if subcmd == "exec" || subcmd == "dlx" {
272 idx += 2;
273 } else if matches!(subcmd, "run" | "run-script") {
274 return None;
276 } else {
277 return None;
279 }
280 } else {
281 return None;
282 }
283 }
284 if idx >= tokens.len() {
285 return None;
286 }
287
288 let binary = tokens[idx].to_string();
289 idx += 1;
290
291 if NODE_RUNNERS.contains(&binary.as_str()) {
293 let mut file_args = Vec::new();
294 let mut config_args = Vec::new();
295
296 while idx < tokens.len() {
297 let token = tokens[idx];
298
299 if matches!(
301 token,
302 "-e" | "--eval" | "-p" | "--print" | "-r" | "--require"
303 ) {
304 idx += 2;
305 continue;
306 }
307
308 if token.starts_with('-') {
309 if let Some(config) = extract_config_arg(token, tokens.get(idx + 1).copied()) {
310 config_args.push(config);
311 if !token.contains('=') {
312 idx += 1;
313 }
314 }
315 idx += 1;
316 continue;
317 }
318
319 if looks_like_file_path(token) {
320 file_args.push(token.to_string());
321 }
322 idx += 1;
323 }
324
325 return Some(ScriptCommand {
326 binary,
327 config_args,
328 file_args,
329 });
330 }
331
332 let mut config_args = Vec::new();
334 let mut file_args = Vec::new();
335
336 while idx < tokens.len() {
337 let token = tokens[idx];
338
339 if let Some(config) = extract_config_arg(token, tokens.get(idx + 1).copied()) {
340 config_args.push(config);
341 if token.contains('=') || token.starts_with("--config=") || token.starts_with("-c=") {
342 idx += 1;
343 } else {
344 idx += 2;
345 }
346 continue;
347 }
348
349 if token.starts_with('-') {
350 idx += 1;
351 continue;
352 }
353
354 if looks_like_file_path(token) {
355 file_args.push(token.to_string());
356 }
357 idx += 1;
358 }
359
360 Some(ScriptCommand {
361 binary,
362 config_args,
363 file_args,
364 })
365}
366
367fn extract_config_arg(token: &str, next: Option<&str>) -> Option<String> {
369 if let Some(value) = token.strip_prefix("--config=")
371 && !value.is_empty()
372 {
373 return Some(value.to_string());
374 }
375 if let Some(value) = token.strip_prefix("-c=")
377 && !value.is_empty()
378 {
379 return Some(value.to_string());
380 }
381 if matches!(token, "--config" | "-c")
383 && let Some(next_token) = next
384 && !next_token.starts_with('-')
385 {
386 return Some(next_token.to_string());
387 }
388 None
389}
390
391fn is_env_assignment(token: &str) -> bool {
393 if let Some(eq_pos) = token.find('=') {
394 let name = &token[..eq_pos];
395 !name.is_empty() && name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
396 } else {
397 false
398 }
399}
400
401fn looks_like_file_path(token: &str) -> bool {
403 const EXTENSIONS: &[&str] = &[
404 ".js", ".ts", ".mjs", ".cjs", ".mts", ".cts", ".jsx", ".tsx", ".json", ".yaml", ".yml",
405 ".toml",
406 ];
407 if EXTENSIONS.iter().any(|ext| token.ends_with(ext)) {
408 return true;
409 }
410 token.starts_with("./")
411 || token.starts_with("../")
412 || (token.contains('/') && !token.starts_with('@') && !token.contains("://"))
413}
414
415fn is_builtin_command(cmd: &str) -> bool {
417 matches!(
418 cmd,
419 "echo"
420 | "cat"
421 | "cp"
422 | "mv"
423 | "rm"
424 | "mkdir"
425 | "rmdir"
426 | "ls"
427 | "cd"
428 | "pwd"
429 | "test"
430 | "true"
431 | "false"
432 | "exit"
433 | "export"
434 | "source"
435 | "which"
436 | "chmod"
437 | "chown"
438 | "touch"
439 | "find"
440 | "grep"
441 | "sed"
442 | "awk"
443 | "xargs"
444 | "tee"
445 | "sort"
446 | "uniq"
447 | "wc"
448 | "head"
449 | "tail"
450 | "sleep"
451 | "wait"
452 | "kill"
453 | "sh"
454 | "bash"
455 | "zsh"
456 )
457}
458
459pub fn resolve_binary_to_package(binary: &str, root: &Path) -> String {
466 let map = binary_to_package_map();
468 if let Some(&pkg) = map.get(binary) {
469 return pkg.to_string();
470 }
471
472 let bin_link = root.join("node_modules/.bin").join(binary);
474 if let Ok(target) = std::fs::read_link(&bin_link)
475 && let Some(pkg_name) = extract_package_from_bin_path(&target)
476 {
477 return pkg_name;
478 }
479
480 binary.to_string()
482}
483
484fn extract_package_from_bin_path(target: &std::path::Path) -> Option<String> {
490 let target_str = target.to_string_lossy();
491 let parts: Vec<&str> = target_str.split('/').collect();
492
493 for (i, part) in parts.iter().enumerate() {
494 if *part == ".." {
495 continue;
496 }
497 if part.starts_with('@') && i + 1 < parts.len() {
499 return Some(format!("{}/{}", part, parts[i + 1]));
500 }
501 return Some(part.to_string());
503 }
504
505 None
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 #[test]
515 fn simple_binary() {
516 let cmds = parse_script("webpack");
517 assert_eq!(cmds.len(), 1);
518 assert_eq!(cmds[0].binary, "webpack");
519 }
520
521 #[test]
522 fn binary_with_args() {
523 let cmds = parse_script("eslint src --ext .ts,.tsx");
524 assert_eq!(cmds.len(), 1);
525 assert_eq!(cmds[0].binary, "eslint");
526 }
527
528 #[test]
529 fn chained_commands() {
530 let cmds = parse_script("tsc --noEmit && eslint src");
531 assert_eq!(cmds.len(), 2);
532 assert_eq!(cmds[0].binary, "tsc");
533 assert_eq!(cmds[1].binary, "eslint");
534 }
535
536 #[test]
537 fn semicolon_separator() {
538 let cmds = parse_script("tsc; eslint src");
539 assert_eq!(cmds.len(), 2);
540 assert_eq!(cmds[0].binary, "tsc");
541 assert_eq!(cmds[1].binary, "eslint");
542 }
543
544 #[test]
545 fn or_chain() {
546 let cmds = parse_script("tsc --noEmit || echo failed");
547 assert_eq!(cmds.len(), 2);
548 assert_eq!(cmds[0].binary, "tsc");
549 assert_eq!(cmds[1].binary, "echo");
550 }
551
552 #[test]
553 fn pipe_operator() {
554 let cmds = parse_script("jest --json | tee results.json");
555 assert_eq!(cmds.len(), 2);
556 assert_eq!(cmds[0].binary, "jest");
557 assert_eq!(cmds[1].binary, "tee");
558 }
559
560 #[test]
561 fn npx_prefix() {
562 let cmds = parse_script("npx eslint src");
563 assert_eq!(cmds.len(), 1);
564 assert_eq!(cmds[0].binary, "eslint");
565 }
566
567 #[test]
568 fn pnpx_prefix() {
569 let cmds = parse_script("pnpx vitest run");
570 assert_eq!(cmds.len(), 1);
571 assert_eq!(cmds[0].binary, "vitest");
572 }
573
574 #[test]
575 fn npx_with_flags() {
576 let cmds = parse_script("npx --yes --package @scope/tool eslint src");
577 assert_eq!(cmds.len(), 1);
578 assert_eq!(cmds[0].binary, "eslint");
579 }
580
581 #[test]
582 fn yarn_exec() {
583 let cmds = parse_script("yarn exec jest");
584 assert_eq!(cmds.len(), 1);
585 assert_eq!(cmds[0].binary, "jest");
586 }
587
588 #[test]
589 fn pnpm_exec() {
590 let cmds = parse_script("pnpm exec vitest run");
591 assert_eq!(cmds.len(), 1);
592 assert_eq!(cmds[0].binary, "vitest");
593 }
594
595 #[test]
596 fn pnpm_dlx() {
597 let cmds = parse_script("pnpm dlx create-react-app my-app");
598 assert_eq!(cmds.len(), 1);
599 assert_eq!(cmds[0].binary, "create-react-app");
600 }
601
602 #[test]
603 fn npm_run_skipped() {
604 let cmds = parse_script("npm run build");
605 assert!(cmds.is_empty());
606 }
607
608 #[test]
609 fn yarn_run_skipped() {
610 let cmds = parse_script("yarn run test");
611 assert!(cmds.is_empty());
612 }
613
614 #[test]
615 fn bare_yarn_skipped() {
616 let cmds = parse_script("yarn build");
618 assert!(cmds.is_empty());
619 }
620
621 #[test]
624 fn cross_env_prefix() {
625 let cmds = parse_script("cross-env NODE_ENV=production webpack");
626 assert_eq!(cmds.len(), 1);
627 assert_eq!(cmds[0].binary, "webpack");
628 }
629
630 #[test]
631 fn dotenv_prefix() {
632 let cmds = parse_script("dotenv -- next build");
633 assert_eq!(cmds.len(), 1);
634 assert_eq!(cmds[0].binary, "next");
635 }
636
637 #[test]
638 fn env_var_assignment_prefix() {
639 let cmds = parse_script("NODE_ENV=production webpack --mode production");
640 assert_eq!(cmds.len(), 1);
641 assert_eq!(cmds[0].binary, "webpack");
642 }
643
644 #[test]
645 fn multiple_env_vars() {
646 let cmds = parse_script("NODE_ENV=test CI=true jest");
647 assert_eq!(cmds.len(), 1);
648 assert_eq!(cmds[0].binary, "jest");
649 }
650
651 #[test]
654 fn node_runner_file_args() {
655 let cmds = parse_script("node scripts/build.js");
656 assert_eq!(cmds.len(), 1);
657 assert_eq!(cmds[0].binary, "node");
658 assert_eq!(cmds[0].file_args, vec!["scripts/build.js"]);
659 }
660
661 #[test]
662 fn tsx_runner_file_args() {
663 let cmds = parse_script("tsx scripts/migrate.ts");
664 assert_eq!(cmds.len(), 1);
665 assert_eq!(cmds[0].binary, "tsx");
666 assert_eq!(cmds[0].file_args, vec!["scripts/migrate.ts"]);
667 }
668
669 #[test]
670 fn node_with_flags() {
671 let cmds = parse_script("node --experimental-specifier-resolution=node scripts/run.mjs");
672 assert_eq!(cmds.len(), 1);
673 assert_eq!(cmds[0].file_args, vec!["scripts/run.mjs"]);
674 }
675
676 #[test]
677 fn node_eval_no_file() {
678 let cmds = parse_script("node -e \"console.log('hi')\"");
679 assert_eq!(cmds.len(), 1);
680 assert_eq!(cmds[0].binary, "node");
681 assert!(cmds[0].file_args.is_empty());
682 }
683
684 #[test]
685 fn node_multiple_files() {
686 let cmds = parse_script("node --test file1.mjs file2.mjs");
687 assert_eq!(cmds.len(), 1);
688 assert_eq!(cmds[0].file_args, vec!["file1.mjs", "file2.mjs"]);
689 }
690
691 #[test]
694 fn config_equals() {
695 let cmds = parse_script("webpack --config=webpack.prod.js");
696 assert_eq!(cmds.len(), 1);
697 assert_eq!(cmds[0].binary, "webpack");
698 assert_eq!(cmds[0].config_args, vec!["webpack.prod.js"]);
699 }
700
701 #[test]
702 fn config_space() {
703 let cmds = parse_script("jest --config jest.config.ts");
704 assert_eq!(cmds.len(), 1);
705 assert_eq!(cmds[0].binary, "jest");
706 assert_eq!(cmds[0].config_args, vec!["jest.config.ts"]);
707 }
708
709 #[test]
710 fn config_short_flag() {
711 let cmds = parse_script("eslint -c .eslintrc.json src");
712 assert_eq!(cmds.len(), 1);
713 assert_eq!(cmds[0].binary, "eslint");
714 assert_eq!(cmds[0].config_args, vec![".eslintrc.json"]);
715 }
716
717 #[test]
720 fn tsc_maps_to_typescript() {
721 let pkg = resolve_binary_to_package("tsc", Path::new("/nonexistent"));
722 assert_eq!(pkg, "typescript");
723 }
724
725 #[test]
726 fn ng_maps_to_angular_cli() {
727 let pkg = resolve_binary_to_package("ng", Path::new("/nonexistent"));
728 assert_eq!(pkg, "@angular/cli");
729 }
730
731 #[test]
732 fn biome_maps_to_biomejs() {
733 let pkg = resolve_binary_to_package("biome", Path::new("/nonexistent"));
734 assert_eq!(pkg, "@biomejs/biome");
735 }
736
737 #[test]
738 fn unknown_binary_is_identity() {
739 let pkg = resolve_binary_to_package("my-custom-tool", Path::new("/nonexistent"));
740 assert_eq!(pkg, "my-custom-tool");
741 }
742
743 #[test]
744 fn run_s_maps_to_npm_run_all() {
745 let pkg = resolve_binary_to_package("run-s", Path::new("/nonexistent"));
746 assert_eq!(pkg, "npm-run-all");
747 }
748
749 #[test]
752 fn bin_path_regular_package() {
753 let path = std::path::Path::new("../webpack/bin/webpack.js");
754 assert_eq!(
755 extract_package_from_bin_path(path),
756 Some("webpack".to_string())
757 );
758 }
759
760 #[test]
761 fn bin_path_scoped_package() {
762 let path = std::path::Path::new("../@babel/cli/bin/babel.js");
763 assert_eq!(
764 extract_package_from_bin_path(path),
765 Some("@babel/cli".to_string())
766 );
767 }
768
769 #[test]
772 fn builtin_commands_not_tracked() {
773 let scripts: HashMap<String, String> =
774 [("postinstall".to_string(), "echo done".to_string())]
775 .into_iter()
776 .collect();
777 let result = analyze_scripts(&scripts, Path::new("/nonexistent"));
778 assert!(result.used_packages.is_empty());
779 }
780
781 #[test]
784 fn analyze_extracts_binaries() {
785 let scripts: HashMap<String, String> = [
786 ("build".to_string(), "tsc --noEmit && webpack".to_string()),
787 ("lint".to_string(), "eslint src".to_string()),
788 ("test".to_string(), "jest".to_string()),
789 ]
790 .into_iter()
791 .collect();
792 let result = analyze_scripts(&scripts, Path::new("/nonexistent"));
793 assert!(result.used_packages.contains("typescript"));
794 assert!(result.used_packages.contains("webpack"));
795 assert!(result.used_packages.contains("eslint"));
796 assert!(result.used_packages.contains("jest"));
797 }
798
799 #[test]
800 fn analyze_extracts_config_files() {
801 let scripts: HashMap<String, String> = [(
802 "build".to_string(),
803 "webpack --config webpack.prod.js".to_string(),
804 )]
805 .into_iter()
806 .collect();
807 let result = analyze_scripts(&scripts, Path::new("/nonexistent"));
808 assert!(result.config_files.contains(&"webpack.prod.js".to_string()));
809 }
810
811 #[test]
812 fn analyze_extracts_entry_files() {
813 let scripts: HashMap<String, String> =
814 [("seed".to_string(), "ts-node scripts/seed.ts".to_string())]
815 .into_iter()
816 .collect();
817 let result = analyze_scripts(&scripts, Path::new("/nonexistent"));
818 assert!(result.entry_files.contains(&"scripts/seed.ts".to_string()));
819 assert!(result.used_packages.contains("ts-node"));
821 }
822
823 #[test]
824 fn analyze_cross_env_with_config() {
825 let scripts: HashMap<String, String> = [(
826 "build".to_string(),
827 "cross-env NODE_ENV=production webpack --config webpack.prod.js".to_string(),
828 )]
829 .into_iter()
830 .collect();
831 let result = analyze_scripts(&scripts, Path::new("/nonexistent"));
832 assert!(result.used_packages.contains("cross-env"));
833 assert!(result.used_packages.contains("webpack"));
834 assert!(result.config_files.contains(&"webpack.prod.js".to_string()));
835 }
836
837 #[test]
838 fn analyze_complex_script() {
839 let scripts: HashMap<String, String> = [(
840 "ci".to_string(),
841 "cross-env CI=true npm run build && jest --config jest.ci.js --coverage".to_string(),
842 )]
843 .into_iter()
844 .collect();
845 let result = analyze_scripts(&scripts, Path::new("/nonexistent"));
846 assert!(result.used_packages.contains("cross-env"));
848 assert!(result.used_packages.contains("jest"));
849 assert!(!result.used_packages.contains("npm"));
850 assert!(result.config_files.contains(&"jest.ci.js".to_string()));
851 }
852
853 #[test]
856 fn env_assignment_valid() {
857 assert!(is_env_assignment("NODE_ENV=production"));
858 assert!(is_env_assignment("CI=true"));
859 assert!(is_env_assignment("PORT=3000"));
860 }
861
862 #[test]
863 fn env_assignment_invalid() {
864 assert!(!is_env_assignment("--config"));
865 assert!(!is_env_assignment("webpack"));
866 assert!(!is_env_assignment("./scripts/build.js"));
867 }
868
869 #[test]
872 fn split_respects_quotes() {
873 let segments = split_shell_operators("echo 'a && b' && jest");
874 assert_eq!(segments.len(), 2);
875 assert!(segments[1].trim() == "jest");
876 }
877
878 #[test]
879 fn split_double_quotes() {
880 let segments = split_shell_operators("echo \"a || b\" || jest");
881 assert_eq!(segments.len(), 2);
882 assert!(segments[1].trim() == "jest");
883 }
884
885 #[test]
888 fn production_script_start() {
889 assert!(super::is_production_script("start"));
890 assert!(super::is_production_script("prestart"));
891 assert!(super::is_production_script("poststart"));
892 }
893
894 #[test]
895 fn production_script_build() {
896 assert!(super::is_production_script("build"));
897 assert!(super::is_production_script("prebuild"));
898 assert!(super::is_production_script("postbuild"));
899 assert!(super::is_production_script("build:prod"));
900 assert!(super::is_production_script("build:esm"));
901 }
902
903 #[test]
904 fn production_script_serve_preview() {
905 assert!(super::is_production_script("serve"));
906 assert!(super::is_production_script("preview"));
907 assert!(super::is_production_script("prepare"));
908 }
909
910 #[test]
911 fn non_production_scripts() {
912 assert!(!super::is_production_script("test"));
913 assert!(!super::is_production_script("lint"));
914 assert!(!super::is_production_script("dev"));
915 assert!(!super::is_production_script("storybook"));
916 assert!(!super::is_production_script("typecheck"));
917 assert!(!super::is_production_script("format"));
918 assert!(!super::is_production_script("e2e"));
919 }
920
921 #[test]
924 fn filter_keeps_production_scripts() {
925 let scripts: HashMap<String, String> = [
926 ("build".to_string(), "webpack".to_string()),
927 ("start".to_string(), "node server.js".to_string()),
928 ("test".to_string(), "jest".to_string()),
929 ("lint".to_string(), "eslint src".to_string()),
930 ("dev".to_string(), "next dev".to_string()),
931 ]
932 .into_iter()
933 .collect();
934
935 let filtered = filter_production_scripts(&scripts);
936 assert!(filtered.contains_key("build"));
937 assert!(filtered.contains_key("start"));
938 assert!(!filtered.contains_key("test"));
939 assert!(!filtered.contains_key("lint"));
940 assert!(!filtered.contains_key("dev"));
941 }
942}