Skip to content

Commit 1dcf4b7

Browse files
nartukyk-gr
authored andcommitted
Add PNPM package manager support
Implement PNPM build info collection with dependency parsing, lock file handling, and test coverage.
1 parent 2d0d559 commit 1dcf4b7

File tree

9 files changed

+1406
-0
lines changed

9 files changed

+1406
-0
lines changed

build/build.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ func (b *Build) AddNpmModule(srcPath string) (*NpmModule, error) {
9898
return newNpmModule(srcPath, b)
9999
}
100100

101+
// AddPnpmModule adds a Pnpm module to this Build. Pass srcPath as an empty string if the root of the Pnpm project is the working directory.
102+
func (b *Build) AddPnpmModule(srcPath string) (*PnpmModule, error) {
103+
return newPnpmModule(srcPath, b)
104+
}
105+
101106
// AddPythonModule adds a Python module to this Build. Pass srcPath as an empty string if the root of the python project is the working directory.
102107
func (b *Build) AddPythonModule(srcPath string, tool pythonutils.PythonTool) (*PythonModule, error) {
103108
return newPythonModule(srcPath, tool, b)

build/pnpm.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package build
2+
3+
import (
4+
"errors"
5+
"os"
6+
"strings"
7+
8+
buildutils "github.com/jfrog/build-info-go/build/utils"
9+
"github.com/jfrog/build-info-go/entities"
10+
"github.com/jfrog/build-info-go/utils"
11+
)
12+
13+
const minSupportedPnpmVersion = "6.0.0"
14+
15+
type PnpmModule struct {
16+
containingBuild *Build
17+
name string
18+
srcPath string
19+
executablePath string
20+
pnpmArgs []string
21+
collectBuildInfo bool
22+
}
23+
24+
// Pass an empty string for srcPath to find the pnpm project in the working directory.
25+
func newPnpmModule(srcPath string, containingBuild *Build) (*PnpmModule, error) {
26+
pnpmVersion, executablePath, err := buildutils.GetPnpmVersionAndExecPath(containingBuild.logger)
27+
if err != nil {
28+
return nil, err
29+
}
30+
if pnpmVersion.Compare(minSupportedPnpmVersion) > 0 {
31+
return nil, errors.New("pnpm CLI must have version " + minSupportedPnpmVersion + " or higher. The current version is: " + pnpmVersion.GetVersion())
32+
}
33+
34+
if srcPath == "" {
35+
wd, err := os.Getwd()
36+
if err != nil {
37+
return nil, err
38+
}
39+
srcPath, err = utils.FindFileInDirAndParents(wd, "package.json")
40+
if err != nil {
41+
return nil, err
42+
}
43+
}
44+
45+
// Read module name - pnpm uses the same package.json format as npm
46+
packageInfo, err := buildutils.ReadPackageInfoFromPackageJsonIfExists(srcPath, pnpmVersion)
47+
if err != nil {
48+
return nil, err
49+
}
50+
name := packageInfo.BuildInfoModuleId()
51+
52+
return &PnpmModule{name: name, srcPath: srcPath, containingBuild: containingBuild, executablePath: executablePath}, nil
53+
}
54+
55+
func (pm *PnpmModule) Build() error {
56+
if len(pm.pnpmArgs) > 0 {
57+
output, _, err := buildutils.RunPnpmCmd(pm.executablePath, pm.srcPath, pm.pnpmArgs, &utils.NullLog{})
58+
if len(output) > 0 {
59+
pm.containingBuild.logger.Output(strings.TrimSpace(string(output)))
60+
}
61+
if err != nil {
62+
return err
63+
}
64+
// After executing the user-provided command, cleaning pnpmArgs is needed.
65+
pm.filterPnpmArgsFlags()
66+
}
67+
if !pm.collectBuildInfo {
68+
return nil
69+
}
70+
return pm.CalcDependencies()
71+
}
72+
73+
func (pm *PnpmModule) CalcDependencies() error {
74+
if !pm.containingBuild.buildNameAndNumberProvided() {
75+
return errors.New("a build name must be provided in order to collect the project's dependencies")
76+
}
77+
buildInfoDependencies, err := buildutils.CalculatePnpmDependenciesList(pm.executablePath, pm.srcPath, pm.name,
78+
buildutils.PnpmTreeDepListParam{Args: pm.pnpmArgs}, true, pm.containingBuild.logger)
79+
if err != nil {
80+
return err
81+
}
82+
buildInfoModule := entities.Module{Id: pm.name, Type: entities.Npm, Dependencies: buildInfoDependencies}
83+
buildInfo := &entities.BuildInfo{Modules: []entities.Module{buildInfoModule}}
84+
return pm.containingBuild.SaveBuildInfo(buildInfo)
85+
}
86+
87+
func (pm *PnpmModule) SetName(name string) {
88+
pm.name = name
89+
}
90+
91+
func (pm *PnpmModule) SetPnpmArgs(pnpmArgs []string) {
92+
pm.pnpmArgs = pnpmArgs
93+
}
94+
95+
func (pm *PnpmModule) SetCollectBuildInfo(collectBuildInfo bool) {
96+
pm.collectBuildInfo = collectBuildInfo
97+
}
98+
99+
func (pm *PnpmModule) AddArtifacts(artifacts ...entities.Artifact) error {
100+
return pm.containingBuild.AddArtifacts(pm.name, entities.Npm, artifacts...)
101+
}
102+
103+
// This function discards the pnpm command in pnpmArgs and keeps only the command flags.
104+
// It is necessary for the pnpm command's name to come before the pnpm command's flags in pnpmArgs for the function to work correctly.
105+
func (pm *PnpmModule) filterPnpmArgsFlags() {
106+
if len(pm.pnpmArgs) == 1 && !strings.HasPrefix(pm.pnpmArgs[0], "-") {
107+
pm.pnpmArgs = []string{}
108+
}
109+
for argIndex := 0; argIndex < len(pm.pnpmArgs); argIndex++ {
110+
if strings.HasPrefix(pm.pnpmArgs[argIndex], "-") {
111+
pm.pnpmArgs = pm.pnpmArgs[argIndex:]
112+
}
113+
}
114+
}

build/pnpm_test.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package build
2+
3+
import (
4+
"path/filepath"
5+
"strconv"
6+
"testing"
7+
"time"
8+
9+
buildutils "github.com/jfrog/build-info-go/build/utils"
10+
"github.com/jfrog/build-info-go/tests"
11+
"github.com/jfrog/build-info-go/utils"
12+
"github.com/stretchr/testify/assert"
13+
)
14+
15+
var pnpmLogger = utils.NewDefaultLogger(utils.INFO)
16+
17+
func TestGenerateBuildInfoForPnpm(t *testing.T) {
18+
pnpmVersion, execPath, err := buildutils.GetPnpmVersionAndExecPath(pnpmLogger)
19+
if err != nil {
20+
t.Skip("pnpm is not installed, skipping test")
21+
}
22+
23+
service := NewBuildInfoService()
24+
pnpmBuild, err := service.GetOrCreateBuild("build-info-go-test-pnpm", strconv.FormatInt(time.Now().Unix(), 10))
25+
assert.NoError(t, err)
26+
defer func() {
27+
assert.NoError(t, pnpmBuild.Clean())
28+
}()
29+
30+
// Create pnpm project.
31+
path, err := filepath.Abs(filepath.Join(".", "testdata"))
32+
assert.NoError(t, err)
33+
projectPath, cleanup := tests.CreatePnpmTest(t, path, "project1")
34+
defer cleanup()
35+
36+
// Install dependencies in the pnpm project.
37+
_, _, err = buildutils.RunPnpmCmd(execPath, projectPath, []string{"install"}, pnpmLogger)
38+
assert.NoError(t, err)
39+
40+
pnpmModule, err := pnpmBuild.AddPnpmModule(projectPath)
41+
assert.NoError(t, err)
42+
43+
err = pnpmModule.CalcDependencies()
44+
assert.NoError(t, err)
45+
46+
buildInfo, err := pnpmBuild.ToBuildInfo()
47+
assert.NoError(t, err)
48+
49+
// Verify results - should have at least one module with dependencies
50+
assert.Len(t, buildInfo.Modules, 1)
51+
assert.Greater(t, len(buildInfo.Modules[0].Dependencies), 0, "Expected at least one dependency")
52+
53+
// Verify pnpm version is supported
54+
assert.True(t, pnpmVersion.AtLeast("6.0.0"), "pnpm version should be at least 6.0.0")
55+
}
56+
57+
func TestFilterPnpmArgsFlags(t *testing.T) {
58+
_, execPath, err := buildutils.GetPnpmVersionAndExecPath(pnpmLogger)
59+
if err != nil {
60+
t.Skip("pnpm is not installed, skipping test")
61+
}
62+
63+
service := NewBuildInfoService()
64+
pnpmBuild, err := service.GetOrCreateBuild("build-info-go-test-pnpm", strconv.FormatInt(time.Now().Unix(), 10))
65+
assert.NoError(t, err)
66+
defer func() {
67+
assert.NoError(t, pnpmBuild.Clean())
68+
}()
69+
70+
// Create pnpm project.
71+
path, err := filepath.Abs(filepath.Join(".", "testdata"))
72+
assert.NoError(t, err)
73+
projectPath, cleanup := tests.CreatePnpmTest(t, path, "project1")
74+
defer cleanup()
75+
76+
// Install dependencies first
77+
_, _, err = buildutils.RunPnpmCmd(execPath, projectPath, []string{"install"}, pnpmLogger)
78+
assert.NoError(t, err)
79+
80+
pnpmModule, err := pnpmBuild.AddPnpmModule(projectPath)
81+
assert.NoError(t, err)
82+
83+
// Test filtering with command and flags
84+
pnpmArgs := []string{"ls", "--json", "--long"}
85+
pnpmModule.SetPnpmArgs(pnpmArgs)
86+
pnpmModule.filterPnpmArgsFlags()
87+
expected := []string{"--json", "--long"}
88+
assert.Equal(t, expected, pnpmModule.pnpmArgs)
89+
90+
// Test filtering with only flags
91+
pnpmArgs = []string{"--prod", "--json"}
92+
pnpmModule.SetPnpmArgs(pnpmArgs)
93+
pnpmModule.filterPnpmArgsFlags()
94+
expected = []string{"--prod", "--json"}
95+
assert.Equal(t, expected, pnpmModule.pnpmArgs)
96+
97+
// Test filtering with single command (no flags)
98+
pnpmArgs = []string{"install"}
99+
pnpmModule.SetPnpmArgs(pnpmArgs)
100+
pnpmModule.filterPnpmArgsFlags()
101+
expected = []string{}
102+
assert.Equal(t, expected, pnpmModule.pnpmArgs)
103+
}
104+
105+
func TestPnpmModuleSetters(t *testing.T) {
106+
_, execPath, err := buildutils.GetPnpmVersionAndExecPath(pnpmLogger)
107+
if err != nil {
108+
t.Skip("pnpm is not installed, skipping test")
109+
}
110+
111+
service := NewBuildInfoService()
112+
pnpmBuild, err := service.GetOrCreateBuild("build-info-go-test-pnpm", strconv.FormatInt(time.Now().Unix(), 10))
113+
assert.NoError(t, err)
114+
defer func() {
115+
assert.NoError(t, pnpmBuild.Clean())
116+
}()
117+
118+
// Create pnpm project.
119+
path, err := filepath.Abs(filepath.Join(".", "testdata"))
120+
assert.NoError(t, err)
121+
projectPath, cleanup := tests.CreatePnpmTest(t, path, "project1")
122+
defer cleanup()
123+
124+
// Install dependencies first
125+
_, _, err = buildutils.RunPnpmCmd(execPath, projectPath, []string{"install"}, pnpmLogger)
126+
assert.NoError(t, err)
127+
128+
pnpmModule, err := pnpmBuild.AddPnpmModule(projectPath)
129+
assert.NoError(t, err)
130+
131+
// Test SetName
132+
pnpmModule.SetName("custom-module-name")
133+
assert.Equal(t, "custom-module-name", pnpmModule.name)
134+
135+
// Test SetPnpmArgs
136+
args := []string{"--prod", "--frozen-lockfile"}
137+
pnpmModule.SetPnpmArgs(args)
138+
assert.Equal(t, args, pnpmModule.pnpmArgs)
139+
140+
// Test SetCollectBuildInfo
141+
pnpmModule.SetCollectBuildInfo(true)
142+
assert.True(t, pnpmModule.collectBuildInfo)
143+
pnpmModule.SetCollectBuildInfo(false)
144+
assert.False(t, pnpmModule.collectBuildInfo)
145+
}
146+
147+
func TestPnpmModuleBuild(t *testing.T) {
148+
_, _, err := buildutils.GetPnpmVersionAndExecPath(pnpmLogger)
149+
if err != nil {
150+
t.Skip("pnpm is not installed, skipping test")
151+
}
152+
153+
service := NewBuildInfoService()
154+
pnpmBuild, err := service.GetOrCreateBuild("build-info-go-test-pnpm", strconv.FormatInt(time.Now().Unix(), 10))
155+
assert.NoError(t, err)
156+
defer func() {
157+
assert.NoError(t, pnpmBuild.Clean())
158+
}()
159+
160+
// Create pnpm project.
161+
path, err := filepath.Abs(filepath.Join(".", "testdata"))
162+
assert.NoError(t, err)
163+
projectPath, cleanup := tests.CreatePnpmTest(t, path, "project1")
164+
defer cleanup()
165+
166+
pnpmModule, err := pnpmBuild.AddPnpmModule(projectPath)
167+
assert.NoError(t, err)
168+
169+
// Test Build with install command
170+
pnpmModule.SetPnpmArgs([]string{"install"})
171+
pnpmModule.SetCollectBuildInfo(false)
172+
err = pnpmModule.Build()
173+
assert.NoError(t, err)
174+
175+
// Test Build with collectBuildInfo enabled
176+
pnpmModule.SetCollectBuildInfo(true)
177+
err = pnpmModule.Build()
178+
assert.NoError(t, err)
179+
180+
buildInfo, err := pnpmBuild.ToBuildInfo()
181+
assert.NoError(t, err)
182+
assert.Len(t, buildInfo.Modules, 1)
183+
}
184+
185+
func TestNewPnpmModule(t *testing.T) {
186+
_, _, err := buildutils.GetPnpmVersionAndExecPath(pnpmLogger)
187+
if err != nil {
188+
t.Skip("pnpm is not installed, skipping test")
189+
}
190+
191+
service := NewBuildInfoService()
192+
pnpmBuild, err := service.GetOrCreateBuild("build-info-go-test-pnpm", strconv.FormatInt(time.Now().Unix(), 10))
193+
assert.NoError(t, err)
194+
defer func() {
195+
assert.NoError(t, pnpmBuild.Clean())
196+
}()
197+
198+
// Create pnpm project.
199+
path, err := filepath.Abs(filepath.Join(".", "testdata"))
200+
assert.NoError(t, err)
201+
projectPath, cleanup := tests.CreatePnpmTest(t, path, "project1")
202+
defer cleanup()
203+
204+
// Test creating module with explicit path
205+
pnpmModule, err := pnpmBuild.AddPnpmModule(projectPath)
206+
assert.NoError(t, err)
207+
assert.NotNil(t, pnpmModule)
208+
assert.Equal(t, projectPath, pnpmModule.srcPath)
209+
assert.NotEmpty(t, pnpmModule.executablePath)
210+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"underscore": {
3+
"version": "1.13.6",
4+
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
5+
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
6+
},
7+
"minimist": {
8+
"version": "1.2.8",
9+
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
10+
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
11+
"dev": true
12+
},
13+
"@jfrog/test-package": {
14+
"version": "1.0.0",
15+
"resolved": "https://registry.npmjs.org/@jfrog/test-package/-/test-package-1.0.0.tgz",
16+
"integrity": "sha512-test==",
17+
"dependencies": {
18+
"nested-dep": {
19+
"version": "2.0.0",
20+
"resolved": "https://registry.npmjs.org/nested-dep/-/nested-dep-2.0.0.tgz",
21+
"integrity": "sha512-nested=="
22+
}
23+
}
24+
}
25+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "no-build-pnpm-project",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"underscore": "1.13.6"
6+
}
7+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "build-info-go-pnpm-tests",
3+
"version": "1.0.0",
4+
"description": "test package for pnpm",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"author": "",
10+
"license": "ISC",
11+
"dependencies": {
12+
"underscore": "1.13.6"
13+
},
14+
"devDependencies": {
15+
"minimist": "1.2.8"
16+
}
17+
}

0 commit comments

Comments
 (0)