Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Loading