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
10 changes: 10 additions & 0 deletions src/nodejs/pnpm/pnpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ type PNPM struct {
Log *libbuildpack.Logger
}

func (p *PNPM) Rebuild(buildDir string) error {
p.Log.Info("Rebuilding any native modules (pnpm)")
cmd := exec.Command("pnpm", "rebuild", "--recursive")
cmd.Dir = buildDir
cmd.Stdout = p.Log.Output()
cmd.Stderr = p.Log.Output()
cmd.Env = append(os.Environ(), "npm_config_nodedir="+os.Getenv("NODE_HOME"))
return p.Command.Run(cmd)
}

func (p *PNPM) Build(buildDir, cacheDir string) error {
p.Log.Info("Installing node modules (pnpm-lock.yaml)")

Expand Down
13 changes: 13 additions & 0 deletions src/nodejs/pnpm/pnpm_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package pnpm_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestPnpm(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Pnpm Suite")
}
150 changes: 150 additions & 0 deletions src/nodejs/pnpm/pnpm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package pnpm_test

import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"reflect"

"github.com/cloudfoundry/libbuildpack"
"github.com/cloudfoundry/libbuildpack/ansicleaner"
p "github.com/cloudfoundry/nodejs-buildpack/src/nodejs/pnpm"
"github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

//go:generate mockgen -source=pnpm.go --destination=mocks_test.go --package=pnpm_test

// MockCommand is a hand-written mock matching the pnpm.Command interface.
type MockCommand struct {
ctrl *gomock.Controller
recorder *MockCommandMockRecorder
}

type MockCommandMockRecorder struct {
mock *MockCommand
}

func NewMockCommand(ctrl *gomock.Controller) *MockCommand {
mock := &MockCommand{ctrl: ctrl}
mock.recorder = &MockCommandMockRecorder{mock}
return mock
}

func (m *MockCommand) EXPECT() *MockCommandMockRecorder { return m.recorder }

func (m *MockCommand) Execute(dir string, stdout, stderr io.Writer, program string, args ...string) error {
varargs := []interface{}{dir, stdout, stderr, program}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Execute", varargs...)
ret0, _ := ret[0].(error)
return ret0
}

func (mr *MockCommandMockRecorder) Execute(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call {
varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockCommand)(nil).Execute), varargs...)
}

func (m *MockCommand) Run(cmd *exec.Cmd) error {
ret := m.ctrl.Call(m, "Run", cmd)
ret0, _ := ret[0].(error)
return ret0
}

func (mr *MockCommandMockRecorder) Run(cmd interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockCommand)(nil).Run), cmd)
}

var _ = Describe("PNPM", func() {
var (
err error
buildDir string
cacheDir string
pnpm *p.PNPM
logger *libbuildpack.Logger
buffer *bytes.Buffer
mockCtrl *gomock.Controller
mockCommand *MockCommand
oldNodeHome string
)

BeforeEach(func() {
buildDir, err = os.MkdirTemp("", "nodejs-buildpack.build.")
Expect(err).To(BeNil())
cacheDir, err = os.MkdirTemp("", "nodejs-buildpack.cache.")
Expect(err).To(BeNil())

buffer = new(bytes.Buffer)
logger = libbuildpack.NewLogger(ansicleaner.New(buffer))

mockCtrl = gomock.NewController(GinkgoT())
mockCommand = NewMockCommand(mockCtrl)

pnpm = &p.PNPM{
Log: logger,
Command: mockCommand,
}

oldNodeHome = os.Getenv("NODE_HOME")
Expect(os.Setenv("NODE_HOME", "test_node_home")).To(Succeed())
})

AfterEach(func() {
mockCtrl.Finish()
Expect(os.Setenv("NODE_HOME", oldNodeHome)).To(Succeed())
Expect(os.RemoveAll(buildDir)).To(Succeed())
Expect(os.RemoveAll(cacheDir)).To(Succeed())
})

Describe("Rebuild", func() {
It("runs pnpm rebuild with NODE_HOME set", func() {
mockCommand.EXPECT().Run(gomock.Any()).DoAndReturn(func(cmd *exec.Cmd) error {
Expect(cmd.Args).To(Equal([]string{"pnpm", "rebuild", "--recursive"}))
Expect(cmd.Dir).To(Equal(buildDir))
Expect(cmd.Env).To(ContainElement("npm_config_nodedir=test_node_home"))
return nil
})
Expect(pnpm.Rebuild(buildDir)).To(Succeed())
Expect(buffer.String()).To(ContainSubstring("Rebuilding any native modules (pnpm)"))
})

It("propagates command errors", func() {
mockCommand.EXPECT().Run(gomock.Any()).Return(fmt.Errorf("pnpm rebuild failed"))
Expect(pnpm.Rebuild(buildDir)).To(MatchError("pnpm rebuild failed"))
})
})

Describe("Build", func() {
Context("online mode (no .pnpm-store in buildDir)", func() {
It("runs pnpm install with cache dir", func() {
mockCommand.EXPECT().Run(gomock.Any()).DoAndReturn(func(cmd *exec.Cmd) error {
Expect(cmd.Args).To(Equal([]string{"pnpm", "install", "--no-frozen-lockfile"}))
Expect(cmd.Dir).To(Equal(buildDir))
Expect(cmd.Env).To(ContainElement("PNPM_HOME=" + cacheDir + "/.pnpm-store"))
return nil
})
Expect(pnpm.Build(buildDir, cacheDir)).To(Succeed())
})
})

Context("offline mode (.pnpm-store exists in buildDir)", func() {
BeforeEach(func() {
Expect(os.MkdirAll(buildDir+"/.pnpm-store", 0755)).To(Succeed())
})

It("runs pnpm install --offline", func() {
mockCommand.EXPECT().Run(gomock.Any()).DoAndReturn(func(cmd *exec.Cmd) error {
Expect(cmd.Args).To(Equal([]string{"pnpm", "install", "--no-frozen-lockfile", "--offline"}))
return nil
})
Expect(pnpm.Build(buildDir, cacheDir)).To(Succeed())
})
})
})
})
51 changes: 51 additions & 0 deletions src/nodejs/supply/mocks_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src/nodejs/supply/supply.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type Yarn interface {

type PNPM interface {
Build(string, string) error
Rebuild(string) error
}

type Stager interface {
Expand Down Expand Up @@ -345,6 +346,12 @@ func (s *Supplier) BuildDependencies() error {
return err
}

case s.UsePNPM && s.IsVendored:
s.Log.Info("Prebuild detected (node_modules already exists)")
if err := s.PNPM.Rebuild(s.Stager.BuildDir()); err != nil {
return err
}

case s.UsePNPM:
if err := s.PNPM.Build(s.Stager.BuildDir(), s.Stager.CacheDir()); err != nil {
return err
Expand Down
29 changes: 29 additions & 0 deletions src/nodejs/supply/supply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var _ = Describe("Supply", func() {
mockCtrl *gomock.Controller
mockYarn *MockYarn
mockNPM *MockNPM
mockPNPM *MockPNPM
mockManifest *MockManifest
mockInstaller *MockInstaller
mockCommand *MockCommand
Expand Down Expand Up @@ -63,6 +64,7 @@ var _ = Describe("Supply", func() {
mockCommand = NewMockCommand(mockCtrl)
mockYarn = NewMockYarn(mockCtrl)
mockNPM = NewMockNPM(mockCtrl)
mockPNPM = NewMockPNPM(mockCtrl)

installNode = func(dep libbuildpack.Dependency, nodeDir string) {
err := os.MkdirAll(filepath.Join(nodeDir, "bin"), 0755)
Expand Down Expand Up @@ -93,6 +95,7 @@ var _ = Describe("Supply", func() {
Stager: stager,
Yarn: mockYarn,
NPM: mockNPM,
PNPM: mockPNPM,
Log: logger,
Manifest: mockManifest,
Installer: mockInstaller,
Expand Down Expand Up @@ -1201,6 +1204,32 @@ var _ = Describe("Supply", func() {
})
})

Context("using pnpm", func() {
BeforeEach(func() {
supplier.UsePNPM = true
})

It("runs pnpm build when node_modules does not exist", func() {
supplier.IsVendored = false
mockPNPM.EXPECT().Build(buildDir, cacheDir).Return(nil)
Expect(supplier.BuildDependencies()).To(Succeed())
})

It("runs pnpm rebuild when node_modules already exists (vendored)", func() {
supplier.IsVendored = true
mockPNPM.EXPECT().Rebuild(buildDir).Return(nil)
Expect(supplier.BuildDependencies()).To(Succeed())
})

It("calls PNPM.Rebuild (not NPM) when pnpm is vendored", func() {
supplier.IsVendored = true
mockPNPM.EXPECT().Rebuild(buildDir).Return(nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test not fit the it description, he no test npm calls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed the test description

mockNPM.EXPECT().Rebuild(gomock.Any()).Times(0)
mockNPM.EXPECT().Build(gomock.Any(), gomock.Any()).Times(0)
Expect(supplier.BuildDependencies()).To(Succeed())
})
})

Describe("using npm", func() {
BeforeEach(func() {
supplier.UseYarn = false
Expand Down
Loading