From 71ab01595d17f6fc0eb5ce229cb011e43f3f1390 Mon Sep 17 00:00:00 2001 From: Jeremy Gonzalez Date: Fri, 10 Apr 2026 16:21:25 -0600 Subject: [PATCH] Expose pnpm and bun workspace packages as individual build targets Extends workspace build target support to pnpm and bun projects, completing the set for all Node.js lockfile formats (yarn, npm, pnpm, bun). - Adds `resolveImporterPaths` to map package names (build targets) to pnpm lockfile importer paths (e.g. `"."`, `"packages/a"`) - Adds `resolveBunWorkspacePaths` to map package names to bun lockfile workspace keys (uses `""` for root, relative paths for members) - Updates `buildGraph` in `PnpmLock.hs` and `BunLock.hs` to accept an optional filter, scoping analysis to selected workspace members - Preserves the existing v9 environment propagation logic in `PnpmLock.buildGraph`, with filtering applied to the importer list Co-Authored-By: Claude Opus 4.6 (1M context) --- Changelog.md | 2 +- .../strategies/languages/nodejs/bun.md | 6 ++ .../strategies/languages/nodejs/pnpm.md | 7 +- src/Strategy/Node.hs | 84 +++++++++++++++++-- src/Strategy/Node/Bun/BunLock.hs | 20 +++-- src/Strategy/Node/Pnpm/PnpmLock.hs | 22 +++-- test/Bun/BunLockSpec.hs | 35 +++++++- test/Node/NodeSpec.hs | 54 +++++++++++- test/Pnpm/PnpmLockSpec.hs | 40 ++++++++- 9 files changed, 243 insertions(+), 27 deletions(-) diff --git a/Changelog.md b/Changelog.md index b4133a5a3..f5c9e57e8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,7 +2,7 @@ ## Unreleased -- Node.js: Yarn and npm workspace packages now appear as individual build targets (e.g. `yarn@./:my-package`, `npm@./:my-package`), enabling per-package dependency scoping via `.fossa.yml`. +- Node.js: Yarn, npm, pnpm, and bun workspace packages now appear as individual build targets (e.g. `yarn@./:my-package`, `npm@./:my-package`, `pnpm@./:my-package`, `bun@./:my-package`), enabling per-package dependency scoping via `.fossa.yml`. - Project edit: Fix 500 error when running `fossa project edit --policy` on existing projects ([#1688](https://github.com/fossas/fossa-cli/pull/1688)) ## 3.17.0 diff --git a/docs/references/strategies/languages/nodejs/bun.md b/docs/references/strategies/languages/nodejs/bun.md index ca6a4571b..76962eea1 100644 --- a/docs/references/strategies/languages/nodejs/bun.md +++ b/docs/references/strategies/languages/nodejs/bun.md @@ -39,6 +39,12 @@ Workspace entries are keyed by their relative path from the root, with `""` representing the root workspace. Each workspace declares its own `dependencies`, `devDependencies`, and `optionalDependencies`. +Each workspace package (including the root) is exposed as an individual build +target (e.g. `bun@./:my-app`, `bun@./:lib-utils`). When a subset of targets +is selected via `.fossa.yml`, only those packages' dependencies are included +in the analysis. When no filtering is applied, all targets are selected and +all dependencies from every workspace package are included. + ### Packages Package keys use a slash-delimited path for nested `node_modules`: diff --git a/docs/references/strategies/languages/nodejs/pnpm.md b/docs/references/strategies/languages/nodejs/pnpm.md index 30a4a099d..c2f2e7e43 100644 --- a/docs/references/strategies/languages/nodejs/pnpm.md +++ b/docs/references/strategies/languages/nodejs/pnpm.md @@ -152,7 +152,12 @@ CLI will infer the package name and version using `/${dependencyName}/${dependen ``` * Peer dependencies will be included in the analysis (they are treated like any other dependency). -* Pnpm workspaces are supported. +* Pnpm workspaces are supported. Each workspace package (including the root) + is exposed as an individual build target (e.g. `pnpm@./:my-app`, + `pnpm@./:lib-utils`). When a subset of targets is selected, only those + packages' dependencies are included in the analysis. When no filtering is + applied, all targets are selected and all dependencies from every workspace + package are included in the analysis. * Development dependencies (`dev: true`) are ignored by default from analysis. To include them in the analysis, execute CLI with `--include-unused` flag e.g. `fossa analyze --include-unused`. * Optional dependencies are included in the analysis by default. They can be ignored in FOSSA UI. * `fossa-cli` supports lockFileVersion: 4.x, 5.x, 6.x, 7.x, 8.x, and 9.x. diff --git a/src/Strategy/Node.hs b/src/Strategy/Node.hs index 17cf7ab16..8acf8f49a 100644 --- a/src/Strategy/Node.hs +++ b/src/Strategy/Node.hs @@ -9,6 +9,8 @@ module Strategy.Node ( getDeps, findWorkspaceBuildTargets, extractDepListsForTargets, + resolveImporterPaths, + resolveBunWorkspacePaths, ) where import Algebra.Graph.AdjacencyMap qualified as AM @@ -39,7 +41,7 @@ import Data.Maybe (catMaybes, isJust, mapMaybe) import Data.Set (Set) import Data.Set qualified as Set import Data.Set.NonEmpty qualified as NonEmptySet -import Data.String.Conversion (decodeUtf8, toString) +import Data.String.Conversion (decodeUtf8, toString, toText) import Data.Tagged (applyTag) import Data.Text (Text) import Data.Text qualified as Text @@ -73,6 +75,7 @@ import Path ( Rel, mkRelFile, parent, + stripProperPrefix, toFilePath, (), ) @@ -99,6 +102,7 @@ import Strategy.Node.Pnpm.PnpmLock qualified as PnpmLock import Strategy.Node.Pnpm.Workspace (PnpmWorkspace (workspaceSpecs)) import Strategy.Node.YarnV1.YarnLock qualified as V1 import Strategy.Node.YarnV2.YarnLock qualified as V2 +import System.FilePath.Posix qualified as FP import Types ( BuildTarget (BuildTarget), DependencyResults (DependencyResults), @@ -160,6 +164,8 @@ mkProject project = do projectBuildTargets' = case project of Yarn _ _ -> findWorkspaceBuildTargets graph NPMLock _ _ -> findWorkspaceBuildTargets graph + Pnpm _ _ -> findWorkspaceBuildTargets graph + Bun _ _ -> findWorkspaceBuildTargets graph _ -> ProjectWithoutTargets Manifest rootManifest <- fromEitherShow $ findWorkspaceRootManifest graph pure $ @@ -204,20 +210,80 @@ getDeps :: m DependencyResults getDeps targets (Yarn yarnLockFile graph) = analyzeYarn targets yarnLockFile graph getDeps targets (NPMLock packageLockFile graph) = analyzeNpmLock targets packageLockFile graph -getDeps _ (Pnpm pnpmLockFile _) = analyzePnpmLock pnpmLockFile -getDeps _ (Bun bunLockFile _) = analyzeBunLock bunLockFile +getDeps targets (Pnpm pnpmLockFile graph) = analyzePnpmLock targets pnpmLockFile graph +getDeps targets (Bun bunLockFile graph) = analyzeBunLock targets bunLockFile graph getDeps _ (NPM graph) = analyzeNpm graph -analyzePnpmLock :: (Has Diagnostics sig m, Has ReadFS sig m, Has Logger sig m) => Manifest -> m DependencyResults -analyzePnpmLock (Manifest pnpmLockFile) = do - result <- PnpmLock.analyze pnpmLockFile +analyzePnpmLock :: (Has Diagnostics sig m, Has ReadFS sig m, Has Logger sig m) => FoundTargets -> Manifest -> PkgJsonGraph -> m DependencyResults +analyzePnpmLock targets (Manifest pnpmLockFile) graph = do + let selectedImporterPaths = resolveImporterPaths targets graph + result <- PnpmLock.analyze selectedImporterPaths pnpmLockFile pure $ DependencyResults result Complete [pnpmLockFile] -analyzeBunLock :: (Has Diagnostics sig m, Has ReadFS sig m) => Manifest -> m DependencyResults -analyzeBunLock (Manifest bunLockFile) = do - result <- BunLock.analyze bunLockFile +analyzeBunLock :: (Has Diagnostics sig m, Has ReadFS sig m) => FoundTargets -> Manifest -> PkgJsonGraph -> m DependencyResults +analyzeBunLock targets (Manifest bunLockFile) graph = do + let selectedWorkspacePaths = resolveBunWorkspacePaths targets graph + result <- BunLock.analyze selectedWorkspacePaths bunLockFile pure $ DependencyResults result Complete [bunLockFile] +-- | Map selected build targets (package names) to pnpm importer paths +-- (relative paths from the workspace root like ".", "packages/a"). +-- When 'ProjectWithoutTargets', returns Nothing (no filtering). +-- When 'FoundTargets', looks up each target's package name in the +-- PkgJsonGraph to find its manifest path, then computes the relative +-- path from the workspace root. +resolveImporterPaths :: FoundTargets -> PkgJsonGraph -> Maybe (Set Text) +resolveImporterPaths ProjectWithoutTargets _ = Nothing +resolveImporterPaths (FoundTargets targets) graph@PkgJsonGraph{..} = + case findWorkspaceRootManifest graph of + Left _ -> Nothing + Right (Manifest rootManifest) -> + Just $ Set.fromList [p | (name, p) <- namePathPairs, name `Set.member` targetNames] + where + rootDir = parent rootManifest + targetNames = Set.map unBuildTarget (NonEmptySet.toSet targets) + + namePathPairs :: [(Text, Text)] + namePathPairs = + [ (name, manifestToImporterPath m) + | (Manifest m, pj) <- Map.toList jsonLookup + , Just name <- [packageName pj] + ] + + manifestToImporterPath :: Path Abs File -> Text + manifestToImporterPath m = + let manifestDir = parent m + in if manifestDir == rootDir + then "." + else maybe "." (toText . FP.dropTrailingPathSeparator . toFilePath) (stripProperPrefix rootDir manifestDir) + +-- | Like 'resolveImporterPaths' but for bun lockfiles, which use @""@ +-- for the root workspace instead of @"."@. +resolveBunWorkspacePaths :: FoundTargets -> PkgJsonGraph -> Maybe (Set Text) +resolveBunWorkspacePaths ProjectWithoutTargets _ = Nothing +resolveBunWorkspacePaths (FoundTargets targets) graph@PkgJsonGraph{..} = + case findWorkspaceRootManifest graph of + Left _ -> Nothing + Right (Manifest rootManifest) -> + Just $ Set.fromList [p | (name, p) <- namePathPairs, name `Set.member` targetNames] + where + rootDir = parent rootManifest + targetNames = Set.map unBuildTarget (NonEmptySet.toSet targets) + + namePathPairs :: [(Text, Text)] + namePathPairs = + [ (name, manifestToWorkspacePath m) + | (Manifest m, pj) <- Map.toList jsonLookup + , Just name <- [packageName pj] + ] + + manifestToWorkspacePath :: Path Abs File -> Text + manifestToWorkspacePath m = + let manifestDir = parent m + in if manifestDir == rootDir + then "" + else maybe "" (toText . FP.dropTrailingPathSeparator . toFilePath) (stripProperPrefix rootDir manifestDir) + analyzeNpmLock :: (Has Diagnostics sig m, Has ReadFS sig m) => FoundTargets -> Manifest -> PkgJsonGraph -> m DependencyResults analyzeNpmLock targets (Manifest npmLockFile) graph = do npmLockVersion <- detectNpmLockVersion npmLockFile diff --git a/src/Strategy/Node/Bun/BunLock.hs b/src/Strategy/Node/Bun/BunLock.hs index 8574c6f14..6079daa5a 100644 --- a/src/Strategy/Node/Bun/BunLock.hs +++ b/src/Strategy/Node/Bun/BunLock.hs @@ -29,6 +29,7 @@ import Data.Aeson ( import Data.Foldable (for_) import Data.Map (Map) import Data.Map qualified as Map +import Data.Set (Set) import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as Text @@ -187,13 +188,17 @@ parseResolution res in (name, Text.drop 1 rest) -- | Analyze a bun.lock file and produce a dependency graph. +-- When @selectedWorkspacePaths@ is 'Nothing', all workspaces are included. +-- When @Just paths@, only workspaces whose key is in @paths@ contribute +-- direct dependencies. analyze :: (Has ReadFS sig m, Has Diagnostics sig m) => + Maybe (Set Text) -> Path Abs File -> m (Graphing Dependency) -analyze file = do +analyze selectedWorkspacePaths file = do lockfile <- context "Parsing bun.lock" $ readContentsJsonc file - context "Building dependency graph" $ pure $ buildGraph lockfile + context "Building dependency graph" $ pure $ buildGraph selectedWorkspacePaths lockfile -- | Build a dependency graph from a parsed bun lockfile. -- @@ -209,9 +214,9 @@ analyze file = do -- Uses 'LabeledGrapher' so that vertices are environment-agnostic and -- environments accumulate as labels, avoiding duplicate vertices when -- the same package appears in both prod and dev across workspaces. -buildGraph :: BunLockfile -> Graphing Dependency -buildGraph lockfile = run . withLabeling vertexToDependency $ do - for_ allWorkspaces $ \workspace -> do +buildGraph :: Maybe (Set Text) -> BunLockfile -> Graphing Dependency +buildGraph selectedWorkspacePaths lockfile = run . withLabeling vertexToDependency $ do + for_ filteredWorkspaces $ \workspace -> do markDirectDeps EnvProduction workspace.wsDependencies markDirectDeps EnvDevelopment workspace.wsDevDependencies markDirectDeps EnvProduction workspace.wsOptionalDependencies @@ -232,6 +237,11 @@ buildGraph lockfile = run . withLabeling vertexToDependency $ do allWorkspaces :: [BunWorkspace] allWorkspaces = Map.elems $ workspaces lockfile + filteredWorkspaces :: [BunWorkspace] + filteredWorkspaces = case selectedWorkspacePaths of + Nothing -> allWorkspaces + Just paths -> Map.elems $ Map.filterWithKey (\k _ -> k `Set.member` paths) (workspaces lockfile) + devDepNames :: Set.Set PackageName devDepNames = Set.fromList $ concatMap (Map.keys . wsDevDependencies) allWorkspaces diff --git a/src/Strategy/Node/Pnpm/PnpmLock.hs b/src/Strategy/Node/Pnpm/PnpmLock.hs index 0d43fd184..cf90da3d4 100644 --- a/src/Strategy/Node/Pnpm/PnpmLock.hs +++ b/src/Strategy/Node/Pnpm/PnpmLock.hs @@ -297,23 +297,33 @@ instance FromJSON Resolution where gitRes :: Object -> Parser Resolution gitRes obj = GitResolve <$> (GitResolution <$> obj .: "repo" <*> obj .: "commit") -analyze :: (Has ReadFS sig m, Has Logger sig m, Has Diagnostics sig m) => Path Abs File -> m (Graphing Dependency) -analyze file = context "Analyzing Npm Lockfile (v3)" $ do +-- | Analyze a pnpm lockfile. When @selectedImporterPaths@ is 'Nothing', +-- all importers are included. When @Just paths@, only importers whose +-- key is in @paths@ are treated as direct dependency sources. +analyze :: (Has ReadFS sig m, Has Logger sig m, Has Diagnostics sig m) => Maybe (Set.Set Text) -> Path Abs File -> m (Graphing Dependency) +analyze selectedImporterPaths file = context "Analyzing Npm Lockfile (v3)" $ do pnpmLockFile <- context "Parsing pnpm-lock file" $ readContentsYaml file case lockFileVersion pnpmLockFile of PnpmLockLt4 raw -> logWarn . pretty $ "pnpm-lock file is using older lockFileVersion: " <> raw <> " of, which is not officially supported!" _ -> pure () - context "Building dependency graph" $ pure $ buildGraph pnpmLockFile + context "Building dependency graph" $ pure $ buildGraph selectedImporterPaths pnpmLockFile -- Build the dependency graph, labeling direct deps with their environment -- (prod/dev). hydrateDepEnvs then propagates those environments to all -- transitive successors. -buildGraph :: PnpmLockfile -> Graphing Dependency -buildGraph lockFile = withoutLocalPackages . hydrateDepEnvs $ +-- +-- When @selectedImporterPaths@ is 'Nothing', all importers are included. +-- When @Just paths@, only importers whose key is in @paths@ are treated as +-- direct dependency sources (used for workspace build target filtering). +buildGraph :: Maybe (Set.Set Text) -> PnpmLockfile -> Graphing Dependency +buildGraph selectedImporterPaths lockFile = withoutLocalPackages . hydrateDepEnvs $ run . withLabeling applyLabels $ do - for_ (toList lockFile.importers) $ \(_, projectImporters) -> do + let filteredImporters = case selectedImporterPaths of + Nothing -> toList lockFile.importers + Just paths -> filter (\(k, _) -> k `Set.member` paths) (toList lockFile.importers) + for_ filteredImporters $ \(_, projectImporters) -> do for_ (Map.toList $ directDependencies projectImporters) $ \(depName, ProjectMapDepMetadata depVersion) -> for_ (toResolvedDependency depName depVersion) $ \dep -> do direct dep diff --git a/test/Bun/BunLockSpec.hs b/test/Bun/BunLockSpec.hs index ad2372050..ae176cad8 100644 --- a/test/Bun/BunLockSpec.hs +++ b/test/Bun/BunLockSpec.hs @@ -50,6 +50,7 @@ spec = do jsoncSpec jsoncPath dependenciesSpec depsPath workspacesSpec wsPath + workspaceFilterSpec wsPath bunProjectSpec bunProjectPath gitDepsSpec gitDepsPath mixedEnvsSpec mixedEnvsPath @@ -152,6 +153,33 @@ workspacesSpec path = names `shouldNotContain` "@types/app" names `shouldNotContain` "utils" +-- | Workspace target filtering: verify that filtering by workspace path +-- scopes which workspaces contribute direct dependencies. +workspaceFilterSpec :: Path Abs File -> Spec +workspaceFilterSpec path = + describe "workspace target filtering" $ do + describe "filtered to root only" $ + checkGraphWithFilter (Just (Set.singleton "")) path $ \graph -> do + let directDeps = Graphing.directList graph + + it "should include root dev deps as direct" $ + directDeps `shouldContainDep` mkDevDep "typescript" "5.3.3" + + it "should not include sub-workspace deps as direct" $ do + let directNames = map dependencyName directDeps + directNames `shouldNotContain` "lodash" + + describe "filtered to sub-workspace only" $ + checkGraphWithFilter (Just (Set.singleton "packages/app")) path $ \graph -> do + let directDeps = Graphing.directList graph + + it "should include sub-workspace deps as direct" $ + directDeps `shouldContainDep` mkProdDep "lodash" "4.17.21" + + it "should not include root deps as direct" $ do + let directNames = map dependencyName directDeps + directNames `shouldNotContain` "typescript" + -- | Bun project: a real-world bun.lock from the bun project itself. -- Covers scoped packages, git refs, nested package keys, optional/peer deps -- in packages, and large dependency counts. @@ -232,13 +260,16 @@ mixedEnvsSpec path = -- | Parse a bun.lock in IO for graph tests (outside the effect stack). checkGraph :: Path Abs File -> (Graphing Dependency -> Spec) -> Spec -checkGraph path graphSpec = do +checkGraph = checkGraphWithFilter Nothing + +checkGraphWithFilter :: Maybe (Set.Set Text) -> Path Abs File -> (Graphing Dependency -> Spec) -> Spec +checkGraphWithFilter wsFilter path graphSpec = do result <- runIO $ parseBunLockIO path case result of Left err -> describe (fromAbsFile path) $ it "should parse" (expectationFailure err) - Right lockfile -> graphSpec (buildGraph lockfile) + Right lockfile -> graphSpec (buildGraph wsFilter lockfile) parseBunLockIO :: Path Abs File -> IO (Either String BunLockfile) parseBunLockIO path = do diff --git a/test/Node/NodeSpec.hs b/test/Node/NodeSpec.hs index e1a937067..21ce9d550 100644 --- a/test/Node/NodeSpec.hs +++ b/test/Node/NodeSpec.hs @@ -13,7 +13,7 @@ import Data.Tagged (applyTag) import Graphing qualified import Path (Abs, Dir, Path, mkRelDir, mkRelFile, ()) import Path.IO (getCurrentDir) -import Strategy.Node (NodeProject (NPMLock), discover, extractDepListsForTargets, findWorkspaceBuildTargets, getDeps) +import Strategy.Node (NodeProject (NPMLock), discover, extractDepListsForTargets, findWorkspaceBuildTargets, getDeps, resolveBunWorkspacePaths, resolveImporterPaths) import Strategy.Node.PackageJson ( FlatDeps (..), Manifest (..), @@ -57,6 +57,8 @@ spec = do npmLockAnalysisSpec currDir workspaceBuildTargetsSpec currDir extractDepListsForTargetsSpec currDir + resolveImporterPathsSpec currDir + resolveBunWorkspacePathsSpec currDir discoveredWorkSpaceProj :: Path Abs Dir -> DiscoveredProject NodeProject discoveredWorkSpaceProj currDir = @@ -238,6 +240,56 @@ extractDepListsForTargetsSpec currDir = describe "extractDepListsForTargets" $ d ] directDeps result `shouldBe` applyTag @Production expectedDirect +resolveImporterPathsSpec :: Path Abs Dir -> Spec +resolveImporterPathsSpec currDir = describe "resolveImporterPaths" $ do + let graph = workspaceGraphWithDeps currDir + + it "returns Nothing for ProjectWithoutTargets" $ do + resolveImporterPaths ProjectWithoutTargets graph `shouldBe` Nothing + + it "maps root target to \".\"" $ do + let targets = + maybe ProjectWithoutTargets FoundTargets . nonEmpty $ + Set.fromList [BuildTarget "workspace-test"] + resolveImporterPaths targets graph `shouldBe` Just (Set.singleton ".") + + it "maps workspace member targets to relative paths" $ do + let targets = + maybe ProjectWithoutTargets FoundTargets . nonEmpty $ + Set.fromList [BuildTarget "pkg-a", BuildTarget "pkg-b"] + resolveImporterPaths targets graph `shouldBe` Just (Set.fromList ["pkg-a", "nested/pkg-b"]) + + it "maps all targets including root" $ do + let targets = + maybe ProjectWithoutTargets FoundTargets . nonEmpty $ + Set.fromList [BuildTarget "workspace-test", BuildTarget "pkg-a", BuildTarget "pkg-b"] + resolveImporterPaths targets graph `shouldBe` Just (Set.fromList [".", "pkg-a", "nested/pkg-b"]) + +resolveBunWorkspacePathsSpec :: Path Abs Dir -> Spec +resolveBunWorkspacePathsSpec currDir = describe "resolveBunWorkspacePaths" $ do + let graph = workspaceGraphWithDeps currDir + + it "returns Nothing for ProjectWithoutTargets" $ do + resolveBunWorkspacePaths ProjectWithoutTargets graph `shouldBe` Nothing + + it "maps root target to empty string" $ do + let targets = + maybe ProjectWithoutTargets FoundTargets . nonEmpty $ + Set.fromList [BuildTarget "workspace-test"] + resolveBunWorkspacePaths targets graph `shouldBe` Just (Set.singleton "") + + it "maps workspace member targets to relative paths" $ do + let targets = + maybe ProjectWithoutTargets FoundTargets . nonEmpty $ + Set.fromList [BuildTarget "pkg-a", BuildTarget "pkg-b"] + resolveBunWorkspacePaths targets graph `shouldBe` Just (Set.fromList ["pkg-a", "nested/pkg-b"]) + + it "maps all targets including root" $ do + let targets = + maybe ProjectWithoutTargets FoundTargets . nonEmpty $ + Set.fromList [BuildTarget "workspace-test", BuildTarget "pkg-a", BuildTarget "pkg-b"] + resolveBunWorkspacePaths targets graph `shouldBe` Just (Set.fromList ["", "pkg-a", "nested/pkg-b"]) + -- | A workspace graph with actual dependencies for testing extractDepListsForTargets. workspaceGraphWithDeps :: Path Abs Dir -> PkgJsonGraph workspaceGraphWithDeps currDir = diff --git a/test/Pnpm/PnpmLockSpec.hs b/test/Pnpm/PnpmLockSpec.hs index 523a81856..a07cb618f 100644 --- a/test/Pnpm/PnpmLockSpec.hs +++ b/test/Pnpm/PnpmLockSpec.hs @@ -89,10 +89,13 @@ lodash = mempty checkGraph :: Path Abs File -> (Graphing Dependency -> Spec) -> Spec -checkGraph pathToFixture buildGraphSpec = do +checkGraph = checkGraphWithFilter Nothing + +checkGraphWithFilter :: Maybe (Set.Set Text) -> Path Abs File -> (Graphing Dependency -> Spec) -> Spec +checkGraphWithFilter importerFilter pathToFixture buildGraphSpec = do eitherDecodedLockFile <- runIO $ decodeFileEither (toString pathToFixture) case eitherDecodedLockFile of - Right pnpmLock -> buildGraphSpec (buildGraph pnpmLock) + Right pnpmLock -> buildGraphSpec (buildGraph importerFilter pnpmLock) Left err -> describe "pnpm-lock" $ it "should parse lockfile" (expectationFailure $ prettyPrintParseException err) @@ -106,6 +109,11 @@ spec = do checkGraph pnpmLockPath pnpmLockGraphSpec checkGraph pnpmLockWithoutWorkspacePath pnpmLockGraphWithoutWorkspaceSpec + -- v5 workspace target filtering + describe "workspace target filtering" $ do + checkGraphWithFilter (Just (Set.singleton ".")) pnpmLockPath pnpmLockFilteredRootOnlySpec + checkGraphWithFilter (Just (Set.singleton "packages/a")) pnpmLockPath pnpmLockFilteredWorkspaceOnlySpec + -- v6 format let pnpmLockV6 = currentDir $(mkRelFile "test/Pnpm/testdata/pnpm-lock-v6.yaml") let pnpmLockV6WithWorkspace = currentDir $(mkRelFile "test/Pnpm/testdata/pnpm-lock-v6-workspace.yaml") @@ -487,3 +495,31 @@ pnpmLockV9MultiVersionSpec graph = do expectDep (mkProdDep "sax@1.2.1") graph -- sax@1.4.4 is dev-only (from app-b) expectDep (mkDevDep "sax@1.4.4") graph + +-- | When filtered to root importer only ("."), workspace package deps +-- (aws-sdk@1.0.0, commander@9.2.0) should not appear as direct. +pnpmLockFilteredRootOnlySpec :: Graphing Dependency -> Spec +pnpmLockFilteredRootOnlySpec graph = do + describe "filtered to root importer only" $ do + it "should only include root direct deps" $ do + expectDirect + [ mkProdDep "aws-sdk@2.1148.0" + , colors + , lodash + , mkDevDep "react@18.1.0" + , mkProdDep "glob@8.0.3" + , mkProdDep "chokidar@1.0.0" + ] + graph + +-- | When filtered to workspace importer only ("packages/a"), root deps +-- (aws-sdk@2.1148.0, colors, lodash, react) should not appear as direct. +pnpmLockFilteredWorkspaceOnlySpec :: Graphing Dependency -> Spec +pnpmLockFilteredWorkspaceOnlySpec graph = do + describe "filtered to workspace importer only" $ do + it "should only include workspace-a direct deps" $ do + expectDirect + [ mkProdDep "aws-sdk@1.0.0" + , mkProdDep "commander@9.2.0" + ] + graph