Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# FOSSA CLI Changelog

## Unreleased

- nodejs: Bun now labels transitive devDependencies via graph hydration, so they are filtered from scan results. ([#1737](https://github.com/fossas/fossa-cli/pull/1737))

## 3.17.15

- Node: Workspaces declared with a leading `./` (for example `./packages/*`) are now matched, so their members are analyzed and their production dependencies are no longer dropped from the results. ([#1733](https://github.com/fossas/fossa-cli/pull/1733))
Expand Down
25 changes: 12 additions & 13 deletions src/Strategy/Node/Bun/BunLock.hs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import DepTypes (
DepType (GitType, NodeJSType),
Dependency (..),
VerConstraint (CEq),
hydrateDepEnvs,
insertEnvironment,
)
import Effect.Grapher (LabeledGrapher, deep, direct, edge, label, run, withLabeling)
Expand Down Expand Up @@ -198,30 +199,31 @@ analyze file = do
-- | Build a dependency graph from a parsed bun lockfile.
--
-- Strategy:
-- 1. Collect all dev dependency names across all workspaces.
-- 2. For each workspace, mark its declared dependencies as direct
-- and label them with their environment.
-- 3. For each supported package (npm, git), add it as a deep dependency,
-- label it with its inferred environment, and create edges to its
-- transitive dependencies.
-- 1. For each workspace, mark its declared dependencies as direct
-- and label them with their environment (prod\/optional →
-- 'EnvProduction', dev → 'EnvDevelopment').
-- 2. For each supported package (npm, git), add it as a deep dependency
-- and create edges to its transitive dependencies. Non-direct nodes
-- carry no environment label of their own.
-- Unsupported types (workspace, file, link, root, module) are excluded.
-- 3. Run 'hydrateDepEnvs' so the environments of the direct roots
-- propagate to their transitive successors. A dep reachable only via
-- dev deps becomes 'EnvDevelopment' (so it is filtered), while a dep
-- also reachable from a prod dep keeps 'EnvProduction'.
--
-- 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
buildGraph lockfile = hydrateDepEnvs . run . withLabeling vertexToDependency $ do
for_ allWorkspaces $ \workspace -> do
markDirectDeps EnvProduction workspace.wsDependencies
markDirectDeps EnvDevelopment workspace.wsDevDependencies
markDirectDeps EnvProduction workspace.wsOptionalDependencies

for_ (packages lockfile) $ \pkg ->
for_ (toVertex pkg) $ \parentVertex -> do
let (name, _) = parseResolution (pkgResolution pkg)
inferredEnv = if Set.member name devDepNames then EnvDevelopment else EnvProduction
deep parentVertex
label parentVertex (BunDepEnv inferredEnv)
for_ (transitiveDepNames pkg) $ \childName ->
case Map.lookup childName (packages lockfile) of
Nothing -> pure ()
Expand All @@ -232,9 +234,6 @@ buildGraph lockfile = run . withLabeling vertexToDependency $ do
allWorkspaces :: [BunWorkspace]
allWorkspaces = Map.elems $ workspaces lockfile

devDepNames :: Set.Set PackageName
devDepNames = Set.fromList $ concatMap (Map.keys . wsDevDependencies) allWorkspaces

markDirectDeps :: (Has (LabeledGrapher BunDepVertex BunDepLabel) sig m) => DepEnvironment -> Map PackageName VersionConstraint -> m ()
markDirectDeps env deps =
for_ (Map.keys deps) $ \depName ->
Expand Down
27 changes: 25 additions & 2 deletions test/Bun/BunLockSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ spec = do
let bunProjectPath = testdata </> $(mkRelFile "bun-project/bun.lock")
let gitDepsPath = testdata </> $(mkRelFile "git-deps/bun.lock")
let mixedEnvsPath = testdata </> $(mkRelFile "mixed-envs/bun.lock")
let transitiveDevPath = testdata </> $(mkRelFile "transitive-dev/bun.lock")

parseResolutionSpec
jsoncSpec jsoncPath
Expand All @@ -53,6 +54,7 @@ spec = do
bunProjectSpec bunProjectPath
gitDepsSpec gitDepsPath
mixedEnvsSpec mixedEnvsPath
transitiveDevSpec transitiveDevPath

parseResolutionSpec :: Spec
parseResolutionSpec = describe "parseResolution" $ do
Expand Down Expand Up @@ -119,8 +121,10 @@ dependenciesSpec path =
expectEdge graph (mkProdDep "express" "4.18.2") (mkProdDep "accepts" "1.3.8")
expectEdge graph (mkProdDep "accepts" "1.3.8") (mkProdDep "mime-types" "2.1.35")

it "labels transitive deps of dev deps as production" $ do
expectEdge graph (mkDevDep "typescript" "5.3.3") (mkProdDep "semver" "7.6.0")
it "labels transitive deps of dev deps as development" $ do
-- semver is reachable only via typescript (a dev dependency), so
-- hydration propagates EnvDevelopment down to it (ANE-2836).
expectEdge graph (mkDevDep "typescript" "5.3.3") (mkDevDep "semver" "7.6.0")

-- | Workspaces: multiple workspaces, workspace refs, and workspace
-- package filtering from the final graph.
Expand Down Expand Up @@ -230,6 +234,25 @@ mixedEnvsSpec path =
let lodashVertices = filter (\d -> dependencyName d == "lodash") (Graphing.vertexList graph)
length lodashVertices `shouldBe` 1

-- | Transitive dev deps: a devDependency pulls in a transitive package that
-- is not declared as a direct dependency anywhere. Graph hydration must
-- propagate the dev environment down to it so it is filtered out, while a
-- transitive dep of a prod dependency stays production. A dep reachable from
-- both a prod and a dev root keeps production (ANE-2836).
transitiveDevSpec :: Path Abs File -> Spec
transitiveDevSpec path =
describe "transitive-dev" $ do
describe "graph" $ do
checkGraph path $ \graph -> do
it "labels a transitive dep of a dev dep as development (ANE-2836)" $
Graphing.vertexList graph `shouldContainDep` mkDevDep "dev-transitive" "1.0.0"

it "keeps a transitive dep of a prod dep as production" $
Graphing.vertexList graph `shouldContainDep` mkProdDep "prod-transitive" "1.0.0"

it "keeps a dep reachable from both prod and dev roots as production" $
Graphing.vertexList graph `shouldContainDep` mkBothEnvsDep "shared" "1.0.0"

-- | 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
Expand Down
21 changes: 21 additions & 0 deletions test/Bun/testdata/transitive-dev/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading