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
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Pnpm: `optionalDependencies` are now read from the lockfile. A project's own optional dependencies are reported as direct dependencies instead of transitive ones, and a package's optional dependencies are connected to it in the graph instead of appearing as unrelated transitive dependencies. Platform packages such as `fsevents`, `sharp`'s `@img/*` libraries, and the `@esbuild/*` binaries are the usual cases. ([#1766](https://github.com/fossas/fossa-cli/pull/1766))
- Diagnostics: When an error or warning group contains multiple errors, each error's `Traceback:` header is now printed on its own line instead of being glued onto the last line of the preceding error message (e.g. `...none passed validationTraceback:`). ([#1758](https://github.com/fossas/fossa-cli/pull/1758))

## 3.18.2
Expand Down
5 changes: 4 additions & 1 deletion src/Strategy/Node/Pnpm/PnpmLock.hs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ buildGraphCore BuildGraphConfig{bgcGetPkgNameVersion, bgcMkPkgKey, bgcToEnv, bgc
run . withLabeling applyLabels $ do
-- Direct dependencies from each importer (workspace package).
for_ (toList (lockfileImporters base)) $ \(_, projectImporters) -> do
for_ (Map.toList $ directDependencies projectImporters) $ \(depName, ProjectMapDepMetadata depVersion) ->
-- Optional dependencies are production dependencies an install may
-- skip on a platform that cannot use them; see 'ProjectMap'.
let prodDependencies = Map.toList (directDependencies projectImporters) <> Map.toList (directOptionalDependencies projectImporters)
for_ prodDependencies $ \(depName, ProjectMapDepMetadata depVersion) ->
let resolvedVersion = resolveCatalogVersion catalogs depName depVersion
in for_ (toResolvedDependency toEnv pkgs mkPkgKey depName resolvedVersion) $ \dep -> do
direct dep
Expand Down
18 changes: 15 additions & 3 deletions src/Strategy/Node/Pnpm/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ instance FromJSON PnpmLockFileSnapshots where
let readTransitiveDepPairs = withObject "Parse dependencies" $
\ds -> do
deps <- ds .:? "dependencies" .!= mempty
pure . HashMap.toList $ deps
optionalDeps <- ds .:? "optionalDependencies" .!= mempty
pure . HashMap.toList $ deps <> optionalDeps
snapshots <- traverse readTransitiveDepPairs o

-- Remove the peer dependency suffix. It's present in the snapshot entry, but it's not present in packages
Expand All @@ -240,9 +241,16 @@ instance FromJSON PnpmLockFileSnapshots where
-- Project map
--

-- | The direct dependencies of one importer (workspace package).
--
-- pnpm lists optional dependencies separately only so that an install can skip
-- the ones its platform cannot use; for analysis they are direct dependencies
-- like any other, and leaving them out of the graph is what made platform
-- packages such as fsevents appear as unrelated transitive dependencies.
data ProjectMap = ProjectMap
{ directDependencies :: Map Text ProjectMapDepMetadata
, directDevDependencies :: Map Text ProjectMapDepMetadata
, directOptionalDependencies :: Map Text ProjectMapDepMetadata
}
deriving (Show, Eq, Ord)

Expand All @@ -251,6 +259,7 @@ instance FromJSON ProjectMap where
ProjectMap
<$> obj .:? "dependencies" .!= mempty
<*> obj .:? "devDependencies" .!= mempty
<*> obj .:? "optionalDependencies" .!= mempty

newtype ProjectMapDepMetadata = ProjectMapDepMetadata
{ version :: Text
Expand Down Expand Up @@ -283,7 +292,9 @@ instance FromJSON PackageData where
<$> (obj .:? "dev" .!= False)
<*> obj .:? "name"
<*> obj .: "resolution"
<*> (obj .:? "dependencies" .!= mempty)
-- A package's optional dependencies are edges like any other; see
-- 'ProjectMap'.
<*> ((<>) <$> (obj .:? "dependencies" .!= mempty) <*> (obj .:? "optionalDependencies" .!= mempty))
<*> (obj .:? "peerDependencies" .!= mempty)

data Resolution
Expand Down Expand Up @@ -362,7 +373,8 @@ parseBaseLockfile (TextLike rawVer) obj = do
packages <- obj .:? "packages" .!= mempty
dependencies <- obj .:? "dependencies" .!= mempty
devDependencies <- obj .:? "devDependencies" .!= mempty
let virtualRootWs = ProjectMap dependencies devDependencies
optionalDependencies <- obj .:? "optionalDependencies" .!= mempty
let virtualRootWs = ProjectMap dependencies devDependencies optionalDependencies
let refinedImporters =
if Map.null importers
then Map.insert "." virtualRootWs importers
Expand Down
21 changes: 21 additions & 0 deletions test/Pnpm/PnpmLockSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
mkBothEnvDep :: Text -> Dependency
mkBothEnvDep nameAtVersion = do
let nameAndVersionSplit = Text.splitOn "@" nameAtVersion
name = head nameAndVersionSplit

Check warning on line 38 in test/Pnpm/PnpmLockSpec.hs

View workflow job for this annotation

GitHub Actions / macOS-arm64-build

In the use of ‘head’

Check warning on line 38 in test/Pnpm/PnpmLockSpec.hs

View workflow job for this annotation

GitHub Actions / Windows-build

In the use of ‘head’

Check warning on line 38 in test/Pnpm/PnpmLockSpec.hs

View workflow job for this annotation

GitHub Actions / macOS-intel-build

In the use of ‘head’
version = last nameAndVersionSplit
Dependency
NodeJSType
Expand All @@ -48,7 +48,7 @@
mkDep :: Text -> Maybe DepEnvironment -> Dependency
mkDep nameAtVersion env = do
let nameAndVersionSplit = Text.splitOn "@" nameAtVersion
name = head nameAndVersionSplit

Check warning on line 51 in test/Pnpm/PnpmLockSpec.hs

View workflow job for this annotation

GitHub Actions / macOS-arm64-build

In the use of ‘head’

Check warning on line 51 in test/Pnpm/PnpmLockSpec.hs

View workflow job for this annotation

GitHub Actions / Windows-build

In the use of ‘head’

Check warning on line 51 in test/Pnpm/PnpmLockSpec.hs

View workflow job for this annotation

GitHub Actions / macOS-intel-build

In the use of ‘head’
version = last nameAndVersionSplit
Dependency
NodeJSType
Expand Down Expand Up @@ -143,6 +143,27 @@
describe "works with pnpm v11 multi-document lockfile" $
checkGraph pnpmLockV11MultiDoc pnpmLockV9LocalDepSpec

-- Both fixtures declare sharp as an optional dependency of the project and
-- fsevents as an optional dependency of chokidar.
let pnpmLockV9Optional = currentDir </> $(mkRelFile "test/Pnpm/testdata/pnpm-9-optional-deps/pnpm-lock.yaml")
let pnpmLockV6Optional = currentDir </> $(mkRelFile "test/Pnpm/testdata/pnpm-lock-v6-optional.yaml")

describe "optional dependencies in a v9 lockfile" $
checkGraph pnpmLockV9Optional optionalDepsSpec

describe "optional dependencies in a v6 lockfile" $
checkGraph pnpmLockV6Optional optionalDepsSpec

optionalDepsSpec :: Graphing Dependency -> Spec
optionalDepsSpec graph = do
it "should report the project's own optional dependencies as direct" $
expectDirect [mkProdDep "chokidar@3.6.0", mkProdDep "sharp@0.33.0"] graph

it "should connect a package's optional dependencies to it" $ do
expectEdge graph (mkProdDep "chokidar@3.6.0") (mkProdDep "readdirp@3.6.0")
expectEdge graph (mkProdDep "chokidar@3.6.0") (mkProdDep "fsevents@2.3.3")
expectDep (mkProdDep "fsevents@2.3.3") graph

pnpmLockGraphSpec :: Graphing Dependency -> Spec
pnpmLockGraphSpec graph = do
let hasEdge :: Dependency -> Dependency -> Expectation
Expand Down
56 changes: 56 additions & 0 deletions test/Pnpm/testdata/pnpm-9-optional-deps/pnpm-lock.yaml

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

46 changes: 46 additions & 0 deletions test/Pnpm/testdata/pnpm-lock-v6-optional.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
lockfileVersion: '6.0'

# Optional dependencies in the v6 format: the root importer declares sharp as
# an optionalDependency, and chokidar's package entry lists fsevents under
# optionalDependencies.

dependencies:
chokidar:
specifier: ^3.6.0
version: 3.6.0

optionalDependencies:
sharp:
specifier: ^0.33.0
version: 0.33.0

packages:

/chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQXDda2XQ==}
engines: {node: '>= 8.10.0'}
dependencies:
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
dev: false

/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: false
optional: true

/readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
dev: false

/sharp@0.33.0:
resolution: {integrity: sha512-99qq0YpfgZUSaJ3wF0LDvDSg2fQVBH2PCLZIpimlzeHVAY4/2FuUJp8mCOEAiEnfy/zX9NHUyF7T4FblWQ/n9w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
requiresBuild: true
dev: false
optional: true
Loading