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
13 changes: 8 additions & 5 deletions builder/parser/code/cnb_versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ var cnbNodeVersions = []CNBVersion{
}

// GetCNBVersions returns the supported CNB versions for a given language.
// Supports composite languages like "dockerfile,Node.js" by checking each part.
func GetCNBVersions(lang string) []CNBVersion {
switch strings.ToLower(lang) {
case "nodejs", "node", "node.js":
return cnbNodeVersions
default:
return []CNBVersion{}
lower := strings.ToLower(lang)
for _, part := range strings.Split(lower, ",") {
switch strings.TrimSpace(part) {
case "nodejs", "node", "node.js":
return cnbNodeVersions
}
}
return []CNBVersion{}
}

// MatchCNBVersion resolves a fuzzy version spec (e.g. "20.x", ">=20.0", "20")
Expand Down
91 changes: 91 additions & 0 deletions builder/parser/code/cnb_versions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package code

import "testing"

func TestGetCNBVersions(t *testing.T) {
tests := []struct {
name string
lang string
wantCount int // 0 means empty
}{
// Single languages
{"nodejs returns versions", "nodejs", len(cnbNodeVersions)},
{"Node.js returns versions", "Node.js", len(cnbNodeVersions)},
{"node returns versions", "node", len(cnbNodeVersions)},
{"NODEJS returns versions (case-insensitive)", "NODEJS", len(cnbNodeVersions)},
{"python returns empty", "python", 0},
{"dockerfile returns empty", "dockerfile", 0},
{"empty string returns empty", "", 0},
// Composite languages (comma-separated)
{"dockerfile,Node.js returns versions", "dockerfile,Node.js", len(cnbNodeVersions)},
{"Node.js,dockerfile returns versions", "Node.js,dockerfile", len(cnbNodeVersions)},
{"dockerfile,nodejs returns versions", "dockerfile,nodejs", len(cnbNodeVersions)},
{"dockerfile,static returns empty", "dockerfile,static", 0},
{"Dockerfile,Node.js (mixed case)", "Dockerfile,Node.js", len(cnbNodeVersions)},
// Whitespace around parts
{"dockerfile, Node.js (space after comma)", "dockerfile, Node.js", len(cnbNodeVersions)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetCNBVersions(tt.lang)
if len(got) != tt.wantCount {
t.Errorf("GetCNBVersions(%q) returned %d versions, want %d", tt.lang, len(got), tt.wantCount)
}
})
}
}

func TestMatchCNBVersion_CompositeLanguage(t *testing.T) {
tests := []struct {
name string
lang string
versionSpec string
want string
}{
// Composite language should resolve version correctly
{"dockerfile,Node.js with major 20", "dockerfile,Node.js", "20", "20.20.0"},
{"dockerfile,Node.js with major 22", "dockerfile,Node.js", "22", "22.22.0"},
{"dockerfile,Node.js with exact", "dockerfile,Node.js", "20.19.6", "20.19.6"},
{"dockerfile,Node.js empty spec returns default", "dockerfile,Node.js", "", "24.13.0"},
// Single language still works
{"nodejs with major 20", "nodejs", "20", "20.20.0"},
{"Node.js with fuzzy 20.x", "Node.js", "20.x", "20.20.0"},
{"Node.js with >=22", "Node.js", ">=22", "22.22.0"},
// Unsupported language returns empty
{"python returns empty", "python", "3.11", ""},
{"dockerfile alone returns empty", "dockerfile", "20", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := MatchCNBVersion(tt.lang, tt.versionSpec)
if got != tt.want {
t.Errorf("MatchCNBVersion(%q, %q) = %q, want %q", tt.lang, tt.versionSpec, got, tt.want)
}
})
}
}

func TestExtractMajorFromSpec(t *testing.T) {
tests := []struct {
spec string
want int
}{
{"20", 20},
{"20.x", 20},
{">=20.0", 20},
{"^22.0.0", 22},
{"~18.10", 18},
{"v24", 24},
{"=20.0.0", 20},
{"", 0},
{"abc", 0},
}
for _, tt := range tests {
t.Run(tt.spec, func(t *testing.T) {
got := extractMajorFromSpec(tt.spec)
if got != tt.want {
t.Errorf("extractMajorFromSpec(%q) = %d, want %d", tt.spec, got, tt.want)
}
})
}
}
14 changes: 10 additions & 4 deletions builder/parser/source_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,13 +534,16 @@ func (d *SourceCodeParse) Parse() ParseErrorList {
// - 纯静态语言:始终使用 nginx,端口 8080
// - Node.js 前端框架(static 类型):构建后由 nginx 托管,端口 8080
// - Node.js 后端框架(dynamic 类型):默认端口 3000(Next.js/Nuxt/Remix/Nest.js/Express)
if d.Lang == code.Static ||
(d.Lang == code.Nodejs && runtimeInfo != nil && runtimeInfo["RUNTIME_TYPE"] == "static") {
// 多语言场景(如 "dockerfile,Node.js")也需要设置 CNB 默认端口
langStr := string(d.Lang)
hasNodejs := strings.Contains(langStr, string(code.Nodejs))
hasStatic := strings.Contains(langStr, string(code.Static))
if hasStatic || (hasNodejs && runtimeInfo != nil && runtimeInfo["RUNTIME_TYPE"] == "static") {
if _, ok := d.ports[8080]; !ok {
d.ports[8080] = &types.Port{ContainerPort: 8080, Protocol: "http"}
}
}
if d.Lang == code.Nodejs && runtimeInfo != nil && runtimeInfo["RUNTIME_TYPE"] == "dynamic" {
if hasNodejs && runtimeInfo != nil && runtimeInfo["RUNTIME_TYPE"] == "dynamic" {
if _, ok := d.ports[3000]; !ok {
d.ports[3000] = &types.Port{ContainerPort: 3000, Protocol: "http"}
}
Expand Down Expand Up @@ -635,8 +638,11 @@ func (d *SourceCodeParse) GetImage() Image {
}

// GetArgs 启动参数
// Nodejs/Static 走 CNB 构建,镜像自带 entrypoint,不需要 slug runner 的 ["start", "web"] 参数。
// 多语言场景(如 "dockerfile,Node.js")同样适用。
func (d *SourceCodeParse) GetArgs() []string {
if d.Lang == code.Nodejs || d.Lang == code.Static {
lang := string(d.Lang)
if strings.Contains(lang, string(code.Nodejs)) || strings.Contains(lang, string(code.Static)) {
return nil
}
return d.args
Expand Down
106 changes: 106 additions & 0 deletions builder/parser/source_code_args_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package parser

import (
"strings"
"testing"

"github.com/goodrain/rainbond/builder/parser/code"
"github.com/goodrain/rainbond/builder/parser/types"
)

func TestGetArgs_MultiLanguage(t *testing.T) {
tests := []struct {
name string
lang code.Lang
wantNil bool
}{
// 单语言
{"pure Node.js returns nil", code.Nodejs, true},
{"pure static returns nil", code.Static, true},
{"pure Python returns args", code.Python, false},
{"pure dockerfile returns args", code.Dockerfile, false},
{"Java-maven returns args", code.JavaMaven, false},
// 多语言(逗号分隔)
{"dockerfile,Node.js returns nil", code.Lang("dockerfile,Node.js"), true},
{"dockerfile,static returns nil", code.Lang("dockerfile,static"), true},
{"Node.js,dockerfile returns nil", code.Lang("Node.js,dockerfile"), true},
// 不应误匹配的语言(大小写敏感)
{"NodeJSStatic not matched (no regression)", code.NodeJSStatic, false},
{"NodeJSDockerfile not matched", code.NodeJSDockerfile, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &SourceCodeParse{
Lang: tt.lang,
args: []string{"start", "web"},
}
got := d.GetArgs()
if tt.wantNil && got != nil {
t.Errorf("GetArgs() = %v; want nil", got)
}
if !tt.wantNil && got == nil {
t.Errorf("GetArgs() = nil; want non-nil")
}
})
}
}

func TestCNBDefaultPorts_MultiLanguage(t *testing.T) {
tests := []struct {
name string
lang code.Lang
runtimeType string // "" means no runtimeInfo
wantPort int // 0 means no CNB port expected
}{
// 纯语言
{"static gets 8080", code.Static, "", 8080},
{"Node.js static gets 8080", code.Nodejs, "static", 8080},
{"Node.js dynamic gets 3000", code.Nodejs, "dynamic", 3000},
{"dockerfile gets no port", code.Dockerfile, "", 0},
// 多语言
{"dockerfile,Node.js static gets 8080", code.Lang("dockerfile,Node.js"), "static", 8080},
{"dockerfile,Node.js dynamic gets 3000", code.Lang("dockerfile,Node.js"), "dynamic", 3000},
{"dockerfile,static gets 8080", code.Lang("dockerfile,static"), "", 8080},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &SourceCodeParse{
Lang: tt.lang,
ports: make(map[int]*types.Port),
}
applyCNBDefaultPorts(d, tt.runtimeType)

if tt.wantPort == 0 {
if len(d.ports) != 0 {
t.Errorf("expected no ports, got %v", d.ports)
}
} else {
if _, ok := d.ports[tt.wantPort]; !ok {
t.Errorf("expected port %d, got %v", tt.wantPort, d.ports)
}
}
})
}
}

// applyCNBDefaultPorts mirrors the port logic in source_code.go Parse() for testability.
func applyCNBDefaultPorts(d *SourceCodeParse, runtimeType string) {
var runtimeInfo map[string]string
if runtimeType != "" {
runtimeInfo = map[string]string{"RUNTIME_TYPE": runtimeType}
}

langStr := string(d.Lang)
hasNodejs := strings.Contains(langStr, string(code.Nodejs))
hasStatic := strings.Contains(langStr, string(code.Static))
if hasStatic || (hasNodejs && runtimeInfo != nil && runtimeInfo["RUNTIME_TYPE"] == "static") {
if _, ok := d.ports[8080]; !ok {
d.ports[8080] = &types.Port{ContainerPort: 8080, Protocol: "http"}
}
}
if hasNodejs && runtimeInfo != nil && runtimeInfo["RUNTIME_TYPE"] == "dynamic" {
if _, ok := d.ports[3000]; !ok {
d.ports[3000] = &types.Port{ContainerPort: 3000, Protocol: "http"}
}
}
}