Skip to content
Open
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
5 changes: 5 additions & 0 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ func (b *Build) AddNpmModule(srcPath string) (*NpmModule, error) {
return newNpmModule(srcPath, b)
}

// 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.
func (b *Build) AddPnpmModule(srcPath string) (*PnpmModule, error) {
return newPnpmModule(srcPath, b)
}

// 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.
func (b *Build) AddPythonModule(srcPath string, tool pythonutils.PythonTool) (*PythonModule, error) {
return newPythonModule(srcPath, tool, b)
Expand Down
114 changes: 114 additions & 0 deletions build/pnpm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package build

import (
"errors"
"os"
"strings"

buildutils "github.com/jfrog/build-info-go/build/utils"
"github.com/jfrog/build-info-go/entities"
"github.com/jfrog/build-info-go/utils"
)

const minSupportedPnpmVersion = "6.0.0"

type PnpmModule struct {
containingBuild *Build
name string
srcPath string
executablePath string
pnpmArgs []string
collectBuildInfo bool
}

// Pass an empty string for srcPath to find the pnpm project in the working directory.
func newPnpmModule(srcPath string, containingBuild *Build) (*PnpmModule, error) {
pnpmVersion, executablePath, err := buildutils.GetPnpmVersionAndExecPath(containingBuild.logger)
if err != nil {
return nil, err
}
if pnpmVersion.Compare(minSupportedPnpmVersion) > 0 {
return nil, errors.New("pnpm CLI must have version " + minSupportedPnpmVersion + " or higher. The current version is: " + pnpmVersion.GetVersion())
}

if srcPath == "" {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
srcPath, err = utils.FindFileInDirAndParents(wd, "package.json")
if err != nil {
return nil, err
}
}

// Read module name - pnpm uses the same package.json format as npm
packageInfo, err := buildutils.ReadPackageInfoFromPackageJsonIfExists(srcPath, pnpmVersion)
if err != nil {
return nil, err
}
name := packageInfo.BuildInfoModuleId()

return &PnpmModule{name: name, srcPath: srcPath, containingBuild: containingBuild, executablePath: executablePath}, nil
}

func (pm *PnpmModule) Build() error {
if len(pm.pnpmArgs) > 0 {
output, _, err := buildutils.RunPnpmCmd(pm.executablePath, pm.srcPath, pm.pnpmArgs, &utils.NullLog{})
if len(output) > 0 {
pm.containingBuild.logger.Output(strings.TrimSpace(string(output)))
}
if err != nil {
return err
}
// After executing the user-provided command, cleaning pnpmArgs is needed.
pm.filterPnpmArgsFlags()
}
if !pm.collectBuildInfo {
return nil
}
return pm.CalcDependencies()
}

func (pm *PnpmModule) CalcDependencies() error {
if !pm.containingBuild.buildNameAndNumberProvided() {
return errors.New("a build name must be provided in order to collect the project's dependencies")
}
buildInfoDependencies, err := buildutils.CalculatePnpmDependenciesList(pm.executablePath, pm.srcPath, pm.name,
buildutils.PnpmTreeDepListParam{Args: pm.pnpmArgs}, true, pm.containingBuild.logger)
if err != nil {
return err
}
buildInfoModule := entities.Module{Id: pm.name, Type: entities.Npm, Dependencies: buildInfoDependencies}
buildInfo := &entities.BuildInfo{Modules: []entities.Module{buildInfoModule}}
return pm.containingBuild.SaveBuildInfo(buildInfo)
}

func (pm *PnpmModule) SetName(name string) {
pm.name = name
}

func (pm *PnpmModule) SetPnpmArgs(pnpmArgs []string) {
pm.pnpmArgs = pnpmArgs
}

func (pm *PnpmModule) SetCollectBuildInfo(collectBuildInfo bool) {
pm.collectBuildInfo = collectBuildInfo
}

func (pm *PnpmModule) AddArtifacts(artifacts ...entities.Artifact) error {
return pm.containingBuild.AddArtifacts(pm.name, entities.Npm, artifacts...)
}

// This function discards the pnpm command in pnpmArgs and keeps only the command flags.
// 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.
func (pm *PnpmModule) filterPnpmArgsFlags() {
if len(pm.pnpmArgs) == 1 && !strings.HasPrefix(pm.pnpmArgs[0], "-") {
pm.pnpmArgs = []string{}
}
for argIndex := 0; argIndex < len(pm.pnpmArgs); argIndex++ {
if strings.HasPrefix(pm.pnpmArgs[argIndex], "-") {
pm.pnpmArgs = pm.pnpmArgs[argIndex:]
}
}
}
210 changes: 210 additions & 0 deletions build/pnpm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package build

import (
"path/filepath"
"strconv"
"testing"
"time"

buildutils "github.com/jfrog/build-info-go/build/utils"
"github.com/jfrog/build-info-go/tests"
"github.com/jfrog/build-info-go/utils"
"github.com/stretchr/testify/assert"
)

var pnpmLogger = utils.NewDefaultLogger(utils.INFO)

func TestGenerateBuildInfoForPnpm(t *testing.T) {
pnpmVersion, execPath, err := buildutils.GetPnpmVersionAndExecPath(pnpmLogger)
if err != nil {
t.Skip("pnpm is not installed, skipping test")
}

service := NewBuildInfoService()
pnpmBuild, err := service.GetOrCreateBuild("build-info-go-test-pnpm", strconv.FormatInt(time.Now().Unix(), 10))
assert.NoError(t, err)
defer func() {
assert.NoError(t, pnpmBuild.Clean())
}()

// Create pnpm project.
path, err := filepath.Abs(filepath.Join(".", "testdata"))
assert.NoError(t, err)
projectPath, cleanup := tests.CreatePnpmTest(t, path, "project1")
defer cleanup()

// Install dependencies in the pnpm project.
_, _, err = buildutils.RunPnpmCmd(execPath, projectPath, []string{"install"}, pnpmLogger)
assert.NoError(t, err)

pnpmModule, err := pnpmBuild.AddPnpmModule(projectPath)
assert.NoError(t, err)

err = pnpmModule.CalcDependencies()
assert.NoError(t, err)

buildInfo, err := pnpmBuild.ToBuildInfo()
assert.NoError(t, err)

// Verify results - should have at least one module with dependencies
assert.Len(t, buildInfo.Modules, 1)
assert.Greater(t, len(buildInfo.Modules[0].Dependencies), 0, "Expected at least one dependency")

// Verify pnpm version is supported
assert.True(t, pnpmVersion.AtLeast("6.0.0"), "pnpm version should be at least 6.0.0")
}

func TestFilterPnpmArgsFlags(t *testing.T) {
_, execPath, err := buildutils.GetPnpmVersionAndExecPath(pnpmLogger)
if err != nil {
t.Skip("pnpm is not installed, skipping test")
}

service := NewBuildInfoService()
pnpmBuild, err := service.GetOrCreateBuild("build-info-go-test-pnpm", strconv.FormatInt(time.Now().Unix(), 10))
assert.NoError(t, err)
defer func() {
assert.NoError(t, pnpmBuild.Clean())
}()

// Create pnpm project.
path, err := filepath.Abs(filepath.Join(".", "testdata"))
assert.NoError(t, err)
projectPath, cleanup := tests.CreatePnpmTest(t, path, "project1")
defer cleanup()

// Install dependencies first
_, _, err = buildutils.RunPnpmCmd(execPath, projectPath, []string{"install"}, pnpmLogger)
assert.NoError(t, err)

pnpmModule, err := pnpmBuild.AddPnpmModule(projectPath)
assert.NoError(t, err)

// Test filtering with command and flags
pnpmArgs := []string{"ls", "--json", "--long"}
pnpmModule.SetPnpmArgs(pnpmArgs)
pnpmModule.filterPnpmArgsFlags()
expected := []string{"--json", "--long"}
assert.Equal(t, expected, pnpmModule.pnpmArgs)

// Test filtering with only flags
pnpmArgs = []string{"--prod", "--json"}
pnpmModule.SetPnpmArgs(pnpmArgs)
pnpmModule.filterPnpmArgsFlags()
expected = []string{"--prod", "--json"}
assert.Equal(t, expected, pnpmModule.pnpmArgs)

// Test filtering with single command (no flags)
pnpmArgs = []string{"install"}
pnpmModule.SetPnpmArgs(pnpmArgs)
pnpmModule.filterPnpmArgsFlags()
expected = []string{}
assert.Equal(t, expected, pnpmModule.pnpmArgs)
}

func TestPnpmModuleSetters(t *testing.T) {
_, execPath, err := buildutils.GetPnpmVersionAndExecPath(pnpmLogger)
if err != nil {
t.Skip("pnpm is not installed, skipping test")
}

service := NewBuildInfoService()
pnpmBuild, err := service.GetOrCreateBuild("build-info-go-test-pnpm", strconv.FormatInt(time.Now().Unix(), 10))
assert.NoError(t, err)
defer func() {
assert.NoError(t, pnpmBuild.Clean())
}()

// Create pnpm project.
path, err := filepath.Abs(filepath.Join(".", "testdata"))
assert.NoError(t, err)
projectPath, cleanup := tests.CreatePnpmTest(t, path, "project1")
defer cleanup()

// Install dependencies first
_, _, err = buildutils.RunPnpmCmd(execPath, projectPath, []string{"install"}, pnpmLogger)
assert.NoError(t, err)

pnpmModule, err := pnpmBuild.AddPnpmModule(projectPath)
assert.NoError(t, err)

// Test SetName
pnpmModule.SetName("custom-module-name")
assert.Equal(t, "custom-module-name", pnpmModule.name)

// Test SetPnpmArgs
args := []string{"--prod", "--frozen-lockfile"}
pnpmModule.SetPnpmArgs(args)
assert.Equal(t, args, pnpmModule.pnpmArgs)

// Test SetCollectBuildInfo
pnpmModule.SetCollectBuildInfo(true)
assert.True(t, pnpmModule.collectBuildInfo)
pnpmModule.SetCollectBuildInfo(false)
assert.False(t, pnpmModule.collectBuildInfo)
}

func TestPnpmModuleBuild(t *testing.T) {
_, _, err := buildutils.GetPnpmVersionAndExecPath(pnpmLogger)
if err != nil {
t.Skip("pnpm is not installed, skipping test")
}

service := NewBuildInfoService()
pnpmBuild, err := service.GetOrCreateBuild("build-info-go-test-pnpm", strconv.FormatInt(time.Now().Unix(), 10))
assert.NoError(t, err)
defer func() {
assert.NoError(t, pnpmBuild.Clean())
}()

// Create pnpm project.
path, err := filepath.Abs(filepath.Join(".", "testdata"))
assert.NoError(t, err)
projectPath, cleanup := tests.CreatePnpmTest(t, path, "project1")
defer cleanup()

pnpmModule, err := pnpmBuild.AddPnpmModule(projectPath)
assert.NoError(t, err)

// Test Build with install command
pnpmModule.SetPnpmArgs([]string{"install"})
pnpmModule.SetCollectBuildInfo(false)
err = pnpmModule.Build()
assert.NoError(t, err)

// Test Build with collectBuildInfo enabled
pnpmModule.SetCollectBuildInfo(true)
err = pnpmModule.Build()
assert.NoError(t, err)

buildInfo, err := pnpmBuild.ToBuildInfo()
assert.NoError(t, err)
assert.Len(t, buildInfo.Modules, 1)
}

func TestNewPnpmModule(t *testing.T) {
_, _, err := buildutils.GetPnpmVersionAndExecPath(pnpmLogger)
if err != nil {
t.Skip("pnpm is not installed, skipping test")
}

service := NewBuildInfoService()
pnpmBuild, err := service.GetOrCreateBuild("build-info-go-test-pnpm", strconv.FormatInt(time.Now().Unix(), 10))
assert.NoError(t, err)
defer func() {
assert.NoError(t, pnpmBuild.Clean())
}()

// Create pnpm project.
path, err := filepath.Abs(filepath.Join(".", "testdata"))
assert.NoError(t, err)
projectPath, cleanup := tests.CreatePnpmTest(t, path, "project1")
defer cleanup()

// Test creating module with explicit path
pnpmModule, err := pnpmBuild.AddPnpmModule(projectPath)
assert.NoError(t, err)
assert.NotNil(t, pnpmModule)
assert.Equal(t, projectPath, pnpmModule.srcPath)
assert.NotEmpty(t, pnpmModule.executablePath)
}
25 changes: 25 additions & 0 deletions build/testdata/pnpm/dependenciesList.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"underscore": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
},
"minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true
},
"@jfrog/test-package": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@jfrog/test-package/-/test-package-1.0.0.tgz",
"integrity": "sha512-test==",
"dependencies": {
"nested-dep": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nested-dep/-/nested-dep-2.0.0.tgz",
"integrity": "sha512-nested=="
}
}
}
}
7 changes: 7 additions & 0 deletions build/testdata/pnpm/noBuildProject/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "no-build-pnpm-project",
"version": "1.0.0",
"dependencies": {
"underscore": "1.13.6"
}
}
17 changes: 17 additions & 0 deletions build/testdata/pnpm/project1/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "build-info-go-pnpm-tests",
"version": "1.0.0",
"description": "test package for pnpm",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"underscore": "1.13.6"
},
"devDependencies": {
"minimist": "1.2.8"
}
}
Loading