use std::path::Path;
use anyhow::{Context, Result};
use clap::ValueEnum;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
pub enum Adapter {
#[default]
Node,
Static,
Bun,
Deno,
Cloudflare,
}
impl Adapter {
fn label(self) -> &'static str {
match self {
Adapter::Node => "node",
Adapter::Static => "static",
Adapter::Bun => "bun",
Adapter::Deno => "deno",
Adapter::Cloudflare => "cloudflare",
}
}
fn run_cmd(self) -> &'static str {
match self {
Adapter::Bun => "bun dist/server/index.mjs",
Adapter::Deno => "deno run -A dist/server/index.mjs",
_ => "node dist/server/index.mjs",
}
}
}
pub fn emit_server(root: &Path, dist: &Path, adapter: Adapter) -> Result<()> {
let server_dir = dist.join("server");
std::fs::create_dir_all(&server_dir)
.with_context(|| format!("dist/server の作成に失敗: {}", server_dir.display()))?;
let runtime_ver =
installed_version(root, "@nowaki-dev/runtime").unwrap_or_else(|| "0.5.0".into());
let preact_ver = installed_version(root, "preact").unwrap_or_else(|| "10.25.0".into());
let prts_ver =
installed_version(root, "preact-render-to-string").unwrap_or_else(|| "6.5.0".into());
let index = format!(
r#"// nowaki デプロイエントリ({label} adapter, 自動生成)。
// `{run}` だけで本番配信できる。nowaki バイナリは不要。
// 依存: @nowaki-dev/runtime / preact / preact-render-to-string(この dir の package.json)。
import path from "node:path";
import {{ fileURLToPath }} from "node:url";
import {{ startServer }} from "@nowaki-dev/runtime/server/app.mjs";
const here = path.dirname(fileURLToPath(import.meta.url)); // dist/server
await startServer({{
clientDir: path.join(here, "../client"),
serverDir: here,
port: Number(process.env.PORT ?? 3000),
}});
"#,
label = adapter.label(),
run = adapter.run_cmd(),
);
std::fs::write(server_dir.join("index.mjs"), index)?;
let pkg = format!(
r#"{{
"name": "nowaki-app-server",
"private": true,
"type": "module",
"scripts": {{
"start": "node index.mjs"
}},
"dependencies": {{
"@nowaki-dev/runtime": "^{runtime_ver}",
"preact": "^{preact_ver}",
"preact-render-to-string": "^{prts_ver}"
}}
}}
"#,
);
std::fs::write(server_dir.join("package.json"), pkg)?;
println!(
"[nowaki] {} adapter: dist/server/index.mjs を出力。配備: `cd dist/server && npm install --omit=dev && {}`",
adapter.label(),
adapter.run_cmd()
);
Ok(())
}
pub fn emit_cloudflare(root: &Path, dist: &Path) -> Result<()> {
let script = root.join("node_modules/@nowaki-dev/runtime/server/edge-build.mjs");
if !script.exists() {
anyhow::bail!("@nowaki-dev/runtime が見つかりません: {}", script.display());
}
let app_name = read_json_field(&root.join("package.json"), "name")
.map(|n| sanitize_worker_name(&n))
.unwrap_or_else(|| "nowaki-app".into());
let status = std::process::Command::new("node")
.arg(&script)
.arg(dist.join("server"))
.arg(dist.join("client"))
.arg(dist.join("worker"))
.arg(&app_name)
.current_dir(root)
.status()
.context("node edge-build.mjs の起動に失敗")?;
if !status.success() {
anyhow::bail!("cloudflare adapter の生成に失敗しました");
}
println!(
"[nowaki] cloudflare adapter: dist/worker を出力。検証: `cd dist/worker && npx wrangler dev` / 配備: `cd dist/worker && npx wrangler deploy`"
);
Ok(())
}
fn sanitize_worker_name(name: &str) -> String {
let s: String = name
.to_lowercase()
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
let trimmed = s.trim_matches('-').to_string();
if trimmed.is_empty() {
"nowaki-app".into()
} else {
trimmed
}
}
fn installed_version(root: &Path, pkg: &str) -> Option<String> {
read_json_field(
&root.join("node_modules").join(pkg).join("package.json"),
"version",
)
}
fn read_json_field(pj: &Path, field: &str) -> Option<String> {
let text = std::fs::read_to_string(pj).ok()?;
let key = format!("\"{field}\"");
let i = text.find(&key)?;
let after = &text[i + key.len()..];
let start = after.find('"')? + 1;
let rest = &after[start..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}