Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions pkg/stackbuild/bun.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package stackbuild

import (
"encoding/json"
"regexp"
"strings"

"github.com/moby/buildkit/client/llb"
"miren.dev/runtime/pkg/imagerefs"
Expand Down Expand Up @@ -34,13 +36,51 @@ func (s *BunStack) Detect() bool {
s.Event("file", "bun.lockb", "Found bun.lockb (Bun runtime, legacy)")
return true
}
if s.hasFile("bunfig.toml") {
s.Event("file", "bunfig.toml", "Found bunfig.toml (Bun runtime)")
return true
}
if s.detectPackageManagerBun() {
s.Event("config", "packageManager", "package.json packageManager field specifies bun")
return true
}
if s.detectBunInScripts() {
s.Event("config", "scripts", "package.json scripts reference bun")
return true
}
if s.detectInFile("Procfile", `web:\s+bun`) {
s.Event("file", "Procfile", "Procfile references bun")
return true
}
return false
}

func (s *BunStack) detectPackageManagerBun() bool {
data, err := s.readFile("package.json")
if err != nil {
return false
}
var pkg struct {
PackageManager string `json:"packageManager"`
}
if err := json.Unmarshal(data, &pkg); err != nil {
return false
}
return strings.HasPrefix(pkg.PackageManager, "bun@")
}

var bunCommandRe = regexp.MustCompile(`(?:^|\s)bunx?(?:\s|$)`)

func (s *BunStack) detectBunInScripts() bool {
scripts := s.getPackageScripts()
for _, cmd := range scripts {
if bunCommandRe.MatchString(cmd) {
return true
}
}
return false
}

func (s *BunStack) Init(opts BuildOptions) {
s.SetCwd("/app")

Expand Down
2 changes: 1 addition & 1 deletion pkg/stackbuild/stackbuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ func DetectStack(dir string, opts BuildOptions) (Stack, error) {
stacks := []Stack{
&RubyStack{MetaStack: ms},
&PythonStack{MetaStack: ms},
&NodeStack{MetaStack: ms},
&BunStack{MetaStack: ms},
&NodeStack{MetaStack: ms},
&GoStack{MetaStack: ms},
&RustStack{MetaStack: ms},
}
Expand Down
121 changes: 121 additions & 0 deletions pkg/stackbuild/stackbuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,127 @@ func TestBun(t *testing.T) {
buildLLB(t, dir, state)
}

func TestBunDetect(t *testing.T) {
testCases := []struct {
name string
files map[string]string
expected bool
}{
{
name: "bun.lock",
files: map[string]string{
"package.json": `{"name": "app"}`,
"bun.lock": "",
},
expected: true,
},
{
name: "bun.lockb legacy",
files: map[string]string{
"package.json": `{"name": "app"}`,
"bun.lockb": "",
},
expected: true,
},
{
name: "bunfig.toml",
files: map[string]string{
"package.json": `{"name": "app"}`,
"bunfig.toml": "[install]\noptional = true\n",
},
expected: true,
},
{
name: "packageManager field",
files: map[string]string{
"package.json": `{"name": "app", "packageManager": "bun@1.1.0"}`,
},
expected: true,
},
{
name: "bun in scripts",
files: map[string]string{
"package.json": `{"name": "app", "scripts": {"start": "bun run index.ts"}}`,
},
expected: true,
},
{
name: "bun as standalone command in scripts",
files: map[string]string{
"package.json": `{"name": "app", "scripts": {"dev": "bun --watch index.ts"}}`,
},
expected: true,
},
{
name: "Procfile with bun",
files: map[string]string{
"package.json": `{"name": "app"}`,
"Procfile": "web: bun run start",
},
expected: true,
},
{
name: "plain package.json no bun signals",
files: map[string]string{
"package.json": `{"name": "app", "scripts": {"start": "node index.js"}}`,
},
expected: false,
},
{
name: "no package.json",
files: map[string]string{
"index.ts": "console.log('hi')",
},
expected: false,
},
{
name: "bunx in scripts",
files: map[string]string{
"package.json": `{"name": "app", "scripts": {"test": "bunx vitest"}}`,
},
expected: true,
},
{
name: "bun at end of script command",
files: map[string]string{
"package.json": `{"name": "app", "scripts": {"start": "npx something && bun"}}`,
},
expected: true,
},
{
name: "bundle in scripts is not bun",
files: map[string]string{
"package.json": `{"name": "app", "scripts": {"start": "bundle exec rails server"}}`,
},
expected: false,
},
{
name: "packageManager field for npm not bun",
files: map[string]string{
"package.json": `{"name": "app", "packageManager": "npm@10.0.0"}`,
},
expected: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()

for name, content := range tc.files {
require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0644))
}

stack := &BunStack{
MetaStack: MetaStack{
dir: dir,
},
}
require.Equal(t, tc.expected, stack.Detect())
})
}
}

func TestGo(t *testing.T) {
if !checkDocker() {
t.Skip("Docker not available")
Expand Down
Loading