nixpacks 0.3.11

Generate an OCI compliant image based off app source
Documentation
use std::path::PathBuf;

use super::Provider;
use crate::nixpacks::{
    app::App,
    environment::Environment,
    nix::pkg::Pkg,
    plan::{
        phase::{Phase, StartPhase},
        BuildPlan,
    },
};
use anyhow::{Context, Result};
use path_slash::PathBufExt;
use regex::Regex;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Default, Debug)]
pub struct DenoTasks {
    pub start: Option<String>,
}

#[derive(Serialize, Deserialize, Default, Debug)]
pub struct DenoJson {
    pub tasks: Option<DenoTasks>,
}

pub struct DenoProvider {}

impl Provider for DenoProvider {
    fn name(&self) -> &str {
        "deno"
    }

    fn detect(&self, app: &App, _env: &Environment) -> Result<bool> {
        let re = Regex::new(
            r##"import .+ from (?:"|'|`)https://deno.land/[^"`']+\.(?:ts|js|tsx|jsx)(?:"|'|`);?"##,
        )
        .unwrap();
        Ok(app.includes_file("deno.json")
            || app.includes_file("deno.jsonc")
            || app.find_match(&re, "**/*.{tsx,ts,js,jsx}")?)
    }

    fn get_build_plan(&self, app: &App, _env: &Environment) -> Result<Option<BuildPlan>> {
        let mut plan = BuildPlan::default();

        let setup_phase = Phase::setup(Some(vec![Pkg::new("deno")]));
        plan.add_phase(setup_phase);

        if let Some(build_cmd) = DenoProvider::get_build_cmd(app)? {
            let build_phase = Phase::build(Some(build_cmd));
            plan.add_phase(build_phase);
        };

        if let Some(start_cmd) = DenoProvider::get_start_cmd(app)? {
            let start_phase = StartPhase::new(start_cmd);
            plan.set_start_phase(start_phase);
        }

        Ok(Some(plan))
    }
}

impl DenoProvider {
    fn get_build_cmd(app: &App) -> Result<Option<String>> {
        if let Some(start_file) = DenoProvider::get_start_file(app)? {
            Ok(Some(format!(
                "deno cache {}",
                start_file
                    .to_slash()
                    .context("Failed to convert start_file to slash_path")?
            )))
        } else {
            Ok(None)
        }
    }

    fn get_start_cmd(app: &App) -> Result<Option<String>> {
        // First check for a deno.json and see if we can rip the start command from there
        if app.includes_file("deno.json") {
            let deno_json: DenoJson = app.read_json("deno.json")?;

            if let Some(tasks) = deno_json.tasks {
                if let Some(start) = tasks.start {
                    return Ok(Some(start));
                }
            }
        }

        // Barring that, just try and start the index with sane defaults
        match DenoProvider::get_start_file(app)? {
            Some(start_file) => Ok(Some(format!(
                "deno run --allow-all {}",
                start_file
                    .to_slash()
                    .context("Failed to convert start_file to slash_path")?
            ))),
            None => Ok(None),
        }
    }

    // Find the first index.ts or index.js file to run
    fn get_start_file(app: &App) -> Result<Option<PathBuf>> {
        // Find the first index.ts or index.js file to run
        let matches = app.find_files("**/index.[tj]s")?;
        let path_to_index = match matches.first() {
            Some(m) => m,
            None => return Ok(None),
        };

        let relative_path_to_index = app.strip_source_path(path_to_index)?;
        Ok(Some(relative_path_to_index))
    }
}