diff --git a/Cargo.lock b/Cargo.lock index 4418b3753..3fb9b9265 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1075,7 +1075,7 @@ dependencies = [ [[package]] name = "nixpacks" -version = "1.29.1" +version = "1.30.0" dependencies = [ "actix-web", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 1e5f44d36..c017b7e4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nixpacks" -version = "1.29.1" +version = "1.30.0" edition = "2021" license = "MIT" authors = ["Railway "] diff --git a/docs/pages/docs/cli.md b/docs/pages/docs/cli.md index d590b0eda..3abec0b6e 100644 --- a/docs/pages/docs/cli.md +++ b/docs/pages/docs/cli.md @@ -68,6 +68,7 @@ nixpacks plan examples/node ``` By default, the plan is output in JSON format. You can output in TOML format with the `--format toml` option. +The generated plan will be outputted to stdout, while some providers expose recoverable errors to stderr. View all plan options with diff --git a/docs/pages/docs/configuration/file.md b/docs/pages/docs/configuration/file.md index 489ebf954..2da16ebe6 100644 --- a/docs/pages/docs/configuration/file.md +++ b/docs/pages/docs/configuration/file.md @@ -146,11 +146,11 @@ Nix packages to be made available through the `LD_LIBRARY_PATH` environment vari ### Nixpkgs archive -Specific version of the Nixpkgs archive to use. By default all builds are built using the version defined [here](https://github.com/railwayapp/nixpacks/blob/6dc1e66e3d0840230def277d19890cd0da4584d3/src/nixpacks/plan/generator.rs#L16). But this value can be overridden to install Nix packages from an older archive. +Specific version of the Nixpkgs archive to use. By default all builds are built using the version defined [here](https://github.com/railwayapp/nixpacks/blob/2d16cd938c95411db4a0c56b81bf7b558252af7b/src/nixpacks/nix/mod.rs#L11). But this value can be overridden to install Nix packages from an older or newer archive. ```toml [phase.name] - nixpkgsArchive = '21de2b973f9fee595a7a1ac4693efff791245c34' + nixpkgsArchive = '5148520bfab61f99fd25fb9ff7bfbb50dad3c9db' ``` ### Apt packages diff --git a/docs/pages/docs/providers/go.md b/docs/pages/docs/providers/go.md index 2ecb5d3ca..972621764 100644 --- a/docs/pages/docs/providers/go.md +++ b/docs/pages/docs/providers/go.md @@ -33,8 +33,14 @@ go get ## Build +If your project has multiple binaries, you can specify which one to run with the `NIXPACKS_GO_BIN` environment variable. +Otherwise, the first binary found in the project's root directory or the project's `cmd` directory will be used. + ``` go build -o out +# Or if there are no .go files in the root directory +go build -o out ./cmd/{name} + ``` ## Start diff --git a/docs/pages/docs/providers/node.md b/docs/pages/docs/providers/node.md index ca9117063..fc944bc8a 100644 --- a/docs/pages/docs/providers/node.md +++ b/docs/pages/docs/providers/node.md @@ -8,7 +8,7 @@ The Node provider supports NPM, Yarn, Yarn 2, PNPM and Bun. ## Environment Variables -The Node provider sets the following environment variables: +The Node provider sets the following environment variables when the container is running (not during build): - `CI=true` - `NODE_ENV=production` @@ -30,6 +30,7 @@ The version can be overridden by - Setting the `NIXPACKS_NODE_VERSION` environment variable - Specifying the `engines.node` field in `package.json` +- Creating a `.nvmrc` file in your project and specify the version or alias (`lts/*`) Only a major version can be specified. For example, `18.x` or `20`. diff --git a/examples/deno-jsonc/deno.jsonc b/examples/deno-jsonc/deno.jsonc new file mode 100644 index 000000000..9064591b1 --- /dev/null +++ b/examples/deno-jsonc/deno.jsonc @@ -0,0 +1,15 @@ +// THIS FILE HAS ALL KINDS OF COMMENTS POSSIBLE TO TEST JSONC +{ + //some random comment + "tasks": { + // random afterline comment + /* :) */ "dev": "deno run --watch main.ts", + "start": "deno start main.ts" + } /* + This + is some multiline comment + */, + "imports": { + "@std/assert": "jsr:@std/assert@1" + } +} diff --git a/examples/deno-jsonc/deno.lock b/examples/deno-jsonc/deno.lock new file mode 100644 index 000000000..3513c0ffe --- /dev/null +++ b/examples/deno-jsonc/deno.lock @@ -0,0 +1,18 @@ +{ + "version": "4", + "specifiers": { + "jsr:@std/assert@1": "1.0.8", + "jsr:@std/internal@^1.0.5": "1.0.5" + }, + "jsr": { + "@std/assert@1.0.8": { + "integrity": "ebe0bd7eb488ee39686f77003992f389a06c3da1bbd8022184804852b2fa641b", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.5": { + "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" + } + } +} diff --git a/examples/deno-jsonc/main.ts b/examples/deno-jsonc/main.ts new file mode 100644 index 000000000..bdf52b985 --- /dev/null +++ b/examples/deno-jsonc/main.ts @@ -0,0 +1,7 @@ +export function add(a: number, b: number): number { + return a + b; +} +// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts +if (import.meta.main) { + console.log("Add 2 + 3 =", add(2, 3)); +} \ No newline at end of file diff --git a/examples/deno-jsonc/main_test.ts b/examples/deno-jsonc/main_test.ts new file mode 100644 index 000000000..3d981e9be --- /dev/null +++ b/examples/deno-jsonc/main_test.ts @@ -0,0 +1,6 @@ +import { assertEquals } from "@std/assert"; +import { add } from "./main.ts"; + +Deno.test(function addTest() { + assertEquals(add(2, 3), 5); +}); diff --git a/examples/go-cmd/.gitignore b/examples/go-cmd/.gitignore new file mode 100644 index 000000000..66fd13c90 --- /dev/null +++ b/examples/go-cmd/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/examples/go-cmd/cmd/server/main.go b/examples/go-cmd/cmd/server/main.go new file mode 100644 index 000000000..c4c5db985 --- /dev/null +++ b/examples/go-cmd/cmd/server/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/gin-gonic/gin" +) + +var Router *gin.Engine + +func main() { + r := gin.Default() + r.GET("/", func(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "Hello world!", + }) + }) + r.Run() +} diff --git a/examples/go-cmd/go.mod b/examples/go-cmd/go.mod new file mode 100644 index 000000000..768fb977e --- /dev/null +++ b/examples/go-cmd/go.mod @@ -0,0 +1,26 @@ +module gin + +go 1.18 + +require github.com/gin-gonic/gin v1.8.1 + +require ( + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.10.0 // indirect + github.com/goccy/go-json v0.9.7 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/ugorji/go/codec v1.2.7 // indirect + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect + golang.org/x/text v0.3.6 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/examples/go-cmd/go.sum b/examples/go-cmd/go.sum new file mode 100644 index 000000000..c81df8068 --- /dev/null +++ b/examples/go-cmd/go.sum @@ -0,0 +1,75 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/go-gin/cmd/server/main.go b/examples/go-gin/cmd/server/main.go new file mode 100644 index 000000000..c4c5db985 --- /dev/null +++ b/examples/go-gin/cmd/server/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/gin-gonic/gin" +) + +var Router *gin.Engine + +func main() { + r := gin.Default() + r.GET("/", func(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "Hello world!", + }) + }) + r.Run() +} diff --git a/examples/node-node-version/.node-version b/examples/node-node-version/.node-version new file mode 100644 index 000000000..dc0bb0f43 --- /dev/null +++ b/examples/node-node-version/.node-version @@ -0,0 +1 @@ +v22.12.0 diff --git a/examples/node-node-version/index.js b/examples/node-node-version/index.js new file mode 100644 index 000000000..140fc15d9 --- /dev/null +++ b/examples/node-node-version/index.js @@ -0,0 +1,2 @@ +console.log("Hello from Node"); +console.log(`Node version: ${process.version}`); diff --git a/examples/node-node-version/package-lock.json b/examples/node-node-version/package-lock.json new file mode 100644 index 000000000..d18e0e0f4 --- /dev/null +++ b/examples/node-node-version/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "node", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "node", + "version": "1.0.0" + } + } +} diff --git a/examples/node-node-version/package.json b/examples/node-node-version/package.json new file mode 100644 index 000000000..2f12d8fba --- /dev/null +++ b/examples/node-node-version/package.json @@ -0,0 +1,8 @@ +{ + "name": "node", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js" + } +} diff --git a/examples/node-nvmrc-invalid-lts/.nvmrc b/examples/node-nvmrc-invalid-lts/.nvmrc new file mode 100644 index 000000000..e40d76f49 --- /dev/null +++ b/examples/node-nvmrc-invalid-lts/.nvmrc @@ -0,0 +1 @@ +some_invalid_version \ No newline at end of file diff --git a/examples/node-nvmrc-invalid-lts/index.js b/examples/node-nvmrc-invalid-lts/index.js new file mode 100644 index 000000000..cd4463680 --- /dev/null +++ b/examples/node-nvmrc-invalid-lts/index.js @@ -0,0 +1 @@ +console.log("Oops this version is invalid. Using default 18"); diff --git a/examples/node-nvmrc-invalid-lts/package-lock.json b/examples/node-nvmrc-invalid-lts/package-lock.json new file mode 100644 index 000000000..0f3eca8df --- /dev/null +++ b/examples/node-nvmrc-invalid-lts/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "node", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "node", + "version": "1.0.0" + } + } + } + \ No newline at end of file diff --git a/examples/node-nvmrc-invalid-lts/package.json b/examples/node-nvmrc-invalid-lts/package.json new file mode 100644 index 000000000..2f12d8fba --- /dev/null +++ b/examples/node-nvmrc-invalid-lts/package.json @@ -0,0 +1,8 @@ +{ + "name": "node", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js" + } +} diff --git a/examples/node-nvmrc-lts/.nvmrc b/examples/node-nvmrc-lts/.nvmrc new file mode 100644 index 000000000..0a47c855e --- /dev/null +++ b/examples/node-nvmrc-lts/.nvmrc @@ -0,0 +1 @@ +lts/iron \ No newline at end of file diff --git a/examples/node-nvmrc-lts/index.js b/examples/node-nvmrc-lts/index.js new file mode 100644 index 000000000..bf464707d --- /dev/null +++ b/examples/node-nvmrc-lts/index.js @@ -0,0 +1 @@ +console.log("Hello from the NVM test"); diff --git a/examples/node-nvmrc-lts/package-lock.json b/examples/node-nvmrc-lts/package-lock.json new file mode 100644 index 000000000..0f3eca8df --- /dev/null +++ b/examples/node-nvmrc-lts/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "node", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "node", + "version": "1.0.0" + } + } + } + \ No newline at end of file diff --git a/examples/node-nvmrc-lts/package.json b/examples/node-nvmrc-lts/package.json new file mode 100644 index 000000000..2f12d8fba --- /dev/null +++ b/examples/node-nvmrc-lts/package.json @@ -0,0 +1,8 @@ +{ + "name": "node", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js" + } +} diff --git a/flake.nix b/flake.nix index 8e4792384..f1e0cd542 100644 --- a/flake.nix +++ b/flake.nix @@ -20,7 +20,7 @@ let package = with nixpkgs; rustPlatform.buildRustPackage { pname = "nixpacks"; - version = "1.29.1"; + version = "1.30.0"; src = ./.; cargoLock = { lockFile = ./Cargo.lock; diff --git a/src/main.rs b/src/main.rs index 3596f28df..b66dc2750 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,7 +76,8 @@ struct Args { #[allow(clippy::large_enum_variant)] #[derive(Subcommand)] enum Commands { - /// Generate a build plan for an app + /// Generate a build plan for an app. + /// Generated plan will be outputted to stdout, while warnings might be outputted to stderr. Plan { /// App source path: String, diff --git a/src/nixpacks/app.rs b/src/nixpacks/app.rs index d94e8416e..7aea9a17f 100644 --- a/src/nixpacks/app.rs +++ b/src/nixpacks/app.rs @@ -190,6 +190,47 @@ impl App { Ok(toml_file) } + /// Parse jsonc files as json by ignoring all kinds of comments + pub fn read_jsonc(&self, name: &str) -> Result + where + T: DeserializeOwned, + { + let mut cleaned_jsonc = String::new(); + let contents = self.read_file(name)?; + let mut chars = contents.chars().peekable(); + while let Some(current_char) = chars.next() { + match current_char { + '/' if chars.peek() == Some(&'/') => { + while let Some(&next_char) = chars.peek() { + chars.next(); + if next_char == '\n' { + break; + } + } + } + '/' if chars.peek() == Some(&'*') => { + chars.next(); + loop { + match chars.next() { + Some('*') if chars.peek() == Some(&'/') => { + chars.next(); + break; + } + None => break, + _ => continue, + } + } + } + _ => cleaned_jsonc.push(current_char), + } + } + let value: T = serde_json::from_str(cleaned_jsonc.as_str()).with_context(|| { + let relative_path = self.strip_source_path(Path::new(name)).unwrap(); + format!("Error reading {} as JSONC", relative_path.to_str().unwrap()) + })?; + Ok(value) + } + /// Try to yaml-parse a file. pub fn read_yaml(&self, name: &str) -> Result where @@ -281,6 +322,14 @@ mod tests { Ok(()) } + #[test] + fn test_read_jsonc_file() -> Result<()> { + let app = App::new("./examples/deno-jsonc")?; + let value: Map = app.read_jsonc("deno.jsonc")?; + assert!(value.get("tasks").is_some()); + Ok(()) + } + #[test] fn test_read_toml_file() -> Result<()> { let app = App::new("./examples/rust-rocket")?; diff --git a/src/nixpacks/images.rs b/src/nixpacks/images.rs index 840f20b42..fb5eebc1d 100644 --- a/src/nixpacks/images.rs +++ b/src/nixpacks/images.rs @@ -1,5 +1,5 @@ -pub const DEBIAN_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1731974648"; -pub const UBUNTU_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:ubuntu-1731974648"; +pub const DEBIAN_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:debian-1736208272"; +pub const UBUNTU_BASE_IMAGE: &str = "ghcr.io/railwayapp/nixpacks:ubuntu-1736208272"; pub const DEFAULT_BASE_IMAGE: &str = UBUNTU_BASE_IMAGE; pub const STANDALONE_IMAGE: &str = "ubuntu:jammy"; diff --git a/src/nixpacks/nix/mod.rs b/src/nixpacks/nix/mod.rs index 1cebca81f..4ca919109 100644 --- a/src/nixpacks/nix/mod.rs +++ b/src/nixpacks/nix/mod.rs @@ -70,7 +70,10 @@ pub fn create_nix_expressions_for_phases(phases: &Phases) -> BTreeMap Vec { .filter(|p| p.uses_nix()) .map(|p| p.nixpkgs_archive.clone()) .collect::>(); - archives.iter().map(nix_file_name).collect() + archives.iter().map(|a| nix_file_name(a.as_ref())).collect() } /// Returns all the Nix expression files used to install Nix dependencies for each phase. @@ -101,7 +104,7 @@ pub fn setup_files_for_phases(phases: &Phases) -> Vec { } /// Generates the filename for each Nix expression file. -fn nix_file_name(archive: &Option) -> String { +fn nix_file_name(archive: Option<&String>) -> String { match archive { Some(archive) => format!("nixpkgs-{archive}.nix"), None => "nixpkgs.nix".to_string(), diff --git a/src/nixpacks/plan/mod.rs b/src/nixpacks/plan/mod.rs index 9fa1ae568..2b2db9e72 100644 --- a/src/nixpacks/plan/mod.rs +++ b/src/nixpacks/plan/mod.rs @@ -407,6 +407,47 @@ mod test { assert_eq!(result, env_plan); } + #[test] + fn test_to_json_and_from_json() { + let original_plan = BuildPlan::from_toml( + r#" + [phases.setup] + nixPkgs = ["nodejs", "yarn"] + aptPkgs = ["git"] + + [phases.install] + cmds = ["yarn install"] + cacheDirectories = ["node_modules"] + dependsOn = ["setup"] + + [phases.build] + cmds = ["yarn build"] + dependsOn = ["install"] + + [start] + cmd = "yarn start" + "#, + ) + .unwrap(); + + let json_str = original_plan.to_json().unwrap(); + let deserialized_plan = BuildPlan::from_json(json_str).unwrap(); + + assert_eq!(original_plan, deserialized_plan); + assert_eq!( + deserialized_plan.get_phase("setup").unwrap().nix_pkgs, + Some(vec!["nodejs".to_string(), "yarn".to_string()]) + ); + assert_eq!( + deserialized_plan.get_phase("setup").unwrap().apt_pkgs, + Some(vec!["git".to_string()]) + ); + assert_eq!( + deserialized_plan.start_phase.unwrap().cmd.unwrap(), + "yarn start".to_string() + ); + } + #[test] fn test_get_phases_with_dependencies() { let setup = Phase::new("setup"); diff --git a/src/providers/deno.rs b/src/providers/deno.rs index a5289ec46..3deef7bee 100644 --- a/src/providers/deno.rs +++ b/src/providers/deno.rs @@ -82,7 +82,7 @@ impl DenoProvider { if app.includes_file("deno.json") || app.includes_file("deno.jsonc") { let deno_json: DenoJson = app .read_json("deno.json") - .or_else(|_| app.read_json("deno.jsonc"))?; + .or_else(|_| app.read_jsonc("deno.jsonc"))?; if let Some(tasks) = deno_json.tasks { if let Some(start) = tasks.start { diff --git a/src/providers/go.rs b/src/providers/go.rs index 230f9ebe7..cb0a27cd3 100644 --- a/src/providers/go.rs +++ b/src/providers/go.rs @@ -65,20 +65,46 @@ impl Provider for GolangProvider { setup.set_nix_archive(archive); plan.add_phase(setup); + let is_go_module = app.includes_file("go.mod"); - if app.includes_file("go.mod") { + if is_go_module { let mut install = Phase::install(Some("go mod download".to_string())); install.add_cache_directory(GO_BUILD_CACHE_DIR.to_string()); plan.add_phase(install); } - let mut build = if app.includes_file("go.mod") { - Phase::build(Some(format!("go build -o {BINARY_NAME}"))) + let has_root_go_files = app.find_files("*.go").ok().map_or(false, |files| { + files + .iter() + .any(|file| file.parent() == Some(app.source.as_path())) + }); + + let build_command = if let Some(name) = env.get_config_variable("GO_BIN") { + Some(format!("go build -o {BINARY_NAME} ./cmd/{name}")) + } else if is_go_module && has_root_go_files { + Some(format!("go build -o {BINARY_NAME}")) + } else if app.includes_directory("cmd") { + // Try to find a command in the cmd directory + app.find_directories("cmd/*") + .ok() + .and_then(|dirs| { + dirs.into_iter() + .find(|path| path.parent().map_or(false, |p| p.ends_with("cmd"))) + }) + .and_then(|path| { + path.file_name() + .and_then(|os_str| os_str.to_str()) + .map(|name| format!("go build -o {BINARY_NAME} ./cmd/{name}")) + }) + } else if is_go_module { + Some(format!("go build -o {BINARY_NAME}")) } else if app.includes_file("main.go") { - Phase::build(Some(format!("go build -o {BINARY_NAME} main.go"))) + Some(format!("go build -o {BINARY_NAME} main.go")) } else { - Phase::build(None) + None }; + + let mut build = Phase::build(build_command); build.add_cache_directory(GO_BUILD_CACHE_DIR.to_string()); build.depends_on_phase("setup"); plan.add_phase(build); diff --git a/src/providers/node/mod.rs b/src/providers/node/mod.rs index 920b9bed6..9d7c9ff22 100644 --- a/src/providers/node/mod.rs +++ b/src/providers/node/mod.rs @@ -373,12 +373,23 @@ impl NodeProvider { let nvmrc_node_version = if app.includes_file(".nvmrc") { let nvmrc = app.read_file(".nvmrc")?; - Some(nvmrc.trim().replace('v', "")) + Some(parse_nvmrc(&nvmrc)) } else { None }; - let node_version = env_node_version.or(pkg_node_version).or(nvmrc_node_version); + let dot_node_version = if app.includes_file(".node-version") { + let node_version_file = app.read_file(".node-version")?; + // Using simple string transform since .node-version don't currently have a convention around the use of lts/* implemented in parse_nvmrc method + Some(node_version_file.trim().replace('v', "")) + } else { + None + }; + + let node_version = env_node_version + .or(pkg_node_version) + .or(nvmrc_node_version) + .or(dot_node_version); let node_version = match node_version { Some(node_version) => node_version, @@ -400,7 +411,7 @@ impl NodeProvider { pkg_manager = "pnpm"; } else if app.includes_file("yarn.lock") { pkg_manager = "yarn"; - } else if app.includes_file("bun.lockb") { + } else if app.includes_file("bun.lockb") || app.includes_file("bun.lock") { pkg_manager = "bun"; } pkg_manager.to_string() @@ -435,7 +446,7 @@ impl NodeProvider { } } else if app.includes_file("package-lock.json") { install_cmd = "npm ci".to_string(); - } else if app.includes_file("bun.lockb") { + } else if app.includes_file("bun.lockb") || app.includes_file("bun.lock") { install_cmd = "bun i --no-save".to_string(); } @@ -658,7 +669,7 @@ fn version_number_to_pkg(version: u32) -> String { fn parse_node_version_into_pkg(node_version: &str) -> String { let default_node_pkg_name = version_number_to_pkg(DEFAULT_NODE_VERSION); let range: Range = node_version.parse().unwrap_or_else(|_| { - println!("Warning: node version {node_version} is not valid, using default node version {default_node_pkg_name}"); + eprintln!("Warning: node version {node_version} is not valid, using default node version {default_node_pkg_name}"); Range::parse(DEFAULT_NODE_VERSION.to_string()).unwrap() }); let mut available_node_versions = AVAILABLE_NODE_VERSIONS.to_vec(); @@ -674,6 +685,35 @@ fn parse_node_version_into_pkg(node_version: &str) -> String { default_node_pkg_name } +fn parse_nvmrc(nvmrc_content: &str) -> String { + let lts_versions: HashMap<&str, u32> = { + let mut nvm_map = HashMap::new(); + nvm_map.insert("lts/*", 22); + nvm_map.insert("lts/jod", 22); + nvm_map.insert("lts/argon", 4); + nvm_map.insert("lts/boron", 6); + nvm_map.insert("lts/carbon", 8); + nvm_map.insert("lts/dubnium", 10); + nvm_map.insert("lts/erbium", 12); + nvm_map.insert("lts/fermium", 14); + nvm_map.insert("lts/gallium", 16); + nvm_map.insert("lts/hydrogen", 18); + nvm_map.insert("lts/iron", 20); + nvm_map + }; + + let trimmed_version = nvmrc_content.trim(); + if let Some(&version) = lts_versions.get(trimmed_version) { + return version.to_string(); + } + + // Only remove v if it is in the starting character, lts/ will never have that in starting + trimmed_version + .strip_prefix('v') + .unwrap_or(trimmed_version) + .to_string() +} + #[cfg(test)] mod test { use std::collections::BTreeMap; @@ -970,6 +1010,57 @@ mod test { Ok(()) } + #[test] + fn test_version_from_node_version_file() -> Result<()> { + assert_eq!( + NodeProvider::get_nix_node_pkg( + &PackageJson { + name: Some(String::default()), + ..Default::default() + }, + &App::new("examples/node-node-version")?, + &Environment::default() + )?, + Pkg::new("nodejs_22") + ); + + Ok(()) + } + + #[test] + fn test_version_from_nvmrc_lts() -> Result<()> { + assert_eq!( + NodeProvider::get_nix_node_pkg( + &PackageJson { + name: Some(String::default()), + ..Default::default() + }, + &App::new("examples/node-nvmrc-lts")?, + &Environment::default() + )?, + Pkg::new("nodejs_20") + ); + + Ok(()) + } + + #[test] + fn test_invalid_version_from_nvmrc_lts() -> Result<()> { + assert_eq!( + NodeProvider::get_nix_node_pkg( + &PackageJson { + name: Some(String::default()), + ..Default::default() + }, + &App::new("examples/node-nvmrc-invalid-lts")?, + &Environment::default() + )?, + Pkg::new("nodejs_18") + ); + + Ok(()) + } + #[test] fn test_engine_invalid_version() -> Result<()> { // this test now defaults to lts diff --git a/src/providers/node/turborepo.rs b/src/providers/node/turborepo.rs index 1f2d34656..604b12e78 100644 --- a/src/providers/node/turborepo.rs +++ b/src/providers/node/turborepo.rs @@ -97,7 +97,7 @@ impl Turborepo { format!("{pkg_manager} --workspace {name} run start") })); } - println!("Warning: Turborepo app `{name}` not found"); + eprintln!("Warning: Turborepo app `{name}` not found"); } if let Some(start_pipeline) = Turborepo::get_start_cmd(&turbo_cfg) { return Ok(Some(start_pipeline)); diff --git a/src/providers/php/mod.rs b/src/providers/php/mod.rs index 85cb0af7c..80a1ea90d 100644 --- a/src/providers/php/mod.rs +++ b/src/providers/php/mod.rs @@ -209,13 +209,13 @@ impl PhpProvider { } else if v.contains("7.4") { "7.4".to_string() } else { - println!( + eprintln!( "Warning: PHP version {v} is not available, using PHP {DEFAULT_PHP_VERSION}" ); DEFAULT_PHP_VERSION.to_string() } } else { - println!("Warning: No PHP version specified, using PHP {DEFAULT_PHP_VERSION}; see https://getcomposer.org/doc/04-schema.md#package-links for how to specify a PHP version."); + eprintln!("Warning: No PHP version specified, using PHP {DEFAULT_PHP_VERSION}; see https://getcomposer.org/doc/04-schema.md#package-links for how to specify a PHP version."); DEFAULT_PHP_VERSION.to_string() }; diff --git a/src/providers/python.rs b/src/providers/python.rs index 30d116556..6b3f88dbf 100644 --- a/src/providers/python.rs +++ b/src/providers/python.rs @@ -86,7 +86,6 @@ impl Provider for PythonProvider { if let Some(poetry_version) = PythonProvider::parse_tool_versions_poetry_version(file_content)? { - println!("Using poetry version from .tool-versions: {poetry_version}"); version = poetry_version; } } @@ -114,7 +113,6 @@ impl Provider for PythonProvider { if let Some(uv_version) = PythonProvider::parse_tool_versions_uv_version(file_content)? { - println!("Using uv version from .tool-versions: {uv_version}"); version = uv_version; } } @@ -379,13 +377,10 @@ impl PythonProvider { Ok(asdf_versions.get("python").map(|s| { let parts: Vec<&str> = s.split('.').collect(); - if parts.len() == 3 { - // this is the expected result, but will be unexpected to users - println!("Patch python version detected in .tool-versions, but not supported in nixpkgs."); - } else if parts.len() == 2 { - println!("Expected a python version string in the format x.y.z from .tool-versions"); - } else { - println!("Could not find a python version string in the format x.y.z or x.y from .tool-versions"); + // We expect there to be 3 or 2 parts (x.y.z) however, only x.y can be parsed. + // So we accept strip x.y.z -> x.y and warn that all other formats are invalid + if parts.len() != 3 && parts.len() != 2 { + eprintln!("Could not find a python version string in the format x.y.z or x.y from .tool-versions. Found {}. Skipping", parts.join(".")); } format!("{}.{}", parts[0], parts[1]) diff --git a/tests/snapshots/generate_plan_tests__deno_jsonc.snap b/tests/snapshots/generate_plan_tests__deno_jsonc.snap new file mode 100644 index 000000000..dd5155698 --- /dev/null +++ b/tests/snapshots/generate_plan_tests__deno_jsonc.snap @@ -0,0 +1,24 @@ +--- +source: tests/generate_plan_tests.rs +expression: plan +--- +{ + "providers": [], + "buildImage": "[build_image]", + "variables": { + "NIXPACKS_METADATA": "deno" + }, + "phases": { + "setup": { + "name": "setup", + "nixPkgs": [ + "deno" + ], + "nixOverlays": [], + "nixpkgsArchive": "[archive]" + } + }, + "start": { + "cmd": "deno start main.ts" + } +} diff --git a/tests/snapshots/generate_plan_tests__go_cmd.snap b/tests/snapshots/generate_plan_tests__go_cmd.snap new file mode 100644 index 000000000..4e0147dd3 --- /dev/null +++ b/tests/snapshots/generate_plan_tests__go_cmd.snap @@ -0,0 +1,52 @@ +--- +source: tests/generate_plan_tests.rs +expression: plan +snapshot_kind: text +--- +{ + "providers": [], + "buildImage": "[build_image]", + "variables": { + "CGO_ENABLED": "0", + "NIXPACKS_METADATA": "go" + }, + "phases": { + "build": { + "name": "build", + "dependsOn": [ + "install", + "setup" + ], + "cmds": [ + "go build -o out ./cmd/server" + ], + "cacheDirectories": [ + "/root/.cache/go-build" + ] + }, + "install": { + "name": "install", + "dependsOn": [ + "setup" + ], + "cmds": [ + "go mod download" + ], + "cacheDirectories": [ + "/root/.cache/go-build" + ] + }, + "setup": { + "name": "setup", + "nixPkgs": [ + "go_1_18" + ], + "nixOverlays": [], + "nixpkgsArchive": "[archive]" + } + }, + "start": { + "cmd": "./out", + "runImage": "ubuntu:jammy" + } +} diff --git a/tests/snapshots/generate_plan_tests__node_node_version.snap b/tests/snapshots/generate_plan_tests__node_node_version.snap new file mode 100644 index 000000000..a7f82aa0f --- /dev/null +++ b/tests/snapshots/generate_plan_tests__node_node_version.snap @@ -0,0 +1,54 @@ +--- +source: tests/generate_plan_tests.rs +expression: plan +--- +{ + "providers": [], + "buildImage": "[build_image]", + "variables": { + "CI": "true", + "NIXPACKS_METADATA": "node", + "NODE_ENV": "production", + "NPM_CONFIG_PRODUCTION": "false" + }, + "phases": { + "build": { + "name": "build", + "dependsOn": [ + "install" + ], + "cacheDirectories": [ + "node_modules/.cache" + ] + }, + "install": { + "name": "install", + "dependsOn": [ + "setup" + ], + "cmds": [ + "npm ci" + ], + "cacheDirectories": [ + "/root/.npm" + ], + "paths": [ + "/app/node_modules/.bin" + ] + }, + "setup": { + "name": "setup", + "nixPkgs": [ + "nodejs_22", + "npm-8_x" + ], + "nixOverlays": [ + "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz" + ], + "nixpkgsArchive": "[archive]" + } + }, + "start": { + "cmd": "npm run start" + } +} diff --git a/tests/snapshots/generate_plan_tests__node_nvmrc_invalid_lts.snap b/tests/snapshots/generate_plan_tests__node_nvmrc_invalid_lts.snap new file mode 100644 index 000000000..ad79a5806 --- /dev/null +++ b/tests/snapshots/generate_plan_tests__node_nvmrc_invalid_lts.snap @@ -0,0 +1,54 @@ +--- +source: tests/generate_plan_tests.rs +expression: plan +--- +{ + "providers": [], + "buildImage": "[build_image]", + "variables": { + "CI": "true", + "NIXPACKS_METADATA": "node", + "NODE_ENV": "production", + "NPM_CONFIG_PRODUCTION": "false" + }, + "phases": { + "build": { + "name": "build", + "dependsOn": [ + "install" + ], + "cacheDirectories": [ + "node_modules/.cache" + ] + }, + "install": { + "name": "install", + "dependsOn": [ + "setup" + ], + "cmds": [ + "npm ci" + ], + "cacheDirectories": [ + "/root/.npm" + ], + "paths": [ + "/app/node_modules/.bin" + ] + }, + "setup": { + "name": "setup", + "nixPkgs": [ + "nodejs_18", + "npm-8_x" + ], + "nixOverlays": [ + "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz" + ], + "nixpkgsArchive": "[archive]" + } + }, + "start": { + "cmd": "npm run start" + } +} diff --git a/tests/snapshots/generate_plan_tests__node_nvmrc_lts.snap b/tests/snapshots/generate_plan_tests__node_nvmrc_lts.snap new file mode 100644 index 000000000..da280c251 --- /dev/null +++ b/tests/snapshots/generate_plan_tests__node_nvmrc_lts.snap @@ -0,0 +1,54 @@ +--- +source: tests/generate_plan_tests.rs +expression: plan +--- +{ + "providers": [], + "buildImage": "[build_image]", + "variables": { + "CI": "true", + "NIXPACKS_METADATA": "node", + "NODE_ENV": "production", + "NPM_CONFIG_PRODUCTION": "false" + }, + "phases": { + "build": { + "name": "build", + "dependsOn": [ + "install" + ], + "cacheDirectories": [ + "node_modules/.cache" + ] + }, + "install": { + "name": "install", + "dependsOn": [ + "setup" + ], + "cmds": [ + "npm ci" + ], + "cacheDirectories": [ + "/root/.npm" + ], + "paths": [ + "/app/node_modules/.bin" + ] + }, + "setup": { + "name": "setup", + "nixPkgs": [ + "nodejs_20", + "npm-8_x" + ], + "nixOverlays": [ + "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz" + ], + "nixpkgsArchive": "[archive]" + } + }, + "start": { + "cmd": "npm run start" + } +}