Skip to content

Commit 78010ff

Browse files
csasarakryanlinkspatten
authored
Pnpm 9 (#1561)
* Basic support for pnpm 9 lockfiles. * fix nonexhaustive patterns. * fix: improve pnpm v9 lockfile parsing 1. Handle package keys without leading slash in v9 format 2. Add support for catalog versions (workspace:*) in v9 format 3. Improve version normalization and handling 4. Add more test cases and documentation * [ANE-2235] Add support for PNPM v9 catalog versions - Add catalogs field to PnpmLockfile type - Create CatalogMap and CatalogEntry types for parsing catalog data - Add getPackageVersion helper to resolve catalog versions - Update version parsing to properly handle v7-v9 lockfiles * [ANE-2235] Fix dev dependency handling in PNPM v9 lockfiles - Track dev dependencies from importers section - Consider both importers and packages sections when determining dev status - Pass dev dependency status through dependency resolution chain - Update toDependency to combine isDev flags from both sources * [ANE-2235] Fix name collision with version field - Rename version field in CatalogEntry to catalogVersion to avoid collision with ProjectMapDepMetadata * [ANE-2235] Fix CatalogMap parsing - Use parseJSON instead of undefined parseCatalogEntry * [ANE-2235] Fix CatalogMap parsing - Use fully qualified Yaml.parseJSON to fix compilation error * [ANE-2235] Fix CatalogMap parsing type error - Add KeyMap import - Convert Object to Map Text using KeyMap.toMap * [ANE-2235] Revert commits to restore pnpm-9 branch to d78d81b Reverting commits from Jan 31 - Feb 3 to restore the branch to how csasarak left it on Jan 3rd (d78d81b) * Process pnpm lockfile v9 workspaces * Read dependency graph from lockfile snapshots where necessary. * Fix snapshots parsing. * Glob negation. * Fix snapshots parsing again * Dev deps for pnpm 9 projects. * Tests for pnpm9 lockfile and workspace project. * Fix compilation error. * Update docs and changelog * Apply suggestions from code review Co-authored-by: Scott Patten <scott@fossa.com> * Address PR feedback. --------- Co-authored-by: ryanlink <ryanlink@gmail.com> Co-authored-by: Ryan Link <47920994+ryanlink@users.noreply.github.com> Co-authored-by: Scott Patten <scott@fossa.com>
1 parent 4fbec2b commit 78010ff

17 files changed

Lines changed: 320 additions & 66 deletions

File tree

Changelog.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# FOSSA CLI Changelog
22

3+
## 3.10.12
4+
5+
- PNPM: Initial support for lockfile version 9.0 ([#1561](https://github.com/fossas/fossa-cli/pull/1561))
6+
37
## 3.10.11
48

59
- container scanning: fix unzipping JARs that symlink to other layers #1555 ([#1555](https://github.com/fossas/fossa-cli/pull/1555))
6-
10+
711
## 3.10.10
812

913
- go: support the `tool` directive introduced in go Feb 2025 ([#1553](https://github.com/fossas/fossa-cli/pull/1553))

docs/references/strategies/languages/nodejs/pnpm.md

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Pnpm
22

3-
[Pnpm](https://pnpm.io/) is a fast, disk space-efficient package manager.
3+
[Pnpm](https://pnpm.io/) is a fast, disk space-efficient package manager.
44
Unlike npm and yarn, pnpm uses symbolic links to create a nested structure
55
of dependencies.
66

@@ -20,8 +20,8 @@ in `pnpm-lock.yaml` to analyze the dependency graph.
2020

2121
- `packages`
2222
- `[packagesKey]`
23-
- `resolution`: infer git URL, git commit, or package source URL.
24-
- `dependencies`: list of transitive dependencies
23+
- `resolution`: infer git URL, git commit, or package source URL.
24+
- `dependencies`: list of transitive dependencies
2525
- `peerDependencies`: list of peer dependencies (will be treated like any other dependency)
2626
- `dev`: to infer if this is used dependency or not. If the value is `true` by default CLI will not include this in the final analysis.
2727

@@ -35,15 +35,15 @@ importers:
3535
specifiers:
3636
some-pkg: https://some-url/pkg.tar.gz
3737
react: '*'
38-
my-local-pkg: file:../libs/my-local-pkg
38+
my-local-pkg: file:../libs/my-local-pkg
3939
dependencies:
4040
some-pkg: '@some-url/pkg.tar.gz'
4141
my-local-pkg: file:../libs/my-local-pkg
4242
devDependencies:
4343
react: 18.1.0
4444

4545
# workspace project in packages/some-ws-pkg directory from root.
46-
packages/some-ws-pkg:
46+
packages/some-ws-pkg:
4747
specifiers:
4848
commander: 9.2.0
4949
dependencies:
@@ -104,7 +104,7 @@ FOSSA will use provided `repo` and `commit` attribute to analyze this dependency
104104
dev: false
105105
```
106106
107-
* If the dependency was resolved using tarball (`resolution` will have `tarball` attribute)
107+
* If the dependency was resolved using tarball (`resolution` will have `tarball` attribute)
108108
FOSSA will use provided URL address to download and analyze this dependency.
109109

110110
```yaml
@@ -118,11 +118,11 @@ FOSSA will use provided URL address to download and analyze this dependency.
118118
```
119119

120120
* If the dependency was resolved using the local directory (`resolution` will have the `type: directory` attribute),
121-
FOSSA will not analyze this dependency. Local dependency's transitive dependencies will be analyzed,
122-
and they will be promoted in place of local dependency.
121+
FOSSA will not analyze this dependency. Local dependency's transitive dependencies will be analyzed,
122+
and they will be promoted in place of local dependency.
123123

124124
```yaml
125-
# FOSSA will not analyze this dependency,
125+
# FOSSA will not analyze this dependency,
126126
# But FOSSA will analyze its transitive dependency (if they are not sourced from the local directory)
127127
#
128128
# FOSSA will promote loose-envify of 1.4.0 in place of unifier.
@@ -136,7 +136,7 @@ and they will be promoted in place of local dependency.
136136
dev: false
137137
```
138138

139-
* If the dependency was resolved using registry resolver, FOSSA will use the registry to analyze the dependency.
139+
* If the dependency was resolved using registry resolver, FOSSA will use the registry to analyze the dependency.
140140
CLI will infer the package name and version using `/${dependencyName}/${dependencyVersion}` scheme from the package's key.
141141

142142
```yaml
@@ -151,7 +151,7 @@ CLI will infer the package name and version using `/${dependencyName}/${dependen
151151
* Pnpm workspaces are supported.
152152
* 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`.
153153
* Optional dependencies are included in the analysis by default. They can be ignored in FOSSA UI.
154-
* `fossa-cli` supports lockFileVersion: 4.x, 5.x, and 6.x.
154+
* `fossa-cli` supports lockFileVersion: 4.x, 5.x, 6.x, 7.x, 8.x, and 9.x.
155155

156156

157157
# F.A.Q
@@ -168,10 +168,3 @@ targets:
168168
only:
169169
- type: pnpm
170170
```
171-
### Are all versions of `pnpm` supported?
172-
173-
At this time, the latest version of pnpm (v9) and its associated v9 lockfiles are not correctly parsed by FOSSA. Please revert to v8 (v6 lockfile) if your dependencies are not resolved in the FOSSA UI: "FOSSA was unable to analyze this dependency. If it is behind a private registry or auth you may need to configure FOSSA's access, then rebuild this dependency." This is due to the version number being appended to the package name:
174-
175-
<img width="796" alt="image" src="https://github.com/user-attachments/assets/d1461506-d3e7-42da-b9be-2b53a87f79f1" />
176-
177-
Please [email](mailto:support@fossa.com) FOSSA support if you are affected by this limitation.

spectrometer.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ library
484484
Strategy.Node.Npm.PackageLockV3
485485
Strategy.Node.PackageJson
486486
Strategy.Node.Pnpm.PnpmLock
487+
Strategy.Node.Pnpm.Workspace
487488
Strategy.Node.YarnV1.YarnLock
488489
Strategy.Node.YarnV2.Lockfile
489490
Strategy.Node.YarnV2.Resolvers

src/Strategy/Node.hs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import Data.Map.Strict qualified as Map
3636
import Data.Maybe (catMaybes, isJust, mapMaybe)
3737
import Data.Set (Set)
3838
import Data.Set qualified as Set
39-
import Data.String.Conversion (decodeUtf8)
39+
import Data.String.Conversion (decodeUtf8, toString)
4040
import Data.Tagged (applyTag)
4141
import Data.Text (Text)
4242
import Data.Text qualified as Text
@@ -59,6 +59,7 @@ import Effect.ReadFS (
5959
doesFileExist,
6060
readContentsBSLimit,
6161
readContentsJson,
62+
readContentsYaml,
6263
)
6364
import GHC.Generics (Generic)
6465
import Path (
@@ -84,13 +85,14 @@ import Strategy.Node.PackageJson (
8485
PkgJsonGraph (..),
8586
PkgJsonLicense (LicenseObj, LicenseText),
8687
PkgJsonLicenseObj (licenseUrl),
87-
PkgJsonWorkspaces (unWorkspaces),
88+
PkgJsonWorkspaces (PkgJsonWorkspaces, unWorkspaces),
8889
Production,
8990
WorkspacePackageNames (WorkspacePackageNames),
9091
pkgFileList,
9192
)
9293
import Strategy.Node.PackageJson qualified as PackageJson
9394
import Strategy.Node.Pnpm.PnpmLock qualified as PnpmLock
95+
import Strategy.Node.Pnpm.Workspace (PnpmWorkspace (workspaceSpecs))
9496
import Strategy.Node.YarnV1.YarnLock qualified as V1
9597
import Strategy.Node.YarnV2.YarnLock qualified as V2
9698
import Types (
@@ -118,8 +120,8 @@ discover ::
118120
m [DiscoveredProject NodeProject]
119121
discover dir = withMultiToolFilter [YarnProjectType, NpmProjectType, PnpmProjectType] $
120122
context "NodeJS" $ do
121-
manifestList <- context "Finding nodejs projects" $ collectManifests dir
122-
manifestMap <- context "Reading package.json files" $ (Map.fromList . catMaybes) <$> traverse loadPackage manifestList
123+
manifestList <- context "Finding nodejs/pnpm projects" $ collectManifests dir
124+
manifestMap <- context "Reading manifest files" $ (Map.fromList . catMaybes) <$> traverse loadPackage manifestList
123125
if Map.null manifestMap
124126
then -- If the map is empty, we found no JS projects, we return early.
125127
pure []
@@ -281,9 +283,14 @@ extractDepLists PkgJsonGraph{..} = foldMap extractSingle $ Map.elems jsonLookup
281283
loadPackage :: (Has Logger sig m, Has ReadFS sig m, Has Diagnostics sig m) => Manifest -> m (Maybe (Manifest, PackageJson))
282284
loadPackage (Manifest file) = do
283285
result <- recover $ readContentsJson @PackageJson file
284-
case result of
285-
Nothing -> pure Nothing
286-
Just contents -> pure $ Just (Manifest file, contents)
286+
-- PNPM projects using v9 of the lockfile have their own way to specify workspaces/child projects.
287+
-- Since there is still a package.json, inject the pnpm-workspace.yaml projects into the ones for that manifest.
288+
let possiblePnpmWorkspaceFile = Path.parent file </> $(mkRelFile "pnpm-workspace.yaml")
289+
pnpmResult <- recover $ readContentsYaml @PnpmWorkspace possiblePnpmWorkspaceFile
290+
pure $ do
291+
contents@PackageJson{packageWorkspaces} <- result
292+
let pnpmGlobs = maybe [] workspaceSpecs pnpmResult
293+
Just (Manifest file, contents{packageWorkspaces = PkgJsonWorkspaces pnpmGlobs <> packageWorkspaces})
287294

288295
buildManifestGraph :: Map Manifest PackageJson -> PkgJsonGraph
289296
buildManifestGraph manifestMap = PkgJsonGraph adjmap manifestMap
@@ -317,7 +324,14 @@ buildManifestGraph manifestMap = PkgJsonGraph adjmap manifestMap
317324

318325
-- True if qualified glob pattern matches the given file.
319326
filterfunc :: Manifest -> Glob Rel -> Manifest -> Bool
320-
filterfunc root glob (Manifest candidate) = candidate `Glob.matches` qualifyGlobPattern root glob
327+
filterfunc root glob (Manifest candidate) = candidate `globMatches` qualifyGlobPattern root glob
328+
329+
-- PNPM workspaces can negate their globs with '!'.
330+
globMatches :: Path Abs File -> Glob Abs -> Bool
331+
globMatches p g = case toString g of
332+
-- labeled unsafe, but we already had a glob before putting it back together again
333+
('!' : rest) -> not $ p `Glob.matches` (Glob.unsafeGlobAbs rest)
334+
_ -> p `Glob.matches` g
321335

322336
-- Yarn appends the filename to the glob, so we match that behavior
323337
-- https://github.com/yarnpkg/yarn/blob/master/src/config.js#L821

src/Strategy/Node/PackageJson.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ buildGraph PackageJson{..} = run . withLabeling toDependency $ do
104104
}
105105

106106
newtype PkgJsonWorkspaces = PkgJsonWorkspaces {unWorkspaces :: [Glob Rel]}
107-
deriving (Eq, Ord, Show, ToJSON)
107+
deriving (Eq, Ord, Show, ToJSON, Semigroup, Monoid)
108108

109109
-- Name and version are required for workspace sub-projects.
110110
data PackageJson = PackageJson

0 commit comments

Comments
 (0)