aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/packagemanager/infer_root.go
diff options
context:
space:
mode:
Diffstat (limited to 'cli/internal/packagemanager/infer_root.go')
-rw-r--r--cli/internal/packagemanager/infer_root.go146
1 files changed, 146 insertions, 0 deletions
diff --git a/cli/internal/packagemanager/infer_root.go b/cli/internal/packagemanager/infer_root.go
new file mode 100644
index 0000000..7920f12
--- /dev/null
+++ b/cli/internal/packagemanager/infer_root.go
@@ -0,0 +1,146 @@
+package packagemanager
+
+import (
+ "path/filepath"
+
+ "github.com/vercel/turbo/cli/internal/doublestar"
+ "github.com/vercel/turbo/cli/internal/turbopath"
+)
+
+// PackageType represents the mode in which turbo is running.
+type PackageType string
+
+const (
+ // Single is for single-package mode.
+ Single PackageType = "single"
+ // Multi is for monorepo mode.
+ Multi PackageType = "multi"
+)
+
+func candidateDirectoryWorkspaceGlobs(directory turbopath.AbsoluteSystemPath) []string {
+ packageManagers := []PackageManager{
+ nodejsNpm,
+ nodejsPnpm,
+ }
+
+ for _, pm := range packageManagers {
+ globs, err := pm.getWorkspaceGlobs(directory)
+ if err != nil {
+ // Try the other package manager workspace formats.
+ continue
+ }
+
+ return globs
+ }
+
+ return nil
+}
+
+func isOneOfTheWorkspaces(globs []string, nearestPackageJSONDir turbopath.AbsoluteSystemPath, currentPackageJSONDir turbopath.AbsoluteSystemPath) bool {
+ for _, glob := range globs {
+ globpattern := currentPackageJSONDir.UntypedJoin(filepath.FromSlash(glob)).ToString()
+ match, _ := doublestar.PathMatch(globpattern, nearestPackageJSONDir.ToString())
+ if match {
+ return true
+ }
+ }
+
+ return false
+}
+
+// InferRoot identifies which directory we should treat as the root, and which mode
+// turbo should be in when operating at that directory.
+func InferRoot(directory turbopath.AbsoluteSystemPath) (turbopath.AbsoluteSystemPath, PackageType) {
+ // Go doesn't have iterators, so this is very not-elegant.
+
+ // Scenarios:
+ // 0. Has a turbo.json but doesn't have a peer package.json. directory + multi
+ // 1. Nearest turbo.json, check peer package.json/pnpm-workspace.yaml.
+ // A. Has workspaces, multi package mode.
+ // B. No workspaces, single package mode.
+ // 2. If no turbo.json find the closest package.json parent.
+ // A. No parent package.json, default to current behavior.
+ // B. Nearest package.json defines workspaces. Can't be in single-package mode, so we bail. (This could be changed in the future.)
+ // 3. Closest package.json does not define workspaces. Traverse toward the root looking for package.jsons.
+ // A. No parent package.json with workspaces. nearestPackageJson + single
+ // B. Stop at the first one that has workspaces.
+ // i. If we are one of the workspaces, directory + multi. (This could be changed in the future.)
+ // ii. If we're not one of the workspaces, nearestPackageJson + single.
+
+ nearestTurboJSON, findTurboJSONErr := directory.Findup("turbo.json")
+ if nearestTurboJSON == "" || findTurboJSONErr != nil {
+ // We didn't find a turbo.json. We're in situation 2 or 3.
+
+ // Unroll the first loop for Scenario 2
+ nearestPackageJSON, nearestPackageJSONErr := directory.Findup("package.json")
+
+ // If we fail to find any package.json files we aren't in single package mode.
+ // We let things go through our existing failure paths.
+ // Scenario 2A.
+ if nearestPackageJSON == "" || nearestPackageJSONErr != nil {
+ return directory, Multi
+ }
+
+ // If we find a package.json which has workspaces we aren't in single package mode.
+ // We let things go through our existing failure paths.
+ // Scenario 2B.
+ if candidateDirectoryWorkspaceGlobs(nearestPackageJSON.Dir()) != nil {
+ // In a future world we could maybe change this behavior.
+ // return nearestPackageJson.Dir(), Multi
+ return directory, Multi
+ }
+
+ // Scenario 3.
+ // Find the nearest package.json that has workspaces.
+ // If found _and_ the nearestPackageJson is one of the workspaces, thatPackageJson + multi.
+ // Else, nearestPackageJson + single
+ cursor := nearestPackageJSON.Dir().UntypedJoin("..")
+ for {
+ nextPackageJSON, nextPackageJSONErr := cursor.Findup("package.json")
+ if nextPackageJSON == "" || nextPackageJSONErr != nil {
+ // We haven't found a parent defining workspaces.
+ // So we're single package mode at nearestPackageJson.
+ // Scenario 3A.
+ return nearestPackageJSON.Dir(), Single
+ }
+
+ // Found a package.json file, see if it has workspaces.
+ // Workspaces are not allowed to be recursive, so we know what to
+ // return the moment we find something with workspaces.
+ globs := candidateDirectoryWorkspaceGlobs(nextPackageJSON.Dir())
+ if globs != nil {
+ if isOneOfTheWorkspaces(globs, nearestPackageJSON.Dir(), nextPackageJSON.Dir()) {
+ // If it has workspaces, and nearestPackageJson is one of them, we're multi.
+ // We don't infer in this scenario.
+ // Scenario 3BI.
+ // TODO: return nextPackageJson.Dir(), Multi
+ return directory, Multi
+ }
+
+ // We found a parent with workspaces, but we're not one of them.
+ // We choose to operate in single package mode.
+ // Scenario 3BII
+ return nearestPackageJSON.Dir(), Single
+ }
+
+ // Loop around and see if we have another parent.
+ cursor = nextPackageJSON.Dir().UntypedJoin("..")
+ }
+ } else {
+ // If there is no sibling package.json we do no inference.
+ siblingPackageJSONPath := nearestTurboJSON.Dir().UntypedJoin("package.json")
+ if !siblingPackageJSONPath.Exists() {
+ // We do no inference.
+ // Scenario 0
+ return directory, Multi
+ }
+
+ if candidateDirectoryWorkspaceGlobs(nearestTurboJSON.Dir()) != nil {
+ // Scenario 1A.
+ return nearestTurboJSON.Dir(), Multi
+ }
+
+ // Scenario 1B.
+ return nearestTurboJSON.Dir(), Single
+ }
+}