1use std::path::Path;
65
66use serde::Deserialize;
67
68use crate::generator::OutputFile;
69
70pub const SCAFFOLD_TARGETS: &[&str] = &["js", "ts", "python", "rust", "go"];
73
74#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum ScaffoldError {
81 Parse(String),
83 UnknownValue {
85 target: String,
87 field: String,
90 value: String,
92 options: Vec<&'static str>,
94 },
95 NotConfigurable {
98 target: String,
100 field: String,
102 reason: &'static str,
104 },
105}
106
107impl std::fmt::Display for ScaffoldError {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 Self::Parse(msg) => write!(f, "bock.project parse error: {msg}"),
111 Self::UnknownValue {
112 target,
113 field,
114 value,
115 options,
116 } => write!(
117 f,
118 "unknown `{field}` value `{value}` for target `{target}`; \
119 supported options: {}",
120 options.join(", ")
121 ),
122 Self::NotConfigurable {
123 target,
124 field,
125 reason,
126 } => write!(
127 f,
128 "`{field}` is not configurable for target `{target}`: {reason}"
129 ),
130 }
131 }
132}
133
134impl std::error::Error for ScaffoldError {}
135
136#[derive(Debug, Clone, Default, PartialEq, Eq)]
142pub struct TargetScaffoldConfig {
143 pub test_framework: Option<String>,
145 pub formatter: Option<String>,
147 pub package: Option<String>,
150 pub module: Option<String>,
152 pub linter: Option<String>,
155 pub package_manager: Option<String>,
158}
159
160#[derive(Debug, Clone, Default, PartialEq, Eq)]
165pub struct ScaffoldConfig {
166 js: TargetScaffoldConfig,
167 ts: TargetScaffoldConfig,
168 python: TargetScaffoldConfig,
169 rust: TargetScaffoldConfig,
170 go: TargetScaffoldConfig,
171}
172
173impl ScaffoldConfig {
174 #[must_use]
178 pub fn target(&self, target: &str) -> Option<&TargetScaffoldConfig> {
179 match target {
180 "js" | "javascript" => Some(&self.js),
181 "ts" | "typescript" => Some(&self.ts),
182 "python" | "py" => Some(&self.python),
183 "rust" | "rs" => Some(&self.rust),
184 "go" | "golang" => Some(&self.go),
185 _ => None,
186 }
187 }
188
189 pub fn from_project_toml(source: &str) -> Result<Self, ScaffoldError> {
206 Ok(Self::from_project_toml_with_name(source)?.0)
207 }
208
209 pub fn from_project_toml_with_name(
217 source: &str,
218 ) -> Result<(Self, Option<String>), ScaffoldError> {
219 let doc: ProjectDoc =
220 toml::from_str(source).map_err(|e| ScaffoldError::Parse(e.to_string()))?;
221 let raw = doc.targets.unwrap_or_default();
222 let name = doc.project.and_then(|p| p.name);
223
224 let config = Self {
225 js: validate_target("js", raw.js)?,
226 ts: validate_target("ts", raw.ts)?,
227 python: validate_target("python", raw.python)?,
228 rust: validate_target("rust", raw.rust)?,
229 go: validate_target("go", raw.go)?,
230 };
231 Ok((config, name))
232 }
233}
234
235fn test_framework_options(target: &str) -> &'static [&'static str] {
243 match target {
244 "js" | "ts" => &["vitest", "jest"],
245 "python" => &["pytest", "unittest"],
246 _ => &[], }
248}
249
250fn formatter_options(target: &str) -> &'static [&'static str] {
252 match target {
253 "js" | "ts" => &["prettier", "none"],
254 "python" => &["black", "ruff", "none"],
255 _ => &[], }
257}
258
259fn linter_options(target: &str) -> &'static [&'static str] {
261 match target {
262 "js" | "ts" => &["eslint"],
263 "python" => &["ruff", "pylint"],
264 "rust" => &["clippy"],
265 "go" => &["golangci-lint"],
266 _ => &[],
267 }
268}
269
270fn package_manager_options(target: &str) -> &'static [&'static str] {
273 match target {
274 "js" | "ts" => &["npm", "pnpm", "yarn"],
275 "python" => &["pip", "poetry", "uv"],
276 _ => &[], }
278}
279
280fn validate_field(
284 target: &str,
285 field: &str,
286 value: Option<String>,
287 options: &[&'static str],
288 universal_reason: &'static str,
289) -> Result<Option<String>, ScaffoldError> {
290 let Some(value) = value else {
291 return Ok(None);
292 };
293 if options.is_empty() {
294 return Err(ScaffoldError::NotConfigurable {
295 target: target.to_string(),
296 field: field.to_string(),
297 reason: universal_reason,
298 });
299 }
300 let normalized = value.to_ascii_lowercase();
301 if options.contains(&normalized.as_str()) {
302 Ok(Some(normalized))
303 } else {
304 Err(ScaffoldError::UnknownValue {
305 target: target.to_string(),
306 field: field.to_string(),
307 value,
308 options: options.to_vec(),
309 })
310 }
311}
312
313fn validate_target(
315 target: &str,
316 raw: Option<RawTarget>,
317) -> Result<TargetScaffoldConfig, ScaffoldError> {
318 let Some(raw) = raw else {
319 return Ok(TargetScaffoldConfig::default());
320 };
321 let scaffolding = raw.scaffolding.unwrap_or_default();
322
323 let test_framework = validate_field(
324 target,
325 "test_framework",
326 raw.test_framework,
327 test_framework_options(target),
328 "test framework is universal (cargo test / go test) and always-on",
329 )?;
330 let formatter = validate_field(
331 target,
332 "formatter",
333 raw.formatter,
334 formatter_options(target),
335 "formatter is universal (rustfmt / gofmt) and always-on",
336 )?;
337 let linter = validate_field(
338 target,
339 "linter",
340 scaffolding.linter,
341 linter_options(target),
342 "no linter is configurable for this target",
343 )?;
344 let package_manager = validate_field(
345 target,
346 "package_manager",
347 scaffolding.package_manager,
348 package_manager_options(target),
349 "package manager is fixed for this target (cargo / go mod)",
350 )?;
351
352 Ok(TargetScaffoldConfig {
353 test_framework,
354 formatter,
355 package: raw.package,
357 module: raw.module,
358 linter,
359 package_manager,
360 })
361}
362
363#[derive(Debug, Deserialize, Default)]
366struct ProjectDoc {
367 project: Option<RawProject>,
368 targets: Option<RawTargets>,
369}
370
371#[derive(Debug, Deserialize, Default)]
372struct RawProject {
373 name: Option<String>,
374}
375
376#[derive(Debug, Deserialize, Default)]
377struct RawTargets {
378 js: Option<RawTarget>,
379 ts: Option<RawTarget>,
380 python: Option<RawTarget>,
381 rust: Option<RawTarget>,
382 go: Option<RawTarget>,
383}
384
385#[derive(Debug, Deserialize, Default)]
386struct RawTarget {
387 test_framework: Option<String>,
388 formatter: Option<String>,
389 package: Option<String>,
390 module: Option<String>,
391 scaffolding: Option<RawScaffolding>,
392}
393
394#[derive(Debug, Deserialize, Default)]
395struct RawScaffolding {
396 linter: Option<String>,
397 package_manager: Option<String>,
398}
399
400pub struct ScaffoldContext<'a> {
411 pub target: &'a str,
413 pub config: &'a TargetScaffoldConfig,
415 pub emitted: &'a [OutputFile],
417 pub project_name: Option<&'a str>,
419}
420
421pub trait Scaffolder {
434 fn target_id(&self) -> &'static str;
436
437 fn scaffold(&self, ctx: &ScaffoldContext<'_>) -> Result<Vec<OutputFile>, ScaffoldError>;
444}
445
446#[must_use]
455pub fn scaffolder_for(target: &str) -> Option<Box<dyn Scaffolder>> {
456 match target {
457 "js" | "javascript" => Some(Box::new(JsScaffolder { target_id: "js" })),
458 "ts" | "typescript" => Some(Box::new(JsScaffolder { target_id: "ts" })),
459 "python" | "py" => Some(Box::new(PythonScaffolder)),
460 "rust" | "rs" => Some(Box::new(RustScaffolder)),
461 "go" | "golang" => Some(Box::new(GoScaffolder)),
462 _ => None,
463 }
464}
465
466pub fn run_scaffolder(
478 target: &str,
479 config: &TargetScaffoldConfig,
480 emitted: &[OutputFile],
481 project_name: Option<&str>,
482) -> Result<Vec<OutputFile>, ScaffoldError> {
483 let scaffolder = scaffolder_for(target).ok_or_else(|| {
484 ScaffoldError::Parse(format!("no scaffolder registered for target `{target}`"))
485 })?;
486 let ctx = ScaffoldContext {
487 target,
488 config,
489 emitted,
490 project_name,
491 };
492 let mut files = scaffolder.scaffold(&ctx)?;
493 files.retain(|f| !emitted.iter().any(|e| e.path == f.path));
494 Ok(files)
495}
496
497const RUST_TOKIO_MARKER: &str = "tokio::";
525
526fn rust_emits_tokio(emitted: &[OutputFile]) -> bool {
534 emitted.iter().any(|f| {
535 f.path.extension().and_then(|e| e.to_str()) == Some("rs")
536 && f.content.contains(RUST_TOKIO_MARKER)
537 })
538}
539
540const DEFAULT_PROJECT_NAME: &str = "bock-app";
542
543fn npm_name(project_name: Option<&str>) -> String {
548 let raw = project_name.unwrap_or(DEFAULT_PROJECT_NAME);
549 let slug: String = raw
550 .chars()
551 .map(|c| {
552 if c.is_ascii_alphanumeric() {
553 c.to_ascii_lowercase()
554 } else {
555 '-'
556 }
557 })
558 .collect();
559 let trimmed = slug.trim_matches('-');
560 if trimmed.is_empty() {
561 DEFAULT_PROJECT_NAME.to_string()
562 } else {
563 let mut out = String::with_capacity(trimmed.len());
565 let mut last_dash = false;
566 for c in trimmed.chars() {
567 if c == '-' {
568 if !last_dash {
569 out.push(c);
570 }
571 last_dash = true;
572 } else {
573 out.push(c);
574 last_dash = false;
575 }
576 }
577 out
578 }
579}
580
581fn python_names(project_name: Option<&str>, package_override: Option<&str>) -> (String, String) {
586 let dist = npm_name(project_name);
587 let module = package_override
588 .map(str::to_string)
589 .unwrap_or_else(|| dist.replace('-', "_"));
590 (dist, module)
591}
592
593#[must_use]
604pub fn effective_test_framework(target: &str, cfg: &TargetScaffoldConfig) -> String {
605 match target {
606 "js" | "javascript" | "ts" | "typescript" => js_test_framework(cfg).to_string(),
607 "python" | "py" => py_test_framework(cfg).to_string(),
608 "rust" | "rs" => "cargo".to_string(),
609 "go" | "golang" => "go".to_string(),
610 _ => String::new(),
611 }
612}
613
614fn js_test_framework(cfg: &TargetScaffoldConfig) -> &str {
617 cfg.test_framework.as_deref().unwrap_or("vitest")
618}
619
620fn js_package_manager(cfg: &TargetScaffoldConfig) -> &str {
623 cfg.package_manager.as_deref().unwrap_or("npm")
624}
625
626fn js_prettier_enabled(cfg: &TargetScaffoldConfig) -> bool {
629 !matches!(cfg.formatter.as_deref(), Some("none"))
630}
631
632fn py_test_framework(cfg: &TargetScaffoldConfig) -> &str {
635 cfg.test_framework.as_deref().unwrap_or("pytest")
636}
637
638fn py_formatter(cfg: &TargetScaffoldConfig) -> &str {
641 cfg.formatter.as_deref().unwrap_or("black")
642}
643
644fn py_package_manager(cfg: &TargetScaffoldConfig) -> &str {
647 cfg.package_manager.as_deref().unwrap_or("pip")
648}
649
650struct JsScaffolder {
661 target_id: &'static str,
662}
663
664impl JsScaffolder {
665 fn is_ts(&self) -> bool {
666 self.target_id == "ts"
667 }
668
669 fn package_json(&self, ctx: &ScaffoldContext<'_>) -> String {
674 let name = npm_name(ctx.project_name);
675 let framework = js_test_framework(ctx.config);
676 let test_script = match framework {
677 "jest" => "jest",
678 _ => "vitest run",
679 };
680 let mut dev_deps: Vec<(&str, &str)> = match framework {
681 "jest" => vec![("jest", "^29.0.0")],
682 _ => vec![("vitest", "^2.0.0")],
683 };
684 if self.is_ts() {
685 dev_deps.push(("typescript", "^5.7.0"));
689 dev_deps.push(("@types/node", "^22.0.0"));
696 }
697 if js_prettier_enabled(ctx.config) {
698 dev_deps.push(("prettier", "^3.0.0"));
699 }
700 if ctx.config.linter.as_deref() == Some("eslint") {
701 dev_deps.push(("eslint", "^9.0.0"));
702 }
703 dev_deps.sort_by(|a, b| a.0.cmp(b.0));
705
706 let mut s = String::new();
707 s.push_str("{\n");
708 s.push_str(&format!(" \"name\": \"{name}\",\n"));
709 s.push_str(" \"version\": \"0.1.0\",\n");
710 s.push_str(" \"private\": true,\n");
711 s.push_str(" \"type\": \"module\",\n");
712 s.push_str(" \"scripts\": {\n");
713 s.push_str(&format!(" \"test\": \"{test_script}\"\n"));
714 s.push_str(" },\n");
715 s.push_str(" \"devDependencies\": {\n");
716 for (i, (dep, ver)) in dev_deps.iter().enumerate() {
717 let comma = if i + 1 < dev_deps.len() { "," } else { "" };
718 s.push_str(&format!(" \"{dep}\": \"{ver}\"{comma}\n"));
719 }
720 s.push_str(" }\n");
721 s.push_str("}\n");
722 s
723 }
724
725 fn tsconfig_json() -> String {
753 "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \
754 \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \
755 \"rewriteRelativeImportExtensions\": true,\n \
756 \"strict\": true,\n \"esModuleInterop\": true,\n \
757 \"skipLibCheck\": true,\n \"forceConsistentCasingInFileNames\": true\n },\n \
758 \"include\": [\"**/*.ts\"],\n \"exclude\": [\"bock.test.ts\"]\n}\n"
759 .to_string()
760 }
761
762 fn prettierrc() -> String {
766 "{\n \"semi\": true,\n \"singleQuote\": false\n}\n".to_string()
767 }
768
769 fn eslint_config() -> String {
773 "export default [\n {\n languageOptions: { ecmaVersion: \"latest\", \
774 sourceType: \"module\" },\n rules: {}\n }\n];\n"
775 .to_string()
776 }
777
778 fn readme(&self, ctx: &ScaffoldContext<'_>) -> String {
779 let name = npm_name(ctx.project_name);
780 let pm = js_package_manager(ctx.config);
781 let framework = js_test_framework(ctx.config);
782 let lang = if self.is_ts() {
783 "TypeScript"
784 } else {
785 "JavaScript"
786 };
787 let install = match pm {
788 "pnpm" => "pnpm install",
789 "yarn" => "yarn",
790 _ => "npm install",
791 };
792 let test_cmd = match pm {
793 "pnpm" => "pnpm test",
794 "yarn" => "yarn test",
795 _ => "npm test",
796 };
797 let run_cmd = if self.is_ts() {
798 "tsc -p . && node main.js"
804 } else {
805 "node main.js"
806 };
807 format!(
808 "# {name}\n\n\
809 {lang} output generated by [Bock](https://bock-lang.org) (project mode).\n\n\
810 ## Run\n\n\
811 ```sh\n{install}\n{run_cmd}\n```\n\n\
812 ## Test\n\n\
813 Tests are generated as {framework} tests:\n\n\
814 ```sh\n{test_cmd}\n```\n\n\
815 > Generated by `bock build`. Re-running the build regenerates this output.\n"
816 )
817 }
818}
819
820impl Scaffolder for JsScaffolder {
821 fn target_id(&self) -> &'static str {
822 self.target_id
823 }
824
825 fn scaffold(&self, ctx: &ScaffoldContext<'_>) -> Result<Vec<OutputFile>, ScaffoldError> {
826 let mut files = vec![
827 out_file("package.json", self.package_json(ctx)),
828 out_file("README.md", self.readme(ctx)),
829 ];
830 if self.is_ts() {
831 files.push(out_file("tsconfig.json", Self::tsconfig_json()));
832 files.push(out_file(
845 "node-globals.d.ts",
846 "declare var process: any;\n".to_string(),
847 ));
848 }
849 if js_prettier_enabled(ctx.config) {
850 files.push(out_file(".prettierrc.json", Self::prettierrc()));
851 }
852 if ctx.config.linter.as_deref() == Some("eslint") {
853 files.push(out_file("eslint.config.js", Self::eslint_config()));
854 }
855 Ok(files)
856 }
857}
858
859struct PythonScaffolder;
868
869impl PythonScaffolder {
870 fn pyproject(ctx: &ScaffoldContext<'_>) -> String {
871 let (dist, _module) = python_names(ctx.project_name, ctx.config.package.as_deref());
872 let framework = py_test_framework(ctx.config);
873 let formatter = py_formatter(ctx.config);
874
875 let mut s = String::new();
876 s.push_str("[project]\n");
877 s.push_str(&format!("name = \"{dist}\"\n"));
878 s.push_str("version = \"0.1.0\"\n");
879 s.push_str("requires-python = \">=3.9\"\n\n");
880
881 s.push_str("[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n");
884 s.push_str(&format!(
885 "[dependency-groups]\n# Test framework: {framework}\ndev = [\n"
886 ));
887 match framework {
888 "unittest" => s.push_str("]\n"),
889 _ => s.push_str(" \"pytest>=8.0\",\n]\n"),
890 }
891 if framework == "pytest" {
892 s.push_str("\n[tool.pytest.ini_options]\ntestpaths = [\".\"]\n");
893 }
894
895 match formatter {
898 "black" => {
899 s.push_str("\n[tool.black]\nline-length = 88\ntarget-version = [\"py39\"]\n");
900 }
901 "ruff" => {
902 s.push_str("\n[tool.ruff]\nline-length = 88\n\n[tool.ruff.format]\nquote-style = \"double\"\n");
903 }
904 _ => {}
905 }
906
907 match ctx.config.linter.as_deref() {
909 Some("ruff") => {
910 if formatter != "ruff" {
912 s.push_str("\n[tool.ruff]\nline-length = 88\n");
913 }
914 s.push_str("\n[tool.ruff.lint]\nselect = [\"E\", \"F\"]\n");
915 }
916 Some("pylint") => {
917 s.push_str(
918 "\n[tool.pylint.main]\n# Baseline Pylint config; add rules as needed.\n",
919 );
920 }
921 _ => {}
922 }
923 s
924 }
925
926 fn readme(ctx: &ScaffoldContext<'_>) -> String {
927 let (dist, _module) = python_names(ctx.project_name, ctx.config.package.as_deref());
928 let pm = py_package_manager(ctx.config);
929 let framework = py_test_framework(ctx.config);
930 let install = match pm {
931 "poetry" => "poetry install",
932 "uv" => "uv sync",
933 _ => "pip install -e .",
934 };
935 let test_cmd = match (pm, framework) {
936 (_, "unittest") => "python -m unittest".to_string(),
937 ("poetry", _) => "poetry run pytest".to_string(),
938 ("uv", _) => "uv run pytest".to_string(),
939 _ => "pytest".to_string(),
940 };
941 format!(
942 "# {dist}\n\n\
943 Python output generated by [Bock](https://bock-lang.org) (project mode).\n\n\
944 ## Run\n\n\
945 ```sh\npython3 main.py\n```\n\n\
946 ## Test\n\n\
947 Tests are generated as {framework} tests:\n\n\
948 ```sh\n{install}\n{test_cmd}\n```\n\n\
949 > Generated by `bock build`. Re-running the build regenerates this output.\n"
950 )
951 }
952}
953
954impl Scaffolder for PythonScaffolder {
955 fn target_id(&self) -> &'static str {
956 "python"
957 }
958
959 fn scaffold(&self, ctx: &ScaffoldContext<'_>) -> Result<Vec<OutputFile>, ScaffoldError> {
960 Ok(vec![
961 out_file("pyproject.toml", Self::pyproject(ctx)),
962 out_file("README.md", Self::readme(ctx)),
963 ])
964 }
965}
966
967struct RustScaffolder;
979
980impl RustScaffolder {
981 fn cargo_toml(needs_tokio: bool) -> String {
992 let mut s = String::from(
1001 "[package]\n\
1002 name = \"bock_app\"\n\
1003 version = \"0.1.0\"\n\
1004 edition = \"2021\"\n\n\
1005 [[bin]]\n\
1006 name = \"bock_app\"\n\
1007 path = \"src/main.rs\"\n\n\
1008 [workspace]\n",
1009 );
1010 if needs_tokio {
1011 s.push_str(
1012 "\n[dependencies]\n\
1013 tokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\", \"sync\", \"time\"] }\n",
1014 );
1015 }
1016 s
1017 }
1018
1019 fn readme(ctx: &ScaffoldContext<'_>) -> String {
1020 let name = npm_name(ctx.project_name);
1021 format!(
1022 "# {name}\n\n\
1023 Rust output generated by [Bock](https://bock-lang.org) (project mode).\n\n\
1024 ## Run\n\n\
1025 ```sh\ncargo run\n```\n\n\
1026 ## Test\n\n\
1027 Tests are generated as `cargo test` tests:\n\n\
1028 ```sh\ncargo test\n```\n\n\
1029 Formatting is `rustfmt` (`cargo fmt`); the output is rustfmt-clean.\n\n\
1030 > Generated by `bock build`. Re-running the build regenerates this output.\n"
1031 )
1032 }
1033}
1034
1035impl Scaffolder for RustScaffolder {
1036 fn target_id(&self) -> &'static str {
1037 "rust"
1038 }
1039
1040 fn scaffold(&self, ctx: &ScaffoldContext<'_>) -> Result<Vec<OutputFile>, ScaffoldError> {
1041 let needs_tokio = rust_emits_tokio(ctx.emitted);
1042 let mut files = vec![
1043 out_file("Cargo.toml", Self::cargo_toml(needs_tokio)),
1044 out_file("README.md", Self::readme(ctx)),
1045 ];
1046 if ctx.config.linter.as_deref() == Some("clippy") {
1048 files.push(out_file(
1049 "clippy.toml",
1050 "# Baseline Clippy config; add lint thresholds as needed.\n".to_string(),
1051 ));
1052 }
1053 Ok(files)
1054 }
1055}
1056
1057struct GoScaffolder;
1065
1066impl GoScaffolder {
1067 fn go_mod(ctx: &ScaffoldContext<'_>) -> String {
1072 let module = ctx
1073 .config
1074 .module
1075 .clone()
1076 .unwrap_or_else(|| npm_name(ctx.project_name).replace('-', "_"));
1077 format!("module {module}\n\ngo 1.21\n")
1078 }
1079
1080 fn readme(ctx: &ScaffoldContext<'_>) -> String {
1081 let name = npm_name(ctx.project_name);
1082 format!(
1083 "# {name}\n\n\
1084 Go output generated by [Bock](https://bock-lang.org) (project mode).\n\n\
1085 ## Run\n\n\
1086 ```sh\ngo run .\n```\n\n\
1087 ## Test\n\n\
1088 Tests are generated as `go test` tests (stdlib `testing`):\n\n\
1089 ```sh\ngo test ./...\n```\n\n\
1090 Formatting is `gofmt` (`go fmt ./...`); the output is gofmt-clean.\n\n\
1091 > Generated by `bock build`. Re-running the build regenerates this output.\n"
1092 )
1093 }
1094}
1095
1096impl Scaffolder for GoScaffolder {
1097 fn target_id(&self) -> &'static str {
1098 "go"
1099 }
1100
1101 fn scaffold(&self, ctx: &ScaffoldContext<'_>) -> Result<Vec<OutputFile>, ScaffoldError> {
1102 let mut files = vec![
1103 out_file("go.mod", Self::go_mod(ctx)),
1104 out_file("README.md", Self::readme(ctx)),
1105 ];
1106 if ctx.config.linter.as_deref() == Some("golangci-lint") {
1108 files.push(out_file(
1109 ".golangci.yml",
1110 "# Baseline golangci-lint config; enable linters as needed.\nlinters:\n enable:\n - gofmt\n".to_string(),
1111 ));
1112 }
1113 Ok(files)
1114 }
1115}
1116
1117fn out_file(path: &str, content: String) -> OutputFile {
1120 OutputFile {
1121 path: Path::new(path).to_path_buf(),
1122 content,
1123 source_map: None,
1124 }
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129 use super::*;
1130
1131 #[test]
1134 fn no_targets_table_yields_all_none() {
1135 let cfg = ScaffoldConfig::from_project_toml("[project]\nname = \"x\"\n").unwrap();
1136 for t in SCAFFOLD_TARGETS {
1137 let tc = cfg.target(t).unwrap();
1138 assert_eq!(*tc, TargetScaffoldConfig::default(), "target {t}");
1139 }
1140 }
1141
1142 #[test]
1143 fn empty_document_yields_default() {
1144 let cfg = ScaffoldConfig::from_project_toml("").unwrap();
1145 assert_eq!(cfg, ScaffoldConfig::default());
1146 }
1147
1148 #[test]
1149 fn empty_per_target_table_is_all_none() {
1150 let cfg = ScaffoldConfig::from_project_toml("[targets.js]\n").unwrap();
1151 assert_eq!(*cfg.target("js").unwrap(), TargetScaffoldConfig::default());
1152 }
1153
1154 #[test]
1155 fn unset_fields_stay_none_when_some_are_set() {
1156 let cfg =
1157 ScaffoldConfig::from_project_toml("[targets.js]\ntest_framework = \"jest\"\n").unwrap();
1158 let js = cfg.target("js").unwrap();
1159 assert_eq!(js.test_framework.as_deref(), Some("jest"));
1160 assert!(js.formatter.is_none());
1161 assert!(js.linter.is_none());
1162 assert!(js.package_manager.is_none());
1163 }
1164
1165 #[test]
1168 fn target_lookup_accepts_aliases() {
1169 let cfg =
1170 ScaffoldConfig::from_project_toml("[targets.python]\nformatter = \"black\"\n").unwrap();
1171 assert_eq!(
1172 cfg.target("py").unwrap().formatter.as_deref(),
1173 Some("black")
1174 );
1175 assert!(cfg.target("javascript").is_some());
1176 assert!(cfg.target("typescript").is_some());
1177 assert!(cfg.target("rs").is_some());
1178 assert!(cfg.target("golang").is_some());
1179 assert!(cfg.target("nope").is_none());
1180 }
1181
1182 #[test]
1185 fn js_ts_valid_deep_and_shallow() {
1186 let src = r#"
1187[targets.js]
1188test_framework = "jest"
1189formatter = "none"
1190[targets.js.scaffolding]
1191linter = "eslint"
1192package_manager = "pnpm"
1193
1194[targets.ts]
1195test_framework = "vitest"
1196formatter = "prettier"
1197[targets.ts.scaffolding]
1198linter = "eslint"
1199package_manager = "yarn"
1200"#;
1201 let cfg = ScaffoldConfig::from_project_toml(src).unwrap();
1202 let js = cfg.target("js").unwrap();
1203 assert_eq!(js.test_framework.as_deref(), Some("jest"));
1204 assert_eq!(js.formatter.as_deref(), Some("none"));
1205 assert_eq!(js.linter.as_deref(), Some("eslint"));
1206 assert_eq!(js.package_manager.as_deref(), Some("pnpm"));
1207 let ts = cfg.target("ts").unwrap();
1208 assert_eq!(ts.test_framework.as_deref(), Some("vitest"));
1209 assert_eq!(ts.formatter.as_deref(), Some("prettier"));
1210 assert_eq!(ts.package_manager.as_deref(), Some("yarn"));
1211 }
1212
1213 #[test]
1214 fn python_valid_deep_and_shallow() {
1215 let src = r#"
1216[targets.python]
1217package = "my_app"
1218test_framework = "unittest"
1219formatter = "ruff"
1220[targets.python.scaffolding]
1221linter = "pylint"
1222package_manager = "uv"
1223"#;
1224 let cfg = ScaffoldConfig::from_project_toml(src).unwrap();
1225 let py = cfg.target("python").unwrap();
1226 assert_eq!(py.package.as_deref(), Some("my_app"));
1227 assert_eq!(py.test_framework.as_deref(), Some("unittest"));
1228 assert_eq!(py.formatter.as_deref(), Some("ruff"));
1229 assert_eq!(py.linter.as_deref(), Some("pylint"));
1230 assert_eq!(py.package_manager.as_deref(), Some("uv"));
1231 }
1232
1233 #[test]
1234 fn rust_go_free_form_and_universal_linters() {
1235 let src = r#"
1236[targets.go]
1237module = "github.com/user/my-app"
1238[targets.go.scaffolding]
1239linter = "golangci-lint"
1240
1241[targets.rust.scaffolding]
1242linter = "clippy"
1243"#;
1244 let cfg = ScaffoldConfig::from_project_toml(src).unwrap();
1245 assert_eq!(
1246 cfg.target("go").unwrap().module.as_deref(),
1247 Some("github.com/user/my-app")
1248 );
1249 assert_eq!(
1250 cfg.target("go").unwrap().linter.as_deref(),
1251 Some("golangci-lint")
1252 );
1253 assert_eq!(
1254 cfg.target("rust").unwrap().linter.as_deref(),
1255 Some("clippy")
1256 );
1257 }
1258
1259 #[test]
1260 fn value_normalized_to_lowercase() {
1261 let cfg = ScaffoldConfig::from_project_toml(
1262 "[targets.python]\nformatter = \"Black\"\ntest_framework = \"PyTest\"\n",
1263 )
1264 .unwrap();
1265 let py = cfg.target("python").unwrap();
1266 assert_eq!(py.formatter.as_deref(), Some("black"));
1267 assert_eq!(py.test_framework.as_deref(), Some("pytest"));
1268 }
1269
1270 #[test]
1273 fn unknown_js_test_framework_errors_with_options() {
1274 let err = ScaffoldConfig::from_project_toml("[targets.js]\ntest_framework = \"mocha\"\n")
1275 .unwrap_err();
1276 match &err {
1277 ScaffoldError::UnknownValue {
1278 target,
1279 field,
1280 value,
1281 options,
1282 } => {
1283 assert_eq!(target, "js");
1284 assert_eq!(field, "test_framework");
1285 assert_eq!(value, "mocha");
1286 assert_eq!(options, &vec!["vitest", "jest"]);
1287 }
1288 other => panic!("expected UnknownValue, got {other:?}"),
1289 }
1290 let msg = err.to_string();
1292 assert!(msg.contains("mocha"));
1293 assert!(msg.contains("vitest"));
1294 assert!(msg.contains("jest"));
1295 }
1296
1297 #[test]
1298 fn unknown_python_formatter_errors() {
1299 let err = ScaffoldConfig::from_project_toml("[targets.python]\nformatter = \"yapf\"\n")
1300 .unwrap_err();
1301 assert!(matches!(
1302 err,
1303 ScaffoldError::UnknownValue { ref field, .. } if field == "formatter"
1304 ));
1305 let msg = err.to_string();
1306 assert!(msg.contains("black"));
1307 assert!(msg.contains("ruff"));
1308 assert!(msg.contains("none"));
1309 }
1310
1311 #[test]
1312 fn unknown_js_formatter_errors() {
1313 let err = ScaffoldConfig::from_project_toml("[targets.js]\nformatter = \"standard\"\n")
1314 .unwrap_err();
1315 assert!(matches!(err, ScaffoldError::UnknownValue { .. }));
1316 }
1317
1318 #[test]
1319 fn unknown_linter_errors() {
1320 let err =
1321 ScaffoldConfig::from_project_toml("[targets.js.scaffolding]\nlinter = \"jshint\"\n")
1322 .unwrap_err();
1323 match err {
1324 ScaffoldError::UnknownValue { field, options, .. } => {
1325 assert_eq!(field, "linter");
1326 assert_eq!(options, vec!["eslint"]);
1327 }
1328 other => panic!("expected UnknownValue, got {other:?}"),
1329 }
1330 }
1331
1332 #[test]
1333 fn unknown_python_linter_errors() {
1334 let err = ScaffoldConfig::from_project_toml(
1335 "[targets.python.scaffolding]\nlinter = \"flake8\"\n",
1336 )
1337 .unwrap_err();
1338 assert!(matches!(
1339 err,
1340 ScaffoldError::UnknownValue { ref options, .. }
1341 if options == &vec!["ruff", "pylint"]
1342 ));
1343 }
1344
1345 #[test]
1346 fn unknown_package_manager_errors() {
1347 let err = ScaffoldConfig::from_project_toml(
1348 "[targets.python.scaffolding]\npackage_manager = \"conda\"\n",
1349 )
1350 .unwrap_err();
1351 match err {
1352 ScaffoldError::UnknownValue { field, options, .. } => {
1353 assert_eq!(field, "package_manager");
1354 assert_eq!(options, vec!["pip", "poetry", "uv"]);
1355 }
1356 other => panic!("expected UnknownValue, got {other:?}"),
1357 }
1358 }
1359
1360 #[test]
1363 fn rust_formatter_not_configurable() {
1364 let err = ScaffoldConfig::from_project_toml("[targets.rust]\nformatter = \"rustfmt\"\n")
1365 .unwrap_err();
1366 match err {
1367 ScaffoldError::NotConfigurable { target, field, .. } => {
1368 assert_eq!(target, "rust");
1369 assert_eq!(field, "formatter");
1370 }
1371 other => panic!("expected NotConfigurable, got {other:?}"),
1372 }
1373 }
1374
1375 #[test]
1376 fn go_test_framework_not_configurable() {
1377 let err = ScaffoldConfig::from_project_toml("[targets.go]\ntest_framework = \"testify\"\n")
1378 .unwrap_err();
1379 assert!(matches!(
1380 err,
1381 ScaffoldError::NotConfigurable { ref field, .. } if field == "test_framework"
1382 ));
1383 }
1384
1385 #[test]
1386 fn rust_package_manager_not_configurable() {
1387 let err = ScaffoldConfig::from_project_toml(
1388 "[targets.rust.scaffolding]\npackage_manager = \"cargo\"\n",
1389 )
1390 .unwrap_err();
1391 assert!(matches!(
1392 err,
1393 ScaffoldError::NotConfigurable { ref field, .. } if field == "package_manager"
1394 ));
1395 }
1396
1397 #[test]
1398 fn go_package_manager_not_configurable() {
1399 let err = ScaffoldConfig::from_project_toml(
1400 "[targets.go.scaffolding]\npackage_manager = \"go\"\n",
1401 )
1402 .unwrap_err();
1403 assert!(matches!(err, ScaffoldError::NotConfigurable { .. }));
1404 }
1405
1406 #[test]
1409 fn invalid_toml_is_parse_error() {
1410 let err = ScaffoldConfig::from_project_toml("not = valid = toml").unwrap_err();
1411 assert!(matches!(err, ScaffoldError::Parse(_)));
1412 }
1413
1414 #[test]
1415 fn wrong_field_type_is_parse_error() {
1416 let err =
1417 ScaffoldConfig::from_project_toml("[targets.js]\ntest_framework = 42\n").unwrap_err();
1418 assert!(matches!(err, ScaffoldError::Parse(_)));
1419 }
1420
1421 #[test]
1422 fn unrelated_sections_are_ignored() {
1423 let src = r#"
1425[project]
1426name = "demo"
1427[strictness]
1428default = "development"
1429[ai]
1430provider = "anthropic"
1431[targets.js]
1432formatter = "prettier"
1433"#;
1434 let cfg = ScaffoldConfig::from_project_toml(src).unwrap();
1435 assert_eq!(
1436 cfg.target("js").unwrap().formatter.as_deref(),
1437 Some("prettier")
1438 );
1439 }
1440
1441 #[test]
1444 fn scaffolder_registered_for_all_targets() {
1445 for t in SCAFFOLD_TARGETS {
1446 assert!(scaffolder_for(t).is_some(), "scaffolder for {t}");
1447 }
1448 assert!(scaffolder_for("nope").is_none());
1449 }
1450
1451 fn file<'a>(files: &'a [OutputFile], rel: &str) -> &'a OutputFile {
1454 files
1455 .iter()
1456 .find(|f| f.path == Path::new(rel))
1457 .unwrap_or_else(|| {
1458 let paths: Vec<String> =
1459 files.iter().map(|f| f.path.display().to_string()).collect();
1460 panic!("no scaffolded `{rel}`; emitted: {paths:?}")
1461 })
1462 }
1463
1464 fn has(files: &[OutputFile], rel: &str) -> bool {
1466 files.iter().any(|f| f.path == Path::new(rel))
1467 }
1468
1469 #[test]
1472 fn npm_name_slugs_and_defaults() {
1473 assert_eq!(npm_name(Some("My App")), "my-app");
1474 assert_eq!(npm_name(Some("Foo__Bar!!")), "foo-bar");
1475 assert_eq!(npm_name(Some("--weird--")), "weird");
1476 assert_eq!(npm_name(Some("")), DEFAULT_PROJECT_NAME);
1477 assert_eq!(npm_name(None), DEFAULT_PROJECT_NAME);
1478 }
1479
1480 #[test]
1481 fn python_names_dist_and_module() {
1482 let (dist, module) = python_names(Some("My App"), None);
1483 assert_eq!(dist, "my-app");
1484 assert_eq!(module, "my_app");
1485 let (_dist, module) = python_names(Some("my-app"), Some("custom_pkg"));
1487 assert_eq!(module, "custom_pkg");
1488 }
1489
1490 #[test]
1493 fn rust_scaffolder_emits_cargo_toml_and_readme() {
1494 let cfg = TargetScaffoldConfig::default();
1495 let files = run_scaffolder("rust", &cfg, &[], Some("my-app")).unwrap();
1496 let cargo = file(&files, "Cargo.toml");
1497 assert!(cargo.content.contains("[package]"));
1498 assert!(cargo.content.contains("name = \"bock_app\""));
1499 assert!(cargo.content.contains("path = \"src/main.rs\""));
1500 assert!(cargo.content.contains("[workspace]"));
1503 assert!(!cargo.content.contains("tokio"));
1505 let readme = file(&files, "README.md");
1506 assert!(readme.content.contains("# my-app"));
1507 assert!(readme.content.contains("cargo run"));
1508 assert!(readme.content.contains("cargo test"));
1509 assert!(!has(&files, "rustfmt.toml"));
1511 assert!(!has(&files, "clippy.toml"));
1513 }
1514
1515 #[test]
1516 fn rust_scaffolder_adds_tokio_when_runtime_present() {
1517 let cfg = TargetScaffoldConfig::default();
1520 let emitted = vec![OutputFile {
1521 path: Path::new("src/bock_runtime.rs").to_path_buf(),
1522 content: "let (tx, rx) = tokio::sync::mpsc::unbounded_channel();".into(),
1523 source_map: None,
1524 }];
1525 let files = run_scaffolder("rust", &cfg, &emitted, None).unwrap();
1526 let cargo = file(&files, "Cargo.toml");
1527 assert!(cargo.content.contains("[dependencies]"));
1528 assert!(cargo.content.contains("tokio"));
1529 }
1530
1531 #[test]
1532 fn rust_scaffolder_adds_tokio_for_bare_host_sleep() {
1533 let cfg = TargetScaffoldConfig::default();
1538 let emitted = vec![OutputFile {
1539 path: Path::new("src/main.rs").to_path_buf(),
1540 content: "#[tokio::main]\nasync fn main() -> () {\n \
1541 tokio::time::sleep(std::time::Duration::from_nanos((0i64) as u64)).await;\n}\n"
1542 .into(),
1543 source_map: None,
1544 }];
1545 let files = run_scaffolder("rust", &cfg, &emitted, None).unwrap();
1546 let cargo = file(&files, "Cargo.toml");
1547 assert!(cargo.content.contains("[dependencies]"));
1548 assert!(cargo.content.contains("tokio"));
1549 assert!(cargo.content.contains("\"time\""));
1551 }
1552
1553 #[test]
1554 fn rust_scaffolder_no_tokio_when_unused() {
1555 let cfg = TargetScaffoldConfig::default();
1558 let emitted = vec![OutputFile {
1559 path: Path::new("src/main.rs").to_path_buf(),
1560 content: "fn main() {\n println!(\"{}\", \"hi\");\n}\n".into(),
1561 source_map: None,
1562 }];
1563 let files = run_scaffolder("rust", &cfg, &emitted, None).unwrap();
1564 let cargo = file(&files, "Cargo.toml");
1565 assert!(!cargo.content.contains("tokio"));
1566 assert!(!cargo.content.contains("[dependencies]"));
1567 }
1568
1569 #[test]
1570 fn rust_clippy_config_only_when_opted_in() {
1571 let cfg = TargetScaffoldConfig {
1572 linter: Some("clippy".into()),
1573 ..Default::default()
1574 };
1575 let files = run_scaffolder("rust", &cfg, &[], None).unwrap();
1576 assert!(has(&files, "clippy.toml"), "clippy linter opted in");
1577 }
1578
1579 #[test]
1582 fn go_scaffolder_emits_go_mod_and_readme() {
1583 let cfg = TargetScaffoldConfig::default();
1584 let files = run_scaffolder("go", &cfg, &[], Some("my-app")).unwrap();
1585 let go_mod = file(&files, "go.mod");
1586 assert!(go_mod.content.contains("module my_app"));
1588 assert!(go_mod.content.contains("go 1.21"));
1589 let readme = file(&files, "README.md");
1590 assert!(readme.content.contains("go run ."));
1591 assert!(readme.content.contains("go test"));
1592 assert!(!has(&files, ".golangci.yml"));
1593 }
1594
1595 #[test]
1596 fn go_module_path_honors_config() {
1597 let cfg = TargetScaffoldConfig {
1598 module: Some("github.com/user/my-app".into()),
1599 ..Default::default()
1600 };
1601 let files = run_scaffolder("go", &cfg, &[], Some("ignored")).unwrap();
1602 assert!(file(&files, "go.mod")
1603 .content
1604 .contains("module github.com/user/my-app"));
1605 }
1606
1607 #[test]
1608 fn go_golangci_config_only_when_opted_in() {
1609 let cfg = TargetScaffoldConfig {
1610 linter: Some("golangci-lint".into()),
1611 ..Default::default()
1612 };
1613 let files = run_scaffolder("go", &cfg, &[], None).unwrap();
1614 assert!(has(&files, ".golangci.yml"));
1615 }
1616
1617 #[test]
1620 fn js_default_scaffolding_is_vitest_prettier_npm() {
1621 let cfg = TargetScaffoldConfig::default();
1622 let files = run_scaffolder("js", &cfg, &[], Some("My App")).unwrap();
1623 let pkg = file(&files, "package.json");
1624 assert!(pkg.content.contains("\"name\": \"my-app\""));
1625 assert!(pkg.content.contains("\"type\": \"module\""));
1627 assert!(pkg.content.contains("\"vitest\""));
1628 assert!(pkg.content.contains("\"vitest run\"")); assert!(!pkg.content.contains("jest"));
1630 assert!(has(&files, ".prettierrc.json"));
1632 assert!(!has(&files, "tsconfig.json"));
1634 assert!(!has(&files, "eslint.config.js"));
1636 let readme = file(&files, "README.md");
1637 assert!(readme.content.contains("npm install"));
1638 assert!(readme.content.contains("node main.js"));
1639 }
1640
1641 #[test]
1642 fn js_jest_and_none_formatter_branches() {
1643 let cfg = TargetScaffoldConfig {
1644 test_framework: Some("jest".into()),
1645 formatter: Some("none".into()),
1646 ..Default::default()
1647 };
1648 let files = run_scaffolder("js", &cfg, &[], None).unwrap();
1649 let pkg = file(&files, "package.json");
1650 assert!(pkg.content.contains("\"jest\""));
1651 assert!(pkg.content.contains("\"test\": \"jest\""));
1652 assert!(!pkg.content.contains("vitest"));
1653 assert!(!has(&files, ".prettierrc.json"));
1655 assert!(!pkg.content.contains("prettier"));
1656 }
1657
1658 #[test]
1659 fn ts_adds_tsconfig_and_typescript_dep() {
1660 let cfg = TargetScaffoldConfig::default();
1661 let files = run_scaffolder("ts", &cfg, &[], Some("app")).unwrap();
1662 let pkg = file(&files, "package.json");
1663 assert!(pkg.content.contains("\"typescript\""));
1664 assert!(pkg.content.contains("\"typescript\": \"^5.7.0\""));
1666 let ts = file(&files, "tsconfig.json");
1667 assert!(ts.content.contains("\"module\": \"NodeNext\""));
1668 assert!(ts.content.contains("\"strict\": true"));
1669 assert!(ts
1673 .content
1674 .contains("\"rewriteRelativeImportExtensions\": true"));
1675 assert!(ts.content.contains("\"exclude\": [\"bock.test.ts\"]"));
1679 assert!(file(&files, "README.md")
1680 .content
1681 .contains("tsc -p . && node main.js"));
1682 assert!(pkg.content.contains("\"@types/node\""));
1686 assert!(has(&files, "node-globals.d.ts"));
1690 assert!(file(&files, "node-globals.d.ts")
1691 .content
1692 .contains("declare var process: any;"));
1693 }
1694
1695 #[test]
1696 fn js_eslint_and_package_manager_hint() {
1697 let cfg = TargetScaffoldConfig {
1698 linter: Some("eslint".into()),
1699 package_manager: Some("pnpm".into()),
1700 ..Default::default()
1701 };
1702 let files = run_scaffolder("js", &cfg, &[], None).unwrap();
1703 assert!(has(&files, "eslint.config.js"));
1704 let pkg = file(&files, "package.json");
1705 assert!(pkg.content.contains("\"eslint\""));
1706 assert!(file(&files, "README.md").content.contains("pnpm install"));
1708 assert!(file(&files, "README.md").content.contains("pnpm test"));
1709 }
1710
1711 #[test]
1714 fn python_default_scaffolding_is_pytest_black_pip() {
1715 let cfg = TargetScaffoldConfig::default();
1716 let files = run_scaffolder("python", &cfg, &[], Some("My App")).unwrap();
1717 let py = file(&files, "pyproject.toml");
1718 assert!(py.content.contains("name = \"my-app\""));
1719 assert!(py.content.contains("pytest"));
1720 assert!(py.content.contains("[tool.pytest.ini_options]"));
1721 assert!(py.content.contains("[tool.black]"));
1723 assert!(!py.content.contains("[tool.ruff.lint]"));
1725 assert!(!py.content.contains("[tool.pylint"));
1726 let readme = file(&files, "README.md");
1727 assert!(readme.content.contains("python3 main.py"));
1728 assert!(readme.content.contains("pip install"));
1729 assert!(readme.content.contains("pytest"));
1730 }
1731
1732 #[test]
1733 fn python_unittest_ruff_uv_branches() {
1734 let cfg = TargetScaffoldConfig {
1735 test_framework: Some("unittest".into()),
1736 formatter: Some("ruff".into()),
1737 linter: Some("ruff".into()),
1738 package_manager: Some("uv".into()),
1739 ..Default::default()
1740 };
1741 let files = run_scaffolder("python", &cfg, &[], Some("app")).unwrap();
1742 let py = file(&files, "pyproject.toml");
1743 assert!(!py.content.contains("[tool.pytest"));
1745 assert!(!py.content.contains("pytest>="));
1746 assert!(py.content.contains("[tool.ruff.format]"));
1748 assert!(py.content.contains("[tool.ruff.lint]"));
1749 let readme = file(&files, "README.md");
1751 assert!(readme.content.contains("uv sync"));
1752 assert!(readme.content.contains("python -m unittest"));
1753 }
1754
1755 #[test]
1756 fn python_pylint_and_package_override() {
1757 let cfg = TargetScaffoldConfig {
1758 package: Some("custom_pkg".into()),
1759 linter: Some("pylint".into()),
1760 ..Default::default()
1761 };
1762 let files = run_scaffolder("python", &cfg, &[], Some("app")).unwrap();
1763 let py = file(&files, "pyproject.toml");
1764 assert!(py.content.contains("[tool.pylint.main]"));
1765 }
1766
1767 #[test]
1770 fn every_target_emits_a_readme() {
1771 let cfg = TargetScaffoldConfig::default();
1772 for t in SCAFFOLD_TARGETS {
1773 let files = run_scaffolder(t, &cfg, &[], Some("demo")).unwrap();
1774 assert!(has(&files, "README.md"), "target {t} README");
1775 }
1776 }
1777
1778 #[test]
1779 fn run_scaffolder_drops_collisions_with_emitted_tree() {
1780 let cfg = TargetScaffoldConfig::default();
1782 let emitted = vec![OutputFile {
1783 path: Path::new("package.json").to_path_buf(),
1784 content: "already here".into(),
1785 source_map: None,
1786 }];
1787 let files = run_scaffolder("js", &cfg, &emitted, None).unwrap();
1788 assert!(!has(&files, "package.json"));
1790 assert!(has(&files, "README.md"));
1791 }
1792
1793 #[test]
1794 fn run_scaffolder_unknown_target_errors() {
1795 let cfg = TargetScaffoldConfig::default();
1796 let err = run_scaffolder("brainfuck", &cfg, &[], None).unwrap_err();
1797 assert!(matches!(err, ScaffoldError::Parse(_)));
1798 }
1799}