Repository: railwayapp/nixpacks
Pull request #355 (2022-07-22 23:12:21)

Build plan snapshot testing
## What does this PR address?

Use [cargo-insta](https://github.com/mitsuhiko/insta) for snapshot testing the build plan tests. This is useful since it will tests all parts of the build plan for each tests as opposed to just the parts that get manually asserted. This also makes updating the build plan format a lot easier since we don't have to go and edit a 900 line file everytime.

I am not snapshot testing and build plans that rely on `ARCH`. This brings up a larger question in that we should be able to build for other platforms in the tests and not rely on the `ARCH` constant. But that should be fixed in another PR.

Milo123459: I'm not sure, this kind of looks painful to maintain.
coffee-cup: > I'm not sure, this kind of looks painful to maintain.

Insta has tools to automatically review and update the snapshots. The snapshot files should never be edited manually.

This video explains how it works fairly well

https://www.youtube.com/watch?v=rCHrMqE4JOY

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 174904e..f09ed90 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -236,6 +236,20 @@ version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "05a0bd019339e5d968b37855180087b7b9d512c5046fbd244cf8c95687927d6e"
 
+[[package]]
+name = "insta"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36fb7ec420af04ce7d1a422945cd19c52bf01772ead45934cee77f056dca1081"
+dependencies = [
+ "console",
+ "once_cell",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "similar",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.2"
@@ -279,6 +293,7 @@ dependencies = [
  "console",
  "globset",
  "indoc",
+ "insta",
  "regex",
  "serde",
  "serde_json",
@@ -465,6 +480,12 @@ dependencies = [
  "yaml-rust",
 ]
 
+[[package]]
+name = "similar"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
+
 [[package]]
 name = "strsim"
 version = "0.10.0"
diff --git a/Cargo.toml b/Cargo.toml
index 57a925a..f61bbe6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,3 +41,6 @@ console = { default-features = false, version = "0.15.0" }
 box_drawing = "0.1.2"
 textwrap = { default-features = false, version = "0.15.0" }
 cargo_toml = "0.11.5"
+
+[dev-dependencies]
+insta = "1.16.0"
diff --git a/src/nixpacks/app.rs b/src/nixpacks/app.rs
index 9770210..c20ca34 100644
--- a/src/nixpacks/app.rs
+++ b/src/nixpacks/app.rs
@@ -1,5 +1,5 @@
 use anyhow::anyhow;
-use std::collections::HashMap;
+use std::collections::BTreeMap;
 use std::path::Path;
 use std::{env, fs, path::PathBuf};
 
@@ -10,7 +10,7 @@ use serde::de::DeserializeOwned;
 use walkdir::WalkDir;
 
 pub static ASSETS_DIR: &str = "/assets/";
-pub type StaticAssets = HashMap<String, String>;
+pub type StaticAssets = BTreeMap<String, String>;
 
 #[derive(Debug, Clone)]
 pub struct App {
diff --git a/src/nixpacks/environment.rs b/src/nixpacks/environment.rs
index 1ec35c9..5499e03 100644
--- a/src/nixpacks/environment.rs
+++ b/src/nixpacks/environment.rs
@@ -1,7 +1,7 @@
 use anyhow::{bail, Result};
-use std::{collections::HashMap, env};
+use std::{collections::BTreeMap, env};
 
-pub type EnvironmentVariables = HashMap<String, String>;
+pub type EnvironmentVariables = BTreeMap<String, String>;
 
 #[derive(Default, Debug)]
 pub struct Environment {
diff --git a/src/providers/clojure.rs b/src/providers/clojure.rs
index 7ecfd2c..60dfe63 100644
--- a/src/providers/clojure.rs
+++ b/src/providers/clojure.rs
@@ -123,7 +123,7 @@ impl ClojureProvider {
 mod test {
     use super::*;
     use crate::nixpacks::{app::App, environment::Environment, nix::pkg::Pkg};
-    use std::collections::HashMap;
+    use std::collections::BTreeMap;
 
     #[test]
     fn test_no_version() -> Result<()> {
@@ -169,7 +169,7 @@ mod test {
         assert_eq!(
             ClojureProvider::get_nix_jdk_package(
                 &App::new("./examples/clojure-jdk-latest")?,
-                &Environment::new(HashMap::from([(
+                &Environment::new(BTreeMap::from([(
                     "NIXPACKS_JDK_VERSION".to_string(),
                     "latest".to_string()
                 )]))
@@ -185,7 +185,7 @@ mod test {
         assert_eq!(
             ClojureProvider::get_nix_jdk_package(
                 &App::new("./examples/clojure")?,
-                &Environment::new(HashMap::from([(
+                &Environment::new(BTreeMap::from([(
                     "NIXPACKS_JDK_VERSION".to_string(),
                     "11".to_string()
                 )]))
diff --git a/src/providers/node.rs b/src/providers/node.rs
index d367d77..ededde0 100644
--- a/src/providers/node.rs
+++ b/src/providers/node.rs
@@ -390,6 +390,8 @@ fn parse_regex_into_pkg(re: &Regex, node_version: String) -> Option<String> {
 
 #[cfg(test)]
 mod test {
+    use std::collections::BTreeMap;
+
     use super::*;
 
     fn engines_node(version: &str) -> Option<HashMap<String, String>> {
@@ -500,7 +502,7 @@ mod test {
                     name: Some(String::default()),
                     ..Default::default()
                 },
-                &Environment::new(HashMap::from([(
+                &Environment::new(BTreeMap::from([(
                     "NIXPACKS_NODE_VERSION".to_string(),
                     "14".to_string()
                 )]))
diff --git a/src/providers/python.rs b/src/providers/python.rs
index 3de1359..6d1bed3 100644
--- a/src/providers/python.rs
+++ b/src/providers/python.rs
@@ -335,7 +335,7 @@ impl PythonProvider {
 mod test {
     use super::*;
     use crate::nixpacks::{app::App, environment::Environment, nix::pkg::Pkg};
-    use std::collections::HashMap;
+    use std::collections::BTreeMap;
 
     #[test]
     fn test_no_version() -> Result<()> {
@@ -368,7 +368,7 @@ mod test {
         assert_eq!(
             PythonProvider::get_nix_python_package(
                 &App::new("./examples/python")?,
-                &Environment::new(HashMap::from([(
+                &Environment::new(BTreeMap::from([(
                     "NIXPACKS_PYTHON_VERSION".to_string(),
                     "2.7".to_string()
                 )]))
diff --git a/src/providers/rust.rs b/src/providers/rust.rs
index 129c1e5..4b5879c 100644
--- a/src/providers/rust.rs
+++ b/src/providers/rust.rs
@@ -238,7 +238,7 @@ impl RustProvider {
 #[cfg(test)]
 mod test {
     use super::*;
-    use std::collections::HashMap;
+    use std::collections::BTreeMap;
 
     #[test]
     fn test_no_version() -> Result<()> {
@@ -284,7 +284,7 @@ mod test {
         assert_eq!(
             RustProvider::get_rust_pkg(
                 &App::new("./examples/rust-custom-toolchain")?,
-                &Environment::new(HashMap::from([(
+                &Environment::new(BTreeMap::from([(
                     "NIXPACKS_RUST_VERSION".to_string(),
                     "1.54.0".to_string()
                 )]))


Test patch:
diff --git a/tests/generate_plan_tests.rs b/tests/generate_plan_tests.rs
index 14da2f9..fa8824d 100644
--- a/tests/generate_plan_tests.rs
+++ b/tests/generate_plan_tests.rs
@@ -1,5 +1,3 @@
-use anyhow::Result;
-use nixpacks::providers::node::NODE_OVERLAY;
 use nixpacks::{
     generate_build_plan,
     nixpacks::{
@@ -14,463 +12,168 @@ fn simple_gen_plan(path: &str) -> BuildPlan {
 }
 
 #[test]
-fn test_node() -> Result<()> {
+fn test_node() {
     let plan = simple_gen_plan("./examples/node");
-    assert_eq!(
-        plan.install.clone().unwrap().cmds,
-        Some(vec!["npm ci".to_string()])
-    );
-    assert_eq!(
-        plan.install.unwrap().cache_directories,
-        Some(vec!["/root/.npm".to_string()])
-    );
-    assert_eq!(plan.build.unwrap().cmds, None);
-    assert_eq!(plan.start.unwrap().cmd, Some("npm run start".to_string()));
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_node_no_lockfile() -> Result<()> {
+fn test_node_no_lockfile() {
     let plan = simple_gen_plan("./examples/node-no-lockfile-canvas");
-    assert_eq!(plan.install.unwrap().cmds, Some(vec!["npm i".to_string()]));
-    assert_eq!(plan.build.unwrap().cmds, None);
-    assert_eq!(plan.start.unwrap().cmd, Some("npm run start".to_string()));
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![
-            Pkg::new("nodejs"),
-            Pkg::new("npm-8_x").from_overlay(NODE_OVERLAY)
-        ]
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_node_npm_old_lockfile() -> Result<()> {
+fn test_node_npm_old_lockfile() {
     let plan = simple_gen_plan("./examples/node-npm-old-lockfile");
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![
-            Pkg::new("nodejs"),
-            Pkg::new("npm-6_x").from_overlay(NODE_OVERLAY)
-        ]
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_npm() -> Result<()> {
+fn test_npm() {
     let plan = simple_gen_plan("./examples/node-npm");
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["npm run build".to_string()])
-    );
-    assert_eq!(plan.start.unwrap().cmd, Some("npm run start".to_string()));
-    assert_eq!(
-        plan.variables.clone().unwrap().get("NODE_ENV"),
-        Some(&"production".to_string())
-    );
-    assert_eq!(
-        plan.variables.unwrap().get("NPM_CONFIG_PRODUCTION"),
-        Some(&"false".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_node_no_scripts() -> Result<()> {
+fn test_node_no_scripts() {
     let plan = simple_gen_plan("./examples/node-no-scripts");
-    assert_eq!(plan.build.unwrap().cmds, None);
-    assert_eq!(plan.start.unwrap().cmd, Some("node index.js".to_string()));
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_node_custom_version() -> Result<()> {
+fn test_node_custom_version() {
     let plan = simple_gen_plan("./examples/node-custom-version");
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![
-            Pkg::new("nodejs-18_x"),
-            Pkg::new("npm-8_x").from_overlay(NODE_OVERLAY)
-        ]
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_node_monorepo() -> Result<()> {
+fn test_node_monorepo() {
     let plan = simple_gen_plan("./examples/node-monorepo");
-    assert_eq!(
-        plan.install.clone().unwrap().cmds,
-        Some(vec!["yarn install --frozen-lockfile".to_string()])
-    );
-    assert_eq!(
-        plan.install.unwrap().cache_directories,
-        Some(vec!["/usr/local/share/.cache/yarn/v6".to_string()])
-    );
-    assert_eq!(plan.build.unwrap().cmds, None);
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_yarn() -> Result<()> {
+fn test_yarn() {
     let plan = simple_gen_plan("./examples/node-yarn");
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec!["yarn install --frozen-lockfile".to_string()])
-    );
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["yarn run build".to_string()])
-    );
-    assert_eq!(plan.start.unwrap().cmd, Some("yarn run start".to_string()));
-    assert_eq!(
-        plan.variables.clone().unwrap().get("NODE_ENV"),
-        Some(&"production".to_string())
-    );
-    assert_eq!(
-        plan.variables.unwrap().get("NPM_CONFIG_PRODUCTION"),
-        Some(&"false".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_yarn_berry() -> Result<()> {
+fn test_yarn_berry() {
     let plan = simple_gen_plan("./examples/node-yarn-berry");
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec![
-            "yarn set version berry && yarn install --check-cache".to_string()
-        ])
-    );
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_yarn_custom_version() -> Result<()> {
+fn test_yarn_custom_version() {
     let plan = simple_gen_plan("./examples/node-yarn-custom-node-version");
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![
-            Pkg::new("nodejs-14_x"),
-            Pkg::new("yarn-1_x").from_overlay(NODE_OVERLAY)
-        ]
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_pnpm() -> Result<()> {
+fn test_pnpm() {
     let plan = simple_gen_plan("./examples/node-pnpm");
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![
-            Pkg::new("nodejs"),
-            Pkg::new("pnpm-6_x").from_overlay(NODE_OVERLAY)
-        ]
-    );
-    assert_eq!(
-        plan.install.clone().unwrap().cmds,
-        Some(vec!["pnpm i --frozen-lockfile".to_string()])
-    );
-    assert_eq!(
-        plan.install.unwrap().cache_directories,
-        Some(vec!["/root/.cache/pnpm".to_string()])
-    );
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["pnpm run build".to_string()])
-    );
-    assert_eq!(plan.start.unwrap().cmd, Some("pnpm run start".to_string()));
-    assert_eq!(
-        plan.variables.clone().unwrap().get("NODE_ENV"),
-        Some(&"production".to_string())
-    );
-    assert_eq!(
-        plan.variables.unwrap().get("NPM_CONFIG_PRODUCTION"),
-        Some(&"false".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_bun() -> Result<()> {
+fn test_bun() {
     let plan = simple_gen_plan("./examples/node-bun");
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![Pkg::new("bun").from_overlay(NODE_OVERLAY)]
-    );
-    assert_eq!(
-        plan.install.clone().unwrap().cmds,
-        Some(vec!["bun i --no-save".to_string()])
-    );
-    assert_eq!(
-        plan.install.unwrap().cache_directories,
-        Some(vec!["/root/.bun".to_string()])
-    );
-    assert_eq!(plan.start.unwrap().cmd, Some("bun run start".to_string()));
-    assert_eq!(
-        plan.variables.clone().unwrap().get("NODE_ENV"),
-        Some(&"production".to_string())
-    );
-    assert_eq!(
-        plan.variables.unwrap().get("NPM_CONFIG_PRODUCTION"),
-        Some(&"false".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_bun_no_start() -> Result<()> {
+fn test_bun_no_start() {
     let plan = simple_gen_plan("./examples/node-bun-no-start");
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![Pkg::new("bun").from_overlay(NODE_OVERLAY)]
-    );
-    assert_eq!(
-        plan.install.clone().unwrap().cmds,
-        Some(vec!["bun i --no-save".to_string()])
-    );
-    assert_eq!(
-        plan.install.unwrap().cache_directories,
-        Some(vec!["/root/.bun".to_string()])
-    );
-    assert_eq!(plan.start.unwrap().cmd, Some("bun index.ts".to_string()));
-    assert_eq!(
-        plan.variables.clone().unwrap().get("NODE_ENV"),
-        Some(&"production".to_string())
-    );
-    assert_eq!(
-        plan.variables.unwrap().get("NPM_CONFIG_PRODUCTION"),
-        Some(&"false".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_bun_web_server() -> Result<()> {
+fn test_bun_web_server() {
     let plan = simple_gen_plan("./examples/node-bun-no-start");
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![Pkg::new("bun").from_overlay(NODE_OVERLAY)]
-    );
-    assert_eq!(
-        plan.install.clone().unwrap().cmds,
-        Some(vec!["bun i --no-save".to_string()])
-    );
-    assert_eq!(
-        plan.install.unwrap().cache_directories,
-        Some(vec!["/root/.bun".to_string()])
-    );
-    assert_eq!(plan.start.unwrap().cmd, Some("bun index.ts".to_string()));
-    assert_eq!(
-        plan.variables.clone().unwrap().get("NODE_ENV"),
-        Some(&"production".to_string())
-    );
-    assert_eq!(
-        plan.variables.unwrap().get("NPM_CONFIG_PRODUCTION"),
-        Some(&"false".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_pnpm_v7() -> Result<()> {
+fn test_pnpm_v7() {
     let plan = simple_gen_plan("./examples/node-pnpm-v7");
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![
-            Pkg::new("nodejs"),
-            Pkg::new("pnpm-7_x").from_overlay(NODE_OVERLAY)
-        ]
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_pnpm_custom_version() -> Result<()> {
+fn test_pnpm_custom_version() {
     let plan = simple_gen_plan("./examples/node-pnpm-custom-node-version");
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![
-            Pkg::new("nodejs-14_x"),
-            Pkg::new("pnpm-6_x").from_overlay(NODE_OVERLAY)
-        ]
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_go() -> Result<()> {
+fn test_go() {
     let plan = simple_gen_plan("./examples/go");
-    assert_eq!(
-        plan.build.clone().unwrap().cmds,
-        Some(vec!["go build -o out main.go".to_string()])
-    );
-    assert_eq!(
-        plan.build.unwrap().cache_directories,
-        Some(vec!["/root/.cache/go-build".to_string()])
-    );
-    assert_eq!(plan.start.clone().unwrap().cmd, Some("./out".to_string()));
-    assert!(plan.start.unwrap().run_image.is_some());
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_go_cgo_enabled() -> Result<()> {
+fn test_go_cgo_enabled() {
     let plan = generate_build_plan(
         "./examples/go",
         vec!["CGO_ENABLED=1"],
         &GeneratePlanOptions::default(),
-    )?;
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["go build -o out main.go".to_string()])
-    );
-    assert_eq!(plan.start.clone().unwrap().cmd, Some("./out".to_string()));
-    assert!(plan.start.unwrap().run_image.is_none());
-
-    Ok(())
+    )
+    .unwrap();
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_go_mod() -> Result<()> {
+fn test_go_mod() {
     let plan = simple_gen_plan("./examples/go-mod");
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["go build -o out".to_string()])
-    );
-    assert_eq!(plan.start.unwrap().cmd, Some("./out".to_string()));
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_go_custom_version() -> Result<()> {
+fn test_go_custom_version() {
     let plan = simple_gen_plan("./examples/go-custom-version");
-    assert_eq!(plan.setup.unwrap().pkgs, vec![Pkg::new("go_1_18")]);
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_deno() -> Result<()> {
+fn test_deno() {
     let plan = simple_gen_plan("./examples/deno");
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["deno cache src/index.ts".to_string()])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("deno run --allow-all src/index.ts".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_deno_fresh() -> Result<()> {
+fn test_deno_fresh() {
     let plan = simple_gen_plan("./examples/deno-fresh");
-    assert_eq!(plan.build.unwrap().cmds, None);
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("deno run -A dev.ts".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_csharp_api() -> Result<()> {
+fn test_csharp_api() {
     let plan = simple_gen_plan("./examples/csharp-api");
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec!["dotnet restore".to_string()])
-    );
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec![
-            "dotnet publish --no-restore -c Release -o out".to_string()
-        ])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("./out/csharp-api".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_fsharp_api() -> Result<()> {
+fn test_fsharp_api() {
     let plan = simple_gen_plan("./examples/fsharp-api");
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec!["dotnet restore".to_string()])
-    );
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec![
-            "dotnet publish --no-restore -c Release -o out".to_string()
-        ])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("./out/fsharp-api".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_csharp_cli() -> Result<()> {
+fn test_csharp_cli() {
     let plan = simple_gen_plan("./examples/csharp-cli");
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec!["dotnet restore".to_string()])
-    );
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec![
-            "dotnet publish --no-restore -c Release -o out".to_string()
-        ])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("./out/csharp-cli".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_procfile() -> Result<()> {
+fn test_procfile() {
     let plan = simple_gen_plan("./examples/procfile");
-    assert_eq!(plan.start.unwrap().cmd, Some("node index.js".to_string()));
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_custom_pkgs() -> Result<()> {
+fn test_custom_pkgs() {
     let plan = generate_build_plan(
         "./examples/shell-hello",
         Vec::new(),
@@ -479,15 +182,13 @@ fn test_custom_pkgs() -> Result<()> {
             custom_pkgs: vec![Pkg::new("cowsay")],
             ..Default::default()
         },
-    )?;
-    assert_eq!(plan.setup.unwrap().pkgs, vec![Pkg::new("cowsay")]);
-    assert_eq!(plan.start.unwrap().cmd, Some("./start.sh".to_string()));
-
-    Ok(())
+    )
+    .unwrap();
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_pin_archive() -> Result<()> {
+fn test_pin_archive() {
     let plan = generate_build_plan(
         "./examples/shell-hello",
         Vec::new(),
@@ -495,14 +196,13 @@ fn test_pin_archive() -> Result<()> {
             pin_pkgs: true,
             ..Default::default()
         },
-    )?;
-    assert!(plan.setup.unwrap().archive.is_some());
-
-    Ok(())
+    )
+    .unwrap();
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_custom_rust_version() -> Result<()> {
+fn test_custom_rust_version() {
     let plan = simple_gen_plan("./examples/rust-custom-version");
     assert_eq!(
         plan.build.unwrap().cmds,
@@ -523,12 +223,10 @@ fn test_custom_rust_version() -> Result<()> {
             .count(),
         1
     );
-
-    Ok(())
 }
 
 #[test]
-fn test_rust_rocket() -> Result<()> {
+fn test_rust_rocket() {
     let plan = simple_gen_plan("./examples/rust-rocket");
     assert_eq!(
         plan.build.unwrap().cmds,
@@ -546,98 +244,51 @@ fn test_rust_rocket() -> Result<()> {
         "./rocket".to_string()
     );
     assert!(plan.start.unwrap().run_image.is_some());
-
-    Ok(())
 }
 
 #[test]
-fn test_rust_rocket_no_musl() -> Result<()> {
+fn test_rust_rocket_no_musl() {
     let plan = generate_build_plan(
         "./examples/rust-rocket",
         vec!["NIXPACKS_NO_MUSL=1"],
         &GeneratePlanOptions::default(),
-    )?;
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec![
-            "cargo build --release".to_string(),
-            "cp target/release/rocket rocket".to_string()
-        ])
-    );
-    assert!(plan
-        .start
-        .clone()
-        .unwrap()
-        .cmd
-        .unwrap()
-        .contains("./rocket"));
-    assert!(plan.start.unwrap().run_image.is_none());
-
-    Ok(())
+    )
+    .unwrap();
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-pub fn test_python() -> Result<()> {
+pub fn test_python() {
     let plan = simple_gen_plan("./examples/python");
-    assert_eq!(plan.build.unwrap().cmds, None);
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec!["python -m venv /opt/venv && . /opt/venv/bin/activate && pip install -r requirements.txt".to_string()])
-    );
-    assert_eq!(plan.start.unwrap().cmd, Some("python main.py".to_string()));
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-pub fn test_python_poetry() -> Result<()> {
+pub fn test_python_poetry() {
     let plan = simple_gen_plan("./examples/python-poetry");
-    assert_eq!(plan.build.unwrap().cmds, None);
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec!["python -m venv /opt/venv && . /opt/venv/bin/activate && pip install poetry==$NIXPACKS_POETRY_VERSION && poetry install --no-dev --no-interaction --no-ansi".to_string()])
-    );
-    assert_eq!(plan.start.unwrap().cmd, Some("python main.py".to_string()));
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_node_main_file() -> Result<()> {
+fn test_node_main_file() {
     let plan = simple_gen_plan("./examples/node-main-file");
-    assert_eq!(plan.build.unwrap().cmds, None);
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("node src/index.js".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-pub fn test_python_setuptools() -> Result<()> {
+pub fn test_python_setuptools() {
     let plan = simple_gen_plan("./examples/python-setuptools");
-    assert_eq!(plan.install.unwrap().cmds, Some(vec!["python -m venv /opt/venv && . /opt/venv/bin/activate && pip install --upgrade build setuptools && pip install .".to_string()]));
-    assert_eq!(plan.build.unwrap().cmds, None);
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("python -m nixpacks-setuptools".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_node_main_file_doesnt_exist() -> Result<()> {
+fn test_node_main_file_doesnt_exist() {
     let plan = simple_gen_plan("./examples/node-main-file-not-exist");
-    assert_eq!(plan.build.unwrap().cmds, None);
-    assert_eq!(plan.start.unwrap().cmd, Some("node index.js".to_string()));
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_haskell_stack() -> Result<()> {
+fn test_haskell_stack() {
     let plan = simple_gen_plan("./examples/haskell-stack");
     assert_eq!(
         plan.install.unwrap().cmds,
@@ -651,41 +302,27 @@ fn test_haskell_stack() -> Result<()> {
         plan.start.unwrap().cmd,
         Some("/root/.local/bin/haskell-stack-exe".to_string())
     );
-    Ok(())
 }
 
 #[test]
-fn test_crystal() -> Result<()> {
+fn test_crystal() {
     let plan = simple_gen_plan("./examples/crystal");
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec!["shards install".to_string()])
-    );
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["shards build --release".to_string()])
-    );
-    assert_eq!(plan.start.unwrap().cmd, Some("./bin/crystal".to_string()));
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_overriding_environment_variables() -> Result<()> {
+fn test_overriding_environment_variables() {
     let plan = generate_build_plan(
         "./examples/node-variables",
         vec!["NODE_ENV=test"],
         &GeneratePlanOptions::default(),
-    )?;
-    assert_eq!(
-        plan.variables.unwrap().get("NODE_ENV"),
-        Some(&"test".to_string())
-    );
-
-    Ok(())
+    )
+    .unwrap();
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_config_from_environment_variables() -> Result<()> {
+fn test_config_from_environment_variables() {
     let plan = generate_build_plan(
         "./examples/shell-hello",
         vec![
@@ -698,194 +335,71 @@ fn test_config_from_environment_variables() -> Result<()> {
             "NIXPACKS_BUILD_CACHE_DIRS=/build,barbaz",
         ],
         &GeneratePlanOptions::default(),
-    )?;
+    )
+    .unwrap();
 
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![Pkg::new("cowsay"), Pkg::new("ripgrep")]
-    );
-
-    assert_eq!(
-        plan.install.clone().unwrap().cmds,
-        Some(vec!["install".to_string()])
-    );
-    assert_eq!(
-        plan.install.unwrap().cache_directories,
-        Some(vec!["/tmp".to_string(), "foobar".to_string()])
-    );
-
-    assert_eq!(
-        plan.build.clone().unwrap().cmds,
-        Some(vec!["build".to_string()])
-    );
-    assert_eq!(
-        plan.build.unwrap().cache_directories,
-        Some(vec!["/build".to_string(), "barbaz".to_string()])
-    );
-
-    assert_eq!(plan.start.clone().unwrap().cmd, Some("start".to_string()));
-    assert_eq!(plan.start.unwrap().run_image, Some("alpine".to_string()));
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_staticfile() -> Result<()> {
+fn test_staticfile() {
     let plan = simple_gen_plan("./examples/staticfile");
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec![
-            "mkdir /etc/nginx/ /var/log/nginx/ /var/cache/nginx/".to_string()
-        ])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("[[ -z \"${PORT}\" ]] && echo \"Environment variable PORT not found. Using PORT 80\" || sed -i \"s/0.0.0.0:80/$PORT/g\" /assets/nginx.conf && nginx -c /assets/nginx.conf".to_string())
-    );
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_php_vanilla() -> Result<()> {
+fn test_php_vanilla() {
     let plan = simple_gen_plan("./examples/php-vanilla");
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec![
-            "mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx".to_string()
-        ])
-    );
-    assert_eq!(plan.build.unwrap().cmds, None);
-    assert!(plan
-        .start
-        .unwrap()
-        .cmd
-        .unwrap()
-        .contains("nginx -c /nginx.conf"));
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_php_laravel() -> Result<()> {
+fn test_php_laravel() {
     let plan = simple_gen_plan("./examples/php-laravel");
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec![
-            "mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx".to_string(),
-            "composer install".to_string(),
-            "npm i".to_string()
-        ])
-    );
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["npm run prod".to_string()])
-    );
-    assert!(plan
-        .start
-        .unwrap()
-        .cmd
-        .unwrap()
-        .contains("nginx -c /nginx.conf"));
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_dart() -> Result<()> {
+fn test_dart() {
     let plan = simple_gen_plan("./examples/dart");
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec!["dart pub get".to_string()])
-    );
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["dart compile exe bin/console_simple.dart".to_string()])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("./bin/console_simple.exe".to_string())
-    );
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_swift() -> Result<()> {
+fn test_swift() {
     let plan = simple_gen_plan("./examples/swift");
-
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec![
-            "CC=clang++ swift build -c release --static-swift-stdlib".to_string(),
-            "cp ./.build/release/swift ./swift && rm -rf ./.build".to_string()
-        ])
-    );
-
-    assert_eq!(plan.start.unwrap().cmd, Some("./swift".to_owned()));
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_swift_vapor() -> Result<()> {
+fn test_swift_vapor() {
     let plan = simple_gen_plan("./examples/swift-vapor");
-
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec![
-            "CC=clang++ swift build -c release --static-swift-stdlib".to_string(),
-            "cp ./.build/release/Run ./Run && rm -rf ./.build".to_string()
-        ])
-    );
-
-    assert_eq!(plan.start.unwrap().cmd, Some("./Run".to_owned()));
-
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_java_maven() -> Result<()> {
+fn test_java_maven() {
     let plan = simple_gen_plan("./examples/java-maven");
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["mvn -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install".to_string()])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("java -Dserver.port=$PORT $JAVA_OPTS -jar target/*jar".to_string())
-    );
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_java_maven_wrapper() -> Result<()> {
+fn test_java_maven_wrapper() {
     let plan = simple_gen_plan("./examples/java-maven-wrapper");
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["./mvnw -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install".to_string()])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("java -Dserver.port=$PORT $JAVA_OPTS -jar target/*jar".to_string())
-    );
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_zig() -> Result<()> {
+fn test_zig() {
     let plan = simple_gen_plan("./examples/zig");
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec!["zig build -Drelease-safe=true".to_string()])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("./zig-out/bin/zig".to_string())
-    );
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[cfg(any(target_arch = "aarch64", target_arch = "x86_64", target_arch = "i386"))]
 #[test]
-fn test_zig_gyro() -> Result<()> {
+fn test_zig_gyro() {
     let plan = simple_gen_plan("./examples/zig-gyro");
+
     assert_eq!(
         plan.build.unwrap().cmds,
         Some(vec!["zig build -Drelease-safe=true".to_string()])
@@ -902,80 +416,28 @@ fn test_zig_gyro() -> Result<()> {
         .get(0)
         .unwrap()
         .contains("mkdir /gyro"));
-    Ok(())
 }
 
 #[test]
-fn test_ruby_rails() -> Result<()> {
+fn test_ruby_rails() {
     let plan = simple_gen_plan("./examples/ruby-rails-postgres");
-    assert_eq!(
-        plan.setup.unwrap().apt_pkgs,
-        Some(vec!["procps".to_string(), "libpq-dev".to_string()])
-    );
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec!["bundle install".to_string()])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some(
-            "rake db:migrate && bundle exec bin/rails server -b 0.0.0.0 -p ${PORT:-3000}"
-                .to_string()
-        )
-    );
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_ruby_sinatra() -> Result<()> {
+fn test_ruby_sinatra() {
     let plan = simple_gen_plan("./examples/ruby-sinatra");
-    assert_eq!(
-        plan.install.unwrap().cmds,
-        Some(vec!["bundle install".to_string()])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("RACK_ENV=production bundle exec puma".to_string())
-    );
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_clojure() -> Result<()> {
+fn test_clojure() {
     let plan = simple_gen_plan("./examples/clojure");
-    let move_file_cmd = "if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi";
-
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![Pkg::new("leiningen"), Pkg::new("jdk8"),]
-    );
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec![format!("{}; {}", "lein uberjar", move_file_cmd)])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("java $JAVA_OPTS -jar /app/target/*standalone.jar".to_string())
-    );
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
 
 #[test]
-fn test_clojure_ring_app() -> Result<()> {
+fn test_clojure_ring_app() {
     let plan = simple_gen_plan("./examples/clojure-ring-app");
-    let move_file_cmd = "if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi";
-
-    assert_eq!(
-        plan.setup.unwrap().pkgs,
-        vec![Pkg::new("leiningen"), Pkg::new("jdk8"),]
-    );
-    assert_eq!(
-        plan.build.unwrap().cmds,
-        Some(vec![format!("{}; {}", "lein ring uberjar", move_file_cmd)])
-    );
-    assert_eq!(
-        plan.start.unwrap().cmd,
-        Some("java $JAVA_OPTS -jar /app/target/*standalone.jar".to_string())
-    );
-    Ok(())
+    insta::assert_json_snapshot!(plan);
 }
diff --git a/tests/snapshots/generate_plan_tests__bun.snap b/tests/snapshots/generate_plan_tests__bun.snap
new file mode 100644
index 0000000..29fa139
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__bun.snap
@@ -0,0 +1,39 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "bun",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "bun i --no-save"
+    ],
+    "cacheDirectories": [
+      "/root/.bun"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "bun run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__bun_no_start.snap b/tests/snapshots/generate_plan_tests__bun_no_start.snap
new file mode 100644
index 0000000..75dac40
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__bun_no_start.snap
@@ -0,0 +1,39 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "bun",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "bun i --no-save"
+    ],
+    "cacheDirectories": [
+      "/root/.bun"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "bun index.ts"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__bun_web_server.snap b/tests/snapshots/generate_plan_tests__bun_web_server.snap
new file mode 100644
index 0000000..75dac40
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__bun_web_server.snap
@@ -0,0 +1,39 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "bun",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "bun i --no-save"
+    ],
+    "cacheDirectories": [
+      "/root/.bun"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "bun index.ts"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__clojure.snap b/tests/snapshots/generate_plan_tests__clojure.snap
new file mode 100644
index 0000000..3f421f1
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__clojure.snap
@@ -0,0 +1,38 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "leiningen"
+      },
+      {
+        "name": "jdk8"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      ""
+    ],
+    "cacheDirectories": [
+      "~/.m2"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "lein uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
+    ]
+  },
+  "start": {
+    "cmd": "java $JAVA_OPTS -jar /app/target/*standalone.jar"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__clojure_ring_app.snap b/tests/snapshots/generate_plan_tests__clojure_ring_app.snap
new file mode 100644
index 0000000..afe2735
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__clojure_ring_app.snap
@@ -0,0 +1,38 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "leiningen"
+      },
+      {
+        "name": "jdk8"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      ""
+    ],
+    "cacheDirectories": [
+      "~/.m2"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "lein ring uberjar; if [ -f /app/target/uberjar/*standalone.jar ]; then  mv /app/target/uberjar/*standalone.jar /app/target/*standalone.jar; fi"
+    ]
+  },
+  "start": {
+    "cmd": "java $JAVA_OPTS -jar /app/target/*standalone.jar"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap b/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
new file mode 100644
index 0000000..a636942
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__config_from_environment_variables.snap
@@ -0,0 +1,52 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "cowsay"
+      },
+      {
+        "name": "ripgrep"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "install"
+    ],
+    "cacheDirectories": [
+      "/tmp",
+      "foobar"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "build"
+    ],
+    "cacheDirectories": [
+      "/build",
+      "barbaz"
+    ]
+  },
+  "start": {
+    "cmd": "start",
+    "runImage": "alpine"
+  },
+  "variables": {
+    "NIXPACKS_BUILD_CACHE_DIRS": "/build,barbaz",
+    "NIXPACKS_BUILD_CMD": "build",
+    "NIXPACKS_INSTALL_CACHE_DIRS": "/tmp,foobar",
+    "NIXPACKS_INSTALL_CMD": "install",
+    "NIXPACKS_PKGS": "cowsay ripgrep",
+    "NIXPACKS_RUN_IMAGE": "alpine",
+    "NIXPACKS_START_CMD": "start"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__crystal.snap b/tests/snapshots/generate_plan_tests__crystal.snap
new file mode 100644
index 0000000..20d4192
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__crystal.snap
@@ -0,0 +1,35 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "crystal"
+      },
+      {
+        "name": "shards"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "shards install"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "shards build --release"
+    ]
+  },
+  "start": {
+    "cmd": "./bin/crystal"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__csharp_api.snap b/tests/snapshots/generate_plan_tests__csharp_api.snap
new file mode 100644
index 0000000..56c9c4a
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__csharp_api.snap
@@ -0,0 +1,36 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "dotnet-sdk"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "dotnet restore"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "dotnet publish --no-restore -c Release -o out"
+    ]
+  },
+  "start": {
+    "cmd": "./out/csharp-api"
+  },
+  "variables": {
+    "ASPNETCORE_ENVIRONMENT": "Production",
+    "ASPNETCORE_URLS": "http://0.0.0.0:3000",
+    "DOTNET_ROOT": "/nix/var/nix/profiles/default/"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__csharp_cli.snap b/tests/snapshots/generate_plan_tests__csharp_cli.snap
new file mode 100644
index 0000000..8aac0cb
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__csharp_cli.snap
@@ -0,0 +1,36 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "dotnet-sdk"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "dotnet restore"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "dotnet publish --no-restore -c Release -o out"
+    ]
+  },
+  "start": {
+    "cmd": "./out/csharp-cli"
+  },
+  "variables": {
+    "ASPNETCORE_ENVIRONMENT": "Production",
+    "ASPNETCORE_URLS": "http://0.0.0.0:3000",
+    "DOTNET_ROOT": "/nix/var/nix/profiles/default/"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__custom_pkgs.snap b/tests/snapshots/generate_plan_tests__custom_pkgs.snap
new file mode 100644
index 0000000..160833e
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__custom_pkgs.snap
@@ -0,0 +1,24 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "cowsay"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {},
+  "start": {
+    "cmd": "./start.sh"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__dart.snap b/tests/snapshots/generate_plan_tests__dart.snap
new file mode 100644
index 0000000..7c4047e
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__dart.snap
@@ -0,0 +1,35 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "dart"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "dart pub get"
+    ],
+    "onlyIncludeFiles": [
+      "pubspec.yaml"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "dart compile exe bin/console_simple.dart"
+    ]
+  },
+  "start": {
+    "cmd": "./bin/console_simple.exe"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__deno.snap b/tests/snapshots/generate_plan_tests__deno.snap
new file mode 100644
index 0000000..c6e2536
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__deno.snap
@@ -0,0 +1,28 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "deno"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {
+    "cmds": [
+      "deno cache src/index.ts"
+    ]
+  },
+  "start": {
+    "cmd": "deno run --allow-all src/index.ts"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__deno_fresh.snap b/tests/snapshots/generate_plan_tests__deno_fresh.snap
new file mode 100644
index 0000000..7a4e181
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__deno_fresh.snap
@@ -0,0 +1,24 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "deno"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {},
+  "start": {
+    "cmd": "deno run -A dev.ts"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__fsharp_api.snap b/tests/snapshots/generate_plan_tests__fsharp_api.snap
new file mode 100644
index 0000000..f3ae83c
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__fsharp_api.snap
@@ -0,0 +1,36 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "dotnet-sdk"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "dotnet restore"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "dotnet publish --no-restore -c Release -o out"
+    ]
+  },
+  "start": {
+    "cmd": "./out/fsharp-api"
+  },
+  "variables": {
+    "ASPNETCORE_ENVIRONMENT": "Production",
+    "ASPNETCORE_URLS": "http://0.0.0.0:3000",
+    "DOTNET_ROOT": "/nix/var/nix/profiles/default/"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__go.snap b/tests/snapshots/generate_plan_tests__go.snap
new file mode 100644
index 0000000..4ff6abb
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__go.snap
@@ -0,0 +1,34 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "go"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {
+    "cmds": [
+      "go build -o out main.go"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/go-build"
+    ]
+  },
+  "start": {
+    "cmd": "./out",
+    "runImage": "debian:bullseye-slim"
+  },
+  "variables": {
+    "CGO_ENABLED": "0"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__go_cgo_enabled.snap b/tests/snapshots/generate_plan_tests__go_cgo_enabled.snap
new file mode 100644
index 0000000..92c1c1a
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__go_cgo_enabled.snap
@@ -0,0 +1,33 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "go"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {
+    "cmds": [
+      "go build -o out main.go"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/go-build"
+    ]
+  },
+  "start": {
+    "cmd": "./out"
+  },
+  "variables": {
+    "CGO_ENABLED": "1"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__go_custom_version.snap b/tests/snapshots/generate_plan_tests__go_custom_version.snap
new file mode 100644
index 0000000..6a4be8c
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__go_custom_version.snap
@@ -0,0 +1,41 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "go_1_18"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "go get"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/go-build"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "go build -o out"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/go-build"
+    ]
+  },
+  "start": {
+    "cmd": "./out",
+    "runImage": "debian:bullseye-slim"
+  },
+  "variables": {
+    "CGO_ENABLED": "0"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__go_mod.snap b/tests/snapshots/generate_plan_tests__go_mod.snap
new file mode 100644
index 0000000..fd751f9
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__go_mod.snap
@@ -0,0 +1,41 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "go"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "go get"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/go-build"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "go build -o out"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/go-build"
+    ]
+  },
+  "start": {
+    "cmd": "./out",
+    "runImage": "debian:bullseye-slim"
+  },
+  "variables": {
+    "CGO_ENABLED": "0"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__java_maven.snap b/tests/snapshots/generate_plan_tests__java_maven.snap
new file mode 100644
index 0000000..2504184
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__java_maven.snap
@@ -0,0 +1,31 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "maven"
+      },
+      {
+        "name": "jdk8"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {
+    "cmds": [
+      "mvn -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install"
+    ]
+  },
+  "start": {
+    "cmd": "java -Dserver.port=$PORT $JAVA_OPTS -jar target/*jar"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap b/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
new file mode 100644
index 0000000..4625986
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__java_maven_wrapper.snap
@@ -0,0 +1,31 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "maven"
+      },
+      {
+        "name": "jdk8"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {
+    "cmds": [
+      "./mvnw -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install"
+    ]
+  },
+  "start": {
+    "cmd": "java -Dserver.port=$PORT $JAVA_OPTS -jar target/*jar"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node.snap b/tests/snapshots/generate_plan_tests__node.snap
new file mode 100644
index 0000000..c2f83b0
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node.snap
@@ -0,0 +1,42 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "npm ci"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "npm run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_custom_version.snap b/tests/snapshots/generate_plan_tests__node_custom_version.snap
new file mode 100644
index 0000000..ff5aaf6
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_custom_version.snap
@@ -0,0 +1,42 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs-18_x"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "npm ci"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "npm run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_main_file.snap b/tests/snapshots/generate_plan_tests__node_main_file.snap
new file mode 100644
index 0000000..34fb205
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_main_file.snap
@@ -0,0 +1,42 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "npm i"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "node src/index.js"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_main_file_doesnt_exist.snap b/tests/snapshots/generate_plan_tests__node_main_file_doesnt_exist.snap
new file mode 100644
index 0000000..2be26e6
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_main_file_doesnt_exist.snap
@@ -0,0 +1,42 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "npm i"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "node index.js"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_monorepo.snap b/tests/snapshots/generate_plan_tests__node_monorepo.snap
new file mode 100644
index 0000000..bbfe207
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_monorepo.snap
@@ -0,0 +1,41 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "yarn-1_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "yarn install --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/usr/local/share/.cache/yarn/v6"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "packages/client/.next/cache",
+      "node_modules/.cache"
+    ]
+  },
+  "start": {},
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_no_lockfile.snap b/tests/snapshots/generate_plan_tests__node_no_lockfile.snap
new file mode 100644
index 0000000..55750e0
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_no_lockfile.snap
@@ -0,0 +1,45 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [
+      "libuuid",
+      "libGL"
+    ],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "npm i"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "npm run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_no_scripts.snap b/tests/snapshots/generate_plan_tests__node_no_scripts.snap
new file mode 100644
index 0000000..2be26e6
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_no_scripts.snap
@@ -0,0 +1,42 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "npm i"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "node index.js"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__node_npm_old_lockfile.snap b/tests/snapshots/generate_plan_tests__node_npm_old_lockfile.snap
new file mode 100644
index 0000000..069a148
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__node_npm_old_lockfile.snap
@@ -0,0 +1,45 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-6_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "npm ci"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "npm run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "npm run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__npm.snap b/tests/snapshots/generate_plan_tests__npm.snap
new file mode 100644
index 0000000..c1bf98e
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__npm.snap
@@ -0,0 +1,45 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "npm ci"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "npm run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "npm run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__overriding_environment_variables.snap b/tests/snapshots/generate_plan_tests__overriding_environment_variables.snap
new file mode 100644
index 0000000..6ded212
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__overriding_environment_variables.snap
@@ -0,0 +1,42 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "npm i"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "node index.js"
+  },
+  "variables": {
+    "NODE_ENV": "test",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__php_laravel.snap b/tests/snapshots/generate_plan_tests__php_laravel.snap
new file mode 100644
index 0000000..b59514e
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__php_laravel.snap
@@ -0,0 +1,57 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "php81"
+      },
+      {
+        "name": "perl"
+      },
+      {
+        "name": "nginx"
+      },
+      {
+        "name": "php81Packages.composer"
+      },
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx",
+      "composer install",
+      "npm i"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "npm run prod"
+    ]
+  },
+  "start": {
+    "cmd": "([ -e /app/storage ] && chmod -R ugo+w /app/storage); perl /assets/transform-config.pl /assets/nginx.template.conf /nginx.conf && echo \"Server starting on port $PORT\" && (php-fpm -y /assets/php-fpm.conf & nginx -c /nginx.conf)"
+  },
+  "variables": {
+    "IS_LARAVEL": "yes",
+    "PORT": "80"
+  },
+  "static_assets": {
+    "nginx.template.conf": "worker_processes 5;\ndaemon off;\n\nworker_rlimit_nofile 8192;\n\nevents {\n  worker_connections  4096;  # Default: 1024\n}\n\nhttp {\n    include    $!{nginx}/conf/mime.types;\n    index    index.html index.htm index.php;\n\n    default_type application/octet-stream;\n    log_format   main '$remote_addr - $remote_user [$time_local]  $status '\n        '\"$request\" $body_bytes_sent \"$http_referer\" '\n        '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n    access_log /dev/stdout;\n    error_log /dev/stdout;\n    sendfile     on;\n    tcp_nopush   on;\n    server_names_hash_bucket_size 128; # this seems to be required for some vhosts\n\n    server {\n        listen ${PORT};\n        listen [::]:${PORT};\n        server_name localhost;\n\n        $if(IS_LARAVEL) (\n            root /app/public;\n        ) else (\n            root /app;\n        )\n     \n        add_header X-Frame-Options \"SAMEORIGIN\";\n        add_header X-Content-Type-Options \"nosniff\";\n     \n        index index.php;\n     \n        charset utf-8;\n     \n        $if(IS_LARAVEL) (\n            location / {\n                try_files $uri $uri/ /index.php?$query_string;\n            }\n        ) else ()\n     \n        location = /favicon.ico { access_log off; log_not_found off; }\n        location = /robots.txt  { access_log off; log_not_found off; }\n     \n        $if(IS_LARAVEL) (\n            error_page 404 /index.php;\n        ) else ()\n     \n        location ~ \\.php$ {\n            fastcgi_pass 127.0.0.1:9000;\n            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n            include $!{nginx}/conf/fastcgi_params;\n            include $!{nginx}/conf/fastcgi.conf;\n        }\n     \n        location ~ /\\.(?!well-known).* {\n            deny all;\n        }\n    }\n}",
+    "php-fpm.conf": "[www]\nlisten = 127.0.0.1:9000\nuser = nobody\npm = dynamic\npm.max_children = 50\npm.min_spare_servers = 4\npm.max_spare_servers = 32\npm.start_servers = 18",
+    "transform-config.pl": "#!/usr/bin/env perl\n\nundef $/;\n\nsub if_stmt {\n    my $condition = $_[0];\n    my $value = $_[1];\n    my $else = $_[2];\n\n    if($ENV{$condition} eq \"yes\") {\n        return $value;\n    } else {\n        return $else;\n    }\n}\n\nsub get_nix_path {\n    my $exe = $_[0];\n    my $path = `which $exe`;\n    $path =~ s/\\n//;\n    my $storePath = `nix-store -q $path`;\n    $storePath =~ s/\\n//;\n    return $storePath;\n}\n\nif ($#ARGV != 1) {\n    print STDERR \"Usage: $0 <config-file> <output-file>\\n\";\n    exit 1;\n}\nmy $infile = $ARGV[0];\nmy $outfile = $ARGV[1];\nopen(FH, '<', $infile) or die \"Could not open configuration file '$infile' $!\";\nmy $out = '';\nwhile (<FH>) {\n\n    # If statements\n    s{\n        \\$if\\s*\\((\\w+)\\)\\s*\\(\n            ([\\s\\S]*?)\n        \\)\\s*else\\s*\\(\n            ([\\s\\S]*?)\n        \\)\n    }{if_stmt($1, $2, $3)}egx;\n\n    # Variables\n    s/\\$\\{(\\w+)\\}/$ENV{$1}/eg;\n    \n    # Nix paths\n    s/\\$\\!\\{(\\w+)\\}/get_nix_path($1)/eg;\n\n    $out .= $_;\n}\nclose(FH);\nopen(FH, '>', $outfile) or die \"Could not write configuration file '$outfile' $!\";\nprint FH $out;\nclose(FH);"
+  }
+}
diff --git a/tests/snapshots/generate_plan_tests__php_vanilla.snap b/tests/snapshots/generate_plan_tests__php_vanilla.snap
new file mode 100644
index 0000000..0c6bbb7
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__php_vanilla.snap
@@ -0,0 +1,43 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "php"
+      },
+      {
+        "name": "perl"
+      },
+      {
+        "name": "nginx"
+      },
+      {
+        "name": "phpPackages.composer"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx"
+    ]
+  },
+  "build": {},
+  "start": {
+    "cmd": "([ -e /app/storage ] && chmod -R ugo+w /app/storage); perl /assets/transform-config.pl /assets/nginx.template.conf /nginx.conf && echo \"Server starting on port $PORT\" && (php-fpm -y /assets/php-fpm.conf & nginx -c /nginx.conf)"
+  },
+  "variables": {
+    "PORT": "80"
+  },
+  "static_assets": {
+    "nginx.template.conf": "worker_processes 5;\ndaemon off;\n\nworker_rlimit_nofile 8192;\n\nevents {\n  worker_connections  4096;  # Default: 1024\n}\n\nhttp {\n    include    $!{nginx}/conf/mime.types;\n    index    index.html index.htm index.php;\n\n    default_type application/octet-stream;\n    log_format   main '$remote_addr - $remote_user [$time_local]  $status '\n        '\"$request\" $body_bytes_sent \"$http_referer\" '\n        '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n    access_log /dev/stdout;\n    error_log /dev/stdout;\n    sendfile     on;\n    tcp_nopush   on;\n    server_names_hash_bucket_size 128; # this seems to be required for some vhosts\n\n    server {\n        listen ${PORT};\n        listen [::]:${PORT};\n        server_name localhost;\n\n        $if(IS_LARAVEL) (\n            root /app/public;\n        ) else (\n            root /app;\n        )\n     \n        add_header X-Frame-Options \"SAMEORIGIN\";\n        add_header X-Content-Type-Options \"nosniff\";\n     \n        index index.php;\n     \n        charset utf-8;\n     \n        $if(IS_LARAVEL) (\n            location / {\n                try_files $uri $uri/ /index.php?$query_string;\n            }\n        ) else ()\n     \n        location = /favicon.ico { access_log off; log_not_found off; }\n        location = /robots.txt  { access_log off; log_not_found off; }\n     \n        $if(IS_LARAVEL) (\n            error_page 404 /index.php;\n        ) else ()\n     \n        location ~ \\.php$ {\n            fastcgi_pass 127.0.0.1:9000;\n            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n            include $!{nginx}/conf/fastcgi_params;\n            include $!{nginx}/conf/fastcgi.conf;\n        }\n     \n        location ~ /\\.(?!well-known).* {\n            deny all;\n        }\n    }\n}",
+    "php-fpm.conf": "[www]\nlisten = 127.0.0.1:9000\nuser = nobody\npm = dynamic\npm.max_children = 50\npm.min_spare_servers = 4\npm.max_spare_servers = 32\npm.start_servers = 18",
+    "transform-config.pl": "#!/usr/bin/env perl\n\nundef $/;\n\nsub if_stmt {\n    my $condition = $_[0];\n    my $value = $_[1];\n    my $else = $_[2];\n\n    if($ENV{$condition} eq \"yes\") {\n        return $value;\n    } else {\n        return $else;\n    }\n}\n\nsub get_nix_path {\n    my $exe = $_[0];\n    my $path = `which $exe`;\n    $path =~ s/\\n//;\n    my $storePath = `nix-store -q $path`;\n    $storePath =~ s/\\n//;\n    return $storePath;\n}\n\nif ($#ARGV != 1) {\n    print STDERR \"Usage: $0 <config-file> <output-file>\\n\";\n    exit 1;\n}\nmy $infile = $ARGV[0];\nmy $outfile = $ARGV[1];\nopen(FH, '<', $infile) or die \"Could not open configuration file '$infile' $!\";\nmy $out = '';\nwhile (<FH>) {\n\n    # If statements\n    s{\n        \\$if\\s*\\((\\w+)\\)\\s*\\(\n            ([\\s\\S]*?)\n        \\)\\s*else\\s*\\(\n            ([\\s\\S]*?)\n        \\)\n    }{if_stmt($1, $2, $3)}egx;\n\n    # Variables\n    s/\\$\\{(\\w+)\\}/$ENV{$1}/eg;\n    \n    # Nix paths\n    s/\\$\\!\\{(\\w+)\\}/get_nix_path($1)/eg;\n\n    $out .= $_;\n}\nclose(FH);\nopen(FH, '>', $outfile) or die \"Could not write configuration file '$outfile' $!\";\nprint FH $out;\nclose(FH);"
+  }
+}
diff --git a/tests/snapshots/generate_plan_tests__pin_archive.snap b/tests/snapshots/generate_plan_tests__pin_archive.snap
new file mode 100644
index 0000000..93f9b60
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__pin_archive.snap
@@ -0,0 +1,19 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [],
+    "archive": "9eb60f25aff0d2218c848dd4574a0ab5e296cabe",
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {},
+  "start": {},
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__pnpm.snap b/tests/snapshots/generate_plan_tests__pnpm.snap
new file mode 100644
index 0000000..f54bd48
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__pnpm.snap
@@ -0,0 +1,45 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "pnpm-6_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "pnpm i --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pnpm"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "pnpm run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "pnpm run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__pnpm_custom_version.snap b/tests/snapshots/generate_plan_tests__pnpm_custom_version.snap
new file mode 100644
index 0000000..a3c1c5f
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__pnpm_custom_version.snap
@@ -0,0 +1,45 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs-14_x"
+      },
+      {
+        "name": "pnpm-6_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "pnpm i --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pnpm"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "pnpm run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "pnpm run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__pnpm_v7.snap b/tests/snapshots/generate_plan_tests__pnpm_v7.snap
new file mode 100644
index 0000000..be1dda5
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__pnpm_v7.snap
@@ -0,0 +1,45 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "pnpm-7_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "pnpm i --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pnpm"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "pnpm run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "pnpm run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__procfile.snap b/tests/snapshots/generate_plan_tests__procfile.snap
new file mode 100644
index 0000000..2be26e6
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__procfile.snap
@@ -0,0 +1,42 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "npm-8_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "npm i"
+    ],
+    "cacheDirectories": [
+      "/root/.npm"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "node index.js"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__python.snap b/tests/snapshots/generate_plan_tests__python.snap
new file mode 100644
index 0000000..3cea759
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__python.snap
@@ -0,0 +1,37 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "python38"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install -r requirements.txt"
+    ],
+    "onlyIncludeFiles": [
+      "requirements.txt"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pip"
+    ],
+    "paths": [
+      "/opt/venv/bin"
+    ]
+  },
+  "build": {},
+  "start": {
+    "cmd": "python main.py"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__python_poetry.snap b/tests/snapshots/generate_plan_tests__python_poetry.snap
new file mode 100644
index 0000000..5417d06
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__python_poetry.snap
@@ -0,0 +1,40 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "python38"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install poetry==$NIXPACKS_POETRY_VERSION && poetry install --no-dev --no-interaction --no-ansi"
+    ],
+    "onlyIncludeFiles": [
+      "poetry.lock",
+      "pyproject.toml"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pip"
+    ],
+    "paths": [
+      "/opt/venv/bin"
+    ]
+  },
+  "build": {},
+  "start": {
+    "cmd": "python main.py"
+  },
+  "variables": {
+    "NIXPACKS_POETRY_VERSION": "1.1.13"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__python_setuptools.snap b/tests/snapshots/generate_plan_tests__python_setuptools.snap
new file mode 100644
index 0000000..86e49cd
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__python_setuptools.snap
@@ -0,0 +1,37 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "python38"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "python -m venv /opt/venv && . /opt/venv/bin/activate && pip install --upgrade build setuptools && pip install ."
+    ],
+    "onlyIncludeFiles": [
+      "pyproject.toml"
+    ],
+    "cacheDirectories": [
+      "/root/.cache/pip"
+    ],
+    "paths": [
+      "/opt/venv/bin"
+    ]
+  },
+  "build": {},
+  "start": {
+    "cmd": "python -m nixpacks-setuptools"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__ruby_rails.snap b/tests/snapshots/generate_plan_tests__ruby_rails.snap
new file mode 100644
index 0000000..3f6e3d1
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__ruby_rails.snap
@@ -0,0 +1,53 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [],
+    "libraries": [],
+    "apt_pkgs": [
+      "procps",
+      "libpq-dev"
+    ],
+    "cmds": [
+      "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
+      "rvm install 3.1.2",
+      "rvm --default use 3.1.2",
+      "gem install bundler:2.3.7",
+      "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
+    ],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "bundle install"
+    ],
+    "onlyIncludeFiles": [
+      "Gemfile*"
+    ],
+    "cacheDirectories": [
+      "/root/.bundle/cache"
+    ],
+    "paths": [
+      "/usr/local/rvm/rubies/3.1.2/bin",
+      "/usr/local/rvm/gems/3.1.2/bin",
+      "/usr/local/rvm/gems/3.1.2@global/bin"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "bundle exec rake assets:precompile"
+    ]
+  },
+  "start": {
+    "cmd": "rake db:migrate && bundle exec bin/rails server -b 0.0.0.0 -p ${PORT:-3000}"
+  },
+  "variables": {
+    "BUNDLE_GEMFILE": "/app/Gemfile",
+    "GEM_HOME": "/usr/local/rvm/gems/3.1.2",
+    "GEM_PATH": "/usr/local/rvm/gems/3.1.2:/usr/local/rvm/gems/3.1.2@global"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__ruby_sinatra.snap b/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
new file mode 100644
index 0000000..cfa6b51
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__ruby_sinatra.snap
@@ -0,0 +1,48 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [],
+    "libraries": [],
+    "apt_pkgs": [
+      "procps"
+    ],
+    "cmds": [
+      "curl -sSL https://get.rvm.io | bash -s stable && . /etc/profile.d/rvm.sh",
+      "rvm install ruby-3.1.2",
+      "rvm --default use ruby-3.1.2",
+      "gem install bundler:2.3.7",
+      "echo 'source /usr/local/rvm/scripts/rvm' >> /root/.profile"
+    ],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "bundle install"
+    ],
+    "onlyIncludeFiles": [
+      "Gemfile*"
+    ],
+    "cacheDirectories": [
+      "/root/.bundle/cache"
+    ],
+    "paths": [
+      "/usr/local/rvm/rubies/ruby-3.1.2/bin",
+      "/usr/local/rvm/gems/ruby-3.1.2/bin",
+      "/usr/local/rvm/gems/ruby-3.1.2@global/bin"
+    ]
+  },
+  "build": {},
+  "start": {
+    "cmd": "RACK_ENV=production bundle exec puma"
+  },
+  "variables": {
+    "BUNDLE_GEMFILE": "/app/Gemfile",
+    "GEM_HOME": "/usr/local/rvm/gems/ruby-3.1.2",
+    "GEM_PATH": "/usr/local/rvm/gems/ruby-3.1.2:/usr/local/rvm/gems/ruby-3.1.2@global"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__rust_rocket_no_musl.snap b/tests/snapshots/generate_plan_tests__rust_rocket_no_musl.snap
new file mode 100644
index 0000000..8481edc
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__rust_rocket_no_musl.snap
@@ -0,0 +1,43 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "gcc"
+      },
+      {
+        "name": "rust-bin.stable.latest.default",
+        "overlay": "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [
+      "binutils"
+    ],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {
+    "cmds": [
+      "cargo build --release",
+      "cp target/release/rocket rocket"
+    ],
+    "cacheDirectories": [
+      "/root/.cargo/git",
+      "/root/.cargo/registry",
+      "target"
+    ]
+  },
+  "start": {
+    "cmd": "./rocket"
+  },
+  "variables": {
+    "NIXPACKS_NO_MUSL": "1",
+    "ROCKET_ADDRESS": "0.0.0.0"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__staticfile.snap b/tests/snapshots/generate_plan_tests__staticfile.snap
new file mode 100644
index 0000000..8dbca11
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__staticfile.snap
@@ -0,0 +1,31 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nginx"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {
+    "cmds": [
+      "mkdir /etc/nginx/ /var/log/nginx/ /var/cache/nginx/"
+    ]
+  },
+  "start": {
+    "cmd": "[[ -z \"${PORT}\" ]] && echo \"Environment variable PORT not found. Using PORT 80\" || sed -i \"s/0.0.0.0:80/$PORT/g\" /assets/nginx.conf && nginx -c /assets/nginx.conf"
+  },
+  "variables": {},
+  "static_assets": {
+    ".htpasswd": "# Username: nixpacks\n# Password: nixpacks\nnixpacks:$apr1$21i3ye0o$d1H1ePSg8mD2cO5O2A9dG.",
+    "nginx.conf": "daemon off;\nerror_log /dev/stdout info;\nworker_processes  auto;\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    include /nix/store/*-user-environment/conf/mime.types;\n    access_log /dev/stdout;\n    default_type  application/octet-stream;\n    sendfile       on;\n    keepalive_timeout  60;\n    types_hash_max_size 4096;\n    server {\n        listen    0.0.0.0:80;\n        gzip  \t  on;\n        root\t  /app/site;\n        location / {\n            auth_basic\t\"Password Required\";\nauth_basic_user_file\t/assets/.htpasswd;\n            autoindex off;\n        }\n\n    }\n}\n"
+  }
+}
diff --git a/tests/snapshots/generate_plan_tests__swift.snap b/tests/snapshots/generate_plan_tests__swift.snap
new file mode 100644
index 0000000..5576daa
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__swift.snap
@@ -0,0 +1,49 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "coreutils"
+      },
+      {
+        "name": "swift"
+      },
+      {
+        "name": "clang"
+      },
+      {
+        "name": "zlib"
+      },
+      {
+        "name": "zlib.dev"
+      }
+    ],
+    "archive": "c82b46413401efa740a0b994f52e9903a4f6dcd5",
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "swift package resolve"
+    ],
+    "onlyIncludeFiles": [
+      "Package.swift"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "CC=clang++ swift build -c release --static-swift-stdlib",
+      "cp ./.build/release/swift ./swift && rm -rf ./.build"
+    ]
+  },
+  "start": {
+    "cmd": "./swift"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__swift_vapor.snap b/tests/snapshots/generate_plan_tests__swift_vapor.snap
new file mode 100644
index 0000000..cfb45a7
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__swift_vapor.snap
@@ -0,0 +1,50 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "coreutils"
+      },
+      {
+        "name": "swift"
+      },
+      {
+        "name": "clang"
+      },
+      {
+        "name": "zlib"
+      },
+      {
+        "name": "zlib.dev"
+      }
+    ],
+    "archive": "c82b46413401efa740a0b994f52e9903a4f6dcd5",
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "swift package resolve"
+    ],
+    "onlyIncludeFiles": [
+      "Package.swift",
+      "Package.resolved"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "CC=clang++ swift build -c release --static-swift-stdlib",
+      "cp ./.build/release/Run ./Run && rm -rf ./.build"
+    ]
+  },
+  "start": {
+    "cmd": "./Run"
+  },
+  "variables": {},
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__yarn.snap b/tests/snapshots/generate_plan_tests__yarn.snap
new file mode 100644
index 0000000..0dc51a9
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__yarn.snap
@@ -0,0 +1,45 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs"
+      },
+      {
+        "name": "yarn-1_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "yarn install --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/usr/local/share/.cache/yarn/v6"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "yarn run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "yarn run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__yarn_berry.snap b/tests/snapshots/generate_plan_tests__yarn_berry.snap
new file mode 100644
index 0000000..7f33f03
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__yarn_berry.snap
@@ -0,0 +1,45 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs-16_x"
+      },
+      {
+        "name": "yarn-1_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "yarn set version berry && yarn install --check-cache"
+    ],
+    "cacheDirectories": [
+      "/usr/local/share/.cache/yarn/v6"
+    ]
+  },
+  "build": {
+    "cmds": [
+      "yarn run build"
+    ],
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "yarn run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__yarn_custom_version.snap b/tests/snapshots/generate_plan_tests__yarn_custom_version.snap
new file mode 100644
index 0000000..29f2ab4
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__yarn_custom_version.snap
@@ -0,0 +1,42 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "nodejs-14_x"
+      },
+      {
+        "name": "yarn-1_x",
+        "overlay": "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {
+    "cmds": [
+      "yarn install --frozen-lockfile"
+    ],
+    "cacheDirectories": [
+      "/usr/local/share/.cache/yarn/v6"
+    ]
+  },
+  "build": {
+    "cacheDirectories": [
+      "node_modules/.cache"
+    ]
+  },
+  "start": {
+    "cmd": "yarn run start"
+  },
+  "variables": {
+    "NODE_ENV": "production",
+    "NPM_CONFIG_PRODUCTION": "false"
+  },
+  "static_assets": {}
+}
diff --git a/tests/snapshots/generate_plan_tests__zig.snap b/tests/snapshots/generate_plan_tests__zig.snap
new file mode 100644
index 0000000..9080e2a
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__zig.snap
@@ -0,0 +1,28 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "version": "0.2.9",
+  "setup": {
+    "pkgs": [
+      {
+        "name": "zig"
+      }
+    ],
+    "libraries": [],
+    "apt_pkgs": [],
+    "baseImage": "ghcr.io/railwayapp/nixpacks:debian-1657820962"
+  },
+  "install": {},
+  "build": {
+    "cmds": [
+      "zig build -Drelease-safe=true"
+    ]
+  },
+  "start": {
+    "cmd": "./zig-out/bin/zig"
+  },
+  "variables": {},
+  "static_assets": {}
+}