diff --git a/.gitignore b/.gitignore index 002785e..c64e0d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ node_modules -dist/ +/dist/ /Dockerfile \ No newline at end of file diff --git a/README.md b/README.md index 0cdce1f..ed1a1eb 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,13 @@ related to this tool. - [x] Automatically detect the runtime and framework used by your project - [x] Use version managers like [asdf](https://github.com/asdf-vm), nvm, rbenv, and pyenv to install the correct version of the runtime -- [x] Make a best effort to detect any install, build, and run commands +- [x] Make a best effort to detect any install, build, and start commands - [x] Generate a Dockerfile with sensible defaults that are configurable via [Docker Build Args](https://docs.docker.com/build/guide/build-args/) - [x] Support for a wide range of the most popular languages and frameworks including Next.js, Phoenix, Spring Boot, Django, and more - [x] Use Debian Slim as the runtime image for a smaller image size and better security, while still supporting the most common dependencies and avoiding deployment headaches caused by Alpine Linux gotchas - [x] Includes `wget` in the runtime image for adding health checks to services, e.g. `wget -nv -t1 --spider 'http://localhost:8080/healthz' || exit 1` - [x] Use multi-stage builds to reduce the size of the final image +- [x] Run the application as a non-root user for better security - [x] Supports multi-platform images that run on both x86 and ARM CPU architectures ## Supported Runtimes @@ -105,8 +106,8 @@ For example, if it finds a `package.json` file, it will assume the project is a a `next.config.js` file is present, in which case it will assume the project is a Next.js project. From there, it will read any `.tool-versions` or other version manager files to determine the correct version -of the runtime to install. It will then make a best effort to detect any install, build, and run commands. -For example, a `serve`, `start`, `start:prod` command in a `package.json` file will be used as the run command. +of the runtime to install. It will then make a best effort to detect any install, build, and start commands. +For example, a `serve`, `start`, `start:prod` command in a `package.json` file will be used as the start command. Runtimes are matched against in the order they appear when you run `new-dockerfile --runtime list`. @@ -251,15 +252,11 @@ Detected in order of precedence: [Java](https://www.java.com/) is a class-based, object-oriented programming language that is designed to have as few implementation dependencies as possible. #### Detected Files - - `build.gradle` - - `gradlew` - `pom.{xml,atom,clj,groovy,rb,scala,yml,yaml}` #### Version Detection JDK version: - `.tool-versions` - `java {VERSION}` -Gradle version: - - `.tool-versions` - `gradle {VERSION}` Maven version: - `.tool-versions` - `maven {VERSION}` @@ -268,7 +265,6 @@ Maven version: #### Build Args - `VERSION` - The version of the JDK to install (default: `17`) - - `GRADLE_VERSION` - The version of Gradle to install (default: `8`) - `MAVEN_VERSION` - The version of Maven to install (default: `3`) - `JAVA_OPTS` - The Java options to pass to the JVM (default: `-Xmx512m -Xms256m`) - `BUILD_CMD` - The command to build the project (default: best guess via source code) @@ -279,13 +275,10 @@ Maven version: #### Build Command - If Maven: `mvn -DoutputFile=target/mvn-dependency-list.log -B -DskipTests clean dependency:list install` -- If Gradle: `./gradlew clean build -x check -x test` #### Start Command - Default: `java $JAVA_OPTS -jar target/*jar` -- If Gradle: `java $JAVA_OPTS -jar $(ls -1 build/libs/*jar | grep -v plain)` - If Spring Boot: `java -Dserver.port=${PORT} $JAVA_OPTS -jar target/*jar` -- If Spring Boot w/ Gradle: `java -Dserver.port=${PORT} $JAVA_OPTS -jar $(ls -1 build/libs/*jar | grep -v plain)` --- @@ -300,6 +293,8 @@ Maven version: #### Version Detection - `.tool-versions` - `nodejs {VERSION}` + - `.nvmrc` - `v{VERSION}` + - `.node-version` - `v{VERSION}` #### Runtime Image `node:${VERSION}-slim` @@ -343,6 +338,8 @@ fi #### Version Detection - `.tool-versions` - `nodejs {VERSION}` + - `.nvmrc` - `v{VERSION}` + - `.node-version` - `v{VERSION}` #### Runtime Image `node:${VERSION}-slim` @@ -365,7 +362,7 @@ In order of precedence: #### Start Command In order of precedence: - `package.json` scripts: `"serve", "start:prod", "start:production", "start-prod", "start-production", "start"` - - `package.json` scripts search for regex matching: `^.*?\bnode(mon)?\b.*?(index|main|server|client)\.js\b` + - `package.json` scripts search for regex matching: `^.*?\b(ts-)?node(mon)?\b.*?(index|main|server|client)\.([cm]?[tj]s)\b` - `package.json` main/module file: `node ${mainFile}` --- @@ -504,6 +501,12 @@ In order of precedence: - `TARGETARCH` - The target architecture for the build (default: `amd64`) - `BIN_NAME` - The name of the release binary (default: detected via `Cargo.toml`) +#### Build Command +```sh +if [ "${TARGETARCH}" = "amd64" ]; then rustup target add x86_64-unknown-linux-gnu; else rustup target add aarch64-unknown-linux-gnu; fi +if [ "${TARGETARCH}" = "amd64" ]; then cargo zigbuild --release --target x86_64-unknown-linux-gnu; else cargo zigbuild --release --target aarch64-unknown-linux-gnu; fi +``` + #### Start Command Determined by the binary name in the `Cargo.toml` file - `["/app/app"]` diff --git a/main.go b/main.go index bb84e22..52fc642 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,4 @@ +// A library for auto-generating Dockerfiles from project source code. package dockerfile import ( @@ -9,7 +10,7 @@ import ( "github.com/flexstack/new-dockerfile/runtime" ) -// Creates a new Dockerfile generator. +// Creates a new Dockerfile generator. If no logger is provided, a default logger is created. func New(log ...*slog.Logger) *Dockerfile { var logger *slog.Logger @@ -50,6 +51,7 @@ func (a *Dockerfile) Write(path string) error { return nil } +// Lists all runtimes that the Dockerfile generator can auto-generate. func (a *Dockerfile) ListRuntimes() []runtime.Runtime { return []runtime.Runtime{ &runtime.Golang{Log: a.log}, @@ -67,6 +69,7 @@ func (a *Dockerfile) ListRuntimes() []runtime.Runtime { } } +// Matches the runtime of the project at the given path. func (a *Dockerfile) MatchRuntime(path string) (runtime.Runtime, error) { for _, r := range a.ListRuntimes() { if r.Match(path) { diff --git a/node/.tool-versions b/node/.tool-versions deleted file mode 100644 index e4ab54c..0000000 --- a/node/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -golang 1.21.1 diff --git a/node/README.md b/node/README.md index 9ae3b28..c0c06de 100644 --- a/node/README.md +++ b/node/README.md @@ -13,12 +13,13 @@ related to this tool. - [x] Automatically detect the runtime and framework used by your project - [x] Use version managers like [asdf](https://github.com/asdf-vm), nvm, rbenv, and pyenv to install the correct version of the runtime -- [x] Make a best effort to detect any install, build, and run commands +- [x] Make a best effort to detect any install, build, and start commands - [x] Generate a Dockerfile with sensible defaults that are configurable via [Docker Build Args](https://docs.docker.com/build/guide/build-args/) - [x] Support for a wide range of the most popular languages and frameworks including Next.js, Phoenix, Spring Boot, Django, and more - [x] Use Debian Slim as the runtime image for a smaller image size and better security, while still supporting the most common dependencies and avoiding deployment headaches caused by Alpine Linux gotchas - [x] Includes `wget` in the runtime image for adding health checks to services, e.g. `wget -nv -t1 --spider 'http://localhost:8080/healthz' || exit 1` - [x] Use multi-stage builds to reduce the size of the final image +- [x] Run the application as a non-root user for better security - [x] Supports multi-platform images that run on both x86 and ARM CPU architectures ## Supported Runtimes @@ -93,8 +94,8 @@ For example, if it finds a `package.json` file, it will assume the project is a a `next.config.js` file is present, in which case it will assume the project is a Next.js project. From there, it will read any `.tool-versions` or other version manager files to determine the correct version -of the runtime to install. It will then make a best effort to detect any install, build, and run commands. -For example, a `serve`, `start`, `start:prod` command in a `package.json` file will be used as the run command. +of the runtime to install. It will then make a best effort to detect any install, build, and start commands. +For example, a `serve`, `start`, `start:prod` command in a `package.json` file will be used as the start command. Runtimes are matched against in the order they appear when you run `new-dockerfile --runtime list`. diff --git a/runtime/bun.go b/runtime/bun.go index d91f90d..2417c57 100644 --- a/runtime/bun.go +++ b/runtime/bun.go @@ -119,6 +119,11 @@ func (d *Bun) GenerateDockerfile(path string) ([]byte, error) { startCMD = string(startCMDJSON) } + if buildCMD != "" { + buildCMDJSON, _ := json.Marshal(buildCMD) + buildCMD = string(buildCMDJSON) + } + var buf bytes.Buffer if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ "Version": *version, diff --git a/runtime/bun_test.go b/runtime/bun_test.go new file mode 100644 index 0000000..76cbd5e --- /dev/null +++ b/runtime/bun_test.go @@ -0,0 +1,100 @@ +package runtime_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/flexstack/new-dockerfile/runtime" +) + +func TestBunMatch(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "Bun project", + path: "../testdata/bun", + expected: true, + }, + { + name: "Bun project with .ts file", + path: "../testdata/bun-bunfig", + expected: true, + }, + { + name: "Not a Bun project", + path: "../testdata/deno", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + bun := &runtime.Bun{Log: logger} + if bun.Match(test.path) != test.expected { + t.Errorf("expected %v, got %v", test.expected, bun.Match(test.path)) + } + }) + } +} + +func TestBunGenerateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + expected []any + }{ + { + name: "Bun project", + path: "../testdata/bun", + expected: []any{`ARG VERSION=1`, `ARG INSTALL_CMD="bun install"`, regexp.MustCompile(`^ARG BUILD_CMD=$`), `ARG START_CMD="bun index.ts"`}, + }, + { + name: "Bun project with .ts file", + path: "../testdata/bun-bunfig", + expected: []any{`ARG VERSION=1.1.4`, `ARG INSTALL_CMD="bun install"`, `ARG BUILD_CMD="bun run build:prod"`, `ARG START_CMD="bun run start:production"`}, + }, + { + name: "Not a Bun project", + path: "../testdata/deno", + expected: []any{`ARG VERSION=1`, regexp.MustCompile(`^ARG INSTALL_CMD="bun install"`), regexp.MustCompile(`^ARG BUILD_CMD=$`), regexp.MustCompile(`^ARG START_CMD=$`)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + bun := &runtime.Bun{Log: logger} + dockerfile, err := bun.GenerateDockerfile(test.path) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, line := range test.expected { + found := false + lines := strings.Split(string(dockerfile), "\n") + + for _, l := range lines { + switch v := line.(type) { + case string: + if strings.Contains(l, v) { + found = true + break + } + case *regexp.Regexp: + if v.MatchString(l) { + found = true + break + } + } + } + + if !found { + t.Errorf("expected %v, not found in %v", line, string(dockerfile)) + } + } + }) + } +} diff --git a/runtime/deno_test.go b/runtime/deno_test.go index 8918475..fa48552 100644 --- a/runtime/deno_test.go +++ b/runtime/deno_test.go @@ -8,7 +8,7 @@ import ( "github.com/flexstack/new-dockerfile/runtime" ) -func TestRuntimeMatch(t *testing.T) { +func TestDenoMatch(t *testing.T) { tests := []struct { name string path string @@ -41,7 +41,7 @@ func TestRuntimeMatch(t *testing.T) { } } -func TestRuntimeGenerateDockerfile(t *testing.T) { +func TestDenoGenerateDockerfile(t *testing.T) { tests := []struct { name string path string diff --git a/runtime/elixir.go b/runtime/elixir.go index b451680..74c0d74 100644 --- a/runtime/elixir.go +++ b/runtime/elixir.go @@ -269,6 +269,10 @@ func isPhoenixProject(path string) bool { } func findBinName(path string) (string, error) { + if _, err := os.Stat(filepath.Join(path, "mix.exs")); err != nil { + return "", nil + } + configFile, err := os.Open(filepath.Join(path, "mix.exs")) if err != nil { return "", err diff --git a/runtime/elixir_test.go b/runtime/elixir_test.go new file mode 100644 index 0000000..96eddb4 --- /dev/null +++ b/runtime/elixir_test.go @@ -0,0 +1,100 @@ +package runtime_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/flexstack/new-dockerfile/runtime" +) + +func TestElixirMatch(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "Elixir project", + path: "../testdata/elixir", + expected: true, + }, + { + name: "Elixir project with .tool-versions", + path: "../testdata/elixir-tool-versions", + expected: true, + }, + { + name: "Not a Elixir project", + path: "../testdata/deno", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + elixir := &runtime.Elixir{Log: logger} + if elixir.Match(test.path) != test.expected { + t.Errorf("expected %v, got %v", test.expected, elixir.Match(test.path)) + } + }) + } +} + +func TestElixirGenerateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + expected []any + }{ + { + name: "Elixir project", + path: "../testdata/elixir", + expected: []any{`ARG VERSION=1.10`, `ARG OTP_VERSION=22`, `ARG BIN_NAME=hello`}, + }, + { + name: "Elixir project with .tool-versions", + path: "../testdata/elixir-tool-versions", + expected: []any{`ARG VERSION=1.11`, `ARG OTP_VERSION=23`, `ARG BIN_NAME=hello`}, + }, + { + name: "Not a Elixir project", + path: "../testdata/deno", + expected: []any{`ARG VERSION=1.12`, `ARG OTP_VERSION=26`, regexp.MustCompile(`^ARG BIN_NAME=$`)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + elixir := &runtime.Elixir{Log: logger} + dockerfile, err := elixir.GenerateDockerfile(test.path) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, line := range test.expected { + found := false + lines := strings.Split(string(dockerfile), "\n") + + for _, l := range lines { + switch v := line.(type) { + case string: + if strings.Contains(l, v) { + found = true + break + } + case *regexp.Regexp: + if v.MatchString(l) { + found = true + break + } + } + } + + if !found { + t.Errorf("expected %v, not found in %v", line, string(dockerfile)) + } + } + }) + } +} diff --git a/runtime/golang_test.go b/runtime/golang_test.go new file mode 100644 index 0000000..21f7c0f --- /dev/null +++ b/runtime/golang_test.go @@ -0,0 +1,100 @@ +package runtime_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/flexstack/new-dockerfile/runtime" +) + +func TestGolangMatch(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "Golang project", + path: "../testdata/go", + expected: true, + }, + { + name: "Golang project with go.mod file", + path: "../testdata/go-mod", + expected: true, + }, + { + name: "Not a Golang project", + path: "../testdata/deno", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + golang := &runtime.Golang{Log: logger} + if golang.Match(test.path) != test.expected { + t.Errorf("expected %v, got %v", test.expected, golang.Match(test.path)) + } + }) + } +} + +func TestGolangGenerateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + expected []any + }{ + { + name: "Golang project", + path: "../testdata/go", + expected: []any{`ARG VERSION=1.16.3`, `ARG PACKAGE=./main.go`}, + }, + { + name: "Golang project with go.mod file", + path: "../testdata/go-mod", + expected: []any{`ARG VERSION=1.22.3`, `ARG PACKAGE=./cmd/hello`}, + }, + { + name: "Not a Golang project", + path: "../testdata/ruby", + expected: []any{`ARG VERSION=1.17`, regexp.MustCompile(`^ARG PACKAGE=$`)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + golang := &runtime.Golang{Log: logger} + dockerfile, err := golang.GenerateDockerfile(test.path) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, line := range test.expected { + found := false + lines := strings.Split(string(dockerfile), "\n") + + for _, l := range lines { + switch v := line.(type) { + case string: + if strings.Contains(l, v) { + found = true + break + } + case *regexp.Regexp: + if v.MatchString(l) { + found = true + break + } + } + } + + if !found { + t.Errorf("expected %v, not found in %v", line, string(dockerfile)) + } + } + }) + } +} diff --git a/runtime/main.go b/runtime/main.go index 03f1b85..b98e23f 100644 --- a/runtime/main.go +++ b/runtime/main.go @@ -1,8 +1,12 @@ package runtime +// An interface that all runtimes must implement. type Runtime interface { + // Returns the name of the runtime. Name() RuntimeName + // Returns true if the runtime can be used for the given path. Match(path string) bool + // Generates a Dockerfile for the given path. GenerateDockerfile(path string) ([]byte, error) } diff --git a/runtime/nextjs_test.go b/runtime/nextjs_test.go new file mode 100644 index 0000000..3c1cdfd --- /dev/null +++ b/runtime/nextjs_test.go @@ -0,0 +1,100 @@ +package runtime_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/flexstack/new-dockerfile/runtime" +) + +func TestNextJSMatch(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "NextJS project", + path: "../testdata/nextjs", + expected: true, + }, + { + name: "NextJS project with standalone output", + path: "../testdata/nextjs-standalone", + expected: true, + }, + { + name: "Not a NextJS project", + path: "../testdata/deno", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + nextjs := &runtime.NextJS{Log: logger} + if nextjs.Match(test.path) != test.expected { + t.Errorf("expected %v, got %v", test.expected, nextjs.Match(test.path)) + } + }) + } +} + +func TestNextJSGenerateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + expected []any + }{ + { + name: "NextJS project", + path: "../testdata/nextjs", + expected: []any{`ARG VERSION=lts`, `CMD ["node_modules/.bin/next", "start", "-H", "0.0.0.0"]`}, + }, + { + name: "NextJS project with standalone output", + path: "../testdata/nextjs-standalone", + expected: []any{`ARG VERSION=16.0.0`, `CMD HOSTNAME="0.0.0.0" node server.js`}, + }, + { + name: "Not a NextJS project", + path: "../testdata/deno", + expected: []any{`ARG VERSION=lts`, `CMD ["node_modules/.bin/next", "start", "-H", "0.0.0.0"]`}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + nextjs := &runtime.NextJS{Log: logger} + dockerfile, err := nextjs.GenerateDockerfile(test.path) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, line := range test.expected { + found := false + lines := strings.Split(string(dockerfile), "\n") + + for _, l := range lines { + switch v := line.(type) { + case string: + if strings.Contains(l, v) { + found = true + break + } + case *regexp.Regexp: + if v.MatchString(l) { + found = true + break + } + } + } + + if !found { + t.Errorf("expected %v, not found in %v", line, string(dockerfile)) + } + } + }) + } +} diff --git a/runtime/node.go b/runtime/node.go index 2c362f3..9c5cb4e 100644 --- a/runtime/node.go +++ b/runtime/node.go @@ -50,16 +50,22 @@ func (d *Node) GenerateDockerfile(path string) ([]byte, error) { return nil, err } - f, err := os.Open(filepath.Join(path, "package.json")) - if err != nil { - return nil, fmt.Errorf("Failed to open package.json file") - } + var packageJSON map[string]interface{} - defer f.Close() + if _, err := os.Stat(filepath.Join(path, "package.json")); err == nil { + f, err := os.Open(filepath.Join(path, "package.json")) + if err != nil { + return nil, fmt.Errorf("Failed to open package.json file") + } - var packageJSON map[string]interface{} - if err := json.NewDecoder(f).Decode(&packageJSON); err != nil { - return nil, fmt.Errorf("Failed to decode package.json file") + defer f.Close() + + if err := json.NewDecoder(f).Decode(&packageJSON); err != nil { + return nil, fmt.Errorf("Failed to decode package.json file") + } + } else { + d.Log.Info("No package.json file found") + packageJSON = map[string]interface{}{} } installCMD := "npm ci" @@ -76,6 +82,7 @@ func (d *Node) GenerateDockerfile(path string) ([]byte, error) { var buildCMD, startCMD string scripts, ok := packageJSON["scripts"].(map[string]interface{}) + if ok { d.Log.Info("Detected scripts in package.json") startCommands := []string{"serve", "start:prod", "start:production", "start-prod", "start-production", "start"} @@ -89,6 +96,7 @@ func (d *Node) GenerateDockerfile(path string) ([]byte, error) { if startCMD == "" { for name, v := range scripts { value, ok := v.(string) + if ok && startScriptRe.MatchString(value) { startCMD = fmt.Sprintf("%s run %s", packageManager, name) break @@ -145,10 +153,10 @@ func (d *Node) GenerateDockerfile(path string) ([]byte, error) { var buf bytes.Buffer if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ - "Version": *version, - "InstallCMD": installCMD, - "BuildScript": buildCMD, - "StartCMD": startCMD, + "Version": *version, + "InstallCMD": installCMD, + "BuildCMD": buildCMD, + "StartCMD": startCMD, }); err != nil { return nil, fmt.Errorf("Failed to execute template") } @@ -156,7 +164,7 @@ func (d *Node) GenerateDockerfile(path string) ([]byte, error) { return buf.Bytes(), nil } -var startScriptRe = regexp.MustCompile(`^.*?\bnode(mon)?\b.*?(index|main|server|client)\.js\b`) +var startScriptRe = regexp.MustCompile(`^.*?\b(ts-)?node(mon)?\b.*?(index|main|server|client)\.([cm]?[tj]s)\b`) var nodeTemplate = strings.TrimSpace(` ARG VERSION={{.Version}} @@ -238,6 +246,10 @@ func findNodeVersion(path string, log *slog.Logger) (*string, error) { version = strings.TrimPrefix(line, "v") log.Info("Detected Node version in " + file + ": " + version) break + } else { + version = line + log.Info("Detected Node version in " + file + ": " + version) + break } } diff --git a/runtime/node_test.go b/runtime/node_test.go new file mode 100644 index 0000000..ae91eeb --- /dev/null +++ b/runtime/node_test.go @@ -0,0 +1,110 @@ +package runtime_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/flexstack/new-dockerfile/runtime" +) + +func TestNodeMatch(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "Node project", + path: "../testdata/node", + expected: true, + }, + { + name: "Node project with pnpm", + path: "../testdata/node-pnpm", + expected: true, + }, + { + name: "Node project with yarn", + path: "../testdata/node-yarn", + expected: true, + }, + { + name: "Not a Node project", + path: "../testdata/deno", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + node := &runtime.Node{Log: logger} + if node.Match(test.path) != test.expected { + t.Errorf("expected %v, got %v", test.expected, node.Match(test.path)) + } + }) + } +} + +func TestNodeGenerateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + expected []any + }{ + { + name: "Node project", + path: "../testdata/node", + expected: []any{`ARG VERSION=lts`, `ARG INSTALL_CMD="npm ci"`, regexp.MustCompile(`^ARG BUILD_CMD=$`), `ARG START_CMD="node index.ts"`}, + }, + { + name: "Node project with pnpm", + path: "../testdata/node-pnpm", + expected: []any{`ARG VERSION=16.0.0`, `ARG INSTALL_CMD="corepack enable pnpm \u0026\u0026 pnpm i --frozen-lockfile"`, `ARG BUILD_CMD="pnpm run build:prod"`, `ARG START_CMD="pnpm run start:production"`}, + }, + { + name: "Node project with yarn", + path: "../testdata/node-yarn", + expected: []any{`ARG VERSION=16.0.0`, `ARG INSTALL_CMD="yarn --frozen-lockfile"`, `ARG BUILD_CMD="yarn run build:prod"`, `ARG START_CMD="yarn run start-it"`}, + }, + { + name: "Not a Node project", + path: "../testdata/deno", + expected: []any{`ARG VERSION=lts`, regexp.MustCompile(`^ARG INSTALL_CMD="npm ci"`), regexp.MustCompile(`^ARG BUILD_CMD=$`), regexp.MustCompile(`^ARG START_CMD=$`)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + node := &runtime.Node{Log: logger} + dockerfile, err := node.GenerateDockerfile(test.path) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, line := range test.expected { + found := false + lines := strings.Split(string(dockerfile), "\n") + + for _, l := range lines { + switch v := line.(type) { + case string: + if strings.Contains(l, v) { + found = true + break + } + case *regexp.Regexp: + if v.MatchString(l) { + found = true + break + } + } + } + + if !found { + t.Errorf("expected %v, not found in %v", line, string(dockerfile)) + } + } + }) + } +} diff --git a/runtime/php.go b/runtime/php.go index 1ee55c8..2931673 100644 --- a/runtime/php.go +++ b/runtime/php.go @@ -57,18 +57,27 @@ func (d *PHP) GenerateDockerfile(path string) ([]byte, error) { } packageManager := "" + npmInstallCMD := "" if _, err := os.Stat(filepath.Join(path, "package-lock.json")); err == nil { packageManager = "npm" - installCMD = installCMD + " && npm ci" + npmInstallCMD = "npm ci" } else if _, err := os.Stat(filepath.Join(path, "pnpm-lock.yaml")); err == nil { packageManager = "pnpm" - installCMD = installCMD + " && corepack enable pnpm && pnpm i --frozen-lockfile" + npmInstallCMD = "corepack enable pnpm && pnpm i --frozen-lockfile" } else if _, err := os.Stat(filepath.Join(path, "yarn.lock")); err == nil { packageManager = "yarn" - installCMD = installCMD + " && yarn --frozen-lockfile" + npmInstallCMD = "yarn --frozen-lockfile" } else if _, err := os.Stat(filepath.Join(path, "bun.lockb")); err == nil { packageManager = "bun" - installCMD = installCMD + " && bun install" + npmInstallCMD = "bun install" + } + + if npmInstallCMD != "" { + if installCMD == "" { + installCMD = npmInstallCMD + } else { + installCMD = fmt.Sprintf("%s && %s", installCMD, npmInstallCMD) + } } buildCMD := "" diff --git a/runtime/php_test.go b/runtime/php_test.go new file mode 100644 index 0000000..a821682 --- /dev/null +++ b/runtime/php_test.go @@ -0,0 +1,110 @@ +package runtime_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/flexstack/new-dockerfile/runtime" +) + +func TestPHPMatch(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "PHP project", + path: "../testdata/php", + expected: true, + }, + { + name: "PHP project with composer", + path: "../testdata/php-composer", + expected: true, + }, + { + name: "PHP project with NPM", + path: "../testdata/php-npm", + expected: true, + }, + { + name: "Not a PHP project", + path: "../testdata/deno", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + php := &runtime.PHP{Log: logger} + if php.Match(test.path) != test.expected { + t.Errorf("expected %v, got %v", test.expected, php.Match(test.path)) + } + }) + } +} + +func TestPHPGenerateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + expected []any + }{ + { + name: "PHP project", + path: "../testdata/php", + expected: []any{`ARG VERSION=8.3`, regexp.MustCompile(`^ARG BUILD_CMD=$`), `ARG START_CMD="apache2-foreground`}, + }, + { + name: "PHP project with composer", + path: "../testdata/php-composer", + expected: []any{`ARG VERSION=5.3`, `ARG INSTALL_CMD="composer update \u0026\u0026 composer install --prefer-dist --no-dev --optimize-autoloader --no-interaction"`, regexp.MustCompile(`^ARG BUILD_CMD=$`), `ARG START_CMD="apache2-foreground`}, + }, + { + name: "PHP project with NPM", + path: "../testdata/php-npm", + expected: []any{`ARG VERSION=8.2.0`, `ARG INSTALL_CMD="yarn --frozen-lockfile"`, `ARG BUILD_CMD="yarn run build"`, `ARG START_CMD="apache2-foreground`}, + }, + { + name: "Not a PHP project", + path: "../testdata/deno", + expected: []any{`ARG VERSION=8.3`, regexp.MustCompile(`^ARG INSTALL_CMD=$`), regexp.MustCompile(`^ARG BUILD_CMD=$`), `ARG START_CMD="apache2-foreground`}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + php := &runtime.PHP{Log: logger} + dockerfile, err := php.GenerateDockerfile(test.path) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, line := range test.expected { + found := false + lines := strings.Split(string(dockerfile), "\n") + + for _, l := range lines { + switch v := line.(type) { + case string: + if strings.Contains(l, v) { + found = true + break + } + case *regexp.Regexp: + if v.MatchString(l) { + found = true + break + } + } + } + + if !found { + t.Errorf("expected %v, not found in %v", line, string(dockerfile)) + } + } + }) + } +} diff --git a/runtime/python.go b/runtime/python.go index e226ee5..dbb760b 100644 --- a/runtime/python.go +++ b/runtime/python.go @@ -92,6 +92,10 @@ func (d *Python) GenerateDockerfile(path string) ([]byte, error) { if name, ok := project["name"].(string); ok { projectName = name } + } else if project, ok := pyprojectTOML["tool.poetry"].(map[string]interface{}); ok { + if name, ok := project["name"].(string); ok { + projectName = name + } } } diff --git a/runtime/python_test.go b/runtime/python_test.go new file mode 100644 index 0000000..3561730 --- /dev/null +++ b/runtime/python_test.go @@ -0,0 +1,154 @@ +package runtime_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/flexstack/new-dockerfile/runtime" +) + +func TestPythonMatch(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "Python project", + path: "../testdata/python", + expected: true, + }, + { + name: "Python project with django", + path: "../testdata/python-django", + expected: true, + }, + { + name: "Python project with pdm", + path: "../testdata/python-pdm", + expected: true, + }, + { + name: "Python project with poetry", + path: "../testdata/python-poetry", + expected: true, + }, + { + name: "Python project with pyproject", + path: "../testdata/python-pyproject", + expected: true, + }, + { + name: "Not a Python project", + path: "../testdata/deno", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + python := &runtime.Python{Log: logger} + if python.Match(test.path) != test.expected { + t.Errorf("expected %v, got %v", test.expected, python.Match(test.path)) + } + }) + } +} + +func TestPythonGenerateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + expected []any + }{ + { + name: "Python project", + path: "../testdata/python", + expected: []any{ + `ARG VERSION=3.12`, + `ARG INSTALL_CMD="pip install -r requirements.txt"`, + `ARG START_CMD="python main.py"`, + }, + }, + { + name: "Python project with django", + path: "../testdata/python-django", + expected: []any{ + `ARG VERSION=3.6.0`, + `ARG INSTALL_CMD="PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy"`, + `ARG START_CMD="python manage.py runserver 0.0.0.0:${PORT}"`, + }, + }, + { + name: "Python project with pdm", + path: "../testdata/python-pdm", + expected: []any{ + `ARG VERSION=3.4.1`, + `ARG INSTALL_CMD="pdm install --prod"`, + `ARG START_CMD="python app.py"`, + }, + }, + { + name: "Python project with poetry", + path: "../testdata/python-poetry", + expected: []any{ + `ARG VERSION=3.8.5`, + `ARG INSTALL_CMD="poetry install --no-dev --no-interactive --no-ansi"`, + `ARG START_CMD="python app/main.py"`, + }, + }, + { + name: "Python project with pyproject", + path: "../testdata/python-pyproject", + expected: []any{ + `ARG VERSION=3.12`, + `ARG INSTALL_CMD="pip install --upgrade build setuptools \u0026\u0026 pip install .`, + `ARG START_CMD="python -m pyproject"`, + }, + }, + { + name: "Not a Python project", + path: "../testdata/deno", + expected: []any{ + `ARG VERSION=3.12`, + regexp.MustCompile(`^ARG INSTALL_CMD=$`), + regexp.MustCompile(`^ARG START_CMD=$`), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + python := &runtime.Python{Log: logger} + dockerfile, err := python.GenerateDockerfile(test.path) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, line := range test.expected { + found := false + lines := strings.Split(string(dockerfile), "\n") + + for _, l := range lines { + switch v := line.(type) { + case string: + if strings.Contains(l, v) { + found = true + break + } + case *regexp.Regexp: + if v.MatchString(l) { + found = true + break + } + } + } + + if !found { + t.Errorf("expected %v, not found in %v", line, string(dockerfile)) + } + } + }) + } +} diff --git a/runtime/ruby.go b/runtime/ruby.go index 27c3e23..74d6036 100644 --- a/runtime/ruby.go +++ b/runtime/ruby.go @@ -217,8 +217,23 @@ func findRubyVersion(path string, log *slog.Logger) (*string, error) { scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() - if strings.Contains(line, "ruby") { - version = strings.Split(line, "'")[1] + if strings.HasPrefix(line, "ruby") { + v := strings.Split(line, "'") + if len(v) < 2 { + v = strings.Split(line, "\"") + } + ruby := v[1] + if gteVersionRe.MatchString(ruby) { + version = gteVersionRe.FindStringSubmatch(ruby)[1] + } else if rangeVersionRe.MatchString(ruby) { + version = rangeVersionRe.FindStringSubmatch(ruby)[2] + } else if tildeVersionRe.MatchString(ruby) { + version = tildeVersionRe.FindStringSubmatch(ruby)[1] + } else if caretVersionRe.MatchString(ruby) { + version = caretVersionRe.FindStringSubmatch(ruby)[1] + } else if exactVersionRe.MatchString(ruby) { + version = ruby + } log.Info("Detected Ruby version from Gemfile: " + version) break } @@ -258,7 +273,7 @@ func isRailsProject(path string) bool { scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() - if strings.Contains(line, "gem 'rails'") { + if strings.HasPrefix(line, "gem 'rails'") || strings.HasPrefix(line, "gem \"rails\"") { return true } } diff --git a/runtime/ruby_test.go b/runtime/ruby_test.go new file mode 100644 index 0000000..62c70da --- /dev/null +++ b/runtime/ruby_test.go @@ -0,0 +1,140 @@ +package runtime_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/flexstack/new-dockerfile/runtime" +) + +func TestRubyMatch(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "Ruby project", + path: "../testdata/ruby", + expected: true, + }, + { + name: "Ruby project with config/environment.rb", + path: "../testdata/ruby-config-environment", + expected: true, + }, + { + name: "Ruby project with config.ru", + path: "../testdata/ruby-config-ru", + expected: true, + }, + { + name: "Ruby project with rails", + path: "../testdata/ruby-rails", + expected: true, + }, + { + name: "Not a Ruby project", + path: "../testdata/deno", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ruby := &runtime.Ruby{Log: logger} + if ruby.Match(test.path) != test.expected { + t.Errorf("expected %v, got %v", test.expected, ruby.Match(test.path)) + } + }) + } +} + +func TestRubyGenerateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + expected []any + }{ + { + name: "Ruby project", + path: "../testdata/ruby", + expected: []any{ + `ARG VERSION=2.0.0`, + regexp.MustCompile(`^ARG INSTALL_CMD="bundle install"$`), + regexp.MustCompile(`^ARG BUILD_CMD=$`), + regexp.MustCompile(`^ARG START_CMD=$`), + }, + }, + { + name: "Ruby project with config/environment.rb", + path: "../testdata/ruby-config-environment", + expected: []any{ + `ARG VERSION=3.0.1`, + regexp.MustCompile(`^ARG INSTALL_CMD="bundle install"$`), + regexp.MustCompile(`^ARG BUILD_CMD=$`), + `ARG START_CMD="bundle exec ruby script/server"`, + }, + }, + { + name: "Ruby project with config.ru", + path: "../testdata/ruby-config-ru", + expected: []any{ + `ARG VERSION=2.3.0`, + regexp.MustCompile(`^ARG INSTALL_CMD="bundle install"$`), + regexp.MustCompile(`^ARG BUILD_CMD=$`), + `ARG START_CMD="bundle exec rackup config.ru -p ${PORT}"`, + }, + }, + { + name: "Ruby project with rails", + path: "../testdata/ruby-rails", + expected: []any{ + `ARG VERSION=3.1`, + `ARG INSTALL_CMD="bundle install \u0026\u0026 corepack enable pnpm \u0026\u0026 pnpm i --frozen-lockfile"`, + `ARG BUILD_CMD="bundle exec rake assets:precompile"`, + `ARG START_CMD="bundle exec rails server -b 0.0.0.0 -p ${PORT}`, + }, + }, + { + name: "Not a Ruby project", + path: "../testdata/deno", + expected: []any{`ARG VERSION=3.1`, regexp.MustCompile(`^ARG INSTALL_CMD="bundle install"$`), regexp.MustCompile(`^ARG BUILD_CMD=$`), regexp.MustCompile(`^ARG START_CMD=$`)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ruby := &runtime.Ruby{Log: logger} + dockerfile, err := ruby.GenerateDockerfile(test.path) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, line := range test.expected { + found := false + lines := strings.Split(string(dockerfile), "\n") + + for _, l := range lines { + switch v := line.(type) { + case string: + if strings.Contains(l, v) { + found = true + break + } + case *regexp.Regexp: + if v.MatchString(l) { + found = true + break + } + } + } + + if !found { + t.Errorf("expected %v, not found in %v", line, string(dockerfile)) + } + } + }) + } +} diff --git a/runtime/rust.go b/runtime/rust.go index 65f5ec4..7004f37 100644 --- a/runtime/rust.go +++ b/runtime/rust.go @@ -45,38 +45,45 @@ func (d *Rust) GenerateDockerfile(path string) ([]byte, error) { var binName string // Parse the Cargo.toml file to get the binary name cargoTomlPath := filepath.Join(path, "Cargo.toml") - f, err := os.Open(cargoTomlPath) - if err != nil { - return nil, fmt.Errorf("Failed to open Cargo.toml") - } - - defer f.Close() + if _, err := os.Stat(cargoTomlPath); err == nil { + f, err := os.Open(cargoTomlPath) + if err != nil { + return nil, fmt.Errorf("Failed to open Cargo.toml") + } - var cargoTOML map[string]interface{} - if err := toml.NewDecoder(f).Decode(&cargoTOML); err != nil { - return nil, fmt.Errorf("Failed to decode Cargo.toml") - } + defer f.Close() - checkBins := []string{"bin", "lib", "package"} - var ok bool - var pkg map[string]interface{} - for _, bin := range checkBins { - pkg, ok = cargoTOML[bin].(map[string]interface{}) - if ok { - break + var cargoTOML map[string]interface{} + if err := toml.NewDecoder(f).Decode(&cargoTOML); err != nil { + return nil, fmt.Errorf("Failed to decode Cargo.toml") } - } - if !ok { - return nil, fmt.Errorf("Failed to determine a binary name from Cargo.toml") - } + checkBins := []string{"bin", "lib", "package"} + var ok bool + var pkg map[string]interface{} + for _, bin := range checkBins { + // [[bin]] + // [lib] + // [package] + if bin == "bin" { + if pkgs, ok := cargoTOML[bin].([]map[string]interface{}); ok { + if len(pkgs) > 0 { + pkg = pkgs[0] + break + } + } + } else if pkg, ok = cargoTOML[bin].(map[string]interface{}); ok { + break + } + } - if binName, ok = pkg["name"].(string); !ok { - return nil, fmt.Errorf("Failed to parse binary name from Cargo.toml") + if binName, ok = pkg["name"].(string); !ok { + d.Log.Warn("Failed to get binary name from Cargo.toml") + } else { + d.Log.Info("Detected binary name: " + binName) + } } - d.Log.Info("Detected binary name: " + binName) - var buf bytes.Buffer if err := tmpl.Option("missingkey=zero").Execute(&buf, map[string]string{ "BinName": binName, diff --git a/runtime/rust_test.go b/runtime/rust_test.go new file mode 100644 index 0000000..3aa23b6 --- /dev/null +++ b/runtime/rust_test.go @@ -0,0 +1,100 @@ +package runtime_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/flexstack/new-dockerfile/runtime" +) + +func TestRustMatch(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "Rust project", + path: "../testdata/rust", + expected: true, + }, + { + name: "Rust project with [[bin]] directive", + path: "../testdata/rust-bin", + expected: true, + }, + { + name: "Not a Rust project", + path: "../testdata/deno", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rust := &runtime.Rust{Log: logger} + if rust.Match(test.path) != test.expected { + t.Errorf("expected %v, got %v", test.expected, rust.Match(test.path)) + } + }) + } +} + +func TestRustGenerateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + expected []any + }{ + { + name: "Rust project", + path: "../testdata/rust", + expected: []any{`ARG BIN_NAME=ingest`}, + }, + { + name: "Rust project with [[bin]] directive", + path: "../testdata/rust-bin", + expected: []any{`ARG BIN_NAME=rg`}, + }, + { + name: "Not a Rust project", + path: "../testdata/deno", + expected: []any{regexp.MustCompile(`^ARG BIN_NAME=$`)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rust := &runtime.Rust{Log: logger} + dockerfile, err := rust.GenerateDockerfile(test.path) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, line := range test.expected { + found := false + lines := strings.Split(string(dockerfile), "\n") + + for _, l := range lines { + switch v := line.(type) { + case string: + if strings.Contains(l, v) { + found = true + break + } + case *regexp.Regexp: + if v.MatchString(l) { + found = true + break + } + } + } + + if !found { + t.Errorf("expected %v, not found in %v", line, string(dockerfile)) + } + } + }) + } +} diff --git a/runtime/static_test.go b/runtime/static_test.go new file mode 100644 index 0000000..0c8ff52 --- /dev/null +++ b/runtime/static_test.go @@ -0,0 +1,120 @@ +package runtime_test + +import ( + "regexp" + "strings" + "testing" + + "github.com/flexstack/new-dockerfile/runtime" +) + +func TestStaticMatch(t *testing.T) { + tests := []struct { + name string + path string + expected bool + }{ + { + name: "Static project", + path: "../testdata/static", + expected: true, + }, + { + name: "Static project with public directory", + path: "../testdata/static-public", + expected: true, + }, + { + name: "Static project with static directory", + path: "../testdata/static-static", + expected: true, + }, + { + name: "Static project with dist directory", + path: "../testdata/static-dist", + expected: true, + }, + { + name: "Not a Static project", + path: "../testdata/deno", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + static := &runtime.Static{Log: logger} + if static.Match(test.path) != test.expected { + t.Errorf("expected %v, got %v", test.expected, static.Match(test.path)) + } + }) + } +} + +func TestStaticGenerateDockerfile(t *testing.T) { + tests := []struct { + name string + path string + expected []any + }{ + { + name: "Static project", + path: "../testdata/static", + expected: []any{`ARG SERVER_ROOT=.`}, + }, + { + name: "Static project with public directory", + path: "../testdata/static-public", + expected: []any{`ARG SERVER_ROOT=public`}, + }, + { + name: "Static project with static directory", + path: "../testdata/static-static", + expected: []any{`ARG SERVER_ROOT=static`}, + }, + { + name: "Static project with dist directory", + path: "../testdata/static-dist", + expected: []any{`ARG SERVER_ROOT=dist`}, + }, + { + name: "Not a Static project", + path: "../testdata/deno", + expected: []any{`ARG SERVER_ROOT=.`}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + static := &runtime.Static{Log: logger} + dockerfile, err := static.GenerateDockerfile(test.path) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, line := range test.expected { + found := false + lines := strings.Split(string(dockerfile), "\n") + + for _, l := range lines { + switch v := line.(type) { + case string: + if strings.Contains(l, v) { + found = true + break + } + case *regexp.Regexp: + if v.MatchString(l) { + found = true + break + } + } + } + + if !found { + t.Errorf("expected %v, not found in %v", line, string(dockerfile)) + } + } + }) + } +} diff --git a/testdata/bun-bunfig/.tool-versions b/testdata/bun-bunfig/.tool-versions new file mode 100644 index 0000000..969bd6b --- /dev/null +++ b/testdata/bun-bunfig/.tool-versions @@ -0,0 +1 @@ +bun 1.1.4 \ No newline at end of file diff --git a/testdata/bun-bunfig/bunfig.toml b/testdata/bun-bunfig/bunfig.toml new file mode 100644 index 0000000..e69de29 diff --git a/testdata/bun-bunfig/package.json b/testdata/bun-bunfig/package.json new file mode 100644 index 0000000..e46aca4 --- /dev/null +++ b/testdata/bun-bunfig/package.json @@ -0,0 +1,8 @@ +{ + "module": "index.ts", + "scripts": { + "build:prod": "NODE_ENV=production bun build", + "build": "bun build", + "start:production": "bun index.ts" + } +} diff --git a/testdata/bun/bun.lockb b/testdata/bun/bun.lockb new file mode 100644 index 0000000..e69de29 diff --git a/testdata/bun/package.json b/testdata/bun/package.json new file mode 100644 index 0000000..cd30c4d --- /dev/null +++ b/testdata/bun/package.json @@ -0,0 +1,3 @@ +{ + "module": "index.ts" +} diff --git a/testdata/elixir-tool-versions/.tool-versions b/testdata/elixir-tool-versions/.tool-versions new file mode 100644 index 0000000..b5365f4 --- /dev/null +++ b/testdata/elixir-tool-versions/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.11 +erlang 23.2.1 \ No newline at end of file diff --git a/testdata/elixir-tool-versions/mix.exs b/testdata/elixir-tool-versions/mix.exs new file mode 100644 index 0000000..7cf543f --- /dev/null +++ b/testdata/elixir-tool-versions/mix.exs @@ -0,0 +1,15 @@ +defmodule Hello.MixProject do + use Mix.Project + + def project do + [ + app: :hello, + version: "0.1.0", + elixir: "~> 1.14", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + aliases: aliases(), + deps: deps() + ] + end +end \ No newline at end of file diff --git a/testdata/elixir/.elixir-version b/testdata/elixir/.elixir-version new file mode 100644 index 0000000..578c71b --- /dev/null +++ b/testdata/elixir/.elixir-version @@ -0,0 +1 @@ +1.10 \ No newline at end of file diff --git a/testdata/elixir/.erlang-version b/testdata/elixir/.erlang-version new file mode 100644 index 0000000..4bd1d6b --- /dev/null +++ b/testdata/elixir/.erlang-version @@ -0,0 +1 @@ +22.3.2 \ No newline at end of file diff --git a/testdata/elixir/mix.exs b/testdata/elixir/mix.exs new file mode 100644 index 0000000..7cf543f --- /dev/null +++ b/testdata/elixir/mix.exs @@ -0,0 +1,15 @@ +defmodule Hello.MixProject do + use Mix.Project + + def project do + [ + app: :hello, + version: "0.1.0", + elixir: "~> 1.14", + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + aliases: aliases(), + deps: deps() + ] + end +end \ No newline at end of file diff --git a/testdata/go-mod/cmd/hello/hello.go b/testdata/go-mod/cmd/hello/hello.go new file mode 100644 index 0000000..cd35a48 --- /dev/null +++ b/testdata/go-mod/cmd/hello/hello.go @@ -0,0 +1 @@ +package hello diff --git a/testdata/go-mod/go.mod b/testdata/go-mod/go.mod new file mode 100644 index 0000000..de75006 --- /dev/null +++ b/testdata/go-mod/go.mod @@ -0,0 +1,3 @@ +module helloworld + +go 1.22.3 diff --git a/testdata/go/.tool-versions b/testdata/go/.tool-versions new file mode 100644 index 0000000..38fe77c --- /dev/null +++ b/testdata/go/.tool-versions @@ -0,0 +1 @@ +golang 1.16.3 \ No newline at end of file diff --git a/testdata/go/main.go b/testdata/go/main.go new file mode 100644 index 0000000..e69de29 diff --git a/testdata/nextjs-standalone/.node-version b/testdata/nextjs-standalone/.node-version new file mode 100644 index 0000000..0e94e31 --- /dev/null +++ b/testdata/nextjs-standalone/.node-version @@ -0,0 +1 @@ +16.0.0 \ No newline at end of file diff --git a/testdata/nextjs-standalone/next.config.mjs b/testdata/nextjs-standalone/next.config.mjs new file mode 100644 index 0000000..9dbf091 --- /dev/null +++ b/testdata/nextjs-standalone/next.config.mjs @@ -0,0 +1,3 @@ +export default { + output: "standalone", +}; diff --git a/testdata/nextjs-standalone/package.json b/testdata/nextjs-standalone/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/testdata/nextjs-standalone/package.json @@ -0,0 +1 @@ +{} diff --git a/testdata/nextjs/next.config.js b/testdata/nextjs/next.config.js new file mode 100644 index 0000000..da1bb77 --- /dev/null +++ b/testdata/nextjs/next.config.js @@ -0,0 +1,3 @@ +module.exports = { + reactStrictMode: true, +}; diff --git a/testdata/nextjs/package.json b/testdata/nextjs/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/testdata/nextjs/package.json @@ -0,0 +1 @@ +{} diff --git a/testdata/node-pnpm/.tool-versions b/testdata/node-pnpm/.tool-versions new file mode 100644 index 0000000..6d143af --- /dev/null +++ b/testdata/node-pnpm/.tool-versions @@ -0,0 +1 @@ +nodejs 16.0.0 \ No newline at end of file diff --git a/testdata/node-pnpm/package.json b/testdata/node-pnpm/package.json new file mode 100644 index 0000000..14dd323 --- /dev/null +++ b/testdata/node-pnpm/package.json @@ -0,0 +1,8 @@ +{ + "module": "index.ts", + "scripts": { + "build:prod": "NODE_ENV=production node build", + "build": "node build", + "start:production": "node index.mjs" + } +} diff --git a/testdata/node-pnpm/pnpm-lock.yaml b/testdata/node-pnpm/pnpm-lock.yaml new file mode 100644 index 0000000..e69de29 diff --git a/testdata/node-yarn/.nvmrc b/testdata/node-yarn/.nvmrc new file mode 100644 index 0000000..b08ffc7 --- /dev/null +++ b/testdata/node-yarn/.nvmrc @@ -0,0 +1 @@ +v16.0.0 \ No newline at end of file diff --git a/testdata/node-yarn/package.json b/testdata/node-yarn/package.json new file mode 100644 index 0000000..39999e8 --- /dev/null +++ b/testdata/node-yarn/package.json @@ -0,0 +1,8 @@ +{ + "module": "index.ts", + "scripts": { + "build:prod": "NODE_ENV=production node build", + "build": "node build", + "start-it": "node index.mjs" + } +} diff --git a/testdata/node-yarn/yarn.lock b/testdata/node-yarn/yarn.lock new file mode 100644 index 0000000..e69de29 diff --git a/testdata/node/package-lock.json b/testdata/node/package-lock.json new file mode 100644 index 0000000..e69de29 diff --git a/testdata/node/package.json b/testdata/node/package.json new file mode 100644 index 0000000..145cf8b --- /dev/null +++ b/testdata/node/package.json @@ -0,0 +1,3 @@ +{ + "main": "index.ts" +} diff --git a/testdata/php-composer/composer.json b/testdata/php-composer/composer.json new file mode 100644 index 0000000..7c87381 --- /dev/null +++ b/testdata/php-composer/composer.json @@ -0,0 +1,7 @@ +{ + "name": "test/test", + "description": "Test", + "require": { + "php": ">=5.3.0" + } +} diff --git a/testdata/php-composer/index.php b/testdata/php-composer/index.php new file mode 100644 index 0000000..e69de29 diff --git a/testdata/php-npm/.tool-versions b/testdata/php-npm/.tool-versions new file mode 100644 index 0000000..86da7f6 --- /dev/null +++ b/testdata/php-npm/.tool-versions @@ -0,0 +1 @@ +php 8.2.0 \ No newline at end of file diff --git a/testdata/php-npm/index.php b/testdata/php-npm/index.php new file mode 100644 index 0000000..e69de29 diff --git a/testdata/php-npm/package.json b/testdata/php-npm/package.json new file mode 100644 index 0000000..87e2947 --- /dev/null +++ b/testdata/php-npm/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "build": "echo 'Building the project...'" + } +} diff --git a/testdata/php-npm/yarn.lock b/testdata/php-npm/yarn.lock new file mode 100644 index 0000000..e69de29 diff --git a/testdata/php/index.php b/testdata/php/index.php new file mode 100644 index 0000000..e69de29 diff --git a/testdata/python-django/.tool-versions b/testdata/python-django/.tool-versions new file mode 100644 index 0000000..62e4ea0 --- /dev/null +++ b/testdata/python-django/.tool-versions @@ -0,0 +1 @@ +python 3.6.0 \ No newline at end of file diff --git a/testdata/python-django/Pipfile b/testdata/python-django/Pipfile new file mode 100644 index 0000000..b73aa42 --- /dev/null +++ b/testdata/python-django/Pipfile @@ -0,0 +1,10 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +requests = "*" +django = "*" + +[dev-packages] diff --git a/testdata/python-django/Pipfile.lock b/testdata/python-django/Pipfile.lock new file mode 100644 index 0000000..3945974 --- /dev/null +++ b/testdata/python-django/Pipfile.lock @@ -0,0 +1,59 @@ +{ + "_meta": { + "hash": { + "sha256": "bb57e0d7853b45999e47c163c46b95bc2fde31c527d8d7b5b5539dc979444a6d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "django": { + "hashes": [ + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + ], + "index": "pypi", + "version": "==2022.12.7" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", + "version": "==1.24.3" + } + }, + "develop": {} +} \ No newline at end of file diff --git a/testdata/python-django/manage.py b/testdata/python-django/manage.py new file mode 100644 index 0000000..e69de29 diff --git a/testdata/python-pdm/.python-version b/testdata/python-pdm/.python-version new file mode 100644 index 0000000..8cf6caf --- /dev/null +++ b/testdata/python-pdm/.python-version @@ -0,0 +1 @@ +3.4.1 \ No newline at end of file diff --git a/testdata/python-pdm/app.py b/testdata/python-pdm/app.py new file mode 100644 index 0000000..e69de29 diff --git a/testdata/python-pdm/pdm.lock b/testdata/python-pdm/pdm.lock new file mode 100644 index 0000000..e69de29 diff --git a/testdata/python-poetry/app/main.py b/testdata/python-poetry/app/main.py new file mode 100644 index 0000000..e69de29 diff --git a/testdata/python-poetry/poetry.lock b/testdata/python-poetry/poetry.lock new file mode 100644 index 0000000..e69de29 diff --git a/testdata/python-poetry/runtime.txt b/testdata/python-poetry/runtime.txt new file mode 100644 index 0000000..1124509 --- /dev/null +++ b/testdata/python-poetry/runtime.txt @@ -0,0 +1 @@ +python-3.8.5 \ No newline at end of file diff --git a/testdata/python-pyproject/pyproject.toml b/testdata/python-pyproject/pyproject.toml new file mode 100644 index 0000000..55ac94d --- /dev/null +++ b/testdata/python-pyproject/pyproject.toml @@ -0,0 +1,2 @@ +[project] +name = "pyproject" \ No newline at end of file diff --git a/testdata/python/main.py b/testdata/python/main.py new file mode 100644 index 0000000..e69de29 diff --git a/testdata/python/requirements.txt b/testdata/python/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/testdata/ruby-config-environment/.ruby-version b/testdata/ruby-config-environment/.ruby-version new file mode 100644 index 0000000..13d683c --- /dev/null +++ b/testdata/ruby-config-environment/.ruby-version @@ -0,0 +1 @@ +3.0.1 \ No newline at end of file diff --git a/testdata/ruby-config-environment/Gemfile b/testdata/ruby-config-environment/Gemfile new file mode 100644 index 0000000..38da745 --- /dev/null +++ b/testdata/ruby-config-environment/Gemfile @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +source "https://rubygems.org" diff --git a/testdata/ruby-config-environment/config/environment.rb b/testdata/ruby-config-environment/config/environment.rb new file mode 100644 index 0000000..e69de29 diff --git a/testdata/ruby-config-ru/.tool-versions b/testdata/ruby-config-ru/.tool-versions new file mode 100644 index 0000000..59993a2 --- /dev/null +++ b/testdata/ruby-config-ru/.tool-versions @@ -0,0 +1 @@ +ruby 2.3.0 \ No newline at end of file diff --git a/testdata/ruby-config-ru/Gemfile b/testdata/ruby-config-ru/Gemfile new file mode 100644 index 0000000..38da745 --- /dev/null +++ b/testdata/ruby-config-ru/Gemfile @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +source "https://rubygems.org" diff --git a/testdata/ruby-config-ru/config.ru b/testdata/ruby-config-ru/config.ru new file mode 100644 index 0000000..e69de29 diff --git a/testdata/ruby-rails/Gemfile b/testdata/ruby-rails/Gemfile new file mode 100644 index 0000000..9336723 --- /dev/null +++ b/testdata/ruby-rails/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "rails" \ No newline at end of file diff --git a/testdata/ruby-rails/package.json b/testdata/ruby-rails/package.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/testdata/ruby-rails/package.json @@ -0,0 +1 @@ +{} diff --git a/testdata/ruby-rails/pnpm-lock.yaml b/testdata/ruby-rails/pnpm-lock.yaml new file mode 100644 index 0000000..e69de29 diff --git a/testdata/ruby-rakefile/Gemfile b/testdata/ruby-rakefile/Gemfile new file mode 100644 index 0000000..38da745 --- /dev/null +++ b/testdata/ruby-rakefile/Gemfile @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +source "https://rubygems.org" diff --git a/testdata/ruby/Gemfile b/testdata/ruby/Gemfile new file mode 100644 index 0000000..fcb5909 --- /dev/null +++ b/testdata/ruby/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +ruby '2.0.0', :patchlevel => '353' diff --git a/testdata/rust-bin/Cargo.toml b/testdata/rust-bin/Cargo.toml new file mode 100644 index 0000000..11099a1 --- /dev/null +++ b/testdata/rust-bin/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ingest" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde_json = "1.0.116" +structured-logger = "1.0.3" +tokio = { version = "1.37.0", features = ["full"] } +warp = "0.3.7" + +[[bin]] +bench = false +path = "src/main.rs" +name = "rg" \ No newline at end of file diff --git a/testdata/rust/Cargo.toml b/testdata/rust/Cargo.toml new file mode 100644 index 0000000..fd81912 --- /dev/null +++ b/testdata/rust/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ingest" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde_json = "1.0.116" +structured-logger = "1.0.3" +tokio = { version = "1.37.0", features = ["full"] } +warp = "0.3.7" diff --git a/testdata/static-dist/dist/index.html b/testdata/static-dist/dist/index.html new file mode 100644 index 0000000..e69de29 diff --git a/testdata/static-public/public/index.html b/testdata/static-public/public/index.html new file mode 100644 index 0000000..e69de29 diff --git a/testdata/static-static/static/index.html b/testdata/static-static/static/index.html new file mode 100644 index 0000000..e69de29 diff --git a/testdata/static/index.html b/testdata/static/index.html new file mode 100644 index 0000000..e69de29