github.com/pulumi/pulumi/sdk/v3@v3.108.1/nodejs/npm/workspaces.go (about) 1 // Copyright 2016-2024, Pulumi Corporation. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package npm 15 16 import ( 17 "encoding/json" 18 "errors" 19 "fmt" 20 "os" 21 "path/filepath" 22 "slices" 23 ) 24 25 var ErrNotInWorkspace = errors.New("not in a workspace") 26 27 // FindWorkspaceRoot determines if we are in a yarn/npm workspace setup and 28 // returns the root directory of the workspace. If the programDirectory is 29 // not in a workspace, it returns ErrNotInWorkspace. 30 func FindWorkspaceRoot(programDirectory string) (string, error) { 31 currentDir := filepath.Dir(programDirectory) 32 nextDir := filepath.Dir(currentDir) 33 for currentDir != nextDir { // We're at the root when the nextDir is the same as the currentDir. 34 p := filepath.Join(currentDir, "package.json") 35 _, err := os.Stat(p) 36 if err != nil { 37 if os.IsNotExist(err) { 38 // No package.json in this directory, continue the search in the next directory up. 39 currentDir = nextDir 40 nextDir = filepath.Dir(currentDir) 41 continue 42 } 43 return "", err 44 } 45 workspaces, err := parseWorkspaces(p) 46 if err != nil { 47 return "", fmt.Errorf("failed to parse workspaces from %s: %w", p, err) 48 } 49 for _, workspace := range workspaces { 50 // See if any of the workspace glob results is the programDirectory. 51 paths, err := filepath.Glob(filepath.Join(currentDir, workspace, "package.json")) 52 if err != nil { 53 return "", err 54 } 55 if paths != nil && slices.Contains(paths, filepath.Join(programDirectory, "package.json")) { 56 return currentDir, nil 57 } 58 } 59 // None of the workspace globs matched the program directory, so we're 60 // in the slightly weird situation where a parent directory has a 61 // package.json with workspaces set up, but the program directory is 62 // not part of this. 63 return "", ErrNotInWorkspace 64 } 65 return "", ErrNotInWorkspace 66 } 67 68 // parseWorkspaces reads a package.json file and returns the list of workspaces. 69 // This supports the simple format for npm and yarn: 70 // 71 // { 72 // "workspaces": ["workspace-a", "workspace-b"] 73 // } 74 // 75 // As well as the extended format for yarn: 76 // 77 // { 78 // "workspaces": { 79 // "packages": ["packages/*"], 80 // "nohoist": ["**/react-native", "**/react-native/**"] 81 // } 82 // } 83 func parseWorkspaces(p string) ([]string, error) { 84 pkgContents, err := os.ReadFile(p) 85 if err != nil { 86 return []string{}, err 87 } 88 pkg := struct { 89 Workspaces []string `json:"workspaces"` 90 }{} 91 err = json.Unmarshal(pkgContents, &pkg) 92 if err == nil { 93 return pkg.Workspaces, nil 94 } 95 // Failed to parse the simple format, try to parse extended yarn workspaces format 96 pkgExtended := struct { 97 Workspaces struct { 98 Packages []string `json:"packages"` 99 } `json:"workspaces"` 100 }{} 101 err = json.Unmarshal(pkgContents, &pkgExtended) 102 if err != nil { 103 return []string{}, err 104 } 105 return pkgExtended.Workspaces.Packages, nil 106 }