amberjs 1.16.1

JavaScript/TypeScript runtime built with Rust and V8
Documentation
//! Production container and cloud deployment scaffolding for Amber (`amber deploy`).
//!
//! Generates production-ready artifacts:
//! - Multi-stage lightweight Dockerfile and docker-compose.yml
//! - Standalone self-contained binary distribution via `amber compile`
//! - Kubernetes manifests (Deployment, Service, HPA) with healthcheck probes

use anyhow::{anyhow, Result};
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeployTarget {
    Docker,
    Standalone,
    K8s,
}

impl DeployTarget {
    pub fn from_str(s: &str) -> Result<Self> {
        match s.to_lowercase().as_str() {
            "docker" | "container" => Ok(Self::Docker),
            "standalone" | "binary" | "sea" => Ok(Self::Standalone),
            "k8s" | "kubernetes" => Ok(Self::K8s),
            _ => Err(anyhow!(
                "Unknown deploy target: '{}'. Valid: docker, standalone, k8s",
                s
            )),
        }
    }
}

pub struct DeployOptions {
    pub target: DeployTarget,
    pub output_dir: PathBuf,
    pub port: u16,
    pub name: Option<String>,
    pub entry: Option<PathBuf>,
}

/// Detect project name and entry file
pub fn detect_project_info(dir: &Path) -> (String, PathBuf) {
    let mut app_name = dir
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| "amber-app".to_string());

    let mut entry = PathBuf::from("app.ts");

    let pkg_json_path = dir.join("package.json");
    if pkg_json_path.exists() {
        if let Ok(content) = fs::read_to_string(&pkg_json_path) {
            if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) {
                if let Some(n) = val.get("name").and_then(|n| n.as_str()) {
                    app_name = n.to_string();
                }
                if let Some(m) = val.get("main").and_then(|m| m.as_str()) {
                    entry = PathBuf::from(m);
                }
            }
        }
    }

    if !dir.join(&entry).exists() {
        // Probe common entrypoint files
        for candidate in &[
            "app.ts",
            "server.ts",
            "index.ts",
            "main.ts",
            "app.js",
            "server.js",
            "index.js",
        ] {
            let p = dir.join(candidate);
            if p.exists() {
                entry = PathBuf::from(candidate);
                break;
            }
        }
    }

    (app_name, entry)
}

/// Generate production Docker scaffolding
pub fn generate_docker_scaffold(
    dir: &Path,
    app_name: &str,
    entry: &Path,
    port: u16,
) -> Result<Vec<PathBuf>> {
    let mut generated = Vec::new();

    let dockerfile_path = dir.join("Dockerfile");
    let dockerfile_content = format!(
        r#"# Generated by Amber Deploy (https://github.com/zh30/amberjs)
# -------------------------------------------------------------
# Stage 1: Build & Bundle
# -------------------------------------------------------------
FROM alpine:3.20 AS builder
WORKDIR /build

# Copy project files
COPY . .

# Stage 2: Production Minimal Runtime
# -------------------------------------------------------------
FROM alpine:3.20 AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT={port}
EXPOSE {port}

COPY . .

# Run application using Amber web server mode
CMD ["amber", "serve", "{entry}", "--port", "{port}", "--host", "0.0.0.0"]
"#,
        port = port,
        entry = entry.display()
    );
    fs::write(&dockerfile_path, dockerfile_content)?;
    generated.push(dockerfile_path);

    let dockerignore_path = dir.join(".dockerignore");
    let dockerignore_content = r#"node_modules
.git
.amberjs_cache
dist
coverage
target
*.log
.DS_Store
"#;
    fs::write(&dockerignore_path, dockerignore_content)?;
    generated.push(dockerignore_path);

    let compose_path = dir.join("docker-compose.yml");
    let compose_content = format!(
        r#"version: '3.8'

services:
  {app_name}:
    build: .
    ports:
      - "{port}:{port}"
    environment:
      - NODE_ENV=production
      - PORT={port}
    restart: unless-stopped
"#,
        app_name = app_name,
        port = port
    );
    fs::write(&compose_path, compose_content)?;
    generated.push(compose_path);

    Ok(generated)
}

/// Generate Kubernetes manifests
pub fn generate_k8s_scaffold(dir: &Path, app_name: &str, port: u16) -> Result<Vec<PathBuf>> {
    let k8s_dir = dir.join("k8s");
    fs::create_dir_all(&k8s_dir)?;
    let mut generated = Vec::new();

    let deploy_path = k8s_dir.join("deployment.yaml");
    let deploy_content = format!(
        r#"apiVersion: apps/v1
kind: Deployment
metadata:
  name: {app_name}
  labels:
    app: {app_name}
spec:
  replicas: 2
  selector:
    matchLabels:
      app: {app_name}
  template:
    metadata:
      labels:
        app: {app_name}
    spec:
      containers:
      - name: {app_name}
        image: {app_name}:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: {port}
        resources:
          limits:
            cpu: "1000m"
            memory: "512Mi"
          requests:
            cpu: "100m"
            memory: "128Mi"
        livenessProbe:
          httpGet:
            path: /
            port: {port}
          initialDelaySeconds: 3
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: {port}
          initialDelaySeconds: 2
          periodSeconds: 5
"#,
        app_name = app_name,
        port = port
    );
    fs::write(&deploy_path, deploy_content)?;
    generated.push(deploy_path);

    let svc_path = k8s_dir.join("service.yaml");
    let svc_content = format!(
        r#"apiVersion: v1
kind: Service
metadata:
  name: {app_name}-service
spec:
  type: ClusterIP
  selector:
    app: {app_name}
  ports:
  - port: 80
    targetPort: {port}
"#,
        app_name = app_name,
        port = port
    );
    fs::write(&svc_path, svc_content)?;
    generated.push(svc_path);

    Ok(generated)
}

/// Run deployment scaffolding
pub fn run_deploy(opts: DeployOptions) -> Result<()> {
    let cwd = std::env::current_dir()?;
    let (detected_name, detected_entry) = detect_project_info(&cwd);
    let app_name = opts.name.unwrap_or(detected_name);
    let entry = opts.entry.unwrap_or(detected_entry);

    println!(
        "🚀 Amber Deploy: Project '{}', Entry '{}'",
        app_name,
        entry.display()
    );

    match opts.target {
        DeployTarget::Docker => {
            let files = generate_docker_scaffold(&opts.output_dir, &app_name, &entry, opts.port)?;
            println!("✅ Generated production Docker scaffolding:");
            for f in files {
                println!("  - {}", f.display());
            }
            println!("\nNext steps:");
            println!("  docker compose up --build");
        }
        DeployTarget::Standalone => {
            let dist_dir = opts.output_dir.join("dist");
            fs::create_dir_all(&dist_dir)?;
            let output_bin = dist_dir.join(&app_name);
            crate::tooling::compiler::compile_binary(&entry, &output_bin)?;
            println!("✅ Generated standalone binary executable:");
            println!("  - {}", output_bin.display());
            println!("\nNext steps:");
            println!("  ./{}", output_bin.display());
        }
        DeployTarget::K8s => {
            let files = generate_k8s_scaffold(&opts.output_dir, &app_name, opts.port)?;
            println!("✅ Generated Kubernetes manifests in k8s/:");
            for f in files {
                println!("  - {}", f.display());
            }
            println!("\nNext steps:");
            println!("  kubectl apply -f k8s/");
        }
    }

    Ok(())
}