From 69d0766db02ef48f7d9cce67433c76f754affd2a Mon Sep 17 00:00:00 2001 From: suraj-jadhav Date: Tue, 17 Feb 2026 12:22:04 +0530 Subject: [PATCH 1/6] 6737: Add resolution support for Pnpm to CLI --- build/docker/alpine.Dockerfile | 4 +- build/docker/debian.Dockerfile | 3 +- .../resolution/file/file_batch_factory.go | 90 +++++++++++++--- .../project/.debricked.multiprojects.txt | 1 + .../subproject/.debricked.multiprojects.txt | 1 + internal/resolution/pm/pm.go | 2 + internal/resolution/pm/pnpm/cmd_factory.go | 40 +++++++ .../resolution/pm/pnpm/cmd_factory_test.go | 20 ++++ internal/resolution/pm/pnpm/job.go | 101 ++++++++++++++++++ internal/resolution/pm/pnpm/job_test.go | 85 +++++++++++++++ internal/resolution/pm/pnpm/pm.go | 24 +++++ internal/resolution/pm/pnpm/pm_test.go | 61 +++++++++++ internal/resolution/pm/pnpm/strategy.go | 27 +++++ .../pm/pnpm/testdata/cmd_factory_mock.go | 25 +++++ .../resolution/strategy/strategy_factory.go | 3 + output.txt | 28 +++++ 16 files changed, 499 insertions(+), 16 deletions(-) create mode 100644 internal/resolution/pm/gradle/testdata/project/.debricked.multiprojects.txt create mode 100644 internal/resolution/pm/gradle/testdata/project/subproject/.debricked.multiprojects.txt create mode 100644 internal/resolution/pm/pnpm/cmd_factory.go create mode 100644 internal/resolution/pm/pnpm/cmd_factory_test.go create mode 100644 internal/resolution/pm/pnpm/job.go create mode 100644 internal/resolution/pm/pnpm/job_test.go create mode 100644 internal/resolution/pm/pnpm/pm.go create mode 100644 internal/resolution/pm/pnpm/pm_test.go create mode 100644 internal/resolution/pm/pnpm/strategy.go create mode 100644 internal/resolution/pm/pnpm/testdata/cmd_factory_mock.go create mode 100644 output.txt diff --git a/build/docker/alpine.Dockerfile b/build/docker/alpine.Dockerfile index c444dcd7..cb92158b 100644 --- a/build/docker/alpine.Dockerfile +++ b/build/docker/alpine.Dockerfile @@ -72,7 +72,9 @@ RUN apk --no-cache --update add dotnet8-sdk --repository=https://dl-cdn.alpineli RUN dotnet --version && npm -v && yarn -v && go version -RUN npm install --global bower && bower -v +# Install pnpm and bower for JavaScript resolution (npm/yarn/pnpm/bower) +RUN npm install --global pnpm && pnpm -v && \ + npm install --global bower && bower -v RUN apk add --no-cache \ git \ diff --git a/build/docker/debian.Dockerfile b/build/docker/debian.Dockerfile index ed85254b..38eaa62c 100644 --- a/build/docker/debian.Dockerfile +++ b/build/docker/debian.Dockerfile @@ -77,9 +77,10 @@ RUN apt -y update && apt -y upgrade && apt -y install nodejs && \ apt -y clean && rm -rf /var/lib/apt/lists/* RUN npm install --global npm@latest && \ npm install --global yarn && \ + npm install --global pnpm && \ npm install --global bower -RUN npm -v && yarn -v && bower -v +RUN npm -v && yarn -v && pnpm -v && bower -v # https://learn.microsoft.com/en-us/dotnet/core/install/linux-scripted-manual#scripted-install # https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian diff --git a/internal/resolution/file/file_batch_factory.go b/internal/resolution/file/file_batch_factory.go index c88399c1..b039777f 100644 --- a/internal/resolution/file/file_batch_factory.go +++ b/internal/resolution/file/file_batch_factory.go @@ -1,6 +1,8 @@ package file import ( + "encoding/json" + "fmt" "os" "path/filepath" "regexp" @@ -8,9 +10,11 @@ import ( "github.com/debricked/cli/internal/resolution/pm" "github.com/debricked/cli/internal/resolution/pm/npm" + "github.com/debricked/cli/internal/resolution/pm/pnpm" "github.com/debricked/cli/internal/resolution/pm/poetry" "github.com/debricked/cli/internal/resolution/pm/uv" "github.com/debricked/cli/internal/resolution/pm/yarn" + "github.com/fatih/color" ) type IBatchFactory interface { @@ -19,8 +23,9 @@ type IBatchFactory interface { } type BatchFactory struct { - pms []pm.IPm - npmPreferred bool + pms []pm.IPm + npmPreferred bool + warnedYarnDefaultPM bool } func NewBatchFactory() *BatchFactory { @@ -51,10 +56,6 @@ func (bf *BatchFactory) Make(files []string) []IBatch { func (bf *BatchFactory) processFile(file string, batchMap map[string]IBatch) { base := filepath.Base(file) for _, p := range bf.pms { - if bf.skipPackageManager(p) { - continue - } - for _, manifest := range p.Manifests() { if bf.shouldProcessManifest(manifest, base, file, p) { compiledRegex, _ := regexp.Compile(manifest) @@ -67,6 +68,25 @@ func (bf *BatchFactory) processFile(file string, batchMap map[string]IBatch) { } func (bf *BatchFactory) shouldProcessManifest(manifest, base, file string, p pm.IPm) bool { + if manifest == `package\.json$` && strings.EqualFold(base, "package.json") { + pmName := detectNodePm(file) + if pmName != "" { + // If we can detect the PM from lockfiles, use that + return pmName == p.Name() + } + + // No lockfiles found: fall back to npmPreferred flag between npm and yarn + switch { + case p.Name() == npm.Name && bf.npmPreferred: + return true + case p.Name() == yarn.Name && !bf.npmPreferred: + bf.warnYarnDefault() + return true + default: + return false + } + } + if manifest == "pyproject.toml" && strings.EqualFold(base, "pyproject.toml") { pmName := detectPyprojectPm(file) @@ -85,17 +105,59 @@ func (bf *BatchFactory) addToBatch(p pm.IPm, file string, batchMap map[string]IB batch.Add(file) } -func (bf *BatchFactory) skipPackageManager(p pm.IPm) bool { - name := p.Name() +func (bf *BatchFactory) warnYarnDefault() { + if bf.warnedYarnDefaultPM { + return + } + + fmt.Printf("%s Unable to detect package manager through package.json file, defaulting to yarn.\n", color.YellowString("⚠️")) + bf.warnedYarnDefaultPM = true +} + +func detectNodePm(packageJSONPath string) string { + // Prefer explicit packageManager field if present + content, err := os.ReadFile(packageJSONPath) + if err == nil { + var pkg struct { + PackageManager string `json:"packageManager"` + } + if jsonErr := json.Unmarshal(content, &pkg); jsonErr == nil && pkg.PackageManager != "" { + name := pkg.PackageManager + if at := strings.Index(name, "@"); at > 0 { + name = name[:at] + } + + switch name { + case pnpm.Name: + return pnpm.Name + case yarn.Name: + return yarn.Name + case npm.Name: + return npm.Name + } + } + } + + // Fallback to lockfile-based detection when available + dir := filepath.Dir(packageJSONPath) + + // Prefer pnpm if pnpm lockfile exists + if fileExists(filepath.Join(dir, "pnpm-lock.yaml")) || fileExists(filepath.Join(dir, "pnpm-lock.yml")) { + return pnpm.Name + } + + // Then yarn if yarn.lock exists + if fileExists(filepath.Join(dir, "yarn.lock")) { + return yarn.Name + } - switch true { - case name == npm.Name && !bf.npmPreferred: - return true - case name == yarn.Name && bf.npmPreferred: - return true + // Then npm if package-lock.json exists + if fileExists(filepath.Join(dir, "package-lock.json")) { + return npm.Name } - return false + // Could not determine + return "" } func detectPyprojectPm(pyprojectPath string) string { diff --git a/internal/resolution/pm/gradle/testdata/project/.debricked.multiprojects.txt b/internal/resolution/pm/gradle/testdata/project/.debricked.multiprojects.txt new file mode 100644 index 00000000..8f5eacb3 --- /dev/null +++ b/internal/resolution/pm/gradle/testdata/project/.debricked.multiprojects.txt @@ -0,0 +1 @@ +C:\Users\sjadhav2\Debricked\cli\internal\resolution\pm\gradle\testdata\project \ No newline at end of file diff --git a/internal/resolution/pm/gradle/testdata/project/subproject/.debricked.multiprojects.txt b/internal/resolution/pm/gradle/testdata/project/subproject/.debricked.multiprojects.txt new file mode 100644 index 00000000..9cc18254 --- /dev/null +++ b/internal/resolution/pm/gradle/testdata/project/subproject/.debricked.multiprojects.txt @@ -0,0 +1 @@ +C:\Users\sjadhav2\Debricked\cli\internal\resolution\pm\gradle\testdata\project\subproject \ No newline at end of file diff --git a/internal/resolution/pm/pm.go b/internal/resolution/pm/pm.go index 39ad5c89..3cbe2170 100644 --- a/internal/resolution/pm/pm.go +++ b/internal/resolution/pm/pm.go @@ -9,6 +9,7 @@ import ( "github.com/debricked/cli/internal/resolution/pm/npm" "github.com/debricked/cli/internal/resolution/pm/nuget" "github.com/debricked/cli/internal/resolution/pm/pip" + "github.com/debricked/cli/internal/resolution/pm/pnpm" "github.com/debricked/cli/internal/resolution/pm/poetry" "github.com/debricked/cli/internal/resolution/pm/sbt" "github.com/debricked/cli/internal/resolution/pm/uv" @@ -29,6 +30,7 @@ func Pms() []IPm { poetry.NewPm(), uv.NewPm(), yarn.NewPm(), + pnpm.NewPm(), npm.NewPm(), bower.NewPm(), nuget.NewPm(), diff --git a/internal/resolution/pm/pnpm/cmd_factory.go b/internal/resolution/pm/pnpm/cmd_factory.go new file mode 100644 index 00000000..44418f0b --- /dev/null +++ b/internal/resolution/pm/pnpm/cmd_factory.go @@ -0,0 +1,40 @@ +package pnpm + +import ( + "os/exec" + "path/filepath" +) + +type ICmdFactory interface { + MakeInstallCmd(command string, file string) (*exec.Cmd, error) +} + +type IExecPath interface { + LookPath(file string) (string, error) +} + +type ExecPath struct{} + +func (ExecPath) LookPath(file string) (string, error) { + return exec.LookPath(file) +} + +type CmdFactory struct { + execPath IExecPath +} + +func (cmdf CmdFactory) MakeInstallCmd(command string, file string) (*exec.Cmd, error) { + path, err := cmdf.execPath.LookPath(command) + + fileDir := filepath.Dir(file) + + return &exec.Cmd{ + Path: path, + Args: []string{ + command, + "install", + "--ignore-scripts", // Avoid risky scripts + }, + Dir: fileDir, + }, err +} diff --git a/internal/resolution/pm/pnpm/cmd_factory_test.go b/internal/resolution/pm/pnpm/cmd_factory_test.go new file mode 100644 index 00000000..74d3cc8d --- /dev/null +++ b/internal/resolution/pm/pnpm/cmd_factory_test.go @@ -0,0 +1,20 @@ +package pnpm + +import ( + "testing" + + "github.com/debricked/cli/internal/resolution/pm/pnpm/testdata" + "github.com/stretchr/testify/assert" +) + +func TestMakeInstallCmd(t *testing.T) { + pnpmCommand := "pnpm" + cmd, err := CmdFactory{ + execPath: testdata.ExecPathMock{}, + }.MakeInstallCmd(pnpmCommand, "file") + assert.NoError(t, err) + assert.NotNil(t, cmd) + args := cmd.Args + assert.Contains(t, args, "pnpm") + assert.Contains(t, args, "install") +} diff --git a/internal/resolution/pm/pnpm/job.go b/internal/resolution/pm/pnpm/job.go new file mode 100644 index 00000000..5ff7af1f --- /dev/null +++ b/internal/resolution/pm/pnpm/job.go @@ -0,0 +1,101 @@ +package pnpm + +import ( + "regexp" + "strings" + + "github.com/debricked/cli/internal/resolution/job" + "github.com/debricked/cli/internal/resolution/pm/util" +) + +const ( + pnpm = "pnpm" + executableNotFoundErrRegex = `executable file not found` +) + +type Job struct { + job.BaseJob + install bool + pnpmCommand string + cmdFactory ICmdFactory +} + +func NewJob( + file string, + install bool, + cmdFactory ICmdFactory, +) *Job { + return &Job{ + BaseJob: job.NewBaseJob(file), + install: install, + cmdFactory: cmdFactory, + } +} + +func (j *Job) Install() bool { + return j.install +} + +func (j *Job) Run() { + if j.install { + status := "installing dependencies" + j.SendStatus(status) + j.pnpmCommand = pnpm + + installCmd, err := j.cmdFactory.MakeInstallCmd(j.pnpmCommand, j.GetFile()) + + if err != nil { + j.handleError(j.createError(err.Error(), installCmd.String(), status)) + + return + } + + if output, err := installCmd.Output(); err != nil { + joined := strings.Join([]string{string(output), j.GetExitError(err, "").Error()}, "") + j.handleError(j.createError(joined, installCmd.String(), status)) + + return + } + } +} + +func (j *Job) createError(error string, cmd string, status string) job.IError { + cmdError := util.NewPMJobError(error) + cmdError.SetCommand(cmd) + cmdError.SetStatus(status) + + return cmdError +} + +func (j *Job) handleError(cmdError job.IError) { + expressions := []string{ + executableNotFoundErrRegex, + } + + for _, expression := range expressions { + regex := regexp.MustCompile(expression) + matches := regex.FindAllStringSubmatch(cmdError.Error(), -1) + + if len(matches) > 0 { + cmdError = j.addDocumentation(expression, matches, cmdError) + j.Errors().Append(cmdError) + + return + } + } + + j.Errors().Append(cmdError) +} + +func (j *Job) addDocumentation(expr string, matches [][]string, cmdError job.IError) job.IError { + documentation := cmdError.Documentation() + + switch expr { + case executableNotFoundErrRegex: + documentation = j.GetExecutableNotFoundErrorDocumentation("PNPM") + } + + cmdError.SetDocumentation(documentation) + + return cmdError +} diff --git a/internal/resolution/pm/pnpm/job_test.go b/internal/resolution/pm/pnpm/job_test.go new file mode 100644 index 00000000..8e9495f8 --- /dev/null +++ b/internal/resolution/pm/pnpm/job_test.go @@ -0,0 +1,85 @@ +package pnpm + +import ( + "errors" + "testing" + + jobTestdata "github.com/debricked/cli/internal/resolution/job/testdata" + "github.com/debricked/cli/internal/resolution/pm/pnpm/testdata" + "github.com/debricked/cli/internal/resolution/pm/util" + "github.com/stretchr/testify/assert" +) + +const ( + badName = "bad-name" +) + +func TestNewJob(t *testing.T) { + j := NewJob("file", false, CmdFactory{ + execPath: ExecPath{}, + }) + assert.Equal(t, "file", j.GetFile()) + assert.False(t, j.Errors().HasError()) +} + +func TestInstall(t *testing.T) { + j := Job{install: true} + assert.Equal(t, true, j.Install()) + + j = Job{install: false} + assert.Equal(t, false, j.Install()) +} + +func TestRunInstallCmdErr(t *testing.T) { + cases := []struct { + name string + error string + doc string + }{ + { + name: "General error", + error: "cmd-error", + doc: util.UnknownError, + }, + { + name: "PNPM not found", + error: " |exec: \"pnpm\": executable file not found in $PATH", + doc: "PNPM wasn't found. Please check if it is installed and accessible by the CLI.", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + cmdErr := errors.New(c.error) + cmdFactoryMock := testdata.NewEchoCmdFactory() + cmdFactoryMock.MakeInstallErr = cmdErr + cmd, _ := cmdFactoryMock.MakeInstallCmd("echo", "pnpm-lock.yaml") + + expectedError := util.NewPMJobError(c.error) + expectedError.SetDocumentation(c.doc) + expectedError.SetStatus("installing dependencies") + expectedError.SetCommand(cmd.String()) + + j := NewJob("file", true, cmdFactoryMock) + + go jobTestdata.WaitStatus(j) + j.Run() + + allErrors := j.Errors().GetAll() + + assert.Len(t, j.Errors().GetAll(), 1) + assert.Contains(t, allErrors, expectedError) + }) + } +} + +func TestRunInstallCmdOutputErr(t *testing.T) { + cmdMock := testdata.NewEchoCmdFactory() + cmdMock.InstallCmdName = badName + j := NewJob("file", true, cmdMock) + + go jobTestdata.WaitStatus(j) + j.Run() + + jobTestdata.AssertPathErr(t, j.Errors()) +} diff --git a/internal/resolution/pm/pnpm/pm.go b/internal/resolution/pm/pnpm/pm.go new file mode 100644 index 00000000..3d0fc4d6 --- /dev/null +++ b/internal/resolution/pm/pnpm/pm.go @@ -0,0 +1,24 @@ +package pnpm + +const Name = "pnpm" + +type Pm struct { + name string +} + +func NewPm() Pm { + return Pm{ + name: Name, + } +} + +func (pm Pm) Name() string { + return pm.name +} + +func (Pm) Manifests() []string { + return []string{ + `package\.json$`, + `pnpm-lock\.yaml$`, + } +} diff --git a/internal/resolution/pm/pnpm/pm_test.go b/internal/resolution/pm/pnpm/pm_test.go new file mode 100644 index 00000000..fe79a1ec --- /dev/null +++ b/internal/resolution/pm/pnpm/pm_test.go @@ -0,0 +1,61 @@ +package pnpm + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewPm(t *testing.T) { + pm := NewPm() + assert.Equal(t, Name, pm.name) +} + +func TestName(t *testing.T) { + pm := NewPm() + assert.Equal(t, Name, pm.Name()) +} + +func TestManifests(t *testing.T) { + pm := Pm{} + manifests := pm.Manifests() + assert.Len(t, manifests, 2) + + // First manifest: package.json + manifestPkg := manifests[0] + assert.Equal(t, `package\.json$`, manifestPkg) + _, err := regexp.Compile(manifestPkg) + assert.NoError(t, err) + + casesPkg := map[string]bool{ + "package.json": true, + "pnpm-lock.yaml": false, + "pnpm-lock.yml": false, + "package-lock.json": false, + } + for file, isMatch := range casesPkg { + t.Run("pkg-"+file, func(t *testing.T) { + matched, _ := regexp.MatchString(manifestPkg, file) + assert.Equal(t, isMatch, matched) + }) + } + + // Second manifest: pnpm-lock.yaml + manifestLock := manifests[1] + assert.Equal(t, `pnpm-lock\.yaml$`, manifestLock) + _, err = regexp.Compile(manifestLock) + assert.NoError(t, err) + + casesLock := map[string]bool{ + "pnpm-lock.yaml": true, + "pnpm-lock.yml": false, + "package.json": false, + } + for file, isMatch := range casesLock { + t.Run("lock-"+file, func(t *testing.T) { + matched, _ := regexp.MatchString(manifestLock, file) + assert.Equal(t, isMatch, matched) + }) + } +} diff --git a/internal/resolution/pm/pnpm/strategy.go b/internal/resolution/pm/pnpm/strategy.go new file mode 100644 index 00000000..a6aab07f --- /dev/null +++ b/internal/resolution/pm/pnpm/strategy.go @@ -0,0 +1,27 @@ +package pnpm + +import "github.com/debricked/cli/internal/resolution/job" + +type Strategy struct { + files []string +} + +func (s Strategy) Invoke() ([]job.IJob, error) { + var jobs []job.IJob + for _, file := range s.files { + jobs = append(jobs, NewJob( + file, + true, + CmdFactory{ + execPath: ExecPath{}, + }, + ), + ) + } + + return jobs, nil +} + +func NewStrategy(files []string) Strategy { + return Strategy{files} +} diff --git a/internal/resolution/pm/pnpm/testdata/cmd_factory_mock.go b/internal/resolution/pm/pnpm/testdata/cmd_factory_mock.go new file mode 100644 index 00000000..a9928df1 --- /dev/null +++ b/internal/resolution/pm/pnpm/testdata/cmd_factory_mock.go @@ -0,0 +1,25 @@ +package testdata + +import "os/exec" + +type CmdFactoryMock struct { + InstallCmdName string + MakeInstallErr error +} + +type ExecPathMock struct{} + +func (ExecPathMock) LookPath(file string) (string, error) { + // Just return the name; Exec.Cmd will use PATH resolution + return file, nil +} + +func NewEchoCmdFactory() CmdFactoryMock { + return CmdFactoryMock{ + InstallCmdName: "echo", + } +} + +func (f CmdFactoryMock) MakeInstallCmd(command string, file string) (*exec.Cmd, error) { + return exec.Command(f.InstallCmdName), f.MakeInstallErr +} diff --git a/internal/resolution/strategy/strategy_factory.go b/internal/resolution/strategy/strategy_factory.go index 04543026..75565a43 100644 --- a/internal/resolution/strategy/strategy_factory.go +++ b/internal/resolution/strategy/strategy_factory.go @@ -12,6 +12,7 @@ import ( "github.com/debricked/cli/internal/resolution/pm/npm" "github.com/debricked/cli/internal/resolution/pm/nuget" "github.com/debricked/cli/internal/resolution/pm/pip" + "github.com/debricked/cli/internal/resolution/pm/pnpm" "github.com/debricked/cli/internal/resolution/pm/poetry" "github.com/debricked/cli/internal/resolution/pm/sbt" "github.com/debricked/cli/internal/resolution/pm/uv" @@ -46,6 +47,8 @@ func (sf Factory) Make(pmFileBatch file.IBatch, paths []string) (IStrategy, erro return uv.NewStrategy(pmFileBatch.Files()), nil case yarn.Name: return yarn.NewStrategy(pmFileBatch.Files()), nil + case pnpm.Name: + return pnpm.NewStrategy(pmFileBatch.Files()), nil case npm.Name: return npm.NewStrategy(pmFileBatch.Files()), nil case bower.Name: diff --git a/output.txt b/output.txt new file mode 100644 index 00000000..e01444a7 --- /dev/null +++ b/output.txt @@ -0,0 +1,28 @@ +OPENTEXT+sjadhav2@OTX-BQSBY24 MINGW64 ~/Debricked/cli (6737-add-new-pm-pnpm) +$ ./debricked.exe resolve ../Repos/PNPM/modern-typescript-monorepo-example-main +⚠️ Unable to get supported formats from the server. Using cached data instead. +✓ Resolving ..\...\modern-typescript-monorepo-example-main\package.json: done +✗ Resolving ..\...\typescript-example-1\package.json: failed +✓ Resolving ..\...\typescript-example-2\package.json: done +Errors +------- +..\Repos\PNPM\modern-typescript-monorepo-example-main\packages\typescript-example-1\package.json +* Critical: installing dependencies failed +Failed to find package "https://registry.yarnpkg.com/@mme%2ftypescript-example-2" that satisfies the requirement from yarn dependencies. Please check that dependencies are correct in your package.json file. +If this is a private dependency, please make sure that the debricked CLI has access to install it or pre-install it before running the debricked CLI. +`C:\Users\sjadhav2\AppData\Roaming\npm\yarn.cmd install --non-interactive --ignore-scripts --ignore-engines --ignore-platform --no-bin-links --production=false` + |yarn install v1.22.22 + |info No lockfile found. + |[1/4] Resolving packages... + |info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command. + |error Error: https://registry.yarnpkg.com/@mme%2ftypescript-example-2: Not found + | at params.callback [as _callback] (C:\Users\sjadhav2\AppData\Roaming\npm\node_modules\yarn\lib\cli.js:66680:18) + | at self.callback (C:\Users\sjadhav2\AppData\Roaming\npm\node_modules\yarn\lib\cli.js:141410:22) + | at Request.emit (node:events:518:28) + | at Request. (C:\Users\sjadhav2\AppData\Roaming\npm\node_modules\yarn\lib\cli.js:142382:10) + | at Request.emit (node:events:518:28) + | at IncomingMessage. (C:\Users\sjadhav2\AppData\Roaming\npm\node_modules\yarn\lib\cli.js:142304:12) + | at Object.onceWrapper (node:events:632:28) + | at IncomingMessage.emit (node:events:530:35) + | at endReadableNT (node:internal/streams/readable:1698:12) + | at process.processTicksAndRejections (node:internal/process/task_queues:90:21) \ No newline at end of file From 99573779fde3c7fdab4c4bd7c41a17f9a194f0b8 Mon Sep 17 00:00:00 2001 From: suraj-jadhav Date: Tue, 17 Feb 2026 12:34:07 +0530 Subject: [PATCH 2/6] 6737: Add resolution support for Pnpm to CLI --- .../project/.debricked.multiprojects.txt | 1 - .../subproject/.debricked.multiprojects.txt | 1 - output.txt | 28 ------------------- 3 files changed, 30 deletions(-) delete mode 100644 internal/resolution/pm/gradle/testdata/project/.debricked.multiprojects.txt delete mode 100644 internal/resolution/pm/gradle/testdata/project/subproject/.debricked.multiprojects.txt delete mode 100644 output.txt diff --git a/internal/resolution/pm/gradle/testdata/project/.debricked.multiprojects.txt b/internal/resolution/pm/gradle/testdata/project/.debricked.multiprojects.txt deleted file mode 100644 index 8f5eacb3..00000000 --- a/internal/resolution/pm/gradle/testdata/project/.debricked.multiprojects.txt +++ /dev/null @@ -1 +0,0 @@ -C:\Users\sjadhav2\Debricked\cli\internal\resolution\pm\gradle\testdata\project \ No newline at end of file diff --git a/internal/resolution/pm/gradle/testdata/project/subproject/.debricked.multiprojects.txt b/internal/resolution/pm/gradle/testdata/project/subproject/.debricked.multiprojects.txt deleted file mode 100644 index 9cc18254..00000000 --- a/internal/resolution/pm/gradle/testdata/project/subproject/.debricked.multiprojects.txt +++ /dev/null @@ -1 +0,0 @@ -C:\Users\sjadhav2\Debricked\cli\internal\resolution\pm\gradle\testdata\project\subproject \ No newline at end of file diff --git a/output.txt b/output.txt deleted file mode 100644 index e01444a7..00000000 --- a/output.txt +++ /dev/null @@ -1,28 +0,0 @@ -OPENTEXT+sjadhav2@OTX-BQSBY24 MINGW64 ~/Debricked/cli (6737-add-new-pm-pnpm) -$ ./debricked.exe resolve ../Repos/PNPM/modern-typescript-monorepo-example-main -⚠️ Unable to get supported formats from the server. Using cached data instead. -✓ Resolving ..\...\modern-typescript-monorepo-example-main\package.json: done -✗ Resolving ..\...\typescript-example-1\package.json: failed -✓ Resolving ..\...\typescript-example-2\package.json: done -Errors -------- -..\Repos\PNPM\modern-typescript-monorepo-example-main\packages\typescript-example-1\package.json -* Critical: installing dependencies failed -Failed to find package "https://registry.yarnpkg.com/@mme%2ftypescript-example-2" that satisfies the requirement from yarn dependencies. Please check that dependencies are correct in your package.json file. -If this is a private dependency, please make sure that the debricked CLI has access to install it or pre-install it before running the debricked CLI. -`C:\Users\sjadhav2\AppData\Roaming\npm\yarn.cmd install --non-interactive --ignore-scripts --ignore-engines --ignore-platform --no-bin-links --production=false` - |yarn install v1.22.22 - |info No lockfile found. - |[1/4] Resolving packages... - |info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command. - |error Error: https://registry.yarnpkg.com/@mme%2ftypescript-example-2: Not found - | at params.callback [as _callback] (C:\Users\sjadhav2\AppData\Roaming\npm\node_modules\yarn\lib\cli.js:66680:18) - | at self.callback (C:\Users\sjadhav2\AppData\Roaming\npm\node_modules\yarn\lib\cli.js:141410:22) - | at Request.emit (node:events:518:28) - | at Request. (C:\Users\sjadhav2\AppData\Roaming\npm\node_modules\yarn\lib\cli.js:142382:10) - | at Request.emit (node:events:518:28) - | at IncomingMessage. (C:\Users\sjadhav2\AppData\Roaming\npm\node_modules\yarn\lib\cli.js:142304:12) - | at Object.onceWrapper (node:events:632:28) - | at IncomingMessage.emit (node:events:530:35) - | at endReadableNT (node:internal/streams/readable:1698:12) - | at process.processTicksAndRejections (node:internal/process/task_queues:90:21) \ No newline at end of file From 7268a129ee8ac30aa9f6a79530db7a87c9c10005 Mon Sep 17 00:00:00 2001 From: suraj-jadhav Date: Tue, 17 Feb 2026 13:53:41 +0530 Subject: [PATCH 3/6] 6737: Add resolution support for Pnpm to CLI --- .../resolution/file/file_batch_factory.go | 111 ++++++++++-------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/internal/resolution/file/file_batch_factory.go b/internal/resolution/file/file_batch_factory.go index b039777f..17cabf67 100644 --- a/internal/resolution/file/file_batch_factory.go +++ b/internal/resolution/file/file_batch_factory.go @@ -68,32 +68,49 @@ func (bf *BatchFactory) processFile(file string, batchMap map[string]IBatch) { } func (bf *BatchFactory) shouldProcessManifest(manifest, base, file string, p pm.IPm) bool { - if manifest == `package\.json$` && strings.EqualFold(base, "package.json") { - pmName := detectNodePm(file) - if pmName != "" { - // If we can detect the PM from lockfiles, use that - return pmName == p.Name() - } + if isNodePackageJSON(manifest, base) { + return bf.shouldProcessNodeManifest(file, p) + } - // No lockfiles found: fall back to npmPreferred flag between npm and yarn - switch { - case p.Name() == npm.Name && bf.npmPreferred: - return true - case p.Name() == yarn.Name && !bf.npmPreferred: - bf.warnYarnDefault() - return true - default: - return false - } + if isPyprojectToml(manifest, base) { + return shouldProcessPyprojectManifest(file, p) } - if manifest == "pyproject.toml" && strings.EqualFold(base, "pyproject.toml") { - pmName := detectPyprojectPm(file) + return true +} +func isNodePackageJSON(manifest, base string) bool { + return manifest == `package\.json$` && strings.EqualFold(base, "package.json") +} + +func (bf *BatchFactory) shouldProcessNodeManifest(file string, p pm.IPm) bool { + pmName := detectNodePm(file) + if pmName != "" { + // If we can detect the PM from lockfiles or package.json, use that return pmName == p.Name() } - return true + // No lockfiles or explicit packageManager found: fall back to npmPreferred flag between npm and yarn + switch { + case p.Name() == npm.Name && bf.npmPreferred: + return true + case p.Name() == yarn.Name && !bf.npmPreferred: + bf.warnYarnDefault() + + return true + default: + return false + } +} + +func isPyprojectToml(manifest, base string) bool { + return manifest == "pyproject.toml" && strings.EqualFold(base, "pyproject.toml") +} + +func shouldProcessPyprojectManifest(file string, p pm.IPm) bool { + pmName := detectPyprojectPm(file) + + return pmName == p.Name() } func (bf *BatchFactory) addToBatch(p pm.IPm, file string, batchMap map[string]IBatch) { @@ -107,6 +124,7 @@ func (bf *BatchFactory) addToBatch(p pm.IPm, file string, batchMap map[string]IB func (bf *BatchFactory) warnYarnDefault() { if bf.warnedYarnDefaultPM { + return } @@ -115,49 +133,38 @@ func (bf *BatchFactory) warnYarnDefault() { } func detectNodePm(packageJSONPath string) string { + return detectNodePmFromPackageJSON(packageJSONPath) +} + +func detectNodePmFromPackageJSON(packageJSONPath string) string { // Prefer explicit packageManager field if present content, err := os.ReadFile(packageJSONPath) - if err == nil { - var pkg struct { - PackageManager string `json:"packageManager"` - } - if jsonErr := json.Unmarshal(content, &pkg); jsonErr == nil && pkg.PackageManager != "" { - name := pkg.PackageManager - if at := strings.Index(name, "@"); at > 0 { - name = name[:at] - } - - switch name { - case pnpm.Name: - return pnpm.Name - case yarn.Name: - return yarn.Name - case npm.Name: - return npm.Name - } - } + if err != nil { + return "" } - // Fallback to lockfile-based detection when available - dir := filepath.Dir(packageJSONPath) - - // Prefer pnpm if pnpm lockfile exists - if fileExists(filepath.Join(dir, "pnpm-lock.yaml")) || fileExists(filepath.Join(dir, "pnpm-lock.yml")) { - return pnpm.Name + var pkg struct { + PackageManager string `json:"packageManager"` + } + if jsonErr := json.Unmarshal(content, &pkg); jsonErr != nil || pkg.PackageManager == "" { + return "" } - // Then yarn if yarn.lock exists - if fileExists(filepath.Join(dir, "yarn.lock")) { - return yarn.Name + name := pkg.PackageManager + if at := strings.Index(name, "@"); at > 0 { + name = name[:at] } - // Then npm if package-lock.json exists - if fileExists(filepath.Join(dir, "package-lock.json")) { + switch name { + case pnpm.Name: + return pnpm.Name + case yarn.Name: + return yarn.Name + case npm.Name: return npm.Name + default: + return "" } - - // Could not determine - return "" } func detectPyprojectPm(pyprojectPath string) string { From 4ca796d94f0e1c67554c951a11389f3d2fb9ca8f Mon Sep 17 00:00:00 2001 From: suraj-jadhav-ot Date: Wed, 18 Feb 2026 11:21:47 +0530 Subject: [PATCH 4/6] 6737: Add resolution support for Pnpm to CLI --- internal/resolution/file/file_batch_factory.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/resolution/file/file_batch_factory.go b/internal/resolution/file/file_batch_factory.go index 17cabf67..c15ad5f3 100644 --- a/internal/resolution/file/file_batch_factory.go +++ b/internal/resolution/file/file_batch_factory.go @@ -90,7 +90,7 @@ func (bf *BatchFactory) shouldProcessNodeManifest(file string, p pm.IPm) bool { return pmName == p.Name() } - // No lockfiles or explicit packageManager found: fall back to npmPreferred flag between npm and yarn + // No explicit packageManager found: fall back to npmPreferred flag between npm and yarn switch { case p.Name() == npm.Name && bf.npmPreferred: return true From f194f302a2dd64f3dcc92be0d0ed0b0b206e2449 Mon Sep 17 00:00:00 2001 From: suraj-jadhav-ot Date: Wed, 18 Feb 2026 12:03:07 +0530 Subject: [PATCH 5/6] 6737: Add resolution support for Pnpm to CLI --- internal/resolution/file/file_batch_factory.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/resolution/file/file_batch_factory.go b/internal/resolution/file/file_batch_factory.go index c15ad5f3..80bbebf4 100644 --- a/internal/resolution/file/file_batch_factory.go +++ b/internal/resolution/file/file_batch_factory.go @@ -109,7 +109,6 @@ func isPyprojectToml(manifest, base string) bool { func shouldProcessPyprojectManifest(file string, p pm.IPm) bool { pmName := detectPyprojectPm(file) - return pmName == p.Name() } From 3930eef87ec77052ff49acd585558b477d9483a1 Mon Sep 17 00:00:00 2001 From: suraj-jadhav-ot Date: Wed, 18 Feb 2026 12:05:19 +0530 Subject: [PATCH 6/6] 6737: Add resolution support for Pnpm to CLI --- internal/resolution/file/file_batch_factory.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/resolution/file/file_batch_factory.go b/internal/resolution/file/file_batch_factory.go index 80bbebf4..c15ad5f3 100644 --- a/internal/resolution/file/file_batch_factory.go +++ b/internal/resolution/file/file_batch_factory.go @@ -109,6 +109,7 @@ func isPyprojectToml(manifest, base string) bool { func shouldProcessPyprojectManifest(file string, p pm.IPm) bool { pmName := detectPyprojectPm(file) + return pmName == p.Name() }