use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
pub const PLACEHOLDER: &str = "appname";
const STARTER_BYNK: &str = include_str!("templates/starter.bynk");
const BYNK_TOML: &str = include_str!("templates/bynk.toml");
const GITIGNORE: &str = include_str!("templates/gitignore");
const SCAFFOLD_IGNORES: &[&str] = &[
".git",
".gitignore",
".hg",
".hgignore",
".svn",
".DS_Store",
];
#[derive(Debug, Clone)]
pub struct NewOptions {
pub path: PathBuf,
pub name: Option<String>,
}
pub fn run(opts: &NewOptions) -> ExitCode {
let name = match opts.name.clone().or_else(|| derive_name(&opts.path)) {
Some(name) => name,
None => {
eprint!("{}", cannot_derive_message(&display(&opts.path)));
return ExitCode::FAILURE;
}
};
if !is_legal_name(&name) {
eprint!("{}", invalid_name_message(&name));
return ExitCode::FAILURE;
}
match target_is_nonempty(&opts.path) {
Ok(true) => {
eprint!("{}", clobber_message(&display(&opts.path)));
return ExitCode::FAILURE;
}
Ok(false) => {}
Err(e) => {
eprintln!("bynk: cannot inspect `{}`: {e}", display(&opts.path));
return ExitCode::FAILURE;
}
}
if let Err(e) = write_scaffold(&opts.path, &name) {
eprintln!("bynk: failed to write the scaffold: {e}");
return ExitCode::FAILURE;
}
print!("{}", next_steps_message(&display(&opts.path)));
ExitCode::SUCCESS
}
fn derive_name(path: &Path) -> Option<String> {
path.file_name().map(|s| s.to_string_lossy().into_owned())
}
pub fn is_legal_name(name: &str) -> bool {
match bynk_syntax::lexer::tokenize(name) {
Ok(tokens) => tokens.len() == 1 && tokens[0].kind == bynk_syntax::lexer::TokenKind::Ident,
Err(_) => false,
}
}
pub fn render(template: &str, name: &str) -> String {
template.replace(PLACEHOLDER, name)
}
pub fn starter_source(name: &str) -> String {
render(STARTER_BYNK, name)
}
fn target_is_nonempty(target: &Path) -> io::Result<bool> {
let entries = match fs::read_dir(target) {
Ok(entries) => entries,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(e) => return Err(e),
};
for entry in entries {
let entry = entry?;
let name = entry.file_name();
if !SCAFFOLD_IGNORES.contains(&name.to_string_lossy().as_ref()) {
return Ok(true);
}
}
Ok(false)
}
fn write_scaffold(target: &Path, name: &str) -> io::Result<()> {
let src_dir = target.join("src");
fs::create_dir_all(&src_dir)?;
fs::write(target.join("bynk.toml"), render(BYNK_TOML, name))?;
fs::write(target.join(".gitignore"), render(GITIGNORE, name))?;
fs::write(src_dir.join(format!("{name}.bynk")), starter_source(name))?;
Ok(())
}
fn display(path: &Path) -> String {
path.display().to_string()
}
pub fn next_steps_message(dir: &str) -> String {
format!(
"Created a new Bynk project in `{dir}`.\n\
\n\
Next steps:\n \
cd {dir}\n \
bynk dev # build and serve it locally\n\
\n\
New to Bynk? `bynk doctor` checks your toolchain is ready.\n"
)
}
pub fn invalid_name_message(name: &str) -> String {
format!(
"bynk: `{name}` isn't a valid Bynk name.\n \
A name must be a single identifier — a letter followed by letters, \
digits, or underscores (no dashes or dots).\n \
Pass `--name <ident>` to choose the project's identifier.\n"
)
}
pub fn cannot_derive_message(path: &str) -> String {
format!(
"bynk: couldn't derive a project name from `{path}`.\n \
Pass `--name <ident>` to name the project.\n"
)
}
pub fn clobber_message(dir: &str) -> String {
format!(
"bynk: `{dir}` already exists and isn't empty — refusing to overwrite.\n \
Choose a different path, or empty that directory first.\n"
)
}