-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpackagemanager.go
289 lines (237 loc) · 8.33 KB
/
packagemanager.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
// Adapted from https://github.com/replit/upm
// Copyright (c) 2019 Neoreason d/b/a Repl.it. All rights reserved.
// SPDX-License-Identifier: MIT
// By the turbo team for turborepo which is licensed the MPL v2.0 license
// https://github.com/vercel/turbo/tree/368b715/cli/internal/packagemanager
// This version remove any dependency on turborepo internals and refactor
// a bit of code to make this more generic package to re-use in other application
// As the License in the original file seems to be MIT all modification made will
// be under the MIT license too.
package packagemanager
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/bmatcuk/doublestar/v4"
"github.com/software-t-rex/packageJson"
)
// PackageManager is an abstraction across package managers
type PackageManager struct {
// The descriptive name of the Package Manager.
Name string
// The unique identifier of the Package Manager.
Slug string
// The command used to invoke the Package Manager.
Command string
// The location of the package spec file used by the Package Manager.
Specfile string
// The location of the package lock file used by the Package Manager.
Lockfile string
// The directory in which package assets are stored by the Package Manager.
PackageDir string
// The location of the file that defines the workspace. Empty if workspaces defined in package.json
WorkspaceConfigurationPath string
// The separator that the Package Manger uses to identify arguments that
// should be passed through to the underlying script.
ArgSeparator []string
// Return the list of workspace glob
getWorkspaceGlobs func(rootpath string) ([]string, error)
// Return the list of workspace ignore globs
getWorkspaceIgnores func(pm PackageManager, rootpath string) ([]string, error)
// Detect if Turbo knows how to produce a pruned workspace for the project
canPrune func(cwd string) (bool, error)
// Test a manager and version tuple to see if it is the Package Manager.
Matches func(manager string, version string) (bool, error)
// Detect if the project is using the Package Manager by inspecting the system.
detect func(projectDirectory string, packageManager *PackageManager) (bool, error)
// @FIXME missing Lockfile support
// Read a lockfile for a given package manager
// UnmarshalLockfile func(contents []byte) (lockfile.Lockfile, error)
// Prune the given pkgJSON to only include references to the given patches
prunePatches func(pkgJSON *packageJson.PackageJSON, patches []string) error
}
var packageManagers = []PackageManager{
nodejsYarn,
nodejsBerry,
nodejsNpm,
nodejsPnpm,
nodejsPnpm6,
}
var (
packageManagerPattern = `(npm|pnpm|yarn)@(\d+)\.\d+\.\d+(-.+)?`
packageManagerRegex = regexp.MustCompile(packageManagerPattern)
)
// ParsePackageManagerString takes a package manager version string parses it into consituent components
func ParsePackageManagerString(packageManager string) (manager string, version string, err error) {
match := packageManagerRegex.FindString(packageManager)
if len(match) == 0 {
return "", "", fmt.Errorf("we could not parse packageManager field in package.json, expected: %s, received: %s", packageManagerPattern, packageManager)
}
return strings.Split(match, "@")[0], strings.Split(match, "@")[1], nil
}
// GetPackageManager attempts all methods for identifying the package manager in use.
func GetPackageManager(projectDirectory string, pkg *packageJson.PackageJSON) (packageManager *PackageManager, err error) {
result, _ := GetPackageManagerFromString(pkg.PackageManager)
if result != nil {
return result, nil
}
return DetectPackageManager(projectDirectory)
}
func GetPackageManagerFromString(packageManagerStr string) (packageManager *PackageManager, err error) {
if packageManagerStr == "" {
return nil, fmt.Errorf("no package manager specified")
}
manager, version, err := ParsePackageManagerString(packageManagerStr)
if err != nil {
return nil, err
}
for _, packageManager := range packageManagers {
isResponsible, err := packageManager.Matches(manager, version)
if isResponsible && (err == nil) {
return &packageManager, nil
}
}
return nil, fmt.Errorf("we didn't find a matching package manager for '%s'", packageManagerStr)
}
// detectPackageManager attempts to detect the package manager by inspecting the project directory state.
func DetectPackageManager(projectDirectory string) (packageManager *PackageManager, err error) {
for _, packageManager := range packageManagers {
isResponsible, err := packageManager.detect(projectDirectory, &packageManager)
if err != nil {
return nil, err
}
if isResponsible {
return &packageManager, nil
}
}
return nil, fmt.Errorf("we did not detect an in-use package manager for your project. Please set the \"packageManager\" property in your root package.json (https://nodejs.org/api/packages.html#packagemanager)")
}
// GetWorkspaces returns the list of package.json files for the current mono[space|repo].
func (pm PackageManager) GetWorkspaces(rootpath string, relativePath bool) ([]string, error) {
globs, err := pm.getWorkspaceGlobs(rootpath)
if err != nil {
return nil, err
}
justJsons := make([]string, len(globs))
for i, space := range globs {
justJsons[i] = filepath.Join(space, "package.json")
}
ignores, err := pm.getWorkspaceIgnores(pm, rootpath)
if err != nil {
return nil, err
}
// f, err := globby.GlobFiles(rootpath, justJsons, ignores)
fs := os.DirFS(rootpath)
var res []string
for _, glob := range justJsons {
founds, err := doublestar.Glob(fs, glob)
if err != nil {
return nil, err
}
res = append(res, founds...)
}
for _, glob := range ignores {
for i, path := range res {
match, err := doublestar.Match(path, glob)
if err != nil {
return nil, err
}
if match {
res = append(res[:i], res[i+1:]...)
}
}
}
// make res fullpath
if !relativePath {
for i, path := range res {
res[i] = filepath.Join(rootpath, path)
}
}
return res, nil
}
// GetWorkspaceIgnores returns an array of globs not to search for workspaces.
func (pm PackageManager) GetWorkspaceIgnores(rootpath string) ([]string, error) {
return pm.getWorkspaceIgnores(pm, rootpath)
}
// CanPrune returns if we can produce a pruned workspace. Can error if fs issues occur
func (pm PackageManager) CanPrune(projectDirectory string) (bool, error) {
if pm.canPrune != nil {
return pm.canPrune(projectDirectory)
}
return false, nil
}
// @FIXME missing lockfile support
// ReadLockfile will read the applicable lockfile into memory
// func (pm PackageManager) ReadLockfile(projectDirectory string) (lockfile.Lockfile, error) {
// if pm.UnmarshalLockfile == nil {
// return nil, nil
// }
// contents, err := os.ReadFile(filepath.Join(projectDirectory, pm.Lockfile))
// if err != nil {
// return nil, fmt.Errorf("reading %s: %w", pm.Lockfile, err)
// }
// return pm.UnmarshalLockfile(contents)
// }
// PrunePatchedPackages will alter the provided pkgJSON to only reference the provided patches
func (pm PackageManager) PrunePatchedPackages(pkgJSON *packageJson.PackageJSON, patches []string) error {
if pm.prunePatches != nil {
return pm.prunePatches(pkgJSON, patches)
}
return nil
}
func (pm PackageManager) GetVersion() (string, error) {
cmd := exec.Command(pm.Command, "--version")
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("could not detect %s version: %w", pm.Name, err)
}
return strings.TrimSpace(string(out)), nil
}
// Same as GetVersion but remove any +suffix
func (pm PackageManager) GetStandardVersion() (string, error) {
version, error := pm.GetVersion()
return strings.Split(version, "+")[0], error
}
// YarnRC Represents contents of .yarnrc.yml
type YarnRC struct {
NodeLinker string `yaml:"nodeLinker"`
}
func FileExists(path string) bool {
info, err := os.Lstat(path)
return err == nil && !info.IsDir()
}
func PathExists(path string) bool {
_, err := os.Lstat(path)
return err == nil
}
func hasFile(name, dir string) (bool, error) {
files, err := os.ReadDir(dir)
if err != nil {
return false, err
}
for _, f := range files {
if name == f.Name() {
return true, nil
}
}
return false, nil
}
func FindupFrom(name, dir string) (string, error) {
for {
found, err := hasFile(name, dir)
if err != nil {
return "", err
}
if found {
return filepath.Join(dir, name), nil
}
parent := filepath.Dir(dir)
if parent == dir {
return "", nil
}
dir = parent
}
}