1use std::fs;
17use std::io;
18use std::path::{Path, PathBuf};
19use std::process::ExitCode;
20
21pub const PLACEHOLDER: &str = "appname";
26
27const STARTER_BYNK: &str = include_str!("templates/starter.bynk");
28const BYNK_TOML: &str = include_str!("templates/bynk.toml");
29const GITIGNORE: &str = include_str!("templates/gitignore");
30
31const SCAFFOLD_IGNORES: &[&str] = &[
35 ".git",
36 ".gitignore",
37 ".hg",
38 ".hgignore",
39 ".svn",
40 ".DS_Store",
41];
42
43#[derive(Debug, Clone)]
45pub struct NewOptions {
46 pub path: PathBuf,
48 pub name: Option<String>,
51}
52
53pub fn run(opts: &NewOptions) -> ExitCode {
57 let name = match opts.name.clone().or_else(|| derive_name(&opts.path)) {
58 Some(name) => name,
59 None => {
60 eprint!("{}", cannot_derive_message(&display(&opts.path)));
61 return ExitCode::FAILURE;
62 }
63 };
64
65 if !is_legal_name(&name) {
66 eprint!("{}", invalid_name_message(&name));
67 return ExitCode::FAILURE;
68 }
69
70 match target_is_nonempty(&opts.path) {
71 Ok(true) => {
72 eprint!("{}", clobber_message(&display(&opts.path)));
73 return ExitCode::FAILURE;
74 }
75 Ok(false) => {}
76 Err(e) => {
77 eprintln!("bynk: cannot inspect `{}`: {e}", display(&opts.path));
78 return ExitCode::FAILURE;
79 }
80 }
81
82 if let Err(e) = write_scaffold(&opts.path, &name) {
83 eprintln!("bynk: failed to write the scaffold: {e}");
84 return ExitCode::FAILURE;
85 }
86
87 print!("{}", next_steps_message(&display(&opts.path)));
88 ExitCode::SUCCESS
89}
90
91fn derive_name(path: &Path) -> Option<String> {
95 path.file_name().map(|s| s.to_string_lossy().into_owned())
96}
97
98pub fn is_legal_name(name: &str) -> bool {
103 match bynk_syntax::lexer::tokenize(name) {
104 Ok(tokens) => tokens.len() == 1 && tokens[0].kind == bynk_syntax::lexer::TokenKind::Ident,
105 Err(_) => false,
106 }
107}
108
109pub fn render(template: &str, name: &str) -> String {
111 template.replace(PLACEHOLDER, name)
112}
113
114pub fn starter_source(name: &str) -> String {
117 render(STARTER_BYNK, name)
118}
119
120fn target_is_nonempty(target: &Path) -> io::Result<bool> {
123 let entries = match fs::read_dir(target) {
124 Ok(entries) => entries,
125 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
126 Err(e) => return Err(e),
127 };
128 for entry in entries {
129 let entry = entry?;
130 let name = entry.file_name();
131 if !SCAFFOLD_IGNORES.contains(&name.to_string_lossy().as_ref()) {
132 return Ok(true);
133 }
134 }
135 Ok(false)
136}
137
138fn write_scaffold(target: &Path, name: &str) -> io::Result<()> {
141 let src_dir = target.join("src");
142 fs::create_dir_all(&src_dir)?;
143 fs::write(target.join("bynk.toml"), render(BYNK_TOML, name))?;
144 fs::write(target.join(".gitignore"), render(GITIGNORE, name))?;
145 fs::write(src_dir.join(format!("{name}.bynk")), starter_source(name))?;
146 Ok(())
147}
148
149fn display(path: &Path) -> String {
150 path.display().to_string()
151}
152
153pub fn next_steps_message(dir: &str) -> String {
160 format!(
161 "Created a new Bynk project in `{dir}`.\n\
162 \n\
163 Next steps:\n \
164 cd {dir}\n \
165 bynk dev # build and serve it locally\n\
166 \n\
167 New to Bynk? `bynk doctor` checks your toolchain is ready.\n"
168 )
169}
170
171pub fn invalid_name_message(name: &str) -> String {
173 format!(
174 "bynk: `{name}` isn't a valid Bynk name.\n \
175 A name must be a single identifier — a letter followed by letters, \
176 digits, or underscores (no dashes or dots).\n \
177 Pass `--name <ident>` to choose the project's identifier.\n"
178 )
179}
180
181pub fn cannot_derive_message(path: &str) -> String {
183 format!(
184 "bynk: couldn't derive a project name from `{path}`.\n \
185 Pass `--name <ident>` to name the project.\n"
186 )
187}
188
189pub fn clobber_message(dir: &str) -> String {
191 format!(
192 "bynk: `{dir}` already exists and isn't empty — refusing to overwrite.\n \
193 Choose a different path, or empty that directory first.\n"
194 )
195}