sqzass.git

sqzass

~ A static site generator written in Rust
git clone https://github.com/debuman/sqzass.git

src/main.rs (7083B)

      1 use anyhow::Result;
      2 use clap::{Parser, Subcommand};
      3 use std::path::PathBuf;
      4 
      5 use sqzass::error::{Kind, kind_of, message};
      6 use sqzass::serve::{ServeOptions, serve};
      7 use sqzass::{BuildOptions, build, display_path};
      8 
      9 #[derive(Parser)]
     10 #[command(
     11     name = "sqzass",
     12     version,
     13     about = "A static site generator written in Rust",
     14     disable_help_subcommand = true
     15 )]
     16 struct Cli {
     17     #[command(subcommand)]
     18     command: Command,
     19 
     20     /// 결과를 JSON 한 줄로 낸다 (스크립트·CI용)
     21     ///
     22     /// 서브커맨드 앞뒤 어디에 써도 되도록 global로 둔다. 사람이 기억해야 하는
     23     /// 플래그 위치가 하나 줄어든다.
     24     #[arg(long, global = true)]
     25     json: bool,
     26 }
     27 
     28 #[derive(Subcommand)]
     29 enum Command {
     30     /// 사이트를 빌드한다
     31     Build(BuildArgs),
     32     /// 라이브 리로드가 붙은 개발 서버를 띄운다
     33     Serve(ServeArgs),
     34     /// 새 사이트를 만든다
     35     Init(InitArgs),
     36     /// 빌드는 통과하지만 알아 둘 만한 것들을 찾는다
     37     Doctor(DoctorArgs),
     38 }
     39 
     40 #[derive(clap::Args)]
     41 struct DoctorArgs {
     42     /// 사이트 루트 (sqzass.toml이 있는 디렉터리)
     43     #[arg(short, long, default_value = ".", value_name = "DIR")]
     44     input: PathBuf,
     45 
     46     /// 이 심각도 이상이 하나라도 있으면 실패로 끝낸다 (note | warn)
     47     ///
     48     /// 기본이 warn인 이유: note는 "알아 두라"는 말이고, 그것 때문에 파이프라인이
     49     /// 멈추면 사람들은 doctor를 끄지 검사를 고치지 않는다.
     50     #[arg(long, default_value = "warn", value_name = "SEVERITY")]
     51     fail_on: sqzass::doctor::Severity,
     52 
     53     /// 드래프트 페이지도 포함해서 본다
     54     #[arg(long)]
     55     drafts: bool,
     56 }
     57 
     58 #[derive(clap::Args)]
     59 struct InitArgs {
     60     /// 만들 디렉터리. 없으면 만든다.
     61     #[arg(default_value = ".", value_name = "DIR")]
     62     dir: PathBuf,
     63 }
     64 
     65 #[derive(clap::Args)]
     66 struct ServeArgs {
     67     /// 사이트 루트 (sqzass.toml이 있는 디렉터리)
     68     #[arg(short, long, default_value = ".", value_name = "DIR")]
     69     input: PathBuf,
     70 
     71     /// 바인드 주소
     72     #[arg(short, long, default_value = "127.0.0.1", value_name = "ADDR")]
     73     bind: std::net::IpAddr,
     74 
     75     /// 포트
     76     #[arg(short, long, default_value_t = 3000)]
     77     port: u16,
     78 
     79     /// 드래프트 페이지도 포함한다
     80     #[arg(long)]
     81     drafts: bool,
     82 
     83     /// 설정의 base_url을 덮어쓴다
     84     #[arg(long, value_name = "URL")]
     85     base_url: Option<String>,
     86 }
     87 
     88 #[derive(clap::Args)]
     89 struct BuildArgs {
     90     /// 사이트 루트 (sqzass.toml이 있는 디렉터리)
     91     #[arg(short, long, default_value = ".", value_name = "DIR")]
     92     input: PathBuf,
     93 
     94     /// 출력 디렉터리. 생략하면 <input>/public.
     95     /// 지정하면 셸의 현재 디렉터리 기준으로 해석한다.
     96     #[arg(short, long, value_name = "DIR")]
     97     output: Option<PathBuf>,
     98 
     99     /// 드래프트 페이지도 포함한다
    100     #[arg(long)]
    101     drafts: bool,
    102 
    103     /// 설정의 base_url을 덮어쓴다
    104     #[arg(long, value_name = "URL")]
    105     base_url: Option<String>,
    106 
    107     /// 페이즈별 소요 시간을 stderr로 낸다
    108     #[arg(long)]
    109     profile: bool,
    110 }
    111 
    112 fn main() {
    113     let cli = Cli::parse();
    114     let json = cli.json;
    115     if let Err(err) = run(cli) {
    116         let kind = kind_of(&err);
    117         if json {
    118             // 실패도 stdout으로 낸다. --json을 켠 쪽은 파이프 하나만 읽는다.
    119             println!(
    120                 "{}",
    121                 serde_json::json!({
    122                     "ok": false,
    123                     "error": message(&err),
    124                     "kind": kind.id(),
    125                     "code": kind.code(),
    126                 })
    127             );
    128         } else {
    129             eprintln!("error: {}", message(&err));
    130         }
    131         std::process::exit(kind.code());
    132     }
    133 }
    134 
    135 fn run(cli: Cli) -> Result<()> {
    136     let json = cli.json;
    137     match cli.command {
    138         Command::Build(args) => {
    139             let stats = build(&BuildOptions {
    140                 input: args.input,
    141                 output: args.output,
    142                 drafts: args.drafts,
    143                 base_url: args.base_url,
    144                 profile: args.profile,
    145             })?;
    146             if json {
    147                 println!(
    148                     "{}",
    149                     serde_json::json!({
    150                         "ok": true,
    151                         "pages": stats.pages_written,
    152                         "output": display_path(&stats.output_dir),
    153                     })
    154                 );
    155             } else {
    156                 println!(
    157                     "{} pages → {}",
    158                     stats.pages_written,
    159                     display_path(&stats.output_dir)
    160                 );
    161             }
    162             Ok(())
    163         }
    164         Command::Init(args) => {
    165             let written = sqzass::init::init(&args.dir).map_err(Kind::Io.tag())?;
    166             if json {
    167                 println!(
    168                     "{}",
    169                     serde_json::json!({
    170                         "ok": true,
    171                         "dir": display_path(&args.dir),
    172                         "files": written,
    173                     })
    174                 );
    175             } else {
    176                 for rel in &written {
    177                     println!("  {}", display_path(&args.dir.join(rel)));
    178                 }
    179                 println!("\nsqzass serve -i {}", display_path(&args.dir));
    180             }
    181             Ok(())
    182         }
    183         Command::Doctor(args) => {
    184             let findings = sqzass::diagnose(&BuildOptions {
    185                 input: args.input,
    186                 output: None,
    187                 drafts: args.drafts,
    188                 base_url: None,
    189                 profile: false,
    190             })?;
    191             let gated = findings
    192                 .iter()
    193                 .filter(|f| f.severity >= args.fail_on)
    194                 .count();
    195 
    196             if json {
    197                 println!(
    198                     "{}",
    199                     serde_json::json!({
    200                         "ok": gated == 0,
    201                         "findings": findings,
    202                         "gated": gated,
    203                     })
    204                 );
    205             } else if findings.is_empty() {
    206                 println!("지적할 것이 없습니다");
    207             } else {
    208                 for f in &findings {
    209                     println!("{f}");
    210                 }
    211                 println!(
    212                     "\n{}건 중 {gated}건이 {} 이상입니다",
    213                     findings.len(),
    214                     args.fail_on
    215                 );
    216             }
    217 
    218             if gated > 0 {
    219                 std::process::exit(sqzass::doctor::FINDINGS_EXIT_CODE);
    220             }
    221             Ok(())
    222         }
    223         Command::Serve(args) => {
    224             // `build`는 동기 경로라 런타임을 서버에서만 만든다.
    225             let rt = tokio::runtime::Runtime::new()?;
    226             rt.block_on(serve(ServeOptions {
    227                 input: args.input,
    228                 bind: args.bind,
    229                 port: args.port,
    230                 drafts: args.drafts,
    231                 base_url: args.base_url,
    232             }))
    233         }
    234     }
    235 }